Coverage for soxspipe/recipes/_base_recipe_.py: 12%

# Coverage for soxspipe/recipes/_base_recipe_.py : 12%

## 261 statements 31 run 230 missing 0 excluded

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1#!/usr/bin/env python

2# encoding: utf-8

3"""

4*The base recipe class which all other recipes inherit*

5

6:Author:

7 David Young & Marco Landoni

8

9:Date Created:

10 January 22, 2020

11"""

12################# GLOBAL IMPORTS ####################

13 from builtins import object

14 import sys

15 import os

16 os.environ['TERM'] = 'vt100'

17 from fundamentals import tools

18 import numpy as np

19 from astropy.nddata import CCDData

20 from astropy import units as u

21 from astropy.stats import mad_std

22 import ccdproc

23 from astropy.nddata.nduncertainty import StdDevUncertainty

24 from ccdproc import Combiner

25 from soxspipe.commonutils import set_of_files

26 from soxspipe.commonutils import keyword_lookup

27 from soxspipe.commonutils import detector_lookup

28 from datetime import datetime

29 from soxspipe.commonutils import filenamer

30 import shutil

31

32

33 class _base_recipe_(object):

34 """

35 The base recipe class which all other recipes inherit

36

37 **Key Arguments:**

38 - ``log`` -- logger

39 - ``settings`` -- the settings dictionary

40

41 **Usage**

42

43 To use this base recipe to create a new `soxspipe` recipe, have a look at the code for one of the simpler recipes (e.g. `soxs_mbias`) - copy and modify the code.

44 """

45

46 def __init__(

47 self,

48 log,

49 settings=False

50 ):

51 self.log = log

52 log.debug("instansiating a new '__init__' object")

53 self.settings = settings

54 self.intermediateRootPath = self._absolute_path(

55 settings["intermediate-data-root"])

56 self.reducedRootPath = self._absolute_path(

57 settings["reduced-data-root"])

58 self.calibrationRootPath = self._absolute_path(

59 settings["calibration-data-root"])

60

61 # SET LATER WHEN VERIFYING FRAMES

62 self.arm = None

63 self.detectorParams = None

64

65 # KEYWORD LOOKUP OBJECT - LOOKUP KEYWORD FROM DICTIONARY IN RESOURCES

66 # FOLDER

67 self.kw = keyword_lookup(

68 log=self.log,

69 settings=self.settings

70 ).get

71

72 return None

73

74 def _prepare_single_frame(

75 self,

76 frame,

77 save=False):

78 """*prepare a single raw frame by converting pixel data from ADU to electrons and adding mask and uncertainty extensions*

79

80 **Key Arguments:**

81 - ``frame`` -- the path to the frame to prepare, of a CCDData object

82

83 **Return:**

84 - ``frame`` -- the prepared frame with mask and uncertainty extensions (CCDData object)

85

86 ```eval_rst

87 .. todo::

88

89 - write a command-line tool for this method

90 ```

91 """

92 self.log.debug('starting the ``_prepare_single_frame`` method')

93

94 kw = self.kw

95 dp = self.detectorParams

96

97 # STORE FILEPATH FOR LATER USE

98 filepath = frame

99

100 # CONVERT FILEPATH TO CCDDATA OBJECT

101 if isinstance(frame, str):

102 # CONVERT RELATIVE TO ABSOLUTE PATHS

103 frame = self._absolute_path(frame)

104 # OPEN THE RAW FRAME - MASK AND UNCERT TO BE POPULATED LATER

105 frame = CCDData.read(frame, hdu=0, unit=u.adu, hdu_uncertainty='ERRS',

106 hdu_mask='QUAL', hdu_flags='FLAGS', key_uncertainty_type='UTYPE')

107

108 # CHECK THE NUMBER OF EXTENSIONS IS ONLY 1 AND "SXSPRE" DOES NOT

109 # EXIST. i.e. THIS IS A RAW UNTOUCHED FRAME

110 if len(frame.to_hdu()) > 1 or "SXSPRE" in frame.header:

111 return filepath

112

113 # MANIPULATE XSH DATA

114 frame = self.xsh2soxs(frame)

115 frame = self._trim_frame(frame)

116

117 # CORRECT FOR GAIN - CONVERT DATA FROM ADU TO ELECTRONS

118 frame = ccdproc.gain_correct(frame, dp["gain"])

119

120 # GENERATE UNCERTAINTY MAP AS EXTENSION

