Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ on:
branches:
- master
pull_request:
branches:
- master

permissions:
contents: read
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ on:
branches:
- master
pull_request:
branches:
- master

jobs:
pytest-shard:
Expand Down Expand Up @@ -54,7 +52,7 @@ jobs:
- name: Require typed graph capabilities
run: python -c "from populace_dynamics.graph._compat import require_graph; require_graph()"
- name: Run graph and existing mortality regressions
run: pytest -q tests/test_graph_mortality.py tests/test_m6_engine_refit.py tests/test_m6_engine_steps.py
run: pytest -q tests/test_graph_mortality.py tests/test_graph_mortality_trajectory.py tests/test_m6_engine_refit.py tests/test_m6_engine_steps.py

# Fan-in jobs keeping the branch-protection context names
# ("pytest (3.11)" / "pytest (3.13)") stable across the shard split.
Expand Down
92 changes: 90 additions & 2 deletions docs/population-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
The optional `populace_dynamics.graph` package fits the existing M6 mortality
model on a historical synthetic panel, applies the fitted artifact to a
separate starting population, and adds surviving observations for the next
year. It produces an engineering report and a content-addressed execution
year, or repeats those transitions through an explicit end year. It produces
an engineering report and a content-addressed execution
manifest. It does not change the existing projection loop, candidate
registries, scientific gates, or committed evidence.

Expand Down Expand Up @@ -116,13 +117,95 @@ leaving fitting, application, draws, and accounting unchanged.

Household accounting is explicitly unsupported and refused by the Python
entry point. Household weight sharing, marriage, births, immigration,
alignment replay, repeated years, and the full M6 loop remain later work.
alignment replay, and the full M6 loop remain later work.
No certified data release or scientific candidate is produced by this graph.

## Annual trajectories

The optional `run_mortality_trajectory` Python entry point builds one graph
with a single mortality fit and separate application, expansion, age-ownership,
snapshot, and evaluation nodes for each year. It uses the same exact
graph/Frame pin as the one-year example. The fit cutoff stays fixed while the
application year
advances. This extends execution of the existing age/sex law; it does not add
a calendar-year mortality improvement model or establish long-horizon validity.

Each application reads only the preceding period's observations. A typed
transition artifact binds each probability and survival decision to its
person and observation identities. EXPAND appends survivor observations with
lineage to that preceding period. Earlier ages, memberships, and trajectory
weights stay unchanged. The mass receipt covers every historical period,
not just the newest pair. After extinction, later years contain no at-risk
people and add no orphan period groups.

Declare one aggregate synthetic holdout for each application year. For example:

```python
import json
from pathlib import Path

from populace_dynamics.graph import run_mortality_trajectory
from populace_dynamics.graph.synthetic import write_synthetic_inputs

root = Path("mortality-trajectory")
sources = write_synthetic_inputs(root / "inputs")
sources.pop("holdout")
holdouts = {}
for year in range(2015, 2018):
path = root / "inputs" / f"aggregate-{year}.json"
path.write_text(json.dumps({
"scope": "synthetic_engineering",
"year": year,
"expected_death_rate": 0.2,
"fixture_max_abs_death_rate_gap": 0.25,
}))
holdouts[year] = path

result = run_mortality_trajectory(
**sources, holdouts=holdouts, end_year=2017, output_dir=root,
)
print(result.report)
```

These small aggregate fixtures are deliberately artificial, with input
tolerances used only for engineering tests. They contain no empirical
acceptance targets. Each evaluation reads a typed snapshot of the actual
materialized population on a separate population version. This keeps the
evaluation outside the next expansion's dependencies under the pinned core.
An annual evaluation depends on its own holdout; changing
or failing that evaluation does not alter later simulation. Extending the
horizon reuses the existing fit and annual nodes in the same verified store.
Changing experiment, replicate, or seed changes application identities while
reusing the fit. All sources remain declared and content-hashed by the executor,
including evaluation sources whose kernels are subsequently guarded.

The output directory contains `trajectory.csv`, `model.json`, `report.json`,
and `manifest.json`. The trajectory includes the initial period and every
completed survivor period, with person identity, age, year, and weight.
Annual reports keep expected and generated deaths, survivor counts, and
period mass separate from fixture and engineering verdicts.

Application ages outside the fitted bands must fail explicitly. In particular,
a survivor aged 120 can be advanced to 121, but cannot enter another mortality
draw under a law with support ending at 120. The graph does not silently assign
such people a zero death probability. A typed failure outcome guards later
applications and expansions, preserving the latest valid population and the
original diagnostic. Blocked application status is reported separately from
the core's execution/cache receipts: this pinned executor still runs guarded
nodes and does not provide native `unreached` receipts. A failed evaluation
does not propagate this application block.

Snapshots include the full materialized history, so their storage grows with
both population size and horizon. This synthetic integration has not been
benchmarked for national-scale projection. Root creation, fitting, store
corruption, and unexpected structural failures can still abort execution;
the retained diagnostic path covers application and evaluation failures.

## Tests

```sh
python -m pytest -q tests/test_graph_mortality.py \
tests/test_graph_mortality_trajectory.py \
tests/test_m6_engine_refit.py tests/test_m6_engine_steps.py
```

Expand All @@ -137,3 +220,8 @@ the optional runtime cases when the required core capabilities are unavailable;
the JSON and dependency
boundary tests still run. Importing `populace_dynamics.graph` remains safe
under Python 3.10–3.12.

The annual tests independently repeat the existing fit, mortality, ageing,
and keyed-draw operations; compare every retained person-period and weighted
diagnostic; and exercise horizon reuse, stream changes, holdout isolation,
extinction, and retained support-failure evidence.
1 change: 1 addition & 0 deletions scripts/first_estimates_birth_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
Path("src/populace_dynamics/graph/model.py"),
Path("src/populace_dynamics/graph/runtime.py"),
Path("src/populace_dynamics/graph/synthetic.py"),
Path("src/populace_dynamics/graph/trajectory.py"),
)
POST_REVIEW_SHARED_SOURCE_BLOBS = {
Path(
Expand Down
12 changes: 11 additions & 1 deletion src/populace_dynamics/graph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ def run_mortality_graph(**kwargs):
return run(**kwargs)


__all__ = ["run_mortality_graph"]
def run_mortality_trajectory(**kwargs):
"""Run the existing mortality/ageing steps across annual graph periods."""
from ._compat import require_graph

require_graph()
from .trajectory import run_mortality_trajectory as run

return run(**kwargs)


__all__ = ["run_mortality_graph", "run_mortality_trajectory"]
Loading