Analyzer Framework
plesty.lib.analyzer formalizes the middle stage of the platform pipeline
device drivers → analyzers → experiments: an Analyzer converts raw
device data into meaningful data through a schema-declared, synchronous
transform. Analyzers never talk to hardware — they consume data-layer
objects produced by devices or experiments, and their results carry a
provenance stamp recording how they were derived.
The contract
A subclass declares two class-level schemas and implements exactly one
method, analyze():
from plesty.lib.analyzer import Analyzer
from plesty.lib.data import PlestyArray
class SpectrumNormalizer(Analyzer):
input_schema = {
"spectrum": {"dtype": "array_float", "unit": "counts",
"description": "Raw detector counts."},
}
output_schema = {
"normalized": {"dtype": "array_float", "unit": "a.u.",
"description": "Counts scaled to peak 1."},
}
def analyze(self, spectrum: PlestyArray) -> dict:
peak = float(spectrum.max()) or 1.0
return {"normalized": PlestyArray(spectrum / peak)}
normalizer = SpectrumNormalizer() # params/calibration go here
result = normalizer(spectrum=raw_counts) # validating __call__
result["normalized"].unit # "a.u." — filled from the schema
result["normalized"].provenance # {"analyzer": ..., "params": ...}
Calling the analyzer runs the base machinery around the transform:
Input validation — names, dtypes, and shapes are checked against
input_schema; plain arrays are coerced toPlestyArraywith the schema unit.analyze()— the subclass transform, pure compute.Output validation — the returned dict must carry exactly the
output_schemanames; missingname/unit/descriptionmetadata is filled from the schema.Provenance stamp — every array/table result gets a
provenanceattribute: analyzer class, module, package version, and the constructor parameters.
Schema entries use the same dtype vocabulary as device schemas
(resolve_dtype): float, int, str, bool,
array_<numpy_dtype>, table2d, … Arrays may declare a shape where None
marks a free dimension ([None, 2]); inputs may declare required: False.
Design rules
Synchronous by design. Analysis is CPU-bound compute, unlike the I/O orchestration of the experiment framework. Experiments needing concurrency wrap analyzer calls in an executor.
Calibration is a constructor parameter, never hidden module state — it lands in
paramsand therefore in the provenance of every result.No hardware access. An analyzer receives data-layer objects; it never imports device drivers.
Experiments call analyzers, not the other way around: a step method may instantiate an analyzer and persist its outputs through the normal run machinery — the provenance stamp survives in the result metadata.
GUI helpers stay outside the contract. Interactive tools (ROI selection, previews) live beside the analyzer and produce its inputs or parameters.
Contract gates: AnalyzerPipeline
plesty.lib.test.analyzer_pipeline.AnalyzerPipeline mirrors the device and
experiment harnesses with five contract gates, run entirely on synthetic
inputs generated from the schema:
Gate |
Verifies |
|---|---|
1 |
Public |
2 |
Both schemas non-empty, dtypes resolvable, shapes well-formed |
3 |
|
4 |
Synthetic inputs produce outputs matching |
5 |
Results carry the provenance stamp |
# tests/test_pipeline.py
from plesty.lib.test.analyzer_pipeline import AnalyzerPipeline
PIPELINE = AnalyzerPipeline(MyAnalyzer, mock_inputs={"frame": example_image})
def test_mock_roundtrip():
PIPELINE.test_mock_roundtrip()
Transforms that need realistic values (e.g. an image with actual features)
override individual synthetic inputs via mock_inputs; everything else is
generated (zeros of the declared shape, neutral scalars).