Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ clarified; remove or rewrite entries that go stale.
- **Platform** — the hardware backend a protocol runs against (`DUMMY`, `QICK`,
`OPX`). Selected globally via the `PLATFORMTYPE` module variable in
`labcore.protocols.base`; parameters and operations dispatch to
platform-specific code (`_dummy_getter`, `_qick_getter`, …) based on it.
platform-specific code (`_dummy_getter`, `_measure_qick`, …) based on it.
Analysis uses a shared default unless an operation supplies a
platform-specific analysis.

- **Report** — a self-contained HTML document assembled by
`ProtocolBase._assemble_report()` after a protocol runs. Each operation
Expand Down
6 changes: 3 additions & 3 deletions docs/user_guide/protocols/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ super-operations, conditions, and the assembled report.
Every operation runs the same five steps in order, on every attempt:

```
◀── platform-specific ──▶ ◀───── platform-agnostic ──────▶
◀── platform-specific ──▶ ◀ optional override ▶ ◀─ platform-agnostic ─▶

measure ──▶ load_data ──▶ analyze ──▶ evaluate ──▶ correct
│ │ │ │ │
Expand All @@ -51,8 +51,8 @@ Every operation runs the same five steps in order, on every attempt:
```

- `measure` — performs the measurement (or generates fake data on `DUMMY`) and saves the raw data to disk.
- `load_data` — reads the raw data back into memory and normalizes its shape and field names so the rest of the lifecycle is platform-agnostic.
- `analyze` — runs fits and statistics over the loaded data and attaches the results to the operation.
- `load_data` — reads the raw data back into memory and normalizes its shape and field names so later steps can usually be shared across platforms.
- `analyze` — runs fits and statistics over the loaded data and attaches the results to the operation. It uses the default analysis unless the selected platform has a specialized implementation.
- `evaluate` — returns named check results and an overall status; pure assessment, no side effects.
- `correct` — the only place parameters get written: fitted outputs on success, a correction strategy on retry.

Expand Down
60 changes: 41 additions & 19 deletions docs/user_guide/protocols/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This page assumes you have read {doc}`parameters`.
## The lifecycle of an operation

```
◀── platform-specific ──▶ ◀───── platform-agnostic ──────▶
◀── platform-specific ──▶ ◀ optional override ▶ ◀─ platform-agnostic ─▶

measure ──▶ load_data ──▶ analyze ──▶ evaluate ──▶ correct
│ │ │ │ │
Expand All @@ -26,10 +26,11 @@ This page assumes you have read {doc}`parameters`.
```

The split between platform-specific and platform-agnostic steps is
deliberate: `analyze`, `evaluate`, and `correct` should run identically no
matter which backend produced the data. Whatever per-platform quirks exist
in field names, units, or array shapes have to be reconciled by
`load_data` so that everything downstream sees a single canonical shape.
deliberate: `evaluate` and `correct` should run identically no matter which
backend produced the data. `analyze` should normally be shared too, but it
can specialize for a platform when the analysis itself genuinely differs.
Per-platform quirks in field names and array shapes should still be
reconciled by `load_data` so later steps see a canonical structure.

- **`measure`** performs the measurement (or generates fake data on
`DUMMY`) and saves it to disk via the standard sweep + DDH5 machinery.
Expand All @@ -39,12 +40,17 @@ in field names, units, or array shapes have to be reconciled by
data so that downstream steps see the same shape and variable names
regardless of platform**. Different backends can save data with
different field names or slightly different shapes; reconciling those
differences here is what lets `analyze` be platform-agnostic. Stores
differences here is what lets analysis use a shared default in most
operations. Stores
the result on the operation as `independents` and `dependents`
dictionaries. Dispatches to `_load_data_dummy` / `_load_data_qick` /
`_load_data_opx`.
- **`analyze`** is platform-agnostic. Run your fits, compute summary
statistics, attach results to `self`. Do **not** mutate parameters here.
- **`analyze`** dispatches to `_analyze_dummy`, `_analyze_qick`, or
`_analyze_opx`. Each inherited platform hook falls back to
`_analyze_default`, so most operations only implement that default.
Override a platform hook only when its analysis genuinely differs. Run
fits, compute summary statistics, and attach results to `self`; do
**not** mutate parameters here.
- **`evaluate`** is **pure assessment**. It returns named check results
and an overall status (`SUCCESS` / `RETRY` / `FAILURE`). No side
effects. By default this just runs every check registered with
Expand Down Expand Up @@ -115,8 +121,9 @@ an attribute on the operation. After the calls above, `self.center()`,
get verified before the protocol runs; outputs are written by `correct()`
on success; correction parameters skip the hardware verification check.

