Coverage for python / lsst / images / serialization / _asdf_utils.py: 71%

116 statements  

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

1# This file is part of lsst-images. 

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# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14import operator 

15 

16__all__ = ( 

17 "ArrayReferenceModel", 

18 "ArrayReferenceQuantityModel", 

19 "InlineArray", 

20 "InlineArrayModel", 

21 "InlineArrayQuantity", 

22 "InlineArrayQuantityModel", 

23 "Quantity", 

24 "QuantityModel", 

25 "Time", 

26 "TimeModel", 

27 "Unit", 

28) 

29 

30from typing import Annotated, Any, Literal 

31 

32import astropy.time 

33import astropy.units 

34import numpy as np 

35import pydantic 

36import pydantic_core.core_schema as pcs 

37 

38from ._dtypes import NumberType 

39 

40 

41class _UnitSerialization: 

42 """Pydantic hooks for unit serialization. 

43 

44 This class provides implementations for the `Unit` type alias for 

45 `astropy.unit.Unit` that adds Pydantic serialization and validation. 

46 """ 

47 

48 @classmethod 

49 def __get_pydantic_core_schema__( 

50 cls, source_type: Any, handler: pydantic.GetCoreSchemaHandler 

51 ) -> pcs.CoreSchema: 

52 from_str_schema = pcs.chain_schema( 

53 [ 

54 pcs.str_schema(), 

55 pcs.no_info_plain_validator_function(cls.from_str), 

56 ] 

57 ) 

58 return pcs.json_or_python_schema( 

59 json_schema=from_str_schema, 

60 python_schema=pcs.union_schema([pcs.is_instance_schema(astropy.units.UnitBase), from_str_schema]), 

61 serialization=pcs.plain_serializer_function_ser_schema(cls.to_str), 

62 ) 

63 

64 @classmethod 

65 def from_str(cls, value: str) -> astropy.units.UnitBase: 

66 return astropy.units.Unit(value, format="vounit") 

67 

68 @staticmethod 

69 def to_str(unit: astropy.units.UnitBase) -> str: 

70 return unit.to_string("vounit") 

71 

72 

73type Unit = Annotated[ 

74 astropy.units.UnitBase, 

75 _UnitSerialization, 

76 pydantic.WithJsonSchema( 

77 { 

78 "type": "string", 

79 "$schema": "http://stsci.edu/schemas/yaml-schema/draft-01", 

80 "id": "http://stsci.edu/schemas/asdf/unit/unit-1.0.0", 

81 "tag": "!unit/unit-1.0.0", 

82 } 

83 ), 

84] 

85 

86 

87class ArrayReferenceModel(pydantic.BaseModel, ser_json_inf_nan="constants"): 

88 """Model for a subset of the ASDF 'ndarray' schema, in the case where the 

89 array data is stored elsewhere. 

90 """ 

91 

92 source: str | int = pydantic.Field(description="Location of the underlying binary data.") 

93 shape: list[int] = pydantic.Field( 

94 # In (e.g.) FITS this is stored outside of the JSON as well, and it 

95 # be hard to get it right if we need to make a reference to a column 

96 # before all rows have been written, so unlike ASDF we allow this to 

97 # be omitted. 

98 default_factory=list, 

99 description="Size of the array in each dimension.", 

100 exclude_if=operator.not_, 

101 ) 

102 datatype: NumberType = pydantic.Field(description="Data type of the array.") 

103 byteorder: Literal["big"] = pydantic.Field(default="big", description="Byte order for the binary data.") 

104 

105 def with_units(self, unit: astropy.units.UnitBase) -> ArrayReferenceQuantityModel: 

106 """Add units, transforming this model into a Quantity model.""" 

107 return ArrayReferenceQuantityModel.model_construct(value=self, unit=unit) 

108 

109 model_config = pydantic.ConfigDict( 

110 json_schema_extra={ 

111 "$schema": "http://stsci.edu/schemas/yaml-schema/draft-01", 

112 "id": "http://stsci.edu/schemas/asdf/core/ndarray-1.1.0", 

113 "tag": "!core/ndarray-1.1.0", 

114 } 

115 ) 

116 

117 

118class InlineArrayModel(pydantic.BaseModel, ser_json_inf_nan="constants"): 

119 """Model for a subset of the ASDF 'ndarray' schema, in the case where the 

120 array data is stored inline. 

121 """ 

122 

123 data: list[Any] 

124 datatype: NumberType 

125 

126 @property 

127 def shape(self) -> tuple[int, ...]: 

128 """The shape of the array (`tuple` [`int`, ...]).""" 

129 return self._extract_shape(self.data) 

130 

131 def with_units(self, unit: astropy.unit.UnitBase) -> InlineArrayQuantityModel: 

