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:
my_lab.double mtrl.resample.pchip— the short aliasdoubleresolves to your qualified name, andresample_pchipresolves to the built-in one.{'factor': 2.0, 'y_field': 'absorbance'}— the effective parameters of the scale operation: the caller-suppliedy_fieldrole beside the factor. The default catalogue declares no field-name default for a role-using operation.('numpy', 'scipy') interp— the distributions thatmtrl.resample.pchipdeclares, and the install extra that supplies SciPy.mtrl.transform.identity— the qualified name the built-in short nameidentityresolves to.my_lab.double— your own qualified name, reached through its aliasdouble.[2. 4.]— your adapter ran on a detached copy of the committed data, and the committed checkpoint still holds[1. 2.].my_lab.double True True— the capability view of the committedmy_lab.doublestep: locally registered and replay-eligible.()— the capability view names no missing distribution, because your descriptor declares no dependency requirement.
What just happened?
Section titled “What just happened?”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.
Choose the route your code needs
Section titled “Choose the route your code needs”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, andcustom=True. The recorded qualified identifier iscustom:<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.
Retired recipes are absent
Section titled “Retired recipes are absent”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') interpmtrl.transform.identitymy_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.
Where next
Section titled “Where next”- Look underneath a result — the registry, short names, and qualified names introduced from the user’s side.
- Use scientific Python libraries — the direct callable route, which records a step without registering it.
- Attach author disclosure — passive source bundles, which stay inert data.
- Reference — the full table of built-in short names and their qualified operations, and the archive safety contract.