feat(utils/laneemden): 1D lane-emden solver
simple rk4 lane emden solver. reimplemented from work in Dartmouth A115
This commit is contained in:
4
src/include/serif/utils/laneemden/laneemden.hpp
Normal file
4
src/include/serif/utils/laneemden/laneemden.hpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include "serif/utils/laneemden/solution.hpp"
|
||||
#include "serif/utils/laneemden/scaling.hpp"
|
||||
63
src/include/serif/utils/laneemden/scaling.hpp
Normal file
63
src/include/serif/utils/laneemden/scaling.hpp
Normal 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);
|
||||
}
|
||||
141
src/include/serif/utils/laneemden/solution.hpp
Normal file
141
src/include/serif/utils/laneemden/solution.hpp
Normal 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(), ")");
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user