121 if frame.header[kw("DPR_TYPE")] == "BIAS":

122 # ERROR IS ONLY FROM READNOISE FOR BIAS FRAMES

123 errorMap = np.ones_like(frame.data) * dp["ron"]

124 # errorMap = StdDevUncertainty(errorMap)

125 frame.uncertainty = errorMap

126 else:

127 # GENERATE UNCERTAINTY MAP AS EXTENSION

128 frame = ccdproc.create_deviation(

129 frame, readnoise=dp["ron"])

130

131 # FIND THE APPROPRIATE BAD-PIXEL BITMAP AND APPEND AS 'FLAG' EXTENSION

132 # NOTE FLAGS NOTE YET SUPPORTED BY CCDPROC THIS THIS WON'T GET SAVED OUT

133 # AS AN EXTENSION

134 arm = self.arm

135 if kw('WIN_BINX') in frame.header:

136 binx = int(frame.header[kw('WIN_BINX')])

137 biny = int(frame.header[kw('WIN_BINY')])

138 else:

139 binx = 1

140 biny = 1

141

142 bitMapPath = self.calibrationRootPath + "/" + dp["bad-pixel map"][f"{binx}x{biny}"]

143

144 if not os.path.exists(bitMapPath):

145 message = "the path to the bitMapPath %s does not exist on this machine" % (

146 bitMapPath,)

147 self.log.critical(message)

148 raise IOError(message)

149 bitMap = CCDData.read(bitMapPath, hdu=0, unit=u.dimensionless_unscaled)

150

151 # BIAS FRAMES HAVE NO 'FLUX', JUST READNOISE, SO ADD AN EMPTY BAD-PIXEL

152 # MAP

153 if frame.header[kw("DPR_TYPE")] == "BIAS":

154 bitMap.data = np.zeros_like(bitMap.data)

155

156 # print(bitMap.data.shape)

157 # print(frame.data.shape)

158

159 frame.flags = bitMap.data

160

161 # FLATTEN BAD-PIXEL BITMAP TO BOOLEAN FALSE (GOOD) OR TRUE (BAD) AND

162 # APPEND AS 'UNCERT' EXTENSION

163 boolMask = bitMap.data.astype(bool).data

164 try:

165 # FAILS IN PYTHON 2.7 AS BOOLMASK IS A BUFFER - NEED TO CONVERT TO

166 # 2D ARRAY

167 boolMask.shape

168

169 except:

170 arr = np.frombuffer(boolMask, dtype=np.uint8)

171 arr.shape = (frame.data.shape)

172 boolMask = arr

173

174 frame.mask = boolMask

175

176 if save:

177 outDir = self.intermediateRootPath

178 else:

179 outDir = self.intermediateRootPath + "/tmp"

180

181 # INJECT THE PRE KEYWORD

182 utcnow = datetime.utcnow()

183 frame.header["SXSPRE"] = (utcnow.strftime(

184 "%Y-%m-%dT%H:%M:%S.%f"), "UTC timestamp")

185

186 # RECURSIVELY CREATE MISSING DIRECTORIES

187 if not os.path.exists(outDir):

188 os.makedirs(outDir)

189 # CONVERT CCDData TO FITS HDU (INCLUDING HEADER) AND SAVE WITH PRE TAG

190 # PREPENDED TO FILENAME

191 basename = os.path.basename(filepath)

192 filenameNoExtension = os.path.splitext(basename)[0]

193 extension = os.path.splitext(basename)[1]

194 filePath = outDir + "/" + \

195 filenameNoExtension + "_pre" + extension

196

197 # SAVE TO DISK

198 self._write(

199 frame=frame,

200 filedir=outDir,

201 filename=filenameNoExtension + "_pre" + extension,

202 overwrite=True

203 )

204

205 self.log.debug('completed the ``_prepare_single_frame`` method')

206 return filePath

207

208 def _absolute_path(

209 self,

210 path):

211 """*convert paths from home directories to absolute paths*

212

213 **Key Arguments:**

214 - ``path`` -- path possibly relative to home directory

215

216 **Return:**

217 - ``absolutePath`` -- absolute path

218

219 **Usage**

220

221 ```python

222 myPath = self._absolute_path(myPath)

223 ```

224 """

225 self.log.debug('starting the ``_absolute_path`` method')

226

227 from os.path import expanduser

228 home = expanduser("~")

229 if path[0] == "~":

230 path = home + "/" + path[1:]

