def bypass_instcal(self, datasetType, pythonType, butlerLocation, dataId):
        # Workaround until I can access the butler
        instcalMap = self.map_instcal(dataId)
        dqmaskMap = self.map_dqmask(dataId)
        wtmapMap = self.map_wtmap(dataId)
        instcalType = getattr(afwImage,
                              instcalMap.getPythonType().split(".")[-1])
        dqmaskType = getattr(afwImage,
                             dqmaskMap.getPythonType().split(".")[-1])
        wtmapType = getattr(afwImage, wtmapMap.getPythonType().split(".")[-1])
        instcal = instcalType(instcalMap.getLocationsWithRoot()[0])
        dqmask = dqmaskType(dqmaskMap.getLocationsWithRoot()[0])
        wtmap = wtmapType(wtmapMap.getLocationsWithRoot()[0])

        mask = self.translate_dqmask(dqmask)
        variance = self.translate_wtmap(wtmap)

        mi = afwImage.MaskedImageF(afwImage.ImageF(instcal.getImage()), mask,
                                   variance)
        md = readMetadata(instcalMap.getLocationsWithRoot()[0])
        wcs = makeSkyWcs(md, strip=True)
        exp = afwImage.ExposureF(mi, wcs)

        exp.setPhotoCalib(
            afwImage.makePhotoCalibFromCalibZeroPoint(
                10**(0.4 * md.getScalar("MAGZERO")), 0))
        visitInfo = self.makeRawVisitInfo(md=md)
        exp.getInfo().setVisitInfo(visitInfo)

        for kw in ('LTV1', 'LTV2'):
            md.remove(kw)

        exp.setMetadata(md)
        return exp
Beispiel #2
0
    def writeFcr(self, dataRefList):
        self.log.info("Write Fcr ...")
        M_LN10 = math.log(10)
        dmag = list()
        for m in self.matchVec:
            if (m.good == True and m.mag != -9999 and m.jstar != -1 and
                m.mag0 != -9999 and m.mag_cat != -9999):
                mag = m.mag
                mag_cat = m.mag_cat
                exp_cor = -2.5*math.log10(self.fexp[m.iexp])
                chip_cor = -2.5*math.log10(self.fchip[m.ichip])
                gain_cor = self.ffpSet[m.iexp].eval(m.u, m.v)
                mag_cor = mag + exp_cor + chip_cor + gain_cor
                dmag.append(mag_cor - mag_cat)
        std, mean, n  = mosaicUtils.clippedStd(numpy.array(dmag), 2.1)
        for dataRef in dataRefList:
            iexp = dataRef.dataId["visit"]
            ichip = dataRef.dataId["ccd"]
            try:
                x0 = self.coeffSet[iexp].x0
                y0 = self.coeffSet[iexp].y0
            except:
                x0 = 0.0
                y0 = 0.0
            newP = measMosaic.convertFluxFitParams(measMosaic.FluxFitParams(self.ffpSet[iexp]),
                                                   self.ccdSet[ichip], x0, y0)
            metadata = measMosaic.metadataFromFluxFitParams(newP)
            exp = afwImage.ExposureI(0,0)
            exp.getMetadata().combine(metadata)
            scale = self.fexp[iexp]*self.fchip[ichip]
            constantPhotoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(1.0/scale, 1.0/scale*std*M_LN10*0.4)
            exp.setPhotoCalib(constantPhotoCalib)
            try:
                dataRef.put(exp, "fcr")
            except Exception as e:
                print("failed to write fcr: %s" % (e))

            # Write the flux fit (including Jacobian) as a PhotoCalib for
            # future compatibility with jointcal.  This is redundant with
            # the above, and should eventually supercede it.
            detector = dataRef.get("camera")[dataRef.dataId["ccd"]]
            nQuarter = detector.getOrientation().getNQuarter()
            bbox = detector.getBBox()
            try:
                # Reading the Wcs we just wrote obviously isn't efficient, but
                # it should be in the noise of the overall runtime and it
                # saves us from doing a bunch of refactoring in a fragile
                # package with no tests.
                wcs = dataRef.get("jointcal_wcs")
            except Exception as e:
                print("failed to read Wcs for PhotoCalib: %s" % (e))
                continue
            bf = measMosaic.FluxFitBoundedField(bbox, newP, wcs,
                                                zeroPoint=constantPhotoCalib.getInstFluxAtZeroMagnitude(),
                                                nQuarter=nQuarter)
            varyingPhotoCalib = afwImage.PhotoCalib(constantPhotoCalib.getCalibrationMean(),
                                                    constantPhotoCalib.getCalibrationErr(),
                                                    bf,
                                                    isConstant=False)
            dataRef.put(varyingPhotoCalib, "jointcal_photoCalib")
Beispiel #3
0
 def makeHSCExposure(self, galData, psfData, pixScale, variance):
     ny, nx = galData.shape
     exposure = afwImg.ExposureF(nx, ny)
     exposure.getMaskedImage().getImage().getArray()[:, :] = galData
     exposure.getMaskedImage().getVariance().getArray()[:, :] = variance
     #Set the PSF
     ngridPsf = psfData.shape[0]
     psfLsst = afwImg.ImageF(ngridPsf, ngridPsf)
     psfLsst.getArray()[:, :] = psfData
     psfLsst = psfLsst.convertD()
     kernel = afwMath.FixedKernel(psfLsst)
     kernelPSF = meaAlg.KernelPsf(kernel)
     exposure.setPsf(kernelPSF)
     #prepare the wcs
     #Rotation
     cdelt = (pixScale * afwGeom.arcseconds)
     CD = afwGeom.makeCdMatrix(cdelt, afwGeom.Angle(0.))  #no rotation
     #wcs
     crval = afwGeom.SpherePoint(afwGeom.Angle(0., afwGeom.degrees),
                                 afwGeom.Angle(0., afwGeom.degrees))
     #crval   =   afwCoord.IcrsCoord(0.*afwGeom.degrees, 0.*afwGeom.degrees) # hscpipe6
     crpix = afwGeom.Point2D(0.0, 0.0)
     dataWcs = afwGeom.makeSkyWcs(crpix, crval, CD)
     exposure.setWcs(dataWcs)
     #prepare the frc
     dataCalib = afwImg.makePhotoCalibFromCalibZeroPoint(63095734448.0194)
     exposure.setPhotoCalib(dataCalib)
     return exposure
Beispiel #4
0
    def __init__(self, *args, **kwargs):
        """Construct a ScaleZeroPointTask
        """
        pipeBase.Task.__init__(self, *args, **kwargs)

        # flux at mag=0 is 10^(zeroPoint/2.5)   because m = -2.5*log10(F/F0)
        fluxMag0 = 10**(0.4 * self.config.zeroPoint)
        self._photoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0)
