Coverage for python/lsst/pipe/tasks/photoCal.py: 11%

266 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-07 03:43 -0700

1# This file is part of pipe_tasks. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

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

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22__all__ = ["PhotoCalTask", "PhotoCalConfig"] 

23 

24import math 

25import sys 

26 

27import numpy as np 

28import astropy.units as u 

29 

30import lsst.pex.config as pexConf 

31import lsst.pipe.base as pipeBase 

32from lsst.afw.image import abMagErrFromFluxErr, makePhotoCalibFromCalibZeroPoint 

33import lsst.afw.table as afwTable 

34from lsst.meas.astrom import DirectMatchTask, DirectMatchConfigWithoutLoader 

35import lsst.afw.display as afwDisplay 

36from lsst.meas.algorithms import getRefFluxField, ReserveSourcesTask 

37from lsst.utils.timer import timeMethod 

38from .colorterms import ColortermLibrary 

39 

40 

41class PhotoCalConfig(pexConf.Config): 

42 """Config for PhotoCal.""" 

43 

44 match = pexConf.ConfigField("Match to reference catalog", 

45 DirectMatchConfigWithoutLoader) 

46 reserve = pexConf.ConfigurableField(target=ReserveSourcesTask, doc="Reserve sources from fitting") 

47 fluxField = pexConf.Field( 

48 dtype=str, 

49 default="slot_CalibFlux_instFlux", 

50 doc=("Name of the source instFlux field to use. The associated flag field\n" 

51 "('<name>_flags') will be implicitly included in badFlags."), 

52 ) 

53 applyColorTerms = pexConf.Field( 

54 dtype=bool, 

55 default=None, 

56 doc=("Apply photometric color terms to reference stars? One of:\n" 

57 "None: apply if colorterms and photoCatName are not None;\n" 

58 " fail if color term data is not available for the specified ref catalog and filter.\n" 

59 "True: always apply colorterms; fail if color term data is not available for the\n" 

60 " specified reference catalog and filter.\n" 

61 "False: do not apply."), 

62 optional=True, 

63 ) 

64 sigmaMax = pexConf.Field( 

65 dtype=float, 

66 default=0.25, 

67 doc="maximum sigma to use when clipping", 

68 optional=True, 

69 ) 

70 nSigma = pexConf.Field( 

71 dtype=float, 

72 default=3.0, 

73 doc="clip at nSigma", 

74 ) 

75 useMedian = pexConf.Field( 

76 dtype=bool, 

77 default=True, 

78 doc="use median instead of mean to compute zeropoint", 

79 ) 

80 nIter = pexConf.Field( 

81 dtype=int, 

82 default=20, 

83 doc="number of iterations", 

84 ) 

85 colorterms = pexConf.ConfigField( 

86 dtype=ColortermLibrary, 

87 doc="Library of photometric reference catalog name: color term dict", 

88 ) 

89 photoCatName = pexConf.Field( 

90 dtype=str, 

91 optional=True, 

92 doc=("Name of photometric reference catalog; used to select a color term dict in colorterms." 

93 " see also applyColorTerms"), 

94 ) 

95 magErrFloor = pexConf.RangeField( 

96 dtype=float, 

97 default=0.0, 

98 doc="Additional magnitude uncertainty to be added in quadrature with measurement errors.", 

99 min=0.0, 

100 ) 

101 

102 def validate(self): 

103 pexConf.Config.validate(self) 

104 if self.applyColorTerms and self.photoCatName is None: 

105 raise RuntimeError("applyColorTerms=True requires photoCatName is non-None") 

106 if self.applyColorTerms and len(self.colorterms.data) == 0: 

107 raise RuntimeError("applyColorTerms=True requires colorterms be provided") 

108 

109 def setDefaults(self): 

110 pexConf.Config.setDefaults(self) 

111 self.match.sourceSelection.doFlags = True 

112 self.match.sourceSelection.flags.bad = [ 

113 "base_PixelFlags_flag_edge", 

114 "base_PixelFlags_flag_interpolated", 

115 "base_PixelFlags_flag_saturated", 

116 ] 

117 self.match.sourceSelection.doUnresolved = True 

118 

119 

120class PhotoCalTask(pipeBase.Task): 

