def dz(self, value):
        self._dz = float(value)
        self.log.info("Using binning factor of {}".format(self.binning))

        # Create configuration file with the proper parameters
        cwfs_config_template = """#Auxiliary Telescope parameters:
Obscuration 				0.423
Focal_length (m)			21.6
Aperture_diameter (m)   		1.2
Offset (m)				{}
Pixel_size (m)			{}
"""
        config_index = "auxtel_latiss"
        path = Path(cwfs.__file__).resolve().parents[3].joinpath(
            "data", config_index)
        if not path.exists():
            os.makedirs(path)
        dest = path.joinpath(f"{config_index}.param")
        with open(dest, "w") as fp:
            # Write the file and set the offset and pixel size parameters
            fp.write(
                cwfs_config_template.format(self._dz * 0.041,
                                            10e-6 * self.binning))

        self.inst = Instrument(config_index, int(self.side * 2 / self.binning))
        self.algo = Algorithm("exp", self.inst, 1)
Exemplo n.º 2
0
    def testMatlab(self):
        global doPlot
        if doPlot:
            fig = plt.figure(figsize=(10, 10))

        j = 0                           # counter for matlab outputs, self.matlabZFile_Tol
        for imgDir, filenameFmt, fldxy, algorithms, model in self.tests:
            imgDir = os.path.join(str(self.rootdir), imgDir)
            intraFile = os.path.join(imgDir, filenameFmt % "intra")
            I1 = Image(readFile(intraFile), fldxy, Image.INTRA)

            extraFile = os.path.join(imgDir, filenameFmt % "extra")
            I2 = Image(readFile(extraFile), fldxy, Image.EXTRA)

            inst = Instrument(self.myinst, I1.sizeinPix)

            for algorithm in algorithms:
                matlabZFile, tol = self.matlabZFile_Tol[j]; j += 1

                algo = Algorithm(algorithm, inst, 1)
                algo.runIt(inst, I1, I2, model)

                zer = algo.zer4UpNm
                matZ = np.loadtxt(os.path.join(self.validationDir, matlabZFile))

                aerr = np.abs(matZ - zer)
                print("%-31s max(abs(err)) = %8.3g median(abs(err)) = %8.3g [Z_%d]" %
                      (matlabZFile, np.max(aerr), np.median(aerr), self.Zernike0 + np.argmax(aerr)))

                if doPlot:
                    ax = plt.subplot(self.nTest, 1, j)
                    plt.plot(self.x, matZ, label='Matlab', marker='o', color='r', markersize=10)
                    plt.plot(self.x, zer, label='Python',  marker='.', color='b', markersize=10)
                    plt.axvline(self.Zernike0 + np.argmax(aerr), ls=':', color='black')
                    plt.legend(loc="best", shadow=True, title=matlabZFile, fancybox=True)
                    ax.get_legend().get_title().set_color("red")
                    plt.xlim(self.x[0] - 0.5, self.x[-1] + 0.5)

                assert np.max(aerr) < tol

        if doPlot:
            plt.show()
Exemplo n.º 3
0
    def testMatlab(self):
        global doPlot
        if doPlot:
            fig = plt.figure(figsize=(10, 10))

        j = 0                           # counter for matlab outputs, self.matlabZFile_Tol
        for imgDir, filenameFmt, fldxy, algorithms, model in self.tests:
            imgDir = os.path.join(str(self.rootdir), imgDir)
            intraFile = os.path.join(imgDir, filenameFmt % "intra")
            I1 = Image(readFile(intraFile), fldxy, Image.INTRA)

            extraFile = os.path.join(imgDir, filenameFmt % "extra")
            I2 = Image(readFile(extraFile), fldxy, Image.EXTRA)

            inst = Instrument(self.myinst, I1.sizeinPix)

            for algorithm in algorithms:
                matlabZFile, tol = self.matlabZFile_Tol[j]; j += 1

                algo = Algorithm(algorithm, inst, 1)
                algo.runIt(inst, I1, I2, model)

                zer = algo.zer4UpNm
                matZ = np.loadtxt(os.path.join(self.validationDir, matlabZFile))

                aerr = np.abs(matZ - zer)
                print("%-31s max(abs(err)) = %8.3g median(abs(err)) = %8.3g [Z_%d], tol=%.0f nm" %
                      (matlabZFile, np.max(aerr), np.median(aerr), self.Zernike0 + np.argmax(aerr), tol))

                if doPlot:
                    ax = plt.subplot(self.nTest, 1, j)
                    plt.plot(self.x, matZ, label='Matlab', marker='o', color='r', markersize=10)
                    plt.plot(self.x, zer, label='Python',  marker='.', color='b', markersize=10)
                    plt.axvline(self.Zernike0 + np.argmax(aerr), ls=':', color='black')
                    plt.legend(loc="best", shadow=True, title=matlabZFile, fancybox=True)
                    ax.get_legend().get_title().set_color("red")
                    plt.xlim(self.x[0] - 0.5, self.x[-1] + 0.5)

                assert np.max(aerr) < tol

        if doPlot:
            plt.show()
