LSST Applications g0fba68d861+bb7a7cfa1f,g1ec0fe41b4+f536777771,g1fd858c14a+470a99fdf4,g216c3ac8a7+0d4d80193f,g35bb328faa+fcb1d3bbc8,g4d2262a081+23bd310d1b,g53246c7159+fcb1d3bbc8,g56a49b3a55+369644a549,g5a012ec0e7+3632fc3ff3,g60b5630c4e+3bfb9058a5,g67b6fd64d1+ed4b5058f4,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g8180f54f50+60bd39f3b6,g8352419a5c+fcb1d3bbc8,g87d29937c9+57a68d035f,g8852436030+4699110379,g89139ef638+ed4b5058f4,g9125e01d80+fcb1d3bbc8,g94187f82dc+3bfb9058a5,g989de1cb63+ed4b5058f4,g9ccd5d7f00+b7cae620c0,g9d31334357+3bfb9058a5,g9f33ca652e+00883ace41,gabe3b4be73+1e0a283bba,gabf8522325+fa80ff7197,gb1101e3267+27b24065a3,gb58c049af0+f03b321e39,gb89ab40317+ed4b5058f4,gc0af124501+708fe67c54,gcf25f946ba+4699110379,gd6cbbdb0b4+bb83cc51f8,gde0f65d7ad+acd5afb0eb,ge1ad929117+3bfb9058a5,ge278dab8ac+d65b3c2b70,ge410e46f29+ed4b5058f4,gf5e32f922b+fcb1d3bbc8,gf67bdafdda+ed4b5058f4,w.2025.17
LSST Data Management Base Package
Loading...
Searching...
No Matches
io.py
Go to the documentation of this file.
1from __future__ import annotations
2
3import json
4import logging
5from abc import ABC, abstractmethod
6from dataclasses import dataclass
7from typing import Any, Callable, ClassVar
8
9import numpy as np
10from numpy.typing import DTypeLike
11
12from .bbox import Box
13from .blend import Blend
14from .component import Component, FactorizedComponent
15from .image import Image
16from .observation import Observation
17from .parameters import FixedParameter
18from .source import Source
19
20__all__ = [
21 "ScarletComponentData",
22 "ScarletFactorizedComponentData",
23 "ScarletSourceData",
24 "ScarletBlendData",
25 "ScarletModelData",
26 "ComponentCube",
27]
28
29logger = logging.getLogger(__name__)
30
31
32@dataclass(kw_only=True)
34 """Base data for a scarlet component"""
35
36 component_registry: ClassVar[dict[str, type[ScarletComponentBaseData]]] = {}
37 component_type: str
38
39 @classmethod
40 def register(cls) -> None:
41 """Register a new component type"""
42 ScarletComponentBaseData.component_registry[cls.component_type] = cls
43
44 @abstractmethod
45 def to_component(self, observation: Observation) -> Component:
46 """Convert the storage data model into a scarlet Component
47
48 Parameters
49 ----------
50 observation :
51 The observation that the component is associated with
52
53 Returns
54 -------
55 component :
56 A scarlet component extracted from persisted data.
57 """
58
59 @abstractmethod
60 def as_dict(self) -> dict:
61 """Return the object encoded into a dict for JSON serialization
62
63 Returns
64 -------
65 result :
66 The object encoded as a JSON compatible dict
67 """
68
69 @staticmethod
70 def from_dict(data: dict, dtype: DTypeLike | None = None) -> ScarletComponentBaseData:
71 """Reconstruct `ScarletComponentBaseData` from JSON compatible
72 dict.
73
74 Parameters
75 ----------
76 data :
77 Dictionary representation of the object
78 dtype :
79 Datatype of the resulting model.
80
81 Returns
82 -------
83 result :
84 The reconstructed object
85 """
86 component_type = data["component_type"]
87 cls = ScarletComponentBaseData.component_registry[component_type]
88 return cls.from_dict(data, dtype=dtype)
89
90 @staticmethod
91 def from_component(component: Component) -> ScarletComponentBaseData:
92 """Reconstruct `ScarletComponentBaseData` from a scarlet Component.
93
94 Parameters
95 ----------
96 component :
97 The scarlet component to be converted.
98
99 Returns
100 -------
101 result :
102 The reconstructed object
103 """
104 if isinstance(component, FactorizedComponent):
105 return ScarletFactorizedComponentData._from_component(component)
106 else:
107 return ScarletComponentData._from_component(component)
108
109
110@dataclass(kw_only=True)
112 """Data for a component expressed as a 3D data cube
113
114 This is used for scarlet component models that are not factorized,
115 storing their entire model as a 3D data cube (bands, y, x).
116
117 Attributes
118 ----------
119 origin :
120 The lower bound of the components bounding box.
121 peak :
122 The peak of the component.
123 model :
124 The model for the component.
125 """
126
127 origin: tuple[int, int]
128 peak: tuple[float, float]
129 model: np.ndarray
130 component_type: str = "component"
131
132 @property
133 def shape(self):
134 return self.model.shape[-2:]
135
136 def to_component(self, observation: Observation) -> ComponentCube:
137 """Convert the storage data model into a scarlet Component
138
139 Parameters
140 ----------
141 observation :
142 The observation that the component is associated with
143
144 Returns
145 -------
146 component :
147 A scarlet component extracted from persisted data.
148 """
149 bbox = Box(self.shape, origin=self.origin)
150 model = self.model
151 if self.peak is None:
152 peak = None
153 else:
154 peak = (int(np.round(self.peak[0])), int(np.round(self.peak[0])))
155 assert peak is not None
156 component = ComponentCube(
157 bands=observation.bands,
158 bbox=bbox,
159 model=Image(model, yx0=bbox.origin, bands=observation.bands), # type: ignore
160 peak=peak,
161 )
162 return component
163
164 def as_dict(self) -> dict:
165 """Return the object encoded into a dict for JSON serialization
166
167 Returns
168 -------
169 result :
170 The object encoded as a JSON compatible dict
171 """
172 return {
173 "origin": self.origin,
174 "shape": self.model.shape,
175 "peak": self.peak,
176 "model": tuple(self.model.flatten().astype(float)),
177 "component_type": "component",
178 }
179
180 @classmethod
181 def from_dict(cls, data: dict, dtype: DTypeLike | None = None) -> ScarletComponentData:
182 """Reconstruct `ScarletComponentData` from JSON compatible dict
183
184 Parameters
185 ----------
186 data :
187 Dictionary representation of the object
188 dtype :
189 Datatype of the resulting model.
190
191 Returns
192 -------
193 result :
194 The reconstructed object
195 """
196 if data["component_type"] != "component":
197 raise ValueError(f"Invalid component type: {data['component_type']}")
198 shape = tuple(data["shape"])
199
200 return cls(
201 origin=tuple(data["origin"]), # type: ignore
202 peak=data["peak"],
203 model=np.array(data["model"]).reshape(shape).astype(dtype),
204 )
205
206 @staticmethod
207 def _from_component(component: Component) -> ScarletComponentData:
208 """Reconstruct `ScarletComponentData` from a scarlet Component.
209
210 Parameters
211 ----------
212 component :
213 The scarlet component to be converted.
214
215 Returns
216 -------
217 result :
218 The reconstructed object
219 """
221 origin=component.bbox.origin, # type: ignore
222 peak=component.peak, # type: ignore
223 model=component.get_model().data,
224 )
225
226
227@dataclass(kw_only=True)
229 """Data for a factorized component
230
231 Attributes
232 ----------
233 origin :
234 The lower bound of the component's bounding box.
235 peak :
236 The ``(y, x)`` peak of the component.
237 spectrum :
238 The SED of the component.
239 morph :
240 The 2D morphology of the component.
241 """
242
243 component_type: str = "factorized"
244 origin: tuple[int, int]
245 peak: tuple[float, float]
246 spectrum: np.ndarray
247 morph: np.ndarray
248
249 @property
250 def shape(self):
251 return self.morph.shape
252
253 def to_component(self, observation: Observation) -> FactorizedComponent:
254 """Convert the storage data model into a scarlet FactorizedComponent
255
256 Parameters
257 ----------
258 observation :
259 The observation that the component is associated with
260
261 Returns
262 -------
263 factorized_component :
264 A scarlet factorized component extracted from persisted data.
265 """
266 bbox = Box(self.shape, origin=self.origin)
267 spectrum = self.spectrum
268 morph = self.morph
269 if self.peak is None:
270 peak = None
271 else:
272 peak = (int(np.round(self.peak[0])), int(np.round(self.peak[1])))
273 assert peak is not None
274 # Note: since we aren't fitting a model, we don't need to
275 # set the RMS of the background.
276 # We set it to NaN just to be safe.
277 component = FactorizedComponent(
278 bands=observation.bands,
279 spectrum=FixedParameter(spectrum),
280 morph=FixedParameter(morph),
281 peak=peak,
282 bbox=bbox,
283 bg_rms=np.full((len(observation.bands),), np.nan),
284 )
285 return component
286
287 def as_dict(self) -> dict:
288 """Return the object encoded into a dict for JSON serialization
289
290 Returns
291 -------
292 result :
293 The object encoded as a JSON compatible dict
294 """
295 return {
296 "origin": tuple(int(o) for o in self.origin),
297 "shape": tuple(int(s) for s in self.morph.shape),
298 "peak": tuple(int(p) for p in self.peak),
299 "spectrum": tuple(self.spectrum.astype(float)),
300 "morph": tuple(self.morph.flatten().astype(float)),
301 "component_type": "factorized",
302 }
303
304 @classmethod
305 def from_dict(cls, data: dict, dtype: DTypeLike = np.float32) -> ScarletFactorizedComponentData:
306 """Reconstruct `ScarletFactorizedComponentData` from JSON compatible
307 dict.
308
309 Parameters
310 ----------
311 data :
312 Dictionary representation of the object
313 dtype :
314 Datatype of the resulting model.
315
316 Returns
317 -------
318 result :
319 The reconstructed object
320 """
321 shape = tuple(data["shape"])
322
323 return cls(
324 origin=tuple(data["origin"]), # type: ignore
325 peak=data["peak"],
326 spectrum=np.array(data["spectrum"]).astype(dtype),
327 morph=np.array(data["morph"]).reshape(shape).astype(dtype),
328 )
329
330 @staticmethod
331 def _from_component(component: FactorizedComponent) -> ScarletFactorizedComponentData:
332 """Reconstruct `ScarletFactorizedComponentData` from a scarlet
333 FactorizedComponent.
334
335 Parameters
336 ----------
337 component :
338 The scarlet component to be converted.
339
340 Returns
341 -------
342 result :
343 The reconstructed object
344 """
346 origin=component.bbox.origin, # type: ignore
347 peak=component.peak, # type: ignore
348 spectrum=component.spectrum,
349 morph=component.morph,
350 )
351
352
353# Register the component types
354ScarletComponentData.register()
355ScarletFactorizedComponentData.register()
356
357
358@dataclass(kw_only=True)
360 """Data for a scarlet source
361
362 Attributes
363 ----------
364 components :
365 The components contained in the source that are not factorized.
366 factorized_components :
367 The components contained in the source that are factorized.
368 peak_id :
369 The peak ID of the source in it's parent's footprint peak catalog.
370 """
371
372 components: list[ScarletComponentBaseData]
373 peak_id: int
374
375 def as_dict(self) -> dict:
376 """Return the object encoded into a dict for JSON serialization
377
378 Returns
379 -------
380 result :
381 The object encoded as a JSON compatible dict
382 """
383 result = {
384 "components": [component.as_dict() for component in self.components],
385 "peak_id": self.peak_id,
386 }
387 return result
388
389 @classmethod
390 def from_dict(cls, data: dict, dtype: DTypeLike = np.float32) -> ScarletSourceData:
391 """Reconstruct `ScarletSourceData` from JSON compatible
392 dict.
393
394 Parameters
395 ----------
396 data :
397 Dictionary representation of the object
398 dtype :
399 Datatype of the resulting model.
400
401 Returns
402 -------
403 result :
404 The reconstructed object
405 """
406 # Check for legacy models
407 if "factorized" in data:
408 components: list[ScarletComponentBaseData] = [
409 ScarletFactorizedComponentData.from_dict(component, dtype=dtype)
410 for component in data["factorized"]
411 ]
412 else:
413 components = [
414 ScarletComponentBaseData.from_dict(component, dtype=dtype) for component in data["components"]
415 ]
416 return cls(components=components, peak_id=int(data["peak_id"]))
417
418 @classmethod
419 def from_source(cls, source: Source) -> ScarletSourceData:
420 """Reconstruct `ScarletSourceData` from a scarlet Source.
421
422 Parameters
423 ----------
424 source :
425 The scarlet source to be converted.
426
427 Returns
428 -------
429 result :
430 The reconstructed object
431 """
432 components = [ScarletComponentBaseData.from_component(component) for component in source.components]
433 return cls(components=components, peak_id=source.peak_id) # type: ignore
434
435
436@dataclass(kw_only=True)
438 """Data for an entire blend.
439
440 Attributes
441 ----------
442 origin :
443 The lower bound of the blend's bounding box.
444 shape :
445 The shape of the blend's bounding box.
446 sources :
447 Data for the sources contained in the blend,
448 indexed by the source id.
449 psf_center :
450 The location used for the center of the PSF for
451 the blend.
452 psf :
453 The PSF of the observation.
454 bands : `list` of `str`
455 The names of the bands.
456 The order of the bands must be the same as the order of
457 the multiband model arrays, and SEDs.
458 """
459
460 origin: tuple[int, int]
461 shape: tuple[int, int]
462 sources: dict[int, ScarletSourceData]
463 psf_center: tuple[float, float]
464 psf: np.ndarray
465 bands: tuple[str]
466
467 def as_dict(self) -> dict:
468 """Return the object encoded into a dict for JSON serialization
469
470 Returns
471 -------
472 result :
473 The object encoded as a JSON compatible dict
474 """
475 result = {
476 "origin": self.origin,
477 "shape": self.shape,
478 "psf_center": self.psf_center,
479 "psf_shape": self.psf.shape,
480 "psf": tuple(self.psf.flatten().astype(float)),
481 "sources": {bid: source.as_dict() for bid, source in self.sources.items()},
482 "bands": self.bands,
483 }
484 return result
485
486 @classmethod
487 def from_dict(cls, data: dict, dtype: DTypeLike = np.float32) -> ScarletBlendData:
488 """Reconstruct `ScarletBlendData` from JSON compatible
489 dict.
490
491 Parameters
492 ----------
493 data :
494 Dictionary representation of the object
495 dtype :
496 Datatype of the resulting model.
497
498 Returns
499 -------
500 result :
501 The reconstructed object
502 """
503 psf_shape = data["psf_shape"]
504 return cls(
505 origin=tuple(data["origin"]), # type: ignore
506 shape=tuple(data["shape"]), # type: ignore
507 psf_center=tuple(data["psf_center"]), # type: ignore
508 psf=np.array(data["psf"]).reshape(psf_shape).astype(dtype),
509 sources={
510 int(bid): ScarletSourceData.from_dict(source, dtype=dtype)
511 for bid, source in data["sources"].items()
512 },
513 bands=tuple(data["bands"]), # type: ignore
514 )
515
516 def minimal_data_to_blend(self, model_psf: np.ndarray, dtype: DTypeLike) -> Blend:
517 """Convert the storage data model into a scarlet lite blend
518
519 Parameters
520 ----------
521 model_psf :
522 PSF in model space (usually a nyquist sampled circular Gaussian).
523 dtype :
524 The data type of the model that is generated.
525
526 Returns
527 -------
528 blend :
529 A scarlet blend model extracted from persisted data.
530 """
531 model_box = Box(self.shape, origin=self.origin)
532 observation = Observation.empty(
533 bands=self.bands,
534 psfs=self.psf,
535 model_psf=model_psf,
536 bbox=model_box,
537 dtype=dtype,
538 )
539 return self.to_blend(observation)
540
541 def to_blend(self, observation: Observation) -> Blend:
542 """Convert the storage data model into a scarlet lite blend
543
544 Parameters
545 ----------
546 observation :
547 The observation that contains the blend.
548 If `observation` is ``None`` then an `Observation` containing
549 no image data is initialized.
550
551 Returns
552 -------
553 blend :
554 A scarlet blend model extracted from persisted data.
555 """
556 sources = []
557 for source_id, source_data in self.sources.items():
558 components: list[Component] = [
559 component.to_component(observation) for component in source_data.components
560 ]
561
562 source = Source(components=components)
563 # Store identifiers for the source
564 source.record_id = source_id # type: ignore
565 source.peak_id = source_data.peak_id # type: ignore
566 sources.append(source)
567
568 return Blend(sources=sources, observation=observation)
569
570 @staticmethod
571 def from_blend(blend: Blend, psf_center: tuple[int, int]) -> ScarletBlendData:
572 """Convert a scarlet lite blend into a persistable data object
573
574 Parameters
575 ----------
576 blend :
577 The blend that is being persisted.
578 psf_center :
579 The center of the PSF.
580
581 Returns
582 -------
583 blend_data :
584 The data model for a single blend.
585 """
586 sources = {}
587 for source in blend.sources:
588 sources[source.record_id] = ScarletSourceData.from_source(source) # type: ignore
589
590 blend_data = ScarletBlendData(
591 origin=blend.bbox.origin, # type: ignore
592 shape=blend.bbox.shape, # type: ignore
593 sources=sources,
594 psf_center=psf_center,
595 psf=blend.observation.psfs,
596 bands=blend.observation.bands, # type: ignore
597 )
598
599 return blend_data
600
601
603 """A container that propagates scarlet models for an entire catalog."""
604
605 def __init__(self, psf: np.ndarray, blends: dict[int, ScarletBlendData] | None = None):
606 """Initialize an instance
607
608 Parameters
609 ----------
610 psf :
611 The 2D array of the PSF in scarlet model space.
612 This is typically a narrow Gaussian integrated over the
613 pixels in the exposure.
614 blends :
615 Map from parent IDs in the source catalog
616 to scarlet model data for each parent ID (blend).
617 """
618 self.psf = psf
619 if blends is None:
620 blends = {}
621 self.blends = blends
622
623 def as_dict(self) -> dict:
624 """Return the object encoded into a dict for JSON serialization
625
626 Returns
627 -------
628 result :
629 The object encoded as a JSON compatible dict
630 """
631 result = {
632 "psfShape": self.psf.shape,
633 "psf": list(self.psf.flatten().astype(float)),
634 "blends": {bid: blend.as_dict() for bid, blend in self.blends.items()},
635 }
636 return result
637
638 def json(self) -> str:
639 """Serialize the data model to a JSON formatted string
640
641 Returns
642 -------
643 result : `str`
644 The result of the object converted into a JSON format
645 """
646 result = self.as_dict()
647 return json.dumps(result)
648
649 @classmethod
650 def parse_obj(cls, data: dict) -> ScarletModelData:
651 """Construct a ScarletModelData from python decoded JSON object.
652
653 Parameters
654 ----------
655 data :
656 The result of json.load(s) on a JSON persisted ScarletModelData
657
658 Returns
659 -------
660 result :
661 The `ScarletModelData` that was loaded the from the input object
662 """
663 model_psf = np.array(data["psf"]).reshape(data["psfShape"]).astype(np.float32)
664 return cls(
665 psf=model_psf,
666 blends={int(bid): ScarletBlendData.from_dict(blend) for bid, blend in data["blends"].items()},
667 )
668
669
671 """Dummy component for a component cube.
672
673 This is duck-typed to a `lsst.scarlet.lite.Component` in order to
674 generate a model from the component.
675
676 If scarlet lite ever implements a component as a data cube,
677 this class can be removed.
678 """
679
680 def __init__(self, bands: tuple[Any, ...], bbox: Box, model: Image, peak: tuple[int, int]):
681 """Initialization
682
683 Parameters
684 ----------
685 bands :
686 model :
687 The 3D (bands, y, x) model of the component.
688 peak :
689 The `(y, x)` peak of the component.
690 bbox :
691 The bounding box of the component.
692 """
693 super().__init__(bands, bbox)
694 self._model = model
695 self.peak = peak
696
697 def get_model(self) -> Image:
698 """Generate the model for the source
699
700 Returns
701 -------
702 model :
703 The model as a 3D `(band, y, x)` array.
704 """
705 return self._model
706
707 def resize(self, model_box: Box) -> bool:
708 """Test whether or not the component needs to be resized"""
709 return False
710
711 def update(self, it: int, input_grad: np.ndarray) -> None:
712 """Implementation of unused abstract method"""
713
714 def parameterize(self, parameterization: Callable) -> None:
715 """Implementation of unused abstract method"""
A class to represent a 2-dimensional array of pixels.
Definition Image.h:51
__init__(self, tuple[Any,...] bands, Box bbox, Image model, tuple[int, int] peak)
Definition io.py:680
None parameterize(self, Callable parameterization)
Definition io.py:714
bool resize(self, Box model_box)
Definition io.py:707
None update(self, int it, np.ndarray input_grad)
Definition io.py:711
Blend to_blend(self, Observation observation)
Definition io.py:541
ScarletBlendData from_dict(cls, dict data, DTypeLike dtype=np.float32)
Definition io.py:487
Blend minimal_data_to_blend(self, np.ndarray model_psf, DTypeLike dtype)
Definition io.py:516
ScarletBlendData from_blend(Blend blend, tuple[int, int] psf_center)
Definition io.py:571
ScarletComponentBaseData from_component(Component component)
Definition io.py:91
Component to_component(self, Observation observation)
Definition io.py:45
ScarletComponentBaseData from_dict(dict data, DTypeLike|None dtype=None)
Definition io.py:70
ScarletComponentData from_dict(cls, dict data, DTypeLike|None dtype=None)
Definition io.py:181
ComponentCube to_component(self, Observation observation)
Definition io.py:136
ScarletComponentData _from_component(Component component)
Definition io.py:207
FactorizedComponent to_component(self, Observation observation)
Definition io.py:253
ScarletFactorizedComponentData _from_component(FactorizedComponent component)
Definition io.py:331
ScarletFactorizedComponentData from_dict(cls, dict data, DTypeLike dtype=np.float32)
Definition io.py:305
__init__(self, np.ndarray psf, dict[int, ScarletBlendData]|None blends=None)
Definition io.py:605
ScarletModelData parse_obj(cls, dict data)
Definition io.py:650
ScarletSourceData from_source(cls, Source source)
Definition io.py:419
ScarletSourceData from_dict(cls, dict data, DTypeLike dtype=np.float32)
Definition io.py:390