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.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_signaland 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_argswas 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_generatorhas three andMetaReader.continuous_readfive. 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:
- 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 thethread_runningflag, which is what allowsset_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/WorkerThreadpair is also popped out ofself.workers/self.threadsand 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 madestop_workers/handle_kill_workerlog “Stopping worker for channel N” for runs that had finished hours earlier. Both areQObject``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.
- 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.
- 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 noastypeat 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 -Nonein a numeric column isNaN, 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.
- 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.md2026-09-14 for why it lives here rather than onMetaView.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 (seeserialize_channels()), so this method no longer asks the plugin anything and no longer hands the worker a lock. It used to emitglobal_signalfor that answer and read it back offself.serial_opson the next statement, which worked only because every hop in that chain was a same-thread automatic connection Qt resolves as a direct call - oneQt.QueuedConnectionanywhere in it would have silently degraded the lock toNone, 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
MetaControllerwhenever the app’s plugin set changes. Subclasses should not need to override it.
- 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.md2026-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.scaleandoffsetexist 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]]
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.