Skip to content

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 units

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

Create one clean environment and install the package from PyPI:

Terminal window
uv venv --python 3.14
uv pip install --python .venv/bin/python vector-mtrl

Check the import with that environment’s interpreter:

Terminal window
.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.

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"] = axis
measurement["absorbance"] = 0.015 * (axis - 2000.0) / 1000.0
measurement["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.0
print(f"recorded peak: {round(float(raw.to_numpy()['absorbance'].max()), 4)}")
print(sample.summary())
fields: ('wavenumber', 'absorbance')
units declared: 2; active records: 1
recorded peak: 1.0075
Sample 'polymer-film-01' — 1 active record; 0 active claims
Records (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.

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.

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.