Serac  0.1
Serac is an implicit thermal strucural mechanics simulation code.
functional_qoi.inl
Go to the documentation of this file.
1 // Copyright (c) 2019-2024, Lawrence Livermore National Security, LLC and
2 // other Serac Project Developers. See the top-level LICENSE file for
3 // details.
4 //
5 // SPDX-License-Identifier: (BSD-3-Clause)
6 
13 namespace serac {
14 
21  QoIProlongation() {}
22 
24  QoIProlongation(MPI_Comm c) : comm(c) {}
25 
27  void Mult(const mfem::Vector&, mfem::Vector&) const { SLIC_ERROR_ROOT("QoIProlongation::Mult() is not defined"); }
28 
30  void MultTranspose(const mfem::Vector& input, mfem::Vector& output) const
31  {
32  // const_cast to work around clang@14.0.6 compiler error:
33  // "argument type 'const double *' doesn't match specified 'MPI' type tag that requires 'double *'"
34  MPI_Allreduce(const_cast<double*>(&input[0]), &output[0], 1, MPI_DOUBLE, MPI_SUM, comm);
35  }
36 
37  MPI_Comm comm;
38 };
39 
52  void ScatterAdd(const mfem::Vector& input, mfem::Vector& output) const { output[0] += input.Sum(); }
53 };
54 
58 template <typename... trials, ExecutionSpace exec>
59 class Functional<double(trials...), exec> {
60  using test = QOI;
61  static constexpr tuple<trials...> trial_spaces{};
62  static constexpr uint32_t num_trial_spaces = sizeof...(trials);
63  static constexpr auto Q = std::max({test::order, trials::order...}) + 1;
64 
65  class Gradient;
66 
67  // clang-format off
68  template <uint32_t i>
69  struct operator_paren_return {
70  using type = typename std::conditional<
71  i == NO_DIFFERENTIATION, // if `i` is greater than or equal to zero,
72  double, // wise, we just return the value
73  serac::tuple<double&, Gradient&> // otherwise, we return the value and the derivative w.r.t arg `i`
74  >::type;
75  };
76  // clang-format on
77 
78 public:
83  Functional(std::array<const mfem::ParFiniteElementSpace*, num_trial_spaces> trial_fes)
84  : test_fec_(0, trial_fes[0]->GetMesh()->Dimension()),
85  test_space_(dynamic_cast<mfem::ParMesh*>(trial_fes[0]->GetMesh()), &test_fec_),
86  trial_space_(trial_fes)
87  {
88  auto* mesh = trial_fes[0]->GetMesh();
89 
90  auto mem_type = mfem::Device::GetMemoryType();
91 
92  for (auto type : {Domain::Type::Elements, Domain::Type::BoundaryElements}) {
93  input_E_[type].resize(num_trial_spaces);
94  }
95 
96  for (uint32_t i = 0; i < num_trial_spaces; i++) {
97  P_trial_[i] = trial_space_[i]->GetProlongationMatrix();
98 
99  input_L_[i].SetSize(P_trial_[i]->Height(), mfem::Device::GetMemoryType());
100 
101  for (auto type : {Domain::Type::Elements, Domain::Type::BoundaryElements}) {
102  if (type == Domain::Type::Elements) {
103  G_trial_[type][i] = BlockElementRestriction(trial_fes[i]);
104  } else {
105  G_trial_[type][i] = BlockElementRestriction(trial_fes[i], FaceType::BOUNDARY);
106  }
107 
108  // note: we have to use "Update" here, as mfem::BlockVector's
109  // copy assignment ctor (operator=) doesn't let you make changes
110  // to the block size
111  input_E_[type][i].Update(G_trial_[type][i].bOffsets(), mem_type);
112  }
113  }
114 
115  for (auto type : {Domain::Type::Elements, Domain::Type::BoundaryElements}) {
116  std::array<uint32_t, mfem::Geometry::NUM_GEOMETRIES> counts{};
117  if (type == Domain::Type::Elements) {
118  counts = geometry_counts(*mesh);
119  } else {
120  counts = boundary_geometry_counts(*mesh);
121  }
122 
123  mfem::Array<int> offsets(mfem::Geometry::NUM_GEOMETRIES + 1);
124  offsets[0] = 0;
125  for (int i = 0; i < mfem::Geometry::NUM_GEOMETRIES; i++) {
126  auto g = mfem::Geometry::Type(i);
127  offsets[g + 1] = offsets[g] + int(counts[uint32_t(g)]);
128  }
129 
130  output_E_[type].Update(offsets, mem_type);
131  }
132 
133  G_test_ = QoIElementRestriction();
134  P_test_ = QoIProlongation(trial_fes[0]->GetParMesh()->GetComm());
135 
136  output_L_.SetSize(1, mem_type);
137 
138  output_T_.SetSize(1, mfem::Device::GetMemoryType());
139 
140  // gradient objects depend on some member variables in
141  // Functional, so we initialize the gradient objects last
142  // to ensure that those member variables are initialized first
143  for (uint32_t i = 0; i < num_trial_spaces; i++) {
144  grad_.emplace_back(*this, i);
145  }
146  }
147 
159  template <int dim, int... args, typename lambda, typename qpt_data_type = Nothing>
160  void AddDomainIntegral(Dimension<dim>, DependsOn<args...>, lambda&& integrand, mfem::Mesh& mesh,
161  std::shared_ptr<QuadratureData<qpt_data_type>> qdata = NoQData)
162  {
163  if (mesh.GetNE() == 0) return;
164 
165  SLIC_ERROR_ROOT_IF(dim != mesh.Dimension(), "invalid mesh dimension for domain integral");
166 
169 
170  using signature = test(decltype(serac::type<args>(trial_spaces))...);
171  integrals_.push_back(
172  MakeDomainIntegral<signature, Q, dim>(EntireDomain(mesh), integrand, qdata, std::vector<uint32_t>{args...}));
173  }
174 
176  template <int dim, int... args, typename lambda, typename qpt_data_type = Nothing>
177  void AddDomainIntegral(Dimension<dim>, DependsOn<args...>, lambda&& integrand, Domain& domain,
178  std::shared_ptr<QuadratureData<qpt_data_type>> qdata = NoQData)
179  {
180  if (domain.mesh_.GetNE() == 0) return;
181 
182  SLIC_ERROR_ROOT_IF(dim != domain.mesh_.Dimension(), "invalid mesh dimension for domain integral");
183 
186 
187  using signature = test(decltype(serac::type<args>(trial_spaces))...);
188  integrals_.push_back(
189  MakeDomainIntegral<signature, Q, dim>(domain, integrand, qdata, std::vector<uint32_t>{args...}));
190  }
191 
203  template <int dim, int... args, typename lambda, typename qpt_data_type = void>
204  void AddBoundaryIntegral(Dimension<dim>, DependsOn<args...>, lambda&& integrand, mfem::Mesh& mesh)
205  {
206  auto num_bdr_elements = mesh.GetNBE();
207  if (num_bdr_elements == 0) return;
208 
210 
211  using signature = test(decltype(serac::type<args>(trial_spaces))...);
212  integrals_.push_back(
213  MakeBoundaryIntegral<signature, Q, dim>(EntireBoundary(mesh), integrand, std::vector<uint32_t>{args...}));
214  }
215 
217  template <int dim, int... args, typename lambda>
218  void AddBoundaryIntegral(Dimension<dim>, DependsOn<args...>, lambda&& integrand, const Domain& domain)
219  {
220  auto num_bdr_elements = domain.mesh_.GetNBE();
221  if (num_bdr_elements == 0) return;
222 
223  SLIC_ERROR_ROOT_IF(dim != domain.dim_, "invalid domain of integration for boundary integral");
224 
226 
227  using signature = test(decltype(serac::type<args>(trial_spaces))...);
228  integrals_.push_back(MakeBoundaryIntegral<signature, Q, dim>(domain, integrand, std::vector<uint32_t>{args...}));
229  }
230 
241  template <int... args, typename lambda, typename qpt_data_type = Nothing>
242  void AddAreaIntegral(DependsOn<args...> which_args, lambda&& integrand, mfem::Mesh& domain,
243  std::shared_ptr<QuadratureData<qpt_data_type>>& data = NoQData)
244  {
245  AddDomainIntegral(Dimension<2>{}, which_args, integrand, domain, data);
246  }
247 
258  template <int... args, typename lambda, typename qpt_data_type = Nothing>
259  void AddVolumeIntegral(DependsOn<args...> which_args, lambda&& integrand, mfem::Mesh& domain,
260  std::shared_ptr<QuadratureData<qpt_data_type>>& data = NoQData)
261  {
262  AddDomainIntegral(Dimension<3>{}, which_args, integrand, domain, data);
263  }
264 
266  template <int... args, typename lambda>
267  void AddSurfaceIntegral(DependsOn<args...> which_args, lambda&& integrand, mfem::Mesh& domain)
268  {
269  AddBoundaryIntegral(Dimension<2>{}, which_args, integrand, domain);
270  }
271 
282  double ActionOfGradient(const mfem::Vector& input_T, uint32_t which) const
283  {
284  P_trial_[which]->Mult(input_T, input_L_[which]);
285 
286  output_L_ = 0.0;
287 
288  // this is used to mark when gather operations have been performed,
289  // to avoid doing them more than once per trial space
290  bool already_computed[Domain::num_types]{}; // default initializes to `false`
291 
292  for (auto& integral : integrals_) {
293  auto type = integral.domain_.type_;
294 
295  if (!already_computed[type]) {
296  G_trial_[type][which].Gather(input_L_[which], input_E_[type][which]);
297  already_computed[type] = true;
298  }
299 
300  integral.GradientMult(input_E_[type][which], output_E_[type], which);
301 
302  // scatter-add to compute residuals on the local processor
303  G_test_.ScatterAdd(output_E_[type], output_L_);
304  }
305 
306  // scatter-add to compute global residuals
307  P_test_.MultTranspose(output_L_, output_T_);
308 
309  return output_T_[0];
310  }
311 
322  template <uint32_t wrt, typename... T>
323  typename operator_paren_return<wrt>::type operator()(DifferentiateWRT<wrt>, double t, const T&... args)
324  {
325  const mfem::Vector* input_T[] = {&static_cast<const mfem::Vector&>(args)...};
326 
327  // get the values for each local processor
328  for (uint32_t i = 0; i < num_trial_spaces; i++) {
329  P_trial_[i]->Mult(*input_T[i], input_L_[i]);
330  }
331 
332  output_L_ = 0.0;
333 
334  // this is used to mark when operations have been performed,
335  // to avoid doing them more than once
336  bool already_computed[Domain::num_types][num_trial_spaces]{}; // default initializes to `false`
337 
338  for (auto& integral : integrals_) {
339  auto type = integral.domain_.type_;
340 
341  for (auto i : integral.active_trial_spaces_) {
342  if (!already_computed[type][i]) {
343  G_trial_[type][i].Gather(input_L_[i], input_E_[type][i]);
344  already_computed[type][i] = true;
345  }
346  }
347 
348  const bool update_state = false;
349  integral.Mult(t, input_E_[type], output_E_[type], wrt, update_state);
350 
351  // scatter-add to compute residuals on the local processor
352  G_test_.ScatterAdd(output_E_[type], output_L_);
353  }
354 
355  // scatter-add to compute global residuals
356  P_test_.MultTranspose(output_L_, output_T_);
357 
358  if constexpr (wrt != NO_DIFFERENTIATION) {
359  // if the user has indicated they'd like to evaluate and differentiate w.r.t.
360  // a specific argument, then we return both the value and gradient w.r.t. that argument
361  //
362  // mfem::Vector arg0 = ...;
363  // mfem::Vector arg1 = ...;
364  // e.g. auto [value, gradient_wrt_arg1] = my_functional(arg0, differentiate_wrt(arg1));
365  return {output_T_[0], grad_[wrt]};
366  }
367 
368  if constexpr (wrt == NO_DIFFERENTIATION) {
369  // if the user passes only `mfem::Vector`s then we assume they only want the output value
370  //
371  // mfem::Vector arg0 = ...;
372  // mfem::Vector arg1 = ...;
373  // e.g. mfem::Vector value = my_functional(arg0, arg1);
374  return output_T_[0];
375  }
376  }
377 
379  template <typename... T>
380  auto operator()(double t, const T&... args)
381  {
382  // below we add 0 so the number of differentiated arguments defaults to 0 if trial spaces are not provided
383  constexpr int num_differentiated_arguments = (std::is_same_v<T, differentiate_wrt_this> + ... + 0);
384  static_assert(num_differentiated_arguments <= 1,
385  "Error: Functional::operator() can only differentiate w.r.t. 1 argument a time");
386  static_assert(sizeof...(T) == num_trial_spaces,
387  "Error: Functional::operator() must take exactly as many arguments as trial spaces");
388 
389  [[maybe_unused]] constexpr uint32_t i = index_of_differentiation<T...>();
390 
391  return (*this)(DifferentiateWRT<i>{}, t, args...);
392  }
393 
394 private:
398  enum class Operation
399  {
400  Mult,
401  GradientMult
402  };
403 
407  class Gradient {
408  public:
413  Gradient(Functional<double(trials...)>& f, uint32_t which = 0)
414  : form_(f), which_argument(which), gradient_L_(f.trial_space_[which]->GetVSize())
415  {
416  }
417 
418  void Mult(const mfem::Vector& x, mfem::Vector& y) const { form_.GradientMult(x, y); }
419 
420  double operator()(const mfem::Vector& x) const { return form_.ActionOfGradient(x, which_argument); }
421 
422  std::unique_ptr<mfem::HypreParVector> assemble()
423  {
424  // The mfem method ParFiniteElementSpace.NewTrueDofVector should really be marked const
425  std::unique_ptr<mfem::HypreParVector> gradient_T(
426  const_cast<mfem::ParFiniteElementSpace*>(form_.trial_space_[which_argument])->NewTrueDofVector());
427 
428  gradient_L_ = 0.0;
429 
430  std::map<mfem::Geometry::Type, ExecArray<double, 3, exec>> element_gradients[Domain::num_types];
431 
432  for (auto& integral : form_.integrals_) {
433  auto& K_elem = element_gradients[integral.domain_.type_];
434  auto& trial_restrictions = form_.G_trial_[integral.domain_.type_][which_argument].restrictions;
435 
436  if (K_elem.empty()) {
437  for (auto& [geom, trial_restriction] : trial_restrictions) {
438  K_elem[geom] = ExecArray<double, 3, exec>(trial_restriction.num_elements, 1,
439  trial_restriction.nodes_per_elem * trial_restriction.components);
440 
441  detail::zero_out(K_elem[geom]);
442  }
443  }
444 
445  integral.ComputeElementGradients(K_elem, which_argument);
446  }
447 
448  for (auto type : {Domain::Type::Elements, Domain::Type::BoundaryElements}) {
449  auto& K_elem = element_gradients[type];
450  auto& trial_restrictions = form_.G_trial_[type][which_argument].restrictions;
451 
452  if (!K_elem.empty()) {
453  for (auto [geom, elem_matrices] : K_elem) {
454  std::vector<DoF> trial_vdofs(trial_restrictions[geom].nodes_per_elem * trial_restrictions[geom].components);
455 
456  for (axom::IndexType e = 0; e < elem_matrices.shape()[0]; e++) {
457  trial_restrictions[geom].GetElementVDofs(e, trial_vdofs);
458 
459  // note: elem_matrices.shape()[1] is 1 for a QoI
460  for (axom::IndexType i = 0; i < elem_matrices.shape()[1]; i++) {
461  for (axom::IndexType j = 0; j < elem_matrices.shape()[2]; j++) {
462  int sign = trial_vdofs[uint32_t(j)].sign();
463  int col = int(trial_vdofs[uint32_t(j)].index());
464  gradient_L_[col] += sign * elem_matrices(e, i, j);
465  }
466  }
467  }
468  }
469  }
470  }
471 
472  form_.P_trial_[which_argument]->MultTranspose(gradient_L_, *gradient_T);
473 
474  return gradient_T;
475  }
476 
477  friend auto assemble(Gradient& g) { return g.assemble(); }
478 
479  private:
483  Functional<double(trials...), exec>& form_;
484 
485  uint32_t which_argument;
486 
487  mfem::Vector gradient_L_;
488  };
489 
491  const mfem::L2_FECollection test_fec_;
492  const mfem::ParFiniteElementSpace test_space_;
493 
495  std::array<const mfem::ParFiniteElementSpace*, num_trial_spaces> trial_space_;
496 
501  const mfem::Operator* P_trial_[num_trial_spaces];
502 
504  mutable mfem::Vector input_L_[num_trial_spaces];
505 
506  BlockElementRestriction G_trial_[Domain::num_types][num_trial_spaces];
507 
508  mutable std::vector<mfem::BlockVector> input_E_[Domain::num_types];
509 
510  std::vector<Integral> integrals_;
511 
512  mutable mfem::BlockVector output_E_[Domain::num_types];
513 
514  QoIElementRestriction G_test_;
515 
517  mutable mfem::Vector output_L_;
518 
519  QoIProlongation P_test_;
520 
522  mutable mfem::Vector output_T_;
523 
525  mutable std::vector<Gradient> grad_;
526 };
527 
528 } // namespace serac
Functional(std::array< const mfem::ParFiniteElementSpace *, num_trial_spaces > trial_fes)
Constructs using a mfem::ParFiniteElementSpace object corresponding to the trial space.
void AddAreaIntegral(DependsOn< args... > which_args, lambda &&integrand, mfem::Mesh &domain, std::shared_ptr< QuadratureData< qpt_data_type >> &data=NoQData)
Adds an area integral, i.e., over 2D elements in R^2.
void AddVolumeIntegral(DependsOn< args... > which_args, lambda &&integrand, mfem::Mesh &domain, std::shared_ptr< QuadratureData< qpt_data_type >> &data=NoQData)
Adds a volume integral, i.e., over 3D elements in R^3.
void AddBoundaryIntegral(Dimension< dim >, DependsOn< args... >, lambda &&integrand, const Domain &domain)
This is an overloaded member function, provided for convenience. It differs from the above function o...
auto operator()(double t, const T &... args)
This is an overloaded member function, provided for convenience. It differs from the above function o...
void AddDomainIntegral(Dimension< dim >, DependsOn< args... >, lambda &&integrand, Domain &domain, std::shared_ptr< QuadratureData< qpt_data_type >> qdata=NoQData)
This is an overloaded member function, provided for convenience. It differs from the above function o...
operator_paren_return< wrt >::type operator()(DifferentiateWRT< wrt >, double t, const T &... args)
this function lets the user evaluate the serac::Functional with the given trial space values
void AddBoundaryIntegral(Dimension< dim >, DependsOn< args... >, lambda &&integrand, mfem::Mesh &mesh)
Adds a boundary integral term to the Functional object.
void AddSurfaceIntegral(DependsOn< args... > which_args, lambda &&integrand, mfem::Mesh &domain)
alias for Functional::AddBoundaryIntegral(Dimension<2>{}, integrand, domain);
double ActionOfGradient(const mfem::Vector &input_T, uint32_t which) const
this function computes the directional derivative of the quantity of interest functional
void AddDomainIntegral(Dimension< dim >, DependsOn< args... >, lambda &&integrand, mfem::Mesh &mesh, std::shared_ptr< QuadratureData< qpt_data_type >> qdata=NoQData)
Adds a domain integral term to the Functional object.
Accelerator functionality.
Definition: serac.cpp:38
Domain EntireBoundary(const mfem::Mesh &mesh)
constructs a domain from all the boundary elements in a mesh
Definition: domain.cpp:418
std::shared_ptr< QuadratureData< Nothing > > NoQData
a single instance of a QuadratureData container of Nothings, since they are all interchangeable
SERAC_HOST_DEVICE auto max(dual< gradient_type > a, double b)
Implementation of max for dual numbers.
Definition: dual.hpp:230
void check_for_missing_nodal_gridfunc(const mfem::Mesh &mesh)
function for verifying that the mesh has been fully initialized
Definition: functional.hpp:73
constexpr uint32_t index_of_differentiation()
given a list of types, this function returns the index that corresponds to the type dual_vector.
Definition: functional.hpp:49
std::array< uint32_t, mfem::Geometry::NUM_GEOMETRIES > boundary_geometry_counts(const mfem::Mesh &mesh)
count the number of boundary elements of each geometry in a mesh
Definition: geometry.hpp:83
constexpr SERAC_HOST_DEVICE auto type(const tuple< T... > &values)
a function intended to be used for extracting the ith type from a tuple.
Definition: tuple.hpp:274
ExecutionSpace
enum used for signalling whether or not to perform certain calculations on the CPU or GPU
Definition: accelerator.hpp:69
void check_for_unsupported_elements(const mfem::Mesh &mesh)
function for verifying that there are no unsupported element types in the mesh
Definition: functional.hpp:82
Domain EntireDomain(const mfem::Mesh &mesh)
constructs a domain from all the elements in a mesh
Definition: domain.cpp:382
std::array< uint32_t, mfem::Geometry::NUM_GEOMETRIES > geometry_counts(const mfem::Mesh &mesh)
count the number of elements of each geometry in a mesh
Definition: geometry.hpp:70
a generalization of mfem::ElementRestriction that works with multiple kinds of element geometries....
Compile-time alias for a dimension.
Definition: geometry.hpp:11
a class for representing a geometric region that can be used for integration
Definition: domain.hpp:21
static constexpr int num_types
the number of entries in the Type enum
Definition: domain.hpp:29
int dim_
the geometric dimension of the domain
Definition: domain.hpp:35
const mfem::Mesh & mesh_
the underyling mesh for this domain
Definition: domain.hpp:32
these classes are a little confusing. These two special types represent the similar (but different) c...
"Quantity of Interest" elements (i.e. elements with a single shape function, 1)
this class behaves like a Restriction operator, except is specialized for the case of a quantity of i...
void ScatterAdd(const mfem::Vector &input, mfem::Vector &output) const
element-to-global ScatterAdd operation used in FEM assembly, for quantities of interest
this class behaves like a Prolongation operator, except is specialized for the case of a quantity of ...
void MultTranspose(const mfem::Vector &input, mfem::Vector &output) const
set the value of output to the distributed sum over input values from different processors
QoIProlongation(MPI_Comm c)
create a QoIProlongation for a Quantity of Interest
MPI_Comm comm
MPI communicator used to carry out the distributed reduction.
void Mult(const mfem::Vector &, mfem::Vector &) const
unimplemented: do not use
A class for storing and access user-defined types at quadrature points.
This is a class that mimics most of std::tuple's interface, except that it is usable in CUDA kernels ...
Definition: tuple.hpp:28