121 """Calculate an Exposure's zero-point given a set of flux measurements 

122 of stars matched to an input catalogue. 

123 

124 Parameters 

125 ---------- 

126 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader` 

127 An instance of LoadReferenceObjectsTasks that supplies an external reference 

128 catalog. 

129 schema : `lsst.afw.table.Schema`, optional 

130 The schema of the detection catalogs used as input to this task. 

131 **kwds 

132 Additional keyword arguments. 

133 

134 Notes 

135 ----- 

136 The type of flux to use is specified by PhotoCalConfig.fluxField. 

137 

138 The algorithm clips outliers iteratively, with parameters set in the configuration. 

139 

140 This task can adds fields to the schema, so any code calling this task must ensure that 

141 these columns are indeed present in the input match list; see `pipe_tasks_photocal_Example`. 

142 

143 Debugging: 

144 

145 The available `~lsst.base.lsstDebug` variables in PhotoCalTask are: 

146 

147 display : 

148 If True enable other debug outputs. 

149 displaySources : 

150 If True, display the exposure on ds9's frame 1 and overlay the source catalogue. 

151 

152 red o : 

153 Reserved objects. 

154 green o : 

155 Objects used in the photometric calibration. 

156 

157 scatterPlot : 

158 Make a scatter plot of flux v. reference magnitude as a function of reference magnitude: 

159 

160 - good objects in blue 

161 - rejected objects in red 

162 

163 (if scatterPlot is 2 or more, prompt to continue after each iteration) 

164 """ 

165 

166 ConfigClass = PhotoCalConfig 

167 _DefaultName = "photoCal" 

168 

169 def __init__(self, refObjLoader, schema=None, **kwds): 

170 pipeBase.Task.__init__(self, **kwds) 

171 self.scatterPlot = None 

172 self.fig = None 

173 if schema is not None: 

174 self.usedKey = schema.addField("calib_photometry_used", type="Flag", 

175 doc="set if source was used in photometric calibration") 

176 else: 

177 self.usedKey = None 

178 self.match = DirectMatchTask(config=self.config.match, refObjLoader=refObjLoader, 

179 name="match", parentTask=self) 

180 self.makeSubtask("reserve", columnName="calib_photometry", schema=schema, 

181 doc="set if source was reserved from photometric calibration") 

182 

183 def getSourceKeys(self, schema): 

184 """Return a struct containing the source catalog keys for fields used 

185 by PhotoCalTask. 

186 

187 Parameters 

188 ---------- 

189 schema : `lsst.afw.table.schema` 

190 Schema of the catalog to get keys from. 

191 

192 Returns 

193 ------- 

194 result : `lsst.pipe.base.Struct` 

195 Results as a struct with attributes: 

196 

197 ``instFlux`` 

198 Instrument flux key. 

199 ``instFluxErr`` 

200 Instrument flux error key. 

201 """ 

202 instFlux = schema.find(self.config.fluxField).key 

203 instFluxErr = schema.find(self.config.fluxField + "Err").key 

204 return pipeBase.Struct(instFlux=instFlux, instFluxErr=instFluxErr) 

205 

206 @timeMethod 

207 def extractMagArrays(self, matches, filterLabel, sourceKeys): 

208 """Extract magnitude and magnitude error arrays from the given matches. 

209 

210 Parameters 

211 ---------- 

212 matches : `lsst.afw.table.ReferenceMatchVector` 

213 Reference/source matches. 

214 filterLabel : `str` 

215 Label of filter being calibrated. 

216 sourceKeys : `lsst.pipe.base.Struct` 

217 Struct of source catalog keys, as returned by getSourceKeys(). 

218 

219 Returns 

220 ------- 

221 result : `lsst.pipe.base.Struct` 

222 Results as a struct with attributes: 

223 

224 ``srcMag`` 

225 Source magnitude (`np.array`). 

226 ``refMag`` 

227 Reference magnitude (`np.array`). 

228 ``srcMagErr`` 

229 Source magnitude error (`np.array`). 

230 ``refMagErr`` 

231 Reference magnitude error (`np.array`). 

232 ``magErr`` 

233 An error in the magnitude; the error in ``srcMag`` - ``refMag``. 

234 If nonzero, ``config.magErrFloor`` will be added to ``magErr`` only 

235 (not ``srcMagErr`` or ``refMagErr``), as 

236 ``magErr`` is what is later used to determine the zero point (`np.array`). 

237 ``refFluxFieldList`` 

238 A list of field names of the reference catalog used for fluxes (1 or 2 strings) (`list`). 

239 """ 

