feat(utils/laneemden): 1D lane-emden solver

simple rk4 lane emden solver. reimplemented from work in Dartmouth A115
This commit is contained in:
2026-11-09 13:39:32 -05:00
parent ba457e7d10
commit 7134f99d7c
5 changed files with 515 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
#pragma once
#include "serif/utils/laneemden/solution.hpp"
#include "serif/utils/laneemden/scaling.hpp"

View File

@@ -0,0 +1,63 @@
#pragma once
#include <expected>
#include <optional>
#include <string>
#include "serif/dimensions/dimensions.hpp"
#include "serif/utils/laneemden/solution.hpp"
namespace serif::utils::laneemden {
/** @brief A physical conversion of one finite-radius dimensionless polytrope, in the caller's consistent units. */
struct PhysicalScales final {
double n;
double radial_scale;
dimensions::QuantityValue<dimensions::Mass> mass;
dimensions::QuantityValue<dimensions::Length> radius;
dimensions::QuantityValue<dimensions::MassDensity> central_density;
dimensions::QuantityValue<dimensions::SpecificEnthalpy> central_enthalpy;
dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant;
// K has n-dependent dimensions; absent for n = 0 or when this optional derived value is not representable.
std::optional<double> polytropic_constant;
};
/** @brief Physical scalar fields; gravity is the outward potential derivative, not the inward force component. */
struct PhysicalSample final {
dimensions::QuantityValue<dimensions::MassDensity> density;
dimensions::QuantityValue<dimensions::Pressure> pressure;
dimensions::QuantityValue<dimensions::SpecificEnthalpy> enthalpy;
dimensions::QuantityValue<dimensions::GravitationalPotential> potential;
dimensions::QuantityValue<dimensions::Mass> enclosed_mass;
dimensions::QuantityValue<dimensions::Acceleration> gravity;
};
/** @brief Fix a reference star by mass and central density; its implied K is an output, never a change to an EOS. */
[[nodiscard]] std::expected<PhysicalScales, std::string> scale_from_mass(
const Solution &solution,
dimensions::QuantityValue<dimensions::Mass> mass,
dimensions::QuantityValue<dimensions::MassDensity> central_density,
dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant);
/** @brief Fix a physical star by central enthalpy and density, including the incompressible n = 0 limit. */
[[nodiscard]] std::expected<PhysicalScales, std::string> scale_from_enthalpy(
const Solution &solution,
dimensions::QuantityValue<dimensions::SpecificEnthalpy> central_enthalpy,
dimensions::QuantityValue<dimensions::MassDensity> central_density,
dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant);
/** @brief Fix a polytropic EOS through its K and central density; n must be strictly positive. */
[[nodiscard]] std::expected<PhysicalScales, std::string> scale_from_polytropic_constant(
const Solution &solution,
double polytropic_constant,
dimensions::QuantityValue<dimensions::MassDensity> central_density,
dimensions::QuantityValue<dimensions::GravitationalConstant> gravitational_constant);
/**
* @brief Evaluate physical fields, continuing into vacuum beyond the stellar surface.
*
* The potential vanishes at infinity. Density, pressure, and enthalpy are zero at and
* beyond the surface, including the exterior side of the incompressible density jump.
*/
[[nodiscard]] std::expected<PhysicalSample, std::string> evaluate_physical(
const Solution &solution, const PhysicalScales &scales, dimensions::QuantityValue<dimensions::Length> radius);
}

View File

