Coverage for tests/test_io_model_data.py: 96%

72 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-09 09:32 +0000

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/>. 

21 

22"""Tests for the LsstScarletModelData schema migrations.""" 

23 

24import copy 

25import unittest 

26 

27import numpy as np 

28 

29import lsst.scarlet.lite as scl 

30import lsst.utils.tests 

31from lsst.meas.extensions.scarlet.io import model_data as model_data_module 

32from lsst.meas.extensions.scarlet.io.model_data import ( 

33 CURRENT_SCHEMA, 

34 MODEL_TYPE, 

35 _to_1_0_0, 

36 _to_1_0_1, 

37 _to_1_0_2, 

38) 

39 

40 

41class TestModelDataMigrations(lsst.utils.tests.TestCase): 

42 """Tests for the migration chain and schema constants in 

43 ``lsst.meas.extensions.scarlet.io.model_data``. 

44 

45 Each migration function bumps the version and adds the keys 

46 introduced at that schema step. A regression in any of them 

47 silently corrupts deblend catalogs read from disk that were 

48 written by an earlier release. 

49 """ 

50 

51 def test_to_1_0_0_adds_isolated_key(self): 

52 """``_to_1_0_0`` adds ``isolated={}``, the ``model_type`` tag, 

53 and the schema version to pre-schema data. 

54 """ 

55 # Pre-schema data carries only the ``blends`` key inherited 

56 # from scarlet_lite's ScarletModelData; no model_type, 

57 # isolated, or version was emitted before 1.0.0. 

58 pre = {"blends": {}} 

59 result = _to_1_0_0(copy.deepcopy(pre)) 

60 self.assertEqual(result["isolated"], {}) 

61 self.assertEqual(result["version"], "1.0.0") 

62 self.assertEqual(result["model_type"], MODEL_TYPE) 

63 

64 def test_to_1_0_1_adds_footprint_metadata(self): 

65 """``_to_1_0_1`` adds ``metadata={"footprint": None}`` and the 

66 schema version to 1.0.0 data that had no metadata at all. 

67 """ 

68 # 1.0.0 data has isolated, model_type, version — but no 

69 # metadata key; 1.0.1 introduced footprint metadata. 

70 v1_0_0 = { 

71 "blends": {}, 

72 "isolated": {}, 

73 "model_type": MODEL_TYPE, 

74 "version": "1.0.0", 

75 } 

76 result = _to_1_0_1(copy.deepcopy(v1_0_0)) 

77 self.assertEqual(result["version"], "1.0.1") 

78 self.assertEqual(result["metadata"], {"footprint": None}) 

79 

80 def test_to_1_0_1_preserves_existing_metadata(self): 

81 """When 1.0.0 data already carries a ``metadata`` dict (without 

82 a ``footprint`` key), ``_to_1_0_1`` adds ``footprint=None`` 

83 and leaves the other keys intact. 

84 """ 

85 # Pins the ``setdefault(...).setdefault(...)`` contract — a 

86 # naive ``data["metadata"] = {"footprint": None}`` rewrite 

87 # would silently drop pre-existing metadata. 

88 v1_0_0 = { 

89 "blends": {}, 

90 "isolated": {}, 

91 "model_type": MODEL_TYPE, 

92 "version": "1.0.0", 

93 "metadata": {"survey": "DES"}, 

94 } 

95 result = _to_1_0_1(copy.deepcopy(v1_0_0)) 

96 self.assertEqual(result["version"], "1.0.1") 

97 self.assertEqual( 

98 result["metadata"], {"survey": "DES", "footprint": None} 

99 ) 

100 

101 def test_to_1_0_1_handles_none_metadata(self): 

102 """``_to_1_0_1`` tolerates an explicit ``metadata=None`` entry 

103 from older payloads. 

104 

105 Regression test for finding IO-3 of 

106 ``audits/audit-2026-05-05.md``. The naive 

107 ``data.setdefault("metadata", {}).setdefault("footprint", None)`` 

108 idiom returned the existing ``None`` and then raised 

109 ``AttributeError: 'NoneType' object has no attribute 'setdefault'`` 

110 when the payload carried ``metadata=None`` explicitly — the 

111 same value emitted by 

112 ``scarlet_model_to_lsst_scarlet_model`` for converted v0 

113 archives. The migration must replace ``None`` with 

114 ``{"footprint": None}``. 

115 """ 

116 v1_0_0 = { 

117 "blends": {}, 

118 "isolated": {}, 

119 "model_type": MODEL_TYPE, 

120 "version": "1.0.0", 

121 "metadata": None, 

122 } 

123 result = _to_1_0_1(copy.deepcopy(v1_0_0)) 

124 self.assertEqual(result["version"], "1.0.1") 

125 self.assertEqual(result["metadata"], {"footprint": None}) 

126 