231

232 self.log.debug('completed the ``_absolute_path`` method')

233 return path.replace("//", "/")

234

235 def prepare_frames(

236 self,

237 save=False):

238 """*prepare raw frames by converting pixel data from ADU to electrons and adding mask and uncertainty extensions*

239

240 **Key Arguments:**

241 - ``save`` -- save out the prepared frame to the intermediate products directory. Default False.

242

243 **Return:**

244 - ``preframes`` -- the new image collection containing the prepared frames

245

246 **Usage**

247

248 Usually called within a recipe class once the input frames have been selected and verified (see `soxs_mbias` code for example):

249

250 ```python

251 self.inputFrames = self.prepare_frames(

252 save=self.settings["save-intermediate-products"])

253 ```

254 """

255 self.log.debug('starting the ``prepare_frames`` method')

256

257 kw = self.kw

258

259 filepaths = self.inputFrames.files_filtered(include_path=True)

260

261 frameCount = len(filepaths)

262 print("# PREPARING %(frameCount)s RAW FRAMES - TRIMMING OVERSCAN, CONVERTING TO ELECTRON COUNTS, GENERATING UNCERTAINTY MAPS AND APPENDING DEFAULT BAD-PIXEL MASK" % locals())

263 preframes = []

264 preframes[:] = [self._prepare_single_frame(

265 frame=frame, save=save) for frame in filepaths]

266 sof = set_of_files(

267 log=self.log,

268 settings=self.settings,

269 inputFrames=preframes

270 )

271 preframes, supplementaryInput = sof.get()

272 preframes.sort([kw('MJDOBS').lower()])

273

274 print("# PREPARED FRAMES - SUMMARY")

275 print(preframes.summary)

276

277 self.log.debug('completed the ``prepare_frames`` method')

278 return preframes

279

280 def _verify_input_frames_basics(

281 self):

282 """*the basic verifications that needs done for all recipes*

283

284 **Return:**

285 - None

286

287 If the fits files conform to required input for the recipe everything will pass silently, otherwise an exception shall be raised.

288 """

289 self.log.debug('starting the ``_verify_input_frames_basics`` method')

290

291 kw = self.kw

292

293 # CHECK WE ACTUALLY HAVE IMAGES

294 if not len(self.inputFrames.files_filtered(include_path=True)):

295 raise FileNotFoundError(

296 "No image frames where passed to the recipe")

297

298 arm = self.inputFrames.values(

299 keyword=kw("SEQ_ARM").lower(), unique=True)

300 # MIXED INPUT ARMS ARE BAD

301 if len(arm) > 1:

302 arms = " and ".join(arms)

303 print(self.inputFrames.summary)

304 raise TypeError(

305 "Input frames are a mix of %(imageTypes)s" % locals())

306 else:

307 self.arm = arm[0]

308

309 # CREATE DETECTOR LOOKUP DICTIONARY - SOME VALUES CAN BE OVERWRITTEN

310 # WITH WHAT IS FOUND HERE IN FITS HEADERS

311 self.detectorParams = detector_lookup(

312 log=self.log,

313 settings=self.settings

314 ).get(self.arm)

315

316 # MIXED BINNING IS BAD

317 cdelt1 = self.inputFrames.values(

318 keyword=kw("CDELT1").lower(), unique=True)

319 cdelt2 = self.inputFrames.values(

320 keyword=kw("CDELT2").lower(), unique=True)

321

322 if len(cdelt1) > 1 or len(cdelt2) > 1:

323 print(self.inputFrames.summary)

324 raise TypeError(

325 "Input frames are a mix of binnings" % locals())

326

327 if cdelt1[0] and cdelt2[0]:

328 self.detectorParams["binning"] = [int(cdelt2[0]), int(cdelt1[0])]

329

330 # MIXED READOUT SPEEDS IS BAD

331 readSpeed = self.inputFrames.values(

332 keyword=kw("DET_READ_SPEED").lower(), unique=True)

333 if len(readSpeed) > 1:

334 print(self.inputFrames.summary)

335 raise TypeError(

336 "Input frames are a mix of readout speeds" % locals())

337

338 # MIXED GAIN SPEEDS IS BAD

339 # HIERARCH ESO DET OUT1 CONAD - Electrons/ADU

340 # CONAD IS REALLY GAIN AND HAS UNIT OF Electrons/ADU

341 gain = self.inputFrames.values(

342 keyword=kw("CONAD").lower(), unique=True)

