From 811bbd969427afad71e358df53e0d5b45b91d7d7 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Mon, 9 Nov 2026 13:48:09 -0500 Subject: [PATCH] feat(backend): Added backend FEM code (specifically MFEM backend) Much of the code to do actual finite element computations lives in this backend. The idea here is that the physics is all seperate and more descriptive than the backend. Further, we could, in principal swap out to another finite element backend (e.g. deal.II) without messing up stuff outside of this backend. Note LLM's made signifigant contributions to this module LLMs (ChatGPT6 astra max and Claude Opus 5.5 medium) made signifigant contributions to this module. All LLM written code has been read over and verified. --- src/include/serif/backend/mfem/README.md | 61 ++ .../mfem/discretization/basis_evaluation.hpp | 260 ++++++ .../mfem/discretization/boundary_faces.hpp | 43 + .../mfem/discretization/boundary_trace.hpp | 95 +++ .../discretization/coefficient_tracking.hpp | 117 +++ .../backend/mfem/discretization/dof_map.hpp | 498 ++++++++++++ .../mfem/discretization/dof_selection.hpp | 158 ++++ .../discretization/element_coordinates.hpp | 59 ++ .../mfem/discretization/field_values.hpp | 120 +++ .../mfem/discretization/mass_diagonal.hpp | 27 + .../discretization/quadrature/concepts.hpp | 44 ++ .../discretization/quadrature/configure.hpp | 32 + .../quadrature/mfem_resolver.hpp | 134 ++++ .../quadrature/mfem_static_order.hpp | 39 + .../mfem/discretization/quadrature/order.hpp | 86 ++ .../mfem/discretization/reconstruction.hpp | 83 ++ .../backend/mfem/discretization/resources.hpp | 43 + .../backend/mfem/discretization/schema.hpp | 26 + .../mfem/discretization/tensor_basis.hpp | 539 +++++++++++++ .../mfem/discretization/term_binding.hpp | 116 +++ .../mfem/discretization/term_coefficients.hpp | 44 ++ .../backend/mfem/discretization/topology.hpp | 12 + .../geometry/deformation/computations.hpp | 168 ++++ .../constrained_radial_extension.hpp | 193 +++++ .../deformation/detail/radial_locator.hpp | 79 ++ .../deformation/displacement_mapping.hpp | 42 + .../geometry/deformation/geometry_step.hpp | 55 ++ .../mfem/geometry/deformation/inputs.hpp | 157 ++++ .../deformation/radial_interpolation.hpp | 30 + .../deformation/reference_geometry.hpp | 53 ++ .../geometry/deformation/surface_metric.hpp | 28 + .../surface/boundary_computations.hpp | 69 ++ .../mfem/geometry/surface/boundary_data.hpp | 81 ++ .../mfem/geometry/surface/boundary_inputs.hpp | 90 +++ .../mfem/geometry/surface/boundary_rows.hpp | 141 ++++ .../geometry/surface/pressure_boundary.hpp | 4 + .../geometry/surface/pressure_field_block.hpp | 54 ++ .../compactification/compactification.hpp | 75 ++ .../mapping/compactification/input_result.hpp | 36 + .../mfem/mapping/compactification/kelvin.hpp | 191 +++++ .../mapping/compactification/variation.hpp | 35 + .../serif/backend/mfem/mapping/context.hpp | 92 +++ .../serif/backend/mfem/mapping/data.hpp | 254 ++++++ .../backend/mfem/mapping/domain_mapper.hpp | 219 ++++++ .../serif/backend/mfem/mapping/evaluator.hpp | 82 ++ .../serif/backend/mfem/mapping/status.hpp | 58 ++ .../backend/mfem/mapping/transformations.hpp | 60 ++ .../serif/backend/mfem/mapping/variation.hpp | 28 + .../backend/mfem/mapping/volume_cache.hpp | 28 + .../serif/backend/mfem/model/binding.hpp | 144 ++++ .../mfem/model/computations/interior.hpp | 94 +++ .../model/computations/physical_blocks.hpp | 49 ++ .../model/computations/potential_infinity.hpp | 40 + .../model/computations/pressure_boundary.hpp | 69 ++ .../mfem/model/computations/scalar_row.hpp | 34 + .../model/computations/shape_derivatives.hpp | 20 + .../mfem/model/computations/solver_layout.hpp | 19 + .../model/computations/surface_potential.hpp | 45 ++ .../serif/backend/mfem/model/deformation.hpp | 69 ++ .../backend/mfem/model/discretization.hpp | 87 ++ .../serif/backend/mfem/model/equilibrium.hpp | 200 +++++ .../mfem/model/equilibrium_evaluator.hpp | 171 ++++ .../serif/backend/mfem/model/failure.hpp | 15 + .../backend/mfem/model/finite_elements.hpp | 136 ++++ .../serif/backend/mfem/model/graph.hpp | 174 ++++ .../serif/backend/mfem/model/inputs.hpp | 66 ++ .../mfem/model/normalization/computations.hpp | 117 +++ .../backend/mfem/model/normalized_system.hpp | 76 ++ .../mfem/model/preconditioned_system.hpp | 78 ++ .../model/preconditioning/computations.hpp | 56 ++ .../serif/backend/mfem/model/scalar_row.hpp | 15 + .../serif/backend/mfem/model/state.hpp | 86 ++ .../backend/mfem/model/surface_potential.hpp | 40 + .../serif/backend/mfem/model/system.hpp | 77 ++ .../serif/backend/mfem/model/volume.hpp | 39 + .../backend/mfem/model/volume_actions.hpp | 89 +++ .../mfem/model/volume_displacement.hpp | 81 ++ .../serif/backend/mfem/operators/actions.hpp | 34 + .../backend/mfem/operators/element_data.hpp | 47 ++ .../mfem/operators/element_evaluation.hpp | 163 ++++ .../mfem/operators/element_preparation.hpp | 327 ++++++++ .../mfem/operators/element_transpose.hpp | 70 ++ .../mfem/operators/geometry_derivatives.hpp | 159 ++++ .../mfem/operators/jacobian_operator.hpp | 47 ++ .../mfem/operators/linearized_block.hpp | 56 ++ .../mfem/operators/operand_evaluation.hpp | 339 ++++++++ .../backend/mfem/operators/preparation.hpp | 55 ++ .../backend/mfem/operators/quadrature.hpp | 61 ++ .../backend/mfem/operators/rejection.hpp | 21 + .../backend/mfem/operators/replaced_rows.hpp | 14 + .../mfem/operators/volume_application.hpp | 367 +++++++++ .../mfem/operators/volume_computations.hpp | 248 ++++++ .../backend/mfem/operators/volume_data.hpp | 168 ++++ .../mfem/operators/volume_field_block.hpp | 570 ++++++++++++++ .../backend/mfem/operators/volume_inputs.hpp | 151 ++++ .../backend/mfem/operators/volume_stages.hpp | 421 ++++++++++ .../backend/mfem/operators/workspace.hpp | 34 + .../serif/backend/mfem/projection/radial.hpp | 84 ++ src/include/serif/backend/mfem/runtime.hpp | 10 + .../serif/backend/mfem/serialize/README.md | 50 ++ .../backend/mfem/serialize/configuration.hpp | 63 ++ .../backend/mfem/serialize/discretization.hpp | 235 ++++++ .../serif/backend/mfem/serialize/options.hpp | 28 + .../backend/mfem/serialize/resources.hpp | 63 ++ .../serif/backend/mfem/serialize/top.hpp | 179 +++++ .../backend/mfem/shared/boundary_elements.hpp | 70 ++ .../serif/backend/mfem/shared/finite.hpp | 32 + .../serif/backend/mfem/shared/invertible.hpp | 23 + .../serif/backend/mfem/shared/parallel.hpp | 58 ++ .../mfem/shared/reference_tracking.hpp | 48 ++ .../mfem/solver/admissibility/geometry.hpp | 156 ++++ .../continuation/detail/physical_transfer.hpp | 57 ++ .../serif/backend/mfem/solver/equilibrium.hpp | 42 + .../backend/mfem/solver/linear/solve.hpp | 30 + .../mfem/solver/nonlinear/stellar_problem.hpp | 134 ++++ .../normalization/metric_computations.hpp | 43 + .../mfem/solver/normalization/operators.hpp | 128 +++ .../mfem/solver/preconditioner/border.hpp | 24 + .../preconditioner/computations/coupling.hpp | 43 + .../preconditioner/computations/inverse.hpp | 113 +++ .../preconditioner/computations/ordering.hpp | 140 ++++ .../computations/physical_blocks.hpp | 69 ++ .../mfem/solver/preconditioner/coupling.hpp | 31 + .../mfem/solver/preconditioner/graph.hpp | 101 +++ .../mfem/solver/preconditioner/inverse.hpp | 18 + .../solver/preconditioner/inverse/amg.hpp | 12 + .../solver/preconditioner/inverse/dense.hpp | 12 + .../preconditioner/inverse/diagonal.hpp | 11 + .../preconditioner/inverse/sparse_direct.hpp | 50 ++ .../preconditioner/inverse/validation.hpp | 28 + .../mfem/solver/preconditioner/solver.hpp | 29 + .../serif/backend/mfem/solver/seed/fitted.hpp | 63 ++ .../serif/backend/mfem/solver/seed/state.hpp | 137 ++++ src/include/serif/backend/mfem/types.hpp | 13 + .../mfem/discretization/basis_evaluation.cpp | 254 ++++++ .../mfem/discretization/boundary_trace.cpp | 170 ++++ .../discretization/element_coordinates.cpp | 119 +++ .../mfem/discretization/mass_diagonal.cpp | 157 ++++ .../mfem/discretization/reconstruction.cpp | 228 ++++++ .../backend/mfem/discretization/topology.cpp | 34 + .../geometry/deformation/geometry_step.cpp | 276 +++++++ .../mfem/geometry/deformation/inputs.cpp | 27 + .../deformation/radial_interpolation.cpp | 229 ++++++ .../geometry/deformation/radial_locator.cpp | 126 +++ .../deformation/reference_geometry.cpp | 223 ++++++ .../geometry/deformation/surface_metric.cpp | 85 ++ .../mfem/mapping/compactification/kelvin.cpp | 269 +++++++ src/lib/serif/backend/mfem/mapping/data.cpp | 133 ++++ .../backend/mfem/mapping/domain_mapper.cpp | 741 ++++++++++++++++++ .../serif/backend/mfem/mapping/evaluator.cpp | 131 ++++ .../backend/mfem/mapping/transformations.cpp | 78 ++ .../backend/mfem/mapping/volume_cache.cpp | 112 +++ .../serif/backend/mfem/model/deformation.cpp | 22 + .../backend/mfem/model/finite_elements.cpp | 249 ++++++ .../serif/backend/mfem/model/scalar_row.cpp | 40 + .../backend/mfem/model/surface_potential.cpp | 260 ++++++ src/lib/serif/backend/mfem/model/system.cpp | 101 +++ .../serif/backend/mfem/model/system_data.hpp | 24 + .../backend/mfem/model/system_selection.cpp | 341 ++++++++ .../serif/backend/mfem/operators/actions.cpp | 254 ++++++ .../mfem/operators/linearized_block.cpp | 109 +++ .../backend/mfem/operators/replaced_rows.cpp | 151 ++++ .../serif/backend/mfem/projection/radial.cpp | 269 +++++++ src/lib/serif/backend/mfem/runtime.cpp | 18 + .../backend/mfem/serialize/resources.cpp | 432 ++++++++++ src/lib/serif/backend/mfem/serialize/top.cpp | 365 +++++++++ .../serif/backend/mfem/shared/parallel.cpp | 64 ++ .../mfem/shared/reference_tracking.cpp | 63 ++ .../backend/mfem/solver/linear/solve.cpp | 179 +++++ .../mfem/solver/normalization/operators.cpp | 106 +++ .../mfem/solver/preconditioner/border.cpp | 151 ++++ .../mfem/solver/preconditioner/coupling.cpp | 378 +++++++++ .../preconditioner/coupling_internal.hpp | 9 + .../preconditioner/coupling_selection.cpp | 118 +++ .../solver/preconditioner/inverse/amg.cpp | 109 +++ .../solver/preconditioner/inverse/dense.cpp | 168 ++++ .../preconditioner/inverse/diagonal.cpp | 79 ++ .../preconditioner/inverse/direct_lu.hpp | 68 ++ .../inverse/direct_lu_accelerate.cpp | 157 ++++ .../preconditioner/inverse/direct_lu_none.cpp | 39 + .../inverse/direct_lu_umfpack.cpp | 120 +++ .../preconditioner/inverse/sparse_direct.cpp | 139 ++++ .../preconditioner/inverse/validation.cpp | 38 + .../mfem/solver/preconditioner/solver.cpp | 43 + 184 files changed, 21058 insertions(+) create mode 100644 src/include/serif/backend/mfem/README.md create mode 100644 src/include/serif/backend/mfem/discretization/basis_evaluation.hpp create mode 100644 src/include/serif/backend/mfem/discretization/boundary_faces.hpp create mode 100644 src/include/serif/backend/mfem/discretization/boundary_trace.hpp create mode 100644 src/include/serif/backend/mfem/discretization/coefficient_tracking.hpp create mode 100644 src/include/serif/backend/mfem/discretization/dof_map.hpp create mode 100644 src/include/serif/backend/mfem/discretization/dof_selection.hpp create mode 100644 src/include/serif/backend/mfem/discretization/element_coordinates.hpp create mode 100644 src/include/serif/backend/mfem/discretization/field_values.hpp create mode 100644 src/include/serif/backend/mfem/discretization/mass_diagonal.hpp create mode 100644 src/include/serif/backend/mfem/discretization/quadrature/concepts.hpp create mode 100644 src/include/serif/backend/mfem/discretization/quadrature/configure.hpp create mode 100644 src/include/serif/backend/mfem/discretization/quadrature/mfem_resolver.hpp create mode 100644 src/include/serif/backend/mfem/discretization/quadrature/mfem_static_order.hpp create mode 100644 src/include/serif/backend/mfem/discretization/quadrature/order.hpp create mode 100644 src/include/serif/backend/mfem/discretization/reconstruction.hpp create mode 100644 src/include/serif/backend/mfem/discretization/resources.hpp create mode 100644 src/include/serif/backend/mfem/discretization/schema.hpp create mode 100644 src/include/serif/backend/mfem/discretization/tensor_basis.hpp create mode 100644 src/include/serif/backend/mfem/discretization/term_binding.hpp create mode 100644 src/include/serif/backend/mfem/discretization/term_coefficients.hpp create mode 100644 src/include/serif/backend/mfem/discretization/topology.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/computations.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/constrained_radial_extension.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/detail/radial_locator.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/displacement_mapping.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/geometry_step.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/inputs.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/radial_interpolation.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/reference_geometry.hpp create mode 100644 src/include/serif/backend/mfem/geometry/deformation/surface_metric.hpp create mode 100644 src/include/serif/backend/mfem/geometry/surface/boundary_computations.hpp create mode 100644 src/include/serif/backend/mfem/geometry/surface/boundary_data.hpp create mode 100644 src/include/serif/backend/mfem/geometry/surface/boundary_inputs.hpp create mode 100644 src/include/serif/backend/mfem/geometry/surface/boundary_rows.hpp create mode 100644 src/include/serif/backend/mfem/geometry/surface/pressure_boundary.hpp create mode 100644 src/include/serif/backend/mfem/geometry/surface/pressure_field_block.hpp create mode 100644 src/include/serif/backend/mfem/mapping/compactification/compactification.hpp create mode 100644 src/include/serif/backend/mfem/mapping/compactification/input_result.hpp create mode 100644 src/include/serif/backend/mfem/mapping/compactification/kelvin.hpp create mode 100644 src/include/serif/backend/mfem/mapping/compactification/variation.hpp create mode 100644 src/include/serif/backend/mfem/mapping/context.hpp create mode 100644 src/include/serif/backend/mfem/mapping/data.hpp create mode 100644 src/include/serif/backend/mfem/mapping/domain_mapper.hpp create mode 100644 src/include/serif/backend/mfem/mapping/evaluator.hpp create mode 100644 src/include/serif/backend/mfem/mapping/status.hpp create mode 100644 src/include/serif/backend/mfem/mapping/transformations.hpp create mode 100644 src/include/serif/backend/mfem/mapping/variation.hpp create mode 100644 src/include/serif/backend/mfem/mapping/volume_cache.hpp create mode 100644 src/include/serif/backend/mfem/model/binding.hpp create mode 100644 src/include/serif/backend/mfem/model/computations/interior.hpp create mode 100644 src/include/serif/backend/mfem/model/computations/physical_blocks.hpp create mode 100644 src/include/serif/backend/mfem/model/computations/potential_infinity.hpp create mode 100644 src/include/serif/backend/mfem/model/computations/pressure_boundary.hpp create mode 100644 src/include/serif/backend/mfem/model/computations/scalar_row.hpp create mode 100644 src/include/serif/backend/mfem/model/computations/shape_derivatives.hpp create mode 100644 src/include/serif/backend/mfem/model/computations/solver_layout.hpp create mode 100644 src/include/serif/backend/mfem/model/computations/surface_potential.hpp create mode 100644 src/include/serif/backend/mfem/model/deformation.hpp create mode 100644 src/include/serif/backend/mfem/model/discretization.hpp create mode 100644 src/include/serif/backend/mfem/model/equilibrium.hpp create mode 100644 src/include/serif/backend/mfem/model/equilibrium_evaluator.hpp create mode 100644 src/include/serif/backend/mfem/model/failure.hpp create mode 100644 src/include/serif/backend/mfem/model/finite_elements.hpp create mode 100644 src/include/serif/backend/mfem/model/graph.hpp create mode 100644 src/include/serif/backend/mfem/model/inputs.hpp create mode 100644 src/include/serif/backend/mfem/model/normalization/computations.hpp create mode 100644 src/include/serif/backend/mfem/model/normalized_system.hpp create mode 100644 src/include/serif/backend/mfem/model/preconditioned_system.hpp create mode 100644 src/include/serif/backend/mfem/model/preconditioning/computations.hpp create mode 100644 src/include/serif/backend/mfem/model/scalar_row.hpp create mode 100644 src/include/serif/backend/mfem/model/state.hpp create mode 100644 src/include/serif/backend/mfem/model/surface_potential.hpp create mode 100644 src/include/serif/backend/mfem/model/system.hpp create mode 100644 src/include/serif/backend/mfem/model/volume.hpp create mode 100644 src/include/serif/backend/mfem/model/volume_actions.hpp create mode 100644 src/include/serif/backend/mfem/model/volume_displacement.hpp create mode 100644 src/include/serif/backend/mfem/operators/actions.hpp create mode 100644 src/include/serif/backend/mfem/operators/element_data.hpp create mode 100644 src/include/serif/backend/mfem/operators/element_evaluation.hpp create mode 100644 src/include/serif/backend/mfem/operators/element_preparation.hpp create mode 100644 src/include/serif/backend/mfem/operators/element_transpose.hpp create mode 100644 src/include/serif/backend/mfem/operators/geometry_derivatives.hpp create mode 100644 src/include/serif/backend/mfem/operators/jacobian_operator.hpp create mode 100644 src/include/serif/backend/mfem/operators/linearized_block.hpp create mode 100644 src/include/serif/backend/mfem/operators/operand_evaluation.hpp create mode 100644 src/include/serif/backend/mfem/operators/preparation.hpp create mode 100644 src/include/serif/backend/mfem/operators/quadrature.hpp create mode 100644 src/include/serif/backend/mfem/operators/rejection.hpp create mode 100644 src/include/serif/backend/mfem/operators/replaced_rows.hpp create mode 100644 src/include/serif/backend/mfem/operators/volume_application.hpp create mode 100644 src/include/serif/backend/mfem/operators/volume_computations.hpp create mode 100644 src/include/serif/backend/mfem/operators/volume_data.hpp create mode 100644 src/include/serif/backend/mfem/operators/volume_field_block.hpp create mode 100644 src/include/serif/backend/mfem/operators/volume_inputs.hpp create mode 100644 src/include/serif/backend/mfem/operators/volume_stages.hpp create mode 100644 src/include/serif/backend/mfem/operators/workspace.hpp create mode 100644 src/include/serif/backend/mfem/projection/radial.hpp create mode 100644 src/include/serif/backend/mfem/runtime.hpp create mode 100644 src/include/serif/backend/mfem/serialize/README.md create mode 100644 src/include/serif/backend/mfem/serialize/configuration.hpp create mode 100644 src/include/serif/backend/mfem/serialize/discretization.hpp create mode 100644 src/include/serif/backend/mfem/serialize/options.hpp create mode 100644 src/include/serif/backend/mfem/serialize/resources.hpp create mode 100644 src/include/serif/backend/mfem/serialize/top.hpp create mode 100644 src/include/serif/backend/mfem/shared/boundary_elements.hpp create mode 100644 src/include/serif/backend/mfem/shared/finite.hpp create mode 100644 src/include/serif/backend/mfem/shared/invertible.hpp create mode 100644 src/include/serif/backend/mfem/shared/parallel.hpp create mode 100644 src/include/serif/backend/mfem/shared/reference_tracking.hpp create mode 100644 src/include/serif/backend/mfem/solver/admissibility/geometry.hpp create mode 100644 src/include/serif/backend/mfem/solver/continuation/detail/physical_transfer.hpp create mode 100644 src/include/serif/backend/mfem/solver/equilibrium.hpp create mode 100644 src/include/serif/backend/mfem/solver/linear/solve.hpp create mode 100644 src/include/serif/backend/mfem/solver/nonlinear/stellar_problem.hpp create mode 100644 src/include/serif/backend/mfem/solver/normalization/metric_computations.hpp create mode 100644 src/include/serif/backend/mfem/solver/normalization/operators.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/border.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/computations/coupling.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/computations/inverse.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/computations/ordering.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/computations/physical_blocks.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/coupling.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/graph.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/inverse.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/inverse/amg.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/inverse/dense.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/inverse/diagonal.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/inverse/validation.hpp create mode 100644 src/include/serif/backend/mfem/solver/preconditioner/solver.hpp create mode 100644 src/include/serif/backend/mfem/solver/seed/fitted.hpp create mode 100644 src/include/serif/backend/mfem/solver/seed/state.hpp create mode 100644 src/include/serif/backend/mfem/types.hpp create mode 100644 src/lib/serif/backend/mfem/discretization/basis_evaluation.cpp create mode 100644 src/lib/serif/backend/mfem/discretization/boundary_trace.cpp create mode 100644 src/lib/serif/backend/mfem/discretization/element_coordinates.cpp create mode 100644 src/lib/serif/backend/mfem/discretization/mass_diagonal.cpp create mode 100644 src/lib/serif/backend/mfem/discretization/reconstruction.cpp create mode 100644 src/lib/serif/backend/mfem/discretization/topology.cpp create mode 100644 src/lib/serif/backend/mfem/geometry/deformation/geometry_step.cpp create mode 100644 src/lib/serif/backend/mfem/geometry/deformation/inputs.cpp create mode 100644 src/lib/serif/backend/mfem/geometry/deformation/radial_interpolation.cpp create mode 100644 src/lib/serif/backend/mfem/geometry/deformation/radial_locator.cpp create mode 100644 src/lib/serif/backend/mfem/geometry/deformation/reference_geometry.cpp create mode 100644 src/lib/serif/backend/mfem/geometry/deformation/surface_metric.cpp create mode 100644 src/lib/serif/backend/mfem/mapping/compactification/kelvin.cpp create mode 100644 src/lib/serif/backend/mfem/mapping/data.cpp create mode 100644 src/lib/serif/backend/mfem/mapping/domain_mapper.cpp create mode 100644 src/lib/serif/backend/mfem/mapping/evaluator.cpp create mode 100644 src/lib/serif/backend/mfem/mapping/transformations.cpp create mode 100644 src/lib/serif/backend/mfem/mapping/volume_cache.cpp create mode 100644 src/lib/serif/backend/mfem/model/deformation.cpp create mode 100644 src/lib/serif/backend/mfem/model/finite_elements.cpp create mode 100644 src/lib/serif/backend/mfem/model/scalar_row.cpp create mode 100644 src/lib/serif/backend/mfem/model/surface_potential.cpp create mode 100644 src/lib/serif/backend/mfem/model/system.cpp create mode 100644 src/lib/serif/backend/mfem/model/system_data.hpp create mode 100644 src/lib/serif/backend/mfem/model/system_selection.cpp create mode 100644 src/lib/serif/backend/mfem/operators/actions.cpp create mode 100644 src/lib/serif/backend/mfem/operators/linearized_block.cpp create mode 100644 src/lib/serif/backend/mfem/operators/replaced_rows.cpp create mode 100644 src/lib/serif/backend/mfem/projection/radial.cpp create mode 100644 src/lib/serif/backend/mfem/runtime.cpp create mode 100644 src/lib/serif/backend/mfem/serialize/resources.cpp create mode 100644 src/lib/serif/backend/mfem/serialize/top.cpp create mode 100644 src/lib/serif/backend/mfem/shared/parallel.cpp create mode 100644 src/lib/serif/backend/mfem/shared/reference_tracking.cpp create mode 100644 src/lib/serif/backend/mfem/solver/linear/solve.cpp create mode 100644 src/lib/serif/backend/mfem/solver/normalization/operators.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/border.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/coupling.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/coupling_internal.hpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/coupling_selection.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/amg.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/dense.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/diagonal.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu.hpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_accelerate.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_none.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_umfpack.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/inverse/validation.cpp create mode 100644 src/lib/serif/backend/mfem/solver/preconditioner/solver.cpp diff --git a/src/include/serif/backend/mfem/README.md b/src/include/serif/backend/mfem/README.md new file mode 100644 index 00000000..1501ef1a --- /dev/null +++ b/src/include/serif/backend/mfem/README.md @@ -0,0 +1,61 @@ +# MFEM backend + +MFEM implementations live here and in the matching `src/lib/serif/backend/mfem` tree. Physics descriptions, equations, EOS relations, normalization rules, Newton acceptance, and continuation scheduling stay in their ordinary SERiF modules. The directory migration preserves physics-oriented namespaces; include paths identify the implementation backend. Helpers whose responsibility changed now follow their owner: `model::make_discretization`, `geometry::surface::AutomaticPressureSurface`, generic preconditioner diagonal regularization, and discretization element coordinates. + +| Directory | Responsibility | +| --- | --- | +| `discretization` | Bind physical terms to MFEM spaces; own coefficient ordering, element orientation, basis evaluation, quadrature, traces, and MFEM mesh extraction. | +| `mapping` | Evaluate physical mappings and their variations from supplied reference geometry and current coefficients. | +| `geometry` | Adapt surface and deformation descriptions to MFEM resources, including pressure traces and boundary metrics. | +| `operators` | Integrate equation contributions and expose their linear actions, transposes, diagonals, and assembly. No EOS-specific pressure construction belongs here. | +| `model` | Connect the preceding adapters into the model's computational graph and publish owning evaluated systems. The deformation-to-equation bridge belongs here. | +| `solver` | Supply MFEM linear solvers, inverses, geometry admissibility, and stellar adapters to the common Newton and continuation algorithms. | +| `projection` | Project reusable radial profiles into MFEM finite-element spaces. | +| `shared` | Small backend facilities with multiple independent consumers: live mesh/space snapshots, boundary traversal, vector spans, and matrix/vector checks. | + +## Ownership and reuse + +The computational graph owns immutable prepared data, including fixed geometry at equation quadrature points. A mapper does not own a second mutable reference-geometry cache. Standalone point and face mapping evaluate their own reference geometry directly. Accepted systems retain their preparation owners and remain independent of subsequent candidate preparation. + +MFEM resources are externally mutable. Snapshot policies distinguish topology, material/boundary labels, finite-element layout, and physical/logical coordinate revisions. Input factories capture current revisions; reusing stale input metadata is rejected. Notify MFEM of direct coordinate changes (for example with `NodesUpdated`) and rebuild the input context after changing its resources. Revision tracking cannot detect unreported writes to external coordinate storage. + +A `DofMap` owns the meaning of reduced coefficients. Public coefficient access validates that meaning. Internal element traversals obtain a validated `Elements` view once, then retain element bounds, support, and vector-size checks without rescanning all mesh attributes for every element. Do not mutate the mesh or space during a traversal. Element orientation is implemented once in `discretization/element_coordinates`. + +Surface normalization and surface preconditioning share validated boundary traversal, but keep their different integrands: vector radial/harmonic metric diagonals versus scalar H1 mass and stiffness matrices. Likewise, model block selection preserves native child assembly and repeated ranges; restriction of an arbitrary preconditioner operator retains its separate unique-index contract. + +## Core boundary + +`serif_core_lib` and `serif_core_dep` have no MFEM dependency. The core header check and the standard-vector Newton tests compile without MFEM include paths. The public `serif` library includes the core and adds this backend. Convenience entry headers may select MFEM adapters, while physical configuration and numerical algorithms remain available separately. + +Continuation families, physical parameter laws, coordinate stepping, and the driver share `serif::solver::continuation` in `solver/continuation`. Its entry header is backend independent; only stellar correction wiring and finite-element state transfer live in `backend/mfem/solver/continuation`. + +The common Newton loop asks its vector adapter only for owned vectors and coefficient spans. The MFEM wrapper supplies linear-solve wiring; it does not duplicate acceptance, backtracking, or checkpoint ownership. A future backend should implement its discretization and execution adapters and reuse these common rules, rather than copy the nonlinear or continuation algorithms. + +Current term bindings require serial, conforming three-dimensional MFEM spaces. Supported value, gradient, divergence, trace, and tensor/dense basis paths retain their explicit compile-time and runtime checks. This layout does not imply that every MFEM space or another finite-element library is already supported. + +## Optional surface density reconstruction + +Pass the numerical choice directly to the stellar factory, after any deformation: + +```cpp +namespace reconstruction = serif::discretization::reconstruction; +using Density = serif::discretization::blocks::density::mass; +auto finite_elements = serif::model::make_stellar_discretization( + star, mesh_options, moving_boundary, + reconstruction::SurfacePower{ + .density_exponent = n, + .region = reconstruction::SurfaceAdjacent< + serif::discretization::domain::StellarSurfaceBoundary>{}}); +``` + +Omitting the argument preserves ordinary interpolation. Exponent 1 takes that same path without a reconstruction plan. Stored coefficients remain physical nodal densities. Only material elements touching the selected boundary change: with inward reference coordinate `t=0` at the surface and `t=1` at their inner face, their zero-trace radial space becomes `t^n P_(p-1)`. Tangential interpolation and deeper elements remain ordinary. The full basis retains surface degrees of freedom and reproduces constants; the vacuum solve enforces their zero values explicitly. + +The plan belongs to the field binding and is immutable throughout a solve. Its exponent is an explicit numerical choice; changing an EOS during continuation does not silently change the representation. A different profile, mesh or exponent requires a new binding and deliberate field transfer. Construction validates matching radial coordinates and the actual hanging-node constraints. Tangential/equatorial refinement is supported when those constraints preserve the reconstructed traces. Incompatible radial hanging layers fail with a SERiF diagnostic. + +`discretization::backend::mfem::FieldValues` reads the same physical representation as the equation operators. Use it for arbitrary-point values, scalar H1 gradients or batches of samples. Its binding, coefficients and mesh resources must outlive the reader. Extra deformation/compactification maps must be supplied for physical gradients and H(div) values. Ordinary MFEM GridFunction interpolation of reconstructed nodal values is not the represented field. + +Reference preparation replaces only the radial tensor factors; forward and transpose contractions retain the existing kernels. Automatic volume rules include a fractional-power allowance before quadrature-policy resolution, so an explicit fixed order remains authoritative. Gaussian quadrature is still approximate and should be checked by increasing its order for a new regime. Nonnegativity certifies the polynomial carrier with its positive weight and requires the selected zero-density trace. This implementation does not provide a certificate for finite-pressure surface lifting or a general EOS surface profile. + +To add a numerical form, provide an ADL-visible `make_reconstruction(specification, binding)` overload, a specification with `using Term`, and an immutable `RadialProfile` that evaluates complete nodal functions and inward derivatives, including endpoints. The profile must preserve nodal values, constants and compatible face traces. Add its admissibility certificate and archive codec explicitly; missing capabilities report errors. No MFEM element subclass or physics-equation rewrite is required. The current surface-power form supports scalar H1 mass density only. + +Weighted archive bindings use schema version 2; older readers reject them rather than reinterpret their coefficients. The live Rayfem adapter sends a higher-order sampled display field labeled `density (sampled)`. This display approximation is never used by the solver or TOP exporter. diff --git a/src/include/serif/backend/mfem/discretization/basis_evaluation.hpp b/src/include/serif/backend/mfem/discretization/basis_evaluation.hpp new file mode 100644 index 00000000..6d4113fd --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/basis_evaluation.hpp @@ -0,0 +1,260 @@ +#pragma once + +/** + * @file basis_evaluation.hpp + * @author Emily M. Boudreaux + * @brief Basis evaluation, pointwise mapping, and transpose application using dense or tensor-product representations. + */ +#include + +#include "mfem/fem/fe/fe_base.hpp" +#include "mfem/fem/intrules.hpp" + +#include "serif/backend/mfem/discretization/tensor_basis.hpp" + +namespace serif::discretization::backend::mfem { + /** + * @brief Reusable scratch storage for basis contractions, reducing repeated allocations. + */ + struct BasisWorkspace { + TensorWorkspace tensor; + ::mfem::Vector reference_values; + ::mfem::Vector mapping_input; + ::mfem::Vector mapping_output; + }; + + /** + * @class BasisEvaluation + * @brief Represents an element-local basis evaluation operator, supporting dense or tensor contractions, pointwise mappings, and transpose application. + * + * Consider a scalar field represented on one finite element: + * + * \f[ + * \rho_h(\xi) = \sum_{j=1}^{n} c_j \phi_j(\xi). + * \f] + * + * Here, \f$c_{j}\f$ is a scalar coefficient, \f$\phi_{j}\f$ is its associated basis function, + * and \f$\xi\f$ is the canonical reference coordinate. The field is expressed in + * these coordinates through the element mapping. + * + * For a quadrature rule with \f$N_{q}\f$ points, define the evaluation matrix \f$\mathbf{E}\f$ by + * + * \f[ + * E_{qj} = \phi_j(\xi_q). + * \f] + * + * \f$\mathbf{E}\f$ therefore has \f$N_{q}\f$ rows and \f$n\f$ columns. The field value at one point is + * + * \f[ + * \rho_h(\xi_q) = \sum_{j=1}^{n} E_{qj} c_j. + * \f] + * + * Collecting the samples into a vector gives + * + * \f[ + * \boldsymbol{\rho}_h = \mathbf{E}\mathbf{c}. + * \f] + * + * This class represents that linear operation without necessarily storing + * the full matrix \f$\mathbf{E}\f$: it can instead apply equivalent tensor contractions. + * + * @par Example + * Evaluate a constant scalar field on the reference cube, then compute + * its integral against each basis function. + * + * @code{.cpp} + * namespace backend = serif::discretization::backend::mfem; + * + * // First-order nodal H1 basis on the reference cube. + * ::mfem::H1_HexahedronElement element(1); + * const ::mfem::IntegrationRule& rule = + * ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 2); + * + * backend::TensorBasisCache cache; + * backend::BasisWorkspace workspace; + * backend::BasisEvaluation basis; + * + * // Prepare E: one scalar basis value per quadrature point and DOF. + * basis.sample( + * element, + * rule, + * backend::ReferenceOperation::value, + * 1, // One copy of the scalar basis. + * cache, + * backend::ContractionMode::automatic + * ); + * + * // For this nodal basis, equal coefficients represent a constant field. + * ::mfem::Vector coefficients(element.GetDof()); + * coefficients = 2.0; + * + * // Compute values = E * coefficients. + * // Every quadrature-point value is approximately 2.0. + * ::mfem::Vector values; + * basis.evaluate(coefficients, values, workspace); + * + * // Supply quadrature weights explicitly: integrate does not add them. + * ::mfem::Vector weighted_values(rule.GetNPoints()); + * for (int q = 0; q < rule.GetNPoints(); ++q) { + * weighted_values[q] = rule.IntPoint(q).weight * values[q]; + * } + * + * // Compute element_integrals = E^T * weighted_values. + * ::mfem::Vector element_integrals; + * basis.integrate(weighted_values, element_integrals, workspace); + * @endcode + * + * Each output entry approximates + * \f[ + * \int_{\hat K} \phi_j(\xi)\rho_h(\xi)\,d\xi. + * \f] + * These are element-DOF integral contributions, not reconstructed field + * coefficients. Integration over a physical element additionally requires + * the appropriate volume Jacobian in the quadrature weights. + */ + class BasisEvaluation final { + public: + /** + * @brief Prepare basis values or derivatives at quadrature points using a dense matrix or tensor-product representation. + * @param element Finite element to sample. + * @param rule Integration rule to use for sampling. + * @param operation Reference operation to perform. + * @param copies Number of independent copies of the finite-element basis; for example, three scalar basis copies for a vector H1 field. + * @param cache Tensor basis cache to use. + * @param mode Contraction mode to use. + */ + void sample( + const ::mfem::FiniteElement& element, + const ::mfem::IntegrationRule& rule, + ReferenceOperation operation, + int copies, + TensorBasisCache& cache, + ContractionMode mode, + const ElementReconstruction* reconstruction = nullptr + ); + + /** + * @brief Set the basis evaluation to a constant basis with the given number of points. + * @param points Number of quadrature points to use for the constant basis. + */ + void set_constant( + int points + ); + + /** + * @brief Allocate storage for a mapped basis using the reference's coefficient ordering and quadrature points. + * @param reference Basis evaluation supplying the dimensions and any shared tensor representation. + * @param components Number of output components per quadrature point. + * + * @note Populate the destination at every quadrature point using map_point before evaluation or transpose application. + */ + void reset_from( + const BasisEvaluation& reference, + int components + ); + + /** + * @brief Apply a pointwise linear map to the reference basis or its geometry variation. + * @tparam MapT Type of the mapping function to apply. + * @param point Quadrature point index to apply the mapping at. + * @param reference Reference basis evaluation to map from. + * @param map Mapping function to apply. + * @param workspace Reusable pointwise input and output buffers. + * + * @pre The mapping callback must be linear in its input and fill every output component. + */ + template + void map_point( + int point, + const BasisEvaluation& reference, + MapT&& map, + BasisWorkspace& workspace + ); + + /** + * @brief Apply the basis to native coefficients; no integration weight is introduced. + * @param coefficients Input coefficients to apply the basis to. + * @param values Output values after applying the basis. + * @param workspace Workspace to use for intermediate computations. + */ + void evaluate( + const ::mfem::Vector& coefficients, + ::mfem::Vector& values, + BasisWorkspace& workspace + ) const; + + /** + * @brief evaluate() for several coefficient vectors stored lane-interleaved (entry i of lane j at i * lanes + j). + * @note Each lane's values equal evaluate() on that lane alone; the output is interleaved the same way. + */ + void evaluate_many( + const ::mfem::Vector& coefficients, + ::mfem::Vector& values, + int lanes, + BasisWorkspace& workspace + ) const; + + /** + * @brief Apply the coordinate transpose to already weighted quadrature duals. + * @param[in] values Quadrature data with any required integration weights already applied. + * @param[out] coefficients Element-DOF contributions computed as E^T times values. + * @param workspace Workspace to use for intermediate computations. + */ + void integrate( + const ::mfem::Vector& values, + ::mfem::Vector& coefficients, + BasisWorkspace& workspace + ) const; + + /// Number of flattened quadrature samples. + [[nodiscard]] int height() const noexcept; + + /// Number of native element coefficients. + [[nodiscard]] int width() const noexcept; + + /// Report whether contractions use verified Cartesian factors. + [[nodiscard]] bool uses_tensor() const noexcept; + + private: + int m_points{0}; ///< Number of quadrature points in the rule + int m_components{0}; ///< Number of output components per point + int m_width{0}; ///< Number of native element coefficients + bool m_identity_mapping{false}; ///< Canonical tensor samples need no pointwise mapping + std::shared_ptr m_tensor; ///< Canonical tensor representation, when the tensor path is selected + ::mfem::DenseMatrix m_dense; ///< Full evaluation matrix, including applied mappings, when the dense path is selected + ::mfem::DenseMatrix m_mapping; ///< Stacked pointwise maps; canonical identity maps require no storage + }; +} // namespace serif::discretization::backend::mfem + +template +void serif::discretization::backend::mfem::BasisEvaluation::map_point(const int point, const BasisEvaluation& reference, MapT&& map, BasisWorkspace& workspace) { + const bool canonical_reference = reference.m_identity_mapping; + if (m_identity_mapping) { + // Directly mapping a canonical destination must preserve the untouched points. + m_mapping.SetSize(m_points * m_components, m_components); + m_mapping = 0.0; + for (int q = 0; q < m_points; ++q) { + for (int component = 0; component < m_components; ++component) { + m_mapping(q * m_components + component, component) = 1.0; + } + } + } + m_identity_mapping = false; + const ::mfem::DenseMatrix& source = reference.m_tensor ? reference.m_mapping : reference.m_dense; + ::mfem::DenseMatrix& destination = m_tensor ? m_mapping : m_dense; + const int columns = canonical_reference ? reference.m_components : source.Width(); + ::mfem::Vector& input = workspace.mapping_input; + ::mfem::Vector& output = workspace.mapping_output; + input.SetSize(reference.m_components); + output.SetSize(m_components); + + for (int column = 0; column < columns; ++column) { + for (int axis = 0; axis < reference.m_components; ++axis) { + input[axis] = canonical_reference ? static_cast(axis == column) : source(point * reference.m_components + axis, column); + } + map(input, output); + for (int axis = 0; axis < m_components; ++axis) { + destination(point * m_components + axis, column) = output[axis]; + } + } +} diff --git a/src/include/serif/backend/mfem/discretization/boundary_faces.hpp b/src/include/serif/backend/mfem/discretization/boundary_faces.hpp new file mode 100644 index 00000000..62a59e97 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/boundary_faces.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include "mfem/mesh/mesh.hpp" +#include "mfem/mesh/ncmesh.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::discretization::backend::mfem { + /** @brief Expand a selected coarse interface label onto its physical leaf faces, integrating each leaf once. */ + [[nodiscard]] inline std::vector boundary_leaf_faces(::mfem::Mesh& mesh, const int attribute) { + using namespace utils::errors; + std::vector labels(mesh.GetNumFaces(), 0); + for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { + const int face = mesh.GetBdrElementFaceIndex(boundary); + const int label = mesh.GetBdrAttribute(boundary); + serif_ensure(labels[face] == 0 || labels[face] == label, + "A weak boundary face has conflicting explicit boundary labels."); + labels[face] = label; + } + std::vector child_count(mesh.GetNumFaces(), 0); + if (mesh.ncmesh != nullptr) { + const auto& faces = mesh.ncmesh->GetFaceList(); + for (const auto& slave : faces.slaves) { + if (labels[slave.master] != attribute) { continue; } + serif_ensure(labels[slave.index] == 0 || labels[slave.index] == attribute, + "A selected coarse boundary label conflicts with an explicit leaf label."); + labels[slave.index] = attribute; + ++child_count[slave.master]; + } + } + std::vector result; + for (int face = 0; face < mesh.GetNumFaces(); ++face) { + if (labels[face] != attribute) { continue; } + if (mesh.GetFaceInformation(face).tag == ::mfem::Mesh::FaceInfoTag::MasterNonconforming) { + serif_ensure(child_count[face] > 0, + "A selected nonconforming master face has no physical leaf faces."); + continue; + } + result.push_back(face); + } + return result; + } +} diff --git a/src/include/serif/backend/mfem/discretization/boundary_trace.hpp b/src/include/serif/backend/mfem/discretization/boundary_trace.hpp new file mode 100644 index 00000000..d1539cfe --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/boundary_trace.hpp @@ -0,0 +1,95 @@ +#pragma once + +/** + * @file boundary_trace.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the definition of the restricted dof space for a scalar boundary trace. Note in FEM a trace is a restriction on a function to the boundary of a domain. Therefore, + * This file contains the definitions for getting the DOFs at the boundary of a scalar field. + */ + +#include +#include + +#include "mfem/linalg/sparsemat.hpp" +#include "serif/backend/mfem/discretization/dof_selection.hpp" + +namespace serif::discretization::backend::mfem +{ + /** + * @brief Concept Limiting the input term to either a global scalar term or a spatial term of rank 0 in the H1 family. Outside of H1, the trace is a vector (e.g. the normal trace + * for a H(div) field or the tangential trace for a H(curl) field). Further, for higher + * rank H1 fields the trace is a tensor. + **/ + template + concept BoundaryTraceTerm = blocks::ScalarTraceTerm; + + /** + * @todo We have this forward definition here to avoid a circular dependency; however, a better refactor should be made here to avoid needing this forward declaration. + */ + class ScalarBoundaryTrace; + + namespace detail + { + /** @brief Construct one interpolation operator in the selected residual-node ordering. */ + [[nodiscard]] ScalarBoundaryTrace build_boundary_trace(std::shared_ptr rows, const DofMap& input, const ::mfem::FiniteElementSpace* input_space); + } + + /** + * @brief An immutable scalar interpolation operator from supported coefficients to selected residual nodes. + * @note Both term bindings must outlive the trace. The sparse matrix and selection are shared immutable data; + * the same matrix supplies the forward evaluation and its exact coordinate transpose without quadrature weights. + */ + class ScalarBoundaryTrace final + { + public: + /** @brief Return the borrowed input coefficient binding. */ + [[nodiscard]] const DofMap& binding() const noexcept; + + /** @brief Return the owned selection defining the interpolation row ordering. */ + [[nodiscard]] const DofSelection& rows() const noexcept; + + /** @brief Return the interpolation matrix after checking its borrowed discretization. */ + [[nodiscard]] const ::mfem::SparseMatrix& interpolation() const; + + /** @brief Reject stale mesh topology, finite-element bindings, or domain and boundary labels. */ + void validate() const; + + private: + /** @brief Publish the completed matrix together with the bindings defining its coordinates. */ + ScalarBoundaryTrace(std::shared_ptr rows, const DofMap& input, std::shared_ptr interpolation); + + friend ScalarBoundaryTrace detail::build_boundary_trace(std::shared_ptr, const DofMap&, const ::mfem::FiniteElementSpace*); + + std::shared_ptr m_rows; + const DofMap& m_binding; + std::shared_ptr m_interpolation; + }; + + /** + * @brief Sample a continuous scalar field at the selected nodal H1 residual points. + * @note Input and residual spaces may have different orders and numbering but must share a mesh. + * Discontinuous fields require an explicit physical trace convention and are intentionally excluded. + */ + template + [[nodiscard]] ScalarBoundaryTrace make_boundary_trace(std::shared_ptr rows, const TermBinding& input) + { + if constexpr (blocks::GlobalScalarTerm) + { + return detail::build_boundary_trace(std::move(rows), input, nullptr); + } + else + { + return detail::build_boundary_trace(std::move(rows), input, &input.space()); + } + } + + /** @brief A trace borrows its input binding, so temporaries cannot supply its coefficient ordering. */ + template + ScalarBoundaryTrace make_boundary_trace(std::shared_ptr, TermBinding&&) = delete; + + /** @brief Const temporary bindings have the same lifetime restriction. */ + template + ScalarBoundaryTrace make_boundary_trace(std::shared_ptr, const TermBinding&&) = delete; + +} diff --git a/src/include/serif/backend/mfem/discretization/coefficient_tracking.hpp b/src/include/serif/backend/mfem/discretization/coefficient_tracking.hpp new file mode 100644 index 00000000..b821a6b8 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/coefficient_tracking.hpp @@ -0,0 +1,117 @@ +#pragma once + +/** + * @file coefficient_tracking.hpp + * @brief Snapshot and comparison policy for MFEM coefficient views. + */ + +#include + +#include "mfem/linalg/vector.hpp" + +namespace serif::discretization::backend::mfem { + /** + * @struct CoefficientTracking + * @brief Track coefficient values using an owned snapshot and the identity of their binding. + * + * A coefficient vector describes numerical values, while its binding + * determines how those values correspond to discretization degrees of freedom. + * Both pieces of information matter when deciding whether cached work can + * be reused. + * + * This policy provides three operations to the dependency system: + * + * - capture copies the current coefficient values into an owned snapshot. + * - matches compares current values against a previously captured snapshot. + * - identity identifies the persistent binding associated with the values. + * + * The vector's allocation is not part of its identity. A different vector + * containing equal coefficients can represent the same input when it uses + * the same binding. Conversely, equal coefficients associated with a + * different binding must not automatically reuse the previous calculation. + * + * This policy supplies comparisons; the dependency system decides which + * computations must run. + * + * @note Value comparison uses floating-point equality without a tolerance. + * It does not check whether the coefficients are finite or admissible; + * those checks belong to input validation. + */ + struct CoefficientTracking { + /** + * @brief Copy the current coefficient values into an independent snapshot. + * + * Subsequent changes to the source vector do not modify the returned + * snapshot. The entire supplied vector is copied; if the view contains + * only selected nodal coefficients, only those selected values are captured. + * + * @tparam ViewT View type exposing its coefficients through a values member. + * @param[in] view View of the current coefficients. + * @return An owned copy of the coefficient vector. + */ + template + [[nodiscard]] static ::mfem::Vector capture(const ViewT& view); + + /** + * @brief Check whether the current coefficients equal a captured snapshot. + * + * The vectors must have the same size and equal corresponding entries. + * Two empty vectors match. Their storage addresses do not affect the + * comparison. + * + * This method compares values only. Binding identity is supplied + * separately by identity and checked by the dependency system. + * + * @tparam ViewT View type exposing its coefficients through a values member. + * @param[in] captured Previously captured coefficient values. + * @param[in] view View of the current coefficients. + * @return True when the sizes and all corresponding values are equal. + */ + template + [[nodiscard]] bool matches( + const ::mfem::Vector& captured, + const ViewT& view + ) const; + + /** + * @brief Identify the persistent binding associated with the coefficients. + * + * The returned address identifies the binding object, rather than the + * temporary view or the coefficient vector's storage. Creating another + * view of the same binding therefore preserves this identity. + * + * @tparam ViewT View type exposing its persistent binding through a binding member. + * @param[in] view View referring to the coefficient binding. + * @return The address of the binding object. + * + * @pre The binding member refers to an object whose lifetime and address + * remain valid while the dependency system relies on this identity. + */ + template + [[nodiscard]] static const void* identity(const ViewT& view); + }; +} // namespace serif::discretization::backend::mfem + +template +::mfem::Vector serif::discretization::backend::mfem::CoefficientTracking::capture(const ViewT& view) { + return ::mfem::Vector(view.values); +} + +template +bool serif::discretization::backend::mfem::CoefficientTracking::matches( + const ::mfem::Vector& captured, + const ViewT& view +) const { + return captured.Size() == view.values.Size() && + (captured.Size() == 0 || + std::equal( + captured.GetData(), + captured.GetData() + captured.Size(), + view.values.GetData() + )); +} + +template +[[nodiscard]] const void* serif::discretization::backend::mfem::CoefficientTracking::identity(const ViewT& view) { + return &view.binding; +} diff --git a/src/include/serif/backend/mfem/discretization/dof_map.hpp b/src/include/serif/backend/mfem/discretization/dof_map.hpp new file mode 100644 index 00000000..7cc50fbe --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/dof_map.hpp @@ -0,0 +1,498 @@ +#pragma once + +/** + * @file dof_map.hpp + * @author Emily M. Boudreaux + * @brief This file contains the @ref DofMap which allows for the full + * dof vector to be projected into a reduced space of dofs on supported elements for some field Generally, + * while much of the code here may be exercised, you will rarely see a DofMap be directly invoked. Rather, + * these are generally implicitly constructed through their child class term_binding. + */ + +#include +#include +#include +#include "serif/backend/mfem/discretization/reconstruction.hpp" +#include + +#include "mfem/fem/fespace.hpp" +#include "mfem/fem/pfespace.hpp" +#include "serif/utils/error/errors.hpp" +#include "serif/utils/error/nonfinite.hpp" + +namespace serif::discretization::backend::mfem { + class ElementCoordinates; + + /** + * @brief A mapping from a full MFEM finite-element space to a reduced set of DOFs that + * are supporter by the given element support. That is to say that if one has some set of + * dofs D (perhaps it looks like [0, 1, 2, 3, 4] as a hypothetical) and dofs 0, 2, and 4 are + * associated to domain A while 1 and 3 are associated to domain B. Then if one were to + * construct a DofMap with element support [1, 0, 1, 0, 1] (where 1 indicates that the element + * is supported and 0 indicates that it is not) then the reduced dof map would be [0, 2, 4] and + * the full to reduced map would be [-1, -1, 0, -1, 1]. + * + * @note Solver coordinates are supported independent (true) DOFs. Element coefficients + * include hanging-node interpolation. Coefficient restriction and dual accumulation + * are separate operations. Distributed spaces are not supported. + */ + class DofMap { + protected: + DofMap( + ::mfem::FiniteElementSpace& space, + const ::mfem::Array& element_support, + std::shared_ptr reconstruction = {} + ) : + m_space(&space), + m_mesh(*space.GetMesh()), + m_space_sequence(space.GetSequence()), + m_mesh_sequence(space.GetMesh()->GetSequence()), + m_attributes(space.GetNE()), + m_element_support(element_support), + m_full_to_reduced(space.GetVSize()), + m_reconstruction(std::move(reconstruction)) { + using namespace utils::errors; + +#ifdef MFEM_USE_MPI + serif_ensure(dynamic_cast<::mfem::ParFiniteElementSpace*>(&space) == nullptr,"DofMap does not yet assemble parallel true DOFs."); +#endif + m_prolongation = space.GetConformingProlongation(); + serif_ensure(!constrained() || !space.IsVariableOrder(), "Constrained term bindings require fixed-order serial spaces; varying element order is not supported."); + serif_ensure(space.GetMesh()->Dimension() == 3 && space.GetMesh()->SpaceDimension() == 3,"Term bindings currently require a three-dimensional mesh."); + serif_ensure(element_support.Size() == space.GetNE(), "Element support does not match the space."); + + m_full_to_reduced = -1; + ::mfem::Array dofs; + + for (int element = 0; element < space.GetNE(); ++element) { + m_attributes[element] = space.GetMesh()->GetAttribute(element); + + if (element_support[element] == 0) { + continue; + } + + space.GetElementVDofs(element, dofs); + for (const int dof : dofs) { + m_full_to_reduced[dof < 0 ? -1 - dof : dof] = 0; + } + } + + if (m_reconstruction) { + m_boundary_attributes.emplace(m_mesh.GetNBE()); + for (int boundary = 0; boundary < m_mesh.GetNBE(); ++boundary) { + (*m_boundary_attributes)[boundary] = m_mesh.GetBdrAttribute(boundary); + } + } + if (m_reconstruction) { m_reconstruction->validate_binding(*this); } + if (constrained()) { + build_constrained_map(); + return; + } + + for (int dof = 0; dof < space.GetVSize(); ++dof) { + if (m_full_to_reduced[dof] == 0) { + m_full_to_reduced[dof] = m_reduced_to_full.Size(); + m_reduced_to_full.Append(dof); + } + } + } + + DofMap( + ::mfem::Mesh& mesh, + const ::mfem::Array& element_support + ) : + m_space(nullptr), + m_mesh(mesh), + m_space_sequence(-1), + m_mesh_sequence(mesh.GetSequence()), + m_attributes(mesh.GetNE()), + m_element_support(element_support), + m_reduced_to_full(1), + m_full_to_reduced(1) { + using namespace utils::errors; + + serif_ensure(mesh.Dimension() == 3 && mesh.SpaceDimension() == 3, "Term bindings currently require a three-dimensional mesh."); + serif_ensure(element_support.Size() == mesh.GetNE(), "Element support does not match the mesh."); + + m_reduced_to_full[0] = m_full_to_reduced[0] = 0; + for (int element = 0; element < mesh.GetNE(); ++element) { + m_attributes[element] = mesh.GetAttribute(element); + } + } + + ~DofMap() = default; + + public: + /** + * @brief Borrow element access for one traversal after checking the complete binding. + * @note Do not retain this view across mesh or space changes. Each element still checks + * its index, vector sizes, local attribute and space revision, without rescanning the mesh. + */ + class Elements final { + public: + /** @brief Extract supported coefficients in the native element's primal orientation. */ + void element_values(int element, const ::mfem::Vector& reduced, ::mfem::Vector& values) const; + + /** @brief Add native element dual coefficients to a full-space vector. */ + void add_element(int element, ::mfem::Vector& values, ::mfem::Vector& full) const; + + /** @brief Describe local coordinates for direct assembly or diagonal extraction. */ + [[nodiscard]] ElementCoordinates coordinates(int element) const; + + private: + friend class DofMap; + friend class ElementCoordinates; + + /** @brief Only DofMap can create a view after validating all domain attributes. */ + explicit Elements(const DofMap& binding) : m_binding(binding) {} + const DofMap& m_binding; + }; + + /** @brief Validate once before traversing elements without repeated mesh-wide scans. */ + [[nodiscard]] Elements validated_elements() const { + validate(); + return Elements(*this); + } + + /** @brief Read the immutable physical-field representation retained by this binding. */ + [[nodiscard]] const std::shared_ptr& reconstruction() const noexcept { return m_reconstruction; } + + /** @brief Select the reference adapter without adding work to ordinary elements. */ + [[nodiscard]] const ElementReconstruction* reconstruction_element(int element) const { + if (m_reconstruction == nullptr) { return nullptr; } + return m_reconstruction->element(element); + } + + DofMap(const DofMap&) = delete; + DofMap& operator=(const DofMap&) = delete; + DofMap(DofMap&&) = delete; + DofMap& operator=(DofMap&&) = delete; + + /** + * @brief Return the borrowed finite-element space associated with this DOF map. + * @return A reference to the finite-element space in m_space + */ + [[nodiscard]] ::mfem::FiniteElementSpace& space() const { + using namespace utils::errors; + serif_ensure(m_space != nullptr, "Global scalars do not have a finite-element space."); + return *m_space; + } + + /** + * @brief Return the borrowed mesh associated with this DOF map. + * @return A reference to the mesh in m_mesh + */ + [[nodiscard]] ::mfem::Mesh& mesh() const noexcept { return m_mesh; } + + /** + * @brief Return the number of full-space DOFs, including those not supported by the element support. + * @return The number of full-space DOFs (not reduced by element support) + */ + [[nodiscard]] int full_size() const noexcept { return m_full_to_reduced.Size(); } + + /** + * @brief Return the number of supported DOFs, which is the size of the reduced coefficient vector. + * @return The number of supported DOFs (reduced by element support) + */ + [[nodiscard]] int size() const noexcept { + return m_reduced_to_full.Size(); + } + + /** + * @brief Locate a local coefficient represented by exactly one supported independent coordinate. + * @param full_dof index of the dof in the full space + * @return An optional index of the dof in the reduced space, or std::nullopt for an unsupported or weighted hanging coefficient. + */ + [[nodiscard]] std::optional reduced_dof(const int full_dof) const { + using namespace utils::errors; + serif_ensure(full_dof >= 0 && full_dof < full_size(), "Full DOF index is out of range."); + const int reduced = m_full_to_reduced[full_dof]; + return reduced < 0 ? std::nullopt : std::optional{reduced}; + } + + /** @brief Return the representative local coefficient selected for one supported independent coordinate. */ + [[nodiscard]] int full_dof(const int reduced) const { + using namespace utils::errors; + + validate_structure(); + serif_ensure(reduced >= 0 && reduced < size(), + "Supported DOF index {} is outside the binding of size {}.", reduced, size()); + return m_reduced_to_full[reduced]; + } + + /** @brief Report whether local coefficients require hanging-node interpolation. */ + [[nodiscard]] bool constrained() const noexcept { return m_prolongation != nullptr; } + + /** @brief Return the full independent coordinate count before selecting material support. */ + [[nodiscard]] int true_size() const noexcept { return m_space == nullptr ? 1 : m_space->GetTrueVSize(); } + + /** @brief Visit the supported independent coefficients contributing to one unsigned local DOF. */ + template + void for_each_coefficient(const int local_dof, Visitor&& visit) const { + using namespace utils::errors; + + validate_structure(); + serif_ensure(local_dof >= 0 && local_dof < full_size(), "Local DOF index is outside its finite-element space."); + if (!constrained()) { + const int index = m_full_to_reduced[local_dof]; + if (index >= 0) { + visit(index, 1.0); + } + return; + } + const int* offsets = m_prolongation->GetI(); + const int* columns = m_prolongation->GetJ(); + const double* weights = m_prolongation->GetData(); + for (int entry = offsets[local_dof]; entry < offsets[local_dof + 1]; ++entry) { + const int index = m_true_to_reduced[columns[entry]]; + if (index >= 0 && weights[entry] != 0.0) { + visit(index, weights[entry]); + } + } + } + + /** @brief Select supported coefficients from a full independent vector, without interpolation. */ + [[nodiscard]] ::mfem::Vector gather_true(const ::mfem::Vector& values) const { + using namespace utils::errors; + + validate(); + serif_ensure(values.Size() == true_size(), + "Independent coefficient vector has size {}, expected {}.", values.Size(), true_size()); + if (!constrained()) { + return gather(values); + } + ::mfem::Vector result(size()); + for (int index = 0; index < size(); ++index) { + result[index] = values[m_reduced_to_true[index]]; + } + return result; + } + + /** @brief Expand supported coefficients into a full independent vector, without interpolation. */ + [[nodiscard]] ::mfem::Vector scatter_true(const ::mfem::Vector& values) const { + validate_values(values); + if (!constrained()) { + return scatter(values); + } + ::mfem::Vector result(true_size()); + result = 0.0; + for (int index = 0; index < size(); ++index) { + result[m_reduced_to_true[index]] = values[index]; + } + return result; + } + + /** @brief Accumulate local dual contributions with the transpose of coefficient expansion. */ + [[nodiscard]] ::mfem::Vector accumulate_dual(const ::mfem::Vector& full) const { + ::mfem::Vector result; + accumulate_dual(full, result); + return result; + } + + /** @brief Accumulate local dual contributions while reusing caller storage. */ + void accumulate_dual(const ::mfem::Vector& full, ::mfem::Vector& result) const { + using namespace utils::errors; + + validate(); + serif_ensure(full.Size() == full_size(), + "Local dual vector has size {}, expected {}.", full.Size(), full_size()); + if (!constrained()) { + gather(full, result); + return; + } + ::mfem::Vector independent(true_size()); + m_prolongation->MultTranspose(full, independent); + result = gather_true(independent); + } + + /** + * @brief Check whether an element is supported by this dof map + * @param element element index + * @return true if element is supported by this dof map, false otherwise + */ + [[nodiscard]] bool supports(const int element) const { + using namespace utils::errors; + + serif_ensure(element >= 0 && element < m_element_support.Size(), "Element index is out of range."); + return m_element_support[element] != 0; + } + + /** + * @brief Validate the dof map + */ + void validate() const { + using namespace utils::errors; + + validate_structure(); + if (m_boundary_attributes) { + serif_ensure(m_mesh.GetNBE() == m_boundary_attributes->Size(), "Rebuild reconstructed bindings after boundary changes."); + for (int boundary = 0; boundary < m_mesh.GetNBE(); ++boundary) { + serif_ensure(m_mesh.GetBdrAttribute(boundary) == (*m_boundary_attributes)[boundary], + "Rebuild reconstructed bindings after boundary labels change."); + } + } + + for (int element = 0; element < m_mesh.GetNE(); ++element) { + serif_ensure(m_mesh.GetAttribute(element) == m_attributes[element],"Term binding domain attributes changed unexpectedly."); + } + } + + /** + * @brief Check supported coefficient count and finiteness. + * @param values vector of coefficients to validate + */ + void validate_values(const ::mfem::Vector& values) const { + using namespace utils::errors; + + validate(); + serif_ensure(values.Size() == size(), "Term binding size is {}, expected {} supported DOFs.", values.Size(), size()); + + for (int i = 0; i < values.Size(); ++i) { + serif_ensure(std::isfinite(values[i]), "Term binding contains a non-finite coefficient."); + } + } + + /** + * @brief Restrict a full coefficient vector to supported DOFs. + * @param full full coefficient vector + * @return vector of supported coefficients + */ + [[nodiscard]] ::mfem::Vector gather(const ::mfem::Vector& full) const { + ::mfem::Vector result; + gather(full, result); + return result; + } + + /** @brief Restrict supported coefficients while reusing the destination allocation. */ + void gather(const ::mfem::Vector& full, ::mfem::Vector& result) const { + using namespace utils::errors; + + validate(); + serif_ensure(full.Size() == full_size(), "Full coefficient vector does not match its space."); + if (full.GetData() == result.GetData()) { + const ::mfem::Vector copy(full); + gather(copy, result); + return; + } + if (constrained()) { + ::mfem::Vector independent(true_size()); + m_space->GetConformingRestriction()->Mult(full, independent); + result = gather_true(independent); + return; + } + result.SetSize(size()); + for (int index = 0; index < size(); ++index) { + result[index] = full[m_reduced_to_full[index]]; + } + } + + /** + * @brief Expand supported independent coefficients into the constrained local field. + * Unsupported independent coordinates are zero. Interpolation can populate local coordinates + * outside the supported elements; element support still controls physical integration. + * @param reduced vector of supported coefficients + * @return full local coefficient vector satisfying hanging-node interpolation + */ + [[nodiscard]] ::mfem::Vector scatter(const ::mfem::Vector& reduced) const { + using namespace utils::errors; + + validate_values(reduced); + + ::mfem::Vector result(full_size()); + if (constrained()) { + m_prolongation->Mult(scatter_true(reduced), result); + return result; + } + result = 0.0; + for (int i = 0; i < size(); ++i) { + result[m_reduced_to_full[i]] = reduced[i]; + } + + return result; + } + + /** + * @brief Extract element coefficients, including primal orientation transforms. + * @param element element index + * @param reduced vector of supported coefficients + * @param values vector to store extracted element coefficients + */ + void element_values(int element, const ::mfem::Vector& reduced, ::mfem::Vector& values) const; + + /** + * @brief Apply dual orientation transforms and add an element residual. + * @param element element index + * @param values vector of element coefficients + * @param full full coefficient vector to which the element residual will be added + */ + void add_element(int element, ::mfem::Vector& values, ::mfem::Vector& full) const; + + private: + friend class ElementCoordinates; + + /** @brief Close material support over hanging-node dependencies and retain independent coordinates. */ + void build_constrained_map() { + using namespace utils::errors; + + m_true_to_reduced.SetSize(true_size()); + m_true_to_reduced = -1; + const int* offsets = m_prolongation->GetI(); + const int* columns = m_prolongation->GetJ(); + const double* weights = m_prolongation->GetData(); + for (int local = 0; local < full_size(); ++local) { + if (m_full_to_reduced[local] < 0) { + continue; + } + for (int entry = offsets[local]; entry < offsets[local + 1]; ++entry) { + if (weights[entry] != 0.0) { + m_true_to_reduced[columns[entry]] = 0; + } + } + } + const ::mfem::SparseMatrix* restriction = m_space->GetConformingRestriction(); + serif_ensure(restriction != nullptr, + "A constrained serial space must provide its coefficient restriction."); + for (int independent = 0; independent < true_size(); ++independent) { + if (m_true_to_reduced[independent] < 0) { + continue; + } + const int begin = restriction->GetI()[independent]; + const int end = restriction->GetI()[independent + 1]; + serif_ensure(end == begin + 1 && restriction->GetData()[begin] == 1.0, + "Fixed-order coefficient restriction must select one independent local DOF; coordinate {} has an unsupported restriction.", independent); + m_true_to_reduced[independent] = m_reduced_to_true.Size(); + m_reduced_to_true.Append(independent); + m_reduced_to_full.Append(restriction->GetJ()[begin]); + } + m_full_to_reduced = -1; + for (int local = 0; local < full_size(); ++local) { + const int begin = offsets[local]; + if (offsets[local + 1] == begin + 1 && weights[begin] == 1.0) { + m_full_to_reduced[local] = m_true_to_reduced[columns[begin]]; + } + } + } + + /** @brief Check borrowed resource revisions in constant time before accessing their arrays. */ + void validate_structure() const { + using namespace utils::errors; + + serif_ensure(m_mesh.GetSequence() == m_mesh_sequence && m_mesh.GetNE() == m_attributes.Size() && (m_space == nullptr || (m_space->GetMesh() == &m_mesh && m_space->GetSequence() == m_space_sequence && m_space->GetVSize() == full_size())), "Rebuild term bindings after discretization updates."); + } + + ::mfem::FiniteElementSpace* m_space; ///< Borrowed pointer to the finite-element space associated with this dof map, or nullptr for a global scalar. This must outlive this class + ::mfem::Mesh& m_mesh; ///< Borrowed reference to the mesh associated with this dof map. This must outlive this class + long m_space_sequence; ///< Sequence number of the finite-element space at construction, or -1 for a global scalar + long m_mesh_sequence; ///< Sequence number of the mesh at construction + ::mfem::Array m_attributes; ///< Array of attribute indices associated with this dof map + ::mfem::Array m_element_support; ///< Array of 0/1 values indicating whether each element is supported by this dof map + const ::mfem::SparseMatrix* m_prolongation{nullptr}; ///< Borrowed constraint matrix, guarded by the space revision. + ::mfem::Array m_true_to_reduced; ///< Independent coordinate to supported coordinate. + ::mfem::Array m_reduced_to_true; ///< Supported coordinate to independent coordinate. + ::mfem::Array m_reduced_to_full; ///< Mapping from reduced to full degrees of freedom + ::mfem::Array m_full_to_reduced; ///< Mapping from full to reduced degrees of freedom + std::optional<::mfem::Array> m_boundary_attributes; ///< Reconstructed bindings depend on boundary labels. + std::shared_ptr m_reconstruction; ///< Immutable; a different representation requires a new binding. + + }; +} // namespace serif::discretization::backend::mfem diff --git a/src/include/serif/backend/mfem/discretization/dof_selection.hpp b/src/include/serif/backend/mfem/discretization/dof_selection.hpp new file mode 100644 index 00000000..e96b678e --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/dof_selection.hpp @@ -0,0 +1,158 @@ +#pragma once + +/** + * @file dof_selection.hpp + * @author Emily M. Boudreaux + * @brief This file contains the @ref DofSelection which allows for the selection of a subset of DOFs from a term binding, with boundary attribute information. + */ + +#include "serif/backend/mfem/discretization/term_binding.hpp" +#include "serif/backend/mfem/discretization/dof_map.hpp" + +namespace serif::discretization::backend::mfem { + + /** + * @brief A selection of supported DOFs from a term binding, with boundary attribute information. + * The purpose here is to allow for the selection of a subset of DOFs from a term binding, + * such as those associated with a particular boundary, while still retaining the ability to + * validate the selection against the underlying mesh and term binding. + */ + class DofSelection final { + public: + /** + * @brief Construct a DOF selection from a term binding and a list of reduced DOFs. + * @param binding A reference to the term binding from which the DOFs are selected. Further, this binding must outlive the DOF selection. + * @param reduced_dofs A list of reduced DOFs to select. This list should be in the reduced space of the term binding. Further, these must be sorted in + * a strictly increasing order and must be unique. This is enforced at construction time. + */ + DofSelection( + const DofMap& binding, + const ::mfem::Array& reduced_dofs, + const bool track_coordinates = false + ) : + m_binding(binding), + m_dofs(reduced_dofs), + m_boundary_attributes(binding.mesh().GetNBE()), + m_nodes_sequence(track_coordinates ? binding.mesh().GetNodesSequence() : -1) { + using namespace utils::errors; + + binding.validate(); + int previous = -1; + + for (const int dof : m_dofs) { + serif_ensure(dof > previous && dof < binding.size(), "Selected DOFs must be sorted, unique, and inside the term binding."); + previous = dof; + } + + for (int boundary = 0; boundary < m_boundary_attributes.Size(); ++boundary) { + m_boundary_attributes[boundary] = binding.mesh().GetBdrAttribute(boundary); + } + } + + /** + * @brief Return the underlying term binding associated with this DOF selection. + * @return A reference to the underlying term binding. + */ + [[nodiscard]] const DofMap& binding() const noexcept { return m_binding; } + + /** + * @brief Return the list of reduced DOFs in this selection. + * @return A reference to the list of reduced DOFs. Note these are in the reduced space of the binding provided at construction time. + */ + [[nodiscard]] const ::mfem::Array& reduced_dofs() const noexcept { return m_dofs; } + + /** + * @brief Return the number of selected DOFs in this selection. + * @return The number of selected DOFs. + */ + [[nodiscard]] int size() const noexcept { return m_dofs.Size(); } + + /** + * @brief Return the list of boundary attributes associated with this selection. + * @return A reference to the list of boundary attributes. These are in the same order as the boundaries in the underlying mesh. + */ + [[nodiscard]] const ::mfem::Array& boundary_attributes() const noexcept { return m_boundary_attributes; } + + /** + * @brief Report whether this selection becomes stale when the fixed mesh coordinates change. + * Point constraints require this tracking even when a caller supplies the selected DOFs explicitly. + * @return True if coordinate updates invalidate the selection. + */ + [[nodiscard]] bool tracks_coordinates() const noexcept { return m_nodes_sequence >= 0; } + + /** + * @brief Validate the DOF selection against the underlying term binding and mesh. + * This checks that the underlying term binding and mesh have not changed since the DOF selection was constructed. + * If they have changed, an exception is thrown. This is important to ensure that the DOF selection remains valid + * and consistent with the underlying data structures. + */ + void validate() const { + using namespace utils::errors; + + m_binding.validate(); + const auto& mesh = m_binding.mesh(); + serif_ensure(m_nodes_sequence < 0 || mesh.GetNodesSequence() == m_nodes_sequence, "Rebuild point selections after fixed-mesh coordinate changes."); + + serif_ensure(mesh.GetNBE() == m_boundary_attributes.Size(), "Rebuild DOF selections after boundary changes."); + + for (int boundary = 0; boundary < m_boundary_attributes.Size(); ++boundary) { + serif_ensure(mesh.GetBdrAttribute(boundary) == m_boundary_attributes[boundary], "Rebuild DOF selections after boundary attributes change."); + } + } + + private: + const DofMap& m_binding; ///< Reference to the underlying term binding. This must outlive the DOF selection. + ::mfem::Array m_dofs; ///< List of reduced DOFs in this selection. These are in the reduced space of the binding provided at construction time. + ::mfem::Array m_boundary_attributes; ///< List of boundary attributes associated with this selection. These are in the same order as the boundaries in the underlying mesh. + long m_nodes_sequence; ///< Coordinate revision, or -1 for selections independent of geometry. + }; + + /** + * @brief Select DOFs associated with a particular boundary from a term binding. + * @tparam BoundaryT The boundary type to select. This must be a valid boundary in the domain schema of the term binding. + * @tparam TermT The term type of the term binding. This must be a valid term type in the domain schema of the term binding. (e.g. something like Density) + * @tparam SchemaT The domain schema type of the term binding. This must be a valid domain schema type that contains the specified boundary. + * @param binding The term binding from which to select DOFs. This must outlive the returned DOF selection. + * @return A DofSelection object representing the selected DOFs associated with the specified boundary. + */ + template + [[nodiscard]] DofSelection select_boundary_dofs( + const TermBinding& binding + ) { + using namespace utils::errors; + + static_assert(SchemaT::template contains_boundary(), "The boundary must be registered in the domain schema."); + binding.validate(); + + ::mfem::Array marker(binding.size()); + marker = 0; + + ::mfem::Array dofs; + ::mfem::Array selected; + + auto& space = binding.space(); + const auto& mesh = binding.mesh(); + + for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { + if (!SchemaT::template boundary_id_matches(mesh.GetBdrAttribute(boundary))) { + continue; + } + + space.GetBdrElementVDofs(boundary, dofs); + + for (const int encoded : dofs) { + bool supported = false; + binding.for_each_coefficient(::mfem::FiniteElementSpace::DecodeDof(encoded), [&](const int index, const double) { + marker[index] = 1; + supported = true; + }); + serif_ensure(supported, "A selected boundary DOF lies outside the term's support."); + } + } + + ::mfem::FiniteElementSpace::MarkerToList(marker, selected); + serif_ensure(selected.Size() > 0, "The requested boundary has no DOFs on this serial mesh."); + + return DofSelection(binding, selected); + } +} // namespace serif::discretization::backend::mfem diff --git a/src/include/serif/backend/mfem/discretization/element_coordinates.hpp b/src/include/serif/backend/mfem/discretization/element_coordinates.hpp new file mode 100644 index 00000000..1c346554 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/element_coordinates.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include "mfem/fem/geom.hpp" +#include "mfem/fem/doftrans.hpp" +#include "serif/backend/mfem/discretization/dof_map.hpp" + +namespace serif::discretization::backend::mfem { + /** @brief One element's native orientation and its supported full/reduced coefficient coordinates. */ + class ElementCoordinates final { + public: + /** @brief Read local coordinates from a binding validated for the current traversal. */ + ElementCoordinates(const DofMap::Elements& elements, int element); + + /** @brief Return the number of native element coefficients. */ + [[nodiscard]] int size() const noexcept { return m_dofs.Size(); } + + /** @brief Return a supported coefficient index, or -1 outside the binding support. */ + [[nodiscard]] int reduced_dof(int local) const; + + /** @brief Visit independent weights for one native coefficient after dual orientation. */ + template + void for_each_coefficient(const int local, Visitor&& visit) const { + using namespace utils::errors; + + serif_ensure(local >= 0 && local < size(), + "Local element coefficient {} is outside the element of size {}.", local, size()); + if (m_supported) { + m_binding.for_each_coefficient(::mfem::FiniteElementSpace::DecodeDof(m_dofs[local]), std::forward(visit)); + } + } + + /** @brief Locate a supported coefficient among this element's native coordinates. */ + [[nodiscard]] int local_dof(int reduced) const; + + /** @brief Express one native local basis direction, including its primal orientation. + * On constrained spaces, callers distribute its contribution through the hanging-node weights. */ + void unit_direction(int column, ::mfem::Vector& direction) const; + + /** @brief Restrict coefficients and apply their primal orientation. */ + void element_values(const ::mfem::Vector& reduced, ::mfem::Vector& values) const; + + /** @brief Convert integrated native coefficients to global dual orientation. */ + void orient_dual(::mfem::Vector& values) const; + + /** @brief Orient and add integrated coefficients into the full-space vector. */ + void add_element(::mfem::Vector& values, ::mfem::Vector& full) const; + + private: + /** @brief Apply MFEM's nontrivial primal transformation, when present. */ + void orient_primal(::mfem::Vector& values) const; + + const DofMap& m_binding; + bool m_supported; + ::mfem::Array m_dofs; + ::mfem::DofTransformation m_transformation; + }; +} // namespace serif::discretization::backend::mfem diff --git a/src/include/serif/backend/mfem/discretization/field_values.hpp b/src/include/serif/backend/mfem/discretization/field_values.hpp new file mode 100644 index 00000000..5f061d61 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/field_values.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include + +#include "serif/backend/mfem/discretization/term_coefficients.hpp" +#include "serif/backend/mfem/operators/operand_evaluation.hpp" + +namespace serif::discretization::backend::mfem { + /** + * @brief Read a field using the same representation and mappings as its equation operators. + * @note This reader borrows its binding and coefficients. They must outlive it, and the mesh + * must not change during a sampling traversal. Each reader reuses scratch storage and is not + * shared between threads. Scalar VALUE fields need no extra map on a moving mesh; gradients + * require the overload accepting the additional SERiF mapping context. + */ + template + class FieldValues final { + public: + using ValueOperand = forms::Operand; + using Value = forms::OperandValue; + + /** @brief Borrow supported physical coefficients, including their hanging-node constraints. */ + FieldValues(const DofMap& binding, const ::mfem::Vector& coefficients) : + m_binding(binding), m_coefficients(coefficients), m_elements(binding.validated_elements()), m_rule(1) { + using namespace utils::errors; + using Traits = blocks::SpatialTermTraits; + + binding.validate_values(coefficients); + constexpr int copies = Traits::rank == 0 ? 1 : 3; + constexpr int continuity = Traits::family == functional_space::Family::h1 ? ::mfem::FiniteElementCollection::CONTINUOUS : ::mfem::FiniteElementCollection::DISCONTINUOUS; + serif_ensure(Traits::rank <= 1 && + (Traits::family == functional_space::Family::h1 || Traits::family == functional_space::Family::l2), + "FieldValues supports scalar/vector H1 and L2 fields."); + serif_ensure(binding.space().GetVDim() == copies && + binding.space().FEColl()->GetContType() == continuity, + "FieldValues term family or rank disagrees with the supplied binding; pass the matching typed coefficient view."); + for (int element = 0; element < binding.space().GetNE(); ++element) { + const auto& finite_element = *binding.space().GetFE(element); + serif_ensure(finite_element.GetOrder() == Traits::order && + finite_element.GetRangeType() == ::mfem::FiniteElement::SCALAR && + finite_element.GetMapType() == ::mfem::FiniteElement::VALUE, + "FieldValues term order or mapping disagrees with the supplied binding; pass the matching typed coefficient view."); + } + } + + /** @brief Retain the field identity carried by a typed coefficient view. */ + template requires std::same_as + explicit FieldValues(const operators::backend::mfem::CoefficientView& field) : + FieldValues(field.binding, field.values) {} + + FieldValues(const DofMap&, ::mfem::Vector&&) = delete; + FieldValues(const DofMap&, const ::mfem::Vector&&) = delete; + + /** @brief Evaluate at a canonical element point, including the fixed MFEM mesh map. */ + [[nodiscard]] Value value(int element, const ::mfem::IntegrationPoint& point) { + return sample(element, point); + } + + /** + * @brief Sample an element's points together, reusing tensor factors for repeated sampling rules. + * @note Vector components are consecutive within each point. As for value(), these samples + * include the fixed MFEM mesh map. + */ + void values(int element, const ::mfem::IntegrationRule& points, ::mfem::Vector& result) { + using Evaluator = operators::backend::mfem::OperandEvaluator; + + m_elements.element_values(element, m_coefficients, m_local); + auto& transformation = *m_binding.mesh().GetElementTransformation(element); + Evaluator::sample_basis(m_binding, element, points, m_basis, m_cache, + ContractionMode::automatic, transformation, m_workspace); + Evaluator::evaluate(m_basis, m_local, result, m_workspace); + } + + /** @brief Evaluate with the additional deformation or compactification map used by the solve. */ + [[nodiscard]] Value value(int element, const ::mfem::IntegrationPoint& point, const mapping::MappingPointContext& context) { + using Evaluator = operators::backend::mfem::OperandEvaluator; + return Evaluator::physical_value(sample(element, point), context); + } + + /** @brief Return a scalar H1 field's gradient in the fixed computational mesh coordinates. */ + [[nodiscard]] std::array gradient(int element, const ::mfem::IntegrationPoint& point) + requires (blocks::SpatialTermTraits::rank == 0 && blocks::SpatialTermTraits::family == functional_space::Family::h1) { + return sample(element, point); + } + + /** @brief Return a scalar H1 field's physical gradient after the additional SERiF map. */ + [[nodiscard]] std::array gradient(int element, const ::mfem::IntegrationPoint& point, const mapping::MappingPointContext& context) + requires (blocks::SpatialTermTraits::rank == 0 && blocks::SpatialTermTraits::family == functional_space::Family::h1) { + using Operand = forms::Operand; + using Evaluator = operators::backend::mfem::OperandEvaluator; + return Evaluator::physical_value(sample(element, point), context); + } + + private: + /** @brief Reuse the equation evaluator so diagnostic samples cannot bypass reconstruction. */ + template + [[nodiscard]] auto sample(int element, const ::mfem::IntegrationPoint& point) { + using Operand = forms::Operand; + using Evaluator = operators::backend::mfem::OperandEvaluator; + + m_elements.element_values(element, m_coefficients, m_local); + m_rule.IntPoint(0) = point; + auto& transformation = *m_binding.mesh().GetElementTransformation(element); + // Arbitrary one-point queries must not retain a separate tensor plan for every query. + Evaluator::sample_basis(m_binding, element, m_rule, m_basis, m_cache, + ContractionMode::dense, transformation, m_workspace); + Evaluator::evaluate(m_basis, m_local, m_values, m_workspace); + return Evaluator::reference_value(m_values, 0); + } + + const DofMap& m_binding; + const ::mfem::Vector& m_coefficients; + DofMap::Elements m_elements; + ::mfem::IntegrationRule m_rule; + ::mfem::Vector m_local, m_values; + BasisEvaluation m_basis; + TensorBasisCache m_cache; + BasisWorkspace m_workspace; + }; +} diff --git a/src/include/serif/backend/mfem/discretization/mass_diagonal.hpp b/src/include/serif/backend/mfem/discretization/mass_diagonal.hpp new file mode 100644 index 00000000..580f95a3 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/mass_diagonal.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include "serif/backend/mfem/discretization/term_binding.hpp" + +namespace serif::discretization::backend::mfem +{ + namespace detail + { + /** @brief Assemble a supported reference mass diagonal for scalar or vector H1 copies. */ + [[nodiscard]] std::expected, std::string> reference_mass_diagonal(const DofMap& binding); + } + + /** + * @brief Measure the supported coefficients in their reference finite-element geometry. + * @return The exact mass-matrix diagonal in the binding's reduced ordering, or an invalid-metric diagnostic. + * @note The binding must remain current. Coordinates belong to the reference mesh, not a trial deformation. + */ + template + [[nodiscard]] std::expected, std::string> reference_mass_diagonal(const TermBinding& binding) + { + return detail::reference_mass_diagonal(binding); + } +} diff --git a/src/include/serif/backend/mfem/discretization/quadrature/concepts.hpp b/src/include/serif/backend/mfem/discretization/quadrature/concepts.hpp new file mode 100644 index 00000000..99728498 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/quadrature/concepts.hpp @@ -0,0 +1,44 @@ +#pragma once + +/** + * @file concepts.hpp + * @author Emily M. Boudreaux + * + * @brief File containing concepts for the quadrature backend module. These concepts are used to constrain template parameters in the quadrature backend module. + */ + +#include "mfem/fem/intrules.hpp" + +namespace serif::discretization::quadrature::backend::mfem { + /** + * @brief A HasReferenceSetIntegrationRule is an integrator that can be configured with a reference to an integration rule. + */ + template + concept HasReferenceSetIntegrationRule = requires (IntegratorT &integrator, const ::mfem::IntegrationRule &integration_rule) { + integrator.SetIntegrationRule(integration_rule); + }; + + /** + * @brief A HasPointerSetIntegrationRule is an integrator that can be configured with a pointer to an integration rule. + */ + template + concept HasPointerSetIntegrationRule = requires (IntegratorT &integrator, const ::mfem::IntegrationRule &integration_rule) { + integrator.SetIntRule(&integration_rule); + }; + + /** + * @brief A HasSetIntRule is an integrator that can be configured with an integration rule by value. + */ + template + concept HasSetIntRule = requires (IntegratorT &integrator, const ::mfem::IntegrationRule &integration_rule) { + integrator.SetIntRule(integration_rule); + }; + + /** + * @brief A ConfigurableIntegrator is an integrator that can be configured with an integration rule in some way (either by reference, by pointer, or by value). + */ + template + concept ConfigurableIntegrator = HasReferenceSetIntegrationRule || + HasPointerSetIntegrationRule || + HasSetIntRule; +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/discretization/quadrature/configure.hpp b/src/include/serif/backend/mfem/discretization/quadrature/configure.hpp new file mode 100644 index 00000000..9f7a00c2 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/quadrature/configure.hpp @@ -0,0 +1,32 @@ +#pragma once + +/** + * @file configure.hpp + * @author Emily M. Boudreaux + * + * @brief File containing utility functions for configuring MFEM integrators with integration rules. + */ + +#include "mfem/fem/intrules.hpp" + +#include "serif/backend/mfem/discretization/quadrature/concepts.hpp" + +namespace serif::discretization::quadrature::backend::mfem { + + /** + * @brief Configure an MFEM integrator with an integration rule. This function will call the appropriate SetIntegrationRule method on the integrator based on its capabilities. + * @tparam IntegratorT The type of the integrator to configure. + * @param integrator The integrator to configure. + * @param integration_rule The integration rule to use for configuration. + */ + template + void set_integration_rule(IntegratorT &integrator, const ::mfem::IntegrationRule &integration_rule) { + if constexpr (HasReferenceSetIntegrationRule) { + integrator.SetIntegrationRule(integration_rule); + } else if constexpr (HasPointerSetIntegrationRule) { + integrator.SetIntRule(&integration_rule); + } else { + integrator.SetIntRule(integration_rule); + } + } +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/discretization/quadrature/mfem_resolver.hpp b/src/include/serif/backend/mfem/discretization/quadrature/mfem_resolver.hpp new file mode 100644 index 00000000..5c2173ea --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/quadrature/mfem_resolver.hpp @@ -0,0 +1,134 @@ +#pragma once + +/** + * @file mfem_resolver.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the MFEMResolver class. This class is used to resolve the appropriate MFEM integration rule for a given quadrature context. + * + * @note We may want to consider renaming this file to runtime to stick to convention. The reason I did not initially is that this is more specific than pure runtime + * projection of compile time invariants (which is what most of the other files named runtime.hpp do). Something to think about. + * + * @note You will see ::mfem throughout. This is an important syntax to use here since MFEM's code lives in the mfem namespace; however, we also have code living in an + * mfem namespace. Outside of translation units that include using namespace serif::discretization::quadrature::backend and outside of the + * serif::discretization::quadrature::backend::mfem namespace itself these two things do not conflict. However, in those two cases there is a degeneracy the compiler + * cannot resolve between mfem:: and mfem::. Using ::mfem explicitly tells the compiler that we are looking from the root of the namespace tree not the current namespace. + */ + +#include +#include +#include + +#include "mfem/fem/geom.hpp" +#include "mfem/fem/intrules.hpp" + +#include "serif/discretization/quadrature/rules.hpp" + +#include "serif/backend/mfem/discretization/quadrature/order.hpp" + +#include "serif/discretization/domain/concepts.hpp" +#include "serif/discretization/quadrature/policy.hpp" +#include "serif/discretization/quadrature/query.hpp" + +#include "serif/backend/mfem/discretization/quadrature/concepts.hpp" +#include "serif/backend/mfem/discretization/quadrature/configure.hpp" +#include "serif/backend/mfem/discretization/quadrature/mfem_static_order.hpp" + +#include "serif/utils/format/strings.hpp" + +namespace serif::discretization::quadrature::backend::mfem { + + /** + * @brief The MFEMQuadratureContext struct is used to describe the context in which we are trying to resolve an MFEM integration rule. This includes the geometry type, the order of the geometry weight, any dynamic orders, the domain or set of domains, and the mapping kind. + * + * The idea here is that there is some information we can know at compile time (e.g. the role the particular integration is playing) and some we can only know + * at runtime (e.g. a manual quadrature boost requested by the user) so QuadratureContext contains all of that information which can then be turned into + * a single quadrature order. This is born out of the recognition that there is really not one good choice for quadrature order; rather, + * different systems have different accuracy requirements and should generally use the lowest order that is sufficient for their needs. + */ + struct MFEMQuadratureContext final { + QuadratureRole role = QuadratureRole::discretization; ///< The role of the quadrature rule. + + ::mfem::Geometry::Type geometry = ::mfem::Geometry::INVALID; ///< The geometry type of the element for which we are trying to resolve an MFEM integration rule. + int geometry_weight_order = 0; ///< The order of the geometry weight. + + std::span dynamic_orders{}; ///< The dynamic orders of the quadrature rule. Dynamics orders come from runtime information. + domain::DomainOrSet domain = domain::AllDomains{}; ///< The domain or set of domains for which we are trying to resolve an MFEM integration rule. + + MappingKind mapping = MappingKind::none; ///< The mapping kind of the quadrature rule. + }; + + /** + * @brief The MFEMRule struct is used to describe the result of resolving an MFEM integration rule. This includes the resolution (which contains the order and any other relevant information) and a pointer to the integration rule itself. + */ + struct MFEMRule final { + Resolution resolution{}; ///< The resolution of the MFEM integration rule, which contains the order and any other relevant information. + const ::mfem::IntegrationRule *integration_rule = nullptr; ///< A pointer to the MFEM integration rule itself. + }; + + /** + * @brief The MFEMResolver class is used to resolve the appropriate MFEM integration rule for a given quadrature context. It uses a policy to determine the appropriate order and other relevant information for the integration rule. + */ + class MFEMResolver final { + public: + /** + * @brief Constructs an MFEMResolver with the given policy. + * + * @param policy The policy used to determine the appropriate order and other relevant information for the integration rule. + */ + explicit MFEMResolver (Policy policy) noexcept : m_policy(std::move(policy)) {} // Do we actually want to use move rvalue semantics here? + + /** + * @brief Resolves the appropriate MFEM integration rule for a given quadrature context and form type. + * + * @tparam FormT The form type for which we are trying to resolve an MFEM integration rule. + * + * @param context The quadrature context describing the situation in which we are trying to resolve an MFEM integration rule. + * + * @return An MFEMRule struct containing the resolution and a pointer to the MFEM integration rule itself. + */ + template + [[nodiscard]] MFEMRule resolve(const MFEMQuadratureContext& context) const { + if (context.geometry_weight_order < 0) { + utils::errors::serif_error("MFEM geometry weight order must be non-negative. Received an order of {}", context.geometry_weight_order); + } + + const int dynamic_order = sum_dynamic_orders(context.dynamic_orders); + const int base_order = mfem_form_static_order + dynamic_order + context.geometry_weight_order; + + const Query query { + .form = forms::form_descriptor, + .role = context.role, + .domain = context.domain, + .mapping = context.mapping, + .base_order = base_order + }; + + const Resolution resolution = m_policy.resolve(query); + + const ::mfem::IntegrationRule &integration_rule = ::mfem::IntRules.Get(context.geometry, resolution.order); + return MFEMRule{.resolution = resolution, .integration_rule = &integration_rule}; + } + + /** + * @brief Configures an MFEM integrator with the appropriate integration rule for a given quadrature context and form type. This function will call the appropriate SetIntegrationRule method on the integrator based on its capabilities. + * + * @tparam FormT The form type for which we are trying to configure the integrator. + * @tparam IntegratorT The type of the integrator to configure. + * + * @param integrator The integrator to configure. + * @param context The quadrature context describing the situation in which we are trying to configure the integrator. + * + * @return The resolution of the MFEM integration rule used to configure the integrator. + */ + template + Resolution configure(IntegratorT &integrator, const MFEMQuadratureContext& context) const { + const MFEMRule rule = resolve(context); + set_integration_rule(integrator, *rule.integration_rule); + return rule.resolution; + } + + private: + Policy m_policy; ///< The policy used to determine the appropriate order and other relevant information for the integration rule. + }; +} diff --git a/src/include/serif/backend/mfem/discretization/quadrature/mfem_static_order.hpp b/src/include/serif/backend/mfem/discretization/quadrature/mfem_static_order.hpp new file mode 100644 index 00000000..8e2a15c3 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/quadrature/mfem_static_order.hpp @@ -0,0 +1,39 @@ +#pragma once + +/** + * @file mfem_static_order.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the mfem_form_static_order trait. This trait is used to determine the static order of an MFEM integrator for a given form type. + */ + +#include "serif/discretization/blocks/concepts.hpp" +#include "serif/discretization/forms/operands.hpp" +#include "serif/discretization/forms/concept.hpp" +#include "serif/utils/types/type_list.hpp" +#include "serif/backend/mfem/discretization/quadrature/order.hpp" + +namespace serif::discretization::quadrature::backend::mfem { + /** + * @brief A helper struct to compute the static order of an MFEM integrator for a given list of operands. + * @tparam OperandListT The list of operand types. + */ + template + struct MFEMOperandListOrder; + + /** + * @brief A specialization of the MFEMOperandListOrder struct for a list of operand types. This struct is used to compute the static order of an MFEM integrator for a given list of operands. + * @tparam OperandTs The operand types that make up the list. + */ + template + struct MFEMOperandListOrder> final { + static constexpr int value = (resolve_operand_order() + ... + 0); ///< The static order of the MFEM integrator for the given list of operands. This is computed by summing the static orders of each operand in the list. + }; + + /** + * @brief A trait to determine the static order of an MFEM integrator for a given form type. + * @tparam FormT The form type to check. + */ + template + inline constexpr int mfem_form_static_order = MFEMOperandListOrder::value; +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/discretization/quadrature/order.hpp b/src/include/serif/backend/mfem/discretization/quadrature/order.hpp new file mode 100644 index 00000000..cfc2f8fb --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/quadrature/order.hpp @@ -0,0 +1,86 @@ +#pragma once + +/** + * @file order.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the resolve_operand_order function and related utilities. These are used to determine the static order of an integrator for a given operand or form type. + */ + +#include + +#include "serif/discretization/functional_space/space.hpp" +#include "serif/discretization/blocks/traits.hpp" +#include "serif/discretization/forms/operands.hpp" +#include "serif/utils/error/errors.hpp" + + +namespace serif::discretization::quadrature::backend::mfem { + + /** + * @brief A function to resolve the static order of an MFEM integrator for a given operand type. + * + * @tparam OperandT The operand type to resolve the static order for. + * + * @return The static order of the MFEM integrator for the given operand type. + * + * @todo We already have some of this information in the form_traits file, we should probably extract the order boost from there. + */ + template + [[nodiscard]] consteval int resolve_operand_order() noexcept { + using Operand = std::remove_cvref_t; + using Term = Operand::term_type; + using Operation = Operand::operation_type; + + if constexpr (!blocks::HasFunctionalSpace) { + static_assert(std::same_as, "Non-Spatial terms only support the value operation at the moment."); + return 0; + } else { + using Space = blocks::SpatialTermTraits; + + constexpr auto family = Space::family; + constexpr int order = Space::order; + + if constexpr (std::same_as) { + return order; + } else if constexpr (std::same_as) { + static_assert(family == functional_space::Family::h1, "Only H1 spaces support the gradient operation at the moment."); + + return order > 0 ? order - 1 : 0; + } else if constexpr (std::same_as) { + static_assert(Space::rank == 1 && family == functional_space::Family::h1, "Divergence requires a vector H1 term."); + + return order > 0 ? order - 1 : 0; + } else if constexpr (std::same_as) { + static_assert(family == functional_space::Family::hcurl, "Only H(curl) spaces support the curl operation at the moment."); + + return order > 0 ? order - 1 : 0; + } else if constexpr (std::same_as) { + static_assert(family == functional_space::Family::hdiv, "Only H(div) spaces support the normal trace operation at the moment."); + + return order; + } else { + static_assert(false, "Unsupported operation type. This indicates that an operation was used which is not currently registered in the operand configuration system. If you are a developer and you are seeing this error, please register your operation in the resolve_operand_order function in discretization/quadrature/detail.hpp. If you are a user and you are seeing this error, please report it to the SERiF developers."); + return 0; + } + } + } + + /** + * @brief Sums the dynamic orders in a span of integers. + * @param dynamic_orders The span of dynamic orders to sum. + * @return The sum of the dynamic orders. + */ + [[nodiscard]] inline int sum_dynamic_orders(const std::span dynamic_orders) { + int total = 0; + for (const int order : dynamic_orders) { + if (order < 0) { + utils::errors::serif_error("Dynamic quadrature order cannot be negative. Received {}.", order); + } + + total += order; + } + return total; + } +} + diff --git a/src/include/serif/backend/mfem/discretization/reconstruction.hpp b/src/include/serif/backend/mfem/discretization/reconstruction.hpp new file mode 100644 index 00000000..15ae0ab0 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/reconstruction.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include + +#include "mfem/fem/fe/fe_base.hpp" +#include "serif/discretization/blocks/fields.hpp" +#include "serif/discretization/reconstruction/surface_power.hpp" + +namespace mfem { class FiniteElementSpace; } + +namespace serif::discretization::backend::mfem { + class DofMap; + + /** @brief An element's radial reconstruction; the enclosing plan owns its profile. */ + struct ElementReconstruction { + const reconstruction::RadialProfile* profile{nullptr}; + int inward_axis{0}; + bool surface_at_high_coordinate{false}; + + /** @brief Compare the mathematical reference operators for tensor-cache reuse. */ + bool operator==(const ElementReconstruction&) const = default; + /** @brief Evaluate radial factors in MFEM's canonical coordinate and ordering. */ + void radial(const ::mfem::FiniteElement& element, double coordinate, ::mfem::Vector& values, ::mfem::Vector& derivatives) const; + /** @brief Evaluate a dense reference oracle, retaining MFEM's native nodal ordering. */ + void sample(const ::mfem::FiniteElement& element, const ::mfem::IntegrationPoint& point, + ::mfem::Vector& values, ::mfem::DenseMatrix& derivatives) const; + }; + + /** @brief Immutable reconstruction selected on material-side surface elements and validated against their interfaces. */ + class ReconstructionPlan final { + public: + /** @brief Bind a radial profile to one named boundary, rejecting incompatible element and constraint layouts. */ + ReconstructionPlan(const DofMap& binding, int boundary_attribute, std::shared_ptr profile); + /** @brief Prevent attaching a resolved element selection to a different finite-element space. */ + void validate_binding(const DofMap& binding) const; + /** @brief Return no adapter on ordinary interior elements. */ + [[nodiscard]] const ElementReconstruction* element(int index) const; + /** @brief Return the profile retained by accepted operators and state readers. */ + [[nodiscard]] const reconstruction::RadialProfile& profile() const noexcept { return *m_profile; } + /** @brief Return the surface label needed to reproduce this plan on archive restoration. */ + [[nodiscard]] int boundary_attribute() const noexcept { return m_boundary_attribute; } + /** @brief Overintegrate fractional radial powers; verify convergence when choosing a new profile. */ + [[nodiscard]] int integration_order_increment() const noexcept { return 8; } + private: + /** @brief Require existing constrained coefficients to represent continuous reconstructed traces. */ + void validate_interfaces(const DofMap& binding) const; + const ::mfem::FiniteElementSpace* m_space; + long m_space_sequence; + long m_mesh_sequence; + std::vector m_domain_attributes, m_boundary_attributes, m_element_support; + int m_boundary_attribute; + std::shared_ptr m_profile; + std::vector m_elements; + }; +} + +namespace serif::discretization::backend::mfem { + /** + * @brief Resolve a surface-power request against a typed binding before publishing the discretization. + * @note Other forms add an ADL-visible make_reconstruction(specification, binding) overload returning + * an immutable ReconstructionPlan. Unsupported fields must report a SERiF error in that overload. + */ + template + [[nodiscard]] std::shared_ptr make_reconstruction( + const reconstruction::SurfacePower& specification, const Binding& binding) { + using namespace utils::errors; + if constexpr (!std::same_as) { + serif_error("SurfacePower supports only scalar H1 mass density; the requested field has no validated reconstruction."); + return {}; + } else { + int boundary_attribute = 0; + for (const auto& boundary : Binding::Schema::boundary_descriptors()) { + if (boundary.name == specification.region.boundary_name) { boundary_attribute = boundary.ID; } + } + serif_ensure(boundary_attribute != 0, + "SurfacePower boundary '{}' is not registered in this discretization's domain schema.", specification.region.boundary_name); + auto profile = std::make_shared(specification.density_exponent); + if (specification.density_exponent == 1.0) { return {}; } + return std::make_shared(binding, boundary_attribute, std::move(profile)); + } + } +} diff --git a/src/include/serif/backend/mfem/discretization/resources.hpp b/src/include/serif/backend/mfem/discretization/resources.hpp new file mode 100644 index 00000000..2a11d217 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/resources.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "mfem/fem/gridfunc.hpp" +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/utils/dependencies/dependency_context.hpp" +#include "serif/utils/dependencies/key_list_operations.hpp" + +namespace serif::discretization::backend::mfem { + /** @brief Own meshes and finite-element resources in reverse dependency order, for creation and archive restoration. */ + struct Resources { + std::vector> meshes; + std::vector> collections; + std::vector> spaces; + std::unique_ptr<::mfem::GridFunction> compactification; + std::unique_ptr mapper; + }; + + template + struct OwnedBindings; + + /** @brief Keep typed bindings alive beside the resources they borrow; neither copying a configuration nor restoring it copies these resources. */ + template + struct OwnedBindings> { + template + using Binding = std::remove_cvref_t().template get())>; + + Resources resources; + std::tuple>...> bindings; + + /** @brief Borrow each stable binding through the existing computational-graph input vocabulary. */ + [[nodiscard]] auto context() const { + using namespace utils::dependencies; + + return make_dependency_context(borrow(*std::get>()>(bindings))...); + } + }; +} diff --git a/src/include/serif/backend/mfem/discretization/schema.hpp b/src/include/serif/backend/mfem/discretization/schema.hpp new file mode 100644 index 00000000..457c451a --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/schema.hpp @@ -0,0 +1,26 @@ +#pragma once + +/** + * @file schema.hpp + * @author Emily M. Boudreaux + * @brief Adapt MFEM meshes to domain schema validation. + */ + +#include "mfem/mesh/mesh.hpp" +#include "serif/backend/mfem/discretization/topology.hpp" +#include "serif/discretization/domain/concepts.hpp" +#include "serif/discretization/domain/schema/validation/all.hpp" + +namespace serif::backend::mfem::discretization { + /** + * @brief Validate an MFEM mesh against a physical domain schema. + * @tparam SchemaT The schema type to validate against. + * @param mesh The MFEM mesh to validate. + * @return The validation result for every relation in the schema. + * @note Reuse make_mesh_topology when several schemas inspect the same mesh. + */ + template <::serif::discretization::domain::schema::IsSchema SchemaT> + [[nodiscard]] ::serif::discretization::domain::schema::validation::SchemaValidationResult validate_schema(const ::mfem::Mesh& mesh) { + return ::serif::discretization::domain::schema::validation::validate_schema(make_mesh_topology(mesh)); + } +} diff --git a/src/include/serif/backend/mfem/discretization/tensor_basis.hpp b/src/include/serif/backend/mfem/discretization/tensor_basis.hpp new file mode 100644 index 00000000..95d07626 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/tensor_basis.hpp @@ -0,0 +1,539 @@ +#pragma once + +/** + * @file tensor_basis.hpp + * @author Emily M. Boudreaux + * @brief Implementation of fast tensor evaluation functions. These do not contribute physics but are used to accelerate evaluations. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "mfem/fem/fe/fe_h1.hpp" +#include "mfem/fem/fe/fe_l2.hpp" +#include "serif/utils/error/errors.hpp" +#include "serif/backend/mfem/discretization/reconstruction.hpp" + +namespace serif::discretization::backend::mfem { + /** + * @enum ReferenceOperation + * @brief Options enum to control what is actually evaluated at the reference points. + */ + enum class ReferenceOperation { + value, ///< Evaluate the basis functions at the reference points + gradient, ///< Evaluate the gradients of the basis functions at the reference points + value_and_gradient ///< Evaluate both the basis functions and their gradients at the reference points + }; + + /** + * @enum ContractionMode + * @brief Options enum to control how the tensor contractions are performed. + */ + enum class ContractionMode { + automatic, ///< Automatically choose the contraction mode based on the number of points and dofs + dense ///< Use a dense contraction mode + }; + + /** + * @struct TensorWorkspace + * @brief A workspace for tensor contractions to avoid repeated allocations and pack memory (TODO: does this actual pack memory in a useful manner when using mfem vectors? Probably something to look into). + */ + struct TensorWorkspace { + ::mfem::Vector coefficients; ///< The coefficients of the basis functions at the reference points + ::mfem::Vector first; ///< The first contraction of the coefficients with the basis functions at the reference points + ::mfem::Vector second; ///< The second contraction of the coefficients with the basis functions at the reference points + }; + + /** + * @class TensorBasis + * @brief A class that represents a tensor basis for a finite element. This class is used to evaluate the basis functions and their derivatives at a set of reference + * points. + */ + class TensorBasis final { + /** + * @struct Product + * @brief A struct that represents a product of one-dimensional basis functions. + */ + struct Product { + std::array<::mfem::DenseMatrix, 3> factors; ///< The one-dimensional basis functions for each axis + ::mfem::Array dofs; ///< The dofs associated with the product + int output; ///< The output index for the product (used for mapping to the correct component of the output vector) + }; + + public: + /// Return no tensor plan unless its factorization agrees with MFEM's canonical basis. + + /** + * @brief Make a tensor basis for a given finite element, integration rule, reference operation, and number of copies. This function returns a shared pointer to a TensorBasis object that can be used to do efficient evaluation of the basis functions and their derivatives at reference points. + * @param element The finite element for which to construct the tensor basis. + * @param rule The integration rule to use for evaluating the basis functions. + * @param operation The reference operation to apply (e.g., value, gradient). + * @param copies The number of copies of the basis functions. + * @returns A shared pointer to a TensorBasis object if the tensor basis can be constructed, or an empty shared pointer if it cannot. + */ + [[nodiscard]] static std::shared_ptr make( + const ::mfem::FiniteElement& element, + const ::mfem::IntegrationRule& rule, + const ReferenceOperation operation, + const int copies, + const ElementReconstruction* reconstruction = nullptr + ) { + const auto* scalar = dynamic_cast(&element); + const bool scalar_hex = dynamic_cast(&element) || dynamic_cast(&element); + + // TODO: This is an awful lot of checks, compress these into some sort of "is this a valid tensor basis" function that can be used in the tests as well. + if (copies <= 0 || !scalar_hex || rule.GetNPoints() == 0 || scalar == nullptr || element.GetMapType() != ::mfem::FiniteElement::VALUE) { + return {}; + } + + const int q = static_cast(std::lround(std::cbrt(rule.GetNPoints()))); + if (q * q * q != rule.GetNPoints()) { + return {}; + } + + for (int z = 0; z < q; ++z) { + for (int y = 0; y < q; ++y) { + for (int x = 0; x < q; ++x) { + const auto& point = rule.IntPoint(x + q * (y + q * z)); + if (point.x != rule.IntPoint(x).x || point.y != rule.IntPoint(q * y).y || point.z != rule.IntPoint(q * q * z).z) { + return {}; + } + } + } + } + + auto result = std::make_shared(); + + result->m_points = rule.GetNPoints(); + result->m_dofs = element.GetDof() * copies; + + const int samples = operation == ReferenceOperation::gradient ? 3 : operation == ReferenceOperation::value_and_gradient ? 4 : 1; + + result->m_components = copies * samples; + + const int closed = element.GetOrder() + 1; + const int block = element.GetDof(); + const auto& native_map = scalar->GetDofMap(); + + if (native_map.Size() != 0 && native_map.Size() != element.GetDof()) { + return {}; + } + + // TODO: I once again make use of a vector since it is the most obvious conceptual datatype. However, the standard backed implementation of + // vector is not actually a vector of bools; rather, it is a bitset. I dont think this matters for what I am doing but is is probably + // worth being aware of this + std::vector seen(element.GetDof(), false); + + for (int dof = 0; dof < element.GetDof(); ++dof) { + const int encoded = native_map.Size() ? native_map[dof] : dof; + const int native = encoded < 0 ? -1 - encoded : encoded; + + if (encoded < 0 || native < 0 || native >= element.GetDof() || seen[native]) { + return {}; + } + + seen[native] = true; + } + + for (int component = 0; component < copies; ++component) { + for (int sample = 0; sample < samples; ++sample) { + auto&[factors, dofs, output] = result->m_products.emplace_back(); + + output = component * samples + sample; + const int derivative = operation == ReferenceOperation::gradient ? sample : operation == ReferenceOperation::value_and_gradient ? sample - 1 : -1; + + int tensor_dofs = 1; + for (int axis = 0; axis < 3; ++axis) { + const int count = closed; + tensor_dofs *= count; + + ::mfem::DenseMatrix& factor = factors[axis]; + factor.SetSize(q, count); + + ::mfem::Vector values(count); + ::mfem::Vector gradients(count); + + const auto& basis = scalar->GetBasis1D(); + for (int point = 0; point < q; ++point) { + const double coordinate = axis == 0 ? rule.IntPoint(point).x : axis == 1 ? rule.IntPoint(q * point).y : rule.IntPoint(q * q * point).z; + + // Note that here this is just two overloads of Eval that mfem::Poly_1D provides, the first one evaluates the gradient and the latter evaluates the value. + if (reconstruction != nullptr && axis == reconstruction->inward_axis) { + reconstruction->radial(element, coordinate, values, gradients); + } + else if (axis == derivative) { + basis.Eval(coordinate, values, gradients); + } + else { + basis.Eval(coordinate, values); + } + + for (int dof = 0; dof < count; ++dof) { + factor(point, dof) = axis == derivative ? gradients[dof] : values[dof]; + } + } + } + + if (tensor_dofs != block) { + return {}; + } + + dofs.SetSize(block); + for (int lex = 0; lex < block; ++lex) { + const int encoded = native_map.Size() ? native_map[lex] : lex; + + dofs[lex] = encoded + component * element.GetDof(); + } + } + } + + return result->agrees_with(element, rule, operation, reconstruction) ? result : nullptr; + } + + /** + * @brief Get the number of quadrature points in the Cartesian rule. + * @returns The number of quadrature points in the Cartesian rule. + */ + [[nodiscard]] int points() const noexcept { + return m_points; + } + + /** + * @brief Get the number of degrees of freedom in the tensor basis. + * @returns The number of degrees of freedom in the tensor basis. + */ + [[nodiscard]] int dofs() const noexcept { + return m_dofs; + } + + /** + * @brief Get the number of canonical output components per point in the tensor basis. + * @returns The number of canonical output components per point in the tensor basis. + */ + [[nodiscard]] int components() const noexcept { + return m_components; + } + + /// Evaluate canonical samples, or their coordinate transpose, with caller-owned scratch. + + /** + * @brief Apply the tensor basis to an input vector and store the results in an output vector. This function performs the tensor contraction of the input + * vector with the tensor basis. + * @param input The input vector to be contracted with the tensor basis. + * @param output The output vector to store the result of the contraction. (Owned by the caller) + * @param workspace The workspace to use for the contraction. (Owned by the caller) + * @param transpose Whether to transpose the contraction (default is false). + * + * @pre The input vector must have a size equal to the number of degrees of freedom in the tensor basis if transpose is false, or equal to the number of points times the number of components if transpose is true. + * @pre The output vector must be constructed + * @post The output vector will have a size equal to the number of points times the number of components if transpose is false, or equal to the number of degrees of freedom in the tensor basis if transpose is true. + * @post The output vector will contain the result of the tensor contraction of the input vector with the tensor basis. + */ + void apply(const ::mfem::Vector& input, ::mfem::Vector& output, TensorWorkspace& workspace, const bool transpose = false) const { + using namespace utils::errors; + + serif_ensure(input.Size() == (transpose ? m_points * m_components : m_dofs), "Tensor contraction input has the wrong coefficient or quadrature size."); + + output.SetSize(transpose ? m_dofs : m_points * m_components); + output = 0.0; + + for (const auto& product : m_products) { + auto& coefficients = workspace.coefficients; + coefficients.SetSize(transpose ? m_points : product.dofs.Size()); + + for (int i = 0; i < coefficients.Size(); ++i) { + const int encoded = transpose ? 0 : product.dofs[i]; + coefficients[i] = transpose ? input[i * m_components + product.output] : encoded < 0 ? -input[-1 - encoded] : input[encoded]; + } + + std::array shape; + for (int axis = 0; axis < 3; ++axis) { + shape[axis] = transpose ? product.factors[axis].Height() : product.factors[axis].Width(); + } + + const ::mfem::Vector* source = &coefficients; + for (int step = 0; step < 3; ++step) { + const int axis = transpose ? 2 - step : step; + auto& destination = step % 2 == 0 ? workspace.first : workspace.second; + + contract_axis(product.factors[axis], axis, shape, *source, destination, transpose); + + shape[axis] = transpose ? product.factors[axis].Width() : product.factors[axis].Height(); + source = &destination; + } + + for (int i = 0; i < source->Size(); ++i) { + if (transpose) { + const int encoded = product.dofs[i]; + output[encoded < 0 ? -1 - encoded : encoded] += (encoded < 0 ? -1.0 : 1.0) * (*source)[i]; + } else { + output[i * m_components + product.output] += (*source)[i]; + } + } + } + } + + /** + * @brief apply() for several coefficient vectors at once, stored lane-interleaved (entry i of lane j at i * lanes + j). + * @note The lanes form the fastest tensor index, so every lane is contracted with exactly the operations and + * summation order apply() uses for it alone; only the loops are shared. + */ + void apply_many(const ::mfem::Vector& input, ::mfem::Vector& output, const int lanes, TensorWorkspace& workspace, const bool transpose = false) const { + using namespace utils::errors; + + serif_ensure(lanes > 0 && input.Size() == lanes * (transpose ? m_points * m_components : m_dofs), "Batched tensor contraction input has the wrong coefficient or quadrature size."); + + output.SetSize(lanes * (transpose ? m_dofs : m_points * m_components)); + output = 0.0; + + for (const auto& product : m_products) { + auto& coefficients = workspace.coefficients; + const int entries = transpose ? m_points : product.dofs.Size(); + coefficients.SetSize(entries * lanes); + + for (int i = 0; i < entries; ++i) { + const int encoded = transpose ? 0 : product.dofs[i]; + for (int lane = 0; lane < lanes; ++lane) { + coefficients[i * lanes + lane] = transpose ? input[(i * m_components + product.output) * lanes + lane] : encoded < 0 ? -input[(-1 - encoded) * lanes + lane] : input[encoded * lanes + lane]; + } + } + + std::array shape; + for (int axis = 0; axis < 3; ++axis) { + shape[axis] = transpose ? product.factors[axis].Height() : product.factors[axis].Width(); + } + + const ::mfem::Vector* source = &coefficients; + for (int step = 0; step < 3; ++step) { + const int axis = transpose ? 2 - step : step; + auto& destination = step % 2 == 0 ? workspace.first : workspace.second; + + contract_axis(product.factors[axis], axis, shape, *source, destination, transpose, lanes); + + shape[axis] = transpose ? product.factors[axis].Width() : product.factors[axis].Height(); + source = &destination; + } + + const int results = source->Size() / lanes; + for (int i = 0; i < results; ++i) { + for (int lane = 0; lane < lanes; ++lane) { + if (transpose) { + const int encoded = product.dofs[i]; + output[(encoded < 0 ? -1 - encoded : encoded) * lanes + lane] += (encoded < 0 ? -1.0 : 1.0) * (*source)[i * lanes + lane]; + } else { + output[(i * m_components + product.output) * lanes + lane] += (*source)[i * lanes + lane]; + } + } + } + } + } + + private: + /** + * @breif apply one factor along one tensor axis, teh other two indices will remain unchanged + * @param factor The one-dimensional basis function to apply along the specified axis. + * @param axis The axis along which to apply the factor (0, 1, or 2). + * @param shape The shape of the input tensor (number of points along each axis). + * @param input The input vector to be contracted with the factor. + * @param output The output vector to store the result of the contraction. + * @param transpose Whether to transpose the factor matrix before applying it. + * @param lanes Number of interleaved independent tensors (the fastest index); 1 for a single tensor. + * + * @pre The input vector must have a size equal to the product of the dimensions specified in the shape array. + * @post The output vector will have a size equal to the product of the dimensions specified in the next array. + * @post The output vector will contain the result of the contraction of the input vector with the factor along the specified axis. + */ + static void contract_axis( + const ::mfem::DenseMatrix& factor, + const int axis, + const std::array& shape, + const ::mfem::Vector& input, + ::mfem::Vector& output, + const bool transpose, + const int lanes = 1 + ) { + const int input_extent = shape[axis]; + const int output_extent = transpose ? factor.Width() : factor.Height(); + int inner = lanes, outer = 1; + for (int dimension = 0; dimension < axis; ++dimension) { + inner *= shape[dimension]; + } + for (int dimension = axis + 1; dimension < 3; ++dimension) { + outer *= shape[dimension]; + } + output.SetSize(outer * output_extent * inner); + output = 0.0; + const int factor_height = factor.Height(); + const double* factors = factor.GetData(); + const double* source = input.GetData(); + double* destination = output.GetData(); + // Each tensor slice is contiguous. Every output still sums columns in ascending order. + for (int block = 0; block < outer; ++block) { + for (int row = 0; row < output_extent; ++row) { + double* output_slice = destination + (block * output_extent + row) * inner; + for (int column = 0; column < input_extent; ++column) { + const double coefficient = transpose ? factors[row * factor_height + column] : factors[column * factor_height + row]; + const double* input_slice = source + (block * input_extent + column) * inner; + for (int entry = 0; entry < inner; ++entry) { + output_slice[entry] += coefficient * input_slice[entry]; + } + } + } + } + } + + /// Verify signed ordering and every requested basis entry before enabling the fast path. + + /** + * @brief Verify that the signed ordering and every requested basis entry agrees with MFEM's canonical basis. + * @param element The finite element to verify against. + * @param rule The integration rule to use for evaluating the basis functions. + * @param operation The reference operation to apply (e.g., value, gradient). + * + * @returns True if the tensor basis agrees with MFEM's canonical basis, false otherwise. + */ + [[nodiscard]] bool agrees_with( + const ::mfem::FiniteElement& element, + const ::mfem::IntegrationRule& rule, + const ReferenceOperation operation, + const ElementReconstruction* reconstruction + ) const { + const int samples = operation == ReferenceOperation::gradient ? 3 : operation == ReferenceOperation::value_and_gradient ? 4 : 1; + + ::mfem::Vector values(element.GetDof()); + ::mfem::DenseMatrix derivatives(element.GetDof(), 3); + + for (int point = 0; point < m_points; ++point) { + if (reconstruction != nullptr) { + reconstruction->sample(element, rule.IntPoint(point), values, derivatives); + } else { + element.CalcShape(rule.IntPoint(point), values); + if (samples > 1) { + element.CalcDShape(rule.IntPoint(point), derivatives); + } + } + + for (const auto&[factors, dofs, output] : m_products) { + const int q = factors[0].Height(), nx = factors[0].Width(), ny = factors[1].Width(); + + for (int dofID = 0; dofID < dofs.Size(); ++dofID) { + const int encoded = dofs[dofID], native = (encoded < 0 ? -1 - encoded : encoded) % element.GetDof(); + + const int derivative = operation == ReferenceOperation::gradient ? output % samples : output % samples - 1; + double expected = samples == 1 || derivative < 0 ? values[native] : derivatives(native, derivative); + + expected *= encoded < 0 ? -1.0 : 1.0; + const double actual = factors[0](point % q, dofID % nx) * factors[1]((point / q) % q, (dofID / nx) % ny) * factors[2](point / (q * q), dofID / (nx * ny)); + + // Similar to above in this file, this is a long series of checks, maybe refactor into a function? + if (!std::isfinite(actual) || !std::isfinite(expected) || std::abs(actual - expected) > 64.0 * std::numeric_limits::epsilon() * std::max(1.0, std::abs(expected))) { + return false; + } + } + } + } + + return true; + } + + int m_points{0}; ///< The number of quadrature points in the Cartesian rule. + int m_dofs{0}; ///< The number of degrees of freedom in the tensor basis. + int m_components{0}; ///< The number of canonical output components per point in the tensor basis. + std::vector m_products; ///< The products of one-dimensional basis functions for each axis. (TOOD: consider if this should be a heap allocated vector of it this could be put on the stack somehow) + }; + + /// Share immutable tensor plans within one operator; copied rules are keyed by their contents. + + /** + * @class TensorBasisCache + * @brief A cache for TensorBasis objects to avoid repeated construction of the same tensor basis. + * This class stores a vector of "Entry" structs which each contain a non-owning pointer to some element, + * options, a shared pointer to a TensorBasis, and the points used to construct the TensorBasis. + */ + class TensorBasisCache final { + + /** + * @struct Entry + * @brief A struct that represents an entry in the TensorBasisCache. Each entry contains a non-owning pointer to a finite element, the reference operation, the number of copies, the points used to construct the TensorBasis, and a shared pointer to the TensorBasis itself. + * + * @todo Consider if we can remove the shared pointer from here, shared pointers introduce overhead with reference counting and that may slow down the cache. + */ + struct Entry { + const ::mfem::FiniteElement* element; + ReferenceOperation operation; + int copies; + ElementReconstruction reconstruction; + std::vector> points; + std::shared_ptr basis; + }; + public: + /// Retrieve a verified plan, caching unsupported combinations as well. + + /** + * @brief Retrieve a verified tensor basis, caching unsupported combinations as well. + * @param element The finite element for which to retrieve the tensor basis. + * @param rule The integration rule to use for evaluating the basis functions. + * @param operation The reference operation to apply (e.g., value, gradient). + * @param copies The number of copies of the basis functions. + * @returns A shared pointer to a TensorBasis object if the tensor basis can be constructed, or an empty shared pointer if it cannot. + * + * @todo I think it would make sense, at some point, to switch this to an std::optional return + */ + [[nodiscard]] std::shared_ptr get( + const ::mfem::FiniteElement& element, + const ::mfem::IntegrationRule& rule, + ReferenceOperation operation, + const int copies, + const ElementReconstruction* reconstruction = nullptr + ) { + std::vector> points; + + for (int q = 0; q < rule.GetNPoints(); ++q) { + const auto& point = rule.IntPoint(q); + points.push_back({point.x, point.y, point.z, point.weight}); + } + + for (const auto& [el, op, cps, representation, pts, bs] : m_entries) { + if (el == &element && op == operation && cps == copies && representation == (reconstruction == nullptr ? ElementReconstruction{} : *reconstruction) && pts == points) { + return bs; + } + } + + const auto basis = TensorBasis::make(element, rule, operation, copies, reconstruction); + + m_entries.push_back({ + .element = &element, + .operation = operation, + .copies = copies, + .reconstruction = reconstruction == nullptr ? ElementReconstruction{} : *reconstruction, + .points = std::move(points), + .basis = basis + }); + + return basis; + } + + /** + * @brief Get the number of entries in the TensorBasisCache. + * @return The number of entries in the cache. + */ + [[nodiscard]] std::size_t size() const noexcept { + return m_entries.size(); + } + + /** + * @brief Clear the TensorBasisCache, removing all entries. + */ + void clear() { + m_entries.clear(); + } + + private: + std::vector m_entries; ///< The entries in the TensorBasisCache, each containing a non-owning pointer to a finite element, the reference operation, the number of copies, the points used to construct the TensorBasis, and a shared pointer to the TensorBasis itself. + }; +} // namespace serif::discretization::backend::mfem diff --git a/src/include/serif/backend/mfem/discretization/term_binding.hpp b/src/include/serif/backend/mfem/discretization/term_binding.hpp new file mode 100644 index 00000000..a4d0537d --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/term_binding.hpp @@ -0,0 +1,116 @@ +#pragma once +/** + * @file term_binding.hpp + * @author Emily M. Boudreaux + * @brief A runtime binding between some term descriptor and its MFEM finite-element space. This is used to project + * down the compile time term descriptors into runtime reinterpretations which can then be used to extract out things like + * the list of dofs, the element support, and the mesh. + */ + +#include + +#include "serif/discretization/blocks/traits.hpp" +#include "serif/discretization/domain/schema/concepts.hpp" +#include "serif/backend/mfem/discretization/dof_map.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::discretization::backend::mfem { + /** + * @brief A runtime binding between a term descriptor and its MFEM finite-element space. This is used to project + * down the compile time term descriptors into runtime reinterpretations which can then be used to extract + * things like the list of dofs, the element support, and the mesh. Note that this is a child of + * DofMap, therefor all term bindings are also dof maps + * @tparam TermT The term descriptor type to bind to. Note that if the FiniteElementSpace is not a global scalar then this must be a spatial term descriptor (@ref SpatialTerm) + * @tparam SchemaT The domain schema type to bind to. This must be a valid schema (@ref IsSchema) + */ + template + requires (blocks::SpatialTerm || blocks::GlobalScalarTerm) + class TermBinding final : public DofMap { + public: + using Term = TermT; + using Schema = SchemaT; + + /** + * @brief Construct a term binding from an MFEM finite-element space. This requires that the term be a valid spatial term (@ref SpatialTerm) + * @param space The finite-element space to bind to. + */ + explicit TermBinding( + ::mfem::FiniteElementSpace& space, std::shared_ptr reconstruction = {} + ) requires blocks::SpatialTerm : + DofMap(space, element_support(checked_mesh(space)), std::move(reconstruction)) { + using Traits = blocks::SpatialTermTraits; + using namespace utils::errors; + + static_assert(Traits::family == functional_space::Family::h1 || Traits::family == functional_space::Family::l2, "The MFEM backend currently only supports H1 and L2 spaces."); + static_assert(Traits::rank <= 1, "Tensor terms are not supported by this backend."); + + if (this->reconstruction()) { + serif_ensure(Traits::family == functional_space::Family::h1 && Traits::rank == 0, + "Radial reconstruction currently supports only scalar H1 fields."); + serif_ensure(std::same_as || + dynamic_cast(&this->reconstruction()->profile()) == nullptr, + "SurfacePower has been validated only for mass density; select that field or implement a compatible reconstruction form."); + } + + const int vector_dimension = Traits::rank == 0 ? 1 : 3; + const int continuity = Traits::family == functional_space::Family::h1 ? ::mfem::FiniteElementCollection::CONTINUOUS: ::mfem::FiniteElementCollection::DISCONTINUOUS; + + serif_ensure(space.GetVDim() == vector_dimension,"Term vector dimension disagrees with its scalar-copy representation."); + serif_ensure(space.FEColl()->GetContType() == continuity,"Term continuity disagrees with its descriptor."); + + for (int element = 0; element < space.GetNE(); ++element) { + const auto* finite_element = space.GetFE(element); + + serif_ensure( + finite_element != nullptr && finite_element->GetOrder() == Traits::order && + finite_element->GetRangeType() == ::mfem::FiniteElement::SCALAR && + finite_element->GetMapType() == ::mfem::FiniteElement::VALUE, + "Term elements must match the registered family order, range, and mapping."); + } + } + + /** + * @brief Construct a term binding from an MFEM mesh. This requires that the term be a valid global scalar term (@ref GlobalScalarTerm). E.g. the only way you + * can construct such a term binding is for things like the total mass, angular momentum, etc... + * @param mesh The mesh to bind to, note that the mesh must be three-dimensional and have a space dimension of three. + */ + explicit TermBinding( + ::mfem::Mesh& mesh + ) requires discretization::blocks::GlobalScalarTerm : + DofMap(mesh, element_support(mesh)) {} + + private: + /** + * @brief Check that the mesh associated with a finite-element space is valid for term binding construction. + * @param space The finite-element space to check. + * @return The mesh associated with the finite-element space. + */ + static ::mfem::Mesh& checked_mesh(const ::mfem::FiniteElementSpace& space) { + using namespace utils::errors; + serif_ensure(space.GetMesh() != nullptr && space.FEColl() != nullptr, "Term bindings require an initialized finite-element space."); + return *space.GetMesh(); + } + + /** + * @brief Translate the mesh into an element support array for the particular term binding + * @param mesh the mesh to translate into an element support array + * @return An array where each entry is 1 if the corresponding element is supported by the term binding and 0 otherwise. This array is parallel to the mesh elements ordering array. + */ + static ::mfem::Array element_support(const ::mfem::Mesh& mesh) { + using namespace utils::errors; + constexpr auto domains = Schema::domain_descriptors(); + ::mfem::Array result(mesh.GetNE()); + + auto ID_matches = [](const auto& domain, const int attribute) -> bool { + return domain.ID == attribute; + }; + + for (int element = 0; element < mesh.GetNE(); ++element) { + const int attribute = mesh.GetAttribute(element); + serif_ensure(std::ranges::any_of(domains, [&](const auto& domain) { return ID_matches(domain, attribute); }), "Element attribute does not match the registered domain."); + result[element] = Schema::template domain_id_belongs_to(attribute); + } + return result; + } + }; +} diff --git a/src/include/serif/backend/mfem/discretization/term_coefficients.hpp b/src/include/serif/backend/mfem/discretization/term_coefficients.hpp new file mode 100644 index 00000000..21429292 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/term_coefficients.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "serif/utils/dependencies/dependency_context.hpp" + +#include "serif/backend/mfem/discretization/term_binding.hpp" + +namespace serif::operators::backend::mfem { + struct MappingCoefficients { + const ::mfem::FiniteElementSpace& binding; + const ::mfem::Vector& values; + }; + + + template + struct CoefficientView { + using Term = BindingT::Term; + const BindingT& binding; + const ::mfem::Vector& values; + }; + + /// Borrow a term binding and its coefficients without extending their lifetimes. + template + [[nodiscard]] CoefficientView bind(const BindingT& binding, const ::mfem::Vector& values) { + return {binding, values}; + } + + template + void bind(const BindingT&, ::mfem::Vector&&) = delete; + + template + void bind(const BindingT&, const ::mfem::Vector&&) = delete; + + template + void bind(const BindingT&&, const ::mfem::Vector&) = delete; + + /// Associate each physical term with its bound coefficients in an ordinary dependency context. + template + [[nodiscard]] auto make_term_coefficients(BindingTs... bindings) { + using namespace utils::dependencies; + + static_assert(sizeof...(BindingTs) > 0, "Coefficient inputs must bind at least one term."); + return make_dependency_context(own(bindings)...); + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/discretization/topology.hpp b/src/include/serif/backend/mfem/discretization/topology.hpp new file mode 100644 index 00000000..2db15ce0 --- /dev/null +++ b/src/include/serif/backend/mfem/discretization/topology.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "serif/discretization/domain/mesh/topology.hpp" + +namespace mfem { + class Mesh; +} + +namespace serif::backend::mfem::discretization { + /** @brief Copy MFEM material labels and connectivity into backend-independent topology. */ + [[nodiscard]] ::serif::discretization::domain::mesh::MeshTopology make_mesh_topology(const ::mfem::Mesh& mesh); +} diff --git a/src/include/serif/backend/mfem/geometry/deformation/computations.hpp b/src/include/serif/backend/mfem/geometry/deformation/computations.hpp new file mode 100644 index 00000000..934c17cd --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/computations.hpp @@ -0,0 +1,168 @@ +#pragma once + +/** + * @file computations.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the factories to make the computation graph for the mfem backend of the geometry/deformation module. + */ + +#include +#include + +#include "serif/backend/mfem/geometry/deformation/constrained_radial_extension.hpp" +#include "serif/backend/mfem/geometry/deformation/inputs.hpp" +#include "serif/backend/mfem/geometry/deformation/radial_interpolation.hpp" +#include "serif/backend/mfem/geometry/deformation/surface_metric.hpp" +#include "serif/geometry/deformation/surface_embedding.hpp" + +namespace serif::geometry::deformation::backend::mfem +{ + //TODO: Some parts of this this detail namespace can be factored into implementation files as + // they are not templated. Keeping them as inline functions in a header may increase compile times + // for an already slow library. + namespace detail + { + /** + * @brief Share a successfully compiled immutable table without copying it into each consumer. + * + * @todo A shared pointer was the easiest way to implement this; however, this introduces some non-zero + * overhead related to reference counting. Generally, shared pointers should only be used when absolutely + * necessary and I have not given deep thought to if that is the case here (and more generally across the + * graph system). + * + * @param result The result of a computation that may be successful or may have failed. + * + * @returns If the inputted result is successful, returns a shared pointer to the result. If the inputted result is a failure, returns the same failure. + */ + template + [[nodiscard]] std::expected, DeformationFailure> share_reference_result(std::expected result) + { + if (!result) { + return std::unexpected(result.error()); + } + + return std::make_shared(std::move(*result)); + } + + /** + * @brief Compile a radial interpolation from a reference discretization and set of material attributes. + * + * @param input The reference discretization containing the scalar and displacement finite element spaces, logical mesh, and material attributes. + * @param selected A vector of integers representing the selected material attributes for the radial interpolation. + * + * @returns If successful, returns a RadialInterpolation object representing the compiled radial interpolation. If there is an error, returns a DeformationFailure object describing the failure. + * + * @todo This is a holdover from the original MeanField code and its bespoke reference tracking system. It is likely that this can be removed in favor of SERiF's more robust dependencies graph system. + * + * Broadly the idea here (I am noticing in this doc writing process I say broadly a lot...) we loop over all elements in the mesh, determine if they have + * stellar or vacuum support + */ + [[nodiscard]] inline std::expected compile_radial_reference( + const ReferenceDiscretization& input, + const std::vector& selected) + { + const ::mfem::Mesh* mesh = input.scalar_space.GetMesh(); + if (!mesh) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Radial deformation requires an initialized scalar reference mesh."}); + } + std::vector stellar_support(mesh->GetNE()); + std::vector vacuum_support(mesh->GetNE()); + + + for (int element = 0; element < mesh->GetNE(); ++element) { + const int attribute = mesh->GetAttribute(element); + + stellar_support[element] = std::ranges::find(input.stellar_attributes, attribute) != input.stellar_attributes.end(); + vacuum_support[element] = std::ranges::find(input.vacuum_attributes, attribute) != input.vacuum_attributes.end(); + + if (stellar_support[element] == 0 && vacuum_support[element] == 0) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "A volume element belongs to neither the stellar nor vacuum deformation support.", .element = element}); + } + } + + return build_radial_interpolation( + input.scalar_space, + input.displacement_space, + input.logical_mesh, + stellar_support, + vacuum_support, + input.surface_attribute, + input.infinity_attribute, + selected + ); + } + } // namespace detail + + /** + * @brief Optionally supply the reference surface mass diagonal in native shape coordinates. + * @note This reference metric is independent of shape coefficients and the volume extension; requesting it must not compile either. + * + */ + [[nodiscard]] inline auto make_surface_metric_computation() + { + using namespace utils::dependencies; + + return make_computation>( + [](const SurfaceDiscretization& input, const std::shared_ptr& reference, const std::shared_ptr& surface) -> std::expected, DeformationFailure> + { + if (!reference || !surface) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_configuration, .message = "The shape metric requires compiled surface reference geometry and its embedding."}); + } + return detail::surface_parameter_mass_diagonal(input.scalar_space, input.surface_attribute, *reference, *surface); + }); + } + + /** + * @brief Compile fixed reference factors and evaluate shape motion with one dependency engine. + * @note Configuration values are named context inputs. Factories declare work; try_prepare performs it explicitly. + */ + template + requires requires(const Surface& model, std::span> positions) + { + { build_surface_embedding(model, positions) } -> std::same_as>; + } + [[nodiscard]] auto make_deformation_computations() + { + using namespace utils::dependencies; + + const auto surface_nodes = make_computation>( + [](const SurfaceDiscretization& input) + { + return select_surface_scalar_dofs(input.scalar_space, input.surface_attribute); + }); + const auto reference_surface = make_computation>( + [](const SurfaceDiscretization& input, const ::mfem::FiniteElementSpace&, const std::vector& selected) + { + return detail::share_reference_result(detail::sample_reference_surface(input.scalar_space, selected)); + }); + const auto radial_interpolation = make_computation>( + [](const ReferenceDiscretization& input, const std::vector& selected) + { + return detail::share_reference_result(detail::compile_radial_reference(input, selected)); + }); + const auto surface_map = make_computation>( + [](const std::shared_ptr& reference, const Surface& model) + { + return detail::share_reference_result(build_surface_embedding(model, reference->positions)); + }); + const auto extension = make_computation>( + [](const std::shared_ptr& interpolation, const ReferenceDiscretization& reference, const PowerLawRadialExtension& interior, const FixedInfinityRadialExtension& vacuum) + { + return detail::share_reference_result(make_constrained_radial_extension(interpolation, reference.displacement_space, interior, vacuum)); + }); + return std::tuple_cat(std::tuple{surface_nodes, reference_surface, radial_interpolation, surface_map, extension}, deformation::make_deformation_computations()); + } + + /** @brief Provide a standalone graph while leaving the same computations available for equation composition. */ + template + [[nodiscard]] auto make_deformation_graph() + { + return std::apply([](auto... computations) + { + return utils::dependencies::make_computation_graph(std::move(computations)...); + }, + make_deformation_computations()); + } +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/include/serif/backend/mfem/geometry/deformation/constrained_radial_extension.hpp b/src/include/serif/backend/mfem/geometry/deformation/constrained_radial_extension.hpp new file mode 100644 index 00000000..6b97276a --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/constrained_radial_extension.hpp @@ -0,0 +1,193 @@ +#pragma once + +#include +#include +#include + +#include "mfem/fem/fespace.hpp" +#ifdef MFEM_USE_MPI +#include "mfem/fem/pfespace.hpp" +#endif +#include "mfem/linalg/sparsemat.hpp" +#include "serif/geometry/deformation/radial_extension.hpp" +#include "serif/geometry/deformation/detail/action_storage.hpp" + +namespace serif::geometry::deformation::backend::mfem { + /** + * @brief Keep radial displacement continuous across hanging faces in local component-block coordinates. + * @note The fixed map is P R E: radial sampling E, coefficient restriction R, and prolongation P. + * Its transpose is E^T R^T P^T. Owned matrices keep published maps independent of later mesh changes. + */ + class ConstrainedRadialExtension { + public: + using InputSpace = RadialExtension::InputSpace; + using OutputSpace = RadialExtension::OutputSpace; + static constexpr bool is_linear = true; + + /** @brief Reserve separate local and independent vectors for each caller's actions. */ + struct Workspace { + ::mfem::Vector local; + ::mfem::Vector independent; + }; + + /** @brief Expose the same immutable radial table as the unmodified physical extension. */ + [[nodiscard]] const std::shared_ptr& interpolation() const noexcept { + return m_radial.interpolation(); + } + + /** @brief Expose the regional radial response before imposing finite-element continuity. */ + [[nodiscard]] std::span radial_weights() const noexcept { + return m_radial.radial_weights(); + } + + /** @brief Return the interleaved Cartesian surface displacement size. */ + [[nodiscard]] std::size_t input_size() const noexcept { + return m_radial.input_size(); + } + + /** @brief Retain local component-block output for the existing deformation consumers. */ + [[nodiscard]] std::size_t output_size() const noexcept { + return m_radial.output_size(); + } + + /** @brief Allocate constraint scratch once, leaving conforming actions allocation-free. */ + [[nodiscard]] Workspace make_workspace() const; + + /** @brief Apply radial motion with temporary constraint storage, preserving the two-argument calling form. */ + [[nodiscard]] DeformationStatus apply(std::span surface, std::span volume) const; + + /** @brief Pull back volume work with temporary constraint storage, preserving the two-argument calling form. */ + [[nodiscard]] DeformationStatus apply_transpose(std::span volume_force, std::span surface_force) const; + + /** @brief Sample radial motion and reconstruct its hanging coefficients from independent values. */ + [[nodiscard]] DeformationStatus apply(std::span surface, std::span volume, Workspace& workspace) const; + + /** @brief Pull back volume work through the exact transpose of the constrained extension. */ + [[nodiscard]] DeformationStatus apply_transpose(std::span volume_force, std::span surface_force, Workspace& workspace) const; + + private: + /** @brief Retain validated radial physics and optional owned finite-element constraints. */ + ConstrainedRadialExtension(RadialExtension radial, std::unique_ptr prolongation, std::unique_ptr restriction); + + /** @brief Reject scratch from another map or buffers that alias caller-owned action data. */ + [[nodiscard]] DeformationStatus validate_workspace(const Workspace& workspace, std::span input, std::span output) const; + + friend std::expected make_constrained_radial_extension( + std::shared_ptr, const ::mfem::FiniteElementSpace&, PowerLawRadialExtension, FixedInfinityRadialExtension); + + RadialExtension m_radial; + std::unique_ptr m_prolongation; + std::unique_ptr m_restriction; + }; + + /** @brief Compile the original radial extension and the matching serial component-block constraints. */ + [[nodiscard]] std::expected make_constrained_radial_extension( + std::shared_ptr interpolation, + const ::mfem::FiniteElementSpace& displacement_space, + PowerLawRadialExtension interior = {}, + FixedInfinityRadialExtension vacuum = {}); +} // namespace serif::geometry::deformation::backend::mfem + +namespace serif::geometry::deformation::backend::mfem { + inline ConstrainedRadialExtension::ConstrainedRadialExtension(RadialExtension radial, + std::unique_ptr prolongation, std::unique_ptr restriction) : + m_radial(std::move(radial)), m_prolongation(std::move(prolongation)), m_restriction(std::move(restriction)) {} + + inline ConstrainedRadialExtension::Workspace ConstrainedRadialExtension::make_workspace() const { + if (!m_prolongation) { + return {}; + } + return {.local = ::mfem::Vector(static_cast(output_size())), .independent = ::mfem::Vector(m_restriction->Height())}; + } + + inline DeformationStatus ConstrainedRadialExtension::apply(std::span surface, std::span volume) const { + Workspace workspace = make_workspace(); + return apply(surface, volume, workspace); + } + + inline DeformationStatus ConstrainedRadialExtension::apply_transpose(std::span volume_force, std::span surface_force) const { + Workspace workspace = make_workspace(); + return apply_transpose(volume_force, surface_force, workspace); + } + + inline DeformationStatus ConstrainedRadialExtension::validate_workspace(const Workspace& workspace, + std::span input, std::span output) const { + if (workspace.local.Size() != static_cast(output_size()) || workspace.independent.Size() != m_restriction->Height()) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_size, .message = "Constrained radial action requires scratch allocated for this extension."}); + } + const std::span local(workspace.local.GetData(), workspace.local.Size()); + const std::span independent(workspace.independent.GetData(), workspace.independent.Size()); + if (const DeformationStatus status = deformation::detail::validate_disjoint_output(local, {input, output, independent}); !status) { + return status; + } + return deformation::detail::validate_disjoint_output(independent, {input, output}); + } + + inline DeformationStatus ConstrainedRadialExtension::apply(std::span surface, + std::span volume, Workspace& workspace) const { + if (!m_prolongation) { + return m_radial.apply(surface, volume); + } + if (const DeformationStatus status = deformation::detail::validate_action_arguments(volume, output_size(), {{surface, input_size()}}); !status) { + return status; + } + if (const DeformationStatus status = validate_workspace(workspace, surface, volume); !status) { + return status; + } + if (const DeformationStatus status = m_radial.apply(surface, {workspace.local.GetData(), static_cast(workspace.local.Size())}); !status) { + return status; + } + m_restriction->Mult(workspace.local, workspace.independent); + ::mfem::Vector output(volume.data(), static_cast(volume.size())); + m_prolongation->Mult(workspace.independent, output); + return validate_deformation_values(volume, output_size()); + } + + inline DeformationStatus ConstrainedRadialExtension::apply_transpose(std::span volume_force, + std::span surface_force, Workspace& workspace) const { + if (!m_prolongation) { + return m_radial.apply_transpose(volume_force, surface_force); + } + if (const DeformationStatus status = deformation::detail::validate_action_arguments(surface_force, input_size(), {{volume_force, output_size()}}); !status) { + return status; + } + if (const DeformationStatus status = validate_workspace(workspace, volume_force, surface_force); !status) { + return status; + } + // MFEM's vector view requires mutable storage; matrix application only reads this force. + const ::mfem::Vector input(const_cast(volume_force.data()), static_cast(volume_force.size())); + m_prolongation->MultTranspose(input, workspace.independent); + m_restriction->MultTranspose(workspace.independent, workspace.local); + return m_radial.apply_transpose({workspace.local.GetData(), static_cast(workspace.local.Size())}, surface_force); + } + + inline std::expected make_constrained_radial_extension( + std::shared_ptr interpolation, const ::mfem::FiniteElementSpace& displacement_space, + PowerLawRadialExtension interior, FixedInfinityRadialExtension vacuum) { +#ifdef MFEM_USE_MPI + if (dynamic_cast(&displacement_space) != nullptr) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Constrained radial extension requires a serial displacement space."}); + } +#endif + auto radial = make_radial_extension(std::move(interpolation), interior, vacuum); + if (!radial) { + return std::unexpected(radial.error()); + } + if (displacement_space.GetVDim() != 3 || displacement_space.GetOrdering() != ::mfem::Ordering::byNODES || + displacement_space.GetVSize() != static_cast(radial->output_size())) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Constrained radial extension requires its three-component reference space in component-block ordering."}); + } + const ::mfem::SparseMatrix* prolongation = displacement_space.GetConformingProlongation(); + if (prolongation == nullptr) { + return ConstrainedRadialExtension(std::move(*radial), nullptr, nullptr); + } + const ::mfem::SparseMatrix* restriction = displacement_space.GetConformingRestriction(); + if (displacement_space.IsVariableOrder() || restriction == nullptr || prolongation->Height() != displacement_space.GetVSize() || + prolongation->Width() != displacement_space.GetTrueVSize() || restriction->Height() != prolongation->Width() || + restriction->Width() != prolongation->Height()) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Constrained radial extension requires fixed-order serial prolongation and matching coefficient restriction matrices."}); + } + return ConstrainedRadialExtension(std::move(*radial), std::make_unique(*prolongation), + std::make_unique(*restriction)); + } +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/include/serif/backend/mfem/geometry/deformation/detail/radial_locator.hpp b/src/include/serif/backend/mfem/geometry/deformation/detail/radial_locator.hpp new file mode 100644 index 00000000..a82f0864 --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/detail/radial_locator.hpp @@ -0,0 +1,79 @@ +#pragma once + +/** + * @file radial_locator.hpp + * @author Emily M. Boudreaux + * + * @brief File containing some utility functions for locating points on a radial shell. + */ + +#include "mfem/fem/intrules.hpp" +#include "mfem/linalg/vector.hpp" +#include "serif/geometry/deformation/failure.hpp" + +namespace mfem { + /** + * @brief Forward definition of the mfem::Mesh class to avoid having to parse the entire mfem mesh header file. + */ + class Mesh; +} + +namespace serif::geometry::deformation::backend::mfem::detail { + /** @struct LocatedBoundaryPoint + * + * @brief A structure representing a located boundary point, including the boundary element index and the integration point on that element. + */ + struct LocatedBoundaryPoint { + int boundary_element; ///< The index of the boundary element where the point is located. + ::mfem::IntegrationPoint point; ///< The integration point on the boundary element where the point is located. + }; + + /** + * @brief compute the cubical local radius of a given position vector. Alternative put: + * compute the \f$L\infty\f$ norm of the position vector (i.e. the maximum absolute value of its components) + * + * @param position The position vector for which to compute the logical radius. + * @returns The logical radius of the position vector, defined as the maximum absolute value of its components (i.e., the L\f$\infty\f$ norm). + */ + [[nodiscard]] double logical_radius(const ::mfem::Vector& position); + + + /** + * @brief Compute the logical radius of a boundary shell in a mesh, given the mesh and the boundary attribute. + * + * @param mesh The mesh containing the boundary shell. + * @param attribute The boundary attribute identifying the shell to compute the logical radius for. + * + * @returns The logical radius of the boundary shell if successful, or a DeformationFailure object if an error occurs (e.g., incompatible discretization, non-finite vertex, or invalid geometry). + * + * @par Explanation + * The algorithm here is as follows + * 1. Loop over all boundary elements. validate they they have the correct attribute id (if not skip them) + * 2. Get the geometry type of the element and validate that it has a valid transformation and is a square (which for mfem really means a quadrilateral). If not return an error + * 3. Get the vertices of the element and loop over them. For each vertex + * a. Transform the vertex into a position vector + * b. validate that each component of the position vector is finite. If not return an error + * c. Compute the logical radius of the position vector (L\f$\infty\f$ norm) and update the minimum and maximum logical radius values seen so far + * d. Increment the number of samples seen so far + * 4. validate that at least one sample was seen, that the minimum values is positive, and that are at least separated by 1e-12 * max(1, maximum). If not return an error + * 5. Return the average of the minimum and maximum logical radius values seen. + * + * Effectively then what this does is find an estimate for the radius of some enclosing surface over the boundary elements. + */ + [[nodiscard]] std::expected boundary_logical_radius(::mfem::Mesh& mesh, int attribute); + + /** + * @brief Locate a projected logical ray on a quadrilateral shell. + * + * @param mesh The mesh containing the boundary shell. + * @param attribute The boundary attribute identifying the shell to locate the point on. + * @param target The target position vector to locate on the boundary shell. + * + * @returns A LocatedBoundaryPoint object representing the located boundary point if successful, or a DeformationFailure object if an error occurs + */ + [[nodiscard]] std::expected locate_boundary_point( + ::mfem::Mesh& mesh, + int attribute, + const ::mfem::Vector& target + ); +} diff --git a/src/include/serif/backend/mfem/geometry/deformation/displacement_mapping.hpp b/src/include/serif/backend/mfem/geometry/deformation/displacement_mapping.hpp new file mode 100644 index 00000000..f9da4f2c --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/displacement_mapping.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include "serif/backend/mfem/discretization/dof_map.hpp" + +namespace serif::geometry::deformation::backend::mfem { + /** + * @brief Visit the same displacement coordinate mapping for values and their derivatives. + * + * Deformation maps use full component blocks, whereas equation bindings use supported + * DOFs in their MFEM space's ordering. Each supported coefficient has one source in + * the full deformation vector. Visiting these pairs also defines the exact transpose + * and the sparse projection without reproducing ordering or support logic. + * @param binding A current, validated displacement binding. + * @param visit Receives the supported row and full component-block column. + */ + template + void for_each_displacement_coefficient(const discretization::backend::mfem::DofMap& binding, Function&& visit) { + const ::mfem::FiniteElementSpace& space = binding.space(); + const int nodes = space.GetNDofs(); + if (binding.constrained()) { + // Restriction selects one local representative per independent coefficient. + // Visiting every identity row of prolongation could count the same coefficient twice. + for (int row = 0; row < binding.size(); ++row) { + const int local = binding.full_dof(row); + const int component = space.GetOrdering() == ::mfem::Ordering::byNODES ? local / nodes : local % space.GetVDim(); + const int node = space.GetOrdering() == ::mfem::Ordering::byNODES ? local % nodes : local / space.GetVDim(); + visit(row, node + component * nodes); + } + return; + } + for (int component = 0; component < space.GetVDim(); ++component) { + for (int node = 0; node < nodes; ++node) { + const std::optional row = binding.reduced_dof(space.DofToVDof(node, component)); + if (row) { + visit(*row, node + component * nodes); + } + } + } + } +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/include/serif/backend/mfem/geometry/deformation/geometry_step.hpp b/src/include/serif/backend/mfem/geometry/deformation/geometry_step.hpp new file mode 100644 index 00000000..d0e9149c --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/geometry_step.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +#include "serif/geometry/deformation/failure.hpp" +#include "serif/geometry/deformation/geometry_step.hpp" + +namespace mfem { + class FiniteElementSpace; + class GridFunction; + class IntegrationRule; + class Vector; +} + +namespace serif::mapping { + class DomainMapper; + struct DMFixedMeshGeometry; +} + +namespace serif::geometry::deformation::backend::mfem { + /** + * @brief Borrow one sampling rule; repeat an element to retain the actual union of downstream rules. + * @note reference_geometry optionally holds sample_fixed_mesh_geometry for every point of this rule on + * the unchanged reference mesh. The reference mesh never moves during a solve, so a caller that already + * owns these samples (the volume equations' Basis nodes) can share them instead of having each step + * limit recompute the high-order mesh Jacobian. Without it, or if its size does not match, samples + * are computed on demand exactly as before. + */ + struct GeometrySampleRule { + int element; + const ::mfem::IntegrationRule* integration_rule; + std::shared_ptr> reference_geometry{}; + }; + + /** + * @brief Find the first sampled mapping boundary along accepted + step direction. + * + * Inputs use the displacement space's true-DOF ordering. The compactification coordinate is fixed + * throughout this straight volume-displacement path. An exterior map must be affine in displacement. + * The result covers exactly the supplied rules, including repeats: quadrature rules need not be nested. + * Only serial spaces are supported. Parallel spaces are rejected before any mapping is evaluated. + */ + [[nodiscard]] std::expected estimate_geometry_step_limit( + const mapping::DomainMapper& mapper, + const ::mfem::FiniteElementSpace& displacement_space, + const ::mfem::GridFunction& compactification_coordinate, + const ::mfem::Vector& accepted_displacement, + const ::mfem::Vector& displacement_direction, + std::span sample_rules, + GeometryStepOptions options = {} + ); +} diff --git a/src/include/serif/backend/mfem/geometry/deformation/inputs.hpp b/src/include/serif/backend/mfem/geometry/deformation/inputs.hpp new file mode 100644 index 00000000..64cf76ea --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/inputs.hpp @@ -0,0 +1,157 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "mfem/fem/fespace.hpp" +#include "serif/discretization/domain/schema/schemas.hpp" +#include "serif/discretization/domain/schema/concepts.hpp" +#include "serif/geometry/deformation/models.hpp" +#include "serif/backend/mfem/shared/reference_tracking.hpp" +#include "serif/geometry/deformation/computations.hpp" +#include "serif/utils/dependencies/dependency_context.hpp" + +namespace serif::geometry::deformation::backend::mfem { + /** @brief Borrow the paired reference meshes; attributes retain their physical domain meaning. */ + struct ReferenceDiscretization { + const ::mfem::FiniteElementSpace& scalar_space; + const ::mfem::FiniteElementSpace& displacement_space; + ::mfem::Mesh& logical_mesh; + std::vector stellar_attributes; + std::vector vacuum_attributes; + int surface_attribute; + int infinity_attribute; + }; + + /** @brief The surface selection needs only its scalar space and boundary label. */ + struct SurfaceDiscretization { + const ::mfem::FiniteElementSpace& scalar_space; + int surface_attribute; + }; + + /** @brief Equal physical values remain reusable when a context is reconstructed. */ + struct DeformationValueTracking : utils::dependencies::ValueSnapshot { + /** @brief Wrapper addresses do not identify the underlying physical input. */ + template + [[nodiscard]] const void* identity(const Value&) const noexcept { + return nullptr; + } + }; + + /** @brief Capture the values behind a shape view, independently of its temporary wrapper. */ + struct ShapeCoefficientTracking : DeformationValueTracking { + /** @brief The cache must retain values rather than another view of mutable state. */ + [[nodiscard]] std::vector capture(std::span coefficients) const { + return {coefficients.begin(), coefficients.end()}; + } + + /** @brief Rebinding equal values reuses work; any changed coefficient invalidates it. */ + [[nodiscard]] bool matches(const std::vector& captured, std::span coefficients) const { + return std::ranges::equal(captured, coefficients); + } + }; + + /** @brief Track topology and labels once, independently of changes to physical coordinates. */ + struct SurfaceDiscretizationTracking : DeformationValueTracking { + struct Snapshot { + ::serif::backend::mfem::shared::ReferenceMeshSnapshot mesh; + ::serif::backend::mfem::shared::ReferenceSpaceSnapshot space; + int surface_attribute; + }; + + /** @brief Capture exactly the metadata that determines the canonical scalar surface nodes. */ + [[nodiscard]] Snapshot capture(const SurfaceDiscretization& input) const; + /** @brief Compare mutable boundary labels without allocating a second snapshot. */ + [[nodiscard]] bool matches(const Snapshot& snapshot, const SurfaceDiscretization& input) const; + }; + + /** @brief Logical interpolation depends on paired topology and logical coordinates, not physical coordinates. */ + struct ReferenceDiscretizationTracking : DeformationValueTracking { + struct Snapshot { + std::array<::serif::backend::mfem::shared::ReferenceMeshSnapshot, 2> meshes; + std::array<::serif::backend::mfem::shared::ReferenceSpaceSnapshot, 2> spaces; + std::vector stellar_attributes; + std::vector vacuum_attributes; + int surface_attribute; + int infinity_attribute; + }; + + /** @brief Capture each paired mesh once; space metadata retains both companion identities. */ + [[nodiscard]] Snapshot capture(const ReferenceDiscretization& input) const; + /** @brief Compare exact topology, numbering, logical coordinates and material selections. */ + [[nodiscard]] bool matches(const Snapshot& snapshot, const ReferenceDiscretization& input) const; + }; + + /** @brief Resolve physical domain names through the schema rather than hard-coded MFEM attributes. */ + template + requires(Schema::template contains_domain() && Schema::template contains_domain() && Schema::template contains_boundary() && Schema::template contains_boundary()) + [[nodiscard]] ReferenceDiscretization deformation_discretization(const ::mfem::FiniteElementSpace& scalar_space, const ::mfem::FiniteElementSpace& displacement_space, ::mfem::Mesh& logical_mesh) { + ReferenceDiscretization result{ + .scalar_space = scalar_space, + .displacement_space = displacement_space, + .logical_mesh = logical_mesh, + .stellar_attributes = {}, + .vacuum_attributes = {}, + .surface_attribute = Schema::template boundary_id(), + .infinity_attribute = Schema::template boundary_id()}; + for (const auto& domain : Schema::domain_descriptors()) { + if (Schema::template domain_id_belongs_to(domain.ID)) { + result.stellar_attributes.push_back(domain.ID); + } + if (Schema::template domain_id_belongs_to(domain.ID)) { + result.vacuum_attributes.push_back(domain.ID); + } + } + return result; + } + + namespace detail { + /** @brief Share physical input construction between borrowed containers and state views. */ + template + [[nodiscard]] auto deformation_inputs(const DomainDeformation& model, const ReferenceDiscretization& discretization, ShapeResource coefficients) { + using namespace utils::dependencies; + + return make_dependency_context( + own(discretization, ReferenceDiscretizationTracking{}), + own(SurfaceDiscretization{.scalar_space = discretization.scalar_space, .surface_attribute = discretization.surface_attribute}, SurfaceDiscretizationTracking{}), + borrow(discretization.scalar_space, track_by([](const ::mfem::FiniteElementSpace& space) { + const ::mfem::Mesh* mesh = space.GetMesh(); + return std::pair{mesh, mesh ? mesh->GetNodesSequence() : -1L}; + })), + own(model.surface, DeformationValueTracking{}), + own(model.stellar_interior, DeformationValueTracking{}), + own(model.vacuum, DeformationValueTracking{}), + std::move(coefficients)); + } + } // namespace detail + + /** + * @brief Bind physical choices and a borrowed shape vector without evaluating deformation. + * @note Meshes, spaces, and the vector must outlive the context. Borrowing the container + * retains its current storage and size even if the caller subsequently resizes it. + * Coordinate edits must notify MFEM through its normal node-update API. + */ + template + [[nodiscard]] auto make_deformation_inputs(const DomainDeformation& model, const ReferenceDiscretization& discretization, const std::vector& coefficients) { + return detail::deformation_inputs(model, discretization, utils::dependencies::borrow(coefficients, DeformationValueTracking{})); + } + + /** + * @brief Bind shape coordinates directly from a packed numerical state. + * @note The context owns the view, not its storage. The underlying coefficients must + * remain alive at a stable address while the context is used. Published linearizations + * own their coefficient values and remain valid after subsequent state changes. + */ + template + [[nodiscard]] auto make_deformation_inputs(const DomainDeformation& model, const ReferenceDiscretization& discretization, std::span coefficients) { + return detail::deformation_inputs(model, discretization, utils::dependencies::own(coefficients, ShapeCoefficientTracking{})); + } + + /** @brief Reject temporary owning containers whose coefficient storage would expire. */ + template + requires (!std::ranges::borrowed_range) + void make_deformation_inputs(const DomainDeformation&, const ReferenceDiscretization&, Coefficients&&) = delete; +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/include/serif/backend/mfem/geometry/deformation/radial_interpolation.hpp b/src/include/serif/backend/mfem/geometry/deformation/radial_interpolation.hpp new file mode 100644 index 00000000..33c84739 --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/radial_interpolation.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include "serif/geometry/deformation/radial_extension.hpp" +#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp" + +namespace serif::geometry::deformation::backend::mfem +{ + /** + * @brief Compile logical-ray interpolation on paired physical and logical meshes. + * Supports are zero/one element masks. The logical radius is the maximum absolute + * Cartesian coordinate, preserving the STROID reference foliation after physical distortion. + * Serial nodal spaces may have hanging nodes away from the conforming stellar surface. + * The table samples local coefficients; the backend extension enforces their constraints. + */ + [[nodiscard]] std::expected build_radial_interpolation( + const ::mfem::FiniteElementSpace& scalar_space, + const ::mfem::FiniteElementSpace& vector_space, + ::mfem::Mesh& logical_mesh, + std::span stellar_element_support, + std::span vacuum_element_support, + int surface_attribute, + int infinity_attribute); + + namespace detail + { + /** @brief Reuse the validated scalar space and surface selection supplied by the reference graph. */ + [[nodiscard]] std::expected build_radial_interpolation(const ::mfem::FiniteElementSpace& scalar_space, const ::mfem::FiniteElementSpace& vector_space, ::mfem::Mesh& logical_mesh, std::span stellar_element_support, std::span vacuum_element_support, int surface_attribute, int infinity_attribute, const std::vector& selected); + } // namespace detail +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/include/serif/backend/mfem/geometry/deformation/reference_geometry.hpp b/src/include/serif/backend/mfem/geometry/deformation/reference_geometry.hpp new file mode 100644 index 00000000..d5d0b8fc --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/reference_geometry.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include "serif/geometry/deformation/failure.hpp" +#include "serif/geometry/deformation/reference_surface.hpp" +#include "serif/geometry/deformation/surface_embedding.hpp" + +namespace mfem +{ + class FiniteElementSpace; + class Mesh; +} // namespace mfem + +namespace serif::geometry::deformation::backend::mfem +{ + /** @brief Reject spaces whose coefficients cannot be interpreted as serial nodal positions. */ + [[nodiscard]] DeformationStatus validate_scalar_reference_space(const ::mfem::FiniteElementSpace& scalar_space); + + /** + * @brief Require identical scalar coefficient meanings in two companion spaces. + * Vector component ordering may differ; element bases and their scalar-node indices must agree. + */ + [[nodiscard]] DeformationStatus validate_companion_numbering( + const ::mfem::FiniteElementSpace& reference_space, + const ::mfem::FiniteElementSpace& companion_space); + + /** @brief Sample a paired mesh at the scalar companion nodes, checking shared-node agreement. */ + [[nodiscard]] std::expected>, DeformationFailure> sample_reference_positions( + const ::mfem::FiniteElementSpace& scalar_space, + ::mfem::Mesh& reference_mesh); + + /** @brief Select canonical surface nodes without retaining a mesh or finite-element-space pointer. */ + [[nodiscard]] std::expected, DeformationFailure> select_surface_scalar_dofs( + const ::mfem::FiniteElementSpace& scalar_space, + int surface_attribute); + + /** @brief Sample physical reference positions in ascending scalar-DOF order. */ + [[nodiscard]] std::expected sample_reference_surface( + const ::mfem::FiniteElementSpace& scalar_space, + int surface_attribute); + + /** @brief Require the same ordered reference nodes and physical positions as a compiled surface embedding. */ + [[nodiscard]] DeformationStatus validate_surface_trace(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding); + + namespace detail + { + /** @brief Sample paired coordinates after the scalar node-selection stage has validated the space. */ + [[nodiscard]] std::expected>, DeformationFailure> sample_reference_positions(const ::mfem::FiniteElementSpace& scalar_space, ::mfem::Mesh& reference_mesh); + /** @brief Reuse the graph's canonical surface nodes when only physical coordinates changed. */ + [[nodiscard]] std::expected sample_reference_surface(const ::mfem::FiniteElementSpace& scalar_space, const std::vector& selected); + } // namespace detail +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/include/serif/backend/mfem/geometry/deformation/surface_metric.hpp b/src/include/serif/backend/mfem/geometry/deformation/surface_metric.hpp new file mode 100644 index 00000000..9f3f3537 --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/deformation/surface_metric.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include "serif/geometry/deformation/failure.hpp" +#include "serif/geometry/deformation/reference_surface.hpp" +#include "serif/geometry/deformation/surface_embedding.hpp" + +namespace mfem +{ + class FiniteElementSpace; +} + +namespace serif::geometry::deformation::backend::mfem +{ + /** + * @brief Measure each shape parameter with the reference boundary mass norm of its Cartesian finite-element trace. + * @note This is diag(S^T (M_boundary tensor I) S), independent of the interior/vacuum extension and of the current shape coefficients. It uses the mass integrator's standard quadrature and does not orthogonalize harmonic coordinates. + */ + [[nodiscard]] std::expected, DeformationFailure> surface_parameter_mass_diagonal(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding); + + namespace detail + { + /** @brief Assemble from the current reference trace and its embedding, already paired by the dependency graph. */ + [[nodiscard]] std::expected, DeformationFailure> surface_parameter_mass_diagonal(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding); + } // namespace detail +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/include/serif/backend/mfem/geometry/surface/boundary_computations.hpp b/src/include/serif/backend/mfem/geometry/surface/boundary_computations.hpp new file mode 100644 index 00000000..959e9058 --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/surface/boundary_computations.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "serif/backend/mfem/geometry/surface/boundary_inputs.hpp" +#include "serif/geometry/surface/computations.hpp" +#include "serif/backend/mfem/shared/finite.hpp" + +namespace serif::geometry::surface::backend::mfem +{ + namespace detail + { + /** @brief Sample the same fixed linear trace used by the boundary tangent and its transpose. */ + template + [[nodiscard]] std::expected, SurfaceFailure> sample_boundary_values(const Boundary& boundary, const Input& input) + { + using namespace utils::errors; + + const auto& trace = boundary.traces.template get(); + serif_ensure(&trace.binding() == &input.binding, "Pressure-boundary coefficients must use the binding that compiled their trace."); + serif_ensure(input.values.Size() == trace.binding().size(), "Pressure-boundary coefficients must match the supported field size."); + if (!utils::checks::is_finite(input.values)) + { + return std::unexpected(SurfaceFailure{.reason = SurfaceFailureReason::non_finite_state, .message = "A pressure-boundary field contains a non-finite coefficient."}); + } + BoundaryValues result{.values = std::vector(boundary.selected_rows->size())}; + ::mfem::Vector values(result.values.data(), static_cast(result.values.size())); + trace.interpolation().Mult(input.values, values); + if (!utils::checks::is_finite(values)) + { + return std::unexpected(SurfaceFailure{.reason = SurfaceFailureReason::non_finite_state, .message = "A finite-element boundary trace produced a non-finite value."}); + } + return result; + } + + /** @brief Declare one sampled value column per unique physical term, without a separate preparation owner. */ + template + [[nodiscard]] auto pressure_boundary_computations(utils::dependencies::KeyList) + { + using namespace utils::dependencies; + + const auto sample = []() + { + return make_computation, InputList>( + [](const auto& boundary, const auto& input) + { + static_assert(std::same_as::Formulation, Formulation>, "The boundary graph must use the formulation that compiled its traces."); + return sample_boundary_values(*boundary, input); + }); + }; + return std::tuple_cat(std::tuple{sample.template operator()()...}, make_pressure_surface_computations()); + } + } + + /** @brief Combine finite-element sampling with the unchanged thermodynamic graph; coefficient sources can also feed volume equations. */ + template + [[nodiscard]] auto make_pressure_boundary_computations() + { + return detail::pressure_boundary_computations(typename Formulation::StateTerms{}); + } + + /** @brief Construct a standalone isobaric boundary graph with the ordinary dependency-cache lifecycle. */ + template + [[nodiscard]] auto make_pressure_boundary_graph() + { + return std::apply([](auto... computations) + { + return utils::dependencies::make_computation_graph(std::move(computations)...); + }, make_pressure_boundary_computations()); + } +} diff --git a/src/include/serif/backend/mfem/geometry/surface/boundary_data.hpp b/src/include/serif/backend/mfem/geometry/surface/boundary_data.hpp new file mode 100644 index 00000000..10357de6 --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/surface/boundary_data.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include + +#include "serif/backend/mfem/discretization/boundary_trace.hpp" +#include "serif/geometry/surface/compilation.hpp" +#include "serif/backend/mfem/discretization/term_coefficients.hpp" + +namespace serif::geometry::surface::backend::mfem +{ + namespace detail + { + /** @brief Strong nodal rows need a unique trace for every thermodynamic input. */ + template + [[nodiscard]] consteval bool supported_boundary_terms(utils::dependencies::KeyList) + { + return (discretization::backend::mfem::BoundaryTraceTerm && ...); + } + } + + /** @brief Isobaric rows are scalar nodal H1 equations with continuous scalar or global EOS inputs. */ + template + concept PressureBoundaryFormulation = PressureSurfaceFormulationType && + discretization::blocks::SpatialTerm && + discretization::blocks::SpatialTermTraits::family == discretization::functional_space::Family::h1 && + detail::supported_boundary_terms(typename Formulation::StateTerms{}); + + /** + * @brief The selected equation rows and the finite-element traces supplying their physical state. + * @note Bindings and their meshes must outlive this object. EOS values and readiness belong to the dependency cache. + */ + template + struct PressureBoundary + { + using Formulation = FormulationT; + std::shared_ptr selected_rows; + Traces traces; + + /** @brief Reject stale bindings and selections even when all coefficient values are unchanged. */ + void validate() const + { + using namespace utils::errors; + + serif_ensure(selected_rows != nullptr, "An isobaric boundary requires a row selection."); + const auto validate_traces = [&](utils::dependencies::KeyList) + { + (traces.template get().validate(), ...); + serif_ensure(((&traces.template get().rows() == selected_rows.get()) && ...), "Every pressure-boundary trace must sample the same ordered equation rows."); + }; + validate_traces(typename Formulation::StateTerms{}); + } + }; + + namespace detail + { + /** @brief Compile each unique field once, including quantities that share one coefficient term. */ + template + [[nodiscard]] auto make_pressure_boundary(const Coefficients& coefficients, utils::dependencies::KeyList) + { + using namespace utils::dependencies; + using namespace discretization::backend::mfem; + using RowBinding = std::remove_cvref_t().binding)>; + + static_assert((std::same_as().binding)>::Schema> && ...), "Pressure-boundary fields must share a domain schema."); + const auto& row_binding = coefficients.template get().binding; + const auto rows = std::make_shared(select_boundary_dofs(row_binding)); + auto traces = make_dependency_context(own(make_boundary_trace(rows, coefficients.template get().binding))...); + using Boundary = PressureBoundary; + return std::make_shared(Boundary{.selected_rows = rows, .traces = std::move(traces)}); + } + } + + /** @brief Compile nodal boundary traces explicitly; this does not sample coefficients or evaluate the EOS. */ + template + requires utils::dependencies::SatisfiesKeyList + [[nodiscard]] auto make_pressure_boundary(const Coefficients& coefficients) + { + return detail::make_pressure_boundary(coefficients, typename Formulation::StateTerms{}); + } +} diff --git a/src/include/serif/backend/mfem/geometry/surface/boundary_inputs.hpp b/src/include/serif/backend/mfem/geometry/surface/boundary_inputs.hpp new file mode 100644 index 00000000..e4f93b45 --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/surface/boundary_inputs.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include "serif/backend/mfem/discretization/coefficient_tracking.hpp" +#include "serif/backend/mfem/geometry/surface/boundary_data.hpp" +#include "serif/geometry/surface/samples.hpp" + +namespace serif::geometry::surface::backend::mfem +{ + namespace keys + { + struct BoundaryDiscretization + { + }; + + /** @brief Keep sampled boundary state distinct from volume coefficient sources for the same term. */ + template + struct BoundarySamples + { + }; + } + + namespace detail + { + /** @brief Shared traces have stable identity; validation catches changes outside the coefficient vectors. */ + struct BoundaryTracking + { + /** @brief Validate the borrowed discretization and retain its trace owner so an old address cannot identify a new boundary. */ + template + [[nodiscard]] std::shared_ptr capture(const std::shared_ptr& boundary) const + { + using namespace utils::errors; + + serif_ensure(boundary != nullptr, "Pressure-boundary inputs require compiled boundary traces."); + boundary->validate(); + return boundary; + } + + /** @brief Validate even unchanged inputs, so stale selections cannot remain publicly ready. */ + template + [[nodiscard]] bool matches(const std::shared_ptr& captured, const std::shared_ptr& boundary) const + { + return captured == capture(boundary); + } + + /** @brief Moving a context does not move the compiled boundary it owns. */ + template + [[nodiscard]] const void* identity(const std::shared_ptr& boundary) const + { + return boundary.get(); + } + }; + + } + + /** @brief Share boundary metadata with a model's common coefficient context. */ + template + [[nodiscard]] auto make_pressure_boundary_metadata(std::shared_ptr boundary, EOSResource equation_of_state, IsobaricSurface condition) + { + using namespace utils::dependencies; + + return make_dependency_context(own(std::move(boundary), detail::BoundaryTracking{}), + std::move(equation_of_state), pressure_condition(std::move(condition))); + } + + namespace detail + { + /** @brief Preserve standard coefficient keys and policies in a standalone boundary context. */ + template + [[nodiscard]] auto pressure_boundary_inputs(std::shared_ptr boundary, EOSResource equation_of_state, IsobaricSurface condition, const Coefficients& coefficients, utils::dependencies::KeyList) + { + using namespace utils::dependencies; + + return combine_dependency_contexts(make_pressure_boundary_metadata(std::move(boundary), std::move(equation_of_state), std::move(condition)), + make_dependency_context(own(coefficients.template get(), discretization::backend::mfem::CoefficientTracking{})...)); + } + } + + /** + * @brief Bind the physical condition, explicitly tracked EOS, and borrowed field coefficients without evaluating them. + * @note Inputs may be reused while coefficient vectors change. The caller's bindings, vectors and borrowed EOS must remain alive. + */ + template + requires std::same_as && + std::same_as, typename Boundary::Formulation::EquationOfState> && + utils::dependencies::SatisfiesKeyList + [[nodiscard]] auto make_pressure_boundary_inputs(std::shared_ptr boundary, EOSResource equation_of_state, IsobaricSurface condition, const Coefficients& coefficients) + { + return detail::pressure_boundary_inputs(std::move(boundary), std::move(equation_of_state), std::move(condition), coefficients, typename Boundary::Formulation::StateTerms{}); + } +} diff --git a/src/include/serif/backend/mfem/geometry/surface/boundary_rows.hpp b/src/include/serif/backend/mfem/geometry/surface/boundary_rows.hpp new file mode 100644 index 00000000..0b7d3dbd --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/surface/boundary_rows.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include "serif/utils/error/nonfinite.hpp" + +#include +#include + +#include "serif/backend/mfem/geometry/surface/boundary_data.hpp" +#include "serif/geometry/surface/linearization.hpp" +#include "serif/backend/mfem/shared/finite.hpp" + +namespace serif::geometry::surface::backend::mfem +{ + namespace detail + { + /** @brief Check that an action uses the current discretization and the complete residual space. */ + template + void validate_boundary_output(const Boundary& boundary, const ::mfem::Vector& result) + { + using namespace utils::errors; + + boundary.validate(); + serif_ensure(result.Size() == boundary.selected_rows->binding().size(), "Pressure-boundary output must match the residual binding size."); + } + + /** @brief The accepted graph result contains one local equation for each ordered boundary node. */ + template + void validate_boundary_samples(const Boundary& boundary, const Linearizations& linearizations) + { + using namespace utils::errors; + + serif_ensure(linearizations.size() == static_cast(boundary.selected_rows->size()), "Pressure-boundary linearizations must match the compiled row selection."); + } + + /** @brief Publish a complete finite action only after all input directions have been read. */ + template + void replace_boundary_rows(const Boundary& boundary, const ::mfem::Vector& values, ::mfem::Vector& result) + { + using namespace utils::errors; + + serif_ensure(utils::checks::is_finite(values), "The pressure-boundary action produced a non-finite value; reject this candidate."); + const auto& rows = boundary.selected_rows->reduced_dofs(); + for (int index = 0; index < rows.Size(); ++index) + { + result[rows[index]] = values[index]; + } + } + + /** @brief Accumulate all physical couplings using the same traces that supplied the EOS state. */ + template + [[nodiscard]] ::mfem::Vector boundary_jacobian_action(const Boundary& boundary, const Linearizations& linearizations, const Directions& directions, utils::dependencies::KeyList) + { + using namespace utils::errors; + + ::mfem::Vector result(boundary.selected_rows->size()); + ::mfem::Vector samples(result.Size()); + result = 0.0; + const auto add_direction = [&]() + { + const auto& trace = boundary.traces.template get(); + const auto& direction = directions.template get(); + serif_ensure(&direction.binding == &trace.binding(), "A pressure-boundary direction must use the binding that compiled its trace."); + trace.binding().validate_values(direction.values); + trace.interpolation().Mult(direction.values, samples); + for (int index = 0; index < result.Size(); ++index) + { + result[index] += linearizations[index].field_derivatives.template get() * samples[index]; + } + }; + (add_direction.template operator()(), ...); + return result; + } + } + + /** @brief Replace selected residual rows with a prepared isobaric equation, preserving all interior rows. */ + template + void apply_residual_rows(const PressureBoundary& boundary, const std::vector>& linearizations, ::mfem::Vector& result) + { + using namespace utils::errors; + + detail::validate_boundary_output(boundary, result); + detail::validate_boundary_samples(boundary, linearizations); + for (const auto& point : linearizations) + { + serif_ensure(std::isfinite(point.residual), "The pressure-boundary residual contains a non-finite value; reject this candidate."); + } + for (int index = 0; index < boundary.selected_rows->size(); ++index) + { + result[boundary.selected_rows->reduced_dofs()[index]] = linearizations[index].residual; + } + } + + /** @brief Replace full Jacobian rows, including every EOS input; directions may alias the output. */ + template + requires utils::dependencies::SatisfiesKeyList + void apply_jacobian_rows(const PressureBoundary& boundary, const std::vector>& linearizations, const Directions& directions, ::mfem::Vector& result) + { + detail::validate_boundary_output(boundary, result); + detail::validate_boundary_samples(boundary, linearizations); + const ::mfem::Vector values = detail::boundary_jacobian_action(boundary, linearizations, directions, typename Formulation::StateTerms{}); + detail::replace_boundary_rows(boundary, values, result); + } + + /** @brief Remove the replaced rows before applying the volume Jacobian transpose. */ + template + void mask_residual_dual(const PressureBoundary& boundary, const ::mfem::Vector& dual, ::mfem::Vector& volume_dual) + { + detail::validate_boundary_output(boundary, dual); + boundary.selected_rows->binding().validate_values(dual); + volume_dual = dual; + for (const int row : boundary.selected_rows->reduced_dofs()) + { + volume_dual[row] = 0.0; + } + } + + /** + * @brief Add one physical term's constrained-row transpose to its existing volume contribution. + * @note The caller supplies the same accepted linearizations as the forward action. Discard the output if a non-finite action throws. + */ + template + requires utils::dependencies::BindingsWithBinding + void add_jacobian_transpose_rows(const PressureBoundary& boundary, const std::vector>& linearizations, const ::mfem::Vector& dual, ::mfem::Vector& result) + { + using namespace utils::errors; + + detail::validate_boundary_output(boundary, dual); + detail::validate_boundary_samples(boundary, linearizations); + boundary.selected_rows->binding().validate_values(dual); + const auto& trace = boundary.traces.template get(); + trace.binding().validate_values(result); + ::mfem::Vector weights(boundary.selected_rows->size()); + for (int index = 0; index < weights.Size(); ++index) + { + weights[index] = linearizations[index].field_derivatives.template get() * dual[boundary.selected_rows->reduced_dofs()[index]]; + } + serif_ensure(utils::checks::is_finite(weights), "The pressure-boundary transpose produced non-finite weights; reject this candidate."); + trace.interpolation().AddMultTranspose(weights, result); + serif_ensure(utils::checks::is_finite(result), "The pressure-boundary transpose produced a non-finite result; discard this output."); + } +} diff --git a/src/include/serif/backend/mfem/geometry/surface/pressure_boundary.hpp b/src/include/serif/backend/mfem/geometry/surface/pressure_boundary.hpp new file mode 100644 index 00000000..f5892173 --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/surface/pressure_boundary.hpp @@ -0,0 +1,4 @@ +#pragma once + +#include "serif/backend/mfem/geometry/surface/boundary_computations.hpp" +#include "serif/backend/mfem/geometry/surface/boundary_rows.hpp" diff --git a/src/include/serif/backend/mfem/geometry/surface/pressure_field_block.hpp b/src/include/serif/backend/mfem/geometry/surface/pressure_field_block.hpp new file mode 100644 index 00000000..5899ff94 --- /dev/null +++ b/src/include/serif/backend/mfem/geometry/surface/pressure_field_block.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include "serif/backend/mfem/geometry/surface/boundary_rows.hpp" +#include "serif/backend/mfem/operators/replaced_rows.hpp" +#include "serif/utils/dependencies/key_list_operations.hpp" + +namespace serif::geometry::surface::backend::mfem { + /** @brief Freeze one EOS partial times its finite-element trace in selected boundary-row coordinates. */ + template + requires utils::dependencies::ContainsKey + [[nodiscard]] std::shared_ptr pressure_field_trace(const PressureBoundary& boundary, const std::vector>& linearizations) { + using namespace utils::errors; + + boundary.validate(); + detail::validate_boundary_samples(boundary, linearizations); + auto result = std::make_shared<::mfem::SparseMatrix>(boundary.traces.template get().interpolation()); + ::mfem::Vector derivatives(boundary.selected_rows->size()); + for (int row = 0; row < derivatives.Size(); ++row) { + derivatives[row] = linearizations[row].field_derivatives.template get(); + serif_ensure(std::isfinite(derivatives[row]), "Pressure-boundary derivative {} is nonfinite. Supply an accepted surface linearization.", row); + } + result->ScaleRows(derivatives); + return result; + } + + /** + * @brief Install one field's accepted isobaric derivative rows on its physical volume block. + * @note Every EOS input uses its own interpolation. Absent fields receive zero constrained rows. Copied derivative-weighted traces keep this accepted block independent of later EOS candidates and of the boundary object's lifetime. + */ + template + [[nodiscard]] operators::backend::mfem::LinearizedBlock pressure_field_block(operators::backend::mfem::LinearizedBlock volume, const PressureBoundary& boundary, const std::vector>& linearizations) { + using namespace utils::errors; + + boundary.validate(); + detail::validate_boundary_samples(boundary, linearizations); + serif_ensure(volume.action()->Height() == boundary.selected_rows->binding().size(), "The selected physical block and pressure boundary must use the same full residual coordinate count."); + std::shared_ptr replacement; + if constexpr (utils::dependencies::ContainsKey) { + replacement = pressure_field_trace(boundary, linearizations); + serif_ensure(replacement->Width() == volume.action()->Width(), "The selected physical field and its pressure trace have different coefficient counts."); + } + else { + auto zero = std::make_shared<::mfem::SparseMatrix>(boundary.selected_rows->size(), volume.action()->Width()); + zero->Finalize(); + replacement = std::move(zero); + } + const ::mfem::Array& selected = boundary.selected_rows->reduced_dofs(); + return operators::backend::mfem::replace_block_rows(std::move(volume), std::vector(selected.begin(), selected.end()), std::move(replacement)); + } +} // namespace serif::geometry::surface::backend::mfem diff --git a/src/include/serif/backend/mfem/mapping/compactification/compactification.hpp b/src/include/serif/backend/mfem/mapping/compactification/compactification.hpp new file mode 100644 index 00000000..0d40f05c --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/compactification/compactification.hpp @@ -0,0 +1,75 @@ +#pragma once + +/** + * @file compactification.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the definition of the ExteriorDomainMap interface, which is used to define the mapping from reference coordinates to physical coordinates in the exterior domain. + */ + +#include +#include + +#include "serif/backend/mfem/mapping/status.hpp" +#include "serif/backend/mfem/mapping/compactification/input_result.hpp" +#include "serif/backend/mfem/mapping/compactification/variation.hpp" + + +namespace serif::mapping::compactification { + + /** + * @class ExteriorDomainMap + * + * @brief Abstract interface for some mapping from reference coordinates to physical coordinates in the exterior domain. + * Currently the only concrete implementation of this is the kelvin compactification. + */ + class ExteriorDomainMap { + public: + virtual ~ExteriorDomainMap() = default; + + /** + * @brief Check if the mapping is affine in the displacement, that is if the mapping is linear in the displacement and does not depend on the displacement. + * + * @return True if the mapping is affine in the displacement, false otherwise. + */ + [[nodiscard]] virtual bool is_affine_in_displacement() const noexcept = 0; + + /** + * @brief Evaluate the mapping at a given point in the reference domain. + * + * @param input The input to the mapping, which contains the reference coordinates and the displacement. + * @param result The result of the mapping, which contains the physical coordinates and the Jacobian of the mapping. + * + * @return The status of the mapping evaluation, which indicates if the evaluation was successful or if there was an error. + * + */ + [[nodiscard]] virtual MappingStatus evaluate( + const ExteriorMapInput &input, + ExteriorMapResult &result + ) const = 0; + + /** + * @brief Evaluate the variation of the mapping at a given point in the reference domain. + * + * @param input The input to the mapping, which contains the reference coordinates and the displacement. + * @param result The result of the mapping, which contains the physical coordinates and the Jacobian of the mapping. + * @param direction The direction of the variation, which is a vector in the reference domain. + * @param variation The variation of the mapping, which contains the variation of the physical coordinates and the variation of the Jacobian. + * + * @return The status of the mapping evaluation, which indicates if the evaluation was successful or if there was an error. + */ + [[nodiscard]] virtual MappingStatus evaluate_variation( + const ExteriorMapInput &input, + const ExteriorMapResult &result, + const ExteriorMapDirection &direction, + ExteriorMapVariation &variation + ) const = 0; + + /** + * @brief Return the name of the mapping, which is used for debugging and logging. + * + * @return The name of the mapping. + */ + [[nodiscard]] virtual std::string_view get_name() const noexcept = 0; + }; +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/mapping/compactification/input_result.hpp b/src/include/serif/backend/mfem/mapping/compactification/input_result.hpp new file mode 100644 index 00000000..561c71a7 --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/compactification/input_result.hpp @@ -0,0 +1,36 @@ +#pragma once + +/** + * @file input_result.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the input and result structures for the compactification mapping. + */ + +#include "mfem/linalg/vector.hpp" +#include "mfem/linalg/densemat.hpp" + +namespace serif::mapping::compactification { + /** + * @struct ExteriorMapInput + * + * @brief Structure representing the input to the exterior domain mapping. This includes the reference position, displaced position, displacement Jacobian, compactification coordinate gradient, and compactification coordinate. + */ + struct ExteriorMapInput { + const mfem::Vector &reference_position; ///< Position in reference space to evaluate the mapping at. + const mfem::Vector &displaced_position; ///< Position in displaced space to evaluate the mapping at (note this is displaced but not yet compactified). + const mfem::DenseMatrix &displacement_jacobian; ///< Jacobian of the displacement mapping at the reference position. + const mfem::Vector &compactification_coordinate_gradient; ///< Gradient of the compactification coordinate at the reference position. + double compactification_coordinate; ///< The compactification coordinate at the reference position. This ranges from 0 to 1, where 0 is the inner surface and 1 is the outer surface of the reference domain. + }; + + /** + * @struct ExteriorMapResult + * + * @brief Structure representing the result of the exterior domain mapping. This includes the physical position and the Jacobian of the mapping. + */ + struct ExteriorMapResult { + mfem::Vector physical_position; ///< Position in physical space after applying the mapping. + mfem::DenseMatrix mapping_jacobian; ///< Jacobian of the mapping at the reference position, which maps from reference coordinates to physical coordinates. + }; +} diff --git a/src/include/serif/backend/mfem/mapping/compactification/kelvin.hpp b/src/include/serif/backend/mfem/mapping/compactification/kelvin.hpp new file mode 100644 index 00000000..4b506908 --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/compactification/kelvin.hpp @@ -0,0 +1,191 @@ +#pragma once + +/** + * @file kelvin.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the definition of the KelvinCompactification class, which is a concrete implementation of the ExteriorDomainMap interface for the Kelvin compactification. + */ + +#include + +#include "mfem/linalg/vector.hpp" + +#include "serif/backend/mfem/mapping/status.hpp" +#include "serif/backend/mfem/mapping/compactification/compactification.hpp" +#include "serif/utils/user_options/mapping/compactification/kelvin.hpp" + +namespace serif::mapping::compactification { + + /** + * @class KelvinCompactification + * + * @brief Concrete implementation of the ExteriorDomainMap interface for the Kelvin compactification. + * + * Broadly, for some reference position \f$\mathbf{x_{r}}\f$ on some reference domain \f$\Omega_{r}\f$ the Kelvin compactification maps to a physical position \f$\mathbf{x_{p}}\f$ on some physical domain \f$\Omega_{p}\f$ through the mapping + * \f[ + * \mathbf{x_{p}} = \frac{\mathbf{x_{r}}}{1 - \frac{|\mathbf{x_{r}}|}{R_{\infty}}} + * \f] + * + * This results in the inner surface of the domain being mapped to the exterior surface of the physical domain while the outer + * surface of the exterior domain is mapped to infinity. There are some implications of this, namely that we + * loose accuracy in all things as we approach the outer surface of the domain. However, it is nice in so far + * as we can apply true boundary conditions at infinity. + */ + class KelvinCompactification final : public ExteriorDomainMap { + public: + /** + * @brief Construct a KelvinCompactification with the given options. + * + * @param options The options for the Kelvin compactification, which must be valid. + * + * @throws std::invalid_argument If the reference stellar radius is not finite or positive + * @throws std::invalid_argument If the reference infinity radius is not finite or positive + * @throws std::invalid_argument If the reference infinity radius is not greater than the reference stellar radius + * @throws std::invalid_argument If the coordinate tolerance is not finite or positive or is greater than or equal to one + */ + explicit KelvinCompactification( + const utils::user_options::KelvinCompactificationOptions& options + ); + + /** + * @brief Check if the mapping is affine in the displacement. For the Kelvin compactification, this is always true + * + * @return false + */ + [[nodiscard]] bool is_affine_in_displacement() const noexcept override; + + /** + * @brief Evaluate the mapping at a given point in the reference domain. + * + * @param[in] input The input point in the reference domain to evaluate the mapping at. + * @param[out] result The result of the mapping evaluation, which will be filled in by the method. + * + * @return A status indicating the success or failure of the mapping evaluation. Mapping may fail if + * - the size of the input reference position is less than or equal to zero + * - the input displaced position size does not match the input reference position size + * - the input compactification coordinate gradient size does not match the input reference position size + * - the displacement_jacobian height does not match the input reference position size + * - the displacement_jacobian width does not match the input reference position size + * - the input reference, displaced, compactification coordinate gradient or jacobian contains non-finite values + * - There is some issue computing the radial factors (this error will be passed through) + * + * The mapping jacobian comes from trivial application of the product rule + * recall: \f$r_i = scale * x_i\f$ where \f$r\f$ is the physical position and \f$x\f$ is the displaced position. + * then we can differentiate wrt. \f$X_j\f$ holding nothing fixed. Note the capital \f$X\f$ here, this is the mesh coordinate not the displaced position. + * Lets call this jacobian \f$F\f$ + * \f[ + * F = \frac{\partial r_i}{\partial X_{j}} + * \f] + * + * \f$F\f$ then tells us how the physical position changes as we move along mesh coordinates + * + * Lets then apply this to the function we have for the kelvin compactification + * \f[ + * F = scale * \frac{\partial x_i}{\partial X_j} + x_i * \frac{\partial scale}{\partial X_j} + * \f] + * + * In the implementation you can see the displacement jacobian (\f$\frac{\partial x_i}{\partial X_j}\f$) and the scale derivative (\f$\frac{\partial scale}{\partial X_j}\f$) being applied to compute the mapping jacobian. + */ + [[nodiscard]] MappingStatus evaluate( + const ExteriorMapInput& input, + ExteriorMapResult& result + ) const override; + + + /** + * @brief Evaluate the variation of the mapping at a given point in the reference domain. + * + * @param[in] input The input point in the reference domain to evaluate the mapping at. + * @param[in] result The result of the mapping evaluation, which will be used in the variation computation. + * @param[in] direction The direction in which to evaluate the variation. + * @param[out] variation The result of the variation evaluation, which will be filled in by the method. + * + * @return A status indicating the success or failure of the variation evaluation. The reasons for a failure are effectively the same + * as the reasons for a failure in the evaluate method just extended to cover the additional inputs. + */ + [[nodiscard]] MappingStatus evaluate_variation( + const ExteriorMapInput& input, + const ExteriorMapResult& result, + const ExteriorMapDirection& direction, + ExteriorMapVariation& variation + ) const override; + + /** + * @brief Get the name of the mapping, which is "KelvinCompactification" for this class. + * @return The name of the mapping. + */ + [[nodiscard]] std::string_view get_name() const noexcept override; + + /** + * @brief Get the reference stellar radius for the mapping, which is the radius of the inner surface of the reference domain. + * @return The reference stellar radius for the mapping. + */ + [[nodiscard]] double get_reference_stellar_radius() const noexcept; + + /** + * @brief Get the reference infinity radius for the mapping, which is the radius of the outer surface of the reference domain. + * @return The reference infinity radius for the mapping. + */ + [[nodiscard]] double get_reference_infinity_radius() const noexcept; + + /** + * @brief Get the coordinate tolerance for the mapping, which is the tolerance for the compactification coordinate. + * @return The coordinate tolerance for the mapping. + */ + [[nodiscard]] double get_coordinate_tolerance() const noexcept; + + /** + * @brief Evaluate the mapping jacobian variation factors at some given compactification coordinate. + * + * @param[in] compactification_coordinate The compactification coordinate at which to evaluate the mapping jacobian variation factors. (Note this runs from 0 to 1, where 0 is the inner surface and 1 is the outer surface of the reference domain) + * @param[in] coordinateGradient The gradient of the compactification coordinate with respect to the mesh coordinates, which must be finite and have size 3. + * + * @param[out] scale The scale factor for the mapping jacobian, which will be filled in by the method. + * @param[out] scaleGradient The gradient of the scale factor with respect to the mesh coordinates, which will be filled in by the method. + * + * @return The status of the mapping evaluation. + */ + [[nodiscard]] MappingStatus evaluate_jacobian_variation_factors( + double compactification_coordinate, + const mfem::Vector &coordinateGradient, + double &scale, + mfem::Vector &scaleGradient + ) const; + + private: + /** + * @struct RadialFactors + * + * @brief A struct to hold the radial factors for the Kelvin compactification mapping. + */ + struct RadialFactors { + double compactification_coordinate; ///< The compactification coordinate, which runs from 0 to 1, where 0 is the inner surface and 1 is the outer surface of the reference domain. + double computational_radius; ///< The computational radius, which is the radius of the reference domain at the given compactification coordinate. + double scale; ///< The scale factor for the mapping jacobian. + double scale_derivative; ///< The derivative of the scale factor with respect to the compactification coordinate. + }; + + private: + /** + * @brief Compute the radial factors for the Kelvin compactification mapping at a given compactification coordinate. + * + * @param[in] compactification_coordinate The compactification coordinate at which to compute the radial factors. (Note this runs from 0 to 1, where 0 is the inner surface and 1 is the outer surface of the reference domain) + * @param[out] radial_factors The radial factors, which will be filled in by the method. + * + * @return The status of the mapping evaluation. + * + * @note See the implementation file for more details on the actual algorithm used here. + */ + [[nodiscard]] MappingStatus compute_radial_factors( + double compactification_coordinate, + RadialFactors &radial_factors + ) const; + + private: + utils::user_options::KelvinCompactificationOptions m_options; ///< The options for the Kelvin compactification, which must be valid. + const std::string_view m_name = "KelvinCompactification"; ///< The name of the mapping, which is "KelvinCompactification" for this class. + }; + + +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/mapping/compactification/variation.hpp b/src/include/serif/backend/mfem/mapping/compactification/variation.hpp new file mode 100644 index 00000000..96196c28 --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/compactification/variation.hpp @@ -0,0 +1,35 @@ +#pragma once + +/** + * @file variation.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the structures for the variation of the compactification mapping. + */ + +#include "mfem/linalg/vector.hpp" +#include "mfem/linalg/densemat.hpp" + +namespace serif::mapping::compactification { + /** + * @struct ExteriorMapDirection + * + * @brief Structure representing the direction of the variation in the exterior domain mapping. + * This includes the variation of the displaced position and the variation of the displacement Jacobian. + */ + struct ExteriorMapDirection { + const mfem::Vector &displaced_position_variation; ///< Variation of the position in displaced space. + const mfem::DenseMatrix &displacement_jacobian_variation; ///< Variation of the Jacobian of the displacement mapping. + }; + + /** + * @struct ExteriorMapVariation + * + * @brief Structure representing the variation of the exterior domain mapping. + * This includes the variation of the physical position and the variation of the mapping Jacobian. + */ + struct ExteriorMapVariation { + mfem::Vector physical_position_variation; ///< Variation of the position in physical space. + mfem::DenseMatrix mapping_jacobian_variation; ///< Variation of the Jacobian of the mapping. + }; +} diff --git a/src/include/serif/backend/mfem/mapping/context.hpp b/src/include/serif/backend/mfem/mapping/context.hpp new file mode 100644 index 00000000..6d8f3f3e --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/context.hpp @@ -0,0 +1,92 @@ +#pragma once + +/** + * @file context.hpp + * @author Emily M. Boudreaux + * + * @brief File containing the mapping context structures for volume and face mappings. + * + * @todo Like a lot of the parts of the mapping module written before I started factoring mfem stuff into + * backend modules, a good amount of this might be movable to some mfem backend + * + * All of these different "context" structures are built to contain sufficient information that the + * caller which gets one of these back can evaluate the mapping and its derivatives properly + * at some point. + */ + +#include "mfem/linalg/densemat.hpp" +#include "mfem/linalg/vector.hpp" + + +namespace serif::mapping { + /** + * @struct VolumeQuadratureContext + * + * @brief A structure representing the context of a volume quadrature point, including the inverse Jacobian of the mapping and the determinant of the mapping. + * + * @todo Add a worked example here + */ + struct VolumeQuadratureContext { + mfem::DenseMatrix J_element_to_physical_inverse; ///< The inverse Jacobian of the mapping from the element to the physical space. + double mapping_determinate; ///< The determinant of the mapping from the element to the physical space, + double physical_integration_weight; ///< The physical integration weight for the quadrature point. + }; + + /** + * @struct FaceQuadratureContext + * + * @brief A structure representing the context of a face quadrature point, including the normal vector, surface weight, and scale factor for the dot product of the velocity and normal vector. + * + * @todo Add a worked example here + */ + struct FaceQuadratureContext { + mfem::Vector normal; ///< The normal vector at the face quadrature point. + double ds; ///< The surface weight for the face quadrature point. + double v_dot_n_scale; ///< The scale factor for the dot product of the velocity and normal vector at the face quadrature point. + }; + + /** + * @struct MappingPointContext + * + * @brief A structure representing the context of a mapping point, including the reference position, displaced position, physical position, displacement Jacobian, mapping Jacobian, inverse mapping Jacobian, mapping determinant, and compactification status. + * + * @todo Add a worked example here + */ + struct MappingPointContext { + mfem::Vector reference_position; ///< The reference position of the mapping point. + mfem::Vector displaced_position; ///< The displaced position of the mapping point. + mfem::Vector physical_position; ///< The physical position of the mapping point. + mfem::DenseMatrix displacement_jacobian; ///< The displacement Jacobian of the mapping point. + mfem::DenseMatrix mapping_jacobian; ///< The mapping Jacobian of the mapping point. + mfem::DenseMatrix inverse_mapping_jacobian; ///< The inverse mapping Jacobian of the mapping point. + double mapping_determinate{0.0}; ///< The determinant of the mapping Jacobian at the mapping point. + bool compactified{false}; ///< A flag indicating whether the mapping point is compactified (true) or not (false). + }; + + /** + * @struct VolumeMappingContext + * + * @brief A structure representing the context of a volume mapping, including the mapping point context and volume quadrature context. + * + * @todo Add a worked example here + */ + struct VolumeMappingContext { + MappingPointContext mapping; ///< The mapping point context for the volume mapping. + VolumeQuadratureContext quadrature; ///< The volume quadrature context for the volume mapping. + }; + + /** + * @struct FaceMappingContext + * + * @brief A structure representing the context of a face mapping, including the mapping point context and face quadrature context. + * + * @todo Add a worked example here + */ + struct FaceMappingContext { + MappingPointContext mapping; ///< The mapping point context for the face mapping. + FaceQuadratureContext quadrature; ///< The face quadrature context for the face mapping. + mfem::Vector reference_normal; ///< The reference normal vector for the face mapping. + double reference_surface_weight{0.0}; ///< The reference surface weight for the face mapping. + double physical_surface_weight{0.0}; ///< The physical surface weight for the face mapping. + }; +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/mapping/data.hpp b/src/include/serif/backend/mfem/mapping/data.hpp new file mode 100644 index 00000000..fcd7baec --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/data.hpp @@ -0,0 +1,254 @@ +#pragma once + +/** + * @file data.hpp + * @author Emily M. Boudreaux + * + * @brief File containing some misc data structures for the mapping module. This was the very first file + * I ported over from MeanField and I think because of that its organization is actually quite wonky. + * For one its int this poorly named "data" file, which could mean any number of things. Regardless, + * these are basically low leval containers for useful stuff and also some scratch workspaces + * for mapping operations. + * + * @todo As said above there is some refactoring to make this cleaner which could happen. + */ + +#include "mfem/fem/fe/fe_base.hpp" +#include "mfem/fem/fespace.hpp" +#include "mfem/linalg/vector.hpp" +#include "mfem/linalg/densemat.hpp" +#include "mfem/mesh/mesh.hpp" + +#include + +#include "serif/backend/mfem/mapping/compactification/kelvin.hpp" +#include "serif/backend/mfem/mapping/context.hpp" +#include "serif/backend/mfem/mapping/data.hpp" + + +namespace serif::mapping { + /** + * @enum FaceElementSide + * + * @brief An enumeration representing the side of a face element, either ELEMENT_1 or ELEMENT_2. There is no inherent ordering + * to these; however, broadly I have been thinking about ELEMENT_1 as the "inner" side and ELEMENT_2 as the "outer" side. + */ + enum class FaceElementSide : uint8_t { + ELEMENT_1, ///< Represents the first side of a face element. + ELEMENT_2 ///< Represents the second side of a face element. + }; + + /** + * @class ElementDisplacementData + * + * @brief A class representing the displacement data for a finite element, including the finite element, degree of freedom matrix, and dimension. + */ + class ElementDisplacementData { + public: + + /** + * @brief Constructs an ElementDisplacementData object with the given finite element and displacement degrees of freedom. + * + * @param element The finite element associated with the displacement data. + * @param displacement_dofs The element vector of displacement degrees of freedom, ordered by nodes within each component as MFEM element vdofs are. + * + * @throws std::invalid_argument if the finite element has zero or negative degrees of freedom, + * or if the size of the displacement_dofs vector is not a positive multiple of the element's degree of freedom count. + * + * The dof matrix is populated such that for row i and column j, with n total dofs, dof_matrix(i, j) = displacement_dofs(i + j * n). + */ + ElementDisplacementData( + const mfem::FiniteElement& element, + mfem::Vector& displacement_dofs + ); + + /** + * @brief Returns the finite element associated with the displacement data. + * @return A constant reference to the finite element. + */ + [[nodiscard]] const mfem::FiniteElement& get_element() const noexcept; + + /** + * @brief Returns the degree of freedom matrix for the displacement data. + * @return A constant reference to the degree of freedom matrix. + */ + [[nodiscard]] const mfem::DenseMatrix &get_dof_matrix() const noexcept; + + /** + * @brief Returns the dimension of the displacement data. + * @return The dimension of the displacement data. + */ + [[nodiscard]] int get_dimension() const noexcept; + + /** + * @brief Returns the degree of freedom count for the displacement data. + * @return The degree of freedom count for the displacement data. + */ + [[nodiscard]] int get_dof_count() const noexcept; + + private: + const mfem::FiniteElement *m_element; ///< Pointer to the finite element associated with the displacement data. + mfem::DenseMatrix m_dof_matrix; ///< Matrix representing the degrees of freedom for the displacement data. + int m_dimension; ///< The dimension of the displacement data (number of components). + }; + + /** + * @struct CompactificationPointData + * + * @brief A structure representing the compactification point data, including the compactification coordinate and coordinate gradient. + */ + struct CompactificationPointData { + double compactification_coordinate{0.0}; ///< The compactification coordinate value at the point. + mfem::Vector coordinate_gradient; ///< The gradient of the compactification coordinate at the point. + }; + + /** + * @brief Creates an ElementDisplacementData object from a finite element and a vector of displacement degrees of freedom. + * + * @param element The finite element associated with the displacement data. + * @param displacement_dofs The vector containing the displacement degrees of freedom. + * @return An ElementDisplacementData object constructed from the provided finite element and displacement degrees of freedom. + * + * @note This is just a convenience function which calls the constructor for ElementDisplacementData. + */ + [[nodiscard]] ElementDisplacementData element_displacement_data_from_element_vdofs( + const mfem::FiniteElement& element, + mfem::Vector &displacement_dofs + ); + + /** + * @class ElementCompactificationData + * + * @brief A class representing the compactification data for a finite element, including the finite element and degrees of freedom. + */ + class ElementCompactificationData { + public: + /** + * @brief Constructs an ElementCompactificationData object with the given finite element and degrees of freedom. + * + * @param element The finite element associated with the compactification data. + * @param dofs The vector containing the degrees of freedom for the compactification data. + * + * @throws std::invalid_argument if the finite element is not a scalar finite element, does not provide a gradient, or has zero or negative degrees of freedom, + * or if the size of the dofs vector does not match the finite element's degree of freedom count. + */ + ElementCompactificationData( + const mfem::FiniteElement& element, + const mfem::Vector& dofs + ); + + /** + * @brief Returns the finite element associated with the compactification data. + * @return A constant reference to the finite element. + */ + [[nodiscard]] const mfem::FiniteElement& get_element() const noexcept; + + /** + * @brief Returns the degrees of freedom for the compactification data. + * @return A constant reference to the vector of degrees of freedom. + */ + [[nodiscard]] const mfem::Vector &get_dofs() const noexcept; + + /** + * @brief Returns the degree of freedom count for the compactification data. + * @return The degree of freedom count for the compactification data. + */ + [[nodiscard]] int get_dof_count() const noexcept; + + private: + const mfem::FiniteElement *m_element; ///< Pointer to the finite element associated with the compactification data. + mfem::Vector m_dofs; ///< Vector representing the degrees of freedom for the compactification data. + }; + + struct ElementMappingData { + const ElementDisplacementData &displacement; + const ElementCompactificationData &compactification; + }; + + /** + * @class DMWorkspace + * + * @brief A class representing a workspace for displacement mapping operations, including various vectors and matrices used in the mapping process. + * primarily this is just a separate class so we can more easily make this code thread-safe in the future. At construction of + * an instance of this class loads of Heap allocation is done (or really whatever mfem does internally for allocation for vector which dependents on + * the selected device) and then that allocation can be reused. + */ + class DMWorkspace { + public: + /** + * @brief Constructs a DMWorkspace object with the specified dimension. + * + * @param dimension The dimension of the workspace (default is 3). + * + * @throws std::invalid_argument if the specified dimension is not positive and finite. + */ + explicit DMWorkspace (int dimension = 3); + + /** + * @brief Sets the dimension of the workspace. + * + * @param dimension The new dimension of the workspace. + * + * @throws std::invalid_argument if the specified dimension is not positive and finite. + * + * @post All internal vectors are sized to dimension + * @post All internal matrices are sized to dimension x dimension + * @post The compactification coordinate is set to 0.0 + */ + void set_dimension(int dimension); + + /** + * @brief Returns the dimension of the workspace. + * @return The dimension of the workspace. + */ + [[nodiscard]] int get_dimension() const noexcept; + + public: // This is a workspace class, these members are kept public so we do not need indirection through getters to access them + mfem::Vector m_shape; ///< Pre-allocated vector for shape function values at a quadrature point. + mfem::DenseMatrix m_reference_dshape; ///< Pre-allocated matrix for reference shape function derivatives at a quadrature point. + mfem::DenseMatrix m_mesh_dshape; ///< Pre-allocated matrix for mesh shape function derivatives at a quadrature point. + mfem::DenseMatrix m_reference_field_jacobian; ///< Pre-allocated matrix for the reference field Jacobian at a quadrature point. + mfem::Vector m_field_value; ///< Pre-allocated vector for field values at a quadrature point. + mfem::DenseMatrix m_field_jacobian; ///< Pre-allocated matrix for field Jacobian at a quadrature point. + + mfem::Vector m_compactification_shape; ///< Pre-allocated vector for compactification shape function values at a quadrature point. + mfem::DenseMatrix m_compactification_dshape; ///< Pre-allocated matrix for compactification shape function derivatives at a quadrature point. + CompactificationPointData m_compactification_point; ///< Pre-allocated data for compactification point. + + mfem::Vector m_reference_normal; ///< Pre-allocated vector for reference normal at a quadrature point. + mfem::Vector m_mapped_normal; ///< Pre-allocated vector for mapped normal at a quadrature point. + mfem::DenseMatrix m_full_element_jacobian; ///< Pre-allocated matrix for full element Jacobian at a quadrature point. + + mfem::Vector m_vector_temp; ///< Pre-allocated temporary vector for intermediate calculations. + mfem::DenseMatrix m_matrix_temp_1; ///< Pre-allocated temporary matrix for intermediate calculations. + mfem::DenseMatrix m_matrix_temp_2; ///< Pre-allocated temporary matrix for intermediate calculations. + + compactification::ExteriorMapResult m_exterior_result; ///< Pre-allocated result for exterior mapping at a quadrature point. + compactification::ExteriorMapVariation m_exterior_variation; ///< Pre-allocated variation for exterior mapping at a quadrature point. + private: + int m_dimension; ///< The number of spatial dimensions for the workspace (e.g., 2D or 3D). + }; + + /** + * @struct DMFixedMeshGeometry + * + * @brief A structure representing the fixed mesh geometry at a specific point, including position, Jacobian, inverse Jacobian, and weight. + */ + struct DMFixedMeshGeometry { + mfem::Vector position; ///< The physical position of the point in the mesh. + mfem::DenseMatrix jacobian; ///< The Jacobian matrix of the mapping from the reference element to the physical element at the point. + mfem::DenseMatrix inverse_jacobian; ///< The inverse of the Jacobian matrix of the mapping from the reference element to the physical element at the point. + double weight{0.0}; ///< The mapping weight associated with the point, typically used for integration purposes. + }; + + /** + * @struct DMReferenceDisplacementSample + * + * @brief A structure representing a sample of the reference displacement, including the value and gradient of the displacement at a specific point. + */ + struct DMReferenceDisplacementSample { + const mfem::Vector& value; ///< The value of the reference displacement at the point. + const mfem::DenseMatrix& gradient; ///< The gradient of the reference displacement at the point. + }; + +} diff --git a/src/include/serif/backend/mfem/mapping/domain_mapper.hpp b/src/include/serif/backend/mfem/mapping/domain_mapper.hpp new file mode 100644 index 00000000..bed9467a --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/domain_mapper.hpp @@ -0,0 +1,219 @@ +#pragma once + +#include +#include + +#include "mfem/linalg/vector.hpp" +#include "mfem/linalg/densemat.hpp" +#include "mfem/fem/eltrans.hpp" +#include "mfem/fem/intrules.hpp" + +#include "serif/backend/mfem/mapping/compactification/kelvin.hpp" +#include "serif/backend/mfem/mapping/context.hpp" +#include "serif/backend/mfem/mapping/data.hpp" +#include "serif/backend/mfem/mapping/variation.hpp" + +#include "serif/utils/user_options/mapping/domain_mapper.hpp" + + +namespace serif::mapping { + /** @brief Sample immutable mesh geometry; callers decide how long its mesh revision remains valid. */ + [[nodiscard]] std::expected sample_fixed_mesh_geometry( + mfem::ElementTransformation& transformation, const mfem::IntegrationPoint& point + ); + + class DomainMapper { + public: + DomainMapper(const DomainMapper&) = delete; + DomainMapper& operator=(const DomainMapper&) = delete; + + ~DomainMapper() = default; + DomainMapper(DomainMapper&&) = default; + DomainMapper& operator=(DomainMapper&&) = default; + + + DomainMapper( + utils::user_options::DomainMapperOptions options, + std::unique_ptr exterior_map + ); + + [[nodiscard]] MappingStatus evaluate_point( + const ElementMappingData& element_data, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint& ip, + DMWorkspace& workspace, + MappingPointContext& context + ) const; + + /** + * @brief Map a point using checked fixed geometry sampled by the caller. + * @note Geometry must come from sample_fixed_mesh_geometry for this exact element, point, + * and mesh revision. Dimensions, finite values, positive weight and invertibility are checked; identity and + * freshness remain the caller's responsibility. No sample is retained by the mapper. + */ + [[nodiscard]] MappingStatus evaluate_point( + const ElementMappingData& element_data, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + DMWorkspace& workspace, + MappingPointContext& context, + const DMFixedMeshGeometry& fixed_geometry + ) const; + + /** @brief Map a volume point, optionally reusing fixed geometry owned by the reference graph node. + * @note Supplied geometry must belong to this element and point at the current mesh revision. + */ + [[nodiscard]] MappingStatus evaluate_volume( + const ElementMappingData& element_data, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + DMWorkspace& workspace, + VolumeMappingContext& context, + const DMReferenceDisplacementSample *displacement_sample = nullptr, + const DMFixedMeshGeometry* fixed_geometry = nullptr + ) const; + + [[nodiscard]] MappingStatus evaluate_face( + const ElementMappingData& element_data, + mfem::FaceElementTransformations& transformation, + FaceElementSide side, + const mfem::IntegrationPoint& ip, + DMWorkspace& workspace, + FaceMappingContext& context + ) const; + + [[nodiscard]] MappingStatus evaluate_point_variation( + const ElementMappingData& element_data, + const ElementDisplacementData& direction, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + const MappingPointContext& context, + DMWorkspace& workspace, + MappingPointVariation& variation + ) const; + + /** + * @brief Differentiate a point map using its already sampled fixed-mesh inverse Jacobian. + * @note The inverse must come from the checked fixed geometry for this exact element, + * point and mesh revision; context must describe the same accepted displacement. Size and + * finite values are checked. Non-VALUE finite elements retain their original physical-gradient + * evaluation path. Neither the inverse nor any field sample is retained. + */ + [[nodiscard]] MappingStatus evaluate_point_variation( + const ElementMappingData& element_data, + const ElementDisplacementData& direction, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + const MappingPointContext& context, + DMWorkspace& workspace, + MappingPointVariation& variation, + const mfem::DenseMatrix& inverse_mesh_jacobian + ) const; + + /** + * @brief Sample the fixed compactification coordinate and its gradient in mesh coordinates. + * @param inverse_mesh_jacobian Optional inverse Jacobian from the same element and point. + * @note Reuse this sample for several displacement directions at this point. Refresh it when + * the compactification coefficients, element, point or reference mesh change. + */ + [[nodiscard]] MappingStatus evaluate_compactification_coordinate( + const ElementCompactificationData& compactification, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + DMWorkspace& workspace, + CompactificationPointData& point_data, + const mfem::DenseMatrix *inverse_mesh_jacobian = nullptr + ) const; + + /** + * @brief Differentiate the physical volume map along one of several directions sharing this point. + * @param direction_sample The displacement direction's value and canonical-element gradient at this point. + * @param compactification_sample Must match this point and the frozen coefficients; it is ignored on + * interior elements. Its dimension and finite values are checked before use. + * @param inverse_mesh_jacobian context.quadrature.J_element_to_physical_inverse times + * context.mapping.mapping_jacobian, formed once per point by the caller. + */ + [[nodiscard]] MappingStatus evaluate_volume_direction_variation( + const ElementMappingData& element_data, + const ElementDisplacementData& direction, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + const VolumeMappingContext& context, + DMWorkspace& workspace, + VolumeMappingVariation& variation, + const DMReferenceDisplacementSample& direction_sample, + const CompactificationPointData* compactification_sample, + const mfem::DenseMatrix& inverse_mesh_jacobian + ) const; + + [[nodiscard]] MappingStatus evaluate_face_variation( + const ElementMappingData& element_data, + const ElementDisplacementData& direction, + mfem::FaceElementTransformations &transformation, + FaceElementSide side, + const mfem::IntegrationPoint& ip, + const FaceMappingContext& context, + DMWorkspace& workspace, + FaceMappingVariation& variation + ) const; + + [[nodiscard]] bool is_compactified_element(const mfem::ElementTransformation& transformation) const noexcept; + [[nodiscard]] int get_dimension() const noexcept; + /** @brief Expose the immutable mapping prescription for reproducible reconstruction. */ + [[nodiscard]] const utils::user_options::DomainMapperOptions& options() const noexcept { return m_options; } + [[nodiscard]] const compactification::ExteriorDomainMap& get_exterior_map() const noexcept; + private: + [[nodiscard]] MappingStatus evaluate_point_impl( + const ElementMappingData& element_data, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + DMWorkspace& workspace, + MappingPointContext& context, + const DMFixedMeshGeometry& geometry, + const DMReferenceDisplacementSample* displacement_sample = nullptr + ) const; + + void validate_element_data( + const ElementMappingData& element_data + ) const; + + void evaluate_displacement_field( + const ElementDisplacementData& field, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + DMWorkspace& workspace, + mfem::Vector& value, + mfem::DenseMatrix& jacobian, + const mfem::DenseMatrix *inverse_mesh_jacobian + ) const; + + [[nodiscard]] MappingStatus evaluate_point_variation_impl( + const ElementMappingData& element_data, + const ElementDisplacementData& direction, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + const MappingPointContext& context, + DMWorkspace& workspace, + MappingPointVariation& variation, + const mfem::DenseMatrix *inverse_mesh_jacobian, + const DMReferenceDisplacementSample* direction_sample = nullptr, + const CompactificationPointData* compactification_sample = nullptr + ) const; + + // These are static since they do not use any class members or workspace. Really these could and + // maybe should be free functions... + [[nodiscard]] static mfem::ElementTransformation& select_face_element_transformation( + const mfem::FaceElementTransformations& transformations, + FaceElementSide side + ); + + [[nodiscard]] static const mfem::IntegrationPoint& select_face_element_integration_point( + const mfem::FaceElementTransformations& transformations, + FaceElementSide side + ); + + private: + utils::user_options::DomainMapperOptions m_options; + std::unique_ptr m_exterior_map; + }; +} diff --git a/src/include/serif/backend/mfem/mapping/evaluator.hpp b/src/include/serif/backend/mfem/mapping/evaluator.hpp new file mode 100644 index 00000000..94a19a70 --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/evaluator.hpp @@ -0,0 +1,82 @@ +#pragma once +#include + +#include "mfem/fem/gridfunc.hpp" +#include "mfem/fem/eltrans.hpp" +#include "mfem/fem/intrules.hpp" +#include "mfem/linalg/vector.hpp" +#include "mfem/general/array.hpp" + +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/backend/mfem/mapping/context.hpp" +#include "serif/backend/mfem/mapping/status.hpp" + +namespace serif::mapping { + // Note this is NOT thread safe. + class GridFunctionMappingEvaluator { + public: + /* + * The evaluator references the supplied grid functions and caches copies of + * their element-local DOFs. You need to make sure you call InvalidateCache() or Refresh() after either + * grid function's values are modified. Changes to the finite-element-space sequence + * should be detected automatically. + * + * This object owns mutable workspace and cache state and *is not* thread-safe. + */ + GridFunctionMappingEvaluator( + const DomainMapper& mapper, + const mfem::GridFunction& displacement, + const mfem::GridFunction& compactification_coordinate + ); + + /** @brief Identify the mesh supplying the mapping fields so callers can validate shared geometry. */ + [[nodiscard]] const mfem::Mesh* mesh() const noexcept { + const mfem::FiniteElementSpace* space = m_displacement.FESpace(); + return space == nullptr ? nullptr : space->GetMesh(); + } + + void invalidate_cache() noexcept; + void refresh(); + + [[nodiscard]] MappingStatus evaluate_point( + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + MappingPointContext &context + ); + + [[nodiscard]] MappingStatus evaluate_volume( + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &integration_point, + VolumeMappingContext &context + ); + + [[nodiscard]] MappingStatus evaluate_face( + mfem::FaceElementTransformations &transformation, + FaceElementSide side, + const mfem::IntegrationPoint &integration_point, + FaceMappingContext &context + ); + private: + void validate_field_bindings() const; + [[nodiscard]] bool invalidate_for_changed_spaces(); + void load_element(int element_id); + + private: + const DomainMapper &m_mapper; + const mfem::GridFunction &m_displacement; + const mfem::GridFunction &m_compactification_coordinate; + const mfem::FiniteElementSpace *m_displacement_space; + const mfem::FiniteElementSpace *m_compactification_space; + long m_displacement_space_sequence; + long m_compactification_space_sequence; + DMWorkspace m_workspace; + + mfem::Array m_displacement_dofs; + mfem::Array m_compactification_dofs; + mfem::Vector m_element_displacement; + mfem::Vector m_element_compactification; + std::unique_ptr m_displacement_data; + std::unique_ptr m_compactification_data; + int m_cached_element_id{-1}; + }; +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/mapping/status.hpp b/src/include/serif/backend/mfem/mapping/status.hpp new file mode 100644 index 00000000..3c08dc30 --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/status.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include "serif/utils/error/codes.hpp" +#include "serif/meta/enums.hpp" + +namespace serif::mapping +{ + enum class MappingStatus : uint16_t { + VALID, + + // Start this at 400 so this is parallel to the mapping error codes. There must be a better way to + // do this but it is not worth the time to sort out now + INVALID_DIMENSION = 400, + NON_FINITE_MAPPING_INPUT, + INVALID_REFERENCE_RADIUS, + AT_COMPACTIFIED_INFINITY, + OUTSIDE_REFERENCE_DOMAIN, + NON_FINITE_MAPPING_RESULT, + NON_POSITIVE_DETERMINATE + }; + + static auto MappingStatus_enum_string_map = meta::enum_to_enum_string_map(); + static auto MappingStatus_string_enum_map = meta::enum_to_string_enum_map(); + + inline std::unordered_map make_MappingStatus_SERiFErrorCode_map() { + std::unordered_map ret; + for (const auto& [MS, MS_name] : MappingStatus_enum_string_map) { + if (std::to_underlying(MS) < 400) { + continue; // Skip VALID since it is not in the error code set + } + ret.insert({MS, utils::errors::SERiFErrorCodeNameToTypeMap.at(MS_name)}); + } + return ret; + } + + inline std::unordered_map make_SERIFErrorCode_MappingStatus_map() { + + std::unordered_map ret; + for (const auto& [MS, MS_name] : MappingStatus_enum_string_map) { + if (std::to_underlying(MS) < 400) { + continue; // Skip VALID since it is not in the error code set + } + ret.insert({utils::errors::SERiFErrorCodeNameToTypeMap.at(MS_name), MS}); + } + return ret; + } + + // We make these parallel lists so to ensure that we can lookup between SERiF error codes + // and equivalent MappingStatus. Note again this is ugly and bad and I am ashamed of this code.... + // anyhoodles there is likely a better way but pick your battles amiright? + static auto MappingStatus_SERiFErrorCode_map = make_MappingStatus_SERiFErrorCode_map(); + static auto SERiFErrorCode_MappingStatus_map = make_SERIFErrorCode_MappingStatus_map(); +} + diff --git a/src/include/serif/backend/mfem/mapping/transformations.hpp b/src/include/serif/backend/mfem/mapping/transformations.hpp new file mode 100644 index 00000000..d5623fa8 --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/transformations.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "serif/backend/mfem/mapping/variation.hpp" +#include "serif/backend/mfem/mapping/context.hpp" + +namespace serif::mapping { + void map_reference_gradient_to_physical( + const MappingPointContext& context, + const mfem::Vector& reference_gradient, + mfem::Vector& physical_gradient + ); + + void map_physical_gradient_to_reference( + const MappingPointContext& context, + const mfem::Vector& physical_gradient, + mfem::Vector& reference_gradient + ); + + void map_reference_vector_gradient_to_physical( + const MappingPointContext& context, + const mfem::DenseMatrix& reference_vector_gradient, + mfem::DenseMatrix& physical_vector_gradient + ); + + void map_physical_vector_gradient_to_reference( + const MappingPointContext& context, + const mfem::DenseMatrix& physical_vector_gradient, + mfem::DenseMatrix& reference_vector_gradient + ); + + void compute_scalar_diffusion_tensor( + const MappingPointContext& context, + mfem::DenseMatrix& diffusion_tensor + ); + + void map_hcurl_field_to_physical( + const MappingPointContext& context, + const mfem::Vector& reference_field, + mfem::Vector& physical_field + ); + + void map_physical_field_to_hcurl_reference( + const MappingPointContext& context, + const mfem::Vector& physical_field, + mfem::Vector& reference_field + ); + + void map_hcurl_curl_to_physical( + const MappingPointContext& context, + const mfem::Vector &reference_curl, + mfem::Vector &physical_curl + ); + + void map_physical_curl_to_hcurl_reference( + const MappingPointContext& context, + const mfem::Vector& physical_curl, + mfem::Vector& reference_curl + ); + +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/mapping/variation.hpp b/src/include/serif/backend/mfem/mapping/variation.hpp new file mode 100644 index 00000000..ea07fcb5 --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/variation.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "mfem/linalg/densemat.hpp" +#include "mfem/linalg/vector.hpp" + +namespace serif::mapping { + struct MappingPointVariation { + mfem::Vector displacement_variation; + mfem::Vector physical_position_variation; + mfem::DenseMatrix displacement_jacobian_variation; + mfem::DenseMatrix mapping_jacobian_variation; + mfem::DenseMatrix inverse_mapping_jacobian_variation; + double mapping_determinate_variation{0.0}; + }; + + struct VolumeMappingVariation { + MappingPointVariation mapping; + double weight_variation{0.0}; + }; + + struct FaceMappingVariation { + MappingPointVariation mapping; + mfem::Vector physical_normal_variation; + + double physical_surface_weight_variation{0.0}; + double normal_flux_scale_variation{0.0}; + }; +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/mapping/volume_cache.hpp b/src/include/serif/backend/mfem/mapping/volume_cache.hpp new file mode 100644 index 00000000..f660faa4 --- /dev/null +++ b/src/include/serif/backend/mfem/mapping/volume_cache.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "mfem/linalg/densemat.hpp" + +#include "serif/backend/mfem/mapping/context.hpp" +#include + +namespace serif::mapping { + class VolumeMappingCache { + public: + void set_size(int point_count, int dimension); + void store(int point, VolumeMappingContext& context); + void load(int point, VolumeMappingContext& context) const; + void load_inverse_jacobian(int point, mfem::DenseMatrix& inverse) const; + [[nodiscard]] int get_point_count() const noexcept; + [[nodiscard]] int get_dimension() const noexcept; + + private: + void validate_point(int point) const; + [[nodiscard]] const double* get_point_data(int point) const; + + std::vector m_data; + int m_point_count{0}; + int m_dimension{0}; + int m_point_stride{0}; + }; + +} \ No newline at end of file diff --git a/src/include/serif/backend/mfem/model/binding.hpp b/src/include/serif/backend/mfem/model/binding.hpp new file mode 100644 index 00000000..b06b7fa4 --- /dev/null +++ b/src/include/serif/backend/mfem/model/binding.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "serif/model/equilibrium_plan.hpp" +#include "serif/backend/mfem/model/inputs.hpp" +#include "serif/backend/mfem/geometry/surface/pressure_boundary.hpp" +#include "serif/backend/mfem/geometry/deformation/inputs.hpp" + +namespace serif::model::backend::mfem +{ + /** @brief Retain physical descriptions and all numerical bindings behind one stable owner. */ + template + struct BoundModel + { + using Description = DescriptionT; + using Plan = PlanT; + std::shared_ptr model; + Description definition; + std::shared_ptr discretization; + Boundary boundary; + + /** @brief Forward an equation binding without copying finite-element resources. */ + template + [[nodiscard]] auto equation_discretization() const + { + return discretization->template equation_discretization(); + } + }; + + /** @brief Compile selections explicitly while leaving thermodynamic and equation evaluation to the graph. */ + template , typename Model, typename Discretization, typename Coefficients> + [[nodiscard]] auto bind_model_definition(std::shared_ptr model, std::shared_ptr discretization, + const Coefficients& coefficients) + { + using namespace utils::errors; + + serif_ensure(model != nullptr && discretization != nullptr, "Binding a stellar model requires both an owning physical description and an owning discretization."); + auto definition = resolve_equilibrium_equations(*model); + using Description = decltype(definition); + using AvailableFields = typename Coefficients::Keys; + using Plan = EquilibriumPlan; + static_assert(Plan::Surface::supported, + "The isobaric condition has no unique supported equation. Check the EOS relations, available boundary fields, derivatives and solved equation rows. For genuinely ambiguous rows an equation may name its IsobaricSurfaceField."); + detail::validate_coefficient_owners(*discretization, coefficients, typename Plan::Coefficients{}); + const auto boundary = [&]() + { + if constexpr (Plan::has_surface && Plan::Surface::supported) + { + using Formulation = typename Plan::Surface::Formulation; + return geometry::surface::backend::mfem::make_pressure_boundary(coefficients); + } + else + { + return NoSurfaceCondition{}; + } + }(); + using Binding = BoundModel; + return std::make_shared(Binding{.model = std::move(model), .definition = std::move(definition), + .discretization = std::move(discretization), .boundary = boundary}); + } + + namespace detail + { + /** @brief Track immutable physics with a policy type independent of the caller's field ordering. */ + template + [[nodiscard]] auto equation_of_state_input(const EOS& eos) { + using namespace utils::dependencies; + + return borrow(eos, track_by([](const EOS& value) { + return std::addressof(value); + })); + } + + /** @brief Count either backend displacement coefficients or independent surface coefficients. */ + template + [[nodiscard]] std::size_t geometry_size(const Geometry& coefficients) + { + if constexpr (requires { coefficients.Size(); }) + { + return static_cast(coefficients.Size()); + } + else + { + return coefficients.size(); + } + } + } + + /** @brief Build one input schema from an ordinary physical state and the selected geometry coordinates. */ + template + [[nodiscard]] auto equilibrium_inputs(const Bound& bound, const Coefficients& coefficients, const Geometry& geometry_coefficients) + { + namespace execution = operators::backend::mfem; + using Description = typename Bound::Description; + using Plan = typename Bound::Plan; + const auto& discretization = *bound.discretization; + detail::validate_coefficient_owners(discretization, coefficients, typename Plan::Coefficients{}); + const auto common = detail::model_inputs(discretization, coefficients, typename Description::InteriorEquations::Equations{}, typename Plan::Coefficients{}); + const auto deformation = [&]() + { + if constexpr (Plan::has_shape) + { + return geometry::deformation::backend::mfem::make_deformation_inputs(discretization.deformation.model, discretization.deformation.reference, geometry_coefficients); + } + else + { + return utils::dependencies::make_dependency_context(utils::dependencies::own( + execution::bind(discretization.template binding(), geometry_coefficients), discretization::backend::mfem::CoefficientTracking{})); + } + }(); + const auto boundary = [&]() + { + using namespace utils::dependencies; + + if constexpr (Plan::has_surface) + { + const auto eos = detail::equation_of_state_input(bound.model->equation_of_state()); + return geometry::surface::backend::mfem::make_pressure_boundary_metadata(bound.boundary, eos, bound.definition.surface_condition); + } + else + { + return make_dependency_context(); + } + }(); + return utils::dependencies::combine_dependency_contexts(common, deformation, boundary); + } + + /** @brief Resolve physical block sizes, including the explicitly supplied independent shape count. */ + template + [[nodiscard]] typename Plan::Layout equilibrium_layout(const Discretization& discretization, const Geometry& geometry_coefficients) { + return discretization::blocks::make_system_layout([&]() -> std::size_t { + if constexpr (std::same_as || std::same_as) { + return detail::geometry_size(geometry_coefficients); + } else { + return static_cast(discretization.template binding().size()); + } + }); + } +} diff --git a/src/include/serif/backend/mfem/model/computations/interior.hpp b/src/include/serif/backend/mfem/model/computations/interior.hpp new file mode 100644 index 00000000..e7693964 --- /dev/null +++ b/src/include/serif/backend/mfem/model/computations/interior.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include + +#include "serif/backend/mfem/model/system.hpp" +#include "serif/backend/mfem/model/volume_actions.hpp" +#include "serif/backend/mfem/model/volume.hpp" +#include "serif/backend/mfem/model/computations/shape_derivatives.hpp" +#include "serif/backend/mfem/model/computations/scalar_row.hpp" +#include "serif/backend/mfem/operators/volume_computations.hpp" +#include "serif/backend/mfem/operators/volume_field_block.hpp" + +namespace serif::model::computations { + /** @brief One equation's accepted contribution to the complete physical system. */ + template + struct InteriorContribution { + }; + + /** @brief Ordinary equations, point constraints and row replacements share one small assembly result. */ + struct Contributions final { + std::vector residual; + std::vector jacobian; + std::vector replacements; + /// Optional complete actions accelerate whole-system products while Jacobian blocks remain independently selectable. + std::optional> actions; + }; + + /** @brief Assemble field and optional geometry columns from the same retained volume result. */ + template + [[nodiscard]] Contributions volume_contribution(std::shared_ptr> accepted, + const Layout& layout, std::optional geometry = {}) { + using namespace utils::dependencies; + using namespace discretization::blocks; + namespace execution = operators::backend::mfem; + using Row = typename Equation::Test; + const int row_offset = static_cast(layout.template offset()); + Contributions result; + result.residual.push_back({row_offset, std::vector(accepted->residual.GetData(), accepted->residual.GetData() + accepted->residual.Size())}); + operators::EquationPlan::for_each_column([&](Column, std::size_t) { + using Field = typename Column::term_type; + if constexpr (ContainsKey) { + result.jacobian.push_back({row_offset, static_cast(layout.template offset()), execution::volume_field_block(accepted)}); + } + }); + if (geometry) { + result.jacobian.push_back({row_offset, geometry->column_offset, serif::operators::backend::mfem::compose_blocks(backend::mfem::volume_geometry_block(accepted), geometry->block)}); + } + result.actions = std::vector{{row_offset, 0, backend::mfem::volume_row_action(accepted, layout, geometry)}}; + return result; + } + + /** @brief Declare one owning contribution per equation; shape coordinates add one explicit shared prerequisite. */ + template + [[nodiscard]] auto interior_contribution(Layout layout) { + using namespace utils::dependencies; + using namespace discretization::blocks; + namespace execution = operators::backend::mfem; + using Row = typename Equation::Test; + using Volume = typename execution::volume_keys::Scoped::Linearization; + + if constexpr (GlobalScalarTerm) { + return make_owning_computation, InputList>>([layout](const auto& volume, const auto& row) { + const int offset = static_cast(layout.template offset()); + return Contributions{ + .residual = {{offset, std::vector(volume->residual.GetData(), volume->residual.GetData() + volume->residual.Size())}}, + .jacobian = {{offset, 0, *row}} + }; + }); + } else if constexpr (ContainsKey) { + return make_owning_computation, InputList>([layout](const auto& volume, const auto& shape) { + return volume_contribution(volume, layout, backend::mfem::ColumnContribution{ + static_cast(layout.template offset()), shape->tangent}); + }); + } + else { + return make_owning_computation, InputList>([layout](const auto& volume) { + return volume_contribution(volume, layout); + }); + } + } + + /** @brief Scalar equations publish a reusable functional before joining ordinary system assembly. */ + template + [[nodiscard]] auto interior_computations(const Layout& layout) { + if constexpr (discretization::blocks::GlobalScalarTerm) { + return std::tuple{scalar_residual_row(layout), interior_contribution(layout)}; + } else { + return std::tuple{interior_contribution(layout)}; + } + } +} // namespace serif::model::computations diff --git a/src/include/serif/backend/mfem/model/computations/physical_blocks.hpp b/src/include/serif/backend/mfem/model/computations/physical_blocks.hpp new file mode 100644 index 00000000..6eec760d --- /dev/null +++ b/src/include/serif/backend/mfem/model/computations/physical_blocks.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#include "serif/backend/mfem/model/system.hpp" +#include "serif/model/keys.hpp" +#include "serif/operators/linearized_system_view.hpp" +#include "serif/utils/dependencies/computation.hpp" + +namespace serif::model::computations { + namespace detail { + /** @brief Extract selected physical rows and columns in the caller's explicit order. */ + template + [[nodiscard]] auto physical_block(const backend::mfem::PhysicalSystem& system, const Layout& layout, + utils::dependencies::KeyList, utils::dependencies::KeyList) { + using Kind = discretization::blocks::RepresentedValueKind; + static_assert(sizeof...(Rows) > 0 && sizeof...(Fields) > 0, "Select at least one equation and one physical coordinate for a solver block."); + + const std::array rows{backend::mfem::CoordinateRange{static_cast(layout.template offset()), static_cast(layout.template size())}...}; + const std::array columns{backend::mfem::CoordinateRange{static_cast(layout.template offset()), static_cast(layout.template size())}...}; + return system.block(rows, columns); + } + } + + /** @brief Publish a requested physical block from the same physical root used for residual and Jacobian actions. */ + template + [[nodiscard]] auto physical_block(const Layout& layout) { + using namespace utils::dependencies; + using namespace utils::errors; + using Kind = discretization::blocks::RepresentedValueKind; + + serif_ensure(layout.template total_size() <= std::numeric_limits::max() && + layout.template total_size() <= std::numeric_limits::max(), "Physical solver block coordinates exceed MFEM's integer index range. Supply a representable system layout."); + return make_computation>([layout](const std::shared_ptr& system) { + serif_ensure(system != nullptr, "Physical solver blocks require an evaluated stellar system."); + serif_ensure(layout.template total_size() == static_cast(system->jacobian().action()->Height()) && + layout.template total_size() == static_cast(system->jacobian().action()->Width()), "The requested solver block layout does not match its frozen physical system. Use that system's coordinate description."); + return detail::physical_block(*system, layout, typename Block::Rows{}, typename Block::Values{}); + }); + } + + /** @brief Supply precisely the requested physical couplings to existing preconditioner computations. */ + template + [[nodiscard]] auto make_physical_block_computations(const Layout& layout, utils::dependencies::KeyList) { + return std::tuple{physical_block(layout)...}; + } +} diff --git a/src/include/serif/backend/mfem/model/computations/potential_infinity.hpp b/src/include/serif/backend/mfem/model/computations/potential_infinity.hpp new file mode 100644 index 00000000..f8596700 --- /dev/null +++ b/src/include/serif/backend/mfem/model/computations/potential_infinity.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "serif/backend/mfem/discretization/dof_selection.hpp" +#include "serif/backend/mfem/model/computations/interior.hpp" + +namespace serif::model::computations { + /** @brief The zero potential trace at compactified infinity, independent of the stellar isobar. */ + struct PotentialInfinityContribution {}; + + /** @brief Replace only the compactified-infinity potential rows by the zero physical trace. */ + template + [[nodiscard]] auto potential_infinity_contribution(Layout layout, const Binding& binding) { + using namespace utils::dependencies; + using namespace discretization::blocks; + using namespace utils::errors; + // Normalization selects this same fixed boundary on the same potential binding. + const auto selected = discretization::backend::mfem::select_boundary_dofs(binding); + return make_computation>( + [layout, selected](const auto& potential) { + selected.validate(); + serif_ensure(&potential.binding == &selected.binding() && potential.values.Size() == potential.binding.size(), + "Infinity trace coefficients must use the selected potential binding."); + const auto& reduced = selected.reduced_dofs(); + const int row_offset = static_cast(layout.template offset()); + auto trace = std::make_shared<::mfem::SparseMatrix>(reduced.Size(), potential.binding.size()); + backend::mfem::RowReplacement replacement; + for (int index = 0; index < reduced.Size(); ++index) { + replacement.selected_rows.push_back(row_offset + reduced[index]); + replacement.residual.push_back(potential.values[reduced[index]]); + trace->Add(index, reduced[index], 1.0); + } + trace->Finalize(); + replacement.jacobian.push_back({.column_offset = static_cast(layout.template offset()), + .block = operators::backend::mfem::linearized_block(std::move(trace))}); + Contributions result; + result.replacements.push_back(std::move(replacement)); + return result; + }); + } +} diff --git a/src/include/serif/backend/mfem/model/computations/pressure_boundary.hpp b/src/include/serif/backend/mfem/model/computations/pressure_boundary.hpp new file mode 100644 index 00000000..f619a767 --- /dev/null +++ b/src/include/serif/backend/mfem/model/computations/pressure_boundary.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include "serif/discretization/blocks/system_layout.hpp" +#include "serif/backend/mfem/geometry/surface/boundary_inputs.hpp" +#include "serif/backend/mfem/model/computations/interior.hpp" +#include "serif/backend/mfem/geometry/surface/pressure_field_block.hpp" + +namespace serif::model::computations +{ + /** @brief The complete isobaric replacement applied after ordinary and generated equation contributions. */ + struct PressureBoundaryContribution + { + }; + + /** + * @brief Publish accepted pressure rows and their solved-field derivatives in system coordinates. + * @note Prescribed EOS inputs still participate in surface evaluation, but have no Newton column. + * The compiled surface formulation selects its physical carrier; no enthalpy assumption enters assembly. + */ + template + requires utils::dependencies::ContainsKey + [[nodiscard]] auto pressure_boundary_contribution(Layout layout) + { + using namespace utils::dependencies; + using namespace utils::errors; + using namespace discretization::blocks; + namespace surface = geometry::surface; + namespace execution = operators::backend::mfem; + using Carrier = typename Formulation::CarrierTerm; + + serif_ensure(layout.template total_size() <= static_cast(std::numeric_limits::max()) && layout.template total_size() <= static_cast(std::numeric_limits::max()), "Pressure-boundary system coordinates must fit MFEM's integer index range."); + return make_computation>( + [layout](const std::vector>& linearizations, const auto& boundary) + { + serif_ensure(boundary != nullptr, "Pressure-boundary assembly requires its compiled trace owner."); + boundary->validate(); + surface::backend::mfem::detail::validate_boundary_samples(*boundary, linearizations); + serif_ensure(layout.template size() == static_cast(boundary->selected_rows->binding().size()), "The pressure carrier's residual layout must match its finite-element row binding."); + const int row_offset = static_cast(layout.template offset()); + backend::mfem::RowReplacement replacement; + replacement.selected_rows.reserve(linearizations.size()); + replacement.residual.reserve(linearizations.size()); + for (int index = 0; index < boundary->selected_rows->size(); ++index) + { + serif_ensure(std::isfinite(linearizations[index].residual), "A pressure replacement must contain finite accepted residuals."); + replacement.selected_rows.push_back(row_offset + boundary->selected_rows->reduced_dofs()[index]); + replacement.residual.push_back(linearizations[index].residual); + } + [&](KeyList) + { + const auto add_column = [&]() + { + if constexpr (ContainsKey) + { + auto matrix = surface::backend::mfem::pressure_field_trace(*boundary, linearizations); + serif_ensure(layout.template size() == static_cast(matrix->Width()), "A solved pressure input's coordinate count must match its finite-element trace."); + replacement.jacobian.push_back({.column_offset = static_cast(layout.template offset()), .block = execution::linearized_block(std::move(matrix))}); + } + }; + (add_column.template operator()(), ...); + }(typename Formulation::StateTerms{}); + Contributions result; + result.replacements.push_back(std::move(replacement)); + return result; + }); + } +} // namespace serif::model::computations diff --git a/src/include/serif/backend/mfem/model/computations/scalar_row.hpp b/src/include/serif/backend/mfem/model/computations/scalar_row.hpp new file mode 100644 index 00000000..47c5103e --- /dev/null +++ b/src/include/serif/backend/mfem/model/computations/scalar_row.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "serif/backend/mfem/model/scalar_row.hpp" +#include "serif/backend/mfem/model/volume_actions.hpp" +#include "serif/backend/mfem/model/computations/shape_derivatives.hpp" +#include "serif/backend/mfem/operators/volume_computations.hpp" + +namespace serif::model::computations { + /** @brief One global scalar equation's accepted derivative in complete physical state coordinates. */ + template + struct ScalarResidualRow { + }; + + /** @brief Rebuild a scalar functional only when its physical equation or shape derivative changes. */ + template + requires discretization::blocks::GlobalScalarTerm + [[nodiscard]] auto scalar_residual_row(Layout layout) { + using namespace utils::dependencies; + using namespace discretization::blocks; + using Row = typename Equation::Test; + using Volume = typename operators::backend::mfem::volume_keys::Scoped::Linearization; + + if constexpr (ContainsKey) { + return make_owning_computation, InputList>([layout](const auto& volume, const auto& shape) { + return backend::mfem::cache_scalar_row(backend::mfem::volume_row_action(volume, layout, backend::mfem::ColumnContribution{ + static_cast(layout.template offset()), shape->tangent})); + }); + } else { + return make_owning_computation, InputList>([layout](const auto& volume) { + return backend::mfem::cache_scalar_row(backend::mfem::volume_row_action(volume, layout)); + }); + } + } +} diff --git a/src/include/serif/backend/mfem/model/computations/shape_derivatives.hpp b/src/include/serif/backend/mfem/model/computations/shape_derivatives.hpp new file mode 100644 index 00000000..5a1742ce --- /dev/null +++ b/src/include/serif/backend/mfem/model/computations/shape_derivatives.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "serif/geometry/deformation/computations.hpp" +#include "serif/backend/mfem/model/deformation.hpp" + +namespace serif::model::computations { + struct ShapeDerivatives { + }; + + /** @brief Prepare the projected shape derivative once per evaluated geometry, retaining its computed owner. */ + [[nodiscard]] inline auto shape_derivatives(const discretization::backend::mfem::DofMap& binding) { + using namespace utils::dependencies; + + const auto projection = backend::mfem::displacement_projection(binding); + return make_owning_computation>([projection](const auto& deformation) { + return backend::mfem::ShapeDerivatives{.deformation = deformation, .projection = projection, + .tangent = serif::operators::backend::mfem::compose_blocks(projection, backend::mfem::deformation_tangent(deformation))}; + }); + } +} diff --git a/src/include/serif/backend/mfem/model/computations/solver_layout.hpp b/src/include/serif/backend/mfem/model/computations/solver_layout.hpp new file mode 100644 index 00000000..0d5b3b56 --- /dev/null +++ b/src/include/serif/backend/mfem/model/computations/solver_layout.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include "serif/discretization/blocks/keys.hpp" +#include "serif/discretization/blocks/system_layout.hpp" +#include "serif/utils/dependencies/computation.hpp" + +namespace serif::model::computations { + /** @brief Give normalization and preconditioning one producer for their shared coordinate layout. */ + template + [[nodiscard]] auto make_solver_layout_computations(const Layout& layout) { + using namespace utils::dependencies; + + return std::tuple{make_computation>([layout]() { + return layout; + })}; + } +} diff --git a/src/include/serif/backend/mfem/model/computations/surface_potential.hpp b/src/include/serif/backend/mfem/model/computations/surface_potential.hpp new file mode 100644 index 00000000..0f891e19 --- /dev/null +++ b/src/include/serif/backend/mfem/model/computations/surface_potential.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "serif/backend/mfem/model/surface_potential.hpp" +#include "serif/geometry/deformation/computations.hpp" +#include "serif/backend/mfem/model/computations/interior.hpp" + +namespace serif::model::computations { + struct ShapeContribution { + }; + + /** @brief Replace the volume force pullback by scalar effective-potential moments on the same surface. */ + template + [[nodiscard]] auto surface_potential_contribution(equations::SurfacePotentialBalance equation, Layout layout) { + using namespace utils::dependencies; + using namespace discretization::blocks; + namespace deformation = geometry::deformation; + using Equation = equations::SurfacePotentialBalance; + return make_computation>( + [equation, layout](const auto& discretization, const auto& reference, const auto& embedding, + const auto& displacement, const auto& potential, const auto& level, const auto& rotation) { + using namespace utils::errors; + serif_ensure(level.values.Size() == 1 && rotation.values.Size() == 1, + "The surface level and rigid angular velocity each require one coefficient."); + auto accepted = backend::mfem::assemble_surface_potential(equation, discretization, *reference, + *embedding, displacement, potential.binding, potential.values, level.values[0], rotation.values[0]); + const int row = static_cast(layout.template offset()); + Contributions result; + result.residual.push_back({.row_offset = row, .values = std::move(accepted.residual)}); + const auto add_column = [&](std::shared_ptr matrix) { + if constexpr (ContainsKey) { + result.jacobian.push_back({.row_offset = row, + .column_offset = static_cast(layout.template offset()), + .block = operators::backend::mfem::linearized_block(std::shared_ptr(std::move(matrix)))}); + } + }; + add_column.template operator()(std::move(accepted.potential)); + add_column.template operator()(std::move(accepted.level)); + add_column.template operator()(std::move(accepted.angular_velocity)); + add_column.template operator()(std::move(accepted.shape)); + return result; + }); + } +} diff --git a/src/include/serif/backend/mfem/model/deformation.hpp b/src/include/serif/backend/mfem/model/deformation.hpp new file mode 100644 index 00000000..8c1197ea --- /dev/null +++ b/src/include/serif/backend/mfem/model/deformation.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include "serif/geometry/deformation/linearization.hpp" +#include +#include "serif/backend/mfem/operators/actions.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::discretization::backend::mfem { + class DofMap; +} + +namespace serif::model::backend::mfem { + using operators::backend::mfem::LinearizedBlock; + + /** @brief Map the deformation's component ordering into the supported displacement ordering once. */ + [[nodiscard]] LinearizedBlock displacement_projection(const discretization::backend::mfem::DofMap& binding); + + /** @brief Share one accepted deformation and its projected derivative across all physical equations. */ + template + struct ShapeDerivatives { + std::shared_ptr deformation; + LinearizedBlock projection; + LinearizedBlock tangent; + }; + + namespace detail { + /** @brief Reject null accepted maps and dimensions that MFEM cannot represent. */ + template + void validate_deformation(const std::shared_ptr>& state) { + using namespace utils::errors; + + serif_ensure(state && state->map, "A model shape action requires an accepted deformation map."); + serif_ensure(state->map->input_size() <= static_cast(std::numeric_limits::max()) && state->map->output_size() <= static_cast(std::numeric_limits::max()), "Deformation coordinate counts exceed the MFEM integer range."); + serif_ensure(state->parameters.size() == state->map->input_size(), "The accepted deformation must contain every shape coordinate."); + } + + /** @brief Preserve the originating deformation diagnostic at the MFEM action boundary. */ + void require_deformation_action(const geometry::deformation::DeformationStatus& status); + } // namespace detail + + /** @brief Retain the accepted shape and expose its displacement tangent E and exact transpose E^T. */ + template + [[nodiscard]] LinearizedBlock deformation_tangent(std::shared_ptr> state) { + detail::validate_deformation(state); + const int rows = static_cast(state->map->output_size()); + const int columns = static_cast(state->map->input_size()); + return operators::backend::mfem::linearized_action(rows, columns, [state, workspace = std::make_shared>(geometry::deformation::make_deformation_workspace(*state->map))](const ::mfem::Vector& input, ::mfem::Vector& output) mutable { + detail::require_deformation_action(geometry::deformation::apply_jacobian(*state, std::span(input.GetData(), input.Size()), std::span(output.GetData(), output.Size()), *workspace)); + }, [state, workspace = std::make_shared>(geometry::deformation::make_deformation_workspace(*state->map))](const ::mfem::Vector& input, ::mfem::Vector& output) mutable { + detail::require_deformation_action(geometry::deformation::apply_jacobian_transpose(*state, std::span(input.GetData(), input.Size()), std::span(output.GetData(), output.Size()), *workspace)); + }, [state, workspaces = std::vector>>{}](std::span inputs, std::span<::mfem::Vector* const> outputs) mutable { + // Directions are independent; each worker owns its workspace, so every column is computed as by Mult. + const auto workers = static_cast(utils::parallel::worker_count()); + while (workspaces.size() < workers) { + workspaces.push_back(std::make_shared>(geometry::deformation::make_deformation_workspace(*state->map))); + } + utils::parallel::for_each_index(static_cast(inputs.size()), [&](const int index, const int worker) { + const auto& input = *inputs[static_cast(index)]; + auto& output = *outputs[static_cast(index)]; + detail::require_deformation_action(geometry::deformation::apply_jacobian(*state, std::span(input.GetData(), input.Size()), std::span(output.GetData(), output.Size()), *workspaces[static_cast(worker)])); + }); + }); + } +} // namespace serif::model::backend::mfem diff --git a/src/include/serif/backend/mfem/model/discretization.hpp b/src/include/serif/backend/mfem/model/discretization.hpp new file mode 100644 index 00000000..1ca75a4b --- /dev/null +++ b/src/include/serif/backend/mfem/model/discretization.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include + +#include "serif/backend/mfem/operators/volume_data.hpp" +#include "serif/backend/mfem/geometry/deformation/inputs.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::model::backend::mfem +{ + /** @brief Prescribe displacement externally when no shape unknown is selected. */ + struct NoDeformation + { + }; + + /** @brief Numerical surface representation and its reference geometry, separate from force equations. */ + template + struct DeformationDiscretization + { + DomainModel model; + geometry::deformation::backend::mfem::ReferenceDiscretization reference; + }; + + /** + * @brief Bind physical terms to existing finite-element resources with one explicit lifetime owner. + * @note This is a numerical binding, not a stellar-model description. The owner must own every + * referenced mesh, space, binding, mapper and compactification field. No equation is evaluated here. + */ + template + struct StellarDiscretization final + { + std::shared_ptr owner; + Bindings term_bindings; + const ::mfem::GridFunction& compactification; + const mapping::DomainMapper& mapper; + Deformation deformation{}; + + /** @brief Retrieve a physical term's existing typed finite-element binding. */ + template + [[nodiscard]] const auto& binding() const + { + return term_bindings.template get(); + } + + /** @brief Describe one equation's discretization using the common physical resources. */ + template + [[nodiscard]] auto equation_discretization() const + { + using Binding = std::remove_cvref_t())>; + using Schema = typename Binding::Schema; + + return operators::backend::mfem::VolumeDiscretization{ + .residual_binding = binding(), + .displacement_binding = binding(), + .compactification = compactification, + .mapper = mapper + }; + } + }; + + /** @brief Retain the supplied resource owner without copying finite-element spaces or changing their addresses. */ + template + [[nodiscard]] auto make_stellar_discretization(std::shared_ptr owner, Bindings bindings, + const ::mfem::GridFunction& compactification, const mapping::DomainMapper& mapper) + { + using namespace utils::errors; + + serif_ensure(owner != nullptr, "A stellar discretization requires an owner for its borrowed finite-element resources."); + return StellarDiscretization, Bindings>{ + .owner = std::move(owner), .term_bindings = std::move(bindings), + .compactification = compactification, .mapper = mapper + }; + } + + /** @brief Add an explicit deformation prescription without changing the physical equation definition. */ + template + [[nodiscard]] auto with_deformation(StellarDiscretization discretization, DomainModel model, + geometry::deformation::backend::mfem::ReferenceDiscretization reference) + { + return StellarDiscretization>{ + .owner = std::move(discretization.owner), .term_bindings = std::move(discretization.term_bindings), + .compactification = discretization.compactification, .mapper = discretization.mapper, + .deformation = {.model = std::move(model), .reference = std::move(reference)} + }; + } +} diff --git a/src/include/serif/backend/mfem/model/equilibrium.hpp b/src/include/serif/backend/mfem/model/equilibrium.hpp new file mode 100644 index 00000000..3f3a49d7 --- /dev/null +++ b/src/include/serif/backend/mfem/model/equilibrium.hpp @@ -0,0 +1,200 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/model/equilibrium_evaluator.hpp" +#include "serif/backend/mfem/model/discretization.hpp" +#include "serif/model/configured_discretization.hpp" +#include "serif/backend/mfem/model/normalization/computations.hpp" +#include "serif/backend/mfem/model/graph.hpp" +#include "serif/model/execution_validation.hpp" + +namespace serif::model::backend::mfem { + namespace detail { + /** @brief Select physical coordinates without adding normalization graph services. */ + struct PhysicalEvaluation final {}; + + /** @brief Ordinary model evaluation adds no caller-selected preparation computations. */ + struct NoAdditionalComputations { + /** @brief Separate computations needed for every evaluation from those needed only for linear preparation. */ + auto operator()(const auto&, const auto&, auto, const auto&) const { return std::pair{std::tuple{}, std::tuple{}}; } + }; + + /** @brief Derive requested result identities directly from the supplied computation declarations. */ + template + [[nodiscard]] auto computation_keys(const std::tuple&) { + return utils::dependencies::KeyList{}; + } + + /** @brief Bind physics once and add only the explicitly selected numerical services. */ + template + [[nodiscard]] auto bind_evaluator(std::shared_ptr model, std::shared_ptr discretization, + const Coefficients& coefficients, const Geometry& geometry, operators::backend::mfem::VolumeOptions options, NumericalOptions numerical_options = {}, Preconditioner preconditioner = {}, Services services = {}, ObserverFactory observer_factory = {}) { + const auto bound = bind_model_definition(std::move(model), std::move(discretization), coefficients); + using Bound = typename decltype(bound)::element_type; + using Plan = typename Bound::Plan; + static_assert(executable_model_topology(), + "The selected model needs complete equation and prescription bindings, including explicit generated-coordinate effects. This check does not prove mathematical closure."); + const auto layout = equilibrium_layout(*bound->discretization, geometry); + const auto construct = [&](ResultKey, auto numerical, auto additional) { + auto inverse = [&]() { + if constexpr (std::same_as) { + return std::tuple{}; + } else { + return preconditioning::make_computations(preconditioner, layout, numerical); + } + }(); + auto numerical_services = std::tuple_cat(std::move(numerical), std::move(inverse)); + auto extensions = services(bound, layout, ResultKey{}, numerical_services); + using EvaluationServices = decltype(computation_keys(extensions.first)); + using LinearServices = decltype(computation_keys(extensions.second)); + auto graph = make_equilibrium_graph(bound, layout, + std::tuple_cat(std::move(numerical_services), std::move(extensions.first), std::move(extensions.second)), options); + const auto inputs = utils::dependencies::combine_dependency_contexts(candidate_inputs(*bound, coefficients, geometry), additional); + // Observation sees the final graph, but does not become a physical input or invalidate cached results. + auto observers = std::invoke(observer_factory, std::type_identity{}); + return EquilibriumEvaluator, decltype(graph), std::remove_cvref_t, decltype(additional), ResultKey, EvaluationServices, LinearServices, decltype(observers)>( + bound, std::move(graph), layout, std::move(additional), std::move(observers)); + }; + if constexpr (std::same_as) { + return construct(keys::PhysicalSystem{}, std::tuple{}, utils::dependencies::make_dependency_context()); + } else { + auto resolved = normalization::resolve_configuration(std::move(numerical_options), *bound->model); + using NormalizationPlan = normalization::PlanFor; + static_assert(solver::normalization::NormalizationPreparable, + "The normalization plan must provide one compatible rule for every unknown and residual row."); + const auto configuration = std::make_shared(std::move(resolved)); + const auto metrics = normalization::MetricsFor{}; + const auto additional = normalization::make_inputs(configuration, *bound->discretization, metrics); + const auto numerical = normalization::make_computations(*bound, *configuration, layout, metrics); + return construct(normalization::PreparedSystem{}, numerical, additional); + } + } + + /** @brief Retain finite-element bindings before constructing temporary views into an owning state. */ + template + [[nodiscard]] auto bind_state_evaluator(Model model, Discretization discretization, const StellarState& state, + const Geometry& geometry, operators::backend::mfem::VolumeOptions options, const Inputs& prescribed, NumericalOptions numerical = {}, Preconditioner preconditioner = {}, Services services = {}, ObserverFactory observer_factory = {}) { + using namespace utils::errors; + using PrescribedKeys = utils::dependencies::CombinedKeyList; + const auto owner = std::make_shared(std::move(discretization)); + const auto fields = state_coefficients(*owner, state, prescribed); + const auto coefficients = fields.context(); + auto evaluator = bind_evaluator(std::make_shared(std::move(model)), + owner, coefficients, geometry, options, std::move(numerical), std::move(preconditioner), std::move(services), std::move(observer_factory)); + static_assert(std::same_as::Layout, typename decltype(evaluator)::Layout>, + "The state's physical coordinate identities must match the selected equations and prescribed fields."); + serif_ensure(state.layout() == evaluator.layout(), + "The stellar state's field or equation sizes do not match the bound equations. Construct it with the matching physical layout."); + return evaluator; + } + } + + /** + * @brief Bind explicitly selected physics and external field views; initial values establish input types only. + * @note The optional final factory receives the completed graph type and supplies cache observers. + * The default installs no observers; timing and other observation are not physical inputs. + */ + template , + utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings, + utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model, + typename Discretization, typename Coefficients, typename Geometry, typename ObserverFactory = utils::dependencies::NoGraphObservers> + requires requires { typename Coefficients::Keys; } + [[nodiscard]] auto bind_stellar_model(Model model, Discretization discretization, const Coefficients& coefficients, + const Geometry& geometry, operators::backend::mfem::VolumeOptions options = {}, ObserverFactory observer_factory = {}) { + return detail::bind_evaluator(std::make_shared(std::move(model)), + std::make_shared(std::move(discretization)), coefficients, geometry, options, detail::PhysicalEvaluation{}, preconditioning::None{}, detail::NoAdditionalComputations{}, std::move(observer_factory)); + } + + /** + * @brief Bind configured numerical coordinates with external physical field views and geometry. + * @note The optional final factory receives the completed graph type and supplies cache observers. + * The default installs no observers; timing and other observation are not physical inputs. + */ + template , + utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings, + utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model, + typename Discretization, typename Options, typename Preconditioner, typename Coefficients, typename Geometry, typename ObserverFactory = utils::dependencies::NoGraphObservers> + requires requires { typename Coefficients::Keys; } + [[nodiscard]] auto bind_stellar_model(DiscretizationOptions configuration, + const Coefficients& coefficients, const Geometry& geometry, operators::backend::mfem::VolumeOptions options = {}, ObserverFactory observer_factory = {}) { + return detail::bind_evaluator(std::make_shared(std::move(configuration.model)), + std::make_shared(std::move(configuration.finite_elements)), coefficients, geometry, options, std::move(configuration.normalization), std::move(configuration.preconditioner), detail::NoAdditionalComputations{}, std::move(observer_factory)); + } + + /** + * @brief Bind a moving star with all solved physical coefficients and shape coordinates in one state. + * @note The optional final factory receives the completed graph type and supplies cache observers. + * The default installs no observers; timing and other observation are not physical inputs. + */ + template , + utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings, + utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model, + typename Discretization, typename Layout, utils::dependencies::IsDependencyContext Inputs = utils::dependencies::DependencyContext<>, + typename ObserverFactory = utils::dependencies::NoGraphObservers> + requires utils::dependencies::ContainsKey + [[nodiscard]] auto bind_stellar_model(Model model, Discretization discretization, const StellarState& state, + operators::backend::mfem::VolumeOptions options = {}, const Inputs& prescribed = Inputs{}, ObserverFactory observer_factory = {}) { + return detail::bind_state_evaluator(std::move(model), std::move(discretization), state, + state.template get(), options, prescribed, detail::PhysicalEvaluation{}, preconditioning::None{}, detail::NoAdditionalComputations{}, std::move(observer_factory)); + } + + /** + * @brief Bind a fixed-geometry star while keeping prescribed displacement outside the solved coordinates. + * @note The optional final factory receives the completed graph type and supplies cache observers. + * The default installs no observers; timing and other observation are not physical inputs. + */ + template , + utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings, + utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model, + typename Discretization, typename Layout, utils::dependencies::IsDependencyContext Inputs = utils::dependencies::DependencyContext<>, + typename ObserverFactory = utils::dependencies::NoGraphObservers> + requires (!utils::dependencies::ContainsKey) + [[nodiscard]] auto bind_stellar_model(Model model, Discretization discretization, const StellarState& state, const ::mfem::Vector& displacement, + operators::backend::mfem::VolumeOptions options = {}, const Inputs& prescribed = Inputs{}, ObserverFactory observer_factory = {}) { + return detail::bind_state_evaluator(std::move(model), std::move(discretization), state, + displacement, options, prescribed, detail::PhysicalEvaluation{}, preconditioning::None{}, detail::NoAdditionalComputations{}, std::move(observer_factory)); + } + + /** + * @brief Bind a moving state with normalization defaults resolved from its authoritative physical model. + * @note The optional final factory receives the completed graph type and supplies cache observers. + * The default installs no observers; timing and other observation are not physical inputs. + */ + template , + utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings, + utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model, + typename Discretization, typename Options, typename Preconditioner, typename Layout, utils::dependencies::IsDependencyContext Inputs = utils::dependencies::DependencyContext<>, + typename Services = detail::NoAdditionalComputations, typename ObserverFactory = utils::dependencies::NoGraphObservers> + requires utils::dependencies::ContainsKey + [[nodiscard]] auto bind_stellar_model(DiscretizationOptions configuration, const StellarState& state, + operators::backend::mfem::VolumeOptions options = {}, const Inputs& prescribed = Inputs{}, Services services = {}, ObserverFactory observer_factory = {}) { + return detail::bind_state_evaluator(std::move(configuration.model), std::move(configuration.finite_elements), state, + state.template get(), options, prescribed, std::move(configuration.normalization), std::move(configuration.preconditioner), std::move(services), std::move(observer_factory)); + } + + /** + * @brief Bind normalized fixed-geometry evaluation without introducing displacement unknowns. + * @note The optional final factory receives the completed graph type and supplies cache observers. + * The default installs no observers; timing and other observation are not physical inputs. + */ + template , + utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings, + utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model, + typename Discretization, typename Options, typename Preconditioner, typename Layout, utils::dependencies::IsDependencyContext Inputs = utils::dependencies::DependencyContext<>, + typename Services = detail::NoAdditionalComputations, typename ObserverFactory = utils::dependencies::NoGraphObservers> + requires (!utils::dependencies::ContainsKey) + [[nodiscard]] auto bind_stellar_model(DiscretizationOptions configuration, const StellarState& state, + const ::mfem::Vector& displacement, operators::backend::mfem::VolumeOptions options = {}, const Inputs& prescribed = Inputs{}, Services services = {}, ObserverFactory observer_factory = {}) { + return detail::bind_state_evaluator(std::move(configuration.model), std::move(configuration.finite_elements), state, + displacement, options, prescribed, std::move(configuration.normalization), std::move(configuration.preconditioner), std::move(services), std::move(observer_factory)); + } +} diff --git a/src/include/serif/backend/mfem/model/equilibrium_evaluator.hpp b/src/include/serif/backend/mfem/model/equilibrium_evaluator.hpp new file mode 100644 index 00000000..92966baa --- /dev/null +++ b/src/include/serif/backend/mfem/model/equilibrium_evaluator.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include +#include + +#include "serif/backend/mfem/model/binding.hpp" +#include "serif/backend/mfem/model/failure.hpp" +#include "serif/backend/mfem/model/state.hpp" +#include "serif/backend/mfem/model/preconditioning/computations.hpp" +#include "serif/backend/mfem/shared/finite.hpp" +#include "serif/utils/dependencies/computation_cache.hpp" + +namespace serif::model::backend::mfem { + namespace detail { + /** @brief Use one shape-view input schema for both packed states and external coefficient containers. */ + template + [[nodiscard]] auto candidate_inputs(const Bound& bound, const Coefficients& coefficients, const Geometry& geometry) { + if constexpr (Bound::Plan::has_shape) { + return equilibrium_inputs(bound, coefficients, std::span{geometry}); + } else { + return equilibrium_inputs(bound, coefficients, geometry); + } + } + } + + /** + * @brief Evaluate explicit stellar candidates using one reusable dependency cache. + * @note Successful evaluation returns an owning frozen result; it does not accept a solver step. + * Retain that handle while evaluating other candidates. Numerical rejection makes the current cache + * unavailable without changing previously returned results. This object never owns the caller's state. + */ + template , + typename ResultKey = keys::PhysicalSystem, + utils::dependencies::IsKeyList EvaluationServices = utils::dependencies::KeyList<>, + utils::dependencies::IsKeyList LinearServices = utils::dependencies::KeyList<>, + typename Observers = utils::dependencies::Observers<>> + class EquilibriumEvaluator final { + public: + using Plan = typename Bound::Plan; + using Layout = typename Plan::Layout; + using SurfaceFormulation = typename Plan::Surface::Formulation; + using Cache = utils::dependencies::ComputationCache; + using Result = std::remove_cvref_t().template get())>; + using Evaluation = std::expected; + + template + using EvaluationFor = std::expected().template get())>, typename Cache::Failure>; + + /** @brief Retain the physical and finite-element owners before constructing the reusable cache. */ + EquilibriumEvaluator(std::shared_ptr bound, Graph graph, Layout layout, AdditionalInputs additional, Observers observers = Observers{}) : + m_bound(std::move(bound)), m_cache(std::move(graph), std::move(observers)), m_layout(std::move(layout)), m_additional(std::move(additional)) { + } + + /** @brief Inspect the independently ordered physical unknown and residual coordinates. */ + [[nodiscard]] const Layout& layout() const noexcept { + return m_layout; + } + + /** @brief Inspect the latest evaluation's work report and requested graph services. */ + [[nodiscard]] const Cache& cache() const noexcept { + return m_cache; + } + + /** @brief Inspect the immutable physical definition used by every evaluation. */ + [[nodiscard]] const auto& model() const noexcept { + return *m_bound->model; + } + + /** @brief Inspect the owned physical and FE bindings for solver geometry and numerical policies. */ + [[nodiscard]] const Bound& binding() const noexcept { + return *m_bound; + } + + private: + /** + * @brief Evaluate explicitly bound fields and geometry, allowing arbitrary caller-owned storage. + * @note Nonfinite coefficients are numerical rejections. Incorrect bindings and sizes are caller + * errors. Either failure invalidates current cache access before returning or reporting the error. + */ + template + requires (detail::coefficient_inputs(typename Plan::Coefficients{})) + [[nodiscard]] auto evaluate_result(const Coefficients& coefficients, const Geometry& geometry, + utils::dependencies::PreparationMode mode = utils::dependencies::PreparationMode::reuse_unchanged) -> EvaluationFor { + m_cache.clear_preparation(); + const bool finite = [&](utils::dependencies::KeyList) { + return (utils::checks::is_finite(coefficients.template get().values) && ...); + }(typename Plan::Coefficients{}); + if (!finite) { + return std::unexpected(typename Cache::Failure{.computation_index = Graph::size, + .error = EquilibriumFailure{operators::backend::mfem::PreparationRejection{ + .reason = operators::backend::mfem::PreparationRejectionReason::NON_FINITE, + .message = "A stellar candidate contains a non-finite physical coefficient. Supply finite field and scalar values before evaluating it." + }}}); + } + const auto inputs = utils::dependencies::combine_dependency_contexts(detail::candidate_inputs(*m_bound, coefficients, geometry), m_additional); + using Requested = utils::dependencies::CombinedKeyList, EvaluationServices, + std::conditional_t, LinearServices, utils::dependencies::KeyList<>>>; + const auto preparation = m_cache.try_prepare(inputs, Requested{}, mode); + if (!preparation) { + return std::unexpected(preparation.error()); + } + return m_cache.template get(); + } + + /** @brief Evaluate a moving star directly from its physical fields and shape in one owning state. */ + template > + requires Plan::has_shape && detail::PrescribedInputsFor + [[nodiscard]] auto evaluate_result(const StellarState& state, const Prescribed& prescribed = Prescribed{}, + utils::dependencies::PreparationMode mode = utils::dependencies::PreparationMode::reuse_unchanged) -> EvaluationFor { + validate_state(state); + const auto fields = detail::state_coefficients(*m_bound->discretization, state, prescribed); + const auto coefficients = fields.context(); + return evaluate_result(coefficients, state.template get(), mode); + } + + /** @brief Evaluate a fixed-geometry star; prescribed displacement is independent of its solved fields. */ + template > + requires (!Plan::has_shape) && detail::PrescribedInputsFor + [[nodiscard]] auto evaluate_result(const StellarState& state, const ::mfem::Vector& displacement, const Prescribed& prescribed = Prescribed{}, + utils::dependencies::PreparationMode mode = utils::dependencies::PreparationMode::reuse_unchanged) -> EvaluationFor { + validate_state(state); + const auto fields = detail::state_coefficients(*m_bound->discretization, state, prescribed); + const auto coefficients = fields.context(); + return evaluate_result(coefficients, displacement, mode); + } + + /** @brief Reject a same-type state with incompatible coordinate sizes before constructing backend views. */ + void validate_state(const StellarState& state) { + using namespace utils::errors; + m_cache.clear_preparation(); + serif_ensure(state.layout() == m_layout, + "The stellar state's field or equation sizes differ from this evaluator's layout. Construct the state with the matching discretization layout."); + } + + public: + /** + * @brief Evaluate a candidate without preparing its inverse. + * @param inputs An owning state (and prescribed inputs), or explicit coefficient and geometry views. + * Fixed-geometry states also take the prescribed displacement. A final PreparationMode is optional. + * Selected evaluation services are prepared in the same transaction; linear-only services remain unrequested. + */ + template + requires requires(EquilibriumEvaluator& evaluator, const InputsT&... inputs) { + evaluator.template evaluate_result(inputs...); + } + [[nodiscard]] Evaluation evaluate(const InputsT&... inputs) { + return evaluate_result(inputs...); + } + + /** + * @brief Prepare an owning Jacobian and matching inverse only when a linear correction is needed. + * @param inputs The same candidate and optional preparation arguments accepted by evaluate(). + * @note A numerical inverse failure preserves every previously returned preparation. + */ + template + requires utils::dependencies::ContainsKey && + requires(EquilibriumEvaluator& evaluator, const InputsT&... inputs) { + evaluator.template evaluate_result(inputs...); + } + [[nodiscard]] auto prepare_linear_system(const InputsT&... inputs) -> decltype(evaluate_result(inputs...)) { + return evaluate_result(inputs...); + } + + private: + std::shared_ptr m_bound; + Cache m_cache; + Layout m_layout; + AdditionalInputs m_additional; + }; + +} diff --git a/src/include/serif/backend/mfem/model/failure.hpp b/src/include/serif/backend/mfem/model/failure.hpp new file mode 100644 index 00000000..b2d3a303 --- /dev/null +++ b/src/include/serif/backend/mfem/model/failure.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include "serif/geometry/deformation/failure.hpp" +#include "serif/geometry/surface/linearization.hpp" +#include "serif/backend/mfem/operators/rejection.hpp" +#include "serif/solver/preconditioner/failure.hpp" + +namespace serif::model::backend::mfem { + /** @brief Retain the original physical or numerical preparation failure and its diagnostic location. */ + using EquilibriumFailure = std::variant; +} diff --git a/src/include/serif/backend/mfem/model/finite_elements.hpp b/src/include/serif/backend/mfem/model/finite_elements.hpp new file mode 100644 index 00000000..557ca884 --- /dev/null +++ b/src/include/serif/backend/mfem/model/finite_elements.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include "serif/backend/mfem/discretization/resources.hpp" +#include "serif/backend/mfem/discretization/term_binding.hpp" +#include "serif/backend/mfem/model/discretization.hpp" +#include "serif/model/equilibrium_plan.hpp" +#include "serif/model/mesh_options.hpp" +#include "serif/model/stellar_model.hpp" + +namespace serif::model::backend::mfem { + /** @brief Generate physical and logical meshes with the standard core/envelope/vacuum labels. */ + [[nodiscard]] discretization::backend::mfem::Resources make_stellar_resources(const StellarMeshOptions& options); + + /** @brief Share identical collections and spaces, retaining native coefficient numbering and storage. */ + [[nodiscard]] ::mfem::FiniteElementSpace& make_space(discretization::backend::mfem::Resources& resources, + std::unique_ptr<::mfem::FiniteElementCollection> collection, int components, int ordering = ::mfem::Ordering::byNODES); + + namespace detail { + /** @brief Recognize a field-specific reconstruction request without prescribing its numerical form. */ + template + concept ReconstructionSpecification = requires { typename Specification::Term; }; + + /** @brief Attach a requested numerical interpretation while the bindings still have a single mutable owner. */ + template + void apply_reconstruction(Owner& owner, utils::dependencies::KeyList, const Specification& specification) { + using namespace utils::errors; + using Term = typename Specification::Term; + using Keys = utils::dependencies::KeyList; + if constexpr (!(std::same_as || ...)) { + serif_error("The reconstruction's field is not present in this stellar model."); + } else { + auto& binding = std::get()>(owner.bindings); + using discretization::backend::mfem::make_reconstruction; + if constexpr (requires { make_reconstruction(specification, *binding); }) { + auto plan = make_reconstruction(specification, *binding); + if constexpr (discretization::blocks::SpatialTerm) { + using Binding = typename Owner::template Binding; + binding = std::make_unique(binding->space(), std::move(plan)); + } else { + serif_error("Spatial reconstruction cannot be attached to a global scalar field."); + } + } else { + serif_error("This reconstruction form has no preparation function for the selected backend and field."); + } + } + } + + /** @brief Reject duplicate field requests before constructing any finite elements. */ + template + void validate_reconstruction_requests() { + using namespace utils::errors; + constexpr bool unique = (([]() { + return (0 + ... + int(std::same_as)) == 1; + }.template operator()()) && ...); + serif_ensure(unique, "Specify at most one reconstruction for each field."); + } + + /** @brief Construct only representations supported by TermBinding, directly from the term's compile-time descriptor. */ + template + [[nodiscard]] ::mfem::FiniteElementSpace& term_space(discretization::backend::mfem::Resources& resources) { + using Traits = discretization::blocks::SpatialTermTraits; + using enum discretization::functional_space::Family; + + static_assert(Traits::rank <= 1 && (Traits::family == h1 || Traits::family == l2), + "The selected backend supports scalar/vector H1 and L2 terms."); + constexpr int components = Traits::rank == 0 ? 1 : 3; + if constexpr (Traits::family == h1) { + return make_space(resources, std::make_unique<::mfem::H1_FECollection>(Traits::order, 3), components, components == 1 ? ::mfem::Ordering::byNODES : ::mfem::Ordering::byVDIM); + } else { + return make_space(resources, std::make_unique<::mfem::L2_FECollection>(Traits::order, 3), components, components == 1 ? ::mfem::Ordering::byNODES : ::mfem::Ordering::byVDIM); + } + } + + /** @brief Own precisely the model's coefficients plus displacement geometry; no particular thermodynamic field is required. */ + template + [[nodiscard]] auto make_bindings(const StellarMeshOptions& options, utils::dependencies::KeyList keys, const Specifications&... specifications) { + using namespace utils::dependencies; + using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema; + using Context = decltype(make_dependency_context(borrow(std::declval&>())...)); + using Owner = discretization::backend::mfem::OwnedBindings; + + validate_reconstruction_requests(); + auto owner = std::make_shared(); + owner->resources = make_stellar_resources(options); + ([&] { + using Binding = typename Owner::template Binding; + auto& binding = std::get>()>(owner->bindings); + if constexpr (discretization::blocks::SpatialTerm) { + binding = std::make_unique(term_space(owner->resources)); + } else { + binding = std::make_unique(*owner->resources.meshes.front()); + } + }(), ...); + (apply_reconstruction(*owner, keys, specifications), ...); + return owner; + } + } + + /** + * @brief Create an owning fixed-geometry discretization using the physical model's term descriptors. + * @note This convenience factory uses the standard spherical mesh and domain schema. The existing + * owner-and-bindings overload remains available for custom meshes, schemas, and native spaces. + */ + template + [[nodiscard]] auto make_stellar_discretization(const Model&, const StellarMeshOptions& options = {}, const Specifications&... specifications) { + using Description = decltype(resolve_equilibrium_equations(std::declval())); + using Plan = EquilibriumPlan; + using Terms = utils::dependencies::CombinedKeyList>; + + auto owner = detail::make_bindings(options, Terms{}, specifications...); + return make_stellar_discretization(owner, owner->context(), *owner->resources.compactification, *owner->resources.mapper); + } + + /** @brief Add a moving surface with reference companions chosen from displacement geometry, independently of EOS variables. */ + template + [[nodiscard]] auto make_stellar_discretization(const Model& model, const StellarMeshOptions& options, + geometry::deformation::DomainDeformation deformation, const Specifications&... specifications) { + using Description = decltype(resolve_equilibrium_equations(std::declval())); + using Plan = EquilibriumPlan; + using Terms = utils::dependencies::CombinedKeyList>; + + auto owner = detail::make_bindings(options, Terms{}, specifications...); + using Displacement = discretization::blocks::displacement::vector; + const auto context = owner->context(); + const auto* collection = context.template get().space().FEColl(); + auto& scalar = make_space(owner->resources, std::unique_ptr<::mfem::FiniteElementCollection>{::mfem::FiniteElementCollection::New(collection->Name())}, 1); + auto& reference_displacement = make_space(owner->resources, + std::unique_ptr<::mfem::FiniteElementCollection>{::mfem::FiniteElementCollection::New(collection->Name())}, 3, ::mfem::Ordering::byNODES); + const auto reference = geometry::deformation::backend::mfem::deformation_discretization( + scalar, reference_displacement, *owner->resources.meshes[1]); + return with_deformation(make_stellar_discretization(owner, context, *owner->resources.compactification, *owner->resources.mapper), + std::move(deformation), reference); + } +} diff --git a/src/include/serif/backend/mfem/model/graph.hpp b/src/include/serif/backend/mfem/model/graph.hpp new file mode 100644 index 00000000..206da3bd --- /dev/null +++ b/src/include/serif/backend/mfem/model/graph.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include +#include +#include +#include + +#include "serif/backend/mfem/model/computations/interior.hpp" +#include "serif/backend/mfem/model/computations/surface_potential.hpp" +#include "serif/backend/mfem/model/computations/potential_infinity.hpp" +#include "serif/backend/mfem/model/computations/pressure_boundary.hpp" +#include "serif/model/equilibrium_plan.hpp" +#include "serif/backend/mfem/geometry/deformation/computations.hpp" +#include "serif/backend/mfem/model/volume_displacement.hpp" +#include "serif/backend/mfem/geometry/surface/boundary_computations.hpp" +#include "serif/model/keys.hpp" +#include "serif/model/plan.hpp" + +namespace serif::model { + namespace detail { + /** @brief Collect all additions before applying any boundary replacement. */ + template + [[nodiscard]] auto physical_system_computation(Layout layout, std::shared_ptr owner, + utils::dependencies::KeyList) { + using namespace utils::dependencies; + using namespace discretization::blocks; + using namespace utils::errors; + + const std::size_t rows = layout.template total_size(); + const std::size_t columns = layout.template total_size(); + serif_ensure(rows <= std::numeric_limits::max() && columns <= std::numeric_limits::max(), "Stellar system dimensions exceed the backend's integer range."); + + return make_computation>([layout, owner = std::move(owner)](const auto&... contributions) { + computations::Contributions combined; + combined.actions.emplace(); + const auto append = [&](const computations::Contributions& part) { + combined.residual.insert(combined.residual.end(), part.residual.begin(), part.residual.end()); + combined.jacobian.insert(combined.jacobian.end(), part.jacobian.begin(), part.jacobian.end()); + combined.replacements.insert(combined.replacements.end(), part.replacements.begin(), part.replacements.end()); + const auto& actions = part.actions ? *part.actions : part.jacobian; + combined.actions->insert(combined.actions->end(), actions.begin(), actions.end()); + }; + (append(contributions), ...); + return backend::mfem::make_physical_system( + static_cast(layout.template total_size()), + static_cast(layout.template total_size()), + std::move(combined.residual), std::move(combined.jacobian), std::move(combined.replacements), owner, std::move(combined.actions)); + }); + } + + /** @brief Every interior equation contributes once; extension keys identify explicit additions or replacements. */ + template + [[nodiscard]] consteval auto contribution_keys(utils::types::TypeList, utils::dependencies::KeyList) { + return utils::dependencies::KeyList..., Additional...>{}; + } + + /** @brief Share each immutable equation with its volume graph, then publish one complete row contribution. */ + template + [[nodiscard]] auto interior_computations(const std::shared_ptr& registry, const Discretization& discretization, + const Layout& layout, operators::backend::mfem::VolumeOptions options, utils::types::TypeList) { + return std::tuple_cat(operators::backend::mfem::make_volume_equation_computations( + std::shared_ptr(registry, ®istry->template get()), + discretization.template equation_discretization(), options)..., + computations::interior_computations(layout)...); + } + } + + /** + * @brief Construct the complete stellar graph at one explicit composition point. + * @note The registry is the authoritative equation list. Additional declarations supply deformation, + * boundary treatment, constraints, normalization or inverses; only named contribution results modify + * the physical system. Dependency preparation schedules the work and publishes complete results. + */ + template + [[nodiscard]] auto make_model_graph(std::shared_ptr registry, std::shared_ptr discretization, + const Layout& layout, std::tuple additional = {}, + utils::dependencies::KeyList contributions = {}, operators::backend::mfem::VolumeOptions options = {}) { + using namespace utils::errors; + using Equations = typename Registry::Equations; + + serif_ensure(registry != nullptr && discretization != nullptr, "Model graph construction requires an owning equation registry and discretization binding."); + const auto interior = detail::interior_computations(registry, *discretization, layout, options, Equations{}); + const auto physical = detail::physical_system_computation(layout, discretization, detail::contribution_keys(Equations{}, contributions)); + + return std::apply([](auto... computations) { + return utils::dependencies::make_computation_graph(std::move(computations)...); + }, std::tuple_cat(interior, std::move(additional), std::tuple{physical})); + } + + /** @brief Retain a standalone registry, including move-only equations supplied as an rvalue. */ + template + [[nodiscard]] auto make_model_graph(Registry registry, std::shared_ptr discretization, + const Layout& layout, std::tuple additional = {}, + utils::dependencies::KeyList contributions = {}, operators::backend::mfem::VolumeOptions options = {}) { + return make_model_graph(std::make_shared(std::move(registry)), std::move(discretization), layout, std::move(additional), contributions, std::move(options)); + } + + namespace detail { + /** @brief One shared deformation feeds all geometry derivatives and the surface-potential shape row. */ + template + [[nodiscard]] auto shape_computations(const std::shared_ptr& bound, const Layout& layout) { + namespace deformation = geometry::deformation::backend::mfem; + namespace execution = operators::backend::mfem; + + if constexpr (Bound::Plan::has_shape) { + using Surface = std::remove_cvref_tdiscretization->deformation.model.surface)>; + using Metadata = typename execution::volume_keys::Scoped::Bindings; + const auto& displacement = bound->discretization->template binding(); + return std::tuple_cat(deformation::make_deformation_computations(), + deformation::make_volume_displacement_computations(displacement), + std::tuple{computations::shape_derivatives(displacement), + computations::surface_potential_contribution(bound->definition.shape_force, layout)}); + } + else { + return std::tuple{}; + } + } + + /** @brief Sample the internally selected pressure formulation through its compiled traces. */ + template + [[nodiscard]] auto boundary_computations(const Bound&, const Layout& layout) { + using Plan = typename Bound::Plan; + if constexpr (Plan::has_surface) { + using Formulation = typename Plan::Surface::Formulation; + return std::tuple_cat(geometry::surface::backend::mfem::make_pressure_boundary_computations(), + std::tuple{computations::pressure_boundary_contribution(layout)}); + } + else { + return std::tuple{}; + } + } + + /** @brief Name exactly the explicitly selected non-interior contributions to final assembly. */ + template + [[nodiscard]] consteval auto equilibrium_contribution_keys() { + using namespace utils::dependencies; + using Shape = std::conditional_t, KeyList<>>; + using Surface = std::conditional_t, KeyList<>>; + using Infinity = std::conditional_t, KeyList<>>; + return CombinedKeyList{}; + } + } + + /** + * @brief Connect the explicit equilibrium definition in one readable place. + * @note Interior physics, shape force and surface replacement remain separate + * declarations. The final physical-system computation collects additions before replacing rows. + */ + template + [[nodiscard]] auto make_equilibrium_graph(std::shared_ptr bound, const Layout& layout, + std::tuple numerical = {}, operators::backend::mfem::VolumeOptions options = {}) { + using namespace utils::errors; + using Description = typename Bound::Description; + using Registry = typename Description::InteriorEquations; + + serif_ensure(bound != nullptr, "An equilibrium graph requires a bound model owner. Bind the physical description and discretization before constructing the graph."); + const auto shape = detail::shape_computations(bound, layout); + const auto surface = detail::boundary_computations(*bound, layout); + const auto infinity = [&]() { + if constexpr (Bound::Plan::has_primal_gravity) { + return std::tuple{computations::potential_infinity_contribution(layout, + bound->discretization->template binding())}; + } else { + return std::tuple{}; + } + }(); + const auto contributions = detail::equilibrium_contribution_keys(); + return make_model_graph(std::shared_ptr(bound, &bound->definition.interior_equations), bound, layout, + std::tuple_cat(shape, surface, infinity, std::move(numerical)), contributions, options); + } +} diff --git a/src/include/serif/backend/mfem/model/inputs.hpp b/src/include/serif/backend/mfem/model/inputs.hpp new file mode 100644 index 00000000..23fd7fa1 --- /dev/null +++ b/src/include/serif/backend/mfem/model/inputs.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include "serif/model/plan.hpp" +#include "serif/backend/mfem/operators/volume_inputs.hpp" + +namespace serif::model::backend::mfem +{ + namespace detail + { + /** @brief Accepted actions may borrow only bindings retained by the declared discretization owner. */ + template + void validate_coefficient_owners(const Discretization& discretization, const Coefficients& coefficients, utils::dependencies::KeyList) + { + using namespace utils::errors; + + const auto validate = [&]() + { + serif_ensure(std::addressof(coefficients.template get().binding) == std::addressof(discretization.template binding()), + "Stellar coefficients must use the exact term bindings retained by their discretization owner. Rebind the coefficient vector through that discretization before preparing the model."); + }; + (validate.template operator()(), ...); + } + + /** @brief Bind common physical coefficients once and keep equation-specific metadata distinct. */ + template + [[nodiscard]] auto model_inputs(const Discretization& discretization, const Coefficients& coefficients, + utils::types::TypeList, utils::dependencies::KeyList) + { + using namespace utils::dependencies; + using namespace utils::errors; + namespace execution = operators::backend::mfem; + + const auto* compactification_space = discretization.compactification.FESpace(); + serif_ensure(compactification_space != nullptr, + "Stellar compactification requires a finite-element space. Bind compactification to the model mesh before preparing its equations."); + return make_dependency_context( + execution::make_volume_binding_input(discretization.template equation_discretization(), coefficients)..., + own(execution::MappingCoefficients{.binding = *compactification_space, .values = discretization.compactification}, discretization::backend::mfem::CoefficientTracking{}), + own(coefficients.template get(), discretization::backend::mfem::CoefficientTracking{})...); + } + + } + + /** @brief Supply the same physical coefficient resources to all explicitly listed interior equations. */ + template + [[nodiscard]] auto make_model_inputs(const Discretization& discretization, const Coefficients& coefficients) + { + using Plan = ModelPlan; + detail::validate_coefficient_owners(discretization, coefficients, typename Plan::Coefficients{}); + return detail::model_inputs(discretization, coefficients, typename Registry::Equations{}, typename Plan::Coefficients{}); + } + + /** @brief Prepare an interior system with a prescribed displacement field, without adding displacement unknowns. */ + template + [[nodiscard]] auto make_model_inputs(const Discretization& discretization, const Coefficients& coefficients, const ::mfem::Vector& displacement) + { + const auto inputs = make_model_inputs(discretization, coefficients); + const auto view = operators::backend::mfem::bind(discretization.template binding(), displacement); + return utils::dependencies::combine_dependency_contexts(inputs, utils::dependencies::make_dependency_context( + utils::dependencies::own(view, discretization::backend::mfem::CoefficientTracking{}))); + } + + /** @brief Reject a displacement temporary whose coefficients would expire before graph preparation. */ + template + void make_model_inputs(const Discretization&, const Coefficients&, const ::mfem::Vector&&) = delete; +} diff --git a/src/include/serif/backend/mfem/model/normalization/computations.hpp b/src/include/serif/backend/mfem/model/normalization/computations.hpp new file mode 100644 index 00000000..b8cdade9 --- /dev/null +++ b/src/include/serif/backend/mfem/model/normalization/computations.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include + +#include "serif/backend/mfem/model/normalized_system.hpp" +#include "serif/backend/mfem/model/computations/solver_layout.hpp" +#include "serif/utils/dependencies/snapshot.hpp" +#include "serif/model/keys.hpp" +#include "serif/model/normalization/plan.hpp" +#include "serif/backend/mfem/solver/normalization/metric_computations.hpp" +#include "serif/solver/normalization/computations.hpp" +#include "serif/backend/mfem/geometry/deformation/computations.hpp" +#include "serif/backend/mfem/geometry/surface/boundary_inputs.hpp" +#include "serif/backend/mfem/discretization/dof_selection.hpp" + +namespace serif::model::normalization { + struct Configuration final {}; + struct PreparedSystem final {}; + + namespace detail { + /** @brief Rebuilding the input context does not change the identity of an immutable configuration. */ + struct ConfigurationTracking final : utils::dependencies::ValueSnapshot { + /** @brief Track the owned configuration rather than a copied shared pointer wrapper. */ + template + [[nodiscard]] const void* identity(const std::shared_ptr& options) const { + return options.get(); + } + }; + + template + struct VolumeMetric { + using Term = void; + }; + + template + struct VolumeMetric> { + using Term = TermT; + }; + + /** @brief Reference volume metrics borrow the same term binding and track its mesh and space revisions. */ + template + [[nodiscard]] auto metric_inputs(const Discretization& discretization) { + using namespace utils::dependencies; + using Term = typename VolumeMetric::Term; + if constexpr (!std::same_as) { + return make_dependency_context(solver::normalization::backend::mfem::reference_metric_binding(discretization.template binding())); + } else { + return make_dependency_context(); + } + } + + /** @brief Supply built-in metrics once; custom metric keys are provided by the user's ordinary graph sources. */ + template + [[nodiscard]] auto metric_computations(const Discretization&) { + using Term = typename VolumeMetric::Term; + if constexpr (!std::same_as) { + using Binding = std::remove_cvref_t().template binding())>; + return std::tuple{solver::normalization::backend::mfem::make_reference_volume_metric_computation()}; + } else if constexpr (std::same_as) { + return std::tuple{geometry::deformation::backend::mfem::make_surface_metric_computation()}; + } else { + return std::tuple{}; + } + } + } + + /** @brief Keep the immutable configuration alive and reference metrics separate from candidate coefficients. */ + template + [[nodiscard]] auto make_inputs(std::shared_ptr options, const Discretization& discretization, utils::dependencies::KeyList) { + using namespace utils::dependencies; + const auto configuration = make_dependency_context(own(options, detail::ConfigurationTracking{})); + return combine_dependency_contexts(configuration, options->sources, detail::metric_inputs(discretization)...); + } + + /** @brief Compose normalization into the physical graph, including the selected surface row replacement. */ + template + [[nodiscard]] auto make_computations(const Bound& bound, const Options& options, const Layout& layout, utils::dependencies::KeyList) { + using namespace utils::dependencies; + namespace numerical = solver::normalization; + const auto scales = make_computation>( + [](const std::shared_ptr& configuration) { + using numerical::prepare_reference_scales; + return prepare_reference_scales(configuration->scheme, configuration->gravitational_constant); + }); + const auto surface = [&]() { + if constexpr (Bound::Plan::has_surface) { + return std::tuple{make_computation>([](const auto& boundary) { + const auto rows = boundary->selected_rows->reduced_dofs(); + return std::vector(rows.begin(), rows.end()); + })}; + } else { + return std::tuple{}; + } + }(); + const auto prepared = make_computation>( + backend::mfem::make_normalized_system); + const auto infinity = [&]() { + if constexpr (Bound::Plan::has_primal_gravity) { + const auto& binding = bound.discretization->template binding(); + // Match potential_infinity_contribution: its zero trace uses potential units, not Poisson-row units. + const auto selected = discretization::backend::mfem::select_boundary_dofs(binding); + return std::tuple{make_computation>([selected] { + selected.validate(); + const auto& rows = selected.reduced_dofs(); + return std::vector(rows.begin(), rows.end()); + })}; + } else { + return std::tuple{}; + } + }(); + return std::tuple_cat(std::tuple{scales, prepared}, computations::make_solver_layout_computations(layout), surface, infinity, + detail::metric_computations(*bound.discretization)..., + numerical::make_normalization_computations(layout), options.computations); + } +} diff --git a/src/include/serif/backend/mfem/model/normalized_system.hpp b/src/include/serif/backend/mfem/model/normalized_system.hpp new file mode 100644 index 00000000..0e51e1c2 --- /dev/null +++ b/src/include/serif/backend/mfem/model/normalized_system.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/model/system.hpp" +#include "serif/backend/mfem/solver/normalization/operators.hpp" +#include "serif/solver/normalization/scales.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::model::backend::mfem { + /** @brief A frozen physical preparation and its owning solver coordinates, safe after later graph preparation. */ + class NormalizedSystem final { + public: + /** @brief Retain the physical operator before creating its normalized action and residual. */ + NormalizedSystem(std::shared_ptr physical, + solver::normalization::DiagonalNormalization coordinates, solver::normalization::StellarReferenceScales scales) : + m_physical(checked_physical(std::move(physical))), m_scales(std::move(scales)), + m_jacobian(*m_physical->jacobian().action(), std::move(coordinates)), m_residual(m_physical->residual().size()) { + m_jacobian.normalization().normalize_residual(m_physical->residual(), m_residual); + } + + /** @brief Inspect the matching dimensional residual, Jacobian and selected physical blocks. */ + [[nodiscard]] const std::shared_ptr& physical() const noexcept { + return m_physical; + } + + /** @brief Convert states, residuals and solver corrections with this preparation's frozen factors. */ + [[nodiscard]] const solver::normalization::DiagonalNormalization& coordinates() const noexcept { + return m_jacobian.normalization(); + } + + /** @brief Inspect the characteristic scales actually used, including the estimated radius. */ + [[nodiscard]] const solver::normalization::StellarReferenceScales& reference_scales() const noexcept { + return m_scales; + } + + /** @brief Read the normalized residual in the model's independent equation ordering. */ + [[nodiscard]] const std::vector& residual() const noexcept { + return m_residual; + } + + /** @brief Apply L J S^-1 or its transpose while retaining the physical operator owner. */ + [[nodiscard]] const solver::normalization::backend::mfem::ScaledJacobianOperator& jacobian() const noexcept { + return m_jacobian; + } + + private: + /** @brief Diagnose a missing preparation before a normalized adapter borrows its Jacobian. */ + [[nodiscard]] static std::shared_ptr checked_physical(std::shared_ptr physical) { + using namespace utils::errors; + serif_ensure(physical != nullptr, "Normalized coordinates require an frozen physical system. Prepare the stellar model first."); + return physical; + } + + std::shared_ptr m_physical; + solver::normalization::StellarReferenceScales m_scales; + solver::normalization::backend::mfem::ScaledJacobianOperator m_jacobian; + std::vector m_residual; + }; + + /** @brief Reject a nonrepresentable normalized residual before publishing the matching physical preparation. */ + [[nodiscard]] inline std::expected, std::string> make_normalized_system( + std::shared_ptr physical, solver::normalization::DiagonalNormalization coordinates, + solver::normalization::StellarReferenceScales reference) { + auto result = std::make_shared(std::move(physical), std::move(coordinates), std::move(reference)); + if (!std::ranges::all_of(result->residual(), [](double value) { return std::isfinite(value); })) { + return std::unexpected("The normalized stellar residual is not finite. Check the reference scales and the physical candidate."); + } + return result; + } +} diff --git a/src/include/serif/backend/mfem/model/preconditioned_system.hpp b/src/include/serif/backend/mfem/model/preconditioned_system.hpp new file mode 100644 index 00000000..0a0c5c23 --- /dev/null +++ b/src/include/serif/backend/mfem/model/preconditioned_system.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include + +#include "serif/backend/mfem/model/normalized_system.hpp" +#include "serif/backend/mfem/solver/preconditioner/solver.hpp" + +namespace serif::model::backend::mfem { + /** + * @brief Retain one evaluated star and its matching inverse for a linear solve. + * @note Later evaluations cannot change these operators. MFEM adapters reuse scratch storage; + * applications of the same preparation must not run concurrently. + */ + template + requires (std::same_as || std::same_as) + class PreconditionedSystem final { + public: + /** @brief Establish physical ownership before constructing an adapter in the selected solver coordinates. */ + PreconditionedSystem(std::shared_ptr system, solver::preconditioner::backend::mfem::Inverse inverse) : + m_system(checked_system(std::move(system))), + m_physical_inverse(std::make_shared(std::move(inverse), physical_jacobian())), + m_inverse(make_solver()) { + } + + /** @brief Inspect the residual and Jacobian belonging to this inverse. */ + [[nodiscard]] const std::shared_ptr& system() const noexcept { + return m_system; + } + + /** @brief Supply MFEM with the matching physical or normalized inverse; unrelated rebinding is rejected. */ + [[nodiscard]] ::mfem::Solver& preconditioner() const noexcept { + return *m_inverse; + } + + /** @brief Apply or inspect the dimensional inverse before the coordinate transformation. */ + [[nodiscard]] const solver::preconditioner::backend::mfem::Solver& physical_preconditioner() const noexcept { + return *m_physical_inverse; + } + + /** @brief Tell the outer solver whether the selected inverse has a fixed linear application. */ + [[nodiscard]] solver::preconditioner::ApplicationContract contract() const noexcept { + return m_physical_inverse->contract(); + } + + private: + /** @brief Report an invalid owner before any adapter borrows an operator from it. */ + [[nodiscard]] static std::shared_ptr checked_system(std::shared_ptr system) { + using namespace utils::errors; + serif_ensure(system != nullptr, + "A preconditioned stellar system requires an evaluated star. Prepare the matching residual and Jacobian before binding its inverse."); + return system; + } + + /** @brief Bind the physical inverse to the dimensional root even when the outer solve is normalized. */ + [[nodiscard]] auto physical_jacobian() const { + if constexpr (std::same_as) { + return m_system->physical()->jacobian().action(); + } else { + return m_system->jacobian().action(); + } + } + + /** @brief Reuse the existing normalization adapter while retaining both of its borrowed owners. */ + [[nodiscard]] std::shared_ptr<::mfem::Solver> make_solver() const { + if constexpr (std::same_as) { + return std::make_shared(*m_physical_inverse, m_system->jacobian()); + } else { + return m_physical_inverse; + } + } + + std::shared_ptr m_system; + std::shared_ptr m_physical_inverse; + std::shared_ptr<::mfem::Solver> m_inverse; + }; +} diff --git a/src/include/serif/backend/mfem/model/preconditioning/computations.hpp b/src/include/serif/backend/mfem/model/preconditioning/computations.hpp new file mode 100644 index 00000000..3e6eb56f --- /dev/null +++ b/src/include/serif/backend/mfem/model/preconditioning/computations.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/model/preconditioned_system.hpp" +#include "serif/backend/mfem/model/computations/physical_blocks.hpp" +#include "serif/backend/mfem/model/computations/solver_layout.hpp" +#include "serif/backend/mfem/geometry/deformation/computations.hpp" +#include "serif/backend/mfem/solver/preconditioner/graph.hpp" + +namespace serif::model::preconditioning { + /** @brief An owning evaluated system and its matching preconditioner. */ + struct PreparedSystem final {}; + + namespace detail { + /** @brief Add a common reference computation only when normalization has not already supplied it. */ + template + [[nodiscard]] auto missing_computation(Computation computation, const std::tuple&) { + if constexpr ((std::same_as || ...)) { + return std::tuple{}; + } else { + return std::tuple{std::move(computation)}; + } + } + } + + /** + * @brief Connect a recipe to the same physical root, layout and reference geometry as model evaluation. + * @note Merely adding these computations does not prepare an inverse. Only requesting PreparedSystem + * reaches this branch, so line-search evaluations can reuse the physical graph without inverse setup. + */ + template + [[nodiscard]] auto make_computations(const Recipe& recipe, const Layout& layout, const std::tuple& existing) { + using namespace utils::dependencies; + namespace numerical = solver::preconditioner; + const auto inverse = numerical::make_preconditioner_computations(recipe, layout); + using Graph = decltype(std::apply([](auto... computations) { + return make_computation_graph(std::move(computations)...); + }, inverse)); + using PhysicalKeys = typename numerical::computations::detail::PhysicalSources::Type; + const auto shared_layout = std::apply([&](auto computation) { + return detail::missing_computation(std::move(computation), existing); + }, computations::make_solver_layout_computations(layout)); + const auto prepared = make_computation>( + [](const auto& system, const numerical::backend::mfem::Inverse& inverse) { + using System = std::remove_const_t::element_type>; + return std::make_shared>(system, inverse); + }); + return std::tuple_cat(inverse, shared_layout, + computations::make_physical_block_computations(layout, PhysicalKeys{}), std::tuple{prepared}); + } +} diff --git a/src/include/serif/backend/mfem/model/scalar_row.hpp b/src/include/serif/backend/mfem/model/scalar_row.hpp new file mode 100644 index 00000000..6ecad691 --- /dev/null +++ b/src/include/serif/backend/mfem/model/scalar_row.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "serif/backend/mfem/operators/linearized_block.hpp" +#include "serif/backend/mfem/operators/rejection.hpp" + +namespace serif::model::backend::mfem { + /** + * @brief Retain one scalar equation's coefficient derivatives instead of repeating its integration. + * @note Applying the exact transpose to one recovers the complete row without probing state columns. + * The returned ordinary matrix owns its coefficients independently of the source action. + * Numerical overflow rejects this preparation; malformed operator dimensions remain caller errors. + */ + [[nodiscard]] std::expected cache_scalar_row(const operators::backend::mfem::LinearizedBlock& source); +} diff --git a/src/include/serif/backend/mfem/model/state.hpp b/src/include/serif/backend/mfem/model/state.hpp new file mode 100644 index 00000000..9f144374 --- /dev/null +++ b/src/include/serif/backend/mfem/model/state.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include + +#include "serif/model/plan.hpp" +#include "serif/model/state.hpp" +#include "serif/backend/mfem/discretization/term_coefficients.hpp" + +namespace serif::model::backend::mfem::detail { + /** @brief Recognize a field view by its physical identity and backend coefficient representation. */ + template + concept CoefficientInput = requires(const Inputs& inputs) { + typename Inputs::Keys; + { inputs.template get().values } -> std::convertible_to; + requires std::same_as().binding)>::Term, Term>; + }; + + /** @brief Check required fields before entering evaluation, including during compile-time API detection. */ + template + [[nodiscard]] consteval bool coefficient_inputs(utils::dependencies::KeyList) { + return (CoefficientInput && ...); + } + + /** @brief Prescribed views must supply every unsolved input and cannot duplicate a solved coordinate. */ + template + concept PrescribedInputsFor = utils::dependencies::IsDependencyContext && + (utils::dependencies::key_count_v> == + utils::dependencies::key_count_v + utils::dependencies::key_count_v) && + coefficient_inputs(typename model::detail::RemoveKeys::Type{}); + + template + class StateCoefficients; + + /** + * @brief Borrow an owning state's fields for one synchronous equation evaluation. + * @note MFEM requires mutable pointers to construct its vector views, but these wrappers are only + * exposed through const coefficient views. Keep this adapter alive until graph evaluation returns. + * Recreate it for each call so copies and moves of StellarState never leave persistent self-references. + */ + template + class StateCoefficients> final { + public: + using Keys = utils::dependencies::CombinedKeyList, typename Prescribed::Keys>; + static_assert(utils::dependencies::key_count_v == sizeof...(Fields) + utils::dependencies::key_count_v, + "A physical field cannot be both solved and prescribed. Remove its prescribed input or its solved coordinate."); + + /** @brief Create backend views without copying the state's physical coefficients. */ + StateCoefficients(const Discretization& discretization, const StellarState& state, const Prescribed& prescribed) : + m_discretization(discretization), m_prescribed(prescribed), m_fields{field_view(state.template get())...} { + } + + /** @brief Reuse ordinary dependency contexts while the temporary MFEM wrappers remain alive. */ + [[nodiscard]] auto context() const & { + using namespace utils::dependencies; + + const auto fields = make_dependency_context(own(operators::backend::mfem::bind( + m_discretization.template binding(), m_fields[key_index>()]))...); + return combine_dependency_contexts(fields, m_prescribed); + } + + /** @brief A coefficient context cannot borrow a temporary set of backend wrappers. */ + auto context() const && = delete; + + private: + /** @brief Check MFEM's index range before constructing a read-only use of its mutable-pointer view. */ + [[nodiscard]] static ::mfem::Vector field_view(std::span values) { + using namespace utils::errors; + serif_ensure(values.size() <= static_cast(std::numeric_limits::max()), + "A stellar field exceeds MFEM's coefficient index range. Supply a representable discretization."); + return ::mfem::Vector(const_cast(values.data()), static_cast(values.size())); + } + + const Discretization& m_discretization; + const Prescribed& m_prescribed; + std::array<::mfem::Vector, sizeof...(Fields)> m_fields; + }; + + /** @brief Keep surface coordinates in the state while exposing only ordinary fields to volume equations. */ + template + [[nodiscard]] auto state_coefficients(const Discretization& discretization, const StellarState& state, const Prescribed& prescribed) { + using Fields = typename model::detail::RemoveKeys>::Type; + return StateCoefficients(discretization, state, prescribed); + } +} diff --git a/src/include/serif/backend/mfem/model/surface_potential.hpp b/src/include/serif/backend/mfem/model/surface_potential.hpp new file mode 100644 index 00000000..747549a5 --- /dev/null +++ b/src/include/serif/backend/mfem/model/surface_potential.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +#include "mfem/linalg/densemat.hpp" +#include "mfem/linalg/sparsemat.hpp" +#include "serif/backend/mfem/discretization/dof_map.hpp" +#include "serif/backend/mfem/geometry/deformation/inputs.hpp" +#include "serif/geometry/deformation/reference_surface.hpp" +#include "serif/geometry/deformation/surface_embedding.hpp" +#include "serif/equations/surface_potential.hpp" + +namespace serif::model::backend::mfem { + /** @brief Accepted surface moments and derivatives in existing shape and field coordinates. */ + struct SurfacePotentialLinearization { + std::vector residual; + std::shared_ptr<::mfem::SparseMatrix> potential; + std::shared_ptr<::mfem::SparseMatrix> shape; + std::shared_ptr<::mfem::DenseMatrix> level; + std::shared_ptr<::mfem::DenseMatrix> angular_velocity; + }; + + /** + * @brief Integrate the effective-potential balance using dS_reference / area_reference. + * @note All matrices own accepted values. The reference measure and radial directions are + * fixed; only the centrifugal potential depends explicitly on material surface motion. + * Displacement contains three Cartesian components per selected reference node; shape rows + * and columns use the embedding's independent radial lengths or harmonic coefficients. + */ + [[nodiscard]] SurfacePotentialLinearization assemble_surface_potential( + const equations::SurfacePotentialBalance& equation, + const geometry::deformation::backend::mfem::SurfaceDiscretization& discretization, + const geometry::deformation::ReferenceSurface& reference, + const geometry::deformation::SurfaceEmbedding& embedding, + std::span displacement, + const discretization::backend::mfem::DofMap& potential_binding, + const ::mfem::Vector& potential, double level, double angular_velocity); +} diff --git a/src/include/serif/backend/mfem/model/system.hpp b/src/include/serif/backend/mfem/model/system.hpp new file mode 100644 index 00000000..c8f50a10 --- /dev/null +++ b/src/include/serif/backend/mfem/model/system.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include "serif/backend/mfem/operators/actions.hpp" + +namespace serif::model::backend::mfem { + using operators::backend::mfem::LinearizedBlock; + /** @brief One contiguous range of equation or state coordinates in an evaluated system. */ + struct CoordinateRange { + int offset; + int size; + }; + + /** @brief One evaluated equation contribution in the independently ordered residual coordinates. */ + struct ResidualContribution { + int row_offset; + std::vector values; + }; + + /** @brief One frozen physical Jacobian coupling; overlapping contributions add. */ + struct JacobianContribution { + int row_offset; + int column_offset; + LinearizedBlock block; + }; + + /** @brief A coupling from state coordinates into an ordered selection of replacement rows. */ + struct ColumnContribution { + int column_offset; + LinearizedBlock block; + }; + + /** @brief Replace complete equations after all ordinary and generated contributions have been added. */ + struct RowReplacement { + std::vector selected_rows; + std::vector residual; + std::vector jacobian; + }; + + namespace detail { + struct SystemData; + } + + /** @brief Own one evaluated residual and Jacobian, retaining the discretization owner used by every child action. */ + class PhysicalSystem final { + public: + /** + * @brief Validate coordinate bounds and row ownership without assuming a square system. + * @note Optional complete actions must represent the same ordinary Jacobian as the blocks. + * They accelerate whole-system products; block selection and assembly keep using the physical blocks. + */ + PhysicalSystem(int residual_size, int state_size, std::vector residuals, std::vector jacobian, std::vector replacements = {}, std::shared_ptr owner = {}, std::optional> complete_actions = {}); + + /** @brief Read the frozen physical residual in the declared residual order. */ + [[nodiscard]] const std::vector& residual() const noexcept; + + /** @brief Access both directions of this frozen physical Jacobian. */ + [[nodiscard]] const LinearizedBlock& jacobian() const noexcept; + + /** @brief Select a physical coupling without applying unrelated equations; the result retains this snapshot and its owner. */ + [[nodiscard]] LinearizedBlock block(int row_offset, int row_size, int column_offset, int column_size) const; + + /** @brief Concatenate selected equation and state ranges in their supplied order, retaining this evaluated system. */ + [[nodiscard]] LinearizedBlock block(std::span rows, std::span columns) const; + + private: + std::shared_ptr m_data; + std::vector m_residual; + LinearizedBlock m_jacobian; + }; + + /** @brief Publish an immutable evaluated system that remains independent of later graph preparation. */ + [[nodiscard]] std::shared_ptr make_physical_system(int residual_size, int state_size, std::vector residuals, std::vector jacobian, std::vector replacements = {}, std::shared_ptr owner = {}, std::optional> complete_actions = {}); +} // namespace serif::model::backend::mfem diff --git a/src/include/serif/backend/mfem/model/volume.hpp b/src/include/serif/backend/mfem/model/volume.hpp new file mode 100644 index 00000000..dae4faa3 --- /dev/null +++ b/src/include/serif/backend/mfem/model/volume.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include "serif/backend/mfem/operators/actions.hpp" +#include "serif/backend/mfem/operators/volume_application.hpp" + +namespace serif::model::backend::mfem { + using operators::backend::mfem::LinearizedBlock; + /** @brief Copy the accepted volume residual for additive physical system assembly. */ + template + [[nodiscard]] std::vector volume_residual_values(const std::shared_ptr>& data) { + using namespace utils::errors; + + serif_ensure(data != nullptr, "Model assembly requires an accepted volume equation."); + operators::backend::mfem::validate_volume_data(*data); + return std::vector(data->residual.GetData(), data->residual.GetData() + data->residual.Size()); + } + + /** @brief Select only the displacement derivative while reusing the equation's complete geometry and transpose kernels. */ + template + [[nodiscard]] LinearizedBlock volume_geometry_block(std::shared_ptr> data) { + using namespace utils::errors; + namespace volume = operators::backend::mfem; + + serif_ensure(data != nullptr, "A model geometry block requires an accepted volume equation."); + const int rows = volume::volume_height(*data); + const int columns = data->configuration->discretization.displacement_binding.size(); + return operators::backend::mfem::linearized_action(rows, columns, [data, workspace = volume::Workspace{}](const ::mfem::Vector& input, ::mfem::Vector& output) mutable { + const std::array::column_count> directions{}; + volume::apply_volume_direction(*data, directions, input, workspace, output); + }, [data, columns, workspace = volume::Workspace{}, packed = ::mfem::Vector{}](const ::mfem::Vector& input, ::mfem::Vector& output) mutable { + volume::apply_jacobian_transpose(*data, input, workspace, packed); + std::copy_n(packed.GetData() + packed.Size() - columns, columns, output.GetData()); + }, [data](std::span inputs, std::span<::mfem::Vector* const> outputs) { + volume::apply_volume_displacements(*data, inputs, outputs); + }); + } +} // namespace serif::model::backend::mfem diff --git a/src/include/serif/backend/mfem/model/volume_actions.hpp b/src/include/serif/backend/mfem/model/volume_actions.hpp new file mode 100644 index 00000000..ee064b4e --- /dev/null +++ b/src/include/serif/backend/mfem/model/volume_actions.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include +#include "serif/discretization/blocks/system_layout.hpp" +#include "serif/backend/mfem/model/system.hpp" +#include "serif/backend/mfem/operators/volume_application.hpp" + +namespace serif::model::backend::mfem { + /** + * @brief Apply every solved field and geometry direction in one traversal of an accepted volume equation. + * @note The optional geometry column maps independent state coordinates into the equation's displacement + * coefficients. Prescribed fields have zero direction but remain part of the frozen physical state. + */ + template + [[nodiscard]] LinearizedBlock volume_row_action(std::shared_ptr> data, const Layout& layout, std::optional geometry = {}) { + using namespace utils::errors; + using namespace utils::dependencies; + using Kind = discretization::blocks::RepresentedValueKind; + using Plan = operators::EquationPlan; + namespace volume = operators::backend::mfem; + + serif_ensure(data != nullptr, "A complete volume action requires an accepted equation."); + const std::size_t columns = layout.template total_size(); + serif_ensure(columns <= static_cast(std::numeric_limits::max()), "Complete volume action state coordinates exceed MFEM's integer range."); + std::array offsets; + offsets.fill(-1); + std::array<::mfem::Vector, Plan::column_count> fields; + Plan::for_each_column([&](Column, std::size_t column) { + using Field = typename Column::term_type; + fields[column].SetSize(data->bindings.columns[column]->size()); + fields[column] = 0.0; + if constexpr (ContainsKey) { + serif_ensure(layout.template size() == static_cast(fields[column].Size()), "Solved field coordinates must match the accepted volume binding."); + offsets[column] = static_cast(layout.template offset()); + } + }); + const int displacement_size = data->configuration->discretization.displacement_binding.size(); + if (geometry) { + const auto& action = geometry->block.action(); + serif_ensure(action->Height() == displacement_size && geometry->column_offset >= 0 && static_cast(geometry->column_offset) <= columns && static_cast(action->Width()) <= columns - geometry->column_offset, "The geometry column must map a valid state interval into the accepted displacement coefficients."); + } + return operators::backend::mfem::linearized_action(volume::volume_height(*data), static_cast(columns), + [data, offsets, fields = std::move(fields), geometry, displacement = ::mfem::Vector(displacement_size), geometry_direction = ::mfem::Vector{}, workspace = volume::Workspace{}](const ::mfem::Vector& input, ::mfem::Vector& output) mutable { + std::array directions; + for (std::size_t column = 0; column < fields.size(); ++column) { + if (offsets[column] >= 0) { + std::copy_n(input.GetData() + offsets[column], fields[column].Size(), fields[column].GetData()); + } + directions[column] = &fields[column]; + } + displacement = 0.0; + if (geometry) { + geometry_direction.SetSize(geometry->block.action()->Width()); + std::copy_n(input.GetData() + geometry->column_offset, geometry_direction.Size(), geometry_direction.GetData()); + geometry->block.action()->Mult(geometry_direction, displacement); + } + if (geometry) { + volume::apply_volume_direction(*data, directions, displacement, workspace, output); + } else { + volume::apply_volume_direction(*data, directions, displacement, workspace, output); + } + }, + [data, offsets, geometry, displacement_size, packed = ::mfem::Vector{}, displacement_dual = ::mfem::Vector(displacement_size), geometry_dual = ::mfem::Vector{}, workspace = volume::Workspace{}](const ::mfem::Vector& input, ::mfem::Vector& output) mutable { + volume::apply_jacobian_transpose(*data, input, workspace, packed); + int source = 0; + for (std::size_t column = 0; column < offsets.size(); ++column) { + const int size = data->bindings.columns[column]->size(); + if (offsets[column] >= 0) { + for (int index = 0; index < size; ++index) { + output[offsets[column] + index] += packed[source + index]; + } + } + source += size; + } + if (geometry) { + std::copy_n(packed.GetData() + source, displacement_size, displacement_dual.GetData()); + geometry_dual.SetSize(geometry->block.action()->Width()); + geometry_dual = 0.0; + geometry->block.action()->MultTranspose(displacement_dual, geometry_dual); + for (int index = 0; index < geometry_dual.Size(); ++index) { + output[geometry->column_offset + index] += geometry_dual[index]; + } + } + }); + } +} // namespace serif::model::backend::mfem diff --git a/src/include/serif/backend/mfem/model/volume_displacement.hpp b/src/include/serif/backend/mfem/model/volume_displacement.hpp new file mode 100644 index 00000000..310ea1af --- /dev/null +++ b/src/include/serif/backend/mfem/model/volume_displacement.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include + +#include "serif/geometry/deformation/computations.hpp" +#include "serif/backend/mfem/geometry/deformation/inputs.hpp" +#include "serif/backend/mfem/geometry/deformation/displacement_mapping.hpp" +#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp" +#include "serif/backend/mfem/operators/volume_inputs.hpp" + +namespace serif::geometry::deformation::backend::mfem { + namespace volume_displacement_keys { + struct CheckedBinding { + }; + struct Coefficients { + }; + } // namespace volume_displacement_keys + + /** + * @brief Connect a full component-block deformation vector to an equation's displacement binding. + * + * Reference-dependent validation proves the two spaces assign the same meaning to each scalar coefficient. + * Subsequent computations own coefficients in the binding's global ordering and supported-DOF numbering, + * then return the existing CoefficientView into that immutable prerequisite. Cache result ownership + * keeps the vector's address stable throughout downstream evaluation and accepted-result reuse. + * MetadataKey selects an equation-scoped binding source when several equations share the graph. + * The binding itself is borrowed and must outlive this graph and its results, as for volume equations. + */ + template + [[nodiscard]] auto make_volume_displacement_computations(const discretization::backend::mfem::TermBinding& binding) { + using namespace utils::dependencies; + using Binding = discretization::backend::mfem::TermBinding; + namespace equations_backend = operators::backend::mfem; + + const auto checked_binding = make_computation>( + [&binding](const ReferenceDiscretization& reference, const auto& metadata) -> std::expected { + binding.validate(); + if (metadata.displacement_binding != &binding) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "The deformation-to-equation connection requires the displacement binding used when constructing it."}); + } + const ::mfem::FiniteElementSpace& space = binding.space(); + if (space.GetVDim() != 3 || reference.displacement_space.GetVDim() != 3 || reference.scalar_space.GetVDim() != 1) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "The deformation connection requires three-component displacements and a scalar reference companion."}); + } + if (const DeformationStatus numbering = validate_companion_numbering(reference.scalar_space, space); !numbering) { + return std::unexpected(numbering.error()); + } + if (const DeformationStatus numbering = validate_companion_numbering(reference.displacement_space, space); !numbering) { + return std::unexpected(numbering.error()); + } + return &binding; + }); + const auto coefficients = make_computation>( + [](const std::vector& displacement, const Binding* binding) -> std::expected<::mfem::Vector, DeformationFailure> { + const ::mfem::FiniteElementSpace& space = binding->space(); + if (const auto validation = validate_deformation_values(displacement, static_cast(binding->full_size())); !validation) { + return std::unexpected(validation.error()); + } + + ::mfem::Vector values(binding->size()); + if (space.GetOrdering() == ::mfem::Ordering::byNODES && binding->size() == binding->full_size()) { + std::ranges::copy(displacement, values.GetData()); + return values; + } + for_each_displacement_coefficient(*binding, [&](const int row, const int column) { + values[row] = displacement[static_cast(column)]; + }); + return values; + }); + const auto view = make_computation>( + [](const ::mfem::Vector& values, const Binding* binding) -> equations_backend::CoefficientView { + return equations_backend::bind(*binding, values); + }); + return std::tuple{checked_binding, coefficients, view}; + } + + /** @brief A graph cannot retain a displacement binding supplied as a temporary. */ + template + void make_volume_displacement_computations(const discretization::backend::mfem::TermBinding&&) = delete; +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/actions.hpp b/src/include/serif/backend/mfem/operators/actions.hpp new file mode 100644 index 00000000..b9c88874 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/actions.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include "serif/backend/mfem/operators/linearized_block.hpp" + +namespace serif::operators::backend::mfem { + using MatrixAction = std::function; + + /** + * @brief Apply the forward action to several directions at once (zeroed outputs, as MatrixAction receives). + * @note Each output must equal what MatrixAction produces for its input; batching only shares traversals. + */ + using MatrixArrayAction = std::function, std::span<::mfem::Vector* const>)>; + + /** @brief Wrap owned callbacks with checked dimensions, finite results, and overlap-safe application. */ + [[nodiscard]] LinearizedBlock linearized_action(int rows, int columns, MatrixAction action, MatrixAction transpose); + + /** @brief As above, with a batched forward action used by the operator's ArrayMult. */ + [[nodiscard]] LinearizedBlock linearized_action(int rows, int columns, MatrixAction action, MatrixAction transpose, MatrixArrayAction batch); + + /** @brief Apply an operator to several directions; ArrayMult for SERiF actions, otherwise one Mult per direction. */ + void apply_many(const ::mfem::Operator& action, std::span inputs, std::span<::mfem::Vector* const> outputs); + + /** @brief Transpose a linearized block without discarding efficient assembly and diagonal operations. */ + [[nodiscard]] LinearizedBlock transpose_block(LinearizedBlock block); + + /** @brief Compose linearized blocks in their written mathematical order. */ + [[nodiscard]] LinearizedBlock compose_blocks(LinearizedBlock left, LinearizedBlock right); + + /** @brief Sum compatible linearized blocks while retaining their native diagonal and sparse assembly operations. */ + [[nodiscard]] LinearizedBlock sum_blocks(int rows, int columns, std::vector blocks); +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/element_data.hpp b/src/include/serif/backend/mfem/operators/element_data.hpp new file mode 100644 index 00000000..4921561f --- /dev/null +++ b/src/include/serif/backend/mfem/operators/element_data.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +#include "mfem/fem/intrules.hpp" +#include "serif/backend/mfem/mapping/volume_cache.hpp" +#include "serif/backend/mfem/mapping/data.hpp" +#include "serif/backend/mfem/discretization/basis_evaluation.hpp" + +namespace serif::operators::backend::mfem { + using discretization::backend::mfem::BasisEvaluation; + /// Fixed-mesh basis samples; independent of the current coefficient values and displacement. + template + struct ElementBasis final { + int element{-1}; + const ::mfem::IntegrationRule* rule{nullptr}; + BasisEvaluation reference_test_basis; + std::array reference_operand_basis; + BasisEvaluation displacement_basis; + std::vector reference_geometry; + }; + + /// Physical mapping and mapped bases for one frozen displacement. + template + struct ElementGeometry final { + BasisEvaluation test_basis; + std::array operand_basis; + ::mfem::Vector displacement; + ::mfem::Vector compactification; + mapping::VolumeMappingCache mapping; + }; + + /// Weighted equation data retains the exact basis, geometry, and samples used to compute it. + template + struct ElementData final { + int element{-1}; + std::shared_ptr> basis; + std::shared_ptr> geometry; + std::array, OperandCount> reference_operand_values; + ::mfem::Vector weighted_residual; + ::mfem::DenseMatrix weighted_field_derivatives; + ::mfem::DenseMatrix weighted_position_derivative; + double validation_integral{0.0}; + }; +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/element_evaluation.hpp b/src/include/serif/backend/mfem/operators/element_evaluation.hpp new file mode 100644 index 00000000..444edd80 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/element_evaluation.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include +#include +#include + +#include "serif/utils/error/nonfinite.hpp" +#include "serif/backend/mfem/operators/operand_evaluation.hpp" +#include "serif/backend/mfem/operators/element_data.hpp" +#include "serif/backend/mfem/operators/workspace.hpp" +#include "serif/operators/plan.hpp" + +namespace serif::operators::backend::mfem { + /// Integrate the cached scalar/vector physics against its actual test operation. + template + void element_residual(const ElementData& data, ::mfem::Vector& result, BasisWorkspace& workspace) { + OperandEvaluator>::integrate(data.geometry->test_basis, data.weighted_residual, result, workspace); + } + + /** + * @brief Finish an element action from its field part (workspace.quadrature_values) and, with Geometry, the + * displacement samples (workspace.displacement_samples): contract the geometry derivatives and integrate. + */ + template + void finish_element_action( + const ElementData::operand_count>& data, + const ::mfem::DenseMatrix& geometry_derivatives, + Workspace& workspace, + ::mfem::Vector& result + ) { + using namespace utils::errors; + using Test = OperandEvaluator>; + + const int points = data.basis->rule->GetNPoints(); + if constexpr (Geometry) { + workspace.reference_test_values.SetSize(points * Test::reference_components); + // MFEM stores each derivative column contiguously; keep the same twelve-term sum. + const int derivative_stride = geometry_derivatives.Height(); + const double* derivatives = geometry_derivatives.GetData(); + const double* displacement = workspace.displacement_samples.GetData(); + double* values = workspace.reference_test_values.GetData(); + for (int point = 0; point < points; ++point) { + for (int output = 0; output < Test::reference_components; ++output) { + const int row = point * Test::reference_components + output; + double value = 0.0; + for (int input = 0; input < 12; ++input) { + value += derivatives[input * derivative_stride + row] * displacement[12 * point + input]; + } + values[row] = value; + } + } + } + if constexpr (Geometry && !Test::geometry_dependent) { + // Both terms have the same test representation, so one integration is sufficient. + workspace.quadrature_values += workspace.reference_test_values; + } + for (int entry = 0; entry < workspace.quadrature_values.Size(); ++entry) { + serif_ensure( + std::isfinite(workspace.quadrature_values[entry]), "Non-finite equation Jacobian action." + ); + } + if constexpr (Fields || !Test::geometry_dependent) { + Test::integrate(data.geometry->test_basis, workspace.quadrature_values, result, workspace.basis); + } else { + result.SetSize(data.basis->reference_test_basis.width()); + result = 0.0; + } + if constexpr (Geometry && Test::geometry_dependent) { + for (int entry = 0; entry < workspace.reference_test_values.Size(); ++entry) { + serif_ensure( + std::isfinite(workspace.reference_test_values[entry]), "Non-finite equation geometry action." + ); + } + data.basis->reference_test_basis.integrate(workspace.reference_test_values, workspace.test_variation_result, workspace.basis); + result += workspace.test_variation_result; + } + } + + /** + * @brief Apply the accepted field derivatives and local displacement derivatives. + * @note Geometry derivatives are graph results, expressed against the fixed-mesh test basis. + * The two compile-time switches exclude structurally absent directions without sampling zero fields. + */ + template + void element_jacobian_action( + const ElementData::operand_count>& data, + const ::mfem::DenseMatrix& geometry_derivatives, + const std::array::column_count>& columns, + const std::array::column_count>& directions, + const discretization::backend::mfem::DofMap::Elements& displacement_binding, + const ::mfem::Vector& displacement_direction, + Workspace& workspace, + ::mfem::Vector& result + ) { + using Plan = EquationPlan; + using Test = OperandEvaluator>; + + const int points = data.basis->rule->GetNPoints(); + workspace.quadrature_values.SetSize(points * Test::components); + workspace.quadrature_values = 0.0; + if constexpr (Fields) { + Plan::template for_each_input([&](OperandT, std::size_t index) { + using Evaluator = OperandEvaluator; + constexpr auto column = Plan::template column_index(); + columns[column].element_values(data.element, *directions[column], workspace.element_values); + Evaluator::evaluate(data.geometry->operand_basis[index], workspace.element_values, workspace.operand_values[index], workspace.basis); + for (int point = 0; point < points; ++point) { + for (int output = 0; output < Test::components; ++output) { + const int row = point * Test::components + output; + for (int input = 0; input < Evaluator::components; ++input) { + workspace.quadrature_values[row] += data.weighted_field_derivatives(row, Plan::template component_offset() + input) + * workspace.operand_values[index][point * Evaluator::components + input]; + } + } + } + }); + } + if constexpr (Geometry) { + displacement_binding.element_values(data.element, displacement_direction, workspace.displacement_direction); + data.basis->displacement_basis.evaluate(workspace.displacement_direction, workspace.displacement_samples, workspace.basis); + } + finish_element_action(data, geometry_derivatives, workspace, result); + } + + /** + * @brief element_jacobian_action for several displacement directions of one element. + * @note The displacement samples of all directions come from one lane-batched basis evaluation (each lane equals + * the single evaluation); every direction is then finished exactly as element_jacobian_action finishes it. + */ + template + void element_geometry_actions( + const ElementData::operand_count>& data, + const ::mfem::DenseMatrix& geometry_derivatives, + const discretization::backend::mfem::DofMap::Elements& displacement_binding, + std::span displacement_directions, + Workspace& workspace, + std::span<::mfem::Vector* const> results + ) { + using Test = OperandEvaluator>; + + const int lanes = static_cast(displacement_directions.size()); + const int points = data.basis->rule->GetNPoints(); + for (int lane = 0; lane < lanes; ++lane) { + displacement_binding.element_values(data.element, *displacement_directions[static_cast(lane)], workspace.displacement_direction); + const int width = workspace.displacement_direction.Size(); + workspace.lane_coefficients.SetSize(width * lanes); + for (int i = 0; i < width; ++i) { + workspace.lane_coefficients[i * lanes + lane] = workspace.displacement_direction[i]; + } + } + data.basis->displacement_basis.evaluate_many(workspace.lane_coefficients, workspace.lane_samples, lanes, workspace.basis); + const int samples = workspace.lane_samples.Size() / lanes; + for (int lane = 0; lane < lanes; ++lane) { + workspace.displacement_samples.SetSize(samples); + for (int i = 0; i < samples; ++i) { + workspace.displacement_samples[i] = workspace.lane_samples[i * lanes + lane]; + } + workspace.quadrature_values.SetSize(points * Test::components); + workspace.quadrature_values = 0.0; + finish_element_action(data, geometry_derivatives, workspace, *results[static_cast(lane)]); + } + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/element_preparation.hpp b/src/include/serif/backend/mfem/operators/element_preparation.hpp new file mode 100644 index 00000000..42625ef7 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/element_preparation.hpp @@ -0,0 +1,327 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/backend/mfem/operators/element_data.hpp" +#include "serif/backend/mfem/operators/operand_evaluation.hpp" +#include "serif/backend/mfem/operators/quadrature.hpp" +#include "serif/backend/mfem/operators/rejection.hpp" +#include "serif/backend/mfem/discretization/term_coefficients.hpp" +#include "serif/backend/mfem/operators/workspace.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" +#include "serif/utils/misc/compensated_sum.hpp" + +namespace serif::operators::backend::mfem { + /// Sample the contribution's quadrature rule and bases on the fixed computational mesh. + template + [[nodiscard]] std::expected::operand_count>, PreparationRejection> sample_element_basis( + const ContributionT& equation, + int element, + const discretization::backend::mfem::DofMap& row, + const discretization::backend::mfem::DofMap& displacement_binding, + const std::array::column_count>& columns, + const mapping::DomainMapper& mapper, + const discretization::quadrature::Policy& policy, + TensorBasisCache& basis_cache, + ContractionMode contraction_mode + ) { + using Plan = EquationPlan; + using Test = OperandEvaluator>; + + auto& transformation = *displacement_binding.space().GetElementTransformation(element); + const auto& displacement_element = *displacement_binding.space().GetFE(element); + const auto mapping_kind = mapper.is_compactified_element(transformation) ? discretization::quadrature::MappingKind::kelvin : discretization::quadrature::MappingKind::general; + ElementBasis basis{ + .element = element + }; + int reconstruction_order = 0; + const auto include_reconstruction = [&](const auto& binding) { + if (binding.reconstruction_element(element) != nullptr) { + reconstruction_order = std::max(reconstruction_order, binding.reconstruction()->integration_order_increment()); + } + }; + include_reconstruction(row); + Plan::template for_each_input([&](OperandT, std::size_t) { + include_reconstruction(*columns[Plan::template column_index()]); + }); + basis.rule = &equation_rule(equation, policy, transformation, displacement_element, mapping_kind, reconstruction_order); + basis.reference_geometry.reserve(basis.rule->GetNPoints()); + for (int point = 0; point < basis.rule->GetNPoints(); ++point) { + auto geometry = mapping::sample_fixed_mesh_geometry(transformation, basis.rule->IntPoint(point)); + if (!geometry) { + return std::unexpected(PreparationRejection{ + PreparationRejectionReason::MAPPING, geometry.error(), element, point, "Invalid reference mesh geometry." + }); + } + basis.reference_geometry.push_back(std::move(*geometry)); + } + BasisWorkspace workspace; + Test::sample_basis(row, element, *basis.rule, basis.reference_test_basis, basis_cache, contraction_mode, transformation, workspace); + basis.displacement_basis.sample( + displacement_element, *basis.rule, ReferenceOperation::value_and_gradient, 3, basis_cache, contraction_mode + ); + Plan::template for_each_input([&](OperandT, std::size_t index) { + static_assert(EvaluableOperand, "The MFEM operator has no evaluator for this operand."); + const auto& binding = *columns[Plan::template column_index()]; + OperandEvaluator::sample_basis( + binding, element, *basis.rule, basis.reference_operand_basis[index], basis_cache, contraction_mode, transformation, workspace + ); + }); + + return basis; + } + + /// Contract one operand's fixed-mesh basis with its current coefficients. + template + requires std::same_as + [[nodiscard]] ::mfem::Vector sample_element_operand( + const ElementBasis::operand_count>& basis, + const CoefficientView& input, + const discretization::backend::mfem::DofMap::Elements& elements, + Workspace& workspace + ) { + using Plan = EquationPlan; + constexpr std::size_t index = equations::OperandValues::template index(); + + ::mfem::Vector values; + elements.element_values(basis.element, input.values, workspace.element_values); + OperandEvaluator::evaluate(basis.reference_operand_basis[index], workspace.element_values, values, workspace.basis); + + return values; + } + + /// Freeze displacement and compactification, then map quadrature points and bases to physical space. + template + [[nodiscard]] std::expected::operand_count>, PreparationRejection> map_element_geometry( + const ElementBasis::operand_count>& basis, + const discretization::backend::mfem::DofMap& displacement_binding, + const discretization::backend::mfem::DofMap::Elements& displacement_elements, + const ::mfem::Vector& displacement, + const MappingCoefficients& compactification, + const mapping::DomainMapper& mapper, + Workspace& workspace + ) { + using Plan = EquationPlan; + using Test = OperandEvaluator>; + + ElementGeometry geometry; + if constexpr (Test::geometry_dependent) { + geometry.test_basis.reset_from(basis.reference_test_basis, Test::components); + } else { + geometry.test_basis = basis.reference_test_basis; + } + Plan::template for_each_input([&](OperandT, std::size_t index) { + using Evaluator = OperandEvaluator; + if constexpr (Evaluator::geometry_dependent) { + geometry.operand_basis[index].reset_from(basis.reference_operand_basis[index], Evaluator::components); + } else { + geometry.operand_basis[index] = basis.reference_operand_basis[index]; + } + }); + + displacement_elements.element_values(basis.element, displacement, geometry.displacement); + ::mfem::Array compactification_dofs; + // A local DofTransformation keeps concurrent element loops off the space's shared one. + ::mfem::DofTransformation compactification_transform; + compactification.binding.GetElementVDofs(basis.element, compactification_dofs, compactification_transform); + compactification.values.GetSubVector(compactification_dofs, geometry.compactification); + if (compactification_transform.GetDofTransformation() != nullptr) { + compactification_transform.InvTransformPrimal(geometry.compactification); + } + + basis.displacement_basis.evaluate(geometry.displacement, workspace.displacement_samples, workspace.basis); + + const auto& displacement_element = *displacement_binding.space().GetFE(basis.element); + const auto displacement_data = mapping::element_displacement_data_from_element_vdofs(displacement_element, geometry.displacement); + const mapping::ElementCompactificationData compactification_data( + *compactification.binding.GetFE(basis.element), geometry.compactification + ); + const mapping::ElementMappingData mapping_data{displacement_data, compactification_data}; + ::mfem::IsoparametricTransformation transformation; + ::serif::utils::parallel::element_transformation(*displacement_binding.space().GetMesh(), basis.element, transformation); + const int point_count = basis.rule->GetNPoints(); + geometry.mapping.set_size(point_count, 3); + for (int point = 0; point < point_count; ++point) { + for (int axis = 0; axis < 3; ++axis) { + workspace.displacement_value[axis] = workspace.displacement_samples[12 * point + 4 * axis]; + for (int derivative = 0; derivative < 3; ++derivative) { + workspace.displacement_gradient(axis, derivative) = workspace.displacement_samples[12 * point + 4 * axis + derivative + 1]; + } + } + const mapping::DMReferenceDisplacementSample sample{workspace.displacement_value, workspace.displacement_gradient}; + const auto status = mapper.evaluate_volume( + mapping_data, transformation, basis.rule->IntPoint(point), + workspace.mapping_workspace, workspace.mapping_context, &sample, &basis.reference_geometry[point] + ); + if (status != mapping::MappingStatus::VALID) { + return std::unexpected(PreparationRejection{ + PreparationRejectionReason::MAPPING, status, basis.element, point, "Invalid equation geometry." + }); + } + geometry.mapping.store(point, workspace.mapping_context); + const auto& context = workspace.mapping_context.mapping; + if constexpr (Test::geometry_dependent) { + Test::map_basis_point(point, context, basis.reference_test_basis, geometry.test_basis, workspace.basis); + } + Plan::template for_each_input([&](OperandT, std::size_t index) { + if constexpr (OperandEvaluator::geometry_dependent) { + OperandEvaluator::map_basis_point(point, context, basis.reference_operand_basis[index], geometry.operand_basis[index], workspace.basis); + } + }); + } + + return geometry; + } + + /// Apply an operand's physical transformation to its cached fixed-mesh samples. + template + [[nodiscard]] ::mfem::Vector map_element_operand( + const ::mfem::Vector& reference, + const ElementGeometry& geometry, + Workspace& workspace + ) { + using Evaluator = OperandEvaluator; + + const int point_count = geometry.mapping.get_point_count(); + ::mfem::Vector values(point_count * Evaluator::components); + for (int point = 0; point < point_count; ++point) { + geometry.mapping.load(point, workspace.mapping_context); + const auto value = Evaluator::physical_value( + Evaluator::reference_value(reference, point), workspace.mapping_context.mapping + ); + for (int component = 0; component < Evaluator::components; ++component) { + values[point * Evaluator::components + component] = discretization::forms::component(value, component); + } + } + + return values; + } + + /// Pack the physical samples requested by this contribution at one quadrature point. + template + [[nodiscard]] equations::EquationPoint element_equation_point( + const std::array, EquationPlan::operand_count>& physical_values, + const mapping::MappingPointContext& context, + int point + ) { + using Plan = EquationPlan; + + equations::EquationPoint values; + Plan::template for_each_input([&](OperandT, std::size_t index) { + using Evaluator = OperandEvaluator; + auto& value = values.fields.template get(); + for (int component = 0; component < Evaluator::components; ++component) { + discretization::forms::component(value, component) = (*physical_values[index])[point * Evaluator::components + component]; + } + }); + for (int axis = 0; axis < 3; ++axis) { + values.position[axis] = context.physical_position[axis]; + } + + return values; + } + + /// Weight a point's residual and derivatives; reject any non-finite stored component. + template + [[nodiscard]] bool store_element_linearization( + const equations::PointLinearization>::Value>& physics, + int point, + double weight, + ElementData::operand_count>& data + ) { + using Plan = EquationPlan; + using Test = OperandEvaluator>; + using Result = typename Test::Value; + + bool finite = true; + for (int output = 0; output < Test::components; ++output) { + const int row_index = point * Test::components + output; + data.weighted_residual[row_index] = weight * discretization::forms::component(physics.residual, output); + finite = finite && std::isfinite(data.weighted_residual[row_index]); + Plan::template for_each_input([&](OperandT, std::size_t) { + using Input = typename OperandEvaluator::Value; + for (int input = 0; input < OperandEvaluator::components; ++input) { + auto& derivative = data.weighted_field_derivatives(row_index, Plan::template component_offset() + input); + derivative = weight * discretization::forms::derivative_component( + physics.field_derivatives.template get(), output, input + ); + finite = finite && std::isfinite(derivative); + } + }); + for (int axis = 0; axis < 3; ++axis) { + auto& derivative = data.weighted_position_derivative(row_index, axis); + derivative = weight * discretization::forms::derivative_component( + physics.position_derivative, output, axis + ); + finite = finite && std::isfinite(derivative); + } + } + + return finite; + } + + /// Evaluate and weight the equation while retaining its immutable preparation inputs. + template + [[nodiscard]] std::expected::operand_count>, PreparationRejection> evaluate_element_equation( + const ContributionT& equation, + double coefficient, + std::shared_ptr::operand_count>> basis, + std::shared_ptr::operand_count>> geometry, + std::array, EquationPlan::operand_count> reference_values, + const std::array, EquationPlan::operand_count>& physical_values, + Workspace& workspace + ) { + using Plan = EquationPlan; + using Test = OperandEvaluator>; + using Result = typename Test::Value; + + ElementData data{ + .element = basis->element, + .basis = std::move(basis), + .geometry = std::move(geometry), + .reference_operand_values = std::move(reference_values) + }; + const int point_count = data.basis->rule->GetNPoints(); + data.weighted_residual.SetSize(point_count * Test::components); + data.weighted_field_derivatives.SetSize(point_count * Test::components, Plan::component_count); + data.weighted_field_derivatives = 0.0; + data.weighted_position_derivative.SetSize(point_count * Test::components, 3); + utils::CompensatedSum element_validation; + for (int point = 0; point < point_count; ++point) { + data.geometry->mapping.load(point, workspace.mapping_context); + const auto values = element_equation_point( + physical_values, workspace.mapping_context.mapping, point + ); + equations::PointLinearization physics; + try { + physics = equation.evaluate(values); + if constexpr (requires { equation.validate_integral(0.0); }) { + // Physical admissibility is independent of equation arithmetic coefficients. + element_validation.add( + workspace.mapping_context.quadrature.physical_integration_weight * equation.validation_integrand(values, physics) + ); + } + } catch (const std::domain_error& error) { + return std::unexpected(PreparationRejection{ + PreparationRejectionReason::EQUATION, mapping::MappingStatus::VALID, data.element, point, error.what() + }); + } + // Geometry variations still need the unscaled physical integration weight. + const double weight = coefficient * workspace.mapping_context.quadrature.physical_integration_weight; + if (!store_element_linearization(physics, point, weight, data)) { + return std::unexpected(PreparationRejection{ + PreparationRejectionReason::NON_FINITE, mapping::MappingStatus::VALID, + data.element, point, "Non-finite equation integrand or derivative." + }); + } + } + + data.validation_integral = element_validation.value(); + return data; + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/element_transpose.hpp b/src/include/serif/backend/mfem/operators/element_transpose.hpp new file mode 100644 index 00000000..a494d79f --- /dev/null +++ b/src/include/serif/backend/mfem/operators/element_transpose.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "serif/backend/mfem/operators/element_evaluation.hpp" + +namespace serif::operators::backend::mfem { + /** @brief Transpose the same accepted field and geometry contractions used by the forward action. */ + template + void element_jacobian_transpose( + const ElementData::operand_count>& data, + const ::mfem::DenseMatrix& geometry_derivatives, + const discretization::backend::mfem::DofMap::Elements& row, + const ::mfem::Vector& residual_dual, + Workspace& workspace, + std::array<::mfem::Vector, EquationPlan::column_count>& column_duals, + ::mfem::Vector& displacement_dual + ) { + using namespace utils::errors; + using Plan = EquationPlan; + using Test = OperandEvaluator>; + + for (::mfem::Vector& column : column_duals) { + column.SetSize(0); + } + row.element_values(data.element, residual_dual, workspace.element_values); + Test::evaluate(data.geometry->test_basis, workspace.element_values, workspace.test_values, workspace.basis); + data.basis->reference_test_basis.evaluate(workspace.element_values, workspace.reference_test_values, workspace.basis); + const int points = data.basis->rule->GetNPoints(); + Plan::template for_each_input([&](OperandT, std::size_t index) { + using Evaluator = OperandEvaluator; + workspace.operand_dual.SetSize(points * Evaluator::components); + for (int point = 0; point < points; ++point) { + for (int input = 0; input < Evaluator::components; ++input) { + double value = 0.0; + for (int output = 0; output < Test::components; ++output) { + const int row_index = point * Test::components + output; + value += data.weighted_field_derivatives(row_index, Plan::template component_offset() + input) * workspace.test_values[row_index]; + } + workspace.operand_dual[point * Evaluator::components + input] = value; + } + } + Evaluator::integrate(data.geometry->operand_basis[index], workspace.operand_dual, workspace.element_result, workspace.basis); + auto& column = column_duals[Plan::template column_index()]; + if (column.Size() == 0) { + column = workspace.element_result; + } else { + column += workspace.element_result; + } + }); + workspace.displacement_dual.SetSize(12 * points); + // Use the same stored columns and component order as the forward geometry action. + const int derivative_stride = geometry_derivatives.Height(); + const double* derivatives = geometry_derivatives.GetData(); + const double* test_values = workspace.reference_test_values.GetData(); + double* displacement = workspace.displacement_dual.GetData(); + for (int point = 0; point < points; ++point) { + for (int input = 0; input < 12; ++input) { + double value = 0.0; + for (int output = 0; output < Test::reference_components; ++output) { + const int row_index = point * Test::reference_components + output; + value += derivatives[input * derivative_stride + row_index] * test_values[row_index]; + } + serif_ensure( + std::isfinite(value), "Non-finite geometry Jacobian transpose action." + ); + displacement[12 * point + input] = value; + } + } + data.basis->displacement_basis.integrate(workspace.displacement_dual, displacement_dual, workspace.basis); + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/geometry_derivatives.hpp b/src/include/serif/backend/mfem/operators/geometry_derivatives.hpp new file mode 100644 index 00000000..39041ec9 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/geometry_derivatives.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include + +#include "serif/backend/mfem/operators/element_data.hpp" +#include "serif/backend/mfem/operators/operand_evaluation.hpp" +#include "serif/backend/mfem/operators/rejection.hpp" +#include "serif/backend/mfem/operators/workspace.hpp" +#include "serif/operators/plan.hpp" +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" + +namespace serif::operators::backend::mfem { + /** @brief Differentiate the weighted physics through position, operand mappings and the volume measure. */ + template + [[nodiscard]] auto geometry_integrand_variation( + const ElementData::operand_count>& data, + int point, + const mapping::VolumeMappingContext& context, + const mapping::VolumeMappingVariation& variation + ) { + using Plan = EquationPlan; + using Test = OperandEvaluator>; + + typename Test::Value result{}; + for (int output = 0; output < Test::components; ++output) { + const int row = point * Test::components + output; + double value = data.weighted_residual[row] * variation.weight_variation / context.quadrature.physical_integration_weight; + for (int axis = 0; axis < 3; ++axis) { + value += data.weighted_position_derivative(row, axis) * variation.mapping.physical_position_variation[axis]; + } + discretization::forms::component(result, output) = value; + } + Plan::template for_each_input([&](Operand, std::size_t index) { + using Evaluator = OperandEvaluator; + if constexpr (Evaluator::geometry_dependent) { + const auto delta = Evaluator::geometry_variation( + Evaluator::reference_value(*data.reference_operand_values[index], point), context.mapping, variation.mapping + ); + for (int output = 0; output < Test::components; ++output) { + for (int input = 0; input < Evaluator::components; ++input) { + discretization::forms::component(result, output) += data.weighted_field_derivatives( + point * Test::components + output, Plan::template component_offset() + input + ) * discretization::forms::component(delta, input); + } + } + } + }); + return result; + } + + /** + * @brief Differentiate one weighted integrand with respect to local displacement values and gradients. + * @note The graph owns this matrix. Its twelve columns are u_c and partial_{xi_k} u_c, ordered + * as the displacement basis samples. Rows are quadrature points times fixed-mesh test components. + * This includes the variation of the test map: subsequent forward and transpose actions therefore + * use the same fixed reference basis and never reevaluate the physical mapping. + * The twelve directions are exact local derivatives, independent of finite-element order; they + * are neither finite differences nor probes of the global coefficient vector. + */ + template + [[nodiscard]] std::expected<::mfem::DenseMatrix, PreparationRejection> element_geometry_derivatives( + const ElementData::operand_count>& data, + const discretization::backend::mfem::DofMap& displacement_binding, + const ::mfem::FiniteElementSpace& compactification_space, + const mapping::DomainMapper& mapper, + Workspace& workspace + ) { + using Test = OperandEvaluator>; + + const auto& element = *displacement_binding.space().GetFE(data.element); + workspace.element_values = data.geometry->displacement; + const auto base = mapping::element_displacement_data_from_element_vdofs(element, workspace.element_values); + const mapping::ElementCompactificationData compactification( + *compactification_space.GetFE(data.element), data.geometry->compactification + ); + const mapping::ElementMappingData mapping_data{base, compactification}; + ::mfem::IsoparametricTransformation transformation; + ::serif::utils::parallel::element_transformation(*displacement_binding.space().GetMesh(), data.element, transformation); + const int points = data.basis->rule->GetNPoints(); + ::mfem::DenseMatrix result(points * Test::reference_components, 12); + const mapping::DMReferenceDisplacementSample sample{workspace.displacement_value, workspace.displacement_gradient}; + mapping::CompactificationPointData compactification_sample; + ::mfem::DenseMatrix inverse_mesh_jacobian(3); + for (int point = 0; point < points; ++point) { + data.geometry->mapping.load(point, workspace.mapping_context); + // All twelve directions share the inverse fixed-mesh Jacobian, the compactification sample + // and the physical test values at this point. + ::mfem::Mult(workspace.mapping_context.quadrature.J_element_to_physical_inverse, + workspace.mapping_context.mapping.mapping_jacobian, inverse_mesh_jacobian); + const mapping::CompactificationPointData* fixed_sample = nullptr; + if (workspace.mapping_context.mapping.compactified) { + const auto status = mapper.evaluate_compactification_coordinate( + compactification, transformation, data.basis->rule->IntPoint(point), + workspace.mapping_workspace, compactification_sample, &inverse_mesh_jacobian + ); + if (status != mapping::MappingStatus::VALID) { + return std::unexpected(PreparationRejection{ + .reason = PreparationRejectionReason::MAPPING, .mapping_status = status, + .element = data.element, .quadrature_point = point, + .message = "Invalid fixed compactification sample for an equation derivative." + }); + } + fixed_sample = &compactification_sample; + } + std::array tests; + for (int test_component = 0; test_component < Test::reference_components; ++test_component) { + typename Test::ReferenceValue reference{}; + discretization::forms::component(reference, test_component) = 1.0; + tests[test_component] = Test::physical_value(reference, workspace.mapping_context.mapping); + } + for (int component = 0; component < 12; ++component) { + workspace.displacement_value = 0.0; + workspace.displacement_gradient = 0.0; + if (component % 4 == 0) { + workspace.displacement_value[component / 4] = 1.0; + } else { + workspace.displacement_gradient(component / 4, component % 4 - 1) = 1.0; + } + const auto status = mapper.evaluate_volume_direction_variation( + mapping_data, base, transformation, data.basis->rule->IntPoint(point), + workspace.mapping_context, workspace.mapping_workspace, workspace.mapping_variation, sample, fixed_sample, + inverse_mesh_jacobian + ); + if (status != mapping::MappingStatus::VALID) { + return std::unexpected(PreparationRejection{ + .reason = PreparationRejectionReason::MAPPING, .mapping_status = status, + .element = data.element, .quadrature_point = point, + .message = "Invalid local displacement derivative of an equation." + }); + } + const auto physics = geometry_integrand_variation( + data, point, workspace.mapping_context, workspace.mapping_variation + ); + for (int test_component = 0; test_component < Test::reference_components; ++test_component) { + typename Test::ReferenceValue reference{}; + discretization::forms::component(reference, test_component) = 1.0; + const auto& test = tests[test_component]; + const auto test_variation = Test::geometry_variation(reference, workspace.mapping_context.mapping, workspace.mapping_variation.mapping); + double value = 0.0; + for (int output = 0; output < Test::components; ++output) { + value += discretization::forms::component(test, output) * discretization::forms::component(physics, output) + + data.weighted_residual[point * Test::components + output] * discretization::forms::component(test_variation, output); + } + if (!std::isfinite(value)) { + return std::unexpected(PreparationRejection{ + .reason = PreparationRejectionReason::NON_FINITE, .element = data.element, + .quadrature_point = point, .message = "Non-finite local displacement derivative of an equation." + }); + } + result(point * Test::reference_components + test_component, component) = value; + } + } + } + return result; + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/jacobian_operator.hpp b/src/include/serif/backend/mfem/operators/jacobian_operator.hpp new file mode 100644 index 00000000..a0d0a40d --- /dev/null +++ b/src/include/serif/backend/mfem/operators/jacobian_operator.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "mfem/linalg/operator.hpp" +#include "serif/backend/mfem/operators/volume_application.hpp" + +namespace serif::operators::backend::mfem { + /// Adapt an accepted dependency cache directly to MFEM's linear operator interface. + /// The cache must outlive this adapter. Calls sharing spaces or a mapper must be serialized. + template + class JacobianOperator final : public ::mfem::Operator { + public: + /// Fix dimensions from an already accepted volume linearization. + explicit JacobianOperator(const CacheT& cache) : + Operator(volume_height(cache.template get()), volume_width(cache.template get())), + m_cache(cache) {} + + JacobianOperator(CacheT&&) = delete; + JacobianOperator(const CacheT&&) = delete; + + /// Apply the latest accepted Jacobian; a failed preparation makes this unavailable. + void Mult(const ::mfem::Vector& direction, ::mfem::Vector& result) const override { + const auto& data = checked_data(); + apply_jacobian(data, direction, m_workspace, result); + } + + /// Apply the coordinate transpose, including operand, test and geometry variations. + void MultTranspose(const ::mfem::Vector& dual, ::mfem::Vector& result) const override { + const auto& data = checked_data(); + apply_jacobian_transpose(data, dual, m_workspace, result); + } + + private: + /// MFEM's fixed dimensions must still match the cache's current accepted result. + [[nodiscard]] const auto& checked_data() const { + using namespace utils::errors; + + const auto& data = m_cache.template get(); + serif_ensure( + volume_width(data) == Width() && volume_height(data) == Height(), + "Equation operator dimensions changed unexpectedly."); + return data; + } + + const CacheT& m_cache; + mutable Workspace m_workspace; + }; +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/linearized_block.hpp b/src/include/serif/backend/mfem/operators/linearized_block.hpp new file mode 100644 index 00000000..ebf0bbb5 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/linearized_block.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include "mfem/linalg/operator.hpp" +#include "mfem/linalg/sparsemat.hpp" + +namespace serif::operators::backend::mfem +{ + /** @brief An accepted physical block with optional efficient diagonal and sparse assembly operations. */ + class LinearizedBlock final + { + public: + using Action = std::shared_ptr; + using Diagonal = std::function()>; + using Assembly = std::function()>; + + /** @brief Retain an immutable action and its matching assembly routines; captures must retain their accepted data. */ + explicit LinearizedBlock(Action action, Diagonal diagonal = {}, Assembly assembly = {}); + + /** @brief Read the physical action belonging to this accepted block. */ + [[nodiscard]] const Action& action() const noexcept; + + /** @brief Whether diagonal extraction is available without global column probing. */ + [[nodiscard]] bool has_diagonal() const noexcept; + + /** @brief Whether this block supports explicit sparse assembly. */ + [[nodiscard]] bool has_assembly() const noexcept; + + /** @brief Extract the physical operator diagonal, including any installed boundary rows. */ + [[nodiscard]] std::vector diagonal() const; + + /** @brief Assemble only this selected physical block. */ + [[nodiscard]] std::shared_ptr assemble() const; + + /** @brief Accepted block identity is stable across copies, but changes when a new linearization is supplied. */ + [[nodiscard]] bool operator==(const LinearizedBlock&) const = default; + + private: + struct Data; + std::shared_ptr m_data; + }; + + /** @brief Extract a diagonal and reject a callback that violates its declared physical block dimensions. */ + [[nodiscard]] std::vector checked_diagonal(const LinearizedBlock& block); + + /** @brief Assemble a non-null finalized sparse matrix whose dimensions match its declared physical action. */ + [[nodiscard]] std::shared_ptr checked_assembly(const LinearizedBlock& block); + + /** @brief Retain an already assembled physical block without copying its matrix. */ + [[nodiscard]] LinearizedBlock linearized_block(std::shared_ptr matrix); + + /** @brief Retain a small assembled physical block, such as a generated scalar constraint. */ + [[nodiscard]] LinearizedBlock linearized_block(std::shared_ptr matrix); +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/operand_evaluation.hpp b/src/include/serif/backend/mfem/operators/operand_evaluation.hpp new file mode 100644 index 00000000..67e88b8a --- /dev/null +++ b/src/include/serif/backend/mfem/operators/operand_evaluation.hpp @@ -0,0 +1,339 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "mfem/fem/fe/fe_base.hpp" +#include "mfem/fem/eltrans.hpp" +#include "serif/operators/concepts.hpp" +#include "serif/backend/mfem/discretization/basis_evaluation.hpp" +#include "serif/backend/mfem/discretization/dof_map.hpp" +#include "serif/backend/mfem/mapping/transformations.hpp" +#include "serif/utils/error/errors.hpp" +#include "serif/utils/misc/compensated_sum.hpp" + +namespace serif::operators::backend::mfem { + using discretization::backend::mfem::BasisEvaluation; + using discretization::backend::mfem::BasisWorkspace; + using discretization::backend::mfem::ContractionMode; + using discretization::backend::mfem::ReferenceOperation; + using discretization::backend::mfem::TensorBasisCache; + + // Intentionally undefined: an operation tag alone never enables evaluation. + // New operations must supply their own implementation and linearization rules. + template + struct OperandEvaluator; + + /// Scalar and vector H1 gradients share the existing value/divergence coefficient contractions. + template + struct OperandEvaluator final { + using Term = typename OperandT::term_type; + using Value = discretization::forms::OperandValue; + + static constexpr int components = discretization::forms::component_count; + static constexpr bool global_scalar = discretization::blocks::GlobalScalarTerm; + static constexpr bool divergence = std::same_as; + static constexpr bool vector_copies = [] { + if constexpr (global_scalar) { + return false; + } else { + return discretization::blocks::SpatialTermTraits::rank == 1; + } + }(); + static constexpr bool h1_divergence = vector_copies && divergence; + static constexpr bool scalar_gradient = ScalarGradientOperand; + static constexpr bool vector_gradient = VectorGradientOperand; + static constexpr bool needs_vector_gradient = h1_divergence || vector_gradient; + static constexpr bool geometry_dependent = needs_vector_gradient || scalar_gradient; + + // H1 divergence and gradient retain G[c,k] = partial_X_k(u_c) in component-major order. + // Only divergence contracts the mapped gradient to a scalar. + using ReferenceValue = std::conditional_t, Value>; + static constexpr int reference_components = needs_vector_gradient ? 9 : components; + + /// A global coefficient has basis one; spatial operands use their native element basis. + static void sample_basis( + const discretization::backend::mfem::DofMap& binding, + int element, + const ::mfem::IntegrationRule& rule, + BasisEvaluation& basis, + TensorBasisCache& cache, + ContractionMode mode, + ::mfem::ElementTransformation& transformation, + BasisWorkspace& workspace + ) { + if constexpr (global_scalar) { + basis.set_constant(rule.GetNPoints()); + } else { + sample_basis(*binding.space().GetFE(element), rule, basis, cache, mode, transformation, workspace, binding.reconstruction_element(element)); + } + } + + /// Sample a canonical tensor/dense basis, then apply MFEM's fixed-mesh mapping. + static void sample_basis( + const ::mfem::FiniteElement& element, + const ::mfem::IntegrationRule& rule, + BasisEvaluation& basis, + TensorBasisCache& cache, + ContractionMode mode, + ::mfem::ElementTransformation& transformation, + BasisWorkspace& workspace, + const discretization::backend::mfem::ElementReconstruction* reconstruction = nullptr + ) requires (!global_scalar) { + using namespace utils::errors; + + serif_ensure( + element.GetRangeType() == ::mfem::FiniteElement::SCALAR && element.GetMapType() == ::mfem::FiniteElement::VALUE, + "Operand sampling requires scalar VALUE bases (one or three copies)." + ); + + const auto operation = (needs_vector_gradient || scalar_gradient) ? ReferenceOperation::gradient : ReferenceOperation::value; + BasisEvaluation canonical; + canonical.sample(element, rule, operation, vector_copies ? 3 : 1, cache, mode, reconstruction); + if constexpr (!geometry_dependent) { + basis = std::move(canonical); + } else { + basis.reset_from(canonical, reference_components); + for (int point = 0; point < rule.GetNPoints(); ++point) { + transformation.SetIntPoint(&rule.IntPoint(point)); + basis.map_point(point, canonical, [&](const ::mfem::Vector& input, ::mfem::Vector& output) { + if constexpr (needs_vector_gradient) { + const ::mfem::DenseMatrix& inverse = transformation.InverseJacobian(); + output = 0.0; + for (int component = 0; component < 3; ++component) { + for (int physical = 0; physical < 3; ++physical) { + for (int reference = 0; reference < 3; ++reference) { + output[3 * component + physical] += input[3 * component + reference] * inverse(reference, physical); + } + } + } + } else { + transformation.InverseJacobian().MultTranspose(input, output); + } + }, workspace); + } + } + } + + /// Apply SERiF's map to small tensor point maps, or to dense basis columns on other elements. + static void map_basis_point( + int point, + const mapping::MappingPointContext& context, + const BasisEvaluation& reference, + BasisEvaluation& result, + BasisWorkspace& workspace, + const mapping::MappingPointVariation* variation = nullptr + ) { + result.map_point(point, reference, [&](const ::mfem::Vector& input, ::mfem::Vector& output) { + ReferenceValue value{}; + for (int axis = 0; axis < reference_components; ++axis) { + discretization::forms::component(value, axis) = input[axis]; + } + const Value mapped = variation ? geometry_variation(value, context, *variation) : physical_value(value, context); + for (int axis = 0; axis < components; ++axis) { + output[axis] = discretization::forms::component(mapped, axis); + } + }, workspace); + } + + /* + * For a scalar VALUE operand, the following contraction evaluates a scalar + * finite-element field at quadrature points. Vector H1 and H(div) use the + * same matrix-vector product with its component copies or gradient bases. + * + * On one element, the field is represented by + * + * ρ_h(ξ) = ∑_{j=1}^{n} c_j N_j(ξ), + * + * where n is the number of local scalar DOFs, c_j is the coefficient + * associated with basis function N_j, and ξ is a coordinate in the + * canonical reference element. + * + * There are two coordinate maps to distinguish: + * + * ξ -> X: MFEM's canonical element to the fixed computational mesh. + * X -> x: SERiF's additional mapping to physical space. + * + * Here ρ_h(ξ) denotes the scalar field pulled back to the canonical + * element. Its basis values use the ordinary scalar VALUE mapping. + * + * sample_basis evaluates each N_j at every quadrature point ξ_q, + * producing the basis evaluation matrix + * + * E_qj = N_j(ξ_q). + * + * The physical E has one row per point for scalar values and divergences, + * and three rows per point for vector values. Before the additional map, + * H1 divergence retains nine gradient entries per point. Vector H1 has three independent component copies of each scalar basis. + * + * element_values extracts this element's coefficients from the supported + * field vector, including the required DOF sign and orientation handling. + * We then size the output vector to the number of quadrature points. + * + * Finally, evaluate performs the contraction represented by the matrix-vector product + * + * operand_values = E c, + * + * using either the dense matrix or equivalent tensor contractions, + * + * so that + * + * operand_values[q] = ∑_j N_j(ξ_q) c_j = ρ_h(ξ_q). + * + * The expansion above writes c_j N_j, while this matrix product writes + * N_j c_j. These are products of individual scalars, whose order can be + * exchanged. Despite appearances (to me at least) we are not exchanging a matrix and a vector. + */ + /// Apply a cached tensor or dense basis using reusable contraction scratch. + static void evaluate( + const BasisEvaluation& basis, + const ::mfem::Vector& coefficients, + ::mfem::Vector& values, + BasisWorkspace& workspace + ) { + basis.evaluate(coefficients, values, workspace); + } + + /// Transpose the same contraction; global scalar reductions retain compensated summation. + static void integrate( + const BasisEvaluation& basis, + const ::mfem::Vector& values, + ::mfem::Vector& result, + BasisWorkspace& workspace + ) { + if constexpr (global_scalar) { + utils::CompensatedSum sum; + for (int point = 0; point < values.Size(); ++point) { + sum.add(values[point]); + } + result.SetSize(1); + result[0] = sum.value(); + } else { + basis.integrate(values, result, workspace); + } + } + + /// Read the reference data needed for one physical operand sample. + static ReferenceValue reference_value( + const ::mfem::Vector& values, + int point + ) { + ReferenceValue result{}; + for (int axis = 0; axis < reference_components; ++axis) { + discretization::forms::component(result, axis) = values[point * reference_components + axis]; + } + + return result; + } + + /// Apply only SERiF's additional fixed-mesh to physical map (MFEM's map is already included). + static Value physical_value( + const ReferenceValue& reference, + const mapping::MappingPointContext& context + ) { + if constexpr (h1_divergence) { + return gradient_trace(reference, context.inverse_mapping_jacobian); + } else if constexpr (vector_gradient) { + return transform_vector_gradient(reference, context.inverse_mapping_jacobian); + } else if constexpr (scalar_gradient) { + return transform_gradient(reference, context.inverse_mapping_jacobian); + } else { + return reference; + } + } + + /// At fixed coefficients: scalar gradients and H1 divergence vary through A^-1; values do not vary. + static Value geometry_variation( + const ReferenceValue& reference, + const mapping::MappingPointContext& context, + const mapping::MappingPointVariation& variation + ) { + if constexpr (h1_divergence) { + return gradient_trace(reference, variation.inverse_mapping_jacobian_variation); + } else if constexpr (vector_gradient) { + return transform_vector_gradient(reference, variation.inverse_mapping_jacobian_variation); + } else if constexpr (scalar_gradient) { + return transform_gradient(reference, variation.inverse_mapping_jacobian_variation); + } else { + return {}; + } + } + + private: + /** @brief Apply G_x = G_X A^-1; the inverse variation gives the same contraction for delta(G_x). */ + static Value transform_vector_gradient(const ReferenceValue& gradient, const ::mfem::DenseMatrix& inverse) requires vector_gradient { + Value result{}; + for (int component = 0; component < 3; ++component) { + for (int physical = 0; physical < 3; ++physical) { + for (int reference = 0; reference < 3; ++reference) { + result[component][physical] += gradient[3 * component + reference] * inverse(reference, physical); + } + } + } + return result; + } + + /// grad_x(z) = A^-T grad_X(z); the inverse variation gives its geometry derivative. + static Value transform_gradient( + const ReferenceValue& gradient, + const ::mfem::DenseMatrix& inverse + ) requires scalar_gradient { + Value result{}; + for (int physical = 0; physical < 3; ++physical) { + for (int reference = 0; reference < 3; ++reference) { + result[physical] += inverse(reference, physical) * gradient[reference]; + } + } + return result; + } + + /// div_x(u) = trace(G A^-1); replacing A^-1 by its variation gives delta(div_x(u)). + static double gradient_trace( + const ReferenceValue& gradient, + const ::mfem::DenseMatrix& inverse + ) requires h1_divergence { + double result = 0.0; + for (int component = 0; component < 3; ++component) { + for (int derivative = 0; derivative < 3; ++derivative) { + result += gradient[3 * component + derivative] * inverse(derivative, component); + } + } + + return result; + } + }; + + /// Check the complete interface used by preparation, Jacobian application, and its transpose. + template + concept EvaluableOperand = requires( + const discretization::backend::mfem::DofMap& binding, + const ::mfem::IntegrationRule& rule, + ::mfem::ElementTransformation& transformation, + const BasisEvaluation& reference_basis, + BasisEvaluation& mapped_basis, + TensorBasisCache& cache, + BasisWorkspace& workspace, + const ::mfem::Vector& coefficients, + ::mfem::Vector& values, + const typename OperandEvaluator::ReferenceValue& reference_value, + const mapping::MappingPointContext& context, + const mapping::MappingPointVariation& variation + ) { + typename OperandEvaluator::Value; + typename OperandEvaluator::ReferenceValue; + { OperandEvaluator::components } -> std::convertible_to; + { OperandEvaluator::reference_components } -> std::convertible_to; + { OperandEvaluator::geometry_dependent } -> std::convertible_to; + { OperandEvaluator::sample_basis(binding, 0, rule, mapped_basis, cache, ContractionMode::automatic, transformation, workspace) } -> std::same_as; + { OperandEvaluator::evaluate(reference_basis, coefficients, values, workspace) } -> std::same_as; + { OperandEvaluator::integrate(reference_basis, coefficients, values, workspace) } -> std::same_as; + { OperandEvaluator::reference_value(coefficients, 0) } -> std::same_as::ReferenceValue>; + { OperandEvaluator::physical_value(reference_value, context) } -> std::same_as::Value>; + { OperandEvaluator::geometry_variation(reference_value, context, variation) } -> std::same_as::Value>; + { OperandEvaluator::map_basis_point(0, context, reference_basis, mapped_basis, workspace) } -> std::same_as; + { OperandEvaluator::map_basis_point(0, context, reference_basis, mapped_basis, workspace, &variation) } -> std::same_as; + }; +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/preparation.hpp b/src/include/serif/backend/mfem/operators/preparation.hpp new file mode 100644 index 00000000..03f78681 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/preparation.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include "serif/utils/dependencies/preparation.hpp" +#include "serif/backend/mfem/operators/rejection.hpp" + +namespace serif::operators::backend::mfem { + using PreparationMode = utils::dependencies::PreparationMode; + + /// Numerical work derived from the dependency engine's most recent report. + struct NumericalWork { + std::size_t reused_elements{0}; + std::size_t geometry_points{0}; + std::size_t sampled_operands{0}; + std::size_t physics_points{0}; + std::size_t refreshed_scalars{0}; + + /// Report whether numerical preparation refreshed any samples or physics. + [[nodiscard]] bool did_work() const noexcept { + return geometry_points != 0 || sampled_operands != 0 || physics_points != 0 || refreshed_scalars != 0; + } + }; + + /// A failed validation or calculation must make the previous accepted result inaccessible. + template + [[nodiscard]] std::expected try_prepare_validated(CacheT& cache, PrepareT&& prepare) { + // Stack unwinding must invalidate the cache without intercepting unrelated caller errors. + struct InvalidateOnFailure final { + CacheT& cache; + bool prepared{false}; + + /** @brief Make previous results unavailable whenever preparation exits unsuccessfully. */ + ~InvalidateOnFailure() { + if (!prepared) { + cache.invalidate(); + } + } + }; + InvalidateOnFailure guard{.cache = cache}; + try { + const auto result = prepare(); + if (!result) { + return std::unexpected(result.error().error); + } + guard.prepared = true; + return {}; + } catch (const std::domain_error& error) { + return std::unexpected(PreparationRejection{ + .reason = PreparationRejectionReason::NON_FINITE, + .message = error.what() + }); + } + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/quadrature.hpp b/src/include/serif/backend/mfem/operators/quadrature.hpp new file mode 100644 index 00000000..7bc8c141 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/quadrature.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include "mfem/fem/eltrans.hpp" +#include "mfem/fem/fe/fe_base.hpp" +#include "serif/backend/mfem/discretization/quadrature/mfem_resolver.hpp" +#include "serif/operators/plan.hpp" + +namespace serif::operators::backend::mfem { + /// Resolve one rule covering all operand forms and the geometry weight. + template + [[nodiscard]] const ::mfem::IntegrationRule& equation_rule( + const ContributionT& equation, + const discretization::quadrature::Policy& policy, + ::mfem::ElementTransformation& transformation, + const ::mfem::FiniteElement& displacement_element, + discretization::quadrature::MappingKind mapping_kind = discretization::quadrature::MappingKind::general, + int reconstruction_order = 0 + ) { + using namespace discretization::quadrature; + + using Plan = EquationPlan; + + const discretization::quadrature::backend::mfem::MFEMResolver resolver(policy); + + const int position_order = std::max(transformation.Order(), displacement_element.GetOrder()); + const int weight_order = transformation.OrderW() + 3 * std::max(0, displacement_element.GetOrder() - 1); + int order = 0; + + Plan::template for_each_input([&]([[maybe_unused]] OperandT operand, std::size_t) { + using namespace utils::errors; + + using Form = Plan::template Form; + std::array extra{equation.additional_order(operand, position_order)}; + + serif_ensure( + extra[0] >= 0 && reconstruction_order >= 0 && + extra[0] <= std::numeric_limits::max() - weight_order - discretization::quadrature::backend::mfem::mfem_form_static_order
&& + reconstruction_order <= std::numeric_limits::max() - weight_order - discretization::quadrature::backend::mfem::mfem_form_static_order - extra[0], + "Equation quadrature order exceeds the supported range." + ); + + // Automatic rules account for the nonpolynomial profile; explicit policy orders remain authoritative. + extra[0] += reconstruction_order; + const auto resolved = resolver.template resolve({ + .geometry = transformation.GetGeometryType(), + .geometry_weight_order = weight_order, + .dynamic_orders = extra, + .domain = equations::SupportOf{}, + .mapping = mapping_kind + }); + + order = std::max(order, resolved.resolution.order); + }); + + return ::mfem::IntRules.Get(transformation.GetGeometryType(), order); + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/rejection.hpp b/src/include/serif/backend/mfem/operators/rejection.hpp new file mode 100644 index 00000000..2c6747f5 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/rejection.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "serif/backend/mfem/mapping/status.hpp" + +namespace serif::operators::backend::mfem { + enum class PreparationRejectionReason { + MAPPING, + EQUATION, + NON_FINITE + }; + + struct PreparationRejection final { + PreparationRejectionReason reason; + mapping::MappingStatus mapping_status{mapping::MappingStatus::VALID}; + int element{-1}; + int quadrature_point{-1}; + std::string message; + }; +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/replaced_rows.hpp b/src/include/serif/backend/mfem/operators/replaced_rows.hpp new file mode 100644 index 00000000..534c993b --- /dev/null +++ b/src/include/serif/backend/mfem/operators/replaced_rows.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include "serif/backend/mfem/operators/linearized_block.hpp" + +namespace serif::operators::backend::mfem +{ + /** + * @brief Replace selected physical equation rows with an immutable sparse row block in the same unknown coordinates. + * @note Replacement row i belongs to selected_rows[i]. Efficient diagonal and sparse assembly remain available when the original block supplies them; no columns are probed. Both matrices must remain immutable after publication. + */ + [[nodiscard]] LinearizedBlock replace_block_rows(LinearizedBlock original, std::vector selected_rows, std::shared_ptr replacement); +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/volume_application.hpp b/src/include/serif/backend/mfem/operators/volume_application.hpp new file mode 100644 index 00000000..96f618d1 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/volume_application.hpp @@ -0,0 +1,367 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/operators/volume_inputs.hpp" +#include "serif/backend/mfem/operators/element_evaluation.hpp" +#include "serif/backend/mfem/operators/element_transpose.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" + +namespace serif::operators::backend::mfem { + /// Reject changes to discretization objects borrowed by an accepted linearization. + template + void validate_volume_data(const VolumeData& data) { + using namespace utils::errors; + + const auto& discretization = data.configuration->discretization; + validate_volume_discretization(discretization); + for (const auto* column : data.bindings.columns) { + column->validate(); + } + serif_ensure( + discretization.residual_binding.mesh().GetNodesSequence() == data.bindings.nodes_sequence, + "The prepared equation mesh changed; prepare again or rebuild its bindings."); + } + + /// Visit each contribution's own batch in equation order, without scanning other contributions. + template + void for_each_volume_element(const VolumeData& data, VisitT&& visit) { + [&](std::index_sequence) { + const auto visit_contribution = [&]() { + using Contribution = std::tuple_element_t>; + + for (const auto& element : data.contributions[Index]) { + visit.template operator()(*element); + } + }; + (visit_contribution.template operator()(), ...); + }(std::make_index_sequence>>{}); + } + + /** @brief Visit equation data together with its graph-owned local geometry derivative matrix. */ + template + void for_each_volume_linearization(const VolumeData& data, VisitT&& visit) { + [&](std::index_sequence) { + const auto visit_contribution = [&]() { + using Contribution = std::tuple_element_t>; + const auto& elements = data.contributions[Index]; + for (std::size_t element = 0; element < elements.size(); ++element) { + visit.template operator()(*elements[element], *data.geometry_derivatives[Index][element]); + } + }; + (visit_contribution.template operator()(), ...); + }(std::make_index_sequence>>{}); + } + + /// Return the supported residual size. + template + [[nodiscard]] int volume_height(const VolumeData& data) { + validate_volume_data(data); + return data.configuration->discretization.residual_binding.size(); + } + + /// Packed directions contain unique coefficient terms followed by displacement. + template + [[nodiscard]] int volume_width(const VolumeData& data) { + validate_volume_data(data); + int size = data.configuration->discretization.displacement_binding.size(); + for (const auto* column : data.bindings.columns) { + size += column->size(); + } + return size; + } + + /// Count contribution-elements using tensor contractions for any sampled basis. + template + [[nodiscard]] std::size_t tensor_element_count(const VolumeData& data) { + validate_volume_data(data); + std::size_t count = 0; + for_each_volume_element(data, [&](const auto& element) { + bool tensor = element.basis->displacement_basis.uses_tensor() || element.geometry->test_basis.uses_tensor(); + for (const auto& basis : element.geometry->operand_basis) { + tensor = tensor || basis.uses_tensor(); + } + count += tensor; + }); + return count; + } + + /// Return the largest contribution quadrature order on a supported mesh element. + template + [[nodiscard]] int integration_order(const VolumeData& data, int element) { + using namespace utils::errors; + + validate_volume_data(data); + int order = -1; + for_each_volume_element(data, [&](const auto& entry) { + if (entry.element == element) { + order = std::max(order, entry.basis->rule->GetOrder()); + } + }); + serif_ensure( + order >= 0, "Element {} is not in the prepared equation support.", element); + return order; + } + + /// Copy the residual already assembled and checked when this result was accepted. + template + void volume_residual(const VolumeData& data, ::mfem::Vector& result) { + validate_volume_data(data); + result = data.residual; + } + + /** @brief Validate coefficient bindings once before a complete element traversal. */ + template + [[nodiscard]] std::array column_elements( + const std::array& columns + ) { + return std::apply([](const auto*... column) { + return std::array{column->validated_elements()...}; + }, columns); + } + + /// Apply only the requested directions and assemble their supported residual contribution. + template + void apply_volume_direction( + const VolumeData& data, + const std::array::column_count>& directions, + const ::mfem::Vector& displacement_direction, + Workspace& workspace, + ::mfem::Vector& result + ) { + using Plan = EquationPlan; + + validate_volume_data(data); + const auto& discretization = data.configuration->discretization; + if constexpr (Geometry) { + discretization.displacement_binding.validate_values(displacement_direction); + } + if constexpr (Fields) { + for (std::size_t column = 0; column < Plan::column_count; ++column) { + data.bindings.columns[column]->validate_values(*directions[column]); + } + } + workspace.full_result.SetSize(discretization.residual_binding.full_size()); + workspace.full_result = 0.0; + workspace.operand_values.resize(Plan::operand_count); + utils::CompensatedSum global_sum; + const auto rows = discretization.residual_binding.validated_elements(); + const auto columns = column_elements(data.bindings.columns); + const auto displacement = discretization.displacement_binding.validated_elements(); + // Element actions are independent and run concurrently, each into its own result. They are then added + // into the residual one by one in the original element order, so every sum is unchanged. + std::vector> actions; + std::vector action_elements; + for_each_volume_linearization(data, [&](const auto& element, const auto& geometry_derivatives) { + action_elements.push_back(element.element); + actions.emplace_back([&](Workspace& local, ::mfem::Vector& element_result) { + element_jacobian_action( + element, geometry_derivatives, columns, directions, displacement, + displacement_direction, local, element_result); + }); + }); + std::vector<::mfem::Vector> element_results(actions.size()); + const int workers = utils::parallel::worker_count(); + std::vector worker_workspaces(static_cast(workers > 1 ? workers - 1 : 0)); + for (auto& local : worker_workspaces) { + local.operand_values.resize(Plan::operand_count); + } + utils::parallel::for_each_index(static_cast(actions.size()), [&](const int index, const int worker) { + Workspace& local = worker == 0 ? workspace : worker_workspaces[static_cast(worker - 1)]; + actions[static_cast(index)](local, element_results[static_cast(index)]); + }); + for (std::size_t index = 0; index < actions.size(); ++index) { + if constexpr (discretization::blocks::GlobalScalarTerm) { + global_sum.add(element_results[index][0]); + } else { + rows.add_element(action_elements[index], element_results[index], workspace.full_result); + } + } + if constexpr (discretization::blocks::GlobalScalarTerm) { + workspace.full_result[0] = global_sum.value(); + } + result = discretization.residual_binding.accumulate_dual(workspace.full_result); + discretization.residual_binding.validate_values(result); + } + + /** + * @brief apply_volume_direction for several displacement directions in one traversal. + * @note Every (direction, element) action runs concurrently with worker-owned scratch, then each direction + * adds its element results in the original element order, so each result equals the single-direction call. + */ + template + void apply_volume_displacements( + const VolumeData& data, + std::span displacement_directions, + std::span<::mfem::Vector* const> results + ) { + using Plan = EquationPlan; + + validate_volume_data(data); + const auto& discretization = data.configuration->discretization; + for (const auto* direction : displacement_directions) { + discretization.displacement_binding.validate_values(*direction); + } + const auto rows = discretization.residual_binding.validated_elements(); + const auto columns = column_elements(data.bindings.columns); + const auto displacement = discretization.displacement_binding.validated_elements(); + std::vector)>> actions; + std::vector action_elements; + for_each_volume_linearization(data, [&](const auto& element, const auto& geometry_derivatives) { + action_elements.push_back(element.element); + actions.emplace_back([&](Workspace& local, std::span<::mfem::Vector* const> element_results) { + element_geometry_actions( + element, geometry_derivatives, displacement, displacement_directions, local, element_results); + }); + }); + const std::size_t elements = actions.size(); + const std::size_t count = displacement_directions.size(); + // element_results[direction * elements + element] + std::vector<::mfem::Vector> element_results(elements * count); + const int workers = utils::parallel::worker_count(); + std::vector worker_workspaces(static_cast(workers)); + for (auto& local : worker_workspaces) { + local.operand_values.resize(Plan::operand_count); + } + utils::parallel::for_each_index(static_cast(elements), [&](const int element, const int worker) { + std::vector<::mfem::Vector*> lanes(count); + for (std::size_t direction = 0; direction < count; ++direction) { + lanes[direction] = &element_results[direction * elements + static_cast(element)]; + } + actions[static_cast(element)](worker_workspaces[static_cast(worker)], lanes); + }); + utils::parallel::for_each_index(static_cast(count), [&](const int direction, int) { + const auto first = static_cast(direction) * elements; + ::mfem::Vector full(discretization.residual_binding.full_size()); + full = 0.0; + utils::CompensatedSum global_sum; + for (std::size_t index = 0; index < elements; ++index) { + if constexpr (discretization::blocks::GlobalScalarTerm) { + global_sum.add(element_results[first + index][0]); + } else { + rows.add_element(action_elements[index], element_results[first + index], full); + } + } + if constexpr (discretization::blocks::GlobalScalarTerm) { + full[0] = global_sum.value(); + } + auto& result = *results[static_cast(direction)]; + result = discretization.residual_binding.accumulate_dual(full); + discretization.residual_binding.validate_values(result); + }); + } + + /// Apply a direction whose terms explicitly name the accepted coefficient bindings. + template + void apply_jacobian( + const VolumeData& data, + const CoefficientsT& direction, + const ::mfem::Vector& displacement_direction, + Workspace& workspace, + ::mfem::Vector& result + ) { + using namespace utils::errors; + using Plan = EquationPlan; + + std::array directions; + Plan::for_each_column([&](OperandT, std::size_t column) { + const auto& input = direction.template get(); + serif_ensure( + &input.binding == data.bindings.columns[column], + "Jacobian directions must use the prepared coefficient bindings."); + directions[column] = &input.values; + }); + apply_volume_direction(data, directions, displacement_direction, workspace, result); + } + + /// Unpack unique coefficient terms and displacement, then use the same numerical action. + template + void apply_jacobian( + const VolumeData& data, + const ::mfem::Vector& direction, + Workspace& workspace, + ::mfem::Vector& result + ) { + using namespace utils::errors; + using Plan = EquationPlan; + + serif_ensure( + direction.Size() == volume_width(data), "Packed equation direction has the wrong size."); + std::array<::mfem::Vector, Plan::column_count> values; + std::array directions; + int offset = 0; + for (std::size_t column = 0; column < Plan::column_count; ++column) { + values[column].SetSize(data.bindings.columns[column]->size()); + std::copy_n(direction.GetData() + offset, values[column].Size(), values[column].GetData()); + directions[column] = &values[column]; + offset += values[column].Size(); + } + ::mfem::Vector displacement_direction(data.configuration->discretization.displacement_binding.size()); + std::copy_n(direction.GetData() + offset, displacement_direction.Size(), displacement_direction.GetData()); + apply_volume_direction(data, directions, displacement_direction, workspace, result); + } + + /// Assemble the full coordinate transpose in the same packed order as the forward action. + template + void apply_jacobian_transpose( + const VolumeData& data, + const ::mfem::Vector& residual_dual, + Workspace& workspace, + ::mfem::Vector& result + ) { + using Plan = EquationPlan; + + validate_volume_data(data); + const auto& discretization = data.configuration->discretization; + discretization.residual_binding.validate_values(residual_dual); + std::array<::mfem::Vector, Plan::column_count> full_columns; + std::array<::mfem::Vector, Plan::column_count> element_columns; + std::array scalar_sums; + for (std::size_t column = 0; column < Plan::column_count; ++column) { + full_columns[column].SetSize(data.bindings.columns[column]->full_size()); + full_columns[column] = 0.0; + } + ::mfem::Vector full_displacement(discretization.displacement_binding.full_size()); + ::mfem::Vector element_displacement; + full_displacement = 0.0; + const auto row_elements = discretization.residual_binding.validated_elements(); + const auto columns = column_elements(data.bindings.columns); + const auto displacement_elements = discretization.displacement_binding.validated_elements(); + for_each_volume_linearization(data, [&](const auto& element, const auto& geometry_derivatives) { + element_jacobian_transpose( + element, geometry_derivatives, row_elements, residual_dual, workspace, + element_columns, element_displacement); + Plan::for_each_column([&](OperandT, std::size_t column) { + if (element_columns[column].Size() == 0) { + return; + } + if constexpr (discretization::blocks::GlobalScalarTerm) { + scalar_sums[column].add(element_columns[column][0]); + } else { + columns[column].add_element(element.element, element_columns[column], full_columns[column]); + } + }); + displacement_elements.add_element(element.element, element_displacement, full_displacement); + }); + ::mfem::Vector packed(volume_width(data)); + int offset = 0; + Plan::for_each_column([&](OperandT, std::size_t column) { + if constexpr (discretization::blocks::GlobalScalarTerm) { + full_columns[column][0] = scalar_sums[column].value(); + } + const auto values = data.bindings.columns[column]->accumulate_dual(full_columns[column]); + data.bindings.columns[column]->validate_values(values); + std::copy_n(values.GetData(), values.Size(), packed.GetData() + offset); + offset += values.Size(); + }); + const auto displacement = discretization.displacement_binding.accumulate_dual(full_displacement); + discretization.displacement_binding.validate_values(displacement); + std::copy_n(displacement.GetData(), displacement.Size(), packed.GetData() + offset); + result = packed; + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/volume_computations.hpp b/src/include/serif/backend/mfem/operators/volume_computations.hpp new file mode 100644 index 00000000..5e4b96c2 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/volume_computations.hpp @@ -0,0 +1,248 @@ +#pragma once + +#include +#include +#include +#include + +#include "serif/backend/mfem/operators/volume_stages.hpp" +#include "serif/backend/mfem/operators/volume_inputs.hpp" +#include "serif/utils/dependencies/dependencies.hpp" + +namespace serif::operators::backend::mfem +{ + /// Values need a mapping node only when the operand's physical representation depends on geometry. + template + using VolumeOperandResult = + std::conditional_t::geometry_dependent, typename volume_keys::Scoped::template PhysicalValues, + typename volume_keys::Scoped::template ReferenceValues>; + + /// Declare one coefficient contraction, followed by a physical mapping when the space requires it. + template + [[nodiscard]] auto make_operand_computations(const std::shared_ptr& scratch) + { + using namespace utils::dependencies; + using Basis = ElementBatch::operand_count>>; + using Geometry = ElementBatch::operand_count>>; + using Binding = discretization::backend::mfem::TermBinding; + using ReferenceValues = typename volume_keys::Scoped::template ReferenceValues; + + const auto sample_values = make_computation::template Basis, typename OperandT::term_type>>( + [scratch](const Basis& basis, const CoefficientView& input) + { + return sample_contribution_operand(basis, input, scratch->workspace); + }); + + if constexpr (OperandEvaluator::geometry_dependent) + { + const auto map_values = make_computation::template PhysicalValues, + InputList::template Geometry>>( + [scratch](const OperandSamples& samples, const Geometry& geometry) + { + return map_contribution_operand(samples, geometry, scratch->workspace); + }); + return std::tuple{sample_values, map_values}; + } + else + { + return std::tuple{sample_values}; + } + } + + /// Build one contribution's branch in the same order as its numerical description. + template + [[nodiscard]] auto make_contribution_computations(const std::shared_ptr>& configuration, + const std::shared_ptr& scratch, ContributionBinding contribution, + utils::types::TypeList) + { + using namespace utils::dependencies; + using Plan = EquationPlan; + using Basis = ElementBatch>; + using Geometry = ElementBatch>; + using DisplacementBinding = typename VolumeConfiguration::DisplacementBinding; + + const auto reference_basis = make_computation::template Basis, InputList::Bindings>>( + [configuration, scratch, contribution](const BasisBindings& bindings) + { + return sample_contribution_basis(*configuration, contribution, bindings, scratch->basis); + }); + + const auto geometry = make_computation::template Geometry, + InputList::template Basis, DisplacementCoefficients, CompactificationCoefficients>>( + [configuration, scratch](const Basis& basis, const CoefficientView& displacement, const MappingCoefficients& compactification) + { + return map_contribution_geometry(*configuration, basis, displacement, compactification, scratch->workspace); + }); + + const auto operand_values = std::tuple_cat(make_operand_computations(scratch)...); + + const auto equation_values = make_computation::template QuadratureLinearization, + InputList::template Basis, + typename volume_keys::Scoped::template Geometry, VolumeOperandResult...>>( + [scratch, contribution](const Basis& basis, const Geometry& geometry, const SamplesFor&... samples) + { + return evaluate_contribution_equation(contribution, basis, geometry, scratch->workspace, samples...); + }); + + const auto geometry_derivatives = make_computation::template GeometryDerivatives, + InputList::template QuadratureLinearization>>( + [configuration, scratch](const ContributionData& data) { + return differentiate_contribution_geometry(*configuration, data, scratch->workspace); + }); + + return std::tuple_cat(std::tuple{reference_basis, geometry}, operand_values, std::tuple{equation_values, geometry_derivatives}); + } + + /// Expose contribution computations so deformation and equations can share one dependency graph. + template + [[nodiscard]] auto connect_volume_contributions(const std::shared_ptr>& configuration, std::index_sequence) + { + using namespace utils::dependencies; + using Plan = EquationPlan; + + const auto scratch = std::make_shared(); + const auto contributions = bind_contributions(configuration->equation); + const auto calculations = std::tuple_cat(make_contribution_computations( + configuration, scratch, std::get(contributions), typename std::tuple_element_t>::Operands{})...); + + const auto volume = + make_computation::Linearization, + InputList::Bindings, typename volume_keys::Scoped::template QuadratureLinearization..., + typename volume_keys::Scoped::template GeometryDerivatives...>>( + [configuration, scratch](const BasisBindings& bindings, const auto&... results) + { + // The first group contains the physical equations; the second their local geometry derivatives. + const auto values = std::tie(results...); + auto volume = collect_volume_results(configuration, bindings, scratch->workspace, std::get(values)...); + if (volume) { + volume->geometry_derivatives = {std::get(values)...}; + } + return volume; + }); + + return std::tuple_cat(calculations, std::tuple{volume}); + } + + /// Convert the dependency engine's node statuses to numerical work counts for one contribution. + template + void report_contribution_preparation(const CacheT& cache, NumericalWork& report) + { + using Plan = EquationPlan; + using Contribution = std::tuple_element_t>; + + const auto& basis = cache.template get::template Basis>(); + const auto& status = cache.report(); + std::size_t points = 0; + for (const auto& element : basis) + { + points += element->rule->GetNPoints(); + } + if (status.template status::template Geometry>() == utils::dependencies::PreparationStatus::computed) + { + report.geometry_points += points; + } + if (status.template status::template QuadratureLinearization>() == utils::dependencies::PreparationStatus::computed) + { + report.physics_points += points; + } + else + { + report.reused_elements += basis.size(); + } + Plan::template for_each_input( + [&](OperandT, std::size_t) + { + if (status.template status::template ReferenceValues>() == utils::dependencies::PreparationStatus::computed) + { + report.sampled_operands += basis.size(); + } + }); + } + + /** + * @brief Declare reusable volume computations without selecting their source of displacement. + * @note This allocates scratch and owns the equation configuration; numerical work begins at try_prepare. + * Borrowed discretization objects must outlive the computations and their accepted results. + */ + template + [[nodiscard]] auto make_volume_equation_computations(std::shared_ptr equation, const VolumeDiscretization& discretization, + VolumeOptions options = {}) + { + utils::errors::serif_ensure(equation != nullptr, "Volume computations require an owning equation; supply a non-null equation or retained model member."); + validate_volume_discretization(discretization); + const auto configuration = std::make_shared>( + VolumeConfiguration{.equation = std::move(equation), .discretization = discretization, .options = std::move(options)}); + return connect_volume_contributions(configuration, std::make_index_sequence>>{}); + } + + /** @brief Own standalone equations while allowing complete models to share their existing immutable members. */ + template + [[nodiscard]] auto make_volume_equation_computations(EquationT equation, const VolumeDiscretization& discretization, VolumeOptions options = {}) + { + return make_volume_equation_computations(std::make_shared(std::move(equation)), discretization, std::move(options)); + } + + /// Construct a standalone cache from the same computations used by a coupled deformation graph. + template + [[nodiscard]] auto make_volume_equation_cache(EquationT equation, const VolumeDiscretization& discretization, const InputsT& inputs, + VolumeOptions options = {}) + { + return std::apply( + [&](auto... calculations) + { + return utils::dependencies::make_computation_cache(inputs, std::move(calculations)...); + }, + make_volume_equation_computations(std::move(equation), discretization, std::move(options))); + } + + /// Recover the equation type from a cache's accepted result type, without accessing a result. + template + using VolumeEquationOf = typename std::remove_cvref_t().template get::Linearization>())>::Equation; + + /// Validate current inputs and atomically publish a complete volume linearization. + template + [[nodiscard]] std::expected try_prepare_volume(CacheT& cache, const DiscretizationT& discretization, const CoefficientsT& term_coefficients, + const ::mfem::Vector& displacement_coefficients, + PreparationMode mode = PreparationMode::reuse_unchanged) + { + return try_prepare_validated(cache, + [&]() + { + const auto inputs = + make_volume_inputs, Scope>(discretization, term_coefficients, displacement_coefficients); + return cache.try_prepare(inputs, {}, mode); + }); + } + + /// Throw a caller-visible numerical error when a complete preparation cannot be accepted. + template + void prepare_volume(CacheT& cache, const DiscretizationT& discretization, const CoefficientsT& term_coefficients, const ::mfem::Vector& displacement_coefficients, + PreparationMode mode = PreparationMode::reuse_unchanged) + { + using namespace utils::errors; + + const auto result = try_prepare_volume(cache, discretization, term_coefficients, displacement_coefficients, mode); + if (!result) + { + serif_error("{} preparation rejected at element {}, point {}: {}", VolumeEquationOf::name, + result.error().element, result.error().quadrature_point, result.error().message); + } + } + + /// Derive work counts from accepted node statuses; no second report lifecycle is stored. + template + [[nodiscard]] NumericalWork volume_work_report(const CacheT& cache) + { + using Equation = VolumeEquationOf; + + NumericalWork report; + if (cache.template contains::Linearization>()) + { + [&](std::index_sequence) + { + (report_contribution_preparation(cache, report), ...); + }(std::make_index_sequence>>{}); + } + return report; + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/volume_data.hpp b/src/include/serif/backend/mfem/operators/volume_data.hpp new file mode 100644 index 00000000..99c6abd8 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/volume_data.hpp @@ -0,0 +1,168 @@ +#pragma once + +#include +#include +#include + +#include "mfem/fem/gridfunc.hpp" + +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/backend/mfem/discretization/term_coefficients.hpp" +#include "serif/backend/mfem/operators/element_data.hpp" +#include "serif/backend/mfem/operators/quadrature.hpp" +#include "serif/backend/mfem/operators/workspace.hpp" + +namespace serif::operators::backend::mfem +{ + using discretization::backend::mfem::ContractionMode; + using discretization::backend::mfem::TensorBasisCache; + namespace volume_keys + { + /** @brief Keep equation-local stages distinct while sharing physical coefficient sources. */ + template + struct Scoped + { + template + struct Basis + { + }; + template + struct Geometry + { + }; + template + struct ReferenceValues + { + }; + template + struct PhysicalValues + { + }; + template + struct QuadratureLinearization + { + }; + /** @brief Weighted local displacement derivatives, owned and invalidated by the graph. */ + template + struct GeometryDerivatives {}; + struct Linearization + { + }; + struct Bindings + { + }; + }; + + template + using Basis = Scoped::Basis; + template + using Geometry = Scoped::Geometry; + template + using ReferenceValues = Scoped::ReferenceValues; + template + using PhysicalValues = Scoped::PhysicalValues; + template + using QuadratureLinearization = Scoped::QuadratureLinearization; + template + using GeometryDerivatives = Scoped::GeometryDerivatives; + using Linearization = Scoped::Linearization; + } // namespace volume_keys + + template + struct BasisBindings + { + const discretization::backend::mfem::DofMap* residual_binding; + const discretization::backend::mfem::DofMap* displacement_binding; + const ::mfem::FiniteElementSpace* compactification_space; + const mapping::DomainMapper* mapper; + long nodes_sequence; + std::array columns; + /// Basis data changes when either the reference mesh or any coefficient binding changes. + bool operator==(const BasisBindings&) const = default; + }; + + /// Borrowed discretization objects must outlive their graph and its accepted results. + template + struct VolumeDiscretization + { + using RowBinding = discretization::backend::mfem::TermBinding; + using DisplacementBinding = discretization::backend::mfem::TermBinding; + + const RowBinding& residual_binding; + const DisplacementBinding& displacement_binding; + const ::mfem::GridFunction& compactification; + const mapping::DomainMapper& mapper; + const ::mfem::FiniteElementSpace* compactification_space{compactification.FESpace()}; + long compactification_sequence{compactification_space == nullptr ? -1 : compactification_space->GetSequence()}; + }; + + template + VolumeDiscretization(const discretization::backend::mfem::TermBinding&, + const discretization::backend::mfem::TermBinding&, const ::mfem::GridFunction&, + const mapping::DomainMapper&) -> VolumeDiscretization; + + /// Numerical choices fixed when the graph is constructed. + struct VolumeOptions + { + /// @todo Change this default. The automatic rule (operators/backend/mfem/quadrature.hpp: equation_rule) adds + /// OrderW() of the geometry (17 for geometry order 6) plus 3 (p_disp - 1) to every form, giving ~order 35 + /// (5,832 points per hexahedron) on the stellar meshes. A fixed discretization order of 11 (216 points) gave + /// the same ESTER/TOP agreement for the n=2, J=0.05 benchmark at 0.96 GiB instead of 13.6 GB + /// (profiling/results/FINDINGS.md, sections 14-15); complete_sandbox already passes it explicitly. Changing + /// the library default changes every solve, so test expectations (tolerances, iteration counts, stored + /// reference values) may need updating, and a fixed order should be checked against higher field orders. + discretization::quadrature::Policy quadrature_policy{{}}; + ContractionMode contraction_mode{ContractionMode::automatic}; + }; + + /// A stage owns immutable element results, shared with downstream stages and accepted linearizations. + template + using ElementBatch = std::vector>; + + /// Reference samples remain available for derivatives through the physical mapping. + struct OperandSamples + { + ElementBatch<::mfem::Vector> reference; + ElementBatch<::mfem::Vector> physical; + }; + + template + struct ContributionData + { + ElementBatch> elements; + double residual_offset{0.0}; + }; + + /// Stable equation ownership also retains temporary and move-only contribution trees. + template + struct VolumeConfiguration + { + using DisplacementBinding = typename VolumeDiscretization::DisplacementBinding; + + std::shared_ptr equation; + VolumeDiscretization discretization; + VolumeOptions options; + }; + + /// One accepted result contains ordered contribution batches and the assembled residual. + template + struct VolumeData + { + using Equation = EquationT; + using Schema = SchemaT; + using Plan = EquationPlan; + + std::shared_ptr> configuration; + BasisBindings bindings; + std::array>, std::tuple_size_v>> contributions; + std::array, std::tuple_size_v>> geometry_derivatives; + ::mfem::Vector residual; + }; + + /// Scratch belongs to graph callables and is never a dependency source. + struct VolumeScratch + { + Workspace workspace; + TensorBasisCache basis; + }; +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/volume_field_block.hpp b/src/include/serif/backend/mfem/operators/volume_field_block.hpp new file mode 100644 index 00000000..5e06418e --- /dev/null +++ b/src/include/serif/backend/mfem/operators/volume_field_block.hpp @@ -0,0 +1,570 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/discretization/element_coordinates.hpp" +#include "serif/backend/mfem/operators/linearized_block.hpp" +#include "serif/backend/mfem/operators/volume_application.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" + +namespace serif::operators::backend::mfem { + namespace detail { + /** @brief Ignore a structurally absent field before gathering coefficients or touching contribution bases. */ + template + void for_each_field_element(const VolumeData& data, Visit&& visit) { + for_each_volume_element(data, [&](const auto& element) { + constexpr bool reads_field = [](utils::types::TypeList) { + return (std::same_as || ...); + }(typename Contribution::Operands{}); + if constexpr (reads_field) { + visit.template operator()(element); + } + }); + } + + /** @brief Contract only the requested physical field; geometry is frozen in this accepted linearization. */ + template + void field_integrand(const ElementData::operand_count>& element, const ::mfem::Vector& direction, ::mfem::Vector& integrand, ::mfem::Vector& samples, BasisWorkspace& workspace) { + using Plan = EquationPlan; + using Test = OperandEvaluator>; + + integrand.SetSize(element.basis->rule->GetNPoints() * Test::components); + integrand = 0.0; + Plan::template for_each_input([&](Operand, const std::size_t index) { + if constexpr (std::same_as) { + using Evaluator = OperandEvaluator; + + Evaluator::evaluate(element.geometry->operand_basis[index], direction, samples, workspace); + for (int point = 0; point < element.basis->rule->GetNPoints(); ++point) { + for (int output = 0; output < Test::components; ++output) { + const int row = point * Test::components + output; + for (int input = 0; input < Evaluator::components; ++input) { + integrand[row] += element.weighted_field_derivatives(row, Plan::template component_offset() + input) * samples[point * Evaluator::components + input]; + } + } + } + } + }); + } + + /** @brief Locate a physical field in the equation's unique coefficient columns. */ + template + [[nodiscard]] consteval std::size_t field_column() { + return equations::OperandValues::Columns>::template index(); + } + + /** @brief Retain accepted data while applying a single field coupling with reusable contraction scratch. */ + template + class VolumeFieldOperator final : public ::mfem::Operator { + public: + /** @brief Borrowed discretization resources still follow the accepted volume result's validity contract. */ + explicit VolumeFieldOperator(std::shared_ptr> data) : ::mfem::Operator(volume_height(*data), data->bindings.columns[field_column()]->size()), m_data(std::move(data)) { + m_input.SetSize(width); + m_full.SetSize(m_data->configuration->discretization.residual_binding.full_size()); + } + + /** + * @brief Apply selected field derivatives without evaluating zero geometry variations. + * @note Element results are computed concurrently, each into its own vector, then added into the + * residual one by one in the original element order, so every sum is unchanged. + */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override { + using namespace utils::errors; + + validate_volume_data(*m_data); + serif_ensure(input.Size() == width, "A physical field block received {} coefficients; expected {}.", input.Size(), width); + m_input = input; + m_full = 0.0; + const auto& row = m_data->configuration->discretization.residual_binding; + const auto& column = *m_data->bindings.columns[field_column()]; + utils::CompensatedSum global_sum; + const auto row_elements = row.validated_elements(); + const auto column_elements = column.validated_elements(); + m_actions.clear(); + m_elements.clear(); + detail::for_each_field_element(*m_data, [&](const auto& element) { + using Test = OperandEvaluator>; + + m_elements.push_back(element.element); + m_actions.emplace_back([&element, &column_elements, this](Scratch& local, ::mfem::Vector& result) { + column_elements.element_values(element.element, m_input, local.direction); + field_integrand(element, local.direction, local.integrand, local.samples, local.workspace); + Test::integrate(element.geometry->test_basis, local.integrand, result, local.workspace); + }); + }); + run_actions(); + for (std::size_t index = 0; index < m_actions.size(); ++index) { + if constexpr (discretization::blocks::GlobalScalarTerm) { + global_sum.add(m_results[index][0]); + } else { + row_elements.add_element(m_elements[index], m_results[index], m_full); + } + } + if constexpr (discretization::blocks::GlobalScalarTerm) { + m_full[0] = global_sum.value(); + } + row.accumulate_dual(m_full, output); + row.validate_values(output); + } + + /** @brief Apply the exact coefficient transpose of the selected physical field coupling (concurrent elements, ordered sums, as Mult). */ + void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override { + using namespace utils::errors; + using Plan = EquationPlan; + + validate_volume_data(*m_data); + serif_ensure(input.Size() == height, "A physical field transpose received {} coefficients; expected {}.", input.Size(), height); + m_dual = input; + const auto& row = m_data->configuration->discretization.residual_binding; + const auto& column = *m_data->bindings.columns[field_column()]; + m_full_transpose.SetSize(column.full_size()); + m_full_transpose = 0.0; + utils::CompensatedSum global_sum; + const auto row_elements = row.validated_elements(); + const auto column_elements = column.validated_elements(); + m_actions.clear(); + m_elements.clear(); + detail::for_each_field_element(*m_data, [&](const auto& element) { + using Test = OperandEvaluator>; + + // One action per field operand, in operand order, each recomputing the element's test samples. + Plan::template for_each_input([&](Operand, const std::size_t index) { + if constexpr (std::same_as) { + m_elements.push_back(element.element); + m_actions.emplace_back([&element, &row_elements, index, this](Scratch& local, ::mfem::Vector& result) { + using Evaluator = OperandEvaluator; + + row_elements.element_values(element.element, m_dual, local.direction); + element.geometry->test_basis.evaluate(local.direction, local.test_samples, local.workspace); + local.integrand.SetSize(element.basis->rule->GetNPoints() * Evaluator::components); + local.integrand = 0.0; + for (int point = 0; point < element.basis->rule->GetNPoints(); ++point) { + for (int field_component = 0; field_component < Evaluator::components; ++field_component) { + for (int test_component = 0; test_component < Test::components; ++test_component) { + const int test_index = point * Test::components + test_component; + local.integrand[point * Evaluator::components + field_component] += element.weighted_field_derivatives(test_index, + Plan::template component_offset() + field_component) * + local.test_samples[test_index]; + } + } + } + Evaluator::integrate(element.geometry->operand_basis[index], local.integrand, result, local.workspace); + }); + } + }); + }); + run_actions(); + for (std::size_t index = 0; index < m_actions.size(); ++index) { + if constexpr (discretization::blocks::GlobalScalarTerm) { + global_sum.add(m_results[index][0]); + } else { + column_elements.add_element(m_elements[index], m_results[index], m_full_transpose); + } + } + if constexpr (discretization::blocks::GlobalScalarTerm) { + m_full_transpose[0] = global_sum.value(); + } + column.accumulate_dual(m_full_transpose, output); + column.validate_values(output); + } + + private: + /** @brief One worker's element scratch. */ + struct Scratch { + ::mfem::Vector direction, integrand, samples, test_samples; + BasisWorkspace workspace; + }; + + /** @brief Run the collected element actions concurrently, each into its own result. */ + void run_actions() const { + m_results.resize(m_actions.size()); + m_scratch.resize(static_cast(std::max(1, utils::parallel::worker_count()))); + utils::parallel::for_each_index(static_cast(m_actions.size()), [&](const int index, const int worker) { + m_actions[static_cast(index)](m_scratch[static_cast(worker)], m_results[static_cast(index)]); + }); + } + + std::shared_ptr> m_data; + mutable ::mfem::Vector m_input, m_full; + mutable ::mfem::Vector m_dual, m_full_transpose; + mutable std::vector> m_actions; + mutable std::vector m_elements; + mutable std::vector<::mfem::Vector> m_results; + mutable std::vector m_scratch; + }; + } // namespace detail + + namespace detail { + /** @brief One element's probed block: column c's integrated values for every local row, column-major. */ + struct ProbedElementBlock { + std::vector rows; ///< Reduced row coordinate of each local row, or -1 when unsupported. + std::vector columns; ///< Reduced column coordinate of each local column, or -1 when skipped. + std::vector values; + }; + + /** @brief Per-worker scratch for probing element blocks. */ + struct ProbeScratch { + ::mfem::Vector direction, integrand, samples, values; + BasisWorkspace workspace; + }; + + /** @brief Weighted reduced coordinates of each local coordinate, as CSR: local i owns [offsets[i], offsets[i+1]). */ + struct CoefficientExpansion { + std::vector offsets{0}; + std::vector indices; + std::vector weights; + }; + + /** @brief One element's probed block on constrained bindings: column-major values and both constraint expansions. */ + struct ConstrainedElementBlock { + int row_count{0}; + std::vector values; + CoefficientExpansion rows; + CoefficientExpansion columns; + }; + + /** @brief Write MFEM-style CSR rows (already in Finalize() order) into an owning SparseMatrix. */ + [[nodiscard]] inline std::shared_ptr make_csr( + const std::vector>& row_columns, const std::vector>& row_values, const int height, const int width + ) { + int* I = new int[static_cast(height) + 1]; + I[0] = 0; + for (int row = 0; row < height; ++row) { I[row + 1] = I[row] + static_cast(row_columns[static_cast(row)].size()); } + int* J = new int[static_cast(I[height])]; + double* A = new double[static_cast(I[height])]; + bool sorted = true; + for (int row = 0; row < height; ++row) { + int last_column = -1; + const auto& columns = row_columns[static_cast(row)]; + const auto& values = row_values[static_cast(row)]; + for (std::size_t k = 0; k < columns.size(); ++k) { + J[I[row] + static_cast(k)] = columns[k]; + A[I[row] + static_cast(k)] = values[k]; + if (last_column > columns[k]) { sorted = false; } + last_column = columns[k]; + } + } + return std::make_shared<::mfem::SparseMatrix>(I, J, A, height, width, true, true, sorted); + } + + /** + * @brief SparseMatrix::Add for every probed entry followed by Finalize(), built without MFEM's linked rows. + * @note MFEM keeps each row as a list with the newest column first, adds every value in insertion order + * starting from 0.0, and Finalize() writes the list head first while dropping exact zeros (a zero diagonal + * finds itself and is dropped too). Here each row replays its own insertions in the original global order + * (element, then column, then local row), so every sum, the column order and the dropped entries match. + * Rows are independent, so they are built concurrently. + */ + [[nodiscard]] inline std::shared_ptr finalize_probed_blocks( + const std::vector& blocks, const int height, const int width + ) { + // Row -> (block, local row) in insertion order: blocks ascending, then local rows ascending. + std::vector offsets(static_cast(height) + 1, 0); + for (const auto& block : blocks) { + for (const int row : block.rows) { + if (row >= 0) { ++offsets[static_cast(row) + 1]; } + } + } + for (int row = 0; row < height; ++row) { offsets[row + 1] += offsets[row]; } + std::vector incidence_block(static_cast(offsets.back())); + std::vector incidence_row(static_cast(offsets.back())); + { + std::vector cursor(offsets.begin(), offsets.end() - 1); + for (int block = 0; block < static_cast(blocks.size()); ++block) { + const auto& rows = blocks[static_cast(block)].rows; + for (int local = 0; local < static_cast(rows.size()); ++local) { + if (rows[local] >= 0) { + const int slot = cursor[static_cast(rows[local])]++; + incidence_block[static_cast(slot)] = block; + incidence_row[static_cast(slot)] = local; + } + } + } + } + + struct RowScratch { + std::vector slot_of_column; + std::vector columns; + std::vector values; + }; + std::vector> row_columns(static_cast(height)); + std::vector> row_values(static_cast(height)); + std::vector scratch(static_cast(utils::parallel::worker_count())); + utils::parallel::for_each_index(height, [&](const int row, const int worker) { + RowScratch& local = scratch[static_cast(worker)]; + local.slot_of_column.resize(static_cast(width), -1); + local.columns.clear(); + local.values.clear(); + for (int entry = offsets[row]; entry < offsets[row + 1];) { + // Insertions for one block come column by column, each column visiting its local rows in order. + const int block_index = incidence_block[static_cast(entry)]; + int end = entry; + while (end < offsets[row + 1] && incidence_block[static_cast(end)] == block_index) { ++end; } + const auto& block = blocks[static_cast(block_index)]; + const int block_rows = static_cast(block.rows.size()); + for (int column = 0; column < static_cast(block.columns.size()); ++column) { + const int column_index = block.columns[static_cast(column)]; + if (column_index < 0) { continue; } + int& slot = local.slot_of_column[static_cast(column_index)]; + for (int k = entry; k < end; ++k) { + if (slot < 0) { + slot = static_cast(local.columns.size()); + local.columns.push_back(column_index); + local.values.push_back(0.0); + } + local.values[static_cast(slot)] += block.values[static_cast(column * block_rows + incidence_row[static_cast(k)])]; + } + } + entry = end; + } + auto& columns = row_columns[static_cast(row)]; + auto& values = row_values[static_cast(row)]; + for (std::size_t slot = local.columns.size(); slot-- > 0;) { + local.slot_of_column[static_cast(local.columns[slot])] = -1; + if (local.values[slot] == 0.0) { continue; } + columns.push_back(local.columns[slot]); + values.push_back(local.values[slot]); + } + }); + + return make_csr(row_columns, row_values, height, width); + } + + /** + * @brief The constrained form of finalize_probed_blocks: SparseMatrix::Add(row_index, column_index, + * row_weight * value * column_weight) for every element, column, local row, row coefficient and column + * coefficient in that order, followed by Finalize(). + * @note Each global row replays its own insertions in the original order (see finalize_probed_blocks), so + * every sum, the column order and the dropped exact zeros match MFEM's linked-row assembly. + */ + [[nodiscard]] inline std::shared_ptr finalize_constrained_blocks( + const std::vector& blocks, const int height, const int width + ) { + // Row -> (block, local row, weight) in insertion order: blocks, then local rows, then their coefficients. + std::vector offsets(static_cast(height) + 1, 0); + for (const auto& block : blocks) { + for (const int row : block.rows.indices) { ++offsets[static_cast(row) + 1]; } + } + for (int row = 0; row < height; ++row) { offsets[row + 1] += offsets[row]; } + std::vector incidence_block(static_cast(offsets.back())); + std::vector incidence_row(static_cast(offsets.back())); + std::vector incidence_weight(static_cast(offsets.back())); + { + std::vector cursor(offsets.begin(), offsets.end() - 1); + for (int block = 0; block < static_cast(blocks.size()); ++block) { + const auto& rows = blocks[static_cast(block)].rows; + for (int local = 0; local + 1 < static_cast(rows.offsets.size()); ++local) { + for (int k = rows.offsets[static_cast(local)]; k < rows.offsets[static_cast(local) + 1]; ++k) { + const int slot = cursor[static_cast(rows.indices[static_cast(k)])]++; + incidence_block[static_cast(slot)] = block; + incidence_row[static_cast(slot)] = local; + incidence_weight[static_cast(slot)] = rows.weights[static_cast(k)]; + } + } + } + } + + struct RowScratch { + std::vector slot_of_column; + std::vector columns; + std::vector values; + }; + std::vector> row_columns(static_cast(height)); + std::vector> row_values(static_cast(height)); + std::vector scratch(static_cast(utils::parallel::worker_count())); + utils::parallel::for_each_index(height, [&](const int row, const int worker) { + RowScratch& local = scratch[static_cast(worker)]; + local.slot_of_column.resize(static_cast(width), -1); + local.columns.clear(); + local.values.clear(); + for (int entry = offsets[row]; entry < offsets[row + 1];) { + const int block_index = incidence_block[static_cast(entry)]; + int end = entry; + while (end < offsets[row + 1] && incidence_block[static_cast(end)] == block_index) { ++end; } + const auto& block = blocks[static_cast(block_index)]; + const auto& columns = block.columns; + for (int column = 0; column + 1 < static_cast(columns.offsets.size()); ++column) { + for (int k = entry; k < end; ++k) { + const double value = block.values[static_cast(column) * static_cast(block.row_count) + + static_cast(incidence_row[static_cast(k)])]; + const double row_weight = incidence_weight[static_cast(k)]; + for (int c = columns.offsets[static_cast(column)]; c < columns.offsets[static_cast(column) + 1]; ++c) { + int& slot = local.slot_of_column[static_cast(columns.indices[static_cast(c)])]; + if (slot < 0) { + slot = static_cast(local.columns.size()); + local.columns.push_back(columns.indices[static_cast(c)]); + local.values.push_back(0.0); + } + // A separate statement keeps the product unfused, as when it was passed to SparseMatrix::Add. + const double contribution = row_weight * value * columns.weights[static_cast(c)]; + local.values[static_cast(slot)] += contribution; + } + } + } + entry = end; + } + auto& columns = row_columns[static_cast(row)]; + auto& values = row_values[static_cast(row)]; + for (std::size_t slot = local.columns.size(); slot-- > 0;) { + local.slot_of_column[static_cast(local.columns[slot])] = -1; + if (local.values[slot] == 0.0) { continue; } + columns.push_back(local.columns[slot]); + values.push_back(local.values[slot]); + } + }); + return make_csr(row_columns, row_values, height, width); + } + } // namespace detail + + /** @brief Assemble a selected field block from accepted weighted derivatives, never by probing the whole system. */ + template + [[nodiscard]] std::shared_ptr assemble_volume_field(const VolumeData& data) { + validate_volume_data(data); + const auto& row_binding = data.configuration->discretization.residual_binding; + const auto& column_binding = *data.bindings.columns[detail::field_column()]; + if (!row_binding.constrained() && !column_binding.constrained()) { + // Probe elements concurrently (the same operations per column as below), then build the rows. + const auto row_elements = row_binding.validated_elements(); + const auto column_elements = column_binding.validated_elements(); + std::vector> probes; + detail::for_each_field_element(data, [&](const auto& element) { + probes.emplace_back([&row_elements, &column_elements, &element](detail::ProbedElementBlock& block, detail::ProbeScratch& scratch) { + using Test = OperandEvaluator>; + + const auto rows = row_elements.coordinates(element.element); + const auto columns = column_elements.coordinates(element.element); + block.rows.resize(static_cast(rows.size())); + block.columns.resize(static_cast(columns.size())); + block.values.assign(static_cast(rows.size()) * static_cast(columns.size()), 0.0); + for (int row = 0; row < rows.size(); ++row) { + block.rows[static_cast(row)] = rows.reduced_dof(row) >= 0 ? rows.reduced_dof(row) : -1; + } + for (int column = 0; column < columns.size(); ++column) { + block.columns[static_cast(column)] = columns.reduced_dof(column) >= 0 ? columns.reduced_dof(column) : -1; + if (columns.reduced_dof(column) < 0) { + continue; + } + columns.unit_direction(column, scratch.direction); + detail::field_integrand(element, scratch.direction, scratch.integrand, scratch.samples, scratch.workspace); + Test::integrate(element.geometry->test_basis, scratch.integrand, scratch.values, scratch.workspace); + rows.orient_dual(scratch.values); + for (int row = 0; row < rows.size(); ++row) { + block.values[static_cast(column) * static_cast(rows.size()) + static_cast(row)] = scratch.values[row]; + } + } + }); + }); + std::vector blocks(probes.size()); + std::vector scratch(static_cast(utils::parallel::worker_count())); + utils::parallel::for_each_index(static_cast(probes.size()), [&](const int index, const int worker) { + probes[static_cast(index)](blocks[static_cast(index)], scratch[static_cast(worker)]); + }); + return detail::finalize_probed_blocks(blocks, row_binding.size(), column_binding.size()); + } + // Hanging-node constraints distribute each local entry over weighted reduced coordinates. Probe elements + // concurrently, then replay the constrained insertions row by row in their original order. + const auto row_elements = row_binding.validated_elements(); + const auto column_elements = column_binding.validated_elements(); + const bool skip_unsupported_columns = !column_binding.constrained(); + std::vector> probes; + detail::for_each_field_element(data, [&](const auto& element) { + probes.emplace_back([&row_elements, &column_elements, &element, skip_unsupported_columns](detail::ConstrainedElementBlock& block, detail::ProbeScratch& scratch) { + using Test = OperandEvaluator>; + + const auto rows = row_elements.coordinates(element.element); + const auto columns = column_elements.coordinates(element.element); + const auto expand = [](const auto& coordinates, const int local, detail::CoefficientExpansion& expansion) { + coordinates.for_each_coefficient(local, [&](const int index, const double weight) { + expansion.indices.push_back(index); + expansion.weights.push_back(weight); + }); + expansion.offsets.push_back(static_cast(expansion.indices.size())); + }; + block.row_count = rows.size(); + block.values.assign(static_cast(rows.size()) * static_cast(columns.size()), 0.0); + for (int row = 0; row < rows.size(); ++row) { + expand(rows, row, block.rows); + } + for (int column = 0; column < columns.size(); ++column) { + if (skip_unsupported_columns && columns.reduced_dof(column) < 0) { + block.columns.offsets.push_back(static_cast(block.columns.indices.size())); + continue; + } + expand(columns, column, block.columns); + columns.unit_direction(column, scratch.direction); + detail::field_integrand(element, scratch.direction, scratch.integrand, scratch.samples, scratch.workspace); + Test::integrate(element.geometry->test_basis, scratch.integrand, scratch.values, scratch.workspace); + rows.orient_dual(scratch.values); + for (int row = 0; row < rows.size(); ++row) { + block.values[static_cast(column) * static_cast(rows.size()) + static_cast(row)] = scratch.values[row]; + } + } + }); + }); + std::vector blocks(probes.size()); + std::vector scratch(static_cast(utils::parallel::worker_count())); + utils::parallel::for_each_index(static_cast(probes.size()), [&](const int index, const int worker) { + probes[static_cast(index)](blocks[static_cast(index)], scratch[static_cast(worker)]); + }); + return detail::finalize_constrained_blocks(blocks, row_binding.size(), column_binding.size()); + } + + /** @brief Extract exact physical diagonals, retaining constraint cross terms when hanging nodes are present. */ + template + [[nodiscard]] std::vector volume_field_diagonal(const VolumeData& data) { + using namespace utils::errors; + + validate_volume_data(data); + const auto& row_binding = data.configuration->discretization.residual_binding; + const auto& column_binding = *data.bindings.columns[detail::field_column()]; + serif_ensure(row_binding.size() == column_binding.size(), "Physical diagonal extraction requires a square selected block."); + if (row_binding.constrained() || column_binding.constrained()) { + // Off-diagonal local entries also contribute to diag(P^T A P). + const auto matrix = assemble_volume_field(data); + ::mfem::Vector diagonal; + matrix->GetDiag(diagonal); + return std::vector(diagonal.GetData(), diagonal.GetData() + diagonal.Size()); + } + std::vector diagonal(row_binding.size(), 0.0); + ::mfem::Vector direction, row_direction, integrand, samples, test_samples; + BasisWorkspace workspace; + const auto row_elements = row_binding.validated_elements(); + const auto column_elements = column_binding.validated_elements(); + detail::for_each_field_element(data, [&](const auto& element) { + const auto rows = row_elements.coordinates(element.element); + const auto columns = column_elements.coordinates(element.element); + for (int column = 0; column < columns.size(); ++column) { + const int index = columns.reduced_dof(column); + if (index < 0) { + continue; + } + const int row = rows.local_dof(index); + if (row < 0) { + continue; + } + columns.unit_direction(column, direction); + rows.unit_direction(row, row_direction); + detail::field_integrand(element, direction, integrand, samples, workspace); + element.geometry->test_basis.evaluate(row_direction, test_samples, workspace); + diagonal[index] += test_samples * integrand; + } + }); + return diagonal; + } + + /** @brief Expose efficient physical block operations from an owning accepted dependency result. */ + template + [[nodiscard]] LinearizedBlock volume_field_block(std::shared_ptr> data) { + utils::errors::serif_ensure(data != nullptr, "A physical block needs an accepted volume linearization."); + auto action = std::make_shared>(data); + return LinearizedBlock(std::move(action), [data]() { + return volume_field_diagonal(*data); + }, [data]() { + return assemble_volume_field(*data); + }); + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/volume_inputs.hpp b/src/include/serif/backend/mfem/operators/volume_inputs.hpp new file mode 100644 index 00000000..22d9a35b --- /dev/null +++ b/src/include/serif/backend/mfem/operators/volume_inputs.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include "serif/backend/mfem/operators/volume_data.hpp" +#include "serif/backend/mfem/discretization/coefficient_tracking.hpp" + +namespace serif::operators::backend::mfem +{ + using discretization::backend::mfem::CoefficientTracking; + + /** @brief Validate external mesh resources before the graph compares their metadata. */ + struct MetadataTracking { + /** @brief Reject stale contexts even when the caller bypasses the ordinary input factory. */ + template + [[nodiscard]] BasisBindings capture(const BasisBindings& input) const { + using namespace utils::errors; + + input.residual_binding->validate(); + input.displacement_binding->validate(); + for (const auto* column : input.columns) { + column->validate(); + } + serif_ensure( + input.residual_binding->mesh().GetNodesSequence() == input.nodes_sequence, + "The volume input context describes an earlier mesh revision; rebuild the inputs before preparing the graph."); + return input; + } + + /** @brief Equal metadata can be reused only while its borrowed mesh and spaces remain valid. */ + template + [[nodiscard]] bool matches(const BasisBindings& captured, const BasisBindings& input) const { + return captured == capture(input); + } + + /** @brief Metadata already contains persistent identities; temporary wrapper addresses are irrelevant. */ + template + [[nodiscard]] const void* identity(const T&) const { + return nullptr; + } + }; + + using MeshAndCoefficientBindings = volume_keys::Scoped::Bindings; + struct DisplacementCoefficients + { + }; + struct CompactificationCoefficients + { + }; + + /// Check borrowed mesh objects before sampling or applying accepted results. + template + void validate_volume_discretization(const VolumeDiscretization& discretization) + { + using namespace utils::errors; + + discretization.residual_binding.validate(); + discretization.displacement_binding.validate(); + const auto* space = discretization.compactification_space; + serif_ensure(space != nullptr && discretization.compactification.FESpace() == space && + space->GetSequence() == discretization.compactification_sequence && + discretization.compactification.Size() == space->GetVSize(), + "Compactification binding changed unexpectedly."); + serif_ensure( + space->GetMesh() == &discretization.residual_binding.mesh() && discretization.displacement_binding.space().GetMesh() == &discretization.residual_binding.mesh(), + "Term bindings must share a mesh."); + serif_ensure(discretization.mapper.get_dimension() == 3 && discretization.compactification.VectorDim() == 1, + "Equation mapping requires a three-dimensional mapper and scalar compactification."); + } + + /** @brief Validate one equation's bindings without constructing duplicate coefficient resources. */ + template + [[nodiscard]] auto make_volume_binding_input(const VolumeDiscretization& discretization, const CoefficientsT& term_coefficients, + utils::types::TypeList) + { + using namespace utils::dependencies; + using namespace utils::errors; + + validate_volume_discretization(discretization); + const auto validate_term = [&](OperandT) + { + const auto& input = term_coefficients.template get(); + static_assert(std::same_as::Schema, SchemaT>, "Term bindings must use the same domain schema."); + input.binding.validate_values(input.values); + serif_ensure(&input.binding.mesh() == &discretization.residual_binding.mesh(), + "Operator inputs must share the residual mesh."); + }; + (validate_term(Columns{}), ...); + + const BasisBindings bindings{.residual_binding = &discretization.residual_binding, + .displacement_binding = &discretization.displacement_binding, + .compactification_space = discretization.compactification_space, + .mapper = &discretization.mapper, + .nodes_sequence = discretization.residual_binding.mesh().GetNodesSequence(), + .columns = {&term_coefficients.template get().binding...}}; + return own::Bindings>(bindings, MetadataTracking{}); + } + + /** @brief Share equation metadata with a model's common physical coefficient context. */ + template + [[nodiscard]] auto make_volume_binding_input(const VolumeDiscretization& discretization, const CoefficientsT& term_coefficients) + { + return make_volume_binding_input(discretization, term_coefficients, typename EquationPlan::Columns{}); + } + + /** @brief Supply precisely the consumed terms for a standalone equation. */ + template + [[nodiscard]] auto make_volume_inputs(const VolumeDiscretization& discretization, const CoefficientsT& term_coefficients, utils::types::TypeList columns) + { + using namespace utils::dependencies; + + const auto bindings = make_volume_binding_input(discretization, term_coefficients, columns); + return make_dependency_context( + bindings, + own(MappingCoefficients{.binding = *discretization.compactification_space, .values = discretization.compactification}, + CoefficientTracking{}), + own(term_coefficients.template get(), CoefficientTracking{})...); + } + + /** @brief Bind equation and reference inputs when displacement will be produced inside the dependency graph. */ + template + [[nodiscard]] auto make_volume_inputs(const VolumeDiscretization& discretization, const CoefficientsT& term_coefficients) + { + return make_volume_inputs(discretization, term_coefficients, typename EquationPlan::Columns{}); + } + + /** @brief Append a checked external displacement while preserving each original resource's tracking policy. */ + template + [[nodiscard]] auto make_volume_inputs(const VolumeDiscretization& discretization, const CoefficientsT& term_coefficients, + const ::mfem::Vector& displacement_coefficients, utils::types::TypeList columns) + { + using namespace utils::dependencies; + + const auto inputs = make_volume_inputs(discretization, term_coefficients, columns); + discretization.displacement_binding.validate_values(displacement_coefficients); + return make_dependency_context(inputs.template resource::Bindings>(), + own(bind(discretization.displacement_binding, displacement_coefficients), CoefficientTracking{}), + inputs.template resource(), inputs.template resource()...); + } + + /// Build a checked source context without retaining the coefficient vectors. + template + [[nodiscard]] auto make_volume_inputs(const VolumeDiscretization& discretization, const CoefficientsT& term_coefficients, + const ::mfem::Vector& displacement_coefficients) + { + return make_volume_inputs(discretization, term_coefficients, displacement_coefficients, typename EquationPlan::Columns{}); + } + template + void make_volume_inputs(const VolumeDiscretization&, const CoefficientsT&, ::mfem::Vector&&) = delete; + + template + void make_volume_inputs(const VolumeDiscretization&, const CoefficientsT&, const ::mfem::Vector&&) = delete; +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/volume_stages.hpp b/src/include/serif/backend/mfem/operators/volume_stages.hpp new file mode 100644 index 00000000..733149d7 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/volume_stages.hpp @@ -0,0 +1,421 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "serif/operators/contributions.hpp" +#include "serif/backend/mfem/operators/element_preparation.hpp" +#include "serif/backend/mfem/operators/element_evaluation.hpp" +#include "serif/backend/mfem/operators/geometry_derivatives.hpp" +#include "serif/backend/mfem/operators/preparation.hpp" +#include "serif/backend/mfem/operators/volume_data.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" + +#include "serif/utils/error/errors.hpp" +#include "serif/utils/types/strings.hpp" + +namespace serif::operators::backend::mfem { + namespace detail { + /** + * @brief Build one immutable batch entry per index, concurrently, with one Workspace per worker. + * @note make(index, workspace) must depend only on its index and write only that workspace, so the + * batch is identical for every thread count. Like the serial loop, the result is the batch or the + * first failure in index order (a rejection, or a rethrown exception). Indices after a known + * failure are skipped; every index before the first failure always runs. + */ + template + [[nodiscard]] std::expected, PreparationRejection> build_element_batch( + const int count, Workspace& workspace, MakeT&& make + ) { + struct Slot { + std::shared_ptr value; + std::optional rejection; + std::exception_ptr error; + }; + std::vector slots(static_cast(count)); + const int workers = ::serif::utils::parallel::worker_count(); + std::vector worker_workspaces(static_cast(workers > 1 ? workers - 1 : 0)); + std::atomic first_failure{count}; + const auto fail = [&first_failure](const int index) { + int current = first_failure.load(std::memory_order_relaxed); + while (index < current && !first_failure.compare_exchange_weak(current, index, std::memory_order_relaxed)) {} + }; + ::serif::utils::parallel::for_each_index(count, [&](const int index, const int worker) { + if (index > first_failure.load(std::memory_order_relaxed)) { + return; + } + Workspace& local = worker == 0 ? workspace : worker_workspaces[static_cast(worker - 1)]; + Slot& slot = slots[static_cast(index)]; + try { + auto value = make(index, local); + if (value) { + slot.value = std::make_shared(std::move(*value)); + return; + } + slot.rejection = std::move(value.error()); + } catch (...) { + slot.error = std::current_exception(); + } + fail(index); + }); + + ElementBatch result; + result.reserve(slots.size()); + for (auto& slot : slots) { + if (slot.error) { + std::rethrow_exception(slot.error); + } + if (slot.rejection) { + return std::unexpected(std::move(*slot.rejection)); + } + result.push_back(std::move(slot.value)); + } + return result; + } + } + + /// Sample the reference discretization once for each supported element of this contribution. + template + [[nodiscard]] std::expected::operand_count>>, PreparationRejection> sample_contribution_basis( + const VolumeConfiguration& configuration, + const ContributionBinding& contribution, + const BasisBindings::column_count>& bindings, + TensorBasisCache& cache + ) { + using namespace utils::errors; + using Plan = EquationPlan; + using Support = equations::SupportOf; + + const auto& discretization = configuration.discretization; + serif_ensure( + bindings.residual_binding == &discretization.residual_binding && + bindings.displacement_binding == &discretization.displacement_binding && + bindings.compactification_space == discretization.compactification_space && + bindings.mapper == &discretization.mapper, + "Prepare this volume graph with the discretization used to construct it."); + + ElementBatch> result; + + for (int element = 0; element < configuration.discretization.residual_binding.mesh().GetNE(); ++element) { + if (!SchemaT::template domain_id_belongs_to(configuration.discretization.residual_binding.mesh().GetAttribute(element))) { + continue; + } + + serif_ensure(configuration.discretization.residual_binding.supports(element), "Contribution support extends outside its residual row."); + + Plan::template for_each_input([&](OperandT, std::size_t) { + serif_ensure(bindings.columns[Plan::template column_index()]->supports(element), "An operand does not cover this contribution's integration support."); + }); + + auto basis = sample_element_basis( + *contribution.equation, element, configuration.discretization.residual_binding, configuration.discretization.displacement_binding, bindings.columns, + configuration.discretization.mapper, configuration.options.quadrature_policy, cache, configuration.options.contraction_mode); + if (!basis) { + return std::unexpected(basis.error()); + } + result.push_back(std::make_shared>(std::move(*basis))); + } + + return result; + } + + /// Only this operand's coefficients and reference basis determine its reference samples. + template + [[nodiscard]] OperandSamples sample_contribution_operand( + const ElementBatch::operand_count>>& basis, + const CoefficientView& input, + Workspace& workspace + ) { + OperandSamples result; + result.reference.reserve(basis.size()); + const auto elements = input.binding.validated_elements(); + + for (const auto& element : basis) { + result.reference.push_back(std::make_shared(sample_element_operand(*element, input, elements, workspace))); + } + + // Scalar values need no second numerical representation; mapped operands replace these pointers later. + result.physical = result.reference; + return result; + } + + /// Freeze mapping data and mapped bases independently of the equation's coefficient inputs. + template + [[nodiscard]] std::expected::operand_count>>, PreparationRejection> + map_contribution_geometry( + const VolumeConfiguration& configuration, + const ElementBatch::operand_count>>& basis, + const CoefficientView::DisplacementBinding>& displacement, + const MappingCoefficients& compactification, + Workspace& workspace + ) { + const auto displacement_elements = displacement.binding.validated_elements(); + return detail::build_element_batch::operand_count>>( + static_cast(basis.size()), workspace, [&](const int element, Workspace& local) { + return map_element_geometry(*basis[static_cast(element)], displacement.binding, + displacement_elements, displacement.values, compactification, configuration.discretization.mapper, local); + }); + } + + /// Map only operands whose physical representation depends on deformation. + template + [[nodiscard]] OperandSamples map_contribution_operand( + const OperandSamples& samples, + const ElementBatch>& geometry, + Workspace& workspace + ) { + OperandSamples result{.reference = samples.reference}; + result.physical.reserve(geometry.size()); + + for (std::size_t element = 0; element < geometry.size(); ++element) { + result.physical.push_back(std::make_shared(map_element_operand(*samples.reference[element], *geometry[element], workspace))); + } + + return result; + } + + /** @brief Compute local displacement derivatives as an immutable graph result for this contribution. */ + template + [[nodiscard]] std::expected, PreparationRejection> differentiate_contribution_geometry( + const VolumeConfiguration& configuration, + const ContributionData::operand_count>& contribution, + Workspace& workspace + ) { + const auto& discretization = configuration.discretization; + return detail::build_element_batch<::mfem::DenseMatrix>( + static_cast(contribution.elements.size()), workspace, [&](const int element, Workspace& local) { + return element_geometry_derivatives( + *contribution.elements[static_cast(element)], discretization.displacement_binding, + *discretization.compactification_space, discretization.mapper, local + ); + }); + } + + /// Associate an operand type pack with equally ordered sample arguments. + template + using SamplesFor = OperandSamples; + + /// Evaluate one equation contribution; its basis, mapping and individual operand samples are already prepared. + template + [[nodiscard]] std::expected::operand_count>, PreparationRejection> + evaluate_contribution_equation( + const ContributionBinding& contribution, + const ElementBatch::operand_count>>& basis, + const ElementBatch::operand_count>>& geometry, + Workspace& workspace, + const SamplesFor&... samples + ) { + using Plan = EquationPlan; + + if (!std::isfinite(contribution.coefficient)) { + return std::unexpected(PreparationRejection{.reason = PreparationRejectionReason::NON_FINITE, .message = "The product of equation coefficients is non-finite."}); + } + + ContributionData result; + if constexpr (requires { contribution.equation->residual_offset(); }) { + static_assert(discretization::blocks::GlobalScalarTerm, "Residual offsets require a global scalar row."); + result.residual_offset = contribution.coefficient * contribution.equation->residual_offset(); + } + + utils::CompensatedSum validation_integral; + for (std::size_t element = 0; element < basis.size(); ++element) { + std::array, Plan::operand_count> reference_values{}, physical_values{}; + + ((reference_values[equations::OperandValues::template index()] = samples.reference[element], physical_values[equations::OperandValues::template index()] = samples.physical[element]), ...); + + auto data = evaluate_element_equation(*contribution.equation, contribution.coefficient, basis[element], geometry[element], std::move(reference_values), physical_values, workspace); + + if (!data) { + return std::unexpected(data.error()); + } + + validation_integral.add(data->validation_integral); + result.elements.push_back(std::make_shared>(std::move(*data))); + } + + if constexpr (requires { contribution.equation->validate_integral(0.0); }) { + try { + contribution.equation->validate_integral(validation_integral.value()); + } catch (const std::domain_error& error) { + return std::unexpected(PreparationRejection{.reason = PreparationRejectionReason::EQUATION, .message = error.what()}); + } + } + return result; + } + + /** + * @brief Collect the results of all contributions into a single VolumeData. This contains the residuals due to + * each contribution for all degrees of freedom as well as data needed for subsequent jacobian calculations + * + * @tparam EquationT The equation type which is being solved, this is used to determine the test and operand types. + * @tparam SchemaT The schema type which is being solved, this is used to constrain the support of the contributions and define which material and boundary attributes belong to what dofs + * @tparam Contributions The contributions which are being evaluated, these are the terms in the equation which are being integrated over the volume + * + * @param configuration The configuration of the volume problem, including the equation and discretization. + * @param bindings The basis bindings for the volume problem, including the residual and displacement bindings + * @param workspace The workspace for the volume problem, including the full result vector and element + * @param contributions The contributions to the volume problem, including their basis, geometry, and operand samples. + * + * @returns A VolumeData object containing the residuals due to each contribution for all degrees of freedom as well as data needed for subsequent jacobian calculations. + * + * @start_compile_errors + * @compile_error If the a contribution defines a residual_offset but that residual offset is not addable to a double producing a double. + * + * @par Detailed Explanation + * This is a really important function on the backend to lets take some time to walk through it. We take in + * some configuration data (including things like what equation we are solving and what discretization we are using, the basis + * binding which contains mappings from full dof space to supported dof space, a runtime workspace, and + * a variable number of contributions. + * + * After some setup we initialize the VolumeData object which will be what we eventually return. The first part is then + * to loop over all contributions and for each contribution accumulate its residual_offset. See @ref AngularMomentumConstraint for more + * detail; however, the gist is that this is some constant term which should be removed from the final integrated quantity to + * evaluate the residual (think the integrated angular momentum - the target angular momentum for the residual). + * + * We first fold over all contributions and accumulate their residuals and following that there is then some basic validation that the total residual offset is finite. + * + * Next we extract the "residual bindings" from the discretization provided by the configuration. The residual bindings is a, after a few inheritances, DofMap. + * This means it fundamentally represents some term and how that term is supported on the mesh. So when we ask for say the size of the residual binding + * what that is saying is "how many dofs is this residual defined for". We make the full result a vector the size of the number of dofs the + * residual is defined for. + * + * We use the CompensatedSum class to help avoid round off and cancellation errors. + * + * We then go through and for each contribution to each element we evaluate the residual from that contribution. The evaluate_residual function + * is the primary function where we actually integrate an equations contributions over a volume and we include this in the global residual sum. + * We fold over and call this function for every contribution and for every element. Thereby getting the total residual for the entire volume due + * to all contributions. + * + * What we end up with is this global_sum. We then need to apply one more modification to it, if we are working with some global scalar (e.g. fixed angular + * momentum) then we add the residual offset (note that this will often be a negative value, AngularMomentumConstraint for example returns -angular momentum target). + * + * Then, regardless of if we are working with a global scalar or not we gather the full result into the residual vector. Gathering here means that we select + * all the residual values on supported dofs and distribute them into the correct indices of the full space residual vector. Dofs which are not + * supported by the residual binding will be set to zero. + * + * @par TL:DR + * Use this function to evaluate the residual of a volume problem, likely you never need to think about the implementation details. + * + * + */ + template + [[nodiscard]] std::expected, PreparationRejection> collect_volume_results( + const std::shared_ptr>& configuration, + const BasisBindings::column_count>& bindings, + Workspace& workspace, + const Contributions&... contributions + ) { + using namespace utils::errors; + using Plan = EquationPlan; + + VolumeData result{ + .configuration = configuration, + .bindings = bindings, + .contributions = {contributions.elements...} + }; + double residual_offset = 0.0; + + auto accumulate_residual_offset = [](const auto& contribution, double& total_offset) { + using namespace utils::errors; + if constexpr (requires { contribution.residual_offset; }) { + static_assert(requires { total_offset += contribution.residual_offset; }, "Residual offsets must be addable to a double. Currently it is not. This is likely an issue with how an equation is defined rather than some issue with the backend."); + total_offset += contribution.residual_offset; + } + }; + (accumulate_residual_offset(contributions, residual_offset), ...); + + if (!std::isfinite(residual_offset)) { + return std::unexpected(PreparationRejection{ + .reason = PreparationRejectionReason::NON_FINITE, + .message = "The global residual offset is non-finite." + }); + } + + const auto& residual_binding = configuration->discretization.residual_binding; + workspace.full_result.SetSize(residual_binding.full_size()); + workspace.full_result = 0.0; + + utils::CompensatedSum global_sum; + std::optional rejection; + + const auto assemble_element_residuals = []( + const auto& l_result, + Workspace& l_workspace, + utils::CompensatedSum& l_sum, + std::optional& l_rejection, + const auto& l_residual_binding + ) { + using Contribution = std::tuple_element_t>; + const auto elements = l_residual_binding.validated_elements(); + + for (const auto& element : l_result.contributions[Index]) { + if (l_rejection) { + return; + } + + // Trace this function if you want to see how the residual is actually being integrated + // This gets the integrated weak residual for some element associated to some test function + // Note that this means that the shape of this result will depend on the shape of the test space. + // The primary distinction here will then be between point wise and global physis. Global constraints + // are integrated against a test function of a constant scalar 1, therefore they have a shape of 1 and only + // one residual is associated per element; whereas point-wise physics end up with a residual for every + // coefficient of the test function space on that element. + // TLDR: Get the residual + element_residual(*element, l_workspace.element_result, l_workspace.basis); + + if constexpr (discretization::blocks::GlobalScalarTerm) { + l_sum.add(l_workspace.element_result[0]); + if (!std::isfinite(l_sum.value())) { + l_rejection = PreparationRejection{ + .reason = PreparationRejectionReason::NON_FINITE, + .element = element->element, + .message = "The scalar integral overflowed while accumulating this element." + }; + } + } else { + elements.add_element(element->element, l_workspace.element_result, l_workspace.full_result); + } + } + }; + + + const auto assemble_contributions = [&assemble_element_residuals]( + std::index_sequence, + const auto &l_result, + Workspace& l_workspace, + utils::CompensatedSum& l_sum, + std::optional& l_rejection, + const auto& l_residual_binding + ) { + (assemble_element_residuals.template operator()(l_result, l_workspace, l_sum, l_rejection, l_residual_binding), ...); + }; + + // Note the side effects on global_sum and rejection (workspace also has side effects but this is less interesting as that's only there to avoid reallocations) + auto index_sequence = std::make_index_sequence{}; + assemble_contributions(index_sequence, result, workspace, global_sum, rejection, residual_binding); + + if (rejection) { + return std::unexpected(*rejection); + } + + if constexpr (discretization::blocks::GlobalScalarTerm) { + workspace.full_result[0] = global_sum.value() + residual_offset; + if (!std::isfinite(workspace.full_result[0])) { + return std::unexpected(PreparationRejection{ + .reason = PreparationRejectionReason::NON_FINITE, + .message = "Non-finite integrated scalar residual." + }); + } + } + + result.residual = residual_binding.accumulate_dual(workspace.full_result); + residual_binding.validate_values(result.residual); + return result; + } +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/operators/workspace.hpp b/src/include/serif/backend/mfem/operators/workspace.hpp new file mode 100644 index 00000000..801e85c2 --- /dev/null +++ b/src/include/serif/backend/mfem/operators/workspace.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "serif/backend/mfem/mapping/data.hpp" +#include "serif/backend/mfem/mapping/variation.hpp" +#include "serif/backend/mfem/discretization/basis_evaluation.hpp" + +namespace serif::operators::backend::mfem { + /// Reusable scratch for applying the accepted element linearization. + struct Workspace final { + mapping::DMWorkspace mapping_workspace{3}; + mapping::VolumeMappingContext mapping_context; + mapping::VolumeMappingVariation mapping_variation; + ::mfem::Vector element_values; + ::mfem::Vector element_result; + ::mfem::Vector quadrature_values; + ::mfem::Vector displacement_direction; + ::mfem::Vector full_result; + discretization::backend::mfem::BasisEvaluation test_basis_variation; + discretization::backend::mfem::BasisWorkspace basis; + ::mfem::Vector displacement_samples; + ::mfem::Vector displacement_value = ::mfem::Vector(3); + ::mfem::DenseMatrix displacement_gradient{3}; + ::mfem::Vector test_values; + ::mfem::Vector reference_test_values; + ::mfem::Vector operand_dual; + ::mfem::Vector displacement_dual; + ::mfem::Vector test_variation_result; + std::vector<::mfem::Vector> operand_values; + ::mfem::Vector lane_coefficients; ///< Lane-interleaved element displacement directions (batched geometry actions) + ::mfem::Vector lane_samples; ///< Their lane-interleaved displacement samples + }; +} // namespace serif::operators::backend::mfem diff --git a/src/include/serif/backend/mfem/projection/radial.hpp b/src/include/serif/backend/mfem/projection/radial.hpp new file mode 100644 index 00000000..5eae2c53 --- /dev/null +++ b/src/include/serif/backend/mfem/projection/radial.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include + +#include "mfem/fem/gridfunc.hpp" +#include "serif/utils/projection/radial.hpp" + +namespace serif::mapping { + class GridFunctionMappingEvaluator; +} + +namespace serif::utils::projection::backend::mfem { + /** + * @brief Sample physical radii about a center, optionally through SERiF's existing geometry mapping. + * The mapping must use the projection space's mesh and must be refreshed after its input fields change. + */ + struct RadialProjectionOptions { + std::array center{}; + mapping::GridFunctionMappingEvaluator* mapping{nullptr}; + // Infinity callbacks return limiting values already pulled back to mesh coordinates. + std::function scalar_at_infinity{}; + std::function(const ::mfem::Vector&)> vector_at_infinity{}; + }; + + using ProjectionResult = std::expected<::mfem::GridFunction, std::string>; + + namespace detail { + /** @brief Implement scalar projection once for all checked radial profile types. */ + [[nodiscard]] ProjectionResult project_scalar( + ::mfem::FiniteElementSpace& space, + const std::function& profile, + const RadialProjectionOptions& options + ); + + /** @brief Implement vector projection once for all checked radial profile types. */ + [[nodiscard]] ProjectionResult project_vector( + ::mfem::FiniteElementSpace& space, + const std::function& radial_amplitude, + const RadialProjectionOptions& options + ); + } + + /** + * @brief Project a radial scalar with the finite element's native coefficient projection. + * + * This uses native interpolation and conforming restriction, not a global L2 fit. + * Hanging-node constraints are enforced before publishing the field. + * The returned field owns its coefficients + * and borrows the supplied space. The profile controls exterior values; no radius clamping or + * mass adjustment is applied. A rejected sample discards the entire unpublished field. + * At compactified infinity an explicit scalar_at_infinity callback may supply the limiting + * value in mesh coordinates, before the finite element's native mapping. Without that + * prescription infinity is rejected. Meshes and spaces must remain alive for the returned field. + */ + template + [[nodiscard]] ProjectionResult project_scalar( + ::mfem::FiniteElementSpace& space, + const Function& profile, + const RadialProjectionOptions& options = {} + ) { + return detail::project_scalar(space, projection::detail::checked_profile(profile), options); + } + + /** + * @brief Project amplitude(r) times the outward radial unit vector into a vector H1/L2 space. + * + * Smooth radial vectors must have zero amplitude at the center. Conforming restriction enforces + * hanging-node constraints before publishing the field; this is not a global L2 fit. The profile + * remains in physical coordinates. + * At compactified infinity, vector_at_infinity must supply the limit in mesh coordinates. The callback + * receives the original mesh position, without subtraction of the configured physical center. + */ + template + [[nodiscard]] ProjectionResult project_vector( + ::mfem::FiniteElementSpace& space, + const Function& radial_amplitude, + const RadialProjectionOptions& options = {} + ) { + return detail::project_vector(space, projection::detail::checked_profile(radial_amplitude), options); + } +} diff --git a/src/include/serif/backend/mfem/runtime.hpp b/src/include/serif/backend/mfem/runtime.hpp new file mode 100644 index 00000000..99e22294 --- /dev/null +++ b/src/include/serif/backend/mfem/runtime.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace serif::backend::mfem { + /** + * @brief Initialize the selected numerical runtime once, before application work begins. + * @note Call on the main thread. An existing MPI session remains externally owned; a session + * started here is finalized by MFEM at process exit. Serial builds need no initialization. + */ + void initialize(); +} diff --git a/src/include/serif/backend/mfem/serialize/README.md b/src/include/serif/backend/mfem/serialize/README.md new file mode 100644 index 00000000..b927bfed --- /dev/null +++ b/src/include/serif/backend/mfem/serialize/README.md @@ -0,0 +1,50 @@ +# MFEM numerical archives + +`save_discretization(discretization)` records the physical and logical reference +meshes, finite-element collections and spaces, term support and reduced numbering, +compactification coefficients, mapping prescription, and deformation prescription. +The application's resource owner and computational caches are not persistent data. + +`restore_discretization(node)` returns an expected owning discretization. +`Original` supplies its compile-time term, domain-schema and deformation types. +The returned object has a new resource-owner type; this is why ordinary +`decode` cannot reconstruct an arbitrary application-owned discretization. +Its bindings and reference spaces borrow only resources retained by that new owner. + +For a complete `make_discretization(...)` configuration, include `configuration.hpp` +and use `restore_configuration(node)`. This restores the +resolved physical equations, independent FE resources, normalization choices and +preconditioner recipe together. The original model's authored callback is replaced +by the saved resolved equations; executable callbacks cannot be recreated from data. + +For an external mesh, call +`save_discretization(discretization, {.embed_mesh = false})`. Supply a `MeshResolver` +to restoration: it receives each requested content fingerprint and returns native +MFEM mesh text. `mesh_text(mesh)` produces the canonical text expected by this +resolver. The same content and reconstruction checks apply to embedded and +external meshes. Paths are deliberately left to the application. + +Mesh text uses the classic locale and `max_digits10`. Restoration disables MFEM's +refinement marking and optional element-orientation repair, then requires the +reconstructed native mesh text to match exactly. Space reconstruction also checks +the native signed element numbering; typed bindings check domain support and +reduced numbering. Existing geometry validation checks paired reference meshes +and shared scalar/displacement companion collections. + +The current native format supports finite conforming serial 3D meshes and +fixed-order standard H1, L2 and RT spaces. Unsupported resource kinds remain +explicit `unsupported` nodes in the archive. Exact restoration currently requires +the same MFEM version, MFEM exception support and its `MFEM_ERROR_THROW` error +policy, so malformed native input can be reported instead of terminating the +application. The loader does not change the application's global MFEM error policy. Additional dependency +versions and MFEM feature configuration are provenance, not reconstructed objects. +MPI library information is queried only if MPI is already running. + +Store physical `StellarState` coefficients alongside the matching numerical +description. The caller supplies that pairing; a bare coefficient vector cannot +identify its originating mesh. State loading validates the compiled coordinate +schema, backend loading validates the mesh and FE representation, and Newton +resumption additionally checks the rebuilt accepted residual metric. Element +samples, residuals, Jacobians and preconditioners are recomputed from the loaded +inputs. A loaded candidate still requires numerical qualification before it can +be reported as a converged equilibrium. diff --git a/src/include/serif/backend/mfem/serialize/configuration.hpp b/src/include/serif/backend/mfem/serialize/configuration.hpp new file mode 100644 index 00000000..f4712b18 --- /dev/null +++ b/src/include/serif/backend/mfem/serialize/configuration.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/serialize/discretization.hpp" +#include "serif/model/configured_discretization.hpp" +#include "serif/utils/serialize/numerics.hpp" + +namespace serif::backend::mfem::serialize { + /** @brief Rebuild a configured model with owned equations and FE resources while retaining its numerical choices. */ + template + using RestoredConfiguration = decltype(model::make_discretization(model::DiscretizationOptions{ + .model = std::declval().model)>>>(), + .finite_elements = std::declval().finite_elements)>>>(), + .normalization = std::declval().normalization)>>(), + .preconditioner = std::declval().preconditioner)>>() + })); + + /** + * @brief Restore the complete model and numerical configuration with independently owned finite-element resources. + * @note Original supplies compile-time physics and numerical types; application callbacks and resource owners are replaced by saved values. + */ + template + [[nodiscard]] utils::serialize::Result> restore_configuration(const utils::serialize::Node& node, + const MeshResolver& resolver = {}) { + using namespace utils::serialize; + using Model = std::remove_cvref_t().model)>; + using FiniteElements = std::remove_cvref_t().finite_elements)>; + using Normalization = std::remove_cvref_t().normalization)>; + using Preconditioner = std::remove_cvref_t().preconditioner)>; + + if (node.version != 1) { + return std::unexpected(Error{"configuration", "Unsupported configured discretization version."}); + } + auto components = read_object(node, "model.configured_discretization", [](Node model, Node finite_elements, Normalization normalization, Preconditioner preconditioner) { + return std::tuple{std::move(model), std::move(finite_elements), std::move(normalization), std::move(preconditioner)}; + }, member("model"), member("finite_elements"), member("normalization"), member("preconditioner")); + if (!components) { + return std::unexpected(components.error()); + } + auto& [model_node, finite_elements_node, normalization, preconditioner] = *components; + auto stellar_model = restore_stellar_model(model_node); + if (!stellar_model) { + return std::unexpected(stellar_model.error()); + } + auto finite_elements = restore_discretization(finite_elements_node, resolver); + if (!finite_elements) { + return std::unexpected(finite_elements.error()); + } + try { + return model::make_discretization(model::DiscretizationOptions{ + .model = std::move(*stellar_model), .finite_elements = std::move(*finite_elements), + .normalization = std::move(normalization), .preconditioner = std::move(preconditioner) + }); + } catch (const std::exception& error) { + return std::unexpected(Error{"configuration", "Saved numerical configuration is invalid: " + std::string{error.what()}}); + } + } +} diff --git a/src/include/serif/backend/mfem/serialize/discretization.hpp b/src/include/serif/backend/mfem/serialize/discretization.hpp new file mode 100644 index 00000000..bed20107 --- /dev/null +++ b/src/include/serif/backend/mfem/serialize/discretization.hpp @@ -0,0 +1,235 @@ +#pragma once + +#include +#include +#include + +#include "serif/backend/mfem/model/discretization.hpp" +#include "serif/backend/mfem/serialize/resources.hpp" +#include "serif/utils/serialize/model.hpp" +#include "serif/utils/serialize/physics.hpp" + +namespace serif::backend::mfem::serialize { + namespace detail { + using discretization::backend::mfem::OwnedBindings; + + /** @brief Record semantic domain and boundary labels, independent of live mesh addresses. */ + template + [[nodiscard]] utils::serialize::Node domain_schema() { + using namespace utils::serialize; + + std::vector domains; + std::vector boundaries; + for (const auto& descriptor : Schema::domain_descriptors()) { + domains.push_back(object("mfem.domain", field("name", descriptor.name), field("attribute", descriptor.ID))); + } + for (const auto& descriptor : Schema::boundary_descriptors()) { + boundaries.push_back(object("mfem.boundary", field("name", descriptor.name), field("attribute", descriptor.ID))); + } + return object("mfem.schema", field("domains", domains), field("boundaries", boundaries)); + } + + /** @brief Restore one typed term only after checking its scientific identity and reduced numbering. */ + template + [[nodiscard]] utils::serialize::Result restore_binding(const utils::serialize::Node& node, OwnedBindings& owner) { + using namespace utils::serialize; + using Binding = typename OwnedBindings::template Binding; + + auto term = read_field(node, "term"); + auto schema = read_field(node, "schema"); + auto coordinates = read_field(node, "coordinates"); + if (node.type != "mfem.term_binding" || + (node.version != 1 && node.version != 2) || + (node.version == 2) != node.children.contains("reconstruction") || !term || !schema || !coordinates || *term != term_schema() || + *schema != domain_schema() || term->type == "unsupported") { + return std::unexpected(Error{"bindings", "Saved term or domain identity does not match the requested typed discretization."}); + } + std::unique_ptr binding; + if constexpr (discretization::blocks::SpatialTerm) { + auto index = read_field(node, "space"); + if (!index || *index >= owner.resources.spaces.size()) { + return std::unexpected(Error{"bindings/space", "Term space is missing or outside the restored resource table."}); + } + binding = std::make_unique(*owner.resources.spaces[*index]); + if (const auto saved = node.children.find("reconstruction"); saved != node.children.end()) { + if constexpr (!std::same_as) { + return std::unexpected(Error{"bindings/reconstruction", "Surface-power reconstruction supports only mass density."}); + } else { + const auto& representation = saved->second; + auto exponent = read_field(representation, "density_exponent"); + auto boundary = read_field(representation, "boundary"); + if (representation.type != "mfem.surface_power" || representation.version != 1 || !exponent || !boundary) { + return std::unexpected(Error{"bindings/reconstruction", "Unknown or incomplete reconstruction; its numerical interpretation cannot be restored."}); + } + const discretization::reconstruction::SurfacePower specification{ + .density_exponent = *exponent, .region = {*boundary}}; + auto plan = discretization::backend::mfem::make_reconstruction(specification, *binding); + binding = std::make_unique(*owner.resources.spaces[*index], std::move(plan)); + } + } + } else { + if (node.children.contains("reconstruction")) { + return std::unexpected(Error{"bindings/reconstruction", "Global scalar fields cannot carry spatial reconstruction."}); + } + auto index = read_field(node, "mesh"); + if (!index || *index >= owner.resources.meshes.size()) { + return std::unexpected(Error{"bindings/mesh", "Global scalar mesh is missing or outside the restored resource table."}); + } + binding = std::make_unique(*owner.resources.meshes[*index]); + } + if (*coordinates != binding_coordinates(*binding)) { + return std::unexpected(Error{"bindings/coordinates", "Term domain support or reduced coefficient numbering changed during reconstruction."}); + } + std::get()>(owner.bindings) = std::move(binding); + return {}; + } + } + + /** @brief Retain all compile-time physical choices while replacing an opaque user owner with archive-owned resources. */ + template + using RestoredDiscretization = model::backend::mfem::StellarDiscretization< + detail::OwnedBindings().term_bindings)>>, + decltype(std::declval().term_bindings)>>>().context()), + std::remove_cvref_t().deformation)>>; + + /** @brief Save physical FE interpretation and numerical geometry, excluding caches and the application's opaque owner. */ + template + [[nodiscard]] utils::serialize::Node save_discretization(const model::backend::mfem::StellarDiscretization& value, + SaveOptions options = {}) { + using namespace utils::serialize; + + ResourceWriter resources(options); + Node bindings{.type = "array"}; + [&](utils::dependencies::KeyList) { + std::size_t index = 0; + ([&] { + using Binding = std::remove_cvref_t())>; + const auto& binding = value.template binding(); + Node record = object("mfem.term_binding", field("term", term_schema()), + field("schema", detail::domain_schema()), field("coordinates", binding_coordinates(binding))); + if constexpr (discretization::blocks::SpatialTerm) { + record.children.emplace("space", encode(resources.space(binding.space()))); + if (const auto& reconstruction = binding.reconstruction()) { + // Version 2 prevents old readers from silently treating weighted coefficients as ordinary polynomials. + record.version = 2; + const auto* profile = dynamic_cast(&reconstruction->profile()); + utils::errors::serif_ensure(profile != nullptr, + "Reconstruction '{}' has no archive codec; add one before saving this field.", reconstruction->profile().name()); + std::string boundary_name; + for (const auto& boundary : Binding::Schema::boundary_descriptors()) { + if (boundary.ID == reconstruction->boundary_attribute()) { boundary_name = boundary.name; } + } + utils::errors::serif_ensure(!boundary_name.empty(), + "Cannot save a reconstruction whose boundary is absent from the field's schema."); + record.children.emplace("reconstruction", object("mfem.surface_power", + field("density_exponent", profile->density_exponent()), field("boundary", boundary_name))); + } + } else { + record.children.emplace("mesh", encode(resources.mesh(binding.mesh()))); + } + bindings.children.emplace(std::to_string(index++), std::move(record)); + }(), ...); + }(typename Bindings::Keys{}); + resources.mapping(value.compactification, value.mapper); + Node result = object("mfem.stellar_discretization", field("bindings", bindings)); + if constexpr (std::same_as) { + result.children.emplace("deformation", Node{.type = "mfem.no_deformation"}); + } else { + result.children.emplace("deformation", encode(value.deformation.model)); + result.children.emplace("reference", resources.reference(value.deformation.reference)); + } + result.children.emplace("resources", std::move(resources).finish()); + return result; + } + + /** + * @brief Recreate a typed discretization with its own meshes, spaces and mapping inputs. + * @note Original supplies compile-time term/schema/deformation types only. Its application-specific owner is never constructed. + */ + template + [[nodiscard]] utils::serialize::Result> restore_discretization(const utils::serialize::Node& node, + const MeshResolver& resolver = {}) { + using namespace utils::serialize; + using Context = std::remove_cvref_t().term_bindings)>; + using Deformation = std::remove_cvref_t().deformation)>; + + if (node.type != "mfem.stellar_discretization" || node.version != 1) { + return std::unexpected(Error{"discretization", "Unsupported MFEM stellar discretization schema."}); + } + try { + auto resources_node = read_field(node, "resources"); + // Inspect each binding version locally; generic vector decoding only accepts version-1 children. + auto bindings = read_field(node, "bindings"); + if (!resources_node || !bindings || bindings->type != "array" || !std::holds_alternative(bindings->value) || bindings->children.size() != utils::dependencies::key_count_v) { + return std::unexpected(Error{"discretization", "Missing resources or incompatible term-binding count."}); + } + auto resources = restore_resources(*resources_node, resolver); + if (!resources) { + return std::unexpected(resources.error()); + } + auto owner = std::make_shared>(); + owner->resources = std::move(*resources); + std::optional error; + [&](utils::dependencies::KeyList) { + std::size_t index = 0; + ([&] { + if (!error) { + const auto saved = bindings->children.find(std::to_string(index)); + if (saved == bindings->children.end()) { + error = Error{"bindings", "Term-binding array indices are not contiguous."}; + } else { + auto status = detail::restore_binding(saved->second, *owner); + if (!status) { error = status.error(); } + } + } + ++index; + }(), ...); + }(typename Context::Keys{}); + if (error) { + return std::unexpected(*error); + } + auto base = model::backend::mfem::make_stellar_discretization(owner, owner->context(), *owner->resources.compactification, *owner->resources.mapper); + auto deformation = read_field(node, "deformation"); + if (!deformation) { + return std::unexpected(deformation.error()); + } + if constexpr (std::same_as) { + if (deformation->type != "mfem.no_deformation") { + return std::unexpected(Error{"deformation", "Saved shape representation differs from the requested fixed-geometry discretization."}); + } + return base; + } else { + using DomainModel = std::remove_cvref_t().model)>; + auto deformation_model = decode(*deformation); + auto reference_node = read_field(node, "reference"); + if (!deformation_model || !reference_node) { + return std::unexpected(Error{"deformation", "Missing or unsupported surface/extension prescription or reference geometry."}); + } + auto reference = restore_reference(*reference_node, owner->resources); + if (!reference) { + return std::unexpected(reference.error()); + } + return model::backend::mfem::with_deformation(std::move(base), std::move(*deformation_model), std::move(*reference)); + } + } catch (const std::exception& error) { + return std::unexpected(Error{"discretization", "Saved numerical interpretation is invalid: " + std::string{error.what()}}); + } + } +} + +namespace serif::utils::serialize { + /** @brief Integrate native discretizations into packs while keeping application-owner reconstruction explicit. */ + template + struct Serializer> { + using Discretization = model::backend::mfem::StellarDiscretization; + static constexpr std::string_view key = "discretization"; + /** @brief Include complete reference meshes unless save_discretization explicitly requests external resolution. */ + static Node save(const model::backend::mfem::StellarDiscretization& value) { + return serif::backend::mfem::serialize::save_discretization(value); + } + /** @brief Explain why reconstructing an arbitrary application resource-owner type is not a generic decoding operation. */ + static Result load(const Node&) { + return std::unexpected(Error{"discretization", "Use serif::backend::mfem::serialize::restore_discretization(node) to reconstruct an independently owned numerical discretization."}); + } + }; +} diff --git a/src/include/serif/backend/mfem/serialize/options.hpp b/src/include/serif/backend/mfem/serialize/options.hpp new file mode 100644 index 00000000..555a4181 --- /dev/null +++ b/src/include/serif/backend/mfem/serialize/options.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "serif/backend/mfem/operators/volume_data.hpp" +#include "serif/utils/serialize/numerics.hpp" + +namespace serif::utils::serialize { + /** @brief Preserve quadrature and contraction choices separately from physical coefficients. */ + template <> + struct Serializer { + static constexpr std::string_view key = "assembly_options"; + /** @brief Record the complete quadrature prescription and the explicit contraction selection. */ + static Node save(const operators::backend::mfem::VolumeOptions& value) { + return object("mfem.volume_options", field("quadrature", value.quadrature_policy), field("contraction", static_cast(value.contraction_mode))); + } + /** @brief Reject unknown modes rather than silently choosing a different assembly strategy. */ + static Result load(const Node& node) { + using Mode = operators::backend::mfem::ContractionMode; + + auto result = read_object(node, "mfem.volume_options", [](discretization::quadrature::Policy quadrature, int contraction) { + return operators::backend::mfem::VolumeOptions{.quadrature_policy = std::move(quadrature), .contraction_mode = static_cast(contraction)}; + }, member("quadrature"), member("contraction")); + if (result && result->contraction_mode != Mode::automatic && result->contraction_mode != Mode::dense) { + return std::unexpected(Error{"contraction", "Unsupported MFEM contraction mode."}); + } + return result; + } + }; +} diff --git a/src/include/serif/backend/mfem/serialize/resources.hpp b/src/include/serif/backend/mfem/serialize/resources.hpp new file mode 100644 index 00000000..8c39b2f1 --- /dev/null +++ b/src/include/serif/backend/mfem/serialize/resources.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/discretization/resources.hpp" +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/backend/mfem/geometry/deformation/inputs.hpp" +#include "serif/backend/mfem/discretization/dof_map.hpp" +#include "serif/utils/serialize/record.hpp" + +namespace serif::backend::mfem::serialize { + /** @brief Choose whether reference meshes travel with the archive or are resolved by their content identity. */ + struct SaveOptions { + bool embed_mesh{true}; + }; + + /** @brief Supply native MFEM mesh text for the requested content fingerprint when a mesh was omitted. */ + using MeshResolver = std::function(std::string_view)>; + + using discretization::backend::mfem::Resources; + + /** + * @brief Collect shared meshes, collections and spaces once while recording their associations. + * @note Pointer identity is used only during writing; the archive contains numbered references. + */ + class ResourceWriter { + public: + /** @brief Select whether each distinct mesh is embedded. */ + explicit ResourceWriter(SaveOptions options = {}); + /** @brief Record one mesh and return its archive index. */ + [[nodiscard]] std::uint64_t mesh(const ::mfem::Mesh& value); + /** @brief Record one fixed-order space and return its archive index. */ + [[nodiscard]] std::uint64_t space(const ::mfem::FiniteElementSpace& value); + /** @brief Record field geometry, compactification and mapping inputs. */ + void mapping(const ::mfem::GridFunction& compactification, const serif::mapping::DomainMapper& mapper); + /** @brief Record the paired spaces and mesh used to interpret shape coefficients. */ + [[nodiscard]] utils::serialize::Node reference(const geometry::deformation::backend::mfem::ReferenceDiscretization& value); + /** @brief Move the complete resource record into its containing discretization. */ + [[nodiscard]] utils::serialize::Node finish() &&; + private: + /** @brief Share collection identity because deformation companions require the same collection object. */ + [[nodiscard]] std::uint64_t collection(const ::mfem::FiniteElementCollection& value); + SaveOptions m_options; + utils::serialize::Node m_record; + std::vector m_meshes; + std::vector m_collections; + std::vector m_spaces; + }; + + /** @brief Reconstruct all resources privately, rejecting changed mesh or coefficient interpretations. */ + [[nodiscard]] utils::serialize::Result restore_resources(const utils::serialize::Node& node, const MeshResolver& resolver = {}); + /** @brief Bind the saved reference roles to already restored resources. */ + [[nodiscard]] utils::serialize::Result restore_reference( + const utils::serialize::Node& node, Resources& resources); + /** @brief Capture domain support and reduced numbering without storing mutable DofMap internals. */ + [[nodiscard]] utils::serialize::Node binding_coordinates(const discretization::backend::mfem::DofMap& binding); + /** @brief Emit deterministic native mesh text with enough digits to preserve binary64 coordinates. */ + [[nodiscard]] std::string mesh_text(const ::mfem::Mesh& mesh); +} diff --git a/src/include/serif/backend/mfem/serialize/top.hpp b/src/include/serif/backend/mfem/serialize/top.hpp new file mode 100644 index 00000000..db0fca69 --- /dev/null +++ b/src/include/serif/backend/mfem/serialize/top.hpp @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/discretization/dof_map.hpp" +#include "serif/backend/mfem/geometry/deformation/computations.hpp" +#include "serif/eos/models/polytropic.hpp" +#include "serif/equations/barotropic_density.hpp" +#include "serif/equations/primal_gravity.hpp" +#include "serif/geometry/surface/isobaric.hpp" +#include "serif/model/description.hpp" +#include "serif/utils/dependencies/computation_cache.hpp" +#include "serif/utils/serialize/top.hpp" + +namespace serif::backend::mfem::serialize::detail { + /** + * @brief Sample copied coefficients on a private deformed geometry; the original state and mesh remain untouched. + * @note The original finite-element space and its mesh must outlive sampling, as for an MFEM GridFunction. + * The original term binding and coefficient buffer may be destroyed after construction. + */ + class TopModelSampler final { + public: + TopModelSampler(const discretization::backend::mfem::DofMap& density_binding, + std::span density, + const geometry::deformation::backend::mfem::ReferenceDiscretization& reference, + std::span component_displacement, + const std::array& axis, const std::array& center); + ~TopModelSampler(); + TopModelSampler(const TopModelSampler&) = delete; + TopModelSampler& operator=(const TopModelSampler&) = delete; + [[nodiscard]] utils::serialize::Result surface_radius(double mu, double phi); + [[nodiscard]] utils::serialize::Result density(double radius, double mu, double phi); + [[nodiscard]] double mass() const noexcept; + [[nodiscard]] std::size_t roundoff_density_clamps() const noexcept; + private: + struct Impl; + std::unique_ptr m_impl; + }; + + /** @brief Verify the actual closure EOS rather than assuming a borrowed equation uses the model's values. */ + template + [[nodiscard]] bool top_matching_polytrope(const EOS& candidate, const eos::models::Polytrope& physical) { + if constexpr (std::same_as, eos::models::Polytrope>) { + return candidate.polytropic_index() == physical.polytropic_index() && + candidate.polytropic_constant() == physical.polytropic_constant(); + } else if constexpr (requires { candidate.get(); }) { + return top_matching_polytrope(candidate.get(), physical); + } else { + return false; + } + } + + template inline constexpr bool top_rigid_barotrope = false; + template inline constexpr bool top_rigid_barotrope> = true; +} + +namespace serif::utils::serialize { + /** + * @brief Export the accepted density and FE surface of a uniformly rotating vacuum polytrope to TOP. + * @note StellarModel owns physics, while finite_elements and state supply its numerical realization. + * Enthalpy is derived only for this external file format; it is not introduced as a SERiF unknown. + * The native TOP polytrope reader does not represent general EOS or finite-pressure surfaces. + */ + template + [[nodiscard]] Result export_top(const std::filesystem::path& directory, + const Model& stellar_model, const FiniteElements& finite_elements, const State& state, + const TopExportOptions& options = {}) { + using Density = discretization::blocks::density::mass; + using Shape = discretization::blocks::surface_deformation::coefficients; + using Omega = discretization::blocks::rotation::angular_velocity; + namespace deformation = geometry::deformation; + namespace deform_backend = deformation::backend::mfem; + namespace adapter = serif::backend::mfem::serialize::detail; + if constexpr (!std::same_as) { + return std::unexpected(Error{"top/model/eos", "TOP's recovered polytrope reader requires the Polytrope EOS; a general stellar EOS needs a different TOP reader."}); + } else if constexpr (!Model::template contains_specification) { + return std::unexpected(Error{"top/model/rotation", "This TOP adapter requires FixedAngularMomentum with a solved uniform angular velocity."}); + } else if constexpr (!requires { + state.template get(); state.template get(); state.template get(); + finite_elements.template binding(); finite_elements.deformation.reference; + finite_elements.deformation.model.surface; + }) { + return std::unexpected(Error{"top/state", "TOP export requires solved density, angular velocity, and a moving stellar surface with matching finite-element bindings."}); + } else { + try { + const auto equations = model::resolve_equilibrium_equations(stellar_model); + if constexpr (!geometry::surface::IsobaricSurfaceType) { + return std::unexpected(Error{"top/model/surface", "TOP polytrope export requires an explicitly prescribed zero-pressure isobar."}); + } else { + if (equations.surface_condition.target_pressure().value() != 0.0) { + return std::unexpected(Error{"top/model/surface", "Finite-pressure surface conditions are not supported by TOP's recovered polytrope reader."}); + } + const auto& rotation = stellar_model.template specification(); + double four_pi_G = 0.0; + int gravity_count = 0, rigid_count = 0; + bool rigid_compatible = true; + equations.interior_equations.for_each([&](const auto& equation) { + using Equation = std::remove_cvref_t; + if constexpr (std::same_as) { + four_pi_G = equation.four_pi_G(); + ++gravity_count; + } + if constexpr (adapter::top_rigid_barotrope) { + ++rigid_count; + rigid_compatible = rigid_compatible && equation.surface_pressure().value() == 0.0 && + adapter::top_matching_polytrope(equation.equation_of_state(), stellar_model.equation_of_state()); + for (int component = 0; component < 3; ++component) { + rigid_compatible = rigid_compatible && + std::abs(equation.rotation().angular_velocity_vector()[component] - rotation.axis()[component]) < 1.0e-13 && + equation.rotation().axis_origin()[component] == rotation.center()[component]; + } + } + }); + if (gravity_count != 1 || rigid_count != 1 || !rigid_compatible) { + return std::unexpected(Error{"top/model/equations", "TOP export requires one PrimalPoissonBalance and one zero-pressure BarotropicDensityBalance using the model's EOS, uniform rotation axis, and center."}); + } + const auto omega = state.template get(); + if (omega.size() != 1 || !std::isfinite(omega[0])) { + return std::unexpected(Error{"top/state/rotation", "The uniform angular velocity must be one finite physical coefficient."}); + } + const auto inputs = deform_backend::make_deformation_inputs(finite_elements.deformation.model, + finite_elements.deformation.reference, state.template get()); + using Surface = std::remove_cvref_t; + auto cache = utils::dependencies::make_computation_cache( + deform_backend::make_deformation_graph(), inputs); + const auto prepared = cache.try_prepare(inputs, utils::dependencies::KeyList{}); + if (!prepared) { + return std::unexpected(Error{"top/geometry", prepared.error().error.message}); + } + auto sampler = std::make_shared(finite_elements.template binding(), + state.template get(), finite_elements.deformation.reference, + cache.template get(), rotation.axis(), rotation.center()); + const auto central_density = sampler->density(0.0, 1.0, 0.0); + if (!central_density) { return std::unexpected(central_density.error()); } + if (!std::isfinite(*central_density) || *central_density <= 0.0) { + return std::unexpected(Error{"top/state/center", "TOP normalization requires positive finite density at the rotation center."}); + } + const auto& eos = stellar_model.equation_of_state(); + const double central_enthalpy = eos.evaluate(serif::eos::relations::SpecificEnthalpyFromDensity{}, + dimensions::DensityValue{*central_density}).value(); + TopPolytropeSource source{ + .polytropic_index = eos.polytropic_index(), + .gravitational_constant = four_pi_G / (4.0 * std::numbers::pi), + .mass = sampler->mass(), .angular_velocity = omega[0], + .central_density = *central_density, .central_enthalpy = central_enthalpy, + .surface_radius = [sampler](double mu, double phi) { return sampler->surface_radius(mu, phi); }, + .enthalpy = [sampler, eos](double radius, double mu, double phi) -> Result { + auto density = sampler->density(radius, mu, phi); + if (!density) { return std::unexpected(density.error()); } + return eos.evaluate(serif::eos::relations::SpecificEnthalpyFromDensity{}, dimensions::DensityValue{*density}).value(); + } + }; + source.roundoff_density_clamps = [sampler] { return sampler->roundoff_density_clamps(); }; + return export_top(directory, source, options); + } + } catch (const std::exception& error) { + return std::unexpected(Error{"top/model", error.what()}); + } + } + } + + /** @brief Export a solver result with the separately owned FE resources that interpret its accepted state. */ + template + requires requires(const Solution& solution) { solution.has_state(); solution.model(); solution.state(); } + [[nodiscard]] Result export_top(const std::filesystem::path& directory, + const FiniteElements& finite_elements, const Solution& solution, const TopExportOptions& options = {}) { + if (!solution.has_state()) { + return std::unexpected(Error{"top/state", "The solve has no accepted stellar state to export."}); + } + return export_top(directory, solution.model(), finite_elements, solution.state(), options); + } +} diff --git a/src/include/serif/backend/mfem/shared/boundary_elements.hpp b/src/include/serif/backend/mfem/shared/boundary_elements.hpp new file mode 100644 index 00000000..4c985d82 --- /dev/null +++ b/src/include/serif/backend/mfem/shared/boundary_elements.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include "mfem/fem/fespace.hpp" + +namespace serif::backend::mfem::shared { + /** @brief Explain why canonical boundary coordinates or their local assembly were rejected. */ + struct BoundaryFailure { + std::string message; + int element{-1}; + bool incompatible_coordinates{true}; + }; + + using BoundaryStatus = std::expected; + + /** @brief Borrow one boundary element and its oriented coordinates during a traversal. */ + struct BoundaryElement { + const ::mfem::FiniteElement& finite_element; + ::mfem::ElementTransformation& transformation; + std::span encoded_dofs; + std::span selected_nodes; + }; + + /** + * @brief Visit a scalar nodal boundary in the caller's selected-node coordinates. + * @note Scratch coordinates are reused between elements. The visitor owns the numerical + * integrand; this traversal only verifies coordinate membership and MFEM orientation. + */ + template + [[nodiscard]] BoundaryStatus for_each_boundary_element(const ::mfem::FiniteElementSpace& space, int attribute, std::span selected_dofs, Visitor&& visit) { + if (space.GetMesh() == nullptr) { + return std::unexpected(BoundaryFailure{.message = "Boundary assembly requires a finite-element space attached to a mesh."}); + } + std::vector selected_index(space.GetNDofs(), -1); + for (std::size_t node = 0; node < selected_dofs.size(); ++node) { + const int dof = selected_dofs[node]; + if (dof < 0 || dof >= space.GetNDofs() || selected_index[dof] >= 0) { + return std::unexpected(BoundaryFailure{.message = "Boundary assembly requires distinct scalar nodes belonging to its finite-element space."}); + } + selected_index[dof] = static_cast(node); + } + ::mfem::Array dofs; + std::vector nodes; + for (int boundary = 0; boundary < space.GetMesh()->GetNBE(); ++boundary) { + if (space.GetMesh()->GetBdrAttribute(boundary) != attribute) { + continue; + } + if (space.GetBdrElementDofs(boundary, dofs) != nullptr || space.GetBE(boundary)->GetDof() != dofs.Size()) { + return std::unexpected(BoundaryFailure{.message = "Boundary assembly requires a canonical scalar nodal trace without additional DOF transformations.", .element = boundary}); + } + nodes.resize(dofs.Size()); + for (int local = 0; local < dofs.Size(); ++local) { + const int dof = ::mfem::FiniteElementSpace::DecodeDof(dofs[local]); + if (dof < 0 || dof >= space.GetNDofs() || selected_index[dof] < 0) { + return std::unexpected(BoundaryFailure{.message = "A boundary trace node is missing from the selected scalar coordinates.", .element = boundary}); + } + nodes[local] = selected_index[dof]; + } + const BoundaryElement element{.finite_element = *space.GetBE(boundary), .transformation = *space.GetBdrElementTransformation(boundary), .encoded_dofs = {dofs.GetData(), static_cast(dofs.Size())}, .selected_nodes = nodes}; + if (BoundaryStatus status = visit(element); !status) { + status.error().element = boundary; + return status; + } + } + return {}; + } +} diff --git a/src/include/serif/backend/mfem/shared/finite.hpp b/src/include/serif/backend/mfem/shared/finite.hpp new file mode 100644 index 00000000..5939c7db --- /dev/null +++ b/src/include/serif/backend/mfem/shared/finite.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "serif/utils/checks/finite.hpp" +#include "mfem/linalg/vector.hpp" +#include "mfem/linalg/densemat.hpp" + +namespace serif::utils::checks { + /** @brief Check every MFEM coefficient before publishing a numerical result. */ + inline bool is_finite(const mfem::Vector& vector) { + for (int i = 0; i < vector.Size(); ++i) { + if (!std::isfinite(vector(i))) { + return false; + } + } + + return true; + } + + /** @brief Check all entries of a small MFEM dense matrix. */ + inline bool is_finite(const mfem::DenseMatrix& matrix) { + for (int i = 0; i < matrix.Height(); ++i) { + for (int j = 0; j < matrix.Width(); ++j) { + if (!std::isfinite(matrix(i, j))) { + return false; + } + } + } + + return true; + } + +} diff --git a/src/include/serif/backend/mfem/shared/invertible.hpp b/src/include/serif/backend/mfem/shared/invertible.hpp new file mode 100644 index 00000000..f3bbb25d --- /dev/null +++ b/src/include/serif/backend/mfem/shared/invertible.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "serif/backend/mfem/shared/finite.hpp" + +namespace serif::utils::checks +{ + /** + * @brief Check a one- to three-dimensional square Jacobian before asking MFEM to invert it. + * @note Use MFEM's scale-relative singularity criterion in all build modes, so invalid geometry can + * be reported as a value instead of triggering its debug assertion. Uniform coordinate scaling is permitted. + */ + [[nodiscard]] inline bool has_stable_inverse(const ::mfem::DenseMatrix& matrix) + { + const int dimension = matrix.Width(); + if (dimension < 1 || dimension > 3 || matrix.Height() != dimension || !is_finite(matrix)) + { + return false; + } + const double determinant = matrix.Det(); + const double threshold = 1.0e-14 * std::pow(matrix.FNorm() / dimension, dimension); + return std::isfinite(determinant) && std::abs(determinant) > threshold; + } +} diff --git a/src/include/serif/backend/mfem/shared/parallel.hpp b/src/include/serif/backend/mfem/shared/parallel.hpp new file mode 100644 index 00000000..6f159b10 --- /dev/null +++ b/src/include/serif/backend/mfem/shared/parallel.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +namespace mfem { + class IsoparametricTransformation; + class Mesh; +} + +namespace serif::utils::parallel { + /** + * @brief Workers an element loop may use: OpenMP's thread limit (OMP_NUM_THREADS), or 1 without + * OpenMP or inside an enclosing parallel region. + */ + [[nodiscard]] int worker_count() noexcept; + + namespace detail { + using Body = void (*)(void* context, int index, int worker); + + /** @brief Run body for every index; body must not throw. Compiled in libserif so OpenMP never depends on the caller's flags. */ + void run(int count, void* context, Body body); + } + + /** + * @brief Call body(index, worker) once for every index in [0, count), possibly concurrently. + * @note Results do not depend on the thread count when each call writes only index-owned results and + * scratch owned by its worker (0 <= worker < worker_count()). Every index runs; the exception of the + * lowest throwing index is then rethrown, which is the one a serial loop would have raised. + */ + template + void for_each_index(const int count, BodyT&& body) { + std::vector errors(static_cast(count > 0 ? count : 0)); + auto guarded = [&](const int index, const int worker) { + try { + body(index, worker); + } catch (...) { + errors[static_cast(index)] = std::current_exception(); + } + }; + detail::run(count, &guarded, [](void* context, const int index, const int worker) { + (*static_cast(context))(index, worker); + }); + for (const auto& error : errors) { + if (error) { + std::rethrow_exception(error); + } + } + } + + /** + * @brief Mesh::GetElementTransformation(element, transformation), safe in concurrent element loops. + * @note MFEM's version gathers curved-mesh nodes through FiniteElementSpace::GetElementVDofs(int, Array&), + * which writes the space's shared mutable DofTransformation. This copy performs the same steps with a + * local DofTransformation, so the transformation and every value derived from it are identical. + */ + void element_transformation(const ::mfem::Mesh& mesh, int element, ::mfem::IsoparametricTransformation& transformation); +} diff --git a/src/include/serif/backend/mfem/shared/reference_tracking.hpp b/src/include/serif/backend/mfem/shared/reference_tracking.hpp new file mode 100644 index 00000000..f12ba656 --- /dev/null +++ b/src/include/serif/backend/mfem/shared/reference_tracking.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +namespace mfem { + class Mesh; + class FiniteElementSpace; + class FiniteElementCollection; +} + +namespace serif::backend::mfem::shared { + /** + * @brief The external mesh metadata consumed by a graph input. + * @note MFEM resources can change outside the computational graph. Their revisions and + * labels must therefore be captured at its boundary; graph invalidation then owns reuse. + * A negative node revision and an empty material list exclude those dependencies. + */ + struct ReferenceMeshSnapshot { + const ::mfem::Mesh* identity{}; + long sequence{-1}; + long nodes_sequence{-1}; + int vertices{}; + int elements{}; + std::vector element_attributes; + std::vector boundary_attributes; + }; + + /** @brief Identity and revision of the space that gives coefficients their physical meaning. */ + struct ReferenceSpaceSnapshot { + const ::mfem::FiniteElementSpace* identity; + const ::mfem::Mesh* mesh; + const ::mfem::FiniteElementCollection* collection; + long sequence; + int size; + int vector_dimension; + int ordering; + + /** @brief Compare coefficient interpretations exactly. */ + bool operator==(const ReferenceSpaceSnapshot&) const = default; + }; + + /** @brief Capture topology and boundary labels, optionally including coordinates and material labels. */ + [[nodiscard]] ReferenceMeshSnapshot mesh_snapshot(const ::mfem::Mesh* mesh, bool coordinates, bool materials); + /** @brief Capture refinement and coefficient layout without copying the finite-element space. */ + [[nodiscard]] ReferenceSpaceSnapshot space_snapshot(const ::mfem::FiniteElementSpace& space); + /** @brief Compare the selected dependencies without allocating another copy of the mesh labels. */ + [[nodiscard]] bool mesh_matches(const ReferenceMeshSnapshot& snapshot, const ::mfem::Mesh* mesh); +} diff --git a/src/include/serif/backend/mfem/solver/admissibility/geometry.hpp b/src/include/serif/backend/mfem/solver/admissibility/geometry.hpp new file mode 100644 index 00000000..089e96eb --- /dev/null +++ b/src/include/serif/backend/mfem/solver/admissibility/geometry.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include + +#include "serif/backend/mfem/geometry/deformation/geometry_step.hpp" +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/backend/mfem/model/graph.hpp" +#include "serif/model/state.hpp" +#include "serif/discretization/blocks/vector_view.hpp" +#include "serif/solver/nonlinear/options.hpp" + +namespace serif::solver::admissibility::backend::mfem { + /** @brief The union of reference rules used by geometry inspection and all volume equations. */ + struct GeometrySamplingRules {}; + + namespace detail { + /** @brief Identify each contribution's reference-basis computation without preparing a physical linearization. */ + template + [[nodiscard]] auto equation_rule_keys() { + return [](std::index_sequence) { + return utils::dependencies::KeyList::template Basis...>{}; + }(std::make_index_sequence>>{}); + } + + /** + * @brief Sample the fixed reference-mesh geometry of one rule once, for reuse by every step limit. + * @note Any failing point leaves the rule unsampled (nullptr), so the step limit samples it on + * demand and reports the failure exactly as it would without this cache. + */ + [[nodiscard]] inline std::shared_ptr> sample_reference_geometry( + ::mfem::Mesh& mesh, const int element, const ::mfem::IntegrationRule& rule) { + ::mfem::ElementTransformation& transformation = *mesh.GetElementTransformation(element); + auto samples = std::make_shared>(); + samples->reserve(static_cast(rule.GetNPoints())); + for (int point = 0; point < rule.GetNPoints(); ++point) { + auto geometry = mapping::sample_fixed_mesh_geometry(transformation, rule.IntPoint(point)); + if (!geometry) { + return nullptr; + } + samples->push_back(std::move(*geometry)); + } + return samples; + } + } + + /** @brief Let the dependency graph retain geometry sampling data for unchanged FE bindings. */ + template + [[nodiscard]] auto make_geometry_computations(const std::shared_ptr& bound) { + using namespace utils::dependencies; + if constexpr (!Bound::Plan::has_shape) { return std::tuple{}; } + else { + // The surface potential has no volume basis batch. The general geometry rules and every active + // interior equation's rules remain in the inspection union. + using Inputs = decltype([](utils::types::TypeList) { + return CombinedKeyList())...>{}; + }(typename Bound::Description::InteriorEquations::Equations{})); + return std::tuple{make_computation([bound](const auto&... basis) { + std::vector rules; + const auto& space = bound->discretization->template binding().space(); + // The reference mesh is fixed for the life of these rules, so its geometry is sampled here once + // rather than in every Newton step limit. Equation rules share the samples their Basis already owns. + for (int element = 0; element < space.GetNE(); ++element) { + const auto* finite_element = space.GetFE(element); + const int order = std::max(finite_element->GetOrder() + 2, 2 * space.GetMesh()->Dimension() * finite_element->GetOrder()); + const auto& rule = ::mfem::IntRules.Get(finite_element->GetGeomType(), order); + rules.push_back({.element = element, .integration_rule = &rule, + .reference_geometry = detail::sample_reference_geometry(*space.GetMesh(), element, rule)}); + } + const auto append = [&](const auto& batch) { + for (const auto& element : batch) { + rules.push_back({.element = element->element, .integration_rule = element->rule, + .reference_geometry = std::shared_ptr>(element, &element->reference_geometry)}); + } + }; + (append(basis), ...); + std::ranges::sort(rules, [](const auto& left, const auto& right) { + if (left.element != right.element) { return left.element < right.element; } + return std::less{}(left.integration_rule, right.integration_rule); + }); + const auto duplicate = std::ranges::unique(rules, [](const auto& left, const auto& right) { + return left.element == right.element && left.integration_rule == right.integration_rule; + }); + rules.erase(duplicate.begin(), duplicate.end()); + return rules; + })}; + } + } + + /** @brief Retain one accepted displacement and direction while backtracking changes the current candidate. */ + class GeometryDirection { + public: + /** + * @brief Reuse the deformation tangent and the union of actual equation sampling rules. + * @note A straight displacement certificate is valid only for a linear shape map. Nonlinear + * maps retain ordinary candidate validation and backtracking, with no incorrect affine bound. + */ + template + [[nodiscard]] auto limit(const Evaluator& evaluator, const model::StellarState& state, std::span direction, + const double maximum_step, const double safety, const bool enabled) + -> std::expected { + geometry::deformation::GeometryStepLimit unrestricted{.step = maximum_step, .boundary_step = maximum_step}; + m_geometry_ready = false; + if constexpr (Evaluator::Plan::has_shape) { + const auto& derivatives = evaluator.cache().template get(); + using Map = std::remove_cvref_tmap)>; + if constexpr (geometry::deformation::is_linear_deformation) { + using Shape = discretization::blocks::surface_deformation::coefficients; + using Displacement = discretization::blocks::displacement::vector; + const auto& displacement = evaluator.binding().discretization->template binding(); + const auto shape_direction = discretization::blocks::StateView(state.layout(), direction).template get(); + ::mfem::Vector shape(const_cast(shape_direction.data()), static_cast(shape_direction.size())); + ::mfem::Vector reduced(displacement.size()); + derivatives.tangent.action()->Mult(shape, reduced); + m_displacement_direction = displacement.scatter_true(reduced); + m_accepted_displacement = displacement.scatter_true(evaluator.cache().template get()); + m_rules = evaluator.cache().template share(); + m_geometry_ready = true; + if (enabled) { + return estimate(evaluator, maximum_step, safety); + } + } + } + return unrestricted; + } + + /** @brief Use a rejected inverted sample to skip unsafe rungs of the existing step ladder. */ + [[nodiscard]] double next(const auto& evaluator, const double step, const Backtracking& options, const bool inverted) const { + if (!inverted || !m_geometry_ready) { + return step * options.contraction; + } + const auto geometry = estimate(evaluator, step, options.fraction_to_boundary); + if (!geometry) { + return step * options.contraction; + } + return next_backtracking_step(step, geometry->minimum_determinant_at_accepted, + geometry->minimum_determinant_at_maximum, true, options); + } + + private: + /** @brief Apply the geometry module to snapshots retained before any trial changes the cache. */ + [[nodiscard]] auto estimate(const auto& evaluator, const double step, const double safety) const { + const auto& discretization = *evaluator.binding().discretization; + return geometry::deformation::backend::mfem::estimate_geometry_step_limit(discretization.mapper, + discretization.template binding().space(), discretization.compactification, + m_accepted_displacement, m_displacement_direction, *m_rules, {.maximum_step = step, .fraction_to_boundary = safety}); + } + + ::mfem::Vector m_accepted_displacement; + ::mfem::Vector m_displacement_direction; + std::shared_ptr> m_rules; + bool m_geometry_ready{false}; + }; +} diff --git a/src/include/serif/backend/mfem/solver/continuation/detail/physical_transfer.hpp b/src/include/serif/backend/mfem/solver/continuation/detail/physical_transfer.hpp new file mode 100644 index 00000000..d06f8fd0 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/continuation/detail/physical_transfer.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include "serif/discretization/blocks/traits.hpp" +#include "serif/backend/mfem/shared/reference_tracking.hpp" +#include "serif/backend/mfem/geometry/deformation/inputs.hpp" + +namespace serif::solver::backend::mfem { + /** @brief Capture the physical interpretation of a reduced coefficient block, not merely its size. */ + template + [[nodiscard]] auto binding_identity(const Binding& binding) { + binding.validate(); + const auto& mesh = binding.mesh(); + const auto common = std::tuple{std::addressof(binding), std::addressof(mesh), mesh.GetSequence(), mesh.GetNodesSequence()}; + if constexpr (discretization::blocks::SpatialTerm) { + const auto& space = binding.space(); + return std::tuple_cat(common, std::tuple{::serif::backend::mfem::shared::space_snapshot(space)}); + } else { + return common; + } + } + + /** @brief Inspect every registered binding, including prescribed fields outside the state vector. */ + template + [[nodiscard]] auto binding_identities(const FiniteElements& finite_elements) { + return [&](utils::dependencies::KeyList) { + return std::tuple{binding_identity(finite_elements.template binding())...}; + }(typename std::remove_cvref_t::Keys{}); + } + + /** @brief Retain the original physical resources and reject reinterpretation between continuation stages. */ + template + [[nodiscard]] auto physical_transfer_guard(const FiniteElements& finite_elements) { + auto common = [owner = finite_elements.owner, bindings = binding_identities(finite_elements), + compactification = &finite_elements.compactification, mapper = &finite_elements.mapper, + compactification_space = finite_elements.compactification.FESpace(), + compactification_values = std::vector(finite_elements.compactification.begin(), finite_elements.compactification.end())] + (const FiniteElements& next) { + return owner.get() == next.owner.get() && compactification == &next.compactification && mapper == &next.mapper && + compactification_space == next.compactification.FESpace() && bindings == binding_identities(next) && + std::ranges::equal(compactification_values, next.compactification); + }; + if constexpr (requires { finite_elements.deformation.reference; }) { + using Tracking = geometry::deformation::backend::mfem::ReferenceDiscretizationTracking; + return [common = std::move(common), reference = Tracking{}.capture(finite_elements.deformation.reference), + deformation = finite_elements.deformation.model](const FiniteElements& next) { + return common(next) && deformation == next.deformation.model && Tracking{}.matches(reference, next.deformation.reference); + }; + } else { + return common; + } + } +} diff --git a/src/include/serif/backend/mfem/solver/equilibrium.hpp b/src/include/serif/backend/mfem/solver/equilibrium.hpp new file mode 100644 index 00000000..98b1295a --- /dev/null +++ b/src/include/serif/backend/mfem/solver/equilibrium.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "serif/backend/mfem/solver/nonlinear/stellar_problem.hpp" +#include "serif/solver/preconditioner/default_preconditioners.hpp" + +namespace serif::solver::backend::mfem { + namespace detail { + /** @brief Add selected immutable solver data to the model's existing dependency graph. */ + [[nodiscard]] auto solver_computations(const auto& strategies) { + return [&strategies](const auto& bound, const auto& layout, auto result_key, const auto& existing) { + auto [evaluation, linear] = strategies.computations(bound, layout, result_key, existing); + return std::pair{std::tuple_cat(std::move(evaluation), + admissibility::backend::mfem::make_geometry_computations(bound)), std::move(linear)}; + }; + } + + /** @brief Select the readable default registry only when the caller has not supplied a recipe. */ + template + [[nodiscard]] auto with_default_preconditioner(model::DiscretizationOptions configuration, + const Layout& layout) { + if constexpr (std::same_as) { + static_assert(preconditioner::PrimalPreconditionerLayout, + "This field layout has no default equilibrium inverse. Supply a preconditioner recipe in the discretization options."); + auto inverse = preconditioner::make_primal_preconditioner(layout); + return model::DiscretizationOptions{ + .model = std::move(configuration.model), .finite_elements = std::move(configuration.finite_elements), + .normalization = std::move(configuration.normalization), .preconditioner = std::move(inverse)}; + } else { + return configuration; + } + } + } + + /** @brief Construct one evaluator/cache and bind fixed or moving geometry for an equilibrium solve. */ + [[nodiscard]] auto make_observed_stellar_problem(auto configuration, const auto& state, const auto& strategies, + operators::backend::mfem::VolumeOptions volume_options, auto prescribed, auto observer_factory, const auto&... geometry) { + auto selected = detail::with_default_preconditioner(std::move(configuration), state.layout()); + auto evaluator = model::backend::mfem::bind_stellar_model(std::move(selected), state, geometry..., + volume_options, prescribed, detail::solver_computations(strategies), std::move(observer_factory)); + return StellarProblem{std::move(evaluator), std::move(prescribed), geometry...}; + } +} diff --git a/src/include/serif/backend/mfem/solver/linear/solve.hpp b/src/include/serif/backend/mfem/solver/linear/solve.hpp new file mode 100644 index 00000000..69f39426 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/linear/solve.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "mfem/linalg/operator.hpp" +#include "mfem/linalg/vector.hpp" +#include "serif/solver/linear/linear.hpp" +#include "serif/solver/preconditioner/contract.hpp" + +namespace serif::solver::linear::backend::mfem { + /** + * @brief Solve a serial linear system and independently verify the resulting correction. + * @param correction On entry, a finite initial guess; on return, the final attempted correction. + * A reused guess is discarded if its true defect exceeds the right-hand-side norm. Convergence + * remains relative to that right-hand side, including when the backend takes zero iterations. + * @note The caller retains the matching operator and inverse owners throughout this call. Numerical + * failure is reported as data; invalid dimensions, stale inverses, and callback errors propagate. + */ + [[nodiscard]] Report solve(FGMRES options, const ::mfem::Operator& operation, ::mfem::Solver& preconditioner, + preconditioner::ApplicationContract contract, const ::mfem::Vector& right_hand_side, + ::mfem::Vector& correction, Control control = {}); + + /** @brief Custom linear solvers implement the same checked-result contract as the built-in solver. */ + template requires (!std::same_as, FGMRES>) + [[nodiscard]] Report solve(const Backend& backend, const ::mfem::Operator& operation, ::mfem::Solver& inverse, + preconditioner::ApplicationContract contract, const ::mfem::Vector& rhs, ::mfem::Vector& correction, Control control) { + return backend.solve(operation, inverse, contract, rhs, correction, control); + } +} diff --git a/src/include/serif/backend/mfem/solver/nonlinear/stellar_problem.hpp b/src/include/serif/backend/mfem/solver/nonlinear/stellar_problem.hpp new file mode 100644 index 00000000..6b49f2dc --- /dev/null +++ b/src/include/serif/backend/mfem/solver/nonlinear/stellar_problem.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include + +#include "serif/backend/mfem/solver/admissibility/geometry.hpp" +#include "serif/backend/mfem/model/equilibrium.hpp" +#include "serif/solver/nonlinear/options.hpp" +#include "serif/backend/mfem/solver/linear/solve.hpp" +#include "serif/types.hpp" + +namespace serif::solver::backend::mfem { + /** + * @brief Adapt existing stellar preparation to Newton without giving acceptance to the model. + * @note The prescribed input context and a copy of fixed geometry are retained for the entire solve. + * Explicitly borrowed resources keep their existing lifetime requirements. The public solver factory + * constructs this object; astronomers normally work with a discretization and a state. + */ + template > + class StellarProblem { + public: + using Vector = serif::Vector; + using System = std::remove_const_t; + using Model = std::remove_cvref_t().model())>; + using Layout = typename Evaluator::Layout; + using Failure = typename Evaluator::Cache::Failure; + + /** @brief Retain one cache and all external inputs throughout a solve. */ + StellarProblem(Evaluator evaluator, Inputs prescribed = Inputs{}, ::mfem::Vector displacement = {}) : + m_evaluator(std::move(evaluator)), m_prescribed(std::move(prescribed)), m_fixed_displacement(std::move(displacement)) {} + + /** @brief Retain the immutable physical definition in the resulting checkpoint. */ + [[nodiscard]] auto model_owner() const { return m_evaluator.binding().model; } + /** @brief Inspect typed FE resources for explicitly selected numerical strategies. */ + [[nodiscard]] const auto& binding() const { return m_evaluator.binding(); } + /** @brief Inspect the coordinate identities used by every physical state in this solve. */ + [[nodiscard]] const Layout& layout() const { return m_evaluator.layout(); } + /** @brief Access the shared physical preparation service for numerical extensions. */ + [[nodiscard]] Evaluator& evaluator() { return m_evaluator; } + + /** @brief Evaluate the complete equations, preserving all previously published systems. */ + [[nodiscard]] auto evaluate(const model::StellarState& state) { + return invoke([&](const auto&... geometry) { return m_evaluator.evaluate(state, geometry..., m_prescribed); }); + } + + /** @brief Prepare the inverse belonging to exactly this physical candidate. */ + [[nodiscard]] auto prepare(const model::StellarState& state) { + return invoke([&](const auto&... geometry) { return m_evaluator.prepare_linear_system(state, geometry..., m_prescribed); }); + } + + /** @brief Solve with the operator and inverse retained by this candidate's preparation. */ + [[nodiscard]] linear::Report solve_linear(const auto& prepared, const Vector& rhs, Vector& correction, const auto& options) { + return linear::backend::mfem::solve(options.linear_solver, prepared->system()->jacobian(), + prepared->preconditioner(), prepared->contract(), rhs, correction, options.method.linear_solve); + } + + /** @brief Preserve the original failure message without classifying by its text. */ + [[nodiscard]] static std::string failure_message(const Failure& failure) { + return std::visit([](const auto& error) -> std::string { + if constexpr (std::same_as, std::string>) { + return error; + } else { + return std::string(error.message); + } + }, failure.error); + } + + /** @brief Geometry preflight already reports an ordinary deformation failure. */ + [[nodiscard]] static std::string failure_message(const geometry::deformation::DeformationFailure& failure) { + return failure.message; + } + + /** @brief Backtracking may repair a numerical candidate, never an incompatible discretization. */ + [[nodiscard]] static bool recoverable(const Failure& failure) { + return std::visit([](const auto& error) { + using Error = std::remove_cvref_t; + if constexpr (std::same_as) { + using Reason = geometry::deformation::DeformationFailureReason; + return error.reason == Reason::non_finite || error.reason == Reason::unresolved_surface || error.reason == Reason::invalid_geometry; + } else if constexpr (std::same_as) { + using Reason = geometry::surface::SurfaceFailureReason; + return error.reason == Reason::non_finite_state || error.reason == Reason::non_finite_eos_response || error.reason == Reason::non_finite_linearization; + } else if constexpr (std::same_as) { + return true; + } else if constexpr (std::same_as) { + return error.reason != operators::backend::mfem::PreparationRejectionReason::MAPPING || + (error.mapping_status != mapping::MappingStatus::INVALID_DIMENSION && + error.mapping_status != mapping::MappingStatus::INVALID_REFERENCE_RADIUS); + } else { + return false; + } + }, failure.error); + } + + /** @brief Snapshot the accepted geometry direction for step restriction and backtracking. */ + [[nodiscard]] auto geometry_limit(const model::StellarState& state, std::span direction, + const double maximum_step, const double safety, const bool enabled) { + return m_geometry.limit(m_evaluator, state, direction, maximum_step, safety, enabled); + } + + /** @brief Skip unsafe backtracking rungs after a structured orientation failure. */ + [[nodiscard]] double next_step(const double step, const Backtracking& options, const auto& evaluation) const { + return m_geometry.next(m_evaluator, step, options, !evaluation && inverted(evaluation.error())); + } + + private: + /** @brief Keep fixed displacement outside the unknown vector and omit it for moving stars. */ + [[nodiscard]] auto invoke(auto callback) { + if constexpr (Evaluator::Plan::has_shape) { + return callback(); + } else { + return callback(m_fixed_displacement); + } + } + + /** @brief Recognize orientation failure from the structured physical rejection. */ + [[nodiscard]] static bool inverted(const Failure& failure) { + if (const auto* rejection = std::get_if(&failure.error)) { + return rejection->reason == operators::backend::mfem::PreparationRejectionReason::MAPPING && + rejection->mapping_status == mapping::MappingStatus::NON_POSITIVE_DETERMINATE; + } + if (const auto* deformation = std::get_if(&failure.error)) { + return deformation->reason == geometry::deformation::DeformationFailureReason::invalid_geometry; + } + return false; + } + + Evaluator m_evaluator; + Inputs m_prescribed; + ::mfem::Vector m_fixed_displacement; + admissibility::backend::mfem::GeometryDirection m_geometry; + }; +} diff --git a/src/include/serif/backend/mfem/solver/normalization/metric_computations.hpp b/src/include/serif/backend/mfem/solver/normalization/metric_computations.hpp new file mode 100644 index 00000000..18ecafb4 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/normalization/metric_computations.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "serif/backend/mfem/discretization/mass_diagonal.hpp" +#include "serif/solver/normalization/keys.hpp" +#include "serif/solver/normalization/metrics.hpp" +#include "serif/utils/dependencies/computation.hpp" +#include "serif/utils/dependencies/resource.hpp" + +namespace serif::solver::normalization::backend::mfem +{ + /** + * @brief Borrow the reference binding with MFEM's mesh and space revision tracking. + * @note Establish MFEM nodal coordinates before tracking; partial assembly would otherwise change the reference revision on first use. + * @note Update geometry through MFEM's mesh API (or call NodesUpdated after editing node coefficients). Reference objects must outlive preparation; trial displacement is deliberately absent. + */ + template + requires discretization::blocks::SpatialTerm + [[nodiscard]] auto reference_metric_binding(BindingT& binding) + { + using namespace utils::dependencies; + + binding.validate(); + binding.mesh().EnsureNodes(); + return borrow>(binding, track_by([](const BindingT& reference) + { + reference.validate(); + const ::mfem::Mesh& mesh = reference.mesh(); + return std::tuple{&reference.space(), mesh.GetSequence(), mesh.GetNodesSequence(), reference.space().GetSequence()}; + })); + } + + /** @brief Assemble one shared reference metric using the existing term support and coefficient ordering. */ + template + [[nodiscard]] auto make_reference_volume_metric_computation() + { + using namespace utils::dependencies; + + return make_computation, InputList>>( + discretization::backend::mfem::reference_mass_diagonal); + } +} diff --git a/src/include/serif/backend/mfem/solver/normalization/operators.hpp b/src/include/serif/backend/mfem/solver/normalization/operators.hpp new file mode 100644 index 00000000..c41e0e1f --- /dev/null +++ b/src/include/serif/backend/mfem/solver/normalization/operators.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include + +#include "mfem/linalg/operator.hpp" +#include "mfem/linalg/vector.hpp" +#include "serif/solver/normalization/diagonal.hpp" + +namespace serif::solver::normalization::backend::mfem +{ + namespace detail + { + /** @brief Translate a prepared coordinate size without narrowing beyond MFEM's index range. */ + [[nodiscard]] int mfem_size(std::size_t size); + + /** @brief Apply a diagonal to MFEM host storage, resizing only after the input has been checked. */ + void scale_vector(std::span factors, const ::mfem::Vector& input, ::mfem::Vector& output, bool inverse); + + /** + * @brief Common algebra for a Jacobian L J S^-1 and an inverse S M^-1 L^-1. + * @note Factor ownership freezes the coordinate system even if a dependency cache is refreshed. + * Recreate the adapter explicitly when adopting refreshed normalization factors. + * Scratch vectors are reused; an instance must not be applied concurrently. + */ + template + class ScaledOperator final : public ::mfem::Operator + { + public: + /** @brief Borrow a physical operator and own its validated coordinate transformation. */ + ScaledOperator(const ::mfem::Operator& physical_operator, DiagonalNormalization normalization); + + /** @brief A temporary operator cannot outlive the borrowed physical action. */ + ScaledOperator(::mfem::Operator&&, DiagonalNormalization) = delete; + + /** @brief A temporary const operator cannot outlive the borrowed physical action. */ + ScaledOperator(const ::mfem::Operator&&, DiagonalNormalization) = delete; + + /** @brief Apply the exact two-sided coordinate transformation. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override; + + /** @brief Apply the transpose with the left and right factors in reversed order. */ + void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override; + + /** @brief Expose the dimensional operator for compatible physical preconditioner binding. */ + [[nodiscard]] const ::mfem::Operator& physical_operator() const noexcept + { + return *m_physical_operator; + } + + /** @brief Inspect the frozen coordinate transformation owned by this operator. */ + [[nodiscard]] const DiagonalNormalization& normalization() const noexcept + { + return m_normalization; + } + + private: + /** @brief Factors belonging to the input physical coordinate. */ + [[nodiscard]] std::span input_factors() const noexcept + { + return Inverse ? m_normalization.residual_factors() : m_normalization.state_factors(); + } + + /** @brief Factors belonging to the output physical coordinate. */ + [[nodiscard]] std::span output_factors() const noexcept + { + return Inverse ? m_normalization.state_factors() : m_normalization.residual_factors(); + } + + const ::mfem::Operator* const m_physical_operator; + const DiagonalNormalization m_normalization; + mutable ::mfem::Vector m_physical_input; + mutable ::mfem::Vector m_physical_output; + }; + + extern template class ScaledOperator; + extern template class ScaledOperator; + } + + using ScaledJacobianOperator = detail::ScaledOperator; ///< Normalized Jacobian L J S^-1. + using ScaledInverseOperator = detail::ScaledOperator; ///< Normalized physical inverse S M^-1 L^-1. + + /** @brief Counts physical bindings and normalized applications without instrumenting the inverse itself. */ + struct ScaledPreconditionerStatistics final + { + std::uint64_t operator_bindings{0}; + std::uint64_t applications{0}; + }; + + /** @brief MFEM solver adapter which always binds the physical inverse to its dimensional Jacobian. */ + class ScaledPreconditioner final : public ::mfem::Solver + { + public: + /** @brief Pair a physical inverse with the exact normalized Jacobian it will precondition. */ + ScaledPreconditioner(::mfem::Solver& physical_inverse, const ScaledJacobianOperator& normalized_jacobian); + + /** @brief The borrowed normalized Jacobian must outlive the preconditioner. */ + ScaledPreconditioner(::mfem::Solver&, ScaledJacobianOperator&&) = delete; + + /** @brief The borrowed const normalized Jacobian must outlive the preconditioner. */ + ScaledPreconditioner(::mfem::Solver&, const ScaledJacobianOperator&&) = delete; + + /** @brief Refuse rebinding to a different preparation and pass the dimensional Jacobian to the inverse. */ + void SetOperator(const ::mfem::Operator& normalized_jacobian) override; + + /** @brief Apply the normalized inverse using the shared coordinate transformation. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override; + + /** @brief Apply the inverse transpose when the physical inverse provides one. */ + void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override; + + /** @brief Inspect physical bindings and normalized inverse applications. */ + [[nodiscard]] const ScaledPreconditionerStatistics& statistics() const noexcept + { + return m_statistics; + } + + private: + /** @brief Let an unconfigured solver establish its dimensions before constructing the coordinate adapter. */ + static ::mfem::Solver& bind_physical_inverse(::mfem::Solver& inverse, const ScaledJacobianOperator& jacobian); + + ::mfem::Solver* m_physical_inverse; + const ScaledJacobianOperator* m_normalized_jacobian; + ScaledInverseOperator m_scaled_inverse; + mutable ScaledPreconditionerStatistics m_statistics; + }; +} diff --git a/src/include/serif/backend/mfem/solver/preconditioner/border.hpp b/src/include/serif/backend/mfem/solver/preconditioner/border.hpp new file mode 100644 index 00000000..bfe71e5b --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/border.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include "serif/backend/mfem/solver/preconditioner/coupling.hpp" +#include "serif/solver/preconditioner/strategies/inverse/dense.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + /** + * @brief Probe and factor the complete small border Schur complement, retaining nonzero border-to-border couplings. + * @note The first group contains fields; the second contains the border unknowns. + * Fixed linear field inverses cache their responses. Variable inverses solve the corrected field right-hand side afresh. + * Empty borders return the field inverse unchanged. Child actions must remain fixed throughout preparation. + */ + [[nodiscard]] std::expected try_bordered_inverse(Inverse fields, TwoBlockCouplings couplings, OperatorHandle border_diagonal, const strategies::inverse::DenseDirect& options = {}); + + /** + * @brief Select equation rows and unknown columns without assuming their coordinate orders coincide. + * @note This owns the operator and copied indices; it borrows no caller index arrays or numerical vectors. + * Applications require preallocated vectors and support overlapping input and output. + */ + [[nodiscard]] std::expected try_restrict_operator(OperatorHandle operation, std::span rows, std::span columns); +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/include/serif/backend/mfem/solver/preconditioner/computations/coupling.hpp b/src/include/serif/backend/mfem/solver/preconditioner/computations/coupling.hpp new file mode 100644 index 00000000..4fdb84f6 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/computations/coupling.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "serif/backend/mfem/operators/linearized_block.hpp" +#include "serif/solver/preconditioner/recipes/coupled.hpp" +#include "serif/solver/preconditioner/recipes/bordered.hpp" +#include "serif/backend/mfem/solver/preconditioner/coupling.hpp" +#include "serif/backend/mfem/solver/preconditioner/border.hpp" +#include "serif/utils/dependencies/computation.hpp" + +namespace serif::solver::preconditioner::computations +{ + /** @brief Couple child results through the authoritative physical cross blocks. */ + template + [[nodiscard]] auto coupling(const CoupledPreconditioner&) + { + using namespace utils::dependencies; + using Recipe = CoupledPreconditioner; + using Inverse = backend::mfem::Inverse; + using Block = operators::backend::mfem::LinearizedBlock; + + static_assert(Policy::order == strategies::coupling::CouplingOrder::second_then_first, "Recipes couple subsystems second-then-first."); + return make_computation>>( + [](const Inverse& first, const Inverse& second, const Block& first_from_second) + { + return backend::mfem::try_coupled_inverse(first, second, {first_from_second.action(), nullptr}, Policy::order); + }); + } + + /** @brief Rebuild the constraint Schur system whenever either its physical couplings or its child inverse change. */ + template + [[nodiscard]] auto border(const BorderedPreconditioner& choice) + { + using namespace utils::dependencies; + using Recipe = BorderedPreconditioner; + using Block = operators::backend::mfem::LinearizedBlock; + + return make_computation, keys::Block, keys::Block>>( + [options = choice.constraint_inverse](const backend::mfem::Inverse& fields, const Block& columns, const Block& rows, const Block& diagonal) + { + return backend::mfem::try_bordered_inverse(fields, {columns.action(), rows.action()}, diagonal.action(), options); + }); + } +} // namespace serif::solver::preconditioner::computations diff --git a/src/include/serif/backend/mfem/solver/preconditioner/computations/inverse.hpp b/src/include/serif/backend/mfem/solver/preconditioner/computations/inverse.hpp new file mode 100644 index 00000000..27cc74d3 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/computations/inverse.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include +#include "serif/utils/error/errors.hpp" +#include "serif/solver/preconditioner/recipes/block.hpp" +#include "serif/backend/mfem/solver/preconditioner/computations/physical_blocks.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse/diagonal.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse/dense.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse/amg.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.hpp" +#include "serif/solver/preconditioner/strategies/inverse/diagonal.hpp" + +namespace serif::solver::preconditioner::computations +{ + /** @brief Identify the inverse strategies whose setup consumes a physical diagonal. */ + template + inline constexpr bool needs_diagonal = std::same_as; + + /** @brief Identify strategies requiring an explicitly assembled selected matrix. */ + template + inline constexpr bool needs_matrix = std::same_as || std::same_as || + std::same_as; + + /** @brief Invert an assembled selected matrix with the chosen dense, sparse direct or AMG backend. */ + template + [[nodiscard]] std::expected invert_matrix(std::shared_ptr matrix, const Strategy& strategy) + { + using namespace backend::mfem; + + if constexpr (std::same_as) + { + ::mfem::DenseMatrix dense; + matrix->ToDenseMatrix(dense); + return try_dense_inverse(dense, strategy); + } + else if constexpr (std::same_as) + { + return try_sparse_direct_inverse(std::move(matrix), strategy); + } + else + { + return try_amg_inverse(std::move(matrix), strategy); + } + } + + /** @brief Declare only the numerical inputs required by this chosen inverse. */ + template + [[nodiscard]] auto inverse(const BlockPreconditioner& choice) + { + using namespace utils::dependencies; + using Recipe = BlockPreconditioner; + using Rows = typename Recipe::Rows; + using Values = typename Recipe::Values; + using Block = operators::backend::mfem::LinearizedBlock; + using Result = backend::mfem::Inverse; + + if constexpr (std::same_as) + { + return make_computation>>( + [](const std::vector& diagonal) + { + return backend::mfem::try_diagonal_inverse(diagonal); + }); + } + else if constexpr (std::same_as) + { + // The computation lives as long as the preconditioner, so Newton steps share one symbolic analysis. + return make_computation>>( + [strategy = choice.inverse, cache = std::make_shared()](const std::shared_ptr& matrix) + { + return backend::mfem::try_sparse_direct_inverse(matrix, strategy, cache.get()); + }); + } + else if constexpr (needs_matrix) + { + return make_computation>>( + [strategy = choice.inverse](const std::shared_ptr& matrix) + { + return invert_matrix(matrix, strategy); + }); + } + else + { + return make_computation>>( + [strategy = choice.inverse](const Block& block) -> std::expected + { + using namespace utils::errors; + + auto result = strategy.try_inverse(block); + if (!result) + { + return result; + } + if (!result->action) + { + return std::unexpected(Failure{SERiFErrorCode::PRECONDITIONER_INVALID_OPERATOR, + format_error_message("A custom inverse returned no action. Return an owned inverse for the selected physical block or a preparation failure.")}); + } + if (result->action->Height() != block.action()->Width() || result->action->Width() != block.action()->Height()) + { + return std::unexpected(Failure{SERiFErrorCode::PRECONDITIONER_SIZE_MISMATCH, + format_error_message("A custom inverse must reverse the dimensions of its selected physical block. Return the inverse in that block's declared equation and unknown coordinates.")}); + } + if (result->contract != ApplicationContract::fixed_linear && result->contract != ApplicationContract::variable) + { + return std::unexpected(Failure{SERiFErrorCode::PRECONDITIONER_INVALID_OPTIONS, + format_error_message("A custom inverse must declare fixed-linear or variable application. Select the contract required by its numerical algorithm.")}); + } + return result; + }); + } + } +} // namespace serif::solver::preconditioner::computations diff --git a/src/include/serif/backend/mfem/solver/preconditioner/computations/ordering.hpp b/src/include/serif/backend/mfem/solver/preconditioner/computations/ordering.hpp new file mode 100644 index 00000000..d03188da --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/computations/ordering.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include "serif/backend/mfem/operators/linearized_block.hpp" +#include "serif/operators/linearized_system_view.hpp" +#include "serif/discretization/blocks/system_layout.hpp" +#include "serif/discretization/blocks/keys.hpp" +#include "serif/backend/mfem/solver/preconditioner/border.hpp" +#include "serif/utils/dependencies/computation.hpp" + +namespace serif::solver::preconditioner::keys +{ + /** @brief Preconditioning consumes the same coordinate layout as every other numerical service. */ + using Layout = discretization::blocks::keys::Layout; + struct Preconditioner + { + }; +} // namespace serif::solver::preconditioner::keys + +namespace serif::solver::preconditioner::computations +{ + namespace detail + { + template + inline constexpr bool is_physical_block = false; + + template + inline constexpr bool is_physical_block> = true; + + template + struct PhysicalSources; + + template + struct PhysicalSources> + { + using Type = utils::dependencies::CombinedKeyList, utils::dependencies::KeyList, utils::dependencies::KeyList<>>...>; + }; + } // namespace detail + + /** @brief Count the declared coordinates of a selected physical block. */ + template + [[nodiscard]] std::size_t selected_size(const Layout& layout, utils::dependencies::KeyList) + { + const std::array sizes{layout.template size()...}; + return discretization::blocks::checked_block_size(sizes); + } + + /** @brief Detect incorrect per-field sizes even when the total packed dimensions happen to agree. */ + template + [[nodiscard]] std::expected validate_physical_layout(const Layout& layout, const operators::backend::mfem::LinearizedBlock& block) + { + using Kind = discretization::blocks::RepresentedValueKind; + + if (selected_size(layout, typename Key::Rows{}) != static_cast(block.action()->Height()) || + selected_size(layout, typename Key::Values{}) != static_cast(block.action()->Width())) + { + return std::unexpected(Failure{utils::errors::SERiFErrorCode::PRECONDITIONER_SIZE_MISMATCH, + "A physical block does not match the sizes of its named equation rows and unknown coordinates. Correct the shared layout or selected block."}); + } + return {}; + } + + /** @brief Map each root coefficient to the recipe's explicit subsystem ordering. */ + template + [[nodiscard]] std::expected, Failure> ordered_indices(const Layout& layout, utils::dependencies::KeyList) + { + const std::size_t count = layout.template total_size(); + if (count > static_cast(std::numeric_limits::max())) + { + return std::unexpected(Failure{utils::errors::SERiFErrorCode::PRECONDITIONER_SIZE_MISMATCH, "Packed system exceeds the MFEM index range."}); + } + std::vector indices(count, -1); + int grouped_offset = 0; + const auto append = [&]() + { + const std::size_t offset = layout.template offset(); + const std::size_t size = layout.template size(); + for (std::size_t index = 0; index < size; ++index) + { + indices[offset + index] = grouped_offset++; + } + }; + (append.template operator()(), ...); + return indices; + } + + /** @brief Bind one inverse to independently ordered state and residual vectors through the common layout. */ + template + [[nodiscard]] auto ordering(utils::dependencies::KeyList) + { + using namespace utils::dependencies; + using Kind = discretization::blocks::RepresentedValueKind; + + static_assert(key_count_v> == key_count_v && + key_count_v == key_count_v, + "The preconditioner must cover every residual identity exactly once."); + static_assert(key_count_v> == key_count_v && + key_count_v == key_count_v, + "The preconditioner must cover every state identity exactly once."); + + return make_computation>( + [](const backend::mfem::Inverse& inverse, const Layout& layout, const auto&... blocks) -> std::expected + { + std::expected validation; + const auto validate = [&](Key, const auto& block) + { + if (validation) + { + validation = validate_physical_layout(layout, block); + } + }; + (validate(PhysicalKeys{}, blocks), ...); + if (!validation) + { + return std::unexpected(validation.error()); + } + auto rows = ordered_indices(layout, typename Recipe::Values{}); + auto columns = ordered_indices(layout, typename Recipe::Rows{}); + if (!rows) + { + return std::unexpected(rows.error()); + } + if (!columns) + { + return std::unexpected(columns.error()); + } + if (inverse.action->Height() != static_cast(rows->size()) || inverse.action->Width() != static_cast(columns->size())) + { + return std::unexpected(Failure{utils::errors::SERiFErrorCode::PRECONDITIONER_SIZE_MISMATCH, "The composed inverse does not match the shared system layout."}); + } + auto ordered = backend::mfem::try_restrict_operator(inverse.action, *rows, *columns); + if (!ordered) + { + return std::unexpected(ordered.error()); + } + return backend::mfem::Inverse{.action = *ordered, .contract = inverse.contract}; + }); + } +} // namespace serif::solver::preconditioner::computations diff --git a/src/include/serif/backend/mfem/solver/preconditioner/computations/physical_blocks.hpp b/src/include/serif/backend/mfem/solver/preconditioner/computations/physical_blocks.hpp new file mode 100644 index 00000000..b5164b7f --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/computations/physical_blocks.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include "serif/backend/mfem/operators/linearized_block.hpp" +#include "serif/solver/preconditioner/failure.hpp" +#include "serif/solver/preconditioner/keys.hpp" +#include "serif/utils/dependencies/computation.hpp" + +namespace serif::solver::preconditioner::computations +{ + /** @brief Make physical diagonal extraction visible as its own graph operation. */ + template + [[nodiscard]] auto physical_diagonal() + { + using namespace utils::dependencies; + + return make_computation, InputList>>( + [](const operators::backend::mfem::LinearizedBlock& block) -> std::expected, Failure> + { + if (!block.has_diagonal() || block.action()->Height() != block.action()->Width()) + { + return std::unexpected(Failure{utils::errors::SERiFErrorCode::PRECONDITIONER_INCOMPATIBLE_STRATEGY, + "This inverse needs a square physical block with an efficient diagonal extractor. Supply the matching field block."}); + } + std::vector diagonal = block.diagonal(); + if (diagonal.size() != static_cast(block.action()->Height())) + { + return std::unexpected(Failure{utils::errors::SERiFErrorCode::PRECONDITIONER_SIZE_MISMATCH, + "The physical diagonal does not match its accepted operator. Correct the selected block extractor."}); + } + return diagonal; + }); + } + + /** @brief Assemble a selected sparse block only for strategies that actually need it. */ + template + [[nodiscard]] auto physical_matrix() + { + using namespace utils::dependencies; + + return make_computation, InputList>>( + [](const operators::backend::mfem::LinearizedBlock& block) -> std::expected, Failure> + { + if (!block.has_assembly()) + { + return std::unexpected(Failure{utils::errors::SERiFErrorCode::PRECONDITIONER_INCOMPATIBLE_STRATEGY, + "This inverse needs a selected sparse matrix. Supply the physical block's assembler or choose a matrix-free inverse."}); + } + auto matrix = block.assemble(); + if (!matrix || matrix->Height() != block.action()->Height() || matrix->Width() != block.action()->Width()) + { + return std::unexpected(Failure{utils::errors::SERiFErrorCode::PRECONDITIONER_SIZE_MISMATCH, + "Selected sparse assembly does not match its accepted action. Correct the block assembler."}); + } + return matrix; + }); + } +} // namespace serif::solver::preconditioner::computations + +namespace serif::solver::preconditioner { + /** @brief Name a single accepted physical coupling as an ordinary dependency source. */ + template + [[nodiscard]] auto physical_block(operators::backend::mfem::LinearizedBlock block) + { + using namespace utils::dependencies; + + return own, KeyList>>(std::move(block)); + } +} diff --git a/src/include/serif/backend/mfem/solver/preconditioner/coupling.hpp b/src/include/serif/backend/mfem/solver/preconditioner/coupling.hpp new file mode 100644 index 00000000..fdf66b07 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/coupling.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "serif/backend/mfem/solver/preconditioner/inverse.hpp" +#include "serif/solver/preconditioner/strategies/coupling/order.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + /** @brief The two Jacobian actions between independently ordered equation and unknown groups. */ + struct TwoBlockCouplings final + { + OperatorHandle first_from_second; + OperatorHandle second_from_first; + }; + + /** + * @brief Compose two inverses with the requested treatment of their cross couplings. + * @note For lower_diagonal_upper the second inverse approximates the Schur complement. + * The returned action owns its prerequisites and reuses scratch; serialize applications to one instance. + * Inputs and outputs must be preallocated; overlapping buffers are supported. + */ + [[nodiscard]] std::expected try_coupled_inverse(Inverse first, Inverse second, TwoBlockCouplings couplings, strategies::coupling::CouplingOrder order); + + /** + * @brief Form the retained Jacobian minus its response through a fixed linear eliminated inverse. + * @note The eliminated group is first and the retained group second: D - C K B. + * This prepares an owning action, without probing or assembling its entries. + */ + [[nodiscard]] std::expected try_schur_operator(OperatorHandle retained_diagonal, Inverse eliminated, TwoBlockCouplings couplings); +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/include/serif/backend/mfem/solver/preconditioner/graph.hpp b/src/include/serif/backend/mfem/solver/preconditioner/graph.hpp new file mode 100644 index 00000000..780c62e7 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/graph.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include "serif/backend/mfem/solver/preconditioner/computations/inverse.hpp" +#include "serif/backend/mfem/solver/preconditioner/computations/coupling.hpp" +#include "serif/backend/mfem/solver/preconditioner/computations/ordering.hpp" +#include "serif/utils/dependencies/computation_graph.hpp" + +namespace serif::solver::preconditioner +{ + namespace detail + { + /** @brief Declare recursive recipe expansion before its composed overloads are defined. */ + template + [[nodiscard]] auto preconditioner_computations(const BlockPreconditioner& choice); + + /** @brief Declare the composed recipe overload for recursive graph expansion. */ + template + [[nodiscard]] auto preconditioner_computations(const CoupledPreconditioner& choice); + + /** @brief Declare the composed recipe overload for recursive graph expansion. */ + template + [[nodiscard]] auto preconditioner_computations(const BorderedPreconditioner& choice); + + /** @brief Assemble exactly the physical representation required by the leaf inverse strategy. */ + template + [[nodiscard]] auto preconditioner_computations(const BlockPreconditioner& choice) + { + using Recipe = BlockPreconditioner; + + if constexpr (computations::needs_diagonal) + { + return std::tuple{computations::physical_diagonal(), computations::inverse(choice)}; + } + else if constexpr (computations::needs_matrix) + { + return std::tuple{computations::physical_matrix(), computations::inverse(choice)}; + } + else + { + return std::tuple{computations::inverse(choice)}; + } + } + + /** @brief The first and second physical branches meet only in their declared coupling computation. */ + template + [[nodiscard]] auto preconditioner_computations(const CoupledPreconditioner& choice) + { + return std::tuple_cat(preconditioner_computations(choice.first), preconditioner_computations(choice.second), std::tuple{computations::coupling(choice)}); + } + + /** @brief The constraint stage depends explicitly on its field inverse, so changing that inverse refreshes Schur setup. */ + template + [[nodiscard]] auto preconditioner_computations(const BorderedPreconditioner& choice) + { + return std::tuple_cat(preconditioner_computations(choice.fields), std::tuple{computations::border(choice)}); + } + } // namespace detail + + /** @brief Declare a recipe's numerical stages for composition into one model dependency graph. */ + template + [[nodiscard]] auto make_preconditioner_computations(const Recipe& choice) + { + return detail::preconditioner_computations(choice); + } + + /** + * @brief Construct the complete preconditioner graph in one place, without preparing any numerical data. + * @note Recipes choose physical branches; computation declarations name all edges. The dependency engine orders execution. + */ + template + [[nodiscard]] auto make_preconditioner_graph(const Recipe& choice) + { + return std::apply([](auto... nodes) + { + return utils::dependencies::make_computation_graph(std::move(nodes)...); + }, make_preconditioner_computations(choice)); + } + + /** @brief Expand a recipe and its coordinate ordering for composition into an existing physical graph. */ + template + [[nodiscard]] auto make_preconditioner_computations(const Recipe& choice, const Layout&, Additional... additional) { + return std::apply([](auto... nodes) { + using Graph = decltype(utils::dependencies::make_computation_graph(std::move(nodes)...)); + using PhysicalKeys = typename computations::detail::PhysicalSources::Type; + return std::tuple{std::move(nodes)..., computations::ordering(PhysicalKeys{})}; + }, std::tuple_cat(std::tuple{std::move(additional)...}, make_preconditioner_computations(choice))); + } + + /** + * @brief Order the inverse in system coordinates, optionally including explicit upstream computations such as reference surface assembly. + * @note Supply keys::Layout in the preparation context. Additional computations must have unique result keys. + */ + template + [[nodiscard]] auto make_preconditioner_graph(const Recipe& choice, const Layout& layout, Additional... additional) { + return std::apply([](auto... nodes) { + return utils::dependencies::make_computation_graph(std::move(nodes)...); + }, make_preconditioner_computations(choice, layout, std::move(additional)...)); + } +} // namespace serif::solver::preconditioner diff --git a/src/include/serif/backend/mfem/solver/preconditioner/inverse.hpp b/src/include/serif/backend/mfem/solver/preconditioner/inverse.hpp new file mode 100644 index 00000000..e469895e --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/inverse.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "mfem/linalg/operator.hpp" +#include "serif/solver/preconditioner/contract.hpp" +#include "serif/solver/preconditioner/failure.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + using OperatorHandle = std::shared_ptr; + + /** @brief An owning approximate inverse and the application guarantee required by its compositions. */ + struct Inverse final + { + OperatorHandle action; + ApplicationContract contract{ApplicationContract::fixed_linear}; + }; +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/include/serif/backend/mfem/solver/preconditioner/inverse/amg.hpp b/src/include/serif/backend/mfem/solver/preconditioner/inverse/amg.hpp new file mode 100644 index 00000000..23b0119c --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/inverse/amg.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include "mfem/linalg/sparsemat.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse.hpp" +#include "serif/solver/preconditioner/strategies/inverse/fixed_amg.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + /** @brief Prepare fixed-cycle BoomerAMG on this process; MPI must already be initialized and remains caller-owned. */ + [[nodiscard]] std::expected try_amg_inverse(std::shared_ptr matrix, const strategies::inverse::FixedAMG& options = {}); +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/include/serif/backend/mfem/solver/preconditioner/inverse/dense.hpp b/src/include/serif/backend/mfem/solver/preconditioner/inverse/dense.hpp new file mode 100644 index 00000000..2019d1f3 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/inverse/dense.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include "mfem/linalg/densemat.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse.hpp" +#include "serif/solver/preconditioner/strategies/inverse/dense.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + /** @brief Copy and scale a dense matrix, check its pivoted LU and infinity-norm reciprocal condition, then own the factorization. */ + [[nodiscard]] std::expected try_dense_inverse(const ::mfem::DenseMatrix& matrix, const strategies::inverse::DenseDirect& options = {}); +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/include/serif/backend/mfem/solver/preconditioner/inverse/diagonal.hpp b/src/include/serif/backend/mfem/solver/preconditioner/inverse/diagonal.hpp new file mode 100644 index 00000000..bcc8bd86 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/inverse/diagonal.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include "serif/backend/mfem/solver/preconditioner/inverse.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + /** @brief Own the reciprocal of every finite nonzero entry, including signed diagonals. */ + [[nodiscard]] std::expected try_diagonal_inverse(std::span diagonal); +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/include/serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.hpp b/src/include/serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.hpp new file mode 100644 index 00000000..82374832 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include "mfem/linalg/sparsemat.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse.hpp" +#include "serif/solver/preconditioner/strategies/inverse/sparse_direct.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + /** @brief The sparse LU backend compiled into this library (Meson option sparse_direct): "accelerate", "umfpack" or "none". */ + [[nodiscard]] std::string_view sparse_direct_backend() noexcept; + + /** @brief True when SparseDirect can be prepared on this machine; with "none", or Accelerate before macOS 15.5, it cannot. */ + [[nodiscard]] bool sparse_direct_available() noexcept; + + namespace detail { class DirectLUPattern; } + + /** + * @brief The symbolic analysis (fill-reducing ordering) of the last matrix factored through this cache. + * @note A preconditioner keeps one per sparse-direct block for its lifetime, so Newton steps whose matrix keeps + * the same sparsity pattern repeat only the numeric factorization (0.03 instead of 0.26 s on the benchmark + * potential block with Accelerate). The reused analysis gives bitwise the same factorization as a fresh one, so + * results do not depend on the cache. Safe to share between threads. + */ + class SparseDirectCache final + { + public: + SparseDirectCache(); + ~SparseDirectCache(); + SparseDirectCache(const SparseDirectCache&) = delete; + SparseDirectCache& operator=(const SparseDirectCache&) = delete; + + /** @brief The cached analysis when it matches this pattern, otherwise a new analysis, which replaces it. */ + [[nodiscard]] std::shared_ptr pattern(int size, const long* column_starts, const int* row_indices, const double* values, int& status); + + private: + struct State; + std::unique_ptr m_state; + }; + + /** + * @brief Factor a finalized square sparse matrix with a pivoted sparse LU; every application is one exact solve. + * @note With a cache, the symbolic analysis is reused while the sparsity pattern is unchanged. ArrayMult solves + * a batch of right-hand sides together, which agrees with Mult to rounding (not bit for bit) on Accelerate. + */ + [[nodiscard]] std::expected try_sparse_direct_inverse(std::shared_ptr matrix, const strategies::inverse::SparseDirect& options = {}, + SparseDirectCache* cache = nullptr); +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/include/serif/backend/mfem/solver/preconditioner/inverse/validation.hpp b/src/include/serif/backend/mfem/solver/preconditioner/inverse/validation.hpp new file mode 100644 index 00000000..24643828 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/inverse/validation.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include "serif/backend/mfem/solver/preconditioner/inverse.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::solver::preconditioner::backend::mfem::detail +{ + /** @brief Keep preparation failures typed while using the common SERiF diagnostic formatter. */ + template + [[nodiscard]] std::unexpected failure(const std::string_view message, Arguments... arguments) + { + return std::unexpected(Failure{.code = Code, .message = utils::errors::format_error_message(message, arguments...)}); + } + + /** @brief Validate a nonempty square inverse input without dereferencing a missing operator. */ + [[nodiscard]] std::expected check_square(const ::mfem::Operator* operation); + + /** @brief Detect exact or partial overlap without comparing unrelated pointers with built-in ordering. */ + [[nodiscard]] bool vectors_overlap(const ::mfem::Vector& first, const ::mfem::Vector& second); + + /** @brief Copy equal-sized host coordinates safely even when their buffers overlap. */ + void copy_coordinates(const ::mfem::Vector& input, ::mfem::Vector& output); + + /** @brief Guard caller-owned vector dimensions before a fixed-size, allocation-stable action. */ + void require_application_size(const ::mfem::Operator& operation, const ::mfem::Vector& input, const ::mfem::Vector& output); +} // namespace serif::solver::preconditioner::backend::mfem::detail diff --git a/src/include/serif/backend/mfem/solver/preconditioner/solver.hpp b/src/include/serif/backend/mfem/solver/preconditioner/solver.hpp new file mode 100644 index 00000000..68c22c86 --- /dev/null +++ b/src/include/serif/backend/mfem/solver/preconditioner/solver.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "serif/backend/mfem/solver/preconditioner/inverse.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + /** @brief Expose one accepted inverse to MFEM while binding it to exactly its accepted physical Jacobian. */ + class Solver final : public ::mfem::Solver + { + public: + /** @brief Retain the inverse and its matching Jacobian; all numerical setup must already have succeeded. */ + Solver(Inverse inverse, OperatorHandle jacobian); + + /** @brief Refuse an unrelated or newer Jacobian instead of silently reusing stale numerical factors. */ + void SetOperator(const ::mfem::Operator& jacobian) override; + + /** @brief Apply from a zero initial guess, independently of the destination's prior contents. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override; + + /** @brief Report whether a fixed outer Krylov method may use this inverse. */ + [[nodiscard]] ApplicationContract contract() const noexcept; + + private: + Inverse m_inverse; + OperatorHandle m_jacobian; + mutable ::mfem::Vector m_input; + mutable ::mfem::Vector m_output; + }; +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/include/serif/backend/mfem/solver/seed/fitted.hpp b/src/include/serif/backend/mfem/solver/seed/fitted.hpp new file mode 100644 index 00000000..ebadc2af --- /dev/null +++ b/src/include/serif/backend/mfem/solver/seed/fitted.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include "serif/backend/mfem/geometry/deformation/computations.hpp" +#include "serif/solver/seed/fitted.hpp" +#include "serif/utils/dependencies/computation_cache.hpp" + +namespace serif::solver::seed::backend::mfem { + namespace detail { + /** @brief Share production surface-coordinate preparation between strict projection and explicit fitting. */ + template + [[nodiscard]] auto radial_seed_surface(const Discretization& discretization) { + namespace deformation = geometry::deformation; + namespace dependencies = utils::dependencies; + using Surface = std::remove_cvref_t; + const std::vector shape; + const auto inputs = deformation::backend::mfem::make_deformation_inputs(discretization.deformation.model, discretization.deformation.reference, shape); + auto cache = dependencies::make_computation_cache(deformation::backend::mfem::make_deformation_graph(), inputs); + using Owner = std::remove_cvref_t())>; + using Result = std::expected; + const auto prepared = cache.try_prepare(inputs, dependencies::KeyList{}); + if (!prepared) { + return Result(std::unexpected(prepared.error().error.message)); + } + return Result(cache.template get()); + } + } + + /** + * @brief Fit only an initial radial guess to the selected spherical reference surface. + * @note Surface radius is the midpoint of its selected FE-coordinate extrema, as in MeanField. + * The original seed, reference mesh, EOS and target constraints are never modified. + */ + template + [[nodiscard]] std::expected fit_radial_seed(const Discretization& discretization, + const RadialSeed& seed, double spherical_relative_tolerance = 5.0e-4) { + if (!std::isfinite(spherical_relative_tolerance) || spherical_relative_tolerance < 0.0) { + return std::unexpected("Radial-guess fitting requires a finite nonnegative spherical tolerance."); + } + const auto surface = detail::radial_seed_surface(discretization); + if (!surface) { + return std::unexpected(surface.error()); + } + double minimum = std::numeric_limits::infinity(); + double maximum = 0.0; + for (const auto& point : (*surface)->reference_positions()) { + const double radius = std::hypot(point[0] - seed.center[0], point[1] - seed.center[1], point[2] - seed.center[2]); + if (!std::isfinite(radius) || radius <= 0.0) { + return std::unexpected("Radial-guess fitting requires finite positive reference-surface radii about the seed center."); + } + minimum = std::min(minimum, radius); + maximum = std::max(maximum, radius); + } + const double radius = minimum + 0.5 * (maximum - minimum); + if (!std::isfinite(radius) || radius <= 0.0 || (maximum - minimum) / radius > spherical_relative_tolerance) { + return std::unexpected("Radial-guess fitting requires a nonempty spherical reference surface within the selected relative tolerance."); + } + return serif::solver::seed::fit_radial_seed(seed, dimensions::LengthValue{radius}); + } +} diff --git a/src/include/serif/backend/mfem/solver/seed/state.hpp b/src/include/serif/backend/mfem/solver/seed/state.hpp new file mode 100644 index 00000000..2620fdfd --- /dev/null +++ b/src/include/serif/backend/mfem/solver/seed/state.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/discretization/dof_selection.hpp" +#include "serif/backend/mfem/model/binding.hpp" +#include "serif/model/state.hpp" +#include "serif/backend/mfem/solver/seed/fitted.hpp" +#include "serif/backend/mfem/mapping/evaluator.hpp" +#include "serif/backend/mfem/projection/radial.hpp" + +namespace serif::solver::seed::backend::mfem { + /** @brief Allow for the reference mesh's finite-order approximation to a spherical seed surface. */ + struct ProjectionOptions final { + double surface_radius_relative_tolerance{5.0e-4}; + }; + + /** + * @brief Project a spherical guess into a moving model's named physical coordinates on its undeformed reference mesh. + * @note Choose a spherical reference mesh close to the seed radius, or explicitly fit the initial guess first. Shape starts at zero; + * the scalar formulation's surface level starts at -GM/R. The selected zero-pressure polytropic field trace is enforced exactly. + * It does not change the EOS, fit the mesh, or certify a discrete equilibrium. + * The vacuum limits assume radial compactification, as used by the stellar Kelvin map. + */ + template + requires (std::same_as || std::same_as) && requires(const Model& model, const Discretization& discretization) { + requires (!std::same_as::ShapeForce, model::NoShapeForce>); + discretization.deformation.model.surface; + discretization.deformation.reference; + } + [[nodiscard]] auto make_polytropic_state(const Model& model, const Discretization& discretization, const Profile& seed, const ProjectionOptions options = {}) { + namespace blocks = serif::discretization::blocks; + namespace dependencies = utils::dependencies; + namespace projection = utils::projection::backend::mfem; + using Description = std::remove_cvref_t; + using Plan = model::EquilibriumPlan>; + using State = model::StellarState; + using Result = std::expected; + + const RadialSeed& source = serif::solver::seed::detail::radial_source(seed); + const dimensions::LengthValue seed_radius = serif::solver::seed::detail::radial_radius(seed); + if (!std::isfinite(options.surface_radius_relative_tolerance) || options.surface_radius_relative_tolerance < 0.0) { + return Result(std::unexpected("The radial seed surface-radius tolerance must be finite and nonnegative.")); + } + if constexpr (std::same_as) { + if (model::resolve_equilibrium_equations(model).surface_condition.target_pressure().value() != 0.0) { + return Result(std::unexpected("A complete Lane-Emden seed requires a zero-pressure isobaric surface.")); + } + } + std::vector shape; + if constexpr (Plan::has_shape) { + const auto prepared = detail::radial_seed_surface(discretization); + if (!prepared) { + return Result(std::unexpected(prepared.error())); + } + const auto& surface = **prepared; + for (const auto& point : surface.reference_positions()) { + const double radius = std::hypot(point[0] - source.center[0], point[1] - source.center[1], point[2] - source.center[2]); + const double scale = std::max(radius, seed_radius.value()); + if (std::abs(radius - seed_radius.value()) > options.surface_radius_relative_tolerance * scale) { + return Result(std::unexpected(std::format( + "The reference surface radius {} differs from the radial seed radius {} (relative error {}, allowed {}). Choose a spherical reference mesh at the seed radius, resolving its geometry sufficiently.", + radius, seed_radius.value(), std::abs(radius - seed_radius.value()) / scale, options.surface_radius_relative_tolerance))); + } + } + shape.resize(surface.input_size(), 0.0); + } + State state(model::backend::mfem::equilibrium_layout(discretization, shape)); + ::mfem::GridFunction displacement(&discretization.template binding().space()); + displacement = 0.0; + mapping::GridFunctionMappingEvaluator mapping(discretization.mapper, displacement, discretization.compactification); + const double gravitational_mass = source.scales.gravitational_constant.value() * serif::solver::seed::detail::radial_mass(seed).value(); + const projection::RadialProjectionOptions projection_options{ + .center = source.center, + .mapping = &mapping, + .scalar_at_infinity = [](const ::mfem::Vector&) -> utils::projection::RadialValue { return 0.0; } + }; + const auto physical = [&](double radius) { + return evaluate_radial_seed(seed, dimensions::LengthValue{radius}); + }; + const auto project = [&](auto member) -> std::expected { + const auto& binding = discretization.template binding(); + const auto profile = [&](double radius) -> utils::projection::RadialValue { + return physical(radius).transform([member](const utils::laneemden::PhysicalSample& sample) { return (sample.*member).value(); }); + }; + auto field = projection::project_scalar(binding.space(), profile, projection_options); + if (!field) { + return std::unexpected(field.error()); + } + const ::mfem::Vector reduced = binding.gather(*field); + std::copy_n(reduced.GetData(), reduced.Size(), state.template get().begin()); + return {}; + }; + std::expected status; + const auto assign = [&](auto member) { + if constexpr (dependencies::ContainsKey) { + if (status) { + status = project.template operator()(member); + } + } + }; + assign.template operator()(&utils::laneemden::PhysicalSample::density); + assign.template operator()(&utils::laneemden::PhysicalSample::potential); + if (!status) { + return Result(std::unexpected(status.error())); + } + if constexpr (std::same_as && + std::same_as) { + using Formulation = typename Plan::Surface::Formulation; + if constexpr (!std::same_as) { + using Carrier = typename Formulation::Test; + if (model::resolve_equilibrium_equations(model).surface_condition.target_pressure().value() == 0.0) { + // Projection roundoff at the analytic surface must not spoil the exact prescribed trace. + const auto selected = serif::discretization::backend::mfem::select_boundary_dofs( + discretization.template binding()); + for (const int dof : selected.reduced_dofs()) { + state.template get()[dof] = 0.0; + } + } + } + } + if constexpr (dependencies::ContainsKey) { + // Use the nonrotating spherical value -GM/R as the initial guess for either surface level or Bernoulli constant. + state.template get()[0] = -gravitational_mass / seed_radius.value(); + } + if constexpr (dependencies::ContainsKey) { + state.template get()[0] = seed.angular_velocity; + } + return Result(std::move(state)); + } +} diff --git a/src/include/serif/backend/mfem/types.hpp b/src/include/serif/backend/mfem/types.hpp new file mode 100644 index 00000000..a32029ad --- /dev/null +++ b/src/include/serif/backend/mfem/types.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "mfem/linalg/vector.hpp" +#include "mfem/linalg/densemat.hpp" +#include "mfem/linalg/sparsemat.hpp" +#include "mfem/linalg/operator.hpp" + +namespace serif::backend::mfem { + using Vector = ::mfem::Vector; ///< Native owned finite-element coefficients. + using DenseMatrix = ::mfem::DenseMatrix; ///< Dense element matrices and small systems. + using SparseMatrix = ::mfem::SparseMatrix; ///< Explicitly assembled sparse matrices. + using LinearOperator = ::mfem::Operator; ///< Assembled or matrix-free linear actions. +} diff --git a/src/lib/serif/backend/mfem/discretization/basis_evaluation.cpp b/src/lib/serif/backend/mfem/discretization/basis_evaluation.cpp new file mode 100644 index 00000000..97133901 --- /dev/null +++ b/src/lib/serif/backend/mfem/discretization/basis_evaluation.cpp @@ -0,0 +1,254 @@ +#include +#include +#include "mfem/fem/fe/fe_base.hpp" +#include "mfem/fem/intrules.hpp" +#include "mfem/linalg/vector.hpp" +#include "mfem/linalg/densemat.hpp" + +#include "serif/backend/mfem/discretization/basis_evaluation.hpp" + +#include "serif/backend/mfem/discretization/tensor_basis.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::discretization::backend::mfem { + void BasisEvaluation::sample( + const ::mfem::FiniteElement& element, + const ::mfem::IntegrationRule& rule, + const ReferenceOperation operation, + const int copies, + TensorBasisCache& cache, + const ContractionMode mode, + const ElementReconstruction* reconstruction + ) { + using namespace utils::errors; + + const bool mapping_supported = element.GetRangeType() == ::mfem::FiniteElement::SCALAR && element.GetMapType() == ::mfem::FiniteElement::VALUE; + + serif_ensure(element.GetDim() == 3 && copies > 0 && mapping_supported, "Canonical basis sampling requires 3D scalar VALUE bases."); + + serif_ensure(reconstruction == nullptr || copies == 1, + "Radial reconstruction currently supports scalar H1 values and gradients only."); + + const int samples = operation == ReferenceOperation::gradient ? 3 : operation == ReferenceOperation::value_and_gradient ? 4 : 1; + m_components = copies * samples; + m_points = rule.GetNPoints(); + m_width = copies * element.GetDof(); + m_tensor = mode == ContractionMode::automatic ? cache.get(element, rule, operation, copies, reconstruction) : nullptr; + m_identity_mapping = static_cast(m_tensor); + + if (m_tensor) { + m_dense.Clear(); + m_mapping.Clear(); + return; + } + + m_mapping.SetSize(0, 0); + m_dense.SetSize(m_points * m_components, m_width); + m_dense = 0.0; + + ::mfem::Vector shape(element.GetDof()); + ::mfem::DenseMatrix gradients(element.GetDof(), 3); + + for (int q = 0; q < m_points; ++q) { + if (reconstruction != nullptr) { + reconstruction->sample(element, rule.IntPoint(q), shape, gradients); + } else { + element.CalcShape(rule.IntPoint(q), shape); + if (samples > 1) { + element.CalcDShape(rule.IntPoint(q), gradients); + } + } + + for (int copy = 0; copy < copies; ++copy) { + for (int sample = 0; sample < samples; ++sample) { + const int derivative = operation == ReferenceOperation::gradient ? sample : sample - 1; + + for (int dof = 0; dof < element.GetDof(); ++dof) { + const double value = samples == 1 || derivative < 0 ? shape[dof] : gradients(dof, derivative); + + m_dense(q * m_components + copy * samples + sample, copy * element.GetDof() + dof) = value; + } + } + } + } + } + + void BasisEvaluation::set_constant(const int points) { + m_tensor.reset(); + m_identity_mapping = false; + m_components = 1; + m_points = points; + m_width = 1; + m_dense.SetSize(points, 1); + m_dense = 1.0; + m_mapping.SetSize(0, 0); + } + + + void BasisEvaluation::reset_from(const BasisEvaluation& reference, const int components) { + m_tensor = reference.m_tensor; + m_identity_mapping = false; + m_points = reference.m_points; + m_width = reference.m_width; + m_components = components; + + if (m_tensor) { + m_mapping.SetSize(m_points * components, m_tensor->components()); + m_dense.SetSize(0, 0); + } else { + m_dense.SetSize(m_points * components, m_width); + m_mapping.SetSize(0, 0); + } + } + + void BasisEvaluation::evaluate(const ::mfem::Vector& coefficients, ::mfem::Vector& values, BasisWorkspace& workspace) const { + using namespace utils::errors; + + serif_ensure( + coefficients.Size() == m_width, + "Basis evaluation coefficient count does not match its element." + ); + if (!m_tensor) { + workspace.reference_values.SetSize(height()); + m_dense.Mult(coefficients, workspace.reference_values); + values = workspace.reference_values; + return; + } + + m_tensor->apply(coefficients, workspace.reference_values, workspace.tensor); + if (m_identity_mapping) { + // Keep the workspace copy: callers may reuse one vector for input and output. + values = workspace.reference_values; + return; + } + + values.SetSize(height()); + const int reference_components = m_tensor->components(); + const int mapping_stride = m_mapping.Height(); + const double* mapping = m_mapping.GetData(); + const double* reference = workspace.reference_values.GetData(); + double* destination = values.GetData(); + + for (int q = 0; q < m_points; ++q) { + const int output_offset = q * m_components; + const int input_offset = q * reference_components; + for (int output = 0; output < m_components; ++output) { + const int row = output_offset + output; + double value = 0.0; + for (int input = 0; input < reference_components; ++input) { + value += mapping[input * mapping_stride + row] * reference[input_offset + input]; + } + destination[row] = value; + } + } + } + + void BasisEvaluation::evaluate_many(const ::mfem::Vector& coefficients, ::mfem::Vector& values, const int lanes, BasisWorkspace& workspace) const { + using namespace utils::errors; + + serif_ensure( + lanes > 0 && coefficients.Size() == m_width * lanes, + "Batched basis evaluation coefficient count does not match its element." + ); + if (!m_tensor) { + // Dense evaluation keeps MFEM's matrix-vector product, one lane at a time. + ::mfem::Vector lane_coefficients(m_width), lane_values(height()); + values.SetSize(height() * lanes); + for (int lane = 0; lane < lanes; ++lane) { + for (int i = 0; i < m_width; ++i) { + lane_coefficients[i] = coefficients[i * lanes + lane]; + } + m_dense.Mult(lane_coefficients, lane_values); + for (int i = 0; i < height(); ++i) { + values[i * lanes + lane] = lane_values[i]; + } + } + return; + } + + m_tensor->apply_many(coefficients, workspace.reference_values, lanes, workspace.tensor); + if (m_identity_mapping) { + values = workspace.reference_values; + return; + } + + values.SetSize(height() * lanes); + const int reference_components = m_tensor->components(); + const int mapping_stride = m_mapping.Height(); + const double* mapping = m_mapping.GetData(); + const double* reference = workspace.reference_values.GetData(); + double* destination = values.GetData(); + std::vector lane_values(static_cast(lanes)); + + for (int q = 0; q < m_points; ++q) { + const int output_offset = q * m_components; + const int input_offset = q * reference_components; + for (int output = 0; output < m_components; ++output) { + const int row = output_offset + output; + std::fill(lane_values.begin(), lane_values.end(), 0.0); + for (int input = 0; input < reference_components; ++input) { + const double weight = mapping[input * mapping_stride + row]; + for (int lane = 0; lane < lanes; ++lane) { + lane_values[static_cast(lane)] += weight * reference[(input_offset + input) * lanes + lane]; + } + } + for (int lane = 0; lane < lanes; ++lane) { + destination[row * lanes + lane] = lane_values[static_cast(lane)]; + } + } + } + } + + void BasisEvaluation::integrate(const ::mfem::Vector& values, ::mfem::Vector& coefficients, BasisWorkspace& workspace) const { + using namespace utils::errors; + + serif_ensure( + values.Size() == height(), + "Basis transpose quadrature size does not match its rule." + ); + if (!m_tensor) { + workspace.reference_values.SetSize(m_width); + m_dense.MultTranspose(values, workspace.reference_values); + coefficients = workspace.reference_values; + return; + } + + if (m_identity_mapping) { + workspace.reference_values = values; + } else { + const int reference_components = m_tensor->components(); + const int mapping_stride = m_mapping.Height(); + const double* mapping = m_mapping.GetData(); + const double* source = values.GetData(); + workspace.reference_values.SetSize(m_points * reference_components); + double* reference = workspace.reference_values.GetData(); + + for (int q = 0; q < m_points; ++q) { + const int output_offset = q * m_components; + const int input_offset = q * reference_components; + for (int input = 0; input < reference_components; ++input) { + const int column = input * mapping_stride; + double value = 0.0; + for (int output = 0; output < m_components; ++output) { + const int row = output_offset + output; + value += mapping[column + row] * source[row]; + } + reference[input_offset + input] = value; + } + } + } + m_tensor->apply(workspace.reference_values, coefficients, workspace.tensor, true); + } + + int BasisEvaluation::height() const noexcept { + return m_points * m_components; + } + + int BasisEvaluation::width() const noexcept { + return m_width; + } + + bool BasisEvaluation::uses_tensor() const noexcept { + return static_cast(m_tensor); + } +} \ No newline at end of file diff --git a/src/lib/serif/backend/mfem/discretization/boundary_trace.cpp b/src/lib/serif/backend/mfem/discretization/boundary_trace.cpp new file mode 100644 index 00000000..18db8813 --- /dev/null +++ b/src/lib/serif/backend/mfem/discretization/boundary_trace.cpp @@ -0,0 +1,170 @@ +#include "serif/backend/mfem/discretization/boundary_trace.hpp" + +#include +#include +#include +#include +#include + +namespace serif::discretization::backend::mfem +{ + namespace + { + using Stencil = std::map; + + /** @brief Check that canonical nodal coordinates represent scalar H1 values. */ + void validate_trace_space(const ::mfem::FiniteElementSpace& space) + { + using namespace utils::errors; + + serif_ensure(space.GetVDim() == 1 && space.FEColl()->GetContType() == ::mfem::FiniteElementCollection::CONTINUOUS, "Boundary interpolation requires a scalar H1 space."); + ::mfem::Array dofs; + for (int element = 0; element < space.GetNE(); ++element) + { + const auto* finite_element = dynamic_cast(space.GetFE(element)); + serif_ensure(finite_element != nullptr && finite_element->GetRangeType() == ::mfem::FiniteElement::SCALAR && finite_element->GetMapType() == ::mfem::FiniteElement::VALUE, "Boundary interpolation requires nodal scalar VALUE elements; Bernstein coefficients are not nodal values."); + const auto* transformation = space.GetElementDofs(element, dofs); + serif_ensure(transformation == nullptr || transformation->IsIdentity(), "Boundary interpolation does not support nonidentity scalar DOF transformations."); + serif_ensure(finite_element->GetNodes().GetNPoints() == dofs.Size(), "Boundary interpolation requires one canonical node per element coefficient."); + } + } + + /** @brief Express a canonical element sample directly in supported input coefficients. */ + [[nodiscard]] Stencil sample_stencil(const DofMap& input, const ::mfem::FiniteElementSpace* space, const int element, const ::mfem::IntegrationPoint& point) + { + using namespace utils::errors; + + if (space == nullptr) + { + return {{0, 1.0}}; + } + + ::mfem::Array dofs; + space->GetElementDofs(element, dofs); + ::mfem::Vector shape(dofs.Size()); + space->GetFE(element)->CalcShape(point, shape); + Stencil stencil; + for (int local = 0; local < dofs.Size(); ++local) + { + const int encoded = dofs[local]; + serif_ensure(std::isfinite(shape[local]), "Boundary interpolation produced a non-finite basis value."); + bool supported = false; + input.for_each_coefficient(::mfem::FiniteElementSpace::DecodeDof(encoded), [&](const int index, const double weight) { + supported = true; + stencil[index] += weight * (encoded < 0 ? -shape[local] : shape[local]); + serif_ensure(std::isfinite(stencil[index]), "Boundary interpolation overflowed while combining repeated input coefficients."); + }); + serif_ensure(supported, "Boundary interpolation encountered an input coefficient outside its declared element support."); + } + return stencil; + } + + /** @brief Shared H1 nodes must produce the same interpolation functional from every supported incident element. */ + [[nodiscard]] bool equivalent_stencils(const Stencil& first, const Stencil& second) + { + std::map difference(first.begin(), first.end()); + long double magnitude = 1.0L; + for (const auto& [column, value] : second) + { + difference[column] -= value; + magnitude += std::abs(static_cast(value)); + } + const long double tolerance = 128.0L * std::numeric_limits::epsilon() * magnitude; + return std::ranges::all_of(difference, [tolerance](const auto& entry) + { + return std::abs(entry.second) <= tolerance; + }); + } + } + + ScalarBoundaryTrace::ScalarBoundaryTrace(std::shared_ptr rows, const DofMap& input, std::shared_ptr interpolation) : m_rows(std::move(rows)), m_binding(input), m_interpolation(std::move(interpolation)) + { + } + + const DofMap& ScalarBoundaryTrace::binding() const noexcept + { + return m_binding; + } + + const DofSelection& ScalarBoundaryTrace::rows() const noexcept + { + return *m_rows; + } + + const ::mfem::SparseMatrix& ScalarBoundaryTrace::interpolation() const + { + validate(); + return *m_interpolation; + } + + void ScalarBoundaryTrace::validate() const + { + m_rows->validate(); + m_binding.validate(); + } + + ScalarBoundaryTrace detail::build_boundary_trace(std::shared_ptr rows, const DofMap& input, const ::mfem::FiniteElementSpace* input_space) + { + using namespace utils::errors; + + serif_ensure(rows != nullptr && rows->size() > 0, "Boundary interpolation requires a nonempty residual-node selection."); + rows->validate(); + input.validate(); + const DofMap& row_binding = rows->binding(); + serif_ensure(&row_binding.mesh() == &input.mesh(), "Boundary interpolation requires input and residual bindings on the same mesh."); + const ::mfem::FiniteElementSpace& row_space = row_binding.space(); + validate_trace_space(row_space); + if (input_space != nullptr) + { + validate_trace_space(*input_space); + } + + std::vector selected(static_cast(row_binding.size()), -1); + std::vector stencils(static_cast(rows->size())); + std::vector sampled(static_cast(rows->size()), false); + for (int row = 0; row < rows->size(); ++row) + { + selected[rows->reduced_dofs()[row]] = row; + } + + ::mfem::Array dofs; + for (int element = 0; element < row_space.GetNE(); ++element) + { + if (!row_binding.supports(element) || !input.supports(element)) + { + continue; + } + row_space.GetElementDofs(element, dofs); + const ::mfem::IntegrationRule& nodes = row_space.GetFE(element)->GetNodes(); + for (int local = 0; local < dofs.Size(); ++local) + { + serif_ensure(dofs[local] >= 0, "Nodal residual coordinates require unsigned scalar H1 DOFs."); + const auto reduced = row_binding.reduced_dof(dofs[local]); + const int row = reduced ? selected[*reduced] : -1; + if (row < 0) + { + continue; + } + Stencil candidate = sample_stencil(input, input_space, element, nodes.IntPoint(local)); + serif_ensure(!sampled[row] || equivalent_stencils(stencils[row], candidate), "Input H1 interpolation is inconsistent between incident elements at selected residual row {}; verify compatible constrained nodal spaces.", row); + if (!sampled[row]) + { + stencils[row] = std::move(candidate); + sampled[row] = true; + } + } + } + + auto interpolation = std::make_shared<::mfem::SparseMatrix>(rows->size(), input.size()); + for (int row = 0; row < rows->size(); ++row) + { + serif_ensure(sampled[row], "Selected residual node {} has no incident element in both the residual and input supports.", row); + for (const auto& [column, weight] : stencils[row]) + { + interpolation->Add(row, column, weight); + } + } + interpolation->Finalize(); + return ScalarBoundaryTrace(std::move(rows), input, std::move(interpolation)); + } +} diff --git a/src/lib/serif/backend/mfem/discretization/element_coordinates.cpp b/src/lib/serif/backend/mfem/discretization/element_coordinates.cpp new file mode 100644 index 00000000..8ef4953a --- /dev/null +++ b/src/lib/serif/backend/mfem/discretization/element_coordinates.cpp @@ -0,0 +1,119 @@ +#include "serif/backend/mfem/discretization/element_coordinates.hpp" + +namespace serif::discretization::backend::mfem { + ElementCoordinates::ElementCoordinates(const DofMap::Elements& elements, const int element) : m_binding(elements.m_binding) { + using namespace utils::errors; + + m_binding.validate_structure(); + m_supported = m_binding.supports(element); + serif_ensure(m_binding.mesh().GetAttribute(element) == m_binding.m_attributes[element], "Term binding domain attributes changed unexpectedly."); + if (m_binding.m_space == nullptr) { + m_dofs.Append(0); + } else { + m_binding.m_space->GetElementVDofs(element, m_dofs, m_transformation); + } + } + + int ElementCoordinates::reduced_dof(const int local) const { + using namespace utils::errors; + + serif_ensure(local >= 0 && local < size(), "Element coordinate index is out of range."); + if (m_binding.m_space == nullptr && !m_supported) { + return -1; + } + const int encoded = m_dofs[local]; + return m_binding.m_full_to_reduced[encoded < 0 ? -1 - encoded : encoded]; + } + + int ElementCoordinates::local_dof(const int reduced) const { + for (int local = 0; local < size(); ++local) { + if (reduced_dof(local) == reduced) { + return local; + } + } + return -1; + } + + void ElementCoordinates::orient_primal(::mfem::Vector& values) const { + if (m_transformation.GetDofTransformation() != nullptr) { + m_transformation.InvTransformPrimal(values); + } + } + + void ElementCoordinates::unit_direction(const int column, ::mfem::Vector& direction) const { + const int reduced = reduced_dof(column); + direction.SetSize(size()); + direction = 0.0; + if (reduced >= 0 || m_binding.constrained()) { + direction[column] = m_dofs[column] < 0 ? -1.0 : 1.0; + } + orient_primal(direction); + } + + void ElementCoordinates::element_values(const ::mfem::Vector& reduced, ::mfem::Vector& values) const { + using namespace utils::errors; + + serif_ensure(reduced.Size() == m_binding.size(), "Element extraction requires a supported coefficient vector."); + values.SetSize(size()); + for (int local = 0; local < size(); ++local) { + if (m_binding.constrained()) { + double value = 0.0; + m_binding.for_each_coefficient(::mfem::FiniteElementSpace::DecodeDof(m_dofs[local]), + [&](const int index, const double weight) { value += weight * reduced[index]; }); + values[local] = m_dofs[local] < 0 ? -value : value; + } else { + const int index = reduced_dof(local); + values[local] = index < 0 ? 0.0 : (m_dofs[local] < 0 ? -reduced[index] : reduced[index]); + } + } + orient_primal(values); + } + + void ElementCoordinates::orient_dual(::mfem::Vector& values) const { + using namespace utils::errors; + + serif_ensure(values.Size() == size(), "Element dual vector does not match the local coordinates."); + if (m_transformation.GetDofTransformation() != nullptr) { + m_transformation.TransformDual(values); + } + for (int local = 0; local < size(); ++local) { + if (m_dofs[local] < 0) { + values[local] = -values[local]; + } + } + } + + void ElementCoordinates::add_element(::mfem::Vector& values, ::mfem::Vector& full) const { + using namespace utils::errors; + + serif_ensure(full.Size() == m_binding.full_size(), "Element assembly vector does not match the full space."); + serif_ensure(values.Size() == size(), "Element assembly vector does not match the local coordinates."); + if (m_transformation.GetDofTransformation() != nullptr) { + m_transformation.TransformDual(values); + } + if (m_binding.m_space == nullptr && !m_supported) { + return; + } + full.AddElementVector(m_dofs, values); + } + + ElementCoordinates DofMap::Elements::coordinates(const int element) const { + return ElementCoordinates(*this, element); + } + + void DofMap::Elements::element_values(const int element, const ::mfem::Vector& reduced, ::mfem::Vector& values) const { + coordinates(element).element_values(reduced, values); + } + + void DofMap::Elements::add_element(const int element, ::mfem::Vector& values, ::mfem::Vector& full) const { + coordinates(element).add_element(values, full); + } + + void DofMap::element_values(const int element, const ::mfem::Vector& reduced, ::mfem::Vector& values) const { + validated_elements().element_values(element, reduced, values); + } + + void DofMap::add_element(const int element, ::mfem::Vector& values, ::mfem::Vector& full) const { + validated_elements().add_element(element, values, full); + } +} // namespace serif::discretization::backend::mfem diff --git a/src/lib/serif/backend/mfem/discretization/mass_diagonal.cpp b/src/lib/serif/backend/mfem/discretization/mass_diagonal.cpp new file mode 100644 index 00000000..8fe95977 --- /dev/null +++ b/src/lib/serif/backend/mfem/discretization/mass_diagonal.cpp @@ -0,0 +1,157 @@ +#include "serif/backend/mfem/discretization/mass_diagonal.hpp" + +#include +#include +#include +#include +#include + +#include "mfem/fem/bilinearform.hpp" +#include "mfem/fem/bilininteg.hpp" +#include "mfem/general/forall.hpp" +#include "serif/utils/error/errors.hpp" +#include "serif/backend/mfem/discretization/element_coordinates.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" + +namespace serif::discretization::backend::mfem { + namespace { + /** + * @brief diag(P^T M P) of a scalar H1 field without assembling M: at each quadrature point, combine the + * constrained shape values of every reduced coefficient, square them and integrate. + * @note Uses MassIntegrator's own rule (plus the reconstruction increment on reconstructed elements, where + * the reconstructed shape replaces the ordinary one), so the integrals equal the assembled diagonal up to + * roundoff. Elements run concurrently; contributions are added in element order. + */ + [[nodiscard]] ::mfem::Vector scalar_mass_diagonal(const DofMap& binding, const ::mfem::Array& marker) { + const auto elements = binding.validated_elements(); + const auto& mesh = binding.mesh(); + const auto* plan = binding.reconstruction().get(); + struct ElementDiagonal { + std::vector indices; + std::vector values; + }; + struct Workspace { + ::mfem::IsoparametricTransformation transformation; + ::mfem::Vector shape, weighted; + ::mfem::DenseMatrix derivatives; + std::vector local_of_reduced; + }; + std::vector results(static_cast(mesh.GetNE())); + std::vector workspaces(static_cast(utils::parallel::worker_count())); + utils::parallel::for_each_index(mesh.GetNE(), [&](const int element, const int worker) { + if (marker[mesh.GetAttribute(element) - 1] == 0) { return; } + Workspace& work = workspaces[static_cast(worker)]; + work.local_of_reduced.resize(static_cast(binding.size()), -1); + const auto& finite_element = *binding.space().GetFE(element); + utils::parallel::element_transformation(mesh, element, work.transformation); + const auto coordinates = elements.coordinates(element); + ElementDiagonal& result = results[static_cast(element)]; + std::vector>> constraint_rows(static_cast(finite_element.GetDof())); + for (int node = 0; node < finite_element.GetDof(); ++node) { + coordinates.for_each_coefficient(node, [&](const int reduced, const double weight) { + int& local = work.local_of_reduced[static_cast(reduced)]; + if (local < 0) { + local = static_cast(result.indices.size()); + result.indices.push_back(reduced); + } + constraint_rows[static_cast(node)].emplace_back(local, weight); + }); + } + for (const int reduced : result.indices) { work.local_of_reduced[static_cast(reduced)] = -1; } + result.values.assign(result.indices.size(), 0.0); + const auto* reconstruction = plan != nullptr ? binding.reconstruction_element(element) : nullptr; + int order = ::mfem::MassIntegrator::GetRule(finite_element, finite_element, work.transformation).GetOrder(); + if (reconstruction != nullptr) { order += plan->integration_order_increment(); } + const auto& rule = ::mfem::IntRules.Get(finite_element.GetGeomType(), order); + std::vector combined(result.indices.size()); + work.shape.SetSize(finite_element.GetDof()); + for (int point = 0; point < rule.GetNPoints(); ++point) { + const auto& location = rule.IntPoint(point); + const ::mfem::Vector* shape = &work.shape; + if (reconstruction != nullptr) { + reconstruction->sample(finite_element, location, work.weighted, work.derivatives); + shape = &work.weighted; + } else { + finite_element.CalcShape(location, work.shape); + } + std::fill(combined.begin(), combined.end(), 0.0); + for (int node = 0; node < finite_element.GetDof(); ++node) { + for (const auto& [local, coefficient] : constraint_rows[static_cast(node)]) { + combined[static_cast(local)] += coefficient * (*shape)[node]; + } + } + work.transformation.SetIntPoint(&location); + const double weight = location.weight * work.transformation.Weight(); + for (std::size_t local = 0; local < combined.size(); ++local) { + result.values[local] += weight * combined[local] * combined[local]; + } + } + }); + ::mfem::Vector diagonal(binding.size()); + diagonal = 0.0; + for (const auto& result : results) { + for (std::size_t k = 0; k < result.indices.size(); ++k) { diagonal[result.indices[k]] += result.values[k]; } + } + return diagonal; + } + } + + std::expected, std::string> detail::reference_mass_diagonal(const DofMap& binding) { + using namespace utils::errors; + + binding.validate(); + if (binding.size() == 0) { + return std::vector{}; + } + + ::mfem::FiniteElementSpace& space = binding.space(); + ::mfem::Array marker(binding.mesh().attributes.Max()); + marker = 0; + for (int element = 0; element < space.GetNE(); ++element) { + if (binding.supports(element)) { + marker[binding.mesh().GetAttribute(element) - 1] = 1; + } + } + + if (space.GetVDim() == 1) { + // The scalar fields (density, potential) need only diag(P^T M P); assembling M and P^T M P to read it + // cost 6 s of a 19 s benchmark solve. + const ::mfem::Vector supported = scalar_mass_diagonal(binding, marker); + std::vector result(static_cast(supported.Size())); + for (int index = 0; index < supported.Size(); ++index) { + if (!std::isfinite(supported[index]) || supported[index] <= 0.0) { + return std::unexpected(format_error_message("Reference mass diagonal entry {} is {}, but every supported coefficient requires positive finite entries. Check the reference mesh geometry and finite-element support.", index, supported[index])); + } + result[static_cast(index)] = supported[index]; + } + return result; + } + + serif_ensure(binding.reconstruction() == nullptr, + "Reconstructed reference metrics currently require a scalar H1 density field."); + // Vector H1 fields assemble their componentwise physical inner product. + ::mfem::BilinearForm mass(&space); + auto integrator = std::make_unique<::mfem::VectorMassIntegrator>(); + integrator->SetVDim(space.GetVDim()); + mass.AddDomainIntegrator(integrator.release(), marker); + mass.Assemble(); + mass.Finalize(); + + if (binding.constrained()) { + const ::mfem::Array essential_dofs; + ::mfem::OperatorHandle system; + mass.FormSystemMatrix(essential_dofs, system); + } + ::mfem::Vector diagonal(binding.constrained() ? space.GetTrueVSize() : space.GetVSize()); + mass.AssembleDiagonal(diagonal); + ::mfem::Vector supported = binding.constrained() ? binding.gather_true(diagonal) : binding.gather(diagonal); + std::vector result(supported.Size()); + for (int index = 0; index < supported.Size(); ++index) { + if (!std::isfinite(supported[index]) || supported[index] <= 0.0) { + return std::unexpected(format_error_message("Reference mass diagonal entry {} is {}, but every supported coefficient requires positive finite entries. Check the reference mesh geometry and finite-element support.", index, supported[index])); + } + result[index] = supported[index]; + } + return result; + } +} diff --git a/src/lib/serif/backend/mfem/discretization/reconstruction.cpp b/src/lib/serif/backend/mfem/discretization/reconstruction.cpp new file mode 100644 index 00000000..37ddaf5e --- /dev/null +++ b/src/lib/serif/backend/mfem/discretization/reconstruction.cpp @@ -0,0 +1,228 @@ +#include "serif/backend/mfem/discretization/reconstruction.hpp" + +#include +#include +#include +#include + +#include "mfem/fem/fe/fe_h1.hpp" +#include "serif/backend/mfem/discretization/boundary_faces.hpp" +#include "serif/backend/mfem/discretization/element_coordinates.hpp" + +namespace serif::discretization::backend::mfem { + namespace { + /** @brief Read one canonical coordinate without relying on IntegrationPoint storage layout. */ + [[nodiscard]] double coordinate(const ::mfem::IntegrationPoint& point, int axis) { + if (axis == 0) { return point.x; } + if (axis == 1) { return point.y; } + return point.z; + } + + /** @brief Convert a canonical point to the element's inward coordinate. */ + [[nodiscard]] double inward(const ElementReconstruction& reconstruction, const ::mfem::IntegrationPoint& point) { + const double value = coordinate(point, reconstruction.inward_axis); + return reconstruction.surface_at_high_coordinate ? 1.0 - value : value; + } + } + + void ElementReconstruction::radial(const ::mfem::FiniteElement& element, double position, + ::mfem::Vector& values, ::mfem::Vector& derivatives) const { + using namespace utils::errors; + const auto* tensor = dynamic_cast(&element); + serif_ensure(profile != nullptr && tensor != nullptr && inward_axis >= 0 && inward_axis < 3, + "Radial reconstruction requires a scalar nodal tensor element and an inward axis in [0,2]."); + const int count = element.GetOrder() + 1; + const auto& ordering = tensor->GetDofMap(); + std::vector nodes(count), radial_values(count), radial_derivatives(count); + for (int index = 0; index < count; ++index) { + const int canonical = surface_at_high_coordinate ? count - 1 - index : index; + const int native = ordering.Size() == 0 ? canonical : ordering[canonical]; + const double node = element.GetNodes().IntPoint(native).x; + nodes[index] = surface_at_high_coordinate ? 1.0 - node : node; + } + const double position_inward = surface_at_high_coordinate ? 1.0 - position : position; + serif_ensure(position_inward >= -5e-13 && position_inward <= 1.0 + 5e-13, + "Reconstructed field query lies outside its reference element (inward coordinate {}).", position_inward); + // Curved inverse maps can place an accepted surface point a few ulps outside the cube. + profile->evaluate(nodes, std::clamp(position_inward, 0.0, 1.0), radial_values, radial_derivatives); + values.SetSize(count); + derivatives.SetSize(count); + for (int index = 0; index < count; ++index) { + const int radial_index = surface_at_high_coordinate ? count - 1 - index : index; + values[index] = radial_values[radial_index]; + derivatives[index] = surface_at_high_coordinate ? -radial_derivatives[radial_index] : radial_derivatives[radial_index]; + } + } + + void ElementReconstruction::sample(const ::mfem::FiniteElement& element, const ::mfem::IntegrationPoint& point, + ::mfem::Vector& values, ::mfem::DenseMatrix& derivatives) const { + using namespace utils::errors; + const auto* tensor = dynamic_cast(&element); + serif_ensure(tensor != nullptr && element.GetGeomType() == ::mfem::Geometry::CUBE, + "Reconstructed field evaluation requires a nodal hexahedron."); + const int count = element.GetOrder() + 1; + std::array<::mfem::Vector, 3> factors, slopes; + for (int axis = 0; axis < 3; ++axis) { + factors[axis].SetSize(count); + slopes[axis].SetSize(count); + if (axis == inward_axis) { + radial(element, coordinate(point, axis), factors[axis], slopes[axis]); + } else { + tensor->GetBasis1D().Eval(coordinate(point, axis), factors[axis], slopes[axis]); + } + } + values.SetSize(element.GetDof()); + derivatives.SetSize(element.GetDof(), 3); + const auto& ordering = tensor->GetDofMap(); + for (int z = 0; z < count; ++z) { + for (int y = 0; y < count; ++y) { + for (int x = 0; x < count; ++x) { + const int lexicographic = x + count * (y + count * z); + const int native = ordering.Size() == 0 ? lexicographic : ordering[lexicographic]; + values[native] = factors[0][x] * factors[1][y] * factors[2][z]; + derivatives(native, 0) = slopes[0][x] * factors[1][y] * factors[2][z]; + derivatives(native, 1) = factors[0][x] * slopes[1][y] * factors[2][z]; + derivatives(native, 2) = factors[0][x] * factors[1][y] * slopes[2][z]; + } + } + } + } + + ReconstructionPlan::ReconstructionPlan(const DofMap& binding, int boundary_attribute, + std::shared_ptr profile) + : m_space(&binding.space()), m_space_sequence(binding.space().GetSequence()), m_mesh_sequence(binding.mesh().GetSequence()), m_boundary_attribute(boundary_attribute), m_profile(std::move(profile)), m_elements(binding.mesh().GetNE()) { + using namespace utils::errors; + serif_ensure(m_profile != nullptr && binding.space().GetVDim() == 1, + "A radial reconstruction requires a profile and a scalar field space."); + auto& mesh = binding.mesh(); + for (int element = 0; element < mesh.GetNE(); ++element) { + m_domain_attributes.push_back(mesh.GetAttribute(element)); + m_element_support.push_back(binding.supports(element)); + } + for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { m_boundary_attributes.push_back(mesh.GetBdrAttribute(boundary)); } + const auto& corners = *::mfem::Geometries.GetVertices(::mfem::Geometry::SQUARE); + int selected = 0; + for (const int face : boundary_leaf_faces(mesh, boundary_attribute)) { + auto* transformations = mesh.GetFaceElementTransformations(face); + serif_ensure(transformations != nullptr, + "Cannot inspect face {} while selecting reconstructed surface elements.", face); + const bool first = transformations->Elem1No >= 0 && binding.supports(transformations->Elem1No); + const bool second = transformations->Elem2No >= 0 && binding.supports(transformations->Elem2No); + serif_ensure(first != second, + "Reconstruction boundary {} must have exactly one supported material side; face {} does not. Select the stellar material/vacuum interface.", boundary_attribute, face); + const int element_index = first ? transformations->Elem1No : transformations->Elem2No; + const auto* finite_element = binding.space().GetFE(element_index); + serif_ensure(dynamic_cast(finite_element) != nullptr, + "Surface reconstruction currently requires standard H1 hexahedra; element {} is incompatible.", element_index); + auto& local = first ? transformations->Loc1 : transformations->Loc2; + int normal_axis = -1; + bool high = false; + for (int axis = 0; axis < 3; ++axis) { + double minimum = 1.0, maximum = 0.0; + for (int corner = 0; corner < corners.GetNPoints(); ++corner) { + ::mfem::IntegrationPoint point; + local.Transform(corners.IntPoint(corner), point); + minimum = std::min(minimum, coordinate(point, axis)); + maximum = std::max(maximum, coordinate(point, axis)); + } + if (maximum < 1e-12 || minimum > 1.0 - 1e-12) { + normal_axis = axis; + high = minimum > 0.5; + } + } + serif_ensure(normal_axis >= 0, + "Surface face {} is not a canonical outer face of element {}; reconstruction needs an aligned surface layer.", face, element_index); + ElementReconstruction description{.profile = m_profile.get(), .inward_axis = normal_axis, .surface_at_high_coordinate = high}; + serif_ensure(m_elements[element_index].profile == nullptr || m_elements[element_index] == description, + "Element {} touches the selected surface in different inward directions. Refine or change the surface layer before using radial reconstruction.", element_index); + if (m_elements[element_index].profile == nullptr) { ++selected; } + m_elements[element_index] = description; + } + serif_ensure(selected > 0, + "Reconstruction boundary attribute {} selects no material elements. Check the boundary type and mesh labels.", boundary_attribute); + validate_interfaces(binding); + } + + void ReconstructionPlan::validate_binding(const DofMap& binding) const { + using namespace utils::errors; + const auto& space = binding.space(); + serif_ensure(&space == m_space, + "Rebuild the reconstruction plan for this field's finite-element space; a selection cannot be reused on another mesh or space."); + const auto& mesh = *space.GetMesh(); + serif_ensure(space.GetSequence() == m_space_sequence && mesh.GetSequence() == m_mesh_sequence && + mesh.GetNE() == static_cast(m_domain_attributes.size()) && mesh.GetNBE() == static_cast(m_boundary_attributes.size()), + "Rebuild the reconstruction plan after changing the mesh or finite-element space."); + for (int element = 0; element < mesh.GetNE(); ++element) { + serif_ensure(mesh.GetAttribute(element) == m_domain_attributes[element] && binding.supports(element) == m_element_support[element], + "Rebuild the reconstruction plan after changing material domain labels or field support."); + } + for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { + serif_ensure(mesh.GetBdrAttribute(boundary) == m_boundary_attributes[boundary], + "Rebuild the reconstruction plan after changing surface boundary labels."); + } + } + + const ElementReconstruction* ReconstructionPlan::element(int index) const { + using namespace utils::errors; + serif_ensure(index >= 0 && index < static_cast(m_elements.size()), + "Reconstruction element {} lies outside the prepared mesh of {} elements.", index, m_elements.size()); + if (m_elements[index].profile == nullptr) { return nullptr; } + return &m_elements[index]; + } + + void ReconstructionPlan::validate_interfaces(const DofMap& binding) const { + using namespace utils::errors; + auto& mesh = binding.mesh(); + const auto& corners = *::mfem::Geometries.GetVertices(::mfem::Geometry::SQUARE); + const auto elements = binding.validated_elements(); + for (int face = 0; face < mesh.GetNumFaces(); ++face) { + if (mesh.GetFaceInformation(face).tag == ::mfem::Mesh::FaceInfoTag::MasterNonconforming) { continue; } + auto* transformations = mesh.GetFaceElementTransformations(face); + if (transformations == nullptr || transformations->Elem1No < 0 || transformations->Elem2No < 0) { continue; } + const int left = transformations->Elem1No, right = transformations->Elem2No; + if (!binding.supports(left) || !binding.supports(right)) { continue; } + const auto* left_profile = element(left); + const auto* right_profile = element(right); + if (left_profile == nullptr && right_profile == nullptr) { continue; } + // Equal inward coordinates on a lateral face give the same weighted trace space. + // The only supported transition to an ordinary cell is the inner face, where t=1. + for (int corner = 0; corner < corners.GetNPoints(); ++corner) { + ::mfem::IntegrationPoint left_point, right_point; + transformations->Loc1.Transform(corners.IntPoint(corner), left_point); + transformations->Loc2.Transform(corners.IntPoint(corner), right_point); + const double left_depth = left_profile == nullptr ? 1.0 : inward(*left_profile, left_point); + const double right_depth = right_profile == nullptr ? 1.0 : inward(*right_profile, right_point); + serif_ensure(std::abs(left_depth - right_depth) < 2e-12, + "Reconstruction is incompatible across face {} between elements {} and {}: inward coordinates {} and {} disagree. Use matching radial layers; independent radial hanging interfaces are not supported.", + face, left, right, left_depth, right_depth); + } + const auto& left_element = *binding.space().GetFE(left); + const auto& right_element = *binding.space().GetFE(right); + const auto left_coordinates = elements.coordinates(left); + const auto right_coordinates = elements.coordinates(right); + const auto& rule = ::mfem::IntRules.Get(::mfem::Geometry::SQUARE, 2 * std::max(left_element.GetOrder(), right_element.GetOrder()) + 4); + ::mfem::Vector left_values, right_values; + ::mfem::DenseMatrix derivatives; + for (int sample = 0; sample < rule.GetNPoints(); ++sample) { + ::mfem::IntegrationPoint left_point, right_point; + transformations->Loc1.Transform(rule.IntPoint(sample), left_point); + transformations->Loc2.Transform(rule.IntPoint(sample), right_point); + if (left_profile != nullptr) { left_profile->sample(left_element, left_point, left_values, derivatives); } + else { left_values.SetSize(left_element.GetDof()); left_element.CalcShape(left_point, left_values); } + if (right_profile != nullptr) { right_profile->sample(right_element, right_point, right_values, derivatives); } + else { right_values.SetSize(right_element.GetDof()); right_element.CalcShape(right_point, right_values); } + std::map difference; + for (int local = 0; local < left_values.Size(); ++local) { + left_coordinates.for_each_coefficient(local, [&](int coefficient, double weight) { difference[coefficient] += weight * left_values[local]; }); + } + for (int local = 0; local < right_values.Size(); ++local) { + right_coordinates.for_each_coefficient(local, [&](int coefficient, double weight) { difference[coefficient] -= weight * right_values[local]; }); + } + for (const auto& [coefficient, mismatch] : difference) { + serif_ensure(std::abs(mismatch) < 2e-11, + "Hanging-node constraints do not preserve reconstructed density across face {} (coefficient {}, mismatch {}). Use matching radial layers or a compatible refinement pattern.", face, coefficient, mismatch); + } + } + } + } +} diff --git a/src/lib/serif/backend/mfem/discretization/topology.cpp b/src/lib/serif/backend/mfem/discretization/topology.cpp new file mode 100644 index 00000000..e766f171 --- /dev/null +++ b/src/lib/serif/backend/mfem/discretization/topology.cpp @@ -0,0 +1,34 @@ +#include "serif/backend/mfem/discretization/topology.hpp" + +#include +#include +#include "mfem/mesh/mesh.hpp" + +namespace serif::backend::mfem::discretization { + ::serif::discretization::domain::mesh::MeshTopology make_mesh_topology(const ::mfem::Mesh& mesh) { + using namespace ::serif::discretization::domain::mesh; + + MeshTopologyData data; + data.elementDomainIDs.resize(mesh.GetNE()); + std::vector active_faces(mesh.GetNumFaces(), -1); + data.boundaryElementBoundaryIDs.resize(mesh.GetNBE()); + data.boundaryElementFaceIDs.resize(mesh.GetNBE()); + for (int elementID = 0; elementID < mesh.GetNE(); ++elementID) { + data.elementDomainIDs[elementID] = mesh.GetAttribute(elementID); + } + for (int faceID = 0; faceID < mesh.GetNumFaces(); ++faceID) { + if (mesh.Nonconforming() && mesh.GetFaceInformation(faceID).tag == ::mfem::Mesh::FaceInfoTag::MasterNonconforming) { + // Fine faces describe the actual adjacency. The coarse bookkeeping face is not exterior. + continue; + } + active_faces[faceID] = static_cast(data.faceElements.size()); + data.faceElements.emplace_back(); + mesh.GetFaceElements(faceID, &data.faceElements.back().firstElementID, &data.faceElements.back().secondElementID); + } + for (int boundaryElementID = 0; boundaryElementID < mesh.GetNBE(); ++boundaryElementID) { + data.boundaryElementBoundaryIDs[boundaryElementID] = mesh.GetBdrAttribute(boundaryElementID); + data.boundaryElementFaceIDs[boundaryElementID] = active_faces[mesh.GetBdrElementFaceIndex(boundaryElementID)]; + } + return MeshTopology(std::move(data)); + } +} diff --git a/src/lib/serif/backend/mfem/geometry/deformation/geometry_step.cpp b/src/lib/serif/backend/mfem/geometry/deformation/geometry_step.cpp new file mode 100644 index 00000000..4b90cac4 --- /dev/null +++ b/src/lib/serif/backend/mfem/geometry/deformation/geometry_step.cpp @@ -0,0 +1,276 @@ +#include "serif/backend/mfem/geometry/deformation/geometry_step.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "mfem/fem/gridfunc.hpp" +#include "mfem/fem/pfespace.hpp" +#include "serif/geometry/deformation/detail/determinant_polynomial.hpp" +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/backend/mfem/shared/finite.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" + +namespace serif::geometry::deformation::backend::mfem { + namespace { + /** @brief Reject incompatible fields before invoking the mapper's programmer-contract checks. */ + [[nodiscard]] DeformationStatus validate_inputs(const mapping::DomainMapper& mapper, const ::mfem::FiniteElementSpace& displacement_space, const ::mfem::GridFunction& compactification_coordinate, const ::mfem::Vector& accepted, const ::mfem::Vector& direction, const std::span rules, const GeometryStepOptions& options) { + if (!std::isfinite(options.maximum_step) || options.maximum_step <= 0.0 || !std::isfinite(options.determinant_floor) || options.determinant_floor < 0.0 || !std::isfinite(options.fraction_to_boundary) || options.fraction_to_boundary <= 0.0 || options.fraction_to_boundary >= 1.0) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_configuration, .message = "Geometry step controls require a finite positive maximum step, nonnegative determinant floor, and fraction to boundary strictly between zero and one."}); + } + + const ::mfem::Mesh* mesh = displacement_space.GetMesh(); + const ::mfem::FiniteElementSpace* compactification_space = compactification_coordinate.FESpace(); + const int dimension = mapper.get_dimension(); + const ::mfem::Ordering::Type ordering = displacement_space.GetOrdering(); + if (mesh == nullptr || compactification_space == nullptr || compactification_space->GetMesh() != mesh || dimension < 1 || dimension > 3 || mesh->Dimension() != dimension || mesh->SpaceDimension() != dimension || displacement_space.GetVDim() != dimension || compactification_space->GetVDim() != 1 || compactification_coordinate.Size() != compactification_space->GetVSize() || (ordering != ::mfem::Ordering::byNODES && ordering != ::mfem::Ordering::byVDIM)) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Geometry step estimation requires compatible scalar compactification and vector displacement spaces on one mesh in one to three dimensions."}); + } +#ifdef MFEM_USE_MPI + if (dynamic_cast(&displacement_space) != nullptr || dynamic_cast(compactification_space) != nullptr) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Geometry step estimation currently requires serial finite-element spaces."}); + } +#endif + if (accepted.Size() != displacement_space.GetTrueVSize() || direction.Size() != displacement_space.GetTrueVSize()) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_size, .message = "Geometry step displacement vectors must match the space's true-DOF count."}); + } + if (!utils::checks::is_finite(accepted) || !utils::checks::is_finite(direction) || !utils::checks::is_finite(compactification_coordinate)) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::non_finite, .message = "Geometry step displacement and compactification coefficients must all be finite."}); + } + if (rules.empty() || rules.size() > static_cast(std::numeric_limits::max())) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_configuration, .message = "Geometry step estimation requires a nonempty list of sampling rules representable by integer rule indices."}); + } + return {}; + } + + /** @brief Check each actual sampling rule instead of assuming that a higher quadrature order covers it. */ + [[nodiscard]] DeformationStatus validate_rule(const mapping::DomainMapper& mapper, const ::mfem::FiniteElementSpace& displacement_space, const ::mfem::FiniteElementSpace& compactification_space, const GeometrySampleRule& rule) { + ::mfem::Mesh& mesh = *displacement_space.GetMesh(); + if (rule.element < 0 || rule.element >= mesh.GetNE() || rule.integration_rule == nullptr || rule.integration_rule->GetNPoints() <= 0) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_configuration, .message = "A geometry sample rule must name an existing element and contain at least one point.", .element = rule.element}); + } + + const ::mfem::ElementTransformation* transformation = mesh.GetElementTransformation(rule.element); + const ::mfem::FiniteElement* displacement_element = displacement_space.GetFE(rule.element); + const ::mfem::FiniteElement* compactification_element = compactification_space.GetFE(rule.element); + const auto supports_mapping = [](const ::mfem::FiniteElement* element) { + return element != nullptr && element->GetDof() > 0 && element->GetRangeType() == ::mfem::FiniteElement::SCALAR && element->GetMapType() == ::mfem::FiniteElement::VALUE && element->GetDerivType() == ::mfem::FiniteElement::GRAD; + }; + if (transformation == nullptr || !supports_mapping(displacement_element) || !supports_mapping(compactification_element) || transformation->GetSpaceDim() != mapper.get_dimension() || displacement_element->GetDim() != mapper.get_dimension() || compactification_element->GetDim() != mapper.get_dimension() || displacement_element->GetGeomType() != compactification_element->GetGeomType()) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Geometry samples require scalar value-mapped finite elements with gradients and compatible dimensions.", .element = rule.element}); + } + if (mapper.is_compactified_element(*transformation) && !mapper.get_exterior_map().is_affine_in_displacement()) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_configuration, .message = "The determinant polynomial requires an exterior map affine in displacement at fixed compactification coordinates.", .element = rule.element}); + } + return {}; + } + + /** @brief Expand serial constrained true DOFs before extracting element coefficients. */ + [[nodiscard]] ::mfem::Vector local_displacement(const ::mfem::FiniteElementSpace& space, const ::mfem::Vector& true_values) { + ::mfem::Vector local(space.GetVSize()); + const ::mfem::Operator* prolongation = space.GetProlongationMatrix(); + if (prolongation != nullptr) { + prolongation->Mult(true_values, local); + } else { + local = true_values; + } + return local; + } + + /** @brief Keep MFEM storage conventions outside the small determinant-polynomial kernel. */ + [[nodiscard]] detail::JacobianEntries jacobian_entries(const ::mfem::DenseMatrix& matrix) { + detail::JacobianEntries entries{}; + for (int row = 0; row < matrix.Height(); ++row) { + for (int column = 0; column < matrix.Width(); ++column) { + entries[row][column] = matrix(row, column); + } + } + return entries; + } + + /** @brief Build one exact displacement-path polynomial using the existing mapping and variation. */ + [[nodiscard]] std::expected sample_polynomial(const mapping::DomainMapper& mapper, const mapping::ElementMappingData& element_data, const mapping::ElementDisplacementData& direction_data, ::mfem::ElementTransformation& transformation, const ::mfem::IntegrationPoint& point, const mapping::DMFixedMeshGeometry* stored_geometry, mapping::DMWorkspace& workspace, mapping::MappingPointContext& context, mapping::MappingPointVariation& variation, const GeometryStepOptions& options) { + if (!std::isfinite(point.x) || (mapper.get_dimension() > 1 && !std::isfinite(point.y)) || (mapper.get_dimension() > 2 && !std::isfinite(point.z))) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::non_finite, .message = "Geometry sample coordinates must be finite."}); + } + // Both maps use this exact point and reference mesh. Keep the checked sample local so + // resetting MFEM's integration point cannot force repeated fixed-Jacobian evaluation. + // A stored sample is the same sample_fixed_mesh_geometry result computed once for the unchanged + // reference mesh; evaluating the high-order mesh Jacobian here dominated every Newton preflight. + std::expected sampled_geometry = std::unexpected(mapping::MappingStatus::VALID); + if (stored_geometry == nullptr) { + sampled_geometry = mapping::sample_fixed_mesh_geometry(transformation, point); + } + const mapping::DMFixedMeshGeometry* fixed_geometry = stored_geometry != nullptr ? stored_geometry : (sampled_geometry ? &*sampled_geometry : nullptr); + if (fixed_geometry == nullptr || mapper.evaluate_point(element_data, transformation, point, workspace, context, *fixed_geometry) != mapping::MappingStatus::VALID || !std::isfinite(context.mapping_determinate) || context.mapping_determinate <= options.determinant_floor) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_geometry, .message = "The accepted displacement must have a valid mapping strictly above the determinant floor at every sample."}); + } + if (mapper.evaluate_point_variation(element_data, direction_data, transformation, point, context, workspace, variation, fixed_geometry->inverse_jacobian) != mapping::MappingStatus::VALID || !utils::checks::is_finite(context.mapping_jacobian) || !utils::checks::is_finite(variation.mapping_jacobian_variation)) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_geometry, .message = "The mapping variation must be finite at every geometry sample."}); + } + + detail::DeterminantPolynomial coefficients = detail::determinant_polynomial(jacobian_entries(context.mapping_jacobian), jacobian_entries(variation.mapping_jacobian_variation), mapper.get_dimension(), options.maximum_step, options.determinant_floor); + // Preserve the mapper's determinant rather than introducing a second round-off path at the accepted state. + coefficients[0] = context.mapping_determinate - options.determinant_floor; + for (const double coefficient : coefficients) { + if (!std::isfinite(coefficient)) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::non_finite, .message = "The displacement path produced a non-finite determinant polynomial."}); + } + } + if (!std::isfinite(detail::evaluate_polynomial(coefficients, 1.0) + options.determinant_floor)) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::non_finite, .message = "The determinant at the maximum displacement step is not finite."}); + } + return coefficients; + } + + /** @brief Retain the earliest boundary together with diagnostics for every sampled polynomial. */ + void accumulate_sample(GeometryStepLimit& result, detail::DeterminantPolynomial& limiting_coefficients, const detail::DeterminantPolynomial& coefficients, const GeometryStepOptions& options, const int element, const int rule, const int point) { + ++result.sampled_point_count; + result.minimum_determinant_at_accepted = std::min(result.minimum_determinant_at_accepted, coefficients[0] + options.determinant_floor); + result.minimum_determinant_at_maximum = std::min(result.minimum_determinant_at_maximum, detail::evaluate_polynomial(coefficients, 1.0) + options.determinant_floor); + + const double boundary_parameter = detail::first_boundary_parameter(coefficients); + const double boundary_step = options.maximum_step * boundary_parameter; + if (std::isfinite(boundary_step) && (!result.limited_by_geometry || boundary_step < result.boundary_step)) { + result.limited_by_geometry = true; + result.boundary_step = boundary_step; + result.limiting_element = element; + result.limiting_rule = rule; + result.limiting_point = point; + limiting_coefficients = coefficients; + } + } + + /** @brief One rule's samples, accumulated in point order exactly as the serial sweep would. */ + struct RuleSamples { + GeometryStepLimit limit{}; + detail::DeterminantPolynomial limiting_coefficients{}; + std::optional failure; + std::exception_ptr error; + }; + + /** + * @brief Fold one rule into the running result in rule order. + * @note Minima are exact and keep the earlier value on ties, and a later boundary replaces the + * current one only when strictly smaller, so folding per-rule results in rule order reproduces the + * single serial sweep bit for bit. + */ + void merge_rule(GeometryStepLimit& result, detail::DeterminantPolynomial& limiting_coefficients, const RuleSamples& rule) { + result.sampled_point_count += rule.limit.sampled_point_count; + result.minimum_determinant_at_accepted = std::min(result.minimum_determinant_at_accepted, rule.limit.minimum_determinant_at_accepted); + result.minimum_determinant_at_maximum = std::min(result.minimum_determinant_at_maximum, rule.limit.minimum_determinant_at_maximum); + if (rule.limit.limited_by_geometry && (!result.limited_by_geometry || rule.limit.boundary_step < result.boundary_step)) { + result.limited_by_geometry = true; + result.boundary_step = rule.limit.boundary_step; + result.limiting_element = rule.limit.limiting_element; + result.limiting_rule = rule.limit.limiting_rule; + result.limiting_point = rule.limit.limiting_point; + limiting_coefficients = rule.limiting_coefficients; + } + } + + /** @brief Per-worker scratch for the point mappings. */ + struct SampleWorkspace { + explicit SampleWorkspace(const int dimension) : mapping(dimension) {} + mapping::DMWorkspace mapping; + mapping::MappingPointContext context; + mapping::MappingPointVariation variation; + }; + } + + std::expected estimate_geometry_step_limit(const mapping::DomainMapper& mapper, const ::mfem::FiniteElementSpace& displacement_space, const ::mfem::GridFunction& compactification_coordinate, const ::mfem::Vector& accepted_displacement, const ::mfem::Vector& displacement_direction, const std::span sample_rules, const GeometryStepOptions options) { + if (const DeformationStatus validation = validate_inputs(mapper, displacement_space, compactification_coordinate, accepted_displacement, displacement_direction, sample_rules, options); !validation) { + return std::unexpected(validation.error()); + } + const ::mfem::FiniteElementSpace& compactification_space = *compactification_coordinate.FESpace(); + for (const GeometrySampleRule& rule : sample_rules) { + if (const DeformationStatus validation = validate_rule(mapper, displacement_space, compactification_space, rule); !validation) { + return std::unexpected(validation.error()); + } + } + + const ::mfem::Vector accepted_local = local_displacement(displacement_space, accepted_displacement); + const ::mfem::Vector direction_local = local_displacement(displacement_space, displacement_direction); + const int rule_count = static_cast(sample_rules.size()); + std::vector rules(sample_rules.size()); + std::vector workspaces(static_cast(serif::utils::parallel::worker_count()), SampleWorkspace(mapper.get_dimension())); + + // Rules are independent: each one is sampled into its own slot, then folded in rule order below. + serif::utils::parallel::for_each_index(rule_count, [&](const int rule_index, const int worker) { + const GeometrySampleRule& rule = sample_rules[static_cast(rule_index)]; + RuleSamples& samples = rules[static_cast(rule_index)]; + SampleWorkspace& workspace = workspaces[static_cast(worker)]; + try { + // Local MFEM state: the space's shared DofTransformation and the mesh's shared transformation are never written. + ::mfem::IsoparametricTransformation transformation; + serif::utils::parallel::element_transformation(*displacement_space.GetMesh(), rule.element, transformation); + ::mfem::Array displacement_dofs; + ::mfem::Array compactification_dofs; + ::mfem::DofTransformation displacement_transform; + ::mfem::DofTransformation compactification_transform; + displacement_space.GetElementVDofs(rule.element, displacement_dofs, displacement_transform); + compactification_space.GetElementDofs(rule.element, compactification_dofs, compactification_transform); + ::mfem::Vector element_accepted; + ::mfem::Vector element_direction; + ::mfem::Vector element_compactification; + accepted_local.GetSubVector(displacement_dofs, element_accepted); + direction_local.GetSubVector(displacement_dofs, element_direction); + compactification_coordinate.GetSubVector(compactification_dofs, element_compactification); + if (displacement_transform.GetDofTransformation() != nullptr) { + displacement_transform.InvTransformPrimal(element_accepted); + displacement_transform.InvTransformPrimal(element_direction); + } + if (compactification_transform.GetDofTransformation() != nullptr) { + compactification_transform.InvTransformPrimal(element_compactification); + } + + // Element VDofs are component-blocked even when the global space uses byVDIM ordering. + const mapping::ElementDisplacementData accepted_data = mapping::element_displacement_data_from_element_vdofs(*displacement_space.GetFE(rule.element), element_accepted); + const mapping::ElementDisplacementData direction_data = mapping::element_displacement_data_from_element_vdofs(*displacement_space.GetFE(rule.element), element_direction); + const mapping::ElementCompactificationData compactification_data(*compactification_space.GetFE(rule.element), element_compactification); + const mapping::ElementMappingData element_data{.displacement = accepted_data, .compactification = compactification_data}; + + // Use stored reference samples only when they cover exactly this rule's points. + const bool stored = rule.reference_geometry != nullptr && + rule.reference_geometry->size() == static_cast(rule.integration_rule->GetNPoints()); + for (int point = 0; point < rule.integration_rule->GetNPoints(); ++point) { + const mapping::DMFixedMeshGeometry* stored_geometry = stored ? &(*rule.reference_geometry)[static_cast(point)] : nullptr; + const auto coefficients = sample_polynomial(mapper, element_data, direction_data, transformation, rule.integration_rule->IntPoint(point), stored_geometry, workspace.mapping, workspace.context, workspace.variation, options); + if (!coefficients) { + DeformationFailure failure = coefficients.error(); + failure.element = rule.element; + failure.point = point; + samples.failure = std::move(failure); + return; + } + accumulate_sample(samples.limit, samples.limiting_coefficients, *coefficients, options, rule.element, rule_index, point); + } + } catch (...) { + samples.error = std::current_exception(); + } + }); + + GeometryStepLimit result{.step = options.maximum_step, .boundary_step = options.maximum_step}; + detail::DeterminantPolynomial limiting_coefficients{}; + for (const RuleSamples& samples : rules) { + if (samples.error) { + std::rethrow_exception(samples.error); + } + if (samples.failure) { + return std::unexpected(*samples.failure); + } + merge_rule(result, limiting_coefficients, samples); + } + + result.limiting_point_determinant_at_step = result.minimum_determinant_at_maximum; + if (result.limited_by_geometry) { + result.step = options.fraction_to_boundary * result.boundary_step; + result.limiting_point_determinant_at_step = detail::evaluate_polynomial(limiting_coefficients, result.step / options.maximum_step) + options.determinant_floor; + } + return result; + } +} diff --git a/src/lib/serif/backend/mfem/geometry/deformation/inputs.cpp b/src/lib/serif/backend/mfem/geometry/deformation/inputs.cpp new file mode 100644 index 00000000..eb5554ef --- /dev/null +++ b/src/lib/serif/backend/mfem/geometry/deformation/inputs.cpp @@ -0,0 +1,27 @@ +#include "serif/backend/mfem/geometry/deformation/inputs.hpp" + +namespace serif::geometry::deformation::backend::mfem { + namespace shared = ::serif::backend::mfem::shared; + + SurfaceDiscretizationTracking::Snapshot SurfaceDiscretizationTracking::capture(const SurfaceDiscretization& input) const { + return {.mesh = shared::mesh_snapshot(input.scalar_space.GetMesh(), false, false), .space = shared::space_snapshot(input.scalar_space), .surface_attribute = input.surface_attribute}; + } + + bool SurfaceDiscretizationTracking::matches(const Snapshot& snapshot, const SurfaceDiscretization& input) const { + return snapshot.space == shared::space_snapshot(input.scalar_space) && snapshot.surface_attribute == input.surface_attribute && shared::mesh_matches(snapshot.mesh, input.scalar_space.GetMesh()); + } + + ReferenceDiscretizationTracking::Snapshot ReferenceDiscretizationTracking::capture(const ReferenceDiscretization& input) const { + return { + .meshes = {shared::mesh_snapshot(input.scalar_space.GetMesh(), false, true), shared::mesh_snapshot(&input.logical_mesh, true, true)}, + .spaces = {shared::space_snapshot(input.scalar_space), shared::space_snapshot(input.displacement_space)}, + .stellar_attributes = input.stellar_attributes, + .vacuum_attributes = input.vacuum_attributes, + .surface_attribute = input.surface_attribute, + .infinity_attribute = input.infinity_attribute}; + } + + bool ReferenceDiscretizationTracking::matches(const Snapshot& snapshot, const ReferenceDiscretization& input) const { + return snapshot.spaces[0] == shared::space_snapshot(input.scalar_space) && snapshot.spaces[1] == shared::space_snapshot(input.displacement_space) && snapshot.stellar_attributes == input.stellar_attributes && snapshot.vacuum_attributes == input.vacuum_attributes && snapshot.surface_attribute == input.surface_attribute && snapshot.infinity_attribute == input.infinity_attribute && shared::mesh_matches(snapshot.meshes[0], input.scalar_space.GetMesh()) && shared::mesh_matches(snapshot.meshes[1], &input.logical_mesh); + } +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/lib/serif/backend/mfem/geometry/deformation/radial_interpolation.cpp b/src/lib/serif/backend/mfem/geometry/deformation/radial_interpolation.cpp new file mode 100644 index 00000000..d0c61dd3 --- /dev/null +++ b/src/lib/serif/backend/mfem/geometry/deformation/radial_interpolation.cpp @@ -0,0 +1,229 @@ +#include +#include +#include +#include "mfem/fem/fespace.hpp" +#ifdef MFEM_USE_MPI +#include "mfem/fem/pfespace.hpp" +#endif +#include "serif/backend/mfem/geometry/deformation/radial_interpolation.hpp" +#include "serif/backend/mfem/geometry/deformation/detail/radial_locator.hpp" + +namespace serif::geometry::deformation::backend::mfem +{ + namespace + { + /** @brief Check the scalar/vector pairing and the logical boundary correspondence before interpolation. */ + [[nodiscard]] DeformationStatus validate_pairing(const ::mfem::FiniteElementSpace& scalar_space, const ::mfem::FiniteElementSpace& vector_space, const ::mfem::Mesh& logical_mesh) + { +#ifdef MFEM_USE_MPI + if (dynamic_cast(&vector_space) != nullptr) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Radial interpolation does not yet support distributed vector DOFs."}); + } +#endif + const ::mfem::Mesh& physical_mesh = *scalar_space.GetMesh(); + if (vector_space.GetMesh() != &physical_mesh || vector_space.FEColl() != scalar_space.FEColl() || vector_space.GetVDim() != 3 || vector_space.GetOrdering() != ::mfem::Ordering::byNODES || vector_space.GetVSize() != 3 * scalar_space.GetNDofs() || vector_space.GetTrueVSize() != 3 * scalar_space.GetTrueVSize()) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Radial interpolation requires a component-block vector space and its scalar companion on one mesh."}); + } + if (const DeformationStatus numbering = validate_companion_numbering(scalar_space, vector_space); !numbering) + { + return numbering; + } + if (logical_mesh.Dimension() != 3 || logical_mesh.SpaceDimension() != 3 || logical_mesh.Nonconforming() != physical_mesh.Nonconforming() || physical_mesh.GetNBE() != logical_mesh.GetNBE() || physical_mesh.GetNE() != logical_mesh.GetNE() || physical_mesh.GetNV() != logical_mesh.GetNV()) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Physical and logical reference meshes must have paired refinement topology."}); + } + ::mfem::Array physical_vertices, logical_vertices; + for (int boundary = 0; boundary < physical_mesh.GetNBE(); ++boundary) + { + physical_mesh.GetBdrElementVertices(boundary, physical_vertices); + logical_mesh.GetBdrElementVertices(boundary, logical_vertices); + if (physical_mesh.GetBdrAttribute(boundary) != logical_mesh.GetBdrAttribute(boundary) || physical_mesh.GetBdrElementGeometry(boundary) != logical_mesh.GetBdrElementGeometry(boundary) || physical_vertices.Size() != logical_vertices.Size() || !std::equal(physical_vertices.begin(), physical_vertices.end(), logical_vertices.begin())) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Physical and logical boundary elements must have identical attributes and vertex ordering.", .element = boundary}); + } + } + return {}; + } + + /** @brief Resolve element support into scalar-node support while retaining shared boundary nodes. */ + [[nodiscard]] std::expected, DeformationFailure> node_support(const ::mfem::FiniteElementSpace& scalar_space, std::span elements) + { + if (elements.size() != static_cast(scalar_space.GetNE())) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_size, .message = "A radial extension support mask must contain one entry per mesh element."}); + } + std::vector result(scalar_space.GetNDofs(), 0); + ::mfem::Array dofs; + for (int element = 0; element < scalar_space.GetNE(); ++element) + { + if (elements[element] != 0 && elements[element] != 1) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_configuration, .message = "Radial element supports must contain only zero and one.", .element = element}); + } + if (elements[element] == 0) + { + continue; + } + scalar_space.GetElementDofs(element, dofs); + for (const int dof : dofs) + { + result[::mfem::FiniteElementSpace::DecodeDof(dof)] = 1; + } + } + return result; + } + + /** @brief Store one surface trace row with the original small-weight and unity tolerances. */ + [[nodiscard]] DeformationStatus append_row(const ::mfem::FiniteElementSpace& scalar_space, const detail::LocatedBoundaryPoint& location, std::span surface_indices, RadialInterpolationData& result) + { + ::mfem::Array dofs; + scalar_space.GetBdrElementDofs(location.boundary_element, dofs); + const ::mfem::FiniteElement* element = scalar_space.GetBE(location.boundary_element); + if (element == nullptr || element->GetDof() != dofs.Size()) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "The surface trace basis does not match the boundary DOFs.", .element = location.boundary_element}); + } + ::mfem::Vector shape(dofs.Size()); + element->CalcShape(location.point, shape); + double partition = 0.0; + for (int node = 0; node < dofs.Size(); ++node) + { + if (!std::isfinite(shape[node])) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::non_finite, .message = "A surface interpolation weight is non-finite.", .element = location.boundary_element}); + } + partition += shape[node]; + if (std::abs(shape[node]) <= 64.0 * std::numeric_limits::epsilon()) + { + continue; + } + const int surface_index = surface_indices[::mfem::FiniteElementSpace::DecodeDof(dofs[node])]; + if (surface_index < 0) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::unresolved_surface, .message = "A surface interpolation DOF is absent from the canonical surface selection."}); + } + result.surface_columns.push_back(surface_index); + result.interpolation_weights.push_back(shape[node]); + } + if (std::abs(partition - 1.0) > 2.0e-12) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_geometry, .message = "A radial interpolation row fails to preserve constants."}); + } + return {}; + } + } // namespace + + std::expected detail::build_radial_interpolation( + const ::mfem::FiniteElementSpace& scalar_space, + const ::mfem::FiniteElementSpace& vector_space, + ::mfem::Mesh& logical_mesh, + std::span stellar_element_support, + std::span vacuum_element_support, + int surface_attribute, + int infinity_attribute, + const std::vector& selected) + { + const DeformationStatus pairing = validate_pairing(scalar_space, vector_space, logical_mesh); + if (!pairing) + { + return std::unexpected(pairing.error()); + } + const auto stellar_support = node_support(scalar_space, stellar_element_support); + const auto vacuum_support = node_support(scalar_space, vacuum_element_support); + const auto positions = detail::sample_reference_positions(scalar_space, logical_mesh); + const auto stellar_radius = detail::boundary_logical_radius(logical_mesh, surface_attribute); + const auto infinity_radius = detail::boundary_logical_radius(logical_mesh, infinity_attribute); + if (!stellar_support) + { + return std::unexpected(stellar_support.error()); + } + if (!vacuum_support) + { + return std::unexpected(vacuum_support.error()); + } + if (!positions) + { + return std::unexpected(positions.error()); + } + if (!stellar_radius) + { + return std::unexpected(stellar_radius.error()); + } + if (!infinity_radius) + { + return std::unexpected(infinity_radius.error()); + } + if (!(*infinity_radius > *stellar_radius)) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_geometry, .message = "Logical infinity must lie outside the logical stellar surface."}); + } + RadialInterpolationData result{ + .surface_scalar_dofs = selected, + .row_offsets = std::vector(positions->size() + 1), + .logical_radii = std::vector(positions->size()), + .stellar_support = *stellar_support, + .vacuum_support = *vacuum_support, + .stellar_radius = *stellar_radius, + .infinity_radius = *infinity_radius}; + std::vector surface_indices(positions->size(), -1); + for (std::size_t node = 0; node < selected.size(); ++node) + { + surface_indices[selected[node]] = static_cast(node); + } + const double center_tolerance = 64.0 * std::numeric_limits::epsilon() * result.infinity_radius; + const double radial_tolerance = 128.0 * std::numeric_limits::epsilon() * result.infinity_radius; + ::mfem::Vector position(3), target(3); + for (std::size_t node = 0; node < positions->size(); ++node) + { + result.row_offsets[node] = static_cast(result.surface_columns.size()); + for (int component = 0; component < 3; ++component) + { + position[component] = (*positions)[node][component]; + } + const double radius = detail::logical_radius(position); + result.logical_radii[node] = radius; + if ((result.stellar_support[node] && radius > result.stellar_radius + radial_tolerance) || (result.vacuum_support[node] && (radius < result.stellar_radius - radial_tolerance || radius > result.infinity_radius + radial_tolerance))) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_geometry, .message = "A supported scalar node lies outside its logical radial interval.", .point = static_cast(node)}); + } + if (radius <= center_tolerance) + { + continue; + } + target = position; + target *= result.stellar_radius / radius; + const auto location = detail::locate_boundary_point(logical_mesh, surface_attribute, target); + if (!location) + { + return std::unexpected(location.error()); + } + const DeformationStatus appended = append_row(scalar_space, *location, surface_indices, result); + if (!appended) + { + return std::unexpected(appended.error()); + } + if (result.vacuum_support[node]) + { + target = position; + target *= result.infinity_radius / radius; + const auto infinity_location = detail::locate_boundary_point(logical_mesh, infinity_attribute, target); + if (!infinity_location) + { + return std::unexpected(infinity_location.error()); + } + } + } + result.row_offsets.back() = static_cast(result.surface_columns.size()); + return make_radial_interpolation(std::move(result)); + } + + std::expected build_radial_interpolation(const ::mfem::FiniteElementSpace& scalar_space, const ::mfem::FiniteElementSpace& vector_space, ::mfem::Mesh& logical_mesh, std::span stellar_element_support, std::span vacuum_element_support, int surface_attribute, int infinity_attribute) + { + return select_surface_scalar_dofs(scalar_space, surface_attribute).and_then([&](const std::vector& selected) + { + return detail::build_radial_interpolation(scalar_space, vector_space, logical_mesh, stellar_element_support, vacuum_element_support, surface_attribute, infinity_attribute, selected); + }); + } +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/lib/serif/backend/mfem/geometry/deformation/radial_locator.cpp b/src/lib/serif/backend/mfem/geometry/deformation/radial_locator.cpp new file mode 100644 index 00000000..ae19e4af --- /dev/null +++ b/src/lib/serif/backend/mfem/geometry/deformation/radial_locator.cpp @@ -0,0 +1,126 @@ +#include +#include +#include +#include "mfem/mesh/mesh.hpp" +#include "serif/backend/mfem/geometry/deformation/detail/radial_locator.hpp" + +namespace serif::geometry::deformation::backend::mfem::detail { + double logical_radius(const ::mfem::Vector& position) { + double radius = 0.0; + for (int component = 0; component < position.Size(); ++component) { + radius = std::max(radius, std::abs(position[component])); + } + return radius; + } + + std::expected boundary_logical_radius(::mfem::Mesh& mesh, const int attribute) { + double minimum = std::numeric_limits::infinity(); + double maximum = 0.0; + int samples = 0; + ::mfem::Vector position(3); + + for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { + if (mesh.GetBdrAttribute(boundary) != attribute) { + continue; + } + + ::mfem::ElementTransformation* transformation = mesh.GetBdrElementTransformation(boundary); + + const int geometry = mesh.GetBdrElementGeometry(boundary); + + if (geometry != ::mfem::Geometry::SQUARE || transformation == nullptr) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "The logical radial foliation requires quadrilateral shell elements.", .element = boundary}); + } + + const ::mfem::IntegrationRule* vertices = ::mfem::Geometries.GetVertices(geometry); + + for (int vertex = 0; vertex < vertices->GetNPoints(); ++vertex) { + transformation->Transform(vertices->IntPoint(vertex), position); + for (int component = 0; component < 3; ++component) { + if (!std::isfinite(position[component])) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::non_finite, .message = "A logical boundary vertex is non-finite.", .element = boundary, .point = vertex}); + } + } + const double radius = logical_radius(position); + + minimum = std::min(minimum, radius); + maximum = std::max(maximum, radius); + + ++samples; + } + } + + if (samples == 0 || !(minimum > 0.0) || maximum - minimum > 1.0e-12 * std::max(1.0, maximum)) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_geometry, .message = "A required logical boundary is absent or is not a positive constant L-infinity-radius shell."}); + } + + return 0.5 * (minimum + maximum); + } + + std::expected locate_boundary_point(::mfem::Mesh& mesh, const int attribute, const ::mfem::Vector& target) { + LocatedBoundaryPoint best{.boundary_element = -1, .point = {}}; + + double best_residual = std::numeric_limits::infinity(); + const double tolerance = 2.0e-11 * std::max(1.0, logical_radius(target)); + + ::mfem::Vector transformed(3), residual(3); + + for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { + if (mesh.GetBdrAttribute(boundary) != attribute) { + continue; + } + + ::mfem::ElementTransformation* transformation = mesh.GetBdrElementTransformation(boundary); + ::mfem::IntegrationPoint point = ::mfem::Geometries.GetCenter(::mfem::Geometry::SQUARE); + + bool nonsingular = true; + + for (int iteration = 0; iteration < 8; ++iteration) { + transformation->Transform(point, transformed); + residual = target; + residual -= transformed; + if (residual.Norml2() <= tolerance) { + break; + } + + transformation->SetIntPoint(&point); + const ::mfem::DenseMatrix& jacobian = transformation->Jacobian(); + double normal00 = 0.0, normal01 = 0.0, normal11 = 0.0; + double right0 = 0.0, right1 = 0.0; + for (int component = 0; component < 3; ++component) { + normal00 += jacobian(component, 0) * jacobian(component, 0); + normal01 += jacobian(component, 0) * jacobian(component, 1); + normal11 += jacobian(component, 1) * jacobian(component, 1); + right0 += jacobian(component, 0) * residual(component); + right1 += jacobian(component, 1) * residual(component); + } + + const double determinant = normal00 * normal11 - normal01 * normal01; + if (!(std::abs(determinant) > std::numeric_limits::epsilon())) { + nonsingular = false; + break; + } + point.x += (normal11 * right0 - normal01 * right1) / determinant; + point.y += (normal00 * right1 - normal01 * right0) / determinant; + } + + if (!nonsingular || !::mfem::Geometry::CheckPoint(::mfem::Geometry::SQUARE, point, 1.0e-10)) { + continue; + } + + transformation->Transform(point, transformed); + transformed -= target; + const double norm = transformed.Norml2(); + if (norm <= tolerance && norm < best_residual) { + best = {.boundary_element = boundary, .point = point}; + best_residual = norm; + } + } + + if (best.boundary_element < 0) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::unresolved_surface, .message = "No logical quadrilateral contains a radial projection onto the required shell."}); + } + + return best; + } +} diff --git a/src/lib/serif/backend/mfem/geometry/deformation/reference_geometry.cpp b/src/lib/serif/backend/mfem/geometry/deformation/reference_geometry.cpp new file mode 100644 index 00000000..83d746df --- /dev/null +++ b/src/lib/serif/backend/mfem/geometry/deformation/reference_geometry.cpp @@ -0,0 +1,223 @@ +#include +#include +#include "mfem/fem/fespace.hpp" +#ifdef MFEM_USE_MPI +#include "mfem/fem/pfespace.hpp" +#endif +#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp" + +namespace serif::geometry::deformation::backend::mfem +{ + DeformationStatus validate_scalar_reference_space(const ::mfem::FiniteElementSpace& scalar_space) + { +#ifdef MFEM_USE_MPI + if (dynamic_cast(&scalar_space) != nullptr) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Reference deformation compilation requires a serial finite-element space; distributed interpolation ownership is not implemented."}); + } +#endif + const ::mfem::Mesh* mesh = scalar_space.GetMesh(); + if (mesh == nullptr || mesh->Dimension() != 3 || mesh->SpaceDimension() != 3 || (scalar_space.GetConformingProlongation() != nullptr && scalar_space.IsVariableOrder())) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Reference deformation compilation requires three-dimensional serial nodal spaces; constrained spaces must have fixed order."}); + } + if (scalar_space.GetVDim() != 1 || scalar_space.FEColl() == nullptr || scalar_space.FEColl()->GetContType() != ::mfem::FiniteElementCollection::CONTINUOUS) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Reference deformation requires a scalar continuous nodal companion space."}); + } + for (int element = 0; element < mesh->GetNE(); ++element) + { + const ::mfem::FiniteElement* finite_element = scalar_space.GetFE(element); + if (dynamic_cast(finite_element) == nullptr || finite_element->GetRangeType() != ::mfem::FiniteElement::SCALAR || finite_element->GetMapType() != ::mfem::FiniteElement::VALUE) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Deformation coordinates require scalar nodal value elements.", .element = element}); + } + } + return {}; + } + + DeformationStatus validate_companion_numbering(const ::mfem::FiniteElementSpace& reference_space, const ::mfem::FiniteElementSpace& companion_space) + { + if (reference_space.GetMesh() == nullptr || reference_space.GetMesh() != companion_space.GetMesh() || reference_space.FEColl() == nullptr || reference_space.FEColl() != companion_space.FEColl() || reference_space.GetNDofs() != companion_space.GetNDofs()) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Deformation companions require the same reference mesh, nodal collection and scalar-node count."}); + } + if (&reference_space == &companion_space) + { + return {}; + } + ::mfem::Array reference_dofs, companion_dofs; + for (int element = 0; element < reference_space.GetNE(); ++element) + { + reference_space.GetElementDofs(element, reference_dofs); + companion_space.GetElementDofs(element, companion_dofs); + if (reference_space.GetFE(element) != companion_space.GetFE(element) || reference_dofs.Size() != companion_dofs.Size() || !std::equal(reference_dofs.begin(), reference_dofs.end(), companion_dofs.begin())) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Deformation companions must use the same element basis and canonical scalar-node numbering.", .element = element}); + } + } + return {}; + } + + std::expected>, DeformationFailure> detail::sample_reference_positions( + const ::mfem::FiniteElementSpace& scalar_space, + ::mfem::Mesh& reference_mesh) + { + const ::mfem::Mesh& physical_mesh = *scalar_space.GetMesh(); + if (reference_mesh.Dimension() != 3 || reference_mesh.SpaceDimension() != 3 || reference_mesh.Nonconforming() != physical_mesh.Nonconforming() || physical_mesh.GetNE() != reference_mesh.GetNE()) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Reference positions require paired three-dimensional elements with matching refinement topology."}); + } + + std::vector> positions(scalar_space.GetNDofs()); + std::vector processed(positions.size(), false); + ::mfem::Array dofs, physical_vertices, reference_vertices; + ::mfem::Vector position(3); + for (int element = 0; element < physical_mesh.GetNE(); ++element) + { + physical_mesh.GetElementVertices(element, physical_vertices); + reference_mesh.GetElementVertices(element, reference_vertices); + if (physical_mesh.GetElementGeometry(element) != reference_mesh.GetElementGeometry(element) || physical_mesh.GetAttribute(element) != reference_mesh.GetAttribute(element) || physical_vertices.Size() != reference_vertices.Size() || !std::equal(physical_vertices.begin(), physical_vertices.end(), reference_vertices.begin())) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Physical and logical elements must have identical attributes, vertex correspondence and local orientation.", .element = element}); + } + const ::mfem::FiniteElement& finite_element = *scalar_space.GetFE(element); + const ::mfem::IntegrationRule& nodes = finite_element.GetNodes(); + scalar_space.GetElementDofs(element, dofs); + if (nodes.GetNPoints() != dofs.Size()) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "The scalar companion nodes do not match the element DOFs.", .element = element}); + } + ::mfem::ElementTransformation* transformation = reference_mesh.GetElementTransformation(element); + if (transformation == nullptr) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_geometry, .message = "A reference element has no coordinate transformation.", .element = element}); + } + for (int node = 0; node < dofs.Size(); ++node) + { + const int dof = ::mfem::FiniteElementSpace::DecodeDof(dofs[node]); + transformation->Transform(nodes.IntPoint(node), position); + double scale = 1.0; + double squared_difference = 0.0; + for (int component = 0; component < 3; ++component) + { + if (!std::isfinite(position[component])) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::non_finite, .message = "A reference node has a non-finite position.", .element = element, .point = node}); + } + scale = std::max(scale, std::abs(position[component])); + const double difference = position[component] - positions[dof][component]; + squared_difference += difference * difference; + } + if (processed[dof] && std::sqrt(squared_difference) > 1.0e-12 * scale) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "A shared scalar node has inconsistent reference coordinates.", .element = element, .point = node}); + } + if (processed[dof]) + { + continue; + } + for (int component = 0; component < 3; ++component) + { + positions[dof][component] = position[component]; + } + processed[dof] = true; + } + } + if (std::find(processed.begin(), processed.end(), false) != processed.end()) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "A scalar node has no reference coordinate."}); + } + return positions; + } + + std::expected, DeformationFailure> select_surface_scalar_dofs(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute) + { + const DeformationStatus status = validate_scalar_reference_space(scalar_space); + if (!status) + { + return std::unexpected(status.error()); + } + std::vector selected(scalar_space.GetNDofs(), false); + ::mfem::Array dofs; + for (int boundary = 0; boundary < scalar_space.GetMesh()->GetNBE(); ++boundary) + { + if (scalar_space.GetMesh()->GetBdrAttribute(boundary) != surface_attribute) + { + continue; + } + scalar_space.GetBdrElementDofs(boundary, dofs); + for (const int dof : dofs) + { + selected[::mfem::FiniteElementSpace::DecodeDof(dof)] = true; + } + } + std::vector result; + const ::mfem::SparseMatrix* prolongation = scalar_space.GetConformingProlongation(); + for (int dof = 0; dof < scalar_space.GetNDofs(); ++dof) + { + if (selected[dof]) + { + if (prolongation != nullptr) + { + const int begin = prolongation->GetI()[dof]; + const int end = prolongation->GetI()[dof + 1]; + if (end != begin + 1 || prolongation->GetData()[begin] != 1.0) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Radial surface deformation requires a conforming stellar trace. Generate the mesh with STROID interface closure.", .point = dof}); + } + } + result.push_back(dof); + } + } + if (result.empty()) + { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::unresolved_surface, .message = "The requested surface attribute has no scalar nodes."}); + } + return result; + } + + std::expected>, DeformationFailure> sample_reference_positions(const ::mfem::FiniteElementSpace& scalar_space, ::mfem::Mesh& reference_mesh) + { + if (const DeformationStatus status = validate_scalar_reference_space(scalar_space); !status) + { + return std::unexpected(status.error()); + } + return detail::sample_reference_positions(scalar_space, reference_mesh); + } + + std::expected detail::sample_reference_surface(const ::mfem::FiniteElementSpace& scalar_space, const std::vector& selected) + { + const auto positions = detail::sample_reference_positions(scalar_space, *scalar_space.GetMesh()); + if (!positions) + { + return std::unexpected(positions.error()); + } + ReferenceSurface result{.scalar_dofs = selected}; + result.positions.reserve(selected.size()); + for (const int dof : selected) + { + result.positions.push_back((*positions)[dof]); + } + return result; + } + + std::expected sample_reference_surface(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute) + { + return select_surface_scalar_dofs(scalar_space, surface_attribute).and_then([&](const std::vector& selected) + { + return detail::sample_reference_surface(scalar_space, selected); + }); + } + DeformationStatus validate_surface_trace(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding) { + const auto current = sample_reference_surface(scalar_space, surface_attribute); + if (!current) { + return std::unexpected(current.error()); + } + if (reference_surface.scalar_dofs != current->scalar_dofs || reference_surface.positions != current->positions || !std::ranges::equal(reference_surface.positions, embedding.reference_positions())) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Surface assembly requires the same ordered reference trace as its surface embedding. Rebuild the reference surface and embedding together."}); + } + return {}; + } + +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/lib/serif/backend/mfem/geometry/deformation/surface_metric.cpp b/src/lib/serif/backend/mfem/geometry/deformation/surface_metric.cpp new file mode 100644 index 00000000..d05de11f --- /dev/null +++ b/src/lib/serif/backend/mfem/geometry/deformation/surface_metric.cpp @@ -0,0 +1,85 @@ +#include "serif/backend/mfem/geometry/deformation/surface_metric.hpp" + +#include +#include +#include + +#include "mfem/fem/bilininteg.hpp" +#include "mfem/fem/fespace.hpp" +#include "mfem/linalg/densemat.hpp" +#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp" +#include "serif/backend/mfem/shared/boundary_elements.hpp" + +namespace serif::geometry::deformation::backend::mfem { + namespace { + /** @brief Incorporate the nodal Cartesian directions before pulling the scalar boundary mass into parameter space. */ + void include_radial_directions(::mfem::DenseMatrix& mass, std::span surface_nodes, std::span encoded_dofs, const SurfaceEmbedding& embedding) { + for (int row = 0; row < mass.Height(); ++row) { + for (int column = 0; column < mass.Width(); ++column) { + double radial_product = 0.0; + for (std::size_t component = 0; component < 3; ++component) { + radial_product += embedding.radial_directions()[surface_nodes[row]][component] * embedding.radial_directions()[surface_nodes[column]][component]; + } + const double orientation = (encoded_dofs[row] < 0) == (encoded_dofs[column] < 0) ? 1.0 : -1.0; + mass(row, column) *= orientation * radial_product; + } + } + } + + /** @brief Accumulate only the requested diagonal, without storing a global mass matrix or changing the harmonic basis. */ + void add_parameter_metric(const ::mfem::DenseMatrix& mass, std::span surface_nodes, const SurfaceEmbedding& embedding, std::span diagonal) { + if (embedding.has_nodal_parameters()) { + for (int row = 0; row < mass.Height(); ++row) { + diagonal[surface_nodes[row]] += mass(row, row); + } + return; + } + const std::span basis = embedding.nodal_basis(); + for (std::size_t parameter = 0; parameter < embedding.input_size(); ++parameter) { + long double entry = 0.0L; + for (int row = 0; row < mass.Height(); ++row) { + const double first = basis[static_cast(surface_nodes[row]) * embedding.input_size() + parameter]; + for (int column = 0; column < mass.Width(); ++column) { + const double second = basis[static_cast(surface_nodes[column]) * embedding.input_size() + parameter]; + entry += static_cast(first) * mass(row, column) * second; + } + } + diagonal[parameter] += entry; + } + } + } // namespace + + std::expected, DeformationFailure> detail::surface_parameter_mass_diagonal(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding) { + namespace shared = ::serif::backend::mfem::shared; + + std::vector diagonal(embedding.input_size(), 0.0L); + ::mfem::DenseMatrix mass; + ::mfem::MassIntegrator integrator; + const shared::BoundaryStatus status = shared::for_each_boundary_element(scalar_space, surface_attribute, reference_surface.scalar_dofs, + [&](const shared::BoundaryElement& element) -> shared::BoundaryStatus { + integrator.AssembleElementMatrix(element.finite_element, element.transformation, mass); + include_radial_directions(mass, element.selected_nodes, element.encoded_dofs, embedding); + add_parameter_metric(mass, element.selected_nodes, embedding, diagonal); + return {}; + }); + if (!status) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = status.error().message, .element = status.error().element}); + } + + std::vector result(diagonal.size()); + for (std::size_t parameter = 0; parameter < diagonal.size(); ++parameter) { + result[parameter] = static_cast(diagonal[parameter]); + if (!std::isfinite(result[parameter]) || result[parameter] <= 0.0) { + return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_geometry, .message = "A shape parameter has a nonpositive or non-finite reference boundary mass norm."}); + } + } + return result; + } + + std::expected, DeformationFailure> surface_parameter_mass_diagonal(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding) { + if (const DeformationStatus status = validate_surface_trace(scalar_space, surface_attribute, reference_surface, embedding); !status) { + return std::unexpected(status.error()); + } + return detail::surface_parameter_mass_diagonal(scalar_space, surface_attribute, reference_surface, embedding); + } +} // namespace serif::geometry::deformation::backend::mfem diff --git a/src/lib/serif/backend/mfem/mapping/compactification/kelvin.cpp b/src/lib/serif/backend/mfem/mapping/compactification/kelvin.cpp new file mode 100644 index 00000000..9336ccbb --- /dev/null +++ b/src/lib/serif/backend/mfem/mapping/compactification/kelvin.cpp @@ -0,0 +1,269 @@ +#include + +#include "serif/backend/mfem/shared/finite.hpp" +#include "serif/backend/mfem/mapping/compactification/kelvin.hpp" +#include "serif/utils/user_options/mapping/compactification/kelvin.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::mapping::compactification { + using namespace serif::utils::user_options; + using namespace serif::utils::errors; + using namespace serif::utils::checks; + + KelvinCompactification::KelvinCompactification(const KelvinCompactificationOptions& options) : m_options(options) { + if (!std::isfinite(m_options.r_star_ref)) { + serif_error("Kelvin compactification requires a finite reference stellar radius. Received {} ", m_options.r_star_ref); + } + + if (!std::isfinite(m_options.r_inf_ref)) { + serif_error("Kelvin compactification requires a finite reference infinity radius. Received {} ", m_options.r_star_ref); + } + + if (m_options.r_star_ref <= 0 || m_options.r_inf_ref <= m_options.r_star_ref) + { + serif_error("Kelvin compactification requires 0 < r_star_ref < r_inf_ref. Got r_star_ref={} and r_inf_ref={}", m_options.r_star_ref, m_options.r_inf_ref); + } + + if (!std::isfinite(m_options.coordinate_tolerance) || m_options.coordinate_tolerance < 0.0 || m_options.coordinate_tolerance >= 1.0) + { + serif_error("Kelvin compactification coordinate tolerance must be finite and on [0, 1). Received: {}", m_options.coordinate_tolerance); + } + } + + bool KelvinCompactification::is_affine_in_displacement() const noexcept { + return true; // Always true for kelvin compactification + } + + MappingStatus KelvinCompactification::evaluate(const ExteriorMapInput& input, ExteriorMapResult& result) const { + const int dimension = input.reference_position.Size(); + + if (dimension <= 0 || input.displaced_position.Size() != dimension || input.compactification_coordinate_gradient.Size() != dimension) { + return MappingStatus::INVALID_DIMENSION; + } + + if (input.displacement_jacobian.Height() != dimension || input.displacement_jacobian.Width() != dimension) { + return MappingStatus::INVALID_DIMENSION; + } + + const bool reference_pos_is_finite = is_finite(input.reference_position); + const bool displaced_pos_is_finite = is_finite(input.displaced_position); + const bool coord_grad_is_finite = is_finite(input.compactification_coordinate_gradient); + const bool displacement_jacobian_is_finite = is_finite(input.displacement_jacobian); + + if (!(reference_pos_is_finite && displaced_pos_is_finite && coord_grad_is_finite && displacement_jacobian_is_finite)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + + RadialFactors factors{}; + + // The key radial factors we use are the scale (which stretches the finite computational domain into the infinite physical domain) and the scale derivative (which is used to compute the mapping jacobian) + const MappingStatus factor_status = compute_radial_factors(input.compactification_coordinate, factors); + if (factor_status != MappingStatus::VALID) { + return factor_status; + } + + result.physical_position.SetSize(dimension); + result.mapping_jacobian.SetSize(dimension, dimension); + + for (int i = 0; i < dimension; ++i) { + // Note how the physical position is just the product of the displaced position and the scale factor. + result.physical_position(i) = factors.scale * input.displaced_position(i); + + // The mapping jacobian comes from trivial application of the product rule + // recall: r_i = scale * x_i where r is the physical position and x is the displaced position. + // then we can differentiate wrt. X_j holding nothing fixed. Note the capital X here, this is the mesh coordinate not the displaced position. + // Lets call this jacobian F + // F = \frac{\partial r_i}{\partial X_{j}} + // F then tells us how the physical position changes as we move along mesh coordinates + // Lets then apply this to the function we have for the kelvin compactification + // F = scale * \frac{\partial x_i}{\partial X_j} + x_i * \frac{\partial scale}{\partial X_j} + // Below you can see the displacement jacobian (\frac{\partial x_i}{\partial X_j}) and the scale derivative (\frac{\partial scale}{\partial X_j}) being applied to compute the mapping jacobian. + for (int j = 0; j < dimension; ++j) { + const double scale_gradient = factors.scale_derivative * input.compactification_coordinate_gradient(j); + result.mapping_jacobian(i, j) = factors.scale * input.displacement_jacobian(i, j) + input.displaced_position(i) * scale_gradient; + } + } + + if (!is_finite(result.physical_position) || !is_finite(result.mapping_jacobian)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + const double mapping_determinant = result.mapping_jacobian.Det(); + + if (!std::isfinite(mapping_determinant)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + if (mapping_determinant <= 0.0) { + return MappingStatus::NON_POSITIVE_DETERMINATE; + } + return MappingStatus::VALID; + } + + MappingStatus KelvinCompactification::evaluate_variation(const ExteriorMapInput& input, const ExteriorMapResult& result, const ExteriorMapDirection& direction, ExteriorMapVariation& variation) const { + const int dimension = input.reference_position.Size(); + + if (dimension <= 0 || input.displaced_position.Size() != dimension || input.compactification_coordinate_gradient.Size() != dimension) { + return MappingStatus::INVALID_DIMENSION; + } + + if (input.displacement_jacobian.Height() != dimension || input.displacement_jacobian.Width() != dimension) { + return MappingStatus::INVALID_DIMENSION; + } + + if (result.physical_position.Size() != dimension || result.mapping_jacobian.Height() != dimension || result.mapping_jacobian.Width() != dimension) { + return MappingStatus::INVALID_DIMENSION; + } + + if (direction.displaced_position_variation.Size() != dimension || direction.displacement_jacobian_variation.Height() != dimension || direction.displacement_jacobian_variation.Width() != dimension) { + return MappingStatus::INVALID_DIMENSION; + } + + const bool reference_position_is_finite = is_finite(input.reference_position); + const bool displaced_position_is_finite = is_finite(input.displaced_position); + const bool compactification_coord_grad_is_finite = is_finite(input.compactification_coordinate_gradient); + const bool displacement_jacobian_is_finite = is_finite(input.displacement_jacobian); + + if (!(reference_position_is_finite && displaced_position_is_finite && compactification_coord_grad_is_finite && displacement_jacobian_is_finite)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + + const bool result_physical_position_is_finite = is_finite(result.physical_position); + const bool result_mapping_jacobian_is_finite = is_finite(result.mapping_jacobian); + const bool direction_displaced_position_is_finite = is_finite(direction.displaced_position_variation); + const bool direction_displacement_jacobian_is_finite = is_finite(direction.displacement_jacobian_variation); + if (!(result_physical_position_is_finite && result_mapping_jacobian_is_finite && direction_displaced_position_is_finite && direction_displacement_jacobian_is_finite)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; // Note we are checking a parameter called result here; however, this is the result from an evaluate call, it is an input to this function. + } + + RadialFactors factors{}; + const MappingStatus factor_status = compute_radial_factors(input.compactification_coordinate, factors); + if (factor_status != MappingStatus::VALID) { + return factor_status; + } + + variation.physical_position_variation.SetSize(dimension); + variation.mapping_jacobian_variation.SetSize(dimension, dimension); + + for (int i = 0; i < dimension; ++i) { + variation.physical_position_variation(i) = factors.scale * direction.displaced_position_variation(i); + + for (int j = 0; j < dimension; ++j) { + const double scale_gradient = factors.scale_derivative * input.compactification_coordinate_gradient(j); + variation.mapping_jacobian_variation(i, j) = factors.scale * direction.displacement_jacobian_variation(i, j) + direction.displaced_position_variation(i) * scale_gradient; + } + } + + if (!is_finite(variation.physical_position_variation) || !is_finite(variation.mapping_jacobian_variation)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + return MappingStatus::VALID; + } + + [[nodiscard]] std::string_view KelvinCompactification::get_name() const noexcept { + return m_name; + } + + [[nodiscard]] double KelvinCompactification::get_reference_stellar_radius() const noexcept { + return m_options.r_star_ref; + } + + [[nodiscard]] double KelvinCompactification::get_reference_infinity_radius() const noexcept { + return m_options.r_inf_ref; + } + + [[nodiscard]] double KelvinCompactification::get_coordinate_tolerance() const noexcept { + return m_options.coordinate_tolerance; + } + + [[nodiscard]] MappingStatus KelvinCompactification::evaluate_jacobian_variation_factors(double compactification_coordinate, const mfem::Vector& coordinateGradient, double& scale, mfem::Vector& scaleGradient) const { + if (coordinateGradient.Size() <= 0) { + return MappingStatus::INVALID_DIMENSION; + } + + if (!is_finite(coordinateGradient)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + + RadialFactors factors{}; + const MappingStatus status = compute_radial_factors(compactification_coordinate, factors); + + if (status != MappingStatus::VALID) { + return status; + } + + scale = factors.scale; + scaleGradient = coordinateGradient; + scaleGradient *= factors.scale_derivative; + + if (!is_finite(scaleGradient)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + return MappingStatus::VALID; + } + + MappingStatus KelvinCompactification::compute_radial_factors(double compactification_coordinate, RadialFactors& radial_factors) const { + if (!std::isfinite(compactification_coordinate)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + + const double tolerance = m_options.coordinate_tolerance; + + if (compactification_coordinate < -tolerance || compactification_coordinate > 1.0 + tolerance) { + return MappingStatus::OUTSIDE_REFERENCE_DOMAIN; + } + + double coordinate = compactification_coordinate; + if (coordinate < 0.0) { + coordinate = 0.0; // Not sure this is the best behavior here... + } + + if (coordinate >= 1.0 - tolerance) { + return MappingStatus::AT_COMPACTIFIED_INFINITY; + } + + // Here we need to do some transformations from the options defined on the mesh to useful computational coordinates + // r_inf_ref is the computational / reference radius of the infinity surface (the edge of the entire domain) and r_star_ref is the radius of the spherical + // stellar model inscribed within. Therefore radial extent is the computational radial distance between the stellar surface and the infinity surface. + // Note that this is separate from the parameterize compactification coordinate. + const double radial_extent = m_options.r_inf_ref - m_options.r_star_ref; + + // This places us at the correct spot in computational space given the current compactification coordinate. Say you have compactification = 0.5, + // an r_star_ref of 2 and a radial extent of 3, this this will place you at 2 + 0.5 * 3 = 3.5 in computational space, which is half way between the stellar surface and the infinity surface. + const double computational_radius = m_options.r_star_ref + coordinate * radial_extent; + + if (!std::isfinite(computational_radius) || computational_radius <= 0.0) { + return MappingStatus::INVALID_REFERENCE_RADIUS; + } + + // Invert the exterior coordinate so it runs from 0 at the star to 1 at compactified infinity + const double one_minus_coordinate = 1.0 - coordinate; + const double denominator = computational_radius * one_minus_coordinate; + + if (!std::isfinite(denominator) || denominator <= 0.0) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + // The scale here is the factor which stretches the finite computational domain into the infinite physical domain. Properties we need this to have + // include that it should go to 1 at the stellar surface and go to infinity at the compactified infinity. + // Mathematically this is scale = |r|/|x| where r is the physical radius and x is the computational radius. + // Put another way, scale is the ratio of the target physical radius for the current compactification coordinate + // to the current mesh radius. + const double scale = m_options.r_star_ref / denominator; + const double scale_derivative = scale * (1.0 / one_minus_coordinate - radial_extent / computational_radius); + + if (!std::isfinite(scale) || !std::isfinite(scale_derivative)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + radial_factors.compactification_coordinate = coordinate; + radial_factors.computational_radius = computational_radius; + radial_factors.scale = scale; + radial_factors.scale_derivative = scale_derivative; + + return MappingStatus::VALID; + + + } +} diff --git a/src/lib/serif/backend/mfem/mapping/data.cpp b/src/lib/serif/backend/mfem/mapping/data.cpp new file mode 100644 index 00000000..e3624e8c --- /dev/null +++ b/src/lib/serif/backend/mfem/mapping/data.cpp @@ -0,0 +1,133 @@ +#include + +#include "mfem/linalg/vector.hpp" +#include "mfem/fem/fe/fe_base.hpp" +#include "mfem/fem/fespace.hpp" + +#include "serif/backend/mfem/mapping/data.hpp" + +#include "serif/utils/error/errors.hpp" + +namespace serif::mapping { + using namespace serif::utils::errors; + + ElementDisplacementData::ElementDisplacementData( + const mfem::FiniteElement& element, + mfem::Vector& displacement_dofs + ) : m_element(&element), m_dimension(0) { + const int dof_count = element.GetDof(); + + // TODO: Change these to serif_ensure calls with appropriate error codes + if (dof_count <= 0) { + serif_error("ElementDisplacementData: Displacement elements must have at least one degree of freedom. Got an element with {} dofs", dof_count); + } + if (displacement_dofs.Size() <= 0 || displacement_dofs.Size() % dof_count != 0) { + serif_error("ElementDisplacementData: The displacement vector size ({}) must be a positive multiple of the element degree of freedom count ({}).", displacement_dofs.Size(), dof_count); + } + + m_dimension = displacement_dofs.Size() / dof_count; + m_dof_matrix.SetSize(dof_count, m_dimension); + + for (int componentID = 0; componentID < m_dimension; ++componentID) { + for (int dofID = 0; dofID < dof_count; ++dofID) { + m_dof_matrix(dofID, componentID) = displacement_dofs(dofID + componentID * dof_count); + } + } + } + + const mfem::FiniteElement& ElementDisplacementData::get_element() const noexcept { + return *m_element; + } + + const mfem::DenseMatrix& ElementDisplacementData::get_dof_matrix() const noexcept { + return m_dof_matrix; + } + + int ElementDisplacementData::get_dimension() const noexcept { + return m_dimension; + } + + int ElementDisplacementData::get_dof_count() const noexcept { + return m_element->GetDof(); + } + + ElementDisplacementData element_displacement_data_from_element_vdofs(const mfem::FiniteElement& element, mfem::Vector& displacement_dofs) { + return ElementDisplacementData(element, displacement_dofs); + } + + ElementCompactificationData::ElementCompactificationData( + const mfem::FiniteElement& element, + const mfem::Vector& dofs + ) : m_element(&element), m_dofs(dofs) { + if (element.GetRangeType() != mfem::FiniteElement::SCALAR) { + serif_error("Compactification coordinate requires a scalar finite element."); + } + + if (element.GetMapType() != mfem::FiniteElement::VALUE) { + serif_error("Compactification coordinate requires a value-mapped scalar finite element"); + } + + if (element.GetDerivType() != mfem::FiniteElement::GRAD) { + serif_error("Compactification coordinate finite element must provide a gradient"); + } + + if (element.GetDof() <= 0) { + serif_error("Compactification coordinatge finite element must have a positive non-zero number of degrees of freedom. Recived {}", element.GetDof()); + } + + if (dofs.Size() != element.GetDof()) { + serif_error("Compactification coordinate degree of freedom count ({}) does not match its finite element dof count ({})", dofs.Size(), element.GetDof()); + } + } + + const mfem::FiniteElement& ElementCompactificationData::get_element() const noexcept { + return *m_element; + } + + const mfem::Vector& ElementCompactificationData::get_dofs() const noexcept { + return m_dofs; + } + + int ElementCompactificationData::get_dof_count() const noexcept { + return m_dofs.Size(); + } + + DMWorkspace::DMWorkspace(const int dimension) { + set_dimension(dimension); + } + + void DMWorkspace::set_dimension(const int dimension) { + if (dimension <= 0) { + serif_error("Domain mapping workspace dimension must be positive and finite. Received {}", dimension); + } + + m_dimension = dimension; + + m_field_value.SetSize(dimension); + m_field_jacobian.SetSize(dimension, dimension); + m_reference_field_jacobian.SetSize(dimension, dimension); + + m_compactification_point.compactification_coordinate = 0.0; + m_compactification_point.coordinate_gradient.SetSize(dimension); + + m_reference_normal.SetSize(dimension); + m_mapped_normal.SetSize(dimension); + m_full_element_jacobian.SetSize(dimension, dimension); + + m_vector_temp.SetSize(dimension); + m_matrix_temp_1.SetSize(dimension, dimension); + m_matrix_temp_2.SetSize(dimension, dimension); + + m_exterior_result.physical_position.SetSize(dimension); + m_exterior_result.mapping_jacobian.SetSize(dimension, dimension); + + m_exterior_variation.physical_position_variation.SetSize(dimension); + m_exterior_variation.mapping_jacobian_variation.SetSize(dimension, dimension); + } + + int DMWorkspace::get_dimension() const noexcept { + return m_dimension; + } + + +} diff --git a/src/lib/serif/backend/mfem/mapping/domain_mapper.cpp b/src/lib/serif/backend/mfem/mapping/domain_mapper.cpp new file mode 100644 index 00000000..26dd0e7e --- /dev/null +++ b/src/lib/serif/backend/mfem/mapping/domain_mapper.cpp @@ -0,0 +1,741 @@ +#include "mfem/linalg/densemat.hpp" + +#include "serif/backend/mfem/mapping/domain_mapper.hpp" +#include "serif/utils/error/errors.hpp" + +#include "serif/backend/mfem/shared/finite.hpp" +#include "serif/backend/mfem/shared/invertible.hpp" + + +// Anonymous namespace for translation unit-local helper functions and types +namespace { + // TODO: Put something in here using variadic args to validate multiple things satisfy some predicate +} + +namespace serif::mapping { + using namespace serif::utils::errors; + using namespace serif::utils::checks; + + DomainMapper::DomainMapper( + utils::user_options::DomainMapperOptions options, + std::unique_ptr exterior_map + ) : + m_options(options), + m_exterior_map(std::move(exterior_map)) { + if (m_options.dimension <= 0) { + serif_error("Domain Mapper requires a positive, non-zero, dimension. Received dimension: {}", m_options.dimension); + } + + if (m_options.vacuum_element_attribute_ID <= 0) { + serif_error("Domain Mapper requires a positive, non-zero, vacuum element attribute ID. Received attribute ID: {}", m_options.vacuum_element_attribute_ID); + } + + if (!m_exterior_map) { + serif_error("Domain Mapper requires a valid exterior map. One was not provided or a nullptr was provided."); + } + } + + std::expected sample_fixed_mesh_geometry( + mfem::ElementTransformation& transformation, const mfem::IntegrationPoint& point + ) { + transformation.SetIntPoint(&point); + DMFixedMeshGeometry geometry; + transformation.Transform(point, geometry.position); + geometry.jacobian = transformation.Jacobian(); + geometry.weight = transformation.Weight(); + // Check before MFEM attempts an inverse: singular reference elements are numerical failures. + if (!is_finite(geometry.position) || !is_finite(geometry.jacobian) || !is_finite(geometry.weight)) { + return std::unexpected(MappingStatus::NON_FINITE_MAPPING_INPUT); + } + if (geometry.weight <= 0.0 || + (geometry.jacobian.Height() == geometry.jacobian.Width() && !has_stable_inverse(geometry.jacobian))) { + return std::unexpected(MappingStatus::NON_POSITIVE_DETERMINATE); + } + geometry.inverse_jacobian = transformation.InverseJacobian(); + if (!is_finite(geometry.inverse_jacobian)) { + return std::unexpected(MappingStatus::NON_FINITE_MAPPING_INPUT); + } + return geometry; + } + + MappingStatus DomainMapper::evaluate_point( + const ElementMappingData &element_data, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &ip, + DMWorkspace &workspace, + MappingPointContext &context + ) const { + const auto geometry = sample_fixed_mesh_geometry(transformation, ip); + if (!geometry) { + return geometry.error(); + } + return evaluate_point_impl(element_data, transformation, ip, workspace, context, *geometry); + } + + MappingStatus DomainMapper::evaluate_point( + const ElementMappingData& element_data, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + DMWorkspace& workspace, + MappingPointContext& context, + const DMFixedMeshGeometry& fixed_geometry + ) const { + if (fixed_geometry.position.Size() != m_options.dimension || + fixed_geometry.jacobian.Height() != m_options.dimension || fixed_geometry.jacobian.Width() != m_options.dimension || + fixed_geometry.inverse_jacobian.Height() != m_options.dimension || fixed_geometry.inverse_jacobian.Width() != m_options.dimension) { + return MappingStatus::INVALID_DIMENSION; + } + if (!is_finite(fixed_geometry.position) || !is_finite(fixed_geometry.jacobian) || + !is_finite(fixed_geometry.inverse_jacobian) || !is_finite(fixed_geometry.weight)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + if (fixed_geometry.weight <= 0.0 || !has_stable_inverse(fixed_geometry.jacobian)) { + return MappingStatus::NON_POSITIVE_DETERMINATE; + } + transformation.SetIntPoint(&ip); + return evaluate_point_impl(element_data, transformation, ip, workspace, context, fixed_geometry); + } + + MappingStatus DomainMapper::evaluate_volume( + const ElementMappingData &element_data, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &ip, + DMWorkspace &workspace, + VolumeMappingContext &context, + const DMReferenceDisplacementSample *displacement_sample, + const DMFixedMeshGeometry* fixed_geometry + ) const { + transformation.SetIntPoint(&ip); + std::expected sampled_geometry; + if (fixed_geometry == nullptr) { + sampled_geometry = sample_fixed_mesh_geometry(transformation, ip); + if (!sampled_geometry) { + return sampled_geometry.error(); + } + fixed_geometry = &*sampled_geometry; + } + const MappingStatus point_status = evaluate_point_impl( + element_data, + transformation, + ip, + workspace, + context.mapping, + *fixed_geometry, + displacement_sample + ); + + if (point_status != MappingStatus::VALID) { + return point_status; + } + + mfem::Mult(context.mapping.mapping_jacobian,fixed_geometry->jacobian, workspace.m_full_element_jacobian); + + context.quadrature.J_element_to_physical_inverse.SetSize(m_options.dimension, m_options.dimension); + mfem::CalcInverse(workspace.m_full_element_jacobian, context.quadrature.J_element_to_physical_inverse); + + context.quadrature.mapping_determinate = context.mapping.mapping_determinate; + context.quadrature.physical_integration_weight = ip.weight * fixed_geometry->weight * context.mapping.mapping_determinate; + + if (!is_finite(context.quadrature.J_element_to_physical_inverse) || !std::isfinite(context.quadrature.physical_integration_weight)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + if (context.quadrature.physical_integration_weight <= 0.0) { + return MappingStatus::NON_POSITIVE_DETERMINATE; + } + + return MappingStatus::VALID; + } + + MappingStatus DomainMapper::evaluate_face( + const ElementMappingData &element_data, + mfem::FaceElementTransformations &transformation, + const FaceElementSide side, + const mfem::IntegrationPoint &ip, + DMWorkspace &workspace, + FaceMappingContext &context + ) const { + transformation.SetAllIntPoints(&ip); + mfem::ElementTransformation& element_transformation = select_face_element_transformation(transformation, side); + const mfem::IntegrationPoint& element_integration_point = select_face_element_integration_point(transformation, side); + + const MappingStatus point_status = evaluate_point(element_data, element_transformation, element_integration_point, workspace, context.mapping); + if (point_status != MappingStatus::VALID) { + return point_status; + } + + // Get the normal vector from the face in reference space + workspace.m_reference_normal.SetSize(m_options.dimension); + mfem::CalcOrtho(transformation.Jacobian(), workspace.m_reference_normal); + if (side == FaceElementSide::ELEMENT_2) { + workspace.m_reference_normal *= -1.0; // Flip the normal for the second element (e.g. the "other" side of the face) + } + + const double reference_normal_magnitude = workspace.m_reference_normal.Norml2(); + if (!is_finite(reference_normal_magnitude) || reference_normal_magnitude <= 0.0) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + context.reference_normal.SetSize(m_options.dimension); + context.reference_normal = workspace.m_reference_normal; + context.reference_normal /= reference_normal_magnitude; + + // Get the normal vector from the face in physical space + context.mapping.inverse_mapping_jacobian.MultTranspose(workspace.m_reference_normal, workspace.m_mapped_normal); + workspace.m_mapped_normal *= context.mapping.mapping_determinate; + + const double mapped_normal_magnitude = workspace.m_mapped_normal.Norml2(); + if (!is_finite(mapped_normal_magnitude) || mapped_normal_magnitude <= 0.0) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + context.quadrature.normal.SetSize(m_options.dimension); + context.quadrature.normal = workspace.m_mapped_normal; + context.quadrature.normal /= mapped_normal_magnitude; + + context.reference_surface_weight = ip.weight * reference_normal_magnitude; + context.physical_surface_weight = ip.weight * mapped_normal_magnitude; + + context.quadrature.ds = context.reference_surface_weight; + context.quadrature.v_dot_n_scale = mapped_normal_magnitude / reference_normal_magnitude; + + if (!is_finite(context.quadrature.normal) || !is_finite(context.reference_surface_weight) || !is_finite(context.physical_surface_weight) || !is_finite(context.quadrature.v_dot_n_scale)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + return MappingStatus::VALID; + } + + MappingStatus DomainMapper::evaluate_point_variation( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &ip, + const MappingPointContext &context, + DMWorkspace &workspace, + MappingPointVariation &variation + ) const { + return evaluate_point_variation_impl(element_data, direction, transformation, ip, context, workspace, variation, nullptr); + } + + MappingStatus DomainMapper::evaluate_point_variation( + const ElementMappingData& element_data, + const ElementDisplacementData& direction, + mfem::ElementTransformation& transformation, + const mfem::IntegrationPoint& ip, + const MappingPointContext& context, + DMWorkspace& workspace, + MappingPointVariation& variation, + const mfem::DenseMatrix& inverse_mesh_jacobian + ) const { + if (inverse_mesh_jacobian.Height() != m_options.dimension || inverse_mesh_jacobian.Width() != m_options.dimension) { + return MappingStatus::INVALID_DIMENSION; + } + if (!is_finite(inverse_mesh_jacobian)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + if (direction.get_element().GetMapType() != mfem::FiniteElement::VALUE) { + // Only VALUE fields admit the reference-gradient pullback used by the reuse path. + return evaluate_point_variation(element_data, direction, transformation, ip, context, workspace, variation); + } + return evaluate_point_variation_impl(element_data, direction, transformation, ip, context, workspace, variation, &inverse_mesh_jacobian); + } + + MappingStatus DomainMapper::evaluate_volume_direction_variation( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &ip, + const VolumeMappingContext &context, + DMWorkspace &workspace, + VolumeMappingVariation &variation, + const DMReferenceDisplacementSample &direction_sample, + const CompactificationPointData* compactification_sample, + const mfem::DenseMatrix &inverse_mesh_jacobian + ) const { + if (inverse_mesh_jacobian.Height() != m_options.dimension || inverse_mesh_jacobian.Width() != m_options.dimension) { + return MappingStatus::INVALID_DIMENSION; + } + const MappingStatus point_status = evaluate_point_variation_impl(element_data, direction, transformation, ip, context.mapping, workspace, variation.mapping, &inverse_mesh_jacobian, &direction_sample, compactification_sample); + if (point_status != MappingStatus::VALID) { + return point_status; + } + + variation.weight_variation = (context.quadrature.physical_integration_weight / context.mapping.mapping_determinate) * variation.mapping.mapping_determinate_variation; + + if (!is_finite(variation.weight_variation)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + return MappingStatus::VALID; + } + + MappingStatus DomainMapper::evaluate_face_variation( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::FaceElementTransformations &transformation, + FaceElementSide side, + const mfem::IntegrationPoint &ip, + const FaceMappingContext &context, + DMWorkspace &workspace, + FaceMappingVariation &variation + ) const { + transformation.SetAllIntPoints(&ip); + mfem::ElementTransformation &element_transformation = select_face_element_transformation(transformation, side); + const mfem::IntegrationPoint &element_integration_point = select_face_element_integration_point(transformation, side); + + const MappingStatus point_status = evaluate_point_variation(element_data, direction, element_transformation, element_integration_point, context.mapping, workspace, variation.mapping); + if (point_status != MappingStatus::VALID) { + return point_status; + } + + workspace.m_reference_normal.SetSize(m_options.dimension); + mfem::CalcOrtho(transformation.Jacobian(), workspace.m_reference_normal); + if (side == FaceElementSide::ELEMENT_2) { + workspace.m_reference_normal *= -1.0; // Flip the normal for the second element (e.g. the "other" side of the face) + } + + const double reference_normal_magnitude = workspace.m_reference_normal.Norml2(); + if (!is_finite(reference_normal_magnitude) || reference_normal_magnitude <= 0) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + context.mapping.inverse_mapping_jacobian.MultTranspose(workspace.m_reference_normal, workspace.m_vector_temp); + workspace.m_mapped_normal = workspace.m_vector_temp; + workspace.m_mapped_normal *= context.mapping.mapping_determinate; + + variation.physical_normal_variation.SetSize(m_options.dimension); + variation.mapping.inverse_mapping_jacobian_variation.MultTranspose(workspace.m_reference_normal, variation.physical_normal_variation); + variation.physical_normal_variation *= context.mapping.mapping_determinate; + variation.physical_normal_variation.Add(variation.mapping.mapping_determinate_variation, workspace.m_vector_temp); + + const double mapped_normal_magnitude = workspace.m_mapped_normal.Norml2(); + if (!is_finite(mapped_normal_magnitude) || mapped_normal_magnitude <= 0) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + const double mapped_normal_magnitude_variation = context.quadrature.normal * variation.physical_normal_variation; + + variation.physical_normal_variation.Add(-mapped_normal_magnitude_variation, context.quadrature.normal); + variation.physical_normal_variation /= mapped_normal_magnitude; + + variation.physical_surface_weight_variation = ip.weight * mapped_normal_magnitude_variation; + variation.normal_flux_scale_variation = mapped_normal_magnitude_variation / reference_normal_magnitude; + + if (!is_finite(variation.physical_normal_variation) || !std::isfinite(variation.physical_surface_weight_variation) || !std::isfinite(variation.normal_flux_scale_variation)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + return MappingStatus::VALID; + } + + bool DomainMapper::is_compactified_element(const mfem::ElementTransformation &transformation) const noexcept { + return transformation.Attribute == m_options.vacuum_element_attribute_ID; + } + + int DomainMapper::get_dimension() const noexcept { + return m_options.dimension; + } + + const compactification::ExteriorDomainMap& DomainMapper::get_exterior_map() const noexcept { + return *m_exterior_map; + } + + MappingStatus DomainMapper::evaluate_point_impl( + const ElementMappingData &element_data, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &ip, + DMWorkspace &workspace, + MappingPointContext &context, + const DMFixedMeshGeometry& geometry, + const DMReferenceDisplacementSample *displacement_sample + ) const { + validate_element_data(element_data); + + //TODO: Factor into its own function + //TODO: Consider if these should return or throw. + if (workspace.get_dimension() != m_options.dimension) { + serif_error("Workspace dimension does not match Domain Mapper dimension. Workspace dimension: {}, Domain Mapper dimension: {}", workspace.get_dimension(), m_options.dimension); + } + if (transformation.GetSpaceDim() != m_options.dimension) { + serif_error("Element transformation space dimension does not match Domain Mapper dimension. Element transformation space dimension: {}, Domain Mapper dimension: {}", transformation.GetSpaceDim(), m_options.dimension); + } + if (transformation.GetGeometryType() != element_data.displacement.get_element().GetGeomType()) { + serif_error("Element transformation geometry type does not match element geometry type. Element transformation geometry type: {}, Element geometry type: {}", transformation.GetGeometryType(), element_data.displacement.get_element().GetGeomType()); + } + + + context.reference_position = geometry.position; + + // Get the displacement field value and its Jacobian at the integration point. Note these may be in current workspace to avoid repeated allocations and Multiplies. + if (displacement_sample) { + // TODO: Generally go through and refactor these large checks into their own functions, at the very least it will be easier to read + if (element_data.displacement.get_element().GetMapType() != mfem::FiniteElement::VALUE || displacement_sample->value.Size() != m_options.dimension || displacement_sample->gradient.Height() != m_options.dimension || displacement_sample->gradient.Width() != m_options.dimension) { + return MappingStatus::INVALID_DIMENSION; + } + + workspace.m_field_value = displacement_sample->value; + workspace.m_field_jacobian.SetSize(m_options.dimension); + mfem::Mult(displacement_sample->gradient, geometry.inverse_jacobian, workspace.m_field_jacobian); + } else { + evaluate_displacement_field(element_data.displacement, transformation, ip, workspace, workspace.m_field_value, workspace.m_field_jacobian, &geometry.inverse_jacobian); + } + + if (!is_finite(context.reference_position) || !is_finite(workspace.m_field_value) || !is_finite(workspace.m_field_jacobian)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + + context.displaced_position.SetSize(m_options.dimension); + + // Get the position of the point in physical space by adding the displacement to the reference position. Note MFEM really dislikes raw arithmetic operators + // so we need to first assign the reference position then use the in place += operator. + context.displaced_position = context.reference_position; + context.displaced_position += workspace.m_field_value; + + context.displacement_jacobian.SetSize(m_options.dimension, m_options.dimension); + context.displacement_jacobian = workspace.m_field_jacobian; + + // Ensure that the diagonal of the displacement Jacobian is incremented by 1.0 to account for the identity mapping from reference to physical space. + // recall that r = x + d (where d is the workspace.m_field_value and x is context.reference_position) then we can differentiate this + // component wise to find the gradient of the displaced position wrt. the mesh coordinate (reference position). E.g as you move along + // the mesh coordinate how much does the physical coordinate change and in what direction. Lets call this F + // F = \frac{\partial r_i}{\partial x_j} where r is the displaced position and x is the mesh position. + // We then have F = \frac{\partial x_i}{\partial x_j} + \frac{\partial d_i}{x_j} where d is the displacement (recall r = x + d) + // By definition the first term is the identity matrix. The second term we get out of EvaluateField. Thus why we need to add the identity matrix here + for (int i = 0; i < m_options.dimension; ++i) { + context.displacement_jacobian(i, i) += 1.0; + } + + context.compactified = is_compactified_element(transformation); + + // This branch only runs for vacuum elements + if (context.compactified) { + // There are two things that we need to the mapping. First is a reference coordinate which stroid embeds into the mesh at mesh generation time, this is + // parameterized from 0 - 1 where 0 is the model surface and 1 is the mesh exterior (what will becomes the compactified infinity, note also we never actually evaluate at s=1; rather we define some arbitrary small tolerance to approach s=1). Lets call this s. We also need + // the gradient of s as we move along the mesh coordinates. All of this is stashes within workspace.m_compactification_point. + const MappingStatus coordinate_status = evaluate_compactification_coordinate(element_data.compactification, transformation, ip, workspace, workspace.m_compactification_point, &geometry.inverse_jacobian); + + if (coordinate_status != MappingStatus::VALID) { + return coordinate_status; + } + + const compactification::ExteriorMapInput exterior_input{ + .reference_position = context.reference_position, + .displaced_position = context.displaced_position, + .displacement_jacobian = context.displacement_jacobian, + .compactification_coordinate_gradient = workspace.m_compactification_point.coordinate_gradient, + .compactification_coordinate = workspace.m_compactification_point.compactification_coordinate, + }; + + // This apply whatever the exterior map is to generate the new physical exterior coordinate and jacobian between physical and reference space. + // In general we have only implemented a kelvin mapping; however, in future additional mappings may be implemented. + const MappingStatus exterior_status = m_exterior_map->evaluate(exterior_input, workspace.m_exterior_result); + if (exterior_status != MappingStatus::VALID) { + return exterior_status; + } + + context.physical_position = workspace.m_exterior_result.physical_position; + context.mapping_jacobian = workspace.m_exterior_result.mapping_jacobian; + } else { + context.physical_position = context.displaced_position; + context.mapping_jacobian = context.displacement_jacobian; + } + + // Validation work + if (!is_finite(context.physical_position) || !is_finite(context.mapping_jacobian)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + context.mapping_determinate = context.mapping_jacobian.Det(); + if (!std::isfinite(context.mapping_determinate)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + if (context.mapping_determinate <= 0.0) { + // This is the most common error we see come out of this function, specifically it is common when we try to deform the mesh too much in one step. + return MappingStatus::NON_POSITIVE_DETERMINATE; + } + + context.inverse_mapping_jacobian.SetSize(m_options.dimension, m_options.dimension); + + // Finally we get the inverse jacobian. + mfem::CalcInverse(context.mapping_jacobian, context.inverse_mapping_jacobian); + + if (!is_finite(context.inverse_mapping_jacobian)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + return MappingStatus::VALID; + } + + void DomainMapper::validate_element_data(const ElementMappingData &element_data) const { + const ElementDisplacementData &displacement = element_data.displacement; + const ElementCompactificationData &compactification = element_data.compactification; + + if (displacement.get_dimension() != m_options.dimension) { + serif_error("Element displacement dimension does not match Domain Mapper dimension. Element displacement dimension: {}, Domain Mapper dimension: {}", displacement.get_dimension(), m_options.dimension); + } + + if (displacement.get_element().GetDim() != m_options.dimension) { + serif_error("Element displacement finite element dimension does not match Domain Mapper dimension. Element displacement finite element dimension: {}, Domain Mapper dimension: {}", displacement.get_element().GetDim(), m_options.dimension); + } + + if (compactification.get_element().GetDim() != m_options.dimension) { + serif_error("Element compactification finite element dimension does not match Domain Mapper dimension. Element compactification finite element dimension: {}, Domain Mapper dimension: {}", compactification.get_element().GetDim(), m_options.dimension); + } + + if (displacement.get_element().GetGeomType() != compactification.get_element().GetGeomType()) { + serif_error("Element displacement finite element geometry type does not match element compactification finite element geometry type. Element displacement finite element geometry type: {}, Element compactification finite element geometry type: {}", displacement.get_element().GetGeomType(), compactification.get_element().GetGeomType()); + } + + if (compactification.get_element().GetRangeType() != mfem::FiniteElement::SCALAR) { + serif_error("Element compactification finite element range type is not scalar. Element compactification finite element range type: {}", compactification.get_element().GetRangeType()); + } + + if (compactification.get_element().GetMapType() != mfem::FiniteElement::VALUE) { + serif_error("Element compactification finite element map type is not value. Element compactification finite element map type: {}", compactification.get_element().GetMapType()); + } + + if (compactification.get_element().GetDerivType() != mfem::FiniteElement::GRAD) { + serif_error("Element compactification finite element derivative type is not gradient. Element compactification finite element derivative type: {}", compactification.get_element().GetDerivType()); + } + + if (compactification.get_dof_count() != compactification.get_element().GetDof()) { + serif_error("Element compactification finite element DOF count does not match the number of DOFs in the finite element. Element compactification finite element DOF count: {}, Finite element DOF count: {}", compactification.get_dof_count(), compactification.get_element().GetDof()); + } + } + + // Note that there is nothing preventing this function from being used with some field other than displacement; perhaps we should tighten this? + void DomainMapper::evaluate_displacement_field( + const ElementDisplacementData &field, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &ip, + DMWorkspace &workspace, + mfem::Vector &value, + mfem::DenseMatrix &jacobian, + const mfem::DenseMatrix *inverse_mesh_jacobian + ) const { + const mfem::FiniteElement &element = field.get_element(); + const mfem::DenseMatrix &dof_matrix = field.get_dof_matrix(); + + workspace.m_shape.SetSize(element.GetDof()); + + element.CalcShape(ip, workspace.m_shape); + value.SetSize(m_options.dimension); + dof_matrix.MultTranspose(workspace.m_shape, value); + jacobian.SetSize(m_options.dimension, m_options.dimension); + + if (inverse_mesh_jacobian != nullptr || element.GetMapType() == mfem::FiniteElement::VALUE) { + workspace.m_reference_dshape.SetSize(element.GetDof(), m_options.dimension); + element.CalcDShape(ip, workspace.m_reference_dshape); + // Contract DOFs before applying fixed-mesh geometry. + mfem::MultAtB(dof_matrix, workspace.m_reference_dshape, workspace.m_reference_field_jacobian); + const mfem::DenseMatrix &inverseMeshJacobian = inverse_mesh_jacobian != nullptr ? *inverse_mesh_jacobian : transformation.InverseJacobian(); + mfem::Mult(workspace.m_reference_field_jacobian, inverseMeshJacobian, jacobian); + } else { + // Retain the original finite-element-specific physical-gradient + // path for mapping types without the ordinary VALUE pullback. + workspace.m_mesh_dshape.SetSize(element.GetDof(), m_options.dimension); + element.CalcPhysDShape(transformation, workspace.m_mesh_dshape); + mfem::MultAtB(dof_matrix, workspace.m_mesh_dshape, jacobian); + } + } + + MappingStatus DomainMapper::evaluate_compactification_coordinate( + const ElementCompactificationData &compactification, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &ip, + DMWorkspace &workspace, + CompactificationPointData &point_data, + const mfem::DenseMatrix *inverse_mesh_jacobian + ) const { + const mfem::FiniteElement& element = compactification.get_element(); + const mfem::Vector& dofs = compactification.get_dofs(); + const int dof_count = element.GetDof(); + + if (workspace.get_dimension() != m_options.dimension || transformation.GetSpaceDim() != m_options.dimension || element.GetDim() != m_options.dimension) { + return MappingStatus::INVALID_DIMENSION; + } + + if (dofs.Size() != dof_count) { + return MappingStatus::INVALID_DIMENSION; + } + + for (int i = 0; i < dof_count; ++i) { + if (!is_finite(dofs(i))) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + } + + if (inverse_mesh_jacobian != nullptr) { + if (inverse_mesh_jacobian->Height() != m_options.dimension || inverse_mesh_jacobian->Width() != m_options.dimension) { + return MappingStatus::INVALID_DIMENSION; + } + if (!is_finite(*inverse_mesh_jacobian)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + } + transformation.SetIntPoint(&ip); + workspace.m_compactification_shape.SetSize(dof_count); + workspace.m_compactification_dshape.SetSize(dof_count, m_options.dimension); + + element.CalcShape(ip, workspace.m_compactification_shape); + if (inverse_mesh_jacobian != nullptr) { + workspace.m_reference_dshape.SetSize(dof_count, m_options.dimension); + element.CalcDShape(ip, workspace.m_reference_dshape); + mfem::Mult(workspace.m_reference_dshape, *inverse_mesh_jacobian, workspace.m_compactification_dshape); + } else { + element.CalcPhysDShape(transformation, workspace.m_compactification_dshape); + } + + point_data.compactification_coordinate = dofs * workspace.m_compactification_shape; + point_data.coordinate_gradient.SetSize(m_options.dimension); + workspace.m_compactification_dshape.MultTranspose(dofs, point_data.coordinate_gradient); + + if (!is_finite(point_data.compactification_coordinate)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + for (int d = 0; d < point_data.coordinate_gradient.Size(); ++d) { + if (!is_finite(point_data.coordinate_gradient(d))) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + } + + return MappingStatus::VALID; + } + + MappingStatus DomainMapper::evaluate_point_variation_impl( + const ElementMappingData &element_data, + const ElementDisplacementData &direction, + mfem::ElementTransformation &transformation, + const mfem::IntegrationPoint &ip, + const MappingPointContext &context, + DMWorkspace &workspace, + MappingPointVariation &variation, + const mfem::DenseMatrix *inverse_mesh_jacobian, + const DMReferenceDisplacementSample* direction_sample, + const CompactificationPointData* compactification_sample + ) const { + validate_element_data(element_data); + // A direction that is the element's own displacement data was validated just above. + if (&direction != &element_data.displacement) { + const ElementMappingData direction_data { + .displacement = direction, + .compactification = element_data.compactification + }; + + validate_element_data(direction_data); + } + + if (element_data.displacement.get_dof_count() != direction.get_dof_count()) { + serif_error("Element displacement DOF count does not match direction DOF count. Element displacement DOF count: {}, Direction DOF count: {}", element_data.displacement.get_dof_count(), direction.get_dof_count()); + } + + if (workspace.get_dimension() != m_options.dimension) { + serif_error("Workspace dimension does not match Domain Mapper dimension. Workspace dimension: {}, Domain Mapper dimension: {}", workspace.get_dimension(), m_options.dimension); + } + + if (context.compactified != is_compactified_element(transformation)) { + serif_error("Mapping context compactification status does not match element transformation compactification status. Mapping context compactified: {}, Element transformation compactified: {}", context.compactified, is_compactified_element(transformation)); + } + + transformation.SetIntPoint(&ip); + // Tensor contractions and transpose actions can supply canonical value/gradient samples directly. + if (direction_sample) { + if (direction.get_element().GetMapType() != mfem::FiniteElement::VALUE || direction_sample->value.Size() != m_options.dimension || direction_sample->gradient.Height() != m_options.dimension || direction_sample->gradient.Width() != m_options.dimension) { + return MappingStatus::INVALID_DIMENSION; + } + workspace.m_field_value = direction_sample->value; + workspace.m_field_jacobian.SetSize(m_options.dimension); + mfem::Mult(direction_sample->gradient, inverse_mesh_jacobian ? *inverse_mesh_jacobian : transformation.InverseJacobian(), workspace.m_field_jacobian); + } else { + evaluate_displacement_field(direction, transformation, ip, workspace, workspace.m_field_value, workspace.m_field_jacobian, inverse_mesh_jacobian); + } + + if (!is_finite(workspace.m_field_jacobian) || !is_finite(workspace.m_field_value)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + + variation.displacement_variation = workspace.m_field_value; + variation.displacement_jacobian_variation = workspace.m_field_jacobian; + + if (context.compactified) { + if (compactification_sample == nullptr) { + const MappingStatus coordinate_status = evaluate_compactification_coordinate(element_data.compactification, transformation, ip, workspace, workspace.m_compactification_point, inverse_mesh_jacobian); + if (coordinate_status != MappingStatus::VALID) { + return coordinate_status; + } + compactification_sample = &workspace.m_compactification_point; + } + if (compactification_sample->coordinate_gradient.Size() != m_options.dimension) { + return MappingStatus::INVALID_DIMENSION; + } + if (!is_finite(compactification_sample->compactification_coordinate) || !is_finite(compactification_sample->coordinate_gradient)) { + return MappingStatus::NON_FINITE_MAPPING_INPUT; + } + + const compactification::ExteriorMapInput exterior_input{ + .reference_position = context.reference_position, + .displaced_position = context.displaced_position, + .displacement_jacobian = context.displacement_jacobian, + .compactification_coordinate_gradient = compactification_sample->coordinate_gradient, + .compactification_coordinate = compactification_sample->compactification_coordinate, + }; + + workspace.m_exterior_result.physical_position = context.physical_position; + workspace.m_exterior_result.mapping_jacobian = context.mapping_jacobian; + + const compactification::ExteriorMapDirection exterior_direction { + .displaced_position_variation = variation.displacement_variation, + .displacement_jacobian_variation = variation.displacement_jacobian_variation + }; + + const MappingStatus exterior_status = m_exterior_map->evaluate_variation(exterior_input, workspace.m_exterior_result, exterior_direction, workspace.m_exterior_variation); + if (exterior_status != MappingStatus::VALID) { + return exterior_status; + } + + variation.physical_position_variation = workspace.m_exterior_variation.physical_position_variation; + variation.mapping_jacobian_variation = workspace.m_exterior_variation.mapping_jacobian_variation; + } else { + variation.physical_position_variation = variation.displacement_variation; + variation.mapping_jacobian_variation = variation.displacement_jacobian_variation; + } + + mfem::Mult(context.inverse_mapping_jacobian, variation.mapping_jacobian_variation, workspace.m_matrix_temp_1); + double trace = 0.0; + for (int dimID = 0; dimID < m_options.dimension; ++dimID) { + trace += workspace.m_matrix_temp_1(dimID, dimID); + } + variation.mapping_determinate_variation = context.mapping_determinate * trace; + + variation.inverse_mapping_jacobian_variation.SetSize(m_options.dimension, m_options.dimension); + mfem::Mult(workspace.m_matrix_temp_1, context.inverse_mapping_jacobian, variation.inverse_mapping_jacobian_variation); + variation.inverse_mapping_jacobian_variation *= -1.0; + + if (!is_finite(variation.physical_position_variation) || !is_finite(variation.mapping_jacobian_variation) || !std::isfinite(variation.mapping_determinate_variation) || !is_finite(variation.inverse_mapping_jacobian_variation)) { + return MappingStatus::NON_FINITE_MAPPING_RESULT; + } + + return MappingStatus::VALID; + } + + mfem::ElementTransformation &DomainMapper::select_face_element_transformation( + const mfem::FaceElementTransformations &transformations, + const FaceElementSide side + ) { + if (side == FaceElementSide::ELEMENT_1) { + serif_assert(transformations.Elem1 != nullptr, "Face element transformation for ELEMENT_1 is null."); + return *transformations.Elem1; + } + + serif_assert(transformations.Elem2 != nullptr, "Face element transformation for ELEMENT_2 is null."); + return *transformations.Elem2; + } + + const mfem::IntegrationPoint &DomainMapper::select_face_element_integration_point( + const mfem::FaceElementTransformations &transformations, + const FaceElementSide side + ) { + mfem::ElementTransformation& element_transformation = select_face_element_transformation(transformations, side); + return element_transformation.GetIntPoint(); + + } +} diff --git a/src/lib/serif/backend/mfem/mapping/evaluator.cpp b/src/lib/serif/backend/mfem/mapping/evaluator.cpp new file mode 100644 index 00000000..a46a11da --- /dev/null +++ b/src/lib/serif/backend/mfem/mapping/evaluator.cpp @@ -0,0 +1,131 @@ +#include "serif/backend/mfem/mapping/evaluator.hpp" + +#include "serif/utils/error/errors.hpp" + + +namespace serif::mapping { + using namespace utils::errors; + + GridFunctionMappingEvaluator::GridFunctionMappingEvaluator( + const DomainMapper &mapper, + const mfem::GridFunction &displacement, + const mfem::GridFunction &compactification_coordinate + ) : + m_mapper(mapper), + m_displacement(displacement), + m_compactification_coordinate(compactification_coordinate), + m_displacement_space(displacement.FESpace()), + m_compactification_space(compactification_coordinate.FESpace()), + m_displacement_space_sequence(m_displacement_space != nullptr ? m_displacement_space->GetSequence() : -1), + m_compactification_space_sequence(m_compactification_space != nullptr ? m_compactification_space->GetSequence() : -1), + m_workspace(mapper.get_dimension()) { + serif_ensure(m_displacement_space != nullptr, "Displacement grid function is not associated with a finite-element space."); + serif_ensure(m_compactification_space != nullptr, "Compactification-coordinate grid function is not associated with a finite-element space."); + serif_ensure(m_displacement_space->GetMesh() == m_compactification_space->GetMesh(), "Displacement and compactification-coordinate grid functions are not associated with the same mesh."); + serif_ensure(m_displacement.VectorDim() == mapper.get_dimension(), "Displacement grid function vector dimension does not match the mapper dimension. ({} vs. {})", m_displacement.VectorDim(), mapper.get_dimension()); + serif_ensure(m_compactification_coordinate.VectorDim() == 1, "Compactification-coordinate grid function must be a scalar field. Instead it has vector dimension {}.", m_compactification_coordinate.VectorDim()); + serif_ensure(m_displacement_space->GetMesh()->SpaceDimension() == mapper.get_dimension(), "Displacement finite-element space mesh dimension does not match the mapper dimension. ({} vs. {})", m_displacement_space->GetMesh()->SpaceDimension(), mapper.get_dimension()); + } + + void GridFunctionMappingEvaluator::invalidate_cache() noexcept { + m_displacement_data.reset(); + m_compactification_data.reset(); + m_cached_element_id = -1; + } + + void GridFunctionMappingEvaluator::validate_field_bindings() const { + serif_ensure(m_displacement.FESpace() == m_displacement_space, "Displacement grid function is no longer associated with the cached finite-element space."); + serif_ensure(m_compactification_coordinate.FESpace() == m_compactification_space, "Compactification-coordinate grid function is no longer associated with the cached finite-element space."); + } + + bool GridFunctionMappingEvaluator::invalidate_for_changed_spaces() { + const long displacement_sequence = m_displacement_space->GetSequence(); + const long compactification_sequence = m_compactification_space->GetSequence(); + + if (displacement_sequence == m_displacement_space_sequence && compactification_sequence == m_compactification_space_sequence) { + return false; + } + + invalidate_cache(); + + m_displacement_space_sequence = displacement_sequence; + m_compactification_space_sequence = compactification_sequence; + return true; + } + + void GridFunctionMappingEvaluator::refresh() { + validate_field_bindings(); + + if (invalidate_for_changed_spaces()) { + return; + } + + const int element_id = m_cached_element_id; + invalidate_cache(); + if (element_id >= 0) { + load_element(element_id); + } + } + + void GridFunctionMappingEvaluator::load_element(const int element_id) { + validate_field_bindings(); + (void)invalidate_for_changed_spaces(); + + if (element_id == m_cached_element_id) { + return; + } + + const mfem::FiniteElementSpace& displacement_space = *m_displacement_space; + const mfem::FiniteElementSpace& compactification_space = *m_compactification_space; + + serif_ensure(element_id >= 0, "Element ID must be non-negative. Instead {} was provided.", element_id); + serif_ensure(element_id < displacement_space.GetMesh()->GetNE(), "Element ID must be less than the number of elements in the mesh ({}). Instead {} was provided.", displacement_space.GetMesh()->GetNE(), element_id); + + mfem::DofTransformation *displacement_transformation = displacement_space.GetElementVDofs(element_id, m_displacement_dofs); + compactification_space.GetElementDofs(element_id, m_compactification_dofs); + + m_displacement.GetSubVector(m_displacement_dofs, m_element_displacement); + m_compactification_coordinate.GetSubVector(m_compactification_dofs, m_element_compactification); + + if (displacement_transformation != nullptr) { + displacement_transformation->InvTransformPrimal(m_element_displacement); + } + + const mfem::FiniteElement &displacement_element = *displacement_space.GetFE(element_id); + const mfem::FiniteElement &compactification_element = *compactification_space.GetFE(element_id); + + m_displacement_data = std::make_unique(element_displacement_data_from_element_vdofs(displacement_element, m_element_displacement)); + m_compactification_data = std::make_unique(compactification_element, m_element_compactification); + m_cached_element_id = element_id; + } + + MappingStatus GridFunctionMappingEvaluator::evaluate_point(mfem::ElementTransformation &transformation, const mfem::IntegrationPoint &integration_point, MappingPointContext &context) { + load_element(transformation.ElementNo); + const ElementMappingData data{ + .displacement = *m_displacement_data, + .compactification = *m_compactification_data + }; + return m_mapper.evaluate_point(data, transformation, integration_point, m_workspace, context); + } + + MappingStatus GridFunctionMappingEvaluator::evaluate_volume(mfem::ElementTransformation &transformation, const mfem::IntegrationPoint &integration_point, VolumeMappingContext &context) { + load_element(transformation.ElementNo); + const ElementMappingData data{ + .displacement = *m_displacement_data, + .compactification = *m_compactification_data + }; + return m_mapper.evaluate_volume(data, transformation, integration_point, m_workspace, context); + } + + MappingStatus GridFunctionMappingEvaluator::evaluate_face(mfem::FaceElementTransformations &transformations, FaceElementSide side, const mfem::IntegrationPoint &integration_point, FaceMappingContext &context) { + mfem::ElementTransformation *transformation = side == FaceElementSide::ELEMENT_1 ? transformations.Elem1 : transformations.Elem2; + serif_ensure(transformation != nullptr, "Face evaluation requested for a side that does not exist."); + + load_element(transformation->ElementNo); + const ElementMappingData data{ + .displacement = *m_displacement_data, + .compactification = *m_compactification_data + }; + return m_mapper.evaluate_face(data, transformations, side, integration_point, m_workspace, context); + } +} diff --git a/src/lib/serif/backend/mfem/mapping/transformations.cpp b/src/lib/serif/backend/mfem/mapping/transformations.cpp new file mode 100644 index 00000000..774562dd --- /dev/null +++ b/src/lib/serif/backend/mfem/mapping/transformations.cpp @@ -0,0 +1,78 @@ +#include "serif/backend/mfem/mapping/transformations.hpp" +#include "serif/utils/error/errors.hpp" + +#include "mfem/linalg/vector.hpp" +#include "mfem/linalg/densemat.hpp" + +namespace serif::mapping { + using namespace serif::utils::errors; + + void map_reference_gradient_to_physical(const MappingPointContext &context, const mfem::Vector &reference_gradient, mfem::Vector &physical_gradient) { + serif_assert(reference_gradient.Size() == context.inverse_mapping_jacobian.Height(), "Reference gradient vector has incompatible dimension with the inverse mapping jacobian ({} <=> {}).", reference_gradient.Size(), context.inverse_mapping_jacobian.Height()); + + physical_gradient.SetSize(reference_gradient.Size()); + context.inverse_mapping_jacobian.MultTranspose(reference_gradient, physical_gradient); + } + + void map_physical_gradient_to_reference(const MappingPointContext &context, const mfem::Vector &physical_gradient, mfem::Vector &reference_gradient) { + serif_assert(physical_gradient.Size() == context.mapping_jacobian.Height(), "Physical gradient vector has incompatible dimension with the mapping jacobian ({} <=> {}).", physical_gradient.Size(), context.mapping_jacobian.Height()); + + reference_gradient.SetSize(physical_gradient.Size()); + context.mapping_jacobian.MultTranspose(physical_gradient, reference_gradient); + } + + void map_reference_vector_gradient_to_physical(const MappingPointContext &context, const mfem::DenseMatrix &reference_vector_gradient, mfem::DenseMatrix &physical_vector_gradient) { + serif_assert(reference_vector_gradient.Width() == context.inverse_mapping_jacobian.Height(), "Reference vector gradient matrix has incompatible dimension with the inverse mapping jacobian ({} <=> {}).", reference_vector_gradient.Width(), context.inverse_mapping_jacobian.Height()); + + physical_vector_gradient.SetSize(reference_vector_gradient.Height(), context.inverse_mapping_jacobian.Width()); + mfem::Mult(reference_vector_gradient, context.inverse_mapping_jacobian, physical_vector_gradient); + } + + void map_physical_vector_gradient_to_reference(const MappingPointContext &context, const mfem::DenseMatrix &physical_vector_gradient, mfem::DenseMatrix &reference_vector_gradient) { + serif_assert(physical_vector_gradient.Width() == context.mapping_jacobian.Height(), "Physical vector gradient matrix has incompatible dimension with the mapping jacobian ({} <=> {}).", physical_vector_gradient.Width(), context.mapping_jacobian.Height()); + + reference_vector_gradient.SetSize(physical_vector_gradient.Height(), context.mapping_jacobian.Width()); + mfem::Mult(physical_vector_gradient, context.mapping_jacobian, reference_vector_gradient); + } + + void compute_scalar_diffusion_tensor(const MappingPointContext &context, mfem::DenseMatrix &diffusion_tensor) { + const int dimension = context.mapping_jacobian.Height(); + serif_assert((context.inverse_mapping_jacobian.Width() == dimension) && (context.inverse_mapping_jacobian.Height() == dimension), "Inverse mapping jacobian is not square ({} x {}). It is expected to have shape {}x{}", context.inverse_mapping_jacobian.Height(), context.inverse_mapping_jacobian.Width(), dimension, dimension); + serif_assert(context.mapping_determinate > 0.0, "Mapping jacobian determinant is non-positive ({}). The mapping may be inverted or degenerate.", context.mapping_determinate); + + diffusion_tensor.SetSize(dimension, dimension); + mfem::MultABt(context.inverse_mapping_jacobian, context.inverse_mapping_jacobian, diffusion_tensor); + diffusion_tensor *= context.mapping_determinate; + } + + void map_hcurl_field_to_physical(const MappingPointContext &context, const mfem::Vector &reference_field, mfem::Vector &physical_field) { + serif_assert(reference_field.Size() == context.inverse_mapping_jacobian.Height(), "Reference field vector has incompatible dimension with the inverse mapping jacobian ({} <=> {}).", reference_field.Size(), context.inverse_mapping_jacobian.Height()); + + physical_field.SetSize(reference_field.Size()); + context.inverse_mapping_jacobian.MultTranspose(reference_field, physical_field); + } + + void map_physical_field_to_hcurl_reference(const MappingPointContext &context, const mfem::Vector &physical_field, mfem::Vector &reference_field) { + serif_assert(physical_field.Size() == context.mapping_jacobian.Height(), "Physical field vector has incompatible dimension with the mapping jacobian ({} <=> {}).", physical_field.Size(), context.mapping_jacobian.Height()); + + reference_field.SetSize(physical_field.Size()); + context.mapping_jacobian.MultTranspose(physical_field, reference_field); + } + + void map_hcurl_curl_to_physical(const MappingPointContext &context, const mfem::Vector &reference_curl, mfem::Vector &physical_curl) { + serif_assert(reference_curl.Size() == context.mapping_jacobian.Width(), "Reference curl vector has incompatible dimension with the mapping jacobian ({} <=> {}).", reference_curl.Size(), context.mapping_jacobian.Height()); + + physical_curl.SetSize(reference_curl.Size()); + context.mapping_jacobian.Mult(reference_curl, physical_curl); + physical_curl /= context.mapping_determinate; + } + + void map_physical_curl_to_hcurl_reference(const MappingPointContext &context, const mfem::Vector &physical_curl, mfem::Vector &reference_curl) { + serif_assert(physical_curl.Size() == context.inverse_mapping_jacobian.Width(), "Physical curl vector has incompatible dimension with the inverse mapping jacobian ({} <=> {}).", physical_curl.Size(), context.inverse_mapping_jacobian.Height()); + + reference_curl.SetSize(physical_curl.Size()); + context.inverse_mapping_jacobian.Mult(physical_curl, reference_curl); + reference_curl *= context.mapping_determinate; + } + +} diff --git a/src/lib/serif/backend/mfem/mapping/volume_cache.cpp b/src/lib/serif/backend/mfem/mapping/volume_cache.cpp new file mode 100644 index 00000000..adfd7747 --- /dev/null +++ b/src/lib/serif/backend/mfem/mapping/volume_cache.cpp @@ -0,0 +1,112 @@ +#include "serif/backend/mfem/mapping/volume_cache.hpp" +#include "serif/utils/error/errors.hpp" + +#include "mfem/linalg/densemat.hpp" +#include "mfem/linalg/vector.hpp" + +namespace serif::mapping { + using namespace utils::errors; + namespace { + // Replace all of these with a fast serialization class at some point. Probably in something like a serif/serialize module + void pack_vector(double*& destination, const mfem::Vector& vector, const int dim) { + serif_ensure(vector.Size() == dim, "Vector dimension {} does not match volume mapping cache dimension {}.", vector.Size(), dim); + std::copy_n(vector.HostRead(), dim, destination); + destination += dim; + } + + void pack_matrix(double*& destination, const mfem::DenseMatrix& matrix, const int dim) { + serif_ensure(matrix.Height() == dim && matrix.Width() == dim, "Matrix shape {}x{} does not match volume mapping cache dimension {}.", matrix.Height(), matrix.Width(), dim); + std::copy_n(matrix.HostRead(), dim * dim, destination); + destination += dim * dim; + } + + void unpack_vector(const double*& source, mfem::Vector& vector, const int dim) { + vector.SetSize(dim); + std::copy_n(source, dim, vector.HostWrite()); + source += dim; + } + + void unpack_matrix(const double*& source, mfem::DenseMatrix& matrix, const int dim) { + matrix.SetSize(dim, dim); + std::copy_n(source, dim * dim, matrix.HostWrite()); + source += dim * dim; + } + } + + void VolumeMappingCache::set_size(const int point_count, const int dimension) { + serif_ensure(point_count >= 0, "Point count must be non-negative for volume mapping cache. Point count: {}", point_count); + serif_ensure(dimension > 0 && dimension <= 3, "Invalid dimension for volume mapping cache. Dimension: {}", dimension); + const int stride = 3 * dimension + 4 * dimension * dimension + 4; + m_data.resize(static_cast(point_count) * stride); + m_point_count = point_count; + m_dimension = dimension; + m_point_stride = stride; + } + + void VolumeMappingCache::validate_point(const int point) const { + serif_ensure(point >= 0 && point < m_point_count, "Point index {} is out of range for volume mapping cache with {} points", point, m_point_count); + } + + const double *VolumeMappingCache::get_point_data(const int point) const { + validate_point(point); + return m_data.data() + static_cast(point) * m_point_stride; + } + + void VolumeMappingCache::store(int point, VolumeMappingContext &context) { + validate_point(point); + double *data = m_data.data() + static_cast(point) * m_point_stride; + + // What we build here is a FIFO data structure. we pack + // all the raw data from the mapping context into a single contiguous array of doubles. + // This allows us to avoid repeated allocations as the vector will grow to some appropriate size and then stay roughly there. + pack_vector(data, context.mapping.reference_position, m_dimension); + pack_vector(data, context.mapping.displaced_position, m_dimension); + pack_vector(data, context.mapping.physical_position, m_dimension); + + pack_matrix(data, context.mapping.displacement_jacobian, m_dimension); + pack_matrix(data, context.mapping.mapping_jacobian, m_dimension); + pack_matrix(data, context.mapping.inverse_mapping_jacobian, m_dimension); + pack_matrix(data, context.quadrature.J_element_to_physical_inverse, m_dimension); + + *data++ = context.mapping.mapping_determinate; + *data++ = context.mapping.compactified ? 1.0 : 0.0; // its possible this could cause issues, have not run into but when unpacked I worry that exact equality to some double value could be a problem. If there are wierd issues this may be a place to look, + *data++ = context.quadrature.mapping_determinate; + *data = context.quadrature.physical_integration_weight; + } + + void VolumeMappingCache::load(int point, VolumeMappingContext &context) const { + const double *data = get_point_data(point); + + // FIFO data structure so need to unpack in the same order as we packed. + // TODO: Redo this with some sort of fast serialization class so we do not need to worry about ordering + unpack_vector(data, context.mapping.reference_position, m_dimension); + unpack_vector(data, context.mapping.displaced_position, m_dimension); + unpack_vector(data, context.mapping.physical_position, m_dimension); + + unpack_matrix(data, context.mapping.displacement_jacobian, m_dimension); + unpack_matrix(data, context.mapping.mapping_jacobian, m_dimension); + unpack_matrix(data, context.mapping.inverse_mapping_jacobian, m_dimension); + unpack_matrix(data, context.quadrature.J_element_to_physical_inverse, m_dimension); + + context.mapping.mapping_determinate = *data++; + context.mapping.compactified = *data++ != 0.0; // See comment in store() + context.quadrature.mapping_determinate = *data++; + context.quadrature.physical_integration_weight = *data; + } + + void VolumeMappingCache::load_inverse_jacobian(int point, mfem::DenseMatrix &inverse) const { + // Again this is unsafe, and really should be replaced. This was a quick cache scheme that solved the direct issue I had; however, it should be replaced + // with something that has at least a modicum of safety built in (we are pretty good about this in other parts of SERiF but its about 8pm and + // Im quite tired.... + const double *data = get_point_data(point) + 3 * m_dimension + 3 * m_dimension * m_dimension; // skip to the inverse jacobian, this is just pointer math...Think back to CS-101. Do that have that in the future, are we still learning about memory like this is a PDP-11? + unpack_matrix(data, inverse, m_dimension); + } + + int VolumeMappingCache::get_point_count() const noexcept { + return m_point_count; + } + + int VolumeMappingCache::get_dimension() const noexcept { + return m_dimension; + } +} diff --git a/src/lib/serif/backend/mfem/model/deformation.cpp b/src/lib/serif/backend/mfem/model/deformation.cpp new file mode 100644 index 00000000..90150087 --- /dev/null +++ b/src/lib/serif/backend/mfem/model/deformation.cpp @@ -0,0 +1,22 @@ +#include "serif/backend/mfem/model/deformation.hpp" + +#include "mfem/linalg/sparsemat.hpp" +#include "serif/backend/mfem/geometry/deformation/displacement_mapping.hpp" + +namespace serif::model::backend::mfem { + LinearizedBlock displacement_projection(const discretization::backend::mfem::DofMap& binding) { + binding.validate(); + auto matrix = std::make_shared<::mfem::SparseMatrix>(binding.size(), binding.full_size()); + geometry::deformation::backend::mfem::for_each_displacement_coefficient(binding, [&](const int row, const int column) { + matrix->Add(row, column, 1.0); + }); + matrix->Finalize(); + return operators::backend::mfem::linearized_block(std::move(matrix)); + } + + namespace detail { + void require_deformation_action(const geometry::deformation::DeformationStatus& status) { + utils::errors::serif_ensure(status.has_value(), "Accepted model deformation action failed: {}", status ? "" : status.error().message); + } + } +} diff --git a/src/lib/serif/backend/mfem/model/finite_elements.cpp b/src/lib/serif/backend/mfem/model/finite_elements.cpp new file mode 100644 index 00000000..69a12742 --- /dev/null +++ b/src/lib/serif/backend/mfem/model/finite_elements.cpp @@ -0,0 +1,249 @@ +#include "serif/backend/mfem/model/finite_elements.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "serif/backend/mfem/mapping/compactification/kelvin.hpp" + +namespace serif::model::backend::mfem { + namespace { + double logical_radius(const double* point) { + return std::max({std::abs(point[0]), std::abs(point[1]), std::abs(point[2])}); + } + + void refine_surface_layer(::mfem::Mesh& mesh, double radius, int levels, double equatorial_cutoff = 0.0) { + using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema; + constexpr int envelope = Schema::domain_id(); + constexpr int vacuum = Schema::domain_id(); + const double tolerance = 64.0 * std::numeric_limits::epsilon() * radius; + for (int level = 0; level < levels; ++level) { + ::mfem::Array<::mfem::Refinement> refinements; + ::mfem::Array vertices; + for (int element = 0; element < mesh.GetNE(); ++element) { + if (mesh.GetAttribute(element) != envelope && mesh.GetAttribute(element) != vacuum) continue; + mesh.GetElementVertices(element, vertices); + std::array latitude{}; + bool at_surface = false; + for (int vertex = 0; vertex < vertices.Size(); ++vertex) { + const double* point = mesh.GetVertex(vertices[vertex]); + const double local_radius = logical_radius(point); + at_surface |= std::abs(local_radius - radius) <= tolerance; + latitude[vertex] = point[2] / local_radius; + } + if (!at_surface) continue; + char split = ::mfem::Refinement::XYZ; + if (equatorial_cutoff > 0.0) { + const auto [lower, upper] = std::minmax_element(latitude.begin(), latitude.end()); + if (*lower >= equatorial_cutoff || *upper <= -equatorial_cutoff) continue; + // Hex local axes are edges 0->1, 0->3, and 0->4. Normalized + // logical z is constant radially; only its meridional axis changes. + const std::array changes{ + std::abs(latitude[1] - latitude[0]), std::abs(latitude[3] - latitude[0]), + std::abs(latitude[4] - latitude[0])}; + const auto axis = std::max_element(changes.begin(), changes.end()); + if (*axis <= 64.0 * std::numeric_limits::epsilon()) continue; + split = static_cast(1 << (axis - changes.begin())); + } + refinements.Append(::mfem::Refinement(element, split)); + } + if (refinements.Size() == 0) break; + // Matched latitude rings keep the stellar trace conforming. Hanging + // faces are permitted away from it and handled by the volume extension. + mesh.GeneralRefinement(refinements, 1, 0); + } + } + + /** @brief Sorted corner coordinates of every stellar-surface quad, to confirm that radial splits leave the surface alone. */ + std::vector> stellar_surface_faces(const ::mfem::Mesh& mesh) { + using namespace utils::errors; + using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema; + const int surface = Schema::boundary_id(); + std::vector> faces; + ::mfem::Array vertices; + for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { + if (mesh.GetBdrAttribute(boundary) != surface) continue; + mesh.GetBdrElementVertices(boundary, vertices); + serif_ensure(vertices.Size() == 4, "Radial surface refinement requires quadrilateral stellar-surface faces."); + std::array, 4> corners; + for (int corner = 0; corner < 4; ++corner) + for (int component = 0; component < 3; ++component) corners[corner][component] = mesh.GetVertex(vertices[corner])[component]; + std::sort(corners.begin(), corners.end()); + std::array face; + for (int corner = 0; corner < 4; ++corner) + for (int component = 0; component < 3; ++component) face[3 * corner + component] = corners[corner][component]; + faces.push_back(face); + } + std::sort(faces.begin(), faces.end()); + return faces; + } + + /** + * @brief Halve the stellar-side surface shell radially, once per level, without touching the vacuum or the surface. + * @note Each envelope hex with a face on the surface is split only along its logical radial axis, so the + * stellar surface keeps its angular faces and the vacuum and core are unchanged; only the shell thins. + */ + void refine_radial_surface_shell(::mfem::Mesh& mesh, double radius, int levels) { + using namespace utils::errors; + using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema; + constexpr int envelope = Schema::domain_id(); + const double tolerance = 256.0 * std::numeric_limits::epsilon() * radius; + const auto original_surface = stellar_surface_faces(mesh); + for (int level = 0; level < levels; ++level) { + ::mfem::Array<::mfem::Refinement> refinements; + ::mfem::Array vertices; + for (int element = 0; element < mesh.GetNE(); ++element) { + if (mesh.GetAttribute(element) != envelope) continue; + mesh.GetElementVertices(element, vertices); + serif_ensure(vertices.Size() == 8, "Radial surface refinement requires hexahedral stellar cells."); + std::array radii{}; + int surface_vertices = 0; + for (int vertex = 0; vertex < 8; ++vertex) { + radii[vertex] = logical_radius(mesh.GetVertex(vertices[vertex])); + surface_vertices += std::abs(radii[vertex] - radius) <= tolerance; + } + if (surface_vertices == 0) continue; + serif_ensure(surface_vertices == 4 && *std::max_element(radii.begin(), radii.end()) <= radius + tolerance, + "A surface-adjacent envelope cell is not a stellar-side radial shell."); + // Hex local axes are edges 0->1, 0->3 and 0->4; the radial one is the only axis that changes radius. + const std::array changes{std::abs(radii[1] - radii[0]), std::abs(radii[3] - radii[0]), std::abs(radii[4] - radii[0])}; + const int axis = static_cast(std::max_element(changes.begin(), changes.end()) - changes.begin()); + serif_ensure(changes[axis] > tolerance, "Cannot identify the radial axis of a surface shell cell."); + for (int other = 0; other < 3; ++other) + serif_ensure(other == axis || changes[other] <= tolerance, "A surface shell cell has more than one logical radial axis."); + constexpr int edges[3][4][2] = {{{0, 1}, {3, 2}, {4, 5}, {7, 6}}, {{0, 3}, {1, 2}, {4, 7}, {5, 6}}, {{0, 4}, {1, 5}, {2, 6}, {3, 7}}}; + for (const auto& edge : edges[axis]) + for (int component = 0; component < 3; ++component) { + const double a = mesh.GetVertex(vertices[edge[0]])[component] / radii[edge[0]]; + const double b = mesh.GetVertex(vertices[edge[1]])[component] / radii[edge[1]]; + serif_ensure(std::abs(a - b) <= 256.0 * std::numeric_limits::epsilon(), + "A radial shell split would change angular coordinates."); + } + refinements.Append(::mfem::Refinement(element, static_cast(1 << axis))); + } + serif_ensure(refinements.Size() == static_cast(original_surface.size()), + "Expected one radial shell parent per stellar-surface face."); + const int before = mesh.GetNE(); + mesh.GeneralRefinement(refinements, 1, 0); + serif_ensure(mesh.GetNE() == before + refinements.Size() && stellar_surface_faces(mesh) == original_surface, + "Radial surface refinement spread beyond the surface shell or changed the stellar surface."); + } + } + + void align_polar_axis(::mfem::Mesh& mesh, const std::array& axis) { + if (axis[0] == 0.0 && axis[1] == 0.0 && axis[2] > 0.0) return; + const double scale = std::max({std::abs(axis[0]), std::abs(axis[1]), std::abs(axis[2])}); + const std::array scaled{axis[0] / scale, axis[1] / scale, axis[2] / scale}; + const double norm = std::hypot(scaled[0], scaled[1], scaled[2]); + const double x = scaled[0] / norm, y = scaled[1] / norm, cosine = scaled[2] / norm; + const double sine = std::hypot(x, y); + const double nx = sine > 0.0 ? -y / sine : 1.0; + const double ny = sine > 0.0 ? x / sine : 0.0; + const auto rotate = [=](std::array point) { + const double along = nx * point[0] + ny * point[1]; + return std::array{ + cosine * point[0] + sine * ny * point[2] + (1.0 - cosine) * nx * along, + cosine * point[1] - sine * nx * point[2] + (1.0 - cosine) * ny * along, + cosine * point[2] + sine * (nx * point[1] - ny * point[0])}; + }; + auto& nodes = *mesh.GetNodes(); + const auto& space = *nodes.FESpace(); + for (int dof = 0; dof < space.GetNDofs(); ++dof) { + const auto point = rotate({nodes[space.DofToVDof(dof, 0)], nodes[space.DofToVDof(dof, 1)], nodes[space.DofToVDof(dof, 2)]}); + for (int component = 0; component < 3; ++component) nodes[space.DofToVDof(dof, component)] = point[component]; + } + // Keep the compatibility vertex array consistent with the curved mesh. + ::mfem::Vector vertex_values; + for (int component = 0; component < 3; ++component) { + nodes.GetNodalValues(vertex_values, component + 1); + for (int vertex = 0; vertex < mesh.GetNV(); ++vertex) mesh.GetVertex(vertex)[component] = vertex_values[vertex]; + } + mesh.NodesUpdated(); + } + + void refine_stellar_surface(stroid::StroidMesh& generated, const StellarSurfaceRefinementOptions& options, double radius) { + if (options.levels > 0 || options.equatorial_levels > 0 || options.radial_levels > 0) { + auto& reference = *generated.reference_mesh; + refine_surface_layer(reference, radius, options.levels); + // On every side face the spherified latitude obeys |z_phys|/r >= + // |z_logical|/(sqrt(3) r_logical). Whole intersecting rings therefore + // cover the requested band while matching across all four side blocks. + const double cutoff = std::sqrt(3.0) * std::sin(options.equatorial_half_angle_degrees * std::numbers::pi / 180.0); + refine_surface_layer(reference, radius, options.equatorial_levels, cutoff); + refine_radial_surface_shell(reference, radius, options.radial_levels); + fourdst::config::Config config; + config.mutate([&](auto& value) { value = generated.config; }); + auto mesh = stroid::utils::BuildProjected(reference, config); + stroid::topology::OptimizeMesh(*mesh, config); + auto exterior = stroid::topology::BuildExteriorCoordinate(*mesh, reference, config); + generated.exterior_coordinate = std::move(exterior); + generated.mesh = std::move(mesh); + } + align_polar_axis(*generated.mesh, options.axis); + } + } + + discretization::backend::mfem::Resources make_stellar_resources(const StellarMeshOptions& options) { + using namespace utils::errors; + + const double radius = options.radius.value(); + serif_ensure(std::isfinite(radius) && radius > 0.0 && + std::isfinite(options.core_radius_fraction) && options.core_radius_fraction > 0.0 && options.core_radius_fraction < 1.0 && + std::isfinite(options.infinity_radius_fraction) && options.infinity_radius_fraction > 1.0 && + std::isfinite(radius * options.infinity_radius_fraction) && options.refinement >= 0 && options.geometry_order > 0, + "A spherical stellar mesh requires a finite positive radius, 0 < core fraction < 1, infinity fraction > 1, nonnegative refinement and positive geometry order."); + const auto& surface = options.surface_refinement; + const double axis_scale = std::max({std::abs(surface.axis[0]), std::abs(surface.axis[1]), std::abs(surface.axis[2])}); + serif_ensure(surface.levels >= 0 && surface.equatorial_levels >= 0 && surface.radial_levels >= 0 && + std::isfinite(surface.equatorial_half_angle_degrees) && surface.equatorial_half_angle_degrees > 0.0 && + surface.equatorial_half_angle_degrees <= 35.0 && axis_scale > 0.0 && + std::ranges::all_of(surface.axis, [](double component) { return std::isfinite(component); }), + "Stellar surface refinement requires nonnegative levels, an equatorial band in (0, 35] degrees, and a finite nonzero axis."); + stroid::config::MeshConfig mesh_options; + mesh_options.refinement_levels = options.refinement; + mesh_options.order = options.geometry_order; + mesh_options.include_external_domain = true; + mesh_options.r_core = options.core_radius_fraction * radius; + mesh_options.r_star = radius; + mesh_options.r_infinity = options.infinity_radius_fraction * radius; + mesh_options.optimization_methods = stroid::config::OptimizationMethods{false, true}; + auto generated = stroid::GenerateMesh(mesh_options); + refine_stellar_surface(generated, surface, radius); + discretization::backend::mfem::Resources resources; + resources.meshes.push_back(std::move(generated.mesh)); + resources.meshes.push_back(std::move(generated.reference_mesh)); + resources.spaces.push_back(std::move(generated.exterior_coordinate->space)); + resources.compactification = std::move(generated.exterior_coordinate->values); + using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema; + resources.mapper = std::make_unique(utils::user_options::DomainMapperOptions{ + .dimension = 3, .vacuum_element_attribute_ID = Schema::domain_id()}, + std::make_unique(utils::user_options::KelvinCompactificationOptions{ + .r_star_ref = radius, .r_inf_ref = *mesh_options.r_infinity})); + return resources; + } + + ::mfem::FiniteElementSpace& make_space(discretization::backend::mfem::Resources& resources, + std::unique_ptr<::mfem::FiniteElementCollection> collection, int components, int ordering) { + const ::mfem::FiniteElementCollection* selected = collection.get(); + for (const auto& existing : resources.collections) { + if (std::string_view{existing->Name()} == collection->Name()) { + selected = existing.get(); + break; + } + } + if (selected == collection.get()) { + resources.collections.push_back(std::move(collection)); + } + for (const auto& space : resources.spaces) { + if (space->FEColl() == selected && space->GetVDim() == components && space->GetOrdering() == ordering) { + return *space; + } + } + resources.spaces.push_back(std::make_unique<::mfem::FiniteElementSpace>(resources.meshes.front().get(), selected, components, ordering)); + return *resources.spaces.back(); + } +} diff --git a/src/lib/serif/backend/mfem/model/scalar_row.cpp b/src/lib/serif/backend/mfem/model/scalar_row.cpp new file mode 100644 index 00000000..15422942 --- /dev/null +++ b/src/lib/serif/backend/mfem/model/scalar_row.cpp @@ -0,0 +1,40 @@ +#include "serif/backend/mfem/model/scalar_row.hpp" + +#include +#include +#include "mfem/linalg/densemat.hpp" +#include "serif/backend/mfem/shared/finite.hpp" +#include "serif/utils/error/errors.hpp" +#include "serif/utils/error/nonfinite.hpp" + +namespace serif::model::backend::mfem { + std::expected cache_scalar_row(const operators::backend::mfem::LinearizedBlock& source) { + using namespace utils::errors; + using operators::backend::mfem::PreparationRejection; + using operators::backend::mfem::PreparationRejectionReason; + + const auto& action = source.action(); + serif_ensure(action->Height() == 1, + "A cached scalar equation must have exactly one residual coordinate; received {}. Use the ordinary volume action for spatial equations.", action->Height()); + ::mfem::Vector unit(1), coefficients(action->Width()); + unit[0] = 1.0; + try { + action->MultTranspose(unit, coefficients); + } catch (const NonFiniteValue& error) { + return std::unexpected(PreparationRejection{ + .reason = PreparationRejectionReason::NON_FINITE, .message = error.what() + }); + } + serif_ensure(coefficients.Size() == action->Width(), + "A scalar equation transpose returned {} coefficients; expected {}. Correct the source action before retaining its row.", coefficients.Size(), action->Width()); + if (!utils::checks::is_finite(coefficients)) { + return std::unexpected(PreparationRejection{ + .reason = PreparationRejectionReason::NON_FINITE, + .message = "A scalar equation transpose produced nonfinite coefficient derivatives. Supply a valid accepted linearization before retaining its row." + }); + } + auto matrix = std::make_shared<::mfem::DenseMatrix>(1, coefficients.Size()); + std::copy_n(coefficients.GetData(), coefficients.Size(), matrix->Data()); + return operators::backend::mfem::linearized_block(std::shared_ptr(std::move(matrix))); + } +} diff --git a/src/lib/serif/backend/mfem/model/surface_potential.cpp b/src/lib/serif/backend/mfem/model/surface_potential.cpp new file mode 100644 index 00000000..955a737d --- /dev/null +++ b/src/lib/serif/backend/mfem/model/surface_potential.cpp @@ -0,0 +1,260 @@ +#include "serif/backend/mfem/model/surface_potential.hpp" + +#include +#include +#include +#include +#include +#include +#include "mfem/fem/intrules.hpp" +#include "serif/backend/mfem/shared/boundary_elements.hpp" +#include "serif/backend/mfem/shared/parallel.hpp" +#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp" + +namespace serif::model::backend::mfem { + namespace { + /** + * @brief Dense stand-in for SparseMatrix::Add followed by Finalize(). + * @note MFEM starts each entry at 0.0 and adds values in call order, keeps each row's columns newest + * first, and Finalize() drops exact zeros (a zero diagonal included). This keeps the same sums and + * remembers first-insertion order, so the finalized matrix matches entry for entry. + */ + class OrderedAccumulator { + public: + OrderedAccumulator(const int height, const int width) : + m_height(height), m_width(width), m_values(static_cast(height) * static_cast(width), 0.0), + m_present(m_values.size(), 0), m_order(static_cast(height)) {} + + void add(const int row, const int column, const double value) { + const std::size_t entry = static_cast(row) * static_cast(m_width) + static_cast(column); + if (m_present[entry] == 0) { + m_present[entry] = 1; + m_order[static_cast(row)].push_back(column); + } + m_values[entry] += value; + } + + [[nodiscard]] std::shared_ptr<::mfem::SparseMatrix> finalize() const { + int* I = new int[static_cast(m_height) + 1]; + I[0] = 0; + for (int row = 0; row < m_height; ++row) { + int kept = 0; + for (const int column : m_order[static_cast(row)]) { kept += value(row, column) != 0.0; } + I[row + 1] = I[row] + kept; + } + int* J = new int[static_cast(I[m_height])]; + double* A = new double[static_cast(I[m_height])]; + bool sorted = true; + for (int row = 0, next = 0; row < m_height; ++row) { + int last_column = -1; + const auto& order = m_order[static_cast(row)]; + for (auto column = order.rbegin(); column != order.rend(); ++column) { + if (value(row, *column) == 0.0) { continue; } + J[next] = *column; + A[next] = value(row, *column); + if (last_column > *column) { sorted = false; } + last_column = *column; + ++next; + } + } + return std::make_shared<::mfem::SparseMatrix>(I, J, A, m_height, m_width, true, true, sorted); + } + + private: + [[nodiscard]] double value(const int row, const int column) const { + return m_values[static_cast(row) * static_cast(m_width) + static_cast(column)]; + } + + int m_height; + int m_width; + std::vector m_values; + std::vector m_present; + std::vector> m_order; + }; + } + + SurfacePotentialLinearization assemble_surface_potential( + const equations::SurfacePotentialBalance& equation, + const geometry::deformation::backend::mfem::SurfaceDiscretization& discretization, + const geometry::deformation::ReferenceSurface& reference, + const geometry::deformation::SurfaceEmbedding& embedding, + const std::span displacement, + const discretization::backend::mfem::DofMap& potential_binding, + const ::mfem::Vector& potential, const double level, const double angular_velocity) { + using namespace utils::errors; + namespace deformation = geometry::deformation::backend::mfem; + namespace boundary = ::serif::backend::mfem::shared; + using Equation = equations::SurfacePotentialBalance; + + potential_binding.validate(); + const auto trace_status = deformation::validate_surface_trace(discretization.scalar_space, + discretization.surface_attribute, reference, embedding); + serif_ensure(trace_status.has_value(), + "Surface potential assembly requires a compatible reference surface and radial embedding: {}", + trace_status ? "" : trace_status.error().message); + serif_ensure(embedding.input_size() <= static_cast(std::numeric_limits::max()) && + displacement.size() == embedding.output_size() && potential.Size() == potential_binding.size(), + "Surface potential assembly received incompatible coefficient counts."); + serif_ensure(potential_binding.space().GetMesh() == discretization.scalar_space.GetMesh() && + potential_binding.space().GetVDim() == 1, + "Surface potential and reference geometry must use scalar traces on the same mesh."); + serif_ensure(std::isfinite(level) && std::isfinite(angular_velocity), + "Surface potential level and angular velocity must be finite."); + + const int count = static_cast(embedding.input_size()); + SurfacePotentialLinearization result{ + .residual = std::vector(count, 0.0), + .potential = nullptr, + .shape = nullptr, + .level = std::make_shared<::mfem::DenseMatrix>(count, 1), + .angular_velocity = std::make_shared<::mfem::DenseMatrix>(count, 1)}; + *result.level = 0.0; + *result.angular_velocity = 0.0; + OrderedAccumulator potential_matrix(count, potential_binding.size()), shape_matrix(count, count); + double reference_area = 0.0; + const auto status = boundary::for_each_boundary_element(discretization.scalar_space, + discretization.surface_attribute, reference.scalar_dofs, + [&](const boundary::BoundaryElement& element) -> boundary::BoundaryStatus { + // MFEM identifies a boundary transformation by its boundary-element index. + const int boundary_index = element.transformation.ElementNo; + const auto& potential_element = *potential_binding.space().GetBE(boundary_index); + ::mfem::Array potential_dofs; + if (potential_binding.space().GetBdrElementDofs(boundary_index, potential_dofs) != nullptr || + potential_dofs.Size() != potential_element.GetDof()) { + return std::unexpected(boundary::BoundaryFailure{.message = "Potential surface assembly requires a canonical scalar nodal trace."}); + } + const int order = 3 * element.finite_element.GetOrder() + potential_element.GetOrder() + element.transformation.OrderW(); + const auto& rule = ::mfem::IntRules.Get(element.finite_element.GetGeomType(), order); + // Nodal shape functions are local. Harmonic modes have global support. + std::vector parameters; + if (embedding.has_nodal_parameters()) { + parameters.assign(element.selected_nodes.begin(), element.selected_nodes.end()); + } else { + for (int parameter = 0; parameter < count; ++parameter) { parameters.push_back(parameter); } + } + const int active = static_cast(parameters.size()); + const int points = rule.GetNPoints(); + const int geometry_count = element.finite_element.GetDof(), potential_count = potential_element.GetDof(); + const bool nodal_parameters = embedding.has_nodal_parameters(); + const auto nodal_basis = embedding.nodal_basis(); + const auto radial_directions = embedding.radial_directions(); + + // 1. The boundary transformation is shared state, so its samples are taken serially. + std::vector weights(static_cast(points)); + std::vector geometry_shapes(static_cast(points * geometry_count)); + std::vector potential_shapes(static_cast(points * potential_count)); + std::vector positions(static_cast(points * 3)); + { + ::mfem::Vector geometry_shape(geometry_count), potential_shape(potential_count), position(3); + for (int quadrature = 0; quadrature < points; ++quadrature) { + const auto& point = rule.IntPoint(quadrature); + element.transformation.SetIntPoint(&point); + weights[static_cast(quadrature)] = point.weight * element.transformation.Weight(); + element.finite_element.CalcShape(point, geometry_shape); + potential_element.CalcShape(point, potential_shape); + element.transformation.Transform(point, position); + std::copy_n(geometry_shape.GetData(), geometry_count, geometry_shapes.begin() + quadrature * geometry_count); + std::copy_n(potential_shape.GetData(), potential_count, potential_shapes.begin() + quadrature * potential_count); + std::copy_n(position.GetData(), 3, positions.begin() + quadrature * 3); + } + } + + // 2. Each point's test functions, surface motions and balance, independently. + struct PointBalance { + double residual, level, angular_velocity, potential; + std::array position_derivative; + }; + std::vector tests(static_cast(points) * static_cast(active)); + std::vector motions(static_cast(points) * static_cast(active)); + std::vector balances(static_cast(points)); + utils::parallel::for_each_index(points, [&](const int quadrature, int) { + const double* geometry_shape = geometry_shapes.data() + quadrature * geometry_count; + const double* potential_shape = potential_shapes.data() + quadrature * potential_count; + double* test = tests.data() + static_cast(quadrature) * static_cast(active); + auto* motion = motions.data() + static_cast(quadrature) * static_cast(active); + equations::EquationPoint physical; + for (int axis = 0; axis < 3; ++axis) { physical.position[axis] = positions[static_cast(quadrature * 3 + axis)]; } + for (int local = 0; local < geometry_count; ++local) { + const int node = element.selected_nodes[local]; + const double value = geometry_shape[local] * (element.encoded_dofs[local] < 0 ? -1.0 : 1.0); + for (int axis = 0; axis < 3; ++axis) { physical.position[axis] += value * displacement[3 * node + axis]; } + for (int parameter = 0; parameter < active; ++parameter) { + double basis = 0.0; + if (nodal_parameters) { + basis = parameters[parameter] == node ? 1.0 : 0.0; + } else { + basis = nodal_basis[static_cast(node) * count + parameters[parameter]]; + } + test[parameter] += value * basis; + for (int axis = 0; axis < 3; ++axis) { + motion[parameter][axis] += value * basis * radial_directions[node][axis]; + } + } + } + for (int local = 0; local < potential_count; ++local) { + const double value = potential_shape[local] * (potential_dofs[local] < 0 ? -1.0 : 1.0); + bool supported = false; + potential_binding.for_each_coefficient(::mfem::FiniteElementSpace::DecodeDof(potential_dofs[local]), [&](const int index, const double weight) { + supported = true; + physical.fields.get() += value * weight * potential[index]; + }); + serif_ensure(supported, "A potential trace node has no supported coefficient."); + } + physical.fields.get() = level; + physical.fields.get() = angular_velocity; + const auto balance = equation.evaluate(physical); + const double weight = weights[static_cast(quadrature)]; + serif_ensure(std::isfinite(weight) && weight > 0.0 && std::isfinite(balance.residual), + "Surface potential integration requires finite geometry and fields."); + balances[static_cast(quadrature)] = {balance.residual, balance.field_derivatives.get(), + balance.field_derivatives.get(), balance.field_derivatives.get(), + {balance.position_derivative[0], balance.position_derivative[1], balance.position_derivative[2]}}; + }); + for (const double weight : weights) { reference_area += weight; } + + // 3. Accumulate by row: each row still sums its points in order, so every value and insertion order is unchanged. + utils::parallel::for_each_index(active, [&](const int row, int) { + const int target = parameters[row]; + for (int quadrature = 0; quadrature < points; ++quadrature) { + const auto& balance = balances[static_cast(quadrature)]; + const double* potential_shape = potential_shapes.data() + quadrature * potential_count; + const auto* motion = motions.data() + static_cast(quadrature) * static_cast(active); + const double tested_weight = weights[static_cast(quadrature)] * + tests[static_cast(quadrature) * static_cast(active) + static_cast(row)]; + result.residual[target] += tested_weight * balance.residual; + (*result.level)(target, 0) += tested_weight * balance.level; + (*result.angular_velocity)(target, 0) += tested_weight * balance.angular_velocity; + for (int column = 0; column < active; ++column) { + double derivative = 0.0; + for (int axis = 0; axis < 3; ++axis) { + derivative += tested_weight * balance.position_derivative[axis] * motion[column][axis]; + } + if (derivative != 0.0) { shape_matrix.add(target, parameters[column], derivative); } + } + for (int local = 0; local < potential_count; ++local) { + potential_binding.for_each_coefficient(::mfem::FiniteElementSpace::DecodeDof(potential_dofs[local]), [&](const int index, const double weight) { + // A separate statement keeps the product unfused, as when it was passed to SparseMatrix::Add. + const double contribution = tested_weight * balance.potential * + potential_shape[local] * (potential_dofs[local] < 0 ? -1.0 : 1.0) * weight; + potential_matrix.add(target, index, contribution); + }); + } + } + }); + return {}; + }); + serif_ensure(status.has_value(), + "Surface potential integration rejected boundary element {}: {}", + status ? -1 : status.error().element, status ? "" : status.error().message); + serif_ensure(std::isfinite(reference_area) && reference_area > 0.0, + "Surface potential integration requires a nonempty surface of positive finite reference area."); + result.potential = potential_matrix.finalize(); + result.shape = shape_matrix.finalize(); + *result.potential *= 1.0 / reference_area; + *result.shape *= 1.0 / reference_area; + *result.level *= 1.0 / reference_area; + *result.angular_velocity *= 1.0 / reference_area; + for (double& value : result.residual) { value /= reference_area; } + return result; + } +} diff --git a/src/lib/serif/backend/mfem/model/system.cpp b/src/lib/serif/backend/mfem/model/system.cpp new file mode 100644 index 00000000..d22e9068 --- /dev/null +++ b/src/lib/serif/backend/mfem/model/system.cpp @@ -0,0 +1,101 @@ +#include "system_data.hpp" + +#include +#include +#include +#include +#include "serif/utils/error/errors.hpp" + +namespace serif::model::backend::mfem { + namespace detail { + void validate_range(int offset, int size, int extent) { + using namespace utils::errors; + + serif_ensure(offset >= 0 && size >= 0 && extent >= 0 && offset <= extent && size <= extent - offset, "Model coordinate range [{}, +{}) lies outside extent {}.", offset, size, extent); + } + } // namespace detail + + namespace { + /** @brief Check physical block extents and ensure that no two boundary prescriptions replace the same equation. */ + [[nodiscard]] std::shared_ptr prepare_data(int rows, int columns, std::vector jacobian, std::vector replacements, std::shared_ptr owner, std::optional> complete_actions) { + using namespace utils::errors; + + serif_ensure(rows >= 0 && columns >= 0, "Model residual and state sizes must be nonnegative."); + auto data = std::make_shared(detail::SystemData{std::move(owner), rows, columns, std::move(jacobian), std::move(replacements), std::vector(rows, false), std::move(complete_actions)}); + for (const auto& contribution : data->jacobian) { + detail::validate_range(contribution.row_offset, contribution.block.action()->Height(), rows); + detail::validate_range(contribution.column_offset, contribution.block.action()->Width(), columns); + } + if (data->complete_actions) { + for (const auto& contribution : *data->complete_actions) { + detail::validate_range(contribution.row_offset, contribution.block.action()->Height(), rows); + detail::validate_range(contribution.column_offset, contribution.block.action()->Width(), columns); + } + } + for (const auto& replacement : data->replacements) { + serif_ensure(replacement.selected_rows.size() == replacement.residual.size(), "Replacement residuals must contain one value per selected equation row."); + for (const int row : replacement.selected_rows) { + detail::validate_range(row, 1, rows); + serif_ensure(!data->replaced_rows[row], "Model residual row {} is replaced more than once; select disjoint boundary equations.", row); + data->replaced_rows[row] = true; + } + for (const auto& contribution : replacement.jacobian) { + serif_ensure(static_cast(contribution.block.action()->Height()) == replacement.selected_rows.size(), "A replacement Jacobian must have one row per selected equation."); + detail::validate_range(contribution.column_offset, contribution.block.action()->Width(), columns); + } + } + return data; + } + + /** @brief Add every physical source before replacing complete boundary equations. */ + [[nodiscard]] std::vector prepare_residual(const detail::SystemData& data, const std::vector& contributions) { + using namespace utils::errors; + + std::vector result(data.rows, 0.0); + for (const auto& contribution : contributions) { + serif_ensure(contribution.values.size() <= static_cast(data.rows), "A model residual contribution exceeds the complete residual size."); + detail::validate_range(contribution.row_offset, static_cast(contribution.values.size()), data.rows); + for (std::size_t index = 0; index < contribution.values.size(); ++index) { + serif_ensure(std::isfinite(contribution.values[index]), "A model residual contribution contains a nonfinite evaluated value."); + result[contribution.row_offset + index] += contribution.values[index]; + } + } + for (const auto& replacement : data.replacements) { + for (std::size_t index = 0; index < replacement.selected_rows.size(); ++index) { + result[replacement.selected_rows[index]] = replacement.residual[index]; + } + } + serif_ensure(std::ranges::all_of(result, [](double value) { + return std::isfinite(value); + }), + "The assembled model residual is nonfinite; reject this candidate."); + return result; + } + } // namespace + + PhysicalSystem::PhysicalSystem(int residual_size, int state_size, std::vector residuals, std::vector jacobian, + std::vector replacements, std::shared_ptr owner, std::optional> complete_actions) : + m_data(prepare_data(residual_size, state_size, std::move(jacobian), std::move(replacements), std::move(owner), std::move(complete_actions))), + m_residual(prepare_residual(*m_data, residuals)), m_jacobian(detail::select_block(m_data, std::array{CoordinateRange{0, residual_size}}, std::array{CoordinateRange{0, state_size}}, true)) { + } + + const std::vector& PhysicalSystem::residual() const noexcept { + return m_residual; + } + + const LinearizedBlock& PhysicalSystem::jacobian() const noexcept { + return m_jacobian; + } + + LinearizedBlock PhysicalSystem::block(int row_offset, int row_size, int column_offset, int column_size) const { + return block(std::array{CoordinateRange{row_offset, row_size}}, std::array{CoordinateRange{column_offset, column_size}}); + } + + LinearizedBlock PhysicalSystem::block(std::span rows, std::span columns) const { + return detail::select_block(m_data, rows, columns); + } + + std::shared_ptr make_physical_system(int residual_size, int state_size, std::vector residuals, std::vector jacobian, std::vector replacements, std::shared_ptr owner, std::optional> complete_actions) { + return std::make_shared(residual_size, state_size, std::move(residuals), std::move(jacobian), std::move(replacements), std::move(owner), std::move(complete_actions)); + } +} // namespace serif::model::backend::mfem diff --git a/src/lib/serif/backend/mfem/model/system_data.hpp b/src/lib/serif/backend/mfem/model/system_data.hpp new file mode 100644 index 00000000..848ceb9b --- /dev/null +++ b/src/lib/serif/backend/mfem/model/system_data.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "serif/backend/mfem/model/system.hpp" + +namespace serif::model::backend::mfem::detail +{ + /** @brief Retain accepted blocks and the stable external resources borrowed by those blocks. */ + struct SystemData + { + std::shared_ptr owner; + int rows; + int columns; + std::vector jacobian; + std::vector replacements; + std::vector replaced_rows; + std::optional> complete_actions; + }; + + /** @brief Reject invalid subranges without overflowing integer additions. */ + void validate_range(int offset, int size, int extent); + + /** @brief Build an independently owned selection of accepted physical couplings. */ + [[nodiscard]] LinearizedBlock select_block(std::shared_ptr data, std::span rows, std::span columns, bool complete_action = false); +} // namespace serif::model::backend::mfem::detail diff --git a/src/lib/serif/backend/mfem/model/system_selection.cpp b/src/lib/serif/backend/mfem/model/system_selection.cpp new file mode 100644 index 00000000..fec51457 --- /dev/null +++ b/src/lib/serif/backend/mfem/model/system_selection.cpp @@ -0,0 +1,341 @@ +#include "system_data.hpp" + +#include "serif/backend/mfem/operators/volume_field_block.hpp" + +#include +#include +#include +#include "serif/utils/error/errors.hpp" + +namespace serif::model::backend::mfem::detail { + namespace { + /** @brief Map a contiguous part of a child action into the caller's selected coordinates. */ + struct SelectedRange { + int source; + int destination; + int size; + }; + + /** @brief Keep only child rows and columns that contribute to the selected physical block. */ + struct SelectedContribution { + LinearizedBlock block; + std::vector> rows; + std::vector columns; + }; + + /** @brief Retain the source owner even when the selected block has no nonzero contributions. */ + struct Selection { + std::shared_ptr data; + std::vector contributions; + int rows; + int columns; + }; + + /** @brief Validate source coordinates and the concatenated MFEM coordinate count without integer overflow. */ + [[nodiscard]] int selected_size(std::span ranges, int extent) { + int size = 0; + for (const CoordinateRange range : ranges) { + validate_range(range.offset, range.size, extent); + utils::errors::serif_ensure(range.size <= std::numeric_limits::max() - size, + "Selected model coordinates exceed MFEM's integer index range. Select fewer equation or state coordinates."); + size += range.size; + } + return size; + } + + /** @brief Intersect requested coordinates with one child interval while retaining the requested order. */ + [[nodiscard]] std::vector selected_ranges(std::span ranges, int source_offset, int size) { + std::vector result; + int destination = 0; + for (const CoordinateRange range : ranges) { + const int first = std::max(source_offset, range.offset); + const int end = std::min(source_offset + size, range.offset + range.size); + if (first < end) { + result.push_back({first - source_offset, destination + first - range.offset, end - first}); + } + destination += range.size; + } + return result; + } + + /** @brief Keep one child action for all its intersecting equation and state ranges. */ + void add_selection(Selection& selection, LinearizedBlock block, int source_column, std::span columns, std::vector> rows) { + auto selected_columns = selected_ranges(columns, source_column, block.action()->Width()); + if (!rows.empty() && !selected_columns.empty()) { + selection.contributions.push_back({std::move(block), std::move(rows), std::move(selected_columns)}); + } + } + + /** @brief Compile row replacement and independent coordinate ordering once, outside repeated matrix actions. */ + [[nodiscard]] std::shared_ptr make_selection(std::shared_ptr data, std::span rows, std::span columns, bool complete_action) { + auto selection = std::make_shared(Selection{data, {}, selected_size(rows, data->rows), selected_size(columns, data->columns)}); + const auto& contributions = complete_action && selection->data->complete_actions ? *selection->data->complete_actions : selection->data->jacobian; + for (const auto& contribution : contributions) { + std::vector> selected_rows; + for (const SelectedRange range : selected_ranges(rows, contribution.row_offset, contribution.block.action()->Height())) { + for (int index = 0; index < range.size; ++index) { + if (!selection->data->replaced_rows[contribution.row_offset + range.source + index]) { + selected_rows.emplace_back(range.source + index, range.destination + index); + } + } + } + add_selection(*selection, contribution.block, contribution.column_offset, columns, std::move(selected_rows)); + } + for (const auto& replacement : selection->data->replacements) { + std::vector> selected_rows; + int destination = 0; + for (const CoordinateRange range : rows) { + for (std::size_t index = 0; index < replacement.selected_rows.size(); ++index) { + const int row = replacement.selected_rows[index]; + if (row >= range.offset && row < range.offset + range.size) { + selected_rows.emplace_back(static_cast(index), destination + row - range.offset); + } + } + destination += range.size; + } + for (const auto& contribution : replacement.jacobian) { + add_selection(*selection, contribution.block, contribution.column_offset, columns, selected_rows); + } + } + return selection; + } + + /** @brief Apply only selected child couplings; masking before transpose is exactly the transpose of row replacement. */ + void apply_selection(const Selection& selection, const ::mfem::Vector& input, ::mfem::Vector& output, bool transpose, ::mfem::Vector& direction, ::mfem::Vector& result) { + for (const auto& contribution : selection.contributions) { + const auto& action = contribution.block.action(); + direction.SetSize(transpose ? action->Height() : action->Width()); + result.SetSize(transpose ? action->Width() : action->Height()); + direction = 0.0; + result = 0.0; + if (transpose) { + for (const auto [source, destination] : contribution.rows) { + direction[source] += input[destination]; + } + action->MultTranspose(direction, result); + utils::errors::serif_ensure(result.Size() == action->Width(), "A selected model transpose changed its declared output size."); + for (const SelectedRange range : contribution.columns) { + for (int index = 0; index < range.size; ++index) { + output[range.destination + index] += result[range.source + index]; + } + } + } + else { + for (const SelectedRange range : contribution.columns) { + for (int index = 0; index < range.size; ++index) { + direction[range.source + index] += input[range.destination + index]; + } + } + action->Mult(direction, result); + utils::errors::serif_ensure(result.Size() == action->Height(), "A selected model action changed its declared output size."); + for (const auto [source, destination] : contribution.rows) { + output[destination] += result[source]; + } + } + } + } + + /** + * @brief Forward apply_selection for several inputs, one batched child action per contribution. + * @note Each output receives the same additions as apply_selection, except that a child is not applied to + * a direction whose selected columns are all zero (its exact contribution is zero). + */ + void apply_selection_many(const Selection& selection, std::span inputs, std::span<::mfem::Vector* const> outputs, std::vector<::mfem::Vector>& directions, std::vector<::mfem::Vector>& results) { + directions.resize(inputs.size()); + results.resize(inputs.size()); + std::vector active_directions; + std::vector<::mfem::Vector*> active_results; + std::vector active; + for (const auto& contribution : selection.contributions) { + const auto& action = contribution.block.action(); + active.clear(); + active_directions.clear(); + active_results.clear(); + for (std::size_t k = 0; k < inputs.size(); ++k) { + auto& direction = directions[k]; + direction.SetSize(action->Width()); + direction = 0.0; + bool nonzero = false; + for (const SelectedRange range : contribution.columns) { + for (int index = 0; index < range.size; ++index) { + direction[range.source + index] += (*inputs[k])[range.destination + index]; + nonzero = nonzero || direction[range.source + index] != 0.0; + } + } + if (nonzero) { + results[k].SetSize(action->Height()); + results[k] = 0.0; + active.push_back(k); + active_directions.push_back(&direction); + active_results.push_back(&results[k]); + } + } + if (active.empty()) { + continue; + } + operators::backend::mfem::apply_many(*action, active_directions, active_results); + for (const std::size_t k : active) { + utils::errors::serif_ensure(results[k].Size() == action->Height(), "A selected model action changed its declared output size."); + for (const auto [source, destination] : contribution.rows) { + (*outputs[k])[destination] += results[k][source]; + } + } + } + } + + /** + * @brief Assemble selected entries of available child matrices without probing global operator columns. + * @note Equivalent to SparseMatrix::Add for every selected child entry (contributions, then their rows, then + * row entries, then column ranges) followed by Finalize(), built without MFEM's linked rows: each destination + * row replays its own insertions in that order, so every sum, the column order and the dropped exact zeros + * match (see operators::backend::mfem::detail::finalize_probed_blocks). Rows are built concurrently. + */ + [[nodiscard]] std::shared_ptr assemble_selection(const Selection& selection) { + std::vector> matrices; + matrices.reserve(selection.contributions.size()); + for (const auto& contribution : selection.contributions) { matrices.push_back(checked_assembly(contribution.block)); } + + // Destination row -> (contribution, source row) in insertion order. + std::vector offsets(static_cast(selection.rows) + 1, 0); + for (const auto& contribution : selection.contributions) { + for (const auto [source, destination] : contribution.rows) { ++offsets[static_cast(destination) + 1]; } + } + for (int row = 0; row < selection.rows; ++row) { offsets[row + 1] += offsets[row]; } + std::vector> incidence(static_cast(offsets.back())); + { + std::vector cursor(offsets.begin(), offsets.end() - 1); + for (int index = 0; index < static_cast(selection.contributions.size()); ++index) { + for (const auto [source, destination] : selection.contributions[static_cast(index)].rows) { + incidence[static_cast(cursor[static_cast(destination)]++)] = {index, source}; + } + } + } + + struct RowScratch { + std::vector slot_of_column; + std::vector columns; + std::vector values; + }; + std::vector> row_columns(static_cast(selection.rows)); + std::vector> row_values(static_cast(selection.rows)); + std::vector scratch(static_cast(utils::parallel::worker_count())); + utils::parallel::for_each_index(selection.rows, [&](const int row, const int worker) { + RowScratch& local = scratch[static_cast(worker)]; + local.slot_of_column.resize(static_cast(selection.columns), -1); + local.columns.clear(); + local.values.clear(); + for (int entry = offsets[row]; entry < offsets[row + 1]; ++entry) { + const auto [index, source] = incidence[static_cast(entry)]; + const auto& matrix = *matrices[static_cast(index)]; + const auto& ranges = selection.contributions[static_cast(index)].columns; + const int* columns = matrix.GetRowColumns(source); + const double* values = matrix.GetRowEntries(source); + for (int k = 0; k < matrix.RowSize(source); ++k) { + for (const SelectedRange range : ranges) { + const int column = columns[k] - range.source; + if (column >= 0 && column < range.size) { + int& slot = local.slot_of_column[static_cast(range.destination + column)]; + if (slot < 0) { + slot = static_cast(local.columns.size()); + local.columns.push_back(range.destination + column); + local.values.push_back(0.0); + } + local.values[static_cast(slot)] += values[k]; + } + } + } + } + auto& columns = row_columns[static_cast(row)]; + auto& values = row_values[static_cast(row)]; + for (std::size_t slot = local.columns.size(); slot-- > 0;) { + local.slot_of_column[static_cast(local.columns[slot])] = -1; + if (local.values[slot] == 0.0) { continue; } + columns.push_back(local.columns[slot]); + values.push_back(local.values[slot]); + } + }); + return operators::backend::mfem::detail::make_csr(row_columns, row_values, selection.rows, selection.columns); + } + + /** @brief Locate a selected diagonal entry in the child's state coordinates, if the child contributes to it. */ + [[nodiscard]] int diagonal_column(const SelectedContribution& contribution, int row) { + for (const SelectedRange range : contribution.columns) { + if (row >= range.destination && row < range.destination + range.size) { + return range.source + row - range.destination; + } + } + return -1; + } + + /** @brief Whether a selected diagonal can use child diagonals directly instead of assembling a matrix. */ + [[nodiscard]] bool uses_child_diagonal(const SelectedContribution& contribution) { + for (const auto [source, destination] : contribution.rows) { + const int column = diagonal_column(contribution, destination); + if (column >= 0 && source != column) { + return false; + } + } + return contribution.block.action()->Height() == contribution.block.action()->Width() && contribution.block.has_diagonal(); + } + + /** @brief Preserve efficient native diagonal extraction, assembling only genuinely off-diagonal child selections. */ + [[nodiscard]] std::vector selection_diagonal(const Selection& selection) { + std::vector result(selection.rows, 0.0); + for (const auto& contribution : selection.contributions) { + if (uses_child_diagonal(contribution)) { + const auto diagonal = checked_diagonal(contribution.block); + for (const auto [source, destination] : contribution.rows) { + if (diagonal_column(contribution, destination) >= 0) { + result[destination] += diagonal[source]; + } + } + } + else { + const auto matrix = checked_assembly(contribution.block); + for (const auto [source, destination] : contribution.rows) { + const int column = diagonal_column(contribution, destination); + if (column >= 0) { + for (int index = 0; index < matrix->RowSize(source); ++index) { + if (matrix->GetRowColumns(source)[index] == column) { + result[destination] += matrix->GetRowEntries(source)[index]; + } + } + } + } + } + } + return result; + } + } // namespace + + LinearizedBlock select_block(std::shared_ptr data, std::span rows, std::span columns, bool complete_action) { + const auto selection = make_selection(data, rows, columns, complete_action); + const auto metadata = complete_action && data->complete_actions ? make_selection(std::move(data), rows, columns, false) : selection; + const auto action = operators::backend::mfem::linearized_action(selection->rows, selection->columns, [selection, direction = ::mfem::Vector{}, result = ::mfem::Vector{}](const auto& input, auto& output) mutable { + apply_selection(*selection, input, output, false, direction, result); + }, [selection, direction = ::mfem::Vector{}, result = ::mfem::Vector{}](const auto& input, auto& output) mutable { + apply_selection(*selection, input, output, true, direction, result); + }, [selection, directions = std::vector<::mfem::Vector>{}, results = std::vector<::mfem::Vector>{}](std::span inputs, std::span<::mfem::Vector* const> outputs) mutable { + apply_selection_many(*selection, inputs, outputs, directions, results); + directions = {}; + results = {}; + }); + LinearizedBlock::Diagonal diagonal; + LinearizedBlock::Assembly assembly; + if (selection->rows == selection->columns && std::ranges::all_of(metadata->contributions, [](const auto& entry) { + return uses_child_diagonal(entry) || entry.block.has_assembly(); + })) { + diagonal = [metadata]() { + return selection_diagonal(*metadata); + }; + } + if (std::ranges::all_of(metadata->contributions, [](const auto& entry) { + return entry.block.has_assembly(); + })) { + assembly = [metadata]() { + return assemble_selection(*metadata); + }; + } + return LinearizedBlock(action.action(), std::move(diagonal), std::move(assembly)); + } +} // namespace serif::model::backend::mfem::detail diff --git a/src/lib/serif/backend/mfem/operators/actions.cpp b/src/lib/serif/backend/mfem/operators/actions.cpp new file mode 100644 index 00000000..455ce86e --- /dev/null +++ b/src/lib/serif/backend/mfem/operators/actions.cpp @@ -0,0 +1,254 @@ +#include "serif/utils/error/nonfinite.hpp" +#include "serif/backend/mfem/operators/actions.hpp" + +#include +#include +#include "serif/backend/mfem/shared/finite.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::operators::backend::mfem { + namespace { + /** @brief Keep reusable scratch outside the captured operators; simultaneous calls require separate adapters. */ + class ActionOperator final : public ::mfem::Operator { + public: + /** @brief Capture both directions of an immutable mathematical action. */ + ActionOperator(int rows, int columns, MatrixAction action, MatrixAction transpose, MatrixArrayAction batch = {}) : ::mfem::Operator(rows, columns), m_action(std::move(action)), m_transpose(std::move(transpose)), m_batch(std::move(batch)) { + } + + /** @brief Batched forward action with the same per-direction checks and storage guarantees as Mult. */ + void ArrayMult(const ::mfem::Array& inputs, ::mfem::Array<::mfem::Vector*>& outputs) const override { + using namespace utils::errors; + + serif_ensure(inputs.Size() == outputs.Size(), "A batched action needs one output per input."); + if (!m_batch) { + for (int k = 0; k < inputs.Size(); ++k) { + Mult(*inputs[k], *outputs[k]); + } + return; + } + const auto count = static_cast(inputs.Size()); + m_inputs.resize(count); + m_outputs.resize(count); + std::vector input_views(count); + std::vector<::mfem::Vector*> output_views(count); + for (std::size_t k = 0; k < count; ++k) { + const auto& input = *inputs[static_cast(k)]; + serif_ensure(input.Size() == width, "Linearized action received {} coefficients; expected {}.", input.Size(), width); + serif_ensure(utils::checks::is_finite(input), "Linearized action directions must be finite."); + m_inputs[k] = input; + m_outputs[k].SetSize(height); + m_outputs[k] = 0.0; + input_views[k] = &m_inputs[k]; + output_views[k] = &m_outputs[k]; + } + m_batch(input_views, output_views); + for (std::size_t k = 0; k < count; ++k) { + serif_ensure(m_outputs[k].Size() == height, "A linearized action changed its declared output size from {} to {}.", height, m_outputs[k].Size()); + serif_ensure(utils::checks::is_finite(m_outputs[k]), "A linearized action produced nonfinite values; reject this linearization or direction."); + } + for (std::size_t k = 0; k < count; ++k) { + *outputs[static_cast(k)] = m_outputs[k]; + } + // Batches are transient (preconditioner setup); do not keep their copies alive with the operator. + m_inputs = {}; + m_outputs = {}; + } + + /** @brief Snapshot the input before publishing any output, including shifted overlapping views. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override { + apply(input, output, width, height, m_action); + } + + /** @brief Apply the exact coordinate transpose with the same storage guarantees. */ + void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override { + apply(input, output, height, width, m_transpose); + } + + private: + /** @brief Reject malformed directions and nonfinite child results before changing caller storage. */ + void apply(const ::mfem::Vector& input, ::mfem::Vector& output, int inputs, int outputs, const MatrixAction& action) const { + using namespace utils::errors; + + serif_ensure(input.Size() == inputs, "Linearized action received {} coefficients; expected {}.", input.Size(), inputs); + serif_ensure(utils::checks::is_finite(input), "Linearized action directions must be finite."); + m_input = input; + m_output.SetSize(outputs); + m_output = 0.0; + action(m_input, m_output); + serif_ensure(m_output.Size() == outputs, "A linearized action changed its declared output size from {} to {}.", outputs, m_output.Size()); + serif_ensure(utils::checks::is_finite(m_output), "A linearized action produced nonfinite values; reject this linearization or direction."); + output = m_output; + } + + MatrixAction m_action, m_transpose; + MatrixArrayAction m_batch; + mutable ::mfem::Vector m_input, m_output; + mutable std::vector<::mfem::Vector> m_inputs, m_outputs; + }; + } // namespace + + LinearizedBlock linearized_action(int rows, int columns, MatrixAction action, MatrixAction transpose) { + using namespace utils::errors; + + serif_ensure(rows >= 0 && columns >= 0, "Linearized action sizes must be nonnegative."); + serif_ensure(action && transpose, "A linearized action must supply forward and transpose callbacks."); + return LinearizedBlock(std::make_shared(rows, columns, std::move(action), std::move(transpose))); + } + + LinearizedBlock linearized_action(int rows, int columns, MatrixAction action, MatrixAction transpose, MatrixArrayAction batch) { + using namespace utils::errors; + + serif_ensure(rows >= 0 && columns >= 0, "Linearized action sizes must be nonnegative."); + serif_ensure(action && transpose && batch, "A batched linearized action must supply forward, transpose and batch callbacks."); + return LinearizedBlock(std::make_shared(rows, columns, std::move(action), std::move(transpose), std::move(batch))); + } + + void apply_many(const ::mfem::Operator& action, std::span inputs, std::span<::mfem::Vector* const> outputs) { + ::mfem::Array in(static_cast(inputs.size())); + ::mfem::Array<::mfem::Vector*> out(static_cast(outputs.size())); + for (std::size_t k = 0; k < inputs.size(); ++k) { + in[static_cast(k)] = inputs[k]; + out[static_cast(k)] = outputs[k]; + } + action.ArrayMult(in, out); + } + + LinearizedBlock transpose_block(LinearizedBlock block) { + const auto original = block.action(); + const auto action = linearized_action(original->Width(), original->Height(), [original](const auto& input, auto& output) { + original->MultTranspose(input, output); + }, [original](const auto& input, auto& output) { + original->Mult(input, output); + }); + LinearizedBlock::Diagonal diagonal; + LinearizedBlock::Assembly assembly; + if (block.has_diagonal()) { + diagonal = [block]() { + return checked_diagonal(block); + }; + } + if (block.has_assembly()) { + assembly = [block]() { + return std::shared_ptr(::mfem::Transpose(*checked_assembly(block))); + }; + } + return LinearizedBlock(action.action(), std::move(diagonal), std::move(assembly)); + } + + LinearizedBlock compose_blocks(LinearizedBlock left, LinearizedBlock right) { + using namespace utils::errors; + + serif_ensure(left.action()->Width() == right.action()->Height(), "Composed linearized blocks have incompatible intermediate dimensions."); + const auto action = linearized_action(left.action()->Height(), right.action()->Width(), [left, right, scratch = ::mfem::Vector{}](const auto& input, auto& output) mutable { + scratch.SetSize(right.action()->Height()); + scratch = 0.0; + right.action()->Mult(input, scratch); + serif_ensure(scratch.Size() == left.action()->Width(), "A composed linearized block changed its intermediate coordinate count."); + left.action()->Mult(scratch, output); + }, [left, right, scratch = ::mfem::Vector{}](const auto& input, auto& output) mutable { + scratch.SetSize(left.action()->Width()); + scratch = 0.0; + left.action()->MultTranspose(input, scratch); + serif_ensure(scratch.Size() == right.action()->Height(), "A composed linearized transpose changed its intermediate coordinate count."); + right.action()->MultTranspose(scratch, output); + }, [left, right, scratch = std::vector<::mfem::Vector>{}](std::span inputs, std::span<::mfem::Vector* const> outputs) mutable { + scratch.resize(inputs.size()); + std::vector<::mfem::Vector*> intermediate(inputs.size()); + for (std::size_t k = 0; k < inputs.size(); ++k) { + scratch[k].SetSize(right.action()->Height()); + scratch[k] = 0.0; + intermediate[k] = &scratch[k]; + } + apply_many(*right.action(), inputs, intermediate); + std::vector next(inputs.size()); + for (std::size_t k = 0; k < inputs.size(); ++k) { + serif_ensure(scratch[k].Size() == left.action()->Width(), "A composed linearized block changed its intermediate coordinate count."); + next[k] = &scratch[k]; + } + apply_many(*left.action(), next, outputs); + scratch = {}; + }); + LinearizedBlock::Assembly assembly; + if (left.has_assembly() && right.has_assembly()) { + assembly = [left, right]() { + return std::shared_ptr(::mfem::Mult(*checked_assembly(left), *checked_assembly(right))); + }; + } + return LinearizedBlock(action.action(), {}, std::move(assembly)); + } + + LinearizedBlock sum_blocks(int rows, int columns, std::vector blocks) { + using namespace utils::errors; + + for (const auto& block : blocks) { + serif_ensure(block.action()->Height() == rows && block.action()->Width() == columns, "Summed linearized blocks must have identical dimensions."); + } + const auto action = linearized_action(rows, columns, [blocks, rows, scratch = ::mfem::Vector{}](const auto& input, auto& output) mutable { + scratch.SetSize(rows); + for (const auto& block : blocks) { + scratch = 0.0; + block.action()->Mult(input, scratch); + serif_ensure(scratch.Size() == rows, "A summed linearized block changed its residual coordinate count."); + output += scratch; + } + }, [blocks, columns, scratch = ::mfem::Vector{}](const auto& input, auto& output) mutable { + scratch.SetSize(columns); + for (const auto& block : blocks) { + scratch = 0.0; + block.action()->MultTranspose(input, scratch); + serif_ensure(scratch.Size() == columns, "A summed linearized transpose changed its state coordinate count."); + output += scratch; + } + }, [blocks, rows, scratch = std::vector<::mfem::Vector>{}](std::span inputs, std::span<::mfem::Vector* const> outputs) mutable { + scratch.resize(inputs.size()); + std::vector<::mfem::Vector*> results(inputs.size()); + for (const auto& block : blocks) { + for (std::size_t k = 0; k < inputs.size(); ++k) { + scratch[k].SetSize(rows); + scratch[k] = 0.0; + results[k] = &scratch[k]; + } + apply_many(*block.action(), inputs, results); + for (std::size_t k = 0; k < inputs.size(); ++k) { + serif_ensure(scratch[k].Size() == rows, "A summed linearized block changed its residual coordinate count."); + *outputs[k] += scratch[k]; + } + } + scratch = {}; + }); + LinearizedBlock::Diagonal diagonal; + LinearizedBlock::Assembly assembly; + if (rows == columns && std::ranges::all_of(blocks, [](const auto& block) { + return block.has_diagonal(); + })) { + diagonal = [blocks, rows]() { + std::vector result(rows, 0.0); + for (const auto& block : blocks) { + const auto values = checked_diagonal(block); + for (int index = 0; index < rows; ++index) { + result[index] += values[index]; + } + } + return result; + }; + } + if (std::ranges::all_of(blocks, [](const auto& block) { + return block.has_assembly(); + })) { + assembly = [blocks, rows, columns]() { + auto result = std::make_shared<::mfem::SparseMatrix>(rows, columns); + for (const auto& block : blocks) { + const auto matrix = checked_assembly(block); + for (int row = 0; row < rows; ++row) { + for (int index = 0; index < matrix->RowSize(row); ++index) { + result->Add(row, matrix->GetRowColumns(row)[index], matrix->GetRowEntries(row)[index]); + } + } + } + result->Finalize(); + return result; + }; + } + return LinearizedBlock(action.action(), std::move(diagonal), std::move(assembly)); + } +} // namespace serif::operators::backend::mfem diff --git a/src/lib/serif/backend/mfem/operators/linearized_block.cpp b/src/lib/serif/backend/mfem/operators/linearized_block.cpp new file mode 100644 index 00000000..183f10ac --- /dev/null +++ b/src/lib/serif/backend/mfem/operators/linearized_block.cpp @@ -0,0 +1,109 @@ +#include "serif/backend/mfem/operators/linearized_block.hpp" + +#include +#include +#include "serif/utils/error/errors.hpp" + +namespace serif::operators::backend::mfem +{ + struct LinearizedBlock::Data final + { + Action action; + Diagonal diagonal; + Assembly assembly; + }; + + LinearizedBlock::LinearizedBlock(Action action, Diagonal diagonal, Assembly assembly) + { + utils::errors::serif_ensure(action != nullptr, + "An accepted physical block requires a non-null operator. Supply the action from a successful linearization."); + m_data = std::make_shared(Data{std::move(action), std::move(diagonal), std::move(assembly)}); + } + + const LinearizedBlock::Action& LinearizedBlock::action() const noexcept + { + return m_data->action; + } + + bool LinearizedBlock::has_diagonal() const noexcept + { + return static_cast(m_data->diagonal); + } + + bool LinearizedBlock::has_assembly() const noexcept + { + return static_cast(m_data->assembly); + } + + std::vector LinearizedBlock::diagonal() const + { + utils::errors::serif_ensure(has_diagonal(), + "This physical block has no diagonal extractor. Supply an element-based extractor or an assembled matrix."); + return m_data->diagonal(); + } + + std::shared_ptr LinearizedBlock::assemble() const + { + utils::errors::serif_ensure(has_assembly(), + "This physical block has no sparse assembler. Supply its selected matrix assembly routine."); + return m_data->assembly(); + } + + std::vector checked_diagonal(const LinearizedBlock& block) + { + std::vector result = block.diagonal(); + utils::errors::serif_ensure(result.size() == static_cast(std::min(block.action()->Height(), block.action()->Width())), + "A physical block diagonal must contain one value per diagonal coordinate. Return the diagonal of the declared operator dimensions."); + return result; + } + + std::shared_ptr checked_assembly(const LinearizedBlock& block) + { + const auto result = block.assemble(); + utils::errors::serif_ensure(result && result->Finalized() && result->Height() == block.action()->Height() && result->Width() == block.action()->Width(), + "A physical block assembler must return a non-null finalized matrix matching its declared operator dimensions."); + return result; + } + + LinearizedBlock linearized_block(std::shared_ptr matrix) + { + utils::errors::serif_ensure(matrix != nullptr && matrix->Finalized(), + "An assembled physical block requires a non-null finalized sparse matrix."); + return LinearizedBlock(matrix, [matrix]() + { + ::mfem::Vector diagonal; + matrix->GetDiag(diagonal); + return std::vector(diagonal.GetData(), diagonal.GetData() + diagonal.Size()); + }, [matrix]() + { + return matrix; + }); + } + + LinearizedBlock linearized_block(std::shared_ptr matrix) + { + utils::errors::serif_ensure(matrix != nullptr, + "An assembled physical block requires a non-null dense matrix."); + return LinearizedBlock(matrix, [matrix]() + { + std::vector diagonal(std::min(matrix->Height(), matrix->Width())); + for (std::size_t index = 0; index < diagonal.size(); ++index) + { + diagonal[index] = (*matrix)(index, index); + } + return diagonal; + }, [matrix]() + { + auto sparse = std::make_shared<::mfem::SparseMatrix>(matrix->Height(), matrix->Width()); + for (int row = 0; row < matrix->Height(); ++row) + { + for (int column = 0; column < matrix->Width(); ++column) + { + sparse->Add(row, column, (*matrix)(row, column)); + } + } + sparse->Finalize(); + return sparse; + }); + } +} // namespace serif::operators::backend::mfem diff --git a/src/lib/serif/backend/mfem/operators/replaced_rows.cpp b/src/lib/serif/backend/mfem/operators/replaced_rows.cpp new file mode 100644 index 00000000..449a720e --- /dev/null +++ b/src/lib/serif/backend/mfem/operators/replaced_rows.cpp @@ -0,0 +1,151 @@ +#include "serif/backend/mfem/operators/replaced_rows.hpp" + +#include +#include +#include +#include "serif/utils/error/errors.hpp" + +namespace serif::operators::backend::mfem +{ + namespace + { + /** @brief Own the accepted original action and the sparse physical rows replacing it. */ + struct ReplacedRows final + { + LinearizedBlock original; + std::vector selected; + std::shared_ptr replacement; + }; + + /** @brief Apply frozen boundary rows without allocating or evaluating unrelated physical fields. */ + class ReplacedRowsOperator final : public ::mfem::Operator + { + public: + /** @brief Allocate reusable input and boundary buffers before publishing the action. */ + explicit ReplacedRowsOperator(std::shared_ptr data) + : ::mfem::Operator(data->original.action()->Height(), data->original.action()->Width()), m_data(std::move(data)), m_input(Width()), m_boundary(m_data->replacement->Height()), m_dual(Height()) + { + } + + /** @brief Copy the input before writing any output so aliased callers preserve all column values. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + using namespace utils::errors; + + serif_ensure(input.Size() == Width() && output.Size() == Height(), "A row-replaced physical block requires input/output lengths {}/{}, received {}/{}.", Width(), Height(), input.Size(), output.Size()); + m_input = input; + m_data->original.action()->Mult(m_input, output); + m_data->replacement->Mult(m_input, m_boundary); + for (int row = 0; row < m_boundary.Size(); ++row) + { + output[m_data->selected[row]] = m_boundary[row]; + } + } + + /** @brief Remove replaced volume rows before adding the exact transpose of their immutable boundary stencils. */ + void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + using namespace utils::errors; + + serif_ensure(input.Size() == Height() && output.Size() == Width(), "A transposed row-replaced physical block requires input/output lengths {}/{}, received {}/{}.", Height(), Width(), input.Size(), output.Size()); + m_dual = input; + for (int row = 0; row < m_boundary.Size(); ++row) + { + const int selected = m_data->selected[row]; + m_boundary[row] = m_dual[selected]; + m_dual[selected] = 0.0; + } + m_data->original.action()->MultTranspose(m_dual, output); + m_data->replacement->MultTranspose(m_boundary, m_input); + output += m_input; + } + + private: + std::shared_ptr m_data; + mutable ::mfem::Vector m_input, m_boundary, m_dual; + }; + + /** @brief Extract self-column entries, not the sum of an interpolating boundary stencil. */ + [[nodiscard]] std::vector replaced_diagonal(const ReplacedRows& data) + { + using namespace utils::errors; + + std::vector diagonal = data.original.diagonal(); + const int size = std::min(data.original.action()->Height(), data.original.action()->Width()); + serif_ensure(diagonal.size() == static_cast(size), "The selected physical diagonal has {} entries; its rectangular self-column range requires {}.", diagonal.size(), size); + for (std::size_t index = 0; index < data.selected.size(); ++index) + { + const int row = data.selected[index]; + if (row < size) + { + diagonal[row] = (*data.replacement)(static_cast(index), row); + } + } + return diagonal; + } + + /** @brief Assemble from sparse rows, dropping all original entries on constrained rows. */ + [[nodiscard]] std::shared_ptr assemble_replaced_rows(const ReplacedRows& data) + { + using namespace utils::errors; + + const auto original = data.original.assemble(); + serif_ensure(original != nullptr && original->Finalized() && original->Height() == data.original.action()->Height() && original->Width() == data.original.action()->Width(), "The original physical assembler must return a finalized matrix matching its accepted action."); + auto result = std::make_shared<::mfem::SparseMatrix>(original->Height(), original->Width()); + std::vector replacement_row(original->Height(), -1); + for (std::size_t index = 0; index < data.selected.size(); ++index) + { + replacement_row[data.selected[index]] = static_cast(index); + } + for (int row = 0; row < original->Height(); ++row) + { + const bool replaced = replacement_row[row] >= 0; + const ::mfem::SparseMatrix& matrix = replaced ? *data.replacement : *original; + const int source_row = replaced ? replacement_row[row] : row; + for (int entry = matrix.GetI()[source_row]; entry < matrix.GetI()[source_row + 1]; ++entry) + { + result->Add(row, matrix.GetJ()[entry], matrix.GetData()[entry]); + } + } + result->Finalize(); + return result; + } + } // namespace + + LinearizedBlock replace_block_rows(LinearizedBlock original, std::vector selected_rows, std::shared_ptr replacement) + { + using namespace utils::errors; + + serif_ensure(replacement != nullptr && replacement->Finalized(), "Physical row replacement requires a finalized immutable sparse row block."); + serif_ensure(selected_rows.size() == static_cast(replacement->Height()) && replacement->Width() == original.action()->Width(), "The replacement matrix must have one row per selected residual coordinate and the same physical columns as the original block."); + std::vector used(original.action()->Height(), false); + for (int row : selected_rows) + { + serif_ensure(row >= 0 && row < original.action()->Height(), "Replacement residual row {} lies outside the physical block of height {}.", row, original.action()->Height()); + serif_ensure(!used[row], "Replacement residual row {} was selected more than once.", row); + used[row] = true; + } + for (int entry = 0; entry < replacement->NumNonZeroElems(); ++entry) + { + serif_ensure(std::isfinite(replacement->GetData()[entry]), "A physical replacement row contains a nonfinite entry. Supply a finite accepted linearization."); + } + auto data = std::make_shared(ReplacedRows{.original = std::move(original), .selected = std::move(selected_rows), .replacement = std::move(replacement)}); + LinearizedBlock::Diagonal diagonal; + LinearizedBlock::Assembly assembly; + if (data->original.has_diagonal()) + { + diagonal = [data]() + { + return replaced_diagonal(*data); + }; + } + if (data->original.has_assembly()) + { + assembly = [data]() + { + return assemble_replaced_rows(*data); + }; + } + return LinearizedBlock(std::make_shared(data), std::move(diagonal), std::move(assembly)); + } +} // namespace serif::operators::backend::mfem diff --git a/src/lib/serif/backend/mfem/projection/radial.cpp b/src/lib/serif/backend/mfem/projection/radial.cpp new file mode 100644 index 00000000..aecc90b4 --- /dev/null +++ b/src/lib/serif/backend/mfem/projection/radial.cpp @@ -0,0 +1,269 @@ +#include "serif/backend/mfem/projection/radial.hpp" + +#include +#include +#include +#include + +#include "mfem/fem/coefficient.hpp" +#include "serif/backend/mfem/mapping/evaluator.hpp" +#include "serif/backend/mfem/mapping/transformations.hpp" +#include "serif/backend/mfem/shared/finite.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::utils::projection::backend::mfem { + namespace { + /** @brief Share checked physical sampling between scalar and radial-vector coefficients. */ + struct RadialSampler { + const std::function& profile; + const RadialProjectionOptions& options; + ::mfem::Vector position; + mapping::MappingPointContext context; + std::optional failure; + double radius{0.0}; + bool at_infinity{false}; + + /** @brief Evaluate physical position and radius, retaining the first numerical failure. */ + [[nodiscard]] bool locate( + ::mfem::ElementTransformation& transformation, + const ::mfem::IntegrationPoint& point + ) { + if (failure) { + return false; + } + at_infinity = false; + if (options.mapping != nullptr) { + const mapping::MappingStatus status = options.mapping->evaluate_point(transformation, point, context); + if (status == mapping::MappingStatus::AT_COMPACTIFIED_INFINITY) { + transformation.Transform(point, position); + if (!checks::is_finite(position)) { + failure = "Radial projection encountered non-finite mesh coordinates at compactified infinity."; + return false; + } + at_infinity = true; + return true; + } + if (status != mapping::MappingStatus::VALID) { + failure = "Radial projection requires a valid physical mapping at every sampled point."; + return false; + } + position = context.physical_position; + } else { + transformation.Transform(point, position); + } + radius = 0.0; + for (int component = 0; component < position.Size(); ++component) { + position[component] -= options.center[component]; + radius = std::hypot(radius, position[component]); + } + if (!std::isfinite(radius)) { + failure = "Radial projection encountered a non-finite physical radius."; + return false; + } + return true; + } + + /** @brief Reject non-finite values even when a profile does not itself report failure. */ + [[nodiscard]] double checked_value(const RadialValue& result) { + if (!result) { + failure = "Radial projection profile rejected a sample: " + result.error(); + return 0.0; + } + if (!std::isfinite(*result)) { + failure = "Radial projection profile returned a non-finite value."; + return 0.0; + } + return *result; + } + }; + + /** @brief Sample a scalar; integral-mapped elements additionally preserve physical volume. */ + class RadialScalarCoefficient final : public ::mfem::Coefficient { + public: + /** @brief Borrow checked sampling for the duration of one native MFEM projection. */ + RadialScalarCoefficient(RadialSampler& sampler, const bool integral_mapping) : + m_sampler(sampler), m_integral_mapping(integral_mapping) {} + + /** @brief Return the scalar in the mesh's coordinates, before its native element mapping. */ + double Eval(::mfem::ElementTransformation& transformation, const ::mfem::IntegrationPoint& point) override { + if (!m_sampler.locate(transformation, point)) { + return 0.0; + } + if (m_sampler.at_infinity) { + if (!m_sampler.options.scalar_at_infinity) { + m_sampler.failure = "Radial scalar projection at compactified infinity requires an explicit scalar_at_infinity limit in mesh coordinates."; + return 0.0; + } + return m_sampler.checked_value(m_sampler.options.scalar_at_infinity(m_sampler.position)); + } + double value = m_sampler.checked_value(m_sampler.profile(m_sampler.radius)); + if (m_integral_mapping && m_sampler.options.mapping != nullptr) { + value *= m_sampler.context.mapping_determinate; + } + if (!std::isfinite(value)) { + m_sampler.failure = "Radial scalar projection overflowed while pulling back the physical value."; + return 0.0; + } + return value; + } + + private: + RadialSampler& m_sampler; + bool m_integral_mapping; + }; + + /** @brief Convert a physical radial vector to the coordinates expected by native vector projection. */ + class RadialVectorCoefficient final : public ::mfem::VectorCoefficient { + public: + /** @brief Sample one physical vector component per spatial dimension. */ + RadialVectorCoefficient(RadialSampler& sampler, const int dimension) : + ::mfem::VectorCoefficient(dimension), m_sampler(sampler) {} + + /** @brief Evaluate the physical radial vector at one mesh point. */ + void Eval( + ::mfem::Vector& value, + ::mfem::ElementTransformation& transformation, + const ::mfem::IntegrationPoint& point + ) override { + value.SetSize(GetVDim()); + value = 0.0; + if (!m_sampler.locate(transformation, point)) { + return; + } + if (m_sampler.at_infinity) { + if (!m_sampler.options.vector_at_infinity) { + m_sampler.failure = "Radial vector projection at compactified infinity requires an explicit vector_at_infinity limit in mesh coordinates."; + return; + } + const auto limit = m_sampler.options.vector_at_infinity(m_sampler.position); + if (!limit) { + m_sampler.failure = "Radial vector projection rejected its infinity limit: " + limit.error(); + return; + } + if (limit->Size() != GetVDim() || !checks::is_finite(*limit)) { + m_sampler.failure = "Radial vector projection requires a finite infinity limit matching the spatial dimension."; + return; + } + value = *limit; + return; + } + const double amplitude = m_sampler.checked_value(m_sampler.profile(m_sampler.radius)); + if (m_sampler.failure) { + return; + } + if (m_sampler.radius == 0.0) { + if (amplitude != 0.0) { + m_sampler.failure = "A radial vector requires zero amplitude at its center to have a unique value."; + } + return; + } + for (int component = 0; component < value.Size(); ++component) { + value[component] = amplitude * (m_sampler.position[component] / m_sampler.radius); + } + if (!checks::is_finite(value)) { + m_sampler.failure = "Radial vector projection overflowed while evaluating the physical vector."; + value = 0.0; + } + } + + private: + RadialSampler& m_sampler; + }; + + /** @brief Reject unsupported caller bindings before MFEM reaches an internal assertion. */ + [[nodiscard]] int validate_space( + ::mfem::FiniteElementSpace& space, const bool vector, const RadialProjectionOptions& options + ) { + const ::mfem::Mesh* mesh = space.GetMesh(); + if (mesh == nullptr || mesh->GetNE() == 0 || mesh->Dimension() != mesh->SpaceDimension() || + mesh->SpaceDimension() < 1 || mesh->SpaceDimension() > 3) { + errors::serif_error( + "Radial projection requires a nonempty volume mesh with one to three spatial dimensions; " + "supply a finite-element space on such a mesh." + ); + } + if (options.mapping != nullptr && options.mapping->mesh() != mesh) { + errors::serif_error( + "Radial projection and its geometry mapping must use the same mesh; " + "supply a mapping constructed from fields on the projection space's mesh." + ); + } + const int map_type = space.GetFE(0)->GetMapType(); + for (int element = 0; element < space.GetNE(); ++element) { + const ::mfem::FiniteElement& finite_element = *space.GetFE(element); + const bool scalar_element = finite_element.GetRangeType() == ::mfem::FiniteElement::SCALAR; + const bool supported_scalar = scalar_element && + (map_type == ::mfem::FiniteElement::VALUE || map_type == ::mfem::FiniteElement::INTEGRAL); + const bool supported_vector = + scalar_element && map_type == ::mfem::FiniteElement::VALUE && space.GetVDim() == mesh->SpaceDimension(); + if (finite_element.GetMapType() != map_type || + (!vector && (!supported_scalar || space.GetVDim() != 1)) || (vector && !supported_vector)) { + errors::serif_error( + "Radial projection received an incompatible finite-element space. Scalars require scalar " + "H1/L2 elements; vectors require one component per spatial dimension." + ); + } + } + return map_type; + } + + /** @brief Numeric center errors are recoverable and never expose a partially projected field. */ + [[nodiscard]] bool finite_center(const RadialProjectionOptions& options) { + for (const double coordinate : options.center) { + if (!std::isfinite(coordinate)) { + return false; + } + } + return true; + } + + /** @brief Publish a projected field only after sampling and all coefficient values succeed. */ + template + [[nodiscard]] ProjectionResult project( + ::mfem::FiniteElementSpace& space, Coefficient& coefficient, RadialSampler& sampler + ) { + ProjectionResult result(std::in_place, &space); + *result = 0.0; + result->ProjectCoefficient(coefficient); + if (sampler.failure) { + return std::unexpected(*sampler.failure); + } + // Native element interpolation does not enforce hanging-node or hanging-face constraints. + result->RestrictConforming(); + if (!checks::is_finite(*result)) { + return std::unexpected("Radial projection produced non-finite finite-element coefficients."); + } + return result; + } + } + + namespace detail { + ProjectionResult project_scalar( + ::mfem::FiniteElementSpace& space, + const std::function& profile, + const RadialProjectionOptions& options + ) { + const int map_type = validate_space(space, false, options); + if (!finite_center(options)) { + return std::unexpected("Radial projection requires a finite center."); + } + RadialSampler sampler{.profile = profile, .options = options}; + RadialScalarCoefficient coefficient(sampler, map_type == ::mfem::FiniteElement::INTEGRAL); + return project(space, coefficient, sampler); + } + + ProjectionResult project_vector( + ::mfem::FiniteElementSpace& space, + const std::function& radial_amplitude, + const RadialProjectionOptions& options + ) { + static_cast(validate_space(space, true, options)); + if (!finite_center(options)) { + return std::unexpected("Radial projection requires a finite center."); + } + RadialSampler sampler{.profile = radial_amplitude, .options = options}; + RadialVectorCoefficient coefficient(sampler, space.GetMesh()->SpaceDimension()); + return project(space, coefficient, sampler); + } + } +} diff --git a/src/lib/serif/backend/mfem/runtime.cpp b/src/lib/serif/backend/mfem/runtime.cpp new file mode 100644 index 00000000..da4bac42 --- /dev/null +++ b/src/lib/serif/backend/mfem/runtime.cpp @@ -0,0 +1,18 @@ +#include "serif/backend/mfem/runtime.hpp" + +#include "mfem/general/communication.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::backend::mfem { + void initialize() { +#ifdef MFEM_USE_MPI + using namespace utils::errors; + + serif_ensure(!::mfem::Mpi::IsFinalized(), + "The numerical runtime cannot restart a finalized MPI session. Initialize SERiF before solving and finalize MPI only after every numerical resource is released."); + if (!::mfem::Mpi::IsInitialized()) { + ::mfem::Mpi::Init(); + } +#endif + } +} diff --git a/src/lib/serif/backend/mfem/serialize/resources.cpp b/src/lib/serif/backend/mfem/serialize/resources.cpp new file mode 100644 index 00000000..0d63aac9 --- /dev/null +++ b/src/lib/serif/backend/mfem/serialize/resources.cpp @@ -0,0 +1,432 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "serif/backend/mfem/serialize/resources.hpp" +#include "mfem/general/version.hpp" +#if defined(MFEM_USE_MPI) && __has_include() +#include +#endif +#if defined(MFEM_USE_METIS) && __has_include() +#include +#endif +#if defined(MFEM_USE_ZLIB) && __has_include() +#include +#endif + +#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp" + +namespace serif::backend::mfem::serialize { + namespace { + using namespace utils::serialize; + + /** @brief Keep unsupported inputs visible in an otherwise inspectable archive. */ + Node unsupported(std::string message) { + return {.type = "unsupported", .value = std::move(message)}; + } + + /** @brief Report the actual linked MFEM configuration and available dependency versions without initializing MPI. */ + Node libraries() { + Node result = object("mfem.libraries", field("mfem_headers", std::string{MFEM_VERSION_STRING}), + field("mfem_library", std::string{::mfem::GetVersionStr()}), field("mfem_git", std::string{::mfem::GetGitStr()}), + field("mfem_features", std::string{::mfem::GetConfigStr()})); +#ifdef HYPRE_RELEASE_VERSION + result.children.emplace("hypre", encode(std::string{HYPRE_RELEASE_VERSION})); +#endif +#ifdef METIS_VER_MAJOR + result.children.emplace("metis", encode(std::to_string(METIS_VER_MAJOR) + "." + std::to_string(METIS_VER_MINOR) + "." + std::to_string(METIS_VER_SUBMINOR))); +#endif +#ifdef ZLIB_VERSION + result.children.emplace("zlib_headers", encode(std::string{ZLIB_VERSION})); + result.children.emplace("zlib_library", encode(std::string{zlibVersion()})); +#endif +#ifdef MFEM_USE_MPI + result.children.emplace("mpi_standard", encode(std::to_string(MPI_VERSION) + "." + std::to_string(MPI_SUBVERSION))); +#ifdef OPEN_MPI + result.children.emplace("mpi_implementation", encode(std::string{"Open MPI "} + std::to_string(OMPI_MAJOR_VERSION) + "." + std::to_string(OMPI_MINOR_VERSION) + "." + std::to_string(OMPI_RELEASE_VERSION))); +#elif defined(MPICH_VERSION) + result.children.emplace("mpi_implementation", encode(std::string{MPICH_VERSION})); +#endif + int initialized = 0; + int finalized = 0; + MPI_Initialized(&initialized); + MPI_Finalized(&finalized); + if (initialized && !finalized) { + char version[MPI_MAX_LIBRARY_VERSION_STRING]; + int length = 0; + if (MPI_Get_library_version(version, &length) == MPI_SUCCESS) { + result.children.emplace("mpi_library", encode(std::string{version, static_cast(length)})); + } + } +#endif + return result; + } + + /** @brief Preserve signed native element numbering, including every element's local extent. */ + std::vector element_numbering(const ::mfem::FiniteElementSpace& space) { + std::vector result; + ::mfem::Array dofs; + for (int element = 0; element < space.GetNE(); ++element) { + space.GetElementVDofs(element, dofs); + result.push_back(dofs.Size()); + result.insert(result.end(), dofs.begin(), dofs.end()); + } + return result; + } + + /** @brief Describe supported constructors rather than accepting arbitrary MFEM collection factory strings. */ + Node save_collection(const ::mfem::FiniteElementCollection& collection) { + int order = collection.GetOrder(); + int closed = -1; + int open = -1; + std::string family; + if (typeid(collection) == typeid(::mfem::H1_FECollection)) { + family = "H1"; + closed = static_cast(collection).GetBasisType(); + } else if (typeid(collection) == typeid(::mfem::L2_FECollection)) { + family = "L2"; + open = static_cast(collection).GetBasisType(); + } else { + return unsupported("Only standard H1 and L2 collections have an MFEM archive constructor."); + } + if (std::string_view{collection.Name()}.contains("Pyr") || (family == "L2" && collection.GetMapType(3) != ::mfem::FiniteElement::VALUE)) { + return unsupported("Nondefault pyramid bases and integral-mapped scalar collections need an explicit archive constructor."); + } + return object("mfem.collection", field("family", family), field("order", order), field("closed_basis", closed), + field("open_basis", open), field("name", std::string{collection.Name()})); + } + + /** @brief Validate constructor parameters before entering MFEM, then verify the complete native basis name. */ + Result> restore_collection(const Node& node) { + auto fields = read_object(node, "mfem.collection", [](std::string family, int order, int closed, int open, std::string name) { + return std::tuple{std::move(family), order, closed, open, std::move(name)}; + }, member("family"), member("order"), member("closed_basis"), member("open_basis"), member("name")); + if (!fields) { + return std::unexpected(fields.error()); + } + const auto& [family, order, closed, open, name] = *fields; + const auto valid = [](int basis) { return basis >= 0 && basis < ::mfem::BasisType::NumBasisTypes; }; + if (order < 0 || (family == "H1" && order == 0) || + (family == "H1" && !valid(closed)) || (family == "L2" && !valid(open))) { + return std::unexpected(Error{"collection", "Invalid finite-element order or basis."}); + } + std::unique_ptr<::mfem::FiniteElementCollection> result; + if (family == "H1") { + result = std::make_unique<::mfem::H1_FECollection>(order, 3, closed); + } else if (family == "L2") { + result = std::make_unique<::mfem::L2_FECollection>(order, 3, open); + } else { + return std::unexpected(Error{"collection/family", "Unsupported finite-element family."}); + } + if (name != result->Name()) { + return std::unexpected(Error{"collection/name", "The saved collection requires a different basis, map type or constructor than this loader supports."}); + } + return result; + } + + /** @brief Reject unsupported mesh ownership and topology before emitting a supposedly restorable payload. */ + bool supported_mesh(const ::mfem::Mesh& mesh) { + if (mesh.Dimension() != 3 || mesh.SpaceDimension() != 3 || mesh.NURBSext != nullptr || typeid(mesh) != typeid(::mfem::Mesh)) { + return false; + } + if (const auto* nodes = mesh.GetNodes()) { + for (int dof = 0; dof < nodes->Size(); ++dof) { + if (!std::isfinite((*nodes)[dof])) { + return false; + } + } + } else { + for (int vertex = 0; vertex < mesh.GetNV(); ++vertex) { + for (int component = 0; component < mesh.SpaceDimension(); ++component) { + if (!std::isfinite(mesh.GetVertex(vertex)[component])) { + return false; + } + } + } + } + return true; + } + + /** @brief Load only the exact ordered mesh represented by the saved content identity. */ + Result> restore_mesh(const Node& node, const MeshResolver& resolver) { + auto identity = read_field(node, "identity"); + if (node.type != "mfem.mesh" || !identity) { + return std::unexpected(Error{"mesh", "Missing supported mesh schema or content identity."}); + } + Result text = std::unexpected(Error{"mesh", "This archive omits its reference mesh. Supply a mesh resolver for its content identity."}); + if (node.children.contains("native")) { + text = read_field(node, "native"); + } else if (resolver) { + text = resolver(*identity); + } + if (!text) { + return std::unexpected(text.error()); + } + if (fingerprint(*text) != *identity) { + return std::unexpected(Error{"mesh/identity", "Reference mesh content differs from the saved identity."}); + } + std::istringstream stream(*text); + stream.imbue(std::locale::classic()); + // Refinement marking and orientation repair can change coefficient numbering. + auto mesh = std::make_unique<::mfem::Mesh>(stream, 1, 0, false); + if (!supported_mesh(*mesh) || mesh_text(*mesh) != *text) { + return std::unexpected(Error{"mesh", "Native mesh reconstruction changed ordered topology or coordinates, or used unsupported mesh capabilities."}); + } + return mesh; + } + } + + std::string mesh_text(const ::mfem::Mesh& mesh) { + std::ostringstream stream; + stream.imbue(std::locale::classic()); + stream << std::setprecision(std::numeric_limits::max_digits10); + mesh.Print(stream); + return stream.str(); + } + + ResourceWriter::ResourceWriter(SaveOptions options) : m_options(options), m_record{.type = "mfem.resources"} { + using namespace utils::serialize; + + m_record.children.emplace("mfem_version", encode(std::string{MFEM_VERSION_STRING})); + m_record.children.emplace("libraries", libraries()); + m_record.children.emplace("meshes", Node{.type = "array"}); + m_record.children.emplace("collections", Node{.type = "array"}); + m_record.children.emplace("spaces", Node{.type = "array"}); + } + + std::uint64_t ResourceWriter::mesh(const ::mfem::Mesh& value) { + using namespace utils::serialize; + + const auto found = std::find(m_meshes.begin(), m_meshes.end(), &value); + if (found != m_meshes.end()) { + return static_cast(found - m_meshes.begin()); + } + const std::uint64_t index = m_meshes.size(); + Node record; + if (supported_mesh(value)) { + const std::string text = mesh_text(value); + record = object("mfem.mesh", field("identity", fingerprint(text))); + if (m_options.embed_mesh) { + record.children.emplace("native", encode(text)); + } + } else { + record = unsupported("Exact MFEM mesh archives require finite serial three-dimensional meshes without NURBS."); + } + m_record.children.at("meshes").children.emplace(std::to_string(index), std::move(record)); + m_meshes.push_back(&value); + return index; + } + + std::uint64_t ResourceWriter::collection(const ::mfem::FiniteElementCollection& value) { + const auto found = std::find(m_collections.begin(), m_collections.end(), &value); + if (found != m_collections.end()) { + return static_cast(found - m_collections.begin()); + } + const std::uint64_t index = m_collections.size(); + m_record.children.at("collections").children.emplace(std::to_string(index), save_collection(value)); + m_collections.push_back(&value); + return index; + } + + std::uint64_t ResourceWriter::space(const ::mfem::FiniteElementSpace& value) { + using namespace utils::serialize; + + const auto found = std::find(m_spaces.begin(), m_spaces.end(), &value); + if (found != m_spaces.end()) { + return static_cast(found - m_spaces.begin()); + } + const std::uint64_t index = m_spaces.size(); + Node record; + if (value.GetMesh() == nullptr || value.FEColl() == nullptr || value.IsVariableOrder() || typeid(value) != typeid(::mfem::FiniteElementSpace)) { + record = unsupported("Exact MFEM space archives currently require initialized fixed-order serial spaces."); + } else { + record = object("mfem.space", field("mesh", mesh(*value.GetMesh())), field("collection", collection(*value.FEColl())), + field("vector_dimension", value.GetVDim()), field("ordering", static_cast(value.GetOrdering())), + field("size", value.GetVSize()), field("element_numbering", element_numbering(value))); + } + m_record.children.at("spaces").children.emplace(std::to_string(index), std::move(record)); + m_spaces.push_back(&value); + return index; + } + + void ResourceWriter::mapping(const ::mfem::GridFunction& compactification, const serif::mapping::DomainMapper& mapper) { + using namespace utils::serialize; + + if (compactification.FESpace() == nullptr) { + m_record.children.emplace("compactification", unsupported("The compactification field has no finite-element space.")); + } else { + m_record.children.emplace("compactification", object("mfem.field", field("space", space(*compactification.FESpace())), + field("values", std::vector(compactification.HostRead(), compactification.HostRead() + compactification.Size())))); + } + const auto* kelvin = dynamic_cast(&mapper.get_exterior_map()); + if (kelvin == nullptr) { + m_record.children.emplace("mapper", unsupported("No serializer is registered for this exterior-domain map.")); + } else { + m_record.children.emplace("mapper", object("mfem.kelvin_mapper", field("dimension", mapper.get_dimension()), + field("vacuum_attribute", mapper.options().vacuum_element_attribute_ID), field("stellar_radius", kelvin->get_reference_stellar_radius()), + field("infinity_radius", kelvin->get_reference_infinity_radius()), field("coordinate_tolerance", kelvin->get_coordinate_tolerance()))); + } + } + + utils::serialize::Node ResourceWriter::reference(const geometry::deformation::backend::mfem::ReferenceDiscretization& value) { + using namespace utils::serialize; + + return object("mfem.deformation_reference", field("scalar_space", space(value.scalar_space)), field("displacement_space", space(value.displacement_space)), + field("logical_mesh", mesh(value.logical_mesh)), field("stellar_attributes", value.stellar_attributes), field("vacuum_attributes", value.vacuum_attributes), + field("surface_attribute", value.surface_attribute), field("infinity_attribute", value.infinity_attribute)); + } + + utils::serialize::Node ResourceWriter::finish() && { + return std::move(m_record); + } + + utils::serialize::Result restore_resources(const utils::serialize::Node& node, const MeshResolver& resolver) { + using namespace utils::serialize; + + if (node.type != "mfem.resources" || node.version != 1) { + return std::unexpected(Error{"resources", "Unsupported MFEM resource schema."}); + } +#ifndef MFEM_USE_EXCEPTIONS + return std::unexpected(Error{"resources", "Safe native MFEM archive parsing requires MFEM_USE_EXCEPTIONS; rebuild MFEM with exception support."}); +#else + if (::mfem::get_error_action() != ::mfem::MFEM_ERROR_THROW) { + return std::unexpected(Error{"resources", "Safe native MFEM archive parsing requires MFEM_ERROR_THROW so invalid input can be reported instead of terminating the application."}); + } + try { + auto version = read_field(node, "mfem_version"); + auto meshes = read_field>(node, "meshes"); + auto collections = read_field>(node, "collections"); + auto spaces = read_field>(node, "spaces"); + if (!version || *version != MFEM_VERSION_STRING || !meshes || !collections || !spaces) { + return std::unexpected(Error{"resources", "Missing resource tables or a different MFEM version; exact native coefficient interpretation is required."}); + } + Resources result; + for (const Node& record : *meshes) { + auto value = restore_mesh(record, resolver); + if (!value) { + return std::unexpected(value.error()); + } + result.meshes.push_back(std::move(*value)); + } + for (const Node& record : *collections) { + auto value = restore_collection(record); + if (!value) { + return std::unexpected(value.error()); + } + result.collections.push_back(std::move(*value)); + } + for (const Node& record : *spaces) { + auto fields = read_object(record, "mfem.space", [](std::uint64_t mesh, std::uint64_t collection, int dimension, int ordering, int size, std::vector numbering) { + return std::tuple{mesh, collection, dimension, ordering, size, std::move(numbering)}; + }, member("mesh"), member("collection"), member("vector_dimension"), member("ordering"), member("size"), member>("element_numbering")); + if (!fields) { + return std::unexpected(fields.error()); + } + const auto& [mesh, collection, dimension, ordering, size, numbering] = *fields; + if (mesh >= result.meshes.size() || collection >= result.collections.size() || dimension < 1 || + (ordering != ::mfem::Ordering::byNODES && ordering != ::mfem::Ordering::byVDIM)) { + return std::unexpected(Error{"spaces", "Invalid mesh/collection association, vector dimension or coefficient ordering."}); + } + auto space = std::make_unique<::mfem::FiniteElementSpace>(result.meshes[mesh].get(), result.collections[collection].get(), dimension, ordering); + if (space->GetVSize() != size || element_numbering(*space) != numbering) { + return std::unexpected(Error{"spaces", "Reconstructed finite-element numbering differs from the archived coefficient interpretation."}); + } + result.spaces.push_back(std::move(space)); + } + auto field = read_field(node, "compactification"); + auto mapper = read_field(node, "mapper"); + if (!field || !mapper) { + return std::unexpected(Error{"resources", "Compactification and mapper records are required."}); + } + auto compactification = read_object(*field, "mfem.field", [](std::uint64_t space, std::vector values) { + return std::pair{space, std::move(values)}; + }, member("space"), member>("values")); + if (!compactification) { + return std::unexpected(compactification.error()); + } + const auto& [space, values] = *compactification; + if (space >= result.spaces.size() || values.size() != static_cast(result.spaces[space]->GetVSize()) || + !std::ranges::all_of(values, [](double value) { return std::isfinite(value); })) { + return std::unexpected(Error{"compactification", "Compactification coefficients do not match their space or are not finite."}); + } + result.compactification = std::make_unique<::mfem::GridFunction>(result.spaces[space].get()); + std::copy(values.begin(), values.end(), result.compactification->HostWrite()); + auto mapping = read_object(*mapper, "mfem.kelvin_mapper", [](int dimension, int vacuum, double stellar, double infinity, double tolerance) { + return std::make_unique(utils::user_options::DomainMapperOptions{.dimension = dimension, .vacuum_element_attribute_ID = vacuum}, + std::make_unique(utils::user_options::KelvinCompactificationOptions{ + .r_star_ref = stellar, .r_inf_ref = infinity, .coordinate_tolerance = tolerance})); + }, member("dimension"), member("vacuum_attribute"), member("stellar_radius"), member("infinity_radius"), member("coordinate_tolerance")); + if (!mapping) { + return std::unexpected(mapping.error()); + } + result.mapper = std::move(*mapping); + return result; + } catch (const std::exception& error) { + return std::unexpected(Error{"resources", "Cannot reconstruct saved MFEM resources: " + std::string{error.what()}}); + } +#endif + } + + utils::serialize::Result restore_reference(const utils::serialize::Node& node, Resources& resources) { + using namespace utils::serialize; + using namespace geometry::deformation::backend::mfem; + + auto fields = read_object(node, "mfem.deformation_reference", [](std::uint64_t scalar, std::uint64_t displacement, std::uint64_t logical, + std::vector stellar, std::vector vacuum, int surface, int infinity) { + return std::tuple{scalar, displacement, logical, std::move(stellar), std::move(vacuum), surface, infinity}; + }, member("scalar_space"), member("displacement_space"), member("logical_mesh"), + member>("stellar_attributes"), member>("vacuum_attributes"), member("surface_attribute"), member("infinity_attribute")); + if (!fields) { + return std::unexpected(fields.error()); + } + const auto& [scalar, displacement, logical, stellar, vacuum, surface, infinity] = *fields; + if (scalar >= resources.spaces.size() || displacement >= resources.spaces.size() || logical >= resources.meshes.size()) { + return std::unexpected(Error{"deformation_reference", "Reference mesh or space index is outside the restored resource tables."}); + } + const auto status = validate_scalar_reference_space(*resources.spaces[scalar]); + const auto companions = validate_companion_numbering(*resources.spaces[scalar], *resources.spaces[displacement]); + const auto positions = sample_reference_positions(*resources.spaces[scalar], *resources.meshes[logical]); + if (!status || !companions || !positions || resources.spaces[displacement]->GetVDim() != 3) { + return std::unexpected(Error{"deformation_reference", "Restored physical/logical meshes and companion spaces do not describe the same ordered reference geometry."}); + } + return ReferenceDiscretization{.scalar_space = *resources.spaces[scalar], .displacement_space = *resources.spaces[displacement], + .logical_mesh = *resources.meshes[logical], .stellar_attributes = stellar, .vacuum_attributes = vacuum, + .surface_attribute = surface, .infinity_attribute = infinity}; + } + + utils::serialize::Node binding_coordinates(const discretization::backend::mfem::DofMap& binding) { + using namespace utils::serialize; + + binding.validate(); + std::vector support(binding.mesh().GetNE()); + std::vector reduction(binding.full_size(), -1); + for (int element = 0; element < binding.mesh().GetNE(); ++element) { + support[element] = binding.supports(element) ? 1 : 0; + } + for (int full = 0; full < binding.full_size(); ++full) { + reduction[full] = binding.reduced_dof(full).value_or(-1); + } + Node record = object("mfem.binding_coordinates", field("support", support), field("full_to_reduced", reduction), field("size", binding.size())); + if (binding.constrained()) { + // Independent numbering alone cannot identify the meaning of hanging coefficients. + std::vector offsets{0}; + std::vector columns; + std::vector weights; + for (int local = 0; local < binding.full_size(); ++local) { + binding.for_each_coefficient(local, [&](const int index, const double weight) { + columns.push_back(index); + weights.push_back(weight); + }); + offsets.push_back(static_cast(columns.size())); + } + record.children.emplace("constraint_offsets", encode(offsets)); + record.children.emplace("constraint_columns", encode(columns)); + record.children.emplace("constraint_weights", encode(weights)); + } + return record; + } +} diff --git a/src/lib/serif/backend/mfem/serialize/top.cpp b/src/lib/serif/backend/mfem/serialize/top.cpp new file mode 100644 index 00000000..fe1a4666 --- /dev/null +++ b/src/lib/serif/backend/mfem/serialize/top.cpp @@ -0,0 +1,365 @@ +#include "serif/backend/mfem/serialize/top.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mfem/fem/gridfunc.hpp" +#include "serif/backend/mfem/discretization/field_values.hpp" + +namespace serif::backend::mfem::serialize::detail { + namespace { + using Vec = std::array; + using utils::serialize::Error; + using utils::serialize::Result; + + /** @brief Retain coefficient interpretation without extending the original space's borrowed lifetime. */ + class DensityBinding final : public discretization::backend::mfem::DofMap { + public: + /** @brief Preserve the source support, constraints, and immutable reconstruction after its binding is destroyed. */ + explicit DensityBinding(const DofMap& source) : + DofMap(source.space(), support(source), source.reconstruction()) {} + private: + /** @brief Copy the material selection independently of a particular compile-time domain schema. */ + static ::mfem::Array support(const DofMap& source) { + source.validate(); + ::mfem::Array result(source.mesh().GetNE()); + for (int element = 0; element < result.Size(); ++element) result[element] = source.supports(element); + return result; + } + }; + + /** @brief Keep exported samples tied to one accepted coefficient snapshot. */ + ::mfem::Vector copy_density(std::span values) { + ::mfem::Vector result(static_cast(values.size())); + std::copy(values.begin(), values.end(), result.GetData()); + return result; + } + + double dot(const Vec& a, const Vec& b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + Vec cross(const Vec& a, const Vec& b) { + return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]}; + } + Vec normalized(Vec value) { + const double norm = std::sqrt(dot(value, value)); + if (!std::isfinite(norm) || norm <= 0.0) { throw std::invalid_argument("TOP sampling requires a finite nonzero direction."); } + for (double& component : value) { component /= norm; } + return value; + } + std::pair transverse_basis(const Vec& axis) { + const auto helper = std::abs(axis[2]) < 0.8 ? Vec{0.0, 0.0, 1.0} : Vec{0.0, 1.0, 0.0}; + Vec first = normalized(cross(helper, axis)); + return {first, cross(axis, first)}; + } + + struct Bounds { + Vec lower, upper; + bool contains(const ::mfem::Vector& point) const { + for (int k = 0; k < 3; ++k) { + if (point[k] < lower[k] || point[k] > upper[k]) { return false; } + } + return true; + } + bool intersects_ray(const Vec& center, const Vec& direction) const { + double near = 0.0, far = std::numeric_limits::infinity(); + for (int k = 0; k < 3; ++k) { + if (std::abs(direction[k]) < 1.0e-15) { + if (center[k] < lower[k] || center[k] > upper[k]) { return false; } + } else { + double a = (lower[k] - center[k]) / direction[k]; + double b = (upper[k] - center[k]) / direction[k]; + if (a > b) { std::swap(a, b); } + near = std::max(near, a); + far = std::min(far, b); + if (near > far) { return false; } + } + } + return far > 0.0; + } + }; + + // These boxes accelerate the search, but are not treated as rigorous polynomial bounds. + // Every failed accelerated search retries all elements, so curved overshoot cannot cause a false exclusion. + Bounds bounds(::mfem::ElementTransformation& transformation) { + Bounds result; + result.lower.fill(std::numeric_limits::infinity()); + result.upper.fill(-std::numeric_limits::infinity()); + const auto& rule = ::mfem::IntRules.Get(transformation.GetGeometryType(), 2 * transformation.Order() + 4); + const auto* vertices = ::mfem::Geometries.GetVertices(transformation.GetGeometryType()); + ::mfem::Vector point(3); + auto visit = [&](const ::mfem::IntegrationPoint& ip) { + transformation.Transform(ip, point); + for (int k = 0; k < 3; ++k) { + if (!std::isfinite(point[k])) { throw std::invalid_argument("TOP sampling found nonfinite deformed geometry."); } + result.lower[k] = std::min(result.lower[k], point[k]); + result.upper[k] = std::max(result.upper[k], point[k]); + } + }; + for (int q = 0; q < rule.GetNPoints(); ++q) { visit(rule.IntPoint(q)); } + for (int q = 0; q < vertices->GetNPoints(); ++q) { visit(vertices->IntPoint(q)); } + double diameter = 0.0; + for (int k = 0; k < 3; ++k) { diameter = std::max(diameter, result.upper[k] - result.lower[k]); } + for (int k = 0; k < 3; ++k) { + result.lower[k] -= 0.05 * diameter; + result.upper[k] += 0.05 * diameter; + } + return result; + } + } + + struct TopModelSampler::Impl { + ::mfem::Mesh mesh; + DensityBinding density_binding; + ::mfem::Vector density_coefficients; + discretization::backend::mfem::FieldValues density_values; + Vec axis, center, first, second; + std::vector material, boundary; + std::vector material_bounds, boundary_bounds; + std::map, double> radii; + double density_scale{0.0}, total_mass{0.0}, length_scale{0.0}; + std::size_t clamps{0}; + int last_element{-1}; + ::mfem::IntegrationPoint last_point; + + Impl(const discretization::backend::mfem::DofMap& binding, std::span density, + const geometry::deformation::backend::mfem::ReferenceDiscretization& reference, + std::span displacement, const Vec& supplied_axis, const Vec& supplied_center) : + mesh(binding.mesh(), true), density_binding(binding), density_coefficients(copy_density(density)), + density_values(density_binding, density_coefficients), axis(normalized(supplied_axis)), center(supplied_center) { + binding.validate(); + if (binding.space().GetVDim() != 1 || binding.space().IsVariableOrder() || + reference.displacement_space.IsVariableOrder() || mesh.Dimension() != 3 || mesh.SpaceDimension() != 3 || + binding.space().GetMesh() != reference.displacement_space.GetMesh() || + density.size() != static_cast(binding.size())) { + throw std::invalid_argument("TOP sampling requires matching fixed-order serial scalar density and three-dimensional displacement spaces."); + } + for (double value : center) { + if (!std::isfinite(value)) { throw std::invalid_argument("TOP sampling requires a finite rotation center."); } + } + std::tie(first, second) = transverse_basis(axis); + ::mfem::GridFunction motion(const_cast<::mfem::FiniteElementSpace*>(&reference.displacement_space)); + if (motion.Size() != static_cast(displacement.size()) || motion.FESpace()->GetVDim() != 3) { + throw std::invalid_argument("TOP displacement coefficients do not match the deformation reference space."); + } + const int nodes = motion.FESpace()->GetNDofs(); + for (int node = 0; node < nodes; ++node) { + for (int component = 0; component < 3; ++component) { + const double value = displacement[node + component * nodes]; + if (!std::isfinite(value)) { throw std::invalid_argument("TOP displacement contains a nonfinite coefficient."); } + motion[motion.FESpace()->DofToVDof(node, component)] = value; + } + } + // Preserve all reference-geometry modes before adding the lower- or higher-order displacement. + int order = reference.displacement_space.GetMaxElementOrder(); + if (mesh.GetNodalFESpace()) { order = std::max(order, mesh.GetNodalFESpace()->GetMaxElementOrder()); } + mesh.SetCurvature(order, false, 3, ::mfem::Ordering::byNODES); + ::mfem::GridFunction nodal_motion(mesh.GetNodes()->FESpace()); + nodal_motion.ProjectGridFunction(motion); + *mesh.GetNodes() += nodal_motion; + mesh.NodesUpdated(); + for (int element = 0; element < mesh.GetNE(); ++element) { + if (binding.supports(element)) { + material.push_back(element); + material_bounds.push_back(bounds(*mesh.GetElementTransformation(element))); + } + } + for (int face = 0; face < mesh.GetNBE(); ++face) { + if (mesh.GetBdrAttribute(face) == reference.surface_attribute) { + boundary.push_back(face); + boundary_bounds.push_back(bounds(*mesh.GetBdrElementTransformation(face))); + } + } + if (material.empty() || boundary.empty()) { throw std::invalid_argument("TOP sampling found no material or stellar surface."); } + for (const auto& box : boundary_bounds) { + for (int k = 0; k < 3; ++k) { + length_scale = std::max({length_scale, std::abs(box.lower[k] - center[k]), std::abs(box.upper[k] - center[k])}); + } + } + if (!(length_scale > 0.0) || !std::isfinite(length_scale)) { throw std::invalid_argument("Invalid stellar length scale for TOP sampling."); } + ::mfem::Vector origin(3); + for (int k = 0; k < 3; ++k) { origin[k] = center[k]; } + const auto central = sample_raw(origin); + if (!central || !std::isfinite(*central) || *central <= 0.0) { + throw std::invalid_argument("TOP requires positive density at a rotation center inside the stellar material."); + } + density_scale = *central; + double correction = 0.0; + ::mfem::Vector density_samples; + for (int element : material) { + auto& transformation = *mesh.GetElementTransformation(element); + // Fractional radial powers are not polynomials; overintegrate only the reconstructed collar. + const int additional_order = binding.reconstruction_element(element) == nullptr ? 0 : binding.reconstruction()->integration_order_increment(); + const int quadrature_order = binding.space().GetFE(element)->GetOrder() + 3 * order + 3 + additional_order; + const auto& rule = ::mfem::IntRules.Get(transformation.GetGeometryType(), quadrature_order); + density_values.values(element, rule, density_samples); + for (int q = 0; q < rule.GetNPoints(); ++q) { + const auto& ip = rule.IntPoint(q); + transformation.SetIntPoint(&ip); + const double determinant = transformation.Jacobian().Det(); + if (!std::isfinite(determinant) || determinant <= 0.0) { + throw std::invalid_argument("TOP sampling rejected a nonpositive deformed material Jacobian."); + } + auto rho = checked_density(density_samples[q]); + if (!rho) { throw std::invalid_argument(rho.error().message); } + const double term = *rho * determinant * ip.weight - correction; + const double next = total_mass + term; + correction = (next - total_mass) - term; + total_mass = next; + } + } + if (!std::isfinite(total_mass) || total_mass <= 0.0) { throw std::invalid_argument("TOP requires positive finite integrated stellar mass."); } + } + + Vec direction(double mu, double phi) const { + if (!std::isfinite(mu) || !std::isfinite(phi) || mu < -1.0 || mu > 1.0) { + throw std::invalid_argument("TOP sampling requires finite angles with -1 <= mu <= 1."); + } + const double sine = std::sqrt(std::max(0.0, 1.0 - mu * mu)); + Vec result; + for (int k = 0; k < 3; ++k) { result[k] = mu * axis[k] + sine * (std::cos(phi) * first[k] + std::sin(phi) * second[k]); } + return result; + } + + Result checked_density(double value) { + if (!std::isfinite(value)) { return std::unexpected(Error{"top/state/density", "The sampled FE density is nonfinite."}); } + if (value < 0.0) { + const double tolerance = 128.0 * std::numeric_limits::epsilon() * density_scale; + if (-value > tolerance) { + return std::unexpected(Error{"top/state/density", "The sampled FE density is negative beyond 128 machine epsilons times the central density; TOP export does not repair the stellar solution."}); + } + ++clamps; + return 0.0; + } + return value; + } + + Result sample_raw(const ::mfem::Vector& point) { + auto attempt = [&](int element, bool previous, bool robust) -> bool { + auto& transformation = *mesh.GetElementTransformation(element); + ::mfem::InverseElementTransformation inverse(&transformation); + inverse.SetMaxIter(40); + inverse.SetPhysicalRelTol(2.0e-13); + inverse.SetReferenceTol(2.0e-13); + inverse.SetPrintLevel(-1); + if (previous) { inverse.SetInitialGuess(last_point); } + if (robust) { inverse.SetInitialGuessType(::mfem::InverseElementTransformation::ClosestPhysNode); } + ::mfem::IntegrationPoint ip; + if (inverse.Transform(point, ip) != ::mfem::InverseElementTransformation::Inside) { return false; } + ::mfem::Vector reconstructed(3); + transformation.Transform(ip, reconstructed); + reconstructed -= point; + if (reconstructed.Norml2() > 2.0e-11 * length_scale) { return false; } + last_element = element; + last_point = ip; + return true; + }; + if (last_element >= 0 && attempt(last_element, true, false)) { + return density_values.value(last_element, last_point); + } + for (std::size_t i = 0; i < material.size(); ++i) { + if (material_bounds[i].contains(point) && attempt(material[i], false, false)) { + return density_values.value(last_element, last_point); + } + } + for (int element : material) { + if (attempt(element, false, true)) { return density_values.value(last_element, last_point); } + } + return std::unexpected(Error{"top/sampling", "A requested physical point could not be located inside the deformed stellar material; no field extrapolation was performed."}); + } + + std::optional intersect(int face, const Vec& ray, const Vec& a, const Vec& b, bool robust) { + auto& transformation = *mesh.GetBdrElementTransformation(face); + auto solve = [&](::mfem::IntegrationPoint ip) -> std::optional { + ::mfem::Vector point(3); + for (int iteration = 0; iteration < 35; ++iteration) { + transformation.Transform(ip, point); + Vec relative{point[0] - center[0], point[1] - center[1], point[2] - center[2]}; + const double f = dot(relative, a), g = dot(relative, b); + if (std::hypot(f, g) <= 5.0e-13 * length_scale) { + const double radius = dot(relative, ray); + if (radius > 0.0 && ::mfem::Geometry::CheckPoint(transformation.GetGeometryType(), ip, 2.0e-10)) { return radius; } + return {}; + } + transformation.SetIntPoint(&ip); + const auto& jacobian = transformation.Jacobian(); + double j00 = 0.0, j01 = 0.0, j10 = 0.0, j11 = 0.0; + for (int k = 0; k < 3; ++k) { + j00 += a[k] * jacobian(k, 0); j01 += a[k] * jacobian(k, 1); + j10 += b[k] * jacobian(k, 0); j11 += b[k] * jacobian(k, 1); + } + const double determinant = j00 * j11 - j01 * j10; + if (!std::isfinite(determinant) || std::abs(determinant) < 1.0e-16 * length_scale * length_scale) { return {}; } + double du = (j11 * f - j01 * g) / determinant; + double dv = (j00 * g - j10 * f) / determinant; + const double scale = std::max({1.0, std::abs(du), std::abs(dv)}); + ip.x -= du / scale; + ip.y -= dv / scale; + if (!std::isfinite(ip.x) || !std::isfinite(ip.y) || ip.x < -0.5 || ip.x > 1.5 || ip.y < -0.5 || ip.y > 1.5) { return {}; } + } + return {}; + }; + if (auto result = solve(::mfem::Geometries.GetCenter(transformation.GetGeometryType()))) { return result; } + if (robust) { + const auto& rule = ::mfem::IntRules.Get(transformation.GetGeometryType(), 4); + for (int q = 0; q < rule.GetNPoints(); ++q) { if (auto result = solve(rule.IntPoint(q))) { return result; } } + } + return {}; + } + + Result surface(double mu, double phi) { + const auto key = std::pair{mu, phi}; + if (const auto found = radii.find(key); found != radii.end()) { return found->second; } + const Vec ray = direction(mu, phi); + const auto [a, b] = transverse_basis(ray); + std::optional result; + for (std::size_t i = 0; i < boundary.size(); ++i) { + if (!boundary_bounds[i].intersects_ray(center, ray)) { continue; } + if (const auto radius = intersect(boundary[i], ray, a, b, true)) { + if (result && std::abs(*result - *radius) > 2.0e-9 * length_scale) { + return std::unexpected(Error{"top/geometry/surface", "The stellar boundary has multiple radial intersections and cannot be represented as a TOP star-shaped surface."}); + } + result = radius; + } + } + if (!result) { + for (int face : boundary) { + if (const auto radius = intersect(face, ray, a, b, true)) { + if (result && std::abs(*result - *radius) > 2.0e-9 * length_scale) { + return std::unexpected(Error{"top/geometry/surface", "The stellar boundary is not single-valued in radius."}); + } + result = radius; + } + } + } + if (!result) { return std::unexpected(Error{"top/geometry/surface", "Could not intersect the requested ray with the actual deformed FE stellar surface."}); } + radii.emplace(key, *result); + return *result; + } + }; + + TopModelSampler::TopModelSampler(const discretization::backend::mfem::DofMap& binding, + std::span density, const geometry::deformation::backend::mfem::ReferenceDiscretization& reference, + std::span displacement, const Vec& axis, const Vec& center) : + m_impl(std::make_unique(binding, density, reference, displacement, axis, center)) {} + TopModelSampler::~TopModelSampler() = default; + Result TopModelSampler::surface_radius(double mu, double phi) { return m_impl->surface(mu, phi); } + Result TopModelSampler::density(double radius, double mu, double phi) { + if (!std::isfinite(radius) || radius < 0.0) { return std::unexpected(Error{"top/sampling/radius", "TOP sampling requires a finite nonnegative physical radius."}); } + if (radius == 0.0) { return m_impl->density_scale; } + const Vec direction = m_impl->direction(mu, phi); + ::mfem::Vector point(3); + for (int k = 0; k < 3; ++k) { point[k] = m_impl->center[k] + radius * direction[k]; } + const auto value = m_impl->sample_raw(point); + if (!value) { return std::unexpected(value.error()); } + return m_impl->checked_density(*value); + } + double TopModelSampler::mass() const noexcept { return m_impl->total_mass; } + std::size_t TopModelSampler::roundoff_density_clamps() const noexcept { return m_impl->clamps; } +} diff --git a/src/lib/serif/backend/mfem/shared/parallel.cpp b/src/lib/serif/backend/mfem/shared/parallel.cpp new file mode 100644 index 00000000..47b54136 --- /dev/null +++ b/src/lib/serif/backend/mfem/shared/parallel.cpp @@ -0,0 +1,64 @@ +#include "serif/backend/mfem/shared/parallel.hpp" + +#include "mfem/fem/eltrans.hpp" +#include "mfem/fem/gridfunc.hpp" +#include "mfem/mesh/mesh.hpp" + +#ifdef _OPENMP +#include +#endif + +namespace serif::utils::parallel { + int worker_count() noexcept { +#ifdef _OPENMP + return omp_in_parallel() ? 1 : omp_get_max_threads(); +#else + return 1; +#endif + } + + void detail::run(const int count, void* context, const Body body) { +#ifdef _OPENMP + if (count > 1 && worker_count() > 1) { + // Elements differ in cost (vacuum points carry the compactification), so hand them out one at a time. + #pragma omp parallel for schedule(dynamic, 1) + for (int index = 0; index < count; ++index) { + body(context, index, omp_get_thread_num()); + } + return; + } +#endif + for (int index = 0; index < count; ++index) { + body(context, index, 0); + } + } + + void element_transformation(const ::mfem::Mesh& mesh, const int element, ::mfem::IsoparametricTransformation& transformation) { + // Mirrors Mesh::GetElementTransformation(int, IsoparametricTransformation*) in MFEM 4.10. + transformation.Attribute = mesh.GetAttribute(element); + transformation.ElementNo = element; + transformation.ElementType = ::mfem::ElementTransformation::ELEMENT; + transformation.mesh = &mesh; + transformation.Reset(); + const ::mfem::GridFunction* nodes = mesh.GetNodes(); + if (nodes == nullptr) { + mesh.GetPointMatrix(element, transformation.GetPointMat()); + transformation.SetFE(::mfem::Mesh::GetTransformationFEforElementType(mesh.GetElementType(element))); + return; + } + ::mfem::DenseMatrix& point_matrix = transformation.GetPointMat(); + ::mfem::Array vdofs; + ::mfem::DofTransformation dof_transformation; + nodes->FESpace()->GetElementVDofs(element, vdofs, dof_transformation); + nodes->HostRead(); + const int space_dimension = mesh.SpaceDimension(); + const int count = vdofs.Size() / space_dimension; + point_matrix.SetSize(space_dimension, count); + for (int k = 0; k < space_dimension; ++k) { + for (int j = 0; j < count; ++j) { + point_matrix(k, j) = (*nodes)(vdofs[count * k + j]); + } + } + transformation.SetFE(nodes->FESpace()->GetFE(element)); + } +} diff --git a/src/lib/serif/backend/mfem/shared/reference_tracking.cpp b/src/lib/serif/backend/mfem/shared/reference_tracking.cpp new file mode 100644 index 00000000..08c9f402 --- /dev/null +++ b/src/lib/serif/backend/mfem/shared/reference_tracking.cpp @@ -0,0 +1,63 @@ +#include "serif/backend/mfem/shared/reference_tracking.hpp" + +#include "mfem/fem/fespace.hpp" + +namespace serif::backend::mfem::shared { + /** @brief Mesh identities and revisions cover coordinates; explicit labels catch direct attribute edits. */ + [[nodiscard]] ReferenceMeshSnapshot mesh_snapshot(const ::mfem::Mesh* mesh, bool coordinates, bool materials) { + if (!mesh) { + return {}; + } + ReferenceMeshSnapshot result{ + .identity = mesh, + .sequence = mesh->GetSequence(), + .nodes_sequence = coordinates ? mesh->GetNodesSequence() : -1, + .vertices = mesh->GetNV(), + .elements = mesh->GetNE(), + .element_attributes = std::vector(materials ? mesh->GetNE() : 0), + .boundary_attributes = std::vector(mesh->GetNBE())}; + for (std::size_t element = 0; element < result.element_attributes.size(); ++element) { + result.element_attributes[element] = mesh->GetAttribute(element); + } + for (int boundary = 0; boundary < mesh->GetNBE(); ++boundary) { + result.boundary_attributes[boundary] = mesh->GetBdrAttribute(boundary); + } + return result; + } + + /** @brief Space sequence numbers distinguish refinement and changed coefficient layouts. */ + [[nodiscard]] ReferenceSpaceSnapshot space_snapshot(const ::mfem::FiniteElementSpace& space) { + return { + .identity = &space, + .mesh = space.GetMesh(), + .collection = space.FEColl(), + .sequence = space.GetSequence(), + .size = space.GetVSize(), + .vector_dimension = space.GetVDim(), + .ordering = static_cast(space.GetOrdering())}; + } + + /** @brief Check unchanged references without allocating another copy of every mesh label. */ + [[nodiscard]] bool mesh_matches(const ReferenceMeshSnapshot& snapshot, const ::mfem::Mesh* mesh) { + if (snapshot.identity != mesh) { + return false; + } + if (!mesh) { + return true; + } + if (snapshot.sequence != mesh->GetSequence() || (snapshot.nodes_sequence >= 0 && snapshot.nodes_sequence != mesh->GetNodesSequence()) || snapshot.vertices != mesh->GetNV() || snapshot.elements != mesh->GetNE() || snapshot.boundary_attributes.size() != static_cast(mesh->GetNBE())) { + return false; + } + for (std::size_t element = 0; element < snapshot.element_attributes.size(); ++element) { + if (snapshot.element_attributes[element] != mesh->GetAttribute(element)) { + return false; + } + } + for (int boundary = 0; boundary < mesh->GetNBE(); ++boundary) { + if (snapshot.boundary_attributes[boundary] != mesh->GetBdrAttribute(boundary)) { + return false; + } + } + return true; + } +} diff --git a/src/lib/serif/backend/mfem/solver/linear/solve.cpp b/src/lib/serif/backend/mfem/solver/linear/solve.cpp new file mode 100644 index 00000000..017fbde3 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/linear/solve.cpp @@ -0,0 +1,179 @@ +#include "serif/backend/mfem/solver/linear/solve.hpp" + +#include +#include +#include +#include +#include +#include "mfem/linalg/solvers.hpp" +#include "serif/solver/linear/failure.hpp" +#include "serif/backend/mfem/shared/finite.hpp" +#include "serif/utils/error/errors.hpp" + +namespace serif::solver::linear::backend::mfem { + namespace { + using Clock = std::chrono::steady_clock; + + /** @brief Account for completed applications without changing the wrapped operator's algebra. */ + class CountedOperator final : public ::mfem::Operator { + public: + /** @brief Borrow one frozen Jacobian or inverse action for this solve. */ + explicit CountedOperator(const ::mfem::Operator& operation) + : ::mfem::Operator(operation.Height(), operation.Width()), m_operation(operation) {} + + /** @brief Apply the frozen linearization and record completed work. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override { + const auto start = Clock::now(); + m_operation.Mult(input, output); + ++applications; + seconds += std::chrono::duration(Clock::now() - start).count(); + utils::errors::serif_ensure( + utils::checks::is_finite(output), "A checked Krylov application produced nonfinite coefficients."); + } + + mutable std::uint64_t applications{0}; + mutable double seconds{0.0}; + + private: + const ::mfem::Operator& m_operation; + }; + + /** @brief Preserve the underlying inverse's operator-identity check when MFEM sees a counting adapter. */ + class CountedPreconditioner final : public ::mfem::Solver { + public: + /** @brief Borrow the matching physical or normalized operator/inverse pair. */ + CountedPreconditioner(const ::mfem::Operator& operation, const CountedOperator& counted, ::mfem::Solver& inverse) + : ::mfem::Solver(inverse.Height(), inverse.Width()), counted_inverse(inverse), m_operation(operation), m_counted(counted), m_inverse(inverse) {} + + /** @brief Forward the original operator identity, never the address of the temporary adapter. */ + void SetOperator(const ::mfem::Operator& operation) override { + utils::errors::serif_ensure(&operation == &m_counted, + "FGMRES attempted to replace the counted operator during a solve. Prepare a new solve for a different linearization."); + m_inverse.SetOperator(m_operation); + } + + /** @brief Reuse the same completed-application accounting and finite-output gate as the Jacobian. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override { + counted_inverse.Mult(input, output); + } + + CountedOperator counted_inverse; + + private: + const ::mfem::Operator& m_operation; + const CountedOperator& m_counted; + ::mfem::Solver& m_inverse; + }; + + /** @brief Measure b-Ax independently of the Krylov recurrence, retaining nonfinite failures. */ + [[nodiscard]] double true_residual(const ::mfem::Operator& operation, const ::mfem::Vector& right_hand_side, + const ::mfem::Vector& correction, ::mfem::Vector& work) { + operation.Mult(correction, work); + work *= -1.0; + work += right_hand_side; + return utils::checks::is_finite(work) ? work.Norml2() : std::numeric_limits::quiet_NaN(); + } + } + + Report solve(FGMRES options, const ::mfem::Operator& operation, ::mfem::Solver& preconditioner, + preconditioner::ApplicationContract contract, const ::mfem::Vector& right_hand_side, + ::mfem::Vector& correction, Control control) { + using namespace utils::errors; + + options.validate(); + control.validate(); + serif_ensure(operation.Height() > 0 && operation.Height() == operation.Width() && + preconditioner.Height() == operation.Width() && preconditioner.Width() == operation.Height() && + right_hand_side.Size() == operation.Height() && correction.Size() == operation.Width(), + "FGMRES requires a nonempty square operator, a matching inverse, and matching right-hand-side and correction vectors."); + serif_ensure(contract == preconditioner::ApplicationContract::fixed_linear || + contract == preconditioner::ApplicationContract::variable, "FGMRES received an unrecognized preconditioner application contract."); + + Report report{.control = control}; + report.right_hand_side_norm = right_hand_side.Norml2(); + if (!utils::checks::is_finite(right_hand_side) || !utils::checks::is_finite(correction) || !std::isfinite(report.right_hand_side_norm)) { + report.status = Status::non_finite; + report.true_residual_norm = std::numeric_limits::quiet_NaN(); + report.relative_true_residual_norm = report.true_residual_norm; + return report; + } + const double threshold = control.convergence_threshold(report.right_hand_side_norm); + // Copy the RHS because callers may supply overlapping vector views. + const ::mfem::Vector rhs(right_hand_side); + ::mfem::Vector work(operation.Height()); + CountedOperator counted_operation(operation); + CountedPreconditioner counted_preconditioner(operation, counted_operation, preconditioner); + ::mfem::FGMRESSolver solver; + solver.SetPreconditioner(counted_preconditioner); + solver.SetOperator(counted_operation); + solver.SetKDim(options.restart_length); + solver.SetPrintLevel(options.print_level); + solver.SetRelTol(0.0); + solver.SetAbsTol(threshold); + solver.SetMaxIter(control.maximum_iterations); + solver.iterative_mode = true; + const auto start = Clock::now(); + + bool application_failed = false; + bool nonfinite_application = false; + report.initial_residual_norm = report.right_hand_side_norm; + try { + if (!std::ranges::all_of(correction, [](double value) { return value == 0.0; })) { + double guess_residual = std::numeric_limits::quiet_NaN(); + try { + guess_residual = true_residual(counted_operation, rhs, correction, work); + } catch (const NonFiniteApplication&) { + // A finite recycled correction may overflow this new Jacobian; retry from zero. + } + if (!std::isfinite(guess_residual) || guess_residual > report.right_hand_side_norm) { + correction = 0.0; + } else { + report.initial_residual_norm = guess_residual; + } + } + // An already converged guess still receives the independent final check below. In particular, + // the exactly zero RHS must not enter a Krylov normalization that divides by its zero norm. + if (report.initial_residual_norm > threshold) { + solver.Mult(rhs, correction); + report.reported_residual_norm = solver.GetFinalNorm(); + } else { + report.reported_residual_norm = report.initial_residual_norm; + } + report.true_residual_norm = std::numeric_limits::quiet_NaN(); + if (utils::checks::is_finite(correction)) { + report.true_residual_norm = true_residual(counted_operation, rhs, correction, work); + } + } catch (const NonFiniteApplication&) { + nonfinite_application = true; + report.true_residual_norm = std::numeric_limits::quiet_NaN(); + } catch (const ApplicationFailure&) { + application_failed = true; + report.true_residual_norm = std::numeric_limits::quiet_NaN(); + } + report.iterations = static_cast(counted_preconditioner.counted_inverse.applications); + report.restarts = report.iterations > 0 ? (report.iterations - 1) / options.restart_length : 0; + if (nonfinite_application) { + report.status = Status::non_finite; + } else if (application_failed) { + report.status = Status::backend_failure; + } else if (!std::isfinite(report.true_residual_norm) || !std::isfinite(report.reported_residual_norm)) { + report.status = Status::non_finite; + } else if (report.true_residual_norm <= threshold) { + report.status = Status::converged; + } else if (report.iterations >= control.maximum_iterations && !solver.GetConverged()) { + report.status = Status::maximum_iterations; + } else { + report.status = solver.GetConverged() ? Status::backend_failure : Status::breakdown; + } + report.relative_true_residual_norm = report.true_residual_norm == 0.0 ? 0.0 : std::numeric_limits::infinity(); + if (report.right_hand_side_norm > 0.0) { + report.relative_true_residual_norm = report.true_residual_norm / report.right_hand_side_norm; + } + report.operator_applications = counted_operation.applications; + report.preconditioner_applications = counted_preconditioner.counted_inverse.applications; + report.operator_seconds = counted_operation.seconds; + report.preconditioner_seconds = counted_preconditioner.counted_inverse.seconds; + report.solve_seconds = std::chrono::duration(Clock::now() - start).count(); + return report; + } +} diff --git a/src/lib/serif/backend/mfem/solver/normalization/operators.cpp b/src/lib/serif/backend/mfem/solver/normalization/operators.cpp new file mode 100644 index 00000000..69c4b8a6 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/normalization/operators.cpp @@ -0,0 +1,106 @@ +#include "serif/backend/mfem/solver/normalization/operators.hpp" + +#include +#include +#include + +#include "serif/utils/error/errors.hpp" + +namespace serif::solver::normalization::backend::mfem +{ + namespace detail + { + int mfem_size(const std::size_t size) + { + using namespace utils::errors; + + serif_ensure(size <= static_cast(std::numeric_limits::max()), "Normalization has {} coordinates, exceeding MFEM's maximum index {}. Use a coordinate layout supported by this backend.", size, std::numeric_limits::max()); + return static_cast(size); + } + + void scale_vector(std::span factors, const ::mfem::Vector& input, ::mfem::Vector& output, const bool inverse) + { + normalization::detail::require_normalization_size(static_cast(input.Size()), factors.size()); + output.SetSize(input.Size()); + const std::span source{input.HostRead(), factors.size()}; + const std::span destination{output.HostWrite(), factors.size()}; + normalization::detail::apply_normalization(factors, source, destination, inverse); + } + + template + ScaledOperator::ScaledOperator(const ::mfem::Operator& physical_operator, DiagonalNormalization normalization) + : ::mfem::Operator(mfem_size(Inverse ? normalization.state_size() : normalization.residual_size()), mfem_size(Inverse ? normalization.residual_size() : normalization.state_size())), + m_physical_operator(&physical_operator), m_normalization(std::move(normalization)), m_physical_input(Width()), m_physical_output(Height()) + { + using namespace utils::errors; + + serif_ensure(physical_operator.Width() == Width() && physical_operator.Height() == Height(), "The physical operator has dimensions {} by {}, but normalization requires {} by {}. Prepare factors using the operator's state and residual coordinate layout.", physical_operator.Height(), physical_operator.Width(), Height(), Width()); + } + + template + void ScaledOperator::Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const + { + scale_vector(input_factors(), input, m_physical_input, true); + if constexpr (Inverse) + { + // An iterative inverse must start from zero rather than a previous scratch result. + m_physical_output = 0.0; + } + m_physical_operator->Mult(m_physical_input, m_physical_output); + scale_vector(output_factors(), m_physical_output, output, false); + } + + template + void ScaledOperator::MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const + { + scale_vector(output_factors(), input, m_physical_output, false); + if constexpr (Inverse) + { + m_physical_input = 0.0; + } + m_physical_operator->MultTranspose(m_physical_output, m_physical_input); + scale_vector(input_factors(), m_physical_input, output, true); + } + + template class ScaledOperator; + template class ScaledOperator; + } + + ScaledPreconditioner::ScaledPreconditioner(::mfem::Solver& physical_inverse, const ScaledJacobianOperator& normalized_jacobian) + : ::mfem::Solver(normalized_jacobian.Width(), normalized_jacobian.Height(), false), + m_physical_inverse(&physical_inverse), m_normalized_jacobian(&normalized_jacobian), + m_scaled_inverse(bind_physical_inverse(physical_inverse, normalized_jacobian), normalized_jacobian.normalization()), + m_statistics{.operator_bindings = 1} + { + } + + void ScaledPreconditioner::SetOperator(const ::mfem::Operator& normalized_jacobian) + { + using namespace utils::errors; + + serif_ensure(&normalized_jacobian == m_normalized_jacobian, "The scaled preconditioner received a different normalized Jacobian. Prepare a new adapter for that Jacobian and its frozen normalization."); + serif_ensure(normalized_jacobian.Width() == Height() && normalized_jacobian.Height() == Width(), "The normalized Jacobian has dimensions {} by {}, but this preconditioner requires {} by {}. Prepare a new adapter for its coordinate layout.", normalized_jacobian.Height(), normalized_jacobian.Width(), Width(), Height()); + bind_physical_inverse(*m_physical_inverse, *m_normalized_jacobian); + ++m_statistics.operator_bindings; + } + + void ScaledPreconditioner::Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const + { + normalization::detail::require_normalization_size(static_cast(output.Size()), static_cast(Height())); + m_scaled_inverse.Mult(input, output); + ++m_statistics.applications; + } + + void ScaledPreconditioner::MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const + { + normalization::detail::require_normalization_size(static_cast(output.Size()), static_cast(Width())); + m_scaled_inverse.MultTranspose(input, output); + ++m_statistics.applications; + } + + ::mfem::Solver& ScaledPreconditioner::bind_physical_inverse(::mfem::Solver& inverse, const ScaledJacobianOperator& jacobian) + { + inverse.SetOperator(jacobian.physical_operator()); + return inverse; + } +} diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/border.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/border.cpp new file mode 100644 index 00000000..1f386107 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/border.cpp @@ -0,0 +1,151 @@ +#include "serif/backend/mfem/solver/preconditioner/border.hpp" + +#include +#include +#include "serif/backend/mfem/shared/finite.hpp" +#include +#include +#include + +#include "mfem/linalg/densemat.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse/dense.hpp" +#include "serif/utils/error/errors.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse/validation.hpp" +#include "coupling_internal.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + namespace + { + using utils::errors::SERiFErrorCode; + + using detail::failure; + + /** @brief ArrayMult in consecutive sub-batches of at most `batch` vectors (each vector's result is unchanged). */ + void apply_in_batches(const ::mfem::Operator& action, const ::mfem::Array& inputs, ::mfem::Array<::mfem::Vector*>& outputs, const int batch) + { + for (int first = 0; first < inputs.Size(); first += batch) + { + const int count = std::min(batch, inputs.Size() - first); + ::mfem::Array in(count); + ::mfem::Array<::mfem::Vector*> out(count); + for (int k = 0; k < count; ++k) + { + in[k] = inputs[first + k]; + out[k] = outputs[first + k]; + } + action.ArrayMult(in, out); + } + } + } // namespace + + std::expected try_bordered_inverse(Inverse fields, TwoBlockCouplings couplings, OperatorHandle border_diagonal, const strategies::inverse::DenseDirect& options) + { + if (!std::isfinite(options.minimum_reciprocal_condition) || options.minimum_reciprocal_condition < 0.0 || options.minimum_reciprocal_condition > 1.0 || (fields.contract != ApplicationContract::fixed_linear && fields.contract != ApplicationContract::variable)) + { + return failure("The border inverse requires a valid application contract and a finite minimum reciprocal condition in [0,1]."); + } + if (!fields.action || !border_diagonal || !couplings.first_from_second || !couplings.second_from_first) + { + return failure("A border preparation requires owned field, diagonal, and cross-coupling operators."); + } + const int field_size = fields.action->Height(); + const int border_size = border_diagonal->Height(); + if (field_size <= 0 || border_size < 0 || fields.action->Width() != field_size || border_diagonal->Width() != border_size || couplings.first_from_second->Height() != field_size || couplings.first_from_second->Width() != border_size || couplings.second_from_first->Height() != border_size || couplings.second_from_first->Width() != field_size) + { + return failure("The border blocks must describe one square two-group system with matching cross couplings."); + } + if (field_size > std::numeric_limits::max() - border_size || (border_size > 0 && (border_size > std::numeric_limits::max() / border_size || field_size > std::numeric_limits::max() / border_size))) + { + return failure("The border matrix or cached responses exceed MFEM's index range."); + } + if (border_size == 0) + { + return fields; + } + + ::mfem::DenseMatrix schur(border_size); + std::shared_ptr<::mfem::DenseMatrix> response; + if (fields.contract == ApplicationContract::fixed_linear) + { + response = std::make_shared<::mfem::DenseMatrix>(field_size, border_size); + } + // Columns are probed in chunks: the coupling actions apply to a whole chunk at once (ArrayMult; SERiF's + // volume actions then traverse the mesh once per chunk), and the field inverse solves each chunk as one + // batch, so a sparse LU reads its factor once per chunk instead of once per column. 32 columns is where the + // batched Accelerate solve stops getting cheaper per column on the benchmark potential block. The coupling + // actions take sub-batches of 8: as fast as 32, without holding 32 field-sized probes in every layer. + constexpr int chunk = 32; + constexpr int probe_chunk = 8; + ::mfem::Vector diagonal(border_size); + const std::size_t width = static_cast(std::min(chunk, border_size)); + std::vector<::mfem::Vector> basis(width, ::mfem::Vector(border_size)); + std::vector<::mfem::Vector> field_rhs(width, ::mfem::Vector(field_size)); + std::vector<::mfem::Vector> field_action(width, ::mfem::Vector(field_size)); + std::vector<::mfem::Vector> coupling(width, ::mfem::Vector(border_size)); + for (int first = 0; first < border_size; first += chunk) + { + const int count = std::min(chunk, border_size - first); + ::mfem::Array unit(count), inputs(count), actions(count); + ::mfem::Array<::mfem::Vector*> probes(count), outputs(count), couples(count); + for (int k = 0; k < count; ++k) + { + const auto slot = static_cast(k); + basis[slot] = 0.0; + basis[slot][first + k] = 1.0; + unit[k] = &basis[slot]; + probes[k] = &field_rhs[slot]; + } + apply_in_batches(*couplings.first_from_second, unit, probes, probe_chunk); + for (int k = 0; k < count; ++k) + { + const auto slot = static_cast(k); + if (!utils::checks::is_finite(field_rhs[slot])) + { + return failure("The border-to-field probe returned a nonfinite value."); + } + field_action[slot] = 0.0; + inputs[k] = &field_rhs[slot]; + outputs[k] = &field_action[slot]; + } + fields.action->ArrayMult(inputs, outputs); + for (int k = 0; k < count; ++k) + { + const auto slot = static_cast(k); + if (!utils::checks::is_finite(field_action[slot])) + { + return failure("The field inverse returned a nonfinite border response."); + } + actions[k] = &field_action[slot]; + couples[k] = &coupling[slot]; + } + apply_in_batches(*couplings.second_from_first, actions, couples, probe_chunk); + for (int k = 0; k < count; ++k) + { + const int column = first + k; + const auto slot = static_cast(k); + const auto& action = field_action[slot]; + border_diagonal->Mult(basis[slot], diagonal); + if (!utils::checks::is_finite(coupling[slot]) || !utils::checks::is_finite(diagonal)) + { + return failure("A border Jacobian probe returned a nonfinite value."); + } + if (response) + { + response->SetCol(column, action); + } + for (int row = 0; row < border_size; ++row) + { + schur(row, column) = diagonal[row] - coupling[slot][row]; + } + } + } + + auto border = try_dense_inverse(schur, options); + if (!border) + { + return std::unexpected(std::move(border.error())); + } + return detail::try_coupled_inverse(std::move(fields), std::move(*border), std::move(couplings), strategies::coupling::CouplingOrder::lower_diagonal_upper, std::move(response)); + } +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/coupling.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/coupling.cpp new file mode 100644 index 00000000..0a42addf --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/coupling.cpp @@ -0,0 +1,378 @@ +#include "serif/utils/error/nonfinite.hpp" +#include "serif/backend/mfem/solver/preconditioner/coupling.hpp" + +#include +#include +#include +#include + +#include "mfem/linalg/vector.hpp" +#include "serif/utils/error/errors.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse/validation.hpp" +#include "coupling_internal.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + namespace + { + using strategies::coupling::CouplingOrder; + using utils::errors::SERiFErrorCode; + + using detail::failure; + + /** @brief Identify orders needing the second-to-first Jacobian action. */ + [[nodiscard]] bool needs_first_coupling(CouplingOrder order) + { + return order == CouplingOrder::second_then_first || order == CouplingOrder::lower_diagonal_upper; + } + + /** @brief Identify orders needing the first-to-second Jacobian action. */ + [[nodiscard]] bool needs_second_coupling(CouplingOrder order) + { + return order == CouplingOrder::lower_diagonal_upper; + } + + /** @brief Test the declared dimensions without applying or modifying an operator. */ + [[nodiscard]] bool matches(const OperatorHandle& operation, int height, int width) + { + return operation && operation->Height() == height && operation->Width() == width; + } + + /** @brief Reject nonfinite numerical actions before publishing the caller's correction. */ + void require_finite(const ::mfem::Vector& values) + { + using namespace utils::errors; + + for (int index = 0; index < values.Size(); ++index) + { + serif_ensure(std::isfinite(values[index]), "A coupled inverse requires finite inputs and finite child actions."); + } + } + + /** @brief Shared numerical application for triangular, independent, and bordered factorizations. */ + class CoupledInverse final : public ::mfem::Operator + { + public: + /** @brief Own validated blocks and allocate all numerical workspace before application. */ + CoupledInverse(Inverse first, Inverse second, TwoBlockCouplings couplings, CouplingOrder order, OperatorHandle response) : Operator(first.action->Height() + second.action->Height()), + m_first(std::move(first)), m_second(std::move(second)), m_couplings(std::move(couplings)), m_order(order), m_response(std::move(response)), + m_first_rhs(m_first.action->Width()), m_second_rhs(m_second.action->Width()), + m_first_action(m_first.action->Height()), m_second_action(m_second.action->Height()), + m_first_coupling(m_first.action->Width()), m_second_coupling(m_second.action->Width()) + { + } + + /** @brief Apply with scratch copies so overlapping caller buffers cannot change the right-hand side. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + using namespace utils::errors; + + serif_ensure(input.Size() == Width() && output.Size() == Height(), "A coupled inverse requires preallocated vectors matching its two groups."); + require_finite(input); + for (int index = 0; index < m_first_rhs.Size(); ++index) + { + m_first_rhs[index] = input[index]; + } + for (int index = 0; index < m_second_rhs.Size(); ++index) + { + m_second_rhs[index] = input[m_first_rhs.Size() + index]; + } + + apply_groups(); + require_finite(m_first_action); + require_finite(m_second_action); + for (int index = 0; index < m_first_action.Size(); ++index) + { + output[index] = m_first_action[index]; + } + for (int index = 0; index < m_second_action.Size(); ++index) + { + output[m_first_action.Size() + index] = m_second_action[index]; + } + } + + /** + * @brief Apply Mult to a batch, stage by stage, so each child inverse receives the whole batch through its + * own ArrayMult. Every per-vector operation is the one Mult performs, so results equal Mult's whenever the + * children's ArrayMult equals their Mult. + */ + void ArrayMult(const ::mfem::Array& inputs, ::mfem::Array<::mfem::Vector*>& outputs) const override + { + using namespace utils::errors; + + const int count = inputs.Size(); + serif_ensure(outputs.Size() == count, "A coupled inverse batch requires one output per input."); + const int first_size = m_first_rhs.Size(), second_size = m_second_rhs.Size(); + Batch first_rhs(count, first_size), second_rhs(count, second_size), first_action(count, first_size), second_action(count, second_size); + for (int k = 0; k < count; ++k) + { + const auto& input = *inputs[k]; + serif_ensure(input.Size() == Width() && outputs[k]->Size() == Height(), "A coupled inverse requires preallocated vectors matching its two groups."); + require_finite(input); + for (int index = 0; index < first_size; ++index) { first_rhs[k][index] = input[index]; } + for (int index = 0; index < second_size; ++index) { second_rhs[k][index] = input[first_size + index]; } + } + + const auto second_after_first = [&] { + for (int k = 0; k < count; ++k) + { + m_couplings.second_from_first->Mult(first_action[k], m_second_coupling); + require_finite(m_second_coupling); + second_rhs[k] -= m_second_coupling; + } + apply_inverse(m_second, second_rhs, second_action); + }; + const auto first_after_second = [&] { + for (int k = 0; k < count; ++k) + { + m_couplings.first_from_second->Mult(second_action[k], m_first_coupling); + require_finite(m_first_coupling); + first_rhs[k] -= m_first_coupling; + } + apply_inverse(m_first, first_rhs, first_action); + }; + if (m_order == CouplingOrder::second_then_first) + { + apply_inverse(m_second, second_rhs, second_action); + first_after_second(); + } + else + { + apply_inverse(m_first, first_rhs, first_action); + if (m_order == CouplingOrder::independent) + { + apply_inverse(m_second, second_rhs, second_action); + } + else + { + second_after_first(); + if (m_response) + { + for (int k = 0; k < count; ++k) + { + m_response->Mult(second_action[k], m_first_coupling); + require_finite(m_first_coupling); + first_action[k] -= m_first_coupling; + } + } + else + { + first_after_second(); + } + } + } + + for (int k = 0; k < count; ++k) + { + require_finite(first_action[k]); + require_finite(second_action[k]); + auto& output = *outputs[k]; + for (int index = 0; index < first_size; ++index) { output[index] = first_action[k][index]; } + for (int index = 0; index < second_size; ++index) { output[first_size + index] = second_action[k][index]; } + } + } + + private: + /** @brief Owned vectors of one batch, with the pointer arrays MFEM's ArrayMult takes. */ + struct Batch + { + Batch(const int count, const int size) : vectors(static_cast(count)) + { + for (auto& vector : vectors) { vector.SetSize(size); } + } + [[nodiscard]] ::mfem::Vector& operator[](const int k) { return vectors[static_cast(k)]; } + [[nodiscard]] ::mfem::Array inputs() const + { + ::mfem::Array result(static_cast(vectors.size())); + for (int k = 0; k < result.Size(); ++k) { result[k] = &vectors[static_cast(k)]; } + return result; + } + [[nodiscard]] ::mfem::Array<::mfem::Vector*> outputs() + { + ::mfem::Array<::mfem::Vector*> result(static_cast(vectors.size())); + for (int k = 0; k < result.Size(); ++k) { result[k] = &vectors[static_cast(k)]; } + return result; + } + std::vector<::mfem::Vector> vectors; + }; + + /** @brief Batch form of apply_inverse: zeroed actions, one ArrayMult, finite results. */ + static void apply_inverse(const Inverse& inverse, const Batch& rhs, Batch& action) + { + for (auto& vector : action.vectors) { vector = 0.0; } + const auto inputs = rhs.inputs(); + auto outputs = action.outputs(); + inverse.action->ArrayMult(inputs, outputs); + for (const auto& vector : action.vectors) { require_finite(vector); } + } + + /** @brief Prevent iterative child operators from reusing the previous correction as an initial guess. */ + static void apply_inverse(const Inverse& inverse, const ::mfem::Vector& rhs, ::mfem::Vector& action) + { + action = 0.0; + inverse.action->Mult(rhs, action); + require_finite(action); + } + + /** @brief Account for a correction in the first group before solving the second group. */ + void solve_second_after_first() const + { + m_couplings.second_from_first->Mult(m_first_action, m_second_coupling); + require_finite(m_second_coupling); + m_second_rhs -= m_second_coupling; + apply_inverse(m_second, m_second_rhs, m_second_action); + } + + /** @brief Solve the first group against the corrected right-hand side. */ + void solve_first_after_second() const + { + m_couplings.first_from_second->Mult(m_second_action, m_first_coupling); + require_finite(m_first_coupling); + m_first_rhs -= m_first_coupling; + apply_inverse(m_first, m_first_rhs, m_first_action); + } + + /** @brief Keep the variable-inverse correction as a fresh solve rather than assuming linearity. */ + void apply_groups() const + { + if (m_order == CouplingOrder::second_then_first) + { + apply_inverse(m_second, m_second_rhs, m_second_action); + solve_first_after_second(); + return; + } + + apply_inverse(m_first, m_first_rhs, m_first_action); + if (m_order == CouplingOrder::independent) + { + apply_inverse(m_second, m_second_rhs, m_second_action); + return; + } + + solve_second_after_first(); + if (m_response) + { + m_response->Mult(m_second_action, m_first_coupling); + require_finite(m_first_coupling); + m_first_action -= m_first_coupling; + } + else + { + solve_first_after_second(); + } + } + + Inverse m_first; + Inverse m_second; + TwoBlockCouplings m_couplings; + CouplingOrder m_order; + OperatorHandle m_response; + mutable ::mfem::Vector m_first_rhs; + mutable ::mfem::Vector m_second_rhs; + mutable ::mfem::Vector m_first_action; + mutable ::mfem::Vector m_second_action; + mutable ::mfem::Vector m_first_coupling; + mutable ::mfem::Vector m_second_coupling; + }; + + /** @brief Apply a fixed linear Schur complement using the same cross-coupling orientation as the inverse. */ + class SchurOperator final : public ::mfem::Operator + { + public: + /** @brief Own accepted actions and prepare reusable vectors without numerical probing. */ + SchurOperator(OperatorHandle diagonal, Inverse eliminated, TwoBlockCouplings couplings) : Operator(diagonal->Height()), m_diagonal(std::move(diagonal)), m_eliminated(std::move(eliminated)), m_couplings(std::move(couplings)), + m_rhs(m_eliminated.action->Width()), m_response(m_eliminated.action->Height()), m_correction(Height()), m_result(Height()) + { + } + + /** @brief Subtract the response through eliminated fields before publishing the retained equations. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + using namespace utils::errors; + + serif_ensure(input.Size() == Width() && output.Size() == Height(), "A Schur action requires preallocated retained-group vectors."); + require_finite(input); + m_couplings.first_from_second->Mult(input, m_rhs); + require_finite(m_rhs); + m_response = 0.0; + m_eliminated.action->Mult(m_rhs, m_response); + require_finite(m_response); + m_couplings.second_from_first->Mult(m_response, m_correction); + m_diagonal->Mult(input, m_result); + m_result -= m_correction; + require_finite(m_result); + output = m_result; + } + + private: + OperatorHandle m_diagonal; + Inverse m_eliminated; + TwoBlockCouplings m_couplings; + mutable ::mfem::Vector m_rhs; + mutable ::mfem::Vector m_response; + mutable ::mfem::Vector m_correction; + mutable ::mfem::Vector m_result; + }; + } // namespace + + std::expected detail::try_coupled_inverse(Inverse first, Inverse second, TwoBlockCouplings couplings, CouplingOrder order, OperatorHandle first_response) + { + if (!first.action || !second.action || first.action->Height() <= 0 || second.action->Height() <= 0 || first.action->Height() != first.action->Width() || second.action->Height() != second.action->Width()) + { + return failure("Both coupled inverses must own nonempty square operators."); + } + if ((first.contract != ApplicationContract::fixed_linear && first.contract != ApplicationContract::variable) || (second.contract != ApplicationContract::fixed_linear && second.contract != ApplicationContract::variable)) + { + return failure("Each inverse must declare either fixed linear or variable application."); + } + const int first_size = first.action->Height(); + const int second_size = second.action->Height(); + if (first_size > std::numeric_limits::max() - second_size) + { + return failure("The combined inverse exceeds MFEM's index range."); + } + if (order != CouplingOrder::independent && order != CouplingOrder::second_then_first && order != CouplingOrder::lower_diagonal_upper) + { + return failure("Choose a supported two-block coupling order."); + } + if ((needs_first_coupling(order) && !matches(couplings.first_from_second, first_size, second_size)) || (needs_second_coupling(order) && !matches(couplings.second_from_first, second_size, first_size))) + { + return failure("The requested Jacobian couplings must map between the two inverse groups."); + } + if (first_response && (order != CouplingOrder::lower_diagonal_upper || first.contract != ApplicationContract::fixed_linear || !matches(first_response, first_size, second_size))) + { + return failure("A cached response requires a fixed linear first inverse and compatible factorization dimensions."); + } + const ApplicationContract contract = first.contract == ApplicationContract::fixed_linear && second.contract == ApplicationContract::fixed_linear ? ApplicationContract::fixed_linear : ApplicationContract::variable; + if (first_response) + { + // Cached responses completely replace B during application; release any temporary assembled B. + couplings.first_from_second.reset(); + } + return Inverse{.action = std::make_shared(std::move(first), std::move(second), std::move(couplings), order, std::move(first_response)), .contract = contract}; + } + + std::expected try_coupled_inverse(Inverse first, Inverse second, TwoBlockCouplings couplings, CouplingOrder order) + { + return detail::try_coupled_inverse(std::move(first), std::move(second), std::move(couplings), order, {}); + } + + std::expected try_schur_operator(OperatorHandle retained_diagonal, Inverse eliminated, TwoBlockCouplings couplings) + { + if (!retained_diagonal || !eliminated.action) + { + return failure("A Schur action requires an owned retained diagonal and eliminated inverse."); + } + const int first_size = eliminated.action->Height(); + const int second_size = retained_diagonal->Height(); + if (first_size <= 0 || second_size <= 0 || eliminated.action->Width() != first_size || retained_diagonal->Width() != second_size || !matches(couplings.first_from_second, first_size, second_size) || !matches(couplings.second_from_first, second_size, first_size)) + { + return failure("The Schur action requires matching square groups and both cross couplings."); + } + if (eliminated.contract != ApplicationContract::fixed_linear) + { + return failure("A fixed Schur operator requires a fixed linear eliminated inverse."); + } + return std::make_shared(std::move(retained_diagonal), std::move(eliminated), std::move(couplings)); + } +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/coupling_internal.hpp b/src/lib/serif/backend/mfem/solver/preconditioner/coupling_internal.hpp new file mode 100644 index 00000000..ac2491fa --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/coupling_internal.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "serif/backend/mfem/solver/preconditioner/coupling.hpp" + +namespace serif::solver::preconditioner::backend::mfem::detail +{ + /** @brief Supply a prepared first-inverse response to the second unknowns when that inverse is fixed linear. */ + [[nodiscard]] std::expected try_coupled_inverse(Inverse first, Inverse second, TwoBlockCouplings couplings, strategies::coupling::CouplingOrder order, OperatorHandle first_response); +} // namespace serif::solver::preconditioner::backend::mfem::detail diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/coupling_selection.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/coupling_selection.cpp new file mode 100644 index 00000000..d5484ee2 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/coupling_selection.cpp @@ -0,0 +1,118 @@ +#include "serif/utils/error/nonfinite.hpp" +#include "serif/backend/mfem/solver/preconditioner/border.hpp" + +#include +#include +#include + +#include "mfem/linalg/vector.hpp" +#include "serif/utils/error/errors.hpp" +#include "serif/backend/mfem/solver/preconditioner/inverse/validation.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + namespace + { + using utils::errors::SERiFErrorCode; + + using detail::failure; + + /** @brief Validate each selected coordinate once before constructing its numerical action. */ + [[nodiscard]] bool valid_indices(std::span indices, int size) + { + if (size < 0 || indices.size() > static_cast(size)) + { + return false; + } + std::vector used(static_cast(size)); + for (const int index : indices) + { + if (index < 0 || index >= size || used[static_cast(index)]) + { + return false; + } + used[static_cast(index)] = true; + } + return true; + } + + /** @brief Embed selected inputs and restrict outputs without changing their independent orders. */ + class SelectedOperator final : public ::mfem::Operator + { + public: + /** @brief Own validated selection metadata and allocate workspace for the original operator. */ + SelectedOperator(OperatorHandle operation, std::span rows, std::span columns) : Operator(static_cast(rows.size()), static_cast(columns.size())), m_operation(std::move(operation)), + m_rows(rows.begin(), rows.end()), m_columns(columns.begin(), columns.end()), + m_input(m_operation->Width()), m_output(m_operation->Height()) + { + } + + /** @brief All input reads precede publication, including when the caller's buffers overlap. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + apply(input, output, false); + } + + /** @brief Transpose both the action and its selected coordinate interpretation. */ + void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + apply(input, output, true); + } + + private: + /** @brief Apply one selection using the original operator's reusable full vectors. */ + void apply(const ::mfem::Vector& input, ::mfem::Vector& output, bool transpose) const + { + using namespace utils::errors; + + const std::vector& input_indices = transpose ? m_rows : m_columns; + const std::vector& output_indices = transpose ? m_columns : m_rows; + ::mfem::Vector& full_input = transpose ? m_output : m_input; + ::mfem::Vector& full_output = transpose ? m_input : m_output; + serif_ensure(input.Size() == static_cast(input_indices.size()) && output.Size() == static_cast(output_indices.size()), "A restricted operator requires preallocated vectors matching the selected rows and columns."); + full_input = 0.0; + for (int index = 0; index < input.Size(); ++index) + { + serif_ensure(std::isfinite(input[index]), "A restricted operator requires finite inputs."); + full_input[input_indices[static_cast(index)]] = input[index]; + } + full_output = 0.0; + if (transpose) + { + m_operation->MultTranspose(full_input, full_output); + } + else + { + m_operation->Mult(full_input, full_output); + } + for (int index = 0; index < full_output.Size(); ++index) + { + serif_ensure(std::isfinite(full_output[index]), "The underlying operator returned nonfinite values before coordinate restriction."); + } + for (int index = 0; index < output.Size(); ++index) + { + output[index] = full_output[output_indices[static_cast(index)]]; + } + } + + OperatorHandle m_operation; + std::vector m_rows; + std::vector m_columns; + mutable ::mfem::Vector m_input; + mutable ::mfem::Vector m_output; + }; + } // namespace + + std::expected try_restrict_operator(OperatorHandle operation, std::span rows, std::span columns) + { + if (!operation) + { + return failure("A coordinate restriction requires an owned operator."); + } + if (!valid_indices(rows, operation->Height()) || !valid_indices(columns, operation->Width())) + { + return failure("Selected equation rows and unknown columns must each be distinct and within their operator dimensions."); + } + return std::make_shared(std::move(operation), rows, columns); + } +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/amg.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/amg.cpp new file mode 100644 index 00000000..74dcd764 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/amg.cpp @@ -0,0 +1,109 @@ +#include "serif/backend/mfem/solver/preconditioner/inverse/amg.hpp" + +#include +#include "serif/backend/mfem/solver/preconditioner/inverse/validation.hpp" + +#ifdef MFEM_USE_MPI +#include "mfem/general/communication.hpp" +#include "mfem/linalg/hypre.hpp" +#endif + +namespace serif::solver::preconditioner::backend::mfem +{ + namespace + { +#ifdef MFEM_USE_MPI + /** @brief Use serial finite-element matrices with one-process Hypre, without a distributed discretization. */ + class AMGInverse final : public ::mfem::Operator + { + public: + /** @brief Copy the sparse snapshot, retain its Hypre partition, and prepare the hierarchy once. */ + AMGInverse(const ::mfem::SparseMatrix& matrix, const int cycles) : Operator(matrix.Height()), m_matrix(matrix), m_partition{0, matrix.Height()}, + m_hypre_matrix(MPI_COMM_SELF, matrix.Height(), m_partition, &m_matrix), m_solver(m_hypre_matrix), m_input(Height()) + { + m_solver.SetMaxIter(cycles); + m_solver.SetTol(0.0); + m_solver.SetPrintLevel(0); + m_solver.iterative_mode = false; + ::mfem::Vector zero(Height()); + zero = 0.0; + m_solver.Setup(zero, zero); + } + + /** @brief Every application starts from zero; scratch belongs to this frozen preparation. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + detail::require_application_size(*this, input, output); + const ::mfem::Vector* source = &input; + if (detail::vectors_overlap(input, output)) + { + m_input = input; + source = &m_input; + } + output = 0.0; + m_solver.Mult(*source, output); + } + + private: + ::mfem::SparseMatrix m_matrix; + HYPRE_BigInt m_partition[2]; + ::mfem::HypreParMatrix m_hypre_matrix; + ::mfem::HypreBoomerAMG m_solver; + mutable ::mfem::Vector m_input; + }; +#endif + + /** @brief Validate the sparse matrix and prepare the one-process hierarchy. */ + [[nodiscard]] std::expected prepare_amg(const std::shared_ptr& matrix, const int cycles) + { + using enum utils::errors::SERiFErrorCode; + + if (const auto valid = detail::check_square(matrix.get()); !valid) + { + return std::unexpected(valid.error()); + } + if (!matrix->Finalized()) + { + return detail::failure("BoomerAMG requires a finalized sparse matrix. Complete assembly before preparing the inverse."); + } + for (int index = 0; index < matrix->NumNonZeroElems(); ++index) + { + if (!std::isfinite(matrix->GetData()[index])) + { + return detail::failure("BoomerAMG sparse entry {} is not finite.", index); + } + } + ::mfem::Vector diagonal; + matrix->GetDiag(diagonal); + for (int index = 0; index < diagonal.Size(); ++index) + { + if (diagonal[index] <= 0.0) + { + return detail::failure("BoomerAMG requires a positive elliptic diagonal; entry {} is {}. Remove unsupported zero rows or choose another inverse.", index, diagonal[index]); + } + } +#ifdef MFEM_USE_MPI + if (!::mfem::Mpi::IsInitialized() || ::mfem::Mpi::IsFinalized()) + { + return detail::failure("Initialize an MPI session before using the one-process BoomerAMG backend. The preconditioner does not own MPI lifetime."); + } + ::mfem::Hypre::Init(); + return Inverse{.action = std::make_shared(*matrix, cycles), .contract = ApplicationContract::fixed_linear}; +#else + (void)cycles; + return detail::failure("BoomerAMG requires an MFEM build with Hypre support. Choose another inverse explicitly; no numerical fallback is selected automatically."); +#endif + } + } // namespace + + std::expected try_amg_inverse(std::shared_ptr matrix, const strategies::inverse::FixedAMG& options) + { + using enum utils::errors::SERiFErrorCode; + + if (options.cycles <= 0) + { + return detail::failure("Fixed BoomerAMG needs at least one cycle, received {}.", options.cycles); + } + return prepare_amg(matrix, options.cycles); + } +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/dense.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/dense.cpp new file mode 100644 index 00000000..c0a7007f --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/dense.cpp @@ -0,0 +1,168 @@ +#include "serif/backend/mfem/solver/preconditioner/inverse/dense.hpp" + +#include +#include +#include +#include "serif/backend/mfem/solver/preconditioner/inverse/validation.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + namespace + { + /** @brief Equilibrate rows before factorization without discarding representable mixed-scale entries. */ + class DenseInverse final : public ::mfem::Operator + { + public: + /** @brief Own scaled LU storage and its pivots at stable addresses. */ + explicit DenseInverse(const ::mfem::DenseMatrix& matrix) : Operator(matrix.Height()), m_matrix(matrix), m_pivots(matrix.Height()), m_factors(m_matrix.Data(), m_pivots.data()), m_row_scales(matrix.Height()) + { + for (int row = 0; row < Height(); ++row) + { + for (int column = 0; column < Width(); ++column) + { + m_row_scales[row] = std::max(m_row_scales[row], std::abs(matrix(row, column))); + } + if (m_row_scales[row] > 0.0) + { + for (int column = 0; column < Width(); ++column) + { + m_matrix(row, column) /= m_row_scales[row]; + } + } + } + } + + /** @brief Reject singular pivots before MFEM can abort inside an unchecked inverse constructor. */ + [[nodiscard]] bool factor() + { + return m_factors.Factor(Height()); + } + + /** @brief Estimate the original inverse norm, undoing equilibration before applying the common condition-number scale. */ + [[nodiscard]] std::expected inverse_norm(const double common_scale) const + { + using enum utils::errors::SERiFErrorCode; + + ::mfem::DenseMatrix inverse(Height()); + m_factors.GetInverseMatrix(Height(), inverse.Data()); + double maximum = 0.0; + for (int row = 0; row < Height(); ++row) + { + double sum = 0.0; + for (int column = 0; column < Width(); ++column) + { + const double entry = inverse(row, column) / m_row_scales[column]; + if (!std::isfinite(entry)) + { + return detail::failure("The dense inverse is not representable in these coordinates. Rescale before preparing it."); + } + sum += std::abs(entry) * common_scale; + } + maximum = std::max(maximum, sum); + } + return maximum; + } + + /** @brief Scale each residual row before solving its equilibrated system. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + solve(input, output, false); + } + + /** @brief Solve the transposed small block using the same frozen factors. */ + void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + solve(input, output, true); + } + + private: + /** @brief Share scaling and alias-safe copying between forward and transposed solves. */ + void solve(const ::mfem::Vector& input, ::mfem::Vector& output, const bool transpose) const + { + detail::require_application_size(*this, input, output); + detail::copy_coordinates(input, output); + if (transpose) + { + m_factors.RightSolve(Height(), 1, output.GetData()); + scale_rows(output); + } + else + { + scale_rows(output); + m_factors.Solve(Height(), 1, output.GetData()); + } + } + + /** @brief Undo row equilibration on the appropriate side of each triangular solve. */ + void scale_rows(::mfem::Vector& values) const + { + for (int index = 0; index < Height(); ++index) + { + values[index] /= m_row_scales[index]; + } + } + + ::mfem::DenseMatrix m_matrix; + std::vector m_pivots; + ::mfem::LUFactors m_factors; + std::vector m_row_scales; + }; + } // namespace + + std::expected try_dense_inverse(const ::mfem::DenseMatrix& matrix, const strategies::inverse::DenseDirect& options) + { + using enum utils::errors::SERiFErrorCode; + + if (const auto valid = detail::check_square(&matrix); !valid) + { + return std::unexpected(valid.error()); + } + if (!std::isfinite(options.minimum_reciprocal_condition) || options.minimum_reciprocal_condition < 0.0 || options.minimum_reciprocal_condition > 1.0) + { + return detail::failure("The minimum reciprocal condition must be finite and in [0,1], received {}.", options.minimum_reciprocal_condition); + } + double scale = 0.0; + for (int row = 0; row < matrix.Height(); ++row) + { + for (int column = 0; column < matrix.Width(); ++column) + { + if (!std::isfinite(matrix(row, column))) + { + return detail::failure("Dense entry ({},{}) is not finite; prepare a finite small-block operator.", row, column); + } + scale = std::max(scale, std::abs(matrix(row, column))); + } + } + if (scale == 0.0) + { + return detail::failure("The dense operator is zero and cannot be inverted."); + } + double norm = 0.0; + for (int row = 0; row < matrix.Height(); ++row) + { + double sum = 0.0; + for (int column = 0; column < matrix.Width(); ++column) + { + sum += std::abs(matrix(row, column) / scale); + } + norm = std::max(norm, sum); + } + + const auto inverse = std::make_shared(matrix); + if (!inverse->factor()) + { + return detail::failure("Pivoted LU found a zero pivot in the dense operator. Remove dependent constraints or choose a nonsingular block."); + } + const auto inverse_norm = inverse->inverse_norm(scale); + if (!inverse_norm) + { + return std::unexpected(inverse_norm.error()); + } + const double reciprocal_condition = (1.0 / norm) / *inverse_norm; + if (!std::isfinite(reciprocal_condition) || reciprocal_condition < options.minimum_reciprocal_condition) + { + return detail::failure("Dense infinity-norm reciprocal condition {} is below the required {}. Rescale or revise the coupled constraints.", reciprocal_condition, options.minimum_reciprocal_condition); + } + return Inverse{.action = inverse}; + } +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/diagonal.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/diagonal.cpp new file mode 100644 index 00000000..f04f33b7 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/diagonal.cpp @@ -0,0 +1,79 @@ +#include "serif/backend/mfem/solver/preconditioner/inverse/diagonal.hpp" + +#include +#include +#include +#include +#include +#include "serif/backend/mfem/solver/preconditioner/inverse/validation.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + namespace + { + /** @brief Own a signed pointwise inverse without an unnecessary solver binding lifecycle. */ + class DiagonalInverse final : public ::mfem::Operator + { + public: + /** @brief Consume validated reciprocals. */ + explicit DiagonalInverse(std::vector reciprocal) : Operator(static_cast(reciprocal.size())), m_reciprocal(std::move(reciprocal)) + { + } + + /** @brief Overwrite every coefficient, including when input and output are the same vector. */ + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + detail::require_application_size(*this, input, output); + if (detail::vectors_overlap(input, output) && std::less{}(input.GetData(), output.GetData())) + { + for (int index = Height() - 1; index >= 0; --index) + { + output[index] = m_reciprocal[index] * input[index]; + } + return; + } + for (int index = 0; index < Height(); ++index) + { + output[index] = m_reciprocal[index] * input[index]; + } + } + + /** @brief A real diagonal inverse is its own coordinate transpose. */ + void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + Mult(input, output); + } + + private: + std::vector m_reciprocal; + }; + } // namespace + + std::expected try_diagonal_inverse(const std::span diagonal) + { + using enum utils::errors::SERiFErrorCode; + + if (diagonal.empty() || diagonal.size() > static_cast(std::numeric_limits::max())) + { + return detail::failure("A diagonal inverse needs a nonempty MFEM-sized coordinate block."); + } + std::vector reciprocal(diagonal.size()); + for (std::size_t index = 0; index < diagonal.size(); ++index) + { + if (!std::isfinite(diagonal[index])) + { + return detail::failure("Diagonal entry {} is not finite. Prepare a finite physical operator before inversion.", index); + } + if (diagonal[index] == 0.0) + { + return detail::failure("Diagonal entry {} is zero; this coordinate has no inverse.", index); + } + reciprocal[index] = 1.0 / diagonal[index]; + if (!std::isfinite(reciprocal[index])) + { + return detail::failure("The reciprocal of diagonal entry {} is not representable. Rescale these coordinates before preparing the inverse.", index); + } + } + return Inverse{.action = std::make_shared(std::move(reciprocal))}; + } +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu.hpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu.hpp new file mode 100644 index 00000000..1ffd55e7 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +namespace serif::solver::preconditioner::backend::mfem::detail +{ + /** + * @brief Symbolic analysis (fill-reducing ordering) of one sparsity pattern, shared by the numeric factorizations + * of every matrix with that pattern. Implemented by the backend chosen with the sparse_direct Meson option + * (direct_lu_accelerate.cpp, direct_lu_umfpack.cpp or direct_lu_none.cpp). + * @note No backend type appears here: Accelerate's headers are not valid C++26 and are confined to a C++20 + * translation unit. A numeric factorization from a reused analysis equals a fresh factorization bit for bit + * (measured for both backends on potential blocks from different Newton states). + */ + class DirectLUPattern final + { + public: + /** @brief Analyze a square matrix in compressed columns; the arrays are copied. Empty on failure, with a backend status. */ + [[nodiscard]] static std::shared_ptr analyze(int size, const long* column_starts, const int* row_indices, const double* values, int& status); + + ~DirectLUPattern(); + DirectLUPattern(const DirectLUPattern&) = delete; + DirectLUPattern& operator=(const DirectLUPattern&) = delete; + + /** @brief True when a matrix has exactly this size, column starts and row indices. */ + [[nodiscard]] bool matches(int size, const long* column_starts, const int* row_indices) const; + + /** @brief Backend-specific analysis; opaque outside the backend file. */ + struct State; + + private: + friend class DirectLU; + explicit DirectLUPattern(std::unique_ptr state); + std::unique_ptr m_state; + }; + + class DirectLU final + { + public: + /** @brief Numerically factor values laid out in pattern's compressed columns; the values are copied. Empty on failure, with a backend status. */ + [[nodiscard]] static std::unique_ptr factor(std::shared_ptr pattern, const double* values, int& status); + + ~DirectLU(); + DirectLU(const DirectLU&) = delete; + DirectLU& operator=(const DirectLU&) = delete; + + /** @brief Solve A x = b; the right-hand side is copied first, so b and x may overlap. */ + void solve(const double* right, double* solution) const; + + /** + * @brief Solve A X = B for count column-major right-hand sides (leading dimension = size); B is copied first. + * @note Accelerate solves the block at once (one pass over the factor); the columns agree with solve() to + * rounding, not bit for bit. UMFPACK solves the columns one by one, as solve(). + */ + void solve_many(int count, const double* right, double* solution) const; + + private: + struct State; + explicit DirectLU(std::unique_ptr state); + std::unique_ptr m_state; + }; + + /** @brief The compiled backend: "accelerate", "umfpack" or "none". */ + [[nodiscard]] const char* direct_lu_backend() noexcept; + + /** @brief True when the compiled backend can factor on this machine (Accelerate also needs macOS 15.5 at run time). */ + [[nodiscard]] bool direct_lu_available() noexcept; +} // namespace serif::solver::preconditioner::backend::mfem::detail diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_accelerate.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_accelerate.cpp new file mode 100644 index 00000000..9562e41d --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_accelerate.cpp @@ -0,0 +1,157 @@ +// sparse_direct=accelerate. Compiled as C++20 in its own static library: Accelerate's headers are not valid C++26. +#include "direct_lu.hpp" + +#include +#include +#include + +#include + +namespace serif::solver::preconditioner::backend::mfem::detail +{ + struct DirectLUPattern::State + { + int size{0}; + std::vector column_starts; + std::vector row_indices; + SparseOpaqueSymbolicFactorization symbolic{}; + }; + + struct DirectLU::State + { + std::shared_ptr pattern; + std::vector values; + SparseOpaqueFactorization_Double factor{}; + mutable std::vector right; + }; + + namespace + { + [[nodiscard]] SparseMatrixStructure structure(const DirectLUPattern::State& pattern) + { + SparseAttributes_t attributes{}; + attributes.kind = SparseOrdinary; + SparseMatrixStructure result{}; + result.rowCount = pattern.size; + result.columnCount = pattern.size; + // Accelerate reads but does not modify the structure arrays. + result.columnStarts = const_cast(pattern.column_starts.data()); + result.rowIndices = const_cast(pattern.row_indices.data()); + result.attributes = attributes; + result.blockSize = 1; + return result; + } + } // namespace + + const char* direct_lu_backend() noexcept { return "accelerate"; } + + bool direct_lu_available() noexcept + { + if (__builtin_available(macOS 15.5, *)) { return true; } + return false; + } + + std::shared_ptr DirectLUPattern::analyze(const int size, const long* column_starts, const int* row_indices, const double*, int& status) + { + if (!direct_lu_available()) + { + status = -1; + return nullptr; + } + auto state = std::make_unique(); + state->size = size; + state->column_starts.assign(column_starts, column_starts + size + 1); + state->row_indices.assign(row_indices, row_indices + column_starts[size]); + // Nested-dissection ordering: on the benchmark potential block (16k rows, 3.4M nonzeros) it halves the + // factorization footprint (271 vs 540 MB) and factors faster (0.25 vs 0.37 s) than the default ordering. + SparseSymbolicFactorOptions symbolic{}; + symbolic.control = SparseDefaultControl; + symbolic.orderMethod = SparseOrderMetis; + symbolic.malloc = std::malloc; + symbolic.free = std::free; + if (__builtin_available(macOS 15.5, *)) + { + state->symbolic = SparseFactor(SparseFactorizationLU, structure(*state), symbolic); + } + status = static_cast(state->symbolic.status); + if (state->symbolic.status != SparseStatusOK) + { + SparseCleanup(state->symbolic); + return nullptr; + } + return std::shared_ptr(new DirectLUPattern(std::move(state))); + } + + DirectLUPattern::DirectLUPattern(std::unique_ptr state) : m_state(std::move(state)) {} + + // Numeric factorizations hold their own reference to the symbolic factorization. + DirectLUPattern::~DirectLUPattern() + { + if (m_state) { SparseCleanup(m_state->symbolic); } + } + + bool DirectLUPattern::matches(const int size, const long* column_starts, const int* row_indices) const + { + return size == m_state->size && std::equal(m_state->column_starts.begin(), m_state->column_starts.end(), column_starts) && + std::equal(m_state->row_indices.begin(), m_state->row_indices.end(), row_indices); + } + + std::unique_ptr DirectLU::factor(std::shared_ptr pattern, const double* values, int& status) + { + const auto& shape = *pattern->m_state; + auto state = std::make_unique(); + state->values.assign(values, values + shape.column_starts[static_cast(shape.size)]); + state->right.resize(static_cast(shape.size)); + SparseMatrix_Double matrix{}; + matrix.structure = structure(shape); + matrix.data = state->values.data(); + SparseNumericFactorOptions numeric{}; + numeric.control = SparseDefaultControl; + numeric.scalingMethod = SparseScalingDefault; + numeric.pivotTolerance = 0.01; + numeric.zeroTolerance = 0.0; + state->factor = SparseFactor(shape.symbolic, matrix, numeric); + status = static_cast(state->factor.status); + if (state->factor.status != SparseStatusOK) + { + SparseCleanup(state->factor); + return nullptr; + } + state->pattern = std::move(pattern); + return std::unique_ptr(new DirectLU(std::move(state))); + } + + DirectLU::~DirectLU() + { + if (m_state) { SparseCleanup(m_state->factor); } + } + + void DirectLU::solve(const double* right, double* solution) const + { + const int size = m_state->pattern->m_state->size; + std::copy(right, right + size, m_state->right.begin()); + DenseVector_Double b{}; + b.count = size; + b.data = m_state->right.data(); + DenseVector_Double x{}; + x.count = size; + x.data = solution; + SparseSolve(m_state->factor, b, x); + } + + void DirectLU::solve_many(const int count, const double* right, double* solution) const + { + const int size = m_state->pattern->m_state->size; + std::vector copy(right, right + static_cast(size) * static_cast(count)); + DenseMatrix_Double b{}; + b.rowCount = size; + b.columnCount = count; + b.columnStride = size; + b.data = copy.data(); + DenseMatrix_Double x = b; + x.data = solution; + SparseSolve(m_state->factor, b, x); + } + + DirectLU::DirectLU(std::unique_ptr state) : m_state(std::move(state)) {} +} // namespace serif::solver::preconditioner::backend::mfem::detail diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_none.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_none.cpp new file mode 100644 index 00000000..471aa491 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_none.cpp @@ -0,0 +1,39 @@ +// sparse_direct=none: the SparseDirect strategy exists, but its preparation reports that no backend was built. +#include "direct_lu.hpp" + +namespace serif::solver::preconditioner::backend::mfem::detail +{ + struct DirectLUPattern::State {}; + + struct DirectLU::State {}; + + const char* direct_lu_backend() noexcept { return "none"; } + + bool direct_lu_available() noexcept { return false; } + + std::shared_ptr DirectLUPattern::analyze(int, const long*, const int*, const double*, int& status) + { + status = -1; + return nullptr; + } + + DirectLUPattern::DirectLUPattern(std::unique_ptr state) : m_state(std::move(state)) {} + + DirectLUPattern::~DirectLUPattern() = default; + + bool DirectLUPattern::matches(int, const long*, const int*) const { return false; } + + std::unique_ptr DirectLU::factor(std::shared_ptr, const double*, int& status) + { + status = -1; + return nullptr; + } + + DirectLU::DirectLU(std::unique_ptr state) : m_state(std::move(state)) {} + + DirectLU::~DirectLU() = default; + + void DirectLU::solve(const double*, double*) const {} + + void DirectLU::solve_many(int, const double*, double*) const {} +} // namespace serif::solver::preconditioner::backend::mfem::detail diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_umfpack.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_umfpack.cpp new file mode 100644 index 00000000..ae15c234 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/direct_lu_umfpack.cpp @@ -0,0 +1,120 @@ +// sparse_direct=umfpack: SuiteSparse UMFPACK, the portable backend (Linux, macOS, Windows). +#include "direct_lu.hpp" + +#include +#include + +#include + +namespace serif::solver::preconditioner::backend::mfem::detail +{ + struct DirectLUPattern::State + { + int size{0}; + std::vector column_starts; // UMFPACK's int interface; MFEM indices are int + std::vector row_indices; + std::vector control = std::vector(UMFPACK_CONTROL); + void* symbolic{nullptr}; + }; + + struct DirectLU::State + { + std::shared_ptr pattern; + std::vector values; + void* numeric{nullptr}; + mutable std::vector right; + mutable std::vector integer_workspace; + mutable std::vector workspace; + }; + + const char* direct_lu_backend() noexcept { return "umfpack"; } + + bool direct_lu_available() noexcept { return true; } + + std::shared_ptr DirectLUPattern::analyze(const int size, const long* column_starts, const int* row_indices, const double* values, int& status) + { + auto state = std::make_unique(); + state->size = size; + state->column_starts.assign(column_starts, column_starts + size + 1); + state->row_indices.assign(row_indices, row_indices + column_starts[size]); + umfpack_di_defaults(state->control.data()); + // The preconditioner needs no iterative refinement; FGMRES corrects the outer solution. + state->control[UMFPACK_IRSTEP] = 0.0; + // METIS: on the benchmark potential block it leaves 20% less fill than the default ordering (11.2M vs 14.0M + // entries) and factors slightly faster. SuiteSparse builds without METIS reject it, so retry with the default. + state->control[UMFPACK_ORDERING] = UMFPACK_ORDERING_METIS; + std::vector info(UMFPACK_INFO); + status = umfpack_di_symbolic(size, size, state->column_starts.data(), state->row_indices.data(), values, + &state->symbolic, state->control.data(), info.data()); + if (status != UMFPACK_OK) { + umfpack_di_defaults(state->control.data()); + state->control[UMFPACK_IRSTEP] = 0.0; + status = umfpack_di_symbolic(size, size, state->column_starts.data(), state->row_indices.data(), values, + &state->symbolic, state->control.data(), info.data()); + } + if (status != UMFPACK_OK) { return nullptr; } + return std::shared_ptr(new DirectLUPattern(std::move(state))); + } + + DirectLUPattern::DirectLUPattern(std::unique_ptr state) : m_state(std::move(state)) {} + + DirectLUPattern::~DirectLUPattern() + { + if (m_state && m_state->symbolic) { umfpack_di_free_symbolic(&m_state->symbolic); } + } + + bool DirectLUPattern::matches(const int size, const long* column_starts, const int* row_indices) const + { + return size == m_state->size && std::equal(m_state->column_starts.begin(), m_state->column_starts.end(), column_starts) && + std::equal(m_state->row_indices.begin(), m_state->row_indices.end(), row_indices); + } + + std::unique_ptr DirectLU::factor(std::shared_ptr pattern, const double* values, int& status) + { + auto& shape = *pattern->m_state; + auto state = std::make_unique(); + state->values.assign(values, values + shape.column_starts[static_cast(shape.size)]); + state->right.resize(static_cast(shape.size)); + state->integer_workspace.resize(static_cast(shape.size)); + state->workspace.resize(5 * static_cast(shape.size)); // wsolve without iterative refinement + std::vector info(UMFPACK_INFO); + // umfpack_di_numeric reads the symbolic object without modifying it. + status = umfpack_di_numeric(shape.column_starts.data(), shape.row_indices.data(), state->values.data(), shape.symbolic, + &state->numeric, shape.control.data(), info.data()); + if (status != UMFPACK_OK) + { + umfpack_di_free_numeric(&state->numeric); + return nullptr; + } + state->pattern = std::move(pattern); + return std::unique_ptr(new DirectLU(std::move(state))); + } + + DirectLU::DirectLU(std::unique_ptr state) : m_state(std::move(state)) {} + + DirectLU::~DirectLU() + { + if (m_state && m_state->numeric) { umfpack_di_free_numeric(&m_state->numeric); } + } + + void DirectLU::solve(const double* right, double* solution) const + { + const auto& shape = *m_state->pattern->m_state; + std::copy(right, right + shape.size, m_state->right.begin()); + std::vector info(UMFPACK_INFO); + umfpack_di_wsolve(UMFPACK_A, shape.column_starts.data(), shape.row_indices.data(), m_state->values.data(), solution, + m_state->right.data(), m_state->numeric, shape.control.data(), info.data(), m_state->integer_workspace.data(), + m_state->workspace.data()); + } + + // UMFPACK has no multiple right-hand-side solve. + void DirectLU::solve_many(const int count, const double* right, double* solution) const + { + const std::size_t size = static_cast(m_state->pattern->m_state->size); + std::vector copy(right, right + size * static_cast(count)); + for (int column = 0; column < count; ++column) + { + solve(copy.data() + size * static_cast(column), solution + size * static_cast(column)); + } + } +} // namespace serif::solver::preconditioner::backend::mfem::detail diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.cpp new file mode 100644 index 00000000..b4077719 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.cpp @@ -0,0 +1,139 @@ +#include "serif/backend/mfem/solver/preconditioner/inverse/sparse_direct.hpp" + +#include +#include +#include +#include +#include "serif/backend/mfem/solver/preconditioner/inverse/validation.hpp" +#include "direct_lu.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + namespace + { + /** @brief Own one sparse LU factorization of a frozen matrix snapshot. */ + class SparseDirectInverse final : public ::mfem::Operator + { + public: + SparseDirectInverse(const int size, std::unique_ptr factor) : Operator(size), m_factor(std::move(factor)) {} + + void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override + { + detail::require_application_size(*this, input, output); + // The factor copies the right-hand side first, so input and output may overlap. + m_factor->solve(input.GetData(), output.GetData()); + } + + /** @brief Solve the whole batch in one backend call; with Accelerate it agrees with Mult to rounding, not bit for bit. */ + void ArrayMult(const ::mfem::Array& inputs, ::mfem::Array<::mfem::Vector*>& outputs) const override + { + const int count = inputs.Size(); + if (count != outputs.Size() || count == 0) + { + Operator::ArrayMult(inputs, outputs); + return; + } + const std::size_t size = static_cast(height); + std::vector right(size * static_cast(count)), solution(right.size()); + for (int column = 0; column < count; ++column) + { + detail::require_application_size(*this, *inputs[column], *outputs[column]); + std::copy_n(inputs[column]->GetData(), size, right.data() + size * static_cast(column)); + } + m_factor->solve_many(count, right.data(), solution.data()); + for (int column = 0; column < count; ++column) + { + std::copy_n(solution.data() + size * static_cast(column), size, outputs[column]->GetData()); + } + } + + private: + std::unique_ptr m_factor; + }; + } // namespace + + struct SparseDirectCache::State + { + std::mutex mutex; + std::shared_ptr pattern; + }; + + SparseDirectCache::SparseDirectCache() : m_state(std::make_unique()) {} + + SparseDirectCache::~SparseDirectCache() = default; + + std::shared_ptr SparseDirectCache::pattern(const int size, const long* column_starts, const int* row_indices, const double* values, int& status) + { + const std::scoped_lock lock(m_state->mutex); + if (m_state->pattern && m_state->pattern->matches(size, column_starts, row_indices)) + { + status = 0; + return m_state->pattern; + } + auto analyzed = detail::DirectLUPattern::analyze(size, column_starts, row_indices, values, status); + if (analyzed) { m_state->pattern = analyzed; } + return analyzed; + } + + std::string_view sparse_direct_backend() noexcept { return detail::direct_lu_backend(); } + + bool sparse_direct_available() noexcept { return detail::direct_lu_available(); } + + std::expected try_sparse_direct_inverse(std::shared_ptr matrix, const strategies::inverse::SparseDirect&, SparseDirectCache* cache) + { + using enum utils::errors::SERiFErrorCode; + + if (const auto valid = detail::check_square(matrix.get()); !valid) + { + return std::unexpected(valid.error()); + } + if (!matrix->Finalized()) + { + return detail::failure("A sparse direct inverse requires a finalized sparse matrix. Complete assembly before preparing the inverse."); + } + if (!sparse_direct_available()) + { + return detail::failure("The sparse direct inverse is unavailable (backend '{}'; Accelerate needs macOS 15.5 or newer). Rebuild with -Dsparse_direct=accelerate or umfpack, or choose another inverse explicitly; no numerical fallback is selected automatically.", + sparse_direct_backend()); + } + const int size = matrix->Height(); + const int nonzeros = matrix->NumNonZeroElems(); + const int* row_starts = matrix->GetI(); + const int* columns = matrix->GetJ(); + const double* data = matrix->GetData(); + for (int index = 0; index < nonzeros; ++index) + { + if (!std::isfinite(data[index])) + { + return detail::failure("Sparse direct inverse entry {} is not finite.", index); + } + } + + // Every backend takes compressed columns; transpose MFEM's compressed rows. + std::vector column_starts(static_cast(size) + 1, 0); + for (int entry = 0; entry < nonzeros; ++entry) { ++column_starts[static_cast(columns[entry]) + 1]; } + for (int column = 0; column < size; ++column) { column_starts[static_cast(column) + 1] += column_starts[static_cast(column)]; } + std::vector row_indices(static_cast(nonzeros)); + std::vector values(static_cast(nonzeros)); + std::vector cursor(column_starts.begin(), column_starts.end() - 1); + for (int row = 0; row < size; ++row) + { + for (int entry = row_starts[row]; entry < row_starts[row + 1]; ++entry) + { + const long slot = cursor[static_cast(columns[entry])]++; + row_indices[static_cast(slot)] = row; + values[static_cast(slot)] = data[entry]; + } + } + int status = 0; // Accelerate: SparseStatus_t; UMFPACK: its status code + auto pattern = cache ? cache->pattern(size, column_starts.data(), row_indices.data(), values.data(), status) + : detail::DirectLUPattern::analyze(size, column_starts.data(), row_indices.data(), values.data(), status); + auto factor = pattern ? detail::DirectLU::factor(std::move(pattern), values.data(), status) : nullptr; + if (!factor) + { + return detail::failure("Sparse LU factorization of the {}-row block failed ({} status {}). The block is singular or not representable; choose another inverse.", + size, sparse_direct_backend(), status); + } + return Inverse{.action = std::make_shared(size, std::move(factor)), .contract = ApplicationContract::fixed_linear}; + } +} // namespace serif::solver::preconditioner::backend::mfem diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/inverse/validation.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/validation.cpp new file mode 100644 index 00000000..a1ab7762 --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/inverse/validation.cpp @@ -0,0 +1,38 @@ +#include "serif/backend/mfem/solver/preconditioner/inverse/validation.hpp" + +#include +#include + +namespace serif::solver::preconditioner::backend::mfem::detail +{ + std::expected check_square(const ::mfem::Operator* operation) + { + using enum utils::errors::SERiFErrorCode; + + if (operation == nullptr || operation->Height() <= 0 || operation->Height() != operation->Width()) + { + return failure("An inverse requires an owned, nonempty square operator. Supply matching residual and correction coordinates."); + } + return {}; + } + + bool vectors_overlap(const ::mfem::Vector& first, const ::mfem::Vector& second) + { + const std::less before; + return before(first.GetData(), second.GetData() + second.Size()) && before(second.GetData(), first.GetData() + first.Size()); + } + + void copy_coordinates(const ::mfem::Vector& input, ::mfem::Vector& output) + { + std::memmove(output.GetData(), input.GetData(), static_cast(input.Size()) * sizeof(double)); + } + + void require_application_size(const ::mfem::Operator& operation, const ::mfem::Vector& input, const ::mfem::Vector& output) + { + using namespace utils::errors; + + serif_ensure(input.Size() == operation.Width() && output.Size() == operation.Height(), + "Preconditioner application received input/output lengths {}/{}, expected {}/{}. Allocate the matching coordinate vectors before applying the frozen inverse.", + input.Size(), output.Size(), operation.Width(), operation.Height()); + } +} // namespace serif::solver::preconditioner::backend::mfem::detail diff --git a/src/lib/serif/backend/mfem/solver/preconditioner/solver.cpp b/src/lib/serif/backend/mfem/solver/preconditioner/solver.cpp new file mode 100644 index 00000000..e22c152e --- /dev/null +++ b/src/lib/serif/backend/mfem/solver/preconditioner/solver.cpp @@ -0,0 +1,43 @@ +#include "serif/backend/mfem/solver/preconditioner/solver.hpp" + +#include +#include "serif/utils/error/errors.hpp" + +namespace serif::solver::preconditioner::backend::mfem +{ + Solver::Solver(Inverse inverse, OperatorHandle jacobian) : m_inverse(std::move(inverse)), m_jacobian(std::move(jacobian)) + { + using namespace utils::errors; + + serif_ensure(m_inverse.action && m_jacobian, + "The preconditioner adapter needs an accepted inverse and physical Jacobian. Prepare both before binding the solver."); + height = m_inverse.action->Height(); + width = m_inverse.action->Width(); + serif_ensure(height == m_jacobian->Width() && width == m_jacobian->Height(), + "The inverse dimensions do not reverse the physical Jacobian dimensions. Bind matching accepted operators."); + m_input.SetSize(width); + m_output.SetSize(height); + iterative_mode = false; + } + + void Solver::SetOperator(const ::mfem::Operator& jacobian) + { + utils::errors::serif_ensure(&jacobian == m_jacobian.get(), + "This preconditioner belongs to a different accepted Jacobian. Prepare and bind a new inverse for the new linearization."); + } + + void Solver::Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const + { + utils::errors::serif_ensure(input.Size() == width && output.Size() == height, + "Preconditioner application needs {} residual and {} state coefficients in preallocated vectors; received {} and {}.", width, height, input.Size(), output.Size()); + m_input = input; + m_output = 0.0; + m_inverse.action->Mult(m_input, m_output); + output = m_output; + } + + ApplicationContract Solver::contract() const noexcept + { + return m_inverse.contract; + } +} // namespace serif::solver::preconditioner::backend::mfem