Skip to content

Read only the data a model needs - #7

Merged
jtravs merged 7 commits into
mainfrom
perf/targeted-catalogue-loads
Oct 24, 2026
Merged

jtravs merged 7 commits into
mainfrom
perf/targeted-catalogue-loads

Conversation

@jtravs

@jtravs jtravs commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Models read only the data they need. No value, model or stored fit changes.

A model that needed a few datasets used to read the whole catalogue, about 8,300 files. On a cold disk, or under an on-access scanner such as Microsoft Defender, that took tens of seconds. solprop's start-up spent most of its time here.

Data files opened, fresh process

Entry point Before After
molecular_rate_model("N2") 8,671 35
ionisation_rate_model("He") 339 34
gas default_model("He") 195 28
raman_response("N2") 557 25
air_model() 799 19
SiO₂ model listing 3,964 92
gas models("He") 3,918 179
sources() (every record) 8,333 762

Cold start: solprop's optmat start-up calls, timed on fresh installs, on a macOS laptop with Defender:

main branch
Cold (first run) 35.9 s 8.0 s
Second run 15.0 s 1.5 s
Fully warm 2.1 s 0.4 s

Most of the remaining cold time is first-time module loading, which is the same on both.

Commits

  1. Look datasets up by key. Models, fits and validations use _catalogue.by_key/dataset instead of {d.key: d for d in datasets()}. The catalogue reuses records already read, so none is read twice. Contract tests now inject synthetic datasets through a shared curated fixture in tests/conftest.py, which overlays the catalogue lookups.
  2. Build only what is asked for. Gas fits, ionisation parameters, atomic and molecular rates, and Raman parameters are each built per gas, on demand. New: fitted_model(name), plus a gas published_model(name) like the solid one. The functions that list everything are unchanged.
  3. ionisation_rate_coverage / molecular_rate_coverage. The gases each rate model covers, read from the rates file alone (2 files). They let a caller tell an atom from a molecule without building models; solprop will use them.
  4. Catalogue index. data/catalogue_index.json is written by tools/catalogue_index.py. For each record it holds the record's size, the materials and quantities of its datasets, and the materials of its formulas. Searches by material, quantity or formula read only the records it lists.
    • tests/test_catalogue_index.py keeps it equal to the records.
    • It is checked on first use with one stat per record. A new record, or one whose size changed, is re-read from its TOML.
    • A CatalogueIndexWarning names records the index gets wrong, so an out-of-date index in a source install is slower, never wrong.
    • An edit that keeps a record's size isn't seen at runtime; the CI test sees it.
    • AGENTS.md and the developers' data page say when to regenerate it.
  5. Read data files only for the datasets used. Source.datasets becomes a cached property built from the new Source.entries, and Source.dataset(id) builds one dataset. The catalogue loads records with load_source(path, defer=True). load_source still builds and checks every dataset by default.
  6. Guards. tests/test_data_reads.py runs the entry points a propagation code uses in a fresh process. It fails if one opens more than about twice what it opens now.

Public API (snapshot re-recorded, CHANGELOG updated)

  • New: Source.entries, Source.dataset, load_source(..., defer=), fitted_model, gas published_model, ionisation_rate_coverage, molecular_rate_coverage, CatalogueIndexWarning.
  • Changed: Source.datasets is a property, not a constructor field. Nothing outside load_source constructs Source.

No OEP: the maintainer approved an exception for this optimisation.

Checks

The full poe check passed locally: 21,507 tests with 1 skipped, 100% statement and branch coverage, and a clean docs build.

🤖 Generated with Claude Code

jtravs and others added 7 commits October 24, 2026 10:55
Every model, fit and validation that needed a few datasets built a dict of
the whole catalogue (some 8,300 files) to look them up. They now read only
the records they need through optmat._catalogue, and the catalogue, when
loaded, reuses the records already read. molecular_rate_model reads 395
data files instead of 8,671.

Contract tests serve synthetic datasets through a curated fixture that
overlays the catalogue lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gas registries built every gas to return one: default_model built all
fifteen fits (each with its Kramers-Kronig absorption term), and the
ionisation parameters, the atomic and molecular rates and the Raman
parameters built every species and every model. Each is now built on
demand and cached; fitted_model and the gas published_model read one
record, as the solid published_model does. The functions that list
everything keep their results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ionisation_rate_coverage and molecular_rate_coverage read only the rates
files, so a caller can tell an atom from a molecule, and list a family's
model names, without building any model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
data/catalogue_index.json lists each source record's size, the materials
and quantities of its datasets and the materials of its formulas, derived
from the records alone by tools/catalogue_index.py. datasets(material=,
quantity=), quantity_datasets and formula_sources read only the records it
lists, instead of every record (air_model: 799 files read, now 19).

The index is checked against the records on first use (one stat each): a
record that is new or changed size is read again, and a
CatalogueIndexWarning names any whose entries the index gets wrong, so an
out-of-date index in a source install is slower, never wrong. A test keeps
the committed index equal to the records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A source's datasets are built from its record's entries when first asked
for: Source.dataset(id) builds one, Source.datasets (now a cached property)
builds all, and the catalogue loads its records with load_source(path,
defer=True). load_source still builds and checks every dataset by default.
A source's citation, errata and formulas now cost no data file: the gas
models of He read 179 files rather than 1,731, and every record 762 rather
than 8,333.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/test_data_reads.py runs the entry points a propagation code uses in a
fresh process and fails if one reads more than about twice what it reads
now, so a full-catalogue load cannot return unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 24, 2026 10:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@read-the-docs-community

Copy link
Copy Markdown

@codecov

codecov Bot commented Oct 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jtravs
jtravs merged commit 2d31729 into main Oct 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants