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

258 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-03 03:39 -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.doFlags = True 

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

111 "base_PixelFlags_flag_edge", 

112 "base_PixelFlags_flag_interpolated", 

113 "base_PixelFlags_flag_saturated", 

114 ] 

115 self.match.sourceSelection.doUnresolved = True 

116 

117 

118class PhotoCalTask(pipeBase.Task): 

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

120 of stars matched to an input catalogue. 

121 

122 Parameters 

123 ---------- 

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

125 An instance of LoadReferenceObjectsTasks that supplies an external reference 

126 catalog. 

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

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

129 **kwds 

130 Additional keyword arguments. 

131 

132 Notes 

133 ----- 

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

135 

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

137 

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

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

140 

141 Debugging: 

142 

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

144 

145 display : 

146 If True enable other debug outputs. 

147 displaySources : 

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

149 

150 red o : 

151 Reserved objects. 

152 green o : 

153 Objects used in the photometric calibration. 

154 

155 scatterPlot : 

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

157 

158 - good objects in blue 

159 - rejected objects in red 

160 

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

162 """ 

163 

164 ConfigClass = PhotoCalConfig 

165 _DefaultName = "photoCal" 

166 

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

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

169 self.scatterPlot = None 

170 self.fig = None 

171 if schema is not None: 

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

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

174 else: 

175 self.usedKey = None 

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

177 name="match", parentTask=self) 

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

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

180 

181 def getSourceKeys(self, schema): 

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

183 by PhotoCalTask. 

184 

185 Parameters 

186 ---------- 

187 schema : `lsst.afw.table.schema` 

188 Schema of the catalog to get keys from. 

189 

190 Returns 

191 ------- 

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

193 Results as a struct with attributes: 

194 

195 ``instFlux`` 

196 Instrument flux key. 

197 ``instFluxErr`` 

198 Instrument flux error key. 

199 """ 

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

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

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

203 

204 @timeMethod 

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

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

207 

208 Parameters 

209 ---------- 

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

211 Reference/source matches. 

212 filterLabel : `str` 

213 Label of filter being calibrated. 

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

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

216 

217 Returns 

218 ------- 

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

220 Results as a struct with attributes: 

221 

222 ``srcMag`` 

223 Source magnitude (`np.array`). 

224 ``refMag`` 

225 Reference magnitude (`np.array`). 

226 ``srcMagErr`` 

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

228 ``refMagErr`` 

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

230 ``magErr`` 

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

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

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

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

235 ``refFluxFieldList`` 

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

237 """ 

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

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

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

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

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

243 srcInstFluxErrArr = np.sqrt(srcInstFluxArr) 

244 

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

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

247 srcInstFluxArr = srcInstFluxArr * referenceFlux 

248 srcInstFluxErrArr = srcInstFluxErrArr * referenceFlux 

249 

250 if not matches: 

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

252 refSchema = matches[0].first.schema 

253 

254 if self.config.applyColorTerms: 

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

256 filterLabel.physicalLabel, self.config.photoCatName) 

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

258 self.config.photoCatName, 

259 doRaise=True) 

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

261 

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

263 refCat.reserve(len(matches)) 

264 for x in matches: 

265 record = refCat.addNew() 

266 record.assign(x.first) 

267 

268 refMagArr, refMagErrArr = colorterm.getCorrectedMagnitudes(refCat) 

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

270 colorterm.secondary)] 

271 else: 

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

273 colorterm = None 

274 

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

276 fluxField = getRefFluxField(refSchema, filterLabel.bandLabel) 

277 fluxKey = refSchema.find(fluxField).key 

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

279 

280 try: 

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

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

283 except KeyError: 

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

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

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

287 refFluxErrArr = np.sqrt(refFluxArr) 

288 

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

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

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

292 

293 # compute the source catalog magnitudes and errors 

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

295 # Fitting with error bars in both axes is hard 

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

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

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

299 if self.config.magErrFloor != 0.0: 

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

301 

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

303 

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

305 

306 return pipeBase.Struct( 

307 srcMag=srcMagArr[good], 

308 refMag=refMagArr[good], 

309 magErr=magErrArr[good], 

310 srcMagErr=srcMagErrArr[good], 

311 refMagErr=refMagErrArr[good], 

312 refFluxFieldList=fluxFieldList, 

313 ) 

314 

315 @timeMethod 

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

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

318 the zero point. 

319 

320 Parameters 

321 ---------- 

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

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

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

325 A catalog of sources to use in the calibration 

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

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

328 the reference object and matched object respectively). 

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

330 expId : `int`, optional 

331 Exposure ID. 

332 

333 Returns 

334 ------- 

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

336 Results as a struct with attributes: 

337 

338 ``photoCalib`` 

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

340 ``arrays`` 

341 Magnitude arrays returned be `PhotoCalTask.extractMagArrays`. 

342 ``matches`` 

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

344 ``zp`` 

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

346 ``sigma`` 

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

348 ``ngood`` 

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

350 

351 Raises 

352 ------ 

353 RuntimeError 

354 Raised if any of the following occur: 

355 - No matches to use for photocal. 

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

357 - No reference stars are available. 

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

359 

360 Notes 

361 ----- 

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

363 used to generate debugging plots). 

364 

365 The reference objects: 

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

367 photometric standards. 

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

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

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

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

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

373 

374 The measured sources: 

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

376 """ 

