From c90804cb4307da34e408b421c5553d8dc0496e66 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 25 Jul 2026 22:21:30 +0200 Subject: [PATCH 1/6] Explain the PROTEUS coupling and fill out the documentation The coupling page documented the call signature and the record schema but never the coupling itself: what Morrigan is for inside a coupled run, when it runs, what happens when an impact lands, or why any of it is arranged that way. A reader could see the interface and still not know how to use it. It now leads with the property that makes this module unlike the others. Every other module is called inside the time loop and updates its slice of the state each pass; Morrigan runs once, before the loop, and returns a schedule the loop replays. That is why the coupling is one way, why the timestepper rather than the model owns the clock, and why nothing the coupled planet does feeds back into the dynamics. The page then covers how one survivor is chosen out of a whole system, which is the coupling's real free decision, how the configuration maps onto the model's arguments, and the order the six consequences of an impact are applied in, which matters because each reads state the previous one wrote. A companion how-to carries the practical side: the minimal block, choosing a selector, turning on volatile delivery and atmospheric erosion, a worked example with the log it produces, and the five ways a run goes wrong, of which "no impacts scheduled" is the most common and the least obvious. Adds the pages the module was missing next to its siblings: an entry point that routes a reader by what they want to do, the ecosystem page with the shared framework schematic, a code architecture page mapping the source files onto the physics, a configuration guide covering how the settings interact rather than what each one means, and a release guide. Adds contributing guidelines and a code of conduct, so that the path for someone wanting to change the model reaches an end. Corrects the description of how systems are seeded, which three pages still stated in the superseded additive form, and removes a migration note from the readme. Prose describes the current state. Adds the PyPI publish workflow so a release is distributable, with a guard that refuses to upload when the version has fallen back to 0.0.0, since that name cannot be reclaimed. --- .github/workflows/publish.yaml | 48 ++++++++ CODE_OF_CONDUCT.md | 128 ++++++++++++++++++++ CONTRIBUTING.md | 63 ++++++++++ README.md | 4 +- docs/Community/CODE_OF_CONDUCT.md | 1 + docs/Community/CONTRIBUTING.md | 1 + docs/Community/contact.md | 11 ++ docs/Explanations/code_architecture.md | 75 ++++++++++++ docs/Explanations/proteus_coupling.md | 119 +++++++++++++++---- docs/How-to/configuration.md | 55 +++++++++ docs/How-to/proteus_coupling.md | 157 +++++++++++++++++++++++++ docs/How-to/releasing.md | 62 ++++++++++ docs/How-to/running.md | 6 +- docs/Reference/api.md | 6 +- docs/Reference/parameters.md | 2 +- docs/getting_started.md | 28 +++++ docs/index.md | 7 +- docs/proteus-framework.md | 43 +++++++ mkdocs.yml | 12 ++ 19 files changed, 796 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/publish.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/Community/CODE_OF_CONDUCT.md create mode 100644 docs/Community/CONTRIBUTING.md create mode 100644 docs/Community/contact.md create mode 100644 docs/Explanations/code_architecture.md create mode 100644 docs/How-to/configuration.md create mode 100644 docs/How-to/proteus_coupling.md create mode 100644 docs/How-to/releasing.md create mode 100644 docs/getting_started.md create mode 100644 docs/proteus-framework.md diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..aaf114e --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,48 @@ +name: Publish on PyPI + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + environment: release + permissions: + # Mandatory for trusted publishing: the workflow exchanges this token + # with PyPI for a short-lived credential, so no API token is stored. + id-token: write + + steps: + - uses: actions/checkout@v6 + with: + # setuptools-scm derives the version from the tag, so the full + # history and its tags have to be present. A shallow clone builds + # the configured fallback version instead and publishes 0.0.0. + fetch-depth: 0 + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Build the distribution + run: | + python -m pip install --upgrade pip + python -m pip install build "setuptools>=64" "setuptools-scm>=8" wheel + python -m build --no-isolation + + - name: Refuse to publish a fallback version + # A tagless checkout yields 0.0.0, which would take that name on PyPI + # permanently and cannot be reclaimed. Fail before uploading. + run: | + set -euo pipefail + if ls dist/ | grep -qE '0\.0\.0'; then + echo "Built a fallback version, so the tag history is missing." >&2 + ls dist/ >&2 + exit 1 + fi + ls dist/ + + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fed5081 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +contact@formingworlds.space. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..adfcaa7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing guidelines + +Morrigan is part of the [PROTEUS](https://proteus-framework.org/PROTEUS) ecosystem and follows its conventions. This page covers what a change has to satisfy before it can land. + +## Getting set up + +```console +git clone git@github.com:FormingWorlds/Morrigan.git +cd Morrigan +pip install -e ".[develop]" +pre-commit install -f +``` + +## Building the documentation + +The documentation is written in [Markdown](https://www.markdownguide.org/basic-syntax/) and built with [Zensical](https://zensical.org/), which reads the existing `mkdocs.yml` directly. + +```console +pip install -e ".[docs]" +zensical serve +``` + +For a one-shot production build: + +```console +zensical build --clean +``` + +The build must finish with no warnings. A warning usually means a link that does not resolve or a footnote that is never referenced, and both render as visible defects on the published site. + +## What a change has to satisfy + +```console +pytest -m "(unit or smoke) and not skip" # the tiers that run on every pull request +ruff check --fix src/ tests/ tools/ +ruff format src/ tests/ tools/ +bash tools/validate_test_structure.sh # every test carries a tier marker +python tools/check_test_quality.py --check # anti-happy-path lint +``` + +All four run in CI and block a merge. The test contract itself is documented in [Testing](https://proteus-framework.org/Morrigan/How-to/testing.html), and it is stricter than most: every physics routine must be pinned against a published value, an analytical limit or a cross-implementation check, and a pinned value needs a discrimination guard showing that the plausible wrong formula would fail the test. + +A pull request that adds or substantially changes more than fifty lines of test code triggers an independent review before it can merge. + +## Changing the physics + +Two things beyond the tests: + +**Cite the equation.** A change that transcribes or corrects a formula names the equation number in the source paper, in the pull request and in a comment at the call site. Transcription slips are this model's most common defect class, and the citation is what makes the next reader able to check it. + +**Say what moved.** Most physics changes shift ensemble outcomes. State what you measured, over how many seeds, and in which configuration, so that someone comparing results across versions knows what to expect. + +## Changing the record schema + +The per-impact record is the interface PROTEUS consumes. Any change to its fields, units or conventions is a breaking change for a coupled run and has to be called out in the pull request that makes it, so the PROTEUS-side pin can be updated in step. See [Coupling to PROTEUS](https://proteus-framework.org/Morrigan/Explanations/proteus_coupling.html). + +## Writing style + +Documentation, comments and commit messages describe the current state of the code, not how it came to be. A reader arriving cold needs to know what is true now; the history is in the commit log where it belongs. + +## Reporting a problem + +Open an issue at [FormingWorlds/Morrigan/issues](https://github.com/FormingWorlds/Morrigan/issues). For a physics problem, the most useful report contains the settings file, the seed, and what you expected instead, because a single system is a statistical realisation and the seed is what makes it reproducible. diff --git a/README.md b/README.md index 0b7dc43..566e43c 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,6 @@ Settings live in a `.toml` file; `initialise.toml` in the repository root is a w morrigan -c initialise.toml ``` -This replaces `python run_model.py`. If you have a launcher script of your own that still calls the old path, that is the line to change. - Keeping several settings files side by side and pointing at whichever you want is the reason for the `-c` flag. The repository ignores `*.toml` apart from `initialise.toml`, so a settings file you want to keep alongside the results it produced has to be added to git explicitly. Results are written under the `save_directory` named in the settings file. A relative `save_directory` is taken from the directory you run the command in, not from wherever the settings file happens to live, so the run prints the full path it is writing to: @@ -63,4 +61,4 @@ The suite is tiered (unit and smoke on every pull request, seed-ensemble statist ## Reproducibility -Each system is seeded from `random_seed` in the settings file plus its own index, so a given settings file reproduces the same systems exactly. +Each system's seed is mixed from `random_seed` in the settings file and the system's own index, so a given settings file reproduces the same systems exactly and two ensembles with neighbouring seeds are independent of each other. diff --git a/docs/Community/CODE_OF_CONDUCT.md b/docs/Community/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..14a6a4a --- /dev/null +++ b/docs/Community/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +{!../CODE_OF_CONDUCT.md!} diff --git a/docs/Community/CONTRIBUTING.md b/docs/Community/CONTRIBUTING.md new file mode 100644 index 0000000..568877b --- /dev/null +++ b/docs/Community/CONTRIBUTING.md @@ -0,0 +1 @@ +{!../CONTRIBUTING.md!} diff --git a/docs/Community/contact.md b/docs/Community/contact.md new file mode 100644 index 0000000..bef0936 --- /dev/null +++ b/docs/Community/contact.md @@ -0,0 +1,11 @@ +# Contact + +We encourage you to reach out. Choose the most appropriate channel below. + +| Channel | Use for | +|---------|---------| +| [Forming Worlds Discussions](https://github.com/orgs/FormingWorlds/discussions) | Questions, installation help, feature suggestions | +| [GitHub Issues](https://github.com/FormingWorlds/Morrigan/issues) | Bug reports, specific feature requests | +| [proteus_dev@formingworlds.space](mailto:proteus_dev@formingworlds.space) | General enquiries | + +Morrigan implements the model of Kimura et al. (2025); questions about the model itself, as opposed to this implementation of it, are best directed to the authors of that paper. diff --git a/docs/Explanations/code_architecture.md b/docs/Explanations/code_architecture.md new file mode 100644 index 0000000..6c84967 --- /dev/null +++ b/docs/Explanations/code_architecture.md @@ -0,0 +1,75 @@ +# Code architecture + +Eleven source files under `src/morrigan/`, laid out so that each stage of the physics has one home. This page maps them onto what the [model overview](model.md) describes. + +## The call graph + +``` + morrigan -c settings.toml + | + v + driver.run_batch parallel over systems + | + v + driver.run_once ONE system, start to finish + | + +-- allocate_a lay out embryos by mutual Hill spacing + | + +-- while t <= max_time: + | | + | +-- sort_planet drop the dead, re-sort by orbit + | | + | +-- secular_solution eccentricities between events + | | + | +-- crossing_pair which pair crosses next, and when + | | | + | | +-- interaction_timescales viscous and collision times + | | + | +-- orbit_cross_K25 resolve the event + | | + | +-- merge_embryo if they collide + | +-- (scatter in place) if they do not + | +-- (ejection branch) if one is excited past e = 1 + | + +-- write tables / return records +``` + +`run_system` is the in-process entry point beside `run_batch`. It drives the same `run_once` core but returns the records in memory instead of writing files, which is what PROTEUS consumes. + +## What each file owns + +| File | Owns | Physics or plumbing | +| --- | --- | --- | +| `driver.py` | Orchestration: settings, layout, the event loop, the record schema, survivor selection, output | physics | +| `crossing_pair.py` | Which adjacent pair goes unstable next and when | physics | +| `interaction_timescales.py` | Viscous stirring and collision timescales, the stability criterion | physics | +| `secular_solution.py` | Laplace-Lagrange eigenmodes between events | physics | +| `orbit_cross_K25.py` | Resolving a crossing into a merger, a scattering or an ejection | physics | +| `merge_embryo.py` | Merger bookkeeping: masses, orbits, eccentricities | physics | +| `helper_functions.py` | Kepler period, Hill radius, escape eccentricity, Rayleigh draws | physics | +| `sort_planet.py` | Dead-planet removal and re-sorting | plumbing | +| `constants.py` | Physical constants and unit conversions | plumbing | +| `__init__.py` | Public API: `run_system` | plumbing | +| `_version.py` | Generated from git tags | plumbing | + +The split matters for testing: every physics file must carry a test that pins it against a published value, an analytical limit or a cross-implementation check, recorded under [Validation anchors](../Validation/index.md). Plumbing files are exempt from that requirement but not from the rest of the test contract. + +## State and where it lives + +There is no state object. A system is a set of parallel numpy arrays, one entry per body, threaded through the loop as arguments: + +`a` (semi-major axes), `masses`, `ecc`, `Rp` (radii), `live_status`, `interact`, `densities`, `planet_id`. + +They are mutated in place by the event functions, which is why `sort_planet` can drop the dead bodies from all of them in one shared permutation, and why a test that reads an input array after calling an event function is reading post-mutation state. + +`planet_id` is the identity that survives sorting: array positions change as bodies die and the system is re-sorted, so it is the id, not the index, that appears in the output records. + +## Randomness + +All Monte Carlo draws go through numpy's global random state, seeded once per system at the top of `run_once`. The seed is mixed from the settings-file seed and the system's index, so a batch's systems are independent of each other and two batches with neighbouring seeds do not overlap. + +`run_system` additionally saves and restores the caller's random state around its own draws, so running a system in-process does not perturb an embedding program's sequence. That matters in a coupled run, where several modules share the same global state. + +## Units + +SI internally: kg, m, s. The settings file is read in Earth masses, AU, degrees and Gyr, converted once in `driver.py`. The returned records are SI apart from times, which are in years using the model's own 365-day year. A physical constant appearing as a literal in a function body rather than coming from `constants.py` is a defect. diff --git a/docs/Explanations/proteus_coupling.md b/docs/Explanations/proteus_coupling.md index 29ed84b..f726f2d 100644 --- a/docs/Explanations/proteus_coupling.md +++ b/docs/Explanations/proteus_coupling.md @@ -1,27 +1,97 @@ # Coupling to PROTEUS -PROTEUS consumes Morrigan through the in-memory entry point `morrigan.run_system`, which evolves one system and returns its survivors and the full impact history of each, without writing any files. The per-impact record schema is the interface between the two codes. - -## The call - -```python -from morrigan import run_system - -out = run_system( - seed=42, - masses=[...], # embryo masses [kg] - eccentricity=0.05, - inner_edge=0.05 * 1.5e11, # [m] - spacing=10.0, # [mutual Hill radii] - density=5500.0, # [kg m-3] - impact_angle=45.0, # [deg]; its sine is the impact parameter - evolution_time=1.0, # [Gyr] - inner_cutoff=0.005 * 1.5e11, # [m] - stellar_mass=1.0, # [Msun] -) +This page is the **theory** of how Morrigan plugs into a PROTEUS coupled run: where it sits in the loop, what the wrapper does with its output, how one planet is chosen out of a whole system, and what each impact changes when it lands. For the practical TOML recipe, see the [how-to page](../How-to/proteus_coupling.md). + +## Morrigan runs once, not every iteration + +Every other PROTEUS module is called inside the time loop and updates its slice of the planet's state on each pass. Morrigan is the exception. It runs **once**, before the loop starts, and what it returns is a **schedule**: a list of impacts with the times they occur and the physical parameters of each. The loop then consumes that schedule as it advances. + +``` + ┌──────────────────────────────────────────────────────────────────────────┐ + │ init_accretion(handler) <-- Morrigan runs HERE, once │ + │ build_parameters(config, hf_row) config + star.mass │ + │ morrigan.run_system(**params) evolve the whole system │ + │ select_planet(survivors, config) pick ONE survivor │ + │ -> [ImpactEvent, ImpactEvent, ...] its impact history │ + │ validate_timeline(events) reject an impossible one │ + │ │ + │ while not done: │ + │ │ + │ t_next_impact = next_event(events, t) # the loop reads the │ + │ dt = min(dt, t_next_impact - t) # schedule, never the model │ + │ │ + │ run_interior(...) # normal cooling step │ + │ │ + │ for event in due_events(events, t, dt): # an impact falls due │ + │ apply_impact(handler, event) # <-- consequences here │ + │ │ + │ run_orbit(...) / run_escape(...) / run_outgassing(...) / ... │ + │ hf_row['Time'] += dt │ + └──────────────────────────────────────────────────────────────────────────┘ ``` -Inputs are SI apart from the stellar mass (solar masses) and the evolution time (Gyr). The returned quantities are SI with times in years, using the model's own 365-day year. +The reason for the split is that the dynamical model and the coupled framework work on different clocks and at different cost. Morrigan integrates a whole system of embryos over hundreds of millions of years in a few seconds; PROTEUS integrates one planet's interior and atmosphere over the same span in hours. Running the dynamics once and replaying its schedule keeps the expensive loop in charge of the timestep. + +A consequence worth stating plainly: **the coupling is one-way**. Nothing the coupled planet does feeds back into the dynamics. If the planet loses half its atmosphere to escape, the impact schedule does not change, because the schedule was fixed before the loop began. + +## Selecting one planet out of a system + +Morrigan evolves a system of embryos and typically leaves several survivors. PROTEUS models one planet. Something has to choose, and that choice is the coupling's main free decision. + +`select_planet` implements four rules, set by `accretion.morrigan.selector`: + +| Selector | Picks | Use when | +|---|---|---| +| `match_config` | the survivor whose initial mass and orbit are closest to the configured planet | you have a planet in mind and want the dynamics to describe it | +| `mass` | the most massive survivor | you want the system's dominant body | +| `semimajoraxis` | the survivor nearest `selector_value`, in AU | you care about a particular orbital distance | +| `id` | the embryo with index `selector_value` | you are reproducing a specific system by hand | + +The selected survivor's impact history becomes the schedule. Every other survivor is discarded. This is why `num_planets` and the initial mass distribution matter even though only one planet is followed: they set the dynamical environment that produced the one you keep. + +## From Morrigan's record to a PROTEUS event + +`build_parameters` translates the TOML block into the arguments `run_system` expects: + +| `run_system` argument | Source | Note | +|---|---|---| +| `seed` | `accretion.morrigan.seed` | fixes the Monte Carlo | +| `masses` | `masses` or `num_planets` × `mass_equal` | converted M⊕ → kg | +| `eccentricity` | `eccentricity_init` | shared by all embryos | +| `inner_edge` | `inner_edge` | converted AU → m | +| `spacing` | `spacing` | mutual Hill radii | +| `density` | `density` | sets the mass-to-radius relation | +| `impact_angle` | `impact_angle` | its sine is the impact parameter | +| `evolution_time` | `evolution_time` | Gyr | +| `inner_cutoff` | `inner_cutoff` | converted AU → m | +| `stellar_mass` | **`star.mass`** | taken from the star block, not repeated here | + +The stellar mass is deliberately not a Morrigan setting. It is read from `config.star.mass` so the dynamical model and the rest of PROTEUS cannot disagree about the host star. + +Each returned record becomes an `ImpactEvent`. The one transformation applied on the way is the **time axis**: Morrigan measures time from disk dispersal, PROTEUS from the start of its own evolution, and `accretion.time_offset` maps between them. Impacts landing before the start of the run are folded into the initial condition rather than replayed. + +`validate_timeline` then rejects a schedule that cannot describe one body: times must increase strictly, each impact's target mass must follow from the previous merged mass, and a body may not gain mass between impacts. A drop of up to 10 % between impacts is allowed, since a consumer may strip an atmosphere in between. + +## What an impact does + +When the loop reaches a scheduled time, `apply_impact` applies six consequences in a fixed order. The order matters: each step reads state the previous one wrote. + +1. **Size the volatile consequences** from the pre-impact state, before anything changes. The erosion fraction, what the target loses, and what the impactor carries are all computed first, so no step sees a half-updated planet. +2. **Grow the planet.** `planet.mass_tot` increases by the impactor's *rock* alone, the merger mass less its volatile content. The volatiles arrive through their own channel below, so counting them here would double-count them. +3. **Re-solve the structure.** The interior module recomputes radius, core size, and pressures at the new mass. +4. **Apply the volatile changes.** The target loses its stripped fraction, the impactor delivers what survives, and the whole-planet element budget is refreshed. +5. **Re-melt the mantle.** A giant impact deposits enough energy to melt the mantle, so the interior is reset to a fully molten state and the injected heat is booked into the energy budget. +6. **Move the orbit.** The semi-major axis is scaled by the impact's *fractional* change and the eccentricity is set to its post-impact value. + +The orbit step is worth expanding. Morrigan's absolute orbits belong to its own system, which need not sit where the PROTEUS planet sits. So the coupling applies the **ratio** `a_after / a_before`, not the absolute value: the planet keeps its configured orbital distance and inherits the dynamical model's fractional kick. + +## What PROTEUS adds that Morrigan does not do + +Morrigan reports bare bodies. Three pieces of physics live entirely on the PROTEUS side: + +- **Impact atmospheric erosion.** Morrigan tracks no atmosphere and its merged masses are exact sums. PROTEUS computes what an impact strips, either as a fixed fraction or through the Kegerreis et al. (2020)[^cite-kegerreis2020] scaling law evaluated by `zephyrus.collision.mass_loss` from the record's own collision parameters. One fraction governs both bodies: the target loses that fraction of its atmosphere, and a volatile-bearing impactor loses the same fraction of its atmospheric part. +- **Volatile delivery.** Whether an impactor carries volatiles at all is a PROTEUS choice: dry, matching the planet's formation composition, or per-element budgets. +- **The thermal consequence.** Re-melting the mantle and booking the impact energy is interior physics, not dynamics. ## The impact record schema @@ -42,7 +112,14 @@ Any change to the record fields, units, or conventions is a breaking interface c ## Random-state hygiene -`run_system` seeds numpy's global random state for its own draws and restores the caller's state afterwards, so an embedding program's random sequence is unaffected by running a system in-process. +`run_system` seeds numpy's global random state for its own draws and restores the caller's state afterwards, so an embedding program's random sequence is unaffected by running a system in-process. This matters in a coupled run, where PROTEUS and other modules draw from the same global state. + +## See also + +- [Coupling to PROTEUS (how-to)](../How-to/proteus_coupling.md) for the TOML recipe, the selector choice, and the pitfalls. +- [Model overview](model.md) for the dynamics that produce the schedule. +- [Limitations](limitations.md) for what the model does not represent. +- The PROTEUS-side code: `src/proteus/accretion/wrapper.py` (dispatch and impact consequences), `src/proteus/accretion/morrigan.py` (this module's adapter), `src/proteus/accretion/common.py` (the `ImpactEvent` schema and timeline validation), `src/proteus/config/_accretion.py` (the config block). ## References diff --git a/docs/How-to/configuration.md b/docs/How-to/configuration.md new file mode 100644 index 0000000..8a9d594 --- /dev/null +++ b/docs/How-to/configuration.md @@ -0,0 +1,55 @@ +# Configuration + +Morrigan reads one TOML settings file, passed with `-c`. `initialise.toml` in the repository root is a worked example. This page covers what each block does and how the settings interact; [Settings and inputs](../Reference/parameters.md) is the field-by-field reference. + +## The three blocks + +```toml +['run_simulation'] + max_time = 1 + random_seed = 1 + save_directory = '1au_system' + +['batch'] + ndisk = 20 + nproc = 8 + +['init_par'] + N = 10 + Mp = [0.1, 1.5, 0.9, 0.6, 0.75, 1.2, 1.4, 0.45, 0.5, 1.0] + e = 0.01 + inner_edge = 1 +``` + +That is an excerpt rather than the full set. [Settings and inputs](../Reference/parameters.md) is the field reference, with every field, its meaning and its units; this page is about how the settings interact. + +`Mp` must have `N` entries. `spacing` is optional and defaults to 10 mutual Hill radii. + +## Choosing the initial conditions + +**Spacing and eccentricity set the instability time**, and it depends steeply on both. A widely spaced, nearly circular system may not go unstable at all inside the evolution time, in which case the run finishes with no mergers. If that happens, tighten the spacing or raise the initial eccentricity before reaching for a longer `max_time`. + +There is an upper limit on spacing. The layout condition has a pole where the requested gap approaches the span it is measured across, roughly where `spacing * ((M1+M2)/3M*)^(1/3)` reaches 2, and beyond it the layout is refused with an error naming the pair. It is reachable with giant-planet masses at wide spacings, not with terrestrial ones. + +**The mass distribution shapes the outcome more than the total.** Equal-mass systems behave differently from staggered ones: the excitation a scattering imparts is set by the ratio of the two masses, so equal-mass pairs share their kick evenly and unequal pairs throw the lighter body much harder. An equal-mass system is the cleaner comparison against published statistics; a staggered one is closer to a real disk. + +**`impact_angle` is a single value for the whole run.** The model does not draw a distribution of impact geometries, so every collision in a run happens at the same angle. Its sine is the impact parameter, so 90° is a grazing hit and 0° is head-on. + +## Seeds and reproducibility + +Each system's seed is mixed from `random_seed` and the system's index within the batch. Two consequences: + +- A settings file reproduces its systems exactly, however many times it is run and whatever `nproc` is set to. +- Two batches with neighbouring `random_seed` values are independent. Estimating the scatter between ensembles by rerunning with a few different seeds is therefore a valid thing to do. + +## Batch size and parallelism + +`ndisk` is how many systems to evolve, `nproc` how many worker processes to spread them over. A single system takes seconds, so `ndisk` is usually set by how many realisations the statistics need rather than by runtime. `nproc` above the core count gains nothing. + +## Output location + +Results are written under `save_directory`. A relative path is taken from the directory the command runs in, not from where the settings file lives, so the run prints the full path it resolved. Keep the settings file alongside the results it produced; the repository ignores `*.toml` other than `initialise.toml`, so a settings file worth keeping has to be added to git explicitly. + +## Driving PROTEUS instead + +None of this applies when Morrigan runs inside PROTEUS: the settings file is not read, and the same quantities come from the `[accretion.morrigan]` block of the PROTEUS configuration. See [Coupling to PROTEUS](proteus_coupling.md). diff --git a/docs/How-to/proteus_coupling.md b/docs/How-to/proteus_coupling.md new file mode 100644 index 0000000..b415891 --- /dev/null +++ b/docs/How-to/proteus_coupling.md @@ -0,0 +1,157 @@ +# Coupling to PROTEUS + +This page is the **recipe**: the TOML to write, the choices to make, and the mistakes that cost an afternoon. For why the coupling is built this way, see the [explanation page](../Explanations/proteus_coupling.md). + +Morrigan is an optional PROTEUS module. Install it with + +```bash +bash tools/get_morrigan.sh +``` + +from a PROTEUS checkout, which clones the pinned commit and installs it editable. + +## Minimal `[accretion]` block + +```toml +[accretion] + module = "morrigan" + time_offset = 0.0 + + [accretion.morrigan] + seed = 1 + num_planets = 10 + mass_equal = 0.5 # M_earth, used when masses = [] + eccentricity_init = 0.01 + inner_edge = 0.1 # AU + spacing = 10.0 # mutual Hill radii + density = 5500.0 # kg m-3 + impact_angle = 45.0 # deg + evolution_time = 1.0 # Gyr + inner_cutoff = 0.005 # AU + selector = "match_config" +``` + +That alone gives a planet that grows by impacts and moves orbit. It delivers no volatiles and strips no atmosphere; both are opt-in below. + +The stellar mass is **not** in this block: it comes from `star.mass`. See the [explanation](../Explanations/proteus_coupling.md#from-morrigans-record-to-a-proteus-event) for why. + +## Choosing a selector + +Morrigan leaves several survivors and PROTEUS follows one. This is the decision that most changes what you get. + +```toml +selector = "match_config" # closest to the configured planet (default) +selector = "mass" # the most massive survivor +selector = "semimajoraxis" # nearest a target orbit +selector_value = 1.0 # AU, required for semimajoraxis +selector = "id" # a specific embryo +selector_value = 3 # embryo index, required for id +``` + +`match_config` is the sensible default: you have configured a planet mass and orbit, and it finds the survivor that most resembles it. Reach for `mass` when you want whatever the system's dominant body turned out to be, and for `semimajoraxis` when the orbital distance is what you are studying. + +`selector_value` is required for `semimajoraxis` and `id` and rejected at config load if missing. + +## Delivering volatiles + +By default impactors are dry: they add rock and iron only, so the planet's bulk volatile concentration falls by dilution as it grows. + +```toml +[accretion] + impactor_volatiles = "match_planet" +``` + +`match_planet` gives every impactor the planet's own initial fractional abundances, scaled to the impactor mass, on the assumption that all embryos formed from the same disk material. The composition is frozen at formation, so it does not track the planet's later evolution. + +For explicit control: + +```toml +[accretion] + impactor_volatiles = "ppmw" + impactor_H_ppmw = 100.0 + impactor_C_ppmw = 50.0 +``` + +Setting a ppmw budget under any other mode is rejected at config load rather than silently ignored, which otherwise produces a run that looks configured but delivers nothing. + +## Stripping atmosphere + +Also off by default. + +```toml +[accretion] + atmloss_module = "zephyrus" # Kegerreis et al. (2020) erosion law +``` + +evaluates the scaling law from each impact's own collision parameters: speed, mass ratio, density ratio and impact angle. It requires ZEPHYRUS, which PROTEUS installs as standard. + +```toml +[accretion] + atmloss_module = "constant" + atmloss_frac = 0.1 # 10 % per impact +``` + +applies a fixed fraction instead, which is useful for isolating the effect of erosion from its dependence on impact geometry. + +One fraction governs both bodies at each impact; the [explanation](../Explanations/proteus_coupling.md#what-proteus-adds-that-morrigan-does-not-do) sets out that convention. + +## Worked example: an accreting Earth analogue + +```toml +[star] + module = "mors" + mass = 1.0 # Morrigan reads the stellar mass here + +[accretion] + module = "morrigan" + time_offset = 0.0 + impactor_volatiles = "match_planet" + atmloss_module = "zephyrus" + + [accretion.morrigan] + seed = 7 + num_planets = 8 + masses = [0.2, 0.9, 0.4, 1.1, 0.3, 0.7, 1.3, 0.5] + eccentricity_init = 0.05 + inner_edge = 0.05 + spacing = 10.0 + density = 5500.0 + impact_angle = 20.0 + evolution_time = 1.0 + inner_cutoff = 0.005 + selector = "mass" + +[interior_energetics] + module = "aragog" # required: SPIDER has no re-melt path +``` + +A run like this reports the schedule it built during start-up, then each impact as it lands: + +``` +[ INFO ] Running giant-impact model for 8 embryos +[ INFO ] Body 6 experienced 1 impacts +[ INFO ] Scheduled 1 impact(s) +... +[ INFO ] Time-stepping: impact at 9.5991e+02 yr, capping dt at 1.00e+02 yr +[ INFO ] Giant impact at t = 9.5991e+02 yr: target 6 struck by 7, adding 0.5000 M_earth +[ INFO ] impactor volatiles: match_planet; atmosphere loss: zephyrus +[ INFO ] impact erosion law: loss fraction 0.554 +[ INFO ] planet is now 1.4999 M_earth at 1.03107 AU, e = 0.0223 +``` + +## Common pitfalls + +**Aragog is required.** `interior_energetics.module = "spider"` with accretion enabled is refused at config load. SPIDER keeps its state in a restart file written by an external binary and has no validated re-melt path, so an impact cannot reset its mantle. + +**No impacts scheduled.** The most common cause is `evolution_time` being short relative to the instability time of the configuration you chose. [Choosing the initial conditions](configuration.md#choosing-the-initial-conditions) covers how spacing and eccentricity set that time. + +**All impacts before the run starts.** Morrigan measures time from disk dispersal. If every impact lands before your `time_offset`, they are folded into the initial condition and the run proceeds with none scheduled. Check the reported impact times against your start time. + +**Spacing too wide for the masses.** Beyond a limit that depends on the embryo masses the layout is refused with an error naming the pair; see [choosing the initial conditions](configuration.md#choosing-the-initial-conditions). + +**A settings file carrying `atm_mass_fraction`.** The model tracks no atmosphere; the key is ignored and a warning says so. Atmospheric loss belongs to `atmloss_module` on the PROTEUS side. + +## Next step + +- [Coupling to PROTEUS (explanation)](../Explanations/proteus_coupling.md) for what happens inside each of these steps. +- [Settings and inputs](../Reference/parameters.md) for the standalone settings file. diff --git a/docs/How-to/releasing.md b/docs/How-to/releasing.md new file mode 100644 index 0000000..152709e --- /dev/null +++ b/docs/How-to/releasing.md @@ -0,0 +1,62 @@ +# Releasing + +Morrigan's version comes from git tags through setuptools-scm, so tagging is what makes a release. There is no version string to edit anywhere in the source. + +## Version scheme + +CalVer, `YY.0M.0D`, matching the rest of the PROTEUS ecosystem. A release cut on 3 August 2026 is tagged `26.08.03`. + +The scheme carries no compatibility promise in the numbers themselves. What changes in a way that breaks a consumer is the impact-record schema, and that is called out in the pull request that changes it rather than encoded in the version. PROTEUS pins a version floor, so a release that changes the schema needs the pin raised in the same change. + +## Before tagging + +1. `main` is green: the test workflow passes on the commit you intend to tag. +2. The documentation builds without warnings, since the docs deploy runs on push to `main`. +3. Anything that changed the record schema, the units, or the physics is described in its pull request, because the release notes are assembled from them. + +## Cutting the release + +```bash +git checkout main +git pull +git tag 26.08.03 +git push origin 26.08.03 +``` + +Then create the GitHub release against that tag, either in the web interface or with + +```bash +gh release create 26.08.03 --repo FormingWorlds/Morrigan \ + --title 26.08.03 --generate-notes +``` + +`--generate-notes` assembles the notes from the merged pull requests, which is why their descriptions matter. + +## Verifying + +```bash +pip install -e . +python -c "import morrigan; print(morrigan.__version__)" +``` + +should print the tag you just pushed. A checkout with no tags in its history reports `0.0.0`, the configured fallback, which is the usual sign that a shallow clone dropped the tag history rather than that anything is wrong with the release. + +## Publication + +Publishing is automatic. Creating the GitHub release triggers the publish workflow, which builds the distribution and uploads it to PyPI as `fwl-morrigan` through trusted publishing, so no API token is stored in the repository. + +The workflow refuses to upload a fallback version. setuptools-scm reports `0.0.0` when it cannot see the tag history, and a shallow checkout is the usual cause; publishing that would take the name `0.0.0` on PyPI permanently, and PyPI does not allow a version to be re-uploaded. The build is checked before the upload step for that reason. + +Confirm the upload landed by querying the index rather than by installing: + +```bash +curl -s https://pypi.org/pypi/fwl-morrigan/json | python -c "import json,sys; print(json.load(sys.stdin)['info']['version'])" +``` + +Installing to check would replace an editable development checkout with the released wheel, which is rarely what you want mid-session. + +Note that PyPI normalises the CalVer tag: `26.08.03` is published as `26.8.3`. Both refer to the same release, and a PROTEUS dependency pin should use the normalised form. + +## Moving PROTEUS onto a new release + +PROTEUS depends on `fwl-morrigan` by version. After a release, bump that floor in the PROTEUS `pyproject.toml` and open a pull request there; the pin is what decides which Morrigan a coupled run uses. diff --git a/docs/How-to/running.md b/docs/How-to/running.md index 0336d7e..a6621e8 100644 --- a/docs/How-to/running.md +++ b/docs/How-to/running.md @@ -19,7 +19,7 @@ Results are written under the `save_directory` named in the settings file. A rel | `data/survivors/` | Final mass, orbit, and eccentricity of each surviving planet | | `batch_summary.csv` | Runtime and surviving-planet count for each system | -A batch of several systems (the `[batch]` table in the settings file) runs on a process pool, one system per worker, each seeded from the base seed plus its own index. +A batch of several systems (the `[batch]` table in the settings file) runs on a process pool, one system per worker, each with its own seed. ## Plotting @@ -32,8 +32,8 @@ python plot.py -c initialise.toml ## In memory, from another program -`morrigan.run_system` evolves one system without writing files and returns the survivors and per-impact records; this is the interface PROTEUS drives. See [Coupling to PROTEUS](../Explanations/proteus_coupling.md) for the call signature and the record schema. +`morrigan.run_system` evolves one system without writing files and returns the survivors and per-impact records; this is the interface PROTEUS drives. See [Coupling to PROTEUS](../Explanations/proteus_coupling.md) for the record schema and how a coupled run consumes it. ## Reproducibility -Each system is seeded from `random_seed` in the settings file plus its own index, so a given settings file reproduces the same systems exactly. The same holds for `run_system` with a fixed seed: one seed, one timeline, bit for bit. +A given settings file reproduces the same systems exactly, whatever the process count, and two batches with neighbouring `random_seed` values are independent of each other. See [Seeds and reproducibility](configuration.md#seeds-and-reproducibility) for how the per-system seed is derived. The same determinism holds for `run_system` with a fixed seed: one seed, one timeline, bit for bit. diff --git a/docs/Reference/api.md b/docs/Reference/api.md index 9804fd5..909233a 100644 --- a/docs/Reference/api.md +++ b/docs/Reference/api.md @@ -1,6 +1,10 @@ # API reference -The public entry point is `morrigan.run_system`; the modules below are the model's internals, documented here for contributors and for readers tracing an equation from the [model overview](../Explanations/model.md) into code. +The public entry point is `morrigan.run_system`, documented first below. The modules after it are the model's internals, documented for contributors and for readers tracing an equation from the [model overview](../Explanations/model.md) into code. + +## Public API + +::: morrigan ## Driver diff --git a/docs/Reference/parameters.md b/docs/Reference/parameters.md index 6c4061e..29e7993 100644 --- a/docs/Reference/parameters.md +++ b/docs/Reference/parameters.md @@ -12,7 +12,7 @@ The command line reads one TOML file (`morrigan -c `). Units at this bound | `flag_event` | 1 to compute the secular solution and first crossing at start; leave at 1 | - | | `a_min` | Perihelion inside which a body has fallen into the star | au | | `max_time` | Length of the dynamical evolution | Gyr | -| `random_seed` | Base seed; system i runs with `random_seed + i` | - | +| `random_seed` | Base seed for the batch; each system's seed is mixed from this and the system index | - | | `save_directory` | Where result tables are written; relative paths resolve from the working directory | - | ### `[init_par]` diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..acc005f --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,28 @@ +# Getting started + +Morrigan can be used two ways: as a standalone model that writes tables of a system's evolution, or in-process as the accretion module of a PROTEUS run. Both come from the same install. + +## Quick path + +```bash +git clone git@github.com:FormingWorlds/Morrigan.git +cd Morrigan +pip install -e . +morrigan -c initialise.toml +``` + +That evolves the worked example in `initialise.toml` and writes its results under the `save_directory` named there. The run prints the full path it is writing to. + +## What do you want to do? + +**Run a system and look at the results.** Start with [running a model](How-to/running.md), which covers the settings file, the output tables and the plotting script. [Settings and inputs](Reference/parameters.md) is the full field reference. + +**Understand what the model does.** [Model overview](Explanations/model.md) walks the physics in the order the code applies it: how embryos are laid out, how the next instability is timed, and how a crossing resolves into a collision, a scattering or an ejection. [Limitations](Explanations/limitations.md) is the companion: what the model deliberately does not represent. + +**Drive a PROTEUS run with it.** [Coupling to PROTEUS (how-to)](How-to/proteus_coupling.md) is the TOML recipe. [Coupling to PROTEUS (explanation)](Explanations/proteus_coupling.md) is why it is built that way, including the one property that makes Morrigan unlike every other PROTEUS module: it runs once, before the loop, and produces a schedule rather than updating state each iteration. + +**Change the model.** [Code architecture](Explanations/code_architecture.md) maps the source files onto the physics. [Testing](How-to/testing.md) is the contract any change has to meet, and [Validation anchors](Validation/index.md) records what each physics routine is pinned against. [Contributing](Community/CONTRIBUTING.md) covers what a change needs before it can land, including the two extra requirements on a physics change. + +## Where the numbers come from + +The model is the semi-analytical Monte Carlo of Kimura et al. (2025), with the ejection prescription from its companion application paper. Rather than integrating orbits, it predicts *when* a system becomes unstable and resolves each instability with analytic prescriptions calibrated against N-body simulations. That makes a single system cheap enough to run inside a coupled framework, at the cost of any individual history being a statistical realisation rather than a trajectory. Ensemble statistics are the meaningful comparison. diff --git a/docs/index.md b/docs/index.md index f7cb0c5..a91f68c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,9 +14,10 @@ Named after Morrigan, a shapeshifting figure from Irish mythology thought to rep ## Where to go -- [Installation](How-to/installation.md) and [running a model](How-to/running.md) -- [Model overview](Explanations/model.md): the physics, equation by equation -- [Coupling to PROTEUS](Explanations/proteus_coupling.md): the impact-record interface +- [Getting started](getting_started.md): the shortest path from a clone to a result, and where to go next +- [Installation](How-to/installation.md), [running a model](How-to/running.md) and [configuration](How-to/configuration.md) +- [Model overview](Explanations/model.md): the physics, equation by equation, and [code architecture](Explanations/code_architecture.md) for where each piece lives in the source +- Coupling to PROTEUS in two parts: the [how-to](How-to/proteus_coupling.md) for the TOML recipe and the pitfalls, the [explanation](Explanations/proteus_coupling.md) for why the coupling is built the way it is - [Settings and inputs](Reference/parameters.md) and the [API reference](Reference/api.md) - [Validation anchors](Validation/index.md): where every physics routine is pinned against a published value, an analytical limit, or a cross-implementation check diff --git a/docs/proteus-framework.md b/docs/proteus-framework.md new file mode 100644 index 0000000..0b15062 --- /dev/null +++ b/docs/proteus-framework.md @@ -0,0 +1,43 @@ +--- +title: Morrigan in the PROTEUS framework +--- + +

