MetaModel

class MetaModel()

Bases: QObject

Abstract base class for models.

Public Methods

Abstract Methods

These methods must be implemented by subclasses.

(none)

Concrete Methods

MetaModel.cache_plot_data(data: List[ndarray], labels: List[str]) → None
MetaModel.call(metaclass: str, key: str, method: str, *args: Any, **kwargs: Any) → Any

Call a method on a data plugin and return its result.

The replacement for emitting global_signal and reading the answer back off an attribute. Failures raise here, at the call site, rather than being logged and swallowed several hops away - which is the whole point of the change: the old path returned silently on four separate conditions, and a caller could not tell a failure from a stale value.

Keyword arguments are accepted, which the signal bus could not carry - its call_args was a positional tuple. That matters because 48 methods on the data-plugin bases have default parameters, several of them deep in the signature: MetaEventFinder.get_event_data_generator has three and MetaReader.continuous_read five. Positionally, setting the last one means passing every earlier one too, and a signature that later gains a parameter in the middle shifts every call site silently, so naming the argument is the safer habit.

Returns Any, so mypy cannot catch a renamed plugin method or a misspelled keyword. That is the accepted price of keeping the plugin API string-keyed: a plugin author must not have to know about app-shell internals to write one.

Parameters:
  • metaclass (str) – the plugin family, e.g. "MetaDatabaseLoader"

  • key (str) – the instance’s unique key

  • method (str) – the name of the method to call on it

  • *args (Any) – positional arguments for that method

  • **kwargs (Any) – keyword arguments for that method

Returns:

whatever the plugin method returned

Return type:

Any

Raises:

AttributeError – if the instance has no such method, or it is not callable

MetaModel.discard_generator(channel: int, key: str) → None

Clear the run state for one (key, channel) once its worker thread has finished.

Formerly reset_lock, which reset no lock - it clears the thread_running flag, which is what allows set_generator() to stage a new run for this key and channel, and drops the spent generator.

The generator is explicitly closed before being dropped. A plugin that declares serial channel operations holds its own lock across the generator’s yields, and that lock is released when the generator is exhausted or closed; relying on the reference count falling to zero to finalize it would make release depend on garbage-collection timing, and a stranded lock here is a silent hang rather than an error. close() is a harmless no-op on a generator that already ran to completion.

The finished Worker/WorkerThread pair is also popped out of self.workers/self.threads and scheduled for deletion here, for the same reason: leaving them in the dicts kept every generator closure and the channel data it touched alive for the rest of the session, and made stop_workers/handle_kill_worker log “Stopping worker for channel N” for runs that had finished hours earlier. Both are QObject``s created on (and never moved from) this thread, so ``deleteLater() is the correct way to release them rather than waiting on Python’s reference counting.

Parameters:
  • channel (int) – the channel whose run has finished

  • key (str) – the plugin key whose run has finished

MetaModel.emit_progress_update(progress: float, identifier: str) → None

Emit the progress update signal

MetaModel.emit_text_to_display(text: str, identifier: str) → None

Relay a worker’s message to the status panel.

Workers run on their own thread and report an early stop this way rather than by raising it as an ERROR record, which QtHandler would turn into a modal dialog.

Parameters:
  • text (str) – The message to show the user.

  • identifier (str) – The reporting worker’s key/channel identifier.

MetaModel.format_cache_data() → DataFrame | None

Lay the cached plot series out side by side, ready to be written as CSV.

The series need not be the same length - a plot may cache an x array and a shorter y - so the short ones are padded to the longest.

Padding is done with ``None`` rather than by coercing to float, which is what this used to do. arr.astype(float) assumed every cached series was a numeric array, and two things broke on that: a series cached as a plain list has no astype at all, and a series of text categories cannot become float even as an array. The categorical histogram caches its category names, so its export raised for as long as it has existed, on any revision. Letting pandas infer each column keeps the numeric columns exactly as they were - None in a numeric column is NaN, which is the empty field the old padding produced - while a text column now survives as text.

The columns are built by position and named afterwards, because a plot of several datasets caches each one under the same axis labels. A mapping keyed by label keeps only the last series of each label, so an overlay exported only its final dataset; pandas allows repeated column names, and every series reaches the file.

Returns:

one column per cached series, or None if nothing is cached

Return type:

Optional[pd.DataFrame]

MetaModel.generate_report(channel: int, key: str) → None

Put one plugin’s status for one channel onto the display panel.

Runs when a worker thread finishes, over a queued connection - so it is a Qt slot, and must not raise. call reports failure by raising, which is right at an ordinary call site and wrong here, where there is nothing above to catch it. The old bus swallowed and logged instead, several hops away; this keeps the swallow but puts it where the call is.

Parameters:
  • channel (int) – the channel whose status to report

  • key (str) – the plugin to ask

