perf(jacobian-action): major updates to jacobian action application by removing redudant quadrature work. ~5x increase in speed
This commit is contained in:
237
tests/seed/lane_emden.cpp
Normal file
237
tests/seed/lane_emden.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <limits>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
namespace {
|
||||
template <
|
||||
typename AnalyticValue,
|
||||
typename AnalyticDerivative>
|
||||
void check_dimensionless_solution(
|
||||
const mean_field::seed::DimensionlessLaneEmdenSolution &solution,
|
||||
AnalyticValue analyticValue,
|
||||
AnalyticDerivative analyticDerivative,
|
||||
const double tolerance
|
||||
) {
|
||||
REQUIRE(solution.coordinate.Size() >= 2);
|
||||
REQUIRE(solution.theta.Size() == solution.coordinate.Size());
|
||||
REQUIRE(solution.thetaDerivative.Size() == solution.coordinate.Size());
|
||||
|
||||
double maximumValueError = 0.0;
|
||||
double maximumDerivativeError = 0.0;
|
||||
for (int index = 0; index < solution.coordinate.Size(); ++index) {
|
||||
const double coordinate = solution.coordinate(index);
|
||||
CHECK(std::isfinite(coordinate));
|
||||
CHECK(std::isfinite(solution.theta(index)));
|
||||
CHECK(std::isfinite(solution.thetaDerivative(index)));
|
||||
if (index > 0) {
|
||||
CHECK(coordinate > solution.coordinate(index - 1));
|
||||
}
|
||||
|
||||
maximumValueError =
|
||||
std::max(maximumValueError, std::abs(solution.theta(index) - analyticValue(coordinate)));
|
||||
maximumDerivativeError = std::max(
|
||||
maximumDerivativeError, std::abs(solution.thetaDerivative(index) - analyticDerivative(coordinate))
|
||||
);
|
||||
}
|
||||
|
||||
CHECK(maximumValueError < tolerance);
|
||||
CHECK(maximumDerivativeError < tolerance);
|
||||
}
|
||||
|
||||
void check_profiles_are_identical(
|
||||
const mean_field::seed::RadialProfile &left,
|
||||
const mean_field::seed::RadialProfile &right
|
||||
) {
|
||||
REQUIRE(left.radius.Size() == right.radius.Size());
|
||||
REQUIRE(left.density.Size() == right.density.Size());
|
||||
REQUIRE(left.specificEnthalpy.Size() == right.specificEnthalpy.Size());
|
||||
|
||||
for (int index = 0; index < left.radius.Size(); ++index) {
|
||||
CHECK(left.radius(index) == right.radius(index));
|
||||
CHECK(left.density(index) == right.density(index));
|
||||
CHECK(left.specificEnthalpy(index) == right.specificEnthalpy(index));
|
||||
}
|
||||
|
||||
CHECK(left.stellarRadius == right.stellarRadius);
|
||||
CHECK(left.centralDensity == right.centralDensity);
|
||||
CHECK(left.centralSpecificEnthalpy == right.centralSpecificEnthalpy);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE(
|
||||
"Lane Emden Integration Matches The Analytic Incompressible Solution",
|
||||
tags::lane_emden_analytic
|
||||
) {
|
||||
using Catch::Approx;
|
||||
|
||||
const mean_field::seed::DimensionlessLaneEmdenSolution solution = mean_field::seed::integrateLaneEmden(0.0, 3.0);
|
||||
|
||||
REQUIRE(solution.firstZeroCoordinate.has_value());
|
||||
CHECK(*solution.firstZeroCoordinate == Approx(std::sqrt(6.0)).margin(2.0e-7));
|
||||
CHECK(solution.theta(solution.theta.Size() - 1) == 0.0);
|
||||
check_dimensionless_solution(
|
||||
solution, [](const double coordinate) { return 1.0 - coordinate * coordinate / 6.0; },
|
||||
[](const double coordinate) { return -coordinate / 3.0; }, 2.0e-7
|
||||
);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Lane Emden Integration Matches The Analytic Index One Solution",
|
||||
tags::lane_emden_analytic
|
||||
) {
|
||||
using Catch::Approx;
|
||||
|
||||
const mean_field::seed::DimensionlessLaneEmdenSolution solution = mean_field::seed::integrateLaneEmden(1.0, 4.0);
|
||||
|
||||
REQUIRE(solution.firstZeroCoordinate.has_value());
|
||||
CHECK(*solution.firstZeroCoordinate == Approx(std::numbers::pi_v<double>).margin(2.0e-7));
|
||||
CHECK(solution.theta(solution.theta.Size() - 1) == 0.0);
|
||||
check_dimensionless_solution(
|
||||
solution, [](const double coordinate) { return coordinate == 0.0 ? 1.0 : std::sin(coordinate) / coordinate; },
|
||||
[](const double coordinate) {
|
||||
if (coordinate == 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
if (coordinate < 1.0e-4) {
|
||||
return -coordinate / 3.0 + coordinate * coordinate * coordinate / 30.0;
|
||||
}
|
||||
return (coordinate * std::cos(coordinate) - std::sin(coordinate)) / (coordinate * coordinate);
|
||||
},
|
||||
2.0e-7
|
||||
);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Lane Emden Integration Matches The Analytic Index Five Infinite Solution",
|
||||
tags::lane_emden_analytic
|
||||
) {
|
||||
using Catch::Approx;
|
||||
|
||||
constexpr double coordinateLimit = 20.0;
|
||||
const mean_field::seed::DimensionlessLaneEmdenSolution solution =
|
||||
mean_field::seed::integrateLaneEmden(5.0, coordinateLimit);
|
||||
|
||||
CHECK_FALSE(solution.firstZeroCoordinate.has_value());
|
||||
CHECK(solution.coordinate(solution.coordinate.Size() - 1) == Approx(coordinateLimit));
|
||||
CHECK(solution.theta(solution.theta.Size() - 1) > 0.0);
|
||||
check_dimensionless_solution(
|
||||
solution, [](const double coordinate) { return 1.0 / std::sqrt(1.0 + coordinate * coordinate / 3.0); },
|
||||
[](const double coordinate) { return -coordinate / 3.0 * std::pow(1.0 + coordinate * coordinate / 3.0, -1.5); },
|
||||
2.0e-7
|
||||
);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Lane Emden Seed Uses The Stellar Model Central Density Phase Condition",
|
||||
tags::lane_emden_seed
|
||||
) {
|
||||
using namespace mean_field;
|
||||
using Catch::Approx;
|
||||
|
||||
const auto stellarModel = model::StellarModel(
|
||||
eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
|
||||
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{2.0}})
|
||||
);
|
||||
const seed::LaneEmden strategy({.radialSampleCount = 64});
|
||||
|
||||
STATIC_CHECK(seed::RadialSeedStrategyFor<seed::LaneEmden, decltype(stellarModel)>);
|
||||
|
||||
const seed::RadialProfile profile = seed::generateRadialProfile(stellarModel, strategy);
|
||||
REQUIRE(profile.radius.Size() == 64);
|
||||
REQUIRE(profile.density.Size() == 64);
|
||||
REQUIRE(profile.specificEnthalpy.Size() == 64);
|
||||
CHECK(profile.centralDensity == dimensions::DensityValue{2.0});
|
||||
CHECK(profile.centralSpecificEnthalpy.value() == Approx(std::cbrt(2.0)));
|
||||
CHECK(profile.radius(0) == 0.0);
|
||||
CHECK(profile.radius(63) == profile.stellarRadius.value());
|
||||
CHECK(profile.density(0) == 2.0);
|
||||
CHECK(profile.density(63) == 0.0);
|
||||
CHECK(profile.specificEnthalpy(0) == profile.centralSpecificEnthalpy.value());
|
||||
CHECK(profile.specificEnthalpy(63) == 0.0);
|
||||
|
||||
for (int index = 1; index < profile.radius.Size(); ++index) {
|
||||
CHECK(profile.radius(index) > profile.radius(index - 1));
|
||||
CHECK(profile.density(index) <= profile.density(index - 1));
|
||||
CHECK(profile.specificEnthalpy(index) <= profile.specificEnthalpy(index - 1));
|
||||
CHECK(profile.density(index) >= 0.0);
|
||||
CHECK(profile.specificEnthalpy(index) >= 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Explicit Lane Emden Seed Density Is Independent Of Model Invariants",
|
||||
tags::lane_emden_seed
|
||||
) {
|
||||
using namespace mean_field;
|
||||
|
||||
const auto unitMassModel = model::StellarModel(
|
||||
eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
|
||||
);
|
||||
const auto largeMassModel = model::StellarModel(
|
||||
eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{9.0}})
|
||||
);
|
||||
const seed::LaneEmden strategy({.centralDensity = dimensions::DensityValue{1.7}, .radialSampleCount = 48});
|
||||
|
||||
const seed::RadialProfile unitMassProfile = seed::generateRadialProfile(unitMassModel, strategy);
|
||||
const seed::RadialProfile largeMassProfile = seed::generateRadialProfile(largeMassModel, strategy);
|
||||
check_profiles_are_identical(unitMassProfile, largeMassProfile);
|
||||
|
||||
CHECK_THROWS_AS(
|
||||
seed::generateRadialProfile(unitMassModel, seed::LaneEmden({.radialSampleCount = 48})), std::invalid_argument
|
||||
);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Legacy Polytropic Structure Seed Is An Exact Adapter Over Lane Emden Generation",
|
||||
tags::lane_emden_seed
|
||||
) {
|
||||
using namespace mean_field;
|
||||
|
||||
const eos::Polytrope equationOfState({.n = 3.0, .K = 0.25});
|
||||
const seed::RadialProfile profile =
|
||||
seed::generateLaneEmdenProfile(equationOfState, dimensions::DensityValue{1.25}, 40);
|
||||
const models::structure::StructureSeed legacySeed =
|
||||
models::structure::PolytropicStructure{equationOfState, 7.0}.makeInitialSeed(
|
||||
{.centralDensity = 1.25, .radialSampleCount = 40}
|
||||
);
|
||||
|
||||
REQUIRE(legacySeed.radius.Size() == profile.radius.Size());
|
||||
for (int index = 0; index < profile.radius.Size(); ++index) {
|
||||
CHECK(legacySeed.radius(index) == profile.radius(index));
|
||||
CHECK(legacySeed.density(index) == profile.density(index));
|
||||
CHECK(legacySeed.enthalpy(index) == profile.specificEnthalpy(index));
|
||||
}
|
||||
CHECK(legacySeed.stellarRadius == profile.stellarRadius.value());
|
||||
CHECK(legacySeed.centralDensity == profile.centralDensity.value());
|
||||
CHECK(legacySeed.centralEnthalpy == profile.centralSpecificEnthalpy.value());
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Lane Emden Seed Rejects Invalid Numerical Prescriptions",
|
||||
tags::lane_emden_seed
|
||||
) {
|
||||
using namespace mean_field;
|
||||
|
||||
CHECK_THROWS_AS(seed::LaneEmden({.radialSampleCount = 1}), std::invalid_argument);
|
||||
CHECK_THROWS_AS(seed::LaneEmden({.centralDensity = dimensions::DensityValue{0.0}}), std::invalid_argument);
|
||||
CHECK_THROWS_AS(
|
||||
seed::LaneEmden({.centralDensity = dimensions::DensityValue{std::numeric_limits<double>::infinity()}}),
|
||||
std::invalid_argument
|
||||
);
|
||||
CHECK_THROWS_AS(
|
||||
seed::generateLaneEmdenProfile(eos::Polytrope({.n = 5.0, .K = 0.25}), dimensions::DensityValue{1.0}, 8),
|
||||
std::invalid_argument
|
||||
);
|
||||
}
|
||||
189
tests/seed/stellar_equilibrium_projection.cpp
Normal file
189
tests/seed/stellar_equilibrium_projection.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <mfem.hpp>
|
||||
|
||||
import mean_field;
|
||||
import test_helpers;
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies make_dependencies() {
|
||||
return {
|
||||
.discretization = {.identity = 7001, .revision = 1},
|
||||
.density = {.identity = 7003, .revision = 1},
|
||||
.surfaceDeformation = {.identity = 7009, .revision = 1},
|
||||
.gravityGradient = {.identity = 7013, .revision = 1},
|
||||
.gravityPotential = {.identity = 7019, .revision = 1},
|
||||
.enthalpy = {.identity = 7027, .revision = 1},
|
||||
.bernoulliConstant = {.identity = 7039, .revision = 1},
|
||||
.rotation = {.identity = 7043, .revision = 1},
|
||||
.targetMass = {.identity = 7057, .revision = 1}
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] mean_field::physics::RigidRotation make_zero_rotation() {
|
||||
mfem::Vector angularVelocity(3);
|
||||
mfem::Vector center(3);
|
||||
angularVelocity = 0.0;
|
||||
center = 0.0;
|
||||
return {angularVelocity, center};
|
||||
}
|
||||
|
||||
template <typename Vector> void check_finite(const Vector &values) {
|
||||
for (int index = 0; index < values.Size(); ++index) {
|
||||
REQUIRE(std::isfinite(values(index)));
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE(
|
||||
"Projected Equilibrium States Preserve Their Compiled Stellar Model Type",
|
||||
tags::stellar_seed_projection_type_contract
|
||||
) {
|
||||
using namespace mean_field;
|
||||
using BaseModel =
|
||||
model::StellarModel<models::SpecificationSet<eos::Polytrope, surface::Isobaric, integral::FixedTotalMass>>;
|
||||
using CentralDensityModel = model::StellarModel<models::SpecificationSet<
|
||||
eos::Polytrope, surface::Isobaric, integral::FixedTotalMass, constraint::FixedCentralDensity>>;
|
||||
using BaseState = seed::ProjectedEquilibriumState<BaseModel>;
|
||||
using CentralDensityState = seed::ProjectedEquilibriumState<CentralDensityModel>;
|
||||
|
||||
STATIC_CHECK_FALSE(std::same_as<BaseState, CentralDensityState>);
|
||||
STATIC_CHECK(std::same_as<typename BaseState::ModelType, BaseModel>);
|
||||
STATIC_CHECK(std::same_as<typename CentralDensityState::ModelType, CentralDensityModel>);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Lane Emden Projection Builds A Complete Compiled Stellar Equilibrium State",
|
||||
tags::stellar_seed_projection
|
||||
) {
|
||||
using namespace mean_field;
|
||||
using Catch::Approx;
|
||||
|
||||
utils::Args args = test_utils::setup_args();
|
||||
fem::FEM finiteElementModel = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(finiteElementModel.okay());
|
||||
|
||||
constexpr double stellarRadius = utils::RADIUS;
|
||||
constexpr double targetMass = utils::MASS;
|
||||
const double polytropicConstant = 2.0 * utils::G * stellarRadius * stellarRadius / std::numbers::pi_v<double>;
|
||||
const double centralDensity =
|
||||
std::numbers::pi_v<double> * targetMass / (4.0 * stellarRadius * stellarRadius * stellarRadius);
|
||||
|
||||
const auto stellarModel = model::StellarModel(
|
||||
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
|
||||
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
|
||||
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
|
||||
);
|
||||
auto problem = equilibrium::discretize(stellarModel, finiteElementModel);
|
||||
|
||||
STATIC_CHECK(seed::RadialSeedStrategyFor<seed::LaneEmden, decltype(stellarModel)>);
|
||||
STATIC_CHECK(
|
||||
std::same_as<
|
||||
decltype(seed::makeProjectedEquilibriumState(problem, seed::LaneEmden{})),
|
||||
seed::ProjectedEquilibriumState<typename std::remove_cvref_t<decltype(problem)>::ModelType>>
|
||||
);
|
||||
|
||||
const auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 4096}));
|
||||
REQUIRE(projected.values.Size() == problem.StateSize());
|
||||
check_finite(projected.values);
|
||||
|
||||
const auto stateView = problem.GetManifest().stateView(projected.values);
|
||||
const mfem::Vector density = stateView.block(utils::blocks::density_field.mass_term);
|
||||
const mfem::Vector surface = stateView.block(utils::blocks::surface_deformation_field.parameters_term);
|
||||
const mfem::Vector gravityGradient = stateView.block(utils::blocks::gravity_field.gradient_term);
|
||||
const mfem::Vector gravityPotential = stateView.block(utils::blocks::gravity_field.poisson_term);
|
||||
const mfem::Vector enthalpy = stateView.block(utils::blocks::enthalpy_field.specific_term);
|
||||
const mfem::Vector fixedMassCoordinate =
|
||||
stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term);
|
||||
const mfem::Vector centralDensityBorder =
|
||||
stateView.block(utils::blocks::fixed_central_density_phase.central_value_term);
|
||||
|
||||
CHECK(density.Norml2() > 0.0);
|
||||
CHECK(gravityGradient.Norml2() > 0.0);
|
||||
CHECK(gravityPotential.Norml2() > 0.0);
|
||||
CHECK(enthalpy.Norml2() > 0.0);
|
||||
CHECK(surface.Normlinf() == 0.0);
|
||||
REQUIRE(fixedMassCoordinate.Size() == 1);
|
||||
CHECK(fixedMassCoordinate(0) == Approx(-utils::G * targetMass / stellarRadius).margin(2.0e-7));
|
||||
REQUIRE(centralDensityBorder.Size() == 1);
|
||||
CHECK(centralDensityBorder(0) == 0.0);
|
||||
|
||||
const operators::PreparedCentralDensityStellarEquilibriumReport preparation =
|
||||
problem.Prepare(projected.values, make_dependencies(), make_zero_rotation());
|
||||
CHECK(preparation.assembledResidual);
|
||||
|
||||
mfem::Vector residual;
|
||||
problem.BuildResidual(residual);
|
||||
REQUIRE(residual.Size() == problem.EquationSize());
|
||||
check_finite(residual);
|
||||
|
||||
const operators::RootConstraintReport massReport = problem.GetPreparedOperator().GetFixedMassReport();
|
||||
CHECK(std::abs(massReport.scaledResidual) < 5.0e-4);
|
||||
const operators::CentralDensityConstraintReport centralDensityReport =
|
||||
problem.GetPreparedOperator().GetCentralDensityReport();
|
||||
CHECK(centralDensityReport.targetDensity == Approx(centralDensity));
|
||||
CHECK(std::abs(centralDensityReport.enthalpyResidual) < 1.0e-10);
|
||||
|
||||
const auto residualView = problem.GetManifest().residualView(residual);
|
||||
const mfem::Vector enthalpyResidual = residualView.block(utils::blocks::enthalpy_field.specific_term);
|
||||
const auto &surfaceRows = problem.GetPressureSurfaceRows();
|
||||
for (const int surfaceRow : surfaceRows.reduced_dofs()) {
|
||||
CHECK(enthalpy(surfaceRow) == 0.0);
|
||||
CHECK(enthalpyResidual(surfaceRow) == 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Lane Emden Projection Rejects A Seed Whose Surface Does Not Match The Reference Discretization",
|
||||
tags::stellar_seed_projection
|
||||
) {
|
||||
using namespace mean_field;
|
||||
|
||||
utils::Args args = test_utils::setup_args();
|
||||
fem::FEM finiteElementModel = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(finiteElementModel.okay());
|
||||
|
||||
const auto stellarModel = model::StellarModel(
|
||||
eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
|
||||
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}})
|
||||
);
|
||||
auto problem = equilibrium::discretize(stellarModel, finiteElementModel);
|
||||
|
||||
const seed::RadialProfile mismatchedProfile =
|
||||
seed::generateRadialProfile(problem.GetStellarModel(), seed::LaneEmden({.radialSampleCount = 64}));
|
||||
CHECK_THROWS_AS(seed::projectRadialProfile(problem, mismatchedProfile), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Lane Emden Projection Rejects A Nonzero Isobaric Surface",
|
||||
tags::stellar_seed_projection
|
||||
) {
|
||||
using namespace mean_field;
|
||||
|
||||
utils::Args args = test_utils::setup_args();
|
||||
fem::FEM finiteElementModel = fem::setup_fem(args.mesh_file, args, 0);
|
||||
REQUIRE(finiteElementModel.okay());
|
||||
|
||||
const double polytropicConstant = 2.0 * utils::G / std::numbers::pi_v<double>;
|
||||
const double centralDensity = std::numbers::pi_v<double> / 4.0;
|
||||
const auto stellarModel = model::StellarModel(
|
||||
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
|
||||
surface::Isobaric({.Psurf = dimensions::PressureValue{0.01}}),
|
||||
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}),
|
||||
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
|
||||
);
|
||||
auto problem = equilibrium::discretize(stellarModel, finiteElementModel);
|
||||
const seed::RadialProfile profile =
|
||||
seed::generateRadialProfile(problem.GetStellarModel(), seed::LaneEmden({.radialSampleCount = 64}));
|
||||
|
||||
CHECK_THROWS_AS(seed::projectRadialProfile(problem, profile), std::invalid_argument);
|
||||
}
|
||||
Reference in New Issue
Block a user