Handle selection and data errors
A lookup can find nothing, a name can match several runs, and the commit boundary can refuse your data. This guide shows how MTRL reports each case and how you recover.
sample.select returns every match and sample.one raises LookupError for none or several; commit-boundary refusals are unchanged, so the core error names below also apply to facade calls.
This guide assumes: you have finished your first record, so you know that a commit creates one revision.
When a lookup finds nothing
Section titled “When a lookup finds nothing”MTRL offers two kinds of lookup. An optional lookup returns None when it finds no match. A
call that needs the record refuses to guess: it raises an exception and changes nothing.
Selection never repairs the archive for you. A failed selection adds no record and creates no revision, so the committed history stays exactly as it was.
import numpy as np
from mtrl import ( InstrumentRun, MaterialData, Mtrl, UnknownCheckpointError, UnknownRunError,)
application = Mtrl(sample_id="polymer-film-01")run = InstrumentRun(run_name="ftir-replicate-01")checkpoint = MaterialData( content=np.array([0.15, 0.21, 0.19]), type="raw", checkpoint_name="spectrum",)run.add_checkpoint(checkpoint)application.add(run)revisions_before = len(application.revisions)
# An optional lookup reports a miss as None and never raises.print(f"run lookup: {application.get_run('no-such-run')}")print(f"checkpoint lookup: {application.get_active_record('no-such-checkpoint')}")
# A call that needs the record fails closed.try: application.active_ref("no-such-checkpoint", run_id="no-such-run")except UnknownRunError as error: print(f"missing run: {error}")
try: application.active_ref("no-such-checkpoint", run_id=run.run_id)except UnknownCheckpointError as error: print(f"missing checkpoint: {error}")
print(f"revisions unchanged: {len(application.revisions) == revisions_before}")
# Recover with the identifiers that the commit kept.committed_run = application.data[run.run_id]checkpoint_id = next(iter(committed_run.checkpoints))ref = application.active_ref(checkpoint_id, run_id=run.run_id)print(f"active checkpoint: {ref.checkpoint_id == checkpoint_id}")print(f"active record: {ref.record_id == committed_run.checkpoints[checkpoint_id].record_id}")run lookup: Nonecheckpoint lookup: Nonemissing run: No run 'no-such-run' in this Mtrl containermissing checkpoint: No checkpoint 'no-such-checkpoint' in run '01a1830d-aa2b-74fe-82e5-292fa5b5bb07'revisions unchanged: Trueactive checkpoint: Trueactive record: TrueThe optional lookups return None; the strict lookups name the missing target and the next
useful action, and the container is unchanged afterwards. The identifier inside the second
report is your own, so it differs from this page.
When a run name repeats
Section titled “When a run name repeats”A run name is for people. It is not an identifier, because MTRL never deduplicates two runs and
never renames one. Two replicate measurements can carry one name, and Mtrl.get_runs_by_name()
returns both, in the order you added them.
Count the matches before you use a name. A guard that refuses zero or several matches keeps the choice visible in your notebook. The guard belongs to your own code: MTRL raises no exception here, because a repeated name is valid.
import numpy as np
from mtrl import InstrumentRun, MaterialData, Mtrl
def select_one_run(application: Mtrl, run_name: str) -> InstrumentRun: """Return the one run with this name, or refuse to choose one.""" matches = application.get_runs_by_name(run_name) if len(matches) != 1: raise ValueError( f"Expected exactly one run named {run_name!r}, found {len(matches)}. " "Choose the run by run_id, or give each run a distinct name." ) return matches[0]
application = Mtrl(sample_id="polymer-film-01")for offset in (0.0, 0.01): run = InstrumentRun(run_name="ftir-replicate") run.add_checkpoint( MaterialData( content=np.array([0.15, 0.21, 0.19]) + offset, type="raw", checkpoint_name="spectrum", ) ) application.add(run)
background = InstrumentRun(run_name="ftir-background")background.add_checkpoint( MaterialData( content=np.array([1.0, 0.98, 1.01]), type="raw", checkpoint_name="spectrum", ))application.add(background)
print(f"runs named ftir-replicate: {len(application.get_runs_by_name('ftir-replicate'))}")
for run_name in ("ftir-replicate", "ftir-replicate-09"): try: select_one_run(application, run_name) except ValueError as error: print(f"{type(error).__name__} -> {error}")
# A unique name selects one run. A repeated name needs an exact identifier.print(f"unique name: {select_one_run(application, 'ftir-background').run_name}")
matches = application.get_runs_by_name("ftir-replicate")chosen = application.get_run(matches[1].run_id)print(f"chosen is the second match: {chosen is matches[1]}")runs named ftir-replicate: 2ValueError -> Expected exactly one run named 'ftir-replicate', found 2. Choose the run by run_id, or give each run a distinct name.ValueError -> Expected exactly one run named 'ftir-replicate-09', found 0. Choose the run by run_id, or give each run a distinct name.unique name: ftir-backgroundchosen is the second match: TrueThe count report names both failures — two matches and zero matches — with the next action each time. A distinct name resolves cleanly, and selecting by position picks the second match.
The exception is a plain ValueError from the guard in this script. It is not an MTRL error, and
it does not select a record. The last two lines choose one run by its run_id, because a name
does not guarantee one match. A run_id names one run, a checkpoint_id names one logical
checkpoint, and Mtrl.active_ref() returns the pair that names the active record version.
When the commit boundary refuses your data
Section titled “When the commit boundary refuses your data”The commit does not reshape your candidate. You convert the DataFrame yourself, and the commit
validates the content it receives, detaches it, and stores the values it supports. After you
convert the DataFrame, the commit checks each NumPy field dtype. A field with an object, text,
datetime, or timedelta dtype fails the commit. A numeric value that arrived as text fails for
that reason: the field would store text, not a number.
The rule concerns the resulting NumPy dtype, not the pandas dtype or the column name. A categorical column whose records carry numbers can yield supported numeric fields. A categorical column whose records carry datetimes, text, or other values needs an explicit conversion first, because the commit judges the NumPy field it would store.
Mtrl.add() rejects the whole run at the commit boundary and keeps no part of it. Convert the
column explicitly, then commit the measurement again.
Required install: vector-mtrl[tabular]
import pandas as pd
from mtrl import ( InstrumentRun, MaterialData, Mtrl, UnsupportedCommittedValueError,)
application = Mtrl(sample_id="polymer-film-01")revisions_before = len(application.revisions)
# The measured values arrived as text, not as numbers.frame = pd.DataFrame( { "wavenumber": [2000.0, 2500.0, 3000.0], "absorbance": ["0.15", "0.21", "0.19"], })print(f"values before conversion: {frame['absorbance'].tolist()}")
run = InstrumentRun(run_name="ftir-replicate-01")run.add_checkpoint( MaterialData( content=frame.to_records(index=False), type="raw", checkpoint_name="spectrum", ))try: application.add(run)except UnsupportedCommittedValueError as error: print(f"refused at the commit boundary: {error}")
print(f"runs: {len(application.data)}")print(f"revisions unchanged: {len(application.revisions) == revisions_before}")
# Convert the numbers explicitly, then commit the same measurement again.numeric_frame = frame.assign(absorbance=pd.to_numeric(frame["absorbance"], errors="raise"))print(f"values after conversion: {numeric_frame['absorbance'].tolist()}")
recovered = InstrumentRun(run_name="ftir-replicate-01")recovered.add_checkpoint( MaterialData( content=numeric_frame.to_records(index=False), type="raw", checkpoint_name="spectrum", ))application.add(recovered)
print(f"runs: {len(application.data)}")print(f"one revision added: {len(application.revisions) == revisions_before + 1}")values before conversion: ['0.15', '0.21', '0.19']refused at the commit boundary: Structured field 'absorbance' has unsupported dtype dtype('O'). Object, datetime, timedelta, fixed-string, Unicode, and void field dtypes are rejected.runs: 0revisions unchanged: Truevalues after conversion: [0.15, 0.21, 0.19]runs: 1one revision added: TrueThe text values, the refusal that names the absorbance field and its object dtype, the
unchanged state, and then the converted floats committing cleanly — one run, one revision.
The refusal names the
one revision added: True.
errors="raise" is deliberate. errors="coerce" replaces an unreadable value with NaN,
which is a missing-value marker and not a measurement. A float field accepts NaN, so a
coercing conversion would store a gap that you did not choose. Decide the missing-data policy
first: convert, fill, or drop the row, and then commit.
What just happened?
Section titled “What just happened?”Each failure above changed nothing that MTRL stores. A refused commit adds no record and no revision, and a strict selection raises before it touches the archive. The committed history stays intact, so you can retry the same measurement after you fix the candidate.
Two rules keep later steps correct. First, a name does not identify a record. A run_id names
one run, a checkpoint_id names one logical checkpoint, and Mtrl.active_ref() returns the pair
that names the active record version. Second, the commit boundary is the place where MTRL checks
the data, so a successful commit means the stored values already had a supported type.
A lookup adds no record and no revision. The commit boundary does not parse numeric text, does
not choose between matching runs, and does not overwrite an existing record. For the
one-checkpoint runs in these examples, a fresh candidate with fresh identifiers adds one record
and one revision when you commit it. To store the same measurement twice, build a fresh
candidate with a new run_id: re-adding a run with a run_id that the container already holds
raises DuplicateRunError and changes nothing, whether or not the earlier candidate was
committed locally.
Where next
Section titled “Where next”- Reference — every call shape, its status, and the errors it raises.
- Two kinds of identity — why a name and an identifier answer different questions.
- Fix a mistake — the recorded route for a correction, which keeps both versions.