starsim-data 2.0.0__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.
- starsim_data-2.0.0/.claude/skills/add-dataset/SKILL.md +81 -0
- starsim_data-2.0.0/.github/workflows/docs.yml +110 -0
- starsim_data-2.0.0/.github/workflows/publish.yml +45 -0
- starsim_data-2.0.0/.github/workflows/tests.yml +87 -0
- starsim_data-2.0.0/.gitignore +110 -0
- starsim_data-2.0.0/CHANGELOG.md +52 -0
- starsim_data-2.0.0/CLAUDE.md +108 -0
- starsim_data-2.0.0/CONTRIBUTING.md +47 -0
- starsim_data-2.0.0/LICENSE +35 -0
- starsim_data-2.0.0/MANIFEST.in +5 -0
- starsim_data-2.0.0/NOTICE.md +70 -0
- starsim_data-2.0.0/PKG-INFO +265 -0
- starsim_data-2.0.0/README.md +227 -0
- starsim_data-2.0.0/docs/01-getting-started.qmd +158 -0
- starsim_data-2.0.0/docs/02-tiers.qmd +83 -0
- starsim_data-2.0.0/docs/03-datasets.qmd +269 -0
- starsim_data-2.0.0/docs/04-locations.qmd +146 -0
- starsim_data-2.0.0/docs/05-spatial.qmd +217 -0
- starsim_data-2.0.0/docs/06-datasources.qmd +188 -0
- starsim_data-2.0.0/docs/07-related-projects.qmd +94 -0
- starsim_data-2.0.0/docs/08-architecture.qmd +182 -0
- starsim_data-2.0.0/docs/09-provenance.qmd +116 -0
- starsim_data-2.0.0/docs/10-building-the-data.qmd +128 -0
- starsim_data-2.0.0/docs/assets/favicon.ico +0 -0
- starsim_data-2.0.0/docs/assets/icon-dark.png +0 -0
- starsim_data-2.0.0/docs/assets/icon.png +0 -0
- starsim_data-2.0.0/docs/assets/logo-dark.png +0 -0
- starsim_data-2.0.0/docs/assets/logo.png +0 -0
- starsim_data-2.0.0/docs/assets/starsim-theme.html +71 -0
- starsim_data-2.0.0/great-docs.yml +83 -0
- starsim_data-2.0.0/licenses/LICENSE_EPYDEMIX +674 -0
- starsim_data-2.0.0/licenses/LICENSE_MISTRY2021 +201 -0
- starsim_data-2.0.0/licenses/LICENSE_PREM2017 +674 -0
- starsim_data-2.0.0/licenses/LICENSE_PREM2021 +674 -0
- starsim_data-2.0.0/plans/laser-init-merge.md +273 -0
- starsim_data-2.0.0/pyproject.toml +84 -0
- starsim_data-2.0.0/scripts/README.md +87 -0
- starsim_data-2.0.0/scripts/build_demographics.py +243 -0
- starsim_data-2.0.0/scripts/build_hdf5.py +297 -0
- starsim_data-2.0.0/scripts/build_locations.py +215 -0
- starsim_data-2.0.0/scripts/build_nodes.py +383 -0
- starsim_data-2.0.0/scripts/download_epydemix.py +89 -0
- starsim_data-2.0.0/scripts/download_wpp.py +160 -0
- starsim_data-2.0.0/scripts/provenance.py +49 -0
- starsim_data-2.0.0/scripts/publish.py +134 -0
- starsim_data-2.0.0/setup.cfg +4 -0
- starsim_data-2.0.0/starsim_data/__init__.py +57 -0
- starsim_data-2.0.0/starsim_data/cache.py +220 -0
- starsim_data-2.0.0/starsim_data/data/README.md +41 -0
- starsim_data-2.0.0/starsim_data/data/adjacency.h5 +0 -0
- starsim_data-2.0.0/starsim_data/data/demographics-2024.h5 +0 -0
- starsim_data-2.0.0/starsim_data/data/fetched.json +416 -0
- starsim_data-2.0.0/starsim_data/data/locations.csv +188 -0
- starsim_data-2.0.0/starsim_data/data/nodes.h5 +0 -0
- starsim_data-2.0.0/starsim_data/data/prem/prem2017.h5 +0 -0
- starsim_data-2.0.0/starsim_data/data/prem2021.h5 +0 -0
- starsim_data-2.0.0/starsim_data/fetching.py +422 -0
- starsim_data-2.0.0/starsim_data/loaders.py +289 -0
- starsim_data-2.0.0/starsim_data/locations.py +280 -0
- starsim_data-2.0.0/starsim_data/nodes.py +223 -0
- starsim_data-2.0.0/starsim_data/plotting.py +267 -0
- starsim_data-2.0.0/starsim_data/rasters.py +390 -0
- starsim_data-2.0.0/starsim_data/registry.py +366 -0
- starsim_data-2.0.0/starsim_data/settings.py +108 -0
- starsim_data-2.0.0/starsim_data/shapes.py +330 -0
- starsim_data-2.0.0/starsim_data/version.py +9 -0
- starsim_data-2.0.0/starsim_data.egg-info/PKG-INFO +265 -0
- starsim_data-2.0.0/starsim_data.egg-info/SOURCES.txt +84 -0
- starsim_data-2.0.0/starsim_data.egg-info/dependency_links.txt +1 -0
- starsim_data-2.0.0/starsim_data.egg-info/requires.txt +15 -0
- starsim_data-2.0.0/starsim_data.egg-info/top_level.txt +1 -0
- starsim_data-2.0.0/tests/README.md +34 -0
- starsim_data-2.0.0/tests/conftest.py +59 -0
- starsim_data-2.0.0/tests/run_tests +10 -0
- starsim_data-2.0.0/tests/test_cache.py +153 -0
- starsim_data-2.0.0/tests/test_demographics.py +98 -0
- starsim_data-2.0.0/tests/test_demographics_data.py +77 -0
- starsim_data-2.0.0/tests/test_docs.py +124 -0
- starsim_data-2.0.0/tests/test_fetching.py +213 -0
- starsim_data-2.0.0/tests/test_load.py +201 -0
- starsim_data-2.0.0/tests/test_locations.py +169 -0
- starsim_data-2.0.0/tests/test_nodes.py +197 -0
- starsim_data-2.0.0/tests/test_plotting.py +129 -0
- starsim_data-2.0.0/tests/test_provenance.py +111 -0
- starsim_data-2.0.0/tests/test_registry.py +116 -0
- starsim_data-2.0.0/tests/test_spatial.py +187 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-dataset
|
|
3
|
+
description: Add a new data source to starsim_data — another country's subnational data, a new contact source, populations, commuting or mobility flows, or any other dataset — by writing its download script and build step and wiring it into the registry, provenance, docs and tests. Use when asked to add, import or package data for a new location or source (e.g. "add Germany's districts", "import Canada's provinces from epydemix", "add the commuter matrix").
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a dataset to starsim_data
|
|
7
|
+
|
|
8
|
+
A dataset here is three things: a **download script** that copies the upstream data into `raw_data/` as readable CSVs, a **build step** that turns those CSVs into a self-describing HDF5 store, and the **wiring** (license, docs, tests) that the repo's invariants require. The US states (`mistry2021_us`, `population_us`) are the worked example to copy: `scripts/download_epydemix.py`, and `build_population_us()` and the tables at the top of `scripts/build_hdf5.py`.
|
|
9
|
+
|
|
10
|
+
Read `CLAUDE.md` first: the tier invariant, the registry, and "Keeping the docs in step" all apply.
|
|
11
|
+
|
|
12
|
+
## 1. Settle these before writing code
|
|
13
|
+
|
|
14
|
+
Ask Cliff about any of these you can't answer from the source itself. They decide everything after.
|
|
15
|
+
|
|
16
|
+
- **License.** Find the exact license of the data (not of the repo it sits in: epydemix-data is GPL-3.0 as a whole, but its Census numbers are public domain). If it can't be redistributed, it can't be a store here; it can only be fetched from upstream at use time, as GADM is (`redistributable=False`). Record the license as it will appear in `NOTICE.md`.
|
|
17
|
+
- **Tier.** Bundled (tier 0, in the wheel) only if it is small and most users need it; Cliff wants the wheel's size unchanged unless he agrees otherwise. Everything else is a fetched store (tier 1) in `fetched/`.
|
|
18
|
+
- **Location keys.** A subnational unit is keyed `<Country>:<Unit>`, and the key must be the unit's `dot_name` in the node table, so that `resolve()`, `load_nodes()` and the new data agree. Check with `ssd.load_nodes(country, level=1).dot_name`. Only levels 0 and 1 are bundled. For level 2 and finer, get the names from `ssd.fetch_nodes(country, level=2)` (needs `[spatial]`), and if the source's names don't match them (e.g. `Landkreis Harburg` against `Harburg`), build a crosswalk from codes rather than fuzzy-matching names. Commit the crosswalk as a CSV in `raw_data/`.
|
|
19
|
+
- **Layout.** Reuse one if it fits: `matrix` (square, by age), `population` and `mortality` (a year × sex × age array), `crude` (one value per year), `nodes`. A new shape, such as origin–destination flows, needs a new shaper in `starsim_data/loaders.py`, registered with `ssd.registry.register_layout()` and documented in `docs/08-architecture.qmd`. Return the same columns an existing dataset returns wherever the meaning is the same: `population_us` uses `population`'s `Time, Sex, AgeGrp, Value`, with `Sex='Both sexes'` because the source doesn't split by sex.
|
|
20
|
+
- **Name.** Lowercase, source then scope: `mistry2021_us`, `population_us`. Don't put new locations into a store that is already published (for example, adding states to `mistry2021`): it changes the store's bytes, and its pinned download, for everyone.
|
|
21
|
+
- **Upstream version.** Pin a commit, release or vintage, so that rerunning the download gives the same files.
|
|
22
|
+
|
|
23
|
+
## 2. Download script: `scripts/download_<source>.py`
|
|
24
|
+
|
|
25
|
+
Model it on `scripts/download_epydemix.py`:
|
|
26
|
+
|
|
27
|
+
- A module docstring saying what it downloads, from where, into which files, and the command to run next.
|
|
28
|
+
- The upstream version pinned in a constant (`COMMIT = ...`), and URLs built from it.
|
|
29
|
+
- Output to `raw_data/`, as plain CSVs a person can read and diff: contact matrices as `raw_data/contact_matrices/<Country>__<Unit>/<source>_contacts_<kind>.csv` (headerless, square; the `__` stands for `:`, which isn't legal in a folder name everywhere), and anything else as one tidy CSV keyed by `location` (e.g. `raw_data/population_us.csv`: `location, age, population`).
|
|
30
|
+
- Check every location against the node table before writing anything, and raise, listing the ones that don't match.
|
|
31
|
+
- Any token goes in `scripts/.env` (git-ignored), as `download_wpp.py` does, and never in the code.
|
|
32
|
+
- Read every CSV with `keep_default_na=False` or the `csv` module (Namibia's ISO-2 code is `NA`).
|
|
33
|
+
- Keep downloading separate from building, so that the build and its `--check` run offline from the committed CSVs.
|
|
34
|
+
|
|
35
|
+
Tell Cliff how big the download and the raw CSVs will be before running it. Raw CSVs are committed, so 30 MB of them is 30 MB in git history.
|
|
36
|
+
|
|
37
|
+
## 3. Build step
|
|
38
|
+
|
|
39
|
+
All fetched stores are listed in `scripts/build_hdf5.py`, which writes the manifest `starsim_data/data/fetched.json` from them. Add the new store there:
|
|
40
|
+
|
|
41
|
+
- Add `name` to `FETCHED_SOURCES`, and bump `RELEASE` to a new `data-*` tag: the new store is downloaded from that GitHub release, which Cliff creates with `scripts/publish.py` once the change is on `main`.
|
|
42
|
+
- `SOURCES[name]`: `source`, `source_url`, `via` (if it came through another project), `license`, `citation`, `note`. This is written into the store as its provenance.
|
|
43
|
+
- `DESCRIPTIONS[name]`: one line for `list_datasets()`.
|
|
44
|
+
- Contact matrices need nothing more than their CSVs and an `AGE_WIDTH` entry: sources are discovered from the file names.
|
|
45
|
+
- Anything else needs a `build_<name>(check=False)` function, like `build_population_us()`. It writes one dataset per location key and a root `spec` attribute for the layout, for example `dict(layout='population', years=[...], sexes=[...], ages=[...], age_col='AgeGrp', value_col='Value')`, plus `provenance` from `stamp()`. Under `check`, it reads each value back and compares it with its CSV. Call it from `main()` when its name is passed to `--fetched`.
|
|
46
|
+
- A bundled store goes into `starsim_data/data/` instead, needs an entry in `registry.DESCRIPTIONS`, and is discovered from its file. Bundling something new is Cliff's call (see Tier).
|
|
47
|
+
|
|
48
|
+
Build only the new store: `python scripts/build_hdf5.py --check --fetched <name>`. Never rebuild a published store, since that changes its bytes and breaks its pinned download. Every run also rewrites the bundled `prem2021.h5` and `prem/prem2017.h5`, with a new build time and the same data, so restore them afterwards with `git restore starsim_data/data/prem2021.h5 starsim_data/data/prem/prem2017.h5` unless their data really changed.
|
|
49
|
+
|
|
50
|
+
Levels are worked out from the keys (`United_States:Massachusetts` is level 1). `fetch()` reads any fetched store with no further code, and `load()` points to it from other datasets of the same layout ("it is in fetch(...)").
|
|
51
|
+
|
|
52
|
+
## 4. Wiring
|
|
53
|
+
|
|
54
|
+
Everything below is required: the tests check some of it, and none of it updates itself.
|
|
55
|
+
|
|
56
|
+
- `NOTICE.md`: a section for a new source (or a bullet under the existing one for the same study), and the list of fetched stores in its introduction.
|
|
57
|
+
- `tests/test_provenance.py::test_licenses`: the new dataset and its license.
|
|
58
|
+
- Tests: one compact test of what's new, like `tests/test_load.py::test_us_states` (shape, schema, a sanity total, and the error that points to it). The offline suite reads fetched stores from `fetched/` through `tests/conftest.py`, so no network is needed. Update `tests/README.md` if a file's coverage changes.
|
|
59
|
+
- Docs: `docs/03-datasets.qmd` (the schema and an example; downloading examples must be static ```` ```python ```` blocks), `docs/06-datasources.qmd` (coverage, license, limitations), `docs/02-tiers.qmd` (the tier table), `docs/10-building-the-data.qmd`, and `docs/04-locations.qmd` if naming changes. Update the comparison table in `docs/07-related-projects.qmd` if coverage changes.
|
|
60
|
+
- `README.md` (the source table), `CHANGELOG.md` (one line), `raw_data/README.md`, `scripts/README.md`, and the commands in `CLAUDE.md`.
|
|
61
|
+
|
|
62
|
+
## 5. Check
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python scripts/build_hdf5.py --check
|
|
66
|
+
pytest -m "not network"
|
|
67
|
+
pytest tests/test_docs.py
|
|
68
|
+
great-docs build # if Quarto is available
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then try it the way a user would, including the errors: a misspelled unit, a unit the dataset lacks, and an argument that doesn't apply (it must be rejected, not ignored). Don't commit; leave everything for Cliff, and remind Cliff to create the new data release once the change is on `main`.
|
|
72
|
+
|
|
73
|
+
## Worked sketch: Germany's districts (not built yet)
|
|
74
|
+
|
|
75
|
+
Requested for MEmilio users: the roughly 400 districts (Kreise), with populations by age group, centroids, and the commuter matrix between them. Verify every URL, table number and license below before relying on it.
|
|
76
|
+
|
|
77
|
+
- **Populations:** Destatis GENESIS-Online, table 12411-0017 (population by district and age group) or its successor. The API needs an account; put the token in `scripts/.env`. The license is probably Datenlizenz Deutschland – Namensnennung 2.0 (`dl-de/by-2-0`), which allows redistribution with attribution. Use the `population` layout with Germany's age groups, keyed `Germany:<Kreis>`.
|
|
78
|
+
- **Commuter flows:** the Federal Employment Agency's commuter statistics (Pendlerstatistik), one spreadsheet per state. MEmilio's `getCommuterMobility` (in `memilio.epidata`) shows the URLs and how to parse them. Check the agency's terms of reuse. This needs a new layout, e.g. `flows`, returning `origin, destination, value` for a location (`Germany`), from a sparse array stored as three columns.
|
|
79
|
+
- **Keys:** districts are identified by their AGS code (Amtlicher Gemeindeschlüssel). Get the level-2 `dot_name`s from `fetch_nodes('Germany', level=2)` and crosswalk AGS to `dot_name`, using codes where the boundary source has them and a committed crosswalk CSV where it doesn't.
|
|
80
|
+
- **Places:** `load_nodes('Germany', level=2)` needs level 2 in the bundled node table, which is a `scripts/build_nodes.py` change and a size decision for Cliff. Otherwise `fetch_nodes()` already gives centroids at level 2.
|
|
81
|
+
- **Size:** about 400 × 400 sparse flows and 400 × (6–21) age groups, so a fetched store of well under 1 MB.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: CI Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build-docs:
|
|
11
|
+
name: "Build Docs"
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0 # Full history for accurate page timestamps
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v6
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
|
|
22
|
+
- name: Install package and dependencies
|
|
23
|
+
run: |
|
|
24
|
+
python -m pip install --upgrade pip
|
|
25
|
+
python -m pip install -e ".[dev,docs]"
|
|
26
|
+
python -m pip install great-docs
|
|
27
|
+
|
|
28
|
+
- name: Set up Quarto
|
|
29
|
+
uses: quarto-dev/quarto-actions/setup@v2
|
|
30
|
+
|
|
31
|
+
- name: Build docs
|
|
32
|
+
run: great-docs build
|
|
33
|
+
|
|
34
|
+
- name: Save docs artifact
|
|
35
|
+
uses: actions/upload-artifact@v7
|
|
36
|
+
with:
|
|
37
|
+
name: docs-html
|
|
38
|
+
path: great-docs/_site
|
|
39
|
+
include-hidden-files: true
|
|
40
|
+
|
|
41
|
+
- name: Upload build timings
|
|
42
|
+
uses: actions/upload-artifact@v7
|
|
43
|
+
with:
|
|
44
|
+
name: build-timings
|
|
45
|
+
path: great-docs/_site/build-timings.json
|
|
46
|
+
|
|
47
|
+
publish-docs:
|
|
48
|
+
name: "Publish Docs"
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
needs: "build-docs"
|
|
51
|
+
if: github.ref == 'refs/heads/main'
|
|
52
|
+
permissions:
|
|
53
|
+
pages: write
|
|
54
|
+
id-token: write
|
|
55
|
+
environment:
|
|
56
|
+
name: github-pages
|
|
57
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/download-artifact@v7
|
|
60
|
+
with:
|
|
61
|
+
name: docs-html
|
|
62
|
+
path: great-docs/_site
|
|
63
|
+
|
|
64
|
+
- name: Upload Pages artifact
|
|
65
|
+
uses: actions/upload-pages-artifact@v5
|
|
66
|
+
with:
|
|
67
|
+
path: great-docs/_site
|
|
68
|
+
include-hidden-files: true
|
|
69
|
+
|
|
70
|
+
- name: Deploy to GitHub Pages
|
|
71
|
+
id: deployment
|
|
72
|
+
uses: actions/deploy-pages@v5
|
|
73
|
+
|
|
74
|
+
preview-docs:
|
|
75
|
+
name: "Preview Docs"
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
needs: "build-docs"
|
|
78
|
+
if: github.event_name == 'pull_request'
|
|
79
|
+
permissions:
|
|
80
|
+
deployments: write
|
|
81
|
+
pull-requests: write
|
|
82
|
+
steps:
|
|
83
|
+
- uses: actions/download-artifact@v7
|
|
84
|
+
with:
|
|
85
|
+
name: docs-html
|
|
86
|
+
path: great-docs/_site
|
|
87
|
+
|
|
88
|
+
# Start deployment
|
|
89
|
+
- name: Configure pull release name
|
|
90
|
+
if: ${{ github.event_name == 'pull_request' }}
|
|
91
|
+
run: |
|
|
92
|
+
echo "RELEASE_NAME=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
|
93
|
+
|
|
94
|
+
- name: Configure branch release name
|
|
95
|
+
if: ${{ github.event_name != 'pull_request' }}
|
|
96
|
+
run: |
|
|
97
|
+
# use branch name, but replace slashes. E.g. feat/a -> feat-a
|
|
98
|
+
echo "RELEASE_NAME=${GITHUB_REF_NAME//\//-}" >> $GITHUB_ENV
|
|
99
|
+
|
|
100
|
+
# Deploy
|
|
101
|
+
- name: Create Github Deployment
|
|
102
|
+
uses: bobheadxi/deployments@v1
|
|
103
|
+
id: deployment
|
|
104
|
+
if: ${{ !github.event.pull_request.head.repo.fork }}
|
|
105
|
+
with:
|
|
106
|
+
step: start
|
|
107
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
108
|
+
env: ${{ env.RELEASE_NAME }}
|
|
109
|
+
ref: ${{ github.head_ref }}
|
|
110
|
+
logs: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Publishes the package to PyPI when a `v*` release is published (e.g. `v2.0.0`), by
|
|
4
|
+
# trusted publishing (no token; set up once on pypi.org). Make the release with
|
|
5
|
+
# `python scripts/publish.py <data tag>`, which also creates the data release the
|
|
6
|
+
# fetched stores are downloaded from. This refuses to publish a package whose fetched
|
|
7
|
+
# stores can't be downloaded.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
release:
|
|
11
|
+
types: [published]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
pypi:
|
|
15
|
+
name: Publish to PyPI
|
|
16
|
+
if: startsWith(github.event.release.tag_name, 'v')
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
environment: pypi
|
|
19
|
+
permissions:
|
|
20
|
+
id-token: write # trusted publishing
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout sources
|
|
23
|
+
uses: actions/checkout@v7
|
|
24
|
+
- uses: actions/setup-python@master
|
|
25
|
+
with:
|
|
26
|
+
python-version: '3.14'
|
|
27
|
+
- name: Check the version matches the tag, and the fetched stores can be downloaded
|
|
28
|
+
run: |
|
|
29
|
+
python - <<'EOF'
|
|
30
|
+
import json, pathlib, re, urllib.request
|
|
31
|
+
tag = '${{ github.event.release.tag_name }}'
|
|
32
|
+
version = re.search(r"__version__ = '(.+)'", pathlib.Path('starsim_data/version.py').read_text())[1]
|
|
33
|
+
assert tag == f'v{version}', f'Tag {tag} does not match version {version}'
|
|
34
|
+
manifest = json.loads(pathlib.Path('starsim_data/data/fetched.json').read_text())
|
|
35
|
+
for name, entry in manifest.items():
|
|
36
|
+
with urllib.request.urlopen(urllib.request.Request(entry['url'], method='HEAD')) as response:
|
|
37
|
+
assert response.status == 200, f'{name}: {entry["url"]} returned {response.status}'
|
|
38
|
+
print(f'Version {version}; {len(manifest)} fetched stores are downloadable')
|
|
39
|
+
EOF
|
|
40
|
+
- name: Build
|
|
41
|
+
run: |
|
|
42
|
+
pip install build
|
|
43
|
+
python -m build
|
|
44
|
+
- name: Publish
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: starsim_data CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
push:
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
pull_request:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
install_and_test:
|
|
13
|
+
name: Tests
|
|
14
|
+
timeout-minutes: 10
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
max-parallel: 8
|
|
18
|
+
matrix:
|
|
19
|
+
os: ['ubuntu-latest']
|
|
20
|
+
python-version: ['3.14']
|
|
21
|
+
runs-on: ${{ matrix.os }}
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout sources
|
|
24
|
+
uses: actions/checkout@v7
|
|
25
|
+
- uses: actions/setup-python@master
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
architecture: x64
|
|
29
|
+
- name: Install starsim_data
|
|
30
|
+
run: pip install -e .[dev]
|
|
31
|
+
- name: Run tests
|
|
32
|
+
# -m "not network" so the suite stays offline by default; the bundled data
|
|
33
|
+
# makes every core test hermetic, and that property is worth protecting.
|
|
34
|
+
run: pytest tests -v -m "not network" --durations=0 --junitxml=test-results.xml
|
|
35
|
+
- name: Publish test report
|
|
36
|
+
uses: mikepenz/action-junit-report@v6
|
|
37
|
+
if: always()
|
|
38
|
+
with:
|
|
39
|
+
report_paths: './test-results.xml'
|
|
40
|
+
check_name: 'Test Report (Python ${{ matrix.python-version }})'
|
|
41
|
+
|
|
42
|
+
check_data_is_reproducible:
|
|
43
|
+
name: Data rebuild is reproducible
|
|
44
|
+
timeout-minutes: 10
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- name: Checkout sources
|
|
48
|
+
uses: actions/checkout@v7
|
|
49
|
+
- uses: actions/setup-python@master
|
|
50
|
+
with:
|
|
51
|
+
python-version: '3.14'
|
|
52
|
+
architecture: x64
|
|
53
|
+
- name: Install starsim_data
|
|
54
|
+
run: pip install -e .[dev]
|
|
55
|
+
# The bundled .h5 stores are build artifacts committed to the repo, so they can
|
|
56
|
+
# drift from the raw data they claim to be built from. Rebuilding and validating
|
|
57
|
+
# here is what keeps `raw_data/` the actual source of truth.
|
|
58
|
+
- name: Rebuild contact matrices and validate against the CSVs
|
|
59
|
+
run: python scripts/build_hdf5.py --check
|
|
60
|
+
- name: Rebuild demographics and validate via the loader
|
|
61
|
+
run: python scripts/build_demographics.py --check
|
|
62
|
+
- name: Check locations.csv is up to date
|
|
63
|
+
run: python scripts/build_locations.py --check
|
|
64
|
+
- name: Tests still pass against the rebuilt data
|
|
65
|
+
run: pytest tests -q -m "not network"
|
|
66
|
+
|
|
67
|
+
# The node table is deliberately left out of the job above: rebuilding it pulls ~420 MB
|
|
68
|
+
# of boundary data from GitHub, which is too much to spend on every pull request. Run it
|
|
69
|
+
# by hand when the boundary sources change.
|
|
70
|
+
check_nodes_are_reproducible:
|
|
71
|
+
name: Node table rebuild is reproducible
|
|
72
|
+
if: github.event_name == 'workflow_dispatch'
|
|
73
|
+
timeout-minutes: 30
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- name: Checkout sources
|
|
77
|
+
uses: actions/checkout@v7
|
|
78
|
+
- uses: actions/setup-python@master
|
|
79
|
+
with:
|
|
80
|
+
python-version: '3.14'
|
|
81
|
+
architecture: x64
|
|
82
|
+
- name: Install starsim_data
|
|
83
|
+
run: pip install -e .[dev,spatial]
|
|
84
|
+
- name: Rebuild the node table and validate via the loader
|
|
85
|
+
run: python scripts/build_nodes.py --check
|
|
86
|
+
- name: Tests still pass against the rebuilt node table
|
|
87
|
+
run: pytest tests -q -m "not network"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# Installer logs
|
|
30
|
+
pip-log.txt
|
|
31
|
+
pip-delete-this-directory.txt
|
|
32
|
+
|
|
33
|
+
# Unit test / coverage reports
|
|
34
|
+
htmlcov/
|
|
35
|
+
.tox/
|
|
36
|
+
.nox/
|
|
37
|
+
.coverage
|
|
38
|
+
.coverage.*
|
|
39
|
+
.cache
|
|
40
|
+
nosetests.xml
|
|
41
|
+
coverage.xml
|
|
42
|
+
*.cover
|
|
43
|
+
*.py,cover
|
|
44
|
+
.hypothesis/
|
|
45
|
+
.pytest_cache/
|
|
46
|
+
cover/
|
|
47
|
+
|
|
48
|
+
# Environments
|
|
49
|
+
.env
|
|
50
|
+
.venv
|
|
51
|
+
env/
|
|
52
|
+
venv/
|
|
53
|
+
ENV/
|
|
54
|
+
env.bak/
|
|
55
|
+
venv.bak/
|
|
56
|
+
|
|
57
|
+
# Jupyter Notebook
|
|
58
|
+
.ipynb_checkpoints
|
|
59
|
+
|
|
60
|
+
# IPython
|
|
61
|
+
profile_default/
|
|
62
|
+
ipython_config.py
|
|
63
|
+
|
|
64
|
+
# pyenv
|
|
65
|
+
.python-version
|
|
66
|
+
|
|
67
|
+
# mypy / ruff / type checkers
|
|
68
|
+
.mypy_cache/
|
|
69
|
+
.dmypy.json
|
|
70
|
+
dmypy.json
|
|
71
|
+
.ruff_cache/
|
|
72
|
+
|
|
73
|
+
# IDEs
|
|
74
|
+
.idea/
|
|
75
|
+
.vscode/
|
|
76
|
+
*.swp
|
|
77
|
+
*.swo
|
|
78
|
+
|
|
79
|
+
# OS
|
|
80
|
+
.DS_Store
|
|
81
|
+
Thumbs.db
|
|
82
|
+
|
|
83
|
+
# Build artifacts (HDF5 data stores are generated from the CSVs; not committed)
|
|
84
|
+
*.h5
|
|
85
|
+
*.hdf5
|
|
86
|
+
|
|
87
|
+
# ...except the bundled national-tier stores, which ship inside the package.
|
|
88
|
+
# These are small (~MBs), built by scripts/build_hdf5.py, and committed so that
|
|
89
|
+
# `pip install` is self-contained. Larger subnational tiers stay uncommitted.
|
|
90
|
+
!starsim_data/data/*.h5
|
|
91
|
+
!starsim_data/data/prem/*.h5
|
|
92
|
+
|
|
93
|
+
# ...and the contact stores that are fetched on demand rather than bundled: committed
|
|
94
|
+
# here, outside the package, and downloaded by fetch() from a pinned commit.
|
|
95
|
+
!fetched/*.h5
|
|
96
|
+
|
|
97
|
+
# UN WPP demographics source. The normalized CSVs (raw_data/demographics/wpp_*.csv)
|
|
98
|
+
# and README are tracked as the demographic source of truth; only the bulky raw
|
|
99
|
+
# downloads (xlsx/gz/zip) and logs are scratch and ignored.
|
|
100
|
+
raw_data/demographics/*.xlsx
|
|
101
|
+
raw_data/demographics/*.csv.gz
|
|
102
|
+
raw_data/demographics/*.zip
|
|
103
|
+
raw_data/demographics/*.log
|
|
104
|
+
|
|
105
|
+
# great-docs build directory (ephemeral; great-docs.yml is the committed source)
|
|
106
|
+
great-docs/
|
|
107
|
+
|
|
108
|
+
# Quarto's execution cache. Deliberately not committed: letting CI execute the code cells
|
|
109
|
+
# afresh means the docs build also checks that the documented examples still run.
|
|
110
|
+
_freeze/
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `starsim_data` are recorded here. Versions follow [semantic versioning](https://semver.org); the *data* is versioned too, so a change to a bundled dataset is a release, not an amendment.
|
|
4
|
+
|
|
5
|
+
## 2.0.0 (2026-10-22)
|
|
6
|
+
|
|
7
|
+
- **Contact matrices for 187 locations by default.** `ssd.load('kenya')` returns the contact matrices of Prem et al. 2021 (`prem2021`), and for the ten locations that study left out (Japan, Australia, Haiti and seven others) those of Prem et al. 2017 (`prem2017`). Pass `which='prem2021'` for that study alone.
|
|
8
|
+
- **A smaller package.** The other contact sources, `prem2017` and `mistry2021`, are downloaded the first time they are used, e.g. `ssd.fetch('france', which='mistry2021')`, rather than shipped with the package.
|
|
9
|
+
- **Subnational populations and borders, offline.** `ssd.load_nodes('kenya', level=1)` now gives the population of every state or province (from WorldPop, 2020), and `ssd.adjacency('kenya', level=1)` says which of them share a border.
|
|
10
|
+
- **Finer levels on request.** `ssd.fetch_nodes('kenya', level=2)` builds the same table (centroids, areas, populations and borders) at any level, e.g. Kenya's 290 sub-counties.
|
|
11
|
+
- **US states.** `ssd.fetch('United States:Massachusetts', which='mistry2021_us')` gives a state's contact matrices (Mistry et al. 2021), and `which='population_us'` its population by single year of age (US Census Bureau, 2023). A subnational unit is named `'<country>:<unit>'`, as in the node table.
|
|
12
|
+
- `fetch()` takes `kind=` for the contact matrices, and rejects arguments that do not apply.
|
|
13
|
+
- The fetched stores are downloaded from a GitHub release rather than from raw.githubusercontent.com, which rate-limits heavily.
|
|
14
|
+
- Requires Sciris 3.5.2 or later.
|
|
15
|
+
|
|
16
|
+
## 1.0.0 (2026-08-08)
|
|
17
|
+
|
|
18
|
+
Merged with the [laser-init](https://github.com/laser-base/laser-init) project with additional features for geospatial modeling, name resolution, etc.
|
|
19
|
+
|
|
20
|
+
### Resolution and provenance
|
|
21
|
+
|
|
22
|
+
- **Location resolution.** Locations can now be given as an ISO-2 or ISO-3 code, an official name, a translation, or a common abbreviation: `ssd.load('USA', ...)`, `ssd.load('KEN', ...)`, `ssd.load("Côte d'Ivoire", ...)` and `ssd.load('DRC', ...)` all work. Matching is exact against a table generated into `data/locations.csv`; fuzzy matching (via `sc.suggest`) is used only to suggest alternatives in the error message when a lookup fails, never to resolve one.
|
|
23
|
+
- `ssd.resolve()`, `ssd.location_info()` and `ssd.list_all_locations()` for working with the location table directly.
|
|
24
|
+
- `ssd.parse_level()`, accepting `'admin1'` / `'ADM2'` / `'3'` / `2`. Only level 0 has data today; this fixes the vocabulary before subnational data arrives.
|
|
25
|
+
- **Provenance.** Every bundled store now records its source, URL, license, citation and build details, written into the HDF5 at build time so it cannot be separated from the data. Read it with `ssd.provenance(which)`, or from `df.attrs['provenance']` on any loaded frame. Tests assert that the licenses recorded in the data match the ones asserted in `NOTICE.md`.
|
|
26
|
+
- **Revision-versioned demographics.** UN WPP data is now stored as `demographics-<revision>.h5`. `load()` uses the newest bundled revision by default; `load(..., revision=2024)` pins one, and `ssd.list_revisions()` lists them. A future WPP release becomes an additive rebuild rather than a silent change to data already in use.
|
|
27
|
+
- `iso3`, `official_name` and `aliases` columns in `data/locations.csv`, generated by the new `scripts/build_locations.py` from `pycountry` (a build-time dependency only).
|
|
28
|
+
- CI (`.github/workflows/tests.yml`), pytest markers (`network`, `slow`, `integration`), and this changelog. CI additionally rebuilds every store from `raw_data/` and re-runs the tests against the result, so the committed build artifacts cannot drift from their sources.
|
|
29
|
+
|
|
30
|
+
### Data loading
|
|
31
|
+
|
|
32
|
+
- **`fetch()` — the data that is deliberately not bundled.** `load()` is still instant, offline and frozen; `fetch()` closes the three gaps the README has always named: annual resolution instead of decadal, complete life tables (`qx`, `lx`, `ex`, ...) instead of just `mx`, and other projection variants instead of Medium only. It reads the UN's public bulk CSVs — no API token, unlike the Data Portal — and caches them. `ssd.fetch('kenya', which='deaths', years=(2000, 2025))`, `ssd.fetch('kenya', which='life_table', years=2020)`, `ssd.fetch('kenya', which='births', variant='High')`. For the four series that are also bundled it returns exactly the schema `load()` returns, verified against the bundled data; `life_table` and `indicators` are fetch-only. See `ssd.list_fetchable()`.
|
|
33
|
+
- **A download cache**, with a provenance sidecar per file recording its URL, timestamp, size and SHA-256. `ssd.cache_dir()`, `ssd.cache_info()`, `ssd.cache_provenance()`, `ssd.clear_cache()`. The location defaults to a platform-appropriate directory and is overridable via `ssd.options.cache_dir` or `STARSIM_DATA_CACHE_DIR`.
|
|
34
|
+
- **`ssd.options`**, following Starsim's `ss.options` conventions, with `set()`, `reset()`, `help()` and a `context()` manager. Settings are read from the environment but acted on lazily: importing the package still creates no directories, opens no files and emits no warnings.
|
|
35
|
+
- **`plot()` — validation figures.** `ssd.plot('kenya')` draws a four-panel summary (age pyramid, crude birth rate, mortality curve, contact matrix); `ssd.plot('kenya', which='population')` draws one panel. The point is to catch the wrong country or the wrong units before the data reaches a simulation.
|
|
36
|
+
- **A dataset registry** (`ssd.registry`) holding a descriptor per dataset — layout, tier, admin level, license, revisions, location count — with public `register_dataset()` and `register_layout()` hooks. `ssd.list_datasets(detailed=True)` returns it as a dataframe, and `list_datasets(tier=0)` selects just the bundled data.
|
|
37
|
+
- **Tiers.** Datasets now declare whether they are bundled in the wheel (0), a downloaded release asset (1) or fetched from upstream (2). `load()` refuses anything above tier 0 and points at `fetch()`, so its "never touches the network" guarantee is now enforced by the code rather than by convention.
|
|
38
|
+
|
|
39
|
+
### Nodes and distances
|
|
40
|
+
|
|
41
|
+
- **The node table** (`ssd.load_nodes`), bundled. For every administrative unit at levels 0 and 1: `nodeid`, `location`, `iso3`, `level`, `name`, `pcode`, `dot_name`, `parent`, `lat`, `lon`, `area_km2`, `population`, `source`. 3,254 nodes across 184 countries in 1.4 MB, readable with no geospatial dependencies — which settles the question the plan left open: it bundles rather than shipping as a release asset. Built by `scripts/build_nodes.py` from UNOCHA COD-AB where it exists (the source of the p-codes, ~110 countries) and geoBoundaries CGAZ v6.0.0 elsewhere; the two disagree in places about how a country subdivides, so every row records which source it came from. `population` is national-only by design.
|
|
42
|
+
- **`ssd.distances()`** — the great-circle distance matrix between node centroids, which with `load_nodes()` gives a gravity model its `P_i`, `P_j` and `D_ij`. The gravity formulation itself stays in the model.
|
|
43
|
+
- **`ssd.fetch_shapes()`** — administrative boundaries with full geometry, from UNOCHA, geoBoundaries or GADM, normalized onto one schema and joining to the node table on `pcode`. `source=None` tries UNOCHA then geoBoundaries. **GADM is never reached automatically**: it is non-commercial only, so it must be named explicitly, and the constraint is enforced as data (`ShapeSource.redistributable`) rather than remembered — the node-table build refuses any source that sets it False.
|
|
44
|
+
- **`ssd.fetch_raster()`, `ssd.aggregate_raster()`, `ssd.fetch_nodes()`** — WorldPop 1 km gridded population, and summing it inside each administrative unit. This is the step that turns a boundary file into a metapopulation. `ssd.rescale_to_national()` reconciles WorldPop's modeled totals with UN WPP's (they differ by about 1.2% for Kenya 2020).
|
|
45
|
+
- **`ssd.choropleth()`**, and a `nodes` panel for `plot()` that draws centroids without needing any geospatial dependency.
|
|
46
|
+
- **`[spatial]` extra** (`geopandas`, `pyogrio`, `rastertoolkit`), imported lazily: `import starsim_data` works without them, and only the spatial functions raise if they are missing.
|
|
47
|
+
- **A documentation site**, built with [great-docs](https://posit-dev.github.io/great-docs/): ten narrative pages in `docs/` covering the tier model, the schema of every dataset, location resolution, the spatial tier, the source comparison, how this package relates to [epydemix-data](https://github.com/epistorm/epydemix-data), [laser-init](https://github.com/laser-base/laser-init) and the other projects distributing the same data, the internal architecture, provenance, and how the bundled files are generated — plus an API reference generated from the docstrings. Code examples are executed at build time, so the frames and figures shown are the package's actual output. Both the sidebar and the API reference are built by great-docs itself — from the pages' `guide-section` frontmatter and from the public API — so neither can fall out of step with the code; the only hand-written styling is three overrides great-docs has no setting for (figure scaling, navbar centering, and the Starsim footer); `.github/workflows/docs.yml` builds and deploys it. NOTICE.md gains entries for UNOCHA, geoBoundaries, WorldPop and GADM.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Version 0.1.0
|
|
51
|
+
|
|
52
|
+
Initial release: UN WPP demographics and `prem2017` / `prem2021` / `mistry2021` contact matrices, bundled as HDF5 and read with `load()`.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`starsim_data` distributes UN WPP demographics, contact matrices, and administrative geography for [Starsim](https://starsim.org) models. It returns dataframes and nothing else — no model code, no simulation logic. See `docs/07-related-projects.qmd` for where that line is drawn and why.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install -e ".[dev]" # runtime + pytest + pycountry + great-docs
|
|
13
|
+
pip install -e ".[dev,spatial]" # also geopandas/pyogrio/rastertoolkit, needed by build_nodes.py
|
|
14
|
+
|
|
15
|
+
pytest # the offline suite
|
|
16
|
+
pytest -m "not network" # what CI runs
|
|
17
|
+
pytest -m network # also hits UN/WorldPop servers (a few hundred MB)
|
|
18
|
+
pytest tests/test_load.py -k matrix # one file, one pattern
|
|
19
|
+
pytest tests/test_load.py::test_matrix
|
|
20
|
+
|
|
21
|
+
great-docs build # -> great-docs/_site/ (needs Quarto)
|
|
22
|
+
great-docs preview # live reload on localhost:3000
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Markers are declared in `pyproject.toml`: `network`, `slow`, `integration`, `spatial` (the last skips automatically without the extra). `tests/conftest.py` redirects the cache to a temp dir so tests never touch the user's real one; set `STARSIM_DATA_CACHE_DIR` to reuse downloads across network-test runs.
|
|
26
|
+
|
|
27
|
+
### Rebuilding the bundled data
|
|
28
|
+
|
|
29
|
+
The `.h5` files in `starsim_data/data/` are **committed build artifacts**, generated from `raw_data/` by `scripts/`. Never hand-edit them. Every script takes `--check` (validate without writing), which is what CI runs to stop the artifacts drifting from their sources:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python scripts/build_locations.py --check # derived columns of locations.csv, from pycountry
|
|
33
|
+
python scripts/build_hdf5.py --check # prem2021.h5, prem/prem2017.h5 and fetched.json, from raw_data/contact_matrices/ (--fetched also rebuilds fetched/*.h5)
|
|
34
|
+
python scripts/build_demographics.py --check # demographics, from raw_data/demographics/
|
|
35
|
+
python scripts/build_nodes.py --check # node table; ~1.4 GB cold, [spatial] required
|
|
36
|
+
python scripts/build_nodes.py --limit 5 # a quick subset while developing
|
|
37
|
+
python scripts/download_wpp.py # refresh raw_data/demographics/ (needs a token in scripts/.env)
|
|
38
|
+
python scripts/download_epydemix.py # refresh the US states in raw_data/ (then build_hdf5.py --fetched mistry2021_us population_us)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`build_nodes.py` is excluded from the per-PR CI job because of its size; it has a `workflow_dispatch`-only job in `.github/workflows/tests.yml`.
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
### The tier invariant
|
|
46
|
+
|
|
47
|
+
Every function belongs to exactly one tier, and this is the design constraint the rest of the package is arranged around:
|
|
48
|
+
|
|
49
|
+
- **Tier 0** — `load()`, `load_nodes()`, `distances()`. Reads only what is inside the wheel. **Never touches the network**, so it is instant, offline and byte-identical for everyone on a given version.
|
|
50
|
+
- **Tier 1** — `fetch()`, `fetch_shapes()`, `fetch_nodes()`, `fetch_raster()`. Downloads and caches, with a provenance sidecar per file.
|
|
51
|
+
|
|
52
|
+
Tier is a field on the dataset descriptor and `load()` enforces it: asking `load()` for a tier-1 dataset raises and names `fetch()` instead. Do not add a network call to a tier-0 path — CI runs `-m "not network"` and `tests/test_docs.py` fails the build if an executed docs cell would fetch.
|
|
53
|
+
|
|
54
|
+
### Registry-driven dispatch
|
|
55
|
+
|
|
56
|
+
`registry.py` holds two tables, and nearly every extension goes through one of them rather than through an `if` branch:
|
|
57
|
+
|
|
58
|
+
- `DATASETS` — name → `Dataset` descriptor (layout, tier, levels, license, revisions, location count, store). Bundled descriptors are **discovered** by opening each `.h5` and reading its root attributes, so the stores are self-describing: adding a new contact source is dropping CSVs in `raw_data/` and re-running `build_hdf5.py`. The contact sources that are fetched rather than bundled (`prem2017`, `mistry2021`, in `fetched/` outside the wheel) are described by the manifest `data/fetched.json`, which `build_hdf5.py` writes with each store's URL (a GitHub release asset), SHA-256, locations and provenance. The default contact matrices, `prem`, are `prem2021` plus the `prem2017` matrices for the ten locations it lacks (bundled in `data/prem/prem2017.h5`, a subfolder so discovery doesn't list it as its own dataset); discovery describes them from both stores, with a `loader` that reads whichever has the location.
|
|
59
|
+
- `LAYOUTS` — layout name → the shaper that turns a stored array into a dataframe (`matrix`, `crude`, `mortality`, `population`, `nodes`).
|
|
60
|
+
|
|
61
|
+
`load()` resolves the descriptor, picks the source for the revision, resolves the location scoped to that dataset, then hands off to the shaper with an `opts` dict carrying layout-specific arguments (`kind` for matrices, `level` for nodes). An argument a layout has no use for is **rejected, not ignored**. `register_dataset()`, `register_layout()` and `plotting.PLOTTERS` are the public hooks; a descriptor with a `loader` makes `load()` delegate entirely, which is how downstream code adds data this package doesn't ship.
|
|
62
|
+
|
|
63
|
+
Descriptor discovery is cached — call `registry.refresh()` if the files change inside a running process (e.g. a test that rebuilds a store).
|
|
64
|
+
|
|
65
|
+
### Import conventions
|
|
66
|
+
|
|
67
|
+
Follow Starsim's convention: **`from` imports live only in `__init__.py`**. Every other module does `import starsim_data as ssd` and reaches everything as `ssd.load`, `ssd.registry.datasets()`, so modules can refer to each other freely without cycles. The import order in `__init__.py` is load-bearing: `registry` must exist before `loaders` (which registers layouts) and `fetching` (which registers tier-1 datasets).
|
|
68
|
+
|
|
69
|
+
Importing the package must stay side-effect free — no directories created, no files opened, no warnings. `options` reads the environment at import but acts on it lazily; the cache dir is resolved and created only when something is written. The `[spatial]` imports are lazy for the same reason: `import starsim_data` works without geopandas, and only the spatial functions raise.
|
|
70
|
+
|
|
71
|
+
### Invariants that are enforced as data, not as memory
|
|
72
|
+
|
|
73
|
+
- **Provenance travels with the data.** `scripts/provenance.py` stamps source/URL/license/citation/build details into each store at build time; `load()` and `fetch()` put the same record in `df.attrs['provenance']`. `tests/test_provenance.py` asserts the licenses recorded in the stores match `NOTICE.md`.
|
|
74
|
+
- **GADM is never redistributed.** Its `ShapeSource` sets `redistributable=False`, `fetch_shapes()` leaves it out of the UNOCHA→geoBoundaries fallback chain, and `build_nodes.py` refuses any source with that flag. Keep the constraint on the descriptor rather than in a comment or a conditional.
|
|
75
|
+
- **Demographic stores are versioned by WPP revision**, as `demographics-<revision>.h5`. A new UN release is an additive rebuild, never an edit to data already in use.
|
|
76
|
+
- **Namibia's ISO-2 code is the string `NA`.** Any CSV read must pass `keep_default_na=False` or use the `csv` module, or pandas turns Namibia into a missing value. There is a regression test for it in `tests/test_locations.py`.
|
|
77
|
+
- **Location resolution is exact.** Fuzzy matching (`sc.suggest`) only builds the "did you mean" error message; it never resolves a name.
|
|
78
|
+
|
|
79
|
+
### Licensing
|
|
80
|
+
|
|
81
|
+
Code is MIT; each bundled dataset keeps its own upstream license (GPL-3.0 for the Prem matrices, Apache-2.0 for `mistry2021`, CC BY 3.0 IGO for WPP). `NOTICE.md` is the operative attribution text and `licenses/` holds the full upstream texts. If you add or change a data source, update `NOTICE.md` — a test checks it against what the stores record.
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
Narrative pages are `.qmd` files in `docs/`, built by [great-docs](https://posit-dev.github.io/great-docs/). There is no navigation or API-reference config to maintain: the sidebar comes from each page's `guide-section` frontmatter and the reference is discovered from the public API. Rules `tests/test_docs.py` enforces:
|
|
86
|
+
|
|
87
|
+
- Every page needs `title` and a `guide-section` from `Getting started` / `The data` / `Internals`, and sections must stay **contiguous in filename order** — inserting a page in an earlier section means renumbering the later ones.
|
|
88
|
+
- Renumbering is safe: site URLs strip the numeric prefix (`docs/09-provenance.qmd` → `user-guide/provenance.html`), so cross-page links are written as bare `provenance.qmd`.
|
|
89
|
+
- Only the landing page (`01-getting-started.qmd`) prefixes links with `user-guide/`.
|
|
90
|
+
- Executed `{python}` cells must not fetch anything; write downloading examples as static ` ```python ` blocks.
|
|
91
|
+
|
|
92
|
+
`docs/08-architecture.qmd` is the long-form version of the Architecture section above, and stays in step with the code by executing against it.
|
|
93
|
+
|
|
94
|
+
### Keeping the docs in step
|
|
95
|
+
|
|
96
|
+
Docs are part of a change, not a follow-up to it. Before treating any change as done, check what it obliges you to update.
|
|
97
|
+
|
|
98
|
+
**When you add or remove a docs page:**
|
|
99
|
+
|
|
100
|
+
1. Give it `title` and a `guide-section`, then **renumber siblings so the sections stay contiguous** in filename order — a new `The data` page means bumping every `Internals` page. Use `git mv`; site URLs strip the numeric prefix, so no links break.
|
|
101
|
+
2. **Do not add navigation config to `great-docs.yml`.** A new page needs no entry there, by design — the sidebar is built from `guide-section` and the reference from the public API, so neither can fall out of step with the code. `test_docs.py::test_config` fails if `sections` or a hand-written `reference.contents` shows up. Wanting to edit that file is usually a sign something else is wrong. The one real exception: introducing a *new* `guide-section` means adding it to `SECTIONS` in `tests/test_docs.py`, which defines both the allowed values and their order.
|
|
102
|
+
3. Link it from at least one sibling page — otherwise it is reachable only from the sidebar.
|
|
103
|
+
4. Update the hand-maintained places that count or list pages: the "N narrative pages" sentence in `CHANGELOG.md`, and the per-file table in `tests/README.md` when tests change.
|
|
104
|
+
5. Run `pytest tests/test_docs.py`, then `great-docs build` if Quarto is available. The tests catch frontmatter, dead cross-links and accidental network use; only a real build catches a cell that raises.
|
|
105
|
+
|
|
106
|
+
**When code changes:** the API reference regenerates itself, but prose does not. A new dataset, layout or source touches `docs/03-datasets.qmd` (schema), `docs/06-datasources.qmd` (coverage, licensing, limitations) and `NOTICE.md`; a new extension point touches `docs/08-architecture.qmd`. Docs cells execute against the package at build time, so a stale *example* fails docs CI — but a stale *sentence* will not, and nothing will catch it for you.
|
|
107
|
+
|
|
108
|
+
README links to the built site are `https://starsimhub.github.io/starsim_data/user-guide/<name>.html`.
|