#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) { const std::array value_sizes{ f.densityFes->GetTrueVSize(), f.displacementFes->GetTrueVSize(), f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize() }; const std::array residual_sizes{ f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize() }; 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) { const std::array value_sizes{ f.densityFes->GetTrueVSize(), f.displacementFes->GetTrueVSize(), f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize() }; const std::array residual_sizes{ f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize() }; return gravity_layout(value_sizes, residual_sizes); } 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); const mfem::Vector displacement = 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.GetDensity(), density, communicator ), Catch::Matchers::WithinAbs(0.0, 0.0) ); CHECK_THAT( global_relative_vector_error( context.GetGeometryContext().GetDisplacement(), displacement, communicator ), Catch::Matchers::WithinAbs(0.0, 0.0) ); CHECK_THAT( global_relative_vector_error( context.GetGravityGradient(), 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::unit &tags::solver &tags::gravity &tags::mfem_operators ) { 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::unit &tags::solver &tags::gravity &tags::mfem_operators ) { 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 = 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::unit &tags::solver &tags::gravity &tags::mfem_operators ) { 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 = make_vacuum_density(f, 1.0); REQUIRE(vacuum_density.Norml2() > 0.0); 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::unit &tags::solver &tags::gravity &tags::mfem_operators ) { 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 = 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::unit &tags::solver &tags::gravity &tags::mfem_operators ) { 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 = make_displacement(f); const mfem::Vector density = 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::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::legacy_comparison ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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_form>(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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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 = 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::unit &tags::solver &tags::gravity &tags::mfem_operators ) { 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::unit &tags::solver &tags::gravity &tags::mfem_operators ) { 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.GetDensity(), communicator) + global_vector_norm( linearization_context.GetGeometryContext().GetDisplacement(), communicator ) + global_vector_norm( linearization_context.GetGravityGradient(), 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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); const mfem::Vector density = get_value_block(state, layout, density_block); const mfem::Vector displacement = get_value_block(state, layout, displacement_block); const mfem::Vector gravity_gradient = get_value_block(state, layout, gravity_gradient_block); 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 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; mfem::Vector expected_poisson_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, *f.domainMapperStateless, gravity_gradient, displacement, fields.displacement_direction_1, expected_gradient_action ); operators::kernels::apply_mapped_source_variation( f, *f.domainMapperStateless, density, displacement, fields.displacement_direction_1, expected_poisson_action ); expected_poisson_action *= -1.0; 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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.GetDensity(), communicator ); const double base_gravity_gradient_norm = global_vector_norm( linearization_context.GetGravityGradient(), 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::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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::integration &tags::solver &tags::gravity &tags::mfem_operators ) { 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 = make_source_variation_density(f); const mfem::Vector displacement = fields.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.b_form != nullptr); REQUIRE(f.gravityContext.BT != nullptr); REQUIRE(f.gravityContext.block_prec != 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 ); 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::integration &tags::solver &tags::gravity &tags::mfem_operators &tags::legacy_comparison ) { 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 std::array value_sizes{ f.densityFes->GetTrueVSize(), f.displacementFes->GetTrueVSize(), f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize() }; const std::array residual_sizes{ f.gravityFluxFes->GetTrueVSize(), f.gravityPotentialFes->GetTrueVSize() }; const blocks::form_layout layout(value_sizes, residual_sizes); 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) ); } } }