LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
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# @package lsst.pipe.tasks.
23import math
24import sys
25
26import numpy as np
27import astropy.units as u
28
29import lsst.pex.config as pexConf
30import lsst.pipe.base as pipeBase
31from lsst.afw.image import abMagErrFromFluxErr, makePhotoCalibFromCalibZeroPoint
32import lsst.afw.table as afwTable
33from lsst.meas.astrom import DirectMatchTask, DirectMatchConfigWithoutLoader
34import lsst.afw.display as afwDisplay
35from lsst.meas.algorithms import getRefFluxField, ReserveSourcesTask
36from lsst.utils.timer import timeMethod
37from .colorterms import ColortermLibrary
38
39__all__ = ["PhotoCalTask", "PhotoCalConfig"]
40
41
42class PhotoCalConfig(pexConf.Config):
43 """Config for PhotoCal"""
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.applyColorTermsapplyColorTerms and self.photoCatNamephotoCatName is None:
105 raise RuntimeError("applyColorTerms=True requires photoCatName is non-None")
106 if self.applyColorTermsapplyColorTerms and len(self.colortermscolorterms.data) == 0:
107 raise RuntimeError("applyColorTerms=True requires colorterms be provided")
108
109 def setDefaults(self):
110 pexConf.Config.setDefaults(self)
111 self.matchmatch.sourceSelection.doFlags = True
112 self.matchmatch.sourceSelection.flags.bad = [
113 "base_PixelFlags_flag_edge",
114 "base_PixelFlags_flag_interpolated",
115 "base_PixelFlags_flag_saturated",
116 ]
117 self.matchmatch.sourceSelection.doUnresolved = True
118
119
120
126
127class PhotoCalTask(pipeBase.Task):
128 r"""!
129@anchor PhotoCalTask_
130
131@brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
132
133@section pipe_tasks_photocal_Contents Contents
134
135 - @ref pipe_tasks_photocal_Purpose
136 - @ref pipe_tasks_photocal_Initialize
137 - @ref pipe_tasks_photocal_IO
138 - @ref pipe_tasks_photocal_Config
139 - @ref pipe_tasks_photocal_Debug
140 - @ref pipe_tasks_photocal_Example
141
142@section pipe_tasks_photocal_Purpose Description
143
144@copybrief PhotoCalTask
145
146Calculate an Exposure's zero-point given a set of flux measurements of stars matched to an input catalogue.
147The type of flux to use is specified by PhotoCalConfig.fluxField.
148
149The algorithm clips outliers iteratively, with parameters set in the configuration.
150
151@note This task can adds fields to the schema, so any code calling this task must ensure that
152these columns are indeed present in the input match list; see @ref pipe_tasks_photocal_Example
153
154@section pipe_tasks_photocal_Initialize Task initialisation
155
156@copydoc \_\_init\_\_
157
158@section pipe_tasks_photocal_IO Inputs/Outputs to the run method
159
160@copydoc run
161
162@section pipe_tasks_photocal_Config Configuration parameters
163
164See @ref PhotoCalConfig
165
166@section pipe_tasks_photocal_Debug Debug variables
167
168The command line task interface supports a
169flag @c -d to import @b debug.py from your @c PYTHONPATH; see
170<a href="https://pipelines.lsst.io/modules/lsstDebug/">the lsstDebug documentation</a>
171for more about @b debug.py files.
172
173The available variables in PhotoCalTask are:
174<DL>
175 <DT> @c display
176 <DD> If True enable other debug outputs
177 <DT> @c displaySources
178 <DD> If True, display the exposure on the display's frame 1 and overlay the source catalogue.
179 <DL>
180 <DT> red o
181 <DD> Reserved objects
182 <DT> green o
183 <DD> Objects used in the photometric calibration
184 </DL>
185 <DT> @c scatterPlot
186 <DD> Make a scatter plot of flux v. reference magnitude as a function of reference magnitude.
187 - good objects in blue
188 - rejected objects in red
189 (if @c scatterPlot is 2 or more, prompt to continue after each iteration)
190</DL>
191
192@section pipe_tasks_photocal_Example A complete example of using PhotoCalTask
193
194This code is in `examples/photoCalTask.py`, and can be run as @em e.g.
195@code
196examples/photoCalTask.py
197@endcode
198@dontinclude photoCalTask.py
199
200Import the tasks (there are some other standard imports; read the file for details)
201@skipline from lsst.pipe.tasks.astrometry
202@skipline measPhotocal
203
204We need to create both our tasks before processing any data as the task constructors
205can add extra columns to the schema which we get from the input catalogue, @c scrCat:
206@skipline getSchema
207
208Astrometry first:
209@skip AstrometryTask.ConfigClass
210@until aTask
211(that @c filterMap line is because our test code doesn't use a filter that the reference catalogue recognises,
212so we tell it to use the @c r band)
213
214Then photometry:
215@skip measPhotocal
216@until pTask
217
218If the schema has indeed changed we need to add the new columns to the source table
219(yes; this should be easier!)
220@skip srcCat
221@until srcCat = cat
222
223We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
224task objects):
225@skip matches
226@until result
227
228We can then unpack and use the results:
229@skip calib
230@until np.log
231
232<HR>
233To investigate the @ref pipe_tasks_photocal_Debug, put something like
234@code{.py}
235 import lsstDebug
236 def DebugInfo(name):
237 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
238 if name.endswith(".PhotoCal"):
239 di.display = 1
240
241 return di
242
243 lsstDebug.Info = DebugInfo
244@endcode
245into your debug.py file and run photoCalTask.py with the @c --debug flag.
246 """
247 ConfigClass = PhotoCalConfig
248 _DefaultName = "photoCal"
249
250 def __init__(self, refObjLoader, schema=None, **kwds):
251 """!Create the photometric calibration task. See PhotoCalTask.init for documentation
252 """
253 pipeBase.Task.__init__(self, **kwds)
254 self.scatterPlotscatterPlot = None
255 self.figfig = None
256 if schema is not None:
257 self.usedKeyusedKey = schema.addField("calib_photometry_used", type="Flag",
258 doc="set if source was used in photometric calibration")
259 else:
260 self.usedKeyusedKey = None
261 self.matchmatch = DirectMatchTask(config=self.config.match, refObjLoader=refObjLoader,
262 name="match", parentTask=self)
263 self.makeSubtask("reserve", columnName="calib_photometry", schema=schema,
264 doc="set if source was reserved from photometric calibration")
265
266 def getSourceKeys(self, schema):
267 """Return a struct containing the source catalog keys for fields used
268 by PhotoCalTask.
269
270
271 Parameters
272 ----------
273 schema : `lsst.afw.table.schema`
274 Schema of the catalog to get keys from.
275
276 Returns
277 -------
278 result : `lsst.pipe.base.Struct`
279 Result struct with components:
280
281 - ``instFlux``: Instrument flux key.
282 - ``instFluxErr``: Instrument flux error key.
283 """
284 instFlux = schema.find(self.config.fluxField).key
285 instFluxErr = schema.find(self.config.fluxField + "Err").key
286 return pipeBase.Struct(instFlux=instFlux, instFluxErr=instFluxErr)
287
288 @timeMethod
289 def extractMagArrays(self, matches, filterLabel, sourceKeys):
290 """!Extract magnitude and magnitude error arrays from the given matches.
291
292 @param[in] matches Reference/source matches, a @link lsst::afw::table::ReferenceMatchVector @endlink
293 @param[in] filterLabel Label of filter being calibrated
294 @param[in] sourceKeys Struct of source catalog keys, as returned by getSourceKeys()
295
296 @return Struct containing srcMag, refMag, srcMagErr, refMagErr, and magErr numpy arrays
297 where magErr is an error in the magnitude; the error in srcMag - refMag
298 If nonzero, config.magErrFloor will be added to magErr *only* (not srcMagErr or refMagErr), as
299 magErr is what is later used to determine the zero point.
300 Struct also contains refFluxFieldList: a list of field names of the reference catalog used for fluxes
301 (1 or 2 strings)
302 @note These magnitude arrays are the @em inputs to the photometric calibration, some may have been
303 discarded by clipping while estimating the calibration (https://jira.lsstcorp.org/browse/DM-813)
304 """
305 srcInstFluxArr = np.array([m.second.get(sourceKeys.instFlux) for m in matches])
306 srcInstFluxErrArr = np.array([m.second.get(sourceKeys.instFluxErr) for m in matches])
307 if not np.all(np.isfinite(srcInstFluxErrArr)):
308 # this is an unpleasant hack; see DM-2308 requesting a better solution
309 self.log.warning("Source catalog does not have flux uncertainties; using sqrt(flux).")
310 srcInstFluxErrArr = np.sqrt(srcInstFluxArr)
311
312 # convert source instFlux from DN to an estimate of nJy
313 referenceFlux = (0*u.ABmag).to_value(u.nJy)
314 srcInstFluxArr = srcInstFluxArr * referenceFlux
315 srcInstFluxErrArr = srcInstFluxErrArr * referenceFlux
316
317 if not matches:
318 raise RuntimeError("No reference stars are available")
319 refSchema = matches[0].first.schema
320
321 applyColorTerms = self.config.applyColorTerms
322 applyCTReason = "config.applyColorTerms is %s" % (self.config.applyColorTerms,)
323 if self.config.applyColorTerms is None:
324 # apply color terms if color term data is available and photoCatName specified
325 ctDataAvail = len(self.config.colorterms.data) > 0
326 photoCatSpecified = self.config.photoCatName is not None
327 applyCTReason += " and data %s available" % ("is" if ctDataAvail else "is not")
328 applyCTReason += " and photoRefCat %s provided" % ("is" if photoCatSpecified else "is not")
329 applyColorTerms = ctDataAvail and photoCatSpecified
330
331 if applyColorTerms:
332 self.log.info("Applying color terms for filter=%r, config.photoCatName=%s because %s",
333 filterLabel.physicalLabel, self.config.photoCatName, applyCTReason)
334 colorterm = self.config.colorterms.getColorterm(filterLabel.physicalLabel,
335 self.config.photoCatName,
336 doRaise=True)
337 refCat = afwTable.SimpleCatalog(matches[0].first.schema)
338
339 # extract the matched refCat as a Catalog for the colorterm code
340 refCat.reserve(len(matches))
341 for x in matches:
342 record = refCat.addNew()
343 record.assign(x.first)
344
345 refMagArr, refMagErrArr = colorterm.getCorrectedMagnitudes(refCat)
346 fluxFieldList = [getRefFluxField(refSchema, filt) for filt in (colorterm.primary,
347 colorterm.secondary)]
348 else:
349 # no colorterms to apply
350 self.log.info("Not applying color terms because %s", applyCTReason)
351 colorterm = None
352
353 fluxFieldList = [getRefFluxField(refSchema, filterLabel.bandLabel)]
354 fluxField = getRefFluxField(refSchema, filterLabel.bandLabel)
355 fluxKey = refSchema.find(fluxField).key
356 refFluxArr = np.array([m.first.get(fluxKey) for m in matches])
357
358 try:
359 fluxErrKey = refSchema.find(fluxField + "Err").key
360 refFluxErrArr = np.array([m.first.get(fluxErrKey) for m in matches])
361 except KeyError:
362 # Reference catalogue may not have flux uncertainties; HACK DM-2308
363 self.log.warning("Reference catalog does not have flux uncertainties for %s;"
364 " using sqrt(flux).", fluxField)
365 refFluxErrArr = np.sqrt(refFluxArr)
366
367 refMagArr = u.Quantity(refFluxArr, u.nJy).to_value(u.ABmag)
368 # HACK convert to Jy until we have a replacement for this (DM-16903)
369 refMagErrArr = abMagErrFromFluxErr(refFluxErrArr*1e-9, refFluxArr*1e-9)
370
371 # compute the source catalog magnitudes and errors
372 srcMagArr = u.Quantity(srcInstFluxArr, u.nJy).to_value(u.ABmag)
373 # Fitting with error bars in both axes is hard
374 # for now ignore reference flux error, but ticket DM-2308 is a request for a better solution
375 # HACK convert to Jy until we have a replacement for this (DM-16903)
376 magErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9)
377 if self.config.magErrFloor != 0.0:
378 magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5
379
380 srcMagErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9)
381
382 good = np.isfinite(srcMagArr) & np.isfinite(refMagArr)
383
384 return pipeBase.Struct(
385 srcMag=srcMagArr[good],
386 refMag=refMagArr[good],
387 magErr=magErrArr[good],
388 srcMagErr=srcMagErrArr[good],
389 refMagErr=refMagErrArr[good],
390 refFluxFieldList=fluxFieldList,
391 )
392
393 @timeMethod
394 def run(self, exposure, sourceCat, expId=0):
395 """!Do photometric calibration - select matches to use and (possibly iteratively) compute
396 the zero point.
397
398 @param[in] exposure Exposure upon which the sources in the matches were detected.
399 @param[in] sourceCat A catalog of sources to use in the calibration
400 (@em i.e. a list of lsst.afw.table.Match with
401 @c first being of type lsst.afw.table.SimpleRecord and @c second type lsst.afw.table.SourceRecord ---
402 the reference object and matched object respectively).
403 (will not be modified except to set the outputField if requested.).
404 @param[in] expId Exposure identifier; used for seeding the random number generator.
405
406 @return Struct of:
407 - photoCalib -- @link lsst::afw::image::PhotoCalib @endlink object containing the calibration
408 - arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays
409 - matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches.
410 - zp ---------- Photometric zero point (mag)
411 - sigma ------- Standard deviation of fit of photometric zero point (mag)
412 - ngood ------- Number of sources used to fit photometric zero point
413
414 The exposure is only used to provide the name of the filter being calibrated (it may also be
415 used to generate debugging plots).
416
417 The reference objects:
418 - Must include a field @c photometric; True for objects which should be considered as
419 photometric standards
420 - Must include a field @c flux; the flux used to impose a magnitude limit and also to calibrate
421 the data to (unless a color term is specified, in which case ColorTerm.primary is used;
422 See https://jira.lsstcorp.org/browse/DM-933)
423 - May include a field @c stargal; if present, True means that the object is a star
424 - May include a field @c var; if present, True means that the object is variable
425
426 The measured sources:
427 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration
428
429 @throws RuntimeError with the following strings:
430
431 <DL>
432 <DT> No matches to use for photocal
433 <DD> No matches are available (perhaps no sources/references were selected by the matcher).
434 <DT> No reference stars are available
435 <DD> No matches are available from which to extract magnitudes.
436 </DL>
437 """
438 import lsstDebug
439
440 display = lsstDebug.Info(__name__).display
441 displaySources = display and lsstDebug.Info(__name__).displaySources
442 self.scatterPlotscatterPlot = display and lsstDebug.Info(__name__).scatterPlot
443
444 if self.scatterPlotscatterPlot:
445 from matplotlib import pyplot
446 try:
447 self.figfig.clf()
448 except Exception:
449 self.figfig = pyplot.figure()
450
451 filterLabel = exposure.getFilter()
452
453 # Match sources
454 matchResults = self.matchmatch.run(sourceCat, filterLabel.bandLabel)
455 matches = matchResults.matches
456
457 reserveResults = self.reserve.run([mm.second for mm in matches], expId=expId)
458 if displaySources:
459 self.displaySourcesdisplaySources(exposure, matches, reserveResults.reserved)
460 if reserveResults.reserved.sum() > 0:
461 matches = [mm for mm, use in zip(matches, reserveResults.use) if use]
462 if len(matches) == 0:
463 raise RuntimeError("No matches to use for photocal")
464 if self.usedKeyusedKey is not None:
465 for mm in matches:
466 mm.second.set(self.usedKeyusedKey, True)
467
468 # Prepare for fitting
469 sourceKeys = self.getSourceKeysgetSourceKeys(matches[0].second.schema)
470 arrays = self.extractMagArraysextractMagArrays(matches, filterLabel, sourceKeys)
471
472 # Fit for zeropoint
473 r = self.getZeroPointgetZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
474 self.log.info("Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
475
476 # Prepare the results
477 flux0 = 10**(0.4*r.zp) # Flux of mag=0 star
478 flux0err = 0.4*math.log(10)*flux0*r.sigma # Error in flux0
479 photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err)
480
481 return pipeBase.Struct(
482 photoCalib=photoCalib,
483 arrays=arrays,
484 matches=matches,
485 zp=r.zp,
486 sigma=r.sigma,
487 ngood=r.ngood,
488 )
489
490 def displaySources(self, exposure, matches, reserved, frame=1):
491 """Display sources we'll use for photocal
492
493 Sources that will be actually used will be green.
494 Sources reserved from the fit will be red.
495
496 Parameters
497 ----------
498 exposure : `lsst.afw.image.ExposureF`
499 Exposure to display.
500 matches : `list` of `lsst.afw.table.RefMatch`
501 Matches used for photocal.
502 reserved : `numpy.ndarray` of type `bool`
503 Boolean array indicating sources that are reserved.
504 frame : `int`
505 Frame number for display.
506 """
507 disp = afwDisplay.getDisplay(frame=frame)
508 disp.mtv(exposure, title="photocal")
509 with disp.Buffering():
510 for mm, rr in zip(matches, reserved):
511 x, y = mm.second.getCentroid()
512 ctype = afwDisplay.RED if rr else afwDisplay.GREEN
513 disp.dot("o", x, y, size=4, ctype=ctype)
514
515 def getZeroPoint(self, src, ref, srcErr=None, zp0=None):
516 """!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars)
517
518 We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists:
519 1. We use the median/interquartile range to estimate the position to clip around, and the
520 "sigma" to use.
521 2. We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently
522 large estimate will prevent the clipping from ever taking effect.
523 3. Rather than start with the median we start with a crude mode. This means that a set of magnitude
524 residuals with a tight core and asymmetrical outliers will start in the core. We use the width of
525 this core to set our maximum sigma (see 2.)
526
527 @return Struct of:
528 - zp ---------- Photometric zero point (mag)
529 - sigma ------- Standard deviation of fit of zero point (mag)
530 - ngood ------- Number of sources used to fit zero point
531 """
532 sigmaMax = self.config.sigmaMax
533
534 dmag = ref - src
535
536 indArr = np.argsort(dmag)
537 dmag = dmag[indArr]
538
539 if srcErr is not None:
540 dmagErr = srcErr[indArr]
541 else:
542 dmagErr = np.ones(len(dmag))
543
544 # need to remove nan elements to avoid errors in stats calculation with numpy
545 ind_noNan = np.array([i for i in range(len(dmag))
546 if (not np.isnan(dmag[i]) and not np.isnan(dmagErr[i]))])
547 dmag = dmag[ind_noNan]
548 dmagErr = dmagErr[ind_noNan]
549
550 IQ_TO_STDEV = 0.741301109252802 # 1 sigma in units of interquartile (assume Gaussian)
551
552 npt = len(dmag)
553 ngood = npt
554 good = None # set at end of first iteration
555 for i in range(self.config.nIter):
556 if i > 0:
557 npt = sum(good)
558
559 center = None
560 if i == 0:
561 #
562 # Start by finding the mode
563 #
564 nhist = 20
565 try:
566 hist, edges = np.histogram(dmag, nhist, new=True)
567 except TypeError:
568 hist, edges = np.histogram(dmag, nhist) # they removed new=True around numpy 1.5
569 imode = np.arange(nhist)[np.where(hist == hist.max())]
570
571 if imode[-1] - imode[0] + 1 == len(imode): # Multiple modes, but all contiguous
572 if zp0:
573 center = zp0
574 else:
575 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
576
577 peak = sum(hist[imode])/len(imode) # peak height
578
579 # Estimate FWHM of mode
580 j = imode[0]
581 while j >= 0 and hist[j] > 0.5*peak:
582 j -= 1
583 j = max(j, 0)
584 q1 = dmag[sum(hist[range(j)])]
585
586 j = imode[-1]
587 while j < nhist and hist[j] > 0.5*peak:
588 j += 1
589 j = min(j, nhist - 1)
590 j = min(sum(hist[range(j)]), npt - 1)
591 q3 = dmag[j]
592
593 if q1 == q3:
594 q1 = dmag[int(0.25*npt)]
595 q3 = dmag[int(0.75*npt)]
596
597 sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian)
598
599 if sigmaMax is None:
600 sigmaMax = 2*sig # upper bound on st. dev. for clipping. multiplier is a heuristic
601
602 self.log.debug("Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
603
604 else:
605 if sigmaMax is None:
606 sigmaMax = dmag[-1] - dmag[0]
607
608 center = np.median(dmag)
609 q1 = dmag[int(0.25*npt)]
610 q3 = dmag[int(0.75*npt)]
611 sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian)
612
613 if center is None: # usually equivalent to (i > 0)
614 gdmag = dmag[good]
615 if self.config.useMedian:
616 center = np.median(gdmag)
617 else:
618 gdmagErr = dmagErr[good]
619 center = np.average(gdmag, weights=gdmagErr)
620
621 q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)]
622 q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)]
623
624 sig = IQ_TO_STDEV*(q3 - q1) # estimate of standard deviation
625
626 good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax) # don't clip too softly
627
628 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
629 if self.scatterPlotscatterPlot:
630 try:
631 self.figfig.clf()
632
633 axes = self.figfig.add_axes((0.1, 0.1, 0.85, 0.80))
634
635 axes.plot(ref[good], dmag[good] - center, "b+")
636 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
637 linestyle='', color='b')
638
639 bad = np.logical_not(good)
640 if len(ref[bad]) > 0:
641 axes.plot(ref[bad], dmag[bad] - center, "r+")
642 axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
643 linestyle='', color='r')
644
645 axes.plot((-100, 100), (0, 0), "g-")
646 for x in (-1, 1):
647 axes.plot((-100, 100), x*0.05*np.ones(2), "g--")
648
649 axes.set_ylim(-1.1, 1.1)
650 axes.set_xlim(24, 13)
651 axes.set_xlabel("Reference")
652 axes.set_ylabel("Reference - Instrumental")
653
654 self.figfig.show()
655
656 if self.scatterPlotscatterPlot > 1:
657 reply = None
658 while i == 0 or reply != "c":
659 try:
660 reply = input("Next iteration? [ynhpc] ")
661 except EOFError:
662 reply = "n"
663
664 if reply == "h":
665 print("Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
666 continue
667
668 if reply in ("", "c", "n", "p", "y"):
669 break
670 else:
671 print("Unrecognised response: %s" % reply, file=sys.stderr)
672
673 if reply == "n":
674 break
675 elif reply == "p":
676 import pdb
677 pdb.set_trace()
678 except Exception as e:
679 print("Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
680
681 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
682
683 old_ngood = ngood
684 ngood = sum(good)
685 if ngood == 0:
686 msg = "PhotoCal.getZeroPoint: no good stars remain"
687
688 if i == 0: # failed the first time round -- probably all fell in one bin
689 center = np.average(dmag, weights=dmagErr)
690 msg += " on first iteration; using average of all calibration stars"
691
692 self.log.warning(msg)
693
694 return pipeBase.Struct(
695 zp=center,
696 sigma=sig,
697 ngood=len(dmag))
698 elif ngood == old_ngood:
699 break
700
701 if False:
702 ref = ref[good]
703 dmag = dmag[good]
704 dmagErr = dmagErr[good]
705
706 dmag = dmag[good]
707 dmagErr = dmagErr[good]
708 zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=True)
709 sigma = np.sqrt(1.0/weightSum)
710 return pipeBase.Struct(
711 zp=zp,
712 sigma=sigma,
713 ngood=len(dmag),
714 )
int min
int max
Record class that must contain a unique ID field and a celestial coordinate field.
Definition: Simple.h:48
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Definition: SortedCatalog.h:42
Record class that contains measurements made on a single exposure.
Definition: Source.h:78
def getZeroPoint(self, src, ref, srcErr=None, zp0=None)
Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars)
Definition: photoCal.py:515
def extractMagArrays(self, matches, filterLabel, sourceKeys)
Extract magnitude and magnitude error arrays from the given matches.
Definition: photoCal.py:289
def __init__(self, refObjLoader, schema=None, **kwds)
Create the photometric calibration task.
Definition: photoCal.py:250
def displaySources(self, exposure, matches, reserved, frame=1)
Definition: photoCal.py:490
def run(self, exposure, sourceCat, expId=0)
Do photometric calibration - select matches to use and (possibly iteratively) compute the zero point.
Definition: photoCal.py:394
def show(frame=None)
Definition: ds9.py:88
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
double abMagErrFromFluxErr(double fluxErr, double flux)
Compute AB magnitude error from flux and flux error in Janskys.
Definition: Calib.h:52
std::shared_ptr< PhotoCalib > makePhotoCalibFromCalibZeroPoint(double instFluxMag0, double instFluxMag0Err)
Construct a PhotoCalib from the deprecated Calib-style instFluxMag0/instFluxMag0Err values.
Definition: PhotoCalib.cc:613
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
Angle abs(Angle const &a)
Definition: Angle.h:106
Lightweight representation of a geometric match between two records.
Definition: Match.h:67