lsst.pipe.tasks g4d3fbbbe9d+52d1362a8c
Loading...
Searching...
No Matches
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.\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 ----------
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 )
def getZeroPoint(self, src, ref, srcErr=None, zp0=None)
Definition: photoCal.py:455
def extractMagArrays(self, matches, filterLabel, sourceKeys)
Definition: photoCal.py:206
def __init__(self, refObjLoader, schema=None, **kwds)
Definition: photoCal.py:168
def displaySources(self, exposure, matches, reserved, frame=1)
Definition: photoCal.py:430