base#

Base procedure classes for implementing experimental workflows in Mesofield.

This module defines a generic Procedure orchestrator that contains zero device-specific logic. Custom experiment subclasses live outside the package (typically under experiments/<name>/procedure.py) and are launched via load_procedure(). A subclass points at its self-contained experiment.json (params + embedded hardware rig) through the class-level Procedure.experiment path.

Lifecycle (subclass hooks shown in bold):

  1. initialize_hardware – bring devices up

  2. prerunsubclass hook (default: no-op)

  3. hardware.arm_all – per-run prep on every device

  4. connect hardware.primary.signals.finished -> _cleanup_procedure

  5. on_startedsubclass hook (default: no-op)

  6. hardware.start_all

  7. on_finishedsubclass hook (default: no-op)

  8. save_data + cleanup

mesofield.base.processor(*, camera, plot=False, **plot_kwargs)[source]#

Mark a Procedure method as a per-frame compute function.

The decorated function is called as func(self, img, idx, ts) and should return a float | None. At procedure init the framework builds a FrameProcessor that wraps it, attaches it to the hardware device whose device_id matches camera, registers it on DataManager, and (when plot=True) tells the GUI to add a SerialWidget.

plot_kwargs are forwarded straight to the widget — recognized keys: label, value_label, value_units, y_range, value_scale, max_points.

Example:

class MyProcedure(Procedure):
    @processor(camera="meso", plot=True, label="Frame Mean")
    def frame_mean(self, img, idx, ts):
        return float(img.mean())
Parameters:
class mesofield.base.ProcedureSignals[source]#

Bases: QObject

All procedure-level signals that a Qt GUI can connect to.

class mesofield.base.Procedure[source]#

Bases: object

Generic orchestrator for a Mesofield experiment.

Subclass this in experiments/<name>/procedure.py and override the extension hooks (prerun(), on_started(), on_finished()) and/or the lifecycle methods (run(), save_data(), cleanup()) as needed. The base class never references a specific device type – multi-camera sync is driven by the YAML primary: true flag and HardwareManager.start_all/stop_all.

__init__(config=None, *, hardware=None, experiment_directory=None, **params)[source]#

Build a procedure.

Parameters#

config:

Path to a self-contained experiment.json – experiment parameters plus an optional embedded hardware rig block. When omitted, the class-level experiment path (if any) is used.

hardware:

Optional rig override: a path to a hardware.yaml file, an in-memory rig mapping, or a list of already-constructed device objects (e.g. Procedure(hardware=[LickDetector(port="COM3")])). A device list/mapping replaces any rig embedded in config; a lone device is the primary by default. None falls back to the embedded rig or the define_hardware() hook.

experiment_directory:

Where acquisition data is written (<dir>/data/sub-.../ses-...). Relative paths resolve against the current working directory. Overrides any value from define_config / JSON.

**params:

Any other experiment parameters (subject, session, task, duration, …) set straight onto the config. These also override define_config / JSON.

Parameters:
  • config (str | None)

  • hardware (Any | None)

  • experiment_directory (str | None)

  • params (Any)

initialize_hardware()[source]#

Boot up hardware and a DataManager.

Return type:

None

load_config(hardware=None, experiment=None)[source]#

Hot-load an experiment JSON and/or hardware YAML into the live config.

The JSON (params + any embedded rig) is applied first; an explicit hardware path then overrides whatever rig the JSON embedded. Callers pass explicit paths (the GUI wizard resolves them from its pickers).

Parameters:
  • hardware (str | None)

  • experiment (str | None)

Return type:

None

define_config()[source]#

Subclass hook to declare experiment parameters in Python.

Override to return a @dataclass instance or a plain mapping; it is applied to config via ExperimentConfig.load_dict(), superseding any experiment.json. Default None -> load JSON.

Return type:

Any

define_hardware()[source]#

Subclass hook to construct hardware devices in Python.

Override to return a list of pre-built device objects (imported and instantiated in the procedure file). They are handed to a fresh HardwareManager, superseding any hardware.yaml. Default None -> load YAML. Device classes should be decorated with @DeviceRegistry.register(...) so the setup can later be exported to a hardware.yaml rig file via HardwareManager.to_yaml.

Return type:

Any

prerun()[source]#

Subclass hook called before arming devices. Override as needed.

Return type:

None

await_trigger()[source]#

Gate the run after arming and before starting devices.

When start_on_trigger is set and a start_gate has been injected (e.g. by the GUI ConfigController), it is invoked here to own the “ready / press to start” interaction; returning False cancels the run. Devices are armed but nothing has started yet, so blocking here holds the whole run. Headless runs with no gate do not block.

This default contains no device-specific logic. Subclasses may still override it for a fully custom trigger.

Return type:

None

on_started()[source]#

Subclass hook called immediately after start_all.

Return type:

None

on_finished()[source]#

Subclass hook called immediately after the primary device finishes.

Return type:

None

property is_running: bool#

True while a run is in progress (started and not yet finished).

Used to refuse destructive actions mid-recording — e.g. a hardware hot-reload, which would tear down devices and abandon their writers.

run()[source]#

Drive a standard experiment run.

Subclasses may override, but the default body is generic and handles any combination of devices declared in hardware.yaml.

Return type:

None

cleanup()[source]#

Public cleanup entry-point (manual stop).

Return type:

None

launch(*, splash=True)[source]#

Open the Mesofield GUI for this procedure and block until closed.

The ordinary-Python alternative to mesofield launch: build the procedure in a script, then call proc.launch(). The acquisition is started from the GUI’s Record button (which calls run()), so the window comes up ready to configure and record.

Returns the Qt application exit code. splash=False skips the ASCII splash screen. The heavy GUI dependencies are imported lazily so headless scripts that never call this keep a light import footprint.

Parameters:

splash (bool)

Return type:

int

run_until_finished(timeout=None)[source]#

Run the procedure and block until cleanup completes.

Starts the procedure via run(), then waits for the procedure_finished (or procedure_error) signal. Handles KeyboardInterrupt and timeout by invoking cleanup() automatically, so callers (e.g. __main__ blocks in experiment scripts) do not need to wire up their own threading events.

Parameters#

timeout:

Optional hard ceiling in seconds. When None (default), waits indefinitely for the primary device’s finished signal. When provided, forces cleanup if the deadline passes.

Returns#

bool

True if the procedure finished on its own, False if cleanup was forced by timeout or interrupt.

Parameters:

timeout (float | None)

Return type:

bool

manifest_extra()[source]#

Override to inject extra session-level metadata into the AcquisitionManifest’s extra block. Default: empty.

Return type:

Dict[str, Any]

mesofield.base.load_procedure(target, **params)[source]#

Build a Procedure for a launch target.

target may be a canonical rig name, the literal dev, or a path to a procedure.py, an experiment.json, a hardware.yaml, or a directory containing them. None (or an unresolvable name) opens in a default state for the Configuration Wizard.

Directory precedence: procedure.py (custom subclass) > experiment.json (self-contained config) > hardware.yaml (rig only).

Parameters:
  • target (str | None)

  • params (Any)

Return type:

Procedure