240 srcInstFluxArr = np.array([m.second.get(sourceKeys.instFlux) for m in matches]) 

241 srcInstFluxErrArr = np.array([m.second.get(sourceKeys.instFluxErr) for m in matches]) 

242 if not np.all(np.isfinite(srcInstFluxErrArr)): 

243 # this is an unpleasant hack; see DM-2308 requesting a better solution 

244 self.log.warning("Source catalog does not have flux uncertainties; using sqrt(flux).") 

245 srcInstFluxErrArr = np.sqrt(srcInstFluxArr) 

246 

247 # convert source instFlux from DN to an estimate of nJy 

248 referenceFlux = (0*u.ABmag).to_value(u.nJy) 

249 srcInstFluxArr = srcInstFluxArr * referenceFlux 

250 srcInstFluxErrArr = srcInstFluxErrArr * referenceFlux 

251 

252 if not matches: 

253 raise RuntimeError("No reference stars are available") 

254 refSchema = matches[0].first.schema 

255 

256 applyColorTerms = self.config.applyColorTerms 

257 applyCTReason = "config.applyColorTerms is %s" % (self.config.applyColorTerms,) 

258 if self.config.applyColorTerms is None: 

259 # apply color terms if color term data is available and photoCatName specified 

260 ctDataAvail = len(self.config.colorterms.data) > 0 

261 photoCatSpecified = self.config.photoCatName is not None 

262 applyCTReason += " and data %s available" % ("is" if ctDataAvail else "is not") 

263 applyCTReason += " and photoRefCat %s provided" % ("is" if photoCatSpecified else "is not") 

264 applyColorTerms = ctDataAvail and photoCatSpecified 

265 

266 if applyColorTerms: 

267 self.log.info("Applying color terms for filter=%r, config.photoCatName=%s because %s", 

268 filterLabel.physicalLabel, self.config.photoCatName, applyCTReason) 

269 colorterm = self.config.colorterms.getColorterm(filterLabel.physicalLabel, 

270 self.config.photoCatName, 

271 doRaise=True) 

272 refCat = afwTable.SimpleCatalog(matches[0].first.schema) 

273 

274 # extract the matched refCat as a Catalog for the colorterm code 

275 refCat.reserve(len(matches)) 

276 for x in matches: 

277 record = refCat.addNew() 

278 record.assign(x.first) 

279 

280 refMagArr, refMagErrArr = colorterm.getCorrectedMagnitudes(refCat) 

281 fluxFieldList = [getRefFluxField(refSchema, filt) for filt in (colorterm.primary, 

282 colorterm.secondary)] 

283 else: 

284 # no colorterms to apply 

285 self.log.info("Not applying color terms because %s", applyCTReason) 

286 colorterm = None 

287 

288 fluxFieldList = [getRefFluxField(refSchema, filterLabel.bandLabel)] 

289 fluxField = getRefFluxField(refSchema, filterLabel.bandLabel) 

290 fluxKey = refSchema.find(fluxField).key 

291 refFluxArr = np.array([m.first.get(fluxKey) for m in matches]) 

292 

293 try: 

294 fluxErrKey = refSchema.find(fluxField + "Err").key 

295 refFluxErrArr = np.array([m.first.get(fluxErrKey) for m in matches]) 

296 except KeyError: 

297 # Reference catalogue may not have flux uncertainties; HACK DM-2308 

298 self.log.warning("Reference catalog does not have flux uncertainties for %s;" 

299 " using sqrt(flux).", fluxField) 

300 refFluxErrArr = np.sqrt(refFluxArr) 

301 

302 refMagArr = u.Quantity(refFluxArr, u.nJy).to_value(u.ABmag) 

303 # HACK convert to Jy until we have a replacement for this (DM-16903) 

304 refMagErrArr = abMagErrFromFluxErr(refFluxErrArr*1e-9, refFluxArr*1e-9) 

305 

306 # compute the source catalog magnitudes and errors 

307 srcMagArr = u.Quantity(srcInstFluxArr, u.nJy).to_value(u.ABmag) 

308 # Fitting with error bars in both axes is hard 

309 # for now ignore reference flux error, but ticket DM-2308 is a request for a better solution 

310 # HACK convert to Jy until we have a replacement for this (DM-16903) 

311 magErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9) 

312 if self.config.magErrFloor != 0.0: 

313 magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5 

314 

315 srcMagErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9) 

316 