377 import lsstDebug 

378 

379 display = lsstDebug.Info(__name__).display 

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

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

382 

383 if self.scatterPlot: 

384 from matplotlib import pyplot 

385 try: 

386 self.fig.clf() 

387 except Exception: 

388 self.fig = pyplot.figure() 

389 

390 filterLabel = exposure.getFilter() 

391 

392 # Match sources 

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

394 matches = matchResults.matches 

395 

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

397 if displaySources: 

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

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

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

401 if len(matches) == 0: 

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

403 if self.usedKey is not None: 

404 for mm in matches: 

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

406 

407 # Prepare for fitting 

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

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

410 

411 # Fit for zeropoint 

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

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

414 

415 # Prepare the results 

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

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

418 photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err) 

419 

420 return pipeBase.Struct( 

421 photoCalib=photoCalib, 

422 arrays=arrays, 

423 matches=matches, 

424 zp=r.zp, 

425 sigma=r.sigma, 

426 ngood=r.ngood, 

427 ) 

428 

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

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

431 

432 Sources that will be actually used will be green. 

433 Sources reserved from the fit will be red. 

434 

435 Parameters 

436 ---------- 

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

438 Exposure to display. 

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

440 Matches used for photocal. 

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

442 Boolean array indicating sources that are reserved. 

443 frame : `int`, optional 

444 Frame number for display. 

445 """ 

446 disp = afwDisplay.getDisplay(frame=frame) 

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

448 with disp.Buffering(): 

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

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

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

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

453 

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

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

456 

457 Returns 

458 ------- 

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

460 Results as a struct with attributes: 

461 

462 ``zp`` 

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

464 ``sigma`` 

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

466 ``ngood`` 

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

468 

469 Notes 

470 ----- 

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

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

473 "sigma" to use. 

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

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

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

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

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

479 """ 

480 sigmaMax = self.config.sigmaMax 

481 

482 dmag = ref - src 

483 

484 indArr = np.argsort(dmag) 

485 dmag = dmag[indArr] 

486 

487 if srcErr is not None: 

488 dmagErr = srcErr[indArr] 

489 else: 

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

491 

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

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

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

495 dmag = dmag[ind_noNan] 

496 dmagErr = dmagErr[ind_noNan] 

497 

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

499 

500 npt = len(dmag) 

501 ngood = npt 

502 good = None # set at end of first iteration 

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

504 if i > 0: 

505 npt = sum(good) 

506 

507 center = None 

508 if i == 0: 

509 # 

510 # Start by finding the mode 

511 # 

512 nhist = 20 

513 try: 

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

515 except TypeError: 

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

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

518 

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

520 if zp0: 

521 center = zp0 

522 else: 

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

524 

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

526 

527 # Estimate FWHM of mode 

528 j = imode[0] 

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

530 j -= 1 

531 j = max(j, 0) 

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

533 

534 j = imode[-1] 

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

536 j += 1 

537 j = min(j, nhist - 1) 

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

539 q3 = dmag[j] 

540 

541 if q1 == q3: 

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

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

544 

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

546 

547 if sigmaMax is None: 

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

549 

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

551 

552 else: 

553 if sigmaMax is None: 

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

555 

556 center = np.median(dmag) 

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

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

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

560 

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

562 gdmag = dmag[good] 

563 if self.config.useMedian: 

564 center = np.median(gdmag) 

565 else: 

566 gdmagErr = dmagErr[good] 

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

568 

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

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

571 

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

573 

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

575 

576 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

577 if self.scatterPlot: 

578 try: 

579 self.fig.clf() 

580 

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

582 

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

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

585 linestyle='', color='b') 

586 

587 bad = np.logical_not(good) 

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

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

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

591 linestyle='', color='r') 

592 

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

594 for x in (-1, 1): 

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

596 

597 axes.set_ylim(-1.1, 1.1) 

598 axes.set_xlim(24, 13) 

599 axes.set_xlabel("Reference") 

600 axes.set_ylabel("Reference - Instrumental") 

601 

602 self.fig.show() 

603 

604 if self.scatterPlot > 1: 

605 reply = None 

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

607 try: 

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

609 except EOFError: 

610 reply = "n" 

611 

612 if reply == "h": 

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

614 continue 

615 

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

617 break 

618 else: 

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

620 

621 if reply == "n": 

622 break 

623 elif reply == "p": 

624 import pdb 

625 pdb.set_trace() 

626 except Exception as e: 

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

628 

629 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

630 

631 old_ngood = ngood 

632 ngood = sum(good) 

633 if ngood == 0: 

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

635 

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

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

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

639 

640 self.log.warning(msg) 

641 

642 return pipeBase.Struct( 

643 zp=center, 

644 sigma=sig, 

645 ngood=len(dmag)) 

646 elif ngood == old_ngood: 

647 break 

648 

649 if False: 

650 ref = ref[good] 

651 dmag = dmag[good] 

652 dmagErr = dmagErr[good] 

653 

654 dmag = dmag[good] 

655 dmagErr = dmagErr[good] 

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

657 sigma = np.sqrt(1.0/weightSum) 

658 return pipeBase.Struct( 

659 zp=zp, 

660 sigma=sigma, 

661 ngood=len(dmag), 

662 )