343 if len(gain) > 1:

344 print(self.inputFrames.summary)

345 raise TypeError(

346 "Input frames are a mix of gain" % locals())

347 if gain[0]:

348 # UVB & VIS

349 self.detectorParams["gain"] = gain[0] * u.electron / u.adu

350 else:

351 # NIR

352 self.detectorParams["gain"] = self.detectorParams[

353 "gain"] * u.electron / u.adu

354

355 # HIERARCH ESO DET OUT1 RON - Readout noise in electrons

356 ron = self.inputFrames.values(

357 keyword=kw("RON").lower(), unique=True)

358

359 # MIXED NOISE

360 if len(ron) > 1:

361 print(self.inputFrames.summary)

362 raise TypeError("Input frames are a mix of readnoise" % locals())

363 if ron[0]:

364 # UVB & VIS

365 self.detectorParams["ron"] = ron[0] * u.electron

366 else:

367 # NIR

368 self.detectorParams["ron"] = self.detectorParams[

369 "ron"] * u.electron

370

371 self.log.debug('completed the ``_verify_input_frames_basics`` method')

372 return None

373

374 def clean_up(

375 self):

376 """*remove intermediate files once recipe is complete*

377

378 **Usage**

379

380 ```python

381 recipe.clean_up()

382 ```

383 """

384 self.log.debug('starting the ``clean_up`` method')

385

386 outDir = self.intermediateRootPath + "/tmp"

387

388 try:

389 shutil.rmtree(outDir)

390 except:

391 pass

392

393 self.log.debug('completed the ``clean_up`` method')

394 return None

395

396 def xsh2soxs(

397 self,

398 frame):

399 """*perform some massaging of the xshooter data so it more closely resembles soxs data - this function can be removed once code is production ready*

400

401 **Key Arguments:**

402 - ``frame`` -- the CCDDate frame to manipulate

403

404 **Return:**

405 - ``frame`` -- the manipulated soxspipe-ready frame

406

407 **Usage:**

408

409 ```python

410 frame = self.xsh2soxs(frame)

411 ```

412 """

413 self.log.debug('starting the ``xsh2soxs`` method')

414

415 kw = self.kw

416 dp = self.detectorParams

417

418 # NP ROTATION OF ARRAYS IS IN COUNTER-CLOCKWISE DIRECTION

419 rotationIndex = int(dp["clockwise-rotation"] / 90.)

420

421 if self.settings["instrument"] == "xsh" and rotationIndex > 0:

422 frame.data = np.rot90(frame.data, rotationIndex)

423

424 self.log.debug('completed the ``xsh2soxs`` method')

425 return frame

426

427 def _trim_frame(

428 self,

429 frame):

430 """*return frame with pre-scan and overscan regions removed*

431

432 **Key Arguments:**

433 - ``frame`` -- the CCDData frame to be trimmed

434 """

435 self.log.debug('starting the ``_trim_frame`` method')

436

437 kw = self.kw

438 arm = self.arm

439 dp = self.detectorParams

440

441 rs, re, cs, ce = dp["science-pixels"]["rows"]["start"], dp["science-pixels"]["rows"][

442 "end"], dp["science-pixels"]["columns"]["start"], dp["science-pixels"]["columns"]["end"]

443

444 binning = dp["binning"]

445 if binning[0] > 1:

446 rs = int(rs / binning[0])

447 re = int(re / binning[0])

448 if binning[1] > 1:

449 cs = int(cs / binning[0])

450 ce = int(ce / binning[0])

451

452 trimmed_frame = ccdproc.trim_image(frame[rs: re, cs: ce])

453

454 self.log.debug('completed the ``_trim_frame`` method')

455 return trimmed_frame

456

457 def _write(

458 self,

459 frame,

460 filedir,

461 filename=False,

462 overwrite=True):

463 """*write frame to disk at the specified location*

464

465 **Key Arguments:**

466 - ``frame`` -- the frame to save to disk (CCDData object)

467 - ``filedir`` -- the location to save the frame

468 - ``filename`` -- the filename to save the file as. Default: **False** (standardised filename generated in code)

469 - ``overwrite`` -- if a file exists at the filepath then choose to overwrite the file. Default: True

470

471 **Usage:**

472

473 Use within a recipe like so:

474

475 ```python

476 self._write(frame, filePath)

477 ```

478 """

479 self.log.debug('starting the ``write`` method')

