LSST Applications g042eb84c57+730a74494b,g04e9c324dd+8c5ae1fdc5,g134cb467dc+1f1e3e7524,g199a45376c+0ba108daf9,g1fd858c14a+fa7d31856b,g210f2d0738+f66ac109ec,g262e1987ae+83a3acc0e5,g29ae962dfc+d856a2cb1f,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+a1e0c9f713,g47891489e3+0d594cb711,g4d44eb3520+c57ec8f3ed,g4d7b6aa1c5+f66ac109ec,g53246c7159+8c5ae1fdc5,g56a1a4eaf3+fd7ad03fde,g64539dfbff+f66ac109ec,g67b6fd64d1+0d594cb711,g67fd3c3899+f66ac109ec,g6985122a63+0d594cb711,g74acd417e5+3098891321,g786e29fd12+668abc6043,g81db2e9a8d+98e2ab9f28,g87389fa792+8856018cbb,g89139ef638+0d594cb711,g8d7436a09f+80fda9ce03,g8ea07a8fe4+760ca7c3fc,g90f42f885a+033b1d468d,g97be763408+a8a29bda4b,g99822b682c+e3ec3c61f9,g9d5c6a246b+0d5dac0c3d,ga41d0fce20+9243b26dd2,gbf99507273+8c5ae1fdc5,gd7ef33dd92+0d594cb711,gdab6d2f7ff+3098891321,ge410e46f29+0d594cb711,geaed405ab2+c4bbc419c6,gf9a733ac38+8c5ae1fdc5,w.2025.38
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 self.centers = centers
321 self.min_snr = min_snr
322 self.monotonicity = monotonicity
323 self.use_sparse_init = use_sparse_init
324 self.is_symmetric = is_symmetric
325
326 # Get the model PSF
327 # Convolve the PSF in order to set the spectrum
328 # of a point source correctly.
329 model_psf = Image(cast(np.ndarray, observation.model_psf)[0])
330 convolved_psf = model_psf.repeat(observation.bands)
331 self.convolved_psf = observation.convolve(convolved_psf, mode="real").data
332 # Get the "spectrum" of the PSF
333 self.py = model_psf.shape[0] // 2
334 self.px = model_psf.shape[1] // 2
335 self.psf_spectrum = self.convolved_psf[:, self.py, self.px]
336
337 # Set the maximum number of components for any source in the blend
338 if max_components < 0 or max_components > 2:
339 raise ValueError(f"max_components must be 0, 1 or 2, got {max_components}")
340 self.max_components = max_components
341 self.initial_bg_thresh = initial_bg_thresh
342 self.bg_thresh = bg_thresh
343 self.padding = padding
344 self.disk_percentile = disk_percentile
345
346 # Initalize all of the sources
347 sources = []
348 for center in centers:
349 if max_components == 0:
350 source = Source([self.get_psf_component(center)])
351 else:
352 source = self.init_source((int(center[0]), int(center[1])))
353 sources.append(source)
354 self.sources = sources
355
356 @property
357 def thresh(self):
358 logger.warning(
359 "thresh is deprecated and will be removed after v29.0. " "Use initial_bg_thresh instead."
360 )
361 return self.initial_bg_thresh
362
363 def get_snr(self, center: tuple[int, int]) -> float:
364 """Get the SNR at the center of a component
365
366 Parameters
367 ----------
368 center:
369 The location of the center of the source.
370
371 Returns
372 -------
373 result:
374 The SNR at the center of the component.
375 """
376 snr = np.floor(
377 calculate_snr(
378 self.observation.images,
379 self.observation.variance,
380 self.observation.psfs,
381 center,
382 )
383 )
384 return snr / self.min_snr
385
386 def get_psf_component(self, center: tuple[int, int]) -> FactorizedComponent:
387 """Create a factorized component with a PSF morphology
388
389 Parameters
390 ----------
391 center:
392 The center of the component.
393
394 Returns
395 -------
396 component:
397 A `FactorizedComponent` with a PSF-like morphology.
398 """
399 local_center = (
400 center[0] - self.observation.bbox.origin[0],
401 center[1] - self.observation.bbox.origin[1],
402 )
403 # There wasn't sufficient flux for an extended source,
404 # so create a PSF source.
405 spectrum_center = (slice(None), local_center[0], local_center[1])
406 spectrum = self.observation.images.data[spectrum_center] / self.psf_spectrum
407 spectrum[spectrum < 0] = 0
408
409 psf = cast(np.ndarray, self.observation.model_psf)[0].copy()
410 py = psf.shape[0] // 2
411 px = psf.shape[1] // 2
412 bbox = Box(psf.shape, origin=(-py + center[0], -px + center[1]))
413 bbox = self.observation.bbox & bbox
414 morph = Image(psf, yx0=cast(tuple[int, int], bbox.origin))[bbox].data
415 component = FactorizedComponent(
416 self.observation.bands,
417 spectrum,
418 morph,
419 bbox,
420 center,
421 self.observation.noise_rms,
422 monotonicity=self.monotonicity,
423 is_symmetric=self.is_symmetric,
424 )
425 return component
426
428 self,
429 center: tuple[int, int],
430 detect: np.ndarray,
431 thresh: float,
432 padding: int,
433 ) -> FactorizedComponent | None:
434 """Initialize parameters for a `FactorizedComponent`
435
436 Parameters
437 ----------
438 center:
439 The location of the center of the source to detect in the
440 full image.
441 detect:
442 The image used for detection of the morphology.
443 thresh:
444 The lower cutoff threshold to use for the morphology.
445 padding:
446 The amount to pad the morphology to allow for extra flux
447 in the first few iterations before resizing.
448
449 Returns
450 -------
451 component:
452 A `FactorizedComponent` created from the detection image.
453
454 """
455 local_center = (
456 center[0] - self.observation.bbox.origin[0],
457 center[1] - self.observation.bbox.origin[1],
458 )
459
460 if self.use_sparse_init:
461 monotonicity = None
462 else:
463 monotonicity = self.monotonicity
464 bbox, morph = init_monotonic_morph(
465 detect,
466 center,
467 self.observation.bbox,
468 padding=padding,
469 normalize=False,
470 monotonicity=monotonicity,
471 threshold=thresh,
472 )
473
474 if morph is None:
475 return None
476 morph = morph[(bbox - self.observation.bbox.origin).slices]
477
478 spectrum_center = (slice(None), local_center[0], local_center[1])
479 images = self.observation.images
480
481 convolved = self.convolved
482 spectrum = images.data[spectrum_center] / convolved.data[spectrum_center]
483 spectrum[spectrum < 0] = 0
484 morph_max = np.max(morph)
485 spectrum *= morph_max
486 morph /= morph_max
487
488 return FactorizedComponent(
489 bands=self.observation.bands,
490 spectrum=spectrum,
491 morph=morph,
492 bbox=bbox,
493 peak=center,
494 bg_rms=self.observation.noise_rms,
495 bg_thresh=self.bg_thresh,
496 monotonicity=self.monotonicity,
497 is_symmetric=self.is_symmetric,
498 )
499
500 def init_source(self, center: tuple[int, int]) -> Source:
501 """Initialize a source from a chi^2 detection.
502
503 Parameter
504 ---------
505 center:
506 The center of the source.
507 init:
508 The initialization parameters common to all of the sources.
509 max_components:
510 The maximum number of components in the source.
511 """
512 # Some operators need the local center, not center in the full image
513 local_center = (
514 center[0] - self.observation.bbox.origin[0],
515 center[1] - self.observation.bbox.origin[1],
516 )
517
518 # Calculate the signal to noise at the center of this source
519 component_snr = self.get_snr(center)
520
521 # Initialize the bbox, morph, and spectrum
522 # for a single component source
523 detect = prox_uncentered_symmetry(self.detect.copy(), local_center, fill=0)
524 thresh = np.mean(self.observation.noise_rms) * self.initial_bg_thresh
525 component = self.get_single_component(center, detect, thresh, self.padding)
526
527 if component is None:
528 # There wasn't enough flux to initialize the source as
529 # as single component, so initialize it with the model PSF.
530 components = [self.get_psf_component(center)]
531 elif component_snr < 2 or self.max_components < 2:
532 # There isn't sufficient flux to add a second component.
533 components = [component]
534 else:
535 # There was enough flux for a 2-component source,
536 # so split the single component model into two components,
537 # using the same algorithm as scarlet main.
538 bulge_morph = component.morph.copy()
539 disk_morph = component.morph
540 # Set the threshold for the bulge.
541 # Since the morphology is monotonic, this selects the inner
542 # of the single component morphology and assigns it to the bulge.
543 flux_thresh = self.disk_percentile / 100
544 mask = disk_morph > flux_thresh
545 # Remove the flux above the threshold so that the disk will have
546 # a flat center.
547 disk_morph[mask] = flux_thresh
548 # Subtract off the thresholded flux (since we're normalizing the
549 # morphology anyway) so that it does not have a sharp
550 # discontinuity at the edge.
551 bulge_morph -= flux_thresh
552 bulge_morph[bulge_morph < 0] = 0
553
554 bulge_morph /= np.max(bulge_morph)
555 disk_morph /= np.max(disk_morph)
556
557 # Fit the spectra assuming that all of the flux in the image
558 # is due to both components. This is not true, but for the
559 # vast majority of sources this is a good approximation.
560 try:
561 bulge_spectrum, disk_spectrum = multifit_spectra(
562 self.observation,
563 [
564 Image(bulge_morph, yx0=cast(tuple[int, int], component.bbox.origin)),
565 Image(disk_morph, yx0=cast(tuple[int, int], component.bbox.origin)),
566 ],
567 )
568
569 components = [
571 bands=self.observation.bands,
572 spectrum=bulge_spectrum,
573 morph=bulge_morph,
574 bbox=component.bbox.copy(),
575 peak=center,
576 bg_rms=self.observation.noise_rms,
577 bg_thresh=self.bg_thresh,
578 monotonicity=self.monotonicity,
579 is_symmetric=self.is_symmetric,
580 ),
582 bands=self.observation.bands,
583 spectrum=disk_spectrum,
584 morph=disk_morph,
585 bbox=component.bbox.copy(),
586 peak=center,
587 bg_rms=self.observation.noise_rms,
588 bg_thresh=self.bg_thresh,
589 monotonicity=self.monotonicity,
590 is_symmetric=self.is_symmetric,
591 ),
592 ]
593 except np.linalg.LinAlgError:
594 components = [component]
595
596 return Source(components) # type: ignore
597
598
599@deprecated(
600 reason="This class is replaced by FactorizedInitialization and will be removed after v29.0",
601 version="v29.0",
602 category=FutureWarning,
603)
605 """Initialize all sources with chi^2 detections
606
607 There are a large number of parameters that are universal for all of the
608 sources being initialized from the same set of observed images.
609 To simplify the API those parameters are all initialized by this class
610 and passed to `init_main_source` for each source.
611 It also creates temporary objects that only need to be created once for
612 all of the sources in a blend.
613
614 Parameters
615 ----------
616 observation:
617 The observation containing the blend
618 centers:
619 The center of each source to initialize.
620 detect:
621 The array that contains a 2D image used for detection.
622 min_snr:
623 The minimum SNR required per component.
624 So a 2-component source requires at least `2*min_snr` while sources
625 with SNR < `min_snr` will be initialized with the PSF.
626 monotonicity:
627 When `monotonicity` is `None`,
628 the component is initialized with only the
629 monotonic pixels, otherwise the monotonicity operator is used to
630 project the morphology to a monotonic solution.
631 disk_percentile:
632 The percentage of the overall flux to attribute to the disk.
633 thresh:
634 The threshold used to trim the morphology,
635 so all pixels below `thresh * bg_rms` are set to zero.
636 padding:
637 The amount to pad the morphology to allow for extra flux
638 in the first few iterations before resizing.
639 """
640
641 pass
642
643
644@deprecated(
645 reason="FactorizedWaveletInitialization will be removed after v29.0 "
646 "since it does not appear to offer any advantages over "
647 "FactorizedChi2Initialization. Consider switching to "
648 "FactorizedInitialization now.",
649 version="v29.0",
650 category=FutureWarning,
651)
653 """Parameters used to initialize all sources with wavelet detections
654
655 There are a large number of parameters that are universal for all of the
656 sources being initialized from the same set of wavelet coefficients.
657 To simplify the API those parameters are all initialized by this class
658 and passed to `init_wavelet_source` for each source.
659
660 Parameters
661 ----------
662 observation:
663 The multiband observation of the blend.
664 centers:
665 The center of each source to initialize.
666 bulge_slice, disk_slice:
667 The slice used to select the wavelet scales used for the
668 bulge/disk.
669 bulge_padding, disk_padding:
670 The number of pixels to grow the bounding box of the bulge/disk
671 to leave extra room for growth in the first few iterations.
672 use_psf:
673 Whether or not to use the PSF for single component sources.
674 If `use_psf` is `False` then only sources with low signal
675 at all scales are initialized with the PSF morphology.
676 scales:
677 Number of wavelet scales to use.
678 wavelets:
679 The array of wavelet coefficients `(scale, y, x)`
680 used for detection.
681 monotonicity:
682 When `monotonicity` is `None`,
683 the component is initialized with only the
684 monotonic pixels, otherwise the monotonicity operator is used to
685 project the morphology to a monotonic solution.
686 min_snr:
687 The minimum SNR required per component.
688 So a 2-component source requires at least `2*min_snr` while sources
689 with SNR < `min_snr` will be initialized with the PSF.
690 """
691
693 self,
694 observation: Observation,
695 centers: Sequence[tuple[int, int]],
696 bulge_slice: slice = slice(None, 2),
697 disk_slice: slice = slice(2, -1),
698 bulge_padding: int = 5,
699 disk_padding: int = 5,
700 use_psf: bool = True,
701 scales: int = 5,
702 wavelets: np.ndarray | None = None,
703 monotonicity: Monotonicity | None = None,
704 min_snr: float = 50,
705 use_sparse_init: bool = True,
706 ):
707 if wavelets is None:
708 wavelets = get_detect_wavelets(
709 observation.images.data,
710 observation.variance.data,
711 scales=scales,
712 )
713 wavelets[wavelets < 0] = 0
714 # The detection coadd for single component sources
715 detectlets = np.sum(wavelets[:-1], axis=0)
716 # The detection coadd for the bulge
717 bulgelets = np.sum(wavelets[bulge_slice], axis=0)
718 # The detection coadd for the disk
719 disklets = np.sum(wavelets[disk_slice], axis=0)
720
721 self.detectlets = detectlets
722 self.bulgelets = bulgelets
723 self.disklets = disklets
724 self.bulge_grow = bulge_padding
725 self.disk_grow = disk_padding
726 self.use_psf = use_psf
727
728 # Initialize the sources
729 super().__init__(
730 observation=observation,
731 centers=centers,
732 detect=detectlets,
733 min_snr=min_snr,
734 monotonicity=monotonicity,
735 use_sparse_init=use_sparse_init,
736 )
737
738 def init_source(self, center: tuple[int, int]) -> Source:
739 """Initialize a source from a chi^2 detection.
740
741 Parameter
742 ---------
743 center:
744 The center of the source.
745 """
746 local_center = (
747 center[0] - self.observation.bbox.origin[0],
748 center[1] - self.observation.bbox.origin[1],
749 )
750 nbr_components = self.get_snr(center)
751 observation = self.observation
752
753 if (nbr_components < 1 and self.use_psf) or self.detectlets[local_center[0], local_center[1]] <= 0:
754 # Initialize the source as an PSF source
755 components = [self.get_psf_component(center)]
756 elif nbr_components < 2:
757 # Inititialize with a single component
758 component = self.get_single_component(center, self.detectlets, 0, self.disk_grow)
759 if component is not None:
760 components = [component]
761 else:
762 # Initialize with a 2 component model
763 bulge_box, bulge_morph = init_monotonic_morph(
764 self.bulgelets, center, observation.bbox, self.bulge_grow
765 )
766 disk_box, disk_morph = init_monotonic_morph(
767 self.disklets, center, observation.bbox, self.disk_grow
768 )
769 if bulge_morph is None or disk_morph is None:
770 if bulge_morph is None:
771 if disk_morph is None:
772 raise RuntimeError("Both components are None")
773 # One of the components was null,
774 # so initialize as a single component
775 component = self.get_single_component(center, self.detectlets, 0, self.disk_grow)
776 if component is not None:
777 components = [component]
778 else:
779 local_bulge_box = bulge_box - self.observation.bbox.origin
780 local_disk_box = disk_box - self.observation.bbox.origin
781 bulge_morph = bulge_morph[local_bulge_box.slices]
782 disk_morph = disk_morph[local_disk_box.slices]
783
784 bulge_spectrum, disk_spectrum = multifit_spectra(
785 observation,
786 [
787 Image(bulge_morph, yx0=cast(tuple[int, int], bulge_box.origin)),
788 Image(disk_morph, yx0=cast(tuple[int, int], disk_box.origin)),
789 ],
790 )
791
792 components = []
793 if np.sum(bulge_spectrum != 0):
794 components.append(
796 observation.bands,
797 bulge_spectrum,
798 bulge_morph,
799 bulge_box,
800 center,
801 monotonicity=self.monotonicity,
802 )
803 )
804 else:
805 logger.debug("cut bulge")
806 if np.sum(disk_spectrum) != 0:
807 components.append(
809 observation.bands,
810 disk_spectrum,
811 disk_morph,
812 disk_box,
813 center,
814 monotonicity=self.monotonicity,
815 )
816 )
817 else:
818 logger.debug("cut disk")
819 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)