23 """Make a look-up-table (LUT) for FGCM calibration. 
   25 This task computes a look-up-table for the range in expected atmosphere 
   26 variation and variation in instrumental throughput (as tracked by the 
   27 transmission_filter products).  By pre-computing linearized integrals, 
   28 the FGCM fit is orders of magnitude faster for stars with a broad range 
   29 of colors and observing bands, yielding precision at the 1-2 mmag level. 
   31 Computing a LUT requires running MODTRAN or with a pre-generated 
   32 atmosphere table packaged with fgcm. 
   40 import lsst.pex.config 
as pexConfig
 
   49 __all__ = [
'FgcmMakeLutParametersConfig', 
'FgcmMakeLutConfig', 
'FgcmMakeLutTask',
 
   54     """Config for parameters if atmosphereTableName not available""" 
   57     elevation = pexConfig.Field(
 
   58         doc=
"Telescope elevation (m)",
 
   62     pmbRange = pexConfig.ListField(
 
   63         doc=(
"Barometric Pressure range (millibar) " 
   64              "Recommended range depends on the site."),
 
   68     pmbSteps = pexConfig.Field(
 
   69         doc=
"Barometric Pressure number of steps",
 
   73     pwvRange = pexConfig.ListField(
 
   74         doc=(
"Precipitable Water Vapor range (mm) " 
   75              "Recommended range depends on the site."),
 
   79     pwvSteps = pexConfig.Field(
 
   80         doc=
"Precipitable Water Vapor number of steps",
 
   84     o3Range = pexConfig.ListField(
 
   85         doc=
"Ozone range (dob)",
 
   87         default=[220.0, 310.0],
 
   89     o3Steps = pexConfig.Field(
 
   90         doc=
"Ozone number of steps",
 
   94     tauRange = pexConfig.ListField(
 
   95         doc=
"Aerosol Optical Depth range (unitless)",
 
   97         default=[0.002, 0.35],
 
   99     tauSteps = pexConfig.Field(
 
  100         doc=
"Aerosol Optical Depth number of steps",
 
  104     alphaRange = pexConfig.ListField(
 
  105         doc=
"Aerosol alpha range (unitless)",
 
  109     alphaSteps = pexConfig.Field(
 
  110         doc=
"Aerosol alpha number of steps",
 
  114     zenithRange = pexConfig.ListField(
 
  115         doc=
"Zenith angle range (degree)",
 
  119     zenithSteps = pexConfig.Field(
 
  120         doc=
"Zenith angle number of steps",
 
  126     pmbStd = pexConfig.Field(
 
  127         doc=(
"Standard Atmosphere pressure (millibar); " 
  128              "Recommended default depends on the site."),
 
  132     pwvStd = pexConfig.Field(
 
  133         doc=(
"Standard Atmosphere PWV (mm); " 
  134              "Recommended default depends on the site."),
 
  138     o3Std = pexConfig.Field(
 
  139         doc=
"Standard Atmosphere O3 (dob)",
 
  143     tauStd = pexConfig.Field(
 
  144         doc=
"Standard Atmosphere aerosol optical depth",
 
  148     alphaStd = pexConfig.Field(
 
  149         doc=
"Standard Atmosphere aerosol alpha",
 
  153     airmassStd = pexConfig.Field(
 
  154         doc=(
"Standard Atmosphere airmass; " 
  155              "Recommended default depends on the survey strategy."),
 
  159     lambdaNorm = pexConfig.Field(
 
  160         doc=
"Aerosol Optical Depth normalization wavelength (Angstrom)",
 
  164     lambdaStep = pexConfig.Field(
 
  165         doc=
"Wavelength step for generating atmospheres (nm)",
 
  169     lambdaRange = pexConfig.ListField(
 
  170         doc=
"Wavelength range for LUT (Angstrom)",
 
  172         default=[3000.0, 11000.0],
 
  177     """Config for FgcmMakeLutTask""" 
  179     filterNames = pexConfig.ListField(
 
  180         doc=
"Filter names to build LUT ('short' names)",
 
  184     stdFilterNames = pexConfig.ListField(
 
  185         doc=(
"Standard filterNames ('short' names). " 
  186              "Each filter in filterName will be calibrated to a matched " 
  187              "stdFilterName.  In regular usage, one has g->g, r->r, ... " 
  188              "In the case of HSC, one would have g->g, r->r2, r2->r2, ... " 
  189              "which allows replacement (or time-variable) filters to be " 
  190              "properly cross-calibrated."),
 
  194     atmosphereTableName = pexConfig.Field(
 
  195         doc=
"FGCM name or filename of precomputed atmospheres",
 
  200     parameters = pexConfig.ConfigField(
 
  201         doc=
"Atmosphere parameters (required if no atmosphereTableName)",
 
  202         dtype=FgcmMakeLutParametersConfig,
 
  208         Validate the config parameters. 
  210         This method behaves differently from the parent validate in the case 
  211         that atmosphereTableName is set.  In this case, the config values 
  212         for standard values, step sizes, and ranges are loaded 
  213         directly from the specified atmosphereTableName. 
  216         self._fields[
'filterNames'].
validate(self)
 
  217         self._fields[
'stdFilterNames'].
validate(self)
 
  224                 raise RuntimeError(
"Could not find atmosphereTableName: %s" %
 
  228             self._fields[
'parameters'].
validate(self)
 
  232     """Subclass of TaskRunner for fgcmMakeLutTask 
  234     fgcmMakeLutTask.run() takes one argument, the butler, and 
  235     does not run on any data in the repository. 
  236     This runner does not use any parallelization. 
  242         Return a list with one element, the butler. 
  244         return [parsedCmd.butler]
 
  250         butler: `lsst.daf.persistence.Butler` 
  254         exitStatus: `list` with `pipeBase.Struct` 
  255            exitStatus (0: success; 1: failure) 
  257         task = self.TaskClass(config=self.config, log=self.log)
 
  261             task.runDataRef(butler)
 
  264                 task.runDataRef(butler)
 
  265             except Exception 
