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

259 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-08 06:53 -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.\nThe associated flag field " 

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

52 ) 

53 applyColorTerms = pexConf.Field( 

54 dtype=bool, 

55 default=False, 

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

57 "`True`: attempt to apply color terms; fail if color term data is " 

58 "not available for the specified reference catalog and filter.\n" 

59 "`False`: do not apply color terms."), 

60 optional=True, 

61 ) 

62 sigmaMax = pexConf.Field( 

63 dtype=float, 

64 default=0.25, 

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

66 optional=True, 

67 ) 

68 nSigma = pexConf.Field( 

69 dtype=float, 

70 default=3.0, 

71 doc="clip at nSigma", 

72 ) 

73 useMedian = pexConf.Field( 

74 dtype=bool, 

75 default=True, 

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

77 ) 

78 nIter = pexConf.Field( 

79 dtype=int, 

80 default=20, 

81 doc="number of iterations", 

82 ) 

83 colorterms = pexConf.ConfigField( 

84 dtype=ColortermLibrary, 

85 doc="Library of photometric reference catalog name: color term dict (see also applyColorTerms).", 

86 ) 

87 photoCatName = pexConf.Field( 

88 dtype=str, 

89 optional=True, 

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

91 "See also applyColorTerms."), 

92 ) 

93 magErrFloor = pexConf.RangeField( 

94 dtype=float, 

95 default=0.0, 

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

97 min=0.0, 

98 ) 

99 

100 def validate(self): 

101 pexConf.Config.validate(self) 

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

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

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

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

106 

107 def setDefaults(self): 

108 pexConf.Config.setDefaults(self) 

109 self.match.sourceSelection.doRequirePrimary = True 

110 self.match.sourceSelection.doFlags = True 

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

112 "base_PixelFlags_flag_edge", 

113 "base_PixelFlags_flag_interpolated", 

114 "base_PixelFlags_flag_saturated", 

115 ] 

116 self.match.sourceSelection.doUnresolved = True 

117 

118 

119class PhotoCalTask(pipeBase.Task): 

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

121 of stars matched to an input catalogue. 

122 

123 Parameters 

124 ---------- 

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

126 An instance of LoadReferenceObjectsTasks that supplies an external reference 

127 catalog. 

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

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

130 **kwds 

131 Additional keyword arguments. 

132 

133 Notes 

134 ----- 

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

136 

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

138 

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

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

141 

142 Debugging: 

143 

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

145 

146 display : 

147 If True enable other debug outputs. 

148 displaySources : 

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

150 

151 red o : 

152 Reserved objects. 

153 green o : 

154 Objects used in the photometric calibration. 

155 

156 scatterPlot : 

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

158 

159 - good objects in blue 

160 - rejected objects in red 

161 

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

163 """ 

164 

165 ConfigClass = PhotoCalConfig 

166 _DefaultName = "photoCal" 

167 

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

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

170 self.scatterPlot = None 

171 self.fig = None 

172 if schema is not None: 

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

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

175 else: 

176 self.usedKey = None 

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

178 name="match", parentTask=self) 

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

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

181 

182 def getSourceKeys(self, schema): 

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

184 by PhotoCalTask. 

185 

186 Parameters 

187 ---------- 

188 schema : `lsst.afw.table.schema` 

189 Schema of the catalog to get keys from. 

190 

191 Returns 

192 ------- 

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

194 Results as a struct with attributes: 

195 

196 ``instFlux`` 

197 Instrument flux key. 

198 ``instFluxErr`` 

199 Instrument flux error key. 

200 """ 

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

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

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

204 

205 @timeMethod 

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

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

208 

209 Parameters 

210 ---------- 

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

212 Reference/source matches. 

213 filterLabel : `str` 

214 Label of filter being calibrated. 

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

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

217 

218 Returns 

219 ------- 

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

221 Results as a struct with attributes: 

222 

223 ``srcMag`` 

224 Source magnitude (`np.array`). 

225 ``refMag`` 

226 Reference magnitude (`np.array`). 

227 ``srcMagErr`` 

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

229 ``refMagErr`` 

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

231 ``magErr`` 

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

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

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

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

