Files
MeanField_SERiF/docs/static/laneemden.md
Emily Boudreaux 6b57738330 docs(docs): built docs
Note for now much of the docs is LLM written. This was done to save time and focus on code development. The docs have been LOOSLEY reviewed. However, the code itself has been either fully written by hand or fully reviewed. Therefore, the docs should at the moment be seen as a loose resource rather than authoratiative (and this is on top of the general ability for docs to drift)
2026-11-09 14:09:44 -05:00

173 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lane–Emden profiles and radial projection
`serif::utils::laneemden` solves the dimensionless, regular-center Lane–Emden equation,
\[
\theta''+\frac{2}{\xi}\theta'+\theta^n=0,
\qquad \theta(0)=1,\qquad\theta'(0)=0.
\]
The dimensionless calculation is independent of MFEM, the equation of state, and any
choice of physical units. Seed construction and normalization can therefore share it.
## Dimensionless solution
```cpp
#include "serif/utils/laneemden/laneemden.hpp"
const auto solution = serif::utils::laneemden::solve(3.0, {
.step = 0.01,
.maximum_radius = 100.0,
.maximum_steps = 100000,
.surface_tolerance = 1e-10
});
```
The result is `std::expected<Solution, std::string>`. Check success before dereferencing.
A solution contains `n`, an ordered vector of `Sample{xi, theta, derivative}`, and an
optional `surface`. The final sample is the first zero when found. Otherwise it is the
requested maximum radius: this is a valid finite-interval solution, including the
infinite-radius profiles with `n >= 5`. Exhausting the step budget is an error.
`evaluate(solution, xi)` interpolates within the stored interval and rejects extrapolation.
The solver retains the central power-series recurrence from Boudreaux's original
`PolytropicStellarModel`, evaluating the coefficients iteratively. Both `theta` and its
derivative are initialized from that series and advance together through classical RK4.
The final approach uses shorter steps and a short root bracket, avoiding nonreal powers
of negative `theta` for fractional indices. `surface_tolerance` controls the final root
localization, **not the accumulated integration error**. Reduce `step` to establish
convergence. Fractional-index surface derivatives need not converge at fourth order,
because the solution loses smoothness there. The regular analytic solutions at
`n = 0, 1, 5` are covered by the tests.
## Choose a physical star
Three functions produce the same small `PhysicalScales` type:
| Function | Physical inputs, besides the solution and explicit `G` |
|---|---|
| `scale_from_mass` | Total mass and central density |
| `scale_from_enthalpy` | Central specific enthalpy and central density |
| `scale_from_polytropic_constant` | Polytropic EOS constant `K` and central density |
Each requires a found finite surface. The index and surface give
`mu = -xi_surface^2 * theta_prime_surface`. Mass and central density then fix
\[
\alpha=\left(\frac{M}{4\pi\rho_c\mu}\right)^{1/3},\qquad
R=\alpha\xi_{\mathrm{surface}},\qquad
h_c=4\pi G\rho_c\alpha^2.
\]
For an EOS prescription, `h_c = (n + 1) K rho_c^(1/n)` instead fixes `alpha`.
`scale_from_polytropic_constant` requires `n > 0`; the incompressible `n = 0` limit
can use either other prescription. `K` is a plain number because its dimensions depend
on `n`. Other physical inputs use SERiF's distinct quantity types.
Choosing mass and central density creates a reference star and reports its implied
`K`; it does not modify an existing EOS. To preserve a prescribed polytropic EOS,
use its `K`. In particular, at `n = 3` a fixed `K` and `G` already fix the mass:
changing central density changes the radius, not that mass.
Use one consistent unit system throughout. SERiF does not convert units implicitly.
For example, CGS inputs use grams, centimetres, seconds, density in grams per cubic
centimetre, and `G` in cubic centimetres per gram per square second. Neither physical
scaling nor integration contains a hardcoded gravitational constant.
`evaluate_physical(solution, scales, radius)` returns density, pressure, specific
enthalpy, gravitational potential, enclosed mass, and `gravity`. Here `gravity` is
positive outward `dPhi/dr`; inward gravitational acceleration is its negative.
Potential is zero at infinity. Beyond the surface the evaluator uses vacuum values:
zero density, pressure, and enthalpy, constant enclosed mass, `Phi = -GM/r`, and
`gravity = GM/r^2`. At the surface it uses the vacuum side of the density jump for
`n = 0`.
## Project a physical profile
Projection accepts a radial callable returning either `double` or
`std::expected<double, std::string>`. The latter propagates sampling failures. This
complete function prepares a reference polytrope and projects its density:
```cpp
#include "serif/utils/laneemden/laneemden.hpp"
#include "serif/utils/projection/projection.hpp"
/** @brief Construct a polytropic reference density on a supplied scalar H1 or L2 space. */
serif::utils::projection::backend::mfem::ProjectionResult polytropic_density(
mfem::FiniteElementSpace& space,
serif::dimensions::QuantityValue<serif::dimensions::Mass> mass,
serif::dimensions::QuantityValue<serif::dimensions::MassDensity> central_density,
serif::dimensions::QuantityValue<serif::dimensions::GravitationalConstant> gravitational_constant
) {
namespace laneemden = serif::utils::laneemden;
namespace projection = serif::utils::projection;
namespace dimensions = serif::dimensions;
const auto solution = laneemden::solve(3.0);
if (!solution) {
return std::unexpected(solution.error());
}
const auto scales = laneemden::scale_from_mass(
*solution, mass, central_density, gravitational_constant
);
if (!scales) {
return std::unexpected(scales.error());
}
const auto density = [&](double radius) -> projection::RadialValue {
return laneemden::evaluate_physical(
*solution, *scales, dimensions::QuantityValue<dimensions::Length>{radius}
).transform([](const laneemden::PhysicalSample& sample) {
return sample.density.value();
});
};
return projection::backend::mfem::project_scalar(space, density);
}
```
The returned grid function owns its coefficients and borrows the supplied finite-element
space. That space and its mesh must remain alive. Projection completes synchronously,
so the profile and scaling objects in the example do not need to outlive the call.
Use `project_vector` with the radial `gravity` amplitude for outward potential gradients
in component-vector spaces. The projection handles MFEM component ordering. For a displaced
star, options can supply `.center = {x, y, z}`. An optional `.mapping = &evaluator` samples
the physical geometry through SERiF's mapping and applies the required scalar-integral pullback.
The mapping must belong to the same mesh and be refreshed after its fields change.
These are the finite element's native coefficient projections, usually interpolation;
hanging-node constraints are enforced by conforming restriction before
publishing the field. They do not enforce a global mass constraint or perform a global L2 fit. There is no
implicit rescaling of the mesh or adjustment of the sampled profile. Numerical sample
failures discard the entire result. Incompatible space or mapping bindings throw a
caller-error exception. Compactified infinity needs an explicit limiting-value prescription,
as shown below.
## Compactified infinity
Finite physical samples are still taken from the radial profile. At points identified
by the mapping as infinity, `scalar_at_infinity` or `vector_at_infinity` can supply an
explicit limit. Each callback receives the original mesh position and returns the
limit **already pulled back into mesh coordinates**, before MFEM's own element mapping.
Without a callback, projection rejects those points. A failed, non-finite, or
wrong-dimensional limit also rejects the result.
For example, a star's vacuum potential tends to zero at infinity:
```cpp
namespace projection = serif::utils::projection;
const projection::backend::mfem::RadialProjectionOptions exterior{
.mapping = &evaluator,
.scalar_at_infinity = [](const mfem::Vector&) -> projection::RadialValue {
return 0.0;
}
};
```
Pass `exterior` as the last argument to the projection call. A displaced exterior or a
different asymptotic field must provide its own limit.
The callback is used only at points the mapper classifies as infinity. Finite points
extremely close to that boundary still use the existing Jacobian determinant and
inverse, whose conditioning can reduce accuracy.