27 import astropy.units 
as u
    31 from lsst.afw.image import abMagFromFlux, abMagErrFromFluxErr, Calib
    36 from .colorterms 
import ColortermLibrary
    38 __all__ = [
"PhotoCalTask", 
"PhotoCalConfig"]
    42     """Config for PhotoCal"""    43     match = pexConf.ConfigField(
"Match to reference catalog",
    44                                 DirectMatchConfigWithoutLoader)
    45     reserve = pexConf.ConfigurableField(target=ReserveSourcesTask, doc=
"Reserve sources from fitting")
    46     fluxField = pexConf.Field(
    48         default=
"slot_CalibFlux_instFlux",
    49         doc=(
"Name of the source instFlux field to use.  The associated flag field\n"    50              "('<name>_flags') will be implicitly included in badFlags."),
    52     applyColorTerms = pexConf.Field(
    55         doc=(
"Apply photometric color terms to reference stars? One of:\n"    56              "None: apply if colorterms and photoCatName are not None;\n"    57              "      fail if color term data is not available for the specified ref catalog and filter.\n"    58              "True: always apply colorterms; fail if color term data is not available for the\n"    59              "      specified reference catalog and filter.\n"    60              "False: do not apply."),
    63     sigmaMax = pexConf.Field(
    66         doc=
"maximum sigma to use when clipping",
    69     nSigma = pexConf.Field(
    74     useMedian = pexConf.Field(
    77         doc=
"use median instead of mean to compute zeropoint",
    79     nIter = pexConf.Field(
    82         doc=
"number of iterations",
    84     colorterms = pexConf.ConfigField(
    85         dtype=ColortermLibrary,
    86         doc=
"Library of photometric reference catalog name: color term dict",
    88     photoCatName = pexConf.Field(
    91         doc=(
"Name of photometric reference catalog; used to select a color term dict in colorterms."    92              " see also applyColorTerms"),
    94     magErrFloor = pexConf.RangeField(
    97         doc=
"Additional magnitude uncertainty to be added in quadrature with measurement errors.",
   102         pexConf.Config.validate(self)
   104             raise RuntimeError(
"applyColorTerms=True requires photoCatName is non-None")
   106             raise RuntimeError(
"applyColorTerms=True requires colorterms be provided")
   109         pexConf.Config.setDefaults(self)
   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",
   116         self.
match.sourceSelection.doUnresolved = 
True   128 @anchor PhotoCalTask_   130 @brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.   132 @section pipe_tasks_photocal_Contents Contents   134  - @ref pipe_tasks_photocal_Purpose   135  - @ref pipe_tasks_photocal_Initialize   136  - @ref pipe_tasks_photocal_IO   137  - @ref pipe_tasks_photocal_Config   138  - @ref pipe_tasks_photocal_Debug   139  - @ref pipe_tasks_photocal_Example   141 @section pipe_tasks_photocal_Purpose    Description   143 @copybrief PhotoCalTask   145 Calculate an Exposure's zero-point given a set of flux measurements of stars matched to an input catalogue.   146 The type of flux to use is specified by PhotoCalConfig.fluxField.   148 The algorithm clips outliers iteratively, with parameters set in the configuration.   150 @note 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 @ref pipe_tasks_photocal_Example   153 @section pipe_tasks_photocal_Initialize Task initialisation   155 @copydoc \_\_init\_\_   157 @section pipe_tasks_photocal_IO         Inputs/Outputs to the run method   161 @section pipe_tasks_photocal_Config       Configuration parameters   163 See @ref PhotoCalConfig   165 @section pipe_tasks_photocal_Debug              Debug variables   167 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a   168 flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files.   170 The available variables in PhotoCalTask are:   173   <DD> If True enable other debug outputs   174   <DT> @c displaySources   175   <DD> If True, display the exposure on ds9's frame 1 and overlay the source catalogue.   178       <DD> Reserved objects   180       <DD> Objects used in the photometric calibration   183   <DD> Make a scatter plot of flux v. reference magnitude as a function of reference magnitude.   184     - good objects in blue   185     - rejected objects in red   186   (if @c scatterPlot is 2 or more, prompt to continue after each iteration)   189 @section pipe_tasks_photocal_Example    A complete example of using PhotoCalTask   191 This code is in @link examples/photoCalTask.py@endlink, and can be run as @em e.g.   193 examples/photoCalTask.py   195 @dontinclude photoCalTask.py   197 Import the tasks (there are some other standard imports; read the file for details)   198 @skipline from lsst.pipe.tasks.astrometry   199 @skipline measPhotocal   201 We need to create both our tasks before processing any data as the task constructors   202 can add extra columns to the schema which we get from the input catalogue, @c scrCat:   206 @skip AstrometryTask.ConfigClass   208 (that @c filterMap line is because our test code doesn't use a filter that the reference catalogue recognises,   209 so we tell it to use the @c r band)   215 If the schema has indeed changed we need to add the new columns to the source table   216 (yes; this should be easier!)   220 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same   225 We can then unpack and use the results:   230 To investigate the @ref pipe_tasks_photocal_Debug, put something like   234         di = lsstDebug.getInfo(name)        # N.b. lsstDebug.Info(name) would call us recursively   235         if name.endswith(".PhotoCal"):   240     lsstDebug.Info = DebugInfo   242 into your debug.py file and run photoCalTask.py with the @c --debug flag.   244     ConfigClass = PhotoCalConfig
   245     _DefaultName = 
"photoCal"   247     def __init__(self, refObjLoader, schema=None, **kwds):
   248         """!Create the photometric calibration task.  See PhotoCalTask.init for documentation   250         pipeBase.Task.__init__(self, **kwds)
   253         if schema 
is not None:
   254             self.
usedKey = schema.addField(
"calib_photometry_used", type=
"Flag",
   255                                            doc=
"set if source was used in photometric calibration")
   259                                      name=
"match", parentTask=self)
   260         self.makeSubtask(
"reserve", columnName=
"calib_photometry", schema=schema,
   261                          doc=
"set if source was reserved from photometric calibration")
   264         """Return a struct containing the source catalog keys for fields used   270         schema : `lsst.afw.table.schema`   271             Schema of the catalog to get keys from.   275         result : `lsst.pipe.base.Struct`   276             Result struct with components:   278             - ``instFlux``: Instrument flux key.   279             - ``instFluxErr``: Instrument flux error key.   281         instFlux = schema.find(self.config.fluxField).key
   282         instFluxErr = schema.find(self.config.fluxField + 
"Err").key
   283         return pipeBase.Struct(instFlux=instFlux, instFluxErr=instFluxErr)
   287         """!Extract magnitude and magnitude error arrays from the given matches.   289         @param[in] matches Reference/source matches, a @link lsst::afw::table::ReferenceMatchVector@endlink   290         @param[in] filterName  Name of filter being calibrated   291         @param[in] sourceKeys  Struct of source catalog keys, as returned by getSourceKeys()   293         @return Struct containing srcMag, refMag, srcMagErr, refMagErr, and magErr numpy arrays   294         where magErr is an error in the magnitude; the error in srcMag - refMag   295         If nonzero, config.magErrFloor will be added to magErr *only* (not srcMagErr or refMagErr), as   296         magErr is what is later used to determine the zero point.   297         Struct also contains refFluxFieldList: a list of field names of the reference catalog used for fluxes   299         @note These magnitude arrays are the @em inputs to the photometric calibration, some may have been   300         discarded by clipping while estimating the calibration (https://jira.lsstcorp.org/browse/DM-813)   302         srcInstFluxArr = np.array([m.second.get(sourceKeys.instFlux) 
for m 
in matches])
   303         srcInstFluxErrArr = np.array([m.second.get(sourceKeys.instFluxErr) 
for m 
in matches])
   304         if not np.all(np.isfinite(srcInstFluxErrArr)):
   306             self.log.
warn(
"Source catalog does not have flux uncertainties; using sqrt(flux).")
   307             srcInstFluxErrArr = np.sqrt(srcInstFluxArr)
   310         JanskysPerABFlux = 3631.0
   311         srcInstFluxArr = srcInstFluxArr * JanskysPerABFlux
   312         srcInstFluxErrArr = srcInstFluxErrArr * JanskysPerABFlux
   315             raise RuntimeError(
"No reference stars are available")
   316         refSchema = matches[0].first.schema
   318         applyColorTerms = self.config.applyColorTerms
   319         applyCTReason = 
"config.applyColorTerms is %s" % (self.config.applyColorTerms,)
   320         if self.config.applyColorTerms 
is None:
   322             ctDataAvail = len(self.config.colorterms.data) > 0
   323             photoCatSpecified = self.config.photoCatName 
is not None   324             applyCTReason += 
" and data %s available" % (
"is" if ctDataAvail 
else "is not")
   325             applyCTReason += 
" and photoRefCat %s provided" % (
"is" if photoCatSpecified 
else "is not")
   326             applyColorTerms = ctDataAvail 
and photoCatSpecified
   329             self.log.
info(
"Applying color terms for filterName=%r, config.photoCatName=%s because %s",
   330                           filterName, self.config.photoCatName, applyCTReason)
   331             colorterm = self.config.colorterms.getColorterm(
   332                 filterName=filterName, photoCatName=self.config.photoCatName, doRaise=
True)
   336             refCat.reserve(len(matches))
   338                 record = refCat.addNew()
   339                 record.assign(x.first)
   341             refMagArr, refMagErrArr = colorterm.getCorrectedMagnitudes(refCat, filterName)
   342             fluxFieldList = [
getRefFluxField(refSchema, filt) 
for filt 
in (colorterm.primary,
   343                                                                            colorterm.secondary)]
   346             self.log.
info(
"Not applying color terms because %s", applyCTReason)
   351             fluxKey = refSchema.find(fluxField).key
   352             refFluxArr = np.array([m.first.get(fluxKey) 
for m 
in matches])
   355                 fluxErrKey = refSchema.find(fluxField + 
"Err").key
   356                 refFluxErrArr = np.array([m.first.get(fluxErrKey) 
for m 
in matches])
   359                 self.log.
warn(
"Reference catalog does not have flux uncertainties for %s; using sqrt(flux).",
   361                 refFluxErrArr = np.sqrt(refFluxArr)
   363             refMagArr = u.Quantity(refFluxArr, u.Jy).to_value(u.ABmag)
   367         srcMagArr = np.array([
abMagFromFlux(sf) 
for sf 
in srcInstFluxArr])
   371         if self.config.magErrFloor != 0.0:
   372             magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5
   376         good = np.isfinite(srcMagArr) & np.isfinite(refMagArr)
   378         return pipeBase.Struct(
   379             srcMag=srcMagArr[good],
   380             refMag=refMagArr[good],
   381             magErr=magErrArr[good],
   382             srcMagErr=srcMagErrArr[good],
   383             refMagErr=refMagErrArr[good],
   384             refFluxFieldList=fluxFieldList,
   388     def run(self, exposure, sourceCat, expId=0):
   389         """!Do photometric calibration - select matches to use and (possibly iteratively) compute   392         @param[in]  exposure  Exposure upon which the sources in the matches were detected.   393         @param[in]  sourceCat  A catalog of sources to use in the calibration   394         (@em i.e. a list of lsst.afw.table.Match with   395         @c first being of type lsst.afw.table.SimpleRecord and @c second type lsst.afw.table.SourceRecord ---   396         the reference object and matched object respectively).   397         (will not be modified  except to set the outputField if requested.).   400          - calib -------  @link lsst::afw::image::Calib@endlink object containing the zero point   401          - arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays   402          - matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches.   403          - zp ---------- Photometric zero point (mag)   404          - sigma ------- Standard deviation of fit of photometric zero point (mag)   405          - ngood ------- Number of sources used to fit photometric zero point   407         The exposure is only used to provide the name of the filter being calibrated (it may also be   408         used to generate debugging plots).   410         The reference objects:   411          - Must include a field @c photometric; True for objects which should be considered as   412             photometric standards   413          - Must include a field @c flux; the flux used to impose a magnitude limit and also to calibrate   414             the data to (unless a color term is specified, in which case ColorTerm.primary is used;   415             See https://jira.lsstcorp.org/browse/DM-933)   416          - May include a field @c stargal; if present, True means that the object is a star   417          - May include a field @c var; if present, True means that the object is variable   419         The measured sources:   420         - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration   422         @throws RuntimeError with the following strings:   425         <DT> No matches to use for photocal   426         <DD> No matches are available (perhaps no sources/references were selected by the matcher).   427         <DT> No reference stars are available   428         <DD> No matches are available from which to extract magnitudes.   434         displaySources = display 
and lsstDebug.Info(__name__).displaySources
   438             from matplotlib 
import pyplot
   442                 self.
fig = pyplot.figure()
   444         filterName = exposure.getFilter().getName()
   447         matchResults = self.
match.
run(sourceCat, filterName)
   448         matches = matchResults.matches
   449         reserveResults = self.reserve.
run([mm.second 
for mm 
in matches], expId=expId)
   452         if reserveResults.reserved.sum() > 0:
   453             matches = [mm 
for mm, use 
in zip(matches, reserveResults.use) 
if use]
   454         if len(matches) == 0:
   455             raise RuntimeError(
"No matches to use for photocal")
   458                 mm.second.set(self.
usedKey, 
True)
   462         arrays = self.
extractMagArrays(matches=matches, filterName=filterName, sourceKeys=sourceKeys)
   465         r = self.
getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
   466         self.log.
info(
"Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
   469         flux0 = 10**(0.4*r.zp)  
   470         flux0err = 0.4*math.log(10)*flux0*r.sigma  
   472         calib.setFluxMag0(flux0, flux0err)
   474         return pipeBase.Struct(
   484         """Display sources we'll use for photocal   486         Sources that will be actually used will be green.   487         Sources reserved from the fit will be red.   491         exposure : `lsst.afw.image.ExposureF`   493         matches : `list` of `lsst.afw.table.RefMatch`   494             Matches used for photocal.   495         reserved : `numpy.ndarray` of type `bool`   496             Boolean array indicating sources that are reserved.   498             Frame number for display.   500         ds9.mtv(exposure, frame=frame, title=
"photocal")
   501         with ds9.Buffering():
   502             for mm, rr 
in zip(matches, reserved):
   503                 x, y = mm.second.getCentroid()
   504                 ctype = ds9.RED 
if rr 
else ds9.GREEN
   505                 ds9.dot(
"o", x, y, size=4, frame=frame, ctype=ctype)
   508         """!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars)   510         We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists:   511         1.  We use the median/interquartile range to estimate the position to clip around, and the   513         2.  We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently   514         large estimate will prevent the clipping from ever taking effect.   515         3.  Rather than start with the median we start with a crude mode.  This means that a set of magnitude   516         residuals with a tight core and asymmetrical outliers will start in the core.  We use the width of   517         this core to set our maximum sigma (see 2.)   520          - zp ---------- Photometric zero point (mag)   521          - sigma ------- Standard deviation of fit of zero point (mag)   522          - ngood ------- Number of sources used to fit zero point   524         sigmaMax = self.config.sigmaMax
   528         indArr = np.argsort(dmag)
   531         if srcErr 
is not None:
   532             dmagErr = srcErr[indArr]
   534             dmagErr = np.ones(len(dmag))
   537         ind_noNan = np.array([i 
for i 
in range(len(dmag))
   538                               if (
not np.isnan(dmag[i]) 
and not np.isnan(dmagErr[i]))])
   539         dmag = dmag[ind_noNan]
   540         dmagErr = dmagErr[ind_noNan]
   542         IQ_TO_STDEV = 0.741301109252802    
   547         for i 
in range(self.config.nIter):
   558                     hist, edges = np.histogram(dmag, nhist, new=
True)
   560                     hist, edges = np.histogram(dmag, nhist)  
   561                 imode = np.arange(nhist)[np.where(hist == hist.max())]
   563                 if imode[-1] - imode[0] + 1 == len(imode):  
   567                         center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
   569                     peak = sum(hist[imode])/len(imode)  
   573                     while j >= 0 
and hist[j] > 0.5*peak:
   576                     q1 = dmag[sum(hist[range(j)])]
   579                     while j < nhist 
and hist[j] > 0.5*peak:
   581                     j = 
min(j, nhist - 1)
   582                     j = 
min(sum(hist[range(j)]), npt - 1)
   586                         q1 = dmag[
int(0.25*npt)]
   587                         q3 = dmag[
int(0.75*npt)]
   594                     self.log.
debug(
"Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
   598                         sigmaMax = dmag[-1] - dmag[0]
   600                     center = np.median(dmag)
   601                     q1 = dmag[
int(0.25*npt)]
   602                     q3 = dmag[
int(0.75*npt)]
   607                 if self.config.useMedian:
   608                     center = np.median(gdmag)
   610                     gdmagErr = dmagErr[good]
   611                     center = np.average(gdmag, weights=gdmagErr)
   613                 q3 = gdmag[
min(
int(0.75*npt + 0.5), npt - 1)]
   614                 q1 = gdmag[
min(
int(0.25*npt + 0.5), npt - 1)]
   616                 sig = IQ_TO_STDEV*(q3 - q1)     
   618             good = 
abs(dmag - center) < self.config.nSigma*
min(sig, sigmaMax)  
   625                     axes = self.
fig.add_axes((0.1, 0.1, 0.85, 0.80))
   627                     axes.plot(ref[good], dmag[good] - center, 
"b+")
   628                     axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
   629                                   linestyle=
'', color=
'b')
   631                     bad = np.logical_not(good)
   632                     if len(ref[bad]) > 0:
   633                         axes.plot(ref[bad], dmag[bad] - center, 
"r+")
   634                         axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
   635                                       linestyle=
'', color=
'r')   637                     axes.plot((-100, 100), (0, 0), "g-")
   639                         axes.plot((-100, 100), x*0.05*np.ones(2), 
"g--")
   641                     axes.set_ylim(-1.1, 1.1)
   642                     axes.set_xlim(24, 13)
   643                     axes.set_xlabel(
"Reference")
   644                     axes.set_ylabel(
"Reference - Instrumental")
   650                         while i == 0 
or reply != 
"c":
   652                                 reply = input(
"Next iteration? [ynhpc] ")
   657                                 print(
"Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
   660                             if reply 
in (
"", 
"c", 
"n", 
"p", 
"y"):
   663                                 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
   670                 except Exception 
as e:
   671                     print(
"Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
   678                 msg = 
"PhotoCal.getZeroPoint: no good stars remain"   681                     center = np.average(dmag, weights=dmagErr)
   682                     msg += 
" on first iteration; using average of all calibration stars"   686                 return pipeBase.Struct(
   690             elif ngood == old_ngood:
   696                 dmagErr = dmagErr[good]
   699         dmagErr = dmagErr[good]
   700         zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=
True)
   701         sigma = np.sqrt(1.0/weightSum)
   702         return pipeBase.Struct(
 
def __init__(self, refObjLoader, schema=None, kwds)
Create the photometric calibration task. 
Angle abs(Angle const &a)
def run(self, exposure, sourceCat, expId=0)
Do photometric calibration - select matches to use and (possibly iteratively) compute the zero point...
def getSourceKeys(self, schema)
double abMagErrFromFluxErr(double fluxErr, double flux)
Compute AB magnitude error from flux and flux error in Janskys. 
def extractMagArrays(self, matches, filterName, sourceKeys)
Extract magnitude and magnitude error arrays from the given matches. 
Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector. 
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
double abMagFromFlux(double flux)
Compute AB magnitude from flux in Janskys. 
Describe an exposure's calibration. 
def getRefFluxField(schema, filterName=None)
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID. 
def getZeroPoint(self, src, ref, srcErr=None, zp0=None)
Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) ...
def displaySources(self, exposure, matches, reserved, frame=1)