Coverage for python/lsst/meas/extensions/scarlet/io/model_data.py: 95%
51 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 02:04 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 02:04 -0700
1# This file is part of meas_extensions_scarlet.
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/>.
22from __future__ import annotations
24from typing import Any
26import numpy as np
27from numpy.typing import DTypeLike
29import lsst.scarlet.lite as scl
31from .source_data import IsolatedSourceData
33__all__ = ["LsstScarletModelData"]
35CURRENT_SCHEMA = "1.0.1"
36SCARLET_LITE_SCHEMA = "1.0.0"
37MODEL_TYPE = "lsst"
38scl.io.migration.MigrationRegistry.set_current(MODEL_TYPE, CURRENT_SCHEMA)
41def _checkScarletLiteSchema(scarletSchema: str, pinnedSchema: str) -> None:
42 """Raise if the installed scarlet_lite schema is not the schema
43 this package was last verified against.
45 Bidirectional drift guard. Any mismatch means the IO layer
46 cannot be trusted to round-trip data: an older installed
47 scarlet may not emit the keys this package expects, and a
48 newer one may have changed them. Either way the user gets an
49 actionable error at import time instead of a confusing failure
50 deep inside a ``from_dict`` call.
52 Parameters
53 ----------
54 scarletSchema : str
55 Schema string from the installed
56 ``scl.io.model_data.CURRENT_SCHEMA``.
57 pinnedSchema : str
58 Schema string this package was last written against
59 (``SCARLET_LITE_SCHEMA``).
61 Raises
62 ------
63 RuntimeError
64 If ``scarletSchema`` differs from ``pinnedSchema``.
65 """
66 if scarletSchema != pinnedSchema:
67 raise RuntimeError(
68 "Version mismatch between meas_extensions_scarlet and scarlet lite. "
69 "This requires updating SCARLET_LITE_SCHEMA, CURRENT_SCHEMA, and a migration step "
70 f"to match the ScarletModelData schema version {scarletSchema}."
71 )
74# Ensure that the ScarletModelData from scarlet lite hasn't changed.
75_checkScarletLiteSchema(scl.io.model_data.CURRENT_SCHEMA, SCARLET_LITE_SCHEMA)
78class LsstScarletModelData(scl.io.ScarletModelData):
79 """A ScarletModelData that includes isolated sources.
81 Attributes
82 ----------
83 isolated : dict[int, IsolatedSourceData]
84 A mapping of isolated source IDs to their data.
85 version : dict[int, scl.io.ScarletBlendBaseData]
86 The schema version of the serialized data.
87 """
88 model_type: str = MODEL_TYPE
89 isolated: dict[int, IsolatedSourceData]
90 version: str = CURRENT_SCHEMA
92 def __init__(
93 self,
94 isolated: dict[int, IsolatedSourceData] | None = None,
95 blends: dict[int, scl.io.ScarletBlendBaseData] | None = None,
96 metadata: dict[str, Any] | None = None,
97 ):
98 super().__init__(blends=blends, metadata=metadata)
99 self.isolated = isolated if isolated is not None else {}
101 def as_dict(self) -> dict[str, Any]:
102 """Convert to a dictionary for serialization
104 Returns
105 -------
106 result : dict[str, Any]
107 The object encoded as a JSON-compatible dictionary.
108 """
109 data = super().as_dict()
110 data.update(
111 {
112 "model_type": MODEL_TYPE,
113 "isolated": {k: v.as_dict() for k, v in self.isolated.items()},
114 "version": self.version,
115 }
116 )
117 return data
119 @classmethod
120 def from_dict(cls, data: dict, dtype: DTypeLike = np.float32) -> LsstScarletModelData:
121 """Reconstruct `LsstScarletModelData` from JSON compatible dict.
123 Parameters
124 ----------
125 data : dict
126 Dictionary representation of the object
127 dtype : DTypeLike
128 Datatype of the resulting model.
130 Returns
131 -------
132 result : LsstScarletModelData
133 The reconstructed object
134 """
135 data = scl.io.migration.MigrationRegistry.migrate(MODEL_TYPE, data)
136 isolated: dict[int, IsolatedSourceData] = {}
137 for sid, source_data in data.get("isolated", {}).items():
138 isolated[int(sid)] = IsolatedSourceData.from_dict(source_data, dtype=dtype)
139 if "metadata" not in data: 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 data["metadata"] = None
141 return super().from_dict(data, dtype=dtype, isolated=isolated)
144@scl.io.migration.migration(MODEL_TYPE, scl.io.migration.PRE_SCHEMA)
145def _to_1_0_0(data: dict) -> dict:
146 """Migrate a pre-schema model to schema version 1.0.0
148 There were no changes to this data model in v1.0.0 but we need
149 to provide a way to migrate pre-schema data.
151 Parameters
152 ----------
153 data : dict
154 The data to migrate.
155 Returns
156 -------
157 result : dict
158 The migrated data.
159 """
160 # Ensure that the model type and version are set and add an
161 # empty isolated sources dictionary.
162 if "model_type" not in data: 162 ↛ 164line 162 didn't jump to line 164 because the condition on line 162 was always true
163 data["model_type"] = MODEL_TYPE
164 data["isolated"] = {}
165 # Pre-``metadata`` archives stored the model PSF as top-level
166 # ``psf`` / ``psfShape`` entries. Promote them into the modern
167 # ``metadata`` shape so ``decode_metadata`` can reconstruct the
168 # array via ``array_keys``. Mirrors scarlet_lite's pre-schema
169 # ``scarlet_model`` migration.
170 if "metadata" not in data and "psfShape" in data:
171 data["metadata"] = {
172 "model_psf": data.pop("psf"),
173 "model_psf_shape": data.pop("psfShape"),
174 "array_keys": ["model_psf"],
175 }
176 data["version"] = "1.0.0"
177 return data
180@scl.io.migration.migration(MODEL_TYPE, "1.0.0")
181def _to_1_0_1(data: dict) -> dict:
182 """Migrate a schema version 1.0.0 model to schema version 1.0.1
184 There were no changes to this data model in v1.0.1 but we need
185 to provide a way to migrate 1.0.0 data.
187 Parameters
188 ----------
189 data : dict
190 The data to migrate.
191 Returns
192 -------
193 result : dict
194 The migrated data.
195 """
196 data["version"] = "1.0.1"
197 if data.get("metadata") is None:
198 data["metadata"] = {}
199 data["metadata"].setdefault("footprint", None)
200 return data