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

136 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-02 08:56 +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 

14__all__ = ( 

15 "ArrayReferenceModel", 

16 "ArrayReferenceQuantityModel", 

17 "InlineArray", 

18 "InlineArrayModel", 

19 "InlineArrayQuantity", 

20 "InlineArrayQuantityModel", 

21 "Quantity", 

22 "QuantityModel", 

23 "Time", 

24 "TimeModel", 

25 "Unit", 

26) 

27 

28from typing import Annotated, Any, Literal 

29 

30import astropy.time 

31import astropy.units 

32import numpy as np 

33import pydantic 

34import pydantic_core.core_schema as pcs 

35from pydantic.json_schema import GetJsonSchemaHandler, JsonSchemaValue 

36 

37from ._dtypes import NumberType 

38 

39 

40class _UnitSerialization: 

41 """Pydantic hooks for unit serialization. 

42 

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

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

45 """ 

46 

47 @classmethod 

48 def __get_pydantic_core_schema__( 

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

50 ) -> pcs.CoreSchema: 

51 from_str_schema = pcs.chain_schema( 

52 [ 

53 pcs.str_schema(), 

54 pcs.no_info_plain_validator_function(cls.from_str), 

55 ] 

56 ) 

57 return pcs.json_or_python_schema( 

58 json_schema=from_str_schema, 

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

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

61 ) 

62 

63 @classmethod 

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

65 try: 

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

67 except ValueError: 

68 pass 

69 # Some important units (e.g. "dn") are not supported by vounit, so 

70 # fall back to letting astropy to infer the format. 

71 return astropy.units.Unit(value) 

72 

73 @staticmethod 

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

75 try: 

76 return unit.to_string("vounit") 

77 except ValueError: 

78 pass 

79 # Some important units (e.g. "dn") are not supported by vounit, so 

80 # fall back to letting astropy use the default format. 

81 return unit.to_string() 

82 

83 

84type Unit = Annotated[ 

85 astropy.units.UnitBase, 

86 _UnitSerialization, 

87 pydantic.WithJsonSchema( 

88 { 

89 "type": "string", 

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

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

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

93 } 

94 ), 

95] 

96 

97 

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

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

100 array data is stored elsewhere. 

101 """ 

102 

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

104 shape: list[int] = pydantic.Field(description="Size of the array in each dimension.") 

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

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

107 

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

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

110 

111 Parameters 

112 ---------- 

113 unit 

114 Units to attach to the array values. 

115 """ 

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

117 

118 model_config = pydantic.ConfigDict( 

119 json_schema_extra={ 

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

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

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

123 } 

124 ) 

125 

126 

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

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

129 array data is stored inline. 

130 """ 

131 

132 data: list[Any] 

133 datatype: NumberType 

134 

135 @property 

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

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

138 return self._extract_shape(self.data) 

139 

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

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

142 

143 Parameters 

144 ---------- 

145 unit 

146 Units to attach to the array values. 

147 """ 

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

149 

150 @classmethod 

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

152 if not data: 152 ↛ 153line 152 didn't jump to line 153 because the condition on line 152 was never true

153 return current + (0,) 

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

155 return current + (len(data),) 

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

157 

158 model_config = pydantic.ConfigDict( 

159 json_schema_extra={ 

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

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

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

163 } 

164 ) 

165 

166 

167class _InlineArraySerialization: 

168 """Pydantic hooks for array serialization. 

169 

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

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

172 """ 

173 

174 @classmethod 

175 def __get_pydantic_core_schema__( 

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

177 ) -> pcs.CoreSchema: 

178 from_model_schema = pcs.chain_schema( 

179 [ 

180 handler(InlineArrayModel), 

181 pcs.no_info_plain_validator_function(cls.from_model), 

182 ] 

183 ) 

184 return pcs.json_or_python_schema( 

185 json_schema=from_model_schema, 

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

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

188 ) 

189 

190 @classmethod 

191 def __get_pydantic_json_schema__( 

192 cls, schema: pcs.CoreSchema, handler: GetJsonSchemaHandler 

193 ) -> JsonSchemaValue: 

194 return handler(InlineArrayModel.__pydantic_core_schema__) 

