# Writing an Analyzer An analyzer is a class based on `Analyzer`, from `plesty.lib.analyzer`. It declares what it takes and what it gives, and implements the calculation in one method. [An Analyzer](../quickstart/analyzer.md) in the quick start builds one step by step. ## The contract ```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() # settings, such as a calibration, go here result = normalizer(spectrum=raw_counts) # call the object, not analyze() result["normalized"].unit # "a.u.", taken from output_schema result["normalized"].provenance # {"analyzer": ..., "params": ...} ``` - **`input_schema`** names each input, with its type (`dtype`), unit and description. - **`output_schema`** names each output the same way. - **`analyze()`** takes the inputs as keyword arguments and returns a dictionary with exactly the outputs of `output_schema`. - An analyzer may also override **`report()`**, which describes what the analysis found; see [Reports](reports.md). Calling the analyzer object runs four steps around `analyze()`: 1. **Check the inputs**: their names, types and shapes must match `input_schema`. A plain NumPy array becomes a `PlestyArray` (a NumPy array that also carries a name, unit and description) with the unit from the schema. 2. **Run `analyze()`**. 3. **Check the outputs**: the returned dictionary must have exactly the names in `output_schema`. A missing name, unit or description is filled in from the schema. 4. **Record the provenance**: every array or table in the outputs gets a `provenance` attribute naming the analyzer's class, its module, its package's version and the settings it was created with. A structured result such as a `SpectrumData` keeps the provenance it already had, and gains these entries. ## Types in a schema A schema's `dtype` uses the same names as a device's parameters (see [Data & Metadata Schemas](../common/data_schemas.md)): | `dtype` | Means | |---|---| | `float`, `int`, `str`, `bool` | a single value | | `array_`, such as `array_float`, `array_uint16` | a NumPy array of that type | | `table2d` | a table | | the kind of a structured data type, such as `spectrum` | that type, for example a `SpectrumData` (see [Experiment data](../common/data_schemas.md#experiment-data)) | An array may declare a `shape`, where `None` means a dimension of any length: `"shape": [None, 2]` is any number of rows with two columns. An input may declare `"required": False`; `analyze()` then needs a default for it. ## Rules - **An analyzer only computes.** It never talks to a device and never imports a device module; it receives data that a device or an experiment produced. - **Settings are constructor arguments**, never values hidden in the module. That is what puts them into the provenance of every result, so a result always says how it was made. - **`analyze()` is an ordinary function, not `async`.** An experiment that must not wait for it runs it in a thread (for example with `asyncio.to_thread`). - **Experiments call analyzers**, not the other way round: a step method creates an analyzer, calls it, and returns the outputs, which are then saved with the step. - **Interactive tools stay outside.** A window for choosing a region or previewing a result belongs next to the analyzer, producing its inputs or settings, not inside it. ## What will not change The contract above has not changed since plesty-lib 0.3.2, and an analyzer written against it will keep working. The parts that are fixed: the two schemas, the single `analyze()`, the four steps of calling the analyzer, the provenance, and the tests of [Testing an Analyzer](../testing/analyzer.md). New additions are possible, such as a new optional schema entry or a new `dtype`, since they do not break an existing analyzer. Anything else would take a new major version, announced in advance. If the contract cannot express what your analyzer needs, open an issue on [plesty-lib](https://gitlab.com/plesty/core/plesty-lib/-/issues) rather than working around it.