317 good = np.isfinite(srcMagArr) & np.isfinite(refMagArr) 

318 

319 return pipeBase.Struct( 

320 srcMag=srcMagArr[good], 

321 refMag=refMagArr[good], 

322 magErr=magErrArr[good], 

323 srcMagErr=srcMagErrArr[good], 

324 refMagErr=refMagErrArr[good], 

325 refFluxFieldList=fluxFieldList, 

326 ) 

327 

328 @timeMethod 

329 def run(self, exposure, sourceCat, expId=0): 

330 """Do photometric calibration - select matches to use and (possibly iteratively) compute 

331 the zero point. 

332 

333 Parameters 

334 ---------- 

335 exposure : `lsst.afw.image.Exposure` 

336 Exposure upon which the sources in the matches were detected. 

337 sourceCat : `lsst.afw.image.SourceCatalog` 

338 A catalog of sources to use in the calibration 

339 (i.e. a `list` of `lsst.afw.table.Match` with 

340 first being of type `lsst.afw.table.SimpleRecord` and second type `lsst.afw.table.SourceRecord` 

341 the reference object and matched object respectively). 

342 Will not be modified except to set the outputField if requested. 

343 expId : `int`, optional 

344 Exposure ID. 

345 

346 Returns 

347 ------- 

348 result : `lsst.pipe.base.Struct` 

349 Results as a struct with attributes: 

350 

351 ``photoCalib`` 

352 Object containing the zero point (`lsst.afw.image.Calib`). 

353 ``arrays`` 

354 Magnitude arrays returned be `PhotoCalTask.extractMagArrays`. 

355 ``matches`` 

356 ReferenceMatchVector, as returned by `PhotoCalTask.selectMatches`. 

357 ``zp`` 

358 Photometric zero point (mag, `float`). 

359 ``sigma`` 

360 Standard deviation of fit of photometric zero point (mag, `float`). 

361 ``ngood`` 

362 Number of sources used to fit photometric zero point (`int`). 

363 

364 Raises 

365 ------ 

366 RuntimeError 

367 Raised if any of the following occur: 

368 - No matches to use for photocal. 

369 - No matches are available (perhaps no sources/references were selected by the matcher). 

370 - No reference stars are available. 

371 - No matches are available from which to extract magnitudes. 

372 

373 Notes 

374 ----- 

375 The exposure is only used to provide the name of the filter being calibrated (it may also be 

376 used to generate debugging plots). 

377 

378 The reference objects: 

379 - Must include a field ``photometric``; True for objects which should be considered as 

380 photometric standards. 

381 - Must include a field ``flux``; the flux used to impose a magnitude limit and also to calibrate 

382 the data to (unless a color term is specified, in which case ColorTerm.primary is used; 

383 See https://jira.lsstcorp.org/browse/DM-933). 

384 - May include a field ``stargal``; if present, True means that the object is a star. 

385 - May include a field ``var``; if present, True means that the object is variable. 

386 

387 The measured sources: 

388 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration. 

389 """ 

390 import lsstDebug 

391 

392 display = lsstDebug.Info(__name__).display 

393 displaySources = display and lsstDebug.Info(__name__).displaySources 

394 self.scatterPlot = display and lsstDebug.Info(__name__).scatterPlot 

395 

396 if self.scatterPlot: 

397 from matplotlib import pyplot 

398 try: 

399 self.fig.clf() 

400 except Exception: 

401 self.fig = pyplot.figure() 

402 

403 filterLabel = exposure.getFilter() 

404 

405 # Match sources 

406 matchResults = self.match.run(sourceCat, filterLabel.bandLabel) 

407 matches = matchResults.matches 

408 

409 reserveResults = self.reserve.run([mm.second for mm in matches], expId=expId) 

410 if displaySources: 

411 self.displaySources(exposure, matches, reserveResults.reserved) 

412 if reserveResults.reserved.sum() > 0: 

413 matches = [mm for mm, use in zip(matches, reserveResults.use) if use] 

414 if len(matches) == 0: 

415 raise RuntimeError("No matches to use for photocal") 

416 if self.usedKey is not None: 

417 for mm in matches: 

418 mm.second.set(self.usedKey, True) 

419 

420 # Prepare for fitting 

421 sourceKeys = self.getSourceKeys(matches[0].second.schema) 

422 arrays = self.extractMagArrays(matches, filterLabel, sourceKeys) 

423 

424 # Fit for zeropoint 