MetaModel.logscale_and_filter_columns(*data: ndarray[tuple[int, ...], dtype[Any]], log_flags: Sequence[bool] | None = None) → Tuple[ndarray[tuple[int, ...], dtype[Any]], ...]

Filters multiple data columns for NaN values and applies logarithmic scaling.

Public, because concrete Models call it, and the only copy - see DECISIONS.md 2026-09-14 for why it lives here rather than on MetaView.

This function takes an arbitrary number of 1D NumPy arrays as input. It first removes any data points (rows) where any of the input arrays contain a NaN value. Then, it optionally applies a base-10 logarithmic scale to specified columns. When applying log scale, it handles potentially negative data by ‘rectifying’ it based on its average sign and filters out any non-positive values after rectification. This filtering is applied sequentially, meaning filtering based on one column affects all others.

Parameters:
  • *data (npt.NDArray[Any]) – A variable number of 1D NumPy arrays representing the data columns.

  • log_flags (Optional[Sequence[bool]]) – A sequence of booleans, one for each data array. If True, the corresponding array will be log-scaled. If None, no log scaling is applied. Defaults to None.

Raises:

ValueError – If log_flags is provided but is not a list or tuple with the same length as the number of data arguments.

Returns:

A tuple containing the processed 1D NumPy arrays. The number of arrays returned matches the number of input arrays.

Return type:

Tuple[npt.NDArray[Any], …]

MetaModel.run_generators(key: str) → None

Start one worker thread per channel for which a generator has been staged under this key.

Serialization is not decided here. A plugin that declares force_serial_channel_operations() now takes its own lock inside its generator (see serialize_channels()), so this method no longer asks the plugin anything and no longer hands the worker a lock. It used to emit global_signal for that answer and read it back off self.serial_ops on the next statement, which worked only because every hop in that chain was a same-thread automatic connection Qt resolves as a direct call - one Qt.QueuedConnection anywhere in it would have silently degraded the lock to None, with no error and no log line.

Parameters:

key (str) – the plugin key whose staged generators should be run

MetaModel.set_generator(generator: Generator[float, bool | None, None], channel: int, key: str, metaclass: str) → None

Add generator and set it to be run by a QThread.

MetaModel.set_plugin_instances(instances: Mapping[str, Mapping[str, object]]) → None

Receive the live data plugin instances, keyed by metaclass then by key.

Called by MetaController whenever the app’s plugin set changes. Subclasses should not need to override it.

Parameters:

instances (Mapping[str, Mapping[str, object]]) – metaclass name -> plugin key -> live instance

Returns:

None

Return type:

None

MetaModel.stop_workers(key: str | None = None, channel: int | None = None, exiting: bool = False) → None

Stop workers based on specified key and/or channel.

MetaModel.time_bases(traces: Sequence[ndarray[tuple[int, ...], dtype[float64]]], samplerate: float, scale: float = 1.0, offset: float = 0.0) → List[ndarray[tuple[int, ...], dtype[float64]]]

Build the time axis for each trace.

The time base is a property of the samples and the rate they were taken at, both of which the Model already owns, so it is derived here rather than in the widget that draws it. Pure styling stays in the View; see DECISIONS.md 2026-09-14.

One array per trace, index-aligned with traces, because the traces need not be the same length - an event may contribute its filtered data, its fit and its raw trace, and a PSD run may drop a channel.

scale and offset exist because the same derivation serves two axes that differ only in units and origin: an event plot wants microseconds from the start of the event (scale=1e6), and a trace plot wants seconds from the start of the recording (offset=start). Four of the five tabs plot event traces this way, which is why this sits on the common base rather than on an event-tab intermediate that does not exist.

Parameters:
  • traces (Sequence[npt.NDArray[np.float64]]) – the sample arrays to build a time axis for

  • samplerate (float) – the sampling rate in Hz, or 1 to fall back to sample indices

  • scale (float) – multiplier applied to the seconds axis, 1e6 for microseconds

  • offset (float) – added after scaling, to place the axis in the recording

Returns:

one time array per trace

Return type:

List[npt.NDArray[np.float64]]

MetaModel.update_available_plugins(available_plugins: Dict[str, List[str]]) → None

Called whenever a new plugin is instantiated elsewhere in the app, to keep an up to date list of possible data sources for use by this plugin.

Parameters:

available_plugins (Dict[str, List[str]]) – dict of lists keyed by MetaClass, listing the identifiers of all instantiated plugins throughout the app.

Private Methods

Abstract Methods

These methods must be implemented by subclasses.

abstractmethod MetaModel._init() → None

Perform additional initialization specific to the algorithm being implemented. Must be implemented by subclasses.

This function is called at the end of the class constructor to perform additional initialization specific to the algorithm being implemented. kwargs provided to the base class constructor are available as class attributes.