Platform-specific work — measurement and data loading — is split exactly
the way parameter getters and setters are:
Platform-specific work is split into hooks in the same way as parameter
getters and setters. Measurement and data loading require a hook for each
supported platform; analysis additionally provides a shared default:

```python
def _measure_dummy(self) -> Path:
Expand All @@ -131,13 +138,28 @@ def _load_data_dummy(self) -> None:
data = datadict_from_hdf5(self.data_loc / "data.ddh5")
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def _analyze_default(self) -> None:
# shared fitting and statistics for every platform without an override
...

def _analyze_qick(self) -> None:
# optional: replace the default only when QICK analysis genuinely differs
...
```

The base class's `measure()` and `load_data()` dispatch to the right
method based on the platform selected with
The base class's `measure()`, `load_data()`, and `analyze()` dispatch to the
right method based on the platform selected with
{py:func}`select_platform <labcore.protocols.select_platform>`. You only
implement the platforms you actually run on; the others raise
`NotImplementedError` if invoked.
implement measurement and loading for platforms you actually run on; the
others raise `NotImplementedError` if invoked. For analysis, an omitted
platform hook automatically calls `_analyze_default`. A platform hook is a
complete replacement, but it can call `_analyze_default()` explicitly when
it only needs to add behavior.

Treat `analyze()` as the public lifecycle method called by the framework.
Normal operations should override `_analyze_default()` or one of the
platform hooks rather than overriding `analyze()` itself.

:::{note}
The leading underscore on methods like `_register_inputs`,
Expand All @@ -148,7 +170,7 @@ protocol, and let the framework call these for you. Whoever is **writing**
an operation absolutely does use them — in `__init__` and in overrides.
The same convention applies everywhere on this page (`_register_outputs`,
`_register_correction_params`, `_register_check`,
`_register_success_update`, `_measure_*`, `_load_data_*`, …).
`_register_success_update`, `_measure_*`, `_load_data_*`, `_analyze_*`, …).
:::

## Correcting itself
Expand Down Expand Up @@ -360,8 +382,8 @@ inline and the lmfit fit report dumped in a code block — all written by

Here is a complete, runnable operation that uses every concept introduced
above — a registered output, a registered check, a registered success
update, platform-specific `measure` and `load_data`, and a
platform-agnostic `analyze`. Copy it into a script, run it, and the
update, platform-specific `measure` and `load_data`, and a default analysis.
Copy it into a script, run it, and the
protocol will execute end-to-end on the `DUMMY` platform:

