Skip to content

feat: add experimental data entities as mirrors of the computational ones - #428

Open
timurbazhirov wants to merge 6 commits into
devfrom
feature/experimental-data-mvp
Open

feat: add experimental data entities as mirrors of the computational ones#428
timurbazhirov wants to merge 6 commits into
devfrom
feature/experimental-data-mvp

Conversation

@timurbazhirov

@timurbazhirov timurbazhirov commented Sep 2, 2026

Copy link
Copy Markdown
Member

What this is

A minimal, working implementation of experimental data support: four root entities, the pieces they compose, and examples built from real instrument and lab-system records. It is deliberately the smallest thing that stands on its own. The concepts are outlined and each is exercised by a record that actually validates.

Design and dataset survey are in plan/upcoming/2026-09-02-experimental-data-*.md (on claude/experimental-data-support-s3bf63).

The idea: mirror, don't fork

Experimental work has the same shape as simulation and different nouns. Each new entity is built as the mirror of the computational one it corresponds to, composing the same mixins and keeping the fields that transfer.

Computational Experimental What changes
material sample basis/lattice become an optional _material reference, because a specimen may have no known structure
software/application instrument version becomes firmware, build becomes serialNumber
job measurement compute becomes instrument, _material becomes _sample
workflow process subworkflows become stages, units become steps

Properties do not fork. A measured film thickness is an ordinary property/holder: same data union, same exabyteId, same repetition. Only source.info widens, from the exabyte job reference to a union over exabyte, measurement and process. The measurement reference is shaped like the job reference (measurementId where a job has jobId, channel where it has unitId), so a consumer that already reads job provenance needs no new code path.