as e:
 
  267                 task.log.fatal(
"Failed: %s" % e)
 
  268                 if not isinstance(e, pipeBase.TaskError):
 
  269                     traceback.print_exc(file=sys.stderr)
 
  271         task.writeMetadata(butler)
 
  274         return [pipeBase.Struct(exitStatus=exitStatus)]
 
  278         Run the task, with no multiprocessing 
  282         parsedCmd: ArgumentParser parsed command line 
  287         if self.precall(parsedCmd):
 
  290             resultList = self(targetList[0])
 
  297     Make Look-Up Table for FGCM. 
  299     This task computes a look-up-table for the range in expected atmosphere 
  300     variation and variation in instrumental throughput (as tracked by the 
  301     transmission_filter products).  By pre-computing linearized integrals, 
  302     the FGCM fit is orders of magnitude faster for stars with a broad range 
  303     of colors and observing bands, yielding precision at the 1-2 mmag level. 
  305     Computing a LUT requires running MODTRAN or with a pre-generated 
  306     atmosphere table packaged with fgcm. 
  309     ConfigClass = FgcmMakeLutConfig
 
  310     RunnerClass = FgcmMakeLutRunner
 
  311     _DefaultName = 
"fgcmMakeLut" 
  315         Instantiate an fgcmMakeLutTask. 
  319         butler: `lsst.daf.persistence.Butler` 
  322         pipeBase.CmdLineTask.__init__(self, **kwargs)
 
  325     def _getMetadataName(self):
 
  331         Make a Look-Up Table for FGCM 
  335         butler:  `lsst.daf.persistence.Butler` 
  340     def _fgcmMakeLut(self, butler):
 
  342         Make a FGCM Look-up Table 
  346         butler: `lsst.daf.persistence.Butler` 
  350         camera = butler.get(
'camera')
 
  354         self.log.