Exemplo n.º 4
0
    def __init__(self, cwfsDir, imageDir, instruFile, algoFile, iSim,
                 imgSizeinPix, band, wavelength, debugLevel):
        self.imageDir = imageDir
        self.obsId = None
        self.iSim = iSim
        self.band = band
        self.iIter = 0
        self.nWFS = 4
        self.nRun = 1
        self.nExp = 1
        self.wfsName = ['intra', 'extra']
        self.halfChip = ['C0', 'C1']  # C0 is always intra, C1 is extra

        self.cwfsDir = cwfsDir
        self.imgSizeinPix = imgSizeinPix
        self.inst = Instrument(instruFile, imgSizeinPix)
        self.algo = Algorithm(algoFile, self.inst, debugLevel)
        self.znwcs = self.algo.numTerms
        self.znwcs3 = self.znwcs - 3
        self.myZn = np.zeros((self.znwcs3 * self.nWFS, 2))
        self.trueZn = np.zeros((self.znwcs3 * self.nWFS, 2))
        aa = instruFile
        if aa[-2:].isdigit():
            aa = aa[:-2]
        aosSrcDir = os.path.split(os.path.abspath(__file__))[0]
        intrinsicFile = '%s/../data/%s/intrinsic_zn.txt' % (aosSrcDir, aa)
        if np.abs(wavelength - 0.5) > 1e-3:
            intrinsicFile = intrinsicFile.replace('zn.txt',
                                                  'zn_%s.txt' % band.upper())
        intrinsicAll = np.loadtxt(intrinsicFile)
        intrinsicAll = intrinsicAll * wavelength
        self.intrinsicWFS = intrinsicAll[-self.nWFS:,
                                         3:self.algo.numTerms].reshape((-1, 1))
        self.covM = np.loadtxt('%s/../data/covM86.txt' %
                               aosSrcDir)  # in unit of nm^2
        self.covM = self.covM * 1e-6  # in unit of um^2

        if debugLevel >= 3:
            print('znwcs3=%d' % self.znwcs3)
            print(self.intrinsicWFS.shape)
            print(self.intrinsicWFS[:5])
Exemplo n.º 5
0
    def __init__(self, cwfsDir, instruFile, algoFile, imgSizeinPix, band, wavelength, aosDataDir, debugLevel=0):
        """

        Initiate the aosWFS class.

        Arguments:
            cwfsDir {[str]} -- cwfs directory.
            instruFile {[str]} -- Instrument folder name.
            algoFile {[str]} -- Algorithm to solve the TIE.
            imgSizeinPix {[int]} -- Pix size in one dimension.
            band {[str]} -- Active filter ("u", "g", "r", "i", "z", "y").
            wavelength {[float]} -- Wavelength in um.
            aosDataDir {[str]} -- AOS data directory.

        Keyword Arguments:
            debugLevel {[int]} -- Debug level. The higher value gives more information.
                                (default: {0})
        """

        # Get the instrument name
        instName, defocalOffset = getInstName(instruFile)

        # Declare the Zk, catalog donut, and calculated z files
        self.zFile = None
        self.catFile = None
        self.zCompFile = None

        # Previous zFile (Zk), which means the z4-zn in the previous iteration
        self.zFile_m1 = None

        # Number of wavefront sensor
        self.nWFS = {self.LSST: 4, self.COMCAM: 9}[instName]

        # Number of run in each iteration of phosim
        self.nRun = {self.LSST: 1, self.COMCAM: 2}[instName]

        # Number of exposure in each run
        # For ComCam, only 1 set of intra and 1 set of extra for each iter
        self.nExp = {self.LSST: 2, self.COMCAM: 1}[instName]

        # Decide the defocal distance offset in mm
        self.offset = [-defocalOffset, defocalOffset]

        # Will refactorize the directory root problem.

        # Record the directory now
        aosDir = os.getcwd()

        # Assign the cwfs directory
        self.cwfsDir = cwfsDir

        # Change the directory now to the cwfs directory
        os.chdir(cwfsDir)

        # Read the instrument and algorithm data
        self.inst = Instrument(instruFile, int(imgSizeinPix))
        self.algo = Algorithm(algoFile, self.inst, debugLevel)

        # Change back the directory
        os.chdir(aosDir)

        # Terms of annular Zernike polynomials
        self.znwcs = self.algo.numTerms

        # Only consider the terms z4-zn. The first three terms are piston, x-tilt, and y-tilt
        self.znwcs3 = self.znwcs - 3

        # Construct the matrix of annular Zernike polynomials for all WFSs.
        self.myZn = np.zeros((self.znwcs3 * self.nWFS, 2))
        self.trueZn = self.myZn.copy()

        # Directory of intrinsic Zn file belong to the specific instrument
        intrinsicZnFileName = "intrinsic_zn_%s.txt" % band.upper()

        # Monochromatic condition in referenced wavelength
        if (wavelength == 0.5):
            intrinsicZnFileName = "intrinsic_zn.txt"

        intrinsicFile = os.path.join(aosDataDir, instName, intrinsicZnFileName)

        # Read the intrinsic Zn data and times the wavelength
        intrinsicAll = np.loadtxt(intrinsicFile)*wavelength

        # Read the intrinsic Zk
        self.intrinsicWFS = intrinsicAll[-self.nWFS:, 3:self.znwcs].reshape((-1, 1))

        # Read the covariance matrix in unit of nm^2
        # Covariance matrix with 86 runs.
        covMFilePath = os.path.join(aosDataDir, "covM86.txt")
        self.covM = np.loadtxt(covMFilePath)

        # Reconstruct the covariance matrix if necessary (not baseline condition)
        # The way to construct the covariance matrix here is weird. Actually, there
        # is no 4x4 repeation in original "covM86.txt". Need to check with Bo for this.

        # Expand the covariance matrix by repeating the matrix
        if (self.nWFS > 4):
            nrepeat = int(np.ceil(self.nWFS/4))
            self.covM = np.tile(self.covM, (nrepeat, nrepeat))

        # Take the needed part
        if (self.nWFS != 4):
            self.covM = self.covM[:(self.znwcs3 * self.nWFS), :(self.znwcs3 * self.nWFS)]

        # Change the unit to um^2
        self.covM *= 1e-6

        # Show the debug information
        if (debugLevel >= 3):
            print("znwcs3=%d" % self.znwcs3)
            print(self.intrinsicWFS.shape)
            print(self.intrinsicWFS[:5])