195 

196 @classmethod 

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

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

199 

200 @classmethod 

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

202 datatype = NumberType.from_numpy(array.dtype) 

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

204 

205 

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

207 

208 

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

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

211 

212 value: pydantic.StrictFloat 

213 unit: Unit 

214 

215 model_config = pydantic.ConfigDict( 

216 json_schema_extra={ 

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

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

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

220 } 

221 ) 

222 

223 

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

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

226 

227 value: InlineArrayModel 

228 unit: Unit 

229 

230 model_config = pydantic.ConfigDict( 

231 json_schema_extra={ 

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

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

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

235 } 

236 ) 

237 

238 

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

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

241 

242 value: ArrayReferenceModel 

243 unit: Unit 

244 

245 model_config = pydantic.ConfigDict( 

246 json_schema_extra={ 

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

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

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

250 } 

251 ) 

252 

253 

254class _QuantitySerialization: 

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

256 

257 @classmethod 

258 def __get_pydantic_core_schema__( 

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

260 ) -> pcs.CoreSchema: 

261 from_model_schema = pcs.chain_schema( 

262 [ 

263 handler(QuantityModel), 

264 pcs.no_info_plain_validator_function(cls.from_model), 

265 ] 

266 ) 

267 return pcs.json_or_python_schema( 

268 json_schema=from_model_schema, 

269 python_schema=pcs.union_schema( 

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

271 ), 

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

273 ) 

274 

275 @classmethod 

276 def __get_pydantic_json_schema__( 

277 cls, schema: pcs.CoreSchema, handler: GetJsonSchemaHandler 

278 ) -> JsonSchemaValue: 

279 return handler(QuantityModel.__pydantic_core_schema__) 

280 

281 @classmethod 

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

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

284 

285 @classmethod 

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

287 assert quantity.isscalar 

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

289 

290 

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

292 

293 

294class _InlineArrayQuantitySerialization: 

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

296 

297 @classmethod 

298 def __get_pydantic_core_schema__( 

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

300 ) -> pcs.CoreSchema: 

301 from_model_schema = pcs.chain_schema( 

302 [ 

303 handler(InlineArrayQuantityModel), 

304 pcs.no_info_plain_validator_function(cls.from_model), 

305 ] 

306 ) 

307 return pcs.json_or_python_schema( 

308 json_schema=from_model_schema, 

309 python_schema=pcs.union_schema( 

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

311 ), 

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

313 ) 

314 

315 @classmethod 

316 def __get_pydantic_json_schema__( 

317 cls, schema: pcs.CoreSchema, handler: GetJsonSchemaHandler 

318 ) -> JsonSchemaValue: 

319 return handler(InlineArrayQuantityModel.__pydantic_core_schema__) 

320 

321 @classmethod 

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

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

324 

325 @classmethod 

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

327 assert quantity.isscalar 

328 return InlineArrayQuantityModel( 

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

330 unit=quantity.unit, 

331 ) 

332 

333 

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

335 

336 

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

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

339 

340 value: str 

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

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

343 

344 model_config = pydantic.ConfigDict( 

345 json_schema_extra={ 

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

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

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

349 } 

350 ) 

351 

352 

353class _TimeSerialization: 

354 """Pydantic hooks for time serialization. 

355 

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

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

358 """ 

359 

360 @classmethod 

361 def __get_pydantic_core_schema__( 

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

363 ) -> pcs.CoreSchema: 

364 from_model_schema = pcs.chain_schema( 

365 [ 

366 TimeModel.__pydantic_core_schema__, 

367 pcs.no_info_plain_validator_function(cls.from_model), 

368 ] 

369 ) 

370 return pcs.json_or_python_schema( 

371 json_schema=from_model_schema, 

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

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

374 ) 

375 

376 @classmethod 

377 def __get_pydantic_json_schema__( 

378 cls, schema: pcs.CoreSchema, handler: GetJsonSchemaHandler 

379 ) -> JsonSchemaValue: 

380 return handler(TimeModel.__pydantic_core_schema__) 

381 

382 @classmethod 

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

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

385 

386 @classmethod 

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

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

389 time = time.tai 

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

391 

392 

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