Beispiel #5
0
    def scaleFromFluxMag0(self, fluxMag0):
        """Compute the scale for the specified fluxMag0

        This is a wrapper around scaleFromPhotoCalib, which see for more information

        @param[in] fluxMag0
        @return a pipeBase.Struct containing:
        - scale, as described in scaleFromPhotoCalib.
        """
        calib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0)
        return self.scaleFromPhotoCalib(calib)
Beispiel #6
0
    def setUp(self):
        self.dataDir = os.path.join(os.path.split(__file__)[0], "data")

        # Check the values below against what was written by comparing with
        # the code in `afw/tests/data/makeTestExposure.py`
        nx = ny = 10
        image = afwImage.ImageF(np.arange(nx*ny, dtype='f').reshape(nx, ny))
        variance = afwImage.ImageF(np.ones((nx, ny), dtype='f'))
        mask = afwImage.MaskX(nx, ny)
        mask.array[5, 5] = 5
        self.maskedImage = afwImage.MaskedImageF(image, mask, variance)

        self.v0PhotoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(1e6, 2e4)
        self.v1PhotoCalib = afwImage.PhotoCalib(1e6, 2e4)
Beispiel #7
0
    def setUp(self):
        self.dataDir = os.path.join(os.path.split(__file__)[0], "data")

        # Check the values below against what was written by comparing with
        # the code in `afw/tests/data/makeTestExposure.py`
        nx = ny = 10
        image = afwImage.ImageF(np.arange(nx*ny, dtype='f').reshape(nx, ny))
        variance = afwImage.ImageF(np.ones((nx, ny), dtype='f'))
        mask = afwImage.MaskX(nx, ny)
        mask.array[5, 5] = 5
        self.maskedImage = afwImage.MaskedImageF(image, mask, variance)

        self.v0PhotoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(1e6, 2e4)
        self.v1PhotoCalib = afwImage.PhotoCalib(1e6, 2e4)
Beispiel #8
0
    def setUp(self):
        '''Create two calibs, one with a valid zero-point and one without. Use these to create two UnitSystem
        objects.
        '''
        self.mag2Flux = lambda m: 10.0**(m/2.5)
        self.flux2Mag = lambda f: 2.5*np.log10(f)

        photoCalibNoZero = afwImage.PhotoCalib()
        photoCalibWithZero = afwImage.makePhotoCalibFromCalibZeroPoint(self.mag2Flux(25))

        scale = 0.2 * geom.arcseconds
        wcs = afwGeom.makeSkyWcs(crpix=geom.Point2D(),
                                 crval=geom.SpherePoint(45.0, 45.0, geom.degrees),
                                 cdMatrix=afwGeom.makeCdMatrix(scale=scale))

        self.unitNoZero = measModel.UnitSystem(wcs, photoCalibNoZero)
        self.unitWithZero = measModel.UnitSystem(wcs, photoCalibWithZero)
    def setUp(self):
        '''Create two calibs, one with a valid zero-point and one without. Use these to create two UnitSystem
        objects.
        '''
        self.mag2Flux = lambda m: 10.0**(m/2.5)
        self.flux2Mag = lambda f: 2.5*np.log10(f)

        photoCalibNoZero = afwImage.PhotoCalib()
        photoCalibWithZero = afwImage.makePhotoCalibFromCalibZeroPoint(self.mag2Flux(25))

        scale = 0.2 * afwGeom.arcseconds
        wcs = afwGeom.makeSkyWcs(crpix=afwGeom.Point2D(),
                                 crval=afwGeom.SpherePoint(45.0, 45.0, afwGeom.degrees),
                                 cdMatrix=afwGeom.makeCdMatrix(scale=scale))

        self.unitNoZero = measModel.UnitSystem(wcs, photoCalibNoZero)
        self.unitWithZero = measModel.UnitSystem(wcs, photoCalibWithZero)
def converttsField(infile, filt, exptime=53.907456):
    """Extract data from a tsField table

    @param[in] infile  path to tsField FITS file
    @param[in] filt  index of filter in tsField FILTERS metadata entry
    @param[in] exptime  exposure time (sec)

    @return a dict with the following entries:
    - photoCalib: an lsst.afw.PhotoCalib
    - gain: gain as a float
    - dateAvg: date of exposure at middle of exposure, as an lsst.daf.base.DateTime
    - exptime: exposure time (sec)
    - airmass: airmass
    """
    ptr = fits.open(infile)
    if ptr[0].header['NFIELDS'] != 1:
        print("INVALID TSFIELD FILE")
        sys.exit(1)
    filts = ptr[0].header['FILTERS'].split()
    idx = filts.index(filt)

    mjdTaiStart = ptr[1].data.field('mjd')[0][idx]        # MJD(TAI) when row 0 was read
    airmass = ptr[1].data.field("airmass")[0][idx]

    gain = float(ptr[1].data.field('gain')[0][idx])  # comes out as numpy.float32
    aa = ptr[1].data.field('aa')[0][idx]         # f0 = 10**(-0.4*aa) counts/second
    aaErr = ptr[1].data.field('aaErr')[0][idx]

    # Conversions
    dateAvg = dafBase.DateTime(mjdTaiStart + 0.5 * exptime / 3600 / 24)
    fluxMag0 = 10**(-0.4 * aa) * exptime
    dfluxMag0 = fluxMag0 * 0.4 * np.log(10.0) * aaErr

    photoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, dfluxMag0)

    ptr.close()

    return TsField(
        photoCalib=photoCalib,
        gain=gain,
        dateAvg=dateAvg,
        exptime=exptime,
        airmass=airmass,
    )
Beispiel #11
0
def applyMosaicResultsExposure(dataRef, calexp=None):
    """Update an Exposure with the Wcs, Calib, and flux scaling from meas_mosaic.

    If None, the calexp will be loaded from the dataRef.  Otherwise it is
    updated in-place.

    This assumes that the mosaic solution exists; an exception will be raised
    in the event that it does not.
    """
    if calexp is None:
        calexp = dataRef.get("calexp", immediate=True)

    nQuarter = calexp.getDetector().getOrientation().getNQuarter()
    dims = calexp.getDimensions()
    hscRun = mosaicUtils.checkHscStack(calexp.getMetadata())

    # Need the dimensions in coordinates used by meas_mosaic which defines 0,0 as the
    # lower-left hand corner on the sky
    if hscRun is None:
        if nQuarter % 2 != 0:
            width, height = calexp.getDimensions()
            dims = afwGeom.Extent2I(height, width)

    # return results in meas_mosaic coordinate system
    mosaic = getMosaicResults(dataRef, dims)

    # rotate wcs back to LSST coordinate system
    if nQuarter % 4 != 0 and hscRun is None:
        import lsst.meas.astrom as measAstrom
        mosaic.wcs = measAstrom.rotateWcsPixelsBy90(mosaic.wcs, 4 - nQuarter,
                                                    dims)
    calexp.setWcs(mosaic.wcs)

    fluxMag0 = mosaic.calib.getInstFluxAtZeroMagnitude()
    calexp.setPhotoCalib(
        afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0))

    mi = calexp.getMaskedImage()
    # rotate photometric correction to LSST coordiantes
    if nQuarter % 4 != 0 and hscRun is None:
        mosaic.fcor = afwMath.rotateImageBy90(mosaic.fcor, 4 - nQuarter)
    mi *= mosaic.fcor

    return Struct(exposure=calexp, mosaic=mosaic)
