24 """Calibration products production task code.""" 25 from __future__
import absolute_import, division, print_function
34 import lsst.eotest.sensor
as sensorTest
38 """Config class for the calibration products production (CP) task.""" 40 ccdKey = pexConfig.Field(
42 doc=
"The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'",
45 fe55 = pexConfig.ConfigurableField(
46 target=sensorTest.Fe55Task,
47 doc=
"The Fe55 analysis task.",
49 doFe55 = pexConfig.Field(
51 doc=
"Measure gains using Fe55?",
54 readNoise = pexConfig.ConfigurableField(
55 target=sensorTest.ReadNoiseTask,
56 doc=
"The read noise task.",
58 doReadNoise = pexConfig.Field(
60 doc=
"Measure the read-noise?",
63 brightPixels = pexConfig.ConfigurableField(
64 target=sensorTest.BrightPixelsTask,
65 doc=
"The bright pixel/column finding task.",
67 doBrightPixels = pexConfig.Field(
69 doc=
"Find bright pixels?",
72 darkPixels = pexConfig.ConfigurableField(
73 target=sensorTest.DarkPixelsTask,
74 doc=
"The dark pixel/column finding task.",
76 doDarkPixels = pexConfig.Field(
78 doc=
"Find dark pixels?",
81 traps = pexConfig.ConfigurableField(
82 target=sensorTest.TrapTask,
83 doc=
"The trap-finding task.",
85 doTraps = pexConfig.Field(
87 doc=
"Find traps using pocket-pumping exposures?",
90 cte = pexConfig.ConfigurableField(
91 target=sensorTest.CteTask,
92 doc=
"The CTE analysis task.",
94 doCTE = pexConfig.Field(
96 doc=
"Measure the charge transfer efficiency?",
99 ptc = pexConfig.ConfigurableField(
100 target=sensorTest.PtcTask,
101 doc=
"The PTC analysis task.",
103 doPTC = pexConfig.Field(
105 doc=
"Measure the photon transfer curve?",
108 flatPair = pexConfig.ConfigurableField(
109 target=sensorTest.FlatPairTask,
110 doc=
"The flat-pair analysis task.",
112 doFlatPair = pexConfig.Field(
114 doc=
"Measure the detector response vs incident flux using flat pairs?",
117 eotestOutputPath = pexConfig.Field(
119 doc=
"Path to which to write the eotest output results. Madatory runtime arg for running eotest.",
122 requireAllEOTests = pexConfig.Field(
124 doc=
"If True, all tests are required to be runnable, and will Raise if data is missing. If False, " 125 "processing will continue if a previous part failed due to the input dataset being incomplete.",
128 flatPairMaxPdFracDev = pexConfig.Field(
130 doc=
"Maximum allowed fractional deviation between photodiode currents for the eotest flatPair task. " 131 "This value is passed to the task's run() method at runtime rather than being stored in the task's" 132 "own pexConfig field.",
137 """Set default config options for the subTasks.""" 139 self.
fe55.temp_set_point = -100
140 self.
fe55.temp_set_point_tol = 20
158 """Override of the valiate() method. 160 The pexConfigs of the subTasks here cannot be validated in the normal way, as they are the configs 161 for eotest, which does illegal things, and this would require an upstream PR to fix. Therefore, we 162 override the validate() method here, and use it to set the output directory for each of the tasks 163 based on the legal pexConfig parameter for the main task. 165 log = lsstLog.Log.getLogger(
"cp.pipe.runEotestConfig")
167 raise RuntimeError(
"Must supply an output path for eotest data. " 168 "Please set config.eotestOutputPath.")
170 taskList = [
'fe55',
'brightPixels',
'darkPixels',
'readNoise',
'traps',
'cte',
'flatPair',
'ptc']
171 for task
in taskList:
172 if getattr(self, task).output_dir !=
'.':
175 log.warn(
"OVERWRITING: Found a user defined output path of %s for %sTask. " 176 "This has been overwritten with %s, as individually specified output paths for " 177 "subTasks are not supported at present" % (getattr(self, task).output_dir,
184 Task to run test stand data through eotest using a butler. 186 This task is used to produce an eotest report (the project's sensor 187 acceptance testing package) 188 Examples of some of its operations are as follows: 189 * Given a set of flat-field images, find the dark pixels and columns. 190 * Given a set of darks, find the bright pixels and columns. 191 * Given a set of Fe55 exposures, calulate the gain of the readout chain, 193 * Given a set of Fe55 exposures, calulate the instrinsic PSF of the silicon, 194 and the degradation of 195 * the PSF due to CTE. 196 * Given a set of flat-pairs, measure the photon transfer curve (PTC). 197 * Given a set of bias frames, calculate the read noise of the system in e-. 198 * Given a set of pocket-pumping exposures, find charge-traps in the silicon. 200 The RunEotestTask.runEotestDirect() is only applicable to LSST sensors, and 201 only for a specific type of dataset. This method takes a 202 dafPersistance.Butler corresponding to a repository in which a full eotest 203 run has been taken and ingested, and runs each of the tasks in eotest 204 directly, allowing for bitwise comparison with results given by the camera 207 See http://ls.st/ldm-151 Chapter 4, Calibration Products Production for 208 further details regarding the inputs and outputs. 211 ConfigClass = RunEotestConfig
212 _DefaultName =
"runEotest" 215 """Constructor for the RunEotestTask.""" 216 if 'lsst.eotest.sensor' not in sys.modules:
217 raise RuntimeError(
'eotest failed to import')
219 pipeBase.CmdLineTask.__init__(self, *args, **kwargs)
227 self.makeSubtask(
"fe55")
228 self.makeSubtask(
"readNoise")
229 self.makeSubtask(
"brightPixels")
230 self.makeSubtask(
"darkPixels")
231 self.makeSubtask(
"traps")
232 self.makeSubtask(
"cte")
233 self.makeSubtask(
"flatPair")
234 self.makeSubtask(
"ptc")
236 def _getMaskFiles(self, path, ccd):
237 """Get all available eotest mask files for a given ccd. 239 Each stage of the processing generates more mask files, so this allows each to be picked up 240 as more and more tests run, and saves having to have clever logic for if some tasks fail. 245 Path on which to find the mask files 246 ccd : `string` or `int` 247 Name/identifier of the CCD 251 maskFiles : iterable of `str` 252 List of mask files, or an empty tuple if none are found 254 pattern =
'*' +
str(ccd) +
'*mask*' 255 maskFiles = glob.glob(os.path.join(path, pattern))
256 return maskFiles
if len(maskFiles) > 0
else ()
258 def _cleanupEotest(self, path):
259 """Delete all the medianed files left behind after eotest has run. 261 Running eotest generates a lot of interim medianed files, so this just cleans them up. 266 Path on which to delete all the eotest medianed files. 268 for filename
in glob.glob(os.path.join(path,
'*_median_*.fits')):
272 """After running eotest, generate pdf(s) of the results. 274 Generate a sensor test report from the output data in config.eotestOutputPath, one for each CCD. 275 The pdf file(s), along with the .tex file(s) and the individual plots are written 276 to the eotestOutputPath. 277 .pdf generation requires a TeX distro including pdflatex to be installed. 279 ccds = butler.queryMetadata(
'raw', self.config.ccdKey)
281 self.log.
info(
"Starting test report generation for %s"%ccd)
283 plotPath = os.path.join(self.config.eotestOutputPath,
'plots')
284 if not os.path.exists(plotPath):
285 os.makedirs(plotPath)
286 plots = sensorTest.EOTestPlots(ccd, self.config.eotestOutputPath, plotPath)
287 eoTestReport = sensorTest.EOTestReport(plots, wl_dir=
'')
288 eoTestReport.make_figures()
289 eoTestReport.make_pdf()
290 except Exception
as e:
291 self.log.
warn(
"Failed to make eotest report for %s: %s"%(ccd, e))
292 self.log.
info(
"Finished test report generation.")
297 Generate calibration products using eotest algorithms. 299 Generate all calibration products possible using the vanilla eotest implementation, 300 given a butler for a TS8 (raft-test) repo. It can contain multiple runs, but must correspond to 301 only a single raft/RTM. 303 - Run all eotest tasks possible, using the butler to gather the data 304 - Write outputs in eotest format 306 In order to replicate the canonical eotest analysis, the tasks should be run in a specific order. 307 This is given/defined in the "Steps" section here: 308 http://lsst-camera.slac.stanford.edu/eTraveler/exp/LSST-CAMERA/displayProcess.jsp?processPath=1179 310 But is replicated here for conveniece: 312 * CCD Read Noise Analysis 313 * Bright Defects Analysis 314 * Dark Defects Analysis 316 * Dark Current X - will not be implemented here 317 * Charge Transfer Efficiencies 318 * Photo-response analysis X - will not be implemented here 319 * Flat Pairs Analysis 320 * Photon Transfer Curve 321 * Quantum Efficiency X - will not be implemented here 323 List of tasks that exist in the eotest package but aren't mentioned on the above link: 330 # TODO: For each eotest task, find out what the standard raft testing does for the optional params. 331 i.e. many have optional params for gains, bias-frames etc - if we want bitwise identicallity then we 332 need to know what is typically provided to these tasks when the camera team runs this code. 333 This can probably be worked out from https://github.com/lsst-camera-dh/lcatr-harness 334 but it sounds like Jim Chiang doesn't recommend trying to do that. 339 butler : `lsst.daf.persistence.butler` 340 Butler for the repo containg the eotest data to be used 342 Optional run number, to be used for repos containing multiple runs 344 self.log.
info(
"Running eotest routines direct")
347 runs = butler.queryMetadata(
'raw', [
'run'])
352 raise RuntimeError(
"Butler query found %s for runs. eotest datasets must have a run number," 353 "and you must specify which run to use if a respoitory contains several." 358 raise RuntimeError(
"Butler query found %s for runs, but the run specified (%s) " 359 "was not among them." % (runs, run))
362 if not os.path.exists(self.config.eotestOutputPath):
363 os.makedirs(self.config.eotestOutputPath)
365 ccds = butler.queryMetadata(
'raw', self.config.ccdKey)
366 imTypes = butler.queryMetadata(
'raw', [
'imageType'])
367 testTypes = butler.queryMetadata(
'raw', [
'testType'])
372 if self.config.doFe55:
373 fe55TaskDataId = {
'run': run,
'testType':
'FE55',
'imageType':
'FE55'}
374 self.log.
info(
"Starting Fe55 pixel task")
376 if 'FE55' not in testTypes:
377 msg =
"No Fe55 tests found. Available data: %s" % testTypes
378 if self.config.requireAllEOTests:
379 raise RuntimeError(msg)
381 self.log.
warn(msg +
"\nSkipping Fe55 task")
383 fe55Filenames = [butler.get(
'raw_filename', dataId={
'visit': visit,
384 self.config.ccdKey: ccd})[0][:-3]
385 for visit
in butler.queryMetadata(
'raw', [
'visit'], dataId=fe55TaskDataId)]
386 self.log.
trace(
"Fe55Task: Processing %s with %s files" % (ccd, len(fe55Filenames)))
387 maskFiles = self.
_getMaskFiles(self.config.eotestOutputPath, ccd)
388 gains = self.fe55.
run(sensor_id=ccd, infiles=fe55Filenames, mask_files=maskFiles)
392 butler.put(gains,
'eotest_gain', dataId={self.config.ccdKey: ccd,
'run': run})
403 if self.config.doReadNoise:
405 self.log.
info(
"Starting readNoise task")
406 noiseTaskDataId = {
'run': run,
'testType':
'FE55',
'imageType':
'BIAS'}
408 if (
'FE55' not in testTypes)
or (
'BIAS' not in imTypes):
409 msg =
"Required data for readNoise unavailable. Available data:\ 410 \ntestTypes: %s\nimageTypes: %s" % (testTypes, imTypes)
411 if self.config.requireAllEOTests:
412 raise RuntimeError(msg)
414 self.log.
warn(msg +
"\nSkipping noise task")
415 noiseFilenames = [butler.get(
'raw_filename', dataId={
'visit': visit,
416 self.config.ccdKey: ccd})[0][:-3]
417 for visit
in butler.queryMetadata(
'raw', [
'visit'],
418 dataId=noiseTaskDataId)]
419 self.log.
trace(
"Fe55Task: Processing %s with %s files" % (ccd, len(noiseFilenames)))
420 maskFiles = self.
_getMaskFiles(self.config.eotestOutputPath, ccd)
421 gains = butler.get(
'eotest_gain', dataId={self.config.ccdKey: ccd,
'run': run})
422 self.readNoise.
run(sensor_id=ccd, bias_files=noiseFilenames,
423 gains=gains, mask_files=maskFiles)
429 if self.config.doBrightPixels:
430 self.log.
info(
"Starting bright pixel task")
431 brightTaskDataId = {
'run': run,
'testType':
'DARK',
'imageType':
'DARK'}
433 if 'DARK' not in testTypes:
434 msg =
"No dark tests found. Available data: %s" % testTypes
435 if self.config.requireAllEOTests:
436 raise RuntimeError(msg)
438 self.log.
warn(msg +
"\nSkipping bright pixel task")
440 darkFilenames = [butler.get(
'raw_filename', dataId={
'visit': visit,
441 self.config.ccdKey: ccd})[0][:-3]
442 for visit
in butler.queryMetadata(
'raw', [
'visit'],
443 dataId=brightTaskDataId)]
444 self.log.
trace(
"BrightTask: Processing %s with %s files" % (ccd, len(darkFilenames)))
445 maskFiles = self.
_getMaskFiles(self.config.eotestOutputPath, ccd)
446 gains = butler.get(
'eotest_gain', dataId={self.config.ccdKey: ccd,
'run': run})
447 self.brightPixels.
run(sensor_id=ccd, dark_files=darkFilenames,
448 mask_files=maskFiles, gains=gains)
454 if self.config.doDarkPixels:
455 self.log.
info(
"Starting dark pixel task")
456 darkTaskDataId = {
'run': run,
'testType':
'SFLAT_500',
'imageType':
'FLAT'}
458 if 'SFLAT_500' not in testTypes:
459 msg =
"No superflats found. Available data: %s" % testTypes
460 if self.config.requireAllEOTests:
461 raise RuntimeError(msg)
463 self.log.
warn(msg +
"\nSkipping dark pixel task")
465 sflatFilenames = [butler.get(
'raw_filename', dataId={
'visit': visit,
466 self.config.ccdKey: ccd})[0][:-3]
467 for visit
in butler.queryMetadata(
'raw', [
'visit'],
468 dataId=darkTaskDataId)]
469 self.log.
trace(
"DarkTask: Processing %s with %s files" % (ccd, len(sflatFilenames)))
470 maskFiles = self.
_getMaskFiles(self.config.eotestOutputPath, ccd)
471 self.darkPixels.
run(sensor_id=ccd, sflat_files=sflatFilenames, mask_files=maskFiles)
477 if self.config.doTraps:
478 self.log.
info(
"Starting trap task")
479 trapTaskDataId = {
'run': run,
'testType':
'TRAP',
'imageType':
'PPUMP'}
481 if (
'TRAP' not in testTypes)
and (
'PPUMP' not in imTypes):
482 msg =
"No pocket pumping exposures found. Available data: %s" % testTypes
483 if self.config.requireAllEOTests:
484 raise RuntimeError(msg)
486 self.log.
warn(msg +
"\nSkipping trap task")
488 trapFilenames = [butler.get(
'raw_filename', dataId={
'visit': visit,
489 self.config.ccdKey: ccd})[0][:-3]
490 for visit
in butler.queryMetadata(
'raw', [
'visit'], dataId=trapTaskDataId)]
491 if len(trapFilenames) != 1:
492 msg =
"Trap Task: Found more than one ppump trap file: %s" % trapFilenames
493 msg +=
" Running using only the first one found." 495 self.log.
trace(
"Trap Task: Processing %s with %s files" % (ccd, len(trapFilenames)))
496 maskFiles = self.
_getMaskFiles(self.config.eotestOutputPath, ccd)
497 gains = butler.get(
'eotest_gain', dataId={self.config.ccdKey: ccd,
'run': run})
498 self.traps.
run(sensor_id=ccd, pocket_pumped_file=trapFilenames[0],
499 mask_files=maskFiles, gains=gains)
505 if self.config.doCTE:
506 self.log.
info(
"Starting CTE task")
507 cteTaskDataId = {
'run': run,
'testType':
'SFLAT_500',
'imageType':
'FLAT'}
509 if 'SFLAT_500' not in testTypes:
510 msg =
"No superflats found. Available data: %s" % testTypes
511 if self.config.requireAllEOTests:
512 raise RuntimeError(msg)
514 self.log.
warn(msg +
"\nSkipping CTE task")
516 sflatFilenames = [butler.get(
'raw_filename', dataId={
'visit': visit,
517 self.config.ccdKey: ccd})[0][:-3]
518 for visit
in butler.queryMetadata(
'raw', [
'visit'], dataId=cteTaskDataId)]
519 self.log.
trace(
"CTETask: Processing %s with %s files" % (ccd, len(sflatFilenames)))
520 maskFiles = self.
_getMaskFiles(self.config.eotestOutputPath, ccd)
521 self.cte.
run(sensor_id=ccd, superflat_files=sflatFilenames, mask_files=maskFiles)
527 if self.config.doFlatPair:
528 self.log.
info(
"Starting flatPair task")
529 flatPairDataId = {
'run': run,
'testType':
'FLAT',
'imageType':
'FLAT'}
531 if 'FLAT' not in testTypes:
532 msg =
"No dataset for flat_pairs found. Available data: %s" % testTypes
533 if self.config.requireAllEOTests:
534 raise RuntimeError(msg)
536 self.log.
warn(msg +
"\nSkipping flatPair task")
538 flatPairFilenames = [butler.get(
'raw_filename', dataId={
'visit': visit,
539 self.config.ccdKey: ccd})[0][:-3]
540 for visit
in butler.queryMetadata(
'raw', [
'visit'],
541 dataId=flatPairDataId)]
550 flatPairFilenames = [os.path.realpath(f)
for f
in flatPairFilenames
if 551 os.path.realpath(f).find(
'flat1') != -1
or 552 os.path.realpath(f).find(
'flat2') != -1]
553 if not flatPairFilenames:
554 raise RuntimeError(
"No flatPair files found.")
555 self.log.
trace(
"FlatPairTask: Processing %s with %s files" % (ccd, len(flatPairFilenames)))
556 maskFiles = self.
_getMaskFiles(self.config.eotestOutputPath, ccd)
557 gains = butler.get(
'eotest_gain', dataId={self.config.ccdKey: ccd,
'run': run})
558 self.flatPair.
run(sensor_id=ccd, infiles=flatPairFilenames, mask_files=maskFiles,
559 gains=gains, max_pd_frac_dev=self.config.flatPairMaxPdFracDev)
565 if self.config.doPTC:
566 self.log.
info(
"Starting PTC task")
567 ptcDataId = {
'run': run,
'testType':
'FLAT',
'imageType':
'FLAT'}
569 if 'FLAT' not in testTypes:
570 msg =
"No dataset for flat_pairs found. Available data: %s" % testTypes
571 if self.config.requireAllEOTests:
572 raise RuntimeError(msg)
574 self.log.
warn(msg +
"\nSkipping PTC task")
576 ptcFilenames = [butler.get(
'raw_filename', dataId={
'visit': visit,
577 self.config.ccdKey: ccd})[0][:-3]
578 for visit
in butler.queryMetadata(
'raw', [
'visit'], dataId=ptcDataId)]
587 ptcFilenames = [os.path.realpath(f)
for f
in ptcFilenames
if 588 os.path.realpath(f).find(
'flat1') != -1
or 589 os.path.realpath(f).find(
'flat2') != -1]
591 raise RuntimeError(
"No flatPair files found")
592 self.log.
trace(
"PTCTask: Processing %s with %s files" % (ccd, len(ptcFilenames)))
593 maskFiles = self.
_getMaskFiles(self.config.eotestOutputPath, ccd)
594 gains = butler.get(
'eotest_gain', dataId={self.config.ccdKey: ccd,
'run': run})
595 self.ptc.
run(sensor_id=ccd, infiles=ptcFilenames, mask_files=maskFiles, gains=gains)
599 self.log.
info(
"Finished running EOTest")
def __init__(self, args, kwargs)
def runEotestDirect(self, butler, run=None)
def _getMaskFiles(self, path, ccd)
def _cleanupEotest(self, path)
def makeEotestReport(self, butler)