```python
Expand Down Expand Up @@ -417,7 +439,7 @@ class MinimalGaussianFit(ProtocolOperation):
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def analyze(self) -> None:
def _analyze_default(self) -> None:
with DatasetAnalysis(self.data_loc, self.name) as ds:
x = np.asarray(self.independents["x_values"])
y = np.asarray(self.dependents["y_values"])
Expand Down Expand Up @@ -469,7 +491,7 @@ That file maps onto the sections of this page like so:
| Registering a check + correction | `_register_check` call in `__init__` |
| Correction subclass | `_ReduceNoiseLevelCorrection` |
| Platform code | `_measure_dummy`, `_load_data_dummy` |
| Analyze | `analyze()` |
| Default analysis | `_analyze_default()` |
| Override of `correct()` | bottom of the class |

## Where to read next
Expand Down
29 changes: 28 additions & 1 deletion src/labcore/protocols/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,35 @@ def measure(self) -> Path:
return loc
raise NotImplementedError(f"Platform type {self.platform_type} not implemented")

def _analyze_default(self) -> None:
"""Analyze loaded data when no platform-specific implementation exists."""
raise NotImplementedError("Default analysis not implemented")

def _analyze_qick(self) -> None:
"""Analyze QICK data, falling back to the default implementation."""
self._analyze_default()

def _analyze_opx(self) -> None:
"""Analyze OPX data, falling back to the default implementation."""
self._analyze_default()

def _analyze_dummy(self) -> None:
"""Analyze DUMMY data, falling back to the default implementation."""
self._analyze_default()

def analyze(self) -> None:
raise NotImplementedError("Analyze method not implemented")
"""Dispatch analysis to the selected platform, with a default fallback."""
match self.platform_type:
case PlatformTypes.QICK:
self._analyze_qick()
case PlatformTypes.OPX:
self._analyze_opx()
case PlatformTypes.DUMMY:
self._analyze_dummy()
case _:
raise NotImplementedError(
f"Platform type {self.platform_type} not implemented"
)

def _load_data_opx(self) -> None:
raise NotImplementedError("Load OPX data method not implemented")
Expand Down
2 changes: 1 addition & 1 deletion src/labcore/testing/protocol_dummy/cosine.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def _load_data_dummy(self) -> None:
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def analyze(self) -> None:
def _analyze_default(self) -> None:
"""Fit the data to a Cosine"""
assert self.data_loc is not None
with DatasetAnalysis(self.data_loc, self.name) as ds:
Expand Down
2 changes: 1 addition & 1 deletion src/labcore/testing/protocol_dummy/exponential.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def _load_data_dummy(self) -> None:
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def analyze(self) -> None:
def _analyze_default(self) -> None:
"""Fit the data to an Exponential"""
assert self.data_loc is not None
with DatasetAnalysis(self.data_loc, self.name) as ds:
Expand Down
2 changes: 1 addition & 1 deletion src/labcore/testing/protocol_dummy/exponential_decay.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _load_data_dummy(self) -> None:
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def analyze(self) -> None:
def _analyze_default(self) -> None:
"""Fit the data to an Exponential Decay"""
assert self.data_loc is not None
with DatasetAnalysis(self.data_loc, self.name) as ds:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def _load_data_dummy(self) -> None:
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def analyze(self) -> None:
def _analyze_default(self) -> None:
"""Fit the data to an Exponentially Decaying Sine"""
assert self.data_loc is not None
with DatasetAnalysis(self.data_loc, self.name) as ds:
Expand Down
2 changes: 1 addition & 1 deletion src/labcore/testing/protocol_dummy/gaussian.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def _load_data_dummy(self) -> None:
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def analyze(self) -> None:
def _analyze_default(self) -> None:
"""Fit the data to a Gaussian"""
assert self.data_loc is not None
with DatasetAnalysis(self.data_loc, self.name) as ds:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def _load_data_dummy(self) -> None:
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def analyze(self) -> None:
def _analyze_default(self) -> None:
assert self.data_loc is not None
with DatasetAnalysis(self.data_loc, self.name) as ds:
x = np.asarray(self.independents["x_values"])
Expand Down
2 changes: 1 addition & 1 deletion src/labcore/testing/protocol_dummy/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def _load_data_dummy(self) -> None:
self.independents["x_values"] = data["x"]["values"]
self.dependents["y_values"] = data["y"]["values"]

def analyze(self) -> None:
def _analyze_default(self) -> None:
"""Fit the data to a Linear function"""
assert self.data_loc is not None
with DatasetAnalysis(self.data_loc, self.name) as ds:
Expand Down
Loading
Loading