Coverage for python / lsst / obs / lsst / script / generateCamera.py: 6%

181 statements  

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

1#!/usr/bin/env python 

2# This file is part of obs_lsst. 

3# 

4# Developed for the LSST Data Management System. 

5# This product includes software developed by the LSST Project 

6# (http://www.lsst.org). 

7# See the COPYRIGHT file at the top-level directory of this distribution 

8# for details of code ownership. 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the LSST License Statement and 

21# the GNU General Public License along with this program. If not, 

22# see <http://www.lsstcorp.org/LegalNotices/>. 

23# 

24 

25__all__ = ("main",) 

26 

27import argparse 

28import os 

29import sys 

30import shutil 

31import yaml 

32import numpy as np 

33 

34 

35def findYamlOnPath(fileName, searchPath): 

36 """Find and return a file somewhere in the directories listed in 

37 searchPath""" 

38 for d in searchPath: 

39 f = os.path.join(d, fileName) 

40 if os.path.exists(f): 

41 return f 

42 

43 raise FileNotFoundError("Unable to find %s on path %s" % (fileName, ":".join(searchPath))) 

44 

45 

46def parseYamlOnPath(fileName, searchPath): 

47 """Find the named file in search path, parse the YAML, and return contents. 

48 """ 

49 yamlFile = findYamlOnPath(fileName, searchPath) 

50 with open(yamlFile) as fd: 

51 content = yaml.load(fd, Loader=yaml.CSafeLoader) 

52 return content 

53 

54 

55def build_argparser(): 

56 """Construct an argument parser for the ``generateCamera.py`` script. 

57 

58 Returns 

59 ------- 

60 argparser : `argparse.ArgumentParser` 

61 The argument parser that defines the ``translate_header.py`` 

62 command-line interface. 

63 """ 

64 

65 parser = argparse.ArgumentParser(description=""" 

66 Generate a camera.yaml file for a camera by assembling descriptions of 

67 rafts, sensors, etc. 

68 

69 Because we have many similar cameras, the assembly uses a :-separated 

70 search path of directories to find desired information. The _first_ 

71 occurrence of a filename is used. 

72 """) 

73 

74 parser.add_argument('outputFile', type=str, help="Name of generated file") 

75 parser.add_argument('--path', type=str, help="List of directories to search for components", 

76 default=False) 

77 parser.add_argument('--copy-to', type=str, help="Additional directory to write output to.", default="") 

78 parser.add_argument('--verbose', action="store_true", help="How chatty should I be?", default=False) 

79 

80 return parser 

81 

82 

83def applyRaftYaw(offset, raftYaw): 

84 """Apply raft yaw angle to internal offsets of the CCDs. 

85 

86 Parameters 

87 ---------- 

88 offset : `list` of `float` 

89 A list of the offsets to rotate: [x, y, z] 

90 raftYaw : `float` 

91 Raft yaw angle in degrees. 

92 

93 Returns 

94 ------- 

95 offsets : `list` of `float 

96 3-item sequence of floats containing the rotated offsets. 

97 """ 

98 if raftYaw == 0.: 

99 return offset 

100 new_offset = np.zeros(3, dtype=float) 

101 sinTheta = np.sin(np.radians(raftYaw)) 

102 cosTheta = np.cos(np.radians(raftYaw)) 

103 new_offset[0] = cosTheta*offset[0] - sinTheta*offset[1] 

104 new_offset[1] = sinTheta*offset[0] + cosTheta*offset[1] 

105 new_offset[2] = offset[2] 

106 return new_offset 

107 

108 

109def generateCamera(cameraFile, path): 

110 """Generate a combined camera YAML definition from component parts. 

111 

112 Parameters 

113 ---------- 

114 cameraFile : `str` 

115 Path to output YAML file. 

116 path : `str` or `list` of `str` 

117 List of directories to search for component YAML files or a 

118 colon-separated path string. If relative paths are given they will be 

119 converted to absolute path by combining with directory specified with 

120 the output ``cameraFile``. 

121 """ 

122 cameraFileDir = os.path.dirname(cameraFile) 

123 # In some places, it's convenient to have aliases to rafts that should be 

124 # removed in the built camera. 

125 raftNameMap = {'R00W': 'R00', 'R44W': 'R44', 'R04W': 'R04', 'R40W': 'R40'} 

126 

127 if not cameraFile.endswith(".yaml"): 

