Artifacts and resolution

The released files, how a name becomes a path, and how an inspiral model is bound to its merger-ringdown arm. Written 2026-09-02, the day the deposit became six files and the digests were checked.

The six released files

Zenodo record 22262339, https://doi.org/10.5281/zenodo.22262339, base URL https://zenodo.org/records/22262339/files/. The v3 family is the default; v2 is deposited so that published v2 numbers stay reproducible (nrhjsurrogate/models.py, the registry, which is the only place the names and checksums live).

name what bytes sha256 (public copy)
NRHJSur3dq8_AA_v3 action-angle inspiral, generation 3, RELEASE_AA 31952616 b6d2d300...1ebf483
NRHJSur3dq8_AdA_v3 adiabatic-angle inspiral, generation 3, RELEASE_ADA 15734472 c0ca9922...f549a1a
NRHJSur3dq8_MR_v3 the merger-ringdown arm both v3 inspirals name, RELEASE_MR_V3 19606296 dd1bf008...235059a
NRHJSur3dq8_AA_v2 previous generation, PREVIOUS_AA 30743544 a05f6e02...97bd06b4
NRHJSur3dq8_AdA_v2 previous generation, PREVIOUS_ADA 15830464 20dad0f5...58bc61ce
NRHJSur3dq8_MR_v2 the arm both v2 inspirals name (the constant is still spelled RELEASE_MR) 19690832 da0ea1b0...59f67546b

Read the names from nrhjsurrogate.RELEASE_AA and RELEASE_ADA rather than typing them. The full digests are in models.MODELS and were reproduced byte for byte on a second scrub run. The wheel ships code only; the files come from the deposit or from models.fetch_model(name), which downloads into the per-user cache and verifies the SHA-256 and size BEFORE the file is moved into place, so a truncated download can never be found by a loader (models.py:fetch_model). An unregistered name is refused with the released names listed, measured: fetch_model("nrhjsur_production_v4_swsph") raises unknown model ...; released models: NRHJSur3dq8_AA_v3, NRHJSur3dq8_AdA_v3.

A model cannot be evaluated without its merger-ringdown arm. Each inspiral file records the arm's NAME (mr_name) and the content digest it was validated against (mr_content_md5); both v3 inspirals record NRHJSur3dq8_MR_v3 and 912502ce1e193e81a91269693508f31c (read off the files' root attributes). Loading resolves that name on every load and refuses to continue without it, rather than returning an inspiral-only model, because a model degraded by accident is indistinguishable from one degraded on purpose (nrhjsurrogate/serialize.py:1206, resolve_mr_arm). The waveform, the remnant and the error estimate all need the arm.

How a name becomes a path

models.resolve_model(name_or_path), models.py:569. First hit wins:

  1. an explicit path that exists, used verbatim;
  2. $NRHJ_MODEL_DIR, colon separated like $PATH;
  3. $LAL_DATA_PATH, the LALSuite convention, so an artifact dropped beside NRSur7dq4 and NRHybSur3dq8 resolves by name with nothing to learn; below the project variable so a project choice beats a shared install, above the cache so a file placed on purpose beats one the package downloaded, and never written to;
  4. the per-user cache, $NRHJ_MODEL_CACHE_DIR, else platformdirs if it happens to be installed, else $XDG_CACHE_HOME/nrhjsurrogate, else ~/.cache/nrhjsurrogate;
  5. model_release/ beside a source checkout, deliberately last.

models.search_path() prints the list: the directory named by NRHJ_MODEL_DIR, then the directories on LAL_DATA_PATH, then ~/.cache/nrhjsurrogate/models. A path with directory components is a specific request and is not searched for elsewhere. A miss raises FileNotFoundError naming the file, its size and digest, every directory searched and the two ways to fix it; measured for an unknown name.

Earlier spellings of these variables still work and warn once, naming the variable to use instead.

Verification is opt-in on load and the internal copies fail it, on purpose. The registry pins the PUBLIC checksums, the scrubbed files in dist_models/ that the deposit holds. The checkout's model_release/ copies carry the construction provenance (build script, cache paths, training ids) that the release strips, so they hash differently: model_release/NRHJSur3dq8_AA_v3.h5 measures 9cac9f97... against the registry's b6d2d300..., and models.verify_model refuses it with checksum mismatch. Evaluation is bitwise identical between the two copies, 192 of 192 arrays over four inspirals and six parameter points. resolve_model(..., verify=True) is how a consumer asks for the check.

The arm binding and its digest

serialize.compose_mr_arm (serialize.py:1352) decides, in order: an explicit merger_ringdown_path wins; else an embedded arm (pre-de-embedding files, none of the six); else mr_representation == "none" is a deliberately arm-less model; else the recorded name is resolved and checked.

The check compares digests, and the digest is computed in an unusual way that has consequences. mr_content_digest_of_arm (serialize.py:965) RE-SERIALISES the in-memory arm through the ordinary writer into an in-memory HDF5 image and hashes the resulting group. The alternative, hashing the dataclass fields, would have been a second definition of "the same arm", free to drift from the on-disk one. Two things follow:

Raise versus warn, all three measured:

case behaviour
the arm resolved BY NAME has a different digest (MR_v2 bytes placed under the MR_v3 name in $NRHJ_MODEL_DIR) ValueError: merger-ringdown arm mismatch; nobody chose that file
an explicit merger_ringdown_path="NRHJSur3dq8_MR_v2" on the v3 inspiral loads, with a warning naming both digests: a deliberate swap is a sanctioned workflow, and must not be reported as the released composition
the arm is absent from every searched directory FileNotFoundError naming the file, the digest expected and the directories; proven by the deposit gate in an isolated venv, because inside a checkout the model_release/ fallback supplies the arm and the case cannot be reproduced there

The representation is checked too: a swsph model composes only with a swsph arm. And the two families are not interchangeable between classes: NRHJSurAdA.load on an AA file, and NRHJSurAA.load_h5 on an AdA file, each raise unsupported format_version naming the tag they found. Those tags are literal strings stored inside the files and one of them still carries a retired spelling of the project name; they identify the on-disk format, not the model.

What the files say about themselves

Root attributes worth reading with h5py before trusting a number:

Limits, next to the capability

Where the code is

what where
registry, resolution, fetching, verification nrhjsurrogate/models.py
arm resolution, digest, composition nrhjsurrogate/serialize.py:965, :1206, :1352
loaders nrhjsurrogate/driver/aa_api.py:594 (load), :690 (load_h5); driver/api.py:204
one name for the arm argument nrhjsurrogate/driver/loader_arguments.py
public copies scripts/make_public_artifacts.py -> dist_models/
deposit gate scripts/deposit_selfsufficiency_gate.py
user documentation docs/USAGE.md section 1; examples/usage/10_finding_the_artifacts.py