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..cad8542 --- /dev/null +++ b/docs/Explanations/code_architecture.md @@ -0,0 +1,80 @@ +# 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.cli console-script entry point + | + v + driver.main read settings, 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 `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 + +| 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. + +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 29ed84b..ced39f9 100644 --- a/docs/Explanations/proteus_coupling.md +++ b/docs/Explanations/proteus_coupling.md @@ -1,27 +1,114 @@ # 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) 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: │ + │ │ + │ pending = next_event(events, t) # the loop reads the │ + │ interior_o.t_next_impact = pending.time # schedule, never the model│ + │ │ + │ run_interior(...) # cooling step; the │ + │ # time-stepper shortens dt │ + │ # to land on t_next_impact │ + │ │ + │ 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(...) / ... │ + └──────────────────────────────────────────────────────────────────────────┘ ``` -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 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. + +## 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 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 + +`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. `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 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 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: `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. + +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, recording the heat that the reset injects, and lifting the solidification latch is interior physics, not dynamics. ## The impact record schema @@ -42,7 +129,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..a3457e3 --- /dev/null +++ b/docs/How-to/configuration.md @@ -0,0 +1,62 @@ +# 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'] + 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' + +['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 +``` + +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. + +## 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..7de655c --- /dev/null +++ b/docs/How-to/proteus_coupling.md @@ -0,0 +1,161 @@ +# 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: a compact system feeding a growing planet + +```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" # 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. + +During start-up, the run reports the system it evolved and the schedule it kept: + +``` +[ INFO ] Running giant-impact model for 8 embryos +[ 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 +``` + +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, 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` 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. + +**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). + +**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..d69f5f9 --- /dev/null +++ b/docs/How-to/releasing.md @@ -0,0 +1,64 @@ +# 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 the Morrigan it builds against, so a release that changes the schema needs that pin moved 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 version-based dependency pin should use the normalised form. + +## Moving PROTEUS onto a new release + +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/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..d099f87 --- /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/) | 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 + +- **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/ 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])