+ +
+ + +
+
+

+ + +Morrigan is the **protoplanet accretion module** of [PROTEUS](https://proteus-framework.org/PROTEUS) (/ˈproʊtiəs/, PROH-tee-əs), a modular Python framework for the coupled evolution of the atmospheres and interiors of rocky planets and exoplanets. A schematic of PROTEUS components and corresponding modules can be found below. Click any module in the diagram to open its documentation, or navigate to it from the sidebar. +
+ +PROTEUS module schematic (light mode) +PROTEUS module schematic (dark mode) + +

Schematic of PROTEUS components and corresponding modules.

+ +## Where Morrigan sits + + +Morrigan occupies the **accretion** slot. It answers the question the rest of the loop cannot: what did this planet's assembly look like, and when did it get hit? + +Unlike the other modules it is not called on each iteration. It runs once, before the loop starts, and returns a schedule of impacts that the loop then replays. See [Coupling to PROTEUS](Explanations/proteus_coupling.md) for why. + +## What Morrigan exchanges with + +The schematic above links every module. Morrigan interacts directly with two of them: + +| Module | Exchange | +| --- | --- | +| [MORS](https://proteus-framework.org/MORS/) and the stellar block | Morrigan reads the host star's mass from `star.mass` rather than repeating it, so the dynamics and the rest of the run cannot disagree about the star | +| [ZEPHYRUS](https://proteus-framework.org/ZEPHYRUS/) | the impacts Morrigan schedules are handed to its collision law to work out how much atmosphere each one erodes. Morrigan itself models no atmosphere | + +## Conventions the ecosystem shares + +- **SI internally.** Modules exchange quantities in kg, m and s. Configuration files use the units a user thinks in, converted once at the boundary. +- **One module per slot per run.** Where several modules serve the same slot they are alternatives, chosen in the configuration. +- **The interface is the contract.** A module's public entry point and the schema it returns are what the framework depends on; changing either is a breaking change for the coupled run and is flagged as such. diff --git a/mkdocs.yml b/mkdocs.yml index ee4002f..1f9a59a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,14 +9,19 @@ copyright: '© 2023-2026 Forming World nav: - Home: - About: index.md + - Getting started: getting_started.md - How-to guides: - Installation: How-to/installation.md - Running a model: How-to/running.md + - Configuration: How-to/configuration.md + - Coupling to PROTEUS: How-to/proteus_coupling.md - Testing: How-to/testing.md + - Releasing: How-to/releasing.md - Explanations: - Model overview: Explanations/model.md + - Code architecture: Explanations/code_architecture.md - Coupling to PROTEUS: Explanations/proteus_coupling.md - Limitations: Explanations/limitations.md @@ -40,7 +45,14 @@ nav: - Issues: - GitHub issues: https://github.com/FormingWorlds/Morrigan/issues + - Community: + - Code of Conduct: Community/CODE_OF_CONDUCT.md + - Contributing: Community/CONTRIBUTING.md + - Contact: Community/contact.md + - Developers: https://proteus-framework.org/people + - Other PROTEUS modules: + - The PROTEUS framework: proteus-framework.md - "\U0001F517 PROTEUS": https://proteus-framework.org/PROTEUS/ - "\U0001F517 MORS": https://proteus-framework.org/MORS/ - "\U0001F517 JANUS": https://proteus-framework.org/JANUS/ From a22776ea36b76c36cf0ed62fca085f620bc81a71 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 25 Jul 2026 22:30:35 +0200 Subject: [PATCH 2/6] Correct the coupling docs against the code they describe Several statements in the coupling and architecture pages did not match what the code does, and the worked example's sample log did not match what its own configuration produces. Impacts landing at or before the start of a run are discarded with a warning and their mass is never applied; the pages said they were folded into the initial condition, which would let a reader believe the planet starts already grown by them. Corrected in both coupling pages and in the PROTEUS-side field description the wording came from. The re-melt step resets the interior to its molten initial condition and reports the impact's kinetic energy for comparison; it does not book that energy into an energy budget, and the state is fully molten only for the temperature modes that guarantee it. The page claimed both. It also listed six consequences where there are seven: clearing the solidification latch is its own step, and it is the one that lets outgassing restart. The worked example claimed one impact where that seed and mass list give two, at different times and with a different impactor, so anyone running it would have concluded their install was broken. The log block is now generated from a real run, and the parts that depend on the coupled atmosphere state are described rather than pinned to invented numbers. The example is also no longer called an Earth analogue: eight embryos inside 0.1 AU going unstable in centuries is not one. Smaller corrections: the loop diagram put the time advance after the impact block rather than before it and mistyped three call signatures; the call graph named a `run_batch` function that does not exist; the settings page omitted four mandatory keys, so the documented file raised a bare KeyError; the release page described a PROTEUS version floor that does not exist, where the pin is a commit ref; the constants rule is scoped so fitted coefficients may stay beside their equations; the selector table now says which rules compare initial values and which compare final ones; and impact erosion is named as PROTEUS's call into ZEPHYRUS rather than something Morrigan does. --- docs/Explanations/code_architecture.md | 11 ++++-- docs/Explanations/proteus_coupling.md | 47 +++++++++++++++++--------- docs/How-to/configuration.md | 9 ++++- docs/How-to/proteus_coupling.md | 24 +++++++------ docs/How-to/releasing.md | 8 +++-- docs/proteus-framework.md | 2 +- src/morrigan/interaction_timescales.py | 8 +++-- 7 files changed, 71 insertions(+), 38 deletions(-) diff --git a/docs/Explanations/code_architecture.md b/docs/Explanations/code_architecture.md index 6c84967..cad8542 100644 --- a/docs/Explanations/code_architecture.md +++ b/docs/Explanations/code_architecture.md @@ -8,7 +8,10 @@ Eleven source files under `src/morrigan/`, laid out so that each stage of the ph morrigan -c settings.toml | v - driver.run_batch parallel over systems + driver.cli console-script entry point + | + v + driver.main read settings, parallel over systems | v driver.run_once ONE system, start to finish @@ -34,7 +37,7 @@ Eleven source files under `src/morrigan/`, laid out so that each stage of the ph +-- write tables / return records ``` -`run_system` is the in-process entry point beside `run_batch`. It drives the same `run_once` core but returns the records in memory instead of writing files, which is what PROTEUS consumes. +`run_system` is the in-process entry point beside `main`. It drives the same `run_once` core but returns the records in memory instead of writing files, which is what PROTEUS consumes. ## What each file owns @@ -72,4 +75,6 @@ All Monte Carlo draws go through numpy's global random state, seeded once per sy ## Units -SI internally: kg, m, s. The settings file is read in Earth masses, AU, degrees and Gyr, converted once in `driver.py`. The returned records are SI apart from times, which are in years using the model's own 365-day year. A physical constant appearing as a literal in a function body rather than coming from `constants.py` is a defect. +SI internally: kg, m, s. The settings file is read in Earth masses, AU, degrees and Gyr, converted once in `driver.py`. The returned records are SI apart from times, which are in years using the model's own 365-day year. + +Unit conversions and universal physical constants belong in `constants.py`; one appearing as a literal in a function body instead is a defect. Coefficients fitted to a specific published result are the exception and stay beside the equation they belong to, where the citation can sit with them: the crossing-time fit in `interaction_timescales.py` and the N-body crossing threshold in `crossing_pair.py` both carry their numbers inline for that reason. diff --git a/docs/Explanations/proteus_coupling.md b/docs/Explanations/proteus_coupling.md index f726f2d..18ff8f7 100644 --- a/docs/Explanations/proteus_coupling.md +++ b/docs/Explanations/proteus_coupling.md @@ -9,27 +9,37 @@ Every other PROTEUS module is called inside the time loop and updates its slice ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ init_accretion(handler) <-- Morrigan runs HERE, once │ - │ build_parameters(config, hf_row) config + star.mass │ + │ build_parameters(config) config + star.mass │ │ morrigan.run_system(**params) evolve the whole system │ │ select_planet(survivors, config) pick ONE survivor │ │ -> [ImpactEvent, ImpactEvent, ...] its impact history │ │ validate_timeline(events) reject an impossible one │ + │ _drop_events_before_start(events, t0) discard the pre-run tail │ │ │ │ while not done: │ │ │ - │ t_next_impact = next_event(events, t) # the loop reads the │ - │ dt = min(dt, t_next_impact - t) # schedule, never the model │ + │ pending = next_event(events, t) # the loop reads the │ + │ interior_o.t_next_impact = pending.time # schedule, never the model│ │ │ - │ run_interior(...) # normal cooling step │ + │ run_interior(...) # cooling step; the │ + │ # time-stepper shortens dt │ + │ # to land on t_next_impact │ │ │ - │ for event in due_events(events, t, dt): # an impact falls due │ - │ apply_impact(handler, event) # <-- consequences here │ + │ hf_row['Time'] += dt # advance FIRST │ + │ │ + │ for event in due_events(events, t_prev, t_now): # impact falls due│ + │ apply_impact(handler, event) # <-- consequences here │ │ │ │ run_orbit(...) / run_escape(...) / run_outgassing(...) / ... │ - │ hf_row['Time'] += dt │ └──────────────────────────────────────────────────────────────────────────┘ ``` +The time advance comes **before** the impact block, so the orbit, structure and +escape steps of the step an impact lands on already see the grown planet. The +clamp that shortens `dt` onto the impact time lives in the interior +time-stepper, not in the main loop; the loop only publishes when the next +impact is due. + The reason for the split is that the dynamical model and the coupled framework work on different clocks and at different cost. Morrigan integrates a whole system of embryos over hundreds of millions of years in a few seconds; PROTEUS integrates one planet's interior and atmosphere over the same span in hours. Running the dynamics once and replaying its schedule keeps the expensive loop in charge of the timestep. A consequence worth stating plainly: **the coupling is one-way**. Nothing the coupled planet does feeds back into the dynamics. If the planet loses half its atmosphere to escape, the impact schedule does not change, because the schedule was fixed before the loop began. @@ -42,11 +52,13 @@ Morrigan evolves a system of embryos and typically leaves several survivors. PRO | Selector | Picks | Use when | |---|---|---| -| `match_config` | the survivor whose initial mass and orbit are closest to the configured planet | you have a planet in mind and want the dynamics to describe it | -| `mass` | the most massive survivor | you want the system's dominant body | -| `semimajoraxis` | the survivor nearest `selector_value`, in AU | you care about a particular orbital distance | +| `match_config` | the survivor whose **initial** mass and orbit are closest to the configured planet | you have a planet in mind and want the dynamics to describe it | +| `mass` | the survivor with the largest **final** mass | you want the system's dominant body | +| `semimajoraxis` | the survivor whose **final** orbit is nearest `selector_value`, in AU | you care about a particular orbital distance | | `id` | the embryo with index `selector_value` | you are reproducing a specific system by hand | +Which end of the history a rule compares is what decides which body you get. `match_config` matches on the state the embryo *started* in, so it answers "which of these bodies began as the planet I configured"; `mass` and `semimajoraxis` match on the state it *ended* in, so they answer "which of these bodies became what I am looking for". A body can easily win one and lose the other. + The selected survivor's impact history becomes the schedule. Every other survivor is discarded. This is why `num_planets` and the initial mass distribution matter even though only one planet is followed: they set the dynamical environment that produced the one you keep. ## From Morrigan's record to a PROTEUS event @@ -68,20 +80,23 @@ The selected survivor's impact history becomes the schedule. Every other survivo The stellar mass is deliberately not a Morrigan setting. It is read from `config.star.mass` so the dynamical model and the rest of PROTEUS cannot disagree about the host star. -Each returned record becomes an `ImpactEvent`. The one transformation applied on the way is the **time axis**: Morrigan measures time from disk dispersal, PROTEUS from the start of its own evolution, and `accretion.time_offset` maps between them. Impacts landing before the start of the run are folded into the initial condition rather than replayed. +Each returned record becomes an `ImpactEvent`. The one transformation applied on the way is the **time axis**: Morrigan measures time from disk dispersal, PROTEUS from the start of its own evolution, and `accretion.time_offset` maps between them. `time_offset` is added to each impact time, and the result is compared against the run's start time. + +Impacts that still land at or before that start time are **discarded**, with a warning naming the mass they would have added. Their mass is not folded into the initial condition and arrives nowhere: the configured `planet.mass_tot` and orbit define the starting state on their own. A schedule whose early impacts fall outside the simulated interval therefore grows the planet less than the dynamics described, so choose `time_offset` to bring the history you care about inside the run. `validate_timeline` then rejects a schedule that cannot describe one body: times must increase strictly, each impact's target mass must follow from the previous merged mass, and a body may not gain mass between impacts. A drop of up to 10 % between impacts is allowed, since a consumer may strip an atmosphere in between. ## What an impact does -When the loop reaches a scheduled time, `apply_impact` applies six consequences in a fixed order. The order matters: each step reads state the previous one wrote. +When the loop reaches a scheduled time, `apply_impact` applies seven consequences in a fixed order. The order matters: each step reads state the previous one wrote. 1. **Size the volatile consequences** from the pre-impact state, before anything changes. The erosion fraction, what the target loses, and what the impactor carries are all computed first, so no step sees a half-updated planet. -2. **Grow the planet.** `planet.mass_tot` increases by the impactor's *rock* alone, the merger mass less its volatile content. The volatiles arrive through their own channel below, so counting them here would double-count them. +2. **Grow the planet.** `planet.mass_tot` increases by the impactor's *rock* alone, the impactor's mass less its volatile content. The volatiles arrive through their own channel below, so counting them here would double-count them. 3. **Re-solve the structure.** The interior module recomputes radius, core size, and pressures at the new mass. 4. **Apply the volatile changes.** The target loses its stripped fraction, the impactor delivers what survives, and the whole-planet element budget is refreshed. -5. **Re-melt the mantle.** A giant impact deposits enough energy to melt the mantle, so the interior is reset to a fully molten state and the injected heat is booked into the energy budget. -6. **Move the orbit.** The semi-major axis is scaled by the impact's *fractional* change and the eccentricity is set to its post-impact value. +5. **Re-melt the mantle.** The interior is reset to its molten initial condition, recomputed for the grown planet, and the next interior solve is told not to clip the resulting temperature jump. How molten that state is depends on `planet.temperature_mode`: it is fully molten for `liquidus_super`, `accretion` and `adiabatic_from_cmb`, and otherwise only as molten as the configured initial condition, which the run warns about at start-up. The impact's kinetic energy is **reported** for comparison against the enthalpy the reset injects, not added to an energy budget; the reset carries no source term, so checking energy conservation across an impact means weighing the two by hand. +6. **Clear the solidification latch.** A mantle that had crystallised is molten again, so the one-way latch is lifted. Without this step outgassing would stay frozen and the volatiles would be treated as locked in a solid mantle for good. +7. **Move the orbit.** The semi-major axis is scaled by the impact's *fractional* change and the eccentricity is set to its post-impact value. The orbit step is worth expanding. Morrigan's absolute orbits belong to its own system, which need not sit where the PROTEUS planet sits. So the coupling applies the **ratio** `a_after / a_before`, not the absolute value: the planet keeps its configured orbital distance and inherits the dynamical model's fractional kick. @@ -91,7 +106,7 @@ Morrigan reports bare bodies. Three pieces of physics live entirely on the PROTE - **Impact atmospheric erosion.** Morrigan tracks no atmosphere and its merged masses are exact sums. PROTEUS computes what an impact strips, either as a fixed fraction or through the Kegerreis et al. (2020)[^cite-kegerreis2020] scaling law evaluated by `zephyrus.collision.mass_loss` from the record's own collision parameters. One fraction governs both bodies: the target loses that fraction of its atmosphere, and a volatile-bearing impactor loses the same fraction of its atmospheric part. - **Volatile delivery.** Whether an impactor carries volatiles at all is a PROTEUS choice: dry, matching the planet's formation composition, or per-element budgets. -- **The thermal consequence.** Re-melting the mantle and booking the impact energy is interior physics, not dynamics. +- **The thermal consequence.** Re-melting the mantle, lifting the solidification latch, and reporting the impact energy is interior physics, not dynamics. ## The impact record schema diff --git a/docs/How-to/configuration.md b/docs/How-to/configuration.md index 8a9d594..a3457e3 100644 --- a/docs/How-to/configuration.md +++ b/docs/How-to/configuration.md @@ -6,6 +6,11 @@ Morrigan reads one TOML settings file, passed with `-c`. `initialise.toml` in th ```toml ['run_simulation'] + t = 0.0 # leave at zero for a fresh run + t_ref = 0.0 # leave at zero for a fresh run + t_event = 0.0 # leave at zero for a fresh run + flag_event = 1 # leave at 1 + a_min = 0.005 max_time = 1 random_seed = 1 save_directory = '1au_system' @@ -21,7 +26,9 @@ Morrigan reads one TOML settings file, passed with `-c`. `initialise.toml` in th inner_edge = 1 ``` -That is an excerpt rather than the full set. [Settings and inputs](../Reference/parameters.md) is the field reference, with every field, its meaning and its units; this page is about how the settings interact. +The `init_par` block is an excerpt; the other two are complete. [Settings and inputs](../Reference/parameters.md) is the field reference, with every field, its meaning and its units; this page is about how the settings interact. + +Every key in `run_simulation` is required and read directly, so omitting one fails with a bare `KeyError` naming it. `random_seed` is the only one with a default. The first four hold the clock and the secular bookkeeping and are always zero, zero, zero and 1 for a fresh run; `initialise.toml` in the repository root carries them with the same values. `Mp` must have `N` entries. `spacing` is optional and defaults to 10 mutual Hill radii. diff --git a/docs/How-to/proteus_coupling.md b/docs/How-to/proteus_coupling.md index b415891..e1f1067 100644 --- a/docs/How-to/proteus_coupling.md +++ b/docs/How-to/proteus_coupling.md @@ -95,7 +95,7 @@ applies a fixed fraction instead, which is useful for isolating the effect of er One fraction governs both bodies at each impact; the [explanation](../Explanations/proteus_coupling.md#what-proteus-adds-that-morrigan-does-not-do) sets out that convention. -## Worked example: an accreting Earth analogue +## Worked example: a compact system feeding a growing planet ```toml [star] @@ -125,27 +125,29 @@ One fraction governs both bodies at each impact; the [explanation](../Explanatio module = "aragog" # required: SPIDER has no re-melt path ``` -A run like this reports the schedule it built during start-up, then each impact as it lands: +Eight embryos between 0.05 and 0.10 AU at this spacing go unstable within a few thousand years, so the whole impact history is over long before the interior has cooled. That is deliberate: it makes the coupling visible in a short run. It is not a model of Earth's accretion, which plays out over 107 to 108 yr at 1 AU. Note also that these embryos need not orbit where the PROTEUS planet orbits; only the *fractional* orbit change is transferred, so the dynamical environment and the simulated planet's distance are independent choices. + +During start-up, the run reports the system it evolved and the schedule it kept: ``` [ INFO ] Running giant-impact model for 8 embryos -[ INFO ] Body 6 experienced 1 impacts -[ INFO ] Scheduled 1 impact(s) -... -[ INFO ] Time-stepping: impact at 9.5991e+02 yr, capping dt at 1.00e+02 yr -[ INFO ] Giant impact at t = 9.5991e+02 yr: target 6 struck by 7, adding 0.5000 M_earth -[ INFO ] impactor volatiles: match_planet; atmosphere loss: zephyrus -[ INFO ] impact erosion law: loss fraction 0.554 -[ INFO ] planet is now 1.4999 M_earth at 1.03107 AU, e = 0.0223 +[ INFO ] Body 6 experienced 2 impacts +[ INFO ] Following body 6 (selector 'mass'): 1.300 -> 2.500 M_earth, 0.0973 -> 0.0959 AU +[ INFO ] Scheduled 2 impact(s) +[ INFO ] first at 9.1183e+02 yr, last at 1.9394e+03 yr ``` +Three of the eight embryos survive; `selector = "mass"` follows the heaviest, which grows from 1.3 to 2.5 M⊕ across two impacts, at about 912 yr (struck by body 5, adding 0.700 M⊕) and 1939 yr (struck by body 7, adding 0.500 M⊕). + +As each impact lands, the loop reports it over five lines: the time-stepper announcing the shortened step, the impact itself with its target, impactor and added mass, the volatile and loss modes in force, the erosion fraction the law returned, and the planet's resulting mass, orbital distance and eccentricity. The added mass, the time and the two body ids come straight from the schedule above; the erosion fraction and the resulting planet state depend on the atmosphere the planet happens to have at that moment, so they differ from run to run even for the same schedule. + ## Common pitfalls **Aragog is required.** `interior_energetics.module = "spider"` with accretion enabled is refused at config load. SPIDER keeps its state in a restart file written by an external binary and has no validated re-melt path, so an impact cannot reset its mantle. **No impacts scheduled.** The most common cause is `evolution_time` being short relative to the instability time of the configuration you chose. [Choosing the initial conditions](configuration.md#choosing-the-initial-conditions) covers how spacing and eccentricity set that time. -**All impacts before the run starts.** Morrigan measures time from disk dispersal. If every impact lands before your `time_offset`, they are folded into the initial condition and the run proceeds with none scheduled. Check the reported impact times against your start time. +**Impacts before the run starts are discarded, not absorbed.** Morrigan measures time from disk dispersal. `time_offset` is added to each impact time, and any impact still landing at or before the run's start time is dropped with a warning naming the mass it would have added. That mass is **not** folded into the initial condition: the configured `planet.mass_tot` stands, and the planet ends the run lighter than the dynamics described. If the warning appears, shift `time_offset` so the history falls inside the simulated interval rather than accepting the truncated schedule. **Spacing too wide for the masses.** Beyond a limit that depends on the embryo masses the layout is refused with an error naming the pair; see [choosing the initial conditions](configuration.md#choosing-the-initial-conditions). diff --git a/docs/How-to/releasing.md b/docs/How-to/releasing.md index 152709e..d69f5f9 100644 --- a/docs/How-to/releasing.md +++ b/docs/How-to/releasing.md @@ -6,7 +6,7 @@ Morrigan's version comes from git tags through setuptools-scm, so tagging is wha CalVer, `YY.0M.0D`, matching the rest of the PROTEUS ecosystem. A release cut on 3 August 2026 is tagged `26.08.03`. -The scheme carries no compatibility promise in the numbers themselves. What changes in a way that breaks a consumer is the impact-record schema, and that is called out in the pull request that changes it rather than encoded in the version. PROTEUS pins a version floor, so a release that changes the schema needs the pin raised in the same change. +The scheme carries no compatibility promise in the numbers themselves. What changes in a way that breaks a consumer is the impact-record schema, and that is called out in the pull request that changes it rather than encoded in the version. PROTEUS pins the Morrigan it builds against, so a release that changes the schema needs that pin moved in the same change. ## Before tagging @@ -55,8 +55,10 @@ curl -s https://pypi.org/pypi/fwl-morrigan/json | python -c "import json,sys; pr Installing to check would replace an editable development checkout with the released wheel, which is rarely what you want mid-session. -Note that PyPI normalises the CalVer tag: `26.08.03` is published as `26.8.3`. Both refer to the same release, and a PROTEUS dependency pin should use the normalised form. +Note that PyPI normalises the CalVer tag: `26.08.03` is published as `26.8.3`. Both refer to the same release, and a version-based dependency pin should use the normalised form. ## Moving PROTEUS onto a new release -PROTEUS depends on `fwl-morrigan` by version. After a release, bump that floor in the PROTEUS `pyproject.toml` and open a pull request there; the pin is what decides which Morrigan a coupled run uses. +PROTEUS records the Morrigan it builds against in its own `pyproject.toml`, under `[tool.proteus.modules.morrigan]`, as a repository URL plus a `ref` naming an exact commit. `tools/get_morrigan.sh` resolves that entry and installs the checkout editable, so the `ref` is what decides which Morrigan a coupled run uses. After a release, update it there and open a pull request against PROTEUS. + +A commit `ref` pins a specific tree rather than a version range, so it does not move on its own; a release that is not followed by that update leaves coupled runs on the previous commit. diff --git a/docs/proteus-framework.md b/docs/proteus-framework.md index 0b15062..d099f87 100644 --- a/docs/proteus-framework.md +++ b/docs/proteus-framework.md @@ -34,7 +34,7 @@ The schematic above links every module. Morrigan interacts directly with two of | Module | Exchange | | --- | --- | | [MORS](https://proteus-framework.org/MORS/) and the stellar block | Morrigan reads the host star's mass from `star.mass` rather than repeating it, so the dynamics and the rest of the run cannot disagree about the star | -| [ZEPHYRUS](https://proteus-framework.org/ZEPHYRUS/) | the impacts Morrigan schedules are handed to its collision law to work out how much atmosphere each one erodes. Morrigan itself models no atmosphere | +| [ZEPHYRUS](https://proteus-framework.org/ZEPHYRUS/) | Morrigan models no atmosphere, so impact erosion is left to the consumer. When a coupled run selects `atmloss_module = "zephyrus"`, PROTEUS passes each scheduled impact's collision parameters to ZEPHYRUS's collision law to work out how much atmosphere it strips. The call is PROTEUS's; Morrigan has no ZEPHYRUS dependency | ## Conventions the ecosystem shares diff --git a/src/morrigan/interaction_timescales.py b/src/morrigan/interaction_timescales.py index 7be0766..c4fc336 100644 --- a/src/morrigan/interaction_timescales.py +++ b/src/morrigan/interaction_timescales.py @@ -72,9 +72,11 @@ def interaction_wrapper(ap, Mp, Ms, ecc, N_affect): #determine if system is stab N_affect : int Number of planets interacting - Returns: - 1e20 if system is stable - Otherwise returns instability timescale from tau_cross_petit + Returns + ------- + float + 1e20 if the system is stable, otherwise the instability timescale + from ``tau_cross_petit`` [s] ''' #N_affect is planets participating in crossing event aM = (Mp[0]*ap[0] + Mp[1]*ap[1]) / (Mp[0] + Mp[1]) From 20ff2917cb1d1ca0c81d113dd24aecc0c5210258 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 25 Jul 2026 22:33:46 +0200 Subject: [PATCH 3/6] Put the start-up log lines in the order the run emits them The selected body is reported by select_planet as soon as the dynamical run returns, before the impact count is logged, so the worked example's transcript had those two lines the wrong way round. --- docs/How-to/proteus_coupling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/How-to/proteus_coupling.md b/docs/How-to/proteus_coupling.md index e1f1067..e7185dd 100644 --- a/docs/How-to/proteus_coupling.md +++ b/docs/How-to/proteus_coupling.md @@ -131,8 +131,8 @@ During start-up, the run reports the system it evolved and the schedule it kept: ``` [ INFO ] Running giant-impact model for 8 embryos -[ INFO ] Body 6 experienced 2 impacts [ INFO ] Following body 6 (selector 'mass'): 1.300 -> 2.500 M_earth, 0.0973 -> 0.0959 AU +[ INFO ] Body 6 experienced 2 impacts [ INFO ] Scheduled 2 impact(s) [ INFO ] first at 9.1183e+02 yr, last at 1.9394e+03 yr ``` From 5a216cd2c9264e7cfdf5e54f67f024aaf06a363f Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 25 Jul 2026 22:48:26 +0200 Subject: [PATCH 4/6] Describe the re-melt energy booking and the molten-state guarantee accurately The Aragog re-melt measures the heat it injects across the cooled-to-molten entropy jump and books it into the energy budget as step_dE_impact_J, so an impact enters the budget as a named source. What is logged rather than booked is the impact's kinetic energy, which is a different quantity. The page had conflated the two and said the heat was not booked at all. Of the temperature modes that suppress the start-up warning, only liquidus_super solves for a profile guaranteed fully molten; adiabatic_from_cmb reaches one only if tcmb_init is pinned high enough, and accretion is not checked against the liquidus at all. The page had presented all three as guarantees. Also: the impact block's line count depends on which consequences apply, so describe its shape rather than fixing it at five, and the eight embryos of the worked example run out to 0.11 AU rather than 0.10. --- docs/Explanations/proteus_coupling.md | 4 ++-- docs/How-to/proteus_coupling.md | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/Explanations/proteus_coupling.md b/docs/Explanations/proteus_coupling.md index 18ff8f7..f2f960d 100644 --- a/docs/Explanations/proteus_coupling.md +++ b/docs/Explanations/proteus_coupling.md @@ -94,7 +94,7 @@ When the loop reaches a scheduled time, `apply_impact` applies seven consequence 2. **Grow the planet.** `planet.mass_tot` increases by the impactor's *rock* alone, the impactor's mass less its volatile content. The volatiles arrive through their own channel below, so counting them here would double-count them. 3. **Re-solve the structure.** The interior module recomputes radius, core size, and pressures at the new mass. 4. **Apply the volatile changes.** The target loses its stripped fraction, the impactor delivers what survives, and the whole-planet element budget is refreshed. -5. **Re-melt the mantle.** The interior is reset to its molten initial condition, recomputed for the grown planet, and the next interior solve is told not to clip the resulting temperature jump. How molten that state is depends on `planet.temperature_mode`: it is fully molten for `liquidus_super`, `accretion` and `adiabatic_from_cmb`, and otherwise only as molten as the configured initial condition, which the run warns about at start-up. The impact's kinetic energy is **reported** for comparison against the enthalpy the reset injects, not added to an energy budget; the reset carries no source term, so checking energy conservation across an impact means weighing the two by hand. +5. **Re-melt the mantle.** The interior is reset to its molten initial condition, recomputed for the grown planet, and the next interior solve is told not to clip the resulting temperature jump. On Aragog the heat this injects is measured across the cooled-to-molten entropy jump and booked into the energy budget as `step_dE_impact_J`, so an impact appears in the budget as a named source rather than as an unexplained residual. The impact's *kinetic* energy is logged alongside it for comparison but is not itself booked; the two are different quantities. How molten the reset state is depends on `planet.temperature_mode`: only `liquidus_super` solves for a profile that is guaranteed fully molten. `adiabatic_from_cmb` can be made molten by pinning `tcmb_init` high enough, and `accretion` usually is, but neither is checked against the liquidus. The remaining modes are only as molten as the configured initial condition, and the run warns about those at start-up. 6. **Clear the solidification latch.** A mantle that had crystallised is molten again, so the one-way latch is lifted. Without this step outgassing would stay frozen and the volatiles would be treated as locked in a solid mantle for good. 7. **Move the orbit.** The semi-major axis is scaled by the impact's *fractional* change and the eccentricity is set to its post-impact value. @@ -106,7 +106,7 @@ Morrigan reports bare bodies. Three pieces of physics live entirely on the PROTE - **Impact atmospheric erosion.** Morrigan tracks no atmosphere and its merged masses are exact sums. PROTEUS computes what an impact strips, either as a fixed fraction or through the Kegerreis et al. (2020)[^cite-kegerreis2020] scaling law evaluated by `zephyrus.collision.mass_loss` from the record's own collision parameters. One fraction governs both bodies: the target loses that fraction of its atmosphere, and a volatile-bearing impactor loses the same fraction of its atmospheric part. - **Volatile delivery.** Whether an impactor carries volatiles at all is a PROTEUS choice: dry, matching the planet's formation composition, or per-element budgets. -- **The thermal consequence.** Re-melting the mantle, lifting the solidification latch, and reporting the impact energy is interior physics, not dynamics. +- **The thermal consequence.** Re-melting the mantle, booking the heat that injects, and lifting the solidification latch is interior physics, not dynamics. ## The impact record schema diff --git a/docs/How-to/proteus_coupling.md b/docs/How-to/proteus_coupling.md index e7185dd..b97fc28 100644 --- a/docs/How-to/proteus_coupling.md +++ b/docs/How-to/proteus_coupling.md @@ -125,7 +125,7 @@ One fraction governs both bodies at each impact; the [explanation](../Explanatio module = "aragog" # required: SPIDER has no re-melt path ``` -Eight embryos between 0.05 and 0.10 AU at this spacing go unstable within a few thousand years, so the whole impact history is over long before the interior has cooled. That is deliberate: it makes the coupling visible in a short run. It is not a model of Earth's accretion, which plays out over 107 to 108 yr at 1 AU. Note also that these embryos need not orbit where the PROTEUS planet orbits; only the *fractional* orbit change is transferred, so the dynamical environment and the simulated planet's distance are independent choices. +Eight embryos between 0.05 and 0.11 AU at this spacing go unstable within a few thousand years, so the whole impact history is over long before the interior has cooled. That is deliberate: it makes the coupling visible in a short run. It is not a model of Earth's accretion, which plays out over 107 to 108 yr at 1 AU. Note also that these embryos need not orbit where the PROTEUS planet orbits; only the *fractional* orbit change is transferred, so the dynamical environment and the simulated planet's distance are independent choices. During start-up, the run reports the system it evolved and the schedule it kept: @@ -139,7 +139,9 @@ During start-up, the run reports the system it evolved and the schedule it kept: Three of the eight embryos survive; `selector = "mass"` follows the heaviest, which grows from 1.3 to 2.5 M⊕ across two impacts, at about 912 yr (struck by body 5, adding 0.700 M⊕) and 1939 yr (struck by body 7, adding 0.500 M⊕). -As each impact lands, the loop reports it over five lines: the time-stepper announcing the shortened step, the impact itself with its target, impactor and added mass, the volatile and loss modes in force, the erosion fraction the law returned, and the planet's resulting mass, orbital distance and eccentricity. The added mass, the time and the two body ids come straight from the schedule above; the erosion fraction and the resulting planet state depend on the atmosphere the planet happens to have at that moment, so they differ from run to run even for the same schedule. +As each impact lands, the loop reports it in a block that opens with the time-stepper announcing the shortened step and the impact itself (its target, impactor and added mass), and closes with the planet's resulting mass, orbital distance and eccentricity. Between those, one indented line is written per consequence that actually applied: the volatile and loss modes in force, the erosion fraction the law returned, what was stripped and what was delivered, the re-melt heat booked into the energy budget, and the mantle reset. How many of those appear depends on the configuration, so expect more lines under `match_planet` and `zephyrus` than under `dry` and `none`. + +The added mass, the time and the two body ids come straight from the schedule above. The erosion fraction and the resulting planet state depend on the atmosphere the planet happens to have at that moment, so they differ from run to run even for the same schedule. ## Common pitfalls From bdd5fb5750ee12593c67b83f1b0681b1ac8554d4 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 25 Jul 2026 23:00:32 +0200 Subject: [PATCH 5/6] State the energy convention, the melt guarantee and the backend choice exactly The re-melt heat column is added to both sides of the energy budget, so the residual stays closed across an impact for any booked value and cannot validate the magnitude. Describing it as a named source that replaces an unexplained residual claimed a check the budget does not perform. The booking also needs a pre-impact entropy profile to measure against, which is now said rather than implied. Nothing checks the accretion temperature mode against the liquidus, so calling it usually molten asserted more than the code supports; only liquidus_super carries a guarantee, and it raises when the requested superheat is unreachable. SPIDER is the module accretion refuses, not the only one it accepts: dummy and boundary re-melt through a surface temperature, so tsurf_init governs their post-impact state and they book no heat. The pitfall now names the real constraint and says what the lighter backends do differently, which is what someone wanting a quick test of the coupling needs. The per-impact log description no longer implies the opening and closing lines are adjacent; the structure re-solve between them is a full interior calculation that prints its own output. --- docs/Explanations/proteus_coupling.md | 4 ++-- docs/How-to/proteus_coupling.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Explanations/proteus_coupling.md b/docs/Explanations/proteus_coupling.md index f2f960d..f4625cb 100644 --- a/docs/Explanations/proteus_coupling.md +++ b/docs/Explanations/proteus_coupling.md @@ -94,7 +94,7 @@ When the loop reaches a scheduled time, `apply_impact` applies seven consequence 2. **Grow the planet.** `planet.mass_tot` increases by the impactor's *rock* alone, the impactor's mass less its volatile content. The volatiles arrive through their own channel below, so counting them here would double-count them. 3. **Re-solve the structure.** The interior module recomputes radius, core size, and pressures at the new mass. 4. **Apply the volatile changes.** The target loses its stripped fraction, the impactor delivers what survives, and the whole-planet element budget is refreshed. -5. **Re-melt the mantle.** The interior is reset to its molten initial condition, recomputed for the grown planet, and the next interior solve is told not to clip the resulting temperature jump. On Aragog the heat this injects is measured across the cooled-to-molten entropy jump and booked into the energy budget as `step_dE_impact_J`, so an impact appears in the budget as a named source rather than as an unexplained residual. The impact's *kinetic* energy is logged alongside it for comparison but is not itself booked; the two are different quantities. How molten the reset state is depends on `planet.temperature_mode`: only `liquidus_super` solves for a profile that is guaranteed fully molten. `adiabatic_from_cmb` can be made molten by pinning `tcmb_init` high enough, and `accretion` usually is, but neither is checked against the liquidus. The remaining modes are only as molten as the configured initial condition, and the run warns about those at start-up. +5. **Re-melt the mantle.** The interior is reset to its molten initial condition, recomputed for the grown planet, and the next interior solve is told not to clip the resulting temperature jump. On Aragog the heat this injects is measured across the cooled-to-molten entropy jump and recorded as `step_dE_impact_J`, whenever a pre-impact entropy profile exists to measure it against. That column is added to *both* sides of the energy budget, so it quantifies the injection under a defined convention: the residual stays closed across an impact for any booked value and so cannot validate the magnitude itself. The impact's *kinetic* energy is logged separately; it is a different quantity, not the booked heat. How molten the reset state is depends on `planet.temperature_mode`: only `liquidus_super` solves for a profile guaranteed fully molten, raising if the requested superheat cannot be reached. `adiabatic_from_cmb` reaches one if `tcmb_init` is pinned high enough, but neither it nor `accretion` is checked against the liquidus. The remaining modes are only as molten as the configured initial condition, and an Aragog run warns about those at start-up. 6. **Clear the solidification latch.** A mantle that had crystallised is molten again, so the one-way latch is lifted. Without this step outgassing would stay frozen and the volatiles would be treated as locked in a solid mantle for good. 7. **Move the orbit.** The semi-major axis is scaled by the impact's *fractional* change and the eccentricity is set to its post-impact value. @@ -106,7 +106,7 @@ Morrigan reports bare bodies. Three pieces of physics live entirely on the PROTE - **Impact atmospheric erosion.** Morrigan tracks no atmosphere and its merged masses are exact sums. PROTEUS computes what an impact strips, either as a fixed fraction or through the Kegerreis et al. (2020)[^cite-kegerreis2020] scaling law evaluated by `zephyrus.collision.mass_loss` from the record's own collision parameters. One fraction governs both bodies: the target loses that fraction of its atmosphere, and a volatile-bearing impactor loses the same fraction of its atmospheric part. - **Volatile delivery.** Whether an impactor carries volatiles at all is a PROTEUS choice: dry, matching the planet's formation composition, or per-element budgets. -- **The thermal consequence.** Re-melting the mantle, booking the heat that injects, and lifting the solidification latch is interior physics, not dynamics. +- **The thermal consequence.** Re-melting the mantle, recording the heat that reset injects, and lifting the solidification latch is interior physics, not dynamics. ## The impact record schema diff --git a/docs/How-to/proteus_coupling.md b/docs/How-to/proteus_coupling.md index b97fc28..ceac1d1 100644 --- a/docs/How-to/proteus_coupling.md +++ b/docs/How-to/proteus_coupling.md @@ -122,7 +122,7 @@ One fraction governs both bodies at each impact; the [explanation](../Explanatio selector = "mass" [interior_energetics] - module = "aragog" # required: SPIDER has no re-melt path + module = "aragog" # SPIDER is refused: it has no re-melt path ``` Eight embryos between 0.05 and 0.11 AU at this spacing go unstable within a few thousand years, so the whole impact history is over long before the interior has cooled. That is deliberate: it makes the coupling visible in a short run. It is not a model of Earth's accretion, which plays out over 107 to 108 yr at 1 AU. Note also that these embryos need not orbit where the PROTEUS planet orbits; only the *fractional* orbit change is transferred, so the dynamical environment and the simulated planet's distance are independent choices. @@ -139,13 +139,13 @@ During start-up, the run reports the system it evolved and the schedule it kept: Three of the eight embryos survive; `selector = "mass"` follows the heaviest, which grows from 1.3 to 2.5 M⊕ across two impacts, at about 912 yr (struck by body 5, adding 0.700 M⊕) and 1939 yr (struck by body 7, adding 0.500 M⊕). -As each impact lands, the loop reports it in a block that opens with the time-stepper announcing the shortened step and the impact itself (its target, impactor and added mass), and closes with the planet's resulting mass, orbital distance and eccentricity. Between those, one indented line is written per consequence that actually applied: the volatile and loss modes in force, the erosion fraction the law returned, what was stripped and what was delivered, the re-melt heat booked into the energy budget, and the mantle reset. How many of those appear depends on the configuration, so expect more lines under `match_planet` and `zephyrus` than under `dry` and `none`. +Each impact is then announced by a `Giant impact at t = ...` line naming its target, impactor and added mass, and closed by a `planet is now ... M_earth at ... AU, e = ...` line. Between the two, an indented line is written for each consequence that applied: which volatile and loss modes are in force, what the erosion law returned, what was stripped and what was delivered, the re-melt heat and the mantle reset. How many appear depends on the configuration, so expect more under `match_planet` and `zephyrus` than under `dry` and `none`. The structure re-solve in the middle is a full interior calculation and prints its own output, so those two anchor lines are not adjacent in the log. The added mass, the time and the two body ids come straight from the schedule above. The erosion fraction and the resulting planet state depend on the atmosphere the planet happens to have at that moment, so they differ from run to run even for the same schedule. ## Common pitfalls -**Aragog is required.** `interior_energetics.module = "spider"` with accretion enabled is refused at config load. SPIDER keeps its state in a restart file written by an external binary and has no validated re-melt path, so an impact cannot reset its mantle. +**SPIDER cannot be used.** `interior_energetics.module = "spider"` with accretion enabled is refused at config load. SPIDER keeps its state in a restart file written by an external binary and has no validated re-melt path, so an impact cannot reset its mantle. Aragog is the production choice; `dummy` and `boundary` also re-melt and are useful for a quick test of the coupling, though they reset a surface temperature rather than an entropy profile, so `planet.tsurf_init` governs their post-impact state rather than `planet.temperature_mode`, and they book no re-melt heat. **No impacts scheduled.** The most common cause is `evolution_time` being short relative to the instability time of the configuration you chose. [Choosing the initial conditions](configuration.md#choosing-the-initial-conditions) covers how spacing and eccentricity set that time. From 3c4d6d8286ba1f20f82d0df21ca71c83c1e39cf0 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 25 Jul 2026 23:12:21 +0200 Subject: [PATCH 6/6] Scope the melt and residual statements to the backend they describe The absolute residual is what stays closed across an impact; the relative one carries the injection in its denominator and can spike on that row, which is the number a reader is most likely to be looking at. Say which is which. The temperature-mode discussion applies to Aragog. The scalar backends never read it: they reset to tsurf_init, so leaving that below the liquidus gives a partial re-melt, and the paragraph now says so instead of implying temperature_mode governs every backend. Also: boundary needs atmos_clim.surf_state = "fixed", so recommending it for a quick test without that lands the reader on a config error about something else entirely. Dummy carries no such condition and is the better suggestion. --- docs/Explanations/proteus_coupling.md | 6 ++++-- docs/How-to/proteus_coupling.md | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/Explanations/proteus_coupling.md b/docs/Explanations/proteus_coupling.md index f4625cb..ced39f9 100644 --- a/docs/Explanations/proteus_coupling.md +++ b/docs/Explanations/proteus_coupling.md @@ -94,7 +94,9 @@ When the loop reaches a scheduled time, `apply_impact` applies seven consequence 2. **Grow the planet.** `planet.mass_tot` increases by the impactor's *rock* alone, the impactor's mass less its volatile content. The volatiles arrive through their own channel below, so counting them here would double-count them. 3. **Re-solve the structure.** The interior module recomputes radius, core size, and pressures at the new mass. 4. **Apply the volatile changes.** The target loses its stripped fraction, the impactor delivers what survives, and the whole-planet element budget is refreshed. -5. **Re-melt the mantle.** The interior is reset to its molten initial condition, recomputed for the grown planet, and the next interior solve is told not to clip the resulting temperature jump. On Aragog the heat this injects is measured across the cooled-to-molten entropy jump and recorded as `step_dE_impact_J`, whenever a pre-impact entropy profile exists to measure it against. That column is added to *both* sides of the energy budget, so it quantifies the injection under a defined convention: the residual stays closed across an impact for any booked value and so cannot validate the magnitude itself. The impact's *kinetic* energy is logged separately; it is a different quantity, not the booked heat. How molten the reset state is depends on `planet.temperature_mode`: only `liquidus_super` solves for a profile guaranteed fully molten, raising if the requested superheat cannot be reached. `adiabatic_from_cmb` reaches one if `tcmb_init` is pinned high enough, but neither it nor `accretion` is checked against the liquidus. The remaining modes are only as molten as the configured initial condition, and an Aragog run warns about those at start-up. +5. **Re-melt the mantle.** The interior is reset to its molten initial condition, recomputed for the grown planet, and the next interior solve is told not to clip the resulting temperature jump. On Aragog the heat this injects is measured across the cooled-to-molten entropy jump and recorded as `step_dE_impact_J`, whenever a pre-impact entropy profile exists to measure it against. That column is added to *both* sides of the energy budget, so it quantifies the injection under a defined convention: `E_residual_cons_J` is unchanged across an impact for any booked value, and so cannot validate the magnitude itself. The *relative* residual is not invariant, because the injection enters its denominator; expect `E_residual_cons_frac` to spike on an impact row when the two are comparable, and read the absolute residual there instead. The impact's *kinetic* energy is logged separately; it is a different quantity, not the booked heat. + +Still on Aragog, how molten the reset state is depends on `planet.temperature_mode`: only `liquidus_super` solves for a profile guaranteed fully molten, raising if the requested superheat cannot be reached. `adiabatic_from_cmb` reaches one if `tcmb_init` is pinned high enough, but neither it nor `accretion` is checked against the liquidus. The remaining modes are only as molten as the configured initial condition, and an Aragog run warns about those at start-up. The scalar backends ignore `temperature_mode` entirely: they reset a surface temperature taken from `planet.tsurf_init`, and warn at the impact if that leaves the mantle short of fully molten. 6. **Clear the solidification latch.** A mantle that had crystallised is molten again, so the one-way latch is lifted. Without this step outgassing would stay frozen and the volatiles would be treated as locked in a solid mantle for good. 7. **Move the orbit.** The semi-major axis is scaled by the impact's *fractional* change and the eccentricity is set to its post-impact value. @@ -106,7 +108,7 @@ Morrigan reports bare bodies. Three pieces of physics live entirely on the PROTE - **Impact atmospheric erosion.** Morrigan tracks no atmosphere and its merged masses are exact sums. PROTEUS computes what an impact strips, either as a fixed fraction or through the Kegerreis et al. (2020)[^cite-kegerreis2020] scaling law evaluated by `zephyrus.collision.mass_loss` from the record's own collision parameters. One fraction governs both bodies: the target loses that fraction of its atmosphere, and a volatile-bearing impactor loses the same fraction of its atmospheric part. - **Volatile delivery.** Whether an impactor carries volatiles at all is a PROTEUS choice: dry, matching the planet's formation composition, or per-element budgets. -- **The thermal consequence.** Re-melting the mantle, recording the heat that reset injects, and lifting the solidification latch is interior physics, not dynamics. +- **The thermal consequence.** Re-melting the mantle, recording the heat that the reset injects, and lifting the solidification latch is interior physics, not dynamics. ## The impact record schema diff --git a/docs/How-to/proteus_coupling.md b/docs/How-to/proteus_coupling.md index ceac1d1..7de655c 100644 --- a/docs/How-to/proteus_coupling.md +++ b/docs/How-to/proteus_coupling.md @@ -139,13 +139,13 @@ During start-up, the run reports the system it evolved and the schedule it kept: Three of the eight embryos survive; `selector = "mass"` follows the heaviest, which grows from 1.3 to 2.5 M⊕ across two impacts, at about 912 yr (struck by body 5, adding 0.700 M⊕) and 1939 yr (struck by body 7, adding 0.500 M⊕). -Each impact is then announced by a `Giant impact at t = ...` line naming its target, impactor and added mass, and closed by a `planet is now ... M_earth at ... AU, e = ...` line. Between the two, an indented line is written for each consequence that applied: which volatile and loss modes are in force, what the erosion law returned, what was stripped and what was delivered, the re-melt heat and the mantle reset. How many appear depends on the configuration, so expect more under `match_planet` and `zephyrus` than under `dry` and `none`. The structure re-solve in the middle is a full interior calculation and prints its own output, so those two anchor lines are not adjacent in the log. +Each impact is then announced by a `Giant impact at t = ...` line naming its target, impactor and added mass, and closed by a `planet is now ... M_earth at ... AU, e = ...` line. Between the two, indented lines report the consequences that applied: which volatile and loss modes are in force, what the erosion law returned, what was stripped and what was delivered, the re-melt heat, the mantle reset, the impact's kinetic energy, and the solidification latch if it was cleared. How many appear depends on the configuration, so expect more under `match_planet` and `zephyrus` than under `dry` and `none`. The structure re-solve in the middle is a full interior calculation and prints its own output, so those two anchor lines are not adjacent in the log. The added mass, the time and the two body ids come straight from the schedule above. The erosion fraction and the resulting planet state depend on the atmosphere the planet happens to have at that moment, so they differ from run to run even for the same schedule. ## Common pitfalls -**SPIDER cannot be used.** `interior_energetics.module = "spider"` with accretion enabled is refused at config load. SPIDER keeps its state in a restart file written by an external binary and has no validated re-melt path, so an impact cannot reset its mantle. Aragog is the production choice; `dummy` and `boundary` also re-melt and are useful for a quick test of the coupling, though they reset a surface temperature rather than an entropy profile, so `planet.tsurf_init` governs their post-impact state rather than `planet.temperature_mode`, and they book no re-melt heat. +**SPIDER cannot be used.** `interior_energetics.module = "spider"` with accretion enabled is refused at config load. SPIDER keeps its state in a restart file written by an external binary and has no validated re-melt path, so an impact cannot reset its mantle. Aragog is the production choice. `dummy` also re-melts and is the quickest way to exercise the coupling; `boundary` does too, but additionally requires `atmos_clim.surf_state = "fixed"`, which the default `"skin"` does not satisfy. Both reset a surface temperature rather than an entropy profile, so `planet.tsurf_init` governs their post-impact state and `planet.temperature_mode` is inert; set `tsurf_init` above the liquidus or the re-melt leaves the mantle partly solid and says so. **No impacts scheduled.** The most common cause is `evolution_time` being short relative to the instability time of the configuration you chose. [Choosing the initial conditions](configuration.md#choosing-the-initial-conditions) covers how spacing and eccentricity set that time.