# 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()`: ```python 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: 1. **Input validation** — names, dtypes, and shapes are checked against `input_schema`; plain arrays are coerced to `PlestyArray` with the schema unit. 2. **`analyze()`** — the subclass transform, pure compute. 3. **Output validation** — the returned dict must carry exactly the `output_schema` names; missing `name`/`unit`/`description` metadata is filled from the schema. 4. **Provenance stamp** — every array/table result gets a `provenance` attribute: analyzer class, module, package version, and the constructor parameters. Schema entries use the same dtype vocabulary as device schemas ([`resolve_dtype`](data_schemas.md)): `float`, `int`, `str`, `bool`, `array_`, `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](experiment.md). Experiments needing concurrency wrap analyzer calls in an executor. - **Calibration is a constructor parameter**, never hidden module state — it lands in `params` and 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 `test_analyzer_subclass` | Public `Analyzer` subclass, instantiable with the given params | | 2 `test_schema_integrity` | Both schemas non-empty, dtypes resolvable, shapes well-formed | | 3 `test_analyze_signature` | `analyze()` accepts exactly the declared input names | | 4 `test_mock_roundtrip` | Synthetic inputs produce outputs matching `output_schema` | | 5 `test_provenance_stamped` | Results carry the provenance stamp | ```python # 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).