Beispiel #12
0
    def makeLsstExposure(galData, psfData, pixScale, variance):
        """
        make an LSST exposure object

        Parameters:
            galData (ndarray):  array of galaxy image
            psfData (ndarray):  array of PSF image
            pixScale (float):   pixel scale
            variance (float):   noise variance

        Returns:
            exposure:   LSST exposure object
        """
        if not with_lsst:
            raise ImportError('Do not have lsstpipe!')
        ny, nx = galData.shape
        exposure = afwImg.ExposureF(nx, ny)
        exposure.getMaskedImage().getImage().getArray()[:, :] = galData
        exposure.getMaskedImage().getVariance().getArray()[:, :] = variance
        #Set the PSF
        ngridPsf = psfData.shape[0]
        psfLsst = afwImg.ImageF(ngridPsf, ngridPsf)
        psfLsst.getArray()[:, :] = psfData
        psfLsst = psfLsst.convertD()
        kernel = afwMath.FixedKernel(psfLsst)
        kernelPSF = meaAlg.KernelPsf(kernel)
        exposure.setPsf(kernelPSF)
        #prepare the wcs
        #Rotation
        cdelt = (pixScale * afwGeom.arcseconds)
        CD = afwGeom.makeCdMatrix(cdelt, afwGeom.Angle(0.))  #no rotation
        #wcs
        crval = afwGeom.SpherePoint(afwGeom.Angle(0., afwGeom.degrees),
                                    afwGeom.Angle(0., afwGeom.degrees))
        #crval   =   afwCoord.IcrsCoord(0.*afwGeom.degrees, 0.*afwGeom.degrees) # hscpipe6
        crpix = afwGeom.Point2D(0.0, 0.0)
        dataWcs = afwGeom.makeSkyWcs(crpix, crval, CD)
        exposure.setWcs(dataWcs)
        #prepare the frc
        dataCalib = afwImg.makePhotoCalibFromCalibZeroPoint(63095734448.0194)
        exposure.setPhotoCalib(dataCalib)
        return exposure
def applyMosaicResultsExposure(dataRef, calexp=None):
    """Update an Exposure with the Wcs, Calib, and flux scaling from meas_mosaic.

    If None, the calexp will be loaded from the dataRef.  Otherwise it is
    updated in-place.

    This assumes that the mosaic solution exists; an exception will be raised
    in the event that it does not.
    """
    if calexp is None:
        calexp = dataRef.get("calexp", immediate=True)

    nQuarter = calexp.getDetector().getOrientation().getNQuarter()
    dims = calexp.getDimensions()
    hscRun = mosaicUtils.checkHscStack(calexp.getMetadata())

    # Need the dimensions in coordinates used by meas_mosaic which defines 0,0 as the
    # lower-left hand corner on the sky
    if hscRun is None:
        if nQuarter%2 != 0:
            width, height = calexp.getDimensions()
            dims = afwGeom.Extent2I(height, width)

    # return results in meas_mosaic coordinate system
    mosaic = getMosaicResults(dataRef, dims)

    # rotate wcs back to LSST coordinate system
    if nQuarter%4 != 0 and hscRun is None:
        import lsst.meas.astrom as measAstrom
        mosaic.wcs = measAstrom.rotateWcsPixelsBy90(mosaic.wcs, 4 - nQuarter, dims)
    calexp.setWcs(mosaic.wcs)

    fluxMag0 = mosaic.calib.getInstFluxAtZeroMagnitude()
    calexp.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 0.0))

    mi = calexp.getMaskedImage()
    # rotate photometric correction to LSST coordiantes
    if nQuarter%4 != 0 and hscRun is None:
        mosaic.fcor = afwMath.rotateImageBy90(mosaic.fcor, 4 - nQuarter)
    mi *= mosaic.fcor

    return Struct(exposure=calexp, mosaic=mosaic)
 def writeFcr(self, butler, ccdIdList, ccdSet, filterName,
              fexp, fchip, ffpSet):
     for ccdId in ccdIdList:
         iexp, ichip = self.decodeCcdExposureId(ccdId)
         if ichip not in ccdSet:
             continue
         x0 = 0.0
         y0 = 0.0
         newP = measMosaic.convertFluxFitParams(measMosaic.FluxFitParams(ffpSet[iexp]),
                                                ccdSet[ichip], x0, y0)
         metadata = measMosaic.metadataFromFluxFitParams(newP)
         exp = afwImage.ExposureI(0,0)
         exp.getMetadata().combine(metadata)
         scale = fexp[iexp] * fchip[ichip]
         exp.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(1.0/scale))
         exp.setFilter(afwImage.Filter(filterName))
         try:
             butler.put(exp, 'fcr', {'visit': iexp, 'ccd': ichip})
         except Exception as e:
             print("failed to write something: %s" % (e))
 def writeFcr(self, butler, ccdIdList, ccdSet, filterName, fexp, fchip,
              ffpSet):
     for ccdId in ccdIdList:
         iexp, ichip = self.decodeCcdExposureId(ccdId)
         if ichip not in ccdSet:
             continue
         x0 = 0.0
         y0 = 0.0
         newP = measMosaic.convertFluxFitParams(
             measMosaic.FluxFitParams(ffpSet[iexp]), ccdSet[ichip], x0, y0)
         metadata = measMosaic.metadataFromFluxFitParams(newP)
         exp = afwImage.ExposureI(0, 0)
         exp.getMetadata().combine(metadata)
         scale = fexp[iexp] * fchip[ichip]
         exp.setPhotoCalib(
             afwImage.makePhotoCalibFromCalibZeroPoint(1.0 / scale))
         exp.setFilter(afwImage.Filter(filterName))
         try:
             butler.put(exp, 'fcr', {'visit': iexp, 'ccd': ichip})
         except Exception as e:
             print("failed to write something: %s" % (e))
