module; #include #include #include #include #include #include #include #include #include #include #include export module experiment.stellar_null_space; import mean_field; import test_helpers; export namespace experiment::null_space { using Form = mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form; using DomainSchema = mean_field::utils::domain::CoreEnvelopeVacuumDomainSchema; using Model = mean_field::models::StellarModel; constexpr auto densityValue = mean_field::utils::blocks::get_value_block
(mean_field::utils::blocks::density_field.mass_term); constexpr auto surfaceDeformationValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::surface_deformation_field.parameters_term ); constexpr auto gravityGradientValue = mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::gravity_field.gradient_term); constexpr auto gravityPotentialValue = mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::gravity_field.poisson_term); constexpr auto enthalpyValue = mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::enthalpy_field.specific_term); constexpr auto bernoulliValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term ); constexpr auto gravityGradientResidual = mean_field::utils::blocks::get_residual_block(mean_field::utils::blocks::gravity_field.gradient_term); constexpr auto gravityPotentialResidual = mean_field::utils::blocks::get_residual_block(mean_field::utils::blocks::gravity_field.poisson_term); constexpr auto densityResidual = mean_field::utils::blocks::get_residual_block(mean_field::utils::blocks::density_field.mass_term); constexpr auto surfaceShapeResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::surface_deformation_field.shape_equilibrium_term ); constexpr auto enthalpyResidual = mean_field::utils::blocks::get_residual_block(mean_field::utils::blocks::enthalpy_field.specific_term); constexpr auto massResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term ); inline constexpr std::array residualBlockNames{"gravity_gradient", "gravity_potential", "closure", "surface_shape", "hydrostatic", "mass"}; template [[nodiscard]] mfem::Vector value_view( mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::value_block block ) { return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block)); } template [[nodiscard]] mfem::Vector const_value_view( const mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::value_block block ) { return mfem::Vector(const_cast(vector.GetData()) + layout.offset(block), layout.size(block)); } template [[nodiscard]] mfem::Vector residual_view( mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::residual_block block ) { return mfem::Vector(vector.GetData() + layout.offset(block), layout.size(block)); } template [[nodiscard]] mfem::Vector const_residual_view( const mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::residual_block block ) { return mfem::Vector(const_cast(vector.GetData()) + layout.offset(block), layout.size(block)); } template void assign_value_block( mfem::Vector &vector, const mean_field::operators::StellarEquilibriumLayout &layout, const mean_field::utils::blocks::value_block block, const mfem::Vector &source ) { MFEM_VERIFY( source.Size() == layout.size(block), "Surface-mode experiment received a block with the wrong size." ); value_view(vector, layout, block) = source; } [[nodiscard]] inline double global_norm( const mfem::Vector &vector, const MPI_Comm communicator ) { const double localNormSquared = vector * vector; double globalNormSquared = 0.0; MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, communicator); return std::sqrt(globalNormSquared); } inline void report_progress( const MPI_Comm communicator, const std::string &message ) { int rank = 0; MPI_Comm_rank(communicator, &rank); if (rank == 0) { std::cout << "[reduced-surface experiment] " << message << std::endl; } } [[nodiscard]] inline mean_field::operators::StellarEquilibriumDependencies make_dependencies() { return { .discretization = {.identity = 2003, .revision = 1}, .density = {.identity = 2011, .revision = 1}, .surfaceDeformation = {.identity = 2017, .revision = 1}, .gravityGradient = {.identity = 2027, .revision = 1}, .gravityPotential = {.identity = 2029, .revision = 1}, .enthalpy = {.identity = 2039, .revision = 1}, .bernoulliConstant = {.identity = 2053, .revision = 1}, .rotation = {.identity = 2063, .revision = 1}, .targetMass = {.identity = 2069, .revision = 1} }; } inline void increment_state_revisions(mean_field::operators::StellarEquilibriumDependencies &dependencies) { ++dependencies.density.revision; ++dependencies.surfaceDeformation.revision; ++dependencies.gravityGradient.revision; ++dependencies.gravityPotential.revision; ++dependencies.enthalpy.revision; ++dependencies.bernoulliConstant.revision; } [[nodiscard]] inline mfem::Vector pack_gravity_state( const mfem::Vector &density, const mfem::Vector &displacement, const mfem::Vector &gravityGradient, const mfem::Vector &gravityPotential ) { const std::array offsets{ 0, density.Size(), density.Size() + displacement.Size(), density.Size() + displacement.Size() + gravityGradient.Size(), density.Size() + displacement.Size() + gravityGradient.Size() + gravityPotential.Size() }; mfem::Vector packed(offsets.back()); mfem::Vector(packed.GetData() + offsets[0], density.Size()) = density; mfem::Vector(packed.GetData() + offsets[1], displacement.Size()) = displacement; mfem::Vector(packed.GetData() + offsets[2], gravityGradient.Size()) = gravityGradient; mfem::Vector(packed.GetData() + offsets[3], gravityPotential.Size()) = gravityPotential; return packed; } [[nodiscard]] inline Model make_model() { const double pi = std::acos(-1.0); const double targetMass = mean_field::utils::MASS; constexpr double dimensionlessMass = 2.0182359509662283534; const double polytropicConstant = pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0); return Model{ mean_field::models::structure::PolytropicStructure{ mean_field::eos::Polytrope{3.0, polytropicConstant}, targetMass }, mean_field::surface::ConstantPressureSurface{mean_field::eos::PressureValue{0.0}} }; } class N3Equilibrium final { public: explicit N3Equilibrium(mean_field::utils::Args args) : m_args(std::move(args)), m_fem( mean_field::fem::setup_fem( m_args.mesh_file, m_args, 0 ) ), m_model(make_model()), m_operator( m_fem, *m_fem.domainMapperStateless, m_model ), m_state(m_operator.GetLayout().value_offsets().Last()), m_dependencies(make_dependencies()) { MFEM_VERIFY(m_fem.okay(), "The null-space experiment could not construct the finite-element problem."); m_state = 0.0; initialize_state(); } [[nodiscard]] mean_field::fem::FEM &fem() noexcept { return m_fem; } [[nodiscard]] const mean_field::fem::FEM &fem() const noexcept { return m_fem; } [[nodiscard]] Model &model() noexcept { return m_model; } [[nodiscard]] mean_field::operators::PreparedStellarEquilibriumOperator &stellar_operator() noexcept { return m_operator; } [[nodiscard]] const mean_field::operators::PreparedStellarEquilibriumOperator & stellar_operator() const noexcept { return m_operator; } [[nodiscard]] const mfem::Vector &state() const noexcept { return m_state; } [[nodiscard]] mean_field::physics::RigidRotation rotation(const double fractionOfKeplerian) const { const double radius = mean_field::utils::RADIUS; const double mass = mean_field::utils::MASS; const double keplerianSpeed = std::sqrt(mean_field::utils::G * mass / (radius * radius * radius)); mfem::Vector angularVelocity(3); angularVelocity = 0.0; angularVelocity(2) = fractionOfKeplerian * keplerianSpeed; mfem::Vector center(3); center = 0.0; return mean_field::physics::RigidRotation(angularVelocity, center); } void prepare( const mfem::Vector &state, const mean_field::physics::RigidRotation &rotation ) { m_currentState = state; increment_state_revisions(m_dependencies); ++m_dependencies.rotation.revision; m_operator.Prepare(state, m_dependencies, rotation); } [[nodiscard]] mfem::Vector residual() const { mfem::Vector result; m_operator.BuildResidual(result); return result; } [[nodiscard]] mfem::Vector jacobian_action(const mfem::Vector &direction) const { mfem::Vector result; m_operator.Mult(direction, result); return result; } [[nodiscard]] mfem::Vector lifted_surface_direction(const mfem::Vector &rootDirection) const { const auto &layout = m_operator.GetLayout(); const mfem::Vector surfaceDirection = const_value_view(rootDirection, layout, surfaceDeformationValue); mfem::Vector volumeDirection(m_operator.GetDomainDeformation().volumeDisplacementSize()); m_operator.GetDomainDeformation().applyJacobian( m_operator.GetSurfaceDeformationParameters(), surfaceDirection, volumeDirection ); return volumeDirection; } private: void initialize_state() { report_progress(m_fem.mesh->GetComm(), "constructing the analytic n=3 Lane-Emden state"); constexpr double surfaceCoordinate = 6.8968486193769603755; constexpr int radialSampleCount = 8192; const double pi = std::acos(-1.0); const double radius = mean_field::utils::RADIUS; const double targetMass = mean_field::utils::MASS; constexpr double dimensionlessMass = 2.0182359509662283534; const double polytropicConstant = pi * mean_field::utils::G * std::pow(targetMass / (4.0 * pi * dimensionlessMass), 2.0 / 3.0); const double centralDensity = std::pow(surfaceCoordinate * std::sqrt(polytropicConstant / (pi * mean_field::utils::G)) / radius, 3.0); const mean_field::models::structure::StructureSeed seed = m_model.makeInitialSeed({.centralDensity = centralDensity, .radialSampleCount = radialSampleCount}); const auto interpolate = [](const mfem::Vector &radii, const mfem::Vector &values, const double r) { if (r <= radii(0)) { return values(0); } const int finalIndex = radii.Size() - 1; if (r >= radii(finalIndex)) { return values(finalIndex); } int lower = 0; int upper = finalIndex; while (upper - lower > 1) { const int middle = lower + (upper - lower) / 2; if (radii(middle) <= r) { lower = middle; } else { upper = middle; } } const double fraction = (r - radii(lower)) / (radii(upper) - radii(lower)); return (1.0 - fraction) * values(lower) + fraction * values(upper); }; mfem::FunctionCoefficient densityCoefficient([&seed, &interpolate](const mfem::Vector &position) { const double r = position.Norml2(); return r >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.density, r); }); mfem::FunctionCoefficient enthalpyCoefficient([&seed, &interpolate](const mfem::Vector &position) { const double r = position.Norml2(); return r >= seed.stellarRadius ? 0.0 : interpolate(seed.radius, seed.enthalpy, r); }); mfem::ParGridFunction densityField(m_fem.densityFes.get()); mfem::ParGridFunction enthalpyField(m_fem.enthalpyFes.get()); mfem::ParGridFunction displacementField(m_fem.displacementFes.get()); densityField = 0.0; enthalpyField = 0.0; displacementField = 0.0; densityField.ProjectCoefficient(densityCoefficient); enthalpyField.ProjectCoefficient(enthalpyCoefficient); *m_fem.displacement = displacementField; report_progress(m_fem.mesh->GetComm(), "solving the gravity field for the seed state"); const mean_field::physics::GravitySolution gravity = mean_field::physics::solve_gravity_field(m_fem, m_args, densityField, displacementField); mfem::Vector densityTrue; mfem::Vector enthalpyTrue; mfem::Vector gravityGradientTrue; mfem::Vector gravityPotentialTrue; densityField.GetTrueDofs(densityTrue); enthalpyField.GetTrueDofs(enthalpyTrue); gravity.gradPhi.GetTrueDofs(gravityGradientTrue); gravity.phi.GetTrueDofs(gravityPotentialTrue); const auto &layout = m_operator.GetLayout(); const mean_field::field::FieldDofMap densityMap = mean_field::field::make_field_dof_map(*m_fem.densityFes); const mean_field::field::FieldDofMap enthalpyMap = mean_field::field::make_field_dof_map(*m_fem.enthalpyFes); mfem::Vector surfaceParameters(layout.size(surfaceDeformationValue)); surfaceParameters = 0.0; assign_value_block(m_state, layout, densityValue, densityMap.gather(densityTrue)); assign_value_block(m_state, layout, surfaceDeformationValue, surfaceParameters); assign_value_block(m_state, layout, gravityGradientValue, gravityGradientTrue); assign_value_block(m_state, layout, gravityPotentialValue, gravityPotentialTrue); assign_value_block(m_state, layout, enthalpyValue, enthalpyMap.gather(enthalpyTrue)); value_view(m_state, layout, bernoulliValue)(0) = -mean_field::utils::G * targetMass / radius; m_currentState = m_state; prepare(m_state, rotation(0.0)); report_progress(m_fem.mesh->GetComm(), "analytic state is prepared"); } mean_field::utils::Args m_args; mean_field::fem::FEM m_fem; Model m_model; mean_field::operators::PreparedStellarEquilibriumOperator m_operator; mfem::Vector m_state; mfem::Vector m_currentState; mean_field::operators::StellarEquilibriumDependencies m_dependencies; }; enum class SurfaceModeKind : std::uint8_t { uniform_radial, translation_like_dipole, oblate_quadrupole, spherical_harmonic }; struct SurfaceMode final { std::string name; SurfaceModeKind kind; int axis; mfem::Vector direction; }; [[nodiscard]] inline const char *surface_mode_kind_name(const SurfaceModeKind kind) noexcept { switch (kind) { case SurfaceModeKind::uniform_radial: return "uniform_radial"; case SurfaceModeKind::translation_like_dipole: return "translation_like_dipole"; case SurfaceModeKind::oblate_quadrupole: return "oblate_quadrupole"; case SurfaceModeKind::spherical_harmonic: return "spherical_harmonic"; } return "unknown"; } [[nodiscard]] inline double zonal_legendre( const int degree, const double cosineOfPolarAngle ) { MFEM_VERIFY(degree >= 0, "A zonal spherical-harmonic degree must be non-negative."); const double coordinate = std::clamp(cosineOfPolarAngle, -1.0, 1.0); if (degree == 0) { return 1.0; } if (degree == 1) { return coordinate; } double previousPrevious = 1.0; double previous = coordinate; for (int order = 2; order <= degree; ++order) { const double current = ((2.0 * static_cast(order) - 1.0) * coordinate * previous - (static_cast(order) - 1.0) * previousPrevious) / static_cast(order); previousPrevious = previous; previous = current; } return previous; } [[nodiscard]] inline std::vector make_surface_modes(N3Equilibrium &fixture) { const auto &layout = fixture.stellar_operator().GetLayout(); auto deformation = fixture.model().compileDomainDeformation(fixture.fem()); const auto &surface = deformation.surfaceDeformationPrescription(); MFEM_VERIFY( surface.parameterCount() == layout.size(surfaceDeformationValue), "The diagnostic surface prescription does not match the root surface block." ); const auto make_root_direction = [&layout](const mfem::Vector &surfaceDirection) { mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; assign_value_block(direction, layout, surfaceDeformationValue, surfaceDirection); return direction; }; std::vector modes; modes.reserve(6); mfem::Vector uniform(surface.parameterCount()); for (int parameter = 0; parameter < uniform.Size(); ++parameter) { uniform(parameter) = surface.referenceRadius(parameter); } modes.push_back( {.name = "uniform_radial_homology", .kind = SurfaceModeKind::uniform_radial, .axis = -1, .direction = make_root_direction(uniform)} ); for (int axis = 0; axis < surface.spatialDimension(); ++axis) { mfem::Vector dipole(surface.parameterCount()); for (int parameter = 0; parameter < dipole.Size(); ++parameter) { dipole(parameter) = surface.radialDirection(parameter, axis); } modes.push_back( {.name = std::string("translation_like_dipole_") + static_cast('x' + axis), .kind = SurfaceModeKind::translation_like_dipole, .axis = axis, .direction = make_root_direction(dipole)} ); } mfem::Vector quadrupole(surface.parameterCount()); for (int parameter = 0; parameter < quadrupole.Size(); ++parameter) { const double polarDirection = surface.radialDirection(parameter, 2); quadrupole(parameter) = surface.referenceRadius(parameter) * (1.0 - 3.0 * polarDirection * polarDirection); } modes.push_back( {.name = "axisymmetric_oblate_quadrupole_z", .kind = SurfaceModeKind::oblate_quadrupole, .axis = 2, .direction = make_root_direction(quadrupole)} ); constexpr int diagnosticAngularDegree = 12; mfem::Vector sphericalHarmonic(surface.parameterCount()); double localMaximumMagnitude = 0.0; for (int parameter = 0; parameter < sphericalHarmonic.Size(); ++parameter) { const double angularValue = zonal_legendre(diagnosticAngularDegree, surface.radialDirection(parameter, 2)); sphericalHarmonic(parameter) = surface.referenceRadius(parameter) * angularValue; localMaximumMagnitude = std::max(localMaximumMagnitude, std::abs(angularValue)); } double globalMaximumMagnitude = 0.0; MPI_Allreduce( &localMaximumMagnitude, &globalMaximumMagnitude, 1, MPI_DOUBLE, MPI_MAX, fixture.fem().mesh->GetComm() ); MFEM_VERIFY(globalMaximumMagnitude > 0.0, "The spherical-harmonic surface mode has zero amplitude."); sphericalHarmonic /= globalMaximumMagnitude; modes.push_back( {.name = "zonal_spherical_harmonic_l12", .kind = SurfaceModeKind::spherical_harmonic, .axis = -1, .direction = make_root_direction(sphericalHarmonic)} ); return modes; } [[nodiscard]] inline std::array< double, 6> residual_block_norms( const mfem::Vector &action, const mean_field::operators::StellarEquilibriumLayout &layout, const MPI_Comm communicator ) { return { global_norm(const_residual_view(action, layout, gravityGradientResidual), communicator), global_norm(const_residual_view(action, layout, gravityPotentialResidual), communicator), global_norm(const_residual_view(action, layout, densityResidual), communicator), global_norm(const_residual_view(action, layout, surfaceShapeResidual), communicator), global_norm(const_residual_view(action, layout, enthalpyResidual), communicator), global_norm(const_residual_view(action, layout, massResidual), communicator) }; } } // namespace experiment::null_space