480

481 if not filename:

482

483 filename = filenamer(

484 log=self.log,

485 frame=frame,

486 settings=self.settings

487 )

488

489 filepath = filedir + "/" + filename

490

491 HDUList = frame.to_hdu(

492 hdu_mask='QUAL', hdu_uncertainty='ERRS', hdu_flags=None)

493 HDUList[0].name = "FLUX"

494 HDUList.writeto(filepath, output_verify='exception',

495 overwrite=overwrite, checksum=True)

496

497 self.log.debug('completed the ``write`` method')

498 return filepath

499

500 def clip_and_stack(

501 self,

502 frames,

503 recipe):

504 """*mean combine input frames after sigma-clipping outlying pixels using a median value with median absolute deviation (mad) as the deviation function*

505

506 **Key Arguments:**

507 - ``frames`` -- an ImageFileCollection of the framers to stack

508 - ``recipe`` -- the name of recipe needed to read the correct settings from the yaml files

509

510 **Return:**

511 - ``combined_frame`` -- the combined master frame (with updated bad-pixel and uncertainty maps)

512

513 **Usage:**

514

515 This snippet can be used within the recipe code to combine individual (using bias frames as an example):

516

517 ```python

518 combined_bias_mean = self.clip_and_stack(

519 frames=self.inputFrames, recipe="soxs_mbias")

520 ```

521

522 ---

523

524 ```eval_rst

525 .. todo::

526

527 - revisit error propagation when combining frames: https://github.com/thespacedoctor/soxspipe/issues/42

528 ```

529 """

530 self.log.debug('starting the ``clip_and_stack`` method')

531

532 arm = self.arm

533 kw = self.kw

534 dp = self.detectorParams

535 imageType = self.imageType

536

537 # ALLOW FOR UNDERSCORE AND HYPHENS

538 recipe = recipe.replace("soxs_", "soxs-")

539

540 # UNPACK SETTINGS

541 clipping_lower_sigma = self.settings[

542 recipe]["clipping-lower-simga"]

543 clipping_upper_sigma = self.settings[

544 recipe]["clipping-upper-simga"]

545 clipping_iteration_count = self.settings[

546 recipe]["clipping-iteration-count"]

547

548 # LIST OF CCDDATA OBJECTS NEEDED BY COMBINER OBJECT

549 # ccds = [c for c in self.inputFrames.ccds()]

550 ccds = [c for c in self.inputFrames.ccds(ccd_kwargs={"hdu_uncertainty": 'ERRS',

551 "hdu_mask": 'QUAL', "hdu_flags": 'FLAGS', "key_uncertainty_type": 'UTYPE'})]

552

553 # COMBINER OBJECT WILL FIRST GENERATE MASKS FOR INDIVIDUAL IMAGES VIA

554 # CLIPPING AND THEN COMBINE THE IMAGES WITH THE METHOD SELECTED. PIXEL

555 # MASKED IN ALL INDIVIDUAL IMAGES ARE MASK IN THE FINAL COMBINED IMAGE

556 combiner = Combiner(ccds)

557

558 print(f"\n# SIGMA-CLIPPING PIXEL WITH OUTLYING VALUES IN INDIVIDUAL {imageType} FRAMES")

559 # PRINT SOME INFO FOR USER

560 badCount = ccds[0].mask.sum()

561 totalPixels = np.size(ccds[0].mask)

562 percent = (float(badCount) / float(totalPixels)) * 100.

563 print(f"The basic bad-pixel mask for the {arm} detector {imageType} frames contains {badCount} pixels ({percent:0.2}% of all pixels)")

564

565 # GENERATE A MASK FOR EACH OF THE INDIVIDUAL INOUT FRAMES - USING

566 # MEDIAN WITH MEDIAN ABSOLUTE DEVIATION (MAD) AS THE DEVIATION FUNCTION

567 old_n_masked = -1

568 # THIS IS THE SUM OF BAD-PIXELS IN ALL INDIVIDUAL FRAME MASKS

569 new_n_masked = combiner.data_arr.mask.sum()

570 iteration = 1

571 while (new_n_masked > old_n_masked and iteration <= clipping_iteration_count):

572 combiner.sigma_clipping(

573 low_thresh=clipping_lower_sigma, high_thresh=clipping_upper_sigma, func=np.ma.median, dev_func=mad_std)

574 old_n_masked = new_n_masked

575 # RECOUNT BAD-PIXELS NOW CLIPPING HAS RUN