class LatissCWFSAlign(salobj.BaseScript):
    """Perform an optical alignment procedure of Auxiliary Telescope with
    the LATISS instrument (ATSpectrograph and ATCamera CSCs). This is for
    use with in-focus images and is performed using Curvature-Wavefront
    Sensing Techniques.

    Parameters
    ----------
    index : `int`
        Index of Script SAL component.

    Notes
    -----
    **Checkpoints**

    None

    **Details**

    This script is used to perform measurements of the wavefront error, then
    propose hexapod offsets based on an input sensitivity matrix to minimize
    the errors. The hexapod offsets are not applied automatically and must be
    performed by the user.

    """
    def __init__(self, index=1, remotes=True):

        super().__init__(
            index=index,
            descr="Perform optical alignment procedure of the Rubin Auxiliary "
            "Telescope with LATISS using Curvature-Wavefront Sensing "
            "Techniques.",
        )

        self.atcs = None
        self.latiss = None
        if remotes:
            self.atcs = ATCS(self.domain, log=self.log)
            self.latiss = LATISS(
                self.domain,
                log=self.log,
                tcs_ready_to_take_data=self.atcs.ready_to_take_data,
            )

        # instantiate the quick measurement class
        try:
            qm_config = QuickFrameMeasurementTask.ConfigClass()
            self.qm = QuickFrameMeasurementTask(config=qm_config)
        except NameError:
            self.log.warning(
                "Library unavailable certain tests will be skipped")

        # Timeouts used for telescope commands
        self.short_timeout = 5.0  # used with hexapod offset command
        self.long_timeout = 30.0  # used to wait for in-position event from hexapod
        # Have discovered that the occasional image will take 12+ seconds
        # to ingest
        self.timeout_get_image = 20.0

        # Sensitivity matrix: mm of hexapod motion for nm of wfs. To figure out
        # the hexapod correction multiply the calculcated zernikes by this.
        # Note that the zernikes must be derotated to
        #         self.sensitivity_matrix = [
        #         [1.0 / 161.0, 0.0, 0.0],
        #         [0.0, -1.0 / 161.0, (107.0/161.0)/4200],
        #         [0.0, 0.0, -1.0 / 4200.0]
        #         ]
        self.sensitivity_matrix = [
            [1.0 / 206.0, 0.0, 0.0],
            [0.0, -1.0 / 206.0, -(109.0 / 206.0) / 4200],
            [0.0, 0.0, 1.0 / 4200.0],
        ]

        # Rotation matrix to take into account angle between camera and
        # boresight
        self.rotation_matrix = lambda angle: np.array([
            [np.cos(np.radians(angle)), -np.sin(np.radians(angle)), 0.0],
            [np.sin(np.radians(angle)),
             np.cos(np.radians(angle)), 0.0],
            [0.0, 0.0, 1.0],
        ])

        # Matrix to map hexapod offset to alt/az offset in the focal plane
        # units are arcsec/mm. X-axis is Elevation
        # Measured with data from AT run SUMMIT-5027, still unverified.
        # x-offset measured with images 2021060800432 - 2021060800452
        # y-offset measured with images 2021060800452 - 2021060800472
        self.hexapod_offset_scale = [
            [52.459, 0.0, 0.0],
            [0.0, 50.468, 0.0],
            [0.0, 0.0, 0.0],
        ]

        # Angle between camera and boresight
        # Assume perfect mechanical mounting
        self.camera_rotation_angle = 0.0

        # The following attributes are set via the configuration:
        self.filter = None
        self.grating = None
        # exposure time for the intra/extra images (in seconds)
        self.exposure_time = None

        # Assume the time-on-target is 10 minutes (600 seconds)
        # for rotator positioning
        self.time_on_target = 600

        # offset for the intra/extra images
        self._dz = None
        # butler data path.
        self.datapath = None
        # end of configurable attributes

        # Set (oversized) stamp size for centroid estimation
        self.pre_side = 300
        # Set stamp size for WFE estimation
        # 192 pix is size for dz=1.5, but gets automatically
        # scaled based on dz later, so can multiply by an
        # arbitrary factor here to make it larger
        self._side = 192 * 1.1  # normally 1.1
        # self.selected_source_centroid = None

        # angle between elevation axis and nasmyth2 rotator
        self.angle = None

        self.intra_visit_id = None
        self.extra_visit_id = None

        self.intra_exposure = None
        self.extra_exposure = None
        self.extra_focal_position_out_of_range = None
        self.detection_exp = None

        self.I1 = []
        self.I2 = []
        self.fieldXY = [0.0, 0.0]

        self.inst = None
        # set binning of images to increase processing speed
        # at the expense of resolution
        self._binning = 1
        self.algo = None

        self.zern = None
        self.hexapod_corr = None

        # make global to expose for unit tests
        self.total_focus_offset = 0.0
        self.total_coma_x_offset = 0.0
        self.total_coma_y_offset = 0.0

        self.data_pool_sleep = 5.0

        self.log.info("latiss_cwfs_align initialized!")

    # define the method that sets the hexapod offset to create intra/extra
    # focal images
    @property
    def dz(self):
        if self._dz is None:
            self.dz = 0.8
        return self._dz

    @property
    def binning(self):
        return self._binning

    @binning.setter
    def binning(self, value):
        self._binning = value
        self.dz = self.dz

    @property
    def side(self):
        # must be an even number
        return int(np.ceil(self._side * self.dz / 1.5 / 2.0) * 2)

    @dz.setter
    def dz(self, value):
        self._dz = float(value)
        self.log.info("Using binning factor of {}".format(self.binning))

        # Create configuration file with the proper parameters
        cwfs_config_template = """#Auxiliary Telescope parameters:
Obscuration 				0.423
Focal_length (m)			21.6
Aperture_diameter (m)   		1.2
Offset (m)				{}
Pixel_size (m)			{}
"""
        config_index = "auxtel_latiss"
        path = Path(cwfs.__file__).resolve().parents[3].joinpath(
            "data", config_index)
        if not path.exists():
            os.makedirs(path)
        dest = path.joinpath(f"{config_index}.param")
        with open(dest, "w") as fp:
            # Write the file and set the offset and pixel size parameters
            fp.write(
                cwfs_config_template.format(self._dz * 0.041,
                                            10e-6 * self.binning))

        self.inst = Instrument(config_index, int(self.side * 2 / self.binning))
        self.algo = Algorithm("exp", self.inst, 1)

    async def take_intra_extra(self):
        """Take pair of Intra/Extra focal images to be used to determine
        the measured wavefront error. Because m2 is being moved, the intra
        focal image occurs when the hexapod receives a positive offset and
        is pushed towards the primary mirror. The extra-focal image occurs
        when the hexapod is pulled back (negative offset) from the
        sbest-focus position.

        Returns
        -------
        images_end_readout_evt: list
            List of endReadout event for the intra and extra images.

        """

        self.log.debug("Moving to intra-focal position")

        await self.hexapod_offset(self.dz)

        intra_image = await self.latiss.take_engtest(
            exptime=self.exposure_time,
            n=1,
            group_id=self.group_id,
            filter=self.filter,
            grating=self.grating,
            reason="INTRA" +
            ("" if self.reason is None else f"_{self.reason}"),
            program=self.program,
        )

        self.log.debug("Moving to extra-focal position")

        # Hexapod offsets are relative, so need to move 2x the offset
        # to get from the intra- to the extra-focal position.
        # then add the offset to compensate for un-equal magnification
        await self.hexapod_offset(-(self.dz * 2.0 + self.extra_focal_offset))

        self.log.debug("Taking extra-focal image")

        extra_image = await self.latiss.take_engtest(
            exptime=self.exposure_time,
            n=1,
            group_id=self.group_id,
            filter=self.filter,
            grating=self.grating,
            reason="EXTRA" +
            ("" if self.reason is None else f"_{self.reason}"),
            program=self.program,
        )

        self.intra_visit_id = int(intra_image[0])

        self.log.info(f"intraImage expId for target: {self.intra_visit_id}")

        self.extra_visit_id = int(extra_image[0])

        self.log.info(f"extraImage expId for target: {self.extra_visit_id}")

        self.angle = 90.0 - await self.atcs.get_bore_sight_angle()
        self.log.info(f"angle used in cwfs algorithm is {self.angle:0.2f}")

        self.log.debug(
            "Moving hexapod back to zero offset (in-focus) position")
        # This is performed such that the telescope is left in the
        # same position it was before running the script
        await self.hexapod_offset(self.dz)

    async def hexapod_offset(self, offset, x=0.0, y=0.0):
        """Applies z-offset to the hexapod to move between
        intra/extra/in-focus positions.

        Parameters
        ----------
        offset: `float`
             Focus offset to the hexapod in mm

        """

        offset = {
            "m1": 0.0,
            "m2": 0.0,
            "x": x,
            "y": y,
            "z": offset,
            "u": 0.0,
            "v": 0.0,
        }

        self.atcs.rem.athexapod.evt_positionUpdate.flush()
        await self.atcs.rem.ataos.cmd_offset.set_start(
            **offset, timeout=self.long_timeout)
        await self.atcs.rem.athexapod.evt_positionUpdate.next(
            flush=False, timeout=self.long_timeout)

    async def run_cwfs(self):
        """Runs CWFS code on intra/extra focal images.

        Returns
        -------
        Dictionary of calculated values
        results = {'zerns' : (self.zern),
                  'rot_zerns': (rot_zern),
                  'hex_offset': (hexapod_offset),
                  'tel_offset': (tel_offset)}

        """

        # get event loop to run blocking tasks
        loop = asyncio.get_event_loop()
        executor = concurrent.futures.ThreadPoolExecutor()

        self.cwfs_selected_sources = []

        if self.intra_visit_id is None or self.extra_visit_id is None:
            self.log.warning(
                "Intra/Extra images not taken. Running take image sequence.")
            await self.take_intra_extra()
        else:
            self.log.info(
                f"Running cwfs on {self.intra_visit_id} and {self.extra_visit_id}."
            )
            self.log.debug(f"Using datapath of {self.datapath}.")
            self.log.debug(
                f"Using a data_id of {parse_visit_id(self.intra_visit_id)} "
                f"and {parse_visit_id(self.extra_visit_id)}.")

        self.intra_exposure, self.extra_exposure = await asyncio.gather(
            get_image(
                parse_visit_id(self.intra_visit_id),
                self.best_effort_isr,
                timeout=self.timeout_get_image,
            ),
            get_image(
                parse_visit_id(self.extra_visit_id),
                self.best_effort_isr,
                timeout=self.timeout_get_image,
            ),
        )

        self.log.debug("Running source detection")

        self.intra_result = await loop.run_in_executor(
            executor,
            functools.partial(self.qm.run,
                              self.intra_exposure,
                              donutDiameter=2 * self.side),
        )
        self.extra_result = await loop.run_in_executor(
            executor,
            functools.partial(self.qm.run,
                              self.extra_exposure,
                              donutDiameter=2 * self.side),
        )
        self.log.debug("Source detection completed")

        # Verify a result was achieved, if not then raising the exception
        if not self.intra_result.success or not self.extra_result.success:
            raise RuntimeError(
                f"Centroid finding algorithm was unsuccessful. "
                f"Intra success is {self.intra_result.success}. "
                f"Extra image success is {self.extra_result.success}.")

        # Verify that results are within 100 pixels of each other (basically
        # the size of a typical donut). This should ensure the same source is
        # used.
        dy = (self.extra_result.brightestObjCentroidCofM[0] -
              self.intra_result.brightestObjCentroidCofM[0])
        dx = (self.extra_result.brightestObjCentroidCofM[1] -
              self.intra_result.brightestObjCentroidCofM[1])
        dr = np.sqrt(dy**2 + dx**2)
        if dr > 100.0:
            self.log.warning(
                "Source finding algorithm found different sources for intra/extra. \n"
                f"intra found [y,x] = [{self.intra_result.brightestObjCentroidCofM[1]},"
                "{self.intra_result.brightestObjCentroidCofM[0]}]\n"
                f"extra found [y,x] = [{self.extra_result.brightestObjCentroidCofM[1]},"
                "{self.extra_result.brightestObjCentroidCofM[0]}]\n"
                "Forcing them to use the intra-location.")
            self.extra_focal_position_out_of_range = True
        else:
            self.extra_focal_position_out_of_range = False

        # Create stamps for CWFS algorithm. Bin (if desired).
        self.create_donut_stamps_for_cwfs()

        # Now we should be ready to run CWFS
        self.log.info("Starting CWFS algorithm calculation.")
        # reset inputs just in case
        self.algo.reset(self.I1[0], self.I2[0])

        # for a slow telescope, should be running in paraxial mode
        await loop.run_in_executor(executor, self.algo.runIt, self.inst,
                                   self.I1[0], self.I2[0], "paraxial")

        self.zern = [
            -self.algo.zer4UpNm[3],  # Coma-X (in detector axes, TBC)
            self.algo.zer4UpNm[4],  # Coma-Y (in detector axes, TBC)
            self.algo.zer4UpNm[0],  # defocus
        ]

        results_dict = self.calculate_results()
        return results_dict

    def get_donut_region(self, center_y, center_x):

        return (
            center_y - self.side,
            center_y + self.side,
            center_x - self.side,
            center_x + self.side,
        )

    def get_intra_donut_center(self):
        return (
            int(self.intra_result.brightestObjCentroidCofM[1]),
            int(self.intra_result.brightestObjCentroidCofM[0]),
        )

    def get_extra_donut_center(self):

        if self.extra_focal_position_out_of_range:
            return self.get_intra_donut_center()

        else:
            return int(self.extra_result.brightestObjCentroidCofM[1]), int(
                self.extra_result.brightestObjCentroidCofM[0])

    def create_donut_stamps_for_cwfs(self):
        """Create square stamps with donuts based on centroids."""
        # reset I1 and I2
        self.I1 = []
        self.I2 = []

        self.log.debug(
            f"Creating stamp for intra_image donut on centroid "
            f"[y,x] = [{self.get_intra_donut_center()}] with a side "
            f"length of {2 * self.side} pixels")

        intra_box = self.get_donut_region(*self.get_intra_donut_center())
        intra_square = self.intra_exposure.image.array[
            intra_box[0]:intra_box[1], intra_box[2]:intra_box[3]]

        extra_box = self.get_donut_region(*self.get_extra_donut_center())
        extra_square = self.extra_exposure.image.array[
            extra_box[0]:extra_box[1], extra_box[2]:extra_box[3]]

        self.log.debug(
            f"Created stamp for extra_image donut on centroid "
            f"[y,x] = [{self.get_extra_donut_center()}] with a side "
            f"length of {2 * self.side} pixels")

        # Bin the images
        if self.binning != 1:
            self.log.info(
                f"Stamps for analysis will be binned by {self.binning} in each dimension."
            )
            intra_square0 = copy.deepcopy(intra_square)
            extra_square0 = copy.deepcopy(extra_square)
            # get tuple array from shape array (which is a tuple) and make
            # an integer
            new_shape = tuple(
                np.asarray(np.asarray(intra_square0.shape) / self.binning,
                           dtype=np.int32))
            intra_square = self.rebin(intra_square0, new_shape)
            extra_square = self.rebin(extra_square0, new_shape)
            self.log.info(f"intra_square shape is {intra_square.shape}")
            self.log.info(f"extra_square shape is {extra_square.shape}")

        self.I1.append(Image(intra_square, self.fieldXY, Image.INTRA))
        self.I2.append(Image(extra_square, self.fieldXY, Image.EXTRA))

        self.log.debug("create_donut_stamps_for_cwfs completed")

    def rebin(self, arr, new_shape):
        """Rebins the array to a new shape via taking the mean of the
        surrounding pixels

        Parameters
        ----------
        arr: `np.array`
            2-D array of arbitrary size
        new_shape: `np.array`
            Tuple 2-element array of size of the output array

        Returns
        -------
        rebinned: `np.array`
            Array binned to new shape

        """
        shape = (
            new_shape[0],
            arr.shape[0] // new_shape[0],
            new_shape[1],
            arr.shape[1] // new_shape[1],
        )
        rebinned = arr.reshape(shape).mean(-1).mean(1)
        return rebinned

    def calculate_results(self):
        """Calculates hexapod and telescope offsets based on
        derotated zernikes.

        Returns
        -------
        results : `dict`
            Dictionary of calculated values

        """
        rot_zern = np.matmul(
            self.zern,
            self.rotation_matrix(self.angle + self.camera_rotation_angle))
        hexapod_offset = np.matmul(rot_zern, self.sensitivity_matrix)
        tel_offset = np.matmul(hexapod_offset, self.hexapod_offset_scale)

        self.log.info(f"""==============================
Measured [coma-X, coma-Y, focus] zernike coefficients [nm]: [{
            (len(self.zern) * '{:0.1f}, ').format(*self.zern)}]
De-rotated [coma-X, coma-Y, focus]  zernike coefficients [nm]: [{
            (len(rot_zern) * '{:0.1f}, ').format(*rot_zern)}]
Hexapod [x, y, z] offsets [mm] : {(len(hexapod_offset) * '{:0.3f}, ').format(*hexapod_offset)}
Telescope offsets [arcsec]: {(len(tel_offset) * '{:0.1f}, ').format(*tel_offset)}
==============================
""")

        results = {
            "zerns": (self.zern),
            "rot_zerns": (rot_zern),
            "hex_offset": (hexapod_offset),
            "tel_offset": (tel_offset),
        }

        return results

    @classmethod
    def get_schema(cls):
        schema_yaml = """
            $schema: http://json-schema.org/draft-07/schema#
            $id: https://github.com/lsst-ts/ts_standardscripts/auxtel/LatissCWFSAlign.yaml
            title: LatissCWFSAlign v1
            description: Configuration for LatissCWFSAlign Script.
            type: object
            properties:
              find_target:
                type: object
                additionalProperties: false
                required:
                  - az
                  - el
                  - mag_limit
                description: >-
                    Optional configuration section. Find a target to perform CWFS in the given
                    position and magnitude range. If not specified, the step is ignored.
                properties:
                  az:
                    type: number
                    description: Azimuth (in degrees) to find a target.
                  el:
                    type: number
                    description: Elevation (in degrees) to find a target.
                  mag_limit:
                    type: number
                    description: Minimum (brightest) V-magnitude limit.
                  mag_range:
                    type: number
                    description: >-
                        Magnitude range. The maximum/faintest limit is defined as
                        mag_limit+mag_range.
                  radius:
                    type: number
                    description: Radius of the cone search (in degrees).
              track_target:
                type: object
                additionalProperties: false
                required:
                  - target_name
                description: >-
                    Optional configuration section. Track a specified target to
                    perform CWFS. If not specified, the step is ignored.
                properties:
                  target_name:
                    description: Target name
                    type: string
                  icrs:
                    type: object
                    additionalProperties: false
                    description: Optional ICRS coordinates.
                    required:
                      - ra
                      - dec
                    properties:
                      ra:
                        description: ICRS right ascension (hour).
                        type: number
                        minimum: 0
                        maximum: 24
                      dec:
                        description: ICRS declination (deg).
                        type: number
                        minimum: -90
                        maximum: 90
              filter:
                description: Which filter to use when taking intra/extra focal images.
                type: string
                default: empty_1
              grating:
                description: Which grating to use when taking intra/extra focal images.
                type: string
                default: empty_1
              exposure_time:
                description: The exposure time to use when taking intra/extra focal images (sec).
                type: number
                default: 30.
              acq_exposure_time:
                description: The exposure time to use when taking an in focus acquisition image(sec).
                type: number
                default: 5.
              take_detection_image:
                description: >-
                    Take an in-focus image before the cwfs loop to use for source
                    detection?
                type: boolean
                default: false
              dz:
                description: De-focus to apply when acquiring the intra/extra focal images (mm).
                type: number
                default: 0.8
              extra_focal_offset:
                description: >-
                    The additional m2 defocus (mm) to apply for the extra-focal image to be the
                    same size as the intra. This value is always positive to compensate for
                    the change in magnification from moving the secondary mirror.
                type: number
                default: 0.0011
              datapath:
                description: Path to the gen3 butler data repository. The default is for the summit.
                type: string
                default: /repo/LATISS
              large_defocus:
                description: >-
                    Defines a large defocus. If the measured defocus is larger than this value,
                    apply only half of correction.
                type: number
                default: 0.08
              threshold:
                 description: >-
                   Focus correction threshold. If correction is lower than this
                   value, stop correction loop.
                 type: number
                 default: 0.01
              coma_threshold:
                 description: >-
                   Coma correction threshold. If correction is lower than this
                   value, stop correction loop.
                 type: number
                 default: 0.2
              offset_telescope:
                description: When correcting coma, also offset the telescope?
                type: boolean
                default: true
              max_iter:
                  description: Maximum number of iterations.
                  type: integer
                  default: 5
              reason:
                description: Optional reason for taking the data.
                anyOf:
                  - type: string
                  - type: "null"
                default: null
              program:
                description: >-
                  Optional name of the program this dataset belongs to.
                type: string
                default: CWFS
            additionalProperties: false
        """
        return yaml.safe_load(schema_yaml)

    async def configure(self, config):
        """Configure script.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Script configuration, as defined by `schema`.
        """

        if hasattr(config, "find_target") and hasattr(config, "track_target"):
            raise RuntimeError(
                "find_target and track_target configuration sections cannot be specified together."
            )
        elif hasattr(config, "find_target"):
            self.log.debug(f"Finding target for cwfs @ {config.find_target}")
            self.cwfs_target = await self.atcs.find_target(**config.find_target
                                                           )
            self.cwfs_target_ra = None
            self.cwfs_target_dec = None
            self.log.debug(f"Using target {self.cwfs_target} for cwfs.")
        elif hasattr(config, "track_target"):
            self.log.debug(f"Tracking target {config.track_target} for cwfs.")
            self.cwfs_target = config.track_target.get("target_name",
                                                       "cwfs_target")
            self.cwfs_target_ra = None
            self.cwfs_target_dec = None
            if "icrs" in config.track_target:
                self.cwfs_target_ra = config.track_target["icrs"]["ra"]
                self.cwfs_target_dec = config.track_target["icrs"]["dec"]
        else:
            self.log.debug("No target configured.")
            self.cwfs_target = None
            self.cwfs_target_ra = None
            self.cwfs_target_dec = None

        self.filter = config.filter
        self.grating = config.grating

        # exposure time for the intra/extra images (in seconds)
        self.exposure_time = config.exposure_time

        self.acq_exposure_time = config.acq_exposure_time

        # offset for the intra/extra images
        self.dz = config.dz
        # delta offset for extra focal image
        self.extra_focal_offset = config.extra_focal_offset

        # butler data path.
        self.datapath = config.datapath

        # Instantiate BestEffortIsr
        self.best_effort_isr = self.get_best_effort_isr()

        self.large_defocus = config.large_defocus

        self.threshold = config.threshold

        self.coma_threshold = config.coma_threshold

        self.offset_telescope = config.offset_telescope

        self.max_iter = config.max_iter

        self.reason = config.reason

        self.program = config.program

        self.take_detection_image = config.take_detection_image

    def get_best_effort_isr(self):
        # Isolate the BestEffortIsr class so it can be mocked
        # in unit tests
        return BestEffortIsr(self.datapath)

    def set_metadata(self, metadata):
        # It takes about 300s to run the cwfs code, plus the two exposures
        metadata.duration = 300.0 + 2.0 * self.exposure_time
        metadata.filter = f"{self.filter},{self.grating}"

    async def arun(self, checkpoint=False):
        """Perform CWFS measurements and hexapod adjustments until the
        thresholds are reached.
        """

        if self.cwfs_target is not None and self.cwfs_target_dec is None:
            if checkpoint:
                await self.checkpoint(f"Slewing to object: {self.cwfs_target}")
            await self.atcs.slew_object(
                name=self.cwfs_target,
                rot=0.0,
                rot_type=RotType.PhysicalSky,
                az_wrap_strategy=WrapStrategy.OPTIMIZE,
                time_on_target=self.time_on_target,
            )
        elif (self.cwfs_target is not None and self.cwfs_target_dec is not None
              and self.cwfs_target_ra is not None):
            if checkpoint:
                await self.checkpoint(
                    f"Slewing to icrs coordinates: {self.cwfs_target} @ "
                    f"ra/dec = {self.cwfs_target_ra}/{self.cwfs_target_dec}.")

            await self.atcs.slew_icrs(
                ra=self.cwfs_target_ra,
                dec=self.cwfs_target_dec,
                target_name=self.cwfs_target,
                rot=0.0,
                rot_type=RotType.PhysicalSky,
                az_wrap_strategy=WrapStrategy.OPTIMIZE,
                time_on_target=self.time_on_target,
            )
        elif (self.cwfs_target_dec is not None
              and self.cwfs_target_ra is None) or (self.cwfs_target_dec is None
                                                   and self.cwfs_target_ra
                                                   is not None):
            raise RuntimeError(
                "Invalid configuration. Only one of ra/dec pair was specified. "
                "Either define both or neither. "
                f"Got ra={self.cwfs_target_ra} and dec={self.cwfs_target_dec}."
            )

        if self.take_detection_image:
            if checkpoint:
                await self.checkpoint("Detection image.")
            await self.latiss.take_engtest(
                self.acq_exposure_time,
                group_id=self.group_id,
                reason="DETECTION_INFOCUS" +
                ("" if self.reason is None else f"_{self.reason}"),
                program=self.program,
            )

        self.total_focus_offset = 0.0
        self.total_coma_x_offset = 0.0
        self.total_coma_y_offset = 0.0
        for i in range(self.max_iter):

            self.log.debug(f"CWFS iteration {i + 1} starting...")

            if checkpoint:
                await self.checkpoint(
                    f"[{i + 1}/{self.max_iter}]: CWFS loop starting...")

            # Setting visit_id's to none so run_cwfs will take a new dataset.
            self.intra_visit_id = None
            self.extra_visit_id = None
            results = await self.run_cwfs()
            coma_x = results["hex_offset"][0]
            coma_y = results["hex_offset"][1]
            focus_offset = results["hex_offset"][2]

            total_coma_offset = np.sqrt(coma_x**2.0 + coma_y**2.0)
            if (abs(focus_offset) < self.threshold
                    and total_coma_offset < self.coma_threshold):
                self.total_focus_offset += focus_offset
                self.log.info(
                    f"Focus ({focus_offset:0.3f}) and coma ({total_coma_offset:0.3f}) offsets "
                    f"inside tolerance level ({self.threshold:0.3f}). "
                    f"Total focus correction: {self.total_focus_offset:0.3f} mm. "
                    f"Total coma-x correction: {self.total_coma_x_offset:0.3f} mm. "
                    f"Total coma-y correction: {self.total_coma_y_offset:0.3f} mm."
                )
                if checkpoint:
                    await self.checkpoint(
                        f"[{i + 1}/{self.max_iter}]: CWFS converged.")
                # Add coma offsets from previous run
                self.total_coma_x_offset += coma_x
                self.total_coma_y_offset += coma_y
                await self.hexapod_offset(focus_offset, x=coma_x, y=coma_y)
                if self.offset_telescope:
                    tel_el_offset, tel_az_offset = (
                        results["tel_offset"][0],
                        results["tel_offset"][1],
                    )
                    self.log.info(
                        f"Applying telescope offset [az,el]: [{tel_az_offset:0.3f}, {tel_el_offset:0.3f}]."
                    )
                    await self.atcs.offset_azel(
                        az=tel_az_offset,
                        el=tel_el_offset,
                        relative=True,
                        persistent=True,
                    )
                current_target = await self.atcs.rem.atptg.evt_currentTarget.aget(
                    timeout=self.short_timeout)
                hexapod_position = (
                    await self.atcs.rem.athexapod.tel_positionStatus.aget(
                        timeout=self.short_timeout))
                self.log.info(
                    f"Hexapod LUT Datapoint - {current_target.targetName} - "
                    f"reported hexapod position is, {hexapod_position.reportedPosition}."
                )
                self.log.debug(
                    "Taking in focus image after applying final results.")
                await self.latiss.take_object(
                    self.acq_exposure_time,
                    group_id=self.group_id,
                    reason="FINAL_INFOCUS" +
                    ("" if self.reason is None else f"_{self.reason}"),
                    program=self.program,
                )
                await self.atcs.add_point_data()

                self.log.info(
                    "latiss_cwfs_align script completed successfully!\n")
                return
            elif abs(focus_offset) > self.large_defocus:
                self.total_focus_offset += focus_offset / 2.0
                self.log.warning(
                    f"Computed focus offset too large: {focus_offset}. "
                    "Applying half correction.")
                if checkpoint:
                    await self.checkpoint(
                        f"[{i + 1}/{self.max_iter}]: CWFS focus error too large."
                    )
                await self.hexapod_offset(focus_offset / 2.0)
            else:
                self.total_focus_offset += focus_offset
                self.log.info(
                    f"Applying offset: x={coma_x}, y={coma_y}, z={focus_offset}."
                )
                if checkpoint:
                    await self.checkpoint(
                        f"[{i + 1}/{self.max_iter}]: CWFS applying coma and focus correction."
                    )
                self.total_coma_x_offset += coma_x
                self.total_coma_y_offset += coma_y
                await self.hexapod_offset(focus_offset, x=coma_x, y=coma_y)
                if self.offset_telescope:
                    tel_el_offset, tel_az_offset = (
                        results["tel_offset"][0],
                        results["tel_offset"][1],
                    )
                    self.log.info(
                        f"Applying telescope offset az/el: {tel_az_offset}/{tel_el_offset}."
                    )
                    await self.atcs.offset_azel(
                        az=tel_az_offset,
                        el=tel_el_offset,
                        relative=True,
                        persistent=True,
                    )

        self.log.warning(
            f"Reached maximum iteration ({self.max_iter}) without convergence.\n"
        )

    async def run(self):
        await self.arun(True)
