Skip to content

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.

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}")
Terminal window
run lookup: None
checkpoint lookup: None
missing run: No run 'no-such-run' in this Mtrl container
missing checkpoint: No checkpoint 'no-such-checkpoint' in run '01a1830d-aa2b-74fe-82e5-292fa5b5bb07'
revisions unchanged: True
active checkpoint: True
active record: True

The 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.

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: 2
ValueError -> 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-background
chosen is the second match: True

The 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: 0
revisions unchanged: True
values after conversion: [0.15, 0.21, 0.19]
runs: 1
one revision added: True

The 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.

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.

  • 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.