def __init__(self, afwDetector, afwCamera, obs_metadata, epoch):
        """
        @param [in] afwDetector is an instantiation of afw.cameraGeom.Detector

        @param [in] afwCamera is an instantiation of afw.cameraGeom.Camera

        @param [in] obs_metadata is an instantiation of ObservationMetaData
        characterizing the telescope pointing

        @param [in] epoch is the epoch in Julian years of the equinox against
        which RA and Dec are measured
        """

        tanSipWcs = tanSipWcsFromDetector(afwDetector, afwCamera, obs_metadata, epoch)

        self.afwDetector = afwDetector
        self.afwCamera = afwCamera
        self.obs_metadata = obs_metadata
        self.epoch = epoch

        self.fitsHeader = tanSipWcs.getFitsMetadata()
        self.fitsHeader.set("EXTTYPE", "IMAGE")

        self.crpix1 = self.fitsHeader.get("CRPIX1")
        self.crpix2 = self.fitsHeader.get("CRPIX2")

        self.afw_crpix1 = self.crpix1
        self.afw_crpix2 = self.crpix2

        self.crval1 = self.fitsHeader.get("CRVAL1")
        self.crval2 = self.fitsHeader.get("CRVAL2")

        self.origin = galsim.PositionD(x=self.crpix1, y=self.crpix2)
    def testTanSipWcs(self):
        """
        Test that tanSipWcsFromDetector works by fitting a TAN WCS and a TAN-SIP WCS to
        a detector with distortions and verifying that the TAN-SIP WCS better approximates
        the truth.
        """

        tanWcs = tanWcsFromDetector(self.detector.getName(), self.camera_wrapper,
                                    self.obs, self.epoch)
        tanSipWcs = tanSipWcsFromDetector(self.detector.getName(), self.camera_wrapper,
                                          self.obs, self.epoch)

        tanWcsRa = []
        tanWcsDec = []
        tanSipWcsRa = []
        tanSipWcsDec = []

        xPixList = []
        yPixList = []
        for xx in np.arange(0.0, 4001.0, 100.0):
            for yy in np.arange(0.0, 4001.0, 100.0):
                xPixList.append(xx)
                yPixList.append(yy)

                pt = afwGeom.Point2D(xx, yy)
                skyPt = tanWcs.pixelToSky(pt).getPosition(LsstGeom.degrees)
                tanWcsRa.append(skyPt.getX())
                tanWcsDec.append(skyPt.getY())

                skyPt = tanSipWcs.pixelToSky(pt).getPosition(LsstGeom.degrees)
                tanSipWcsRa.append(skyPt.getX())
                tanSipWcsDec.append(skyPt.getY())

        tanWcsRa = np.radians(np.array(tanWcsRa))
        tanWcsDec = np.radians(np.array(tanWcsDec))

        tanSipWcsRa = np.radians(np.array(tanSipWcsRa))
        tanSipWcsDec = np.radians(np.array(tanSipWcsDec))

        xPixList = np.array(xPixList)
        yPixList = np.array(yPixList)

        (raTest,
         decTest) = self.camera_wrapper._raDecFromPixelCoords(xPixList, yPixList,
                                                              [self.detector.getName()]*len(xPixList),
                                                              obs_metadata=self.obs,
                                                              epoch=self.epoch)

        tanDistanceList = arcsecFromRadians(haversine(raTest, decTest, tanWcsRa, tanWcsDec))
        tanSipDistanceList = arcsecFromRadians(haversine(raTest, decTest, tanSipWcsRa, tanSipWcsDec))

        maxDistanceTan = tanDistanceList.max()
        maxDistanceTanSip = tanSipDistanceList.max()

        msg = 'max error in TAN WCS %e arcsec; in TAN-SIP %e arcsec' % (maxDistanceTan, maxDistanceTanSip)
        self.assertLess(maxDistanceTanSip, 0.01, msg=msg)
        self.assertGreater(maxDistanceTan-maxDistanceTanSip, 1.0e-10, msg=msg)
    def testTanSipWcs(self):
        """
        Test that tanSipWcsFromDetector works by fitting a TAN WCS and a TAN-SIP WCS to
        a detector with distortions and verifying that the TAN-SIP WCS better approximates
        the truth.
        """


        tanWcs = tanWcsFromDetector(self.detector, self.camera, self.obs, self.epoch)
        tanSipWcs = tanSipWcsFromDetector(self.detector, self.camera, self.obs, self.epoch)

        tanWcsRa = []
        tanWcsDec = []
        tanSipWcsRa = []
        tanSipWcsDec = []

        xPixList = []
        yPixList = []
        for xx in numpy.arange(0.0, 4001.0, 100.0):
            for yy in numpy.arange(0.0, 4001.0, 100.0):
                xPixList.append(xx)
                yPixList.append(yy)

                pt = afwGeom.Point2D(xx ,yy)
                skyPt = tanWcs.pixelToSky(pt).getPosition()
                tanWcsRa.append(skyPt.getX())
                tanWcsDec.append(skyPt.getY())

                skyPt = tanSipWcs.pixelToSky(pt).getPosition()
                tanSipWcsRa.append(skyPt.getX())
                tanSipWcsDec.append(skyPt.getY())

        tanWcsRa = numpy.radians(numpy.array(tanWcsRa))
        tanWcsDec = numpy.radians(numpy.array(tanWcsDec))

        tanSipWcsRa = numpy.radians(numpy.array(tanSipWcsRa))
        tanSipWcsDec = numpy.radians(numpy.array(tanSipWcsDec))

        xPixList = numpy.array(xPixList)
        yPixList = numpy.array(yPixList)

        raTest, decTest = _raDecFromPixelCoords(xPixList, yPixList,
                                                [self.detector.getName()]*len(xPixList),
                                                camera=self.camera, obs_metadata=self.obs,
                                                epoch=self.epoch)

        tanDistanceList = arcsecFromRadians(haversine(raTest, decTest, tanWcsRa, tanWcsDec))
        tanSipDistanceList = arcsecFromRadians(haversine(raTest, decTest, tanSipWcsRa, tanSipWcsDec))

        maxDistanceTan = tanDistanceList.max()
        maxDistanceTanSip = tanSipDistanceList.max()

        msg = 'max error in TAN WCS %e arcsec; in TAN-SIP %e arcsec' % (maxDistanceTan, maxDistanceTanSip)
        self.assertLess(maxDistanceTanSip, 0.01, msg=msg)
        self.assertGreater(maxDistanceTan-maxDistanceTanSip, 1.0e-10, msg=msg)
    def __init__(self, afwDetector, afwCamera, obs_metadata, epoch, photParams=None, wcs=None):
        """
        @param [in] afwDetector is an instantiation of afw.cameraGeom.Detector

        @param [in] afwCamera is an instantiation of afw.cameraGeom.Camera

        @param [in] obs_metadata is an instantiation of ObservationMetaData
        characterizing the telescope pointing

        @param [in] epoch is the epoch in Julian years of the equinox against
        which RA and Dec are measured

        @param [in] photParams is an instantiation of PhotometricParameters
        (it will contain information about gain, exposure time, etc.)

        @param [in] wcs is a kwarg that is used by the method _newOrigin().
        The wcs kwarg in this constructor method should not be used by users.
        """

        if wcs is None:
            self._tanSipWcs = tanSipWcsFromDetector(afwDetector, afwCamera, obs_metadata, epoch)
        else:
            self._tanSipWcs = wcs

        self.afwDetector = afwDetector
        self.afwCamera = afwCamera
        self.obs_metadata = obs_metadata
        self.photParams = photParams
        self.epoch = epoch

        self.fitsHeader = self._tanSipWcs.getFitsMetadata()
        self.fitsHeader.set("EXTTYPE", "IMAGE")

        if self.obs_metadata.bandpass is not None:
            if not isinstance(self.obs_metadata.bandpass, list) and not isinstance(self.obs_metadata.bandpass, numpy.ndarray):
                self.fitsHeader.set("FILTER", self.obs_metadata.bandpass)

        if self.obs_metadata.mjd is not None:
            self.fitsHeader.set("MJD-OBS", self.obs_metadata.mjd.TAI)

        if self.photParams is not None:
            self.fitsHeader.set("EXPTIME", self.photParams.nexp*self.photParams.exptime)

        self.crpix1 = self.fitsHeader.get("CRPIX1")
        self.crpix2 = self.fitsHeader.get("CRPIX2")

        self.afw_crpix1 = self.crpix1
        self.afw_crpix2 = self.crpix2

        self.crval1 = self.fitsHeader.get("CRVAL1")
        self.crval2 = self.fitsHeader.get("CRVAL2")

        self.origin = galsim.PositionD(x=self.crpix1, y=self.crpix2)
    def __init__(self,
                 detectorName,
                 cameraWrapper,
                 obs_metadata,
                 epoch,
                 photParams=None,
                 wcs=None):
        """
        @param [in] detectorName is the name of the detector as stored
        by afw

        @param [in] cameraWrapper is an instantionat of a GalSimCameraWrapper

        @param [in] obs_metadata is an instantiation of ObservationMetaData
        characterizing the telescope pointing

        @param [in] epoch is the epoch in Julian years of the equinox against
        which RA and Dec are measured

        @param [in] photParams is an instantiation of PhotometricParameters
        (it will contain information about gain, exposure time, etc.)

        @param [in] wcs is a kwarg that is used by the method _newOrigin().
        The wcs kwarg in this constructor method should not be used by users.
        """

        if not isinstance(cameraWrapper, GalSimCameraWrapper):
            raise RuntimeError(
                "You must pass GalSim_afw_TanSipWCS an instantiation "
                "of GalSimCameraWrapper or one of its daughter "
                "classes")

        if wcs is None:
            self._tanSipWcs = tanSipWcsFromDetector(detectorName,
                                                    cameraWrapper,
                                                    obs_metadata, epoch)
        else:
            self._tanSipWcs = wcs

        self.detectorName = detectorName
        self.cameraWrapper = cameraWrapper
        self.obs_metadata = obs_metadata
        self.photParams = photParams
        self.epoch = epoch

        # this is needed to match the GalSim v1.5 API
        self._color = None

        self.fitsHeader = self._tanSipWcs.getFitsMetadata()
        self.fitsHeader.set("EXTTYPE", "IMAGE")

        if self.obs_metadata.bandpass is not None:
            if (not isinstance(self.obs_metadata.bandpass, list) and
                    not isinstance(self.obs_metadata.bandpass, np.ndarray)):

                self.fitsHeader.set("FILTER", self.obs_metadata.bandpass)

        if self.obs_metadata.mjd is not None:
            self.fitsHeader.set("MJD-OBS", self.obs_metadata.mjd.TAI)

        if self.photParams is not None:
            self.fitsHeader.set("EXPTIME",
                                self.photParams.nexp * self.photParams.exptime)

        # Add pointing information to FITS header.
        if self.obs_metadata.pointingRA is not None:
            self.fitsHeader.set('RATEL', obs_metadata.pointingRA)
        if self.obs_metadata.pointingDec is not None:
            self.fitsHeader.set('DECTEL', obs_metadata.pointingDec)
        if self.obs_metadata.rotSkyPos is not None:
            self.fitsHeader.set('ROTANGLE', obs_metadata.rotSkyPos)

        self.crpix1 = self.fitsHeader.getScalar("CRPIX1")
        self.crpix2 = self.fitsHeader.getScalar("CRPIX2")

        self.afw_crpix1 = self.crpix1
        self.afw_crpix2 = self.crpix2

        self.crval1 = self.fitsHeader.getScalar("CRVAL1")
        self.crval2 = self.fitsHeader.getScalar("CRVAL2")

        self.origin = galsim.PositionD(x=self.crpix1, y=self.crpix2)
        self._color = None
    def __init__(self,
                 afwDetector,
                 afwCamera,
                 obs_metadata,
                 epoch,
                 photParams=None,
                 wcs=None):
        """
        @param [in] afwDetector is an instantiation of afw.cameraGeom.Detector

        @param [in] afwCamera is an instantiation of afw.cameraGeom.Camera

        @param [in] obs_metadata is an instantiation of ObservationMetaData
        characterizing the telescope pointing

        @param [in] epoch is the epoch in Julian years of the equinox against
        which RA and Dec are measured

        @param [in] photParams is an instantiation of PhotometricParameters
        (it will contain information about gain, exposure time, etc.)

        @param [in] wcs is a kwarg that is used by the method _newOrigin().
        The wcs kwarg in this constructor method should not be used by users.
        """

        if wcs is None:
            self._tanSipWcs = tanSipWcsFromDetector(afwDetector, afwCamera,
                                                    obs_metadata, epoch)
        else:
            self._tanSipWcs = wcs

        self.afwDetector = afwDetector
        self.afwCamera = afwCamera
        self.obs_metadata = obs_metadata
        self.photParams = photParams
        self.epoch = epoch

        self.fitsHeader = self._tanSipWcs.getFitsMetadata()
        self.fitsHeader.set("EXTTYPE", "IMAGE")

        if self.obs_metadata.bandpass is not None:
            if (not isinstance(self.obs_metadata.bandpass, list) and
                    not isinstance(self.obs_metadata.bandpass, np.ndarray)):

                self.fitsHeader.set("FILTER", self.obs_metadata.bandpass)

        if self.obs_metadata.mjd is not None:
            self.fitsHeader.set("MJD-OBS", self.obs_metadata.mjd.TAI)

        if self.photParams is not None:
            self.fitsHeader.set("EXPTIME",
                                self.photParams.nexp * self.photParams.exptime)

        self.crpix1 = self.fitsHeader.get("CRPIX1")
        self.crpix2 = self.fitsHeader.get("CRPIX2")

        self.afw_crpix1 = self.crpix1
        self.afw_crpix2 = self.crpix2

        self.crval1 = self.fitsHeader.get("CRVAL1")
        self.crval2 = self.fitsHeader.get("CRVAL2")

        self.origin = galsim.PositionD(x=self.crpix1, y=self.crpix2)
    def __init__(self, detectorName, cameraWrapper, obs_metadata, epoch, photParams=None, wcs=None):
        """
        @param [in] detectorName is the name of the detector as stored
        by afw

        @param [in] cameraWrapper is an instantionat of a GalSimCameraWrapper

        @param [in] obs_metadata is an instantiation of ObservationMetaData
        characterizing the telescope pointing

        @param [in] epoch is the epoch in Julian years of the equinox against
        which RA and Dec are measured

        @param [in] photParams is an instantiation of PhotometricParameters
        (it will contain information about gain, exposure time, etc.)

        @param [in] wcs is a kwarg that is used by the method _newOrigin().
        The wcs kwarg in this constructor method should not be used by users.
        """

        if not isinstance(cameraWrapper, GalSimCameraWrapper):
            raise RuntimeError("You must pass GalSim_afw_TanSipWCS an instantiation "
                               "of GalSimCameraWrapper or one of its daughter "
                               "classes")

        if wcs is None:
            self._tanSipWcs = tanSipWcsFromDetector(detectorName, cameraWrapper, obs_metadata, epoch)
        else:
            self._tanSipWcs = wcs

        self.detectorName = detectorName
        self.cameraWrapper = cameraWrapper
        self.obs_metadata = obs_metadata
        self.photParams = photParams
        self.epoch = epoch

        # this is needed to match the GalSim v1.5 API
        self._color = None

        self.fitsHeader = self._tanSipWcs.getFitsMetadata()
        self.fitsHeader.set("EXTTYPE", "IMAGE")

        if self.obs_metadata.bandpass is not None:
            if (not isinstance(self.obs_metadata.bandpass, list) and not
                isinstance(self.obs_metadata.bandpass, np.ndarray)):
                self.fitsHeader.set("FILTER", self.obs_metadata.bandpass)

        if self.obs_metadata.mjd is not None:
            self.fitsHeader.set("MJD-OBS", self.obs_metadata.mjd.TAI)
            mjd_obs = astropy.time.Time(self.obs_metadata.mjd.TAI, format='mjd')
            self.fitsHeader.set('DATE-OBS', mjd_obs.isot)

        if self.photParams is not None:
            exptime = self.photParams.nexp*self.photParams.exptime
            self.fitsHeader.set("EXPTIME", exptime)
            with warnings.catch_warnings():
                warnings.filterwarnings('ignore', 'ERFA function', ErfaWarning)
                mjd_end = mjd_obs + astropy.time.TimeDelta(exptime, format='sec')
                self.fitsHeader.set('DATE-END', mjd_end.isot)

        # Add pointing information to FITS header.
        if self.obs_metadata.pointingRA is not None:
            self.fitsHeader.set('RATEL', obs_metadata.pointingRA)
        if self.obs_metadata.pointingDec is not None:
            self.fitsHeader.set('DECTEL', obs_metadata.pointingDec)
        if self.obs_metadata.rotSkyPos is not None:
            self.fitsHeader.set('ROTANGLE', obs_metadata.rotSkyPos)

        # Add airmass, needed by jointcal.
        if self.obs_metadata.OpsimMetaData is not None:
            try:
                airmass = self.obs_metadata.OpsimMetaData['airmass']
            except KeyError:
                pass
            else:
                self.fitsHeader.set('AIRMASS', airmass)

        # Add boilerplate keywords requested by DM.
        self.fitsHeader.set('TELESCOP', 'LSST')
        self.fitsHeader.set('INSTRUME', 'CAMERA')
        self.fitsHeader.set('SIMULATE', True)
        self.fitsHeader.set('ORIGIN', 'IMSIM')
        observatory = LsstObservatory()
        self.fitsHeader.set('OBS-LONG', observatory.getLongitude().asDegrees())
        self.fitsHeader.set('OBS-LAT', observatory.getLatitude().asDegrees())
        self.fitsHeader.set('OBS-ELEV', observatory.getElevation())
        obs_location = observatory.getLocation()
        self.fitsHeader.set('OBSGEO-X', obs_location.geocentric[0].value)
        self.fitsHeader.set('OBSGEO-Y', obs_location.geocentric[1].value)
        self.fitsHeader.set('OBSGEO-Z', obs_location.geocentric[2].value)

        self.crpix1 = self.fitsHeader.getScalar("CRPIX1")
        self.crpix2 = self.fitsHeader.getScalar("CRPIX2")

        self.afw_crpix1 = self.crpix1
        self.afw_crpix2 = self.crpix2

        self.crval1 = self.fitsHeader.getScalar("CRVAL1")
        self.crval2 = self.fitsHeader.getScalar("CRVAL2")

        self.origin = galsim.PositionD(x=self.crpix1, y=self.crpix2)
        self._color = None
