Skip to content

Control the operation registry

You want to offer a custom operation of your own, or to lock an application down so it offers only the operations you have approved.

The registry is core behaviour: sample.transform takes your own callable, and an approved named operation is committed through sample.core.transform as shown here.

This guide assumes: you have finished the tutorial path, in particular Look underneath a result, where you first used a registered operation and met the registry, short names, and qualified names.

Every Mtrl application resolves operation names through a registry: the set of named, versioned operations that one application instance knows how to run. By default each instance builds its own registry of the core built-ins. When you construct the application you can inject a registry you built yourself, which is the hook for both halves of this problem: add a trusted operation of your own, or start empty and approve each operation by hand.

import numpy as np
from mtrl import (
InstrumentRun,
MaterialData,
Mtrl,
OperationDescriptor,
OperationRegistry,
)
def double(data):
return data * 2.0
registry = OperationRegistry.with_builtins()
registry.register(
OperationDescriptor(
qualified_identifier="my_lab.double",
aliases=("double",),
implementation_package="my_lab",
replay_eligible=True,
),
double,
)
snapshot = registry.snapshot()
print(snapshot.aliases["double"], snapshot.aliases["resample_pchip"])
print(
snapshot.descriptors["mtrl.arithmetic.scale"].parameters(
{"factor": 2.0, "y_field": "absorbance"}
)
)
pchip = snapshot.descriptors["mtrl.resample.pchip"]
print(tuple(pchip.supported_dependency_versions), pchip.dependency_extras["scipy"])
application = Mtrl(registry=registry)
run = InstrumentRun(run_name="ftir-replicate-01")
checkpoint = MaterialData(
content=np.array([1.0, 2.0]),
type="raw",
checkpoint_name="signal",
)
run.add_checkpoint(checkpoint)
application.add(run)
result = application.transform(run.run_id, checkpoint.checkpoint_id, "identity")
print(result.lineage.provenance.qualified_identifier)
doubled = application.transform(run.run_id, checkpoint.checkpoint_id, "double")
print(doubled.lineage.provenance.qualified_identifier)
print(doubled.content)
view = application.capability_view(run.run_id, doubled.checkpoint_id)
for step in view.operations:
print(step.qualified_identifier, step.registered, step.replay_eligible)
print(view.missing_distributions)

The example asks the registry what it holds before it runs anything. The printed lines are:

  1. my_lab.double mtrl.resample.pchip — the short alias double resolves to your qualified name, and resample_pchip resolves to the built-in one.
  2. {'factor': 2.0, 'y_field': 'absorbance'} — the effective parameters of the scale operation: the caller-supplied y_field role beside the factor. The default catalogue declares no field-name default for a role-using operation.
  3. ('numpy', 'scipy') interp — the distributions that mtrl.resample.pchip declares, and the install extra that supplies SciPy.
  4. mtrl.transform.identity — the qualified name the built-in short name identity resolves to.
  5. my_lab.double — your own qualified name, reached through its alias double.
  6. [2. 4.] — your adapter ran on a detached copy of the committed data, and the committed checkpoint still holds [1. 2.].
  7. my_lab.double True True — the capability view of the committed my_lab.double step: locally registered and replay-eligible.
  8. () — the capability view names no missing distribution, because your descriptor declares no dependency requirement.

OperationRegistry.with_builtins() created a fresh registry pre-loaded with the approved core built-ins, and registry.register(descriptor, adapter) added your operation to it. The descriptor is pure data: a qualified identifier, optional short-name aliases, and the contracts the operation declares. The adapter is a local callable. MTRL holds the callable in the registry only; it is never serialised into an archive. What the archive records is the qualified identifier and the effective parameters, written into the new checkpoint’s provenance.

registry.snapshot() returns one frozen view of the registry: descriptors keyed by qualified identifier, aliases mapped to qualified identifiers, and the resolved operations. The view copies its binding maps, so it cannot change the registry and the registry cannot change it. Ask the snapshot, not the archive, what one operation accepts.

descriptor.parameters({...}) validates the names you pass against the parameter schema and returns the effective parameters: your supplied values merged over the declared defaults. A name the schema does not declare fails before any adapter runs, and the archive records the same effective mapping in the new checkpoint’s provenance. The descriptor also declares its implementation package, its contract version, its supported dependency ranges, and its install extras. The capability view reads the same declarations for one committed target. It reports each required operation as registered or not and as replay-eligible or not. It also names every declared requirement, the installed version, and each missing distribution.

