Skip to content

Report the pre-impact eccentricity, and enforce the formatter - #14

Merged
timlichtenberg merged 6 commits into
mainfrom
tl/report-pre-impact-eccentricity
Jul 26, 2026
Merged

Report the pre-impact eccentricity, and enforce the formatter#14
timlichtenberg merged 6 commits into
mainfrom
tl/report-pre-impact-eccentricity

Conversation

@timlichtenberg

Copy link
Copy Markdown
Member

What this does

Adds e_before to each impact record, so both orbital elements are reported on either side of a collision, and turns on formatter enforcement in CI, which was written into the contributor guide but checked nowhere.

This changes the PROTEUS record schema and is a breaking interface change. PROTEUS reads the fields by name and refuses a record missing any of them, so a PROTEUS accretion run needs a release carrying e_before.

Why

An impact record carried the target's semi-major axis on both sides of the collision but its eccentricity only after. PROTEUS follows a planet that generally sits on a different orbit from the recorded body, so it applies the semi-major axis as the fractional change the collision made rather than transplanting an absolute value that belongs to this system. It had no way to do the same for the eccentricity, so it overwrote whatever the user configured. Tidal heating scales as the square of eccentricity, so that was a large change applied silently: a planet configured at 0.1 was reset to 0.004 at the first impact, and with tides off the orbit is re-pinned from the configuration every step, so it never recovered.

Reporting the eccentricity on both sides lets a consumer apply the difference, which is the eccentricity's analogue of the ratio already used for the semi-major axis. A difference rather than a ratio because eccentricity is dimensionless and routinely zero.

Separately, linting was enforced and formatting was not, so style had drifted: sixteen of the twenty-four source and test files did not match the formatter, and the pre-commit hook for it was present but commented out.

Changes

  • orbit_cross_K25.py: capture the target's eccentricity before the merger overwrites it, and report it as e_before.
  • driver.py: carry e_before into the per-impact record.
  • Coupling contract and the docs page that documents the schema updated to match.
  • Whole tree brought into line with the formatter, as its own commit so this diff stays readable.
  • CI runs ruff format --check alongside ruff check; the pre-commit hook is enabled. The generated _version.py is excluded, since the build writes it in its own style to an untracked path, so no contributor could bring it into line.

Testing

  • Full suite green, 47 passed.
  • ruff check and ruff format --check both clean over src/, tests/, tools/.
  • Marker validation and the test-quality lint both clean.
  • The merge-record test now pins e_before. Worth noting what it pins: the recorded value is the eccentricity at the moment of the collision, not the value the system started from. Viscous stirring excites the orbits first, so a pair seeded at 0.05 collides at 0.207. That is the geometry the collision actually happened at, which is what a consumer reconstructing the impact needs. The test also asserts it differs from e_after, since reading the same array one line later would report the merged value.

Each impact record carries the target's semi-major axis on both sides of the collision but its eccentricity only after. A consumer following a planet on a different orbit from the recorded body can therefore apply the semi-major axis as the fractional change the collision made, which is what PROTEUS does, but has nothing to do for the eccentricity except transplant an absolute value that belongs to this body's orbit rather than to the planet being simulated. That silently overwrites a configured eccentricity, and since tidal heating goes as the square of it, the effect is large and invisible.

The merge record now reports e_before alongside a_before, read from the state before the merger overwrites it, so both elements can be applied as changes. That value is the eccentricity at the moment of the collision, which viscous stirring has usually excited well above the value the system started from, so it is the geometry the collision actually happened at.
A formatting sweep with no behaviour change, so the check added next has a clean tree to enforce against and its first run does not bury a real change under hundreds of reformatted lines.
Linting was checked but formatting was not, so style drifted freely: sixteen of the twenty-four source and test files did not match the formatter. The pre-commit hook for it was present but commented out, and CI ran only the linter, which leaves the rule written down in the contributor guide and enforced nowhere.

CI now runs the format check alongside the lint, and the pre-commit hook is enabled so it is caught before a push rather than in review.

The generated version file is excluded, since the build writes it in its own style into a path that is not tracked, so no contributor could bring it into line and every fresh checkout would fail the check.
Three gaps in the previous two commits.

The record's pre-impact eccentricity was pinned only in a unit test on an equal-mass pair, where both bodies carry bit-identical eccentricities and reading the wrong one is invisible. Copying the post-impact value into the field, the mistake this diff most invites, passed the whole suite. There is now a pinned case on a 1.0 and 0.3 Earth-mass pair, where the rejection sampling scales each body's excitation by the square root of the other's mass so the two are distinguishable, and the full-run test asserts both that the field stays in range on every record and that it differs from the post-impact value. Both mistakes now fail.

The format gate was scoped to the packaged directories, which left the tracked plotting script at the repository root unchecked, and it was already out of format. It is now formatted and inside the gate, so the enforcement covers every tracked Python file rather than most of them.

CI installed the formatter unpinned while the commit hook pinned it, so a future release that shifts the formatter's output would have made the two disagree and turned the hook into a source of failures rather than a way to avoid them. CI now installs the version the hook uses.
The excitation applied to a pair before a collision is drawn without an upper bound, and the fallback taken when no draw satisfies the overlap condition is not bounded either, so a body can reach the merge branch already past e = 1. The ejection branch elsewhere treats exactly that condition as grounds for removing a body, so the two branches disagree about what an unbound orbit means. Over twenty thousand randomised crossings, five of 8268 merge records came out describing an open orbit, three of them through the post-impact value alone, which has been reported that way all along.

Everything a consumer derives from these records assumes a closed orbit: periapsis, the time-averaged separation, the Hill radius. The coupled framework refuses an entire impact history rather than use one that is not closed, so a single such record loses a whole run.

Both eccentricities are now capped where the record is written, and a capped record warns, naming the body and which side of the collision it applied to. The model's own state is untouched: the cap makes the record usable, it does not decide what the prescription should do with a body excited past a closed orbit, which is a question about the model rather than about its bookkeeping.
The excitation applied to a pair before a collision is drawn without an upper bound, and neither is the fallback taken when no draw satisfies the overlap condition, so a body can enter the collision branch already past unity eccentricity while the ejection branch treats that as grounds for removal. The two branches disagree about what an unbound orbit means.

Capping the values in the record was the wrong answer. A body that acquires an unbound eccentricity is removed on the same step by the perihelion cut and never survives to be reported, so the histories run_system returns carry no such value: across 432 systems and 2424 delivered records spanning the model's usual mass, spacing and separation ranges, not one record reached the cap. What the cap did reach was the case where only the pre-collision value needed it, and there it replaced a true eccentricity with a fixed one, so the change a consumer applies came out wrong by an unbounded amount with only a warning to say so. A quietly wrong number is worse than the rejection it was avoiding.

The behaviour is recorded in the limitations instead, where the caveat belongs: it is visible only to a caller reading the raw merger list rather than the returned histories, and what to do about the disagreement is a question about the prescription rather than about the bookkeeping.
@timlichtenberg
timlichtenberg merged commit 87202be into main Jul 26, 2026
4 checks passed
@timlichtenberg
timlichtenberg deleted the tl/report-pre-impact-eccentricity branch July 26, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant