Coverage for python / lsst / obs / lsst / script / generateCamera.py: 6%
181 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-06 09:01 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-06 09:01 +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#
25__all__ = ("main",)
27import argparse
28import os
29import sys
30import shutil
31import yaml
32import numpy as np
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
43 raise FileNotFoundError("Unable to find %s on path %s" % (fileName, ":".join(searchPath)))
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
55def build_argparser():
56 """Construct an argument parser for the ``generateCamera.py`` script.
58 Returns
59 -------
60 argparser : `argparse.ArgumentParser`
61 The argument parser that defines the ``translate_header.py``
62 command-line interface.
63 """
65 parser = argparse.ArgumentParser(description="""
66 Generate a camera.yaml file for a camera by assembling descriptions of
67 rafts, sensors, etc.
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 """)
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)
80 return parser
83def applyRaftYaw(offset, raftYaw):
84 """Apply raft yaw angle to internal offsets of the CCDs.
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.
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
109def generateCamera(cameraFile, path):
110 """Generate a combined camera YAML definition from component parts.
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'}
127 if not cameraFile.endswith(".yaml"):
128 raise RuntimeError(f"Output file name ({cameraFile}) does not end with .yaml")
130 if isinstance(path, str):
131 path = path.split(":")
132 searchPath = [os.path.join(cameraFileDir, d) for d in path]
134 cameraSkl = parseYamlOnPath("cameraHeader.yaml", searchPath)
135 cameraTransforms = parseYamlOnPath("cameraTransforms.yaml", searchPath)
136 raftData = parseYamlOnPath("rafts.yaml", searchPath)
137 ccdData = parseYamlOnPath("ccdData.yaml", searchPath)
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"]
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)
166 nindent = 0 # current number of indents
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 " "
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)
182 print("""
183#
184# Define our specific devices
185#
186# All the CCDs present in this file
187#
188CCDs :\
189""", file=fd)
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
198 try:
199 detectorType = raftCcdData["detectorType"]
200 except KeyError:
201 raise RuntimeError("Unable to lookup detector type for %s" % raftName)
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)
208 try:
209 sensorTypes = raftCcdData["sensorTypes"]
210 except KeyError:
211 sensorTypes = None
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
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)
229 try:
230 crosstalkCoeffs = ccdData["crosstalk"][detectorType]
231 except KeyError:
232 crosstalkCoeffs = None
234 nindent += 1
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', {})
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
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)
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)
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))
293 print(indent(), "amplifiers :", file=fd)
294 nindent += 1
295 for ampName, ampData in amps.items():
296 print(indent(), "%s :" % ampName, file=fd)
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))
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
312 nindent -= 1
314 nindent -= 1
317def main():
318 args = build_argparser().parse_args()
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