Beispiel #16
0
 def __init__(self, **kwargs):
     pipeBase.Task.__init__(self, **kwargs)
     self.cat = None
     self.footprints = []
     self.calib = afwImage.makePhotoCalibFromCalibZeroPoint(
         10**(0.4 * self.config.commonZp))
Beispiel #17
0
class MockCoaddTestData:
    """Generate repeatable simulated exposures with consistent metadata that
    are realistic enough to test the image coaddition algorithms.

    Notes
    -----
    The simple GaussianPsf used by lsst.meas.algorithms.testUtils.plantSources
    will always return an average position of (0, 0).
    The bounding box of the exposures MUST include (0, 0), or else the PSF will
    not be valid and `AssembleCoaddTask` will fail with the error
    'Could not find a valid average position for CoaddPsf'.

    Parameters
    ----------
    shape : `lsst.geom.Extent2I`, optional
        Size of the bounding box of the exposures to be simulated, in pixels.
    offset : `lsst.geom.Point2I`, optional
        Pixel coordinate of the lower left corner of the bounding box.
    backgroundLevel : `float`, optional
        Background value added to all pixels in the simulated images.
    seed : `int`, optional
        Seed value to initialize the random number generator.
    nSrc : `int`, optional
        Number of sources to simulate.
    fluxRange : `float`, optional
        Range in flux amplitude of the simulated sources.
    noiseLevel : `float`, optional
        Standard deviation of the noise to add to each pixel.
    sourceSigma : `float`, optional
        Average amplitude of the simulated sources,
        relative to ``noiseLevel``
    minPsfSize : `float`, optional
        The smallest PSF width (sigma) to use, in pixels.
    maxPsfSize : `float`, optional
        The largest PSF width (sigma) to use, in pixels.
    pixelScale : `lsst.geom.Angle`, optional
        The plate scale of the simulated images.
    ra : `lsst.geom.Angle`, optional
        Right Ascension of the boresight of the camera for the observation.
    dec : `lsst.geom.Angle`, optional
        Declination of the boresight of the camera for the observation.
    ccd : `int`, optional
        CCD number to put in the metadata of the exposure.
    patch : `int`, optional
        Unique identifier for a subdivision of a tract.
    patchGen2 : `str`, optional
        Unique identifier for a subdivision of a tract.
        In Gen 2 the patch identifier consists
        of two integers separated by a comma.
    tract : `int`, optional
        Unique identifier for a tract of a skyMap.

    Raises
    ------
    ValueError
        If the bounding box does not contain the pixel coordinate (0, 0).
        This is due to `GaussianPsf` that is used by `lsst.meas.algorithms.testUtils.plantSources`
        lacking the option to specify the pixel origin.
    """
    rotAngle = 0. * degrees
    "Rotation of the pixel grid on the sky, East from North (`lsst.geom.Angle`)."
    filterLabel = None
    """The filter definition, usually set in the current instruments' obs package.
    For these tests, a simple filter is defined without using an obs package (`lsst.afw.image.FilterLabel`).
    """
    rngData = None
    """Pre-initialized random number generator for constructing the test images
    repeatably (`numpy.random.Generator`).
    """
    rngMods = None
    """Pre-initialized random number generator for applying modifications to
    the test images for only some test cases (`numpy.random.Generator`).
    """
    kernelSize = None
    "Width of the kernel used for simulating sources, in pixels."
    exposures = {}
    "The simulated test data, with variable PSF sizes (`dict` of `lsst.afw.image.Exposure`)"
    matchedExposures = {}
    """The simulated exposures, all with PSF width set to `maxPsfSize`
    (`dict` of `lsst.afw.image.Exposure`).
    """
    photoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(27, 10)
    "The photometric zero point to use for converting counts to flux units (`lsst.afw.image.PhotoCalib`)."
    badMaskPlanes = ["NO_DATA", "BAD"]
    "Mask planes that, if set, the associated pixel should not be included in the coaddTempExp."
    detector = None
    "Properties of the CCD for the exposure (`lsst.afw.cameraGeom.Detector`)."

    def __init__(self,
                 shape=geom.Extent2I(201, 301),
                 offset=geom.Point2I(-123, -45),
                 backgroundLevel=314.592,
                 seed=42,
                 nSrc=37,
                 fluxRange=2.,
                 noiseLevel=5,
                 sourceSigma=200.,
                 minPsfSize=1.5,
                 maxPsfSize=3.,
                 pixelScale=0.2 * arcseconds,
                 ra=209. * degrees,
                 dec=-20.25 * degrees,
                 ccd=37,
                 patch=42,
                 patchGen2="2,3",
                 tract=0):
        self.ra = ra
        self.dec = dec
        self.pixelScale = pixelScale
        self.patch = patch
        self.patchGen2 = patchGen2
        self.tract = tract
        self.filterLabel = afwImage.FilterLabel(band="gTest", physical="gTest")
        self.rngData = np.random.default_rng(seed)
        self.rngMods = np.random.default_rng(seed + 1)
        self.bbox = geom.Box2I(offset, shape)
        if not self.bbox.contains(0, 0):
            raise ValueError(
                f"The bounding box must contain the coordinate (0, 0). {repr(self.bbox)}"
            )
        self.wcs = self.makeDummyWcs()

        # Set up properties of the simulations
        nSigmaForKernel = 5
        self.kernelSize = (int(maxPsfSize * nSigmaForKernel + 0.5) //
                           2) * 2 + 1  # make sure it is odd

        bufferSize = self.kernelSize // 2
        x0, y0 = self.bbox.getBegin()
        xSize, ySize = self.bbox.getDimensions()
        # Set the pixel coordinates and fluxes of the simulated sources.
        self.xLoc = self.rngData.random(nSrc) * (
            xSize - 2 * bufferSize) + bufferSize + x0
        self.yLoc = self.rngData.random(nSrc) * (
            ySize - 2 * bufferSize) + bufferSize + y0
        self.flux = (self.rngData.random(nSrc) *
                     (fluxRange - 1.) + 1.) * sourceSigma * noiseLevel

        self.backgroundLevel = backgroundLevel
        self.noiseLevel = noiseLevel
        self.minPsfSize = minPsfSize
        self.maxPsfSize = maxPsfSize
        self.detector = DetectorWrapper(name=f"detector {ccd}",
                                        id=ccd).detector

    def setDummyCoaddInputs(self, exposure, expId):
        """Generate an `ExposureCatalog` as though the exposures had been
        processed using `warpAndPsfMatch`.

        Parameters
        ----------
        exposure : `lsst.afw.image.Exposure`
            The exposure to construct a `CoaddInputs` `ExposureCatalog` for.
        expId : `int`
            A unique identifier for the visit.
        """
        badPixelMask = afwImage.Mask.getPlaneBitMask(self.badMaskPlanes)
        nGoodPix = np.sum(exposure.getMask().getArray() & badPixelMask == 0)

        config = CoaddInputRecorderConfig()
        inputRecorder = CoaddInputRecorderTask(config=config,
                                               name="inputRecorder")
        tempExpInputRecorder = inputRecorder.makeCoaddTempExpRecorder(expId,
                                                                      num=1)
        tempExpInputRecorder.addCalExp(exposure, expId, nGoodPix)
        tempExpInputRecorder.finish(exposure, nGoodPix=nGoodPix)

    def makeCoaddTempExp(self, rawExposure, visitInfo, expId):
        """Add the metadata required by `AssembleCoaddTask` to an exposure.

        Parameters
        ----------
        rawExposure : `lsst.afw.image.Exposure`
            The simulated exposure.
        visitInfo : `lsst.afw.image.VisitInfo`
            VisitInfo containing metadata for the exposure.
        expId : `int`
            A unique identifier for the visit.

        Returns
        -------
        tempExp : `lsst.afw.image.Exposure`
            The exposure, with all of the metadata needed for coaddition.
        """
        tempExp = rawExposure.clone()
        tempExp.setWcs(self.wcs)

        tempExp.setFilter(self.filterLabel)
        tempExp.setPhotoCalib(self.photoCalib)
        tempExp.getInfo().setVisitInfo(visitInfo)
        tempExp.getInfo().setDetector(self.detector)
        self.setDummyCoaddInputs(tempExp, expId)
        return tempExp

    def makeDummyWcs(self,
                     rotAngle=None,
                     pixelScale=None,
                     crval=None,
                     flipX=True):
        """Make a World Coordinate System object for testing.

        Parameters
        ----------
        rotAngle : `lsst.geom.Angle`
            Rotation of the CD matrix, East from North
        pixelScale : `lsst.geom.Angle`
            Pixel scale of the projection.
        crval : `lsst.afw.geom.SpherePoint`
            Coordinates of the reference pixel of the wcs.
        flipX : `bool`, optional
            Flip the direction of increasing Right Ascension.

        Returns
        -------
        wcs : `lsst.afw.geom.skyWcs.SkyWcs`
            A wcs that matches the inputs.
        """
        if rotAngle is None:
            rotAngle = self.rotAngle
        if pixelScale is None:
            pixelScale = self.pixelScale
        if crval is None:
            crval = geom.SpherePoint(self.ra, self.dec)
        crpix = geom.Box2D(self.bbox).getCenter()
        cdMatrix = afwGeom.makeCdMatrix(scale=pixelScale,
                                        orientation=rotAngle,
                                        flipX=flipX)
        wcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix)
        return wcs

    def makeDummyVisitInfo(self, exposureId, randomizeTime=False):
        """Make a self-consistent visitInfo object for testing.

        Parameters
        ----------
        exposureId : `int`, optional
            Unique integer identifier for this observation.
        randomizeTime : `bool`, optional
            Add a random offset within a 6 hour window to the observation time.

        Returns
        -------
        visitInfo : `lsst.afw.image.VisitInfo`
            VisitInfo for the exposure.
        """
        lsstLat = -30.244639 * u.degree
        lsstLon = -70.749417 * u.degree
        lsstAlt = 2663. * u.m
        lsstTemperature = 20. * u.Celsius
        lsstHumidity = 40.  # in percent
        lsstPressure = 73892. * u.pascal
        loc = EarthLocation(lat=lsstLat, lon=lsstLon, height=lsstAlt)

        time = Time(2000.0, format="jyear", scale="tt")
        if randomizeTime:
            # Pick a random time within a 6 hour window
            time += 6 * u.hour * (self.rngMods.random() - 0.5)
        radec = SkyCoord(dec=self.dec.asDegrees(),
                         ra=self.ra.asDegrees(),
                         unit='deg',
                         obstime=time,
                         frame='icrs',
                         location=loc)
        airmass = float(1.0 / np.sin(radec.altaz.alt))
        obsInfo = makeObservationInfo(
            location=loc,
            detector_exposure_id=exposureId,
            datetime_begin=time,
            datetime_end=time,
            boresight_airmass=airmass,
            boresight_rotation_angle=Angle(0. * u.degree),
            boresight_rotation_coord='sky',
            temperature=lsstTemperature,
            pressure=lsstPressure,
            relative_humidity=lsstHumidity,
            tracking_radec=radec,
            altaz_begin=radec.altaz,
            observation_type='science',
        )
        visitInfo = MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(
            obsInfo)
        return visitInfo

    def makeTestImage(self,
                      expId,
                      noiseLevel=None,
                      psfSize=None,
                      backgroundLevel=None,
                      detectionSigma=5.,
                      badRegionBox=None):
        """Make a reproduceable PSF-convolved masked image for testing.

        Parameters
        ----------
        expId : `int`
            A unique identifier to use to refer to the visit.
        noiseLevel : `float`, optional
            Standard deviation of the noise to add to each pixel.
        psfSize : `float`, optional
            Width of the PSF of the simulated sources, in pixels.
        backgroundLevel : `float`, optional
            Background value added to all pixels in the simulated images.
        detectionSigma : `float`, optional
            Threshold amplitude of the image to set the "DETECTED" mask.
        badRegionBox : `lsst.geom.Box2I`, optional
            Add a bad region bounding box (set to "BAD").
        """
        if backgroundLevel is None:
            backgroundLevel = self.backgroundLevel
        if noiseLevel is None:
            noiseLevel = 5.
        visitInfo = self.makeDummyVisitInfo(expId, randomizeTime=True)

        if psfSize is None:
            psfSize = self.rngMods.random() * (
                self.maxPsfSize - self.minPsfSize) + self.minPsfSize
        nSrc = len(self.flux)
        sigmas = [psfSize for src in range(nSrc)]
        sigmasPsfMatched = [self.maxPsfSize for src in range(nSrc)]
        coordList = list(zip(self.xLoc, self.yLoc, self.flux, sigmas))
        coordListPsfMatched = list(
            zip(self.xLoc, self.yLoc, self.flux, sigmasPsfMatched))
        xSize, ySize = self.bbox.getDimensions()
        model = plantSources(self.bbox,
                             self.kernelSize,
                             self.backgroundLevel,
                             coordList,
                             addPoissonNoise=False)
        modelPsfMatched = plantSources(self.bbox,
                                       self.kernelSize,
                                       self.backgroundLevel,
                                       coordListPsfMatched,
                                       addPoissonNoise=False)
        model.variance.array = np.abs(model.image.array) + noiseLevel
        modelPsfMatched.variance.array = np.abs(
            modelPsfMatched.image.array) + noiseLevel
        noise = self.rngData.random((ySize, xSize)) * noiseLevel
        noise -= np.median(noise)
        model.image.array += noise
        modelPsfMatched.image.array += noise
        detectedMask = afwImage.Mask.getPlaneBitMask("DETECTED")
        detectionThreshold = self.backgroundLevel + detectionSigma * noiseLevel
        model.mask.array[
            model.image.array > detectionThreshold] += detectedMask

        if badRegionBox is not None:
            model.mask[badRegionBox] = afwImage.Mask.getPlaneBitMask("BAD")

        exposure = self.makeCoaddTempExp(model, visitInfo, expId)
        matchedExposure = self.makeCoaddTempExp(modelPsfMatched, visitInfo,
                                                expId)
        return exposure, matchedExposure

    @staticmethod
    def makeGen2DataRefList(exposures,
                            matchedExposures,
                            tract=0,
                            patch="2,3",
                            coaddName="deep"):
        """Make data references from the simulated exposures that can be
        retrieved using the Gen 2 Butler API.

        Parameters
        ----------
        tract : `int`
            Unique identifier for a tract of a skyMap.
        patch : `str`
            Unique identifier for a subdivision of a tract.
        coaddName : `str`
            The type of coadd being produced. Typically 'deep'.

        Returns
        -------
        dataRefList : `list` of `MockGen2WarpReference`
            The data references.

        """
        dataRefList = []
        for expId in exposures:
            exposure = exposures[expId]
            exposurePsfMatched = matchedExposures[expId]
            dataRef = MockGen2WarpReference(
                exposure,
                exposurePsfMatched=exposurePsfMatched,
                coaddName=coaddName,
                tract=tract,
                patch=patch,
                visit=expId)
            dataRefList.append(dataRef)
        return dataRefList

    @staticmethod
    def makeDataRefList(exposures,
                        matchedExposures,
                        warpType,
                        tract=0,
                        patch=42,
                        coaddName="deep"):
        """Make data references from the simulated exposures that can be
        retrieved using the Gen 3 Butler API.

        Parameters
        ----------
        warpType : `str`
            Either 'direct' or 'psfMatched'.
        tract : `int`, optional
            Unique identifier for a tract of a skyMap.
        patch : `int`, optional
            Unique identifier for a subdivision of a tract.
        coaddName : `str`, optional
            The type of coadd being produced. Typically 'deep'.

        Returns
        -------
        dataRefList : `list` of `MockWarpReference`
            The data references.

        Raises
        ------
        ValueError
            If an unknown `warpType` is supplied.
        """
        dataRefList = []
        for expId in exposures:
            if warpType == 'direct':
                exposure = exposures[expId]
            elif warpType == 'psfMatched':
                exposure = matchedExposures[expId]
            else:
                raise ValueError(
                    "warpType must be one of 'direct' or 'psfMatched'")
            dataRef = MockWarpReference(exposure,
                                        coaddName=coaddName,
                                        tract=tract,
                                        patch=patch,
                                        visit=expId)
            dataRefList.append(dataRef)
        return dataRefList