132 """Add units, transforming this model in to a Quantity model.""" 

133 return InlineArrayQuantityModel.model_construct(value=self, unit=unit) 

134 

135 @classmethod 

136 def _extract_shape(cls, data: list[Any], current: tuple[int, ...] = ()) -> tuple[int, ...]: 

137 if not data: 

138 return current + (0,) 

139 if not isinstance(data[0], list): 

140 return current + (len(data),) 

141 return cls._extract_shape(data[0], current + (len(data),)) 

142 

143 model_config = pydantic.ConfigDict( 

144 json_schema_extra={ 

145 "$schema": "http://stsci.edu/schemas/yaml-schema/draft-01", 

146 "id": "http://stsci.edu/schemas/asdf/core/ndarray-1.1.0", 

147 "tag": "!core/ndarray-1.1.0", 

148 } 

149 ) 

150 

151 

152class _InlineArraySerialization: 

153 """Pydantic hooks for array serialization. 

154 

155 This class provides implementations for the `Array` type alias for 

156 `numpy.ndarray` that adds Pydantic serialization and validation. 

157 """ 

158 

159 @classmethod 

160 def __get_pydantic_core_schema__( 

161 cls, source_type: Any, handler: pydantic.GetCoreSchemaHandler 

162 ) -> pcs.CoreSchema: 

163 from_model_schema = pcs.chain_schema( 

164 [ 

165 handler(InlineArrayModel), 

166 pcs.no_info_plain_validator_function(cls.from_model), 

167 ] 

168 ) 

169 return pcs.json_or_python_schema( 

170 json_schema=from_model_schema, 

171 python_schema=pcs.union_schema([pcs.is_instance_schema(np.ndarray), from_model_schema]), 

172 serialization=pcs.plain_serializer_function_ser_schema(cls.to_model), 

173 ) 

174 

175 @classmethod 

176 def from_model(cls, model: InlineArrayModel) -> np.ndarray: 

177 return np.array(model.data, dtype=model.datatype.to_numpy()) 

178 

179 @classmethod 

180 def to_model(cls, array: np.ndarray) -> InlineArrayModel: 

181 datatype = NumberType.from_numpy(array.dtype) 

182 return InlineArrayModel(data=array.tolist(), datatype=datatype) 

183 

184 

185type InlineArray = Annotated[np.ndarray, _InlineArraySerialization] 

186 

187 

188class QuantityModel(pydantic.BaseModel, ser_json_inf_nan="constants"): 

189 """Model for a subset of the ASDF 'quantity' schema for scalars.""" 

190 

191 value: pydantic.StrictFloat 

192 unit: Unit 

193 

194 model_config = pydantic.ConfigDict( 

195 json_schema_extra={ 

196 "$schema": "http://stsci.edu/schemas/yaml-schema/draft-01", 

197 "id": "http://stsci.edu/schemas/asdf/unit/quantity-1.2.0", 

198 "tag": "!unit/quantity-1.2.0", 

199 } 

200 ) 

201 

202 

203class InlineArrayQuantityModel(pydantic.BaseModel, ser_json_inf_nan="constants"): 

204 """Model for a subset of the ASDF 'quantity' schema for inline arrays.""" 

205 

206 value: InlineArrayModel 

207 unit: Unit 

208 

209 model_config = pydantic.ConfigDict( 

210 json_schema_extra={ 

211 "$schema": "http://stsci.edu/schemas/yaml-schema/draft-01", 

212 "id": "http://stsci.edu/schemas/asdf/unit/quantity-1.2.0", 

213 "tag": "!unit/quantity-1.2.0", 

214 } 

215 ) 

216 

217 

218class ArrayReferenceQuantityModel(pydantic.BaseModel, ser_json_inf_nan="constants"): 

219 """Model for a subset of the ASDF 'quantity' schema for external arrays.""" 

220 

221 value: ArrayReferenceModel 

222 unit: Unit 

223 

224 model_config = pydantic.ConfigDict( 

225 json_schema_extra={ 

226 "$schema": "http://stsci.edu/schemas/yaml-schema/draft-01", 

227 "id": "http://stsci.edu/schemas/asdf/unit/quantity-1.2.0", 

228 "tag": "!unit/quantity-1.2.0", 

229 } 

230 ) 

231 

232 

233class _QuantitySerialization: 

234 """Pydantic hooks for scalar quantity serialization.""" 

235 

236 @classmethod 

237 def __get_pydantic_core_schema__( 

238 cls, source_type: Any, handler: pydantic.GetCoreSchemaHandler 

239 ) -> pcs.CoreSchema: 

240 from_model_schema = pcs.chain_schema( 

241 [ 

242 handler(QuantityModel), 

243 pcs.no_info_plain_validator_function(cls.from_model), 

244 ] 

245 ) 