236 ``refFluxFieldList`` 

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

238 """ 

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

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

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

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

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

244 srcInstFluxErrArr = np.sqrt(srcInstFluxArr) 

245 

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

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

248 srcInstFluxArr = srcInstFluxArr * referenceFlux 

249 srcInstFluxErrArr = srcInstFluxErrArr * referenceFlux 

250 

251 if not matches: 

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

253 refSchema = matches[0].first.schema 

254 

255 if self.config.applyColorTerms: 

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

257 filterLabel.physicalLabel, self.config.photoCatName) 

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

259 self.config.photoCatName, 

260 doRaise=True) 

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

262 

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

264 refCat.reserve(len(matches)) 

265 for x in matches: 

266 record = refCat.addNew() 

267 record.assign(x.first) 

268 

269 refMagArr, refMagErrArr = colorterm.getCorrectedMagnitudes(refCat) 

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

271 colorterm.secondary)] 

272 else: 

273 self.log.info("Not applying color terms.") 

274 colorterm = None 

275 

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

277 fluxField = getRefFluxField(refSchema, filterLabel.bandLabel) 

278 fluxKey = refSchema.find(fluxField).key 

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

280 

281 try: 

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

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

284 except KeyError: 

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

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

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

288 refFluxErrArr = np.sqrt(refFluxArr) 

289 

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

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

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

293 

294 # compute the source catalog magnitudes and errors 

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

296 # Fitting with error bars in both axes is hard 

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

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

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

300 if self.config.magErrFloor != 0.0: 

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

302 

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

304 

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

306 

307 return pipeBase.Struct( 

308 srcMag=srcMagArr[good], 

309 refMag=refMagArr[good], 

310 magErr=magErrArr[good], 

311 srcMagErr=srcMagErrArr[good], 

312 refMagErr=refMagErrArr[good], 

313 refFluxFieldList=fluxFieldList, 

314 ) 

315 

316 @timeMethod 

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

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

319 the zero point. 

320 

321 Parameters 

322 ---------- 

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

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

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

326 A catalog of sources to use in the calibration 

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

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

329 the reference object and matched object respectively). 

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

331 expId : `int`, optional 

332 Exposure ID. 

333 

334 Returns 

335 ------- 

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

337 Results as a struct with attributes: 

338 

339 ``photoCalib`` 

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

341 ``arrays`` 

342 Magnitude arrays returned be `PhotoCalTask.extractMagArrays`. 

343 ``matches`` 

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

345 ``zp`` 

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

347 ``sigma`` 

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

349 ``ngood`` 

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

351 

352 Raises 

353 ------ 

354 RuntimeError 

355 Raised if any of the following occur: 

356 - No matches to use for photocal. 

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

358 - No reference stars are available. 

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

360 

361 Notes 

362 ----- 

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

364 used to generate debugging plots). 

365 

366 The reference objects: 

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

368 photometric standards. 

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

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

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

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

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

374 

375 The measured sources: 

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

377 """ 

378 import lsstDebug 

379 

380 display = lsstDebug.Info(__name__).display 

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

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

383 

384 if self.scatterPlot: 

385 from matplotlib import pyplot 

386 try: 

387 self.fig.clf() 

388 except Exception: 

389 self.fig = pyplot.figure() 

390 

391 filterLabel = exposure.getFilter() 

392 

393 # Match sources 

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

395 matches = matchResults.matches 

396 

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

398 if displaySources: 

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

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

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

402 if len(matches) == 0: 

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

404 if self.usedKey is not None: 

405 for mm in matches: 

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

407 

408 # Prepare for fitting 

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

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

411 

412 # Fit for zeropoint 

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

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

415 

416 # Prepare the results 

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

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

419 photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err) 

420 

421 return pipeBase.Struct( 

422 photoCalib=photoCalib, 

423 arrays=arrays, 

424 matches=matches, 

425 zp=r.zp, 

426 sigma=r.sigma, 

427 ngood=r.ngood, 

428 ) 

429 

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

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

432 

433 Sources that will be actually used will be green. 

434 Sources reserved from the fit will be red. 

435 

