21from __future__
import annotations
23from copy
import deepcopy
28 "FactorizedComponent",
29 "default_fista_parameterization",
30 "default_adaprox_parameterization",
33from abc
import ABC, abstractmethod
34from functools
import partial
35from typing
import TYPE_CHECKING, Any, Callable, cast
40from .image
import Image
41from .operators
import Monotonicity, prox_uncentered_symmetry
42from .parameters
import AdaproxParameter, FistaParameter, Parameter, parameter, relative_step
43from .utils
import convert_indices
46 from .io
import ScarletComponentBaseData, ScarletCubeComponentData
50Logger = logging.getLogger(__name__)
54 """A base component in scarlet lite.
59 The bands used when the component model is created.
61 The bounding box for this component.
73 def bbox(self) -> Box:
74 """The bounding box that contains the component in the full image"""
79 """The bands in the component model"""
83 def resize(self, model_box: Box) -> bool:
84 """Test whether or not the component needs to be resized
86 This should be overriden in inherited classes and return `True`
87 if the component needs to be resized.
91 def update(self, it: int, input_grad: np.ndarray) ->
None:
92 """Update the component parameters from an input gradient
97 The current iteration of the optimizer.
99 Gradient of the likelihood wrt the component model
104 """Generate a model for the component
106 This must be implemented in inherited classes.
111 The image of the component model.
116 """Convert the component parameter arrays into Parameter instances
120 parameterization: Callable
121 A function to use to convert parameters of a given type into
122 a `Parameter` in place. It should take a single argument that
123 is the `Component` or `Source` that is to be parameterized.
127 def to_data(self) -> ScarletComponentBaseData:
128 """Convert the component to persistable ScarletComponentBaseData
132 component_data: ScarletComponentBaseData
133 The data object containing the component information
138 """Get a sub-component corresponding to the given indices.
143 The indices to use to slice the component model.
147 sub_component: Component
148 A new component that is a sub-component of this one.
153 If the index includes a ``Box`` or spatial indices.
158 """Create a copy of this component.
162 component : Component
163 A new component that is a copy of this one.
168 """Create a deep copy of this component.
172 component : Component
173 A new component that is a deep copy of this one.
176 def copy(self, deep: bool =
False) -> Component:
177 """Create a copy of this component.
181 deep : bool, optional
182 If `True`, a deep copy is made. If `False`, a shallow copy is made.
187 component : Component
188 A new component that is a copy of this one.
196 """A component that can be factorized into spectrum and morphology
202 The bands of the spectral dimension, in order.
204 The parameter to store and update the spectrum.
206 The parameter to store and update the morphology.
208 Location of the peak for the source.
210 The `Box` in the `model_bbox` that contains the source.
212 The RMS of the background used to threshold, grow,
213 and shrink the component.
215 The threshold to use for the background RMS.
216 If `None`, no background thresholding is applied, otherwise
217 a sparsity constraint is applied to the morpholigy that
218 requires flux in at least one band to be bg_thresh multiplied by
219 `bg_rms` in that band.
221 Minimum value of the spectrum or center morphology pixel
222 (depending on which is normalized).
224 The monotonicity operator to use for making the source monotonic.
225 If this parameter is `None`, the source will not be made monotonic.
227 The amount of padding to add to the component bounding box
228 when resizing the component.
230 Whether the component is symmetric or not.
231 If `True`, the morphology will be symmetrized using
232 `prox_uncentered_symmetry`.
233 If `False`, the morphology will not be symmetrized.
239 spectrum: Parameter | np.ndarray,
240 morph: Parameter | np.ndarray,
242 peak: tuple[int, int] |
None =
None,
243 bg_rms: np.ndarray |
None =
None,
244 bg_thresh: float |
None = 0.25,
245 floor: float = 1e-20,
246 monotonicity: Monotonicity |
None =
None,
248 is_symmetric: bool =
False,
267 def peak(self) -> tuple[int, int] | None:
268 """The peak of the component
273 The peak of the component
279 """The center of the component in its bounding box
281 This is likely to be different than `Component.center`,
282 since `Component.center` is the center of the component in the
283 full model, whereas `component_center` is the center of the component
284 inside its bounding box.
289 The center of the component in its bounding box
295 _center[0] - self.
bbox.origin[-2],
296 _center[1] - self.
bbox.origin[-1],
302 """The array of spectrum values"""
307 """The array of morphology values"""
312 """Shape of the resulting model image"""
316 """Build the model from the spectrum and morphology"""
321 model = spectrum[:,
None,
None] * morph[
None, :, :]
322 return Image(model, bands=self.
bands, yx0=cast(tuple[int, int], self.
bbox.origin))
324 def grad_spectrum(self, input_grad: np.ndarray, spectrum: np.ndarray, morph: np.ndarray):
325 """Gradient of the spectrum wrt. the component model"""
326 return np.einsum(
"...jk,jk", input_grad, morph)
328 def grad_morph(self, input_grad: np.ndarray, morph: np.ndarray, spectrum: np.ndarray):
329 """Gradient of the morph wrt. the component model"""
330 return np.einsum(
"i,i...", spectrum, input_grad)
333 """Apply a prox-like update to the spectrum"""
336 spectrum[~np.isfinite(spectrum)] = self.
floor
340 """Apply a prox-like update to the morphology"""
343 if self.
peak is None:
344 peak = (shape[0] // 2, shape[1] // 2)
347 self.
peak[0] - self.
bbox.origin[-2],
348 self.
peak[1] - self.
bbox.origin[-1],
358 morph = prox_uncentered_symmetry(morph, peak, fill=0.0)
363 model = self.
spectrum[:,
None,
None] * morph[
None, :, :]
364 morph[np.all(model < bg_thresh[:,
None,
None], axis=0)] = 0
370 morph[peak] = np.max([morph[peak], self.
floor])
373 morph[~np.isfinite(morph)] = 0
376 max_value = np.max(morph)
378 morph[:] = morph / max_value
381 def resize(self, model_box: Box) -> bool:
382 """Test whether or not the component needs to be resized"""
388 model = self.
spectrum[:,
None,
None] * self.
morph[
None, :, :]
390 significant = np.any(model >= bg_thresh[:,
None,
None], axis=0)
391 if np.sum(significant) == 0:
397 new_box =
Box((1, 1), center).grow(self.
padding) & model_box
400 Box.from_data(significant, threshold=0).grow(self.
padding) + self.
bbox.origin
402 if new_box == self.
bbox:
410 def update(self, it: int, input_grad: np.ndarray):
411 """Update the spectrum and morphology parameters"""
419 """Convert the component parameter arrays into Parameter instances
423 parameterization: Callable
424 A function to use to convert parameters of a given type into
425 a `Parameter` in place. It should take a single argument that
426 is the `Component` or `Source` that is to be parameterized.
429 parameterization(self)
436 def to_data(self) -> ScarletComponentBaseData:
437 """Convert the component to persistable ScarletComponentBaseData
441 component_data: ScarletComponentBaseData
442 The data object containing the component information
444 from .io
import ScarletFactorizedComponentData
446 return ScarletFactorizedComponentData(
447 origin=self.
bbox.origin,
455 f
"FactorizedComponent<\n bands={self.bands},\n center={self.peak},\n "
456 f
"spectrum={self.spectrum},\n morph_shape={self.morph.shape}\n>"
464 """Get a sub-component corresponding to the given indices.
469 The indices to use to slice the component model.
473 component: FactorizedComponent
474 A new component that is a sub-component of this one.
479 If the index includes a ``Box`` or spatial indices.
482 band_indices = convert_indices(self.
bands, indices)
483 if isinstance(band_indices, slice):
484 bands = self.
bands[band_indices]
486 bands = tuple(self.
bands[i]
for i
in band_indices)
489 spectrum = self.
_spectrum.x[band_indices,]
506 """Create a deep copy of this component.
511 The memoization dictionary used by `copy.deepcopy`.
515 component : FactorizedComponent
516 A new component that is a deep copy of this one.
520 return memo[id(self)]
523 component = FactorizedComponent.__new__(FactorizedComponent)
524 memo[id(self)] = component
528 bands=deepcopy(self.
bands, memo),
529 spectrum=deepcopy(self.
spectrum, memo),
530 morph=deepcopy(self.
morph, memo),
531 bbox=deepcopy(self.
bbox, memo),
532 peak=deepcopy(self.
peak, memo),
533 bg_rms=deepcopy(self.
bg_rms, memo),
543 """Create a copy of this component.
547 component : FactorizedComponent
548 A new component that is a shallow copy of this one.
566 """Dummy component for a component cube.
568 This is duck-typed to a `lsst.scarlet.lite.Component` in order to
569 generate a model from the component but it is currently not functional
570 in that it cannot be optimized, only persisted and loaded.
572 If scarlet lite ever implements a component as a data cube,
573 this class can be removed.
576 def __init__(self, model: Image, peak: tuple[int, int]):
583 The 3D (bands, y, x) model of the component.
585 The `(y, x)` peak of the component.
587 The bounding box of the component.
589 super().
__init__(model.bands, model.bbox)
594 """Generate the model for the source
599 The model as a 3D `(band, y, x)` array.
603 def resize(self, model_box: Box) -> bool:
604 """Resize the component if needed and return whether it was resized"""
605 Logger.warning(
"CubeComponent does not support resizing")
608 def update(self, it: int, input_grad: np.ndarray) ->
None:
609 """Implementation of unused abstract method"""
610 Logger.warning(
"CubeComponent does not support updates")
613 """Implementation of unused abstract method"""
614 Logger.warning(
"CubeComponent does not support parameterization")
616 def to_data(self) -> ScarletCubeComponentData:
617 """Convert the component to persistable ScarletComponentData
621 component_data: ScarletComponentData
622 The data object containing the component information
624 from .io
import ScarletCubeComponentData
627 origin=self.
bbox.origin,
633 """Get a sub-component corresponding to the given indices.
638 The indices to select.
642 A new component that is a sub-component of this one.
644 band_indices = convert_indices(self.
bands, indices)
645 if isinstance(band_indices, slice):
646 bands = self.
bands[band_indices]
648 bands = tuple(self.
bands[i]
for i
in band_indices)
650 data = self.
get_model()._data[band_indices,]
651 model =
Image(data=data, bands=bands, yx0=cast(tuple[int, int], self.
bbox.origin))
655 """Create a copy of this component.
659 component : ComponentCube
660 A new component that is a shallow copy of this one.
665 """Create a deep copy of this component.
670 The memoization dictionary used by `copy.deepcopy`.
674 component : ComponentCube
675 A new component that is a deep copy of this one.
678 return memo[id(self)]
681 component = CubeComponent.__new__(CubeComponent)
682 memo[id(self)] = component
693 """Initialize a factorized component to use FISTA PGM for optimization"""
694 if isinstance(component, FactorizedComponent):
695 component._spectrum =
FistaParameter(component.spectrum, step=0.5)
698 raise NotImplementedError(f
"Unrecognized component type {component}")
702 """Initialize a factorized component to use Proximal ADAM
705 if noise_rms
is None:
707 if isinstance(component, FactorizedComponent):
710 step=partial(relative_step, factor=1e-2, minimum=noise_rms),
717 raise NotImplementedError(f
"Unrecognized component type {component}")
A class to represent a 2-dimensional array of pixels.
bool resize(self, Box model_box)
None update(self, int it, np.ndarray input_grad)
Component __getitem__(self, Any indices)
ScarletComponentBaseData to_data(self)
Component __deepcopy__(self, dict[int, Any] memo)
None parameterize(self, Callable parameterization)
__init__(self, tuple bands, Box bbox)
Component copy(self, bool deep=False)
ScarletCubeComponentData to_data(self)
CubeComponent __copy__(self)
None update(self, int it, np.ndarray input_grad)
CubeComponent __getitem__(self, Any indices)
None parameterize(self, Callable parameterization)
bool resize(self, Box model_box)
__init__(self, Image model, tuple[int, int] peak)
CubeComponent __deepcopy__(self, dict[int, Any] memo)
tuple[int, int]|None component_center(self)
bool resize(self, Box model_box)
update(self, int it, np.ndarray input_grad)
tuple[int, int]|None _peak
grad_morph(self, np.ndarray input_grad, np.ndarray morph, np.ndarray spectrum)
np.ndarray spectrum(self)
tuple[int, int]|None peak(self)
None parameterize(self, Callable parameterization)
FactorizedComponent __getitem__(self, Any indices)
np.ndarray prox_spectrum(self, np.ndarray spectrum)
ScarletComponentBaseData to_data(self)
grad_spectrum(self, np.ndarray input_grad, np.ndarray spectrum, np.ndarray morph)
FactorizedComponent __copy__(self)
FactorizedComponent __deepcopy__(self, dict[int, Any] memo)
np.ndarray prox_morph(self, np.ndarray morph)
__init__(self, tuple bands, Parameter|np.ndarray spectrum, Parameter|np.ndarray morph, Box bbox, tuple[int, int]|None peak=None, np.ndarray|None bg_rms=None, float|None bg_thresh=0.25, float floor=1e-20, Monotonicity|None monotonicity=None, int padding=5, bool is_symmetric=False)
default_fista_parameterization(Component component)
default_adaprox_parameterization(Component component, float|None noise_rms=None)