lsst.pipe.tasks g584c84fe5e+111e082d69
photoCal.py
Go to the documentation of this file.
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 ----------
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 )
def getZeroPoint(self, src, ref, srcErr=None, zp0=None)
Definition: photoCal.py:467
def extractMagArrays(self, matches, filterLabel, sourceKeys)
Definition: photoCal.py:207
def __init__(self, refObjLoader, schema=None, **kwds)
Definition: photoCal.py:169
def displaySources(self, exposure, matches, reserved, frame=1)
Definition: photoCal.py:442
def run(self, exposure, sourceCat, expId=0)
Definition: photoCal.py:329