576 new_n_masked = combiner.data_arr.mask.sum()

577 diff = new_n_masked - old_n_masked

578 extra = ""

579 if diff == 0:

580 extra = " - we're done"

581 print(" Clipping iteration %(iteration)s finds %(diff)s more rogue pixels in the set of input frames%(extra)s" % locals())

582 iteration += 1

583

584 # GENERATE THE COMBINED MEDIAN

585 print("\n# MEAN COMBINING FRAMES - WITH UPDATED BAD-PIXEL MASKS")

586 combined_frame = combiner.average_combine()

587

588 # MASSIVE FUDGE - NEED TO CORRECTLY WRITE THE HEADER FOR COMBINED

589 # IMAGES

590

591 combined_frame.header = ccds[0].header

592 try:

593 combined_frame.wcs = ccds[0].wcs

594 except:

595 pass

596 combined_frame.header[

597 kw("DPR_CATG")] = "MASTER_%(imageType)s_%(arm)s" % locals()

598

599 # CALCULATE NEW PIXELS ADDED TO MASK

600 newBadCount = combined_frame.mask.sum()

601 diff = newBadCount - badCount

602 print("%(diff)s new pixels made it into the combined bad-pixel map" % locals())

603

604 self.log.debug('completed the ``clip_and_stack`` method')

605 return combined_frame

606

607 def subtract_calibrations(

608 self,

609 inputFrame,

610 master_bias=False,

611 dark=False):

612 """*subtract calibration frames from an input frame*

613

614 **Key Arguments:**

615 - ``inputFrame`` -- the input frame to have calibrations subtracted. CCDData object.

616 - ``master_bias`` -- the master bias frame to be subtracted. CCDData object. Default *False*.

617 - ``dark`` -- a dark frame to be subtracted. CCDData object. Default *False*.

618

619 **Return:**

620 - ``calibration_subtracted_frame`` -- the input frame with the calibration frame(s) subtracted. CCDData object.

621

622 **Usage:**

623

624 Within a soxspipe recipe use `subtract_calibrations` like so:

625

626 ```python

627 myCalibratedFrame = self.subtract_calibrations(

628 inputFrame=inputFrameCCDObject, master_bias=masterBiasCCDObject, dark=darkCCDObject)

629 ```

630

631 ---

632

633 ```eval_rst

634 .. todo::

635

636 - code needs written to scale dark frame to exposure time of science/calibration frame

637 ```

638 """

639 self.log.debug('starting the ``subtract_calibrations`` method')

640

641 arm = self.arm

642 kw = self.kw

643 dp = self.detectorParams

644

645 # VERIFY DATA IS IN ORDER

646 if master_bias == False and dark == False:

647 raise TypeError(

648 "subtract_calibrations method needs a master-bias frame and/or a dark frame to subtract")

649 if master_bias == False and dark.header[kw("EXPTIME")] != inputFrame.header[kw("EXPTIME")]:

650 raise AttributeError(

651 "Dark and science/calibration frame have differing exposure-times. A master-bias frame needs to be supplied to scale the dark frame to same exposure time as input science/calibration frame")

652 if master_bias != False and dark != False and dark.header[kw("EXPTIME")] != inputFrame.header[kw("EXPTIME")]:

653 raise AttributeError(

654 "CODE NEEDS WRITTEN HERE TO SCALE DARK FRAME TO EXPOSURE TIME OF SCIENCE/CALIBRATION FRAME")

655

656 # DARK WITH MATCHING EXPOSURE TIME

657 if dark != False and dark.header[kw("EXPTIME")] == inputFrame.header[kw("EXPTIME")]:

658 calibration_subtracted_frame = inputFrame.subtract(dark)

659 calibration_subtracted_frame.header = inputFrame.header

660 try:

661 calibration_subtracted_frame.wcs = inputFrame.wcs

662 except:

663 pass

664

665 # ONLY A MASTER BIAS FRAME, NO DARK

666 if dark == False and master_bias != False:

667 calibration_subtracted_frame = inputFrame.subtract(master_bias)

668 calibration_subtracted_frame.header = inputFrame.header

669 try:

670 calibration_subtracted_frame.wcs = inputFrame.wcs

671 except:

672 pass

673

674 self.log.debug('completed the ``subtract_calibrations`` method')

675 return calibration_subtracted_frame

676

677 # use the tab-trigger below for new method

678 # xt-class-method