Coverage for core / src / sensorkit / std / instrument.py: 76%

84 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-02 00:03 +0000

1# SPDX-License-Identifier: Apache-2.0 

2from datetime import UTC 

3from enum import StrEnum 

4from typing import Literal 

5 

6from pydantic import AwareDatetime, BaseModel 

7 

8import sensorkit.api as sk 

9from sensorkit.std.traits import TemperatureUnit 

10 

11 

12class FrameType(StrEnum): 

13 """Astronomical frame type for a camera capture.""" 

14 

15 LIGHT = "light" 

16 BIAS = "bias" 

17 DARK = "dark" 

18 FLAT = "flat" 

19 

20 

21class AcquireData(sk.DeviceCommand): 

22 """Control instrument data acquisition. 

23 

24 While nothing stops a device from implementing this command directly, typically commands 

25 derived from it, such as `CameraCapture`, which add fields and specialize the command 

26 semantics for a particular device archetype, are implemented instead. 

27 

28 Four distinct actions are defined here, indicated by the `action` field: 

29 

30 - *acquire* - Executes a single, self-contained acquisition cycle. This is used for 

31 one-shot data collection operations where the device performs a complete measurement 

32 and returns the result. The acquisition begins and ends within a single command execution. 

33 The command returns when the acquisition is complete. 

34 

35 - *start* - Initiates a passive acquisition process. This begins an ongoing operation where 

36 the device continuously collects data until explicitly stopped by another `AcquireData` 

37 command (graceful), by an `Abort` command (forcible), or an error occurs. The context 

38 parameter allows implementation-specific configuration passthrough and provides metadata for 

39 data products in the case where the implementation outputs data products continuously. The 

40 command returns once the acquisition has successfully begun. 

41 

42 - *continue* - Maintains a passive acquisition that was previously started. This action is 

43 used to update the acquisition context or to act as a keepalive signal, should those things 

44 be supported or required by the device implementation. If a passive collection is not 

45 underway, an error is raised. In all cases, this command should return immediately. 

46 

47 - *stop* - Ends a passive acquisition that was previously started. If the device writes its 

48 output data product once at the end of acquisition, it may support the context parameter in 

49 this mode. 

50 

51 Note that the storage or transfer of the acquired data is outside the scope of this command. 

52 Typically, the device implementing the command will also be the receiver of the acquired data 

53 and will write it to a configured `DataGraph`, but it is also possible for data to be handled 

54 differently, e.g., received by a different entity or even handled entirely out-of-band of 

55 SensorKit. 

56 

57 Attributes: 

58 action: either "acquire", "start", "continue", or "stop" 

59 context: dictionary containing context Keywords 

60 """ 

61 

62 action: Literal["acquire", "start", "continue", "stop"] 

63 context: sk.Context | None 

64 

65 

66@sk.declare_keyword 

67class Binning(BaseModel): 

68 """Camera pixel binning configuration. 

69 

70 Attributes: 

71 x: horizontal binning factor 

72 y: vertical binning factor 

73 """ 

74 

75 x: float 

76 y: float 

77 

78 

79@sk.declare_keyword 

80class CameraSensorSize(BaseModel): 

81 """Camera sensor physical dimensions in pixels. 

82 

83 Attributes: 

84 x: sensor width in pixels 

85 y: sensor height in pixels 

86 """ 

87 

88 x: int 

89 y: int 

90 

91 @property 

92 def width(self): 

93 return self.x 

94 

95 @property 

96 def height(self): 

97 return self.y 

98 

99 

100@sk.declare_keyword 

101class CameraSensorTemperature(BaseModel): 

102 """Camera sensor temperature measurement or setpoint. 

103 

104 Attributes: 

105 temperature: sensor temperature value 

106 units: temperature unit (Fahrenheit, Celsius, or Kelvin) 

107 """ 

108 

109 temperature: float 

110 units: TemperatureUnit 

111 

112 

113class ConfigureCameraCooler(sk.DeviceCommand): 

114 """Set the target temperature of a camera cooler. 

115 

116 Attributes: 

117 enable: whether the cooler should be enabled 

118 setpoint: the target `CameraSensorTemperature` 

119 """ 

120 

121 enable: bool 

122 setpoint: CameraSensorTemperature 

123 

124 

125class ConfigureCameraSensor(sk.DeviceCommand): 

126 """Configure the camera sensor and other settings.""" 

127 

128 binning: Binning | None = None 

129 bias: float | None = None 

130 gain: float | None = None 

131 readout_mode: int | None = None 

132 

133 