Beispiel #18
0
 def makePhotoCalib(self, zeroPoint):
     fluxMag0 = 10**(0.4 * zeroPoint)
     return afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0, 1.0)
 def __init__(self, **kwargs):
     pipeBase.Task.__init__(self, **kwargs)
     self.multi_matches = None
     self.calib = afwImage.makePhotoCalibFromCalibZeroPoint(
         10**(0.4 * self.config.commonZp))
     self.calibDict = defaultdict(dict)
Beispiel #20
0
    def run(self, exposure, sourceCat, expId=0):
        """!Do photometric calibration - select matches to use and (possibly iteratively) compute
        the zero point.

        @param[in]  exposure  Exposure upon which the sources in the matches were detected.
        @param[in]  sourceCat  A catalog of sources to use in the calibration
        (@em i.e. a list of lsst.afw.table.Match with
        @c first being of type lsst.afw.table.SimpleRecord and @c second type lsst.afw.table.SourceRecord ---
        the reference object and matched object respectively).
        (will not be modified  except to set the outputField if requested.).

        @return Struct of:
         - photoCalib -- @link lsst::afw::image::PhotoCalib@endlink object containing the calibration
         - arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays
         - matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches.
         - zp ---------- Photometric zero point (mag)
         - sigma ------- Standard deviation of fit of photometric zero point (mag)
         - ngood ------- Number of sources used to fit photometric zero point

        The exposure is only used to provide the name of the filter being calibrated (it may also be
        used to generate debugging plots).

        The reference objects:
         - Must include a field @c photometric; True for objects which should be considered as
            photometric standards
         - Must include a field @c flux; the flux used to impose a magnitude limit and also to calibrate
            the data to (unless a color term is specified, in which case ColorTerm.primary is used;
            See https://jira.lsstcorp.org/browse/DM-933)
         - May include a field @c stargal; if present, True means that the object is a star
         - May include a field @c var; if present, True means that the object is variable

        The measured sources:
        - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration

        @throws RuntimeError with the following strings:

        <DL>
        <DT> No matches to use for photocal
        <DD> No matches are available (perhaps no sources/references were selected by the matcher).
        <DT> No reference stars are available
        <DD> No matches are available from which to extract magnitudes.
        </DL>
        """
        import lsstDebug

        display = lsstDebug.Info(__name__).display
        displaySources = display and lsstDebug.Info(__name__).displaySources
        self.scatterPlot = display and lsstDebug.Info(__name__).scatterPlot

        if self.scatterPlot:
            from matplotlib import pyplot
            try:
                self.fig.clf()
            except Exception:
                self.fig = pyplot.figure()

        filterName = exposure.getFilter().getName()

        # Match sources
        matchResults = self.match.run(sourceCat, filterName)
        matches = matchResults.matches

        reserveResults = self.reserve.run([mm.second for mm in matches], expId=expId)
        if displaySources:
            self.displaySources(exposure, matches, reserveResults.reserved)
        if reserveResults.reserved.sum() > 0:
            matches = [mm for mm, use in zip(matches, reserveResults.use) if use]
        if len(matches) == 0:
            raise RuntimeError("No matches to use for photocal")
        if self.usedKey is not None:
            for mm in matches:
                mm.second.set(self.usedKey, True)

        # Prepare for fitting
        sourceKeys = self.getSourceKeys(matches[0].second.schema)
        arrays = self.extractMagArrays(matches=matches, filterName=filterName, sourceKeys=sourceKeys)

        # Fit for zeropoint
        r = self.getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
        self.log.info("Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)

        # Prepare the results
        flux0 = 10**(0.4*r.zp)  # Flux of mag=0 star
        flux0err = 0.4*math.log(10)*flux0*r.sigma  # Error in flux0
        photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err)

        return pipeBase.Struct(
            photoCalib=photoCalib,
            arrays=arrays,
            matches=matches,
            zp=r.zp,
            sigma=r.sigma,
            ngood=r.ngood,
        )
    def setUp(self):
        tract = 0
        band = 'r'
        patch = 0
        visits = [100, 101]
        # Good to test crossing 0.
        ra_center = 0.0
        dec_center = -45.0
        pixel_scale = 0.2
        coadd_zp = 27.0

        # Generate a mock skymap with one patch
        config = DiscreteSkyMap.ConfigClass()
        config.raList = [ra_center]
        config.decList = [dec_center]
        config.radiusList = [150 * pixel_scale / 3600.]
        config.patchInnerDimensions = (350, 350)
        config.patchBorder = 50
        config.tractOverlap = 0.0
        config.pixelScale = pixel_scale
        sky_map = DiscreteSkyMap(config)

        visit_summaries = [
            makeMockVisitSummary(visit,
                                 ra_center=ra_center,
                                 dec_center=dec_center) for visit in visits
        ]
        visit_summary_refs = [
            MockVisitSummaryReference(visit_summary, visit)
            for visit_summary, visit in zip(visit_summaries, visits)
        ]
        self.visit_summary_dict = {
            visit: ref.get()
            for ref, visit in zip(visit_summary_refs, visits)
        }

        # Generate an input map.  Note that this does not need to be consistent
        # with the visit_summary projections, we're just tracking values.
        input_map = hsp.HealSparseMap.make_empty(
            nside_coverage=256,
            nside_sparse=32768,
            dtype=hsp.WIDE_MASK,
            wide_mask_maxbits=len(visits) * 2)
        patch_poly = afwGeom.Polygon(
            geom.Box2D(sky_map[tract][patch].getOuterBBox()))
        sph_pts = sky_map[tract].getWcs().pixelToSky(
            patch_poly.convexHull().getVertices())
        patch_poly_radec = np.array([(sph.getRa().asDegrees(),
                                      sph.getDec().asDegrees())
                                     for sph in sph_pts])
        poly = hsp.Polygon(ra=patch_poly_radec[:-1, 0],
                           dec=patch_poly_radec[:-1, 1],
                           value=[0])
        poly_pixels = poly.get_pixels(nside=input_map.nside_sparse)
        # The input map has full coverage for bits 0 and 1
        input_map.set_bits_pix(poly_pixels, [0])
        input_map.set_bits_pix(poly_pixels, [1])

        input_map_ref = MockInputMapReference(input_map,
                                              patch=patch,
                                              tract=tract)
        self.input_map_dict = {patch: input_map_ref}

        coadd = afwImage.ExposureF(sky_map[tract][patch].getOuterBBox(),
                                   sky_map[tract].getWcs())
        instFluxMag0 = 10.**(coadd_zp / 2.5)
        pc = afwImage.makePhotoCalibFromCalibZeroPoint(instFluxMag0)
        coadd.setPhotoCalib(pc)

        # Mock the coadd input ccd table
        schema = afwTable.ExposureTable.makeMinimalSchema()
        schema.addField("ccd", type="I")
        schema.addField("visit", type="I")
        schema.addField("weight", type="F")
        ccds = afwTable.ExposureCatalog(schema)
        ccds.resize(2)
        ccds['id'] = np.arange(2)
        ccds['visit'][0] = visits[0]
        ccds['visit'][1] = visits[1]
        ccds['ccd'][0] = 0
        ccds['ccd'][1] = 1
        ccds['weight'] = 10.0
        for ccd_row in ccds:
            summary = self.visit_summary_dict[ccd_row['visit']].find(
                ccd_row['ccd'])
            ccd_row.setWcs(summary.getWcs())
            ccd_row.setPsf(summary.getPsf())
            ccd_row.setBBox(summary.getBBox())
            ccd_row.setPhotoCalib(summary.getPhotoCalib())

        inputs = afwImage.CoaddInputs()
        inputs.ccds = ccds
        coadd.getInfo().setCoaddInputs(inputs)

        coadd_ref = MockCoaddReference(coadd, patch=patch, tract=tract)
        self.coadd_dict = {patch: coadd_ref}

        self.tract = tract
        self.band = band
        self.sky_map = sky_map
        self.input_map = input_map
