Coverage for python / lsst / images / _transforms / _camera_frame_set.py: 26%

97 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 08:34 +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__ = ("CameraFrameSet", "CameraFrameSetSerializationModel") 

15 

16from typing import Any 

17 

18import astropy.units as u 

19import pydantic 

20 

21from .._geom import Bounds, Box 

22from ..serialization import ArchiveTree, InputArchive, OutputArchive 

23from . import _ast as astshim 

24from . import _frames # use this import style to facilitate pattern matching 

25from ._frame_set import FrameLookupError, FrameSet 

26from ._transform import Transform 

27 

28 

29class CameraFrameSet(FrameSet): 

30 """A `FrameSet` that manages the coordinate systems of a camera. 

31 

32 The `CameraFrameSet` class constructor is considered a private 

33 implementation detail. At present, instances can only be obtained by 

34 loading them from storage (`deserialize`) or converting a legacy 

35 `lsst.afw.cameraGeom` object (`from_legacy`). 

36 """ 

37 

38 # This constructor is kept private while we support both the astshim 

39 # and starlink-pyast AST wrappers. For now: 

40 # 'instrument': the short (butler dimension) name. 

41 # 'ast': an astshim.FrameSet as returned by 

42 # lsst.afw.cameraGeom.TransformMap.makeFrameSet. 

43 # Should have frames with Ident values FOCAL_PLANE, FIELD_ANGLE 

44 # and DETECTOR_${ID}, and the focal plane frame must know its 

45 # units. 

46 def __init__(self, instrument: str, ast: astshim.FrameSet): 

47 self._ast = ast 

48 self._focal_plane_frame_id: int = 0 

49 self._field_angle_frame_id: int = 0 

50 self._detector_frame_ids: dict[int, int] = {} 

51 for frame_id in range(1, self._ast.nFrame + 1): 

52 ast_frame = self._ast.getFrame(frame_id, copy=False) 

53 match ast_frame.ident: 

54 case "FOCAL_PLANE": 

55 self._focal_plane_frame_id = frame_id 

56 case "FIELD_ANGLE": 

57 self._field_angle_frame_id = frame_id 

58 case str(s) if s.startswith("DETECTOR_"): 

59 detector_id = int(s.removeprefix("DETECTOR_")) 

60 self._detector_frame_ids[detector_id] = frame_id 

61 case _: 

62 raise ValueError(f"Unexpected frame in camera AST FrameSet:\n{ast_frame.show()}.") 

63 if self._focal_plane_frame_id == 0: 

64 raise ValueError("No FOCAL_PLANE frame in camera AST FrameSet.") 

65 self._focal_plane_frame = _frames.FocalPlaneFrame( 

66 instrument=instrument, 

67 unit=u.Unit(self._ast.getFrame(self._focal_plane_frame_id, copy=False).getUnit(1)), 

68 ) 

69 self._field_angle_frame = _frames.FieldAngleFrame(instrument=instrument) 

70 if self._field_angle_frame_id == 0: 

71 raise ValueError("No FIELD_ANGLE frame in camera AST FrameSet.") 

72 

73 @property 

74 def instrument(self) -> str: 

75 """Name of the instrument (`str`).""" 

76 return self._focal_plane_frame.instrument 

77 

78 def focal_plane(self, visit: int | None = None) -> _frames.FocalPlaneFrame: 

79 """Return a focal plane frame for this instrument. 

80 

81 Parameters 

82 ---------- 

83 visit 

84 ID for the visit this frame will correspond to. This only needs 

85 to be provided in contexts where camera frames will be related to 

86 the sky via a `Projection`. 

87 """ 

88 if visit is None: 

89 return self._focal_plane_frame 

90 else: 

91 return self._focal_plane_frame.model_copy(update={"visit": visit}) 

92 

93 def field_angle(self, visit: int | None = None) -> _frames.FieldAngleFrame: 

94 """Return a field angle frame for this instrument. 

95 

96 Parameters 

97 ---------- 

98 visit 

99 ID for the visit this frame will correspond to. This only needs 

100 to be provided in contexts where camera frames will be related to 

101 the sky via a `Projection`. 

102 """ 

103 if visit is None: 

104 return self._field_angle_frame 

105 else: 

106 return self._field_angle_frame.model_copy(update={"visit": visit}) 

107 

108 def detector(self, detector: int, *, visit: int | None = None) -> _frames.DetectorFrame: 

109 """Return a detector pixel-coordinate frame for this instrument. 

110 

111 Parameters 

112 ---------- 

113 detector 

114 ID of the detector. 

115 visit 

116 ID for the visit this frame will correspond to. This only needs 

117 to be provided in contexts where camera frames will be related to 

118 the sky via a `Projection`. 

119 """ 

