Serac  0.1
Serac is an implicit thermal strucural mechanics simulation code.
functional.hpp
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 #pragma once
14 
15 #include "mfem.hpp"
16 
21 #include "serac/numerics/functional/integral.hpp"
22 #include "serac/numerics/functional/dof_numbering.hpp"
23 #include "serac/numerics/functional/differentiate_wrt.hpp"
24 
25 #include "serac/numerics/functional/element_restriction.hpp"
26 
28 
29 #include <array>
30 #include <vector>
31 
32 namespace serac {
33 
34 template <int... i>
35 struct DependsOn {
36 };
37 
48 template <typename... T>
49 constexpr uint32_t index_of_differentiation()
50 {
51  constexpr uint32_t n = sizeof...(T);
52  bool matching[] = {std::is_same_v<T, differentiate_wrt_this>...};
53  for (uint32_t i = 0; i < n; i++) {
54  if (matching[i]) {
55  return i;
56  }
57  }
58  return NO_DIFFERENTIATION;
59 }
60 
64 template <int ind>
65 struct Index {
69  constexpr operator int() { return ind; }
70 };
71 
73 inline void check_for_missing_nodal_gridfunc(const mfem::Mesh& mesh)
74 {
75  if (mesh.GetNodes() == nullptr) {
76  SLIC_ERROR_ROOT(
77  R"errmsg(
78  the provided mesh does not have a nodal gridfunction.
79  If you created an mfem::Mesh manually, make sure that the
80  following member functions are invoked before use
81 
82  > mfem::Mesh::EnsureNodes();
83  > mfem::Mesh::ExchangeFaceNbrData();
84 
85  or else the mfem::Mesh won't be fully initialized
86  )errmsg";);
87  }
88 }
89 
91 inline void check_for_unsupported_elements(const mfem::Mesh& mesh)
92 {
93  int num_elements = mesh.GetNE();
94  for (int e = 0; e < num_elements; e++) {
95  auto type = mesh.GetElementType(e);
96  if (type == mfem::Element::POINT || type == mfem::Element::WEDGE || type == mfem::Element::PYRAMID) {
97  SLIC_ERROR_ROOT("Mesh contains unsupported element type");
98  }
99  }
100 }
101 
110 template <typename function_space>
111 inline std::pair<std::unique_ptr<mfem::ParFiniteElementSpace>, std::unique_ptr<mfem::FiniteElementCollection>>
112 generateParFiniteElementSpace(mfem::ParMesh* mesh)
113 {
114  const int dim = mesh->Dimension();
115  std::unique_ptr<mfem::FiniteElementCollection> fec;
116  const auto ordering = mfem::Ordering::byNODES;
117 
118  switch (function_space::family) {
119  case Family::H1:
120  fec = std::make_unique<mfem::H1_FECollection>(function_space::order, dim);
121  break;
122  case Family::HCURL:
123  fec = std::make_unique<mfem::ND_FECollection>(function_space::order, dim);
124  break;
125  case Family::HDIV:
126  fec = std::make_unique<mfem::RT_FECollection>(function_space::order, dim);
127  break;
128  case Family::L2:
129  // We use GaussLobatto basis functions as this is what is used for the serac::Functional FE kernels
130  fec = std::make_unique<mfem::L2_FECollection>(function_space::order, dim, mfem::BasisType::GaussLobatto);
131  break;
132  default:
133  return std::pair<std::unique_ptr<mfem::ParFiniteElementSpace>, std::unique_ptr<mfem::FiniteElementCollection>>(
134  nullptr, nullptr);
135  break;
136  }
137 
138  auto fes = std::make_unique<mfem::ParFiniteElementSpace>(mesh, fec.get(), function_space::components, ordering);
139 
140  return std::pair(std::move(fes), std::move(fec));
141 }
142 
144 template <typename T, ExecutionSpace exec = serac::default_execution_space>
145 class Functional;
147 
188 template <typename test, typename... trials, ExecutionSpace exec>
189 class Functional<test(trials...), exec> {
190  static constexpr tuple<trials...> trial_spaces{};
191  static constexpr uint32_t num_trial_spaces = sizeof...(trials);
192  static constexpr auto Q = std::max({test::order, trials::order...}) + 1;
193 
194  static constexpr mfem::Geometry::Type elem_geom[4] = {mfem::Geometry::INVALID, mfem::Geometry::SEGMENT,
195  mfem::Geometry::SQUARE, mfem::Geometry::CUBE};
196  static constexpr mfem::Geometry::Type simplex_geom[4] = {mfem::Geometry::INVALID, mfem::Geometry::SEGMENT,
197  mfem::Geometry::TRIANGLE, mfem::Geometry::TETRAHEDRON};
198 
199  class Gradient;
200 
201  // clang-format off
202  template <uint32_t i>
203  struct operator_paren_return {
204  using type = typename std::conditional<
205  i == NO_DIFFERENTIATION, // if `i` indicates that we want to skip differentiation
206  mfem::Vector&, // we just return the value
207  serac::tuple<mfem::Vector&, Gradient&> // otherwise we return the value and the derivative w.r.t arg `i`
208  >::type;
209  };
210  // clang-format on
211 
212 public:
218  Functional(const mfem::ParFiniteElementSpace* test_fes,
219  std::array<const mfem::ParFiniteElementSpace*, num_trial_spaces> trial_fes)
220  : update_qdata_(false), test_space_(test_fes), trial_space_(trial_fes)
221  {
222  auto mem_type = mfem::Device::GetMemoryType();
223 
224  for (auto type : {Domain::Type::Elements, Domain::Type::BoundaryElements}) {
225  input_E_[type].resize(num_trial_spaces);
226  }
227 
228  for (uint32_t i = 0; i < num_trial_spaces; i++) {
229  P_trial_[i] = trial_space_[i]->GetProlongationMatrix();
230 
231  input_L_[i].SetSize(P_trial_[i]->Height(), mfem::Device::GetMemoryType());
232 
233  // L->E
234  for (auto type : {Domain::Type::Elements, Domain::Type::BoundaryElements}) {
235  if (type == Domain::Type::Elements) {
236  G_trial_[type][i] = BlockElementRestriction(trial_fes[i]);
237  } else {
238  G_trial_[type][i] = BlockElementRestriction(trial_fes[i], FaceType::BOUNDARY);
239  }
240 
241  // note: we have to use "Update" here, as mfem::BlockVector's
242  // copy assignment ctor (operator=) doesn't let you make changes
243  // to the block size
244  input_E_[type][i].Update(G_trial_[type][i].bOffsets(), mem_type);
245  }
246  }
247 
248  for (auto type : {Domain::Type::Elements, Domain::Type::BoundaryElements}) {
249  if (type == Domain::Type::Elements) {
250  G_test_[type] = BlockElementRestriction(test_fes);
251  } else {
252  G_test_[type] = BlockElementRestriction(test_fes, FaceType::BOUNDARY);
253  }
254 
255  output_E_[type].Update(G_test_[type].bOffsets(), mem_type);
256  }
257 
258  P_test_ = test_space_->GetProlongationMatrix();
259 
260  output_L_.SetSize(P_test_->Height(), mem_type);
261 
262  output_T_.SetSize(test_fes->GetTrueVSize(), mem_type);
263 
264  // gradient objects depend on some member variables in
265  // Functional, so we initialize the gradient objects last
266  // to ensure that those member variables are initialized first
267  for (uint32_t i = 0; i < num_trial_spaces; i++) {
268  grad_.emplace_back(*this, i);
269  }
270  }
271 
283  template <int dim, int... args, typename lambda, typename qpt_data_type = Nothing>
284  void AddDomainIntegral(Dimension<dim>, DependsOn<args...>, lambda&& integrand, mfem::Mesh& domain,
285  std::shared_ptr<QuadratureData<qpt_data_type>> qdata = NoQData)
286  {
287  if (domain.GetNE() == 0) return;
288 
289  SLIC_ERROR_ROOT_IF(dim != domain.Dimension(), "invalid mesh dimension for domain integral");
290 
293 
294  using signature = test(decltype(serac::type<args>(trial_spaces))...);
295  integrals_.push_back(
296  MakeDomainIntegral<signature, Q, dim>(EntireDomain(domain), integrand, qdata, std::vector<uint32_t>{args...}));
297  }
298 
300  template <int dim, int... args, typename lambda, typename qpt_data_type = Nothing>
301  void AddDomainIntegral(Dimension<dim>, DependsOn<args...>, lambda&& integrand, Domain& domain,
302  std::shared_ptr<QuadratureData<qpt_data_type>> qdata = NoQData)
303  {
304  if (domain.mesh_.GetNE() == 0) return;
305 
306  SLIC_ERROR_ROOT_IF(dim != domain.mesh_.Dimension(), "invalid mesh dimension for domain integral");
307 
308  check_for_unsupported_elements(domain.mesh_);
309  check_for_missing_nodal_gridfunc(domain.mesh_);
310 
311  using signature = test(decltype(serac::type<args>(trial_spaces))...);
312  integrals_.push_back(
313  MakeDomainIntegral<signature, Q, dim>(domain, integrand, qdata, std::vector<uint32_t>{args...}));
314  }
315 
325  template <int dim, int... args, typename lambda>
326  void AddBoundaryIntegral(Dimension<dim>, DependsOn<args...>, lambda&& integrand, mfem::Mesh& domain)
327  {
328  auto num_bdr_elements = domain.GetNBE();
329  if (num_bdr_elements == 0) return;
330 
332 
333  using signature = test(decltype(serac::type<args>(trial_spaces))...);
334  integrals_.push_back(
335  MakeBoundaryIntegral<signature, Q, dim>(EntireBoundary(domain), integrand, std::vector<uint32_t>{args...}));
336  }
337 
339  template <int dim, int... args, typename lambda>
340  void AddBoundaryIntegral(Dimension<dim>, DependsOn<args...>, lambda&& integrand, const Domain& domain)
341  {
342  auto num_bdr_elements = domain.mesh_.GetNBE();
343  if (num_bdr_elements == 0) return;
344 
345  SLIC_ERROR_ROOT_IF(dim != domain.dim_, "invalid domain of integration for boundary integral");
346 
347  check_for_missing_nodal_gridfunc(domain.mesh_);
348 
349  using signature = test(decltype(serac::type<args>(trial_spaces))...);
350  integrals_.push_back(MakeBoundaryIntegral<signature, Q, dim>(domain, integrand, std::vector<uint32_t>{args...}));
351  }
352 
362  template <int... args, typename lambda, typename qpt_data_type = Nothing>
363  void AddAreaIntegral(DependsOn<args...> which_args, lambda&& integrand, mfem::Mesh& domain,
364  std::shared_ptr<QuadratureData<qpt_data_type>> data = NoQData)
365  {
366  AddDomainIntegral(Dimension<2>{}, which_args, integrand, domain, data);
367  }
368 
378  template <int... args, typename lambda, typename qpt_data_type = Nothing>
379  void AddVolumeIntegral(DependsOn<args...> which_args, lambda&& integrand, mfem::Mesh& domain,
380  std::shared_ptr<QuadratureData<qpt_data_type>> data = NoQData)
381  {
382  AddDomainIntegral(Dimension<3>{}, which_args, integrand, domain, data);
383  }
384 
386  template <int... args, typename lambda>
387  void AddSurfaceIntegral(DependsOn<args...> which_args, lambda&& integrand, mfem::Mesh& domain)
388  {
389  AddBoundaryIntegral(Dimension<2>{}, which_args, integrand, domain);
390  }
391 
403  void ActionOfGradient(const mfem::Vector& input_T, mfem::Vector& output_T, uint32_t which) const
404  {
405  P_trial_[which]->Mult(input_T, input_L_[which]);
406 
407  output_L_ = 0.0;
408 
409  // this is used to mark when gather operations have been performed,
410  // to avoid doing them more than once per trial space
411  bool already_computed[Domain::num_types]{}; // default initializes to `false`
412 
413  for (auto& integral : integrals_) {
414  auto type = integral.domain_.type_;
415 
416  if (!already_computed[type]) {
417  G_trial_[type][which].Gather(input_L_[which], input_E_[type][which]);
418  already_computed[type] = true;
419  }
420 
421  integral.GradientMult(input_E_[type][which], output_E_[type], which);
422 
423  // scatter-add to compute residuals on the local processor
424  G_test_[type].ScatterAdd(output_E_[type], output_L_);
425  }
426 
427  // scatter-add to compute global residuals
428  P_test_->MultTranspose(output_L_, output_T);
429  }
430 
443  template <uint32_t wrt, typename... T>
444  typename operator_paren_return<wrt>::type operator()(DifferentiateWRT<wrt>, double t, const T&... args)
445  {
446  const mfem::Vector* input_T[] = {&static_cast<const mfem::Vector&>(args)...};
447 
448  // get the values for each local processor
449  for (uint32_t i = 0; i < num_trial_spaces; i++) {
450  P_trial_[i]->Mult(*input_T[i], input_L_[i]);
451  }
452 
453  output_L_ = 0.0;
454 
455  // this is used to mark when operations have been performed,
456  // to avoid doing them more than once
457  bool already_computed[Domain::num_types][num_trial_spaces]{}; // default initializes to `false`
458 
459  for (auto& integral : integrals_) {
460  auto type = integral.domain_.type_;
461 
462  for (auto i : integral.active_trial_spaces_) {
463  if (!already_computed[type][i]) {
464  G_trial_[type][i].Gather(input_L_[i], input_E_[type][i]);
465  already_computed[type][i] = true;
466  }
467  }
468 
469  integral.Mult(t, input_E_[type], output_E_[type], wrt, update_qdata_);
470 
471  // scatter-add to compute residuals on the local processor
472  G_test_[type].ScatterAdd(output_E_[type], output_L_);
473  }
474 
475  // scatter-add to compute global residuals
476  P_test_->MultTranspose(output_L_, output_T_);
477 
478  if constexpr (wrt != NO_DIFFERENTIATION) {
479  // if the user has indicated they'd like to evaluate and differentiate w.r.t.
480  // a specific argument, then we return both the value and gradient w.r.t. that argument
481  //
482  // mfem::Vector arg0 = ...;
483  // mfem::Vector arg1 = ...;
484  // e.g. auto [value, gradient_wrt_arg1] = my_functional(arg0, differentiate_wrt(arg1));
485  return {output_T_, grad_[wrt]};
486  }
487  if constexpr (wrt == NO_DIFFERENTIATION) {
488  // if the user passes only `mfem::Vector`s then we assume they only want the output value
489  //
490  // mfem::Vector arg0 = ...;
491  // mfem::Vector arg1 = ...;
492  // e.g. mfem::Vector value = my_functional(arg0, arg1);
493  return output_T_;
494  }
495  }
496 
498  template <typename... T>
499  auto operator()(double t, const T&... args)
500  {
501  constexpr int num_differentiated_arguments = (std::is_same_v<T, differentiate_wrt_this> + ...);
502  static_assert(num_differentiated_arguments <= 1,
503  "Error: Functional::operator() can only differentiate w.r.t. 1 argument a time");
504  static_assert(sizeof...(T) == num_trial_spaces,
505  "Error: Functional::operator() must take exactly as many arguments as trial spaces");
506 
507  [[maybe_unused]] constexpr uint32_t i = index_of_differentiation<T...>();
508 
509  return (*this)(DifferentiateWRT<i>{}, t, args...);
510  }
511 
520  void updateQdata(bool update_flag) { update_qdata_ = update_flag; }
521 
522 private:
524  bool update_qdata_;
525 
531  class Gradient : public mfem::Operator {
532  public:
537  Gradient(Functional<test(trials...), exec>& f, uint32_t which = 0)
538  : mfem::Operator(f.test_space_->GetTrueVSize(), f.trial_space_[which]->GetTrueVSize()),
539  form_(f),
540  lookup_tables(f.G_test_[Domain::Type::Elements], f.G_trial_[Domain::Type::Elements][which]),
541  which_argument(which),
542  test_space_(f.test_space_),
543  trial_space_(f.trial_space_[which]),
544  df_(f.test_space_->GetTrueVSize())
545  {
546  }
547 
553  virtual void Mult(const mfem::Vector& dx, mfem::Vector& df) const override
554  {
555  form_.ActionOfGradient(dx, df, which_argument);
556  }
557 
559  mfem::Vector& operator()(const mfem::Vector& dx)
560  {
561  form_.ActionOfGradient(dx, df_, which_argument);
562  return df_;
563  }
564 
566  std::unique_ptr<mfem::HypreParMatrix> assemble()
567  {
568  // the CSR graph (sparsity pattern) is reusable, so we cache
569  // that and ask mfem to not free that memory in ~SparseMatrix()
570  constexpr bool sparse_matrix_frees_graph_ptrs = false;
571 
572  // the CSR values are NOT reusable, so we pass ownership of
573  // them to the mfem::SparseMatrix, to be freed in ~SparseMatrix()
574  constexpr bool sparse_matrix_frees_values_ptr = true;
575 
576  constexpr bool col_ind_is_sorted = true;
577 
578  double* values = new double[lookup_tables.nnz]{};
579 
580  std::map<mfem::Geometry::Type, ExecArray<double, 3, exec>> element_gradients[Domain::num_types];
581 
582  for (auto& integral : form_.integrals_) {
583  auto& K_elem = element_gradients[integral.domain_.type_];
584  auto& test_restrictions = form_.G_test_[integral.domain_.type_].restrictions;
585  auto& trial_restrictions = form_.G_trial_[integral.domain_.type_][which_argument].restrictions;
586 
587  if (K_elem.empty()) {
588  for (auto& [geom, test_restriction] : test_restrictions) {
589  auto& trial_restriction = trial_restrictions[geom];
590 
591  K_elem[geom] = ExecArray<double, 3, exec>(test_restriction.num_elements,
592  trial_restriction.nodes_per_elem * trial_restriction.components,
593  test_restriction.nodes_per_elem * test_restriction.components);
594 
595  detail::zero_out(K_elem[geom]);
596  }
597  }
598 
599  integral.ComputeElementGradients(K_elem, which_argument);
600  }
601 
602  for (auto type : {Domain::Type::Elements, Domain::Type::BoundaryElements}) {
603  auto& K_elem = element_gradients[type];
604  auto& test_restrictions = form_.G_test_[type].restrictions;
605  auto& trial_restrictions = form_.G_trial_[type][which_argument].restrictions;
606 
607  if (!K_elem.empty()) {
608  for (auto [geom, elem_matrices] : K_elem) {
609  std::vector<DoF> test_vdofs(test_restrictions[geom].nodes_per_elem * test_restrictions[geom].components);
610  std::vector<DoF> trial_vdofs(trial_restrictions[geom].nodes_per_elem * trial_restrictions[geom].components);
611 
612  for (axom::IndexType e = 0; e < elem_matrices.shape()[0]; e++) {
613  test_restrictions[geom].GetElementVDofs(e, test_vdofs);
614  trial_restrictions[geom].GetElementVDofs(e, trial_vdofs);
615 
616  for (uint32_t i = 0; i < uint32_t(elem_matrices.shape()[1]); i++) {
617  int col = int(trial_vdofs[i].index());
618 
619  for (uint32_t j = 0; j < uint32_t(elem_matrices.shape()[2]); j++) {
620  int row = int(test_vdofs[j].index());
621 
622  int sign = test_vdofs[j].sign() * trial_vdofs[i].sign();
623 
624  // note: col / row appear backwards here, because the element matrix kernel
625  // is actually transposed, as a result of being row-major storage.
626  //
627  // This is kind of confusing, and will be fixed in a future refactor
628  // of the element gradient kernel implementation
629  [[maybe_unused]] auto nz = lookup_tables(row, col);
630  values[lookup_tables(row, col)] += sign * elem_matrices(e, i, j);
631  }
632  }
633  }
634  }
635  }
636  }
637 
638  // Copy the column indices to an auxilliary array as MFEM can mutate these during HypreParMatrix construction
639  col_ind_copy_ = lookup_tables.col_ind;
640 
641  auto J_local =
642  mfem::SparseMatrix(lookup_tables.row_ptr.data(), col_ind_copy_.data(), values, form_.output_L_.Size(),
643  form_.input_L_[which_argument].Size(), sparse_matrix_frees_graph_ptrs,
644  sparse_matrix_frees_values_ptr, col_ind_is_sorted);
645 
646  auto* R = form_.test_space_->Dof_TrueDof_Matrix();
647 
648  auto* A =
649  new mfem::HypreParMatrix(test_space_->GetComm(), test_space_->GlobalVSize(), trial_space_->GlobalVSize(),
650  test_space_->GetDofOffsets(), trial_space_->GetDofOffsets(), &J_local);
651 
652  auto* P = trial_space_->Dof_TrueDof_Matrix();
653 
654  std::unique_ptr<mfem::HypreParMatrix> K(mfem::RAP(R, A, P));
655 
656  delete A;
657 
658  return K;
659  };
660 
661  friend auto assemble(Gradient& g) { return g.assemble(); }
662 
663  private:
665  Functional<test(trials...), exec>& form_;
666 
672  GradientAssemblyLookupTables lookup_tables;
673 
678  std::vector<int> col_ind_copy_;
679 
690  uint32_t which_argument;
691 
693  const mfem::ParFiniteElementSpace* test_space_;
694 
696  const mfem::ParFiniteElementSpace* trial_space_;
697 
699  mfem::Vector df_;
700  };
701 
703  const mfem::ParFiniteElementSpace* test_space_;
704 
706  std::array<const mfem::ParFiniteElementSpace*, num_trial_spaces> trial_space_;
707 
712  const mfem::Operator* P_trial_[num_trial_spaces];
713 
715  mutable mfem::Vector input_L_[num_trial_spaces];
716 
717  BlockElementRestriction G_trial_[Domain::num_types][num_trial_spaces];
718 
719  mutable std::vector<mfem::BlockVector> input_E_[Domain::num_types];
720 
721  std::vector<Integral> integrals_;
722 
723  mutable mfem::BlockVector output_E_[Domain::num_types];
724 
725  BlockElementRestriction G_test_[Domain::num_types];
726 
728  mutable mfem::Vector output_L_;
729 
730  const mfem::Operator* P_test_;
731 
733  mutable mfem::Vector output_T_;
734 
736  mutable std::vector<Gradient> grad_;
737 };
738 
739 } // namespace serac
740 
741 #include "functional_qoi.inl"
many of the functions in this file amount to extracting element indices from an mfem::Mesh like
This file contains helper traits and enumerations for classifying finite elements.
a specialization of serac::Functional for quantities of interest
This file contains the all the necessary functions and macros required for logging as well as a helpe...
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
tuple(T...) -> tuple< T... >
Class template argument deduction rule for tuples.
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::pair< std::unique_ptr< mfem::ParFiniteElementSpace >, std::unique_ptr< mfem::FiniteElementCollection > > generateParFiniteElementSpace(mfem::ParMesh *mesh)
create an mfem::ParFiniteElementSpace from one of serac's tag types: H1, Hcurl, L2
Definition: functional.hpp:103
Definitions of quadrature rules for quads and hexes.
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
Compile-time alias for index of differentiation.
Definition: functional.hpp:65
these classes are a little confusing. These two special types represent the similar (but different) c...
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
Implementation of the tensor class used by Functional.