Beispiel #22
0
    def run(self, exposure, sourceCat, expId=0):
        """!Do photometric calibration - select matches to use and (possibly iteratively) compute
        the zero point.

        @param[in]  exposure  Exposure upon which the sources in the matches were detected.
        @param[in]  sourceCat  A catalog of sources to use in the calibration
        (@em i.e. a list of lsst.afw.table.Match with
        @c first being of type lsst.afw.table.SimpleRecord and @c second type lsst.afw.table.SourceRecord ---
        the reference object and matched object respectively).
        (will not be modified  except to set the outputField if requested.).

        @return Struct of:
         - photoCalib -- @link lsst::afw::image::PhotoCalib@endlink object containing the calibration
         - arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays
         - matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches.
         - zp ---------- Photometric zero point (mag)
         - sigma ------- Standard deviation of fit of photometric zero point (mag)
         - ngood ------- Number of sources used to fit photometric zero point

        The exposure is only used to provide the name of the filter being calibrated (it may also be
        used to generate debugging plots).

        The reference objects:
         - Must include a field @c photometric; True for objects which should be considered as
            photometric standards
         - Must include a field @c flux; the flux used to impose a magnitude limit and also to calibrate
            the data to (unless a color term is specified, in which case ColorTerm.primary is used;
            See https://jira.lsstcorp.org/browse/DM-933)
         - May include a field @c stargal; if present, True means that the object is a star
         - May include a field @c var; if present, True means that the object is variable

        The measured sources:
        - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration

        @throws RuntimeError with the following strings:

        <DL>
        <DT> No matches to use for photocal
        <DD> No matches are available (perhaps no sources/references were selected by the matcher).
        <DT> No reference stars are available
        <DD> No matches are available from which to extract magnitudes.
        </DL>
        """
        import lsstDebug

        display = lsstDebug.Info(__name__).display
        displaySources = display and lsstDebug.Info(__name__).displaySources
        self.scatterPlot = display and lsstDebug.Info(__name__).scatterPlot

        if self.scatterPlot:
            from matplotlib import pyplot
            try:
                self.fig.clf()
            except Exception:
                self.fig = pyplot.figure()

        filterLabel = exposure.getFilterLabel()

        # Match sources
        matchResults = self.match.run(sourceCat, filterLabel.bandLabel)
        matches = matchResults.matches

        reserveResults = self.reserve.run([mm.second for mm in matches],
                                          expId=expId)
        if displaySources:
            self.displaySources(exposure, matches, reserveResults.reserved)
        if reserveResults.reserved.sum() > 0:
            matches = [
                mm for mm, use in zip(matches, reserveResults.use) if use
            ]
        if len(matches) == 0:
            raise RuntimeError("No matches to use for photocal")
        if self.usedKey is not None:
            for mm in matches:
                mm.second.set(self.usedKey, True)

        # Prepare for fitting
        sourceKeys = self.getSourceKeys(matches[0].second.schema)
        arrays = self.extractMagArrays(matches, filterLabel, sourceKeys)

        # Fit for zeropoint
        r = self.getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
        self.log.info("Magnitude zero point: %f +/- %f from %d stars", r.zp,
                      r.sigma, r.ngood)

        # Prepare the results
        flux0 = 10**(0.4 * r.zp)  # Flux of mag=0 star
        flux0err = 0.4 * math.log(10) * flux0 * r.sigma  # Error in flux0
        photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err)

        return pipeBase.Struct(
            photoCalib=photoCalib,
            arrays=arrays,
            matches=matches,
            zp=r.zp,
            sigma=r.sigma,
            ngood=r.ngood,
        )