Exemplo n.º 7
0
Arquivo: cwfs.py Projeto: bxin/cwfs
def main():

    parser = argparse.ArgumentParser(
        description='-----This is cwfs (Curvature Wavefront Sensing) code----')

    parser.add_argument('intra', help='intra focal image file name (no path)')
    parser.add_argument('extra', help='extra focal image file name (no path)')

    parser.add_argument('-dir', dest='imgDir',
                        help='relative or absolute path for input images')
    parser.add_argument('-ixy', dest='intra_xy', nargs=2,
                        type=float, default=[0, 0],
                        help='intra focal field (x,y) in deg, default=[0 0]')
    parser.add_argument('-exy', dest='extra_xy', nargs=2,
                        type=float, default=[0, 0],
                        help='extra focal field (x,y) in deg, default=[0 0]')
    parser.add_argument('-i', dest='instruFile', default='lsst',
                        help='instrument parameter file, default=lsst,\
                         ".param" is appended automatically \
                        default path is data/lsst/')
    parser.add_argument('-a', dest='algoFile', default='fft',
                        help='algorithm parameter file, default=fft,\
                         ".algo" is appended automatically\
                        default path is data/algo/')
    parser.add_argument('-m', dest='model',
                        choices=('paraxial', 'onAxis', 'offAxis'),
                        default='paraxial',
                        help='Optical model to be used, default=paraxial')
    parser.add_argument('-op', dest='outputParam', default='',
                        help='file name for dumping all parameters, \
                        default=no output')
    parser.add_argument('-oz', dest='outputZerFile', default='',
                        help='file name for output Zernikes (in unit of nm), \
                        default=no output')
    parser.add_argument('-v', '--version', action='version',
                        version='%(prog)s 1.0')
    parser.add_argument('-d', dest='debugLevel', type=int,
                        default=0, choices=(-1, 0, 1, 2, 3),
                        help='debug level, -1=quiet, 0=Zernikes, \
                        1=operator, 2=expert, 3=everything, default=0')
    args = parser.parse_args()

    if args.debugLevel >= 1:
        print(args)

    # load intra and extra focal images
    intraFile, extraFile = args.intra, args.extra

    if args.imgDir:
        intraFile = os.path.join(args.imgDir, intraFile)
        extraFile = os.path.join(args.imgDir, extraFile)

    I1 = Image(readFile(intraFile), args.intra_xy, Image.INTRA, intraFile)
    I2 = Image(readFile(extraFile), args.extra_xy, Image.EXTRA, intraFile)

    # load instrument and algorithm parameters
    inst = Instrument(args.instruFile, I1.sizeinPix)
    algo = Algorithm(args.algoFile, inst, args.debugLevel)

    # run it
    algo.runIt(inst, I1, I2, args.model)

    # output Zernikes 4 and up
    if not(args.outputZerFile == '') or args.debugLevel >= 0:
        outZer4Up(algo.zer4UpNm, 'nm', args.outputZerFile)

    # output parameters
    if not(args.outputParam == '') or args.debugLevel >= 1:
        outParam(args.outputParam, algo, inst, I1, I2, args.model)