Skip to content

Validation: src/proteus/orbit/parameterized.py

This page tracks the @pytest.mark.reference_pinned tests that anchor the prescribed migration laws in proteus.orbit.parameterized. The module is closed-form, so the sigmoid anchor is an analytical limit and the high-eccentricity anchor is the published law of Postolec et al. (2026): there is no external data set to compare against, and the contract is the mathematics the laws are defined by.

Test id Reference Source page Scope
tests/orbit/test_parameterized.py::test_sigmoid_matches_the_cubic_smoothstep_across_the_window Analytical limit: the cubic Hermite interpolant with zero slope at both ends, S(u) = 3u^2 - 2u^3, at u = 1/4, 1/2 and 3/4 Standard Pins sigmoid_migration across its migration window. Separates the cubic from a linear ramp and from the quintic smootherstep, and catches a swapped sma_init/sma_final.
tests/orbit/test_parameterized.py::test_high_ecc_energy_rate_at_the_epoch_is_twice_the_energy_change_over_tau Analytical limit: dE/dt of E = -G M m (1 - e_mig^2 x) / (2 a_f) at x = 1 equals 2 dE / tau Derived below Pins orbital_energy_rate at the high-eccentricity epoch for the TOI-561 b setup (dE = -2.86e35 J, -1.8e21 W at tau = 1e7 yr), with guards on the factor 2, the sign and the unit. Last compared 2026-10-02.
tests/orbit/test_parameterized.py::test_high_ecc_circularises_as_a_pure_exponential_in_eccentricity Postolec et al. (2026), submitted to ApJ, arXiv:2609.03144 (doi:10.48550/arXiv.2609.03144) Sect. 2.2, Eqs. 1-4 Pins e(t) = e_mig exp(-(t - t_mig) / tau), a form the source never evaluates, plus the one-tau e-folding. The semi-latus-rectum equality a (1 - e^2) = a_f is kept as a sanity check only: it is an identity of the implementation, not an emergent conservation law. Also asserts the pericentre a (1 - e) is not conserved.

Re-derivation notes

Sigmoid migration

The law is a cubic S curve on a migration window of finite length, clamped to its endpoints outside it:

u    = (t - t_mig) / tau,  clamped to [0, 1]
S(u) = 3u^2 - 2u^3
a(t) = a_0 + (a_f - a_0) S(u)

S is the unique cubic with S(0) = 0, S(1) = 1 and S'(0) = S'(1) = 0. Both endpoint conditions matter: the value conditions make a(t) continuous at the window edges, and the slope conditions make da/dt continuous there too. A discontinuous orbit would hand the atmosphere a discontinuous instellation, and a kinked one would hand it a discontinuous heating rate.

With the test endpoints a_0 = 2.0 AU and a_f = 0.8 AU, S(1/4) = 5/32 gives 1.8125 AU, S(1/2) = 1/2 gives 1.4 AU and S(3/4) = 27/32 gives 0.9875 AU. The quarter point is the discriminating one: a linear ramp puts it at 1.7 AU and the quintic smootherstep 6u^5 - 15u^4 + 10u^3 at 1.8757 AU, both far outside the tolerance. The cubic is symmetric about the window centre, S(u) + S(1 - u) = 1, which the test also asserts and which holds whatever the endpoints are.

The clamp is not cosmetic. Continued past its window the cubic runs away: S(2) = -4 puts the orbit at 6.8 AU and S(-1) = 5 puts it at -4.0 AU, so an unclamped evaluation at any time outside the window returns an unphysical orbit.

Here tau is the length of the migration window, not an exponential decay constant. It carries that second meaning in the high-eccentricity law below, so the two regimes read the same configuration key differently.

The rel=1e-12 tolerance is machine precision: the expression is closed-form algebra with no solver or lookup in the path.

High-eccentricity migration

The law (Postolec et al. 2026, Sect. 2.2, Eqs. 1-4) is

e_mig = sqrt(1 - a_f / a_0)
a(t)  = a_f / (1 - e_mig^2 exp(-2 (t - t_mig) / tau))
e(t)  = sqrt(1 - a_f / a(t))

