ProteinModel

class ProteinModel(**kwargs)

Bases: MetaSubsetTabModel

Subclass of MetaSubsetTabModel for protein volume/shape-factor fitting data and storage.

Public Methods

ProteinModel.build_all_points_histogram(event_generator: Iterator[Dict[str, Any]], plot_type: str, bins: Any, sizes: bool) → DataFrame | None

Average every event’s fractional-blockage histogram into one.

The generator is walked twice, because the bins cannot be chosen until the extremes across all the events are known: the first pass takes the limits and the second counts into the edges they decide. Each event contributes its own shape rather than its length - the counts are divided by the event’s sample count before they are summed, and the total by the number of events - so a long event does not outweigh a short one.

Unlike the per-event binning, there is no Freedman-Diaconis fallback here: the rule is per-event sample count and interquartile range, and a histogram averaged over many events of different lengths has neither.

Parameters:
  • event_generator (Iterator[Dict[str, Any]]) – the events of one subset, as the loader yields them

  • plot_type (str) – ‘Raw Histogram’ or ‘Filtered Histogram’

  • bins (Any) – a bin count, or a bin width when sizes is True, or None

  • sizes (bool) – does bins refer to a bin width (True) or a count (False)

Returns:

the averaged histogram, or None if no event could be used

Return type:

Optional[pd.DataFrame]

Raises:

ValueError – if plot_type is unrecognised, or bins cannot be resolved

ProteinModel.build_event_histograms(events: Sequence[Dict[str, Any]], plot_type: str, bins: Any, sizes: bool) → List[Tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]] | None]

Bin each event’s fractional blockage, one histogram per event.

Each event is binned over its own range, so the edges of one subplot say nothing about its neighbours. The individual-distribution path used to let the limits accumulate across the events of a single plot, which made the edges depend on the order they arrived in; both paths follow the same rule now, which is the one the event-histogram path already documented.

An event that cannot be binned contributes None rather than being dropped, so the result stays index-aligned with events and the drawing half still lays out one subplot per event in the original order.

An unusable bin request or an unrecognised plot type raises out of the helpers rather than being absorbed per event: both are the same for every event, so reporting once is what the caller can act on.

Parameters:
  • events (Sequence[Dict[str, Any]]) – the events to bin, in the order they are drawn

  • plot_type (str) – ‘Raw Histogram’ or ‘Filtered Histogram’

  • bins (Any) – a bin count, or a bin width when sizes is True, or None

  • sizes (bool) – does bins refer to a bin width (True) or a count (False)

Returns:

one (bin centers, amplitude) pair per event, or None where none could be built

Return type:

List[Optional[Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]]]

ProteinModel.drop_fit_columns(loader: str, table: str, columns: Sequence[str]) → bool

Delete an existing set of fit columns so a new one can replace it.

Owns the SQL rather than assembling it in the Controller and using the Model as a conduit to the loader: the layer that makes the plugin call is the layer that writes what it sends. ClusteringModel.drop_cluster_columns is the same shape, for the same reason. DECISIONS.md (2026-08-25) accepts the f-string interpolation itself, because the database is a local file owned by the user running the app; this is about where the SQL lives.

Both statements are needed per column: one drops it from the events table and one removes its row from the columns metadata table, which is what tells the rest of the application the column exists.

Parameters:
  • loader (str) – the database loader’s plugin key

  • table (str) – the table the fit columns are in

  • columns (Sequence[str]) – the fit columns to remove

Returns:

True if the loader reported success

Return type:

bool

ProteinModel.fit_histogram(bins: ndarray[tuple[int, ...], dtype[float64]], amplitude: ndarray[tuple[int, ...], dtype[float64]]) → Tuple[ndarray[tuple[int, ...], dtype[float64]] | None, ndarray[tuple[int, ...], dtype[float64]] | None]

Fit a double gaussian to one histogram and evaluate it at the same bins.

The evaluated curve comes back alongside the parameters because every caller that wants a fit also draws it, and returning both is what keeps _double_gaussian out of the View entirely rather than moving the fit and leaving its evaluation behind.

Parameters:
  • bins (npt.NDArray[np.float64]) – numpy array of bin centers

  • amplitude (npt.NDArray[np.float64]) – numpy array of amplitude in bins

Returns:

(fit parameters, fitted curve at bins), or (None, None) if the fit failed its sanity checks

Return type:

Tuple[Optional[npt.NDArray[np.float64]], Optional[npt.NDArray[np.float64]]]

ProteinModel.fit_histograms(histograms: Sequence[Tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]] | None]) → List[Tuple[ndarray[tuple[int, ...], dtype[float64]] | None, ndarray[tuple[int, ...], dtype[float64]] | None]]

Fit a double gaussian to each histogram in turn.

The per-event plotting paths fit one histogram per event. Doing them in a single call keeps every answer off the widget: a per-event round trip would work - a same-thread Qt signal is synchronous, measured - but only by parking each answer somewhere for the loop body to read back, which is the pattern would be read back by mistake. It also means the View does not depend on the connection staying Direct.

None stands for an event whose histogram could not be built at all, and answers the same way as one that could not be fitted, so the result is index-aligned with the input either way and the caller skips exactly the events that have no fit.

Parameters:

histograms (Sequence[Optional[Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]]]) – (bins, amplitude) pairs, one per event, or None where no histogram could be built

Returns:

one (fit parameters, fitted curve) pair per input histogram, index-aligned with it

Return type:

List[Tuple[Optional[npt.NDArray[np.float64]], Optional[npt.NDArray[np.float64]]]]

ProteinModel.sample_event_geometries(fits: Sequence[Tuple[ndarray[tuple[int, ...], dtype[float64]] | None, ndarray[tuple[int, ...], dtype[float64]] | None]], histograms: Sequence[Tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]] | None], event_data: Sequence[Dict[str, Any]], d: float, L: float, N: int) → Tuple[DataFrame, DataFrame, DataFrame]

Sample every fitted event’s geometry, and summarise each one to a row.

Every list is index-aligned with event_data, so an event whose histogram could not be built and one whose fit was refused drop out the same way and neither shifts the others. The per-event row is the median of that event’s own ensembles, which is what the fit columns written back to the database hold.

Parameters:
  • fits (Sequence[Tuple[Optional[npt.NDArray[np.float64]], Optional[npt.NDArray[np.float64]]]]) – one (fit parameters, fitted curve) pair per event

  • histograms (Sequence[Optional[Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]]]) – one histogram per event, or None where none could be built

  • event_data (Sequence[Dict[str, Any]]) – the events that were fitted

  • d (float) – the diameter of the pore in nanometers

  • L (float) – the length of the pore in nanometers

  • N (int) – target number of samples to draw for each family

Returns:

every event’s prolate solutions, its oblate solutions, and one summary row per fitted event

Return type:

Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]

ProteinModel.sample_vm_solutions(popt: ndarray[tuple[int, ...], dtype[float64]], d: float, L: float, N: int) → Tuple[DataFrame, DataFrame]

Monte Carlo the prolate and oblate geometries one fit is consistent with.

A pair of blockage levels does not determine a shape: a long thin spheroid and a flat wide one can block the same pore by the same amounts. Both families are sampled, and it is for the reader to say which is physical.

Parameters:
  • popt (npt.NDArray[np.float64]) – the six fitted parameters, two amplitude/mean/sigma triples

  • d (float) – the diameter of the pore in nanometers

  • L (float) – the length of the pore in nanometers

  • N (int) – target number of samples to draw for each family

Returns:

the prolate and the oblate solutions

Return type:

Tuple[pd.DataFrame, pd.DataFrame]

Private Methods

ProteinModel._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.