Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+aff6b1d179,g1e78f5e6d3+b2e1eec3e3,g1fd858c14a+eb8d917efb,g35bb328faa+fcb1d3bbc8,g436fd98eb5+de86862952,g4af146b050+2f70285269,g4d2262a081+c17cfe15e3,g4e0f332c67+8616b824a5,g53246c7159+fcb1d3bbc8,g5a012ec0e7+d65fd7031a,g60b5630c4e+de86862952,g67b6fd64d1+c0248a1c13,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g7b71ed6315+fcb1d3bbc8,g87b7deb4dc+1a71b41694,g8852436030+40f6ec51d1,g89139ef638+c0248a1c13,g9125e01d80+fcb1d3bbc8,g94187f82dc+de86862952,g989de1cb63+c0248a1c13,g9f33ca652e+62adb22cd2,g9f7030ddb1+d892b2cb3e,ga2b97cdc51+de86862952,gabe3b4be73+1e0a283bba,gabf8522325+83c19109ce,gb1101e3267+1371da34ff,gb58c049af0+f03b321e39,gb89ab40317+c0248a1c13,gcf25f946ba+40f6ec51d1,gd6cbbdb0b4+d9e8db455e,gd9a9a58781+fcb1d3bbc8,gde0f65d7ad+fc726a16be,gded526ad44+763ef31e97,ge278dab8ac+4ce6343b44,ge410e46f29+c0248a1c13,gf67bdafdda+c0248a1c13,gfe06eef73a+95f9f0e40c,v29.0.0.rc3
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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, MatcherFailure
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 if self.fluxField != self.match.sourceSelection.signalToNoise.fluxField:
107 raise RuntimeError("Configured flux field %s does not match source selector field %s",
108 self.fluxField, self.match.sourceSelection.signalToNoise.fluxField)
109 if self.fluxField + "Err" != self.match.sourceSelection.signalToNoise.errField:
110 raise RuntimeError("Configured flux field %sErr does not match source selector error field %s",
111 self.fluxField, self.match.sourceSelection.signalToNoise.errField)
112
113 def setDefaults(self):
114 pexConf.Config.setDefaults(self)
115 self.match.sourceSelection.doRequirePrimary = True
116 self.match.sourceSelection.doFlags = True
117 self.match.sourceSelection.flags.bad = [
118 "base_PixelFlags_flag_edge",
119 "base_PixelFlags_flag_nodata",
120 "base_PixelFlags_flag_interpolated",
121 "base_PixelFlags_flag_saturated",
122 ]
123 self.match.sourceSelection.doUnresolved = True
124 self.match.sourceSelection.doSignalToNoise = True
125 self.match.sourceSelection.signalToNoise.minimum = 10.0
126 self.match.sourceSelection.signalToNoise.fluxField = self.fluxField
127 self.match.sourceSelection.signalToNoise.errField = self.fluxField + "Err"
128
129
130class PhotoCalTask(pipeBase.Task):
131 """Calculate an Exposure's zero-point given a set of flux measurements
132 of stars matched to an input catalogue.
133
134 Parameters
135 ----------
136 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`
137 A reference object loader object; gen3 pipeline tasks will pass `None`
138 and call `match.setRefObjLoader` in `runQuantum`.
139 schema : `lsst.afw.table.Schema`, optional
140 The schema of the detection catalogs used as input to this task.
141 **kwds
142 Additional keyword arguments.
143
144 Notes
145 -----
146 The type of flux to use is specified by PhotoCalConfig.fluxField.
147
148 The algorithm clips outliers iteratively, with parameters set in the configuration.
149
150 This task can adds fields to the schema, so any code calling this task must ensure that
151 these columns are indeed present in the input match list; see `pipe_tasks_photocal_Example`.
152
153 Debugging:
154
155 The available `~lsst.base.lsstDebug` variables in PhotoCalTask are:
156
157 display :
158 If True enable other debug outputs.
159 displaySources :
160 If True, display the exposure on ds9's frame 1 and overlay the source catalogue.
161
162 red o :
163 Reserved objects.
164 green o :
165 Objects used in the photometric calibration.
166
167 scatterPlot :
168 Make a scatter plot of flux v. reference magnitude as a function of reference magnitude:
169
170 - good objects in blue
171 - rejected objects in red
172
173 (if scatterPlot is 2 or more, prompt to continue after each iteration)
174 """
175
176 ConfigClass = PhotoCalConfig
177 _DefaultName = "photoCal"
178
179 def __init__(self, refObjLoader=None, schema=None, **kwds):
180 pipeBase.Task.__init__(self, **kwds)
181 self.scatterPlot = None
182 self.fig = None
183 if schema is not None:
184 self.usedKey = schema.addField("calib_photometry_used", type="Flag",
185 doc="set if source was used in photometric calibration")
186 else:
187 self.usedKey = None
188 self.match = DirectMatchTask(config=self.config.match, refObjLoader=refObjLoader,
189 name="match", parentTask=self)
190 self.makeSubtask("reserve", columnName="calib_photometry", schema=schema,
191 doc="set if source was reserved from photometric calibration")
192
193 def getSourceKeys(self, schema):
194 """Return a struct containing the source catalog keys for fields used
195 by PhotoCalTask.
196
197 Parameters
198 ----------
199 schema : `lsst.afw.table.schema`
200 Schema of the catalog to get keys from.
201
202 Returns
203 -------
204 result : `lsst.pipe.base.Struct`
205 Results as a struct with attributes:
206
207 ``instFlux``
208 Instrument flux key.
209 ``instFluxErr``
210 Instrument flux error key.
211 """
212 instFlux = schema.find(self.config.fluxField).key
213 instFluxErr = schema.find(self.config.fluxField + "Err").key
214 return pipeBase.Struct(instFlux=instFlux, instFluxErr=instFluxErr)
215
216 @timeMethod
217 def extractMagArrays(self, matches, filterLabel, sourceKeys):
218 """Extract magnitude and magnitude error arrays from the given matches.
219
220 Parameters
221 ----------
222 matches : `lsst.afw.table.ReferenceMatchVector`
223 Reference/source matches.
224 filterLabel : `str`
225 Label of filter being calibrated.
226 sourceKeys : `lsst.pipe.base.Struct`
227 Struct of source catalog keys, as returned by getSourceKeys().
228
229 Returns
230 -------
231 result : `lsst.pipe.base.Struct`
232 Results as a struct with attributes:
233
234 ``srcMag``
235 Source magnitude (`np.array`).
236 ``refMag``
237 Reference magnitude (`np.array`).
238 ``srcMagErr``
239 Source magnitude error (`np.array`).
240 ``refMagErr``
241 Reference magnitude error (`np.array`).
242 ``magErr``
243 An error in the magnitude; the error in ``srcMag`` - ``refMag``.
244 If nonzero, ``config.magErrFloor`` will be added to ``magErr`` only
245 (not ``srcMagErr`` or ``refMagErr``), as
246 ``magErr`` is what is later used to determine the zero point (`np.array`).
247 ``refFluxFieldList``
248 A list of field names of the reference catalog used for fluxes (1 or 2 strings) (`list`).
249 """
250 srcInstFluxArr = np.array([m.second.get(sourceKeys.instFlux) for m in matches])
251 srcInstFluxErrArr = np.array([m.second.get(sourceKeys.instFluxErr) for m in matches])
252 if not np.all(np.isfinite(srcInstFluxErrArr)):
253 # this is an unpleasant hack; see DM-2308 requesting a better solution
254 self.log.warning("Source catalog does not have flux uncertainties; using sqrt(flux).")
255 srcInstFluxErrArr = np.sqrt(srcInstFluxArr)
256
257 # convert source instFlux from DN to an estimate of nJy
258 referenceFlux = (0*u.ABmag).to_value(u.nJy)
259 srcInstFluxArr = srcInstFluxArr * referenceFlux
260 srcInstFluxErrArr = srcInstFluxErrArr * referenceFlux
261
262 if not matches:
263 raise MatcherFailure("No reference stars are available")
264 refSchema = matches[0].first.schema
265
266 if self.config.applyColorTerms:
267 self.log.info("Applying color terms for filter=%r, config.photoCatName=%s",
268 filterLabel.physicalLabel, self.config.photoCatName)
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 self.log.info("Not applying color terms.")
285 colorterm = None
286
287 fluxFieldList = [getRefFluxField(refSchema, filterLabel.bandLabel)]
288 fluxField = getRefFluxField(refSchema, filterLabel.bandLabel)
289 fluxKey = refSchema.find(fluxField).key
290 refFluxArr = np.array([m.first.get(fluxKey) for m in matches])
291
292 try:
293 fluxErrKey = refSchema.find(fluxField + "Err").key
294 refFluxErrArr = np.array([m.first.get(fluxErrKey) for m in matches])
295 except KeyError:
296 # Reference catalogue may not have flux uncertainties; HACK DM-2308
297 self.log.warning("Reference catalog does not have flux uncertainties for %s;"
298 " using sqrt(flux).", fluxField)
299 refFluxErrArr = np.sqrt(refFluxArr)
300
301 refMagArr = u.Quantity(refFluxArr, u.nJy).to_value(u.ABmag)
302 # HACK convert to Jy until we have a replacement for this (DM-16903)
303 refMagErrArr = abMagErrFromFluxErr(refFluxErrArr*1e-9, refFluxArr*1e-9)
304
305 # compute the source catalog magnitudes and errors
306 srcMagArr = u.Quantity(srcInstFluxArr, u.nJy).to_value(u.ABmag)
307 # Fitting with error bars in both axes is hard
308 # for now ignore reference flux error, but ticket DM-2308 is a request for a better solution
309 # HACK convert to Jy until we have a replacement for this (DM-16903)
310 magErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9)
311 if self.config.magErrFloor != 0.0:
312 magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5
313
314 srcMagErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9)
315
316 good = np.isfinite(srcMagArr) & np.isfinite(refMagArr)
317
318 return pipeBase.Struct(
319 srcMag=srcMagArr[good],
320 refMag=refMagArr[good],
321 magErr=magErrArr[good],
322 srcMagErr=srcMagErrArr[good],
323 refMagErr=refMagErrArr[good],
324 refFluxFieldList=fluxFieldList,
325 )
326
327 @timeMethod
328 def run(self, exposure, sourceCat, expId=0):
329 """Do photometric calibration - select matches to use and (possibly iteratively) compute
330 the zero point.
331
332 Parameters
333 ----------
334 exposure : `lsst.afw.image.Exposure`
335 Exposure upon which the sources in the matches were detected.
336 sourceCat : `lsst.afw.table.SourceCatalog`
337 Good stars selected for use in calibration.
338 expId : `int`, optional
339 Exposure ID.
340
341 Returns
342 -------
343 result : `lsst.pipe.base.Struct`
344 Results as a struct with attributes:
345
346 ``photoCalib``
347 Object containing the zero point (`lsst.afw.image.Calib`).
348 ``arrays``
349 Magnitude arrays returned be `PhotoCalTask.extractMagArrays`.
350 ``matches``
351 ReferenceMatchVector, as returned by the matcher
352 ``matchMeta`` : metadata needed to unpersist matches, as returned
353 by the matcher (`lsst.daf.base.PropertyList`)
354 ``zp``
355 Photometric zero point (mag, `float`).
356 ``sigma``
357 Standard deviation of fit of photometric zero point (mag, `float`).
358 ``ngood``
359 Number of sources used to fit photometric zero point (`int`).
360
361 Raises
362 ------
363 RuntimeError
364 Raised if any of the following occur:
365 - No matches to use for photocal.
366 - No matches are available (perhaps no sources/references were selected by the matcher).
367 - No reference stars are available.
368 - No matches are available from which to extract magnitudes.
369
370 Notes
371 -----
372 The exposure is only used to provide the name of the filter being calibrated (it may also be
373 used to generate debugging plots).
374
375 The reference objects:
376 - Must include a field ``photometric``; True for objects which should be considered as
377 photometric standards.
378 - Must include a field ``flux``; the flux used to impose a magnitude limit and also to calibrate
379 the data to (unless a color term is specified, in which case ColorTerm.primary is used;
380 See https://jira.lsstcorp.org/browse/DM-933).
381 - May include a field ``stargal``; if present, True means that the object is a star.
382 - May include a field ``var``; if present, True means that the object is variable.
383
384 The measured sources:
385 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration.
386 """
387 import lsstDebug
388
389 display = lsstDebug.Info(__name__).display
390 displaySources = display and lsstDebug.Info(__name__).displaySources
391 self.scatterPlot = display and lsstDebug.Info(__name__).scatterPlot
392
393 if self.scatterPlot:
394 from matplotlib import pyplot
395 try:
396 self.fig.clf()
397 except Exception:
398 self.fig = pyplot.figure()
399
400 filterLabel = exposure.getFilter()
401
402 # Match sources
403 matchResults = self.match.run(sourceCat, filterLabel.bandLabel)
404 matches = matchResults.matches
405
406 reserveResults = self.reserve.run([mm.second for mm in matches], expId=expId)
407 if displaySources:
408 self.displaySources(exposure, matches, reserveResults.reserved)
409 if reserveResults.reserved.sum() > 0:
410 matches = [mm for mm, use in zip(matches, reserveResults.use) if use]
411 if len(matches) == 0:
412 raise MatcherFailure("No matches to use for photocal.")
413 if self.usedKey is not None:
414 for mm in matches:
415 mm.second.set(self.usedKey, True)
416
417 # Prepare for fitting
418 sourceKeys = self.getSourceKeys(matches[0].second.schema)
419 arrays = self.extractMagArrays(matches, filterLabel, sourceKeys)
420
421 # Fit for zeropoint
422 r = self.getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
423 self.log.info("Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
424
425 # Prepare the results
426 flux0 = 10**(0.4*r.zp) # Flux of mag=0 star
427 flux0err = 0.4*math.log(10)*flux0*r.sigma # Error in flux0
428 photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err)
429 self.log.info("Photometric calibration factor (nJy/ADU): %f +/- %f",
430 photoCalib.getCalibrationMean(),
431 photoCalib.getCalibrationErr())
432
433 return pipeBase.Struct(
434 photoCalib=photoCalib,
435 arrays=arrays,
436 matches=matches,
437 matchMeta=matchResults.matchMeta,
438 zp=r.zp,
439 sigma=r.sigma,
440 ngood=r.ngood,
441 )
442
443 def displaySources(self, exposure, matches, reserved, frame=1):
444 """Display sources we'll use for photocal.
445
446 Sources that will be actually used will be green.
447 Sources reserved from the fit will be red.
448
449 Parameters
450 ----------
451 exposure : `lsst.afw.image.ExposureF`
452 Exposure to display.
453 matches : `list` of `lsst.afw.table.RefMatch`
454 Matches used for photocal.
455 reserved : `numpy.ndarray` of type `bool`
456 Boolean array indicating sources that are reserved.
457 frame : `int`, optional
458 Frame number for display.
459 """
460 disp = afwDisplay.getDisplay(frame=frame)
461 disp.mtv(exposure, title="photocal")
462 with disp.Buffering():
463 for mm, rr in zip(matches, reserved):
464 x, y = mm.second.getCentroid()
465 ctype = afwDisplay.RED if rr else afwDisplay.GREEN
466 disp.dot("o", x, y, size=4, ctype=ctype)
467
468 def getZeroPoint(self, src, ref, srcErr=None, zp0=None):
469 """Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars).
470
471 Returns
472 -------
473 result : `lsst.pipe.base.Struct`
474 Results as a struct with attributes:
475
476 ``zp``
477 Photometric zero point (mag, `float`).
478 ``sigma``
479 Standard deviation of fit of photometric zero point (mag, `float`).
480 ``ngood``
481 Number of sources used to fit photometric zero point (`int`).
482
483 Notes
484 -----
485 We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists:
486 - We use the median/interquartile range to estimate the position to clip around, and the
487 "sigma" to use.
488 - We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently
489 large estimate will prevent the clipping from ever taking effect.
490 - Rather than start with the median we start with a crude mode. This means that a set of magnitude
491 residuals with a tight core and asymmetrical outliers will start in the core. We use the width of
492 this core to set our maximum sigma (see second bullet).
493 """
494 sigmaMax = self.config.sigmaMax
495
496 dmag = ref - src
497
498 indArr = np.argsort(dmag)
499 dmag = dmag[indArr]
500
501 if srcErr is not None:
502 dmagErr = srcErr[indArr]
503 else:
504 dmagErr = np.ones(len(dmag))
505
506 # need to remove nan elements to avoid errors in stats calculation with numpy
507 ind_noNan = np.array([i for i in range(len(dmag))
508 if (not np.isnan(dmag[i]) and not np.isnan(dmagErr[i]))])
509 dmag = dmag[ind_noNan]
510 dmagErr = dmagErr[ind_noNan]
511
512 IQ_TO_STDEV = 0.741301109252802 # 1 sigma in units of interquartile (assume Gaussian)
513
514 npt = len(dmag)
515 ngood = npt
516 good = None # set at end of first iteration
517 for i in range(self.config.nIter):
518 if i > 0:
519 npt = sum(good)
520
521 center = None
522 if i == 0:
523 #
524 # Start by finding the mode
525 #
526 nhist = 20
527 try:
528 hist, edges = np.histogram(dmag, nhist, new=True)
529 except TypeError:
530 hist, edges = np.histogram(dmag, nhist) # they removed new=True around numpy 1.5
531 imode = np.arange(nhist)[np.where(hist == hist.max())]
532
533 if imode[-1] - imode[0] + 1 == len(imode): # Multiple modes, but all contiguous
534 if zp0:
535 center = zp0
536 else:
537 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
538
539 peak = sum(hist[imode])/len(imode) # peak height
540
541 # Estimate FWHM of mode
542 j = imode[0]
543 while j >= 0 and hist[j] > 0.5*peak:
544 j -= 1
545 j = max(j, 0)
546 q1 = dmag[sum(hist[range(j)])]
547
548 j = imode[-1]
549 while j < nhist and hist[j] > 0.5*peak:
550 j += 1
551 j = min(j, nhist - 1)
552 j = min(sum(hist[range(j)]), npt - 1)
553 q3 = dmag[j]
554
555 if q1 == q3:
556 q1 = dmag[int(0.25*npt)]
557 q3 = dmag[int(0.75*npt)]
558
559 sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian)
560
561 if sigmaMax is None:
562 sigmaMax = 2*sig # upper bound on st. dev. for clipping. multiplier is a heuristic
563
564 self.log.debug("Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
565
566 else:
567 if sigmaMax is None:
568 sigmaMax = dmag[-1] - dmag[0]
569
570 center = np.median(dmag)
571 q1 = dmag[int(0.25*npt)]
572 q3 = dmag[int(0.75*npt)]
573 sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian)
574
575 if center is None: # usually equivalent to (i > 0)
576 gdmag = dmag[good]
577 if self.config.useMedian:
578 center = np.median(gdmag)
579 else:
580 gdmagErr = dmagErr[good]
581 center = np.average(gdmag, weights=gdmagErr)
582
583 q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)]
584 q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)]
585
586 sig = IQ_TO_STDEV*(q3 - q1) # estimate of standard deviation
587
588 good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax) # don't clip too softly
589
590 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
591 if self.scatterPlot:
592 try:
593 self.fig.clf()
594
595 axes = self.fig.add_axes((0.1, 0.1, 0.85, 0.80))
596
597 axes.plot(ref[good], dmag[good] - center, "b+")
598 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
599 linestyle='', color='b')
600
601 bad = np.logical_not(good)
602 if len(ref[bad]) > 0:
603 axes.plot(ref[bad], dmag[bad] - center, "r+")
604 axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
605 linestyle='', color='r')
606
607 axes.plot((-100, 100), (0, 0), "g-")
608 for x in (-1, 1):
609 axes.plot((-100, 100), x*0.05*np.ones(2), "g--")
610
611 axes.set_ylim(-1.1, 1.1)
612 axes.set_xlim(24, 13)
613 axes.set_xlabel("Reference")
614 axes.set_ylabel("Reference - Instrumental")
615
616 self.fig.show()
617
618 if self.scatterPlot > 1:
619 reply = None
620 while i == 0 or reply != "c":
621 try:
622 reply = input("Next iteration? [ynhpc] ")
623 except EOFError:
624 reply = "n"
625
626 if reply == "h":
627 print("Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
628 continue
629
630 if reply in ("", "c", "n", "p", "y"):
631 break
632 else:
633 print("Unrecognised response: %s" % reply, file=sys.stderr)
634
635 if reply == "n":
636 break
637 elif reply == "p":
638 import pdb
639 pdb.set_trace()
640 except Exception as e:
641 print("Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
642
643 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
644
645 old_ngood = ngood
646 ngood = sum(good)
647 if ngood == 0:
648 msg = "PhotoCal.getZeroPoint: no good stars remain"
649
650 if i == 0: # failed the first time round -- probably all fell in one bin
651 center = np.average(dmag, weights=dmagErr)
652 msg += " on first iteration; using average of all calibration stars"
653
654 self.log.warning(msg)
655
656 return pipeBase.Struct(
657 zp=center,
658 sigma=sig,
659 ngood=len(dmag))
660 elif ngood == old_ngood:
661 break
662
663 if False:
664 ref = ref[good]
665 dmag = dmag[good]
666 dmagErr = dmagErr[good]
667
668 dmag = dmag[good]
669 dmagErr = dmagErr[good]
670 zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=True)
671 sigma = np.sqrt(1.0/weightSum)
672 return pipeBase.Struct(
673 zp=zp,
674 sigma=sigma,
675 ngood=len(dmag),
676 )
displaySources(self, exposure, matches, reserved, frame=1)
Definition photoCal.py:443
run(self, exposure, sourceCat, expId=0)
Definition photoCal.py:328
extractMagArrays(self, matches, filterLabel, sourceKeys)
Definition photoCal.py:217
getZeroPoint(self, src, ref, srcErr=None, zp0=None)
Definition photoCal.py:468
__init__(self, refObjLoader=None, schema=None, **kwds)
Definition photoCal.py:179