134class CameraCapture(AcquireData): 

135 """Execute a single camera exposure. 

136 

137 Attributes: 

138 action: action type for the command (always "acquire") 

139 integration_time: exposure time in seconds 

140 frame_type: frame type to capture (light, dark, bias, flat) 

141 """ 

142 

143 action: Literal["acquire"] = "acquire" 

144 integration_time: float 

145 frame_type: FrameType = FrameType.LIGHT 

146 

147 

148@sk.declare_keyword 

149class ExposureInfo(BaseModel): 

150 """Parameters of the exposure that produced an image. 

151 

152 Captures the acquisition-time settings and measurements describing *how* an exposure 

153 was taken -- the circumstances of the shot as recorded by us. This is distinct from 

154 `ImageInfo`, which carries only what is needed to interpret the pixel buffer itself and 

155 is therefore recoverable from any image regardless of origin. `ExposureInfo` fields map 

156 onto the conventional FITS cards written for a science frame. 

157 

158 Attributes: 

159 date_obs: Start of the exposure, as a timezone-aware datetime. -> DATE-OBS 

160 exposure_time: Actual exposure duration in seconds. -> EXPTIME 

161 instrument: Instrument / camera identifier. -> INSTRUME 

162 image_type: Frame type (light, dark, bias, flat). -> IMAGETYP 

163 readout_mode: Camera readout mode, as an index or a name. -> READOUTM 

164 gain: Camera gain. -> GAIN 

165 offset: Camera offset / bias level. -> OFFSET 

166 ccd_temperature: Sensor temperature during the exposure, in degrees Celsius. -> CCD-TEMP 

167 set_temperature: Cooler setpoint, in degrees Celsius. -> SET-TEMP 

168 max_adu: Saturation level in ADU under the active readout configuration. -> SATURATE 

169 """ 

170 

171 date_obs: AwareDatetime 

172 exposure_time: float 

173 instrument: str 

174 image_type: FrameType | None = None 

175 readout_mode: int | str | None = None 

176 gain: float | None = None 

177 offset: float | None = None 

178 ccd_temperature: float | None = None 

179 set_temperature: float | None = None 

180 max_adu: int | None = None 

181 

182 def get_fits_cards(self): 

183 utc = self.date_obs.astimezone(UTC).replace(tzinfo=None) 

184 

185 yield "DATE-OBS", (utc.isoformat(), "UTC start of exposure") 

186 yield "EXPTIME", (self.exposure_time, "Exposure time [s]") 

187 yield "INSTRUME", (self.instrument, "Instrument name") 

188 

189 if self.image_type is not None: 

190 yield "IMAGETYP", (str(self.image_type), "Frame type") 

191 if self.readout_mode is not None: 

192 yield "READOUTM", (self.readout_mode, "Readout mode") 

193 if self.gain is not None: 

194 yield "GAIN", (self.gain, "Camera gain") 

195 if self.offset is not None: 

196 yield "OFFSET", (self.offset, "Camera offset") 

197 if self.ccd_temperature is not None: 

198 yield "CCD-TEMP", (self.ccd_temperature, "CCD temperature [C]") 

199 if self.set_temperature is not None: 

200 yield "SET-TEMP", (self.set_temperature, "Cooler setpoint [C]") 

201 if self.max_adu is not None: 

202 yield "SATURATE", (self.max_adu, "Saturation level [ADU]") 

203 

204 

205StandardCamera = sk.declare_archetype( 

206 "camera", 

207 required_commands=(CameraCapture,), 

208 optional_commands=(ConfigureCameraCooler,), 

209) 

210"""A standard camera device that can capture images with configurable integration time. 

211 

212This archetype defines the interface for camera devices capable of image acquisition 

213with specified exposure durations. It supports optional temperature control for 

214cooled camera sensors. 

215""" 

216 

217 

218@sk.declare_keyword 

219class RotatorPosition(BaseModel): 

220 """Current angular position of the rotator.""" 

221 position: float 

222 

223 

224class ChangeRotatorPosition(sk.DeviceCommand): 

225 """Move the rotator to a specified orientation. 

226 

227 Attributes: 

228 position: target position value for the rotator 

229 """ 

230 

231 position: float 

232 

233 

234StandardRotator = sk.declare_archetype( 

235 "rotator", 

236 required_commands=(ChangeRotatorPosition,), 

237) 

238"""A device that can rotate an optical element to a specified position. 

239 

240This archetype defines the interface for rotator devices that control the orientation of optical 

241components such as cameras instrument adapters within an optical system. 

242"""