Architecture¶
The detailed reference. For the high‑level picture read
ONBOARDING.md; for the
rationale, see Design — reference NIRS dataset bank.
1. The lifecycle pipeline¶
A dataset flows through eight stages; each module owns one. The CLI (n4a-datasets) is a thin Typer
layer that lazy‑imports the heavy deps.
NIRS DB/v2.0/<leaf>/dataset_card.json + X*/Y/M.csv
│ bootstrap.py DESCRIBE card.json -> catalog/datasets/<id>.yaml (schema-2.0 descriptor)
▼
organize.py ─► canonical.py CANONICAL id-keyed CSV -> datasets/<id>/canonical/ (per-source Parquet,
│ joined by sample_id), + dataset.json
▼
manifest.py MANIFEST content-addressed manifest.json (drives incrementality)
│
▼
qualify/profile.py QUALIFY card.json + card.md + croissant.json + assets/*.png
│ (registry.py metrics, plots.py, anonymize.py, croissant.py, datasheet.py)
▼
catalog.py INDEX catalog/datasets.yaml + a whole-bank `summary`
│
▼
health.py HEALTH probe origins -> catalog/health.json (degrade dead public)
│
▼
status.py STATUS docs/DATASET_STATUS.md + docs/PRIVATE_DATASETS.md
│ + catalog/validation.yaml (human review)
├─► site/ SITE the static catalog site (pure-render)
▼
access.py ◄── get("<id>") USE local-first, else fetch by DOI/origin, verify, cache -> NirsDataset
▲
publish.py + dataverse.py PUBLISH (future) personal-Dataverse governance for protected data
Bulk vs one dataset. bulk.py runs organize+qualify across a process pool (spawn, with a serial
fallback if a worker dies) with per‑dataset failure isolation; discover‑style bulk authoring is
bootstrap.py over the whole v2.0/ tree (idempotent, managed‑orphan prune, catalog/reconciliation.json).
2. Module map¶
Module |
Role |
|---|---|
|
The contract: |
|
v2.0 |
|
id‑keyed CSV → per‑source Parquet + per‑sample |
|
|
|
Place raw → call |
|
Build |
|
Extensible, protocol‑versioned metric registry (scopes: source/variable/dataset). |
|
Pure numerics; per‑source + per‑variable plots (Agg). |
|
The anonymized‑tier transform + the |
|
MLCommons Croissant JSON‑LD + Datasheets‑for‑Datasets |
|
|
|
|
|
The index entry + the whole‑bank |
|
Origin liveness probe (injectable session) → |
|
Per‑dataset status (state/origin/validation/distribution) + the reports + the validation registry. |
|
The static‑site generator (pure‑render: pyyaml + stdlib; tier‑gating in |
|
Parallel organize+qualify with failure isolation. |
|
The |
|
Token hygiene; the Dataverse REST client; the publish/governance flow. |
3. Schema 2.0 (the descriptor)¶
catalog/datasets/<id>.yaml → DatasetDescriptor:
sources: list[Source](≥1) —source_id, instrument, modality, axis unit/range, n_observations, n_variables.variables: list[Variable](may be empty) —name,role(target|metadata),type(numeric|categorical|text|identifier|datetime), unit, classes..targets/.metadata_variablesare properties. Notask_type— the task is a consumer concern.ids: IdentitySpec—observation_id(per spectrum),sample_id(physical sample),sample_id_available.alignment_level—observation|sample.splits: list[SplitRef](applied=False).tier— public | private | anonymized.versions: Versions—content+schema_protocol.governance(license + open‑data fields; no visibility/confidentiality),provenance,origin_sources: list[OriginSource](where the bytes live — never checksums),publications,datacite,dataverse,reproducibility,generation(managed‑descriptor provenance).
Two‑level validation (schema.py + catalog/scripts/validate.py): schema validity (every field
well‑formed) is separate from publishability (publication_blockers(), which gates only the public
tier: open license + open non‑SCRIPT origins + the responsible‑release fields).
4. Canonical on‑disk layout¶
datasets/<id>/canonical/
dataset.json {format_version, id, join_key:"sample_id", alignment_level,
sources:[{source_id,path,n_observations,n_variables,axis_*}], variables, splits}
sources/<source_id>.parquet observation_id (str), sample_id (str), <wavelength cols> (float32)
variables.parquet sample_id (str), <all non-spectral Y/M cols> (native dtype) [optional]
splits/<name>.parquet sample_id (str), partition (str) [optional]
Sources may have different row counts (asymmetric repetitions). Everything is joined by sample_id,
never by row position. variables.parquet is per‑sample (one row per sample_id); the standardization
script’s source_* provenance‑plumbing columns are excluded. Parquet is chosen because Dataverse does
not auto‑ingest it, so uploaded bytes stay byte‑identical to local ones (the SHA‑256 verify depends on it).
5. Tiers and anonymization (load‑bearing)¶
public shows everything and is openly fetchable; private shows metadata + metrics but export needs a
token; anonymized masks variable names (var_NNN), z‑scores numeric targets, and removes identifying
free text. The anonymized tier is enforced automatically by tier through one chokepoint —
qualify.anonymize.public_card / public_descriptor:
qualify()writescard.jsonitself already‑anonymized for the anonymized tier (and renderscard.md/croissant.jsonfrom the masked view);the catalog entry,
NirsDataset.descriptor/variables()/card(), the site, and Dataverse publish metadata all derive their displayed fields from the public card/descriptor.
So no tracked artifact, index entry, public API, or published metadata can leak an anonymized identity.
(Reviewed by Codex: GO. Regression test: tests/test_anon_enforcement.py.)
6. Incrementality + the two axes¶
processing_hashcovers only byte‑determining descriptor fields (sources, ids, alignment, splits,versions.content); editing a name/tier/variable‑role/origin or bumpingversions.schema_protocolnever rebuilds canonical bytes.metadata_hashcovers the displayed content and drives card re‑render (card_metadata_fresh).needs_rebuildcompares raw‑file hashes + the converter identity +processing_hashagainst the previous manifest. A content version bumps on a byte change; a metric‑protocol version bumps to re‑qualify cards (build-all --protocol-refresh) without rebuilding data.
7. Status + validation¶
status.py derives, per dataset: state (described→canonical→qualified=metrics computed),
materialized (canonical + SHA‑256), origin (reachability from the health probe), distribution
(open / on_dataverse / upload_pending). Validation is the human axis, recorded in
catalog/validation.yaml (pending→reviewed→approved), which bootstrap never touches. n4a-datasets status refreshes the registry and writes docs/DATASET_STATUS.md + docs/PRIVATE_DATASETS.md.
8. Access model (and the PyPI caveat)¶
get(name, *, root=".", source, split, token, …) resolves local‑first (<root>/datasets/<id>/ canonical + <root>/catalog/datasets/<id>.yaml), else fetches by the descriptor’s Dataverse DOI (token
for private/anonymized) or an open origin, verifies SHA‑256, and caches. The token travels only in the
X-Dataverse-key header and is never sent on an S3 redirect.
PyPI note. The wheel ships the code and the bundled cross-language
catalog/index.json. A pip‑installed Python consumer still pointsget(root=<checkout>)at a clone of this repo for the high-levelget()/list()/card()surface, because that layer reads descriptors/cards and returnsNirsDataset. Non-Python bindings consumecatalog/index.jsondirectly:n4ds_resolvereturns the byte contract plus the tier-sanitized descriptor, so R/WASM/Rust can inspect sources/variables and read verified Parquet without the Python provider package. SeeRELEASING.md.
9. Conventions¶
Python 3.11+, Google docstrings, ruff (line length 220) + mypy, type hints on public APIs, py.typed.
nirs4all is an optional extra ([nirs4all]) — imported lazily, degrading gracefully when absent. Each
green gate: ruff check . + mypy --config-file pyproject.toml src + validate.py (+ --check-publish)
pytest. The enum mirror (AxisUnit/SignalType/Modalitymirror nirs4all by value) is guarded bytests/test_schema.py.