128 raise RuntimeError(f"Output file name ({cameraFile}) does not end with .yaml") 

129 

130 if isinstance(path, str): 

131 path = path.split(":") 

132 searchPath = [os.path.join(cameraFileDir, d) for d in path] 

133 

134 cameraSkl = parseYamlOnPath("cameraHeader.yaml", searchPath) 

135 cameraTransforms = parseYamlOnPath("cameraTransforms.yaml", searchPath) 

136 raftData = parseYamlOnPath("rafts.yaml", searchPath) 

137 ccdData = parseYamlOnPath("ccdData.yaml", searchPath) 

138 

139 # See if we have an override of the name 

140 try: 

141 nameYaml = parseYamlOnPath("name.yaml", searchPath) 

142 except FileNotFoundError: 

143 nameOverride = None 

144 else: 

145 nameOverride = nameYaml["name"] 

146 

147 # Copy the camera header, replacing the name if needed. We can not 

148 # write out the cameraSkl dataset because that will expand all the 

149 # YAML references. We must edit the file itself. 

150 inputHeader = findYamlOnPath("cameraHeader.yaml", searchPath) 

151 if nameOverride: 

152 with open(inputHeader) as infd: 

153 with open(cameraFile, "w") as outfd: 

154 replaced = False 

155 for line in infd: 

156 if not replaced and line.startswith("name :") or line.startswith("name:"): 

157 line = f"name : {nameOverride}\n" 

158 replaced = True 

159 print(line, file=outfd, end="") 

160 if not replaced: 

161 raise RuntimeError(f"Override name {nameOverride} specified but no name" 

162 f" to replace in {inputHeader}") 

163 else: 

164 shutil.copyfile(inputHeader, cameraFile) 

165 

166 nindent = 0 # current number of indents 

167 

168 def indent(): 

169 """Return the current indent string""" 

170 dindent = 2 # number of spaces per indent 

171 return (nindent*dindent - 1) * " " # print will add the extra " " 

172 

173 with open(cameraFile, "a") as fd: 

174 print(""" 

175# 

176# Specify the geometrical transformations relevant to the camera in all appropriate 

177# (and known!) coordinate systems 

178#""", file=fd) 

179 for k, v in cameraTransforms.items(): 

180 print("%s : %s" % (k, v), file=fd) 

181 

182 print(""" 

183# 

184# Define our specific devices 

185# 

186# All the CCDs present in this file 

187# 

188CCDs :\ 

189""", file=fd) 

190 

191 for raftName, perRaftData in raftData["rafts"].items(): 

192 try: 

193 raftCcdData = parseYamlOnPath(f"{raftName}.yaml", searchPath)[raftName] 

194 except FileNotFoundError: 

195 print("Unable to load CCD descriptions for raft %s" % raftName, file=sys.stderr) 

196 continue 

197 

198 try: 

199 detectorType = raftCcdData["detectorType"] 

200 except KeyError: 

201 raise RuntimeError("Unable to lookup detector type for %s" % raftName) 

202 

203 try: 

204 _ccds = cameraSkl['RAFT_%s' % detectorType]["ccds"] # describe this *type* of raft 

205 except KeyError: 

206 raise RuntimeError("No raft for detector type %s" % detectorType) 

207 

208 try: 

209 sensorTypes = raftCcdData["sensorTypes"] 

210 except KeyError: 

211 sensorTypes = None 

212 

213 # only include CCDs in the raft for which we have a serial 

214 # (the value isn't checked) 

215 ccds = {} 

216 for ccdName in raftCcdData["ccdSerials"]: 

217 try: 

218 ccds[ccdName] = _ccds[ccdName] 

219 except KeyError: 

220 raise RuntimeError("Unable to look up CCD %s in %s" % 

221 (ccdName, 'RAFT_%s' % detectorType)) 

222 del _ccds 

223 

224 try: 

225 amps = cameraSkl['CCD_%s' % detectorType]["amplifiers"] # describe this *type* of ccd 

226 except KeyError: 

227 raise RuntimeError("Unable to lookup amplifiers for CCD type CCD_%s" % detectorType) 

228 

229 try: 

230 crosstalkCoeffs = ccdData["crosstalk"][detectorType] 

231 except KeyError: 

232 crosstalkCoeffs = None 

233 

234 nindent += 1 

235 

236 raftOffset = perRaftData["offset"] 

237 if len(raftOffset) == 2: 

238 raftOffset.append(0.0) # Default offset_z is 0.0 