246 return pcs.json_or_python_schema( 

247 json_schema=from_model_schema, 

248 python_schema=pcs.union_schema( 

249 [pcs.is_instance_schema(astropy.units.Quantity), from_model_schema] 

250 ), 

251 serialization=pcs.plain_serializer_function_ser_schema(cls.to_model), 

252 ) 

253 

254 @classmethod 

255 def from_model(cls, model: QuantityModel) -> astropy.units.Quantity: 

256 return astropy.units.Quantity(model.value, unit=model.unit) 

257 

258 @classmethod 

259 def to_model(cls, quantity: astropy.units.Quantity) -> QuantityModel: 

260 assert quantity.isscalar 

261 return QuantityModel(value=quantity.to_value(), unit=quantity.unit) 

262 

263 

264type Quantity = Annotated[astropy.units.Quantity, _QuantitySerialization] 

265 

266 

267class _InlineArrayQuantitySerialization: 

268 """Pydantic hooks for inline array quantity serialization.""" 

269 

270 @classmethod 

271 def __get_pydantic_core_schema__( 

272 cls, source_type: Any, handler: pydantic.GetCoreSchemaHandler 

273 ) -> pcs.CoreSchema: 

274 from_model_schema = pcs.chain_schema( 

275 [ 

276 handler(InlineArrayQuantityModel), 

277 pcs.no_info_plain_validator_function(cls.from_model), 

278 ] 

279 ) 

280 return pcs.json_or_python_schema( 

281 json_schema=from_model_schema, 

282 python_schema=pcs.union_schema( 

283 [pcs.is_instance_schema(astropy.units.Quantity), from_model_schema] 

284 ), 

285 serialization=pcs.plain_serializer_function_ser_schema(cls.to_model), 

286 ) 

287 

288 @classmethod 

289 def from_model(cls, model: InlineArrayQuantityModel) -> astropy.units.Quantity: 

290 return astropy.units.Quantity(_InlineArraySerialization.from_model(model.value), unit=model.unit) 

291 

292 @classmethod 

293 def to_model(cls, quantity: astropy.units.Quantity) -> InlineArrayQuantityModel: 

294 assert quantity.isscalar 

295 return InlineArrayQuantityModel( 

296 value=_InlineArraySerialization.to_model(quantity.to_value()), 

297 unit=quantity.unit, 

298 ) 

299 

300 

301type InlineArrayQuantity = Annotated[astropy.units.Quantity, _InlineArrayQuantitySerialization] 

302 

303 

304class TimeModel(pydantic.BaseModel, ser_json_inf_nan="constants"): 

305 """Model for a subset of the ASDF 'time' schema.""" 

306 

307 value: str 

308 scale: Literal["utc", "tai"] 

309 format: Literal["iso"] = "iso" 

310 

311 model_config = pydantic.ConfigDict( 

312 json_schema_extra={ 

313 "$schema": "http://stsci.edu/schemas/yaml-schema/draft-01", 

314 "id": "http://stsci.edu/schemas/asdf/time/time-1.2.0", 

315 "tag": "!time/time-1.2.0", 

316 } 

317 ) 

318 

319 

320class _TimeSerialization: 

321 """Pydantic hooks for time serialization. 

322 

323 This class provides implementations for the `Time` type alias for 

324 `astropy.time.Time` that adds Pydantic serialization and validation. 

325 """ 

326 

327 @classmethod 

328 def __get_pydantic_core_schema__( 

329 cls, source_type: Any, handler: pydantic.GetCoreSchemaHandler 

330 ) -> pcs.CoreSchema: 

331 from_model_schema = pcs.chain_schema( 

332 [ 

333 TimeModel.__pydantic_core_schema__, 

334 pcs.no_info_plain_validator_function(cls.from_model), 

335 ] 

336 ) 

337 return pcs.json_or_python_schema( 

338 json_schema=from_model_schema, 

339 python_schema=pcs.union_schema([pcs.is_instance_schema(astropy.time.Time), from_model_schema]), 

340 serialization=pcs.plain_serializer_function_ser_schema(cls.to_model, info_arg=False), 

341 ) 

342 

343 @classmethod 

344 def from_model(cls, model: TimeModel) -> astropy.time.Time: 

345 return astropy.time.Time(model.value, scale=model.scale, format=model.format) 

346 

347 @classmethod 

348 def to_model(cls, time: astropy.time.Time) -> TimeModel: 

349 if time.scale != "utc" and time.scale != "tai": 

350 time = time.tai 

351 return TimeModel(value=time.to_value("iso"), scale=time.scale, format="iso") 

352 

353 

354type Time = Annotated[astropy.time.Time, _TimeSerialization]