Skip to content

Record scientific assertions

Your archive holds measurements, but you also need to record conclusions such as “this film is PET” as typed, evidence-linked claims that can later be superseded or retracted without erasing what you first believed.

For the common case, sample.claim commits one operator-entered claim and sample.claims reads its active heads; evidence, typed subjects and the claim lifecycle stay core operations.

This guide assumes: you have finished the tutorial path. The concept page two kinds of identity is helpful background, because assertions lean on both record identity and physical sample identity.

A scientific assertion records one typed property value about the physical sample, one run, or one exact record. Each property has a property identifier (a namespaced lower-case token such as polymer.identity), and each assertion names the subject it describes through a SubjectRef. To make a claim about the physical sample, the container first needs a sample UID (a portable identity for the physical object, distinct from the human-facing sample_id). assign_sample_uid mints one for you.

import numpy as np
from mtrl import InstrumentRun, MaterialData, Mtrl
from mtrl.assertions import AssertionDraft, SubjectRef
application = Mtrl(sample_id="polymer-film-01")
sample_uid = application.assign_sample_uid()
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)
draft = AssertionDraft(
subject=SubjectRef.sample(sample_uid),
property_id="polymer.identity",
value="polyethylene terephthalate",
author="analyst",
)
assertion = application.assert_properties([draft])[0]
print(f"property: {assertion.property_id}; value: {assertion.value}")
for claim in application.list_assertions(property_id="polymer.identity"):
print(f"claim id: {claim.claim_id}; status: {claim.status.value}")
corrected = application.supersede_assertion(
assertion.assertion_id,
value="polypropylene",
reason="The reference library match favours polypropylene.",
)
print(f"supersedes the original: {corrected.supersedes == assertion.assertion_id}")
print(
f"corrected: {corrected.value}; "
f"original: {application.get_assertion(assertion.assertion_id).value}"
)
Terminal window
property: polymer.identity; value: polyethylene terephthalate
claim id: 01a1830d-adf8-7251-abc7-ccf5d3ef0490; status: asserted
supersedes the original: True
corrected: polypropylene; original: polyethylene terephthalate

The first printed line is property: polymer.identity; value: polyethylene terephthalate. The loop then prints the claim identifier (a UUID, so yours will differ) and the status asserted. After the supersede call, the third line is supersedes the original: True: the new version points back at the old one. The last line is corrected: polypropylene; original: polyethylene terephthalate, because the corrected version heads the claim while the original version remains readable in the history.

assign_sample_uid gave the container a portable physical sample identity, a UUIDv7 string, while leaving sample_id untouched. SubjectRef.sample(sample_uid) then aimed the draft at the physical film rather than at a run or a record.

assert_properties committed your draft as one revision. The batch form is atomic: if any draft is invalid, nothing changes. When you have a single claim, assert_property is the convenience route with the same fields as keyword arguments, for example application.assert_property(subject=..., property_id=..., value=...); it commits one claim in one revision.

Now the claim/version distinction, which is the heart of this feature. A claim is the continuing statement (“the identity of polymer-film-01”), and it keeps one ClaimId across its whole life. A version is one committed wording of that claim, and each version gets its own AssertionId. When supersede_assertion corrected your identification, it did not overwrite anything. It added a second version under the same ClaimId, marked as superseding the first AssertionId. Both versions stay in the audit history, which is why get_assertion could still read the original value.

retract_assertion withdraws a claim outright and requires a non-empty reason. Use list_assertions(active_only=False, include_retracted=True) to see every version, including retracted ones, and pass as_of_revision to read the claim state as it stood at an earlier revision. Independent claims about the same subject and property coexist, so a colleague’s claim never silently overwrites yours.