LSST Applications g063fba187b+cac8b7c890,g0f08755f38+6aee506743,g1653933729+a8ce1bb630,g168dd56ebc+a8ce1bb630,g1a2382251a+b4475c5878,g1dcb35cd9c+8f9bc1652e,g20f6ffc8e0+6aee506743,g217e2c1bcf+73dee94bd0,g28da252d5a+1f19c529b9,g2bbee38e9b+3f2625acfc,g2bc492864f+3f2625acfc,g3156d2b45e+6e55a43351,g32e5bea42b+1bb94961c2,g347aa1857d+3f2625acfc,g35bb328faa+a8ce1bb630,g3a166c0a6a+3f2625acfc,g3e281a1b8c+c5dd892a6c,g3e8969e208+a8ce1bb630,g414038480c+5927e1bc1e,g41af890bb2+8a9e676b2a,g7af13505b9+809c143d88,g80478fca09+6ef8b1810f,g82479be7b0+f568feb641,g858d7b2824+6aee506743,g89c8672015+f4add4ffd5,g9125e01d80+a8ce1bb630,ga5288a1d22+2903d499ea,gb58c049af0+d64f4d3760,gc28159a63d+3f2625acfc,gcab2d0539d+b12535109e,gcf0d15dbbd+46a3f46ba9,gda6a2b7d83+46a3f46ba9,gdaeeff99f8+1711a396fd,ge79ae78c31+3f2625acfc,gef2f8181fd+0a71e47438,gf0baf85859+c1f95f4921,gfa517265be+6aee506743,gfa999e8aa5+17cd334064,w.2024.51
LSST Data Management Base Package
Loading...
Searching...
No Matches
initialization.py
Go to the documentation of this file.
1# This file is part of scarlet_lite.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22import logging
23from abc import ABC, abstractmethod
24from typing import Sequence, cast
25
26import numpy as np
27
28from .bbox import Box
29from .component import FactorizedComponent
30from .detect import bounds_to_bbox, get_detect_wavelets
31from .image import Image
32from .measure import calculate_snr
33from .observation import Observation
34from .operators import Monotonicity, prox_monotonic_mask, prox_uncentered_symmetry
35from .source import Source
36
37logger = logging.getLogger("scarlet.lite.initialization")
38
39
41 morph: np.ndarray,
42 bg_thresh: float = 0,
43 padding: int = 5,
44) -> tuple[np.ndarray, Box]:
45 """Trim the morphology up to pixels above a threshold
46
47 Parameters
48 ----------
49 morph:
50 The morphology to be trimmed.
51 bg_thresh:
52 The morphology is trimmed to pixels above the threshold.
53 padding:
54 The amount to pad each side to allow the source to grow.
55
56 Returns
57 -------
58 morph:
59 The trimmed morphology
60 box:
61 The box that contains the morphology.
62 """
63 # trim morph to pixels above threshold
64 mask = morph > bg_thresh
65 morph[~mask] = 0
66 bbox = Box.from_data(morph, threshold=0).grow(padding)
67 return morph, bbox
68
69
71 detect: np.ndarray,
72 center: tuple[int, int],
73 full_box: Box,
74 padding: int = 5,
75 normalize: bool = True,
76 monotonicity: Monotonicity | None = None,
77 thresh: float = 0,
78) -> tuple[Box, np.ndarray | None]:
79 """Initialize a morphology for a monotonic source
80
81 Parameters
82 ----------
83 detect:
84 The 2D detection image contained in `full_box`.
85 center:
86 The center of the monotonic source.
87 full_box:
88 The bounding box of `detect`.
89 padding:
90 The number of pixels to grow the morphology in each direction.
91 This can be useful if initializing a source with a kernel that
92 is known to be narrower than the expected value of the source.
93 normalize:
94 Whether or not to normalize the morphology.
95 monotonicity:
96 When `monotonicity` is `None`,
97 the component is initialized with only the
98 monotonic pixels, otherwise the monotonicity operator is used to
99 project the morphology to a monotonic solution.
100 thresh:
101 The threshold (fraction above the background) to use for trimming the
102 morphology.
103
104 Returns
105 -------
106 bbox:
107 The bounding box of the morphology.
108 morph:
109 The initialized morphology.
110 """
111 center: tuple[int, int] = tuple(center[i] - full_box.origin[i] for i in range(2)) # type: ignore
112
113 if monotonicity is None:
114 _, morph, bounds = prox_monotonic_mask(detect, center, max_iter=0)
115 bbox = bounds_to_bbox(bounds)
116 if bbox.shape == (1, 1) and morph[bbox.slices][0, 0] == 0:
117 return Box((0, 0)), None
118
119 if thresh > 0:
120 morph, bbox = trim_morphology(morph, bg_thresh=thresh, padding=padding)
121
122 # Shift the bounding box to account for the non-zero origin
123 bbox += full_box.origin
124
125 else:
126 morph = monotonicity(detect, center)
127
128 # truncate morph at thresh * bg_rms
129 morph, bbox = trim_morphology(morph, bg_thresh=thresh, padding=padding)
130 # Shift the bounding box to account for the non-zero origin
131 bbox += full_box.origin
132
133 if np.max(morph) == 0:
134 return Box((0, 0), origin=full_box.origin), None
135
136 if normalize:
137 morph /= np.max(morph)
138
139 if padding is not None and padding > 0:
140 # Pad the morphology to allow it to grow
141 bbox = bbox.grow(padding)
142
143 # Ensure that the bounding box is inside the full box,
144 # even after padding.
145 bbox = bbox & full_box
146 return bbox, morph
147
148
150 observation: Observation,
151 morphs: Sequence[Image],
152 model: Image | None = None,
153) -> np.ndarray:
154 """Fit the spectra of multiple components simultaneously
155
156 Parameters
157 ----------
158 observation:
159 The class containing the observation data.
160 morphs:
161 The morphology of each component.
162 model:
163 An optional model for sources that are not factorized,
164 and thus will not have their spectra fit.
165 This model is subtracted from the data before fitting the other
166 spectra.
167
168 Returns
169 -------
170 spectra:
171 The spectrum for each component, in the same order as `morphs`.
172 """
173 _bands = observation.bands
174 n_bands = len(_bands)
175 dtype = observation.images.dtype
176
177 if model is not None:
178 image = observation.images - model
179 else:
180 image = observation.images.copy()
181
182 morph_images = np.zeros((n_bands, len(morphs), image.data[0].size), dtype=dtype)
183 for idx, morph in enumerate(morphs):
184 _image = morph.repeat(observation.bands)
185 _image = Image.from_box(image.bbox, bands=image.bands).insert(_image)
186 morph_images[:, idx] = observation.convolve(_image).data.reshape(n_bands, -1)
187
188 spectra = np.zeros((len(morphs), n_bands), dtype=dtype)
189
190 for b in range(n_bands):
191 a = np.vstack(morph_images[b]).T
192 spectra[:, b] = np.linalg.lstsq(a, image[observation.bands[b]].data.flatten(), rcond=None)[0]
193 spectra[spectra < 0] = 0
194 return spectra
195
196
198 """Common variables and methods for both Factorized Component schemes
199
200 Parameters
201 ----------
202 observation:
203 The observation containing the blend
204 centers:
205 The center of each source to initialize.
206 min_snr:
207 The minimum SNR required per component.
208 So a 2-component source requires at least `2*min_snr` while sources
209 with SNR < `min_snr` will be initialized with the PSF.
210 monotonicity:
211 When `monotonicity` is `None`,
212 the component is initialized with only the
213 monotonic pixels, otherwise the monotonicity operator is used to
214 project the morphology to a monotonic solution.
215 use_sparse_init:
216 Use a monotonic mask to prevent initial source models from growing
217 too large.
218 """
219
221 self,
222 observation: Observation,
223 convolved: Image,
224 centers: Sequence[tuple[int, int]],
225 min_snr: float = 50,
226 monotonicity: Monotonicity | None = None,
227 use_sparse_init: bool = True,
228 ):
229 self.observation = observation
230 self.convolved = convolved
231 self.centers = centers
232 self.min_snr = min_snr
233 self.monotonicity = monotonicity
234 self.use_sparse_init = use_sparse_init
235
236 # Get the model PSF
237 # Convolve the PSF in order to set the spectrum
238 # of a point source correctly.
239 model_psf = Image(cast(np.ndarray, observation.model_psf)[0])
240 convolved = model_psf.repeat(observation.bands)
241 self.convolved_psf = observation.convolve(convolved, mode="real").data
242 # Get the "spectrum" of the PSF
243 self.py = model_psf.shape[0] // 2
244 self.px = model_psf.shape[1] // 2
245 self.psf_spectrum = self.convolved_psf[:, self.py, self.px]
246
247 # Initalize all of the sources
248 sources = []
249 for center in centers:
250 source = self.init_source((int(center[0]), int(center[1])))
251 sources.append(source)
252 self.sources = sources
253
254 def get_snr(self, center: tuple[int, int]) -> float:
255 """Get the SNR at the center of a component
256
257 Parameters
258 ----------
259 center:
260 The location of the center of the source.
261
262 Returns
263 -------
264 result:
265 The SNR at the center of the component.
266 """
267 snr = np.floor(
268 calculate_snr(
269 self.observation.images,
270 self.observation.variance,
271 self.observation.psfs,
272 center,
273 )
274 )
275 return snr / self.min_snr
276
277 def get_psf_component(self, center: tuple[int, int]) -> FactorizedComponent:
278 """Create a factorized component with a PSF morphology
279
280 Parameters
281 ----------
282 center:
283 The center of the component.
284
285 Returns
286 -------
287 component:
288 A `FactorizedComponent` with a PSF-like morphology.
289 """
290 local_center = (
291 center[0] - self.observation.bbox.origin[0],
292 center[1] - self.observation.bbox.origin[1],
293 )
294 # There wasn't sufficient flux for an extended source,
295 # so create a PSF source.
296 spectrum_center = (slice(None), local_center[0], local_center[1])
297 spectrum = self.observation.images.data[spectrum_center] / self.psf_spectrum
298 spectrum[spectrum < 0] = 0
299
300 psf = cast(np.ndarray, self.observation.model_psf)[0].copy()
301 py = psf.shape[0] // 2
302 px = psf.shape[1] // 2
303 bbox = Box(psf.shape, origin=(-py + center[0], -px + center[1]))
304 bbox = self.observation.bbox & bbox
305 morph = Image(psf, yx0=cast(tuple[int, int], bbox.origin))[bbox].data
306 component = FactorizedComponent(
307 self.observation.bands,
308 spectrum,
309 morph,
310 bbox,
311 center,
312 self.observation.noise_rms,
313 monotonicity=self.monotonicity,
314 )
315 return component
316
318 self,
319 center: tuple[int, int],
320 detect: np.ndarray,
321 thresh: float,
322 padding: int,
323 ) -> FactorizedComponent | None:
324 """Initialize parameters for a `FactorizedComponent`
325
326 Parameters
327 ----------
328 center:
329 The location of the center of the source to detect in the
330 full image.
331 detect:
332 The image used for detection of the morphology.
333 thresh:
334 The lower cutoff threshold to use for the morphology.
335 padding:
336 The amount to pad the morphology to allow for extra flux
337 in the first few iterations before resizing.
338
339 Returns
340 -------
341 component:
342 A `FactorizedComponent` created from the detection image.
343
344 """
345 local_center = (
346 center[0] - self.observation.bbox.origin[0],
347 center[1] - self.observation.bbox.origin[1],
348 )
349
350 if self.use_sparse_init:
351 monotonicity = None
352 else:
353 monotonicity = self.monotonicity
354 bbox, morph = init_monotonic_morph(
355 detect,
356 center,
357 self.observation.bbox,
358 padding=padding,
359 normalize=False,
360 monotonicity=monotonicity,
361 thresh=thresh,
362 )
363
364 if morph is None:
365 return None
366 morph = morph[(bbox - self.observation.bbox.origin).slices]
367
368 spectrum_center = (slice(None), local_center[0], local_center[1])
369 images = self.observation.images
370
371 convolved = self.convolved
372 spectrum = images.data[spectrum_center] / convolved.data[spectrum_center]
373 spectrum[spectrum < 0] = 0
374 morph_max = np.max(morph)
375 spectrum *= morph_max
376 morph /= morph_max
377
378 return FactorizedComponent(
379 self.observation.bands,
380 spectrum,
381 morph,
382 bbox,
383 center,
384 self.observation.noise_rms,
385 monotonicity=self.monotonicity,
386 )
387
388 @abstractmethod
389 def init_source(self, center: tuple[int, int]) -> Source | None:
390 """Initialize a source
391
392 Parameters
393 ----------
394 center:
395 The center of the source.
396 """
397
398
400 """Initialize all sources with chi^2 detections
401
402 There are a large number of parameters that are universal for all of the
403 sources being initialized from the same set of observed images.
404 To simplify the API those parameters are all initialized by this class
405 and passed to `init_main_source` for each source.
406 It also creates temporary objects that only need to be created once for
407 all of the sources in a blend.
408
409 Parameters
410 ----------
411 observation:
412 The observation containing the blend
413 centers:
414 The center of each source to initialize.
415 detect:
416 The array that contains a 2D image used for detection.
417 min_snr:
418 The minimum SNR required per component.
419 So a 2-component source requires at least `2*min_snr` while sources
420 with SNR < `min_snr` will be initialized with the PSF.
421 monotonicity:
422 When `monotonicity` is `None`,
423 the component is initialized with only the
424 monotonic pixels, otherwise the monotonicity operator is used to
425 project the morphology to a monotonic solution.
426 disk_percentile:
427 The percentage of the overall flux to attribute to the disk.
428 thresh:
429 The threshold used to trim the morphology,
430 so all pixels below `thresh * bg_rms` are set to zero.
431 padding:
432 The amount to pad the morphology to allow for extra flux
433 in the first few iterations before resizing.
434 """
435
437 self,
438 observation: Observation,
439 centers: Sequence[tuple[int, int]],
440 detect: np.ndarray | None = None,
441 min_snr: float = 50,
442 monotonicity: Monotonicity | None = None,
443 disk_percentile: float = 25,
444 thresh: float = 0.5,
445 padding: int = 2,
446 ):
447 if detect is None:
448 # Build the morphology detection image
449 detect = np.sum(
450 observation.images.data / (observation.noise_rms**2)[:, None, None],
451 axis=0,
452 )
453 self.detect = detect
454 _detect = Image(detect)
455 # Convolve the detection image.
456 # This may seem counter-intuitive,
457 # since this is effectively growing the model,
458 # but this is exactly what convolution will do to the model
459 # in each iteration.
460 # So we create the convolved model in order
461 # to correctly set the spectrum.
462 convolved = observation.convolve(_detect.repeat(observation.bands), mode="real")
463
464 # Set the input parameters
465 self.disk_percentile = disk_percentile
466 self.thresh = thresh
467 self.padding = padding
468
469 # Initialize the sources
470 super().__init__(observation, convolved, centers, min_snr, monotonicity)
471
472 def init_source(self, center: tuple[int, int]) -> Source | None:
473 """Initialize a source from a chi^2 detection.
474
475 Parameter
476 ---------
477 center:
478 The center of the source.
479 init:
480 The initialization parameters common to all of the sources.
481 max_components:
482 The maximum number of components in the source.
483 """
484 # Some operators need the local center, not center in the full image
485 local_center = (
486 center[0] - self.observationobservation.bbox.origin[0],
487 center[1] - self.observationobservation.bbox.origin[1],
488 )
489
490 # Calculate the signal to noise at the center of this source
491 component_snr = self.get_snr(center)
492
493 # Initialize the bbox, morph, and spectrum
494 # for a single component source
495 detect = prox_uncentered_symmetry(self.detect.copy(), local_center, fill=0)
496 thresh = np.mean(self.observationobservation.noise_rms) * self.thresh
497 component = self.get_single_component(center, detect, thresh, self.padding)
498
499 if component is None:
500 components = [self.get_psf_component(center)]
501 elif component_snr < 2:
502 components = [component]
503 else:
504 # There was enough flux for a 2-component source,
505 # so split the single component model into two components,
506 # using the same algorithm as scarlet main.
507 bulge_morph = component.morph.copy()
508 disk_morph = component.morph
509 # Set the threshold for the bulge.
510 # Since the morphology is monotonic, this selects the inner
511 # of the single component morphology and assigns it to the bulge.
512 flux_thresh = self.disk_percentile / 100
513 mask = disk_morph > flux_thresh
514 # Remove the flux above the threshold so that the disk will have
515 # a flat center.
516 disk_morph[mask] = flux_thresh
517 # Subtract off the thresholded flux (since we're normalizing the
518 # morphology anyway) so that it does not have a sharp
519 # discontinuity at the edge.
520 bulge_morph -= flux_thresh
521 bulge_morph[bulge_morph < 0] = 0
522
523 bulge_morph /= np.max(bulge_morph)
524 disk_morph /= np.max(disk_morph)
525
526 # Fit the spectra assuming that all of the flux in the image
527 # is due to both components. This is not true, but for the
528 # vast majority of sources this is a good approximation.
529 try:
530 bulge_spectrum, disk_spectrum = multifit_spectra(
532 [
533 Image(bulge_morph, yx0=cast(tuple[int, int], component.bbox.origin)),
534 Image(disk_morph, yx0=cast(tuple[int, int], component.bbox.origin)),
535 ],
536 )
537
538 components = [
540 self.observationobservation.bands,
541 bulge_spectrum,
542 bulge_morph,
543 component.bbox.copy(),
544 center,
545 self.observationobservation.noise_rms,
546 monotonicity=self.monotonicity,
547 ),
549 self.observationobservation.bands,
550 disk_spectrum,
551 disk_morph,
552 component.bbox.copy(),
553 center,
554 self.observationobservation.noise_rms,
555 monotonicity=self.monotonicity,
556 ),
557 ]
558 except np.linalg.LinAlgError:
559 components = [component]
560
561 return Source(components) # type: ignore
562
563
565 """Parameters used to initialize all sources with wavelet detections
566
567 There are a large number of parameters that are universal for all of the
568 sources being initialized from the same set of wavelet coefficients.
569 To simplify the API those parameters are all initialized by this class
570 and passed to `init_wavelet_source` for each source.
571
572 Parameters
573 ----------
574 observation:
575 The multiband observation of the blend.
576 centers:
577 The center of each source to initialize.
578 bulge_slice, disk_slice:
579 The slice used to select the wavelet scales used for the
580 bulge/disk.
581 bulge_padding, disk_padding:
582 The number of pixels to grow the bounding box of the bulge/disk
583 to leave extra room for growth in the first few iterations.
584 use_psf:
585 Whether or not to use the PSF for single component sources.
586 If `use_psf` is `False` then only sources with low signal
587 at all scales are initialized with the PSF morphology.
588 scales:
589 Number of wavelet scales to use.
590 wavelets:
591 The array of wavelet coefficients `(scale, y, x)`
592 used for detection.
593 monotonicity:
594 When `monotonicity` is `None`,
595 the component is initialized with only the
596 monotonic pixels, otherwise the monotonicity operator is used to
597 project the morphology to a monotonic solution.
598 min_snr:
599 The minimum SNR required per component.
600 So a 2-component source requires at least `2*min_snr` while sources
601 with SNR < `min_snr` will be initialized with the PSF.
602 """
603
605 self,
606 observation: Observation,
607 centers: Sequence[tuple[int, int]],
608 bulge_slice: slice = slice(None, 2),
609 disk_slice: slice = slice(2, -1),
610 bulge_padding: int = 5,
611 disk_padding: int = 5,
612 use_psf: bool = True,
613 scales: int = 5,
614 wavelets: np.ndarray | None = None,
615 monotonicity: Monotonicity | None = None,
616 min_snr: float = 50,
617 ):
618 if wavelets is None:
619 wavelets = get_detect_wavelets(
620 observation.images.data,
621 observation.variance.data,
622 scales=scales,
623 )
624 wavelets[wavelets < 0] = 0
625 # The detection coadd for single component sources
626 detectlets = np.sum(wavelets[:-1], axis=0)
627 # The detection coadd for the bulge
628 bulgelets = np.sum(wavelets[bulge_slice], axis=0)
629 # The detection coadd for the disk
630 disklets = np.sum(wavelets[disk_slice], axis=0)
631
632 # The convolved image, used to initialize the spectrum
633 detect = Image(detectlets)
634 convolved = observation.convolve(detect.repeat(observation.bands), mode="real")
635
636 self.detectlets = detectlets
637 self.bulgelets = bulgelets
638 self.disklets = disklets
639 self.bulge_grow = bulge_padding
640 self.disk_grow = disk_padding
641 self.use_psf = use_psf
642
643 # Initialize the sources
644 super().__init__(observation, convolved, centers, min_snr, monotonicity)
645
646 def init_source(self, center: tuple[int, int]) -> Source | None:
647 """Initialize a source from a chi^2 detection.
648
649 Parameter
650 ---------
651 center:
652 The center of the source.
653 """
654 local_center = (
655 center[0] - self.observation.bbox.origin[0],
656 center[1] - self.observation.bbox.origin[1],
657 )
658 nbr_components = self.get_snr(center)
659 observation = self.observation
660
661 if (nbr_components < 1 and self.use_psf) or self.detectlets[local_center[0], local_center[1]] <= 0:
662 # Initialize the source as an PSF source
663 components = [self.get_psf_component(center)]
664 elif nbr_components < 2:
665 # Inititialize with a single component
666 component = self.get_single_component(center, self.detectlets, 0, self.disk_grow)
667 if component is not None:
668 components = [component]
669 else:
670 # Initialize with a 2 component model
671 bulge_box, bulge_morph = init_monotonic_morph(
672 self.bulgelets, center, observation.bbox, self.bulge_grow
673 )
674 disk_box, disk_morph = init_monotonic_morph(
675 self.disklets, center, observation.bbox, self.disk_grow
676 )
677 if bulge_morph is None or disk_morph is None:
678 if bulge_morph is None:
679 if disk_morph is None:
680 return None
681 # One of the components was null,
682 # so initialize as a single component
683 component = self.get_single_component(center, self.detectlets, 0, self.disk_grow)
684 if component is not None:
685 components = [component]
686 else:
687 local_bulge_box = bulge_box - self.observation.bbox.origin
688 local_disk_box = disk_box - self.observation.bbox.origin
689 bulge_morph = bulge_morph[local_bulge_box.slices]
690 disk_morph = disk_morph[local_disk_box.slices]
691
692 bulge_spectrum, disk_spectrum = multifit_spectra(
693 observation,
694 [
695 Image(bulge_morph, yx0=cast(tuple[int, int], bulge_box.origin)),
696 Image(disk_morph, yx0=cast(tuple[int, int], disk_box.origin)),
697 ],
698 )
699
700 components = []
701 if np.sum(bulge_spectrum != 0):
702 components.append(
704 observation.bands,
705 bulge_spectrum,
706 bulge_morph,
707 bulge_box,
708 center,
709 monotonicity=self.monotonicity,
710 )
711 )
712 else:
713 logger.debug("cut bulge")
714 if np.sum(disk_spectrum) != 0:
715 components.append(
717 observation.bands,
718 disk_spectrum,
719 disk_morph,
720 disk_box,
721 center,
722 monotonicity=self.monotonicity,
723 )
724 )
725 else:
726 logger.debug("cut disk")
727 return Source(components) # type: ignore
A class to represent a 2-dimensional array of pixels.
Definition Image.h:51
Source|None init_source(self, tuple[int, int] center)
__init__(self, Observation observation, Sequence[tuple[int, int]] centers, np.ndarray|None detect=None, float min_snr=50, Monotonicity|None monotonicity=None, float disk_percentile=25, float thresh=0.5, int padding=2)
__init__(self, Observation observation, Image convolved, Sequence[tuple[int, int]] centers, float min_snr=50, Monotonicity|None monotonicity=None, bool use_sparse_init=True)
Source|None init_source(self, tuple[int, int] center)
FactorizedComponent|None get_single_component(self, tuple[int, int] center, np.ndarray detect, float thresh, int padding)
FactorizedComponent get_psf_component(self, tuple[int, int] center)
__init__(self, Observation observation, Sequence[tuple[int, int]] centers, slice bulge_slice=slice(None, 2), slice disk_slice=slice(2, -1), int bulge_padding=5, int disk_padding=5, bool use_psf=True, int scales=5, np.ndarray|None wavelets=None, Monotonicity|None monotonicity=None, float min_snr=50)
np.ndarray multifit_spectra(Observation observation, Sequence[Image] morphs, Image|None model=None)
tuple[np.ndarray, Box] trim_morphology(np.ndarray morph, float bg_thresh=0, int padding=5)
tuple[Box, np.ndarray|None] init_monotonic_morph(np.ndarray detect, tuple[int, int] center, Box full_box, int padding=5, bool normalize=True, Monotonicity|None monotonicity=None, float thresh=0)