436 Parameters 

437 ---------- 

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

439 Exposure to display. 

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

441 Matches used for photocal. 

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

443 Boolean array indicating sources that are reserved. 

444 frame : `int`, optional 

445 Frame number for display. 

446 """ 

447 disp = afwDisplay.getDisplay(frame=frame) 

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

449 with disp.Buffering(): 

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

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

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

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

454 

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

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

457 

458 Returns 

459 ------- 

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

461 Results as a struct with attributes: 

462 

463 ``zp`` 

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

465 ``sigma`` 

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

467 ``ngood`` 

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

469 

470 Notes 

471 ----- 

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

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

474 "sigma" to use. 

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

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

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

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

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

480 """ 

481 sigmaMax = self.config.sigmaMax 

482 

483 dmag = ref - src 

484 

485 indArr = np.argsort(dmag) 

486 dmag = dmag[indArr] 

487 

488 if srcErr is not None: 

489 dmagErr = srcErr[indArr] 

490 else: 

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

492 

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

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

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

496 dmag = dmag[ind_noNan] 

497 dmagErr = dmagErr[ind_noNan] 

498 

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

500 

501 npt = len(dmag) 

502 ngood = npt 

503 good = None # set at end of first iteration 

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

505 if i > 0: 

506 npt = sum(good) 

507 

508 center = None 

509 if i == 0: 

510 # 

511 # Start by finding the mode 

512 # 

513 nhist = 20 

514 try: 

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

516 except TypeError: 

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

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

519 

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

521 if zp0: 

522 center = zp0 

523 else: 

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

525 

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

527 

528 # Estimate FWHM of mode 

529 j = imode[0] 

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

531 j -= 1 

532 j = max(j, 0) 

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

534 

535 j = imode[-1] 

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

537 j += 1 

538 j = min(j, nhist - 1) 

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

540 q3 = dmag[j] 

541 

542 if q1 == q3: 

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

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

545 

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

547 

548 if sigmaMax is None: 

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

550 

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

552 

553 else: 

554 if sigmaMax is None: 

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

556 

557 center = np.median(dmag) 

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

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

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

561 

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

563 gdmag = dmag[good] 

564 if self.config.useMedian: 

565 center = np.median(gdmag) 

566 else: 

567 gdmagErr = dmagErr[good] 

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

569 

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

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

572 

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

574 

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

576 

577 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

578 if self.scatterPlot: 

579 try: 

580 self.fig.clf() 

581 

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

583 

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

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

586 linestyle='', color='b') 

587 

588 bad = np.logical_not(good) 

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

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

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

592 linestyle='', color='r') 

593 

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

595 for x in (-1, 1): 

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

597 

598 axes.set_ylim(-1.1, 1.1) 

599 axes.set_xlim(24, 13) 

600 axes.set_xlabel("Reference") 

601 axes.set_ylabel("Reference - Instrumental") 

602 

603 self.fig.show() 

604 

605 if self.scatterPlot > 1: 

606 reply = None 

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

608 try: 

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

610 except EOFError: 

611 reply = "n" 

612 

613 if reply == "h": 

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

615 continue 

616 

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

618 break 

619 else: 

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

621 

622 if reply == "n": 

623 break 

624 elif reply == "p": 

625 import pdb 

626 pdb.set_trace() 

627 except Exception as e: 

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

629 

630 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

631 

632 old_ngood = ngood 

633 ngood = sum(good) 

634 if ngood == 0: 

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

636 

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

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

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

640 

641 self.log.warning(msg) 

642 

643 return pipeBase.Struct( 

644 zp=center, 

645 sigma=sig, 

646 ngood=len(dmag)) 

647 elif ngood == old_ngood: 

648 break 

649 

650 if False: 

651 ref = ref[good] 

652 dmag = dmag[good] 

653 dmagErr = dmagErr[good] 

654 

655 dmag = dmag[good] 

656 dmagErr = dmagErr[good] 

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

658 sigma = np.sqrt(1.0/weightSum) 

659 return pipeBase.Struct( 

660 zp=zp, 

661 sigma=sigma, 

662 ngood=len(dmag), 

663 )