def make_star_grid_instcat(instcat,
                           star_truth_db=None,
                           detectors=None,
                           x_pixels=None,
                           y_pixels=None,
                           mag_range=(16.3, 21),
                           max_x_offset=0,
                           max_y_offset=0,
                           y_stagger=4,
                           outdir=None,
                           sorted_mags=False):
    """
    Create an instance catalog consisting of grids of stars on each
    chip, using the instance catalog from a simulated visit to provide
    the info for constructing the WCS per CCD.

    Parameters
    ----------
    instcat: str
        The instance catalog corresponding to the desired visit.
    star_truth_db: str [None]
        sqlite3 file containing the truth_summary table for stars so that
        the true band-specific magnitudes can be used for selection.
        If None, then make selection using the mag_norm value.
    detectors: sequence of ints [None]
        The detectors to process. If None, then use range(189).
    x_pixels: sequence of ints [None]
        The pixel coordinates in the x (i.e., serial)-direction. If None,
        then use np.linspace(150, 4050, 40).
    y_pixels: sequence of ints [None]
        The pixel coordinates in the y (i.e., parallel)-direction. If None,
        then use np.linspace(100, 3900, 39).
    mag_range: tuple [(16.3, 21)]
        Range of magnituide values to sample from the input star_cat file.
    max_x_offset: float [0]
        Maximum offset in pixels to be drawn in the x-direction to
        displace each star from its nominal grid position.  These
        offsets helps prevent failures in the astrometric solution
        that arises from trying to match to a regular grid of
        reference stars.
    max_y_offset: float [0]
        Maximum offset in pixels to be drawn in the y-direction to
        displace each star from its nominal grid position.  These
        offsets helps prevent failures in the astrometric solution
        that arises from trying to match to a regular grid of
        reference stars.
    y_stagger: int [4]
        Stagger rows by y_step/y_stagger*(ix % ystagger)
    outdir: str [None]
        Output directory for instance catalog files.  If None, then use
        f'v{visit}-{band}_grid'.
    sorted_mags: bool [False]
        Flag to sort magnitudes so that brightest objects are at the bottom
        of the CCD.

    Returns
    -------
    The full path to the star grid instance catalog.
    """
    if detectors is None:
        detectors = range(189)
    if x_pixels is None:
        x_pixels = np.linspace(200, 3800, 36)
    if y_pixels is None:
        y_pixels = np.linspace(200, 3800, 36)

    star_cat, visit, band = parse_instcat(instcat)
    obs_md \
        = desc.imsim.phosim_obs_metadata(desc.imsim.metadata_from_file(instcat))

    num_stars = len(detectors) * len(x_pixels) * len(y_pixels)
    stars = shuffled_objects(star_cat,
                             band,
                             star_truth_db=star_truth_db,
                             mag_range=mag_range)[:num_stars]
    num_stars = min(num_stars, len(stars))
    if sorted_mags:
        stars.sort()

    if outdir is None:
        outdir = f'v{visit}-{band}_grid'
    os.makedirs(outdir, exist_ok=True)

    star_grid_cat = 'star_grid_{visit}.txt'.format(**locals())
    phosim_cat_file = write_phosim_cat(instcat, outdir, star_grid_cat)

    outfile = os.path.join(outdir, star_grid_cat)
    y_step = y_pixels[1] - y_pixels[0]
    with open(outfile, 'w') as output:
        my_id = 0
        for detector in detectors:
            print("processing", detector)
            wcs = tanSipWcsFromDetector(det_name[detector],
                                        camera_wrapper,
                                        obs_md,
                                        epoch=2000.)
            if not sorted_mags:
                # Re-shuffle the entries for each CCD.
                np.random.shuffle(stars)
            for ix, x_pix in enumerate(x_pixels):
                # Stagger rows by quarter steps
                y_offset = y_step / y_stagger * (ix % y_stagger)
                for y_pix in y_pixels:
                    dx = np.random.uniform(high=max_x_offset)
                    dy = np.random.uniform(high=max_y_offset)
                    ra, dec = [
                        _.asDegrees()
                        for _ in wcs.pixelToSky(x_pix + dx, y_pix + dy +
                                                y_offset)
                    ]
                    tokens = stars[my_id % num_stars].split()
                    # Replace the uniqueID, ra, dec fields with the
                    # recomputed values.
                    tokens[1] = str(my_id)
                    tokens[2] = f'{ra:.15f}'
                    tokens[3] = f'{dec:.15f}'
                    output.write(' '.join(tokens) + '\n')
                    my_id += 1
    return phosim_cat_file