Skip to content

feat(provenance): add PROV-O RDF export for BrainKB ingestion - #386

Open
dhritimandas wants to merge 7 commits into
neuronets:alphafrom
dhritimandas:feat/provenance-rdf-export
Open

feat(provenance): add PROV-O RDF export for BrainKB ingestion#386
dhritimandas wants to merge 7 commits into
neuronets:alphafrom
dhritimandas:feat/provenance-rdf-export

Conversation

@dhritimandas

Copy link
Copy Markdown
Contributor

Summary

What: This adds a command. The command is nobrainer provenance export.
It reads a saved model bundle. It emits a PROV-O graph in Turtle or JSON-LD.
Why: No structured provenance graph existed for a training run.
croissant.json records model and dataset facts, but nothing links them as
an activity. BrainKB needs that graph to ingest nobrainer runs.

The graph

The run is a prov:Activity. It prov:used a dataset entity and
prov:generated a model entity. Both link to one or more
prov:wasAssociatedWith agents (always the nobrainer/pytorch software
agents; optionally a caller-supplied human/org agent via --agent, never
invented by this module on its own).

Determinism

Every IRI is content-derived: a file/model's own sha256, or a stable hash
of a canonical-JSON payload for the run/dataset/agent nodes. No timestamps,
no random UUIDs, no blank nodes anywhere. Re-exporting the same input from
a different location on disk produces a byte-identical graph -- verified
directly, not just asserted (test_run_iri_unchanged_after_directory_move,
test_repeated_export_is_byte_identical).

DOMAIN provenance only -- enforced, not just documented

This module emits facts about what nobrainer itself did. It does not
emit BrainKB ingestion-activity triples (e.g. "this graph was loaded into
BrainKB at time T"). Named-graph registration and POSTing this output to
BrainKB's own ingestion API are separate steps the caller performs against
BrainKB's API. A runtime check (_assert_domain_only, called on every
build_graph()) rejects: a second prov:Activity, any agent not
associated with the run, forbidden PROV predicates
(wasInformedBy/wasStartedBy/wasEndedBy/qualifiedAssociation/
hadPlan), and any term containing "brainkb". A positive-control test
injects a second activity and confirms the check actually fires.

Inputs

  • --bundle DIR (required): a Segmentation.save() directory
    (model.pth + croissant.json). Read with plain json.load -- not via
    nobrainer.processing.croissant, since importing that module pulls in
    torch through processing/__init__.py (verified). This module is
    stdlib + rdflib only.
  • --dataspec PATH.json (optional): a DataSpec manifest
    (nobrainer/data/spec.py). When given, dataset provenance includes both
    image and label checksums. When omitted, falls back to croissant's
    source_datasets, which only ever records image paths.
  • --agent, --base-iri, --strict, --format {turtle,json-ld}.

Edge cases handled explicitly, not silently

  • A non-finite final_loss/best_loss is omitted (nb:lossStatus "non-finite") rather than serialized: rdflib emits a bare NaN in
    JSON-LD, which is not valid JSON (verified).
  • A model_args/optimizer.args value that landed in croissant.json as a
    Python repr() memory address (write_model_croissant uses
    default=str) is elided rather than hashed -- it would otherwise make
    run IRIs nondeterministic across machines.
  • Empty block_shape/None fields emit no triple (absence, not "").
  • Missing checksums fall back to on-disk recomputation, then to a visibly
    unaddressed x-nosha/ IRI, recorded via nb:checksumStatus.
  • write_model_croissant and write_checkpoint_croissant disagree on
    model_architecture vocabulary (registry name vs. torch class name);
    the disagreement is recorded (nb:architectureVocabulary), not guessed.

The one new dependency

rdflib was already present transitively (via mlcroissant) but
undeclared. Now [project.optional-dependencies] provenance = ["rdflib >= 7.0"], added to all.

Known gap, flagged rather than papered over

training_date in croissant.json is the model-save timestamp, not a run
start/end. It is emitted as prov:generatedAtTime on the model (where
it's literally true) and as a neutral nb:metadataRecordedAt on the run
-- never as prov:endedAtTime, which would misrepresent it. The graph
genuinely cannot express training duration until write_model_croissant
records real start/end times.

Test plan

  • uv run pytest nobrainer/tests/unit/test_rdf_export.py -q -- 52
    passed (hashing primitives, graph shape, --agent, --dataspec,
    edge cases, the DOMAIN-only boundary with a positive control,
    turtle/JSON-LD equivalence, determinism, no-torch-import isolation,
    CLI subprocess tests).
  • uv run pytest nobrainer/tests/unit/ -m "not gpu" -q -- 446 passed,
    no regressions.
  • pre-commit run --all-files clean.
  • Real end-to-end run against an actual bundle: Turtle and JSON-LD both
    re-parse cleanly with a fresh rdflib.Graph().parse() and produce
    the same triple count (81 == 81).
  • CI

dhritimandas and others added 7 commits April 10, 2026 16:52
Add versioned dataset contract with DataLad-aware preflight validation:
- DataSpec dataclass with JSON serialization
- validate() with symlink-aware file checks, spacing/orientation/label validation
- inspect_entry() for NIfTI/Zarr metadata inspection
- CLI commands: validate, inspect, --skip-validate on predict/convert-to-zarr
- 33 unit tests covering all branches
- Replace inline @context dicts with CROISSANT_CONTEXT constant
- Change @type from cr:Dataset to sc:Dataset per Croissant 1.0 spec
- Add conformsTo and sha256 fields required by mlcroissant validation
- Update test assertions to match corrected @type
No structured provenance graph existed for a training run -- croissant.json
records model/dataset facts but no prov:Activity linking them. This adds
`nobrainer provenance export`, reading a saved model bundle (and optionally
a DataSpec manifest for image+label dataset provenance) into a PROV-O graph:
the run as prov:Activity, used dataset entity, generated model entity, and
associated software/caller agents.

Every IRI is content-derived (sha256 for files/models, stable digests for
runs/datasets/agents) -- no blank nodes, no timestamps or UUIDs in identity,
so re-export of the same input is byte-identical regardless of its path on
disk. Emits DOMAIN provenance only: named-graph registration and POSTing to
BrainKB's ingestion API are separate steps against BrainKB's own API,
enforced at runtime (not just documented) by rejecting a second Activity,
any non-SoftwareAgent floating free of the run, or any brainkb-named term.

rdflib is the one new dependency, gated behind a new [provenance] extra
(it was already present transitively via mlcroissant, but undeclared).
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