425 r = self.getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr) 

426 self.log.info("Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood) 

427 

428 # Prepare the results 

429 flux0 = 10**(0.4*r.zp) # Flux of mag=0 star 

430 flux0err = 0.4*math.log(10)*flux0*r.sigma # Error in flux0 

431 photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err) 

432 

433 return pipeBase.Struct( 

434 photoCalib=photoCalib, 

435 arrays=arrays, 

436 matches=matches, 

437 zp=r.zp, 

438 sigma=r.sigma, 

439 ngood=r.ngood, 

440 ) 

441 

442 def displaySources(self, exposure, matches, reserved, frame=1): 

443 """Display sources we'll use for photocal. 

444 

445 Sources that will be actually used will be green. 

446 Sources reserved from the fit will be red. 

447 

448 Parameters 

449 ---------- 

450 exposure : `lsst.afw.image.ExposureF` 

451 Exposure to display. 

452 matches : `list` of `lsst.afw.table.RefMatch` 

453 Matches used for photocal. 

454 reserved : `numpy.ndarray` of type `bool` 

455 Boolean array indicating sources that are reserved. 

456 frame : `int`, optional 

457 Frame number for display. 

458 """ 

459 disp = afwDisplay.getDisplay(frame=frame) 

460 disp.mtv(exposure, title="photocal") 

461 with disp.Buffering(): 

462 for mm, rr in zip(matches, reserved): 

463 x, y = mm.second.getCentroid() 

464 ctype = afwDisplay.RED if rr else afwDisplay.GREEN 

465 disp.dot("o", x, y, size=4, ctype=ctype) 

466 

467 def getZeroPoint(self, src, ref, srcErr=None, zp0=None): 

468 """Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars). 

469 

470 Returns 

471 ------- 

472 result : `lsst.pipe.base.Struct` 

473 Results as a struct with attributes: 

474 

475 ``zp`` 

476 Photometric zero point (mag, `float`). 

477 ``sigma`` 

478 Standard deviation of fit of photometric zero point (mag, `float`). 

479 ``ngood`` 

480 Number of sources used to fit photometric zero point (`int`). 

481 

482 Notes 

483 ----- 

484 We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists: 

485 - We use the median/interquartile range to estimate the position to clip around, and the 

486 "sigma" to use. 

487 - We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently 

488 large estimate will prevent the clipping from ever taking effect. 

489 - Rather than start with the median we start with a crude mode. This means that a set of magnitude 

490 residuals with a tight core and asymmetrical outliers will start in the core. We use the width of 

491 this core to set our maximum sigma (see second bullet). 

492 """ 

493 sigmaMax = self.config.sigmaMax 

494 

495 dmag = ref - src 

496 

497 indArr = np.argsort(dmag) 

498 dmag = dmag[indArr] 

499 

500 if srcErr is not None: 

501 dmagErr = srcErr[indArr] 

502 else: 

503 dmagErr = np.ones(len(dmag)) 

504 

505 # need to remove nan elements to avoid errors in stats calculation with numpy 

506 ind_noNan = np.array([i for i in range(len(dmag)) 

507 if (not np.isnan(dmag[i]) and not np.isnan(dmagErr[i]))]) 

508 dmag = dmag[ind_noNan] 

509 dmagErr = dmagErr[ind_noNan] 

510 

511 IQ_TO_STDEV = 0.741301109252802 # 1 sigma in units of interquartile (assume Gaussian) 

512 

513 npt = len(dmag) 

514 ngood = npt 

515 good = None # set at end of first iteration 

516 for i in range(self.config.nIter): 

517 if i > 0: 

518 npt = sum(good) 

519 

520 center = None 

521 if i == 0: 

522 # 

523 # Start by finding the mode 

524 # 

525 nhist = 20 

526 try: 

527 hist, edges = np.histogram(dmag, nhist, new=True) 

528 except TypeError: 

529 hist, edges = np.histogram(dmag, nhist) # they removed new=True around numpy 1.5 

530 imode = np.arange(nhist)[np.where(hist == hist.max())] 

531 

532 if imode[-1] - imode[0] + 1 == len(imode): # Multiple modes, but all contiguous 

533 if zp0: 

534 center = zp0 

535 else: 

536 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1]) 

537 

538 peak = sum(hist[imode])/len(imode) # peak height 

539 

540 # Estimate FWHM of mode 

541 j = imode[0] 

