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

97 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-23 22:23 -0700

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 _frames # use this import style to facilitate pattern matching 

24from ._frame_set import FrameLookupError, FrameSet 

25from ._transform import Transform 

26 

27 

28class CameraFrameSet(FrameSet): 

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

30 

31 The `CameraFrameSet` class constructor is considered a private 

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

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

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

35 """ 

36 

37 # After we've moved from astshim to pyast we can make this constructor 

38 # public and document it. For now: 

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

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

41 # lsst.afw.cameraGeom.TransformMap.makeFrameSet. 

42 # Should have frames with Ident values FOCAL_PLANE, FIELD_ANGLE 

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

44 # units. 

45 def __init__(self, instrument: str, ast: Any): 

46 self._ast = ast 

47 self._focal_plane_frame_id: int = 0 

48 self._field_angle_frame_id: int = 0 

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

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

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

52 match ast_frame.ident: 

53 case "FOCAL_PLANE": 

54 self._focal_plane_frame_id = frame_id 

55 case "FIELD_ANGLE": 

56 self._field_angle_frame_id = frame_id 

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

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

59 self._detector_frame_ids[detector_id] = frame_id 

60 case _: 

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

62 if self._focal_plane_frame_id == 0: 

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

64 self._focal_plane_frame = _frames.FocalPlaneFrame( 

65 instrument=instrument, 

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

67 ) 

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

69 if self._field_angle_frame_id == 0: 

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

71 

72 @property 

73 def instrument(self) -> str: 

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

75 return self._focal_plane_frame.instrument 

76 

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

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

79 

80 Parameters 

81 ---------- 

82 visit 

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

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

85 the sky via a `Projection`. 

86 """ 

87 if visit is None: 

88 return self._focal_plane_frame 

89 else: 

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

91 

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

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

94 

95 Parameters 

96 ---------- 

97 visit 

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

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

100 the sky via a `Projection`. 

101 """ 

102 if visit is None: 

103 return self._field_angle_frame 

104 else: 

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

106 

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

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

109 

110 Parameters 

111 ---------- 

112 detector 

113 ID of the detector. 

114 visit 

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

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

117 the sky via a `Projection`. 

118 """ 

119 try: 

120 frame_id = self._detector_frame_ids[detector] 

121 except KeyError: 

122 raise FrameLookupError( 

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

124 ) from None 

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

126 bbox = Box.factory[ 

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

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

129 ] 

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

131 

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

133 try: 

134 self._parse_frame_arg(frame) 

135 return True 

136 except FrameLookupError: 

137 return False 

138 

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

140 in_frame, out_frame = key 

141 in_frame_id, in_bounds = self._parse_frame_arg(in_frame) 

142 out_frame_id, out_bounds = self._parse_frame_arg(out_frame) 

143 return Transform( 

144 in_frame, 

145 out_frame, 

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

147 in_bounds=in_bounds, 

148 out_bounds=out_bounds, 

149 ) 

150 

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

152 bounds: Bounds | None = None 

153 match frame: 

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

155 try: 

156 frame_id = self._detector_frame_ids[detector_id] 

157 except KeyError: 

158 raise FrameLookupError( 

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

160 ) from None 

161 bounds = frame.bbox 

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

163 frame_id = self._focal_plane_frame_id 

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

165 frame_id = self._field_angle_frame_id 

166 case _: 

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

168 return frame_id, bounds 

169 

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

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

172 

173 Parameters 

174 ---------- 

175 archive 

176 Archive to serialize to. 

177 

178 Returns 

179 ------- 

180 `CameraFrameSetSerializationModel` 

181 Serialized form of the frame set. 

182 """ 

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

184 

185 @staticmethod 

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

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

188 

189 Parameters 

190 ---------- 

191 model 

192 Seralized form of the frame set. 

193 archive 

194 Archive to read from. 

195 """ 

196 import astshim 

197 

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

199 

200 @staticmethod 

201 def _get_archive_tree_type( 

202 pointer_type: type[pydantic.BaseModel], 

203 ) -> type[CameraFrameSetSerializationModel]: 

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

205 type that uses the given pointer type. 

206 """ 

207 return CameraFrameSetSerializationModel 

208 

209 @classmethod 

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

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

212 

213 Parameters 

214 ---------- 

215 camera 

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

217 """ 

218 transform_map = camera.getTransformMap() 

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

220 return CameraFrameSet("HSC", ast_frame_set) 

221 

222 

223class CameraFrameSetSerializationModel(ArchiveTree): 

224 """Serialization model for `CameraFrameSet`.""" 

225 

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

227 ast: str = pydantic.Field( 

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

229 )