Rearranging the eccentricity relation gives 1 - e^2 = a_f / a, hence

a (1 - e^2) = a_f

for every t >= t_mig. Since the specific orbital angular momentum is h = sqrt(G M a (1 - e^2)), holding a (1 - e^2) fixed is exactly conservation of h.

Read carefully, though, that equality is weaker than it looks as a test. The source computes e from a via e = sqrt(1 - a_f/a), so a (1 - e^2) = a_f follows algebraically for any a(t) whatsoever, including a wrong one. It verifies that e(t) inverts a(t) exactly; it cannot see an error in the time dependence. The test therefore also pins a(t) directly against the closed form above, written out independently, and that pin is the real anchor. A reader treating the semi-latus-rectum equality as an emergent conservation law would overstate what is checked.

At t = t_mig the decay factor is 1, so a = a_f / (1 - e_mig^2) = a_0 and e = e_mig: the orbit is still at its starting semi-major axis with the eccentricity excited to the value that places it on the final angular momentum.

Substituting a(t) into 1 - e^2 = a_f / a collapses the eccentricity to a pure exponential with half the decay rate of the semi-major axis:

e(t) = e_mig exp(-(t - t_mig) / tau)

so e e-folds in exactly one tau. That is the form the test pins, because the source never evaluates it: it recovers e from a, so an error in the time dependence cannot show up in the semi-latus rectum. Checked at the half decay point, sqrt(0.6) exp(-0.5 ln 2) = 0.547723, which is sqrt(0.3).

The same route costs precision at late times. Recovering e as sqrt(1 - a_f / a) subtracts two numbers that approach each other, and the quantity under the root is e^2, so it drops below double precision once e falls near 1e-8 and the returned eccentricity floors to exactly zero. The exponential is therefore pinned over the first five migration widths, and the floor is asserted separately as a boundedness property: the value reaches zero without passing through a negative intermediate, which would have produced a nan.

The pericentre a (1 - e) is not conserved: with the test endpoints it rises from 0.45 AU to 0.80 AU over the circularisation. This distinguishes the law from pericentre-conserving tidal circularisation, where the tide acts at pericentre and a_final tends to a_0 (1 - e_0). Anyone comparing model tracks against a tidal-circularisation result should start from that difference.

The rtol=1e-10 tolerance on the invariant absorbs the round-off of the sqrt round trip through e and back.

Orbital energy rate

The orbital energy is E = -G M_star M_planet / (2 a), so at fixed masses dE/dt = G M_star M_planet (da/dt) / (2 a^2). On the high-eccentricity track 1/a = (1 - e_mig^2 x) / a_f with x = exp(-2 (t - t_mig) / tau), which gives

dE/dt = -G M_star M_planet e_mig^2 x / (a_f tau)

At the epoch x = 1 and e_mig^2 = 1 - a_f / a_0, so the rate is 2 dE / tau with dE = E(a_f) - E(a_0) = -(G M_star M_planet / 2)(1/a_f - 1/a_0) the energy change of the whole circularisation, negative for inward migration. The rate depends on time through x alone, so one tau after the epoch it has fallen by exactly e^-2; a decay written with exp(-(t - t_mig) / tau) would fall by e^-1. The closure test integrates the rate with scipy.integrate.quad and recovers E(a_end) - E(a_0) to rel=1e-8 for the high-eccentricity track and for the sigmoid inward and outward, which a missing factor of 1/2 or a rate left per year instead of per second would miss by a factor of 2 or 3.2e7.

End-to-end check against dummy runs

The pinned tests above exercise the three laws as pure functions. A complementary check runs each regime as a dummy PROTEUS simulation and compares the recorded semimajorax and eccentricity against the same closed forms, so the config plumbing, the AU-to-metre conversion and the dispatch in evolve_orbit_star are covered as well as the algebra.

Regime max abs da [au] max abs de
none 0 0
instant 0 0
sigmoid 3.6e-11 0
high_ecc 4.3e-11 2.8e-09

The figure and the run settings are in Star-planet models.