542 while j >= 0 and hist[j] > 0.5*peak: 

543 j -= 1 

544 j = max(j, 0) 

545 q1 = dmag[sum(hist[range(j)])] 

546 

547 j = imode[-1] 

548 while j < nhist and hist[j] > 0.5*peak: 

549 j += 1 

550 j = min(j, nhist - 1) 

551 j = min(sum(hist[range(j)]), npt - 1) 

552 q3 = dmag[j] 

553 

554 if q1 == q3: 

555 q1 = dmag[int(0.25*npt)] 

556 q3 = dmag[int(0.75*npt)] 

557 

558 sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian) 

559 

560 if sigmaMax is None: 

561 sigmaMax = 2*sig # upper bound on st. dev. for clipping. multiplier is a heuristic 

562 

563 self.log.debug("Photo calibration histogram: center = %.2f, sig = %.2f", center, sig) 

564 

565 else: 

566 if sigmaMax is None: 

567 sigmaMax = dmag[-1] - dmag[0] 

568 

569 center = np.median(dmag) 

570 q1 = dmag[int(0.25*npt)] 

571 q3 = dmag[int(0.75*npt)] 

572 sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian) 

573 

574 if center is None: # usually equivalent to (i > 0) 

575 gdmag = dmag[good] 

576 if self.config.useMedian: 

577 center = np.median(gdmag) 

578 else: 

579 gdmagErr = dmagErr[good] 

580 center = np.average(gdmag, weights=gdmagErr) 

581 

582 q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)] 

583 q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)] 

584 

585 sig = IQ_TO_STDEV*(q3 - q1) # estimate of standard deviation 

586 

587 good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax) # don't clip too softly 

588 

589 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

590 if self.scatterPlot: 

591 try: 

592 self.fig.clf() 

593 

594 axes = self.fig.add_axes((0.1, 0.1, 0.85, 0.80)) 

595 

596 axes.plot(ref[good], dmag[good] - center, "b+") 

597 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good], 

598 linestyle='', color='b') 

599 

600 bad = np.logical_not(good) 

601 if len(ref[bad]) > 0: 

602 axes.plot(ref[bad], dmag[bad] - center, "r+") 

603 axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad], 

604 linestyle='', color='r') 

605 

606 axes.plot((-100, 100), (0, 0), "g-") 

607 for x in (-1, 1): 

608 axes.plot((-100, 100), x*0.05*np.ones(2), "g--") 

609 

610 axes.set_ylim(-1.1, 1.1) 

611 axes.set_xlim(24, 13) 

612 axes.set_xlabel("Reference") 

613 axes.set_ylabel("Reference - Instrumental") 

614 

615 self.fig.show() 

616 

617 if self.scatterPlot > 1: 

618 reply = None 

619 while i == 0 or reply != "c": 

620 try: 

621 reply = input("Next iteration? [ynhpc] ") 

622 except EOFError: 

623 reply = "n" 

624 

625 if reply == "h": 

626 print("Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr) 

627 continue 

628 

629 if reply in ("", "c", "n", "p", "y"): 

630 break 

631 else: 

632 print("Unrecognised response: %s" % reply, file=sys.stderr) 

633 

634 if reply == "n": 

635 break 

636 elif reply == "p": 

637 import pdb 

638 pdb.set_trace() 

639 except Exception as e: 

640 print("Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr) 

641 

642 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

643 

644 old_ngood = ngood 

645 ngood = sum(good) 

646 if ngood == 0: 

647 msg = "PhotoCal.getZeroPoint: no good stars remain" 

648 

649 if i == 0: # failed the first time round -- probably all fell in one bin 

650 center = np.average(dmag, weights=dmagErr) 

651 msg += " on first iteration; using average of all calibration stars" 

652 

653 self.log.warning(msg) 

654 

655 return pipeBase.Struct( 

656 zp=center, 

657 sigma=sig, 

658 ngood=len(dmag)) 

659 elif ngood == old_ngood: 

660 break 

661 

662 if False: 

663 ref = ref[good] 

664 dmag = dmag[good] 

665 dmagErr = dmagErr[good] 

666 

667 dmag = dmag[good] 

668 dmagErr = dmagErr[good] 

669 zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=True) 

670 sigma = np.sqrt(1.0/weightSum) 

671 return pipeBase.Struct( 

672 zp=zp, 

673 sigma=sigma, 

674 ngood=len(dmag), 

675 )