{"action":"created","comment":{"url":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/comments/233155819","pull_request_review_id":174504037,"id":233155819,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIzMzE1NTgxOQ==","diff_hunk":"@@ -0,0 +1,1596 @@\n+# See COPYRIGHT file at the top of the source tree.\n+#\n+# This file is part of fgcmcal.\n+#\n+# Developed for the LSST Data Management System.\n+# This product includes software developed by the LSST Project\n+# (https://www.lsst.org).\n+# See the COPYRIGHT file at the top-level directory of this distribution\n+# for details of code ownership.\n+#\n+# This program is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+#\n+# This program is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+#\n+# You should have received a copy of the GNU General Public License\n+# along with this program.  If not, see <https://www.gnu.org/licenses/>.\n+\n+import sys\n+import traceback\n+\n+import numpy as np\n+\n+import lsst.pex.config as pexConfig\n+import lsst.pipe.base as pipeBase\n+import lsst.afw.table as afwTable\n+import lsst.afw.geom as afwGeom\n+import lsst.afw.cameraGeom as afwCameraGeom\n+import lsst.geom\n+\n+import fgcm\n+\n+__all__ = ['FgcmFitCycleConfig', 'FgcmFitCycleTask']\n+\n+\n+class FgcmFitCycleConfig(pexConfig.Config):\n+    \"\"\"Config for FgcmFitCycle\"\"\"\n+\n+    bands = pexConfig.ListField(\n+        doc=\"Bands to run calibration (in wavelength order)\",\n+        dtype=str,\n+        default=(\"NO_DATA\",),\n+    )\n+    fitFlag = pexConfig.ListField(\n+        doc=\"Flag for bands to fit\",\n+        dtype=int,\n+        default=(0,),\n+    )\n+    requiredFlag = pexConfig.ListField(\n+        doc=\"Flag for bands to require to be a calibration star\",\n+        dtype=int,\n+        default=(0,),\n+    )\n+    filterToBand = pexConfig.DictField(\n+        doc=\"filterName to band mapping\",\n+        keytype=str,\n+        itemtype=str,\n+        default={},\n+    )\n+    nCore = pexConfig.Field(\n+        doc=\"Number of cores to use\",\n+        dtype=int,\n+        default=4,\n+    )\n+    nStarPerRun = pexConfig.Field(\n+        doc=\"Number of stars to run in each chunk\",\n+        dtype=int,\n+        default=200000,\n+    )\n+    nExpPerRun = pexConfig.Field(\n+        doc=\"Number of exposures to run in each chunk\",\n+        dtype=int,\n+        default=1000,\n+    )\n+    reserveFraction = pexConfig.Field(\n+        doc=\"Fraction of stars to reserve for testing\",\n+        dtype=float,\n+        default=0.1,\n+    )\n+    freezeStdAtmosphere = pexConfig.Field(\n+        doc=\"Freeze atmosphere parameters to standard (for testing)\",\n+        dtype=bool,\n+        default=False,\n+    )\n+    precomputeSuperStarInitialCycle = pexConfig.Field(\n+        doc=\"Precompute superstar flat for initial cycle\",\n+        dtype=bool,\n+        default=False,\n+    )\n+    superStarSubCcd = pexConfig.Field(\n+        doc=\"Compute superstar flat on sub-ccd scale\",\n+        dtype=bool,\n+        default=True,\n+    )\n+    superStarSubCcdChebyshevOrder = pexConfig.Field(\n+        doc=\"Order of chebyshev polynomials for sub-ccd superstar fit\",\n+        dtype=int,\n+        default=1,\n+    )\n+    superStarSigmaClip = pexConfig.Field(\n+        doc=\"Number of sigma to clip outliers when selecting for superstar flats\",\n+        dtype=float,\n+        default=5.0,\n+    )\n+    cycleNumber = pexConfig.Field(\n+        doc=\"Fit Cycle Number\",\n+        dtype=int,\n+        default=None,\n+    )\n+    maxIter = pexConfig.Field(\n+        doc=\"Max iterations\",\n+        dtype=int,\n+        default=50,\n+    )\n+    utBoundary = pexConfig.Field(\n+        doc=\"Boundary (in UTC) from day-to-day\",\n+        dtype=float,\n+        default=None,\n+    )\n+    washMjds = pexConfig.ListField(\n+        doc=\"Mirror wash MJDs\",\n+        dtype=float,\n+        default=(0.0,),\n+    )\n+    epochMjds = pexConfig.ListField(\n+        doc=\"Epoch boundaries in MJD\",\n+        dtype=float,\n+        default=(0.0,),\n+    )\n+    minObsPerBand = pexConfig.Field(\n+        doc=\"Minimum good observations per band\",\n+        dtype=int,\n+        default=2,\n+    )\n+    latitude = pexConfig.Field(\n+        doc=\"Observatory latitude\",\n+        dtype=float,\n+        default=None,\n+    )\n+    pixelScale = pexConfig.Field(\n+        doc=\"Pixel scale (arcsec/pixel) (temporary)\",\n+        dtype=float,\n+        default=None,\n+    )\n+    brightObsGrayMax = pexConfig.Field(\n+        doc=\"Maximum gray extinction to be considered bright observation\",\n+        dtype=float,\n+        default=0.15,\n+    )\n+    minStarPerCcd = pexConfig.Field(\n+        doc=\"Minimum number of good stars per CCD for calibration\",\n+        dtype=int,\n+        default=5,\n+    )\n+    minCcdPerExp = pexConfig.Field(\n+        doc=\"Minimum number of good CCDs per exposure\",\n+        dtype=int,\n+        default=5,\n+    )\n+    maxCcdGrayErr = pexConfig.Field(\n+        doc=\"Maximum error on CCD gray offset to be considered good\",\n+        dtype=float,\n+        default=0.05,\n+    )\n+    minStarPerExp = pexConfig.Field(\n+        doc=\"Minimum number of good stars per exposure to be considered good\",\n+        dtype=int,\n+        default=600,\n+    )\n+    minExpPerNight = pexConfig.Field(\n+        doc=\"Minimum number of good exposures to consider a good night\",\n+        dtype=int,\n+        default=10,\n+    )\n+    expGrayInitialCut = pexConfig.Field(\n+        doc=\"Maximum exposure gray value for initial cut\",\n+        dtype=float,\n+        default=-0.25,\n+    )\n+    expGrayPhotometricCut = pexConfig.ListField(\n+        doc=\"Negative exposure gray cut for photometric selection\",\n+        dtype=float,\n+        default=(0.0,),\n+    )\n+    expGrayHighCut = pexConfig.ListField(\n+        doc=\"Positive exposure gray cut for photometric selection\",\n+        dtype=float,\n+        default=(0.0,),\n+    )\n+    expGrayRecoverCut = pexConfig.Field(\n+        doc=\"Maximum exposure gray to be able to recover bad ccds\",\n+        dtype=float,\n+        default=-1.0,\n+    )\n+    expVarGrayPhotometricCut = pexConfig.Field(\n+        doc=\"Maximum exposure variance to be considered possibly photometric\",\n+        dtype=float,\n+        default=0.0005,\n+    )\n+    expGrayErrRecoverCut = pexConfig.Field(\n+        doc=\"Maximum exposure gray error to be able to recover bad ccds\",\n+        dtype=float,\n+        default=0.05,\n+    )\n+    illegalValue = pexConfig.Field(\n+        doc=\"Sentinal value for no-values\",\n+        dtype=float,\n+        default=-9999.0,\n+    )\n+    aperCorrFitNBins = pexConfig.Field(\n+        doc=\"Aperture correction number of bins\",\n+        dtype=int,\n+        default=None,\n+    )\n+    sedFudgeFactors = pexConfig.ListField(\n+        doc=\"Fudge factors for computing linear SED from colors\",\n+        dtype=float,\n+        default=(0,),\n+    )\n+    sigFgcmMaxErr = pexConfig.Field(\n+        doc=\"Maximum mag error for fitting sigma_FGCM\",\n+        dtype=float,\n+        default=0.01,\n+    )\n+    sigFgcmMaxEGray = pexConfig.Field(\n+        doc=\"Maximum (absolute) gray value for observation in sigma_FGCM\",\n+        dtype=float,\n+        default=0.05,\n+    )\n+    ccdGrayMaxStarErr = pexConfig.Field(\n+        doc=\"Maximum error on a star observation to use in ccd gray computation\",\n+        dtype=float,\n+        default=0.10,\n+    )\n+    approxThroughput = pexConfig.Field(\n+        doc=\"Approximate overall throughput at start of calibration observations\",\n+        dtype=float,\n+        default=1.0,\n+    )\n+    sigma0Cal = pexConfig.Field(\n+        doc=\"Systematic error floor for all observations\",\n+        dtype=float,\n+        default=0.003,\n+    )\n+    sigma0Phot = pexConfig.Field(\n+        doc=\"Systematic error floor for all zeropoints\",\n+        dtype=float,\n+        default=0.003,\n+    )\n+    mapLongitudeRef = pexConfig.Field(\n+        doc=\"Reference longitude for plotting maps\",\n+        dtype=float,\n+        default=0.0,\n+    )\n+    mapNSide = pexConfig.Field(\n+        doc=\"Healpix nside for plotting maps\",\n+        dtype=int,\n+        default=256,\n+    )\n+    varNSig = pexConfig.Field(\n+        doc=\"Number of sigma to be tested as a variable\",\n+        dtype=float,\n+        # Default recommendation in FGCM is to set this very large to\n+        # turn off the variable detection\n+        default=100.0,\n+    )\n+    varMinBand = pexConfig.Field(\n+        doc=\"Minimum number of bands with variability to be flagged as variable\",\n+        dtype=int,\n+        default=2,\n+    )\n+    cameraGain = pexConfig.Field(\n+        doc=\"Gain value for the typical CCD\",\n+        dtype=float,\n+        default=None,\n+    )\n+    outfileBase = pexConfig.Field(\n+        doc=\"Filename start for plot output files\",\n+        dtype=str,\n+        default=None,\n+    )\n+    starColorCuts = pexConfig.ListField(\n+        doc=\"Encoded star-color cuts (to be cleaned up)\",\n+        dtype=str,\n+        default=(\"NO_DATA\",),\n+    )\n+    colorSplitIndices = pexConfig.ListField(\n+        doc=\"Band indices to use to split stars by color\",\n+        dtype=int,\n+        default=None,\n+    )\n+    modelMagErrors = pexConfig.Field(\n+        doc=\"Should FGCM model the magnitude errors from sky/fwhm? (False means trust inputs)\",\n+        dtype=bool,\n+        default=True,\n+    )\n+    outputStandards = pexConfig.Field(\n+        doc=\"Output standard stars? (Usually only for final iteration)\",\n+        dtype=bool,\n+        default=False,\n+    )\n+\n+    def setDefaults(self):\n+        pass\n+\n+\n+class FgcmFitCycleRunner(pipeBase.ButlerInitializedTaskRunner):\n+    \"\"\"Subclass of TaskRunner for fgcmFitCycleTask\n+\n+    fgcmFitCycleTask.run() takes one argument, the butler, and uses\n+    stars and visits previously extracted from dataRefs by\n+    fgcmBuildStars.\n+    This runner does not use any parallelization, although the\n+    FGCM code uses multiprocessing\n+    \"\"\"\n+\n+    @staticmethod\n+    def getTargetList(parsedCmd):\n+        \"\"\"\n+        Return a list with one element, the butler.\n+        \"\"\"\n+        return [parsedCmd.butler]\n+\n+    # This overrides the pipe_base config saving which would fail because it\n+    # requires the %(fgcmcycle)d dataId key.\n+\n+    def __call__(self, butler):\n+        \"\"\"\n+        Parameters\n+        ----------\n+        butler: `lsst.daf.persistence.Butler`\n+\n+        Returns\n+        -------\n+        None if self.doReturnResults is False\n+        An empty list if self.doReturnResults is True\n+        \"\"\"\n+\n+        task = self.TaskClass(config=self.config, log=self.log)\n+\n+        exitStatus = 0\n+        if self.doRaise:\n+            results = task.runDataRef(butler)\n+        else:\n+            try:\n+                results = task.runDataRef(butler)\n+            except Exception as e:\n+                exitStatus = 1\n+                task.log.fatal(\"Failed: %s\" % e)\n+                if not isinstance(e, pipeBase.TaskError):\n+                    traceback.print_exc(file=sys.stderr)\n+\n+        task.writeMetadata(butler)\n+\n+        if self.doReturnResults:\n+            # Note that there's not much results to return (empty list)\n+            return [pipeBase.Struct(exitStatus=exitStatus,\n+                                    results=results)]\n+        else:\n+            return [pipeBase.Struct(exitStatus=exitStatus)]\n+\n+    def run(self, parsedCmd):\n+        \"\"\"\n+        Run the task, with no multiprocessing\n+\n+        Parameters\n+        ----------\n+        parsedCmd: ArgumentParser parsed command line\n+        \"\"\"\n+\n+        resultList = []\n+\n+        if self.precall(parsedCmd):\n+            targetList = self.getTargetList(parsedCmd)\n+            # make sure that we only get 1\n+            resultList = self(targetList[0])\n+\n+        return resultList\n+\n+\n+class FgcmFitCycleTask(pipeBase.CmdLineTask):\n+    \"\"\"\n+    Run Single fit cycle for FGCM global calibration\n+    \"\"\"\n+\n+    ConfigClass = FgcmFitCycleConfig\n+    RunnerClass = FgcmFitCycleRunner\n+    _DefaultName = \"fgcmFitCycle\"\n+\n+    def __init__(self, butler=None, **kwargs):\n+        \"\"\"\n+        Instantiate an fgcmFitCycle.\n+\n+        Parameters\n+        ----------\n+        butler : `lsst.daf.persistence.Butler`\n+        \"\"\"\n+\n+        pipeBase.CmdLineTask.__init__(self, **kwargs)\n+\n+    @classmethod\n+    def _makeArgumentParser(cls):\n+        \"\"\"Create an argument parser\"\"\"\n+\n+        parser = pipeBase.ArgumentParser(name=cls._DefaultName)\n+\n+        return parser\n+\n+    # no saving of metadata for now\n+    def _getMetadataName(self):\n+        return None\n+\n+    @pipeBase.timeMethod\n+    def runDataRef(self, butler):\n+        \"\"\"\n+        Run a single fit cycle for FGCM\n+\n+        Parameters\n+        ----------\n+        butler:  `lsst.daf.persistence.Butler`\n+\n+        Returns\n+        -------\n+        Empty list\n+        \"\"\"\n+\n+        self._fgcmFitCycle(butler)\n+\n+        return []\n+\n+    def writeConfig(self, butler, clobber=False, doBackup=True):\n+        \"\"\"Write the configuration used for processing the data, or check that an existing\n+        one is equal to the new one if present.  This is an override of the regular\n+        version from pipe_base that knows about fgcmcycle.\n+\n+        Parameters\n+        ----------\n+        butler : `lsst.daf.persistence.Butler`\n+            Data butler used to write the config. The config is written to dataset type\n+            `CmdLineTask._getConfigName`.\n+        clobber : `bool`, optional\n+            A boolean flag that controls what happens if a config already has been saved:\n+            - `True`: overwrite or rename the existing config, depending on ``doBackup``.\n+            - `False`: raise `TaskError` if this config does not match the existing config.\n+        doBackup : `bool`, optional\n+            Set to `True` to backup the config files if clobbering.\n+        \"\"\"\n+        configName = self._getConfigName()\n+        if configName is None:\n+            return\n+        if clobber:\n+            butler.put(self.config, configName, doBackup=doBackup, fgcmcycle=self.config.cycleNumber)\n+        elif butler.datasetExists(configName, write=True, fgcmcycle=self.config.cycleNumber):\n+            # this may be subject to a race condition; see #2789\n+            try:\n+                oldConfig = butler.get(configName, immediate=True, fgcmcycle=self.config.cycleNumber)\n+            except Exception as exc:\n+                raise type(exc)(\"Unable to read stored config file %s (%s); consider using --clobber-config\" %\n+                                (configName, exc))\n+\n+            def logConfigMismatch(msg):\n+                self.log.fatal(\"Comparing configuration: %s\", msg)\n+\n+            if not self.config.compare(oldConfig, shortcut=False, output=logConfigMismatch):\n+                raise pipeBase.TaskError(\n+                    (\"Config does not match existing task config %r on disk; tasks configurations \" +\n+                     \"must be consistent within the same output repo (override with --clobber-config)\") %\n+                    (configName,))\n+        else:\n+            butler.put(self.config, configName, fgcmcycle=self.config.cycleNumber)\n+\n+    def _fgcmFitCycle(self, butler):\n+        \"\"\"\n+        Run the fit cycle\n+\n+        Parameters\n+        ----------\n+        butler: `lsst.daf.persistence.Butler`\n+           (used for mapper information)\n+\n+        \"\"\"\n+\n+        if not butler.datasetExists('fgcmVisitCatalog'):\n+            raise RuntimeError(\"Could not find fgcmVisitCatalog in repo!\")\n+        if not butler.datasetExists('fgcmStarObservations'):\n+            raise RuntimeError(\"Could not find fgcmStarObservations in repo!\")\n+        if not butler.datasetExists('fgcmStarIds'):\n+            raise RuntimeError(\"Could not find fgcmStarIds in repo!\")\n+        if not butler.datasetExists('fgcmStarIndices'):\n+            raise RuntimeError(\"Could not find fgcmStarIndices in repo!\")\n+        if not butler.datasetExists('fgcmLookUpTable'):\n+            raise RuntimeError(\"Could not find fgcmLookUpTable in repo!\")\n+\n+        # Need additional datasets if we are not the initial cycle\n+        if (self.config.cycleNumber > 0):\n+            if not butler.datasetExists('fgcmFitParameters',\n+                                        fgcmcycle=self.config.cycleNumber-1):\n+                raise RuntimeError(\"Could not find fgcmFitParameters for previous cycle (%d) in repo!\" %\n+                                   (self.config.cycleNumber-1))\n+            if not butler.datasetExists('fgcmFlaggedStars',\n+                                        fgcmcycle=self.config.cycleNumber-1):\n+                raise RuntimeError(\"Could not find fgcmFlaggedStars for previous cycle (%d) in repo!\" %\n+                                   (self.config.cycleNumber-1))\n+\n+        camera = butler.get('camera')\n+        configDict = self._makeConfigDict(camera)\n+\n+        fgcmLut, lutIndexVals, lutStd = self._loadFgcmLut(butler,\n+                                                          filterToBand=self.config.filterToBand)\n+\n+        # next we need the exposure/visit information\n+\n+        fgcmExpInfo = self._loadVisitCatalog(butler)\n+\n+        ccdOffsets = self._loadCcdOffsets(butler)\n+\n+        noFitsDict = {'lutIndex': lutIndexVals,\n+                      'lutStd': lutStd,\n+                      'expInfo': fgcmExpInfo,\n+                      'ccdOffsets': ccdOffsets}\n+\n+        # set up the fitter object\n+        fgcmFitCycle = fgcm.FgcmFitCycle(configDict, useFits=False,\n+                                         noFitsDict=noFitsDict)\n+\n+        # create the parameter object\n+        if (fgcmFitCycle.initialCycle):\n+            # cycle = 0, initial cycle\n+            fgcmPars = fgcm.FgcmParameters.newParsWithArrays(fgcmFitCycle.fgcmConfig,\n+                                                             fgcmLut,\n+                                                             fgcmExpInfo)\n+        else:\n+            inParInfo, inParams, inSuperStar = self._loadParameters(butler)\n+            fgcmPars = fgcm.FgcmParameters.loadParsWithArrays(fgcmFitCycle.fgcmConfig,\n+                                                              fgcmExpInfo,\n+                                                              inParInfo,\n+                                                              inParams,\n+                                                              inSuperStar)\n+\n+        lastCycle = configDict['cycleNumber'] - 1\n+\n+        # set up the stars...\n+        fgcmStars = fgcm.FgcmStars(fgcmFitCycle.fgcmConfig)\n+\n+        starObs = butler.get('fgcmStarObservations')\n+        starIds = butler.get('fgcmStarIds')\n+        starIndices = butler.get('fgcmStarIndices')\n+\n+        # grab the flagged stars if available\n+        if butler.datasetExists('fgcmFlaggedStars', fgcmcycle=lastCycle):\n+            flaggedStars = butler.get('fgcmFlaggedStars', fgcmcycle=lastCycle)\n+            flagId = flaggedStars['objId'][:]\n+            flagFlag = flaggedStars['objFlag'][:]\n+        else:\n+            flagId = None\n+            flagFlag = None\n+\n+        # match star observations to visits\n+        visitIndex = np.searchsorted(fgcmExpInfo['VISIT'], starObs['visit'][starIndices['obsIndex']])\n+\n+        # note that we only need the star observations from specific indices\n+\n+        fgcmStars.loadStars(fgcmPars,\n+                            starObs['visit'][starIndices['obsIndex']],\n+                            starObs['ccd'][starIndices['obsIndex']],\n+                            np.rad2deg(starObs['ra'][starIndices['obsIndex']]),\n+                            np.rad2deg(starObs['dec'][starIndices['obsIndex']]),\n+                            starObs['instMag'][starIndices['obsIndex']],\n+                            starObs['instMagErr'][starIndices['obsIndex']],\n+                            fgcmExpInfo['FILTERNAME'][visitIndex],\n+                            starIds['fgcm_id'][:],\n+                            starIds['ra'][:],\n+                            starIds['dec'][:],\n+                            starIds['obsArrIndex'][:],\n+                            starIds['nObs'][:],\n+                            obsX=starObs['x'][starIndices['obsIndex']],\n+                            obsY=starObs['y'][starIndices['obsIndex']],\n+                            flagID=flagId,\n+                            flagFlag=flagFlag,\n+                            computeNobs=True)\n+\n+        # clear star memory\n+        starObs = None\n+        starIds = None\n+        starIndices = None\n+        flagId = None\n+        flagFlag = None\n+        flaggedStars = None\n+\n+        # and set the bits in the cycle object\n+        fgcmFitCycle.setLUT(fgcmLut)\n+        fgcmFitCycle.setStars(fgcmStars)\n+        fgcmFitCycle.setPars(fgcmPars)\n+\n+        # finish the setup\n+        fgcmFitCycle.finishSetup()\n+\n+        # and run\n+        fgcmFitCycle.run()\n+\n+        ##################\n+        # Persistance\n+        ##################\n+\n+        # parameters\n+        parInfo, pars = fgcmFitCycle.fgcmPars.parsToArrays()\n+\n+        parSchema = afwTable.Schema()\n+\n+        comma = ','\n+        lutFilterNameString = comma.join([n.decode('utf-8')\n+                                          for n in parInfo['LUTFILTERNAMES'][0]])\n+        fitBandString = comma.join([n.decode('utf-8')\n+                                    for n in parInfo['FITBANDS'][0]])\n+        notFitBandString = comma.join([n.decode('utf-8')\n+                                       for n in parInfo['NOTFITBANDS'][0]])\n+\n+        parSchema = self._makeParSchema(parInfo, pars, fgcmFitCycle.fgcmPars.parSuperStarFlat,\n+                                        lutFilterNameString, fitBandString, notFitBandString)\n+        parCat = afwTable.BaseCatalog(parSchema)\n+        parCat.reserve(1)\n+\n+        self._fillParCatalog(parCat, parInfo, pars, fgcmFitCycle.fgcmPars.parSuperStarFlat,\n+                             lutFilterNameString, fitBandString, notFitBandString)\n+\n+        butler.put(parCat, 'fgcmFitParameters', fgcmcycle=self.config.cycleNumber)\n+\n+        # Save the flagged stars\n+        flagStarSchema = self._makeFlagStarSchema()\n+        flagStarStruct = fgcmFitCycle.fgcmStars.getFlagStarIndices()\n+        flagStarCat = self._makeFlagStarCat(flagStarSchema, flagStarStruct)\n+\n+        butler.put(flagStarCat, 'fgcmFlaggedStars', fgcmcycle=self.config.cycleNumber)\n+\n+        # Save the zeropoint information\n+        zptSchema = self._makeZptSchema(fgcmFitCycle.fgcmZpts.zpStruct['FGCM_FZPT_CHEB'].shape[1])\n+        zptCat = self._makeZptCat(zptSchema, fgcmFitCycle.fgcmZpts.zpStruct)\n+\n+        butler.put(zptCat, 'fgcmZeropoints', fgcmcycle=self.config.cycleNumber)\n+\n+        # Save atmosphere values\n+\n+        atmSchema = self._makeAtmSchema()\n+        atmCat = self._makeAtmCat(atmSchema, fgcmFitCycle.fgcmZpts.atmStruct)\n+\n+        butler.put(atmCat, 'fgcmAtmosphereParameters', fgcmcycle=self.config.cycleNumber)\n+\n+        if self.config.outputStandards:\n+            stdSchema = self._makeStdSchema()\n+            stdStruct = fgcmFitCycle.fgcmStars.retrieveStdStarCatalog(fgcmFitCycle.fgcmPars)\n+            stdCat = self._makeStdCat(stdSchema, stdStruct)\n+\n+            butler.put(stdCat, 'fgcmStandardStars', fgcmcycle=self.config.cycleNumber)\n+\n+        # Output the config for the next cycle\n+        # We need to make a copy since the input one has been frozen\n+\n+        outConfig = FgcmFitCycleConfig()\n+        outConfig.update(**self.config.toDict())\n+\n+        outConfig.cycleNumber += 1\n+        outConfig.precomputeSuperStarInitialCycle = False\n+        outConfig.freezeStdAtmosphere = False\n+        configFileName = '%s_cycle%02d_config.py' % (outConfig.outfileBase,\n+                                                     outConfig.cycleNumber)\n+        outConfig.save(configFileName)\n+\n+        if self.config.maxIter == 0 and self.config.outputStandards:\n+            # We are done, there is no more warning\n+            self.log.info(\"Everything is in place to run fgcmOutputProducts.py\")\n+        else:\n+            self.log.info(\"Saved config for next cycle to %s\" % (configFileName))\n+            self.log.info(\"Be sure to look at:\")\n+            self.log.info(\"   config.expGrayPhotometricCut\")\n+            self.log.info(\"   config.expGrayHighCut\")\n+            self.log.info(\"If you are satisfied with the fit, please set:\")\n+            self.log.info(\"   config.maxIter = 0\")\n+            self.log.info(\"   config.outputStandards = True\")\n+\n+    def _loadFgcmLut(self, butler, filterToBand=None):\n+        \"\"\"\n+        \"\"\"\n+\n+        # set up the look-up-table\n+        lutCat = butler.get('fgcmLookUpTable')\n+\n+        # first we need the lutIndexVals\n+        # dtype is set for py2/py3/fits/fgcm compatibility\n+        lutFilterNames = np.array(lutCat[0]['filterNames'].split(','), dtype='a')\n+        lutStdFilterNames = np.array(lutCat[0]['stdFilterNames'].split(','), dtype='a')\n+\n+        # FIXME: check that lutBands equal listed bands!\n+\n+        lutIndexVals = np.zeros(1, dtype=[('FILTERNAMES', lutFilterNames.dtype.str,\n+                                           lutFilterNames.size),\n+                                          ('STDFILTERNAMES', lutStdFilterNames.dtype.str,\n+                                           lutStdFilterNames.size),\n+                                          ('PMB', 'f8', lutCat[0]['pmb'].size),\n+                                          ('PMBFACTOR', 'f8', lutCat[0]['pmbFactor'].size),\n+                                          ('PMBELEVATION', 'f8'),\n+                                          ('LAMBDANORM', 'f8'),\n+                                          ('PWV', 'f8', lutCat[0]['pwv'].size),\n+                                          ('O3', 'f8', lutCat[0]['o3'].size),\n+                                          ('TAU', 'f8', lutCat[0]['tau'].size),\n+                                          ('ALPHA', 'f8', lutCat[0]['alpha'].size),\n+                                          ('ZENITH', 'f8', lutCat[0]['zenith'].size),\n+                                          ('NCCD', 'i4')])\n+\n+        lutIndexVals['FILTERNAMES'][:] = lutFilterNames\n+        lutIndexVals['STDFILTERNAMES'][:] = lutStdFilterNames\n+        lutIndexVals['PMB'][:] = lutCat[0]['pmb']\n+        lutIndexVals['PMBFACTOR'][:] = lutCat[0]['pmbFactor']\n+        lutIndexVals['PMBELEVATION'] = lutCat[0]['pmbElevation']\n+        lutIndexVals['LAMBDANORM'] = lutCat[0]['lambdaNorm']\n+        lutIndexVals['PWV'][:] = lutCat[0]['pwv']\n+        lutIndexVals['O3'][:] = lutCat[0]['o3']\n+        lutIndexVals['TAU'][:] = lutCat[0]['tau']\n+        lutIndexVals['ALPHA'][:] = lutCat[0]['alpha']\n+        lutIndexVals['ZENITH'][:] = lutCat[0]['zenith']\n+        lutIndexVals['NCCD'] = lutCat[0]['nCcd']\n+\n+        # now we need the Standard Values\n+        lutStd = np.zeros(1, dtype=[('PMBSTD', 'f8'),\n+                                    ('PWVSTD', 'f8'),\n+                                    ('O3STD', 'f8'),\n+                                    ('TAUSTD', 'f8'),\n+                                    ('ALPHASTD', 'f8'),\n+                                    ('ZENITHSTD', 'f8'),\n+                                    ('LAMBDARANGE', 'f8', 2),\n+                                    ('LAMBDASTEP', 'f8'),\n+                                    ('LAMBDASTD', 'f8', lutFilterNames.size),\n+                                    ('LAMBDASTDFILTER', 'f8', lutStdFilterNames.size),\n+                                    ('I0STD', 'f8', lutFilterNames.size),\n+                                    ('I1STD', 'f8', lutFilterNames.size),\n+                                    ('I10STD', 'f8', lutFilterNames.size),\n+                                    ('LAMBDAB', 'f8', lutFilterNames.size),\n+                                    ('ATMLAMBDA', 'f8', lutCat[0]['atmLambda'].size),\n+                                    ('ATMSTDTRANS', 'f8', lutCat[0]['atmStdTrans'].size)])\n+        lutStd['PMBSTD'] = lutCat[0]['pmbStd']\n+        lutStd['PWVSTD'] = lutCat[0]['pwvStd']\n+        lutStd['O3STD'] = lutCat[0]['o3Std']\n+        lutStd['TAUSTD'] = lutCat[0]['tauStd']\n+        lutStd['ALPHASTD'] = lutCat[0]['alphaStd']\n+        lutStd['ZENITHSTD'] = lutCat[0]['zenithStd']\n+        lutStd['LAMBDARANGE'][:] = lutCat[0]['lambdaRange'][:]\n+        lutStd['LAMBDASTEP'] = lutCat[0]['lambdaStep']\n+        lutStd['LAMBDASTD'][:] = lutCat[0]['lambdaStd']\n+        lutStd['LAMBDASTDFILTER'][:] = lutCat[0]['lambdaStdFilter']\n+        lutStd['I0STD'][:] = lutCat[0]['i0Std']\n+        lutStd['I1STD'][:] = lutCat[0]['i1Std']\n+        lutStd['I10STD'][:] = lutCat[0]['i10Std']\n+        lutStd['LAMBDAB'][:] = lutCat[0]['lambdaB']\n+        lutStd['ATMLAMBDA'][:] = lutCat[0]['atmLambda'][:]\n+        lutStd['ATMSTDTRANS'][:] = lutCat[0]['atmStdTrans'][:]\n+\n+        lutTypes = []\n+        for row in lutCat:\n+            lutTypes.append(row['luttype'])\n+\n+        # And the flattened look-up-table\n+        lutFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('I0', 'f4'),\n+                                                         ('I1', 'f4')])\n+\n+        try:\n+            lutFlat['I0'][:] = lutCat[lutTypes.index('I0')]['lut'][:]\n+            lutFlat['I1'][:] = lutCat[lutTypes.index('I1')]['lut'][:]\n+        except:\n+            # need to raise exception\n+            pass\n+\n+        lutDerivFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('D_PWV', 'f4'),\n+                                                              ('D_O3', 'f4'),\n+                                                              ('D_LNTAU', 'f4'),\n+                                                              ('D_ALPHA', 'f4'),\n+                                                              ('D_SECZENITH', 'f4'),\n+                                                              ('D_PWV_I1', 'f4'),\n+                                                              ('D_O3_I1', 'f4'),\n+                                                              ('D_LNTAU_I1', 'f4'),\n+                                                              ('D_ALPHA_I1', 'f4'),\n+                                                              ('D_SECZENITH_I1', 'f4')])\n+\n+        try:\n+            for name in lutDerivFlat.dtype.names:\n+                lutDerivFlat[name][:] = lutCat[lutTypes.index(name)]['lut'][:]\n+        except:\n+            # raise a helpful exception\n+            pass\n+\n+        # and clear out the memory from the big object\n+        lutCat = None\n+\n+        fgcmLut = fgcm.FgcmLUT(lutIndexVals, lutFlat, lutDerivFlat, lutStd,\n+                               filterToBand=self.config.filterToBand)\n+\n+        # and clear out the memory of the large temporary objects\n+        lutFlat = None\n+        lutDerivFlat = None\n+\n+        return fgcmLut, lutIndexVals, lutStd\n+\n+    def _loadVisitCatalog(self, butler):\n+        \"\"\"\n+        \"\"\"\n+\n+        # next we need the exposure/visit information\n+        visitCat = butler.get('fgcmVisitCatalog')\n+\n+        fgcmExpInfo = np.zeros(len(visitCat), dtype=[('VISIT', 'i8'),\n+                                                     ('MJD', 'f8'),\n+                                                     ('EXPTIME', 'f8'),\n+                                                     ('PSFSIGMA', 'f8'),\n+                                                     ('DELTA_APER', 'f8'),\n+                                                     ('SKYBACKGROUND', 'f8'),\n+                                                     ('DEEPFLAG', 'i2'),\n+                                                     ('TELHA', 'f8'),\n+                                                     ('TELRA', 'f8'),\n+                                                     ('TELDEC', 'f8'),\n+                                                     ('PMB', 'f8'),\n+                                                     ('FILTERNAME', 'a2')])\n+        fgcmExpInfo['VISIT'][:] = visitCat['visit']\n+        fgcmExpInfo['MJD'][:] = visitCat['mjd']\n+        fgcmExpInfo['EXPTIME'][:] = visitCat['exptime']\n+        fgcmExpInfo['DEEPFLAG'][:] = visitCat['deepFlag']\n+        fgcmExpInfo['TELHA'][:] = visitCat['telha']\n+        fgcmExpInfo['TELRA'][:] = visitCat['telra']\n+        fgcmExpInfo['TELDEC'][:] = visitCat['teldec']\n+        fgcmExpInfo['PMB'][:] = visitCat['pmb']\n+        fgcmExpInfo['PSFSIGMA'][:] = visitCat['psfSigma']\n+        fgcmExpInfo['DELTA_APER'][:] = visitCat['deltaAper']\n+        fgcmExpInfo['SKYBACKGROUND'][:] = visitCat['skyBackground']\n+        # Note that we have to go through asAstropy() to get a string\n+        #  array out of an afwTable\n+        fgcmExpInfo['FILTERNAME'][:] = visitCat.asAstropy()['filtername']\n+\n+        return fgcmExpInfo\n+\n+    def _loadCcdOffsets(self, butler):\n+        \"\"\"\n+        \"\"\"\n+        camera = butler.get('camera')\n+\n+        # and we need to know the ccd offsets from the camera geometry\n+        ccdOffsets = np.zeros(len(camera), dtype=[('CCDNUM', 'i4'),\n+                                                  ('DELTA_RA', 'f8'),\n+                                                  ('DELTA_DEC', 'f8'),\n+                                                  ('RA_SIZE', 'f8'),\n+                                                  ('DEC_SIZE', 'f8'),\n+                                                  ('X_SIZE', 'i4'),\n+                                                  ('Y_SIZE', 'i4')])\n+\n+        extent = afwGeom.Extent2D(self.config.pixelScale, self.config.pixelScale)\n+\n+        for i, detector in enumerate(camera):\n+            # new version, using proper rotations\n+            #  but I worry this only works with HSC, as there's a unit inconsistency\n+\n+            camPoint = detector.getCenter(afwCameraGeom.PIXELS)\n+            bbox = detector.getBBox()\n+            orient = detector.getOrientation()\n+\n+            ccdOffsets['CCDNUM'][i] = detector.getId()\n+\n+            xform = orient.makePixelFpTransform(extent)\n+            pointXform = xform.applyForward(camPoint)\n+            # this requires a pixelScale\n+            # NOTE that this now works properly with HSC, but I need to work on\n+            # generalizing this properly\n+            ccdOffsets['DELTA_RA'][i] = -pointXform.getY() * self.config.pixelScale / 3600.0\n+            ccdOffsets['DELTA_DEC'][i] = -pointXform.getX() * self.config.pixelScale / 3600.0\n+\n+            # but this does not (for the delta)\n+            boxXform = xform.applyForward(afwGeom.Point2D(bbox.getMaxX(), bbox.getMaxY()))\n+            ccdOffsets['RA_SIZE'][i] = 2. * np.abs(boxXform.getY() -\n+                                                   pointXform.getY()) / 3600.0\n+            ccdOffsets['DEC_SIZE'][i] = 2. * np.abs(boxXform.getX() -\n+                                                    pointXform.getX()) / 3600.0\n+\n+            ccdOffsets['X_SIZE'][i] = bbox.getMaxX()\n+            ccdOffsets['Y_SIZE'][i] = bbox.getMaxY()\n+\n+        return ccdOffsets\n+\n+    def _makeConfigDict(self, camera):\n+        \"\"\"\n+        Make the FGCM configuration dict\n+\n+        Parameters\n+        ----------\n+        camera: 'cameraType`\n+            Camera from the mapper\n+\n+        Returns\n+        -------\n+        configDict: `dict`\n+        \"\"\"\n+\n+        fitFlag = np.array(self.config.fitFlag, dtype=np.bool)\n+        requiredFlag = np.array(self.config.requiredFlag, dtype=np.bool)\n+\n+        fitBands = [b for i, b in enumerate(self.config.bands) if fitFlag[i]]\n+        notFitBands = [b for i, b in enumerate(self.config.bands) if not fitFlag[i]]\n+        requiredBands = [b for i, b in enumerate(self.config.bands) if requiredFlag[i]]\n+\n+        # process the starColorCuts\n+        starColorCutList = []\n+        for ccut in self.config.starColorCuts:\n+            parts = ccut.split(',')\n+            starColorCutList.append([parts[0], parts[1], float(parts[2]), float(parts[3])])\n+\n+        if self.config.maxIter == 0:\n+            resetParameters = False\n+        else:\n+            resetParameters = True\n+\n+        # Mirror area in cm**2\n+        mirrorArea = np.pi*(camera.telescopeDiameter*100./2.)**2.\n+\n+        # create a configuration dictionary for fgcmFitCycle\n+        configDict = {'outfileBase': self.config.outfileBase,\n+                      'logger': self.log,\n+                      'exposureFile': None,\n+                      'obsFile': None,\n+                      'indexFile': None,\n+                      'lutFile': None,\n+                      'mirrorArea': mirrorArea,\n+                      'cameraGain': self.config.cameraGain,\n+                      'ccdStartIndex': camera[0].getId(),\n+                      'expField': 'VISIT',\n+                      'ccdField': 'CCD',\n+                      'seeingField': 'DELTA_APER',\n+                      'fwhmField': 'PSFSIGMA',\n+                      'skyBrightnessField': 'SKYBACKGROUND',\n+                      'deepFlag': 'DEEPFLAG',  # unused\n+                      'bands': list(self.config.bands),\n+                      'fitBands': list(fitBands),\n+                      'notFitBands': list(notFitBands),\n+                      'requiredBands': list(requiredBands),\n+                      'filterToBand': dict(self.config.filterToBand),\n+                      'logLevel': 'INFO',  # FIXME\n+                      'nCore': self.config.nCore,\n+                      'nStarPerRun': self.config.nStarPerRun,\n+                      'nExpPerRun': self.config.nExpPerRun,\n+                      'reserveFraction': self.config.reserveFraction,\n+                      'freezeStdAtmosphere': self.config.freezeStdAtmosphere,\n+                      'precomputeSuperStarInitialCycle': self.config.precomputeSuperStarInitialCycle,\n+                      'superStarSubCCD': self.config.superStarSubCcd,\n+                      'superStarSubCCDChebyshevOrder': self.config.superStarSubCcdChebyshevOrder,\n+                      'superStarSigmaClip': self.config.superStarSigmaClip,\n+                      'cycleNumber': self.config.cycleNumber,\n+                      'maxIter': self.config.maxIter,\n+                      'UTBoundary': self.config.utBoundary,\n+                      'washMJDs': self.config.washMjds,\n+                      'epochMJDs': self.config.epochMjds,\n+                      'minObsPerBand': self.config.minObsPerBand,\n+                      'latitude': self.config.latitude,\n+                      'brightObsGrayMax': self.config.brightObsGrayMax,\n+                      'minStarPerCCD': self.config.minStarPerCcd,\n+                      'minCCDPerExp': self.config.minCcdPerExp,\n+                      'maxCCDGrayErr': self.config.maxCcdGrayErr,\n+                      'minStarPerExp': self.config.minStarPerExp,\n+                      'minExpPerNight': self.config.minExpPerNight,\n+                      'expGrayInitialCut': self.config.expGrayInitialCut,\n+                      'expGrayPhotometricCut': np.array(self.config.expGrayPhotometricCut),\n+                      'expGrayHighCut': np.array(self.config.expGrayHighCut),\n+                      'expGrayRecoverCut': self.config.expGrayRecoverCut,\n+                      'expVarGrayPhotometricCut': self.config.expVarGrayPhotometricCut,\n+                      'expGrayErrRecoverCut': self.config.expGrayErrRecoverCut,\n+                      'illegalValue': self.config.illegalValue,\n+                      'starColorCuts': starColorCutList,\n+                      'aperCorrFitNBins': self.config.aperCorrFitNBins,\n+                      'sedFudgeFactors': np.array(self.config.sedFudgeFactors),\n+                      'colorSplitIndices': np.array(self.config.colorSplitIndices),\n+                      'sigFgcmMaxErr': self.config.sigFgcmMaxErr,\n+                      'sigFgcmMaxEGray': self.config.sigFgcmMaxEGray,\n+                      'ccdGrayMaxStarErr': self.config.ccdGrayMaxStarErr,\n+                      'approxThroughput': self.config.approxThroughput,\n+                      'sigma0Cal': self.config.sigma0Cal,\n+                      'sigma0Phot': self.config.sigma0Phot,\n+                      'mapLongitudeRef': self.config.mapLongitudeRef,\n+                      'mapNSide': self.config.mapNSide,\n+                      'varNSig': self.config.varNSig,\n+                      'varMinBand': self.config.varMinBand,\n+                      'useRetrievedPWV': False,\n+                      'useNightlyRetrievedPWV': False,\n+                      'pwvRetrievalSmoothBlock': 25,\n+                      'useRetrievedTauInit': False,\n+                      'tauRetrievalMinCCDPerNight': 500,\n+                      'modelMagErrors': self.config.modelMagErrors,\n+                      'printOnly': False,\n+                      'outputStars': False,\n+                      'clobber': True,\n+                      'useSedLUT': False,\n+                      'resetParameters': resetParameters}\n+\n+        return configDict\n+\n+    def _loadParameters(self, butler):\n+        \"\"\"\n+        Load FGCM parameters from a previous fit cycle\n+\n+        Parameters\n+        ----------\n+        butler:  `lsst.daf.persistence.Butler`\n+\n+        Returns\n+        -------\n+        inParInfo: `np.ndarray`\n+           Numpy array parameter information formatted for input to fgcm\n+        inParameters: `np.ndarray`\n+           Numpy array parameter values formatted for input to fgcm\n+        inSuperStar: `np.array`\n+           Superstar flat formatted for input to fgcm\n+        \"\"\"\n+\n+        # note that we already checked that this is available\n+        parCat = butler.get('fgcmFitParameters', fgcmcycle=self.config.cycleNumber-1)\n+\n+        parLutFilterNames = np.array(parCat[0]['lutFilterNames'].split(','))\n+        parFitBands = np.array(parCat[0]['fitBands'].split(','))\n+        parNotFitBands = np.array(parCat[0]['notFitBands'].split(','))\n+\n+        # FIXME: check that these are the same as in the config, to be sure","path":"python/lsst/fgcmcal/fgcmFitCycle.py","position":1027,"original_position":1027,"commit_id":"bf03b490d7030224fba7b6b7142343f515db06d2","original_commit_id":"bf03b490d7030224fba7b6b7142343f515db06d2","user":{"login":"erykoff","id":5167891,"node_id":"MDQ6VXNlcjUxNjc4OTE=","avatar_url":"https://avatars3.githubusercontent.com/u/5167891?v=4","gravatar_id":"","url":"https://api.github.com/users/erykoff","html_url":"https://github.com/erykoff","followers_url":"https://api.github.com/users/erykoff/followers","following_url":"https://api.github.com/users/erykoff/following{/other_user}","gists_url":"https://api.github.com/users/erykoff/gists{/gist_id}","starred_url":"https://api.github.com/users/erykoff/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/erykoff/subscriptions","organizations_url":"https://api.github.com/users/erykoff/orgs","repos_url":"https://api.github.com/users/erykoff/repos","events_url":"https://api.github.com/users/erykoff/events{/privacy}","received_events_url":"https://api.github.com/users/erykoff/received_events","type":"User","site_admin":false},"body":"I have removed this comment since it's handled by `fgcm` (as above).","created_at":"2018-11-13T17:53:48Z","updated_at":"2018-11-13T17:53:48Z","html_url":"https://github.com/parejkoj/fgcmcal/pull/1#discussion_r233155819","pull_request_url":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/1","author_association":"NONE","_links":{"self":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/comments/233155819"},"html":{"href":"https://github.com/parejkoj/fgcmcal/pull/1#discussion_r233155819"},"pull_request":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/1"}},"in_reply_to_id":232796483},"pull_request":{"url":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/1","id":229202348,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI5MjAyMzQ4","html_url":"https://github.com/parejkoj/fgcmcal/pull/1","diff_url":"https://github.com/parejkoj/fgcmcal/pull/1.diff","patch_url":"https://github.com/parejkoj/fgcmcal/pull/1.patch","issue_url":"https://api.github.com/repos/parejkoj/fgcmcal/issues/1","number":1,"state":"open","locked":false,"title":"Flattened all commits, so I can try making a giant PR.","user":{"login":"parejkoj","id":1606089,"node_id":"MDQ6VXNlcjE2MDYwODk=","avatar_url":"https://avatars3.githubusercontent.com/u/1606089?v=4","gravatar_id":"","url":"https://api.github.com/users/parejkoj","html_url":"https://github.com/parejkoj","followers_url":"https://api.github.com/users/parejkoj/followers","following_url":"https://api.github.com/users/parejkoj/following{/other_user}","gists_url":"https://api.github.com/users/parejkoj/gists{/gist_id}","starred_url":"https://api.github.com/users/parejkoj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/parejkoj/subscriptions","organizations_url":"https://api.github.com/users/parejkoj/orgs","repos_url":"https://api.github.com/users/parejkoj/repos","events_url":"https://api.github.com/users/parejkoj/events{/privacy}","received_events_url":"https://api.github.com/users/parejkoj/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-11-07T23:26:10Z","updated_at":"2018-11-13T17:53:48Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0ccb3e2a5bd9617d400713ac6546e56434c94402","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/1/commits","review_comments_url":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/1/comments","review_comment_url":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/comments{/number}","comments_url":"https://api.github.com/repos/parejkoj/fgcmcal/issues/1/comments","statuses_url":"https://api.github.com/repos/parejkoj/fgcmcal/statuses/bf03b490d7030224fba7b6b7142343f515db06d2","head":{"label":"parejkoj:u/parejkoj/review-flattened","ref":"u/parejkoj/review-flattened","sha":"bf03b490d7030224fba7b6b7142343f515db06d2","user":{"login":"parejkoj","id":1606089,"node_id":"MDQ6VXNlcjE2MDYwODk=","avatar_url":"https://avatars3.githubusercontent.com/u/1606089?v=4","gravatar_id":"","url":"https://api.github.com/users/parejkoj","html_url":"https://github.com/parejkoj","followers_url":"https://api.github.com/users/parejkoj/followers","following_url":"https://api.github.com/users/parejkoj/following{/other_user}","gists_url":"https://api.github.com/users/parejkoj/gists{/gist_id}","starred_url":"https://api.github.com/users/parejkoj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/parejkoj/subscriptions","organizations_url":"https://api.github.com/users/parejkoj/orgs","repos_url":"https://api.github.com/users/parejkoj/repos","events_url":"https://api.github.com/users/parejkoj/events{/privacy}","received_events_url":"https://api.github.com/users/parejkoj/received_events","type":"User","site_admin":false},"repo":{"id":156620952,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MjA5NTI=","name":"fgcmcal","full_name":"parejkoj/fgcmcal","private":false,"owner":{"login":"parejkoj","id":1606089,"node_id":"MDQ6VXNlcjE2MDYwODk=","avatar_url":"https://avatars3.githubusercontent.com/u/1606089?v=4","gravatar_id":"","url":"https://api.github.com/users/parejkoj","html_url":"https://github.com/parejkoj","followers_url":"https://api.github.com/users/parejkoj/followers","following_url":"https://api.github.com/users/parejkoj/following{/other_user}","gists_url":"https://api.github.com/users/parejkoj/gists{/gist_id}","starred_url":"https://api.github.com/users/parejkoj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/parejkoj/subscriptions","organizations_url":"https://api.github.com/users/parejkoj/orgs","repos_url":"https://api.github.com/users/parejkoj/repos","events_url":"https://api.github.com/users/parejkoj/events{/privacy}","received_events_url":"https://api.github.com/users/parejkoj/received_events","type":"User","site_admin":false},"html_url":"https://github.com/parejkoj/fgcmcal","description":"Forward Global Calibration Method","fork":true,"url":"https://api.github.com/repos/parejkoj/fgcmcal","forks_url":"https://api.github.com/repos/parejkoj/fgcmcal/forks","keys_url":"https://api.github.com/repos/parejkoj/fgcmcal/keys{/key_id}","collaborators_url":"https://api.github.com/repos/parejkoj/fgcmcal/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/parejkoj/fgcmcal/teams","hooks_url":"https://api.github.com/repos/parejkoj/fgcmcal/hooks","issue_events_url":"https://api.github.com/repos/parejkoj/fgcmcal/issues/events{/number}","events_url":"https://api.github.com/repos/parejkoj/fgcmcal/events","assignees_url":"https://api.github.com/repos/parejkoj/fgcmcal/assignees{/user}","branches_url":"https://api.github.com/repos/parejkoj/fgcmcal/branches{/branch}","tags_url":"https://api.github.com/repos/parejkoj/fgcmcal/tags","blobs_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/refs{/sha}","trees_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/trees{/sha}","statuses_url":"https://api.github.com/repos/parejkoj/fgcmcal/statuses/{sha}","languages_url":"https://api.github.com/repos/parejkoj/fgcmcal/languages","stargazers_url":"https://api.github.com/repos/parejkoj/fgcmcal/stargazers","contributors_url":"https://api.github.com/repos/parejkoj/fgcmcal/contributors","subscribers_url":"https://api.github.com/repos/parejkoj/fgcmcal/subscribers","subscription_url":"https://api.github.com/repos/parejkoj/fgcmcal/subscription","commits_url":"https://api.github.com/repos/parejkoj/fgcmcal/commits{/sha}","git_commits_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/commits{/sha}","comments_url":"https://api.github.com/repos/parejkoj/fgcmcal/comments{/number}","issue_comment_url":"https://api.github.com/repos/parejkoj/fgcmcal/issues/comments{/number}","contents_url":"https://api.github.com/repos/parejkoj/fgcmcal/contents/{+path}","compare_url":"https://api.github.com/repos/parejkoj/fgcmcal/compare/{base}...{head}","merges_url":"https://api.github.com/repos/parejkoj/fgcmcal/merges","archive_url":"https://api.github.com/repos/parejkoj/fgcmcal/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/parejkoj/fgcmcal/downloads","issues_url":"https://api.github.com/repos/parejkoj/fgcmcal/issues{/number}","pulls_url":"https://api.github.com/repos/parejkoj/fgcmcal/pulls{/number}","milestones_url":"https://api.github.com/repos/parejkoj/fgcmcal/milestones{/number}","notifications_url":"https://api.github.com/repos/parejkoj/fgcmcal/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/parejkoj/fgcmcal/labels{/name}","releases_url":"https://api.github.com/repos/parejkoj/fgcmcal/releases{/id}","deployments_url":"https://api.github.com/repos/parejkoj/fgcmcal/deployments","created_at":"2018-11-07T23:15:22Z","updated_at":"2018-11-07T23:15:24Z","pushed_at":"2018-11-12T05:46:34Z","git_url":"git://github.com/parejkoj/fgcmcal.git","ssh_url":"git@github.com:parejkoj/fgcmcal.git","clone_url":"https://github.com/parejkoj/fgcmcal.git","svn_url":"https://github.com/parejkoj/fgcmcal","homepage":"","size":682,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"parejkoj:first-commit","ref":"first-commit","sha":"41c06e5b4b35bc606cb88b0fbe1f8f43fe60e12c","user":{"login":"parejkoj","id":1606089,"node_id":"MDQ6VXNlcjE2MDYwODk=","avatar_url":"https://avatars3.githubusercontent.com/u/1606089?v=4","gravatar_id":"","url":"https://api.github.com/users/parejkoj","html_url":"https://github.com/parejkoj","followers_url":"https://api.github.com/users/parejkoj/followers","following_url":"https://api.github.com/users/parejkoj/following{/other_user}","gists_url":"https://api.github.com/users/parejkoj/gists{/gist_id}","starred_url":"https://api.github.com/users/parejkoj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/parejkoj/subscriptions","organizations_url":"https://api.github.com/users/parejkoj/orgs","repos_url":"https://api.github.com/users/parejkoj/repos","events_url":"https://api.github.com/users/parejkoj/events{/privacy}","received_events_url":"https://api.github.com/users/parejkoj/received_events","type":"User","site_admin":false},"repo":{"id":156620952,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MjA5NTI=","name":"fgcmcal","full_name":"parejkoj/fgcmcal","private":false,"owner":{"login":"parejkoj","id":1606089,"node_id":"MDQ6VXNlcjE2MDYwODk=","avatar_url":"https://avatars3.githubusercontent.com/u/1606089?v=4","gravatar_id":"","url":"https://api.github.com/users/parejkoj","html_url":"https://github.com/parejkoj","followers_url":"https://api.github.com/users/parejkoj/followers","following_url":"https://api.github.com/users/parejkoj/following{/other_user}","gists_url":"https://api.github.com/users/parejkoj/gists{/gist_id}","starred_url":"https://api.github.com/users/parejkoj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/parejkoj/subscriptions","organizations_url":"https://api.github.com/users/parejkoj/orgs","repos_url":"https://api.github.com/users/parejkoj/repos","events_url":"https://api.github.com/users/parejkoj/events{/privacy}","received_events_url":"https://api.github.com/users/parejkoj/received_events","type":"User","site_admin":false},"html_url":"https://github.com/parejkoj/fgcmcal","description":"Forward Global Calibration Method","fork":true,"url":"https://api.github.com/repos/parejkoj/fgcmcal","forks_url":"https://api.github.com/repos/parejkoj/fgcmcal/forks","keys_url":"https://api.github.com/repos/parejkoj/fgcmcal/keys{/key_id}","collaborators_url":"https://api.github.com/repos/parejkoj/fgcmcal/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/parejkoj/fgcmcal/teams","hooks_url":"https://api.github.com/repos/parejkoj/fgcmcal/hooks","issue_events_url":"https://api.github.com/repos/parejkoj/fgcmcal/issues/events{/number}","events_url":"https://api.github.com/repos/parejkoj/fgcmcal/events","assignees_url":"https://api.github.com/repos/parejkoj/fgcmcal/assignees{/user}","branches_url":"https://api.github.com/repos/parejkoj/fgcmcal/branches{/branch}","tags_url":"https://api.github.com/repos/parejkoj/fgcmcal/tags","blobs_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/refs{/sha}","trees_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/trees{/sha}","statuses_url":"https://api.github.com/repos/parejkoj/fgcmcal/statuses/{sha}","languages_url":"https://api.github.com/repos/parejkoj/fgcmcal/languages","stargazers_url":"https://api.github.com/repos/parejkoj/fgcmcal/stargazers","contributors_url":"https://api.github.com/repos/parejkoj/fgcmcal/contributors","subscribers_url":"https://api.github.com/repos/parejkoj/fgcmcal/subscribers","subscription_url":"https://api.github.com/repos/parejkoj/fgcmcal/subscription","commits_url":"https://api.github.com/repos/parejkoj/fgcmcal/commits{/sha}","git_commits_url":"https://api.github.com/repos/parejkoj/fgcmcal/git/commits{/sha}","comments_url":"https://api.github.com/repos/parejkoj/fgcmcal/comments{/number}","issue_comment_url":"https://api.github.com/repos/parejkoj/fgcmcal/issues/comments{/number}","contents_url":"https://api.github.com/repos/parejkoj/fgcmcal/contents/{+path}","compare_url":"https://api.github.com/repos/parejkoj/fgcmcal/compare/{base}...{head}","merges_url":"https://api.github.com/repos/parejkoj/fgcmcal/merges","archive_url":"https://api.github.com/repos/parejkoj/fgcmcal/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/parejkoj/fgcmcal/downloads","issues_url":"https://api.github.com/repos/parejkoj/fgcmcal/issues{/number}","pulls_url":"https://api.github.com/repos/parejkoj/fgcmcal/pulls{/number}","milestones_url":"https://api.github.com/repos/parejkoj/fgcmcal/milestones{/number}","notifications_url":"https://api.github.com/repos/parejkoj/fgcmcal/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/parejkoj/fgcmcal/labels{/name}","releases_url":"https://api.github.com/repos/parejkoj/fgcmcal/releases{/id}","deployments_url":"https://api.github.com/repos/parejkoj/fgcmcal/deployments","created_at":"2018-11-07T23:15:22Z","updated_at":"2018-11-07T23:15:24Z","pushed_at":"2018-11-12T05:46:34Z","git_url":"git://github.com/parejkoj/fgcmcal.git","ssh_url":"git@github.com:parejkoj/fgcmcal.git","clone_url":"https://github.com/parejkoj/fgcmcal.git","svn_url":"https://github.com/parejkoj/fgcmcal","homepage":"","size":682,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/1"},"html":{"href":"https://github.com/parejkoj/fgcmcal/pull/1"},"issue":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/issues/1"},"comments":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/parejkoj/fgcmcal/statuses/bf03b490d7030224fba7b6b7142343f515db06d2"}},"author_association":"OWNER"}}
{"actor":{"display_login":"erykoff"}}