What is in the diff

  • Four root entities and their components: sample (+ layer, position, library), instrument (+ component, and the instrument_property mixin mirroring job/compute_property), measurement (+ data channel, scanning probe parameters), process (+ stage, step, and the target/precursor/gas source union).
  • Foundation: core/abstract/multidimensional_array and core/reusable/array_data for rasters and grids (core/abstract/3d_grid is a k-point mesh descriptor, not a data grid); core/reusable/file_reference for binary instrument output, which system/file_source cannot hold because it requires text; quantity/*, environment, identifier; system/activity, the status and timing mixin shared by measurement and process.
  • techniques_category, narrowing the same CateCom tiers that categorize models and methods.
  • Three properties: film_thickness, surface_roughness, hysteresis_loop, registered the usual five-touch way.
  • Units: 18 new families (temperature, time, voltage, current, flow rate, deposition rate, and so on) plus Torr-scale pressures. Existing families are extended, never redefined.
  • Wiring: ENTITY_DOMAINS gains the four nouns so the layer taxonomy stays total; the ontology map gains an experiment colour family; a concept page joins the docs.
  • Pydantic models regenerated with the datamodel-code-generator==0.28.5 that pyproject.toml pins and synced into src/py/mat3ra/esse/models/ (details below).

Why it composes what it composes

process/step reuses workflow/unit/mixins/base unchanged, so a process is walked with the same head/next/flowchartId mechanics as a workflow: a deposition recipe and a calculation are the same kind of object to a traversal. system/activity exists so measurement and process cannot grow two different status vocabularies. instrument/instrument_property mirrors job/compute_property so "has an instrument" is defined once.

Examples are real records

Not invented: an HTEM combinatorial library (6705) and one of its 44 positions, an Asylum Research Jupiter DART piezoresponse scan and switching spectroscopy with their actual scan parameters, and a sputter-and-anneal run from the NLR lab system.

Two shapes corrected by real data

Both were found by validating a real record against the schemas, not by inspection:

  1. activity.operators was reusing the bibliographic author schema, which requires a surname. A lab record identifies an operator by whatever the instrument logged, often a single username, so it is now a name and an optional affiliation.
  2. vendor and model are no longer required on an instrument. Identity comes from the name the entity mixin supplies, and a lab record often knows a chamber only by the short name its operators use (pdac_com5).

Tightened after a review pass

Each of these was confirmed with a probe record against the resolved schemas before and after (commit 1104d14):

  • Technique unions on process, stage and instrument use anyOf, not oneOf. A record classified only to tier1 satisfied both branches and oneOf rejected it as ambiguous, so a run not yet classified below tier1 could not be stored at all.
  • The stage composes system/activity instead of re-declaring status and timing by hand, which was exactly the drift the mixin exists to prevent.
  • A sample's _parent is a system/_sample reference, as measurement._sample already was; a parent pointing at a Material is now rejected.
  • The technique subtype vocabularies were defined but referenced by nothing, leaving subtype a free string. They are now referenced from each branch, so a misspelt subtype fails validation.
  • The step keeps the workflow-unit status vocabulary and its description says why: run-level states such as cancelled belong to the stage and process, which carry the activity vocabulary.

And the guard tests the existing suites did not make, in tests/js/experimentalSchemas.tests.ts (commits e06e451, 9b24c75):

  • a holder whose source is a measurement validates, and the job-sourced example still does (the regression test for the one existing schema this PR widens);
  • a measurement source without a measurement id is rejected;
  • every schema this PR adds carries a description, because those become the pydantic docstrings and the map's detail panel;
  • no example carries an inline array over the budget stated on array_data;
  • the measurement example's parameters validate against measurement/parameters/scanning_probe_microscopy, which nothing narrowed until now.

How the models were synced

The pre-commit hook writes datamodel-codegen output to dist/py, which is gitignored, while the committed models live under src/py/mat3ra/esse/models/ as a real directory rather than the link pyproject.toml anticipates. Commit caec728 copies the hook's output over the committed tree, generated with the pinned 0.28.5 so untouched modules keep their headers. To make the 270-file diff reviewable, this is what it contains, measured against a regeneration on the base commit:

Files What
50 new modules for the schemas this PR adds; every one imports, with field descriptions as docstrings
9 changed in substance property/holder (source union and three data variants), properties_directory/enum_options, and seven property modules whose pressure, force or frequency unit enums gained the new units
152 changed by renumbering only the generator's global counter for de-duplicated class names (Units197 becomes Units221); safe to skip
58 removed, 14 refreshed drift that predates this branch: 54 modules whose schemas no longer exist, 4 stale duplicates of modules the generator places under a package, and 14 modules behind their dev schemas (job, workflow, unit execution)

Importing every module file: 670 of 700 succeed. The 30 that do not are the versioned apse directories, whose dotted names are not importable, and the title-derived Reusable_schema_for_energy_value… modules; both fail identically on dev and are a generator-configuration question, not this PR's.

Verification

npm run lint-entity-graph     # passes; 610 schemas, 0 cycles, 0 unresolved refs
npm test                      # 39 passing
python -m unittest discover --start-directory tests/py/esse/    # 267 passing
npm run build-docs-pages -- --output site && npm run check-site-links --site site

Example coverage rises from 209/564 (37%) to 254/610 (42%). Pinned graph counts in tests/js/entityGraph.tests.ts are updated from the lint output, not by hand.

The round trip is exercised end to end in a companion PR on mat3ra/parsers, which parses a real NLR deposition record into a process config and validates it against the schema added here.

Two things reviewers should decide

  • Whether the model drift lands here. The regeneration necessarily folds in the pre-existing drift listed above (54 orphaned modules removed, 14 refreshed). It is a single commit at the top of the branch, so it can be dropped from here and opened against dev on its own if that is preferred.
  • Scope of the relaxations. Removing required from instrument_properties and reshaping activity.operators are the two places this PR touches judgement rather than adding files.

Not in this PR

Vendor file formats under apse/ (Nanonis, Asylum, HTEM exports), technique catalogue entries beyond the parameter block, and the HTEM crosswalk. Those are phases 4 and 5 of the plan and want real files inspected first.

🤖 Generated with Claude Code

https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS


Generated by Claude Code

…ones

ESSE describes simulation: a material is characterized by properties produced
by a model through a workflow inside a job. Experimental work has the same
shape and different nouns, so this adds four root entities, each built as the
mirror of the computational one it corresponds to rather than as a separate
vocabulary:

  material              -> sample       a specimen; no basis/lattice required,
                                        nominal structure via _material
  software/application  -> instrument   vendor, model, serial, firmware, with
                                        probes and sources as components
  job                   -> measurement  instrument instead of compute, sample
                                        instead of material
  workflow              -> process      stages instead of subworkflows, steps
                                        instead of units, reusing the unit
                                        sequencing mixin unchanged

Properties deliberately do not fork. A measured value is an ordinary
property/holder: same data union, same exabyteId, same repetition. Only
source.info widens, from the exabyte job reference to a union over exabyte,
measurement and process. The measurement reference is shaped like the job
reference -- measurementId where a job has jobId, channel where it has unitId
-- so a consumer that already reads job provenance needs no new code path.

Supporting pieces the four entities compose:

  - core/abstract/multidimensional_array and core/reusable/array_data, for
    microscopy rasters and spectroscopy grids. core/abstract/3d_grid is a
    k-point mesh descriptor rather than a data grid, which is why this is new
  - core/reusable/file_reference, for binary instrument output referenced
    rather than embedded; system/file_source requires the content as text
  - core/reusable/quantity/*, environment and identifier
  - system/activity, the status, timing and operator mixin shared by
    measurement and process so their vocabularies cannot drift apart
  - techniques_category, narrowing the same CateCom tiers that categorize
    models and methods
  - film_thickness, surface_roughness and hysteresis_loop properties

Examples are built from real records rather than invented: an HTEM
combinatorial library and one of its positions, an Asylum Research DART
piezoresponse scan and switching spectroscopy, and a sputter-and-anneal run
from the National Laboratory of the Rockies lab system.

Two shapes were corrected by validating a real record against the schemas
instead of by inspection. Operators were reusing the bibliographic author
schema, which requires a surname; a lab record identifies an operator by
whatever the instrument logged, often a single username, so activity.operators
is now a name and an optional affiliation. And vendor and model are no longer
required on an instrument: identity comes from the name the entity mixin
supplies, and a lab record often knows a chamber only by the short name its
operators use.

Additive throughout: unit families are extended, never redefined; the holder
gains union branches without changing its required list; no existing $id moves.

Design and dataset survey: plan/upcoming/2026-09-02-experimental-data-*.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS
Both were found by parsing an actual laboratory deposition record and
validating the result against these schemas, rather than by inspection.

Operators reused core/reference/literature/name, which requires a surname
because a citation needs one. A laboratory record identifies an operator by
whatever the instrument logged, which is often a single username with no
surname to give. system/activity.operators is now a name and an optional
affiliation, plus a reference to a platform account for operators who have
one.

vendor and model were required on an instrument. A lab record frequently
knows a chamber only by the short name its operators use, such as pdac_com5,
with the vendor and model recorded elsewhere or not at all. Identity comes
from the name the entity mixin already supplies, so neither field is required
now; both remain documented as the useful things to record when known.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS
Each of these was confirmed against the resolved schemas with a probe record
before and after.

- Technique unions on process, stage and instrument use anyOf, not oneOf. A
  record classified only to tier1 satisfies both branches and oneOf rejected
  it as ambiguous, so a run whose technique was not yet classified below tier1
  could not be stored at all.
- The stage composes system/activity instead of re-declaring status and timing
  by hand, which was exactly the drift the mixin was introduced to prevent.
- A sample's _parent is a system/_sample reference, as measurement._sample and
  process._inputSamples already were; a parent pointing at a Material is now
  rejected, so the join key is as strict on the sample side as on the
  measurement side.
- The technique subtype vocabularies were defined but referenced by nothing,
  leaving subtype a free string. They are now referenced from each branch, so
  a misspelt subtype fails validation. Tying subtype to type is left to the
  per-narrowing leaf files a later phase adds, as models_category does.
- The step keeps the workflow-unit status vocabulary, and its description now
  says why: a step is a unit, and run-level states such as cancelled belong to
  the stage and process, which carry the activity vocabulary.
- Descriptions of parameters on measurement and stage no longer claim a
  catalogue directory that does not exist yet.
- docs/03 said the holder sits on one provenance reference; it is a union of
  three now. The map comment and legend say what the experiment family actually
  colours: the components and vocabulary, not the root hexagons, which take the
  root-entity colour like every root.
- The measurements context document gains a dated addendum with the new
  baseline, as the graph test's own comment requires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS
…e corpus

Four assertions the existing suites did not make:

- a property holder whose source is a measurement validates, and the
  job-sourced example still does -- the regression test for the source.info
  union, which is the one existing schema this change widens
- a measurement source without a measurement id is rejected, so the union
  cannot quietly accept an empty provenance
- every schema the experimental-data change added carries a description,
  because those become the pydantic docstrings and the map's detail panel
- no example carries an inline array over the budget stated on array_data,
  so nobody commits a real 256x256 image into examples.py by accident

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS
… probe block

Nothing narrows measurement.parameters until a catalogue entry binds a technique to
its block, so the example's parameters were never validated against
measurement/parameters/scanning_probe_microscopy. This asserts it explicitly, which
also gives that schema its first consumer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS
The pre-commit hook writes datamodel-codegen output to dist/py, which is
gitignored, while the committed models live under src/py/mat3ra/esse/models
as a real directory rather than the link pyproject.toml anticipates. This
copies the hook's output over the committed tree, generated with the
datamodel-code-generator 0.28.5 that pyproject pins, so untouched modules
keep the headers they have.

What changed and why, so the churn can be skipped in review:

- 50 new modules for the sample, instrument, measurement, process,
  techniques_category, core and property schemas this branch adds. Every
  one of them imports; the field descriptions become their docstrings.
- 9 modules change in substance: property/holder (the source.info union and
  the three new data variants), properties_directory/enum_options (the three
  new property names), and seven property modules whose pressure, force or
  frequency unit enums gained the experimental units.
- 152 modules differ only in the numeric suffixes the generator appends to
  de-duplicated class names. The counter is global across the run, so adding
  schemas renumbers every module generated after them.
- 54 modules are removed because their schemas no longer exist, and 4 stale
  duplicates go where the generator now places the module under a package
  (software/application, material/metadata, workflow/unit/context/item). 14
  modules were also behind their schemas on dev (job, workflow and the unit
  execution schemas). That drift predates this branch; a clean regeneration
  cannot carry it, and it is spelled out here so it is reviewed knowingly.
- materials_category/defective_structures/three_dimensional/__init__.py stays
  the empty file it has been since 69ac2e8, because the generator does not
  emit it and it was added by hand.

Verified by importing every module file: 670 of 700 import. The 30 that do
not are the versioned apse directories, whose dotted names are not importable
as packages, and the title-derived Reusable_schema_for_energy_value modules;
both fail identically on the base commit and are left for the generator
configuration to fix separately.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS
timurbazhirov pushed a commit to mat3ra/parsers that referenced this pull request Sep 2, 2026
…the process schema

The released package returns None for get_schema_by_id("process"), which the
validation test then handed to jsonschema and failed on a TypeError. The schema
arrives with mat3ra/esse#428; until a release carrying it is the floor in
pyproject.toml, the test is skipped with that reason, so the parser tests run
everywhere while the cross-repository check keeps its teeth wherever the schema
exists. Verified both ways: 3 passed, 1 skipped against mat3ra-esse
2026.8.31.post0, which is what CI installs; 4 passed against an editable install
of the ESSE branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS
timurbazhirov pushed a commit that referenced this pull request Sep 2, 2026
…VP is

The review's change request (l) asked how dist/py codegen output reaches the
committed models before anything was built. Answer, established while landing
#428: the hook's output is copied verbatim over
src/py/mat3ra/esse/models with the pinned generator, and the committed tree is
nothing but that output. The overview now also points at the MVP PR and its
parser round trip so the plan says where the work actually is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pSxTJQwszq7y7n4SKRQRS
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