LSST Applications g013ef56533+d2224463a4,g199a45376c+0ba108daf9,g19c4beb06c+9f335b2115,g1fd858c14a+2459ca3e43,g210f2d0738+2d3d333a78,g262e1987ae+abbb004f04,g2825c19fe3+eedc38578d,g29ae962dfc+0cb55f06ef,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+19c3a54948,g47891489e3+501a489530,g4cdb532a89+a047e97985,g511e8cfd20+ce1f47b6d6,g53246c7159+8c5ae1fdc5,g54cd7ddccb+890c8e1e5d,g5fd55ab2c7+951cc3f256,g64539dfbff+2d3d333a78,g67b6fd64d1+501a489530,g67fd3c3899+2d3d333a78,g74acd417e5+0ea5dee12c,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+501a489530,g8d7436a09f+5ea4c44d25,g8ea07a8fe4+81eaaadc04,g90f42f885a+34c0557caf,g9486f8a5af+165c016931,g97be763408+d5e351dcc8,gbf99507273+8c5ae1fdc5,gc2a301910b+2d3d333a78,gca7fc764a6+501a489530,gce8aa8abaa+8c5ae1fdc5,gd7ef33dd92+501a489530,gdab6d2f7ff+0ea5dee12c,ge410e46f29+501a489530,geaed405ab2+e3b4b2a692,gf9a733ac38+8c5ae1fdc5,w.2025.41
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 typing import Sequence, cast
24
25import numpy as np
26from deprecated.sphinx import deprecated # type: ignore
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 threshold: float = 0,
43 padding: int = 5,
44 bg_thresh: float | None = None,
45) -> tuple[np.ndarray, Box]:
46 """Trim the morphology up to pixels above a threshold
47
48 Parameters
49 ----------
50 morph:
51 The morphology to be trimmed.
52 thresh:
53 The morphology is trimmed to pixels above the threshold.
54 bg_thresh:
55 Deprecated in favor of `thresh`.
56 padding:
57 The amount to pad each side to allow the source to grow.
58
59 Returns
60 -------
61 morph:
62 The trimmed morphology
63 box:
64 The box that contains the morphology.
65 """
66 # Temporarily support bg_thresh
67 if bg_thresh is not None:
68 logger.warning("bg_thresh is deprecated and will be after v29.0. " "Use threshold instead.")
69 threshold = bg_thresh
70
71 # trim morph to pixels above threshold
72 mask = morph > threshold
73 morph[~mask] = 0
74 bbox = Box.from_data(morph, threshold=0).grow(padding)
75 return morph, bbox
76
77
79 detect: np.ndarray,
80 center: tuple[int, int],
81 full_box: Box,
82 padding: int = 5,
83 normalize: bool = True,
84 monotonicity: Monotonicity | None = None,
85 threshold: float = 0,
86 max_iter: int = 0,
87 center_radius: int = 1,
88 variance_factor: float = 0,
89) -> tuple[Box, np.ndarray | None]:
90 """Initialize a morphology for a monotonic source
91
92 Parameters
93 ----------
94 detect:
95 The 2D detection image contained in `full_box`.
96 center:
97 The center of the monotonic source.
98 full_box:
99 The bounding box of `detect`.
100 padding:
101 The number of pixels to grow the morphology in each direction.
102 This can be useful if initializing a source with a kernel that
103 is known to be narrower than the expected value of the source.
104 normalize:
105 Whether or not to normalize the morphology.
106 monotonicity:
107 When `monotonicity` is `None`,
108 the component is initialized with only the
109 monotonic pixels, otherwise the monotonicity operator is used to
110 project the morphology to a monotonic solution.
111 threshold:
112 The minimum value to use for trimming the
113 morphology.
114 max_iter:
115 The maximum number of iterations to use in the monotonicity operator.
116 Only used if `monotonicity` is `None`.
117 center_radius:
118 The amount that the center can be shifted to a local maximum.
119 Only used if `monotonicity` is `None`.
120 variance_factor:
121 The average variance in the image.
122 This is used to allow pixels to be non-monotonic up to
123 `variance` * `noise_rms`, so setting `variance = 0`
124 will force strict monotonicity in the mask.
125 Only used if `monotonicity` is `None`.
126
127 Returns
128 -------
129 bbox:
130 The bounding box of the morphology.
131 morph:
132 The initialized morphology.
133 """
134 center: tuple[int, int] = tuple(center[i] - full_box.origin[i] for i in range(2)) # type: ignore
135
136 if monotonicity is None:
137 _, morph, bounds = prox_monotonic_mask(
138 x=detect,
139 center=center,
140 center_radius=center_radius,
141 variance=variance_factor,
142 max_iter=max_iter,
143 )
144 bbox = bounds_to_bbox(bounds)
145 if bbox.shape == (1, 1) and morph[bbox.slices][0, 0] == 0:
146 return Box((0, 0)), None
147
148 if threshold > 0:
149 morph, bbox = trim_morphology(morph, threshold=threshold, padding=padding)
150
151 else:
152 morph = monotonicity(detect, center)
153
154 # truncate morph at thresh * bg_rms
155 morph, bbox = trim_morphology(morph, threshold=threshold, padding=padding)
156
157 # Shift the bounding box to account for the non-zero origin
158 bbox += full_box.origin
159
160 if np.max(morph) == 0:
161 return Box((0, 0), origin=full_box.origin), None
162
163 if normalize:
164 morph /= np.max(morph)
165
166 if padding is not None and padding > 0:
167 # Pad the morphology to allow it to grow
168 bbox = bbox.grow(padding)
169
170 # Ensure that the bounding box is inside the full box,
171 # even after padding.
172 bbox = bbox & full_box
173 return bbox, morph
174
175
177 observation: Observation,
178 morphs: Sequence[Image],
179 model: Image | None = None,
180) -> np.ndarray:
181 """Fit the spectra of multiple components simultaneously
182
183 Parameters
184 ----------
185 observation:
186 The class containing the observation data.
187 morphs:
188 The morphology of each component.
189 model:
190 An optional model for sources that are not factorized,
191 and thus will not have their spectra fit.
192 This model is subtracted from the data before fitting the other
193 spectra.
194
195 Returns
196 -------
197 spectra:
198 The spectrum for each component, in the same order as `morphs`.
199 """
200 _bands = observation.bands
201 n_bands = len(_bands)
202 dtype = observation.images.dtype
203
204 if model is not None:
205 image = observation.images - model
206 else:
207 image = observation.images.copy()
208
209 morph_images = np.zeros((n_bands, len(morphs), image.data[0].size), dtype=dtype)
210 for idx, morph in enumerate(morphs):
211 _image = morph.repeat(observation.bands)
212 _image = Image.from_box(image.bbox, bands=image.bands).insert(_image)
213 morph_images[:, idx] = observation.convolve(_image).data.reshape(n_bands, -1)
214
215 spectra = np.zeros((len(morphs), n_bands), dtype=dtype)
216
217 for b in range(n_bands):
218 a = np.vstack(morph_images[b]).T
219 spectra[:, b] = np.linalg.lstsq(a, image[observation.bands[b]].data.flatten(), rcond=None)[0]
220 spectra[spectra < 0] = 0
221 return spectra
222
223
225 """Common variables and methods for both Factorized Component schemes
226
227 There are a large number of parameters that are universal for all of the
228 sources being initialized from the same set of observed images.
229 To simplify the API those parameters are all initialized by this class
230 and passed to `init_source` for each source.
231 It also creates temporary objects that only need to be created once for
232 all of the sources in a blend.
233
234 Parameters
235 ----------
236 observation:
237 The observation containing the blend
238 centers:
239 The center of each source to initialize.
240 detect:
241 The array that contains a 2D image used for detection.
242 min_snr:
243 The minimum SNR required per component.
244 So a 2-component source requires at least `2*min_snr` while sources
245 with SNR < `min_snr` will be initialized with the PSF.
246 monotonicity:
247 When `monotonicity` is `None`,
248 the component is initialized with only the
249 monotonic pixels, otherwise the monotonicity operator is used to
250 project the morphology to a monotonic solution.
251 disk_percentile:
252 The percentage of the overall flux to attribute to the disk.
253 bg_thresh:
254 The fraction of the background RMS to use as a threshold for the
255 morphology (in other words the threshold is set at
256 `bg_thresh * noise_rms`).
257 initital_bg_thresh:
258 The same as bg_thresh, but this allows a different background
259 threshold to be used for initialization.
260 thresh:
261 Deprecated. Use `initial_bg_thresh` instead.
262 padding:
263 The amount to pad the morphology to allow for extra flux
264 in the first few iterations before resizing.
265 use_sparse_init:
266 Use a monotonic mask to prevent initial source models from growing
267 too large.
268 max_components:
269 The maximum number of components in the source.
270 This should be one or two.
271 convolved:
272 Deprecated. This is now calculated in __init__, but the
273 old API is supported until v29.0.
274 is_symmetric:
275 Whether or not the sources are symmetric.
276 This is used to determine whether to use the symmetry operator
277 for initialization.
278 """
279
281 self,
282 observation: Observation,
283 centers: Sequence[tuple[int, int]],
284 detect: np.ndarray | None = None,
285 min_snr: float = 50,
286 monotonicity: Monotonicity | None = None,
287 disk_percentile: float = 25,
288 initial_bg_thresh: float = 0.5,
289 bg_thresh: float = 0.25,
290 thresh: float | None = None,
291 padding: int = 2,
292 use_sparse_init: bool = True,
293 max_components: int = 2,
294 convolved: Image | None = None,
295 is_symmetric: bool = False,
296 ):
297 if detect is None:
298 # Build the morphology detection image
299 detect = np.sum(
300 observation.images.data / (observation.noise_rms**2)[:, None, None],
301 axis=0,
302 )
303 self.detect = detect
304
305 if convolved is None:
306 _detect = Image(detect)
307 convolved = observation.convolve(_detect.repeat(observation.bands), mode="real")
308 else:
309 logger.warning(
310 "convolved is deprecated and will be removed after v29.0. "
311 "The convolved image is now calculated in __init__ and does "
312 "not need to be specified."
313 )
314
315 if thresh is not None:
316 initial_bg_thresh = thresh
317
318 self.observation = observation
319 self.convolved = convolved
320 # Ensure that each center is a tuple of integers
321 centers = [(int(round(c[0])), int(round(c[1]))) for c in centers]
322 self.centers = centers
323 self.min_snr = min_snr
324 self.monotonicity = monotonicity
325 self.use_sparse_init = use_sparse_init
326 self.is_symmetric = is_symmetric
327
328 # Get the model PSF
329 # Convolve the PSF in order to set the spectrum
330 # of a point source correctly.
331 model_psf = Image(cast(np.ndarray, observation.model_psf)[0])
332 convolved_psf = model_psf.repeat(observation.bands)
333 self.convolved_psf = observation.convolve(convolved_psf, mode="real").data
334 # Get the "spectrum" of the PSF
335 self.py = model_psf.shape[0] // 2
336 self.px = model_psf.shape[1] // 2
337 self.psf_spectrum = self.convolved_psf[:, self.py, self.px]
338
339 # Set the maximum number of components for any source in the blend
340 if max_components < 0 or max_components > 2:
341 raise ValueError(f"max_components must be 0, 1 or 2, got {max_components}")
342 self.max_components = max_components
343 self.initial_bg_thresh = initial_bg_thresh
344 self.bg_thresh = bg_thresh
345 self.padding = padding
346 self.disk_percentile = disk_percentile
347
348 # Initalize all of the sources
349 sources = []
350 for center in centers:
351 if max_components == 0:
352 source = Source([self.get_psf_component(center)])
353 else:
354 source = self.init_source(center)
355 sources.append(source)
356 self.sources = sources
357
358 @property
359 def thresh(self):
360 logger.warning(
361 "thresh is deprecated and will be removed after v29.0. " "Use initial_bg_thresh instead."
362 )
363 return self.initial_bg_thresh
364
365 def get_snr(self, center: tuple[int, int]) -> float:
366 """Get the SNR at the center of a component
367
368 Parameters
369 ----------
370 center:
371 The location of the center of the source.
372
373 Returns
374 -------
375 result:
376 The SNR at the center of the component.
377 """
378 snr = np.floor(
379 calculate_snr(
380 self.observation.images,
381 self.observation.variance,
382 self.observation.psfs,
383 center,
384 )
385 )
386 return snr / self.min_snr
387
388 def get_psf_component(self, center: tuple[int, int]) -> FactorizedComponent:
389 """Create a factorized component with a PSF morphology
390
391 Parameters
392 ----------
393 center:
394 The center of the component.
395
396 Returns
397 -------
398 component:
399 A `FactorizedComponent` with a PSF-like morphology.
400 """
401 local_center = (
402 center[0] - self.observation.bbox.origin[0],
403 center[1] - self.observation.bbox.origin[1],
404 )
405 # There wasn't sufficient flux for an extended source,
406 # so create a PSF source.
407 spectrum_center = (slice(None), local_center[0], local_center[1])
408 spectrum = self.observation.images.data[spectrum_center] / self.psf_spectrum
409 spectrum[spectrum < 0] = 0
410
411 psf = cast(np.ndarray, self.observation.model_psf)[0].copy()
412 py = psf.shape[0] // 2
413 px = psf.shape[1] // 2
414 bbox = Box(psf.shape, origin=(-py + center[0], -px + center[1]))
415 bbox = self.observation.bbox & bbox
416 morph = Image(psf, yx0=cast(tuple[int, int], bbox.origin))[bbox].data
417 component = FactorizedComponent(
418 self.observation.bands,
419 spectrum,
420 morph,
421 bbox,
422 center,
423 self.observation.noise_rms,
424 monotonicity=self.monotonicity,
425 is_symmetric=self.is_symmetric,
426 )
427 return component
428
430 self,
431 center: tuple[int, int],
432 detect: np.ndarray,
433 thresh: float,
434 padding: int,
435 ) -> FactorizedComponent | None:
436 """Initialize parameters for a `FactorizedComponent`
437
438 Parameters
439 ----------
440 center:
441 The location of the center of the source to detect in the
442 full image.
443 detect:
444 The image used for detection of the morphology.
445 thresh:
446 The lower cutoff threshold to use for the morphology.
447 padding:
448 The amount to pad the morphology to allow for extra flux
449 in the first few iterations before resizing.
450
451 Returns
452 -------
453 component:
454 A `FactorizedComponent` created from the detection image.
455
456 """
457 local_center = (
458 center[0] - self.observation.bbox.origin[0],
459 center[1] - self.observation.bbox.origin[1],
460 )
461
462 if self.use_sparse_init:
463 monotonicity = None
464 else:
465 monotonicity = self.monotonicity
466 bbox, morph = init_monotonic_morph(
467 detect,
468 center,
469 self.observation.bbox,
470 padding=padding,
471 normalize=False,
472 monotonicity=monotonicity,
473 threshold=thresh,
474 )
475
476 if morph is None:
477 return None
478 morph = morph[(bbox - self.observation.bbox.origin).slices]
479
480 spectrum_center = (slice(None), local_center[0], local_center[1])
481 images = self.observation.images
482
483 convolved = self.convolved
484 spectrum = images.data[spectrum_center] / convolved.data[spectrum_center]
485 spectrum[spectrum < 0] = 0
486 morph_max = np.max(morph)
487 spectrum *= morph_max
488 morph /= morph_max
489
490 return FactorizedComponent(
491 bands=self.observation.bands,
492 spectrum=spectrum,
493 morph=morph,
494 bbox=bbox,
495 peak=center,
496 bg_rms=self.observation.noise_rms,
497 bg_thresh=self.bg_thresh,
498 monotonicity=self.monotonicity,
499 is_symmetric=self.is_symmetric,
500 )
501
502 def init_source(self, center: tuple[int, int]) -> Source:
503 """Initialize a source from a chi^2 detection.
504
505 Parameter
506 ---------
507 center:
508 The center of the source.
509 init:
510 The initialization parameters common to all of the sources.
511 max_components:
512 The maximum number of components in the source.
513 """
514 # Some operators need the local center, not center in the full image
515 local_center = (
516 center[0] - self.observation.bbox.origin[0],
517 center[1] - self.observation.bbox.origin[1],
518 )
519
520 # Calculate the signal to noise at the center of this source
521 component_snr = self.get_snr(center)
522
523 # Initialize the bbox, morph, and spectrum
524 # for a single component source
525 detect = prox_uncentered_symmetry(self.detect.copy(), local_center, fill=0)
526 thresh = np.mean(self.observation.noise_rms) * self.initial_bg_thresh
527 component = self.get_single_component(center, detect, thresh, self.padding)
528
529 if component is None:
530 # There wasn't enough flux to initialize the source as
531 # as single component, so initialize it with the model PSF.
532 components = [self.get_psf_component(center)]
533 elif component_snr < 2 or self.max_components < 2:
534 # There isn't sufficient flux to add a second component.
535 components = [component]
536 else:
537 # There was enough flux for a 2-component source,
538 # so split the single component model into two components,
539 # using the same algorithm as scarlet main.
540 bulge_morph = component.morph.copy()
541 disk_morph = component.morph
542 # Set the threshold for the bulge.
543 # Since the morphology is monotonic, this selects the inner
544 # of the single component morphology and assigns it to the bulge.
545 flux_thresh = self.disk_percentile / 100
546 mask = disk_morph > flux_thresh
547 # Remove the flux above the threshold so that the disk will have
548 # a flat center.
549 disk_morph[mask] = flux_thresh
550 # Subtract off the thresholded flux (since we're normalizing the
551 # morphology anyway) so that it does not have a sharp
552 # discontinuity at the edge.
553 bulge_morph -= flux_thresh
554 bulge_morph[bulge_morph < 0] = 0
555
556 bulge_morph /= np.max(bulge_morph)
557 disk_morph /= np.max(disk_morph)
558
559 # Fit the spectra assuming that all of the flux in the image
560 # is due to both components. This is not true, but for the
561 # vast majority of sources this is a good approximation.
562 try:
563 bulge_spectrum, disk_spectrum = multifit_spectra(
564 self.observation,
565 [
566 Image(bulge_morph, yx0=cast(tuple[int, int], component.bbox.origin)),
567 Image(disk_morph, yx0=cast(tuple[int, int], component.bbox.origin)),
568 ],
569 )
570
571 components = [
573 bands=self.observation.bands,
574 spectrum=bulge_spectrum,
575 morph=bulge_morph,
576 bbox=component.bbox.copy(),
577 peak=center,
578 bg_rms=self.observation.noise_rms,
579 bg_thresh=self.bg_thresh,
580 monotonicity=self.monotonicity,
581 is_symmetric=self.is_symmetric,
582 ),
584 bands=self.observation.bands,
585 spectrum=disk_spectrum,
586 morph=disk_morph,
587 bbox=component.bbox.copy(),
588 peak=center,
589 bg_rms=self.observation.noise_rms,
590 bg_thresh=self.bg_thresh,
591 monotonicity=self.monotonicity,
592 is_symmetric=self.is_symmetric,
593 ),
594 ]
595 except np.linalg.LinAlgError:
596 components = [component]
597
598 return Source(components) # type: ignore
599
600
601@deprecated(
602 reason="This class is replaced by FactorizedInitialization and will be removed after v29.0",
603 version="v29.0",
604 category=FutureWarning,
605)
607 """Initialize all sources with chi^2 detections
608
609 There are a large number of parameters that are universal for all of the
610 sources being initialized from the same set of observed images.
611 To simplify the API those parameters are all initialized by this class
612 and passed to `init_main_source` for each source.
613 It also creates temporary objects that only need to be created once for
614 all of the sources in a blend.
615
616 Parameters
617 ----------
618 observation:
619 The observation containing the blend
620 centers:
621 The center of each source to initialize.
622 detect:
623 The array that contains a 2D image used for detection.
624 min_snr:
625 The minimum SNR required per component.
626 So a 2-component source requires at least `2*min_snr` while sources
627 with SNR < `min_snr` will be initialized with the PSF.
628 monotonicity:
629 When `monotonicity` is `None`,
630 the component is initialized with only the
631 monotonic pixels, otherwise the monotonicity operator is used to
632 project the morphology to a monotonic solution.
633 disk_percentile:
634 The percentage of the overall flux to attribute to the disk.
635 thresh:
636 The threshold used to trim the morphology,
637 so all pixels below `thresh * bg_rms` are set to zero.
638 padding:
639 The amount to pad the morphology to allow for extra flux
640 in the first few iterations before resizing.
641 """
642
643 pass
644
645
646@deprecated(
647 reason="FactorizedWaveletInitialization will be removed after v29.0 "
648 "since it does not appear to offer any advantages over "
649 "FactorizedChi2Initialization. Consider switching to "
650 "FactorizedInitialization now.",
651 version="v29.0",
652 category=FutureWarning,
653)
655 """Parameters used to initialize all sources with wavelet detections
656
657 There are a large number of parameters that are universal for all of the
658 sources being initialized from the same set of wavelet coefficients.
659 To simplify the API those parameters are all initialized by this class
660 and passed to `init_wavelet_source` for each source.
661
662 Parameters
663 ----------
664 observation:
665 The multiband observation of the blend.
666 centers:
667 The center of each source to initialize.
668 bulge_slice, disk_slice:
669 The slice used to select the wavelet scales used for the
670 bulge/disk.
671 bulge_padding, disk_padding:
672 The number of pixels to grow the bounding box of the bulge/disk
673 to leave extra room for growth in the first few iterations.
674 use_psf:
675 Whether or not to use the PSF for single component sources.
676 If `use_psf` is `False` then only sources with low signal
677 at all scales are initialized with the PSF morphology.
678 scales:
679 Number of wavelet scales to use.
680 wavelets:
681 The array of wavelet coefficients `(scale, y, x)`
682 used for detection.
683 monotonicity:
684 When `monotonicity` is `None`,
685 the component is initialized with only the
686 monotonic pixels, otherwise the monotonicity operator is used to
687 project the morphology to a monotonic solution.
688 min_snr:
689 The minimum SNR required per component.
690 So a 2-component source requires at least `2*min_snr` while sources
691 with SNR < `min_snr` will be initialized with the PSF.
692 """
693
695 self,
696 observation: Observation,
697 centers: Sequence[tuple[int, int]],
698 bulge_slice: slice = slice(None, 2),
699 disk_slice: slice = slice(2, -1),
700 bulge_padding: int = 5,
701 disk_padding: int = 5,
702 use_psf: bool = True,
703 scales: int = 5,
704 wavelets: np.ndarray | None = None,
705 monotonicity: Monotonicity | None = None,
706 min_snr: float = 50,
707 use_sparse_init: bool = True,
708 ):
709 if wavelets is None:
710 wavelets = get_detect_wavelets(
711 observation.images.data,
712 observation.variance.data,
713 scales=scales,
714 )
715 wavelets[wavelets < 0] = 0
716 # The detection coadd for single component sources
717 detectlets = np.sum(wavelets[:-1], axis=0)
718 # The detection coadd for the bulge
719 bulgelets = np.sum(wavelets[bulge_slice], axis=0)
720 # The detection coadd for the disk
721 disklets = np.sum(wavelets[disk_slice], axis=0)
722
723 self.detectlets = detectlets
724 self.bulgelets = bulgelets
725 self.disklets = disklets
726 self.bulge_grow = bulge_padding
727 self.disk_grow = disk_padding
728 self.use_psf = use_psf
729
730 # Initialize the sources
731 super().__init__(
732 observation=observation,
733 centers=centers,
734 detect=detectlets,
735 min_snr=min_snr,
736 monotonicity=monotonicity,
737 use_sparse_init=use_sparse_init,
738 )
739
740 def init_source(self, center: tuple[int, int]) -> Source:
741 """Initialize a source from a chi^2 detection.
742
743 Parameter
744 ---------
745 center:
746 The center of the source.
747 """
748 local_center = (
749 center[0] - self.observation.bbox.origin[0],
750 center[1] - self.observation.bbox.origin[1],
751 )
752 nbr_components = self.get_snr(center)
753 observation = self.observation
754
755 if (nbr_components < 1 and self.use_psf) or self.detectlets[local_center[0], local_center[1]] <= 0:
756 # Initialize the source as an PSF source
757 components = [self.get_psf_component(center)]
758 elif nbr_components < 2:
759 # Inititialize with a single component
760 component = self.get_single_component(center, self.detectlets, 0, self.disk_grow)
761 if component is not None:
762 components = [component]
763 else:
764 # Initialize with a 2 component model
765 bulge_box, bulge_morph = init_monotonic_morph(
766 self.bulgelets, center, observation.bbox, self.bulge_grow
767 )
768 disk_box, disk_morph = init_monotonic_morph(
769 self.disklets, center, observation.bbox, self.disk_grow
770 )
771 if bulge_morph is None or disk_morph is None:
772 if bulge_morph is None:
773 if disk_morph is None:
774 raise RuntimeError("Both components are None")
775 # One of the components was null,
776 # so initialize as a single component
777 component = self.get_single_component(center, self.detectlets, 0, self.disk_grow)
778 if component is not None:
779 components = [component]
780 else:
781 local_bulge_box = bulge_box - self.observation.bbox.origin
782 local_disk_box = disk_box - self.observation.bbox.origin
783 bulge_morph = bulge_morph[local_bulge_box.slices]
784 disk_morph = disk_morph[local_disk_box.slices]
785
786 bulge_spectrum, disk_spectrum = multifit_spectra(
787 observation,
788 [
789 Image(bulge_morph, yx0=cast(tuple[int, int], bulge_box.origin)),
790 Image(disk_morph, yx0=cast(tuple[int, int], disk_box.origin)),
791 ],
792 )
793
794 components = []
795 if np.sum(bulge_spectrum != 0):
796 components.append(
798 observation.bands,
799 bulge_spectrum,
800 bulge_morph,
801 bulge_box,
802 center,
803 monotonicity=self.monotonicity,
804 )
805 )
806 else:
807 logger.debug("cut bulge")
808 if np.sum(disk_spectrum) != 0:
809 components.append(
811 observation.bands,
812 disk_spectrum,
813 disk_morph,
814 disk_box,
815 center,
816 monotonicity=self.monotonicity,
817 )
818 )
819 else:
820 logger.debug("cut disk")
821 return Source(components) # type: ignore
A class to represent a 2-dimensional array of pixels.
Definition Image.h:51
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, np.ndarray|None detect=None, float min_snr=50, Monotonicity|None monotonicity=None, float disk_percentile=25, float initial_bg_thresh=0.5, float bg_thresh=0.25, float|None thresh=None, int padding=2, bool use_sparse_init=True, int max_components=2, Image|None convolved=None, bool is_symmetric=False)
__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, bool use_sparse_init=True)
np.ndarray multifit_spectra(Observation observation, Sequence[Image] morphs, Image|None model=None)
tuple[np.ndarray, Box] trim_morphology(np.ndarray morph, float threshold=0, int padding=5, float|None bg_thresh=None)
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 threshold=0, int max_iter=0, int center_radius=1, float variance_factor=0)