127 def test_to_1_0_2_promotes_real_hierarchical_spans(self): 

128 """``_to_1_0_2`` converts a legacy ``hierarchical`` blend that 

129 carries real ``spans``/``origin`` into an ``lsst_hierarchical`` 

130 blend, promoting the spans verbatim with ``legacy_spans=False``. 

131 """ 

132 spans = np.zeros((5, 6), dtype=bool) 

133 spans[1:4, 2:5] = True 

134 child = scl.io.ScarletBlendData(origin=(10, 20), shape=(5, 6), sources={}) 

135 legacy = scl.io.HierarchicalBlendData( 

136 children={1: child}, 

137 metadata={"spans": spans.astype(int), "origin": (10, 20)}, 

138 ) 

139 data = { 

140 "blends": {7: legacy.as_dict()}, 

141 "isolated": {}, 

142 "model_type": MODEL_TYPE, 

143 "version": "1.0.1", 

144 "metadata": {}, 

145 } 

146 result = _to_1_0_2(copy.deepcopy(data)) 

147 self.assertEqual(result["version"], "1.0.2") 

148 blend = result["blends"][7] 

149 self.assertEqual(blend["blend_type"], "lsst_hierarchical") 

150 converted = scl.io.ScarletBlendBaseData.from_dict(blend) 

151 self.assertIsInstance(converted, model_data_module.LsstHierarchicalBlendData) 

152 np.testing.assert_array_equal(converted.span_array, spans) 

153 self.assertEqual(tuple(converted.origin), (10, 20)) 

154 self.assertFalse(converted.legacy_spans) 

155 

156 def test_to_1_0_2_synthesizes_missing_spans(self): 

157 """When a legacy ``hierarchical`` blend has no spans, ``_to_1_0_2`` 

158 synthesizes a filled rectangle from the children bbox and marks it 

159 ``legacy_spans=True``. 

160 """ 

161 child = scl.io.ScarletBlendData(origin=(10, 20), shape=(5, 6), sources={}) 

162 legacy = scl.io.HierarchicalBlendData(children={1: child}) 

163 data = { 

164 "blends": {7: legacy.as_dict()}, 

165 "isolated": {}, 

166 "model_type": MODEL_TYPE, 

167 "version": "1.0.1", 

168 "metadata": {}, 

169 } 

170 result = _to_1_0_2(copy.deepcopy(data)) 

171 converted = scl.io.ScarletBlendBaseData.from_dict(result["blends"][7]) 

172 self.assertTrue(converted.legacy_spans) 

173 self.assertEqual(converted.span_array.shape, (5, 6)) 

174 self.assertTrue(converted.span_array.all()) 

175 self.assertEqual(tuple(converted.origin), (10, 20)) 

176 

177 def test_to_1_0_2_ignores_flat_blends(self): 

178 """``_to_1_0_2`` leaves non-hierarchical (flat) top-level blends 

179 untouched — they have no meas-specific spans to promote. 

180 """ 

181 flat = scl.io.ScarletBlendData(origin=(0, 0), shape=(3, 3), sources={}) 

182 data = { 

183 "blends": {7: flat.as_dict()}, 

184 "isolated": {}, 

185 "model_type": MODEL_TYPE, 

186 "version": "1.0.1", 

187 "metadata": {}, 

188 } 

189 result = _to_1_0_2(copy.deepcopy(data)) 

190 self.assertEqual(result["blends"][7]["blend_type"], "blend") 

191 

192 def test_schema_version_constants_match(self): 

193 """``CURRENT_SCHEMA`` matches what's recorded as current for 

194 ``MODEL_TYPE`` in the migration registry. 

195 """ 

196 self.assertEqual( 

197 CURRENT_SCHEMA, 

198 scl.io.migration.MigrationRegistry.current[MODEL_TYPE], 

199 ) 

200 

201 def test_metadata_retains_deprecated_keys(self): 

202 """During the deprecation period the promoted typed attributes 

203 ``bands``/``model_psf``/``psf`` remain readable through 

204 ``metadata`` for backwards compatibility. Throwaway once the keys 

205 are removed after v31. (Membership tests do not warn.) 

206 """ 

207 model = model_data_module.LsstScarletModelData( 

208 bands=("g", "r"), 

209 model_psf=np.ones((5, 5), dtype=np.float32), 

210 psf=np.ones((2, 3, 3), dtype=np.float32), 

211 ) 

212 for key in ("bands", "model_psf", "psf"): 

213 self.assertIn(key, model.metadata) 

214 

215 

216def setup_module(module): 

217 lsst.utils.tests.init() 

218 

219 

220class MemoryTester(lsst.utils.tests.MemoryTestCase): 

221 pass 

222 

223 

224if __name__ == "__main__": 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true

225 lsst.utils.tests.init() 

226 unittest.main()