Record your first measurement
In this lesson you will install MTRL and record an FTIR measurement of a polymer film. By the end you will have named fields, declared units and a result you can inspect without accidentally changing it. It takes about ten minutes.
You need Python 3.12 or newer, below Python 4, and a terminal. The base package is enough for the whole tutorial; you do not need plotting or spreadsheet tools to get started.
A place for the sample and its measurements
Section titled “A place for the sample and its measurements”Think of a Sample as the folder on your lab bench for one specimen. Here the specimen is
a polymer film labelled polymer-film-01. Each measurement has its own run, and a
checkpoint gives a result within that run a lasting identity. A record is one exact,
immutable version of that result.
Sample your polymer film and its history└── Run one FTIR measurement └── Checkpoint the measured spectrum └── Record this exact version of its values and unitsYou do not need to build that structure yourself. One sample.record(...) call puts the
measurement in the right place and hands you back a result handle: an object that lets
you read the exact version you recorded. We will look more closely at those identities
in lesson 4. For now, the important distinction is between your working array and the
committed record that MTRL keeps.
These nine lessons follow the same film through one continuous session. Each lesson adds
a little new work to the same Sample, so it is worth keeping your Python session open
as you move through them.
Set up a small working environment
Section titled “Set up a small working environment”Create one clean environment and install the package from PyPI:
uv venv --python 3.14uv pip install --python .venv/bin/python vector-mtrlCheck the import with that environment’s interpreter:
.venv/bin/python -c "from mtrl import Sample; print(Sample.__name__)"You should see: Sample. Use that same interpreter for the examples in every lesson.
Give your spectrum fields and units
Section titled “Give your spectrum fields and units”A spectrum needs more than a list of numbers: a reader needs to know which column locates
each point and which column holds the measured value. Our two fields, or named columns,
are wavenumber and absorbance. A NumPy structured array keeps them together, and the
units argument declares what their values mean.
You write each unit as the text your instrument or notebook reports, such as "cm^-1" or
"a.u.". MTRL matches the spellings it knows against the QUDT standard vocabulary — inverse
length for wavenumber, time, or dimensionless for arbitrary units — and keeps your exact
text alongside. A unit it does not recognise is preserved verbatim, never rewritten or
invented. If two fields disagree, MTRL tells you rather than converting behind your back.
The example makes 101 synthetic points with a rising baseline and one peak. These stand in
for your instrument’s values, so you can follow along without finding a data file first.
Save the example as first_record.py and run it with .venv/bin/python first_record.py.
Watch the last few lines: they deliberately change the working array after recording it.
import numpy as np
from mtrl import Sample
axis = np.linspace(2000.0, 3000.0, 101)measurement = np.empty( axis.size, dtype=[("wavenumber", "<f8"), ("absorbance", "<f8")],)measurement["wavenumber"] = axismeasurement["absorbance"] = 0.015 * (axis - 2000.0) / 1000.0measurement["absorbance"] += np.exp(-((axis - 2500.0) / 35.0) ** 2)
sample = Sample("polymer-film-01")raw = sample.record( measurement, name="ftir-replicate-01", units={"wavenumber": "cm^-1", "absorbance": "a.u."},)print(f"fields: {raw.to_numpy().dtype.names}")print(f"units declared: {len(raw.units)}; active records: {len(sample.records)}")
measurement["absorbance"] = 0.0print(f"recorded peak: {round(float(raw.to_numpy()['absorbance'].max()), 4)}")print(sample.summary())fields: ('wavenumber', 'absorbance')units declared: 2; active records: 1recorded peak: 1.0075Sample 'polymer-film-01' — 1 active record; 0 active claimsRecords (commit order): [0] 'ftir-replicate-01' 101 values, shape=(101,) fields: wavenumber (cm^-1), absorbance (a.u.)The first lines name your fields, count the two unit declarations and one recorded result. The last line is the useful surprise: even after you set the input absorbance to zero, the recorded peak is still there.
The last lines print sample.summary(): a one-screen recap of what this sample holds,
listing each active record in commit order with its name, value count, shape and declared
units. The text carries no generated identifiers, so the same recorded work always prints the
same
summary.
What just happened?
Section titled “What just happened?”The call to record committed one measurement and added one revision to the archive history.
At that boundary, MTRL detached the values from your input array. You can continue working
with the array, but edits to it no longer reach the committed record.
The handle raw gives you access to that record. Calling raw.to_numpy() returns another
detached, writable copy, not permission to edit the stored values. This separation is what
lets later lessons add processing and corrections without quietly rewriting the past.
The units travel with the fields, too. They tell a future reader what you declared, but they do not prove that the instrument was calibrated or that the measurement is correct.
Where next
Section titled “Where next”Your record currently lives only in memory. In
Save, load, and verify you will turn it into an archive you can
keep, share and check. Keep your session open: the next lesson carries on with sample.
When your own instrument file needs to come in, Author a connector
shows how to write the loader that brings it in.