120 try: 

121 frame_id = self._detector_frame_ids[detector] 

122 except KeyError: 

123 raise FrameLookupError( 

124 f"No frame for detector {detector!r} in camera for {self.instrument!r}." 

125 ) from None 

126 ast_frame = self._ast.getFrame(frame_id, copy=False) 

127 bbox = Box.factory[ 

128 int(ast_frame.getBottom(2)) : int(ast_frame.getTop(2)), 

129 int(ast_frame.getBottom(1)) : int(ast_frame.getTop(1)), 

130 ] 

131 return _frames.DetectorFrame(instrument=self.instrument, detector=detector, visit=visit, bbox=bbox) 

132 

133 def __contains__(self, frame: _frames.Frame) -> bool: 

134 try: 

135 self._parse_frame_arg(frame) 

136 return True 

137 except FrameLookupError: 

138 return False 

139 

140 def __getitem__[I: _frames.Frame, O: _frames.Frame](self, key: tuple[I, O]) -> Transform[I, O]: 

141 in_frame, out_frame = key 

142 in_frame_id, in_bounds = self._parse_frame_arg(in_frame) 

143 out_frame_id, out_bounds = self._parse_frame_arg(out_frame) 

144 return Transform( 

145 in_frame, 

146 out_frame, 

147 self._ast.getMapping(in_frame_id, out_frame_id), 

148 in_bounds=in_bounds, 

149 out_bounds=out_bounds, 

150 ) 

151 

152 def _parse_frame_arg(self, frame: _frames.Frame) -> tuple[int, Bounds | None]: 

153 bounds: Bounds | None = None 

154 match frame: 

155 case _frames.DetectorFrame(instrument=self.instrument, detector=detector_id): 

156 try: 

157 frame_id = self._detector_frame_ids[detector_id] 

158 except KeyError: 

159 raise FrameLookupError( 

160 f"No frame for detector {detector_id!r} in camera for {self.instrument!r}." 

161 ) from None 

162 bounds = frame.bbox 

163 case _frames.FocalPlaneFrame(instrument=self.instrument): 

164 frame_id = self._focal_plane_frame_id 

165 case _frames.FieldAngleFrame(instrument=self.instrument): 

166 frame_id = self._field_angle_frame_id 

167 case _: 

168 raise FrameLookupError(f"Invalid frame for camera {self.instrument}: {frame!r}.") 

169 return frame_id, bounds 

170 

171 def serialize(self, archive: OutputArchive[Any]) -> CameraFrameSetSerializationModel: 

172 """Serialize the frame set to an archive. 

173 

174 Parameters 

175 ---------- 

176 archive 

177 Archive to serialize to. 

178 

179 Returns 

180 ------- 

181 `CameraFrameSetSerializationModel` 

182 Serialized form of the frame set. 

183 """ 

184 return CameraFrameSetSerializationModel(instrument=self.instrument, ast=self._ast.show()) 

185 

186 @staticmethod 

187 def deserialize(model: CameraFrameSetSerializationModel, archive: InputArchive[Any]) -> CameraFrameSet: 

188 """Deserialize a frame set from an archive. 

189 

190 Parameters 

191 ---------- 

192 model 

193 Seralized form of the frame set. 

194 archive 

195 Archive to read from. 

196 """ 

197 return CameraFrameSet(model.instrument, astshim.FrameSet.fromString(model.ast)) 

198 

199 @staticmethod 

200 def _get_archive_tree_type( 

201 pointer_type: type[pydantic.BaseModel], 

202 ) -> type[CameraFrameSetSerializationModel]: 

203 """Return the serialization model type for this object for an archive 

204 type that uses the given pointer type. 

205 """ 

206 return CameraFrameSetSerializationModel 

207 

208 @classmethod 

209 def from_legacy(cls, camera: Any) -> CameraFrameSet: 

210 """Construct a transform from a legacy `lsst.afw.cameraGeom.Camera`. 

211 

212 Parameters 

213 ---------- 

214 camera 

215 An `lsst.afw.cameraGeom.Camera` instance to convert. 

216 """ 

217 transform_map = camera.getTransformMap() 

218 ast_frame_set = transform_map.makeFrameSet(list(camera)) 

219 return CameraFrameSet("HSC", ast_frame_set) 

220 

221 

222class CameraFrameSetSerializationModel(ArchiveTree): 

223 """Serialization model for `CameraFrameSet`.""" 

224 

225 instrument: str = pydantic.Field(description="Name of the instrument.") 

226 ast: str = pydantic.Field( 

227 description="A serialized Starlink AST FrameSet, using the AST native encoding." 

228 )