#include #include #include #include #include import mean_field; import test_helpers; namespace hydrostatic_kernel_test_utils { mfem::Vector project_scalar( mfem::ParFiniteElementSpace &finiteElementSpace, mfem::Coefficient &coefficient ) { mfem::ParGridFunction field(&finiteElementSpace); field.ProjectCoefficient(coefficient); mfem::Vector trueVector; field.GetTrueDofs(trueVector); return trueVector; } mfem::Vector make_constant_field( mfem::ParFiniteElementSpace &finiteElementSpace, const double value ) { mfem::ConstantCoefficient coefficient(value); return project_scalar(finiteElementSpace, coefficient); } mfem::Vector make_enthalpy(const mean_field::fem::FEM &f) { mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { return 1.10 + 0.035 * position(0) - 0.021 * position(1) + 0.014 * position(2); }); return project_scalar(*f.enthalpyFes, coefficient); } mfem::Vector make_potential(const mean_field::fem::FEM &f) { mfem::FunctionCoefficient coefficient([](const mfem::Vector &position) { return -0.72 + 0.018 * position(0) + 0.011 * position(1) - 0.025 * position(2); }); return project_scalar(*f.gravityPotentialFes, coefficient); } mean_field::physics::RigidRotation make_rotation() { mfem::Vector angularVelocity(3); angularVelocity(0) = 0.21; angularVelocity(1) = -0.13; angularVelocity(2) = 0.48; mfem::Vector center(3); center(0) = 0.04; center(1) = -0.03; center(2) = 0.02; return mean_field::physics::RigidRotation(angularVelocity, center); } mean_field::physics::RigidRotation make_zero_rotation() { mfem::Vector angularVelocity(3); mfem::Vector center(3); angularVelocity = 0.0; center = 0.0; return mean_field::physics::RigidRotation(angularVelocity, center); } mfem::Vector centered_difference( const mfem::Vector &plusResidual, const mfem::Vector &minusResidual, const double epsilon ) { mfem::Vector difference(plusResidual); difference -= minusResidual; difference *= 1.0 / (2.0 * epsilon); return difference; } double sum_normalized_error( const mfem::Vector &computed, const mfem::Vector &reference, const double normalization, const MPI_Comm communicator ) { mfem::Vector difference(computed); difference -= reference; return gravity_prepared_test_utils::global_norm( difference, communicator ) / std::max(normalization, std::numeric_limits::epsilon()); } mfem::Vector make_vacuum_supported_potential(const mean_field::fem::FEM &f) { mfem::Vector attributeValues(f.mesh->attributes.Max()); attributeValues = 0.0; const int vacuumAttribute = f.domainMapperStateless->GetVacuumElementAttribute(); for (int attributeIndex = 0; attributeIndex < f.mesh->attributes.Size(); ++attributeIndex) { const int attribute = f.mesh->attributes[attributeIndex]; if (attribute == vacuumAttribute) { attributeValues(attribute - 1) = 1.0; } } mfem::PWConstCoefficient coefficient(attributeValues); return project_scalar(*f.gravityPotentialFes, coefficient); } class HydrostaticEnthalpyMassOperator final : public mfem::Operator { public: HydrostaticEnthalpyMassOperator( const mean_field::fem::FEM &f, const mean_field::mapping::DomainMapperStateless &domainMapper, const mfem::Vector &displacementTrue ) : mfem::Operator(f.enthalpyFes->GetTrueVSize()), f_(f), domainMapper_(domainMapper), displacementTrue_(displacementTrue) { } void Mult( const mfem::Vector &input, mfem::Vector &output ) const override { mean_field::operators::kernels:: apply_hydrostatic_equilibrium_enthalpy_action( f_, domainMapper_, input, displacementTrue_, output ); } private: const mean_field::fem::FEM &f_; const mean_field::mapping::DomainMapperStateless &domainMapper_; const mfem::Vector &displacementTrue_; }; } // namespace hydrostatic_kernel_test_utils TEST_CASE( "Rigid Rotation Potential Derivative Matches Centered Differences", tags::barotrope &tags::hydro &tags::jacobian &tags::physics &tags::unit ) { const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_rotation(); mfem::Vector position(3); mfem::Vector direction(3); position(0) = 0.71; position(1) = -0.42; position(2) = 0.36; direction(0) = -0.17; direction(1) = 0.29; direction(2) = 0.11; constexpr double epsilon = 1.0e-7; mfem::Vector plusPosition(position); mfem::Vector minusPosition(position); plusPosition.Add(epsilon, direction); minusPosition.Add(-epsilon, direction); const double centeredDerivative = (rotation.potential(plusPosition) - rotation.potential(minusPosition)) / (2.0 * epsilon); const double analyticDerivative = rotation.potential_directional_derivative(position, direction); const double relativeError = std::abs(centeredDerivative - analyticDerivative) / std::max( std::abs(analyticDerivative), std::numeric_limits::epsilon() ); INFO("Rigid-rotation derivative error = " << relativeError); CHECK(relativeError < 2.0e-9); } TEST_CASE( "Hydrostatic Residual Vanishes For A Manufactured Rotating State", tags::barotrope &tags::hydro &tags::integration &tags::kernels &tags::physics &tags::residuals ) { auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_rotation(); constexpr double bernoulliConstant = 0.73; constexpr double potentialValue = -0.21; constexpr double constantOffset = 0.40; mfem::FunctionCoefficient enthalpyCoefficient( [&rotation](const mfem::Vector &position) { return bernoulliConstant - potentialValue + rotation.potential(position); } ); const mfem::Vector interpolatedEnthalpy = hydrostatic_kernel_test_utils::project_scalar( *f.enthalpyFes, enthalpyCoefficient ); const mfem::Vector potential = hydrostatic_kernel_test_utils::make_constant_field( *f.gravityPotentialFes, potentialValue ); const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 0.0); const MPI_Comm communicator = f.mesh->GetComm(); /* * First measure the residual of the nodally interpolated * analytic equilibrium. Because the order-3 enthalpy space * cannot exactly represent the quadratic rotation potential * on an order-4 curved mesh, this measures the representation * floor rather than an algebraic residual. */ mfem::Vector interpolatedResidual; mfem::Vector interpolatedReferenceResidual; mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, rotation, interpolatedEnthalpy, potential, displacement, bernoulliConstant, interpolatedResidual ); mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, rotation, interpolatedEnthalpy, potential, displacement, bernoulliConstant + constantOffset, interpolatedReferenceResidual ); const double interpolatedResidualNorm = gravity_prepared_test_utils::global_norm( interpolatedResidual, communicator ); const double interpolatedReferenceNorm = gravity_prepared_test_utils::global_norm( interpolatedReferenceResidual, communicator ); REQUIRE(interpolatedReferenceNorm > 1.0e-12); const double representationFloor = interpolatedResidualNorm / interpolatedReferenceNorm; INFO( "Interpolated rotating-state residual norm = " << interpolatedResidualNorm ); INFO( "Interpolated rotating-state relative " "representation floor = " << representationFloor ); /* * This remains an independent physical/sign check. A wrong * rotation sign or coordinate convention would produce an * order-unity error rather than the observed projection floor. */ CHECK(representationFloor < 2.0e-5); /* * Construct the weakly manufactured discrete equilibrium. * * If r_I is the residual of the nodal interpolant, solve * * M_h delta_h = -r_I, * * where M_h is exactly the stellar-domain enthalpy action. * Then h_I + delta_h satisfies the discrete weak equilibrium. * * Vacuum-only enthalpy DOFs form a nullspace, but the right-hand * side is in the range of M_h. Starting CG from zero keeps the * iteration in the active stellar subspace. */ hydrostatic_kernel_test_utils::HydrostaticEnthalpyMassOperator enthalpyMassOperator(f, *f.domainMapperStateless, displacement); mfem::Vector correctionRightHandSide(interpolatedResidual); correctionRightHandSide *= -1.0; mfem::Vector enthalpyCorrection(f.enthalpyFes->GetTrueVSize()); enthalpyCorrection = 0.0; mfem::CGSolver projectionSolver(communicator); projectionSolver.SetOperator(enthalpyMassOperator); projectionSolver.SetRelTol(1.0e-12); projectionSolver.SetAbsTol(1.0e-15); projectionSolver.SetMaxIter(1000); projectionSolver.SetPrintLevel(0); projectionSolver.Mult(correctionRightHandSide, enthalpyCorrection); INFO( "Discrete-equilibrium projection converged = " << projectionSolver.GetConverged() ); INFO( "Discrete-equilibrium projection iterations = " << projectionSolver.GetNumIterations() ); INFO( "Discrete-equilibrium projection final norm = " << projectionSolver.GetFinalNorm() ); REQUIRE(projectionSolver.GetConverged()); mfem::Vector correctionEquationResidual; enthalpyMassOperator.Mult(enthalpyCorrection, correctionEquationResidual); correctionEquationResidual -= correctionRightHandSide; const double correctionEquationNorm = gravity_prepared_test_utils::global_norm( correctionEquationResidual, communicator ); INFO( "Discrete-equilibrium correction-equation " "residual norm = " << correctionEquationNorm ); CHECK( correctionEquationNorm <= std::max(5.0e-12 * interpolatedResidualNorm, 5.0e-15) ); mfem::Vector discreteEnthalpy(interpolatedEnthalpy); discreteEnthalpy += enthalpyCorrection; mfem::Vector exactResidual; mfem::Vector referenceResidual; mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, rotation, discreteEnthalpy, potential, displacement, bernoulliConstant, exactResidual ); mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, rotation, discreteEnthalpy, potential, displacement, bernoulliConstant + constantOffset, referenceResidual ); const double exactNorm = gravity_prepared_test_utils::global_norm(exactResidual, communicator); const double referenceNorm = gravity_prepared_test_utils::global_norm( referenceResidual, communicator ); const double correctionNorm = gravity_prepared_test_utils::global_norm( enthalpyCorrection, communicator ); INFO("Enthalpy representation correction norm = " << correctionNorm); INFO("Discrete manufactured residual norm = " << exactNorm); INFO("Discrete reference residual norm = " << referenceNorm); REQUIRE(referenceNorm > 1.0e-12); CHECK(exactNorm <= 5.0e-12 * referenceNorm); } TEST_CASE( "Exact Constant Hydrostatic Equilibrium Remains Zero Under Deformation", tags::barotrope &tags::hydro &tags::integration &tags::jacobian &tags::kernels &tags::mapping &tags::physics ) { auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_zero_rotation(); constexpr double enthalpyValue = 1.20; constexpr double potentialValue = -0.35; constexpr double bernoulliConstant = enthalpyValue + potentialValue; const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_constant_field( *f.enthalpyFes, enthalpyValue ); const mfem::Vector potential = hydrostatic_kernel_test_utils::make_constant_field( *f.gravityPotentialFes, potentialValue ); const mfem::Vector displacementVariation = gravity_prepared_test_utils::make_displacement(f, 0.67); const MPI_Comm communicator = f.mesh->GetComm(); for (const double deformationScale : {0.0, 0.5, 1.0}) { DYNAMIC_SECTION("Deformation scale = " << deformationScale) { const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement( f, deformationScale ); mfem::Vector exactResidual; mfem::Vector referenceResidual; mfem::Vector exactGeometryAction; mfem::Vector referenceGeometryAction; mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, exactResidual ); mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant + 0.50, referenceResidual ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_displacement_action( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, displacementVariation, exactGeometryAction ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_displacement_action( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant + 0.50, displacementVariation, referenceGeometryAction ); const double exactResidualNorm = gravity_prepared_test_utils::global_norm( exactResidual, communicator ); const double referenceResidualNorm = gravity_prepared_test_utils::global_norm( referenceResidual, communicator ); const double exactGeometryNorm = gravity_prepared_test_utils::global_norm( exactGeometryAction, communicator ); const double referenceGeometryNorm = gravity_prepared_test_utils::global_norm( referenceGeometryAction, communicator ); REQUIRE(referenceResidualNorm > 1.0e-12); REQUIRE(referenceGeometryNorm > 1.0e-14); CHECK(exactResidualNorm <= 5.0e-12 * referenceResidualNorm); CHECK(exactGeometryNorm <= 5.0e-12 * referenceGeometryNorm); } } } TEST_CASE( "Hydrostatic Equilibrium Excludes Vacuum Elements", tags::barotrope &tags::hydro &tags::kernels &tags::mapping &tags::physics &tags::unit ) { auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_zero_rotation(); const mfem::Vector zeroEnthalpy(f.enthalpyFes->GetTrueVSize()); mfem::Vector enthalpy(zeroEnthalpy); enthalpy = 0.0; const mfem::Vector vacuumPotential = hydrostatic_kernel_test_utils::make_vacuum_supported_potential(f); const mfem::Vector stellarPotential = hydrostatic_kernel_test_utils::make_constant_field( *f.gravityPotentialFes, 1.0 ); const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 1.0); mfem::Vector residual; mfem::Vector vacuumAction; mfem::Vector stellarAction; mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, rotation, enthalpy, vacuumPotential, displacement, 0.0, residual ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_potential_action( f, *f.domainMapperStateless, vacuumPotential, displacement, vacuumAction ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_potential_action( f, *f.domainMapperStateless, stellarPotential, displacement, stellarAction ); const MPI_Comm communicator = f.mesh->GetComm(); const double residualNorm = gravity_prepared_test_utils::global_norm(residual, communicator); const double vacuumActionNorm = gravity_prepared_test_utils::global_norm(vacuumAction, communicator); const double stellarActionNorm = gravity_prepared_test_utils::global_norm(stellarAction, communicator); REQUIRE(stellarActionNorm > 1.0e-12); CHECK(residualNorm <= 5.0e-13 * stellarActionNorm); CHECK(vacuumActionNorm <= 5.0e-13 * stellarActionNorm); } TEST_CASE( "Hydrostatic Jacobian Matches Blocks And Centered Differences", tags::barotrope &tags::hydro &tags::integration &tags::jacobian &tags::kernels &tags::mapping &tags::physics ) { auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_rotation(); const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); const mfem::Vector potential = hydrostatic_kernel_test_utils::make_potential(f); const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 1.0); const mfem::Vector enthalpyVariation = gravity_prepared_test_utils::make_deterministic_vector( f.enthalpyFes->GetTrueVSize(), 0.23 ); const mfem::Vector potentialVariation = gravity_prepared_test_utils::make_deterministic_vector( f.gravityPotentialFes->GetTrueVSize(), 0.47 ); const mfem::Vector displacementVariation = gravity_prepared_test_utils::make_deterministic_vector( f.displacementFes->GetTrueVSize(), 0.71 ); constexpr double bernoulliConstant = 0.41; constexpr double constantVariation = -0.37; constexpr double epsilon = 1.0e-7; mfem::Vector enthalpyAction; mfem::Vector potentialAction; mfem::Vector constantAction; mfem::Vector displacementAction; mfem::Vector completeAction; mean_field::operators::kernels:: apply_hydrostatic_equilibrium_enthalpy_action( f, *f.domainMapperStateless, enthalpyVariation, displacement, enthalpyAction ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_potential_action( f, *f.domainMapperStateless, potentialVariation, displacement, potentialAction ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_constant_action( f, *f.domainMapperStateless, constantVariation, displacement, constantAction ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_displacement_action( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, displacementVariation, displacementAction ); mean_field::operators::kernels::apply_hydrostatic_equilibrium_action( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, enthalpyVariation, potentialVariation, constantVariation, displacementVariation, completeAction ); mfem::Vector blockAction(enthalpyAction); blockAction += potentialAction; blockAction += constantAction; blockAction += displacementAction; const MPI_Comm communicator = f.mesh->GetComm(); const double blockError = gravity_prepared_test_utils::relative_error( completeAction, blockAction, communicator ); INFO("Hydrostatic block reconstruction error = " << blockError); CHECK(blockError < 5.0e-13); auto evaluate_residual = [&f, &rotation]( const mfem::Vector &trialEnthalpy, const mfem::Vector &trialPotential, const mfem::Vector &trialDisplacement, const double trialConstant ) { mfem::Vector residual; mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, rotation, trialEnthalpy, trialPotential, trialDisplacement, trialConstant, residual ); return residual; }; mfem::Vector plusEnthalpy(enthalpy); mfem::Vector minusEnthalpy(enthalpy); plusEnthalpy.Add(epsilon, enthalpyVariation); minusEnthalpy.Add(-epsilon, enthalpyVariation); const mfem::Vector enthalpyDifference = hydrostatic_kernel_test_utils::centered_difference( evaluate_residual( plusEnthalpy, potential, displacement, bernoulliConstant ), evaluate_residual( minusEnthalpy, potential, displacement, bernoulliConstant ), epsilon ); mfem::Vector plusPotential(potential); mfem::Vector minusPotential(potential); plusPotential.Add(epsilon, potentialVariation); minusPotential.Add(-epsilon, potentialVariation); const mfem::Vector potentialDifference = hydrostatic_kernel_test_utils::centered_difference( evaluate_residual( enthalpy, plusPotential, displacement, bernoulliConstant ), evaluate_residual( enthalpy, minusPotential, displacement, bernoulliConstant ), epsilon ); const mfem::Vector constantDifference = hydrostatic_kernel_test_utils::centered_difference( evaluate_residual( enthalpy, potential, displacement, bernoulliConstant + epsilon * constantVariation ), evaluate_residual( enthalpy, potential, displacement, bernoulliConstant - epsilon * constantVariation ), epsilon ); mfem::Vector plusDisplacement(displacement); mfem::Vector minusDisplacement(displacement); plusDisplacement.Add(epsilon, displacementVariation); minusDisplacement.Add(-epsilon, displacementVariation); const mfem::Vector displacementDifference = hydrostatic_kernel_test_utils::centered_difference( evaluate_residual( enthalpy, potential, plusDisplacement, bernoulliConstant ), evaluate_residual( enthalpy, potential, minusDisplacement, bernoulliConstant ), epsilon ); const double enthalpyError = gravity_prepared_test_utils::relative_error( enthalpyAction, enthalpyDifference, communicator ); const double potentialError = gravity_prepared_test_utils::relative_error( potentialAction, potentialDifference, communicator ); const double constantError = gravity_prepared_test_utils::relative_error( constantAction, constantDifference, communicator ); const double displacementError = gravity_prepared_test_utils::relative_error( displacementAction, displacementDifference, communicator ); INFO("Hydrostatic enthalpy-block error = " << enthalpyError); INFO("Hydrostatic potential-block error = " << potentialError); INFO("Hydrostatic constant-block error = " << constantError); INFO("Hydrostatic displacement-block error = " << displacementError); CHECK(enthalpyError < 2.0e-8); CHECK(potentialError < 2.0e-8); CHECK(constantError < 2.0e-8); CHECK(displacementError < 2.0e-7); mfem::Vector combinedPlusEnthalpy(enthalpy); mfem::Vector combinedMinusEnthalpy(enthalpy); mfem::Vector combinedPlusPotential(potential); mfem::Vector combinedMinusPotential(potential); mfem::Vector combinedPlusDisplacement(displacement); mfem::Vector combinedMinusDisplacement(displacement); combinedPlusEnthalpy.Add(epsilon, enthalpyVariation); combinedMinusEnthalpy.Add(-epsilon, enthalpyVariation); combinedPlusPotential.Add(epsilon, potentialVariation); combinedMinusPotential.Add(-epsilon, potentialVariation); combinedPlusDisplacement.Add(epsilon, displacementVariation); combinedMinusDisplacement.Add(-epsilon, displacementVariation); const mfem::Vector combinedDifference = hydrostatic_kernel_test_utils::centered_difference( evaluate_residual( combinedPlusEnthalpy, combinedPlusPotential, combinedPlusDisplacement, bernoulliConstant + epsilon * constantVariation ), evaluate_residual( combinedMinusEnthalpy, combinedMinusPotential, combinedMinusDisplacement, bernoulliConstant - epsilon * constantVariation ), epsilon ); const double blockNormSum = gravity_prepared_test_utils::global_norm(enthalpyAction, communicator) + gravity_prepared_test_utils::global_norm( potentialAction, communicator ) + gravity_prepared_test_utils::global_norm(constantAction, communicator) + gravity_prepared_test_utils::global_norm( displacementAction, communicator ); const double simultaneousError = hydrostatic_kernel_test_utils::sum_normalized_error( completeAction, combinedDifference, blockNormSum, communicator ); INFO("Hydrostatic simultaneous Jacobian error = " << simultaneousError); CHECK(simultaneousError < 2.0e-7); } TEST_CASE( "Hydrostatic Displacement Action Is Linear In Its Direction", tags::barotrope &tags::hydro &tags::integration &tags::jacobian &tags::mapping &tags::physics &tags::unit ) { auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); const mean_field::physics::RigidRotation rotation = hydrostatic_kernel_test_utils::make_rotation(); const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); const mfem::Vector potential = hydrostatic_kernel_test_utils::make_potential(f); const mfem::Vector displacement = gravity_prepared_test_utils::make_displacement(f, 1.0); const mfem::Vector firstDirection = gravity_prepared_test_utils::make_deterministic_vector( f.displacementFes->GetTrueVSize(), 0.31 ); const mfem::Vector secondDirection = gravity_prepared_test_utils::make_deterministic_vector( f.displacementFes->GetTrueVSize(), 0.83 ); constexpr double firstScale = 0.43; constexpr double secondScale = -0.29; constexpr double bernoulliConstant = 0.41; const mfem::Vector combinedDirection = gravity_prepared_test_utils::linear_combination( firstDirection, firstScale, secondDirection, secondScale ); mfem::Vector firstAction; mfem::Vector secondAction; mfem::Vector combinedAction; mean_field::operators::kernels:: apply_hydrostatic_equilibrium_displacement_action( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, firstDirection, firstAction ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_displacement_action( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, secondDirection, secondAction ); mean_field::operators::kernels:: apply_hydrostatic_equilibrium_displacement_action( f, *f.domainMapperStateless, rotation, enthalpy, potential, displacement, bernoulliConstant, combinedDirection, combinedAction ); const mfem::Vector expectedAction = gravity_prepared_test_utils::linear_combination( firstAction, firstScale, secondAction, secondScale ); const double linearityError = gravity_prepared_test_utils::relative_error( combinedAction, expectedAction, f.mesh->GetComm() ); INFO("Hydrostatic displacement-linearity error = " << linearityError); CHECK(linearityError < 5.0e-12); } TEST_CASE( "Hydrostatic Residual Is Translationally Invariant On Deformed Geometry", tags::barotrope &tags::hydro &tags::integration &tags::kernels &tags::mapping &tags::physics &tags::residuals ) { auto args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); mfem::Vector angularVelocity(3); angularVelocity(0) = 0.21; angularVelocity(1) = -0.13; angularVelocity(2) = 0.48; mfem::Vector center(3); center(0) = 0.04; center(1) = -0.03; center(2) = 0.02; mfem::Vector translation(3); translation(0) = 0.071; translation(1) = -0.053; translation(2) = 0.037; mfem::Vector translatedCenter(center); translatedCenter += translation; const mean_field::physics::RigidRotation baseRotation( angularVelocity, center ); const mean_field::physics::RigidRotation translatedRotation( angularVelocity, translatedCenter ); const mfem::Vector enthalpy = hydrostatic_kernel_test_utils::make_enthalpy(f); const mfem::Vector potential = hydrostatic_kernel_test_utils::make_potential(f); /* * Use a nontrivially deformed base state so this checks rotation * and mapped geometry simultaneously. The comparison state adds * an exactly representable rigid translation to that deformation. */ const mfem::Vector baseDisplacement = gravity_prepared_test_utils::make_displacement(f, 0.73); mfem::ParGridFunction translationField(f.displacementFes.get()); mfem::VectorFunctionCoefficient translationCoefficient( f.mesh->Dimension(), [&translation](const mfem::Vector &, mfem::Vector &value) { value.SetSize(translation.Size()); value = translation; } ); translationField.ProjectCoefficient(translationCoefficient); mfem::Vector translationTrue; translationField.GetTrueDofs(translationTrue); mfem::Vector translatedDisplacement(baseDisplacement); translatedDisplacement += translationTrue; constexpr double bernoulliConstant = 0.41; mfem::Vector baseResidual; mfem::Vector translatedResidual; mfem::Vector untranslatedCenterResidual; mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, baseRotation, enthalpy, potential, baseDisplacement, bernoulliConstant, baseResidual ); mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, translatedRotation, enthalpy, potential, translatedDisplacement, bernoulliConstant, translatedResidual ); /* * Negative control: translate the geometry but leave the rotation * center fixed. This must not agree with the covariant result. */ mean_field::operators::kernels::apply_hydrostatic_equilibrium( f, *f.domainMapperStateless, baseRotation, enthalpy, potential, translatedDisplacement, bernoulliConstant, untranslatedCenterResidual ); const MPI_Comm communicator = f.mesh->GetComm(); const double baseResidualNorm = gravity_prepared_test_utils::global_norm(baseResidual, communicator); const double translatedResidualNorm = gravity_prepared_test_utils::global_norm( translatedResidual, communicator ); const double translationInvarianceError = gravity_prepared_test_utils::relative_error( translatedResidual, baseResidual, communicator ); const double fixedCenterDifference = gravity_prepared_test_utils::relative_error( untranslatedCenterResidual, translatedResidual, communicator ); INFO("Base deformed hydrostatic residual norm = " << baseResidualNorm); INFO("Translated hydrostatic residual norm = " << translatedResidualNorm); INFO( "Mapped-rotation translation invariance error = " << translationInvarianceError ); INFO( "Relative change with untranslated rotation center = " << fixedCenterDifference ); REQUIRE(baseResidualNorm > 1.0e-12); REQUIRE(translatedResidualNorm > 1.0e-12); REQUIRE(fixedCenterDifference > 1.0e-5); CHECK(translationInvarianceError < 5.0e-12); }