info(
"Found %d ccds for look-up table" % (nCcd))
 
  362         self.log.
info(
"Making the LUT maker object")
 
  370         throughputLambda = np.arange(self.
fgcmLutMaker.lambdaRange[0],
 
  374         self.log.
info(
"Built throughput lambda, %.1f-%.1f, step %.2f" %
 
  375                       (throughputLambda[0], throughputLambda[-1],
 
  376                        throughputLambda[1]-throughputLambda[0]))
 
  379         for i, filterName 
in enumerate(self.config.filterNames):
 
  381             tDict[
'LAMBDA'] = throughputLambda
 
  382             for ccdIndex, detector 
in enumerate(camera):
 
  384             throughputDict[filterName] = tDict
 
  390         self.log.
info(
"Making LUT")
 
  397         filterNameString = comma.join(self.config.filterNames)
 
  398         stdFilterNameString = comma.join(self.config.stdFilterNames)
 
  400         atmosphereTableName = 
'NoTableWasUsed' 
  401         if self.config.atmosphereTableName 
is not None:
 
  402             atmosphereTableName = self.config.atmosphereTableName
 
  404         lutSchema = self.
_makeLutSchema(filterNameString, stdFilterNameString,
 
  407         lutCat = self.
_makeLutCat(lutSchema, filterNameString,
 
  408                                   stdFilterNameString, atmosphereTableName)
 
  409         butler.put(lutCat, 
'fgcmLookUpTable')
 
  411     def _createLutConfig(self, nCcd):
 
  413         Create the fgcmLut config dictionary 
  418            Number of CCDs in the camera 
  423         lutConfig[
'logger'] = self.log
 
  424         lutConfig[
'filterNames'] = self.config.filterNames
 
  425         lutConfig[
'stdFilterNames'] = self.config.stdFilterNames
 
  426         lutConfig[
'nCCD'] = nCcd
 
  429         if self.config.atmosphereTableName 
is not None:
 
  430             lutConfig[
'atmosphereTableName'] = self.config.atmosphereTableName
 
  433             lutConfig[
'elevation'] = self.config.parameters.elevation
 
  434             lutConfig[
'pmbRange'] = self.config.parameters.pmbRange
 
  435             lutConfig[
'pmbSteps'] = self.config.parameters.pmbSteps
 
  436             lutConfig[
'pwvRange'] = self.config.parameters.pwvRange
 
  437             lutConfig[
'pwvSteps'] = self.config.parameters.pwvSteps
 
  438             lutConfig[
'o3Range'] = self.config.parameters.o3Range
 
  439             lutConfig[
'o3Steps'] = self.config.parameters.o3Steps
 
  440             lutConfig[
'tauRange'] = self.config.parameters.tauRange
 
  441             lutConfig[
'tauSteps'] = self.config.parameters.tauSteps
 
  442             lutConfig[
'alphaRange'] = self.config.parameters.alphaRange
 
  443             lutConfig[
'alphaSteps'] = self.config.parameters.alphaSteps
 
  444             lutConfig[
'zenithRange'] = self.config.parameters.zenithRange
 
  445             lutConfig[
'zenithSteps'] = self.config.parameters.zenithSteps
 
  446             lutConfig[
'pmbStd'] = self.config.parameters.pmbStd
 
  447             lutConfig[
'pwvStd'] = self.config.parameters.pwvStd
 
  448             lutConfig[
'o3Std'] = self.config.parameters.o3Std
 
  449             lutConfig[
'tauStd'] = self.config.parameters.tauStd
 
  450             lutConfig[
'alphaStd'] = self.config.parameters.alphaStd
 
  451             lutConfig[
'airmassStd'] = self.config.parameters.airmassStd
 
  452             lutConfig[
'lambdaRange'] = self.config.parameters.lambdaRange
 
  453             lutConfig[
'lambdaStep'] = self.config.parameters.lambdaStep
 
  454             lutConfig[
'lambdaNorm'] = self.config.parameters.lambdaNorm
 
  458     def _loadThroughputs(self, butler, camera):
 
  459         """Internal method to load throughput data for filters 
  463         butler: `lsst.daf.persistence.butler.Butler` 
  464            A butler with the transmission info 
  465         camera: `lsst.afw.cameraGeom.Camera` 
  470         for detector 
in camera:
 
  472                                                                      dataId={
'ccd': detector.getId()})
 
  474         for filterName 
in self.config.filterNames:
 
  478             aliases = f.getAliases()
 
  479             aliases.extend(filterName)
 
  480             for alias 
in f.getAliases():
 
  483                                                                        dataId={
'filter': alias})
 
  489                 raise ValueError(
"Could not find transmission for filter %s via any alias." % (filterName))
 
  491     def _getThroughputDetector(self, detector, filterName, throughputLambda):
 
  492         """Internal method to get throughput for a detector. 
  494         Returns the throughput at the center of the detector for a given filter. 
  498         detector: `lsst.afw.cameraGeom._detector.Detector` 
  501            Short name for filter 
  502         throughputLambda: `np.array(dtype=np.float64)` 
  503            Wavelength steps (Angstrom) 
  507         throughput: `np.array(dtype=np.float64)` 
  508            Throughput (max 1.0) at throughputLambda 
  511         c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
 
  512         c.scale(1.0/detector.getPixelSize()[0])  
 
  515                                                        wavelengths=throughputLambda)
 
  518                                                                            wavelengths=throughputLambda)
 
  521                                                                      wavelengths=throughputLambda)
 
  524         throughput = np.clip(throughput, 0.0, 1.0)
 
  528     def _makeLutSchema(self, filterNameString, stdFilterNameString,
 
  529                        atmosphereTableName):
 
  535         filterNameString: `str` 
  536            Combined string of all the filterNames 
  537         stdFilterNameString: `str` 
  538            Combined string of all the standard filterNames 
  539         atmosphereTableName: `str` 
  540            Name of the atmosphere table used to generate LUT 
  544         lutSchema: `afwTable.schema` 
  549         lutSchema.addField(
'tablename', type=str, doc=
'Atmosphere table name',
 
  550                            size=len(atmosphereTableName))
 
  551         lutSchema.addField(
'elevation', type=float, doc=
"Telescope elevation used for LUT")
 
  552         lutSchema.addField(
'filterNames', type=str, doc=
'filterNames in LUT',
 
  553                            size=len(filterNameString))
 
  554         lutSchema.addField(
'stdFilterNames', type=str, doc=
'Standard filterNames in LUT',
 
  555                            size=len(stdFilterNameString))
 
  556         lutSchema.addField(
'pmb', type=
'ArrayD', doc=
'Barometric Pressure',
 
  558         lutSchema.addField(
'pmbFactor', type=
'ArrayD', doc=
'PMB scaling factor',
 
  560         lutSchema.addField(
'pmbElevation', type=np.float64, doc=
'PMB Scaling at elevation')
 
  561         lutSchema.addField(
'pwv', type=
'ArrayD', doc=
'Preciptable Water Vapor',
 
  563         lutSchema.addField(
'o3', type=
'ArrayD', doc=
'Ozone',
 
  565         lutSchema.addField(
'tau', type=
'ArrayD', doc=
'Aerosol optical depth',
 
  567         lutSchema.addField(
'lambdaNorm', type=np.float64, doc=
'AOD wavelength')
 
  568         lutSchema.addField(
'alpha', type=
'ArrayD', doc=
'Aerosol alpha',
 
  570         lutSchema.addField(
'zenith', type=
'ArrayD', doc=
'Zenith angle',
 
  572         lutSchema.addField(
'nCcd', type=np.int32, doc=
'Number of CCDs')
 
  575         lutSchema.addField(
'pmbStd', type=np.float64, doc=
'PMB Standard')
 
  576         lutSchema.addField(
'pwvStd', type=np.float64, doc=
'PWV Standard')
 
  577         lutSchema.addField(
'o3Std', type=np.float64, doc=
'O3 Standard')
 
  578         lutSchema.addField(
'tauStd', type=np.float64, doc=
'Tau Standard')
 
  579         lutSchema.addField(
'alphaStd', type=np.float64, doc=
'Alpha Standard')
 
  580         lutSchema.addField(
'zenithStd', type=np.float64, doc=
'Zenith angle Standard')
 
  581         lutSchema.addField(
'lambdaRange', type=
'ArrayD', doc=
'Wavelength range',
 
  583         lutSchema.addField(
'lambdaStep', type=np.float64, doc=
'Wavelength step')
 
  584         lutSchema.addField(
'lambdaStd', type=
'ArrayD', doc=
'Standard Wavelength',
 
  586         lutSchema.addField(
'lambdaStdFilter', type=
'ArrayD', doc=
'Standard Wavelength (raw)',
 
  588         lutSchema.addField(
'i0Std', type=
'ArrayD', doc=
'I0 Standard',
 
  590         lutSchema.addField(
'i1Std', type=
'ArrayD', doc=
'I1 Standard',
 
  592         lutSchema.addField(
'i10Std', type=
'ArrayD', doc=
'I10 Standard',
 
  594         lutSchema.addField(
'i2Std', type=
'ArrayD', doc=
'I2 Standard',
 
  596         lutSchema.addField(
'lambdaB', type=
'ArrayD', doc=
'Wavelength for passband (no atm)',
 
  598         lutSchema.addField(
'atmLambda', type=
'ArrayD', doc=
'Atmosphere wavelengths (Angstrom)',
 
  600         lutSchema.addField(
'atmStdTrans', type=
'ArrayD', doc=
'Standard Atmosphere Throughput',
 
  604         lutSchema.addField(
'luttype', type=str, size=20, doc=
'Look-up table type')
 
  605         lutSchema.addField(
'lut', type=
'ArrayF', doc=
'Look-up table for luttype',
 
  610     def _makeLutCat(self, lutSchema, filterNameString, stdFilterNameString,
 
  611                     atmosphereTableName):
 
  617         lutSchema: `afwTable.schema` 
  619         filterNameString: `str` 
  620            Combined string of all the filterNames 
  621         stdFilterNameString: `str` 
  622            Combined string of all the standard filterNames 
  623         atmosphereTableName: `str` 
  624            Name of the atmosphere table used to generate LUT 
  628         lutCat: `afwTable.BaseCatalog` 
  629            Lut catalog for persistence 
  637         lutCat.table.preallocate(14)
 
  640         rec = lutCat.addNew()
 
  642         rec[
'tablename'] = atmosphereTableName
 
  643         rec[
'elevation'] = self.
fgcmLutMaker.atmosphereTable.elevation
 
  644         rec[
'filterNames'] = filterNameString
 
  645         rec[
'stdFilterNames'] = stdFilterNameString
 
  666         rec[
'lambdaStdFilter'][:] = self.
fgcmLutMaker.lambdaStdFilter
 
  675         rec[
'luttype'] = 
'I0' 
  679         rec = lutCat.addNew()
 
  680         rec[
'luttype'] = 
'I1' 
  683         derivTypes = [
'D_PMB', 
'D_LNPWV', 
'D_O3', 
'D_LNTAU', 
'D_ALPHA', 
'D_SECZENITH',
 
  684                       'D_PMB_I1', 
'D_LNPWV_I1', 
'D_O3_I1', 
'D_LNTAU_I1', 
'D_ALPHA_I1',
 
  686         for derivType 
in derivTypes:
 
  687             rec = lutCat.addNew()
 
  688             rec[
'luttype'] = derivType
 
  689             rec[
'lut'][:] = self.
fgcmLutMaker.lutDeriv[derivType].flatten()