Beispiel #23
0
def makeMockVisitSummary(visit,
                         ra_center=0.0,
                         dec_center=-45.0,
                         physical_filter='TEST-I',
                         band='i',
                         mjd=59234.7083333334,
                         psf_sigma=3.0,
                         zenith_distance=45.0,
                         zero_point=30.0,
                         sky_background=100.0,
                         sky_noise=10.0,
                         mean_var=100.0,
                         exposure_time=100.0,
                         detector_size=200,
                         pixel_scale=0.2):
    """Make a mock visit summary catalog.

    This will contain two square detectors with the same metadata,
    with a small (20 pixel) gap between the detectors.  There is no
    rotation, as each detector is simply offset in RA from the
    specified boresight.

    Parameters
    ----------
    visit : `int`
        Visit number.
    ra_center : `float`
        Right ascension of the center of the "camera" boresight (degrees).
    dec_center : `float`
        Declination of the center of the "camera" boresight (degrees).
    physical_filter : `str`
        Arbitrary name for the physical filter.
    band : `str`
        Name of the associated band.
    mjd : `float`
        Modified Julian Date.
    psf_sigma : `float`
        Sigma width of Gaussian psf.
    zenith_distance : `float`
        Distance from zenith of the visit (degrees).
    zero_point : `float`
        Constant zero point for the visit (magnitudes).
    sky_background : `float`
        Background level for the visit (counts).
    sky_noise : `float`
        Noise level for the background of the visit (counts).
    mean_var : `float`
        Mean of the variance plane of the visit (counts).
    exposure_time : `float`
        Exposure time of the visit (seconds).
    detector_size : `int`
        Size of each square detector in the visit (pixels).
    pixel_scale : `float`
        Size of the pixel in arcseconds per pixel.

    Returns
    -------
    visit_summary : `lsst.afw.table.ExposureCatalog`
    """
    # We are making a 2 detector "camera"
    n_detector = 2

    schema = ConsolidateVisitSummaryTask()._makeVisitSummarySchema()
    visit_summary = afwTable.ExposureCatalog(schema)
    visit_summary.resize(n_detector)

    bbox = geom.Box2I(x=geom.IntervalI(min=0, max=detector_size - 1),
                      y=geom.IntervalI(min=0, max=detector_size - 1))

    for detector_id in range(n_detector):
        row = visit_summary[detector_id]

        row['id'] = detector_id
        row.setBBox(bbox)
        row['visit'] = visit
        row['physical_filter'] = physical_filter
        row['band'] = band
        row['zenithDistance'] = zenith_distance
        row['zeroPoint'] = zero_point
        row['skyBg'] = sky_background
        row['skyNoise'] = sky_noise
        row['meanVar'] = mean_var

        # Generate a photocalib
        instFluxMag0 = 10.**(zero_point / 2.5)
        row.setPhotoCalib(
            afwImage.makePhotoCalibFromCalibZeroPoint(instFluxMag0))

        # Generate a WCS and set values accordingly
        crpix = geom.Point2D(detector_size / 2., detector_size / 2.)
        # Create a 20 pixel gap between the two detectors (each offset 10 pixels).
        if detector_id == 0:
            delta_ra = -1.0 * ((detector_size + 10) * pixel_scale /
                               3600.) / np.cos(np.deg2rad(dec_center))
            delta_dec = 0.0
        elif detector_id == 1:
            delta_ra = ((detector_size + 10) * pixel_scale / 3600.) / np.cos(
                np.deg2rad(dec_center))
            delta_dec = 0.0
        crval = geom.SpherePoint(ra_center + delta_ra, dec_center + delta_dec,
                                 geom.degrees)
        cd_matrix = afwGeom.makeCdMatrix(scale=pixel_scale * geom.arcseconds,
                                         orientation=0.0 * geom.degrees)
        wcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cd_matrix)
        row.setWcs(wcs)

        sph_pts = wcs.pixelToSky(geom.Box2D(bbox).getCorners())
        row['raCorners'] = np.array(
            [float(sph.getRa().asDegrees()) for sph in sph_pts])
        row['decCorners'] = np.array(
            [float(sph.getDec().asDegrees()) for sph in sph_pts])
        sph_pt = wcs.pixelToSky(bbox.getCenter())
        row['ra'] = sph_pt.getRa().asDegrees()
        row['decl'] = sph_pt.getDec().asDegrees()

        # Generate a visitInfo.
        # This does not need to be consistent with the zenith angle in the table,
        # it just needs to be valid and have sufficient information to compute
        # exposure time and parallactic angle.
        date = DateTime(date=mjd, system=DateTime.DateSystem.MJD)
        visit_info = afwImage.VisitInfo(
            exposureId=visit,
            exposureTime=exposure_time,
            date=date,
            darkTime=0.0,
            boresightRaDec=geom.SpherePoint(ra_center, dec_center,
                                            geom.degrees),
            era=45.1 * geom.degrees,
            observatory=Observatory(11.1 * geom.degrees, 0.0 * geom.degrees,
                                    0.333),
            boresightRotAngle=0.0 * geom.degrees,
            rotType=afwImage.RotType.SKY)
        row.setVisitInfo(visit_info)

        # Generate a PSF and set values accordingly
        psf = GaussianPsf(15, 15, psf_sigma)
        row.setPsf(psf)
        psfAvgPos = psf.getAveragePosition()
        shape = psf.computeShape(psfAvgPos)
        row['psfSigma'] = psf.getSigma()
        row['psfIxx'] = shape.getIxx()
        row['psfIyy'] = shape.getIyy()
        row['psfIxy'] = shape.getIxy()
        row['psfArea'] = shape.getArea()

    return visit_summary