239 id0 = perRaftData['id0'] 

240 try: 

241 raftYaw = perRaftData['yaw'] 

242 except KeyError: 

243 raftYaw = 0. 

244 geometryWithinRaft = raftCcdData.get('geometryWithinRaft', {}) 

245 

246 for ccdName, ccdLayout in ccds.items(): 

247 if ccdName in geometryWithinRaft: 

248 doffset = geometryWithinRaft[ccdName]['offset'] 

249 if len(doffset) == 2: 

250 doffset.append(0.0) # Default offset_z is 0.0 

251 yaw = geometryWithinRaft[ccdName]['yaw'] + raftYaw 

252 else: 

253 doffset = (0.0, 0.0, 0.0) 

254 yaw = None 

255 

256 print(indent(), "%s_%s : " % (raftNameMap.get(raftName, raftName), ccdName), file=fd) 

257 nindent += 1 

258 print(indent(), "<< : *%s_%s" % (ccdName, detectorType), file=fd) 

259 if sensorTypes is not None: 

260 print(indent(), "detectorType : %i" % (sensorTypes[ccdName]), file=fd) 

261 print(indent(), "id : %s" % (id0 + ccdLayout['id']), file=fd) 

262 print(indent(), "serial : %s" % (raftCcdData['ccdSerials'][ccdName]), file=fd) 

263 print(indent(), "physicalType : %s" % (detectorType), file=fd) 

264 print(indent(), "refpos : %s" % (ccdLayout['refpos']), file=fd) 

265 if len(ccdLayout['offset']) == 2: 

266 ccdLayout['offset'].append(0.0) # Default offset_z is 0.0 

267 ccdLayoutOffset = applyRaftYaw([el1+el2 for el1, el2 in zip(ccdLayout['offset'], doffset)], 

268 raftYaw) 

269 print(indent(), "offset : [%g, %g, %g]" % (ccdLayoutOffset[0] + raftOffset[0], 

270 ccdLayoutOffset[1] + raftOffset[1], 

271 ccdLayoutOffset[2] + raftOffset[2]), 

272 file=fd) 

273 if yaw is not None: 

274 print(indent(), "yaw : %g" % (yaw), file=fd) 

275 

276 if crosstalkCoeffs is not None: 

277 print(indent(), "crosstalk : [", file=fd) 

278 nindent += 1 

279 print(indent(), file=fd, end="") 

280 for iAmp in amps: 

281 for jAmp in amps: 

282 print("%11.3e," % crosstalkCoeffs[iAmp][jAmp], file=fd, end='') 

283 print(file=fd, end="\n" + indent()) 

284 nindent -= 1 

285 print("]", file=fd) 

286 

287 try: 

288 amplifierData = raftCcdData['amplifiers'][ccdName] 

289 except KeyError: 

290 raise RuntimeError("Unable to lookup amplifier data for detector %s_%s" % 

291 (raftName, ccdName)) 

292 

293 print(indent(), "amplifiers :", file=fd) 

294 nindent += 1 

295 for ampName, ampData in amps.items(): 

296 print(indent(), "%s :" % ampName, file=fd) 

297 

298 if ampName not in amplifierData: 

299 raise RuntimeError("Unable to lookup amplifier data for amp %s in detector %s_%s" % 

300 (ampName, raftName, ccdName)) 

301 

302 nindent += 1 

303 print(indent(), "<< : *%s_%s" % (ampName, detectorType), file=fd) 

304 print(indent(), "gain : %g" % (amplifierData[ampName]['gain']), file=fd) 

305 print(indent(), "readNoise : %g" % (amplifierData[ampName]['readNoise']), file=fd) 

306 saturation = amplifierData[ampName].get('saturation') 

307 if saturation: # if known, override the per-CCD-type default from cameraHeader.yaml 

308 print(indent(), "saturation : %g" % (saturation), file=fd) 

309 nindent -= 1 

310 nindent -= 1 

311 

312 nindent -= 1 

313 

314 nindent -= 1 

315 

316 

317def main(): 

318 args = build_argparser().parse_args() 

319 

320 try: 

321 generateCamera(args.outputFile, args.path) 

322 if args.copy_to: 

323 shutil.copyfile(args.outputFile, os.path.join(args.copy_to, os.path.basename(args.outputFile))) 

324 except Exception as e: 

325 print(f"{e}", file=sys.stderr) 

326 return 1 

327 return 0