Passing registry=registry made this application use your registry instead of building a default one. Registries are per-application: two Mtrl instances share nothing unless you hand them the same registry object. For a locked-down application, start from an empty OperationRegistry() instead of with_builtins() and register only the descriptors you approve. Any other name then fails at the registry with UnknownOperationError, and the message lists the available local choices; through the application.transform route the same failure surfaces as InvalidTransformationError.

Two owner labels are reserved. The owner is the first dotted segment of the qualified identifier: mtrl belongs to the core built-ins, and custom belongs to ad-hoc callables passed directly to application.transform. The public register path refuses both and raises ReservedOperationOwnerError. The owner must also match the descriptor’s implementation_package, compared after normalisation that lowercases the text and folds underscores to hyphens, so a reserved owner cannot hide behind an alternate spelling. That is why the example declares implementation_package="my_lab" to match the my_lab.double identifier.

One guarantee ties this together: archive content can never register or import an operation. Loading an archive does not import operation code, does not change a registry, and does not execute stored source. A recipient who replays your chain resolves each step only through their own local registry, so a recorded identifier is a name to resolve, never code to run.

Three routes can take local code into a record, and they differ in what the archive holds. Choose the weakest route that answers your question.

  • Direct callable — application.transform(...) records one custom provenance step: the callable name, the supplied parameters, and custom=True. The recorded qualified identifier is custom:<function name>. Replay refuses the step.
  • Passive source bundle — application.attach_source_bundle(...) records inert author text bound to one custom output record. A bundle never registers an operation and never grants replay.
  • Reviewed registration — registry.register(descriptor, adapter) records the qualified identifier, the contract version, the implementation facts, and your replay decision. The step is replay-eligible only when the descriptor says so.

Start with the first route while you explore. A direct callable gets no registry identifier and no alias, so no other application can resolve it by name. The record still marks it with the reserved owner custom, as custom:<function name>. The interoperability guide shows that route.

Use the second route to record how a step was produced. MTRL never imports, compiles, or executes bundle text, and it never resolves the bundle through the registry, so the bundle explains the step but enables nothing. The disclosure guide shows the attach call and its limits.

Choose the third route when other people must resolve the name. You supply the identity, the implementation package, the contracts, and the replay flag yourself. The registry checks the namespace rules and the owner match, and it infers no scientific contract. replay_eligible defaults to False, so a new operation stays recorded only until its author states otherwise. Set replay_eligible=True only when a fresh run can reproduce the recorded output from the same inputs.

OperationRegistry.with_builtins() takes no arguments and registers exactly the eight general operations:

snapshot = OperationRegistry.with_builtins().snapshot()
print(sorted(snapshot.aliases))
my_lab.double mtrl.resample.pchip
{'factor': 2.0, 'y_field': 'absorbance'}
('numpy', 'scipy') interp
mtrl.transform.identity
my_lab.double
[2. 4.]
my_lab.double True True
()
['identity', 'mean', 'median', 'offset', 'resample_cubic', 'resample_linear', 'resample_pchip', 'scale']

The output walks the same story as the code: your alias beside a built-in one, the scale parameters it declares, the dependency distributions of the SciPy resamples, the canonical identity of a built-in step beside your custom one, the doubled values your function returns, and the capability view that marks your step as local-only with an empty tuple of portable requirements.

mtrl.arithmetic.normalise-peak and mtrl.arithmetic.zero-at-point are gone: no catalogue, module, alias, or function body supplies them. snapshot.resolve(name) reports either name as UnknownOperationError, and no keyword argument restores it.

Historical archives still work — loading and verification treat recorded operation metadata as data, so an old archive loads and verifies without those bindings. A replay plan reports such a step as unregistered, and a rerun refuses it before any mutation. Record the equivalent calculation as an ordinary callable transform, then select the committed state that step produced. See Prepare an ML dataset.

The read-only views mtrl.operations.base.TRANSFORMATIONS and mtrl.operations.base.DEFAULT_PARAMETERS describe the six current transforms only. Every field role is required, so apply() raises InvalidTransformationError when a call omits one. New code calls application.transform with its own function, or registers a reviewed descriptor.