#include "profile.h" #include #include #include #include #include import mean_field; import test_helpers; using namespace mean_field; using Catch::Matchers::WithinAbs; namespace { namespace blocks = utils::blocks; using form = blocks::gravity_field_form; constexpr auto density_block = blocks::get_value_block
(blocks::density_field.mass_term); constexpr auto displacement_block = blocks::get_value_block(blocks::displacement_field.geometry_term); constexpr auto gravity_gradient_block = blocks::get_value_block(blocks::gravity_field.gradient_term); constexpr auto gravity_potential_block = blocks::get_value_block(blocks::gravity_field.poisson_term); constexpr auto gravity_gradient_residual_block = blocks::get_residual_block(blocks::gravity_field.gradient_term); constexpr auto gravity_poisson_residual_block = blocks::get_residual_block(blocks::gravity_field.poisson_term); blocks::form_layout make_gravity_layout(const fem::FEM &f) { using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const auto density_map = field::make_field_dof_map(*f.densityFes); const auto displacement_map = field::make_field_dof_map( *f.displacementFes); const auto flux_map = field::make_field_dof_map( *f.gravityFluxFes); const auto potential_map = field::make_field_dof_map( *f.gravityPotentialFes); const std::array value_sizes{ density_map.reduced_size(), displacement_map.reduced_size(), flux_map.reduced_size(), potential_map.reduced_size()}; const std::array residual_sizes{ flux_map.reduced_size(), potential_map.reduced_size()}; return blocks::form_layout(value_sizes, residual_sizes); } template void set_block(mfem::Vector &vector, const mfem::Array &offsets, const Block block, const mfem::Vector &values) { const int block_id = block; const int begin = offsets[block_id]; const int size = offsets[block_id + 1] - begin; REQUIRE(values.Size() == size); for (int i = 0; i < size; ++i) vector(begin + i) = values(i); } template mfem::Vector get_block(const mfem::Vector &vector, const mfem::Array &offsets, const Block block) { const int block_id = block; const int begin = offsets[block_id]; const int size = offsets[block_id + 1] - begin; mfem::Vector result(size); for (int i = 0; i < size; ++i) result(i) = vector(begin + i); return result; } mfem::Vector make_test_vector(const int size, const double phase) { mfem::Vector vector(size); for (int i = 0; i < size; ++i) vector(i) = 0.4 * std::sin(0.37 * static_cast(i + 1) + phase) + 0.2 * std::cos(0.19 * static_cast(i + 1) - phase); return vector; } mfem::Vector make_displacement(const fem::FEM &f) { auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.015 * position(0) + 0.004 * position(1); value(1) = -0.003 * position(0) + 0.012 * position(1); value(2) = -0.008 * position(2); }; mfem::VectorFunctionCoefficient coefficient(3, displacement_function); mfem::ParGridFunction displacement(f.displacementFes.get()); mfem::Vector displacement_true; displacement.ProjectCoefficient(coefficient); displacement.GetTrueDofs(displacement_true); return displacement_true; } mfem::Vector make_constant_density(const fem::FEM &f, const double value) { mfem::ConstantCoefficient coefficient(value); mfem::ParGridFunction density(f.densityFes.get()); mfem::Vector density_true; density.ProjectCoefficient(coefficient); density.GetTrueDofs(density_true); return density_true; } mfem::Vector make_vacuum_density(const fem::FEM &f, const double value) { mfem::ParGridFunction density(f.densityFes.get()); density = 0.0; mfem::Array element_dofs; mfem::Vector element_values; for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { if (f.mesh->GetAttribute(element_id) != field_dof_test_utils::vacuum_material_attribute) 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 == field_dof_test_utils::vacuum_material_attribute; 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::DomainMapper::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 = field_dof_test_utils::vacuum_material_attribute; for (int element_id = 0; element_id < fem.mesh->GetNE(); ++element_id) { const mfem::FiniteElement &gravity_element = *fem.gravityFluxFes->GetFE(element_id); const mfem::FiniteElement &displacement_element = *fem.displacementFes->GetFE(element_id); const mfem::FiniteElement &compactification_element = *fem.compactificationFes->GetFE(element_id); mfem::ElementTransformation &transformation = *fem.mesh->GetElementTransformation(element_id); const bool is_vacuum = transformation.Attribute == vacuum_attribute; mfem::DofTransformation *gravity_dof_transformation = fem.gravityFluxFes->GetElementVDofs(element_id, gravity_dofs); mfem::DofTransformation *displacement_dof_transformation = fem.displacementFes->GetElementVDofs(element_id, displacement_dofs); mfem::DofTransformation *compactification_dof_transformation = fem.compactificationFes->GetElementDofs(element_id, compactification_dofs); gravity_gradient_local.GetSubVector(gravity_dofs, element_gravity_gradient); displacement_local.GetSubVector(displacement_dofs, element_displacement); fem.compactificationCoordinate->GetSubVector(compactification_dofs, element_compactification); if (gravity_dof_transformation != nullptr) { gravity_dof_transformation->InvTransformPrimal(element_gravity_gradient); } if (displacement_dof_transformation != nullptr) { displacement_dof_transformation->InvTransformPrimal(element_displacement); } if (compactification_dof_transformation != nullptr) { compactification_dof_transformation->InvTransformPrimal( element_compactification); } const mapping::ElementDisplacementData displacement_data = mapping::ElementDisplacementDataFromElementVDofs(displacement_element, element_displacement); const mapping::ElementCompactificationData compactification_data( compactification_element, element_compactification); const mapping::ElementMappingData mapping_data{ .displacement = displacement_data, .compactification = compactification_data}; const int gravity_dof_count = gravity_element.GetDof(); const int dimension = transformation.GetSpaceDim(); element_action.SetSize(gravity_dof_count); element_action = 0.0; quadrature_flux.SetSize(dimension); mapped_quadrature_flux.SetSize(dimension); vector_shape.SetSize(gravity_dof_count, dimension); mapped_mass_tensor.SetSize(dimension, dimension); const mfem::IntegrationRule &integration_rule = get_stateless_hdiv_reference_rule(fem, gravity_element, transformation); for (int quadrature_point = 0; quadrature_point < integration_rule.GetNPoints(); ++quadrature_point) { const mfem::IntegrationPoint &integration_point = integration_rule.IntPoint(quadrature_point); transformation.SetIntPoint(&integration_point); const mapping::MappingStatus status = fem.domainMapperStateless->EvaluateVolume( mapping_data, transformation, integration_point, workspace, mapping_context); MFEM_VERIFY(status == mean_field::mapping::MappingStatus::valid, "Stateless mapping failed while evaluating the H(div) " "quadrature reference." << "\nElement ID = " << element_id << "\nElement attribute = " << transformation.Attribute << "\nQuadrature point = " << quadrature_point << "\nMapping status = " << mapping_status_name(status)); gravity_element.CalcVShape(transformation, vector_shape); mean_field::mapping::ComputeHDivMassTensor(mapping_context.mapping, mapped_mass_tensor); // Evaluate B*x at this quadrature point. vector_shape.MultTranspose(element_gravity_gradient, quadrature_flux); // Apply the mapped H(div) mass tensor. mapped_mass_tensor.Mult(quadrature_flux, mapped_quadrature_flux); const double weight = integration_point.weight * transformation.Weight(); // Accumulate B^T*D*B*x directly. for (int dof = 0; dof < gravity_dof_count; ++dof) { double contribution = 0.0; for (int component = 0; component < dimension; ++component) { contribution += vector_shape(dof, component) * mapped_quadrature_flux(component); } element_action(dof) += weight * contribution; } const double mapping_determinant = mapping_context.mapping.mapping_determinant; MFEM_VERIFY(std::isfinite(mapping_determinant) && mapping_determinant > 0.0, "The quadrature reference encountered an invalid mapping " "determinant."); if (is_vacuum) { reference.minimum_vacuum_determinant = std::min(reference.minimum_vacuum_determinant, mapping_determinant); reference.maximum_vacuum_determinant = std::max(reference.maximum_vacuum_determinant, mapping_determinant); ++reference.vacuum_quadrature_points; } else { reference.minimum_stellar_determinant = std::min( reference.minimum_stellar_determinant, mapping_determinant); reference.maximum_stellar_determinant = std::max( reference.maximum_stellar_determinant, mapping_determinant); ++reference.stellar_quadrature_points; } } if (gravity_dof_transformation != nullptr) { gravity_dof_transformation->TransformDual(element_action); } total_local.AddElementVector(gravity_dofs, element_action); if (is_vacuum) { vacuum_local.AddElementVector(gravity_dofs, element_action); ++reference.vacuum_elements; } else { stellar_local.AddElementVector(gravity_dofs, element_action); ++reference.stellar_elements; } } reference_local_to_true(*fem.gravityFluxFes, total_local, reference.total_action); reference_local_to_true(*fem.gravityFluxFes, stellar_local, reference.stellar_action); reference_local_to_true(*fem.gravityFluxFes, vacuum_local, reference.vacuum_action); const MPI_Comm communicator = fem.gravityFluxFes->GetComm(); const long long local_counts[4]{ reference.stellar_elements, reference.vacuum_elements, reference.stellar_quadrature_points, reference.vacuum_quadrature_points}; long long global_counts[4]{}; MPI_Allreduce(local_counts, global_counts, 4, MPI_LONG_LONG, MPI_SUM, communicator); reference.stellar_elements = global_counts[0]; reference.vacuum_elements = global_counts[1]; reference.stellar_quadrature_points = global_counts[2]; reference.vacuum_quadrature_points = global_counts[3]; const double local_minimums[2]{reference.minimum_stellar_determinant, reference.minimum_vacuum_determinant}; const double local_maximums[2]{reference.maximum_stellar_determinant, reference.maximum_vacuum_determinant}; double global_minimums[2]{}; double global_maximums[2]{}; MPI_Allreduce(local_minimums, global_minimums, 2, MPI_DOUBLE, MPI_MIN, communicator); MPI_Allreduce(local_maximums, global_maximums, 2, MPI_DOUBLE, MPI_MAX, communicator); reference.minimum_stellar_determinant = global_minimums[0]; reference.minimum_vacuum_determinant = global_minimums[1]; reference.maximum_stellar_determinant = global_maximums[0]; reference.maximum_vacuum_determinant = global_maximums[1]; return reference; } using gravity_form = blocks::gravity_field_form; using gravity_layout = blocks::form_layout; gravity_layout make_gravity_jacobian_layout(const fem::FEM &f) { return make_gravity_layout(f); } template void set_value_block(mfem::Vector &vector, const gravity_layout &layout, const blocks::value_block block, const mfem::Vector &values) { REQUIRE(values.Size() == layout.size(block)); for (int i = 0; i < values.Size(); ++i) vector(layout.offset(block) + i) = values(i); } template mfem::Vector get_value_block(const mfem::Vector &vector, const gravity_layout &layout, const blocks::value_block block) { mfem::Vector values(layout.size(block)); for (int i = 0; i < values.Size(); ++i) values(i) = vector(layout.offset(block) + i); return values; } template mfem::Vector get_residual_block(const mfem::Vector &vector, const gravity_layout &layout, const blocks::residual_block block) { mfem::Vector values(layout.size(block)); for (int i = 0; i < values.Size(); ++i) values(i) = vector(layout.offset(block) + i); return values; } template void fill_value_block(mfem::Vector &vector, const gravity_layout &layout, const blocks::value_block block, const double scale, const double phase) { for (int i = 0; i < layout.size(block); ++i) { const double index_value = static_cast(i + 1); vector(layout.offset(block) + i) = scale * (std::sin(0.31 * index_value + phase) + 0.37 * std::cos(0.17 * index_value - phase)); } } mfem::Vector make_gravity_jacobian_state(const fem::FEM &f, const gravity_layout &layout, const bool deformed) { mfem::Vector state(layout.value_offsets().Last()); state = 0.0; fill_value_block(state, layout, density_block, 0.7, 0.11); fill_value_block(state, layout, gravity_gradient_block, 0.4, 0.23); fill_value_block(state, layout, gravity_potential_block, 0.3, 0.37); using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const auto displacement_map = field::make_field_dof_map( *f.displacementFes); const mfem::Vector displacement = displacement_map.gather( make_stateless_reference_displacement(f, deformed)); set_value_block(state, layout, displacement_block, displacement); return state; } mfem::Vector make_density_direction(const gravity_layout &layout) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; fill_value_block(direction, layout, density_block, 0.13, 0.19); return direction; } mfem::Vector make_gravity_gradient_direction(const gravity_layout &layout) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; fill_value_block(direction, layout, gravity_gradient_block, 0.11, 0.29); return direction; } mfem::Vector make_gravity_potential_direction(const gravity_layout &layout) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; fill_value_block(direction, layout, gravity_potential_block, 0.09, 0.41); return direction; } mfem::Vector make_combined_fixed_geometry_direction(const gravity_layout &layout) { mfem::Vector direction = make_density_direction(layout); const mfem::Vector gravity_gradient_direction = make_gravity_gradient_direction(layout); const mfem::Vector gravity_potential_direction = make_gravity_potential_direction(layout); direction += gravity_gradient_direction; direction += gravity_potential_direction; return direction; } mfem::Vector make_displacement_direction(const gravity_layout &layout, const mfem::Vector &displacement_direction) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; set_value_block(direction, layout, displacement_block, displacement_direction); return direction; } mfem::Vector evaluate_centered_difference(operators::GravityFieldOperator &gravity_operator, const mfem::Vector &state, const mfem::Vector &direction, const double step) { mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(step, direction); minus_state.Add(-step, direction); mfem::Vector plus_residual; mfem::Vector minus_residual; gravity_operator.Mult(plus_state, plus_residual); gravity_operator.Mult(minus_state, minus_residual); mfem::Vector difference(plus_residual); difference -= minus_residual; difference /= 2.0 * step; return difference; } struct HdivMassVariationTestFields { mfem::Vector gravity_gradient; mfem::Vector displacement; mfem::Vector displacement_direction_1; mfem::Vector displacement_direction_2; }; double global_relative_error(const mfem::Vector &computed, const mfem::Vector &reference, MPI_Comm communicator) { REQUIRE(computed.Size() == reference.Size()); mfem::Vector difference(computed); difference -= reference; return global_norm(difference, communicator) / std::max(global_norm(reference, communicator), 1.0e-14); } HdivMassVariationTestFields make_hdiv_mass_variation_test_fields(const fem::FEM &f) { const int dimension = f.mesh->Dimension(); auto gravity_gradient_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.4 + 0.18 * position(0) - 0.07 * position(1) * position(2); value(1) = -0.3 + 0.11 * position(1) + 0.05 * position(0) * position(2); value(2) = 0.2 - 0.09 * position(2) + 0.04 * position(0) * position(1); }; auto displacement_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.025 * position(0) + 0.006 * position(1) * position(2); value(1) = -0.018 * position(1) + 0.005 * position(0) * position(2); value(2) = 0.014 * position(2) + 0.004 * position(0) * position(1); }; auto displacement_direction_1_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = 0.16 * position(0) + 0.03 * position(1); value(1) = -0.11 * position(1) + 0.02 * position(2); value(2) = 0.13 * position(2) - 0.025 * position(0); }; auto displacement_direction_2_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = -0.07 * position(1) + 0.025 * position(2); value(1) = 0.09 * position(0) + 0.04 * position(2); value(2) = -0.08 * position(2) + 0.03 * position(0) * position(1); }; mfem::VectorFunctionCoefficient gravity_gradient_coefficient( dimension, gravity_gradient_function); mfem::VectorFunctionCoefficient displacement_coefficient( dimension, displacement_function); mfem::VectorFunctionCoefficient displacement_direction_1_coefficient( dimension, displacement_direction_1_function); mfem::VectorFunctionCoefficient displacement_direction_2_coefficient( dimension, displacement_direction_2_function); mfem::ParGridFunction gravity_gradient_grid(f.gravityFluxFes.get()); mfem::ParGridFunction displacement_grid(f.displacementFes.get()); mfem::ParGridFunction displacement_direction_1_grid(f.displacementFes.get()); mfem::ParGridFunction displacement_direction_2_grid(f.displacementFes.get()); gravity_gradient_grid.ProjectCoefficient(gravity_gradient_coefficient); displacement_grid.ProjectCoefficient(displacement_coefficient); displacement_direction_1_grid.ProjectCoefficient( displacement_direction_1_coefficient); displacement_direction_2_grid.ProjectCoefficient( displacement_direction_2_coefficient); HdivMassVariationTestFields fields; gravity_gradient_grid.GetTrueDofs(fields.gravity_gradient); displacement_grid.GetTrueDofs(fields.displacement); displacement_direction_1_grid.GetTrueDofs(fields.displacement_direction_1); displacement_direction_2_grid.GetTrueDofs(fields.displacement_direction_2); return fields; } mfem::Vector centered_hdiv_mass_geometry_difference( const fem::FEM &f, const mapping::DomainMapper &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::DomainMapper &domain_mapper, const mfem::Vector &density, const mfem::Vector &displacement, const mfem::Vector &displacement_direction, const double difference_step) { mfem::Vector plus_displacement(displacement); mfem::Vector minus_displacement(displacement); plus_displacement.Add(difference_step, displacement_direction); minus_displacement.Add(-difference_step, displacement_direction); mfem::Vector plus_action; mfem::Vector minus_action; operators::kernels::apply_mapped_source(f, domain_mapper, density, plus_displacement, plus_action); operators::kernels::apply_mapped_source(f, domain_mapper, density, minus_displacement, minus_action); plus_action -= minus_action; plus_action /= 2.0 * difference_step; return plus_action; } double global_dot(const mfem::Vector &left, const mfem::Vector &right, MPI_Comm communicator) { REQUIRE(left.Size() > 0); REQUIRE(left.Size() == right.Size()); const double local_dot = left * right; double global_dot_value = 0.0; MPI_Allreduce(&local_dot, &global_dot_value, 1, MPI_DOUBLE, MPI_SUM, communicator); return global_dot_value; } mfem::Vector make_secondary_gravity_gradient(const fem::FEM &f) { auto gravity_gradient_function = [](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = -0.17 + 0.09 * position(1) + 0.03 * position(0) * position(2); value(1) = 0.31 - 0.14 * position(0) + 0.05 * position(1) * position(2); value(2) = -0.22 + 0.12 * position(2) - 0.04 * position(0) * position(1); }; mfem::VectorFunctionCoefficient coefficient(f.mesh->Dimension(), gravity_gradient_function); mfem::ParGridFunction grid_function(f.gravityFluxFes.get()); mfem::Vector true_dofs; grid_function.ProjectCoefficient(coefficient); grid_function.GetTrueDofs(true_dofs); return true_dofs; } mfem::Vector make_secondary_source_density(const fem::FEM &f) { auto density_function = [](const mfem::Vector &position) { return 0.8 - 0.11 * position(0) + 0.13 * position(1) - 0.06 * position(2) + 0.03 * position(1) * position(2); }; mfem::FunctionCoefficient coefficient(density_function); mfem::ParGridFunction grid_function(f.densityFes.get()); mfem::Vector true_dofs; grid_function.ProjectCoefficient(coefficient); grid_function.GetTrueDofs(true_dofs); return true_dofs; } mfem::Vector make_vacuum_only_density(const fem::FEM &f, const int vacuum_attribute) { mfem::ParGridFunction grid_function(f.densityFes.get()); grid_function = 0.0; mfem::Array element_dofs; mfem::Vector element_values; for (int element_id = 0; element_id < f.mesh->GetNE(); ++element_id) { mfem::ElementTransformation *transformation = f.mesh->GetElementTransformation(element_id); REQUIRE(transformation != nullptr); if (transformation->Attribute != vacuum_attribute) continue; const mfem::FiniteElement &element = *f.densityFes->GetFE(element_id); mfem::DofTransformation *dof_transformation = f.densityFes->GetElementDofs(element_id, element_dofs); element_values.SetSize(element.GetDof()); for (int i = 0; i < element_values.Size(); ++i) element_values(i) = 0.9 + 0.01 * static_cast(i); if (dof_transformation != nullptr) dof_transformation->TransformPrimal(element_values); grid_function.SetSubVector(element_dofs, element_values); } mfem::Vector true_dofs; grid_function.GetTrueDofs(true_dofs); return true_dofs; } template mfem::Vector make_read_only_value_view(const mfem::Vector &vector, const mfem::Array &offsets, const blocks::value_block) { const int offset = offsets[index]; const int size = offsets[index + 1] - offset; return mfem::Vector(const_cast(vector.GetData()) + offset, size); } void check_linearization_context_matches_state( const operators::context::gravity_field::GravityFieldLinearizationContext &context, const mfem::Vector &state, const mfem::Array &state_offsets, MPI_Comm communicator) { using form = blocks::gravity_field_form; constexpr auto density_block = utils::blocks::get_value_block(blocks::density_field.mass_term); constexpr auto displacement_block = utils::blocks::get_value_block( blocks::displacement_field.geometry_term); constexpr auto gravity_gradient_block = utils::blocks::get_value_block(blocks::gravity_field.gradient_term); const mfem::Vector density = make_read_only_value_view(state, state_offsets, density_block); const mfem::Vector displacement = make_read_only_value_view(state, state_offsets, displacement_block); const mfem::Vector gravity_gradient = make_read_only_value_view(state, state_offsets, gravity_gradient_block); CHECK_THAT(global_relative_vector_error( context.GetDensityTrue(), context.GetDensityMap().scatter(density), communicator), Catch::Matchers::WithinAbs(0.0, 0.0)); CHECK_THAT(global_relative_vector_error( context.GetGeometryContext().GetDisplacementTrue(), context.GetDisplacementMap().scatter(displacement), communicator), Catch::Matchers::WithinAbs(0.0, 0.0)); CHECK_THAT(global_relative_vector_error( context.GetGravityGradientTrue(), context.GetGravityGradientMap().scatter(gravity_gradient), communicator), Catch::Matchers::WithinAbs(0.0, 0.0)); } } // namespace TEST_CASE("Gravity Field Operator Mult Preserves Zero State And Static Blocks", tags::gravity_operator_unit) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); 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); const auto &geometry_context = linearization_context.GetGeometryContext(); const auto &flux_map = linearization_context.GetGravityGradientMap(); const auto &potential_map = linearization_context.GetGravityPotentialMap(); const mfem::Vector gravity_potential_true = potential_map.scatter(gravity_potential); mfem::Vector expected_gradient_residual_true(flux_map.full_size()); geometry_context.GetTransposeDivergenceOperator().Mult( gravity_potential_true, expected_gradient_residual_true); const mfem::Vector expected_gradient_residual = flux_map.gather(expected_gradient_residual_true); 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); const mfem::Vector gravity_gradient_true = flux_map.scatter(gravity_gradient); mfem::Vector expected_poisson_residual_true(potential_map.full_size()); geometry_context.GetDivergenceOperator().Mult(gravity_gradient_true, expected_poisson_residual_true); const mfem::Vector expected_poisson_residual = potential_map.gather(expected_poisson_residual_true); CHECK(gradient_only_residual.Norml2() > 0.0); CHECK_THAT( relative_difference(poisson_only_residual, expected_poisson_residual), WithinAbs(0.0, 1.0e-13)); const double adjoint_lhs = gravity_gradient * expected_gradient_residual; const double adjoint_rhs = gravity_potential * expected_poisson_residual; const double adjoint_scale = std::max({std::abs(adjoint_lhs), std::abs(adjoint_rhs), 1.0e-14}); CHECK_THAT(std::abs(adjoint_lhs - adjoint_rhs) / adjoint_scale, WithinAbs(0.0, 1.0e-12)); } TEST_CASE( "Gravity Field Operator Mult Applies Stellar Source With Correct Sign", tags::gravity_operator_unit) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); mfem::Vector state(layout.value_offsets().Last()); mfem::Vector residual; state = 0.0; constexpr operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; const mfem::Vector density = linearization_context.GetDensityMap().gather( make_constant_density(f, 1.0)); set_block(state, layout.value_offsets(), density_block, density); gravity_operator.Prepare(state, revisions); gravity_operator.Mult(state, residual); const mfem::Vector gradient_residual = get_block( residual, layout.residual_offsets(), gravity_gradient_residual_block); const mfem::Vector poisson_residual = get_block( residual, layout.residual_offsets(), gravity_poisson_residual_block); CHECK_THAT(gradient_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); CHECK(poisson_residual.Norml2() > 0.0); const mfem::Vector constant_test = make_constant_density(f, 1.0); const double integrated_source_residual = constant_test * poisson_residual; INFO("Integrated Poisson source residual = " << integrated_source_residual); CHECK(integrated_source_residual < 0.0); mfem::Vector doubled_state(state); mfem::Vector doubled_density(density); doubled_density *= 2.0; set_block(doubled_state, layout.value_offsets(), density_block, doubled_density); mfem::Vector doubled_residual; gravity_operator.Mult(doubled_state, doubled_residual); mfem::Vector expected_doubled_residual(residual); expected_doubled_residual *= 2.0; CHECK_THAT(relative_difference(doubled_residual, expected_doubled_residual), WithinAbs(0.0, 1.0e-12)); } TEST_CASE("Gravity Field Operator Mult Ignores Vacuum Density", tags::gravity_operator_unit) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); mfem::Vector state(layout.value_offsets().Last()); mfem::Vector residual; state = 0.0; const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; const mfem::Vector vacuum_density_true = make_vacuum_density(f, 1.0); REQUIRE(vacuum_density_true.Norml2() > 0.0); const mfem::Vector vacuum_density = linearization_context.GetDensityMap().gather(vacuum_density_true); set_block(state, layout.value_offsets(), density_block, vacuum_density); gravity_operator.Prepare(state, revisions); gravity_operator.Mult(state, residual); INFO("Vacuum-source residual norm = " << residual.Norml2()); CHECK_THAT(residual.Norml2(), WithinAbs(0.0, 1.0e-13)); } TEST_CASE("Gravity Field Operator Mult Produces A Symmetric Positive Hdiv Mass " "Action", tags::gravity_operator_unit) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(make_displacement(f)); const mfem::Vector gravity_gradient_a = make_test_vector(layout.size(gravity_gradient_block), 0.27); const mfem::Vector gravity_gradient_b = make_test_vector(layout.size(gravity_gradient_block), 1.13); mfem::Vector state_a(layout.value_offsets().Last()); mfem::Vector state_b(layout.value_offsets().Last()); mfem::Vector residual_a; mfem::Vector residual_b; state_a = 0.0; state_b = 0.0; set_block(state_a, layout.value_offsets(), displacement_block, displacement); set_block(state_a, layout.value_offsets(), gravity_gradient_block, gravity_gradient_a); set_block(state_b, layout.value_offsets(), displacement_block, displacement); set_block(state_b, layout.value_offsets(), gravity_gradient_block, gravity_gradient_b); const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; gravity_operator.Prepare(state_a, revisions); gravity_operator.Mult(state_a, residual_a); gravity_operator.Mult(state_b, residual_b); const mfem::Vector mass_action_a = get_block( residual_a, layout.residual_offsets(), gravity_gradient_residual_block); const mfem::Vector mass_action_b = get_block( residual_b, layout.residual_offsets(), gravity_gradient_residual_block); const double energy_a = gravity_gradient_a * mass_action_a; const double energy_b = gravity_gradient_b * mass_action_b; const double cross_ab = gravity_gradient_a * mass_action_b; const double cross_ba = gravity_gradient_b * mass_action_a; const double symmetry_scale = std::max({std::abs(cross_ab), std::abs(cross_ba), 1.0e-14}); INFO("Mapped H(div) energy A = " << energy_a); INFO("Mapped H(div) energy B = " << energy_b); INFO("Mapped H(div) cross action A-M-B = " << cross_ab); INFO("Mapped H(div) cross action B-M-A = " << cross_ba); CHECK(std::isfinite(energy_a)); CHECK(std::isfinite(energy_b)); CHECK(energy_a > 0.0); CHECK(energy_b > 0.0); CHECK_THAT(std::abs(cross_ab - cross_ba) / symmetry_scale, WithinAbs(0.0, 1.0e-11)); } TEST_CASE( "Gravity Field Operator Mult Is Additive In Fields At Fixed Displacement", tags::gravity_operator_unit) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const blocks::form_layout layout = make_gravity_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(make_displacement(f)); const mfem::Vector density = linearization_context.GetDensityMap().gather( make_constant_density(f, 0.73)); const mfem::Vector gravity_gradient = make_test_vector(layout.size(gravity_gradient_block), 0.41); const mfem::Vector gravity_potential = make_test_vector(layout.size(gravity_potential_block), 0.89); mfem::Vector displacement_state(layout.value_offsets().Last()); mfem::Vector density_state(layout.value_offsets().Last()); mfem::Vector gradient_state(layout.value_offsets().Last()); mfem::Vector potential_state(layout.value_offsets().Last()); mfem::Vector complete_state(layout.value_offsets().Last()); displacement_state = 0.0; density_state = 0.0; gradient_state = 0.0; potential_state = 0.0; complete_state = 0.0; set_block(displacement_state, layout.value_offsets(), displacement_block, displacement); set_block(density_state, layout.value_offsets(), displacement_block, displacement); set_block(density_state, layout.value_offsets(), density_block, density); set_block(gradient_state, layout.value_offsets(), displacement_block, displacement); set_block(gradient_state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); set_block(potential_state, layout.value_offsets(), displacement_block, displacement); set_block(potential_state, layout.value_offsets(), gravity_potential_block, gravity_potential); set_block(complete_state, layout.value_offsets(), displacement_block, displacement); set_block(complete_state, layout.value_offsets(), density_block, density); set_block(complete_state, layout.value_offsets(), gravity_gradient_block, gravity_gradient); set_block(complete_state, layout.value_offsets(), gravity_potential_block, gravity_potential); mfem::Vector displacement_residual; mfem::Vector density_residual; mfem::Vector gradient_residual; mfem::Vector potential_residual; mfem::Vector complete_residual; const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; gravity_operator.Prepare(complete_state, revisions); gravity_operator.Mult(displacement_state, displacement_residual); gravity_operator.Mult(density_state, density_residual); gravity_operator.Mult(gradient_state, gradient_residual); gravity_operator.Mult(potential_state, potential_residual); gravity_operator.Mult(complete_state, complete_residual); CHECK_THAT(displacement_residual.Norml2(), WithinAbs(0.0, 1.0e-14)); mfem::Vector additive_residual(density_residual); additive_residual += gradient_residual; additive_residual += potential_residual; CHECK_THAT(relative_difference(complete_residual, additive_residual), WithinAbs(0.0, 1.0e-11)); } TEST_CASE( "Gravity Field Operator Hdiv Mass Action Matches Stateless Quadrature " "Reference", tags::gravity_operator_integration) { const utils::Args args = test_utils::setup_args(); fem::FEM fem = fem::setup_fem(args.mesh_file, args, 0); *fem.displacement = 0.0; constexpr auto displacement_block = mean_field::utils::blocks::get_value_block( blocks::displacement_field.geometry_term); constexpr auto gravity_gradient_block = mean_field::utils::blocks::get_value_block( blocks::gravity_field.gradient_term); constexpr auto gravity_gradient_residual_block = mean_field::utils::blocks::get_residual_block( blocks::gravity_field.gradient_term); const blocks::form_layout layout = make_gravity_layout(fem); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(fem, *fem.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( fem, *fem.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); const MPI_Comm communicator = fem.gravityFluxFes->GetComm(); const mfem::Vector gravity_gradient_true = make_full_support_gravity_gradient(fem); for (const bool deformed : {false, true}) { DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { CAPTURE(deformed); const mfem::Vector displacement_true = make_stateless_reference_displacement(fem, deformed); mfem::Vector state(layout.value_offsets().Last()); state = 0.0; for (int i = 0; i < gravity_gradient_true.Size(); ++i) { state(layout.offset(gravity_gradient_block) + i) = gravity_gradient_true(i); } for (int i = 0; i < displacement_true.Size(); ++i) { state(layout.offset(displacement_block) + i) = displacement_true(i); } const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; gravity_operator.Prepare(state, revisions); mfem::Vector residual; gravity_operator.Mult(state, residual); mfem::Vector operator_action( layout.size(gravity_gradient_residual_block)); for (int i = 0; i < operator_action.Size(); ++i) { operator_action(i) = residual(layout.offset(gravity_gradient_residual_block) + i); } const StatelessHDivMassReference reference = evaluate_stateless_hdiv_mass_quadrature_reference( fem, gravity_gradient_true, displacement_true); mfem::Vector decomposed_reference(reference.stellar_action); decomposed_reference += reference.vacuum_action; mfem::Vector action_difference(operator_action); action_difference -= reference.total_action; const double operator_norm = global_vector_norm(operator_action, communicator); const double reference_norm = global_vector_norm(reference.total_action, communicator); const double stellar_action_norm = global_vector_norm(reference.stellar_action, communicator); const double vacuum_action_norm = global_vector_norm(reference.vacuum_action, communicator); const double absolute_action_error = global_vector_norm(action_difference, communicator); const double relative_action_error = global_relative_vector_error( operator_action, reference.total_action, communicator); const double decomposition_error = global_relative_vector_error( reference.total_action, decomposed_reference, communicator); const double operator_energy = global_vector_dot( gravity_gradient_true, operator_action, communicator); const double reference_energy = global_vector_dot( gravity_gradient_true, reference.total_action, communicator); const double stellar_energy = global_vector_dot( gravity_gradient_true, reference.stellar_action, communicator); const double vacuum_energy = global_vector_dot( gravity_gradient_true, reference.vacuum_action, communicator); const double relative_energy_error = std::abs(operator_energy - reference_energy) / std::max(std::abs(reference_energy), std::numeric_limits::epsilon()); const double vacuum_action_fraction = vacuum_action_norm / std::max(reference_norm, std::numeric_limits::epsilon()); const double vacuum_energy_fraction = vacuum_energy / reference_energy; const double stellar_vacuum_dot = global_vector_dot( reference.stellar_action, reference.vacuum_action, communicator); const double stellar_vacuum_alignment = stellar_vacuum_dot / std::max(stellar_action_norm * vacuum_action_norm, std::numeric_limits::epsilon()); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("Stellar elements = " << reference.stellar_elements); INFO("Vacuum elements = " << reference.vacuum_elements); INFO("Stellar quadrature points = " << reference.stellar_quadrature_points); INFO("Vacuum quadrature points = " << reference.vacuum_quadrature_points); INFO("Stellar mapping determinant range = [" << reference.minimum_stellar_determinant << ", " << reference.maximum_stellar_determinant << "]"); INFO("Vacuum mapping determinant range = [" << reference.minimum_vacuum_determinant << ", " << reference.maximum_vacuum_determinant << "]"); INFO("Operator mass-action norm = " << operator_norm); INFO("Stateless quadrature-reference norm = " << reference_norm); INFO("Stellar action norm = " << stellar_action_norm); INFO("Vacuum action norm = " << vacuum_action_norm); INFO("Vacuum action fraction = " << vacuum_action_fraction); INFO("Stellar-vacuum action alignment = " << stellar_vacuum_alignment); INFO("Absolute action error = " << absolute_action_error); INFO("Relative action error = " << relative_action_error); INFO("Domain-decomposition error = " << decomposition_error); INFO("Operator quadratic energy = " << operator_energy); INFO("Quadrature-reference energy = " << reference_energy); INFO("Stellar energy = " << stellar_energy); INFO("Vacuum energy = " << vacuum_energy); INFO("Vacuum energy fraction = " << vacuum_energy_fraction); INFO("Relative energy error = " << relative_energy_error); REQUIRE(reference.stellar_elements > 0); REQUIRE(reference.vacuum_elements > 0); REQUIRE(reference.stellar_quadrature_points > 0); REQUIRE(reference.vacuum_quadrature_points > 0); REQUIRE(reference.minimum_stellar_determinant > 0.0); REQUIRE(reference.minimum_vacuum_determinant > 0.0); REQUIRE(reference_energy > 0.0); REQUIRE(stellar_energy > 0.0); REQUIRE(vacuum_energy > 0.0); constexpr double decomposition_tolerance = 5.0e-14; constexpr double action_tolerance = 1.0e-11; constexpr double energy_tolerance = 1.0e-11; CHECK_THAT(decomposition_error, Catch::Matchers::WithinAbs(0.0, decomposition_tolerance)); CHECK_THAT(relative_action_error, Catch::Matchers::WithinAbs(0.0, action_tolerance)); CHECK_THAT(relative_energy_error, Catch::Matchers::WithinAbs(0.0, energy_tolerance)); } } } TEST_CASE( "Gravity Field Jacobian Fixed Geometry Blocks Match Centered Differences", tags::gravity_operator_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; const gravity_layout layout = make_gravity_jacobian_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); mfem::Vector state(layout.value_offsets().Last()); state = 0.0; const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather( make_stateless_reference_displacement(f, true)); set_value_block(state, layout, displacement_block, displacement); const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; gravity_operator.Prepare(state, revisions); mfem::Operator &gradient = gravity_operator.GetGradient(state); MPI_Comm communicator = f.mesh->GetComm(); struct DirectionCase { std::string name; mfem::Vector direction; bool expect_gradient_residual; bool expect_poisson_residual; }; std::vector cases; cases.push_back({"density", make_density_direction(layout), false, true}); cases.push_back({"gravity gradient", make_gravity_gradient_direction(layout), true, true}); cases.push_back({"gravity potential", make_gravity_potential_direction(layout), true, false}); for (const DirectionCase &direction_case : cases) { DYNAMIC_SECTION("Direction = " << direction_case.name) { constexpr double difference_step = 1.0e-6; mfem::Vector jacobian_action; gradient.Mult(direction_case.direction, jacobian_action); const mfem::Vector finite_difference = evaluate_centered_difference( gravity_operator, state, direction_case.direction, difference_step); const mfem::Vector gradient_action = get_residual_block( jacobian_action, layout, gravity_gradient_residual_block); const mfem::Vector poisson_action = get_residual_block( jacobian_action, layout, gravity_poisson_residual_block); const double relative_error = global_relative_vector_error( jacobian_action, finite_difference, communicator); const double gradient_norm = global_vector_norm(gradient_action, communicator); const double poisson_norm = global_vector_norm(poisson_action, communicator); INFO("Direction = " << direction_case.name); INFO("Jacobian action norm = " << global_vector_norm(jacobian_action, communicator)); INFO("Finite-difference action norm = " << global_vector_norm(finite_difference, communicator)); INFO("Gradient residual action norm = " << gradient_norm); INFO("Poisson residual action norm = " << poisson_norm); INFO("Relative Jacobian error = " << relative_error); constexpr double jacobian_tolerance = 2.0e-8; constexpr double zero_tolerance = 1.0e-13; CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, jacobian_tolerance)); if (direction_case.expect_gradient_residual) { CHECK(gradient_norm > zero_tolerance); } else { CHECK_THAT(gradient_norm, Catch::Matchers::WithinAbs(0.0, zero_tolerance)); } if (direction_case.expect_poisson_residual) { CHECK(poisson_norm > zero_tolerance); } else { CHECK_THAT(poisson_norm, Catch::Matchers::WithinAbs(0.0, zero_tolerance)); } } } } TEST_CASE( "Gravity Field Jacobian Combined Fixed Geometry Direction Matches Centered " "Differences", tags::gravity_operator_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; const gravity_layout layout = make_gravity_jacobian_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); const mfem::Vector direction = make_combined_fixed_geometry_direction(layout); operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; gravity_operator.Prepare(state, revisions); mfem::Operator &gradient = gravity_operator.GetGradient(state); mfem::Vector jacobian_action; gradient.Mult(direction, jacobian_action); MPI_Comm communicator = f.mesh->GetComm(); double best_error = std::numeric_limits::infinity(); for (const double step : std::array{1.0e-4, 1.0e-6, 1.0e-7}) { const mfem::Vector finite_difference = evaluate_centered_difference(gravity_operator, state, direction, step); const double relative_error = global_relative_vector_error( jacobian_action, finite_difference, communicator); best_error = std::min(best_error, relative_error); INFO("Centered-difference step = " << step); INFO("Relative Jacobian error = " << relative_error); CHECK(relative_error < 2.0e-7); } INFO("Best relative Jacobian error = " << best_error); CHECK(best_error < 2.0e-8); } TEST_CASE("Gravity Field Jacobian Action Is Linear At Fixed Geometry", tags::gravity_operator_unit) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; const gravity_layout layout = make_gravity_jacobian_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); const mfem::Vector state = make_gravity_jacobian_state(f, layout, false); const mfem::Vector direction_a = make_density_direction(layout); const mfem::Vector direction_b = make_gravity_gradient_direction(layout); const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; gravity_operator.Prepare(state, revisions); mfem::Operator &gradient = gravity_operator.GetGradient(state); constexpr double scale_a = 1.7; constexpr double scale_b = -0.6; mfem::Vector combined_direction(direction_a); combined_direction *= scale_a; combined_direction.Add(scale_b, direction_b); mfem::Vector action_a; mfem::Vector action_b; mfem::Vector combined_action; gradient.Mult(direction_a, action_a); gradient.Mult(direction_b, action_b); gradient.Mult(combined_direction, combined_action); mfem::Vector expected_action(action_a); expected_action *= scale_a; expected_action.Add(scale_b, action_b); MPI_Comm communicator = f.mesh->GetComm(); const double relative_error = global_relative_vector_error( combined_action, expected_action, communicator); INFO("Relative Jacobian linearity error = " << relative_error); CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, 2.0e-13)); } TEST_CASE("Gravity Field Prepare Updates Shared Linearization Context", tags::gravity_operator_unit) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; const gravity_layout layout = make_gravity_jacobian_layout(f); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); mfem::Vector state_a = make_gravity_jacobian_state(f, layout, false); const mfem::Vector state_b = make_gravity_jacobian_state(f, layout, true); const operators::context::gravity_field::GravityFieldRevisions revisions_a{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; const operators::context::gravity_field::GravityFieldRevisions revisions_b{ .discretization = {1}, .displacement = {2}, .density = {2}, .gravity_gradient = {2}, .gravity_potential = {2}}; MPI_Comm communicator = f.mesh->GetComm(); gravity_operator.Prepare(state_a, revisions_a); mfem::Operator &returned_gradient_a = gravity_operator.GetGradient(state_a); REQUIRE(&returned_gradient_a == static_cast(&gravity_jacobian)); check_linearization_context_matches_state( linearization_context, state_a, layout.value_offsets(), communicator); state_a = 0.0; CHECK(linearization_context.IsPrepared()); const double stored_state_norm = global_vector_norm(linearization_context.GetDensityTrue(), communicator) + global_vector_norm( linearization_context.GetGeometryContext().GetDisplacementTrue(), communicator) + global_vector_norm(linearization_context.GetGravityGradientTrue(), communicator); CHECK(stored_state_norm > 0.0); gravity_operator.Prepare(state_b, revisions_b); mfem::Operator &returned_gradient_b = gravity_operator.GetGradient(state_b); REQUIRE(&returned_gradient_b == static_cast(&gravity_jacobian)); check_linearization_context_matches_state( linearization_context, state_b, layout.value_offsets(), communicator); } TEST_CASE("Mapped Hdiv Mass Variation Is Linear In Displacement Direction", tags::gravity_kernel_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); mfem::Vector zero_direction(f.displacementFes->GetTrueVSize()); mfem::Vector zero_gravity_gradient(f.gravityFluxFes->GetTrueVSize()); zero_direction = 0.0; zero_gravity_gradient = 0.0; mfem::Vector zero_direction_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, zero_direction, zero_direction_action); INFO("Zero-direction action norm = " << global_norm(zero_direction_action, communicator)); CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); mfem::Vector zero_gravity_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, zero_gravity_gradient, fields.displacement, fields.displacement_direction_1, zero_gravity_action); INFO("Zero-gravity action norm = " << global_norm(zero_gravity_action, communicator)); CHECK(global_norm(zero_gravity_action, communicator) < 1.0e-13); constexpr double scale_1 = 1.7; constexpr double scale_2 = -0.6; mfem::Vector combined_direction(fields.displacement_direction_1); combined_direction *= scale_1; combined_direction.Add(scale_2, fields.displacement_direction_2); mfem::Vector action_1; mfem::Vector action_2; mfem::Vector combined_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, action_1); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_2, action_2); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, combined_direction, combined_action); mfem::Vector expected_action(action_1); expected_action *= scale_1; expected_action.Add(scale_2, action_2); const double linearity_error = global_relative_error(combined_action, expected_action, communicator); INFO("Combined action norm = " << global_norm(combined_action, communicator)); INFO("Expected action norm = " << global_norm(expected_action, communicator)); INFO("Displacement-direction linearity error = " << linearity_error); CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12)); } TEST_CASE("Mapped Hdiv Mass Variation Matches Centered Geometry Differences", tags::gravity_kernel_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); constexpr double difference_step = 1.0e-5; constexpr double comparison_tolerance = 2.0e-7; mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); identity_displacement = 0.0; for (const bool deformed : std::array{false, true}) { const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; mfem::Vector analytic_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, analytic_action); const mfem::Vector finite_difference_action = centered_hdiv_mass_geometry_difference( f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, difference_step); const double relative_error = global_relative_error( analytic_action, finite_difference_action, communicator); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("Analytic variation norm = " << global_norm(analytic_action, communicator)); INFO("Finite-difference variation norm = " << global_norm(finite_difference_action, communicator)); INFO("Relative H(div) mass-variation error = " << relative_error); CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); } } TEST_CASE( "Mapped Hdiv Mass Geometry Difference Converges To Analytic Variation", tags::gravity_kernel_convergence) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); mfem::Vector analytic_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, analytic_action); constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; std::array errors{}; for (std::size_t i = 0; i < difference_steps.size(); ++i) { const mfem::Vector finite_difference_action = centered_hdiv_mass_geometry_difference( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, difference_steps[i]); errors[i] = global_relative_error(finite_difference_action, analytic_action, communicator); INFO("Difference step = " << difference_steps[i]); INFO("Relative error = " << errors[i]); } const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); INFO("First observed convergence order = " << first_observed_order); INFO("Second observed convergence order = " << second_observed_order); CHECK(errors[1] < errors[0]); CHECK(errors[2] < errors[1]); CHECK(first_observed_order > 1.8); CHECK(second_observed_order > 1.8); } TEST_CASE("Mapped Source Variation Is Linear In Displacement Direction", tags::gravity_kernel_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector density = make_source_variation_density(f); MPI_Comm communicator = f.mesh->GetComm(); mfem::Vector zero_density(f.densityFes->GetTrueVSize()); mfem::Vector zero_direction(f.displacementFes->GetTrueVSize()); zero_density = 0.0; zero_direction = 0.0; mfem::Vector zero_direction_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, zero_direction, zero_direction_action); REQUIRE(zero_direction_action.Size() == f.gravityPotentialFes->GetTrueVSize()); CHECK(global_norm(zero_direction_action, communicator) < 1.0e-13); mfem::Vector zero_density_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, zero_density, fields.displacement, fields.displacement_direction_1, zero_density_action); REQUIRE(zero_density_action.Size() == f.gravityPotentialFes->GetTrueVSize()); CHECK(global_norm(zero_density_action, communicator) < 1.0e-13); constexpr double scale_1 = 1.4; constexpr double scale_2 = -0.7; mfem::Vector combined_direction(fields.displacement_direction_1); combined_direction *= scale_1; combined_direction.Add(scale_2, fields.displacement_direction_2); mfem::Vector action_1; mfem::Vector action_2; mfem::Vector combined_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, action_1); operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, fields.displacement_direction_2, action_2); operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, combined_direction, combined_action); REQUIRE(action_1.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(action_2.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize()); mfem::Vector expected_action(action_1); expected_action *= scale_1; expected_action.Add(scale_2, action_2); const double linearity_error = global_relative_error(combined_action, expected_action, communicator); INFO("Combined source-variation norm = " << global_norm(combined_action, communicator)); INFO("Expected source-variation norm = " << global_norm(expected_action, communicator)); INFO("Source-variation linearity error = " << linearity_error); CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 2.0e-12)); } TEST_CASE("Mapped Source Variation Matches Centered Geometry Differences", tags::gravity_kernel_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector density = make_source_variation_density(f); MPI_Comm communicator = f.mesh->GetComm(); constexpr double difference_step = 1.0e-5; constexpr double comparison_tolerance = 2.0e-7; mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); identity_displacement = 0.0; for (const bool deformed : std::array{false, true}) { const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; mfem::Vector analytic_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, base_displacement, fields.displacement_direction_1, analytic_action); const mfem::Vector finite_difference_action = centered_source_geometry_difference( f, domain_mapper, density, base_displacement, fields.displacement_direction_1, difference_step); REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize()); const double relative_error = global_relative_error( analytic_action, finite_difference_action, communicator); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("Analytic source-variation norm = " << global_norm(analytic_action, communicator)); INFO("Finite-difference source-variation norm = " << global_norm(finite_difference_action, communicator)); INFO("Relative source-variation error = " << relative_error); CHECK_THAT(relative_error, Catch::Matchers::WithinAbs(0.0, comparison_tolerance)); } } TEST_CASE("Mapped Source Geometry Difference Converges To Analytic Variation", tags::gravity_kernel_convergence) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector density = make_source_variation_density(f); MPI_Comm communicator = f.mesh->GetComm(); mfem::Vector analytic_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, analytic_action); REQUIRE(analytic_action.Size() == f.gravityPotentialFes->GetTrueVSize()); constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; std::array errors{}; for (std::size_t i = 0; i < difference_steps.size(); ++i) { const mfem::Vector finite_difference_action = centered_source_geometry_difference( f, domain_mapper, density, fields.displacement, fields.displacement_direction_1, difference_steps[i]); REQUIRE(finite_difference_action.Size() == f.gravityPotentialFes->GetTrueVSize()); errors[i] = global_relative_error(finite_difference_action, analytic_action, communicator); } const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); INFO("Errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); INFO("First observed convergence order = " << first_observed_order); INFO("Second observed convergence order = " << second_observed_order); CHECK(errors[1] < errors[0]); CHECK(errors[2] < errors[1]); CHECK(first_observed_order > 1.8); CHECK(second_observed_order > 1.8); } TEST_CASE("Mapped Hdiv Mass Variation Is Symmetric", tags::gravity_kernel_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector second_gravity_gradient = make_secondary_gravity_gradient(f); MPI_Comm communicator = f.gravityFluxFes->GetComm(); mfem::Vector identity_displacement(f.displacementFes->GetTrueVSize()); identity_displacement = 0.0; for (const bool deformed : std::array{false, true}) { const mfem::Vector &base_displacement = deformed ? fields.displacement : identity_displacement; mfem::Vector first_action; mfem::Vector second_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, base_displacement, fields.displacement_direction_1, first_action); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, second_gravity_gradient, base_displacement, fields.displacement_direction_1, second_action); REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize()); REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize()); const double left_pairing = global_dot(fields.gravity_gradient, second_action, communicator); const double right_pairing = global_dot(second_gravity_gradient, first_action, communicator); const double symmetry_error = std::abs(left_pairing - right_pairing) / std::max({std::abs(left_pairing), std::abs(right_pairing), 1.0e-14}); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("g1^T delta_M g2 = " << left_pairing); INFO("g2^T delta_M g1 = " << right_pairing); INFO("Relative symmetry error = " << symmetry_error); CHECK_THAT(symmetry_error, Catch::Matchers::WithinAbs(0.0, 1.0e-11)); } } TEST_CASE("Mapped Geometry Variations Are Linear In Base Fields", tags::gravity_kernel_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector second_gravity_gradient = make_secondary_gravity_gradient(f); const mfem::Vector first_density = make_source_variation_density(f); const mfem::Vector second_density = make_secondary_source_density(f); constexpr double scale_1 = 1.3; constexpr double scale_2 = -0.4; { MPI_Comm communicator = f.gravityFluxFes->GetComm(); mfem::Vector combined_gravity_gradient(fields.gravity_gradient); combined_gravity_gradient *= scale_1; combined_gravity_gradient.Add(scale_2, second_gravity_gradient); mfem::Vector first_action; mfem::Vector second_action; mfem::Vector combined_action; operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, fields.gravity_gradient, fields.displacement, fields.displacement_direction_1, first_action); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, second_gravity_gradient, fields.displacement, fields.displacement_direction_1, second_action); operators::kernels::apply_mapped_hdiv_mass_variation( f, domain_mapper, combined_gravity_gradient, fields.displacement, fields.displacement_direction_1, combined_action); REQUIRE(first_action.Size() == f.gravityFluxFes->GetTrueVSize()); REQUIRE(second_action.Size() == f.gravityFluxFes->GetTrueVSize()); REQUIRE(combined_action.Size() == f.gravityFluxFes->GetTrueVSize()); mfem::Vector expected_action(first_action); expected_action *= scale_1; expected_action.Add(scale_2, second_action); const double linearity_error = global_relative_error(combined_action, expected_action, communicator); INFO("H(div) base-field linearity error = " << linearity_error); CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12)); } { MPI_Comm communicator = f.mesh->GetComm(); mfem::Vector combined_density(first_density); combined_density *= scale_1; combined_density.Add(scale_2, second_density); mfem::Vector first_action; mfem::Vector second_action; mfem::Vector combined_action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, first_density, fields.displacement, fields.displacement_direction_1, first_action); operators::kernels::apply_mapped_source_variation( f, domain_mapper, second_density, fields.displacement, fields.displacement_direction_1, second_action); operators::kernels::apply_mapped_source_variation( f, domain_mapper, combined_density, fields.displacement, fields.displacement_direction_1, combined_action); REQUIRE(first_action.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(second_action.Size() == f.gravityPotentialFes->GetTrueVSize()); REQUIRE(combined_action.Size() == f.gravityPotentialFes->GetTrueVSize()); mfem::Vector expected_action(first_action); expected_action *= scale_1; expected_action.Add(scale_2, second_action); const double linearity_error = global_relative_error(combined_action, expected_action, communicator); INFO("Source base-field linearity error = " << linearity_error); CHECK_THAT(linearity_error, Catch::Matchers::WithinAbs(0.0, 3.0e-12)); } } TEST_CASE("Mapped Source Variation Ignores Vacuum Density", tags::gravity_kernel_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); const mapping::DomainMapper &domain_mapper = *f.domainMapperStateless; const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const int vacuum_attribute = field_dof_test_utils::vacuum_material_attribute; const mfem::Vector vacuum_density = make_vacuum_only_density(f, vacuum_attribute); MPI_Comm communicator = f.mesh->GetComm(); const double vacuum_density_norm = global_norm(vacuum_density, communicator); REQUIRE(vacuum_density_norm > 0.0); mfem::Vector action; operators::kernels::apply_mapped_source_variation( f, domain_mapper, vacuum_density, fields.displacement, fields.displacement_direction_1, action); REQUIRE(action.Size() == f.gravityPotentialFes->GetTrueVSize()); const double action_norm = global_norm(action, communicator); INFO("Vacuum density norm = " << vacuum_density_norm); INFO("Source-variation action norm = " << action_norm); CHECK_THAT(action_norm, Catch::Matchers::WithinAbs(0.0, 1.0e-14)); } TEST_CASE("Gravity Field Jacobian Displacement Blocks Match Geometry Kernels", tags::gravity_operator_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); const operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; gravity_operator.Prepare(state, revisions); mfem::Vector jacobian_action; gravity_jacobian.Mult(direction, jacobian_action); REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); const mfem::Vector &density_true = linearization_context.GetDensityTrue(); const mfem::Vector &displacement_true = linearization_context.GetGeometryContext().GetDisplacementTrue(); const mfem::Vector &gravity_gradient_true = linearization_context.GetGravityGradientTrue(); const mfem::Vector gradient_action = get_residual_block( jacobian_action, layout, gravity_gradient_residual_block); const mfem::Vector poisson_action = get_residual_block( jacobian_action, layout, gravity_poisson_residual_block); mfem::Vector expected_gradient_action_true; mfem::Vector expected_poisson_action_true; operators::kernels::apply_mapped_hdiv_mass_variation( f, *f.domainMapperStateless, gravity_gradient_true, displacement_true, fields.displacement_direction_1, expected_gradient_action_true); operators::kernels::apply_mapped_source_variation( f, *f.domainMapperStateless, density_true, displacement_true, fields.displacement_direction_1, expected_poisson_action_true); expected_poisson_action_true *= -1.0; const mfem::Vector expected_gradient_action = linearization_context.GetGravityGradientMap().gather( expected_gradient_action_true); const mfem::Vector expected_poisson_action = linearization_context.GetGravityPotentialMap().gather( expected_poisson_action_true); MPI_Comm communicator = f.mesh->GetComm(); const double gradient_error = global_relative_error( gradient_action, expected_gradient_action, communicator); const double poisson_error = global_relative_error( poisson_action, expected_poisson_action, communicator); INFO("Gradient displacement action norm = " << global_norm(gradient_action, communicator)); INFO("Poisson displacement action norm = " << global_norm(poisson_action, communicator)); INFO("Gradient displacement-block error = " << gradient_error); INFO("Poisson displacement-block error = " << poisson_error); REQUIRE(global_norm(gradient_action, communicator) > 1.0e-12); REQUIRE(global_norm(poisson_action, communicator) > 1.0e-12); CHECK_THAT(gradient_error, WithinAbs(0.0, 2.0e-12)); CHECK_THAT(poisson_error, WithinAbs(0.0, 2.0e-12)); } TEST_CASE("Gravity Field Jacobian Displacement Direction Matches Centered " "Differences", tags::gravity_operator_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); constexpr double difference_step = 1.0e-5; constexpr double comparison_tolerance = 3.0e-7; constexpr double nonzero_tolerance = 1.0e-12; MPI_Comm communicator = f.mesh->GetComm(); REQUIRE(direction.Size() == layout.value_offsets().Last()); REQUIRE(global_vector_norm(direction, communicator) > nonzero_tolerance); for (const bool deformed : std::array{false, true}) { DYNAMIC_SECTION("Geometry = " << (deformed ? "deformed" : "identity")) { const mfem::Vector state = make_gravity_jacobian_state(f, layout, deformed); REQUIRE(state.Size() == layout.value_offsets().Last()); const operators::context::gravity_field::GravityFieldRevisions base_revisions{.discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; gravity_operator.Prepare(state, base_revisions); REQUIRE(linearization_context.IsPrepared()); check_linearization_context_matches_state( linearization_context, state, layout.value_offsets(), communicator); const double base_density_norm = global_vector_norm( linearization_context.GetDensityTrue(), communicator); const double base_gravity_gradient_norm = global_vector_norm( linearization_context.GetGravityGradientTrue(), communicator); INFO("Base density norm = " << base_density_norm); INFO("Base gravity-gradient norm = " << base_gravity_gradient_norm); REQUIRE(base_density_norm > nonzero_tolerance); REQUIRE(base_gravity_gradient_norm > nonzero_tolerance); mfem::Operator &gradient = gravity_operator.GetGradient(state); REQUIRE(&gradient == static_cast(&gravity_jacobian)); mfem::Vector jacobian_action; gradient.Mult(direction, jacobian_action); REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(difference_step, direction); minus_state.Add(-difference_step, direction); auto plus_revisions = base_revisions; plus_revisions.displacement.value++; mfem::Vector plus_residual; gravity_operator.Prepare(plus_state, plus_revisions); gravity_operator.Mult(plus_state, plus_residual); REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); auto minus_revisions = plus_revisions; minus_revisions.displacement.value++; mfem::Vector minus_residual; gravity_operator.Prepare(minus_state, minus_revisions); gravity_operator.Mult(minus_state, minus_residual); REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); mfem::Vector finite_difference(plus_residual); finite_difference -= minus_residual; finite_difference /= 2.0 * difference_step; const mfem::Vector gradient_action = get_residual_block( jacobian_action, layout, gravity_gradient_residual_block); const mfem::Vector poisson_action = get_residual_block( jacobian_action, layout, gravity_poisson_residual_block); const mfem::Vector finite_difference_gradient = get_residual_block( finite_difference, layout, gravity_gradient_residual_block); const mfem::Vector finite_difference_poisson = get_residual_block( finite_difference, layout, gravity_poisson_residual_block); const double jacobian_gradient_norm = global_vector_norm(gradient_action, communicator); const double jacobian_poisson_norm = global_vector_norm(poisson_action, communicator); const double finite_difference_gradient_norm = global_vector_norm(finite_difference_gradient, communicator); const double finite_difference_poisson_norm = global_vector_norm(finite_difference_poisson, communicator); REQUIRE(jacobian_gradient_norm > nonzero_tolerance); REQUIRE(jacobian_poisson_norm > nonzero_tolerance); REQUIRE(finite_difference_gradient_norm > nonzero_tolerance); REQUIRE(finite_difference_poisson_norm > nonzero_tolerance); mfem::Vector gradient_difference(gradient_action); gradient_difference -= finite_difference_gradient; mfem::Vector poisson_difference(poisson_action); poisson_difference -= finite_difference_poisson; mfem::Vector combined_difference(jacobian_action); combined_difference -= finite_difference; const double absolute_gradient_error = global_vector_norm(gradient_difference, communicator); const double absolute_poisson_error = global_vector_norm(poisson_difference, communicator); const double absolute_combined_error = global_vector_norm(combined_difference, communicator); const double gradient_error = global_relative_error( gradient_action, finite_difference_gradient, communicator); const double poisson_error = global_relative_error( poisson_action, finite_difference_poisson, communicator); const double combined_error = global_relative_error( jacobian_action, finite_difference, communicator); INFO("Geometry = " << (deformed ? "deformed" : "identity")); INFO("Jacobian gradient action norm = " << jacobian_gradient_norm); INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm); INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm); INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm); INFO("Absolute gradient residual error = " << absolute_gradient_error); INFO("Absolute Poisson residual error = " << absolute_poisson_error); INFO("Absolute combined residual error = " << absolute_combined_error); INFO("Gradient residual relative error = " << gradient_error); INFO("Poisson residual relative error = " << poisson_error); INFO("Combined residual relative error = " << combined_error); REQUIRE(std::isfinite(gradient_error)); REQUIRE(std::isfinite(poisson_error)); REQUIRE(std::isfinite(combined_error)); CHECK_THAT(gradient_error, WithinAbs(0.0, comparison_tolerance)); CHECK_THAT(poisson_error, WithinAbs(0.0, comparison_tolerance)); CHECK_THAT(combined_error, WithinAbs(0.0, comparison_tolerance)); } } } TEST_CASE( "Gravity Field Jacobian Displacement Difference Converges At Second Order", tags::gravity_operator_convergence) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); const mfem::Vector direction = make_displacement_direction(layout, fields.displacement_direction_1); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); operators::context::gravity_field::GravityFieldRevisions revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; MPI_Comm communicator = f.mesh->GetComm(); gravity_operator.Prepare(state, revisions); REQUIRE(linearization_context.IsPrepared()); check_linearization_context_matches_state( linearization_context, state, layout.value_offsets(), communicator); mfem::Operator &gradient = gravity_operator.GetGradient(state); REQUIRE(&gradient == static_cast(&gravity_jacobian)); mfem::Vector jacobian_action; gradient.Mult(direction, jacobian_action); REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); const double jacobian_action_norm = global_vector_norm(jacobian_action, communicator); INFO("Jacobian action norm = " << jacobian_action_norm); REQUIRE(jacobian_action_norm > 1.0e-12); auto evaluate_geometry_centered_difference = [&](const double difference_step) { mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(difference_step, direction); minus_state.Add(-difference_step, direction); revisions.displacement.value++; mfem::Vector plus_residual; gravity_operator.Prepare(plus_state, revisions); gravity_operator.Mult(plus_state, plus_residual); revisions.displacement.value++; mfem::Vector minus_residual; gravity_operator.Prepare(minus_state, revisions); gravity_operator.Mult(minus_state, minus_residual); REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); mfem::Vector finite_difference(plus_residual); finite_difference -= minus_residual; finite_difference /= 2.0 * difference_step; return finite_difference; }; constexpr std::array difference_steps{8.0e-2, 4.0e-2, 2.0e-2}; std::array errors{}; std::array finite_difference_norms{}; for (std::size_t i = 0; i < difference_steps.size(); ++i) { const mfem::Vector finite_difference = evaluate_geometry_centered_difference(difference_steps[i]); finite_difference_norms[i] = global_vector_norm(finite_difference, communicator); errors[i] = global_relative_error(finite_difference, jacobian_action, communicator); INFO("Step = " << difference_steps[i] << ", finite-difference norm = " << finite_difference_norms[i] << ", relative error = " << errors[i]); REQUIRE(finite_difference_norms[i] > 1.0e-12); REQUIRE(std::isfinite(errors[i])); REQUIRE(errors[i] > 0.0); } const double first_observed_order = std::log(errors[0] / errors[1]) / std::log(2.0); const double second_observed_order = std::log(errors[1] / errors[2]) / std::log(2.0); INFO("Difference steps = [" << difference_steps[0] << ", " << difference_steps[1] << ", " << difference_steps[2] << "]"); INFO("Finite-difference norms = [" << finite_difference_norms[0] << ", " << finite_difference_norms[1] << ", " << finite_difference_norms[2] << "]"); INFO("Relative errors = [" << errors[0] << ", " << errors[1] << ", " << errors[2] << "]"); INFO("First observed convergence order = " << first_observed_order); INFO("Second observed convergence order = " << second_observed_order); REQUIRE(std::isfinite(first_observed_order)); REQUIRE(std::isfinite(second_observed_order)); CHECK(errors[1] < errors[0]); CHECK(errors[2] < errors[1]); CHECK(first_observed_order > 1.8); CHECK(second_observed_order > 1.8); } TEST_CASE("Gravity Field Jacobian Complete Coupled Direction Matches Centered " "Differences", tags::gravity_operator_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); *f.displacement = 0.0; REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector state = make_gravity_jacobian_state(f, layout, true); mfem::Vector direction = make_combined_fixed_geometry_direction(layout); set_value_block(direction, layout, displacement_block, fields.displacement_direction_2); operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); operators::context::gravity_field::GravityFieldRevisions base_revisions{ .discretization = {1}, .displacement = {1}, .density = {1}, .gravity_gradient = {1}, .gravity_potential = {1}}; MPI_Comm communicator = f.mesh->GetComm(); REQUIRE(state.Size() == layout.value_offsets().Last()); REQUIRE(direction.Size() == layout.value_offsets().Last()); REQUIRE(global_vector_norm(direction, communicator) > 1.0e-12); gravity_operator.Prepare(state, base_revisions); REQUIRE(linearization_context.IsPrepared()); check_linearization_context_matches_state( linearization_context, state, layout.value_offsets(), communicator); mfem::Operator &gradient = gravity_operator.GetGradient(state); REQUIRE(&gradient == static_cast(&gravity_jacobian)); mfem::Vector jacobian_action; gradient.Mult(direction, jacobian_action); REQUIRE(jacobian_action.Size() == layout.residual_offsets().Last()); constexpr double difference_step = 1.0e-5; mfem::Vector plus_state(state); mfem::Vector minus_state(state); plus_state.Add(difference_step, direction); minus_state.Add(-difference_step, direction); auto plus_revisions = base_revisions; plus_revisions.displacement.value++; plus_revisions.density.value++; plus_revisions.gravity_gradient.value++; plus_revisions.gravity_potential.value++; mfem::Vector plus_residual; gravity_operator.Prepare(plus_state, plus_revisions); gravity_operator.Mult(plus_state, plus_residual); REQUIRE(plus_residual.Size() == layout.residual_offsets().Last()); auto minus_revisions = plus_revisions; minus_revisions.displacement.value++; minus_revisions.density.value++; minus_revisions.gravity_gradient.value++; minus_revisions.gravity_potential.value++; mfem::Vector minus_residual; gravity_operator.Prepare(minus_state, minus_revisions); gravity_operator.Mult(minus_state, minus_residual); REQUIRE(minus_residual.Size() == layout.residual_offsets().Last()); mfem::Vector finite_difference(plus_residual); finite_difference -= minus_residual; finite_difference /= 2.0 * difference_step; const mfem::Vector gradient_action = get_residual_block( jacobian_action, layout, gravity_gradient_residual_block); const mfem::Vector poisson_action = get_residual_block( jacobian_action, layout, gravity_poisson_residual_block); const mfem::Vector finite_difference_gradient = get_residual_block( finite_difference, layout, gravity_gradient_residual_block); const mfem::Vector finite_difference_poisson = get_residual_block( finite_difference, layout, gravity_poisson_residual_block); const double jacobian_gradient_norm = global_vector_norm(gradient_action, communicator); const double jacobian_poisson_norm = global_vector_norm(poisson_action, communicator); const double finite_difference_gradient_norm = global_vector_norm(finite_difference_gradient, communicator); const double finite_difference_poisson_norm = global_vector_norm(finite_difference_poisson, communicator); constexpr double nonzero_tolerance = 1.0e-12; REQUIRE(jacobian_gradient_norm > nonzero_tolerance); REQUIRE(jacobian_poisson_norm > nonzero_tolerance); REQUIRE(finite_difference_gradient_norm > nonzero_tolerance); REQUIRE(finite_difference_poisson_norm > nonzero_tolerance); mfem::Vector gradient_difference(gradient_action); gradient_difference -= finite_difference_gradient; mfem::Vector poisson_difference(poisson_action); poisson_difference -= finite_difference_poisson; mfem::Vector combined_difference(jacobian_action); combined_difference -= finite_difference; const double absolute_gradient_error = global_vector_norm(gradient_difference, communicator); const double absolute_poisson_error = global_vector_norm(poisson_difference, communicator); const double absolute_combined_error = global_vector_norm(combined_difference, communicator); const double gradient_error = global_relative_error( gradient_action, finite_difference_gradient, communicator); const double poisson_error = global_relative_error( poisson_action, finite_difference_poisson, communicator); const double combined_error = global_relative_error(jacobian_action, finite_difference, communicator); INFO("Jacobian gradient action norm = " << jacobian_gradient_norm); INFO("Finite-difference gradient norm = " << finite_difference_gradient_norm); INFO("Jacobian Poisson action norm = " << jacobian_poisson_norm); INFO("Finite-difference Poisson norm = " << finite_difference_poisson_norm); INFO("Absolute gradient residual error = " << absolute_gradient_error); INFO("Absolute Poisson residual error = " << absolute_poisson_error); INFO("Absolute combined residual error = " << absolute_combined_error); INFO("Gradient residual relative error = " << gradient_error); INFO("Poisson residual relative error = " << poisson_error); INFO("Combined residual relative error = " << combined_error); REQUIRE(std::isfinite(gradient_error)); REQUIRE(std::isfinite(poisson_error)); REQUIRE(std::isfinite(combined_error)); CHECK_THAT(gradient_error, WithinAbs(0.0, 5.0e-7)); CHECK_THAT(poisson_error, WithinAbs(0.0, 5.0e-7)); CHECK_THAT(combined_error, WithinAbs(0.0, 5.0e-7)); } TEST_CASE( "Reduced Gravity Field Operator Solves Deformed Gravity System With MINRES", tags::gravity_operator_integration) { auto args = test_utils::setup_args(); fem::FEM f = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.domainMapperStateless != nullptr); REQUIRE(f.domainMapperStateless != nullptr); const gravity_layout layout = make_gravity_jacobian_layout(f); const HdivMassVariationTestFields fields = make_hdiv_mass_variation_test_fields(f); const mfem::Vector density_true = make_source_variation_density(f); const mfem::Vector displacement_true = fields.displacement; operators::context::gravity_field::GravityFieldLinearizationContext linearization_context(f, *f.domainMapperStateless); const mfem::Vector density = linearization_context.GetDensityMap().gather(density_true); const mfem::Vector displacement = linearization_context.GetDisplacementMap().gather(displacement_true); operators::GravityFieldJacobianOperator gravity_jacobian( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), layout.residual_offsets()); operators::GravityFieldOperator gravity_operator( f, *f.domainMapperStateless, linearization_context, layout.value_offsets(), gravity_jacobian); operators::context::gravity_field::GravityFieldGeometryContext reduced_geometry_context(f, *f.domainMapperStateless); operators::ReducedGravityFieldOperator reduced_operator( gravity_operator, reduced_geometry_context, displacement); operators::ReducedGravityFieldPreconditioner reduced_preconditioner( f, reduced_geometry_context); 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()); REQUIRE(reduced_preconditioner.Width() == reduced_operator.Width()); REQUIRE(reduced_preconditioner.Height() == reduced_operator.Height()); REQUIRE(reduced_preconditioner.GetOffsets().Size() == reduced_operator.GetGravityOffsets().Size()); for (int i = 0; i < reduced_preconditioner.GetOffsets().Size(); ++i) { CHECK(reduced_preconditioner.GetOffsets()[i] == reduced_operator.GetGravityOffsets()[i]); } 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(reduced_preconditioner); minres.SetRelTol(relative_solver_tolerance); minres.SetAbsTol(absolute_solver_tolerance); minres.SetMaxIter(maximum_iterations); minres.SetPrintLevel(0); 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)); }