fwl-morrigan 26.7.25__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fwl_morrigan-26.7.25/.github/.claude/rules/morrigan-code-review.md +67 -0
- fwl_morrigan-26.7.25/.github/.claude/rules/morrigan-tests.md +253 -0
- fwl_morrigan-26.7.25/.github/copilot-instructions.md +346 -0
- fwl_morrigan-26.7.25/.github/workflows/docs.yaml +33 -0
- fwl_morrigan-26.7.25/.github/workflows/nightly.yml +61 -0
- fwl_morrigan-26.7.25/.github/workflows/publish-test-badges.yml +212 -0
- fwl_morrigan-26.7.25/.github/workflows/publish.yaml +48 -0
- fwl_morrigan-26.7.25/.github/workflows/tests.yaml +160 -0
- fwl_morrigan-26.7.25/.gitignore +253 -0
- fwl_morrigan-26.7.25/.pre-commit-config.yaml +32 -0
- fwl_morrigan-26.7.25/CLAUDE.md +346 -0
- fwl_morrigan-26.7.25/CODE_OF_CONDUCT.md +128 -0
- fwl_morrigan-26.7.25/CONTRIBUTING.md +63 -0
- fwl_morrigan-26.7.25/LICENSE +201 -0
- fwl_morrigan-26.7.25/PKG-INFO +102 -0
- fwl_morrigan-26.7.25/README.md +64 -0
- fwl_morrigan-26.7.25/docs/Community/CODE_OF_CONDUCT.md +1 -0
- fwl_morrigan-26.7.25/docs/Community/CONTRIBUTING.md +1 -0
- fwl_morrigan-26.7.25/docs/Community/contact.md +11 -0
- fwl_morrigan-26.7.25/docs/Explanations/code_architecture.md +80 -0
- fwl_morrigan-26.7.25/docs/Explanations/limitations.md +32 -0
- fwl_morrigan-26.7.25/docs/Explanations/model.md +70 -0
- fwl_morrigan-26.7.25/docs/Explanations/proteus_coupling.md +143 -0
- fwl_morrigan-26.7.25/docs/How-to/configuration.md +62 -0
- fwl_morrigan-26.7.25/docs/How-to/installation.md +33 -0
- fwl_morrigan-26.7.25/docs/How-to/proteus_coupling.md +161 -0
- fwl_morrigan-26.7.25/docs/How-to/releasing.md +64 -0
- fwl_morrigan-26.7.25/docs/How-to/running.md +39 -0
- fwl_morrigan-26.7.25/docs/How-to/testing.md +47 -0
- fwl_morrigan-26.7.25/docs/Reference/api.md +43 -0
- fwl_morrigan-26.7.25/docs/Reference/parameters.md +40 -0
- fwl_morrigan-26.7.25/docs/Validation/crossing_pair.md +9 -0
- fwl_morrigan-26.7.25/docs/Validation/driver.md +9 -0
- fwl_morrigan-26.7.25/docs/Validation/ensemble_statistics.md +10 -0
- fwl_morrigan-26.7.25/docs/Validation/helper_functions.md +11 -0
- fwl_morrigan-26.7.25/docs/Validation/index.md +16 -0
- fwl_morrigan-26.7.25/docs/Validation/interaction_timescales.md +14 -0
- fwl_morrigan-26.7.25/docs/Validation/merge_embryo.md +10 -0
- fwl_morrigan-26.7.25/docs/Validation/orbit_cross_K25.md +9 -0
- fwl_morrigan-26.7.25/docs/Validation/secular_solution.md +9 -0
- fwl_morrigan-26.7.25/docs/assets/favicon-light.png +0 -0
- fwl_morrigan-26.7.25/docs/assets/favicon.png +0 -0
- fwl_morrigan-26.7.25/docs/assets/favicon.svg +3 -0
- fwl_morrigan-26.7.25/docs/assets/glyph_phase_dark.png +0 -0
- fwl_morrigan-26.7.25/docs/getting_started.md +28 -0
- fwl_morrigan-26.7.25/docs/index.md +26 -0
- fwl_morrigan-26.7.25/docs/javascripts/header-links.js +26 -0
- fwl_morrigan-26.7.25/docs/javascripts/katex.js +10 -0
- fwl_morrigan-26.7.25/docs/overrides/main.html +54 -0
- fwl_morrigan-26.7.25/docs/proteus-framework.md +43 -0
- fwl_morrigan-26.7.25/docs/stylesheets/extra.css +183 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/OFL.txt +95 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/fonts.css +68 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/instrument-sans/InstrumentSans-Medium.ttf +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/instrument-sans/InstrumentSans-Medium.woff2 +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/instrument-sans/InstrumentSans-Regular.ttf +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/instrument-sans/InstrumentSans-Regular.woff2 +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/instrument-sans/InstrumentSans-SemiBold.ttf +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/instrument-sans/InstrumentSans-SemiBold.woff2 +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/sora/Sora-Bold.ttf +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/sora/Sora-Bold.woff2 +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/sora/Sora-ExtraBold.ttf +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/sora/Sora-ExtraBold.woff2 +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/sora/Sora-SemiBold.ttf +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/sora/Sora-SemiBold.woff2 +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/spline-sans-mono/SplineSansMono-Medium.ttf +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/spline-sans-mono/SplineSansMono-Medium.woff2 +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/spline-sans-mono/SplineSansMono-Regular.ttf +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/fonts/spline-sans-mono/SplineSansMono-Regular.woff2 +0 -0
- fwl_morrigan-26.7.25/docs/stylesheets/footnotes.css +21 -0
- fwl_morrigan-26.7.25/docs/stylesheets/layout.css +185 -0
- fwl_morrigan-26.7.25/docs/stylesheets/widths.css +23 -0
- fwl_morrigan-26.7.25/fwl_morrigan.egg-info/PKG-INFO +102 -0
- fwl_morrigan-26.7.25/fwl_morrigan.egg-info/SOURCES.txt +110 -0
- fwl_morrigan-26.7.25/fwl_morrigan.egg-info/dependency_links.txt +1 -0
- fwl_morrigan-26.7.25/fwl_morrigan.egg-info/entry_points.txt +2 -0
- fwl_morrigan-26.7.25/fwl_morrigan.egg-info/requires.txt +22 -0
- fwl_morrigan-26.7.25/fwl_morrigan.egg-info/scm_file_list.json +105 -0
- fwl_morrigan-26.7.25/fwl_morrigan.egg-info/scm_version.json +8 -0
- fwl_morrigan-26.7.25/fwl_morrigan.egg-info/top_level.txt +1 -0
- fwl_morrigan-26.7.25/initialise.toml +29 -0
- fwl_morrigan-26.7.25/mkdocs.yml +177 -0
- fwl_morrigan-26.7.25/plot.py +287 -0
- fwl_morrigan-26.7.25/pyproject.toml +134 -0
- fwl_morrigan-26.7.25/setup.cfg +4 -0
- fwl_morrigan-26.7.25/src/morrigan/__init__.py +27 -0
- fwl_morrigan-26.7.25/src/morrigan/_version.py +24 -0
- fwl_morrigan-26.7.25/src/morrigan/constants.py +11 -0
- fwl_morrigan-26.7.25/src/morrigan/crossing_pair.py +132 -0
- fwl_morrigan-26.7.25/src/morrigan/driver.py +554 -0
- fwl_morrigan-26.7.25/src/morrigan/helper_functions.py +33 -0
- fwl_morrigan-26.7.25/src/morrigan/interaction_timescales.py +173 -0
- fwl_morrigan-26.7.25/src/morrigan/merge_embryo.py +108 -0
- fwl_morrigan-26.7.25/src/morrigan/orbit_cross_K25.py +200 -0
- fwl_morrigan-26.7.25/src/morrigan/secular_solution.py +92 -0
- fwl_morrigan-26.7.25/src/morrigan/sort_planet.py +61 -0
- fwl_morrigan-26.7.25/tests/test_crossing_pair.py +162 -0
- fwl_morrigan-26.7.25/tests/test_driver.py +456 -0
- fwl_morrigan-26.7.25/tests/test_ensemble_statistics.py +170 -0
- fwl_morrigan-26.7.25/tests/test_generate_test_badges.py +115 -0
- fwl_morrigan-26.7.25/tests/test_helper_functions.py +183 -0
- fwl_morrigan-26.7.25/tests/test_interaction_timescales.py +219 -0
- fwl_morrigan-26.7.25/tests/test_merge_embryo.py +185 -0
- fwl_morrigan-26.7.25/tests/test_orbit_cross_K25.py +232 -0
- fwl_morrigan-26.7.25/tests/test_secular_solution.py +135 -0
- fwl_morrigan-26.7.25/tests/test_sort_planet.py +82 -0
- fwl_morrigan-26.7.25/tools/check_file_sizes.sh +25 -0
- fwl_morrigan-26.7.25/tools/check_test_quality.py +671 -0
- fwl_morrigan-26.7.25/tools/generate_test_badges.py +218 -0
- fwl_morrigan-26.7.25/tools/test_quality_baseline.json +1 -0
- fwl_morrigan-26.7.25/tools/update_coverage_threshold.py +203 -0
- fwl_morrigan-26.7.25/tools/validate_test_structure.sh +130 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Morrigan Code Review Criteria
|
|
2
|
+
|
|
3
|
+
When reviewing Morrigan code (either your own or via code-reviewer agents), apply these domain-specific checks in addition to standard code quality review.
|
|
4
|
+
|
|
5
|
+
> **Discovery note.** Morrigan keeps its Claude-Code rule files under `.github/.claude/rules/` (not the conventional repo-root `.claude/`) so they can be tracked in git and shared across collaborators. Claude does NOT auto-discover them at this path; the repo-root `CLAUDE.md` (symlinked to `.github/copilot-instructions.md`) names this file and `morrigan-tests.md` explicitly. **Before opening any review pass, read both this file and `morrigan-tests.md`.**
|
|
6
|
+
|
|
7
|
+
## Physics plausibility
|
|
8
|
+
|
|
9
|
+
- Masses, radii, densities, and semi-major axes must be strictly positive everywhere. Flag any code path where a merger, a scattering, or the sorting step could produce a non-positive value.
|
|
10
|
+
- Eccentricities must stay in `[0, 1)`. Flag any orbital update that can push `e` to or past unity without an explicit hyperbolic-orbit treatment (there is none in this model; such a state is a bug).
|
|
11
|
+
- The impact parameter is `b = sin(beta)` and lives in `[0, 1]`.
|
|
12
|
+
- The collision speed at contact is `sqrt(v_inf^2 + v_esc^2)` and therefore never below the mutual escape speed. Flag any speed computed another way.
|
|
13
|
+
- Merged masses are exact sums. Flag anything that reduces a body's mass in a merger; the model sheds nothing and the coupled framework validates that chain to machine precision.
|
|
14
|
+
- The planet count is non-increasing over a run; a merger removes exactly one body.
|
|
15
|
+
- Timeline records are strictly ordered in time, and successive records for one body chain their masses.
|
|
16
|
+
|
|
17
|
+
## Unit convention boundaries
|
|
18
|
+
|
|
19
|
+
Morrigan is SI internally with a thin conversion layer at the edges:
|
|
20
|
+
|
|
21
|
+
- **Internal state**: kg, m, s, radians where angles appear.
|
|
22
|
+
- **Config boundary** (`driver.py`): Earth masses, AU, degrees, Gyr. Converted once on read; flag any second conversion site.
|
|
23
|
+
- **Record boundary** (`run_system` output): SI everywhere except `time` in years, using the model's own 365-day year (`gyr2sec / 1e9`). Flag any record field built from a different year convention.
|
|
24
|
+
- **Constants**: single source `src/morrigan/constants.py`. A physical constant introduced as a literal in a function body is a red flag.
|
|
25
|
+
|
|
26
|
+
## Global random state
|
|
27
|
+
|
|
28
|
+
The Monte Carlo layer draws from numpy's global random state, seeded once at the `run_system` entry:
|
|
29
|
+
|
|
30
|
+
- Library code must NOT call `np.random.seed` anywhere except the documented entry-point seeding. A hidden reseed inside a helper silently correlates ensemble members and destroys the seed-to-timeline determinism contract.
|
|
31
|
+
- New stochastic code paths must draw through the global state (consistent with the rest of the model) and must keep the same-seed-same-timeline determinism test green.
|
|
32
|
+
- Flag any use of `random` (the stdlib module) alongside `np.random`; a second, unseeded RNG breaks determinism invisibly.
|
|
33
|
+
|
|
34
|
+
## The PROTEUS record-schema contract
|
|
35
|
+
|
|
36
|
+
`run_system` returns the per-impact records PROTEUS's accretion coupling consumes. The schema (field names, units, and conventions) is documented in `.github/copilot-instructions.md` and is an interface, not an implementation detail:
|
|
37
|
+
|
|
38
|
+
- Any field rename, unit change, or convention change (contact speed, perfect-merger mass, bulk radii) is a breaking change for PROTEUS and must be called out in the PR description.
|
|
39
|
+
- `v_esc` in the record is recomputed from the pair's masses and radii with the mutual formula; keep it consistent with the speed convention.
|
|
40
|
+
- Densities in the record are recovered from mass and radius so each record is exactly self-consistent; flag any change that writes an independent density.
|
|
41
|
+
- The model carries no atmosphere, so `M_merged_after` is always the plain sum of the two bodies and nothing is shed between impacts. Impact erosion belongs to the consumer: in a coupled run PROTEUS applies `zephyrus.collision.mass_loss` to its own atmosphere, from the geometry the record carries. A change that reintroduces atmosphere tracking here would break that separation and the exact mass chain PROTEUS validates against.
|
|
42
|
+
|
|
43
|
+
## Fitted-law fidelity
|
|
44
|
+
|
|
45
|
+
Several formulas are fits transcribed from Kimura et al. (2025). For any transcription change:
|
|
46
|
+
|
|
47
|
+
- Verify the base of every exponential against the paper (`10**` vs `exp` is a historical bug class here).
|
|
48
|
+
- Verify every ratio's denominator against the paper (`M_i / M_tot` vs `M_i / M_t` is a historical bug class here).
|
|
49
|
+
- Verify the fit's validity domain is stated in the docstring, and that any clamp needed to keep the output physical is present.
|
|
50
|
+
- A transcription PR must cite the equation number in the paper.
|
|
51
|
+
|
|
52
|
+
## Config mutability
|
|
53
|
+
|
|
54
|
+
The parsed settings dictionary carries user input and is not mutated after the run starts. Flag any code that writes back into it mid-run; use local variables.
|
|
55
|
+
|
|
56
|
+
## Test marker discipline
|
|
57
|
+
|
|
58
|
+
Every test file must begin with a module-level `pytestmark = [pytest.mark.<tier>, pytest.mark.timeout(<budget>)]` (unit/30 s, smoke/60 s, integration/300 s, slow/3600 s). Per-function markers are additive; CI runs the fast tiers on PR and the slow tiers nightly, and any file missing the tier marker ships untested.
|
|
59
|
+
|
|
60
|
+
## Test quality (cross-reference)
|
|
61
|
+
|
|
62
|
+
Test-content rules (anti-happy-path, discriminating-value guards including the argument-order rule, physics-invariant tiering, certification markers, seeding discipline, mocking discipline) live in [`morrigan-tests.md`](morrigan-tests.md). When reviewing tests, apply both files: this one for marker discipline and the source-side checks, the deep-dive for the content contract.
|
|
63
|
+
|
|
64
|
+
## Sister rules (cross-link)
|
|
65
|
+
|
|
66
|
+
- [`.github/copilot-instructions.md`](../../copilot-instructions.md) "Testing Standards" -- high-level rules visible to all readers. Repo-root `CLAUDE.md` is a symlink to this file.
|
|
67
|
+
- [`morrigan-tests.md`](morrigan-tests.md) -- test quality deep-dive.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Morrigan test quality deep-dive. Anti-happy-path patterns, discriminating-value guards, physics-invariant tiering, validation certification markers, global-RNG seeding discipline, timeline-chain invariants. Extends the Testing Standards section in `.github/copilot-instructions.md`.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Morrigan Test Quality Rules
|
|
6
|
+
|
|
7
|
+
This file is the canonical deep-dive on test quality. The high-level summary lives in [`.github/copilot-instructions.md`](../../copilot-instructions.md) under "Testing Standards". The two files MUST stay in sync. If you change one, mirror the change in the other.
|
|
8
|
+
|
|
9
|
+
> **Discovery note.** Morrigan keeps its Claude-Code rule files under `.github/.claude/rules/` (not the conventional repo-root `.claude/`) so they can be tracked in git and shared across collaborators. Claude does NOT auto-discover them at this path; the repo-root `CLAUDE.md` (symlinked to `.github/copilot-instructions.md`) names this file and `morrigan-code-review.md` explicitly. **When opening or editing any file under `tests/**` or `src/morrigan/**`, read this file first.**
|
|
10
|
+
|
|
11
|
+
Morrigan is scientific simulation code and the test suite is held to physics-grade rigor. A test that asserts the wrong thing, or that passes for the wrong reason, is worse than no test because it generates false confidence.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Anti-happy-path rules (every new test)
|
|
16
|
+
|
|
17
|
+
Every new test function MUST include:
|
|
18
|
+
|
|
19
|
+
1. **At least one edge case**: a boundary value (`b = 0` head-on, `b = 1` grazing, `e = 0` circular, a two-planet minimal system, an extreme mass ratio), an empty input, or an extreme physical parameter.
|
|
20
|
+
2. **At least one path that exercises the error contract**:
|
|
21
|
+
- If the function under test has documented validation, test that the error fires AND that no side effect ran.
|
|
22
|
+
- If the function is closed-form mathematics (Kepler period, Hill radius, timescales), exercise the **limit-input behavior** (equal masses, coincident orbits, zero eccentricity) and assert the corresponding mathematical invariant.
|
|
23
|
+
- "No validation in source therefore no error test" is not an exemption; the limit-input substitute is.
|
|
24
|
+
3. **Assertion values NOT trivially derivable from the implementation**: discriminating numeric pins (Section 2) or property-based assertions (monotonicity, conservation, symmetry, boundedness).
|
|
25
|
+
|
|
26
|
+
### Forbidden patterns
|
|
27
|
+
|
|
28
|
+
These are flagged by `tools/check_test_quality.py` and rejected at PR time.
|
|
29
|
+
|
|
30
|
+
- **Single-assert test functions.** Exception: a single assertion of a hard-fail invariant (mass closure within `1e-12`) is acceptable if the test is the only test of that invariant in the file.
|
|
31
|
+
- **Weak assertions standing alone as the sole meaningful check**: `assert result is not None`, `assert result > 0`, `assert len(result) > 0`, `assert isinstance(result, dict)`, `assert result is None` on an implicit-None return. The three-class discrimination guard's secondary lines (sign guard, scale guard) are NOT flagged when paired with a stronger primary assertion in the same test.
|
|
32
|
+
- **Tests with no function-level docstring.**
|
|
33
|
+
- **`==` adjacent to a float literal.** Use `pytest.approx` or `np.testing.assert_allclose`.
|
|
34
|
+
- **Tests asserting on a fixture's implicit default.**
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. Discriminating test values
|
|
39
|
+
|
|
40
|
+
The test contract: a regression that introduces a plausible bug must fail the test. Plausible bugs in this codebase are known from its history: a wrong mass denominator in a ratio, `np.exp` where `10**` is required, a wrong pair index in a mass product, an unclamped fitted power law, a mass assigned into a fraction array. Pick input values where the wrong-formula result is far from the correct one.
|
|
41
|
+
|
|
42
|
+
### Bad / good examples
|
|
43
|
+
|
|
44
|
+
| Pattern | Bad (any-formula-passes) | Good (discriminates) |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| Crossing timescale with `10**` fit | Inputs where `exp` and `10**` nearly coincide | Inputs where the two bases differ by orders of magnitude |
|
|
47
|
+
| Kepler period | `a` chosen so numeric coincidences mask a wrong exponent | Two semi-major axes spanning a decade; assert the `a^(3/2)` scaling between them |
|
|
48
|
+
| Merger bookkeeping | Equal masses (mass-index errors degenerate) | Unequal masses on the two bodies |
|
|
49
|
+
|
|
50
|
+
### Discrimination guard (REQUIRED for pinned-value tests)
|
|
51
|
+
|
|
52
|
+
When a test pins a numeric value, include explicit assertions that the wrong-formula result would differ for each plausible bug class:
|
|
53
|
+
|
|
54
|
+
1. **Exponent or factor error**: `abs(val - wrong_value)` outside tolerance.
|
|
55
|
+
2. **Sign error**: assert the sign explicitly.
|
|
56
|
+
3. **Unit-conversion error**: pin the absolute scale with the unit named in a comment (seconds vs years and metres vs AU are the recurring boundaries here).
|
|
57
|
+
4. **Wrong-denominator / wrong-index selection**: when a ratio or a pair indexing is the formula's core, the guard MUST include the value the plausible wrong selection would produce.
|
|
58
|
+
|
|
59
|
+
**Argument-order pins.** A difference-based guard (`abs(f(a) - f(b)) > delta`) does NOT pin argument order: a dispatch that permutes its arguments permutes the two values and leaves the difference unchanged. Pin each side to its absolute value instead, plus the direction.
|
|
60
|
+
|
|
61
|
+
**Carve-out for conservation-style invariants.** When the primary assertion IS a conservation closure, the exponent guard is satisfied by the closure itself; sign and scale guards remain mandatory.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 3. Physics-invariant assertions (tiered)
|
|
66
|
+
|
|
67
|
+
### When required
|
|
68
|
+
|
|
69
|
+
Every unit test on a **physics source** must assert at least one of the four invariant families. Physics sources:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
src/morrigan/driver.py
|
|
73
|
+
src/morrigan/orbit_cross_K25.py
|
|
74
|
+
src/morrigan/interaction_timescales.py
|
|
75
|
+
src/morrigan/crossing_pair.py
|
|
76
|
+
src/morrigan/merge_embryo.py
|
|
77
|
+
src/morrigan/secular_solution.py
|
|
78
|
+
src/morrigan/helper_functions.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Utility sources are exempt from the physics-invariant requirement but still subject to all anti-happy-path rules:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
src/morrigan/__init__.py (re-exports)
|
|
85
|
+
src/morrigan/_version.py (auto-generated by setuptools-scm)
|
|
86
|
+
src/morrigan/constants.py (pure constants, no derivation)
|
|
87
|
+
src/morrigan/sort_planet.py (dead-planet removal and sorting, pure bookkeeping)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### The four invariant families, in Morrigan terms
|
|
91
|
+
|
|
92
|
+
1. **Conservation**
|
|
93
|
+
- Merger mass closure: `M_merged_after = M_target_before + M_impactor`, exactly. Merging is perfect and the model sheds nothing, so this is an equality, not a bound.
|
|
94
|
+
- Chain continuity: successive impacts on one body satisfy `M_target_before(n+1) == M_merged_after(n)` exactly; a body only ever grows, and nothing removes mass between impacts.
|
|
95
|
+
2. **Positivity / boundedness**
|
|
96
|
+
- Masses, radii, densities, semi-major axes strictly positive; eccentricities in `[0, 1)`.
|
|
97
|
+
- `b = sin(beta)` in `[0, 1]`.
|
|
98
|
+
- `v_impact >= v_esc` (the contact speed carries the mutual escape speed as a floor).
|
|
99
|
+
- Timeline times strictly increasing; system planet count non-increasing.
|
|
100
|
+
3. **Monotonicity or symmetry**
|
|
101
|
+
- Kepler period scaling `P propto a^(3/2)` at fixed stellar mass.
|
|
102
|
+
- Hill radius increasing with planet mass and semi-major axis.
|
|
103
|
+
- Escape eccentricity symmetric under swapping the two bodies.
|
|
104
|
+
4. **Pinned numeric value with a discrimination guard**: Section 2. Acceptable as the sole invariant when a closed form or a published value is the contract.
|
|
105
|
+
|
|
106
|
+
### Validation certification markers
|
|
107
|
+
|
|
108
|
+
- **`@pytest.mark.physics_invariant`**: the test asserts at least one of the four families. Tag every qualifying test in a physics-source test file.
|
|
109
|
+
- **`@pytest.mark.reference_pinned`**: the test pins behavior against a **published benchmark** (Kimura et al. 2025 statistics and closed forms), an **analytical limit** (Kepler's third law, the mutual-Hill-radius closed form, Laplace-Lagrange two-planet eigenfrequencies), or a **cross-implementation cross-check**.
|
|
110
|
+
- **Per-source-file**: each of the seven physics files needs at least one `reference_pinned` test in `tests/test_<file>.py`, recorded in `docs/Validation/<file>.md`.
|
|
111
|
+
- **Status report**: `python tools/check_test_quality.py --reference-pinned-status` prints the punch list.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 4. Determinism and the global random state
|
|
116
|
+
|
|
117
|
+
Morrigan's Monte Carlo layer (`rayleigh` draws, crossing-time sampling, scattering outcomes) uses numpy's **global** random state, seeded once by `run_system(seed=...)`. This makes seeding discipline the single most important determinism rule in the repo:
|
|
118
|
+
|
|
119
|
+
- **Every test that triggers any random draw seeds explicitly** and states the seed in its docstring. Either `np.random.seed(<n>)` immediately before the call, or a seeded `run_system(...)`.
|
|
120
|
+
- **A determinism pin exists and must stay**: two `run_system` calls with identical config and seed produce identical timelines, field for field. Any new stochastic code path must keep this test green.
|
|
121
|
+
- **Never assert on an unseeded draw.** Never rely on test ordering for random state: pytest may reorder, parallelise, or subset.
|
|
122
|
+
- **Ensemble statistics** (integration tier) sweep an explicit, fixed list of seeds, so the ensemble is reproducible and its size is visible in the test body.
|
|
123
|
+
- Library code must not reseed the global state outside `run_system`'s entry (a hidden `np.random.seed` in a helper would silently correlate ensemble members; this is also a review-gate item in `morrigan-code-review.md`).
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 5. Mocking discipline
|
|
128
|
+
|
|
129
|
+
- Default to `unittest.mock` for external calls in unit tests. Morrigan has few: file I/O on the TOML settings path is the main one.
|
|
130
|
+
- The Monte Carlo internals are NOT mocked in unit tests; they are seeded. A seeded draw is deterministic and real, which beats a mock that hides distributional bugs.
|
|
131
|
+
- A mocked function must return physically plausible values.
|
|
132
|
+
- NEVER mock the function under test.
|
|
133
|
+
- Smoke and integration tiers run the real full system.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 6. Optional-dependency imports
|
|
138
|
+
|
|
139
|
+
Any test importing an optional dependency MUST call `pytest.importorskip` at module top:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import pytest
|
|
143
|
+
|
|
144
|
+
hypothesis = pytest.importorskip('hypothesis')
|
|
145
|
+
pytest.importorskip('zephyrus.collision')
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
No test currently imports an optional dependency, and neither `hypothesis` nor
|
|
149
|
+
`zephyrus` is installed by any extra. The linter still recognises both in its
|
|
150
|
+
`OPTIONAL_DEPS` list, so the rule fires the moment one is reintroduced; add the
|
|
151
|
+
package to the `develop` extra in the same change.
|
|
152
|
+
|
|
153
|
+
`pylaplace` is a hard dependency and is imported unguarded.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 7. Marker discipline and timeouts
|
|
158
|
+
|
|
159
|
+
Every test file MUST begin with:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
import pytest
|
|
163
|
+
|
|
164
|
+
pytestmark = [pytest.mark.<tier>, pytest.mark.timeout(<budget>)]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Budgets: `unit` -> `timeout(30)` (target < 100 ms per test), `smoke` -> `timeout(60)` (target < 30 s), `integration` -> `timeout(300)`, `slow` -> `timeout(3600)`.
|
|
168
|
+
|
|
169
|
+
PR CI runs `pytest -m "(unit or smoke) and not skip and not slow and not integration"`; the nightly runs `(integration or slow) and not skip`. Per-function markers are additive, not a replacement. The seed-ensemble statistics carry `integration` and run nightly; a full-system single run with a small system fits `unit` or `smoke` depending on wall time.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 8. Float and numerical comparison
|
|
174
|
+
|
|
175
|
+
- NEVER `==` for floats; `pytest.approx` or `np.testing.assert_allclose`.
|
|
176
|
+
- State the tolerance rationale when non-obvious ("rel=0.2 because the paper quotes a 20 percent scatter against N-body").
|
|
177
|
+
- Closed-form algebra pins at `rel=1e-12`; Monte Carlo ensemble statistics at the anchor's stated scatter.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 9. Voice rule for test artifacts
|
|
182
|
+
|
|
183
|
+
The repo-wide voice rule (zero AI-process disclosure in any public artifact) applies to test code with the same strictness as to source. In scope: test-skip reasons, test-file and test-function docstrings, test names, parametrize ids, log-capture assertions, commit messages on test-touching commits, PR titles and bodies, CI job and step names, inline `src/morrigan/**` comments, shipped log strings. Out of scope: this file, `morrigan-code-review.md`, `copilot-instructions.md`.
|
|
184
|
+
|
|
185
|
+
Banned inside in-scope artifacts: "audit", "review pass", "adversarial review", AI-roadmap labels, `claude-config/...` paths, "Generated with Claude", AI-tool names, em-dashes, en-dashes (except bibliographic page ranges), process meta-commentary. Write the OUTCOME, never the PROCESS. First-person voice.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 10. Fixture and parameter conventions
|
|
190
|
+
|
|
191
|
+
- SI units internally (kg, m, s); the config boundary uses Earth masses, AU, and degrees, converted once in `driver.py`. Tests state which side of the boundary their inputs live on.
|
|
192
|
+
- Use `@pytest.mark.parametrize` for multiple physical regimes; ids read as scenarios, not tuples.
|
|
193
|
+
- Seeds per Section 4. Hypothesis tests use `@settings(derandomize=True)`.
|
|
194
|
+
- Use `tmp_path` for temporary files (the TOML settings path).
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 11. Documentation per test
|
|
199
|
+
|
|
200
|
+
- **File-level docstring**: names the source under test and the invariants exercised. Required.
|
|
201
|
+
- **Function-level docstring**: physical scenario or contract clause, plus the seed when stochastic. Required (lint-enforced).
|
|
202
|
+
- **Inline comments**: why this input range ("masses spanning a decade so the `a^(3/2)` and Hill `m^(1/3)` scalings are resolved above tolerance").
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 12. Naming
|
|
207
|
+
|
|
208
|
+
- Test names describe behavior: `test_kepler_period_scales_as_a_to_three_halves`, NOT `test_kepler_period`.
|
|
209
|
+
- Test file names mirror source 1:1. Documented exception: `tests/test_ensemble_statistics.py`, the cross-cutting seed-ensemble statistics exercising the whole model through `run_system` (integration tier).
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 13. Independent review trigger
|
|
214
|
+
|
|
215
|
+
A PR that adds or substantially modifies > 50 lines of test code across all its commits triggers an independent review pass before merge. PR-level denominator: `git diff origin/main...HEAD -- 'tests/**'`. The reviewer cites the anti-happy-path rule, the discrimination-guard requirement (including the argument-order rule from Section 2), the physics-invariant tier, and the seeding discipline from Section 4.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 14. Tooling
|
|
220
|
+
|
|
221
|
+
- `bash tools/validate_test_structure.sh` -- marker presence and file naming.
|
|
222
|
+
- `python tools/check_test_quality.py --check` -- AST scan, blocking on PR.
|
|
223
|
+
- `python tools/check_test_quality.py --baseline` -- regenerate the floor after a deliberate sweep.
|
|
224
|
+
- `python tools/check_test_quality.py --reference-pinned-status` -- punch list.
|
|
225
|
+
- `python tools/update_coverage_threshold.py` -- one-way ratchet, capped at 90.
|
|
226
|
+
- `python tools/generate_test_badges.py --out badge_payload/` -- regenerate the test-count badge JSON. Published from `main` to the `badges` branch; asserts that the tier counts partition the total.
|
|
227
|
+
- `ruff check src/ tests/ tools/` and `ruff format src/ tests/ tools/`.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 15. Failure modes to recognize on review
|
|
232
|
+
|
|
233
|
+
Real patterns from this codebase's history. The lint script catches some; reviewers catch the rest.
|
|
234
|
+
|
|
235
|
+
| Pattern | Example | Why it slipped | Fix |
|
|
236
|
+
|---|---|---|---|
|
|
237
|
+
| **Wrong mass denominator in a fitted ratio** | The loss law evaluated `M_i / M_t` where the fit prescribes `M_i / (M_i + M_t)`; the error is invisible at small mass ratios and ~11-20 percent at the ratios this model produces | The docstring wrote the correct formula while the code evaluated the wrong one; no asymmetric-pair pin existed | Pin an asymmetric pair with the wrong-denominator value in the guard |
|
|
238
|
+
| **`exp` vs `10**` in a fitted timescale** | A crossing-time fit published as a base-10 power law evaluated with `np.exp` | Both give "a big number"; no absolute pin at a tabulated point | Pin one tabulated point absolutely; guard with the wrong-base value |
|
|
239
|
+
| **Wrong pair index in a mass product** | A stirring term used `Mp[0]*Mp[2]` where the interacting pair was `[0, 1]` | Symmetric test systems (equal masses) hide index errors | Unequal masses across the system in at least one test |
|
|
240
|
+
| **Unseeded draw in a test** | A test asserting on stochastic output without seeding | Passed locally by luck of import order | Seed explicitly per Section 4; state the seed in the docstring |
|
|
241
|
+
| **Difference-based argument-order guard** | `abs(f(a) - f(b)) > delta` claimed to pin the argument mapping | A permuted dispatch permutes the values and preserves the difference | Absolute pins on both sides plus a direction assertion |
|
|
242
|
+
| **Silent skip in helper** | `if actual is None: continue` masking broken introspection | Helper hides a real failure as a no-op | Hard assertion with a message |
|
|
243
|
+
|
|
244
|
+
When you spot a new variant, add it here.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 16. Sister rules (cross-link)
|
|
249
|
+
|
|
250
|
+
- `.github/copilot-instructions.md` "Testing Standards" -- the high-level summary.
|
|
251
|
+
- `.github/.claude/rules/morrigan-code-review.md` -- the review-pass gate and the domain-aware source-review checks (orbital bounds, unit boundaries, the PROTEUS record-schema contract, global-RNG mutation).
|
|
252
|
+
|
|
253
|
+
Any change to the rule set: update both files in the same commit and call out the cross-reference in the commit body.
|