PLESTY's pipeline has three tiers, and until now only two of them had a fixed shape. This month the middle tier — the analyzer — got one: a base class in plesty-lib that every analysis module builds on, a test suite the developer tool enforces before publishing, and a report each analysis writes about what it found. The new fine-structure analyzer, plesty-fss, is the pilot built on it end to end, and the contract is now declared stable.
What an analyzer is
An analyzer is a Python class, based on Analyzer from plesty.lib.analyzer, that turns data a device or experiment recorded into a result — the position of a peak, the location of an emitter on a chip, the splitting of two spectral lines. It declares three things:
input_schema— a dictionary naming each input, with its type (dtype), unit and description. The types are the same words device modules already use for their parameters:float,array_float,table2d(a table), or a structured data kind such asspectrum.output_schema— the same, for each output.analyze()— the calculation itself: one ordinary (notasync) method that takes the inputs as keyword arguments and returns a dictionary with exactly the declared outputs.
Because the interface is declared rather than implied, the base class can check data on the way in and out, and every result can say how it was made. That record is its provenance: a small dictionary attached to each output naming the analyzer class, its module, the installed package version, and every setting the analyzer was created with.
What happens when you call one
You never call analyze() yourself — you call the analyzer object, and the base class wraps your calculation in four steps:
input_schema. A plain NumPy array is converted and given the schema's unit.analyze()output_schema.provenance attribute.A wrong input fails loudly with an AnalysisValidationError that names the input and what was expected — before any computation runs.
Using an analyzer
For usersRun it and read what it made
from plesty.lib.data import SpectrumData
from plesty.fss import FineStructure
# A polarization sweep: one spectrum per half-wave-plate angle, read from a finished run.
spectrum = SpectrumData.from_run(run_dir, coordinates=("hwp_deg",))
# Settings go to the constructor — they end up in every result's provenance.
fss = FineStructure(angle="hwp_deg", rotator="hwp", fit_options={"peaks": [780.1], "window": 0.3})
outputs = fss(spectrum=spectrum)
result = outputs["fine_structure"]
result["fss"], result["fss_err"] # the splitting in µeV, ± σ, one per line
result.provenance # which analyzer, which version, which settings
The provenance travels with the result. Months later, a saved splitting still tells you which analyzer produced it, at which version, with which rotator and fit settings — no lab notebook needed to reconstruct it.
Keep a report of what it found
New this month: every analyzer can describe its result as a report — an ordered document of text, tables and figures, saved as a file next to the outputs. The file can later be turned into HTML or LaTeX by anyone, without having the analyzer installed:
from plesty.lib.report import REPORT_SUFFIX, Report
fss.report(outputs).save(run_dir / f"fine_structure{REPORT_SUFFIX}") # fine_structure.report.json
report = Report.load(run_dir / "fine_structure.report.json")
report.to_html() # tables, equations, figures as inline SVG
report.to_latex() # tables for booktabs, figures for pgfplots — paste into a paper
Values in a report carry their uncertainty and are printed only to the precision it allows, so 11.5 ± 0.3 never appears as 11.4987 ± 0.3. Rendering equations as MathML in HTML needs the optional report extra of plesty-lib; without it they show as their LaTeX source.
The pilot: plesty-fss
A contract is only credible once a real module fits it. plesty-fss measures the fine-structure splitting of a quantum-dot exciton: the tiny energy gap between its two emission components, which are polarized at right angles to each other. Turning a polarization element in front of the spectrometer makes each line's position oscillate with the angle, and the size of that oscillation is the splitting. The analyzer does it in two stages — SpectrumFit from plesty-lib fits every line in every spectrum, then a sine is fitted to each line's position against the angle.
| Declared | Name | Type | Meaning |
|---|---|---|---|
| input | spectrum | spectrum | The sweep: one spectrum per rotator angle, the angle kept as a coordinate |
| output | fine_structure | fine_structure, result | Per line: splitting in µeV and in wavelength, polarization axis, mean position — each ± σ — plus linewidth and a validity flag |
| output | fit | spectrum_fit, intermediate | The line fits of every spectrum |
| output | curve | array_float, intermediate | The fitted line position at each angle |
It exercises the whole contract, not only its minimum. Its input and main output are typed experiment data: plesty-lib's spectrum, and a type of its own, FineStructureData, registered as fine_structure so any other analyzer or experiment can declare it. fit and curve are declared intermediate — saved so a reader can check how the splitting was found, left out of the report. Its settings (angle, rotator, fit_options) are constructor arguments, and a bad fit option is refused when the analyzer is created rather than halfway through an analysis.
Its custom report() opens with a sentence on what the sweep found, then a table of the lines with the splitting in µeV and pm, then the sweep drawn exactly as the live view draws it in the bench console. The analyzer also refuses to overclaim: a line whose position swings by as much as its linewidth is flagged fss_valid = 0 and named in a warning, because the fit then followed a neighbouring line rather than measuring a splitting.
The older positionestimate analyzer, which locates emitters on a chip from microscope images, was also moved onto the contract this month and now meets the quantum standard.
title and role schema entries they use, are on the plesty-lib 0.5 line, currently published as development snapshots (0.5.0.devN). plesty-fss is built on that line and needs plesty-lib>=0.5.0.dev6.Writing your own analyzer
For developers & contributors1 · Scaffold the module
plesty-sdk, PLESTY's developer tool, creates a ready analyzer project — package layout, CI, and an example analyzer with its contract tests already wired:
plesty init analyzer
The generated analyzer.py holds a working example (normalising a signal to its peak). Replace its schemas and analyze() body with your own.
2 · Declare, compute, and optionally report
import numpy as np
from plesty.lib.analyzer import Analyzer
from plesty.lib.data import PlestyArray
from plesty.lib.report import Report, Text
class PowerPeak(Analyzer):
input_schema = {
"wavelength": {"dtype": "array_float", "unit": "nm", "description": "Scan wavelengths."},
"power": {"dtype": "array_float", "unit": "W", "description": "Power at each wavelength."},
}
output_schema = {
"normalized": {"dtype": "array_float", "unit": "a.u.",
"role": "intermediate"}, # kept, but left out of the report
"peak_wavelength": {"dtype": "float", "unit": "nm",
"title": "Peak wavelength"}, # role defaults to "result"
}
def analyze(self, wavelength, power):
peak = float(power.max()) or 1.0
return {
"normalized": PlestyArray(power / peak),
"peak_wavelength": float(wavelength[int(np.argmax(power))]),
}
# Optional: say more than the default table.
def report(self, outputs):
return Report([Text(f"Power peaks at {outputs['peak_wavelength']:.2f} nm.")],
title="Power peak", provenance=self.provenance())
Two optional entries on each output shape the default report: role is "result" (shown) or "intermediate" (kept in the outputs, left out of the report — say, the single-line fits a final number came from), and title heads its table. A custom report() can also use Table (with cells spanning rows or columns, and Value(x, err=…) for numbers with uncertainty) and Figure, built from the same plot elements as PLESTY's live data viewers.
3 · The rules the contract relies on
- An analyzer only computes. It never talks to a device or imports a device module — an experiment passes it the data.
- Settings are constructor arguments, including calibration data, never values hidden in module state. That is what puts them in the provenance of every result.
- Experiments call analyzers, not the other way round. An experiment that must not wait runs the analyzer in a thread, e.g.
asyncio.to_thread. - Interactive tools stay outside. A window for picking a region of interest produces the analyzer's inputs; it does not live inside it.
4 · Pass the contract tests gate a1
AnalyzerPipeline, in plesty.lib.test.analyzer_pipeline, checks an analyzer against its own declarations using inputs it generates from the schema — no real data needed:
| Test | Checks |
|---|---|
test_analyzer_subclass | the module exposes a public Analyzer subclass that can be created |
test_schema_integrity | both schemas are non-empty, with known types and well-formed shapes |
test_analyze_signature | analyze() takes exactly the declared inputs |
test_mock_roundtrip | generated inputs produce outputs that match output_schema |
test_provenance_stamped | the outputs carry their provenance |
# tests/test_pipeline.py — the scaffold writes this for you
PIPELINE = AnalyzerPipeline(PowerPeak, mock_inputs={"power": np.array([0.002, 0.008, 0.004])})
def test_mock_roundtrip():
PIPELINE.test_mock_roundtrip()
# … one function per test above
Supply realistic data in mock_inputs only where the calculation needs it (an image with actual features, a curve with a peak); everything else is generated as zeros of the declared shape. plesty check looks for these five tests and runs them as gate a1 before the module may be published. As of this month the round trip runs once and is shared by the last two tests, so a slow analysis is not computed twice per test run.
analyze(), the four steps of a call, the provenance and the five tests will not change. Additions that cannot break an existing analyzer — a new optional schema entry, a new type — may still come; anything else would need a new major version, announced in advance. If the contract cannot express what your analysis needs, open an issue on plesty-lib rather than working around it.plesty-lib also ships a first built-in analyzer on this contract, SpectrumFit, which fits the lines of a stack of spectra. Full documentation: the Analyzer Framework chapter and the An Analyzer quick-start page in the plesty-lib docs.