Source code for pylorenzmie.analysis.Feature

import numpy as np
import pandas as pd
from pylorenzmie.analysis import Mask, Estimator, Optimizer
from pylorenzmie.theory import LorenzMie, Particle
from pylorenzmie.lib import LMObject
from pylorenzmie.lib.types import Image, Coordinates, Properties


[docs] class Feature(LMObject): '''A holographic feature associated with a single particle. Bundles image data, pixel coordinates, a pixel mask, a generative scattering model, an initial-parameter estimator, and an optimizer for a single particle crop. Parameters ---------- data : numpy.ndarray, optional Normalized hologram crop. coordinates : numpy.ndarray, optional Pixel coordinates of shape ``(2, npts)``. mask : Mask, optional Pixel selection mask. Default: ``Mask()``. model : LorenzMie, optional Generative scattering model. Default: ``LorenzMie()``. fixed : list[str], optional Model properties held constant during fitting. Default: the Optimizer default (``['noise', 'numerical_aperture']``). ''' def __init__(self, data: Image | None = None, coordinates: Coordinates | None = None, mask: Mask | None = None, model: LorenzMie | None = None, fixed: list[str] | None = None) -> None: super().__init__() self._coordinates = None self._data = None self.mask = mask or Mask() self.model = model or LorenzMie() self.data = data self.coordinates = coordinates self.estimator = Estimator(instrument=self.model.instrument) self.optimizer = Optimizer(model=self.model) if fixed is not None: self.optimizer.fixed = fixed @property def mask(self) -> Mask: '''Pixel selection mask.''' return self._mask @mask.setter def mask(self, mask: Mask) -> None: self._mask = mask self._mask_data() @property def data(self) -> Image: '''Normalized hologram crop.''' return self._data @data.setter def data(self, data: Image) -> None: self._data = data self._mask_data() def _mask_data(self) -> None: if self.data is None: return self.mask.shape = self.data.shape self.mask.exclude = ( (self.data == np.max(self.data)) | np.isnan(self.data) | np.isinf(self.data) ) @property def coordinates(self) -> Coordinates: '''Pixel coordinates, shape ``(2, npts)``.''' return self._coordinates @coordinates.setter def coordinates(self, coordinates: Coordinates) -> None: self._coordinates = coordinates @property def particle(self) -> Particle: '''Particle associated with the scattering model.''' return self.model.particle @particle.setter def particle(self, particle: Particle) -> None: self.model.particle = particle @property def model(self) -> LorenzMie: '''Generative scattering model.''' return self._model @model.setter def model(self, model: LorenzMie) -> None: self._model = model @property def fraction(self) -> float: '''Fraction of pixels passed to the optimizer.''' return self.mask.fraction @fraction.setter def fraction(self, fraction: float) -> None: self.mask.fraction = fraction @LMObject.properties.getter def properties(self) -> Properties: '''Feature pipeline configuration.''' return dict(fraction=self.mask.fraction)
[docs] def estimate(self) -> pd.Series: '''Estimate initial particle parameters from the hologram crop. Sets z_p, a_p, n_p on the particle from the azimuthal profile. Sets x_p, y_p to the center of the feature in the coordinate system of :attr:`coordinates` (local or frame). Returns ------- properties : pandas.Series Estimated particle properties. ''' properties = self.estimator.estimate(self.data) self.particle.properties = properties if self.coordinates is not None: self.particle.x_p = float(self.coordinates[0].mean()) self.particle.y_p = float(self.coordinates[1].mean()) return properties
[docs] def optimize(self) -> pd.Series: '''Optimize particle parameters to fit the hologram crop. Returns ------- result : pandas.Series Fitted values, uncertainties, and goodness-of-fit statistics. ''' mask = self.mask() self.optimizer.data = self.data[mask] ndx = np.nonzero(mask.ravel()) self.model.coordinates = np.take( self.coordinates, ndx, axis=1).squeeze() return self.optimizer.optimize()
[docs] def hologram(self) -> Image: '''Hologram predicted by the current model over all pixels. Returns ------- hologram : numpy.ndarray Predicted intensity, same shape as :attr:`data`. ''' self.model.coordinates = self.coordinates return self.model.hologram().reshape(self.data.shape)
[docs] def residuals(self) -> Image: '''Difference between the predicted hologram and the data. Returns ------- residuals : numpy.ndarray ``hologram() - data``, same shape as :attr:`data`. ''' return self.hologram() - self.data
[docs] @classmethod def example(cls) -> None: # pragma: no cover from time import perf_counter from pylorenzmie.utilities import example_hologram data = example_hologram() feature = cls() feature.data = data feature.coordinates = feature.model.meshgrid(data.shape) feature.mask.fraction = 0.25 instrument = feature.model.instrument instrument.wavelength = 0.447 instrument.magnification = 0.048 instrument.n_m = 1.34 particle = feature.model.particle particle.r_p = [data.shape[1] / 2., data.shape[0] / 2., 330.] particle.a_p = 1.1 particle.n_p = 1.4 feature.optimizer.variables = 'x_p y_p z_p a_p n_p'.split() print(f'Initial estimates:\n{particle}') feature.model.hologram() # warm up JIT / caches start = perf_counter() result = feature.optimize() print(f'Refined estimates:\n{particle}') print(f'Time to fit: {perf_counter() - start:.3f} s') print(feature.optimizer.report())
if __name__ == '__main__': # pragma: no cover Feature.example()