Source code for pylorenzmie.analysis.Feature
import numpy as np
import pandas as pd
from pylorenzmie.analysis import Mask, DEEstimator, Optimizer
from pylorenzmie.analysis.Hologram import Hologram
from pylorenzmie.theory import LorenzMie, Particle
from pylorenzmie.lib.lmtypes import Image
[docs]
class Feature(Hologram):
'''A holographic feature associated with a single particle.
A Feature is a Hologram of a single particle augmented with an
estimator and optimizer for localizing and characterizing the particle.
Parameters
----------
hologram : Hologram
Normalized hologram crop with pixel coordinates.
model : LorenzMie, optional
Generative scattering model. Default: ``LorenzMie()``.
fixed : list[str], optional
Model properties held constant during fitting.
Default: the Optimizer default.
'''
def __init__(self,
hologram: Hologram,
model: LorenzMie | None = None,
fixed: list[str] | None = None) -> None:
# Copy Hologram state directly; super().__init__() is skipped to avoid
# calling __post_init__ (which would rerun meshgrid on hologram.data).
self.data = hologram.data
self.corner = hologram.corner
self._coordinates = hologram._coordinates
self.model = model or LorenzMie()
m = Mask()
m.shape = self.data.shape
# TODO: detect bad pixels to exclude from Mask
m.exclude = (
(self.data == np.max(self.data))
| np.isnan(self.data)
| np.isinf(self.data)
)
self.estimator = DEEstimator(model=self.model, exclude=m.exclude)
self.optimizer = Optimizer(model=self.model, mask=m)
if fixed is not None:
self.optimizer.fixed = fixed
@property
def mask(self) -> Mask:
'''Shared pixel exclusion mask for estimator and optimizer.
Bad pixels (saturated, dead) set via :attr:`~Mask.exclude` are
excluded from both the DE estimation and the LM optimization.
Each component controls its own pixel fraction independently.
'''
return self.optimizer.mask
@mask.setter
def mask(self, mask: Mask) -> None:
self.estimator.exclude = mask.exclude
self.optimizer.mask = mask
@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
[docs]
def estimate(self) -> pd.Series:
'''Estimate initial particle parameters from the hologram crop.
Returns
-------
properties : pandas.Series
Estimated particle properties.
'''
return self.estimator.estimate(self)
[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.
Notes
-----
When a mask with ``fraction < 1.0`` is set, a fresh random pixel
subsample is drawn on every call.
'''
return self.optimizer.optimize(self)
[docs]
def predicted(self) -> Image:
'''Hologram predicted by the current model over all pixels.
Returns
-------
predicted : numpy.ndarray
Predicted intensity, same shape as this Feature.
'''
self.model.coordinates = self.flat_coordinates
return self.model.hologram().reshape(self.shape)
[docs]
def residuals(self) -> Image:
'''Difference between the predicted hologram and the data.
Returns
-------
residuals : numpy.ndarray
``predicted() - data``, same shape as this Feature.
'''
return self.predicted() - self.data
[docs]
@classmethod
def example(cls) -> None: # pragma: no cover
from time import perf_counter
from pylorenzmie.utilities import example_hologram
feature = cls(example_hologram())
instrument = feature.model.instrument
instrument.wavelength = 0.447
instrument.magnification = 0.048
instrument.n_m = 1.34
instrument.noise = 0.05
feature.estimator.fraction = 0.01
feature.optimizer.fraction = 0.25
feature.optimizer.variables = 'x_p y_p z_p a_p n_p'.split()
feature.model.coordinates = feature.flat_coordinates
feature.model.hologram() # warm up JIT / caches
start = perf_counter()
estimate = feature.estimate()
print(f'Estimated in {perf_counter() - start:.3f} s')
print(estimate)
start = perf_counter()
result = feature.optimize()
print(f'Optimized in {perf_counter() - start:.3f} s')
print(feature.optimizer.report())
if __name__ == '__main__': # pragma: no cover
Feature.example()