#include "profile.h" #include #include #include #include #include import mean_field; import test_helpers; using namespace mean_field; using Catch::Matchers::WithinAbs; namespace { namespace blocks = utils::blocks; using form = blocks::gravity_field_form; constexpr auto density_block = blocks::get_value_block
(blocks::density_field.mass_term); constexpr auto displacement_block = blocks::get_value_block(blocks::displacement_field.geometry_term); constexpr auto gravity_gradient_block = blocks::get_value_block(blocks::gravity_field.gradient_term); constexpr auto gravity_potential_block = blocks::get_value_block(blocks::gravity_field.poisson_term); constexpr auto gravity_gradient_residual_block = blocks::get_residual_block(blocks::gravity_field.gradient_term); constexpr auto gravity_poisson_residual_block = blocks::get_residual_block(blocks::gravity_field.poisson_term); blocks::form_layout make_gravity_layout(const fem::FEM &f) { using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const auto density_map = field::make_field_dof_map(*f.densityFes); const auto displacement_map = field::make_field_dof_map(*f.displacementFes); const auto flux_map = field::make_field_dof_map(*f.gravityFluxFes); const auto potential_map = field::make_field_dof_map(*f.gravityPotentialFes); const std::array value_sizes{ density_map.reduced_size(), displacement_map.reduced_size(), flux_map.reduced_size(), potential_map.reduced_size() }; const std::array residual_sizes{ flux_map.reduced_size(), potential_map.reduced_size() }; return blocks::form_layout(value_sizes, residual_sizes); } template void set_block( mfem::Vector &vector, const mfem::Array &offsets, const Block block, const mfem::Vector &values ) { const int block_id = block; const int begin = offsets[block_id]; const int size = offsets[block_id + 1] - begin; REQUIRE(values.Size() == size); for (int i = 0; i < size; ++i) vector(begin + i) = values(i); } template mfem::Vector get_block( const mfem::Vector &vector, const mfem::Array &offsets, const Block block ) { const int block_id = block; const int begin = offsets[block_id]; const int size = offsets[block_id + 1] - begin; mfem::Vector result(size); for (int i = 0; i < size; ++i) result(i) = vector(begin + i); return result; } mfem::Vector make_test_vector( const int size, const double phase ) { mfem::Vector vector(size); for (int i = 0; i < size; ++i) vector(i) = 0.4 * std::sin(0.37 * static_cast(i + 1) + phase) + 0.2 * std::cos(0.19 * static_cast(i + 1) - phase); return vector; } mfem::Vector make_displacement(const fem::FEM &f) { auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.015 * position(0) + 0.004 * position(1); value(1) = -0.003 * position(0) + 0.012 * position(1); value(2) = -0.008 * position(2); }; mfem::VectorFunctionCoefficient coefficient(3, displacement_function); mfem::ParGridFunction displacement(f.displacementFes.get()); mfem::Vector displacement_true; displacement.ProjectCoefficient(coefficient); displacement.GetTrueDofs(displacement_true); return displacement_true; } mfem::Vector make_constant_density( const fem::FEM &f, const double value ) { mfem::ConstantCoefficient coefficient(value); mfem::ParGridFunction density(f.densityFes.get()); mfem::Vector density_true; density.ProjectCoefficient(coefficient); density.GetTrueDofs(density_true); return density_true; } mfem::Vector make_vacuum_density( const fem::FEM &f, const double value ) { mfem::ParGridFunction density(f.densityFes.get()); density = 0.0; mfem::Array element_dofs; mfem::Vector element_values; for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { if (f.mesh->GetAttribute(element_id) != f.domainMapperStateless->GetVacuumElementAttribute()) continue; f.densityFes->GetElementDofs(element_id, element_dofs); element_values.SetSize(element_dofs.Size()); element_values = value; density.SetSubVector(element_dofs, element_values); } mfem::Vector density_true; density.GetTrueDofs(density_true); return density_true; } double relative_difference( const mfem::Vector &lhs, const mfem::Vector &rhs ) { REQUIRE(lhs.Size() == rhs.Size()); mfem::Vector difference(lhs); difference -= rhs; return difference.Norml2() / std::max({lhs.Norml2(), rhs.Norml2(), 1.0e-14}); } mfem::Vector make_core_supported_gravity_gradient(const fem::FEM &f) { constexpr double support_radius = 0.15 * utils::RADIUS; constexpr double support_radius_squared = support_radius * support_radius; auto field_function = [](const mfem::Vector &position, mfem::Vector &value) { const double radius_squared = position * position; value.SetSize(3); value = 0.0; if (radius_squared >= support_radius_squared) return; const double normalized_radius_squared = radius_squared / support_radius_squared; const double envelope = std::pow(1.0 - normalized_radius_squared, 3.0); value(0) = envelope; value(1) = -0.4 * envelope; value(2) = 0.7 * envelope; }; mfem::VectorFunctionCoefficient coefficient(3, field_function); mfem::ParGridFunction gravity_gradient(f.gravityFluxFes.get()); mfem::Vector gravity_gradient_true; gravity_gradient.ProjectCoefficient(coefficient); gravity_gradient.GetTrueDofs(gravity_gradient_true); return gravity_gradient_true; } double global_norm( const mfem::Vector &vector, MPI_Comm communicator ) { REQUIRE(vector.Size() > 0); const double local_norm_squared = vector * vector; double global_norm_squared = 0.0; MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator); return std::sqrt(global_norm_squared); } double global_relative_difference( const mfem::Vector &lhs, const mfem::Vector &rhs, MPI_Comm communicator ) { REQUIRE(lhs.Size() == rhs.Size()); mfem::Vector difference(lhs); difference -= rhs; const double difference_norm = global_norm(difference, communicator); const double lhs_norm = global_norm(lhs, communicator); const double rhs_norm = global_norm(rhs, communicator); return difference_norm / std::max({lhs_norm, rhs_norm, 1.0e-14}); } struct StatelessHDivMassReference { mfem::Vector total_action; mfem::Vector stellar_action; mfem::Vector vacuum_action; long long stellar_elements{0}; long long vacuum_elements{0}; long long stellar_quadrature_points{0}; long long vacuum_quadrature_points{0}; double minimum_stellar_determinant{std::numeric_limits::infinity()}; double maximum_stellar_determinant{0.0}; double minimum_vacuum_determinant{std::numeric_limits::infinity()}; double maximum_vacuum_determinant{0.0}; }; void reference_true_to_local( const mfem::ParFiniteElementSpace &fes, const mfem::Vector &true_vector, mfem::Vector &local_vector ) { local_vector.SetSize(fes.GetVSize()); const mfem::Operator *prolongation = fes.GetProlongationMatrix(); if (prolongation != nullptr) { prolongation->Mult(true_vector, local_vector); } else { local_vector = true_vector; } } void reference_local_to_true( const mfem::ParFiniteElementSpace &fes, const mfem::Vector &local_vector, mfem::Vector &true_vector ) { true_vector.SetSize(fes.GetTrueVSize()); true_vector = 0.0; const mfem::Operator *prolongation = fes.GetProlongationMatrix(); if (prolongation != nullptr) { prolongation->MultTranspose(local_vector, true_vector); } else { true_vector = local_vector; } } double global_vector_norm( const mfem::Vector &vector, MPI_Comm communicator ) { const double local_norm_squared = vector * vector; double global_norm_squared = 0.0; MPI_Allreduce(&local_norm_squared, &global_norm_squared, 1, MPI_DOUBLE, MPI_SUM, communicator); return std::sqrt(global_norm_squared); } double global_vector_dot( const mfem::Vector &lhs, const mfem::Vector &rhs, MPI_Comm communicator ) { const double local_dot = lhs * rhs; double global_dot = 0.0; MPI_Allreduce(&local_dot, &global_dot, 1, MPI_DOUBLE, MPI_SUM, communicator); return global_dot; } double global_relative_vector_error( const mfem::Vector &computed, const mfem::Vector &reference, MPI_Comm communicator ) { mfem::Vector difference(computed); difference -= reference; return global_vector_norm(difference, communicator) / std::max(global_vector_norm(reference, communicator), std::numeric_limits::epsilon()); } const mfem::IntegrationRule &get_stateless_hdiv_reference_rule( const fem::FEM &f, const mfem::FiniteElement &element, const mfem::ElementTransformation &transformation ) { const bool is_vacuum = transformation.Attribute == f.domainMapperStateless->GetVacuumElementAttribute(); const quadrature::Query query{ .term = quadrature::Term::gravity_hdiv_mass, .role = quadrature::QuadratureRole::discretization, .domain = utils::DOMAINS::ALL, .mapping = is_vacuum ? quadrature::MappingKind::kelvin : quadrature::MappingKind::general, .trial_order = element.GetOrder(), .test_order = element.GetOrder(), .coefficient_order = 0, .geometry_weight_order = transformation.OrderW() }; return *f.quadratureFactory->get(query, transformation.GetGeometryType()).integration_rule; } mfem::Vector make_full_support_gravity_gradient(const fem::FEM &f) { mfem::Vector gravity_gradient(f.gravityFluxFes->GetTrueVSize()); for (int i = 0; i < gravity_gradient.Size(); ++i) { const double index = static_cast(i + 1); gravity_gradient(i) = std::sin(0.37 * index) + 0.31 * std::cos(0.19 * index); } return gravity_gradient; } mfem::Vector make_stateless_reference_displacement( const fem::FEM &f, const bool deformed ) { mfem::ParGridFunction displacement(f.displacementFes.get()); displacement = 0.0; if (deformed) { auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.04 * position(0) + 0.01 * position(1) * position(2); value(1) = -0.03 * position(1) + 0.008 * position(0) * position(2); value(2) = 0.02 * position(2) - 0.006 * position(0) * position(1); }; mfem::VectorFunctionCoefficient displacement_coefficient(3, displacement_function); displacement.ProjectCoefficient(displacement_coefficient); } mfem::Vector displacement_true; displacement.GetTrueDofs(displacement_true); return displacement_true; } std::string_view mapping_status_name(const mapping::MappingStatus status) { switch (status) { case mapping::MappingStatus::valid: return "valid"; case mapping::MappingStatus::invalid_dimension: return "invalid_dimension"; case mapping::MappingStatus::non_finite_input: return "non_finite_input"; case mapping::MappingStatus::invalid_reference_radius: return "invalid_reference_radius"; case mapping::MappingStatus::at_compactified_infinity: return "at_compactified_infinity"; case mapping::MappingStatus::outside_reference_domain: return "outside_reference_domain"; case mapping::MappingStatus::non_finite_result: return "non_finite_result"; case mapping::MappingStatus::non_positive_determinant: return "non_positive_determinant"; } return "unknown"; } StatelessHDivMassReference evaluate_stateless_hdiv_mass_quadrature_reference( const fem::FEM &fem, const mfem::Vector &gravity_gradient_true, const mfem::Vector &displacement_true ) { MFEM_VERIFY(fem.domainMapperStateless != nullptr, "The stateless domain mapper is unavailable."); MFEM_VERIFY(fem.compactificationFes != nullptr, "The compactification finite-element space is unavailable."); MFEM_VERIFY(fem.compactificationCoordinate != nullptr, "The compactification coordinate is unavailable."); MFEM_VERIFY( gravity_gradient_true.Size() == fem.gravityFluxFes->GetTrueVSize(), "The gravity-gradient vector has the wrong size." ); MFEM_VERIFY( displacement_true.Size() == fem.displacementFes->GetTrueVSize(), "The displacement vector has the wrong size." ); mfem::Vector gravity_gradient_local; mfem::Vector displacement_local; reference_true_to_local(*fem.gravityFluxFes, gravity_gradient_true, gravity_gradient_local); reference_true_to_local(*fem.displacementFes, displacement_true, displacement_local); mfem::Vector total_local(fem.gravityFluxFes->GetVSize()); mfem::Vector stellar_local(fem.gravityFluxFes->GetVSize()); mfem::Vector vacuum_local(fem.gravityFluxFes->GetVSize()); total_local = 0.0; stellar_local = 0.0; vacuum_local = 0.0; StatelessHDivMassReference reference; mapping::DomainMapperStateless::Workspace workspace(fem.mesh->Dimension()); mapping::VolumeMappingContext mapping_context; mfem::Array gravity_dofs; mfem::Array displacement_dofs; mfem::Array compactification_dofs; mfem::Vector element_gravity_gradient; mfem::Vector element_displacement; mfem::Vector element_compactification; mfem::Vector element_action; mfem::Vector quadrature_flux; mfem::Vector mapped_quadrature_flux; mfem::DenseMatrix vector_shape; mfem::DenseMatrix mapped_mass_tensor; const int vacuum_attribute = fem.domainMapperStateless->GetVacuumElementAttribute(); for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { const mfem::FiniteElement &gravity_element = *fem.gravityFluxFes->GetFE(element_id); const mfem::FiniteElement &displacement_element = *fem.displacementFes->GetFE(element_id); const mfem::FiniteElement &compactification_element = *fem.compactificationFes->GetFE(element_id); mfem::ElementTransformation &transformation = *fem.mesh->GetElementTransformation(element_id); const bool is_vacuum = transformation.Attribute == vacuum_attribute; mfem::DofTransformation *gravity_dof_transformation = fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); mfem::DofTransformation *displacement_dof_transformation = fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); mfem::DofTransformation *compactification_dof_transformation = fem.compactificationFes->GetElementDofs(element_id, compactification_dofs); gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient); displacement_local.GetSubVector(displacement_dofs, element_displacement); fem.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); if (gravity_dof_transformation != nullptr) { gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient); } if (displacement_dof_transformation != nullptr) { displacement_dof_transformation->InvTransformPrimal(element_displacement); } if (compactification_dof_transformation != nullptr) { compactification_dof_transformation->InvTransformPrimal(element_compactification); } const mapping::ElementDisplacementData displacement_data = mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement); const mapping::ElementCompactificationData compactification_data( compactification_element, element_compactification ); const mapping::ElementMappingData mapping_data{ .displacement = displacement_data, .compactification = compactification_data }; const int gravity_dof_count = gravity_element.GetDof(); const int dimension = transformation.GetSpaceDim(); element_action.SetSize(gravity_dof_count); element_action = 0.0; quadrature_flux.SetSize(dimension); mapped_quadrature_flux.SetSize(dimension); vector_shape.SetSize(gravity_dof_count, dimension); mapped_mass_tensor.SetSize(dimension, dimension); const mfem::IntegrationRule &integration_rule = get_stateless_hdiv_reference_rule(fem, gravity_element, transformation); for (int quadrature_point = 0; quadrature_point < integration_rule.GetNPoints(); ++quadrature_point) { const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(quadrature_point); transformation.SetIntPoint(&integration_point); const mapping::MappingStatus status = fem.domainMapperStateless->EvaluateVolume( mapping_data, transformation, integration_point, workspace, mapping_context ); MFEM_VERIFY( status == mean_field::mapping::MappingStatus::valid, "Stateless mapping failed while evaluating the H(div) " "quadrature reference." << "\nElement ID = " << element_id << "\nElement attribute = " << transformation.Attribute << "\nQuadrature point = " << quadrature_point << "\nMapping status = " << mapping_status_name(status) ); gravity_element.CalcVShape(transformation, vector_shape); mean_field::mapping::ComputeHDivMassTensor(mapping_context.mapping, mapped_mass_tensor); // Evaluate B*x at this quadrature point. vector_shape.MultTranspose(element_gravity_gradient, quadrature_flux); // Apply the mapped H(div) mass tensor. mapped_mass_tensor.Mult(quadrature_flux, mapped_quadrature_flux); const double weight = integration_point.weight * transformation.Weight(); // Accumulate B^T*D*B*x directly. for (int dof = 0; dof < gravity_dof_count; ++dof) { double contribution = 0.0; for (int component = 0; component < dimension; ++component) { contribution += vector_shape(dof, component) * mapped_quadrature_flux(component); } element_action(dof) += weight * contribution; } const double mapping_determinant = mapping_context.mapping.mapping_determinant; MFEM_VERIFY( std::isfinite(mapping_determinant) && mapping_determinant > 0.0, "The quadrature reference encountered an invalid mapping " "determinant." ); if (is_vacuum) { reference.minimum_vacuum_determinant = std::min(reference.minimum_vacuum_determinant, mapping_determinant); reference.maximum_vacuum_determinant = std::max(reference.maximum_vacuum_determinant, mapping_determinant); ++reference.vacuum_quadrature_points; } else { reference.minimum_stellar_determinant = std::min(reference.minimum_stellar_determinant, mapping_determinant); reference.maximum_stellar_determinant = std::max(reference.maximum_stellar_determinant, mapping_determinant); ++reference.stellar_quadrature_points; } } if (gravity_dof_transformation != nullptr) { gravity_dof_transformation->TransformDual(element_action); } total_local.AddElementVector(gravity_dofs, element_action); if (is_vacuum) { vacuum_local.AddElementVector(gravity_dofs, element_action); ++reference.vacuum_elements; } else { stellar_local.AddElementVector(gravity_dofs, element_action); ++reference.stellar_elements; } } reference_local_to_true(*fem.gravityFluxFes, total_local, reference.total_action); reference_local_to_true(*fem.gravityFluxFes, stellar_local, reference.stellar_action); reference_local_to_true(*fem.gravityFluxFes, vacuum_local, reference.vacuum_action); const MPI_Comm communicator = fem.gravityFluxFes->GetComm(); const long long local_counts[4]{ reference.stellar_elements, reference.vacuum_elements, reference.stellar_quadrature_points, reference.vacuum_quadrature_points }; long long global_counts[4]{}; MPI_Allreduce(local_counts, global_counts, 4, MPI_LONG_LONG, MPI_SUM, communicator); reference.stellar_elements = global_counts[0]; reference.vacuum_elements = global_counts[1]; reference.stellar_quadrature_points = global_counts[2]; reference.vacuum_quadrature_points = global_counts[3]; const double local_minimums[2]{reference.minimum_stellar_determinant, reference.minimum_vacuum_determinant}; const double local_maximums[2]{reference.maximum_stellar_determinant, reference.maximum_vacuum_determinant}; double global_minimums[2]{}; double global_maximums[2]{}; MPI_Allreduce(local_minimums, global_minimums, 2, MPI_DOUBLE, MPI_MIN, communicator); MPI_Allreduce(local_maximums, global_maximums, 2, MPI_DOUBLE, MPI_MAX, communicator); reference.minimum_stellar_determinant = global_minimums[0]; reference.minimum_vacuum_determinant = global_minimums[1]; reference.maximum_stellar_determinant = global_maximums[0]; reference.maximum_vacuum_determinant = global_maximums[1]; return reference; } using gravity_form = blocks::gravity_field_form; using gravity_layout = blocks::form_layout; gravity_layout make_gravity_jacobian_layout(const fem::FEM &f) { return make_gravity_layout(f); } template void set_value_block( mfem::Vector &vector, const gravity_layout &layout, const blocks::value_block block, const mfem::Vector &values ) { REQUIRE(values.Size() == layout.size(block)); for (int i = 0; i < values.Size(); ++i) vector(layout.offset(block) + i) = values(i); } template mfem::Vector get_value_block( const mfem::Vector &vector, const gravity_layout &layout, const blocks::value_block block ) { mfem::Vector values(layout.size(block)); for (int i = 0; i < values.Size(); ++i) values(i) = vector(layout.offset(block) + i); return values; } template mfem::Vector get_residual_block( const mfem::Vector &vector, const gravity_layout &layout, const blocks::residual_block block ) { mfem::Vector values(layout.size(block)); for (int i = 0; i < values.Size(); ++i) values(i) = vector(layout.offset(block) + i); return values; } template void fill_value_block( mfem::Vector &vector, const gravity_layout &layout, const blocks::value_block block, const double scale, const double phase ) { for (int i = 0; i < layout.size(block); ++i) { const double index_value = static_cast(i + 1); vector(layout.offset(block) + i) = scale * (std::sin(0.31 * index_value + phase) + 0.37 * std::cos(0.17 * index_value - phase)); } } mfem::Vector make_gravity_jacobian_state( const fem::FEM &f, const gravity_layout &layout, const bool deformed ) { mfem::Vector state(layout.value_offsets().Last()); state = 0.0; fill_value_block(state, layout, density_block, 0.7, 0.11); fill_value_block(state, layout, gravity_gradient_block, 0.4, 0.23); fill_value_block(state, layout, gravity_potential_block, 0.3, 0.37); using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const auto displacement_map = field::make_field_dof_map(*f.displacementFes); const mfem::Vector displacement = displacement_map.gather(make_stateless_reference_displacement(f, deformed)); set_value_block(state, layout, displacement_block, displacement); return state; } mfem::Vector make_density_direction(const gravity_layout &layout) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; fill_value_block(direction, layout, density_block, 0.13, 0.19); return direction; } mfem::Vector make_gravity_gradient_direction(const gravity_layout &layout) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; fill_value_block(direction, layout, gravity_gradient_block, 0.11, 0.29); return direction; } mfem::Vector make_gravity_potential_direction(const gravity_layout &layout) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; fill_value_block(direction, layout, gravity_potential_block, 0.09, 0.41); return direction; } mfem::Vector make_combined_fixed_geometry_direction(const gravity_layout &layout) { mfem::Vector direction = make_density_direction(layout); const mfem::Vector gravity_gradient_direction = make_gravity_gradient_direction(layout); const mfem::Vector gravity_potential_direction = make_gravity_potential_direction(layout); direction += gravity_gradient_direction; direction += gravity_potential_direction; return direction; } mfem::Vector make_displacement_direction( const gravity_layout &layout, const mfem::Vector &displacement_direction ) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; set_value_block(direction, layout, displacement_block, displacement_direction); return direction; } mfem::Vector evaluate_centered_difference( operators::GravityFieldOperator &gravity_operator, const mfem::Vector &state, const mfem::Vector &direction, const double step ) { mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(step, direction); minus_state.Add(-step, direction); mfem::Vector plus_residual; mfem::Vector minus_residual; gravity_operator.Mult(plus_state, plus_residual); gravity_operator.Mult(minus_state, minus_residual); mfem::Vector difference(plus_residual); difference -= minus_residual; difference /= 2.0 * step; return difference; } struct HdivMassVariationTestFields { mfem::Vector gravity_gradient; mfem::Vector displacement; mfem::Vector displacement_direction_1; mfem::Vector displacement_direction_2; }; double global_relative_error( const mfem::Vector &computed, const mfem::Vector &reference, MPI_Comm communicator ) { REQUIRE(computed.Size() == reference.Size()); mfem::Vector difference(computed); difference -= reference; return global_norm(difference, communicator) / std::max(global_norm(reference, communicator), 1.0e-14); } HdivMassVariationTestFields make_hdiv_mass_variation_test_fields(const fem::FEM &f) { const int dimension = f.mesh->Dimension(); auto gravity_gradient_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.4 + 0.18 * position(0) - 0.07 * position(1) * position(2); value(1) = -0.3 + 0.11 * position(1) + 0.05 * position(0) * position(2); value(2) = 0.2 - 0.09 * position(2) + 0.04 * position(0) * position(1); }; auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.025 * position(0) + 0.006 * position(1) * position(2); value(1) = -0.018 * position(1) + 0.005 * position(0) * position(2); value(2) = 0.014 * position(2) + 0.004 * position(0) * position(1); }; auto displacement_direction_1_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.16 * position(0) + 0.03 * position(1); value(1) = -0.11 * position(1) + 0.02 * position(2); value(2) = 0.13 * position(2) - 0.025 * position(0); }; auto displacement_direction_2_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = -0.07 * position(1) + 0.025 * position(2); value(1) = 0.09 * position(0) + 0.04 * position(2); value(2) = -0.08 * position(2) + 0.03 * position(0) * position(1); }; mfem::VectorFunctionCoefficient gravity_gradient_coefficient(dimension, gravity_gradient_function); mfem::VectorFunctionCoefficient displacement_coefficient(dimension, displacement_function); mfem::VectorFunctionCoefficient displacement_direction_1_coefficient( dimension, displacement_direction_1_function ); mfem::VectorFunctionCoefficient displacement_direction_2_coefficient( dimension, displacement_direction_2_function ); mfem::ParGridFunction gravity_gradient_grid(f.gravityFluxFes.get()); mfem::ParGridFunction displacement_grid(f.displacementFes.get()); mfem::ParGridFunction displacement_direction_1_grid(f.displacementFes.get()); mfem::ParGridFunction displacement_direction_2_grid(f.displacementFes.get()); gravity_gradient_grid.ProjectCoefficient(gravity_gradient_coefficient); displacement_grid.ProjectCoefficient(displacement_coefficient); displacement_direction_1_grid.ProjectCoefficient(displacement_direction_1_coefficient); displacement_direction_2_grid.ProjectCoefficient(displacement_direction_2_coefficient); HdivMassVariationTestFields fields; gravity_gradient_grid.GetTrueDofs(fields.gravity_gradient); displacement_grid.GetTrueDofs(fields.displacement); displacement_direction_1_grid.GetTrueDofs(fields.displacement_direction_1); displacement_direction_2_grid.GetTrueDofs(fields.displacement_direction_2); return fields; } mfem::Vector centered_hdiv_mass_geometry_difference( const fem::FEM &f, const mapping::DomainMapperStateless &domain_mapper, const mfem::Vector &gravity_gradient, const mfem::Vector &displacement, const mfem::Vector &displacement_direction, const double difference_step ) { mfem::Vector plus_displacement(displacement); mfem::Vector minus_displacement(displacement); plus_displacement.Add(difference_step, displacement_direction); minus_displacement.Add(-difference_step, displacement_direction); mfem::Vector plus_action; mfem::Vector minus_action; operators::kernels::apply_mapped_hdiv_mass(f, domain_mapper, gravity_gradient, plus_displacement, plus_action); operators::kernels::apply_mapped_hdiv_mass( f, domain_mapper, gravity_gradient, minus_displacement, minus_action ); plus_action -= minus_action; plus_action /= 2.0 * difference_step; return plus_action; } mfem::Vector make_source_variation_density(const fem::FEM &f) { auto density_function = [](const mfem::Vector &position) { return 1.2 + 0.16 * position(0) - 0.09 * position(1) + 0.07 * position(2) + 0.04 * position(0) * position(1); }; mfem::FunctionCoefficient density_coefficient(density_function); mfem::ParGridFunction density_grid(f.densityFes.get()); mfem::Vector density_true; density_grid.ProjectCoefficient(density_coefficient); density_grid.GetTrueDofs(density_true); return density_true; } mfem::Vector centered_source_geometry_difference( const fem::FEM &f, const mapping::DomainMapperStateless &domain_mapper, const mfem::Vector &density, const mfem::Vector &displacement, const mfem::Vector &displacement_direction, const double difference_step ) { mfem::Vector plus_displacement(displacement); mfem::Vector minus_displacement(displacement); plus_displacement.Add(difference_step, displacement_direction); minus_displacement.Add(-difference_step, displacement_direction); mfem::Vector plus_action; mfem::Vector minus_action; operators::kernels::apply_mapped_source(f, domain_mapper, density, plus_displacement, plus_action); operators::kernels::apply_mapped_source(f, domain_mapper, density, minus_displacement, minus_action); plus_action -= minus_action; plus_action /= 2.0 * difference_step; return plus_action; } double global_dot( const mfem::Vector &left, const mfem::Vector &right, MPI_Comm communicator ) { REQUIRE(left.Size() > 0); REQUIRE(left.Size() == right.Size()); const double local_dot = left * right; double global_dot_value = 0.0; MPI_Allreduce(&local_dot, &global_dot_value, 1, MPI_DOUBLE, MPI_SUM, communicator); return global_dot_value; } mfem::Vector make_secondary_gravity_gradient(const fem::FEM &f) { auto gravity_gradient_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = -0.17 + 0.09 * position(1) + 0.03 * position(0) * position(2); value(1) = 0.31 - 0.14 * position(0) + 0.05 * position(1) * position(2); value(2) = -0.22 + 0.12 * position(2) - 0.04 * position(0) * position(1); }; mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), gravity_gradient_function); mfem::ParGridFunction grid_function(f.gravityFluxFes.get()); mfem::Vector true_dofs; grid_function.ProjectCoefficient(coefficient); grid_function.GetTrueDofs(true_dofs); return true_dofs; } mfem::Vector make_secondary_source_density(const fem::FEM &f) { auto density_function = [](const mfem::Vector &position) { return 0.8 - 0.11 * position(0) + 0.13 * position(1) - 0.06 * position(2) + 0.03 * position(1) * position(2); }; mfem::FunctionCoefficient coefficient(density_function); mfem::ParGridFunction grid_function(f.densityFes.get()); mfem::Vector true_dofs; grid_function.ProjectCoefficient(coefficient); grid_function.GetTrueDofs(true_dofs); return true_dofs; } mfem::Vector make_vacuum_only_density( const fem::FEM &f, const int vacuum_attribute ) { mfem::ParGridFunction grid_function(f.densityFes.get()); grid_function = 0.0; mfem::Array element_dofs; mfem::Vector element_values; for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); REQUIRE(transformation != nullptr); if (transformation->Attribute != vacuum_attribute) continue; const mfem::FiniteElement &element = *f.densityFes->GetFE(element_id); mfem::DofTransformation *dof_transformation = f.densityFes->GetElementDofs(element_id, element_dofs); element_values.SetSize(element.GetDof()); for (int i = 0; i < element_values.Size(); ++i) element_values(i) = 0.9 + 0.01 * static_cast(i); if (dof_transformation != nullptr) dof_transformation->TransformPrimal(element_values); grid_function.SetSubVector(element_dofs, element_values); } mfem::Vector true_dofs; grid_function.GetTrueDofs(true_dofs); return true_dofs; } template mfem::Vector make_read_only_value_view( const mfem::Vector &vector, const mfem::Array &offsets, const blocks::value_block ) { const int offset = offsets[index]; const int size = offsets[index + 1] - offset; return mfem::Vector(const_cast(vector.GetData()) + offset, size); } void check_linearization_context_matches_state( const operators::context::gravity_field::GravityFieldLinearizationContext &context, const mfem::Vector &state, const mfem::Array &state_offsets, MPI_Comm communicator ) { using form = blocks::gravity_field_form; constexpr auto density_block = utils::blocks::get_value_block(blocks::density_field.mass_term); constexpr auto displacement_block = utils::blocks::get_value_block(blocks::displacement_field.geometry_term); constexpr auto gravity_gradient_block = utils::blocks::get_value_block(blocks::gravity_field.gradient_term); const mfem::Vector density = make_read_only_value_view(state, state_offsets, density_block); const mfem::Vector displacement = make_read_only_value_view(state, state_offsets, displacement_block); const mfem::Vector gravity_gradient = make_read_only_value_view(state, state_offsets, gravity_gradient_block); CHECK_THAT( global_relative_vector_error( context.GetDensityTrue(), context.GetDensityMap().scatter(density), communicator ), Catch::Matchers::WithinAbs(0.0, 0.0) ); CHECK_THAT( global_relative_vector_error( context.GetGeometryContext().GetDisplacementTrue(), context.GetDisplacementMap().scatter(displacement), communicator ), Catch::Matchers::WithinAbs(0.0, 0.0) ); CHECK_THAT( global_relative_vector_error( context.GetGravityGradientTrue(), context.GetGravityGradientMap().scatter(gravity_gradient), communicator ), Catch::Matchers::WithinAbs(0.0, 0.0) ); } StatelessHDivMassReference assemble_legacy_hdiv_mass_reference( const fem::FEM &f, const mfem::Vector &gravity_gradient_true ) { MFEM_VERIFY(f.mesh != nullptr, "The legacy H(div) reference requires a mesh."); MFEM_VERIFY(f.gravityFluxFes != nullptr, "The legacy H(div) reference requires the RT finite-element space."); MFEM_VERIFY(f.mapping != nullptr, "The legacy H(div) reference requires the legacy domain mapper."); MFEM_VERIFY(f.domainMapperStateless != nullptr, "The vacuum attribute is unavailable."); mfem::Vector gravity_gradient_local; reference_true_to_local(*f.gravityFluxFes, gravity_gradient_true, gravity_gradient_local); mfem::Vector total_local(f.gravityFluxFes->GetVSize()); mfem::Vector stellar_local(f.gravityFluxFes->GetVSize()); mfem::Vector vacuum_local(f.gravityFluxFes->GetVSize()); total_local = 0.0; stellar_local = 0.0; vacuum_local = 0.0; StatelessHDivMassReference reference; const int vacuum_attribute = f.domainMapperStateless->GetVacuumElementAttribute(); for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { const mfem::FiniteElement &gravity_element = *f.gravityFluxFes->GetFE(element_id); mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); MFEM_VERIFY( transformation != nullptr, "The legacy H(div) reference received a null element " "transformation." ); const bool is_vacuum = transformation->Attribute == vacuum_attribute; mfem::Array gravity_dofs; f.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); mfem::Vector element_gravity_gradient; gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient); const int gravity_dof_count = gravity_element.GetDof(); const int dimension = transformation->GetSpaceDim(); mfem::DenseMatrix element_matrix(gravity_dof_count); mfem::DenseMatrix vector_shape(gravity_dof_count, dimension); mfem::DenseMatrix mapping_jacobian(dimension); mfem::DenseMatrix mapped_mass_tensor(dimension); element_matrix = 0.0; const mfem::IntegrationRule &integration_rule = get_stateless_hdiv_reference_rule(f, gravity_element, *transformation); for (int q = 0; q < integration_rule.GetNPoints(); ++q) { const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(q); transformation->SetIntPoint(&integration_point); /* * This reproduces the legacy MappedHDivMassCoefficient. * * In particular, it does not evaluate or otherwise use * f.compactificationCoordinate. */ f.mapping->ComputeJacobian(*transformation, mapping_jacobian); const double mapping_determinant = mapping_jacobian.Det(); CAPTURE(element_id, q, transformation->Attribute); REQUIRE(std::isfinite(mapping_determinant)); REQUIRE(mapping_determinant > 0.0); mfem::MultAtB(mapping_jacobian, mapping_jacobian, mapped_mass_tensor); mapped_mass_tensor *= 1.0 / std::abs(mapping_determinant); gravity_element.CalcVShape(*transformation, vector_shape); const double weight = integration_point.weight * transformation->Weight(); for (int i = 0; i < gravity_dof_count; ++i) { for (int j = 0; j < gravity_dof_count; ++j) { double entry = 0.0; for (int row = 0; row < dimension; ++row) { for (int column = 0; column < dimension; ++column) { entry += vector_shape(i, row) * mapped_mass_tensor(row, column) * vector_shape(j, column); } } element_matrix(i, j) += weight * entry; } } if (is_vacuum) { ++reference.vacuum_quadrature_points; } else { ++reference.stellar_quadrature_points; } } mfem::Vector element_action(gravity_dof_count); element_matrix.Mult(element_gravity_gradient, element_action); total_local.AddElementVector(gravity_dofs, element_action); if (is_vacuum) { vacuum_local.AddElementVector(gravity_dofs, element_action); ++reference.vacuum_elements; } else { stellar_local.AddElementVector(gravity_dofs, element_action); ++reference.stellar_elements; } } reference_local_to_true(*f.gravityFluxFes, total_local, reference.total_action); reference_local_to_true(*f.gravityFluxFes, stellar_local, reference.stellar_action); reference_local_to_true(*f.gravityFluxFes, vacuum_local, reference.vacuum_action); MPI_Comm communicator = f.gravityFluxFes->GetComm(); const long long local_counts[4]{ reference.stellar_elements, reference.vacuum_elements, reference.stellar_quadrature_points, reference.vacuum_quadrature_points }; long long global_counts[4]{}; MPI_Allreduce(local_counts, global_counts, 4, MPI_LONG_LONG, MPI_SUM, communicator); reference.stellar_elements = global_counts[0]; reference.vacuum_elements = global_counts[1]; reference.stellar_quadrature_points = global_counts[2]; reference.vacuum_quadrature_points = global_counts[3]; return reference; } } // namespace TEST_CASE( "Gravity Field Operator Mult Preserves Zero State And Static Blocks", tags::gravity_operator_unit ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); mfem::Vector state(layout.value_offsets().Last()); mfem::Vector residual; state = 0.0; constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, revisions); gravity_operator.Mult(state, residual); REQUIRE(residual.Size() == layout.residual_offsets().Last()); CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-14)); const mfem::Vector gravity_potential = make_test_vector(layout.size(gravity_potential_block), 0.31); set_block(state, layout.value_offsets(), gravity_potential_block, gravity_potential); gravity_operator.Mult(state, residual); const mfem::Vector gradient_residual = get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block); const mfem::Vector poisson_residual = get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block); mfem::Vector expected_gradient_residual(layout.size(gravity_gradient_residual_block)); f.gravityContext.BT->Mult(gravity_potential, expected_gradient_residual); CHECK_THAT(relative_difference(gradient_residual, expected_gradient_residual), WithinAbs(0.0, 1.0e-13)); CHECK_THAT(poisson_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); state = 0.0; const mfem::Vector gravity_gradient = make_test_vector(layout.size(gravity_gradient_block), 0.73); set_block(state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); gravity_operator.Mult(state, residual); const mfem::Vector gradient_only_residual = get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block); const mfem::Vector poisson_only_residual = get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block); mfem::Vector expected_poisson_residual(layout.size(gravity_poisson_residual_block)); f.gravityContext.b_form->Mult(gravity_gradient, expected_poisson_residual); CHECK(gradient_only_residual.Norml2() > 0.0); CHECK_THAT(relative_difference(poisson_only_residual, expected_poisson_residual), WithinAbs(0.0, 1.0e-13)); const double adjoint_lhs = gravity_gradient * expected_gradient_residual; const double adjoint_rhs = gravity_potential * expected_poisson_residual; const double adjoint_scale = std::max({std::abs(adjoint_lhs), std::abs(adjoint_rhs), 1.0e-14}); CHECK_THAT(std::abs(adjoint_lhs - adjoint_rhs) / adjoint_scale, WithinAbs(0.0, 1.0e-12)); } TEST_CASE( "Gravity Field Operator Mult Applies Stellar Source With Correct Sign", tags::gravity_operator_unit ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); mfem::Vector state(layout.value_offsets().Last()); mfem::Vector residual; state = 0.0; constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; const mfem::Vector density = linearization_context.GetDensityMap().gather(make_constant_density(f, 1.0)); set_block(state, layout.value_offsets(), density_block, density); gravity_operator.Prepare(state, revisions); gravity_operator.Mult(state, residual); const mfem::Vector gradient_residual = get_block(residual, layout.residual_offsets(), gravity_gradient_residual_block); const mfem::Vector poisson_residual = get_block(residual, layout.residual_offsets(), gravity_poisson_residual_block); CHECK_THAT(gradient_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); CHECK(poisson_residual.Norml2() > 0.0); const mfem::Vector constant_test = make_constant_density(f, 1.0); const double integrated_source_residual = constant_test * poisson_residual; INFO("Integrated Poisson source residual = " << integrated_source_residual); CHECK(integrated_source_residual < 0.0); mfem::Vector doubled_state(state); mfem::Vector doubled_density(density); doubled_density *= 2.0; set_block(doubled_state, layout.value_offsets(), density_block, doubled_density); mfem::Vector doubled_residual; gravity_operator.Mult(doubled_state, doubled_residual); mfem::Vector expected_doubled_residual(residual); expected_doubled_residual *= 2.0; CHECK_THAT(relative_difference(doubled_residual, expected_doubled_residual), WithinAbs(0.0, 1.0e-12)); } TEST_CASE( "Gravity Field Operator Mult Ignores Vacuum Density", tags::gravity_operator_unit ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); mfem::Vector state(layout.value_offsets().Last()); mfem::Vector residual; state = 0.0; const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; const mfem::Vector vacuum_density_true = make_vacuum_density(f, 1.0); REQUIRE(vacuum_density_true.Norml2() > 0.0); const mfem::Vector vacuum_density = linearization_context.GetDensityMap().gather(vacuum_density_true); set_block(state, layout.value_offsets(), density_block, vacuum_density); gravity_operator.Prepare(state, revisions); gravity_operator.Mult(state, residual); INFO("Vacuum-source residual norm = " << residual.Norml2()); CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-13)); } TEST_CASE( "Gravity Field Operator Mult Produces A Symmetric Positive Hdiv Mass " "Action", tags::gravity_operator_unit ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(make_displacement(f)); const mfem::Vector gravity_gradient_a = make_test_vector(layout.size(gravity_gradient_block), 0.27); const mfem::Vector gravity_gradient_b = make_test_vector(layout.size(gravity_gradient_block), 1.13); mfem::Vector state_a(layout.value_offsets().Last()); mfem::Vector state_b(layout.value_offsets().Last()); mfem::Vector residual_a; mfem::Vector residual_b; state_a = 0.0; state_b = 0.0; set_block(state_a, layout.value_offsets(), displacement_block, displacement); set_block(state_a, layout.value_offsets(), gravity_gradient_block, gravity_gradient_a); set_block(state_b, layout.value_offsets(), displacement_block, displacement); set_block(state_b, layout.value_offsets(), gravity_gradient_block, gravity_gradient_b); const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state_a, revisions); gravity_operator.Mult(state_a, residual_a); gravity_operator.Mult(state_b, residual_b); const mfem::Vector mass_action_a = get_block(residual_a, layout.residual_offsets(), gravity_gradient_residual_block); const mfem::Vector mass_action_b = get_block(residual_b, layout.residual_offsets(), gravity_gradient_residual_block); const double energy_a = gravity_gradient_a * mass_action_a; const double energy_b = gravity_gradient_b * mass_action_b; const double cross_ab = gravity_gradient_a * mass_action_b; const double cross_ba = gravity_gradient_b * mass_action_a; const double symmetry_scale = std::max({std::abs(cross_ab), std::abs(cross_ba), 1.0e-14}); INFO("Mapped H(div) energy A = " << energy_a); INFO("Mapped H(div) energy B = " << energy_b); INFO("Mapped H(div) cross action A-M-B = " << cross_ab); INFO("Mapped H(div) cross action B-M-A = " << cross_ba); CHECK(std::isfinite(energy_a)); CHECK(std::isfinite(energy_b)); CHECK(energy_a > 0.0); CHECK(energy_b > 0.0); CHECK_THAT(std::abs(cross_ab - cross_ba) / symmetry_scale, WithinAbs(0.0, 1.0e-11)); } TEST_CASE( "Gravity Field Operator Mult Is Additive In Fields At Fixed Displacement", tags::gravity_operator_unit ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(make_displacement(f)); const mfem::Vector density = linearization_context.GetDensityMap().gather(make_constant_density(f, 0.73)); const mfem::Vector gravity_gradient = make_test_vector(layout.size(gravity_gradient_block), 0.41); const mfem::Vector gravity_potential = make_test_vector(layout.size(gravity_potential_block), 0.89); mfem::Vector displacement_state(layout.value_offsets().Last()); mfem::Vector density_state(layout.value_offsets().Last()); mfem::Vector gradient_state(layout.value_offsets().Last()); mfem::Vector potential_state(layout.value_offsets().Last()); mfem::Vector complete_state(layout.value_offsets().Last()); displacement_state = 0.0; density_state = 0.0; gradient_state = 0.0; potential_state = 0.0; complete_state = 0.0; set_block(displacement_state, layout.value_offsets(), displacement_block, displacement); set_block(density_state, layout.value_offsets(), displacement_block, displacement); set_block(density_state, layout.value_offsets(), density_block, density); set_block(gradient_state, layout.value_offsets(), displacement_block, displacement); set_block(gradient_state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); set_block(potential_state, layout.value_offsets(), displacement_block, displacement); set_block(potential_state, layout.value_offsets(), gravity_potential_block, gravity_potential); set_block(complete_state, layout.value_offsets(), displacement_block, displacement); set_block(complete_state, layout.value_offsets(), density_block, density); set_block(complete_state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); set_block(complete_state, layout.value_offsets(), gravity_potential_block, gravity_potential); mfem::Vector displacement_residual; mfem::Vector density_residual; mfem::Vector gradient_residual; mfem::Vector potential_residual; mfem::Vector complete_residual; const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(complete_state, revisions); gravity_operator.Mult(displacement_state, displacement_residual); gravity_operator.Mult(density_state, density_residual); gravity_operator.Mult(gradient_state, gradient_residual); gravity_operator.Mult(potential_state, potential_residual); gravity_operator.Mult(complete_state, complete_residual); CHECK_THAT(displacement_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); mfem::Vector additive_residual(density_residual); additive_residual += gradient_residual; additive_residual += potential_residual; CHECK_THAT(relative_difference(complete_residual, additive_residual), WithinAbs(0.0, 1.0e-11)); } TEST_CASE( "Gravity Field Operator Stellar Hdiv Mass Matches Legacy Assembled Mass", tags::gravity_legacy ) { constexpr double parity_tolerance = 1.0e-10; constexpr double vacuum_tolerance = 1.0e-13; auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.mapping != nullptr); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); const mfem::Vector gravity_gradient = make_core_supported_gravity_gradient(f); const mfem::Vector deformed_displacement = make_displacement(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); REQUIRE(global_norm(gravity_gradient, communicator) > 0.0); mfem::ParGridFunction local_gravity_gradient(f.gravityFluxFes.get()); local_gravity_gradient.SetFromTrueDofs(gravity_gradient); double local_maximum_vacuum_dof = 0.0; mfem::Array element_vdofs; mfem::Vector element_values; for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { if (f.mesh->GetAttribute(element_id) != f.domainMapperStateless->GetVacuumElementAttribute()) continue; f.gravityFluxFes->GetElementVDofs(element_id, element_vdofs); local_gravity_gradient.GetSubVector(element_vdofs, element_values); if (element_values.Size() > 0) local_maximum_vacuum_dof = std::max(local_maximum_vacuum_dof, element_values.Normlinf()); } double global_maximum_vacuum_dof = 0.0; MPI_Allreduce(&local_maximum_vacuum_dof, &global_maximum_vacuum_dof, 1, MPI_DOUBLE, MPI_MAX, communicator); INFO("Maximum gravity-gradient DOF on vacuum elements = " << global_maximum_vacuum_dof); REQUIRE_THAT(global_maximum_vacuum_dof, WithinAbs(0.0, vacuum_tolerance)); for (const bool use_deformation : std::array{false, true}) { DYNAMIC_SECTION("Geometry = " << (use_deformation ? "deformed" : "identity")) { mfem::Vector displacement(layout.size(displacement_block)); displacement = 0.0; if (use_deformation) displacement = deformed_displacement; mfem::ParGridFunction legacy_displacement(f.displacementFes.get()); legacy_displacement.SetFromTrueDofs(displacement); f.mapping->SetDisplacement(legacy_displacement); physics::update_stiffness_matrix(f); REQUIRE(f.gravityContext.m_form != nullptr); REQUIRE(f.gravityContext.b_form != nullptr); REQUIRE(f.gravityContext.BT != nullptr); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); mfem::Vector state(layout.value_offsets().Last()); mfem::Vector matrix_free_residual; state = 0.0; set_block(state, layout.value_offsets(), displacement_block, displacement); set_block(state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, revisions); gravity_operator.Mult(state, matrix_free_residual); const mfem::Vector matrix_free_mass_action = get_block(matrix_free_residual, layout.residual_offsets(), gravity_gradient_residual_block); mfem::Vector legacy_mass_action(f.gravityFluxFes->GetTrueVSize()); f.gravityContext.m_form->Mult(gravity_gradient, legacy_mass_action); const double matrix_free_mass_norm = global_norm(matrix_free_mass_action, communicator); const double legacy_mass_norm = global_norm(legacy_mass_action, communicator); const double mass_error = global_relative_difference(matrix_free_mass_action, legacy_mass_action, communicator); mfem::Vector mass_difference(matrix_free_mass_action); mass_difference -= legacy_mass_action; INFO("Geometry = " << (use_deformation ? "deformed" : "identity")); INFO("Matrix-free stellar mass norm = " << matrix_free_mass_norm); INFO("Legacy stellar mass norm = " << legacy_mass_norm); INFO("Absolute stellar mass difference norm = " << global_norm(mass_difference, communicator)); INFO("Relative stellar mass-action error = " << mass_error); REQUIRE(matrix_free_mass_norm > 0.0); REQUIRE(legacy_mass_norm > 0.0); CHECK_THAT(mass_error, WithinAbs(0.0, parity_tolerance)); } } } TEST_CASE( "Gravity Field Operator Hdiv Mass Action Matches Stateless Quadrature " "Reference", tags::gravity_operator_integration ) { const utils::Args args = test_utils::setup_args(); fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); fem.mapping->ResetDisplacement(); physics::update_stiffness_matrix(fem); constexpr auto displacement_block = mean_field::utils::blocks::get_value_block( blocks::displacement_field.geometry_term ); constexpr auto gravity_gradient_block = mean_field::utils::blocks::get_value_block(blocks::gravity_field.gradient_term); constexpr auto gravity_gradient_residual_block = mean_field::utils::blocks::get_residual_block(blocks::gravity_field.gradient_term); const blocks::form_layout layout = make_gravity_layout(fem); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( fem, *fem.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); const MPI_Comm communicator = fem.gravityFluxFes->GetComm(); const mfem::Vector gravity_gradient_true = make_full_support_gravity_gradient(fem); for (const bool deformed : {false, true}) { DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { CAPTURE(deformed); const mfem::Vector displacement_true = make_stateless_reference_displacement(fem, deformed); mfem::Vector state(layout.value_offsets().Last()); state = 0.0; for (int i = 0; i < gravity_gradient_true.Size(); ++i) { state(layout.offset(gravity_gradient_block) + i) = gravity_gradient_true(i); } for (int i = 0; i < displacement_true.Size(); ++i) { state(layout.offset(displacement_block) + i) = displacement_true(i); } const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, revisions); mfem::Vector residual; gravity_operator.Mult(state, residual); mfem::Vector operator_action(layout.size(gravity_gradient_residual_block)); for (int i = 0; i < operator_action.Size(); ++i) { operator_action(i) = residual(layout.offset(gravity_gradient_residual_block) + i); } const StatelessHDivMassReference reference = evaluate_stateless_hdiv_mass_quadrature_reference(fem, gravity_gradient_true, displacement_true); mfem::Vector decomposed_reference(reference.stellar_action); decomposed_reference += reference.vacuum_action; mfem::Vector action_difference(operator_action); action_difference -= reference.total_action; const double operator_norm = global_vector_norm(operator_action, communicator); const double reference_norm = global_vector_norm(reference.total_action, communicator); const double stellar_action_norm = global_vector_norm(reference.stellar_action, communicator); const double vacuum_action_norm = global_vector_norm(reference.vacuum_action, communicator); const double absolute_action_error = global_vector_norm(action_difference, communicator); const double relative_action_error = global_relative_vector_error(operator_action, reference.total_action, communicator); const double decomposition_error = global_relative_vector_error(reference.total_action, decomposed_reference, communicator); const double operator_energy = global_vector_dot(gravity_gradient_true, operator_action, communicator); const double reference_energy = global_vector_dot(gravity_gradient_true, reference.total_action, communicator); const double stellar_energy = global_vector_dot(gravity_gradient_true, reference.stellar_action, communicator); const double vacuum_energy = global_vector_dot(gravity_gradient_true, reference.vacuum_action, communicator); const double relative_energy_error = std::abs(operator_energy - reference_energy) / std::max(std::abs(reference_energy), std::numeric_limits::epsilon()); const double vacuum_action_fraction = vacuum_action_norm / std::max(reference_norm, std::numeric_limits::epsilon()); const double vacuum_energy_fraction = vacuum_energy / reference_energy; const double stellar_vacuum_dot = global_vector_dot(reference.stellar_action, reference.vacuum_action, communicator); const double stellar_vacuum_alignment = stellar_vacuum_dot / std::max(stellar_action_norm * vacuum_action_norm, std::numeric_limits::epsilon()); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("Stellar elements = " << reference.stellar_elements); INFO("Vacuum elements = " << reference.vacuum_elements); INFO("Stellar quadrature points = " << reference.stellar_quadrature_points); INFO("Vacuum quadrature points = " << reference.vacuum_quadrature_points); INFO( "Stellar mapping determinant range = [" << reference.minimum_stellar_determinant << ", " << reference.maximum_stellar_determinant << "]" ); INFO( "Vacuum mapping determinant range = [" << reference.minimum_vacuum_determinant << ", " << reference.maximum_vacuum_determinant << "]" ); INFO("Operator mass-action norm = " << operator_norm); INFO("Stateless quadrature-reference norm = " << reference_norm); INFO("Stellar action norm = " << stellar_action_norm); INFO("Vacuum action norm = " << vacuum_action_norm); INFO("Vacuum action fraction = " << vacuum_action_fraction); INFO("Stellar-vacuum action alignment = " << stellar_vacuum_alignment); INFO("Absolute action error = " << absolute_action_error); INFO("Relative action error = " << relative_action_error); INFO("Domain-decomposition error = " << decomposition_error); INFO("Operator quadratic energy = " << operator_energy); INFO("Quadrature-reference energy = " << reference_energy); INFO("Stellar energy = " << stellar_energy); INFO("Vacuum energy = " << vacuum_energy); INFO("Vacuum energy fraction = " << vacuum_energy_fraction); INFO("Relative energy error = " << relative_energy_error); REQUIRE(reference.stellar_elements > 0); REQUIRE(reference.vacuum_elements > 0); REQUIRE(reference.stellar_quadrature_points > 0); REQUIRE(reference.vacuum_quadrature_points > 0); REQUIRE(reference.minimum_stellar_determinant > 0.0); REQUIRE(reference.minimum_vacuum_determinant > 0.0); REQUIRE(reference_energy > 0.0); REQUIRE(stellar_energy > 0.0); REQUIRE(vacuum_energy > 0.0); constexpr double decomposition_tolerance = 5.0e-14; constexpr double action_tolerance = 1.0e-11; constexpr double energy_tolerance = 1.0e-11; CHECK_THAT(decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); CHECK_THAT(relative_action_error, Catch::Matchers::WithinAbs(0.0, action_tolerance)); CHECK_THAT(relative_energy_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); } } } TEST_CASE( "Gravity Field Jacobian Fixed Geometry Blocks Match Centered Differences", tags::gravity_operator_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); f.mapping->ResetDisplacement(); physics::update_stiffness_matrix(f); const gravity_layout layout = make_gravity_jacobian_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); mfem::Vector state(layout.value_offsets().Last()); state = 0.0; const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(make_stateless_reference_displacement(f, true)); set_value_block(state, layout, displacement_block, displacement); const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, revisions); mfem::Operator &gradient = gravity_operator.GetGradient(state); MPI_Comm communicator = f.mesh->GetComm(); struct DirectionCase { std::string name; mfem::Vector direction; bool expect_gradient_residual; bool expect_poisson_residual; }; std::vector cases; cases.push_back({"density", make_density_direction(layout), false, true}); cases.push_back({"gravity gradient", make_gravity_gradient_direction(layout), true, true}); cases.push_back({"gravity potential", make_gravity_potential_direction(layout), true, false}); for (const DirectionCase &direction_case : cases) { DYNAMIC_SECTION("Direction = " << direction_case.name) { constexpr double difference_step = 1.0e-6; mfem::Vector jacobian_action; gradient.Mult(direction_case.direction, jacobian_action); const mfem::Vector finite_difference = evaluate_centered_difference(gravity_operator, state, direction_case.direction, difference_step); const mfem::Vector gradient_action = get_residual_block(jacobian_action, layout, gravity_gradient_residual_block); const mfem::Vector poisson_action = get_residual_block(jacobian_action, layout, gravity_poisson_residual_block); const double relative_error = global_relative_vector_error(jacobian_action, finite_difference, communicator); const double gradient_norm = global_vector_norm(gradient_action, communicator); const double poisson_norm = global_vector_norm(poisson_action, communicator); INFO("Direction = " << direction_case.name); INFO("Jacobian action norm = " << global_vector_norm(jacobian_action, communicator)); INFO("Finite-difference action norm = " << global_vector_norm(finite_difference, communicator)); INFO("Gradient residual action norm = " << gradient_norm); INFO("Poisson residual action norm = " << poisson_norm); INFO("Relative Jacobian error = " << relative_error); constexpr double jacobian_tolerance = 2.0e-8; constexpr double zero_tolerance = 1.0e-13; CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, jacobian_tolerance)); if (direction_case.expect_gradient_residual) { CHECK(gradient_norm > zero_tolerance); } else { CHECK_THAT(gradient_norm, Catch::Matchers::WithinAbs(0.0, zero_tolerance)); } if (direction_case.expect_poisson_residual) { CHECK(poisson_norm > zero_tolerance); } else { CHECK_THAT(poisson_norm, Catch::Matchers::WithinAbs(0.0, zero_tolerance)); } } } } TEST_CASE( "Gravity Field Jacobian Combined Fixed Geometry Direction Matches Centered " "Differences", tags::gravity_operator_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); f.mapping->ResetDisplacement(); physics::update_stiffness_matrix(f); const gravity_layout layout = make_gravity_jacobian_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); const mfem::Vector direction = make_combined_fixed_geometry_direction(layout); operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, revisions); mfem::Operator &gradient = gravity_operator.GetGradient(state); mfem::Vector jacobian_action; gradient.Mult(direction, jacobian_action); MPI_Comm communicator = f.mesh->GetComm(); double best_error = std::numeric_limits::infinity(); for (const double step : std::array{1.0e-4, 1.0e-6, 1.0e-7}) { const mfem::Vector finite_difference = evaluate_centered_difference(gravity_operator, state, direction, step); const double relative_error = global_relative_vector_error(jacobian_action, finite_difference, communicator); best_error = std::min(best_error, relative_error); INFO("Centered-difference step = " << step); INFO("Relative Jacobian error = " << relative_error); CHECK(relative_error < 2.0e-7); } INFO("Best relative Jacobian error = " << best_error); CHECK(best_error < 2.0e-8); } TEST_CASE( "Gravity Field Jacobian Action Is Linear At Fixed Geometry", tags::gravity_operator_unit ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); f.mapping->ResetDisplacement(); physics::update_stiffness_matrix(f); const gravity_layout layout = make_gravity_jacobian_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); const mfem::Vector state = make_gravity_jacobian_state(f, layout, false); const mfem::Vector direction_a = make_density_direction(layout); const mfem::Vector direction_b = make_gravity_gradient_direction(layout); const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, revisions); mfem::Operator &gradient = gravity_operator.GetGradient(state); constexpr double scale_a = 1.7; constexpr double scale_b = -0.6; mfem::Vector combined_direction(direction_a); combined_direction *= scale_a; combined_direction.Add(scale_b, direction_b); mfem::Vector action_a; mfem::Vector action_b; mfem::Vector combined_action; gradient.Mult(direction_a, action_a); gradient.Mult(direction_b, action_b); gradient.Mult(combined_direction, combined_action); mfem::Vector expected_action(action_a); expected_action *= scale_a; expected_action.Add(scale_b, action_b); MPI_Comm communicator = f.mesh->GetComm(); const double relative_error = global_relative_vector_error(combined_action, expected_action, communicator); INFO("Relative Jacobian linearity error = " << relative_error); CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, 2.0e-13)); } TEST_CASE( "Gravity Field Prepare Updates Shared Linearization Context", tags::gravity_operator_unit ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); f.mapping->ResetDisplacement(); physics::update_stiffness_matrix(f); const gravity_layout layout = make_gravity_jacobian_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); mfem::Vector state_a = make_gravity_jacobian_state(f, layout, false); const mfem::Vector state_b = make_gravity_jacobian_state(f, layout, true); const operators::context::gravity_field::GravityFieldRevisions revisions_a{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; const operators::context::gravity_field::GravityFieldRevisions revisions_b{ .discretization = {1}, .displacement = {2}, .density = {2}, .gravity_gradient = {2}, .gravity_potential = {2} }; MPI_Comm communicator = f.mesh->GetComm(); gravity_operator.Prepare(state_a, revisions_a); mfem::Operator &returned_gradient_a = gravity_operator.GetGradient(state_a); REQUIRE(&returned_gradient_a == static_cast(&gravity_jacobian)); check_linearization_context_matches_state(linearization_context, state_a, layout.value_offsets(), communicator); state_a = 0.0; CHECK(linearization_context.IsPrepared()); const double stored_state_norm = global_vector_norm(linearization_context.GetDensityTrue(), communicator) + global_vector_norm(linearization_context.GetGeometryContext().GetDisplacementTrue(), communicator) + global_vector_norm(linearization_context.GetGravityGradientTrue(), communicator); CHECK(stored_state_norm > 0.0); gravity_operator.Prepare(state_b, revisions_b); mfem::Operator &returned_gradient_b = gravity_operator.GetGradient(state_b); REQUIRE(&returned_gradient_b == static_cast(&gravity_jacobian)); check_linearization_context_matches_state(linearization_context, state_b, layout.value_offsets(), communicator); } TEST_CASE( "Mapped Hdiv Mass Variation Is Linear In Displacement Direction", tags::gravity_kernel_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); mfem::Vector zero_direction(f.displacementFes->GetTrueVSize()); mfem::Vector zero_gravity_gradient(f.gravityFluxFes->GetTrueVSize()); zero_direction = 0.0; zero_gravity_gradient = 0.0; mfem::Vector zero_direction_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, zero_direction, zero_direction_action ); INFO("Zero-direction action norm = " << global_norm(zero_direction_action, communicator)); CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); mfem::Vector zero_gravity_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, zero_gravity_gradient, fields.displacement, fields.displacement_direction_1, zero_gravity_action ); INFO("Zero-gravity action norm = " << global_norm(zero_gravity_action, communicator)); CHECK(global_norm(zero_gravity_action, communicator) < 1.0e-13); constexpr double scale_1 = 1.7; constexpr double scale_2 = -0.6; mfem::Vector combined_direction(fields.displacement_direction_1); combined_direction *= scale_1; combined_direction.Add(scale_2, fields.displacement_direction_2); mfem::Vector action_1; mfem::Vector action_2; mfem::Vector combined_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, action_1 ); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_2, action_2 ); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, combined_direction, combined_action ); mfem::Vector expected_action(action_1); expected_action *= scale_1; expected_action.Add(scale_2, action_2); const double linearity_error = global_relative_error(combined_action, expected_action, communicator); INFO("Combined action norm = " << global_norm(combined_action, communicator)); INFO("Expected action norm = " << global_norm(expected_action, communicator)); INFO("Displacement-direction linearity error = " << linearity_error); CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12)); } TEST_CASE( "Mapped Hdiv Mass Variation Matches Centered Geometry Differences", tags::gravity_kernel_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); constexpr double difference_step = 1.0e-5; constexpr double comparison_tolerance = 2.0e-7; mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); identity_displacement = 0.0; for (const bool deformed : std::array{false, true}) { const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; mfem::Vector analytic_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, analytic_action ); const mfem::Vector finite_difference_action = centered_hdiv_mass_geometry_difference( f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, difference_step ); const double relative_error = global_relative_error(analytic_action, finite_difference_action, communicator); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("Analytic variation norm = " << global_norm(analytic_action, communicator)); INFO("Finite-difference variation norm = " << global_norm(finite_difference_action, communicator)); INFO("Relative H(div) mass-variation error = " << relative_error); CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); } } TEST_CASE( "Mapped Hdiv Mass Geometry Difference Converges To Analytic Variation", tags::gravity_kernel_convergence ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); mfem::Vector analytic_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, analytic_action ); constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; std::array errors{}; for (std::size_t i = 0; i < difference_steps.size(); ++i) { const mfem::Vector finite_difference_action = centered_hdiv_mass_geometry_difference( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, difference_steps[i] ); errors[i] = global_relative_error(finite_difference_action, analytic_action, communicator); INFO("Difference step = " << difference_steps[i]); INFO("Relative error = " << errors[i]); } const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); INFO("First observed convergence order = " << first_observed_order); INFO("Second observed convergence order = " << second_observed_order); CHECK(errors[1] < errors[0]); CHECK(errors[2] < errors[1]); CHECK(first_observed_order > 1.8); CHECK(second_observed_order > 1.8); } TEST_CASE( "Mapped Source Variation Is Linear In Displacement Direction", tags::gravity_kernel_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector density = make_source_variation_density(f); MPI_Comm communicator = f.mesh->GetComm(); mfem::Vector zero_density(f.densityFes->GetTrueVSize()); mfem::Vector zero_direction(f.displacementFes->GetTrueVSize()); zero_density = 0.0; zero_direction = 0.0; mfem::Vector zero_direction_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, zero_direction, zero_direction_action ); REQUIRE(zero_direction_action.Size() == f.gravityPotentialFes->GetTrueVSize()); CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); mfem::Vector zero_density_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, zero_density, fields.displacement, fields.displacement_direction_1, zero_density_action ); REQUIRE(zero_density_action.Size() == f.gravityPotentialFes->GetTrueVSize()); CHECK(global_norm(zero_density_action, communicator) < 1.0e-13); constexpr double scale_1 = 1.4; constexpr double scale_2 = -0.7; mfem::Vector combined_direction(fields.displacement_direction_1); combined_direction *= scale_1; combined_direction.Add(scale_2, fields.displacement_direction_2); mfem::Vector action_1; mfem::Vector action_2; mfem::Vector combined_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, action_1 ); operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, fields.displacement_direction_2, action_2 ); operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, combined_direction, combined_action ); REQUIRE(action_1.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(action_2.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize()); mfem::Vector expected_action(action_1); expected_action *= scale_1; expected_action.Add(scale_2, action_2); const double linearity_error = global_relative_error(combined_action, expected_action, communicator); INFO("Combined source-variation norm = " << global_norm(combined_action, communicator)); INFO("Expected source-variation norm = " << global_norm(expected_action, communicator)); INFO("Source-variation linearity error = " << linearity_error); CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12)); } TEST_CASE( "Mapped Source Variation Matches Centered Geometry Differences", tags::gravity_kernel_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector density = make_source_variation_density(f); MPI_Comm communicator = f.mesh->GetComm(); constexpr double difference_step = 1.0e-5; constexpr double comparison_tolerance = 2.0e-7; mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); identity_displacement = 0.0; for (const bool deformed : std::array{false, true}) { const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; mfem::Vector analytic_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, base_displacement, fields.displacement_direction_1, analytic_action ); const mfem::Vector finite_difference_action = centered_source_geometry_difference( f, domain_mapper, density, base_displacement, fields.displacement_direction_1, difference_step ); REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize()); const double relative_error = global_relative_error(analytic_action, finite_difference_action, communicator); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("Analytic source-variation norm = " << global_norm(analytic_action, communicator)); INFO("Finite-difference source-variation norm = " << global_norm(finite_difference_action, communicator)); INFO("Relative source-variation error = " << relative_error); CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); } } TEST_CASE( "Mapped Source Geometry Difference Converges To Analytic Variation", tags::gravity_kernel_convergence ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector density = make_source_variation_density(f); MPI_Comm communicator = f.mesh->GetComm(); mfem::Vector analytic_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, analytic_action ); REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; std::array errors{}; for (std::size_t i = 0; i < difference_steps.size(); ++i) { const mfem::Vector finite_difference_action = centered_source_geometry_difference( f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, difference_steps[i] ); REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize()); errors[i] = global_relative_error(finite_difference_action, analytic_action, communicator); } const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); INFO("First observed convergence order = " << first_observed_order); INFO("Second observed convergence order = " << second_observed_order); CHECK(errors[1] < errors[0]); CHECK(errors[2] < errors[1]); CHECK(first_observed_order > 1.8); CHECK(second_observed_order > 1.8); } TEST_CASE( "Mapped Hdiv Mass Variation Is Symmetric", tags::gravity_kernel_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector second_gravity_gradient = make_secondary_gravity_gradient(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); identity_displacement = 0.0; for (const bool deformed : std::array{false, true}) { const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; mfem::Vector first_action; mfem::Vector second_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, first_action ); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, second_gravity_gradient, base_displacement, fields.displacement_direction_1, second_action ); REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize()); REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize()); const double left_pairing = global_dot(fields.gravity_gradient, second_action, communicator); const double right_pairing = global_dot(second_gravity_gradient, first_action, communicator); const double symmetry_error = std::abs(left_pairing - right_pairing) / std::max({std::abs(left_pairing), std::abs(right_pairing), 1.0e-14}); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("g1^T delta_M g2 = " << left_pairing); INFO("g2^T delta_M g1 = " << right_pairing); INFO("Relative symmetry error = " << symmetry_error); CHECK_THAT(symmetry_error, Catch::Matchers::WithinAbs(0.0, 1.0e-11)); } } TEST_CASE( "Mapped Geometry Variations Are Linear In Base Fields", tags::gravity_kernel_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector second_gravity_gradient = make_secondary_gravity_gradient(f); const mfem::Vector first_density = make_source_variation_density(f); const mfem::Vector second_density = make_secondary_source_density(f); constexpr double scale_1 = 1.3; constexpr double scale_2 = -0.4; { MPI_Comm communicator = f.gravityFluxFes->GetComm(); mfem::Vector combined_gravity_gradient(fields.gravity_gradient); combined_gravity_gradient *= scale_1; combined_gravity_gradient.Add(scale_2, second_gravity_gradient); mfem::Vector first_action; mfem::Vector second_action; mfem::Vector combined_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, first_action ); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, second_gravity_gradient, fields.displacement, fields.displacement_direction_1, second_action ); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, combined_gravity_gradient, fields.displacement, fields.displacement_direction_1, combined_action ); REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize()); REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize()); REQUIRE(combined_action.Size() == f.gravityFluxFes->GetTrueVSize()); mfem::Vector expected_action(first_action); expected_action *= scale_1; expected_action.Add(scale_2, second_action); const double linearity_error = global_relative_error(combined_action, expected_action, communicator); INFO("H(div) base-field linearity error = " << linearity_error); CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12)); } { MPI_Comm communicator = f.mesh->GetComm(); mfem::Vector combined_density(first_density); combined_density *= scale_1; combined_density.Add(scale_2, second_density); mfem::Vector first_action; mfem::Vector second_action; mfem::Vector combined_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, first_density, fields.displacement, fields.displacement_direction_1, first_action ); operators::kernels::apply_mapped_source_variation( f, domain_mapper, second_density, fields.displacement, fields.displacement_direction_1, second_action ); operators::kernels::apply_mapped_source_variation( f, domain_mapper, combined_density, fields.displacement, fields.displacement_direction_1, combined_action ); REQUIRE(first_action.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(second_action.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize()); mfem::Vector expected_action(first_action); expected_action *= scale_1; expected_action.Add(scale_2, second_action); const double linearity_error = global_relative_error(combined_action, expected_action, communicator); INFO("Source base-field linearity error = " << linearity_error); CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12)); } } TEST_CASE( "Mapped Source Variation Ignores Vacuum Density", tags::gravity_kernel_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapperStateless &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const int vacuum_attribute = domain_mapper.GetVacuumElementAttribute(); const mfem::Vector vacuum_density = make_vacuum_only_density(f, vacuum_attribute); MPI_Comm communicator = f.mesh->GetComm(); const double vacuum_density_norm = global_norm(vacuum_density, communicator); REQUIRE(vacuum_density_norm > 0.0); mfem::Vector action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, vacuum_density, fields.displacement, fields.displacement_direction_1, action ); REQUIRE(action.Size() == f.gravityPotentialFes->GetTrueVSize()); const double action_norm = global_norm(action, communicator); INFO("Vacuum density norm = " << vacuum_density_norm); INFO("Source-variation action norm = " << action_norm); CHECK_THAT(action_norm, Catch::Matchers::WithinAbs(0.0, 1.0e-14)); } TEST_CASE( "Gravity Field Jacobian Displacement Blocks Match Geometry Kernels", tags::gravity_operator_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); f.mapping->ResetDisplacement(); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, revisions); mfem::Vector jacobian_action; gravity_jacobian.Mult(direction, jacobian_action); REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); const mfem::Vector &density_true = linearization_context.GetDensityTrue(); const mfem::Vector &displacement_true = linearization_context.GetGeometryContext().GetDisplacementTrue(); const mfem::Vector &gravity_gradient_true = linearization_context.GetGravityGradientTrue(); const mfem::Vector gradient_action = get_residual_block(jacobian_action, layout, gravity_gradient_residual_block); const mfem::Vector poisson_action = get_residual_block(jacobian_action, layout, gravity_poisson_residual_block); mfem::Vector expected_gradient_action_true; mfem::Vector expected_poisson_action_true; operators::kernels::apply_mapped_hdiv_mass_variation( f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, fields.displacement_direction_1, expected_gradient_action_true ); operators::kernels::apply_mapped_source_variation( f, *f.domainMapperStateless, density_true, displacement_true, fields.displacement_direction_1, expected_poisson_action_true ); expected_poisson_action_true *= -1.0; const mfem::Vector expected_gradient_action = linearization_context.GetGravityGradientMap().gather(expected_gradient_action_true); const mfem::Vector expected_poisson_action = linearization_context.GetGravityPotentialMap().gather(expected_poisson_action_true); MPI_Comm communicator = f.mesh->GetComm(); const double gradient_error = global_relative_error(gradient_action, expected_gradient_action, communicator); const double poisson_error = global_relative_error(poisson_action, expected_poisson_action, communicator); INFO("Gradient displacement action norm = " << global_norm(gradient_action, communicator)); INFO("Poisson displacement action norm = " << global_norm(poisson_action, communicator)); INFO("Gradient displacement-block error = " << gradient_error); INFO("Poisson displacement-block error = " << poisson_error); REQUIRE(global_norm(gradient_action, communicator) > 1.0e-12); REQUIRE(global_norm(poisson_action, communicator) > 1.0e-12); CHECK_THAT(gradient_error, WithinAbs(0.0, 2.0e-12)); CHECK_THAT(poisson_error, WithinAbs(0.0, 2.0e-12)); } TEST_CASE( "Gravity Field Jacobian Displacement Direction Matches Centered " "Differences", tags::gravity_operator_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); f.mapping->ResetDisplacement(); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); constexpr double difference_step = 1.0e-5; constexpr double comparison_tolerance = 3.0e-7; constexpr double nonzero_tolerance = 1.0e-12; MPI_Comm communicator = f.mesh->GetComm(); REQUIRE(direction.Size() == layout.value_offsets().Last()); REQUIRE(global_vector_norm(direction, communicator) > nonzero_tolerance); for (const bool deformed : std::array{false, true}) { DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { const mfem::Vector state = make_gravity_jacobian_state(f, layout, deformed); REQUIRE(state.Size() == layout.value_offsets().Last()); const operators::context::gravity_field::GravityFieldRevisions base_revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, base_revisions); REQUIRE(linearization_context.IsPrepared()); check_linearization_context_matches_state( linearization_context, state, layout.value_offsets(), communicator ); const double base_density_norm = global_vector_norm(linearization_context.GetDensityTrue(), communicator); const double base_gravity_gradient_norm = global_vector_norm(linearization_context.GetGravityGradientTrue(), communicator); INFO("Base density norm = " << base_density_norm); INFO("Base gravity-gradient norm = " << base_gravity_gradient_norm); REQUIRE(base_density_norm > nonzero_tolerance); REQUIRE(base_gravity_gradient_norm > nonzero_tolerance); mfem::Operator &gradient = gravity_operator.GetGradient(state); REQUIRE(&gradient == static_cast(&gravity_jacobian)); mfem::Vector jacobian_action; gradient.Mult(direction, jacobian_action); REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(difference_step, direction); minus_state.Add(-difference_step, direction); auto plus_revisions = base_revisions; plus_revisions.displacement.value++; mfem::Vector plus_residual; gravity_operator.Prepare(plus_state, plus_revisions); gravity_operator.Mult(plus_state, plus_residual); REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); auto minus_revisions = plus_revisions; minus_revisions.displacement.value++; mfem::Vector minus_residual; gravity_operator.Prepare(minus_state, minus_revisions); gravity_operator.Mult(minus_state, minus_residual); REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); mfem::Vector finite_difference(plus_residual); finite_difference -= minus_residual; finite_difference /= 2.0 * difference_step; const mfem::Vector gradient_action = get_residual_block(jacobian_action, layout, gravity_gradient_residual_block); const mfem::Vector poisson_action = get_residual_block(jacobian_action, layout, gravity_poisson_residual_block); const mfem::Vector finite_difference_gradient = get_residual_block(finite_difference, layout, gravity_gradient_residual_block); const mfem::Vector finite_difference_poisson = get_residual_block(finite_difference, layout, gravity_poisson_residual_block); const double jacobian_gradient_norm = global_vector_norm(gradient_action, communicator); const double jacobian_poisson_norm = global_vector_norm(poisson_action, communicator); const double finite_difference_gradient_norm = global_vector_norm(finite_difference_gradient, communicator); const double finite_difference_poisson_norm = global_vector_norm(finite_difference_poisson, communicator); REQUIRE(jacobian_gradient_norm > nonzero_tolerance); REQUIRE(jacobian_poisson_norm > nonzero_tolerance); REQUIRE(finite_difference_gradient_norm > nonzero_tolerance); REQUIRE(finite_difference_poisson_norm > nonzero_tolerance); mfem::Vector gradient_difference(gradient_action); gradient_difference -= finite_difference_gradient; mfem::Vector poisson_difference(poisson_action); poisson_difference -= finite_difference_poisson; mfem::Vector combined_difference(jacobian_action); combined_difference -= finite_difference; const double absolute_gradient_error = global_vector_norm(gradient_difference, communicator); const double absolute_poisson_error = global_vector_norm(poisson_difference, communicator); const double absolute_combined_error = global_vector_norm(combined_difference, communicator); const double gradient_error = global_relative_error(gradient_action, finite_difference_gradient, communicator); const double poisson_error = global_relative_error(poisson_action, finite_difference_poisson, communicator); const double combined_error = global_relative_error(jacobian_action, finite_difference, communicator); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("Jacobian gradient action norm = " << jacobian_gradient_norm); INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm); INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm); INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm); INFO("Absolute gradient residual error = " << absolute_gradient_error); INFO("Absolute Poisson residual error = " << absolute_poisson_error); INFO("Absolute combined residual error = " << absolute_combined_error); INFO("Gradient residual relative error = " << gradient_error); INFO("Poisson residual relative error = " << poisson_error); INFO("Combined residual relative error = " << combined_error); REQUIRE(std::isfinite(gradient_error)); REQUIRE(std::isfinite(poisson_error)); REQUIRE(std::isfinite(combined_error)); CHECK_THAT(gradient_error, WithinAbs(0.0, comparison_tolerance)); CHECK_THAT(poisson_error, WithinAbs(0.0, comparison_tolerance)); CHECK_THAT(combined_error, WithinAbs(0.0, comparison_tolerance)); } } } TEST_CASE( "Gravity Field Jacobian Displacement Difference Converges At Second Order", tags::gravity_operator_convergence ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); f.mapping->ResetDisplacement(); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; MPI_Comm communicator = f.mesh->GetComm(); gravity_operator.Prepare(state, revisions); REQUIRE(linearization_context.IsPrepared()); check_linearization_context_matches_state(linearization_context, state, layout.value_offsets(), communicator); mfem::Operator &gradient = gravity_operator.GetGradient(state); REQUIRE(&gradient == static_cast(&gravity_jacobian)); mfem::Vector jacobian_action; gradient.Mult(direction, jacobian_action); REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); const double jacobian_action_norm = global_vector_norm(jacobian_action, communicator); INFO("Jacobian action norm = " << jacobian_action_norm); REQUIRE(jacobian_action_norm > 1.0e-12); auto evaluate_geometry_centered_difference = [&](const double difference_step) { mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(difference_step, direction); minus_state.Add(-difference_step, direction); revisions.displacement.value++; mfem::Vector plus_residual; gravity_operator.Prepare(plus_state, revisions); gravity_operator.Mult(plus_state, plus_residual); revisions.displacement.value++; mfem::Vector minus_residual; gravity_operator.Prepare(minus_state, revisions); gravity_operator.Mult(minus_state, minus_residual); REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); mfem::Vector finite_difference(plus_residual); finite_difference -= minus_residual; finite_difference /= 2.0 * difference_step; return finite_difference; }; constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; std::array errors{}; std::array finite_difference_norms{}; for (std::size_t i = 0; i < difference_steps.size(); ++i) { const mfem::Vector finite_difference = evaluate_geometry_centered_difference(difference_steps[i]); finite_difference_norms[i] = global_vector_norm(finite_difference, communicator); errors[i] = global_relative_error(finite_difference, jacobian_action, communicator); INFO( "Step = " << difference_steps[i] << ", finite-difference norm = " << finite_difference_norms[i] << ", relative error = " << errors[i] ); REQUIRE(finite_difference_norms[i] > 1.0e-12); REQUIRE(std::isfinite(errors[i])); REQUIRE(errors[i] > 0.0); } const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); INFO( "Difference steps = [" << difference_steps[0] << ", " << difference_steps[1] << ", " << difference_steps[2] << "]" ); INFO( "Finite-difference norms = [" << finite_difference_norms[0] << ", " << finite_difference_norms[1] << ", " << finite_difference_norms[2] << "]" ); INFO("Relative errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); INFO("First observed convergence order = " << first_observed_order); INFO("Second observed convergence order = " << second_observed_order); REQUIRE(std::isfinite(first_observed_order)); REQUIRE(std::isfinite(second_observed_order)); CHECK(errors[1] < errors[0]); CHECK(errors[2] < errors[1]); CHECK(first_observed_order > 1.8); CHECK(second_observed_order > 1.8); } TEST_CASE( "Gravity Field Jacobian Complete Coupled Direction Matches Centered " "Differences", tags::gravity_operator_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); f.mapping->ResetDisplacement(); physics::update_stiffness_matrix(f); REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); mfem::Vector direction = make_combined_fixed_geometry_direction(layout); set_value_block(direction, layout, displacement_block, fields.displacement_direction_2); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); operators::context::gravity_field::GravityFieldRevisions base_revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; MPI_Comm communicator = f.mesh->GetComm(); REQUIRE(state.Size() == layout.value_offsets().Last()); REQUIRE(direction.Size() == layout.value_offsets().Last()); REQUIRE(global_vector_norm(direction, communicator) > 1.0e-12); gravity_operator.Prepare(state, base_revisions); REQUIRE(linearization_context.IsPrepared()); check_linearization_context_matches_state(linearization_context, state, layout.value_offsets(), communicator); mfem::Operator &gradient = gravity_operator.GetGradient(state); REQUIRE(&gradient == static_cast(&gravity_jacobian)); mfem::Vector jacobian_action; gradient.Mult(direction, jacobian_action); REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); constexpr double difference_step = 1.0e-5; mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(difference_step, direction); minus_state.Add(-difference_step, direction); auto plus_revisions = base_revisions; plus_revisions.displacement.value++; plus_revisions.density.value++; plus_revisions.gravity_gradient.value++; plus_revisions.gravity_potential.value++; mfem::Vector plus_residual; gravity_operator.Prepare(plus_state, plus_revisions); gravity_operator.Mult(plus_state, plus_residual); REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); auto minus_revisions = plus_revisions; minus_revisions.displacement.value++; minus_revisions.density.value++; minus_revisions.gravity_gradient.value++; minus_revisions.gravity_potential.value++; mfem::Vector minus_residual; gravity_operator.Prepare(minus_state, minus_revisions); gravity_operator.Mult(minus_state, minus_residual); REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); mfem::Vector finite_difference(plus_residual); finite_difference -= minus_residual; finite_difference /= 2.0 * difference_step; const mfem::Vector gradient_action = get_residual_block(jacobian_action, layout, gravity_gradient_residual_block); const mfem::Vector poisson_action = get_residual_block(jacobian_action, layout, gravity_poisson_residual_block); const mfem::Vector finite_difference_gradient = get_residual_block(finite_difference, layout, gravity_gradient_residual_block); const mfem::Vector finite_difference_poisson = get_residual_block(finite_difference, layout, gravity_poisson_residual_block); const double jacobian_gradient_norm = global_vector_norm(gradient_action, communicator); const double jacobian_poisson_norm = global_vector_norm(poisson_action, communicator); const double finite_difference_gradient_norm = global_vector_norm(finite_difference_gradient, communicator); const double finite_difference_poisson_norm = global_vector_norm(finite_difference_poisson, communicator); constexpr double nonzero_tolerance = 1.0e-12; REQUIRE(jacobian_gradient_norm > nonzero_tolerance); REQUIRE(jacobian_poisson_norm > nonzero_tolerance); REQUIRE(finite_difference_gradient_norm > nonzero_tolerance); REQUIRE(finite_difference_poisson_norm > nonzero_tolerance); mfem::Vector gradient_difference(gradient_action); gradient_difference -= finite_difference_gradient; mfem::Vector poisson_difference(poisson_action); poisson_difference -= finite_difference_poisson; mfem::Vector combined_difference(jacobian_action); combined_difference -= finite_difference; const double absolute_gradient_error = global_vector_norm(gradient_difference, communicator); const double absolute_poisson_error = global_vector_norm(poisson_difference, communicator); const double absolute_combined_error = global_vector_norm(combined_difference, communicator); const double gradient_error = global_relative_error(gradient_action, finite_difference_gradient, communicator); const double poisson_error = global_relative_error(poisson_action, finite_difference_poisson, communicator); const double combined_error = global_relative_error(jacobian_action, finite_difference, communicator); INFO("Jacobian gradient action norm = " << jacobian_gradient_norm); INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm); INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm); INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm); INFO("Absolute gradient residual error = " << absolute_gradient_error); INFO("Absolute Poisson residual error = " << absolute_poisson_error); INFO("Absolute combined residual error = " << absolute_combined_error); INFO("Gradient residual relative error = " << gradient_error); INFO("Poisson residual relative error = " << poisson_error); INFO("Combined residual relative error = " << combined_error); REQUIRE(std::isfinite(gradient_error)); REQUIRE(std::isfinite(poisson_error)); REQUIRE(std::isfinite(combined_error)); CHECK_THAT(gradient_error, WithinAbs(0.0, 5.0e-7)); CHECK_THAT(poisson_error, WithinAbs(0.0, 5.0e-7)); CHECK_THAT(combined_error, WithinAbs(0.0, 5.0e-7)); } TEST_CASE( "Reduced Gravity Field Operator Solves Deformed Gravity System With MINRES", tags::gravity_operator_integration ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.mapping != nullptr); REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector density_true = make_source_variation_density(f); const mfem::Vector displacement_true = fields.displacement; mfem::ParGridFunction legacy_displacement(f.displacementFes.get()); legacy_displacement.SetFromTrueDofs(displacement_true); f.mapping->SetDisplacement(legacy_displacement); physics::update_stiffness_matrix(f); REQUIRE(f.gravityContext.b_form != nullptr); REQUIRE(f.gravityContext.BT != nullptr); REQUIRE(f.gravityContext.block_prec != nullptr); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); const mfem::Vector density = linearization_context.GetDensityMap().gather(density_true); const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(displacement_true); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); operators::context::gravity_field::GravityFieldGeometryContext reduced_geometry_context( f, *f.domainMapperStateless ); operators::ReducedGravityFieldOperator reduced_operator(gravity_operator, reduced_geometry_context, displacement); REQUIRE(reduced_geometry_context.IsPrepared()); REQUIRE(reduced_operator.Width() == layout.residual_offsets().Last()); REQUIRE(reduced_operator.Height() == layout.residual_offsets().Last()); REQUIRE(reduced_operator.Width() == reduced_operator.Height()); const std::uint64_t mass_preparations_before_solve = reduced_geometry_context.GetMassOperator().GetPreparationCount(); const std::uint64_t source_preparations_before_solve = reduced_geometry_context.GetSourceOperator().GetPreparationCount(); REQUIRE(mass_preparations_before_solve > 0); REQUIRE(source_preparations_before_solve > 0); mfem::Vector right_hand_side; reduced_operator.BuildRightHandSide(density, right_hand_side); REQUIRE(right_hand_side.Size() == reduced_operator.Height()); MPI_Comm communicator = f.mesh->GetComm(); const double right_hand_side_norm = global_norm(right_hand_side, communicator); INFO("Reduced gravity-system size = " << reduced_operator.Height()); INFO("Gravity right-hand-side norm = " << right_hand_side_norm); INFO("Mass preparations before solve = " << mass_preparations_before_solve); INFO("Source preparations before solve = " << source_preparations_before_solve); REQUIRE(right_hand_side_norm > 0.0); mfem::Vector gravity_state(reduced_operator.Width()); gravity_state = 0.0; constexpr double relative_solver_tolerance = 1.0e-14; constexpr double absolute_solver_tolerance = 1.0e-14; const int maximum_iterations = 2000; mfem::MINRESSolver minres(communicator); minres.SetOperator(reduced_operator); minres.SetPreconditioner(*f.gravityContext.block_prec); minres.SetRelTol(relative_solver_tolerance); minres.SetAbsTol(absolute_solver_tolerance); minres.SetMaxIter(maximum_iterations); minres.SetPrintLevel(1); MEAN_FIELD_PROFILE_RESET(); MEAN_FIELD_PROFILE_CALL_WARMUP("MINRES total", 0, minres.Mult(right_hand_side, gravity_state)); MEAN_FIELD_PROFILE_PRINT(communicator); REQUIRE(gravity_state.Size() == reduced_operator.Width()); const std::uint64_t mass_preparations_after_solve = reduced_geometry_context.GetMassOperator().GetPreparationCount(); const std::uint64_t source_preparations_after_solve = reduced_geometry_context.GetSourceOperator().GetPreparationCount(); INFO("Mass preparations after solve = " << mass_preparations_after_solve); INFO("Source preparations after solve = " << source_preparations_after_solve); CHECK(mass_preparations_after_solve == mass_preparations_before_solve); CHECK(source_preparations_after_solve == source_preparations_before_solve); mfem::Vector operator_action; reduced_operator.Mult(gravity_state, operator_action); mfem::Vector reduced_residual(operator_action); reduced_residual -= right_hand_side; const mfem::Vector gradient_residual = get_residual_block(reduced_residual, layout, gravity_gradient_residual_block); const mfem::Vector poisson_residual = get_residual_block(reduced_residual, layout, gravity_poisson_residual_block); const mfem::Vector solved_gravity_gradient = get_residual_block(gravity_state, layout, gravity_gradient_residual_block); const mfem::Vector solved_gravity_potential = get_residual_block(gravity_state, layout, gravity_poisson_residual_block); const double gravity_state_norm = global_norm(gravity_state, communicator); const double gravity_gradient_norm = global_norm(solved_gravity_gradient, communicator); const double gravity_potential_norm = global_norm(solved_gravity_potential, communicator); const double residual_norm = global_norm(reduced_residual, communicator); const double gradient_residual_norm = global_norm(gradient_residual, communicator); const double poisson_residual_norm = global_norm(poisson_residual, communicator); const double relative_residual = residual_norm / right_hand_side_norm; const double relative_gradient_residual = gradient_residual_norm / right_hand_side_norm; const double relative_poisson_residual = poisson_residual_norm / right_hand_side_norm; INFO("MINRES converged = " << minres.GetConverged()); INFO("MINRES iterations = " << minres.GetNumIterations()); INFO("MINRES reported final norm = " << minres.GetFinalNorm()); INFO("Gravity-state norm = " << gravity_state_norm); INFO("Solved gravity-gradient norm = " << gravity_gradient_norm); INFO("Solved gravity-potential norm = " << gravity_potential_norm); INFO("Direct residual norm = " << residual_norm); INFO("Direct relative residual = " << relative_residual); INFO("Relative gradient-equation residual = " << relative_gradient_residual); INFO("Relative Poisson-equation residual = " << relative_poisson_residual); CHECK(minres.GetConverged()); CHECK(minres.GetNumIterations() <= maximum_iterations); REQUIRE(gravity_state_norm > 0.0); REQUIRE(gravity_gradient_norm > 0.0); REQUIRE(gravity_potential_norm > 0.0); constexpr double direct_residual_tolerance = 1.0e-11; CHECK_THAT(relative_residual, WithinAbs(0.0, direct_residual_tolerance)); CHECK_THAT(relative_gradient_residual, WithinAbs(0.0, direct_residual_tolerance)); CHECK_THAT(relative_poisson_residual, WithinAbs(0.0, direct_residual_tolerance)); mfem::Vector full_state(layout.value_offsets().Last()); full_state = 0.0; set_value_block(full_state, layout, density_block, density); set_value_block(full_state, layout, displacement_block, displacement); set_value_block(full_state, layout, gravity_gradient_block, solved_gravity_gradient); set_value_block(full_state, layout, gravity_potential_block, solved_gravity_potential); const operators::context::gravity_field::GravityFieldRevisions full_state_revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(full_state, full_state_revisions); REQUIRE(linearization_context.IsPrepared()); check_linearization_context_matches_state(linearization_context, full_state, layout.value_offsets(), communicator); mfem::Vector full_residual; gravity_operator.Mult(full_state, full_residual); REQUIRE(full_residual.Size() == layout.residual_offsets().Last()); mfem::Vector residual_representation_difference(full_residual); residual_representation_difference -= reduced_residual; const double residual_representation_error = global_norm(residual_representation_difference, communicator) / right_hand_side_norm; const double full_relative_residual = global_norm(full_residual, communicator) / right_hand_side_norm; INFO("Full gravity residual relative norm = " << full_relative_residual); INFO("Reduced/full residual representation error = " << residual_representation_error); CHECK_THAT(full_relative_residual, WithinAbs(0.0, direct_residual_tolerance)); CHECK_THAT(residual_representation_error, WithinAbs(0.0, 1.0e-12)); } TEST_CASE( "Gravity Hdiv Operator Difference Is Localized To Vacuum Compactification", tags::gravity_legacy ) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.mapping != nullptr); REQUIRE(f.domainMapperStateless != nullptr); REQUIRE(f.compactificationCoordinate != nullptr); using form = blocks::gravity_field_form; constexpr auto displacement_block = mean_field::utils::blocks::get_value_block(blocks::displacement_field.geometry_term); constexpr auto gravity_gradient_block = mean_field::utils::blocks::get_value_block(blocks::gravity_field.gradient_term); constexpr auto gravity_gradient_residual_block = mean_field::utils::blocks::get_residual_block(blocks::gravity_field.gradient_term); const blocks::form_layout layout = make_gravity_layout(f); const mfem::Vector gravity_gradient_true = make_full_support_gravity_gradient(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); REQUIRE(global_vector_norm(gravity_gradient_true, communicator) > 0.0); for (const bool deformed : std::array{false, true}) { DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { const mfem::Vector displacement_true = make_stateless_reference_displacement(f, deformed); /* * Prepare the legacy geometry and assemble its PA operator. * The legacy mapper performs its vacuum map without consulting * the exterior-coordinate GridFunction. */ mfem::ParGridFunction legacy_displacement(f.displacementFes.get()); legacy_displacement.SetFromTrueDofs(displacement_true); f.mapping->SetDisplacement(legacy_displacement); physics::update_stiffness_matrix(f); REQUIRE(f.gravityContext.m_form != nullptr); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context( f, *f.domainMapperStateless ); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets() ); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian ); mfem::Vector state(layout.value_offsets().Last()); state = 0.0; for (int i = 0; i < displacement_true.Size(); ++i) { state(layout.offset(displacement_block) + i) = displacement_true(i); } for (int i = 0; i < gravity_gradient_true.Size(); ++i) { state(layout.offset(gravity_gradient_block) + i) = gravity_gradient_true(i); } const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1} }; gravity_operator.Prepare(state, revisions); mfem::Vector new_residual; gravity_operator.Mult(state, new_residual); mfem::Vector new_operator_mass_action(layout.size(gravity_gradient_residual_block)); for (int i = 0; i < new_operator_mass_action.Size(); ++i) { new_operator_mass_action(i) = new_residual(layout.offset(gravity_gradient_residual_block) + i); } mfem::Vector legacy_operator_mass_action(f.gravityFluxFes->GetTrueVSize()); legacy_operator_mass_action = 0.0; f.gravityContext.m_form->Mult(gravity_gradient_true, legacy_operator_mass_action); const StatelessHDivMassReference new_reference = evaluate_stateless_hdiv_mass_quadrature_reference(f, gravity_gradient_true, displacement_true); const StatelessHDivMassReference legacy_reference = assemble_legacy_hdiv_mass_reference(f, gravity_gradient_true); mfem::Vector new_decomposed_action(new_reference.stellar_action); new_decomposed_action += new_reference.vacuum_action; mfem::Vector legacy_decomposed_action(legacy_reference.stellar_action); legacy_decomposed_action += legacy_reference.vacuum_action; mfem::Vector total_gap(new_reference.total_action); total_gap -= legacy_reference.total_action; mfem::Vector stellar_gap(new_reference.stellar_action); stellar_gap -= legacy_reference.stellar_action; mfem::Vector vacuum_gap(new_reference.vacuum_action); vacuum_gap -= legacy_reference.vacuum_action; mfem::Vector decomposed_gap(stellar_gap); decomposed_gap += vacuum_gap; mfem::Vector unexplained_by_vacuum(total_gap); unexplained_by_vacuum -= vacuum_gap; const double new_operator_reference_error = global_relative_difference(new_operator_mass_action, new_reference.total_action, communicator); const double legacy_operator_reference_error = global_relative_difference(legacy_operator_mass_action, legacy_reference.total_action, communicator); const double new_decomposition_error = global_relative_difference(new_reference.total_action, new_decomposed_action, communicator); const double legacy_decomposition_error = global_relative_difference(legacy_reference.total_action, legacy_decomposed_action, communicator); const double gap_decomposition_error = global_relative_difference(total_gap, decomposed_gap, communicator); const double stellar_relative_difference = global_relative_difference(new_reference.stellar_action, legacy_reference.stellar_action, communicator); const double vacuum_relative_difference = global_relative_difference(new_reference.vacuum_action, legacy_reference.vacuum_action, communicator); const double total_relative_difference = global_relative_difference(new_reference.total_action, legacy_reference.total_action, communicator); const double total_gap_norm = global_vector_norm(total_gap, communicator); const double stellar_gap_norm = global_vector_norm(stellar_gap, communicator); const double vacuum_gap_norm = global_vector_norm(vacuum_gap, communicator); const double unexplained_gap_norm = global_vector_norm(unexplained_by_vacuum, communicator); const double stellar_fraction_of_gap = stellar_gap_norm / std::max(total_gap_norm, std::numeric_limits::epsilon()); const double unexplained_fraction_of_gap = unexplained_gap_norm / std::max(total_gap_norm, std::numeric_limits::epsilon()); const double new_total_energy = global_vector_dot(gravity_gradient_true, new_reference.total_action, communicator); const double legacy_total_energy = global_vector_dot(gravity_gradient_true, legacy_reference.total_action, communicator); const double new_stellar_energy = global_vector_dot(gravity_gradient_true, new_reference.stellar_action, communicator); const double legacy_stellar_energy = global_vector_dot(gravity_gradient_true, legacy_reference.stellar_action, communicator); const double new_vacuum_energy = global_vector_dot(gravity_gradient_true, new_reference.vacuum_action, communicator); const double legacy_vacuum_energy = global_vector_dot(gravity_gradient_true, legacy_reference.vacuum_action, communicator); const double total_energy_gap = new_total_energy - legacy_total_energy; const double stellar_energy_gap = new_stellar_energy - legacy_stellar_energy; const double vacuum_energy_gap = new_vacuum_energy - legacy_vacuum_energy; const double energy_gap_decomposition_error = std::abs(total_energy_gap - stellar_energy_gap - vacuum_energy_gap) / std::max(std::abs(total_energy_gap), std::numeric_limits::epsilon()); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("New operator/reference error = " << new_operator_reference_error); INFO("Legacy operator/reference error = " << legacy_operator_reference_error); INFO("New regional decomposition error = " << new_decomposition_error); INFO("Legacy regional decomposition error = " << legacy_decomposition_error); INFO("Gap decomposition error = " << gap_decomposition_error); INFO("New/legacy total mass-action difference = " << total_relative_difference); INFO("New/legacy stellar mass-action difference = " << stellar_relative_difference); INFO("New/legacy vacuum mass-action difference = " << vacuum_relative_difference); INFO("Total operator-gap norm = " << total_gap_norm); INFO("Stellar operator-gap norm = " << stellar_gap_norm); INFO("Vacuum operator-gap norm = " << vacuum_gap_norm); INFO("Stellar fraction of operator gap = " << stellar_fraction_of_gap); INFO("Operator gap unexplained by vacuum = " << unexplained_fraction_of_gap); INFO("New total H(div) energy = " << new_total_energy); INFO("Legacy total H(div) energy = " << legacy_total_energy); INFO("New stellar H(div) energy = " << new_stellar_energy); INFO("Legacy stellar H(div) energy = " << legacy_stellar_energy); INFO("New vacuum H(div) energy = " << new_vacuum_energy); INFO("Legacy vacuum H(div) energy = " << legacy_vacuum_energy); INFO("Total H(div) energy gap = " << total_energy_gap); INFO("Stellar H(div) energy gap = " << stellar_energy_gap); INFO("Vacuum H(div) energy gap = " << vacuum_energy_gap); INFO("Energy-gap decomposition error = " << energy_gap_decomposition_error); REQUIRE(new_reference.stellar_elements > 0); REQUIRE(new_reference.vacuum_elements > 0); REQUIRE(legacy_reference.stellar_elements > 0); REQUIRE(legacy_reference.vacuum_elements > 0); REQUIRE(new_total_energy > 0.0); REQUIRE(legacy_total_energy > 0.0); REQUIRE(new_stellar_energy > 0.0); REQUIRE(legacy_stellar_energy > 0.0); REQUIRE(new_vacuum_energy > 0.0); REQUIRE(legacy_vacuum_energy > 0.0); constexpr double operator_reference_tolerance = 1.0e-11; constexpr double decomposition_tolerance = 5.0e-13; constexpr double stellar_parity_tolerance = 1.0e-10; constexpr double gap_localization_tolerance = 1.0e-9; CHECK_THAT(new_operator_reference_error, Catch::Matchers::WithinAbs(0.0, operator_reference_tolerance)); CHECK_THAT(legacy_operator_reference_error, Catch::Matchers::WithinAbs(0.0, operator_reference_tolerance)); CHECK_THAT(new_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); CHECK_THAT(legacy_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); CHECK_THAT(gap_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); CHECK_THAT(stellar_relative_difference, Catch::Matchers::WithinAbs(0.0, stellar_parity_tolerance)); /* * The previous global comparison already showed a material * difference. Confirm that this test continues to exercise it. */ REQUIRE(total_gap_norm > 1.0e-12); REQUIRE(vacuum_gap_norm > 1.0e-12); REQUIRE(vacuum_relative_difference > 1.0e-8); /* * If these pass, the old/new difference has been directly * localized to the vacuum compactification prescription. */ CHECK_THAT(stellar_fraction_of_gap, Catch::Matchers::WithinAbs(0.0, gap_localization_tolerance)); CHECK_THAT(unexplained_fraction_of_gap, Catch::Matchers::WithinAbs(0.0, gap_localization_tolerance)); CHECK_THAT(energy_gap_decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); } } }