@@ -0,0 +1,141 @@
#pragma once
#include <cstddef>
#include <expected>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <format>
#include "serif/utils/error/errors.hpp"
namespace serif::utils::laneemden {
/** @brief Dimensionless integration controls; the surface tolerance does not bound the global integration error. */
struct IntegrationOptions final {
double step = 0.01;
double maximum_radius = 100.0;
std::size_t maximum_steps = 100000;
double surface_tolerance = 1e-10;
std::size_t central_series_terms = 12;
/** @brief Compare the numerical prescription when tracking reference dependencies. */
friend bool operator==(const IntegrationOptions &, const IntegrationOptions &) = default;
};
/** @brief The regular Lane–Emden solution and its first derivative at one dimensionless radius. */
struct Sample final {
double xi;
double theta;
double derivative;
};
/** @brief A regular profile, ending at its first zero or the requested integration radius. */
struct Solution final {
double n;
std::vector<Sample> samples;
std::optional<Sample> surface;
};
/**
* @brief Integrate theta'' + 2 theta'/xi + theta^n = 0 from the regular center.
*
* The central series follows Boudreaux's original power-series recurrence. Both dependent
* variables now advance through the same four Runge–Kutta stages. A missing surface is a
* valid finite-interval result, including the n >= 5 solutions. Exhausting the step budget
* or producing an unrepresentable numerical state returns a failure instead.
*/
[[nodiscard]] std::expected<Solution, std::string> solve(double n, IntegrationOptions options = {});
/** @brief Evaluate a solved dimensionless profile with cubic Hermite interpolation, without extrapolation. */
[[nodiscard]] std::expected<Sample, std::string> evaluate(const Solution &solution, double xi);
}
template <>
struct std::formatter<serif::utils::laneemden::IntegrationOptions> {
template <typename FormatContext>
auto format(const serif::utils::laneemden::IntegrationOptions &options, FormatContext &ctx) const {
return std::format_to(ctx.out(), "step={}, maximum_radius={}, maximum_steps={}, surface_tolerance={}, central_series_terms={}",
options.step, options.maximum_radius, options.maximum_steps, options.surface_tolerance, options.central_series_terms);
}
};
/** @brief Select all sample values, or one named component with :xi, :theta, or :derivative. */
template <>
struct std::formatter<serif::utils::laneemden::Sample> {
enum class Component { all, xi, theta, derivative };
Component component{Component::all};
/** @brief Validate the selector during literal format checking or runtime format parsing. */
constexpr auto parse(std::format_parse_context &ctx) {
using namespace serif::utils::errors;
component = Component::all;
auto end = ctx.begin();
while (end != ctx.end() && *end != '}') {
++end;
}
if (end == ctx.begin()) {
return end;
}
const std::string_view selection(ctx.begin(), end);
if (selection == "all") {
component = Component::all;
} else if (selection == "xi") {
component = Component::xi;
} else if (selection == "theta") {
component = Component::theta;
} else if (selection == "derivative") {
component = Component::derivative;
} else {
serif_error<std::format_error, SERiFErrorCode::INVALID_ARGUMENT>(
"Unknown Lane–Emden sample selector '{}'. Use an empty specifier, 'all', 'xi', 'theta', or 'derivative'.", selection);
}
return end;
}
/** @brief Keep the labeled tuple for complete samples and emit a bare number for a selected component. */
template <typename FormatContext>
auto format(const serif::utils::laneemden::Sample &sample, FormatContext &ctx) const {
switch (component) {
case Component::xi:
return std::format_to(ctx.out(), "{}", sample.xi);
case Component::theta:
return std::format_to(ctx.out(), "{}", sample.theta);
case Component::derivative:
return std::format_to(ctx.out(), "{}", sample.derivative);
default:
return std::format_to(ctx.out(), "(ξ={}, θ={}, dθ/dξ={})", sample.xi, sample.theta, sample.derivative);
}
}
};
/** @brief Describe the solution while applying one shared selection to its profile and optional surface sample. */
template <>
struct std::formatter<serif::utils::laneemden::Solution> {
std::formatter<serif::utils::laneemden::Sample> sample_formatter;
/** @brief Delegate selection to Sample so the two formatters cannot develop different grammars. */
constexpr auto parse(std::format_parse_context &ctx) {
return sample_formatter.parse(ctx);
}
/** @brief Stream the profile through its parsed sample formatter without building intermediate strings. */
template <typename FormatContext>
auto format(const serif::utils::laneemden::Solution &solution, FormatContext &ctx) const {
ctx.advance_to(std::format_to(ctx.out(), "Solution(n={}, samples=[", solution.n));
for (std::size_t index = 0; index < solution.samples.size(); ++index) {
if (index != 0) {
ctx.advance_to(std::format_to(ctx.out(), ", "));
}
ctx.advance_to(sample_formatter.format(solution.samples[index], ctx));
}
ctx.advance_to(std::format_to(ctx.out(), "], surface="));
if (solution.surface) {
ctx.advance_to(sample_formatter.format(*solution.surface, ctx));
} else {
ctx.advance_to(std::format_to(ctx.out(), "none"));
}
return std::format_to(ctx.out(), ")");
}
};