Beispiel #1
0
class PhosimCmpt(object):
    def __init__(self, tele):
        """Initialization of PhoSim component class.

        WEP: wavefront estimation pipeline.

        Parameters
        ----------
        tele : TeleFacade
            Telescope instance.
        """

        # Configuration directory
        self.configDir = getConfigDir()

        # Telescope setting file
        settingFilePath = os.path.join(self.configDir,
                                       "phosimCmptSetting.yaml")
        self._phosimCmptSettingFile = ParamReader(filePath=settingFilePath)

        # OPD metrology
        self.metr = OpdMetrology()

        # TeleFacade instance
        self.tele = tele

        # Output directory of data
        self.outputDir = ""

        # Output directory of image
        self.outputImgDir = ""

        # Seed number
        self.seedNum = 0

        # M1M3 force error
        self.m1m3ForceError = 0.05

    def setM1M3ForceError(self, m1m3ForceError):
        """Set the M1M3 force error.

        Parameters
        ----------
        m1m3ForceError : float
            Ratio of actuator force error between 0 and 1.
        """

        self.m1m3ForceError = m1m3ForceError

    def getM1M3ForceError(self):
        """Get the M1M3 force error.

        Returns
        -------
        float
            Ratio of actuator force error.
        """

        return self.m1m3ForceError

    def getSettingFile(self):
        """Get the setting file.

        Returns
        -------
        lsst.ts.wep.ParamReader
            Setting file.
        """

        return self._phosimCmptSettingFile

    def getTele(self):
        """Get the telescope object.

        Returns
        -------
        TeleFacade
            Telescope object.
        """

        return self.tele

    def getNumOfZk(self):
        """Get the number of Zk (annular Zernike polynomial).

        Returns
        -------
        int
            Number of Zk.
        """

        return int(self._phosimCmptSettingFile.getSetting("numOfZk"))

    def getIntraFocalDirName(self):
        """Get the intra-focal directory name.

        Returns
        -------
        str
            Intra-focal directory name.
        """

        return self._phosimCmptSettingFile.getSetting("intraDirName")

    def getExtraFocalDirName(self):
        """Get the extra-focal directory name.

        Returns
        -------
        str
            Extra-focal directory name.
        """

        return self._phosimCmptSettingFile.getSetting("extraDirName")

    def getOpdMetr(self):
        """Get the OPD metrology object.

        OPD: optical path difference.

        Returns
        -------
        OpdMetrology
            OPD metrology object.
        """

        return self.metr

    def setOutputDir(self, outputDir):
        """Set the output directory.

        The output directory will be constructed if there is no existed one.

        Parameters
        ----------
        outputDir : str
            Output directory.
        """

        self._makeDir(outputDir)
        self.outputDir = outputDir

    def _makeDir(self, newDir, exist_ok=True):
        """Make the new directory.

        Super-mkdir; create a leaf directory and all intermediate ones. Works
        like mkdir, except that any intermediate path segment (not just the
        rightmost) will be created if it does not exist.

        Parameters
        ----------
        newDir : str
            New directory.
        exist_ok : bool, optional
            If the target directory already exists, raise an OSError if
            exist_ok is False. Otherwise no exception is raised. (the default
            is True.)
        """

        os.makedirs(newDir, exist_ok=exist_ok)

    def getOutputDir(self):
        """Get the output directory.

        Returns
        -------
        str
            Output directory.
        """

        return self.outputDir

    def setOutputImgDir(self, outputImgDir):
        """Set the output image directory.

        The output image directory will be constructed if there is no existed
        one.

        Parameters
        ----------
        outputImgDir : str
            Output image directory
        """

        self._makeDir(outputImgDir)
        self.outputImgDir = outputImgDir

    def getOutputImgDir(self):
        """Get the output image directory.

        Returns
        -------
        str
            Output image directory
        """

        return self.outputImgDir

    def setSeedNum(self, seedNum):
        """Set the seed number for the M1M3 mirror surface purturbation.

        Parameters
        ----------
        seedNum : int
            Seed number.
        """

        self.seedNum = int(seedNum)

    def getSeedNum(self):
        """Get the seed number for the M1M3 random surface purturbation.

        Returns
        -------
        int or None
            Seed number. None means there is no random purturbation.
        """

        return self.seedNum

    def setSurveyParam(self,
                       obsId=None,
                       filterType=None,
                       boresight=None,
                       zAngleInDeg=None,
                       rotAngInDeg=None):
        """Set the survey parameters.

        Parameters
        ----------
        obsId : int, optional
            Observation Id. (the default is None.)
        filterType : enum 'FilterType' in lsst.ts.wep.Utility, optional
            Active filter type. (the default is None.)
        boresight : tuple, optional
            Telescope boresight in (ra, decl). (the default is None.)
        zAngleInDeg : float, optional
            Zenith angle in degree. (the default is None.)
        rotAngInDeg : float, optional
            Camera rotation angle in degree between -90 and 90 degrees. (the
            default is None.)
        """

        self.tele.setSurveyParam(obsId=obsId,
                                 filterType=filterType,
                                 boresight=boresight,
                                 zAngleInDeg=zAngleInDeg,
                                 rotAngInDeg=rotAngInDeg)

    def addOpdFieldXYbyDeg(self, fieldXInDegree, fieldYInDegree):
        """Add the OPD new field X, Y in degree.

        OPD: optical path difference.

        Parameters
        ----------
        fieldXInDegree : float, list, or numpy.ndarray
            New field X in degree.
        fieldYInDegree : float, list, or numpy.ndarray
            New field Y in degree.
        """

        self.metr.addFieldXYbyDeg(fieldXInDegree, fieldYInDegree)

    def accDofInUm(self, dofInUm):
        """Accumulate the aggregated degree of freedom (DOF) in um.

        idx 0-4: M2 dz, dx, dy, rx, ry
        idx 5-9: Cam dz, dx, dy, rx, ry
        idx 10-29: M1M3 20 bending modes
        idx 30-49: M2 20 bending modes

        Parameters
        ----------
        dofInUm : list or numpy.ndarray
            DOF in um.
        """

        self.tele.accDofInUm(dofInUm)

    def setDofInUm(self, dofInUm):
        """Set the accumulated degree of freedom (DOF) in um.

        idx 0-4: M2 dz, dx, dy, rx, ry
        idx 5-9: Cam dz, dx, dy, rx, ry
        idx 10-29: M1M3 20 bending modes
        idx 30-49: M2 20 bending modes

        Parameters
        ----------
        dofInUm : list or numpy.ndarray
            DOF in um.
        """

        self.tele.setDofInUm(dofInUm)

    def getDofInUm(self):
        """Get the accumulated degree of freedom (DOF) in um.

        idx 0-4: M2 dz, dx, dy, rx, ry
        idx 5-9: Cam dz, dx, dy, rx, ry
        idx 10-29: M1M3 20 bending modes
        idx 30-49: M2 20 bending modes

        Returns
        -------
        numpy.ndarray
            DOF in um.
        """

        return self.tele.getDofInUm()

    def saveDofInUmFileForNextIter(self,
                                   dofInUm,
                                   dofInUmFileName="dofPertInNextIter.mat"):
        """Save the DOF in um data to file for the next iteration.

        DOF: degree of freedom.

        Parameters
        ----------
        dofInUm : list or numpy.ndarray
            DOF in um.
        dofInUmFileName : str, optional
            File name to save the DOF in um. (the default is
            "dofPertInNextIter.mat".)
        """

        filePath = os.path.join(self.outputDir, dofInUmFileName)
        header = "The followings are the DOF in um:"
        np.savetxt(filePath, np.transpose(dofInUm), header=header)

    def runPhoSim(self, argString):
        """Run the PhoSim program.

        Parameters
        ----------
        argString : str
            Arguments for PhoSim.
        """

        self.tele.runPhoSim(argString)

    def getComCamOpdArgsAndFilesForPhoSim(
            self,
            cmdFileName="opd.cmd",
            instFileName="opd.inst",
            logFileName="opdPhoSim.log",
            cmdSettingFileName="opdDefault.cmd",
            instSettingFileName="opdDefault.inst"):
        """Get the OPD calculation arguments and files of ComCam for the PhoSim
        calculation.

        OPD: optical path difference.
        ComCam: commissioning camera.

        Parameters
        ----------
        cmdFileName : str, optional
            Physical command file name. (the default is "opd.cmd".)
        instFileName : str, optional
            OPD instance file name. (the default is "opd.inst".)
        logFileName : str, optional
            Log file name. (the default is "opdPhoSim.log".)
        cmdSettingFileName : str, optional
            Physical command setting file name. (the default is
            "opdDefault.cmd".)
        instSettingFileName : str, optional
            Instance setting file name. (the default is "opdDefault.inst".)

        Returns
        -------
        str
            Arguments to run the PhoSim.
        """

        # Set the default ComCam OPD field positions
        self.metr.setDefaultComcamGQ()

        argString = self._getOpdArgsAndFilesForPhoSim(cmdFileName,
                                                      instFileName,
                                                      logFileName,
                                                      cmdSettingFileName,
                                                      instSettingFileName)

        return argString

    def _getOpdArgsAndFilesForPhoSim(self, cmdFileName, instFileName,
                                     logFileName, cmdSettingFileName,
                                     instSettingFileName):
        """Get the OPD calculation arguments and files for the PhoSim
        calculation.

        OPD: optical path difference.

        Parameters
        ----------
        cmdFileName : str
            Physical command file name.
        instFileName : str
            OPD instance file name.
        logFileName : str
            Log file name.
        cmdSettingFileName : str
            Physical command setting file name.
        instSettingFileName : str
            Instance setting file name.

        Returns
        -------
        str
            Arguments to run the PhoSim.
        """

        # Write the command file
        cmdFilePath = self._writePertAndCmdFiles(cmdSettingFileName,
                                                 cmdFileName)

        # Write the instance file
        instSettingFile = self._getInstSettingFilePath(instSettingFileName)
        instFilePath = self.tele.writeOpdInstFile(
            self.outputDir,
            self.metr,
            instSettingFile=instSettingFile,
            instFileName=instFileName)

        # Get the argument to run the PhoSim
        argString = self._getPhoSimArgs(logFileName, instFilePath, cmdFilePath)

        return argString

    def _writePertAndCmdFiles(self, cmdSettingFileName, cmdFileName):
        """Write the physical perturbation and command files.

        Parameters
        ----------
        cmdSettingFileName : str
            Physical command setting file name.
        cmdFileName : str
            Physical command file name.

        Returns
        -------
        str
            Command file path.
        """

        # Write the perturbation file
        pertCmdFileName = "pert.cmd"
        pertCmdFilePath = os.path.join(self.outputDir, pertCmdFileName)
        if (not os.path.exists(pertCmdFilePath)):
            self.tele.writePertBaseOnConfigFile(
                self.outputDir,
                seedNum=self.seedNum,
                m1m3ForceError=self.m1m3ForceError,
                saveResMapFig=True,
                pertCmdFileName=pertCmdFileName)

        # Write the physical command file
        cmdSettingFile = os.path.join(self.configDir, "cmdFile",
                                      cmdSettingFileName)
        cmdFilePath = os.path.join(self.outputDir, cmdFileName)
        if (not os.path.exists(cmdFilePath)):
            self.tele.writeCmdFile(self.outputDir,
                                   cmdSettingFile=cmdSettingFile,
                                   pertFilePath=pertCmdFilePath,
                                   cmdFileName=cmdFileName)

        return cmdFilePath

    def _getInstSettingFilePath(self, instSettingFileName):
        """Get the instance setting file path.

        Parameters
        ----------
        instSettingFileName : str
            Instance setting file name.

        Returns
        -------
        str
            Instance setting file path.
        """

        instSettingFile = os.path.join(self.configDir, "instFile",
                                       instSettingFileName)

        return instSettingFile

    def _getPhoSimArgs(self, logFileName, instFilePath, cmdFilePath):
        """Get the arguments needed to run the PhoSim.

        Parameters
        ----------
        logFileName : str
            Log file name.
        instFilePath: str
            Instance file path.
        cmdFilePath : str
            Physical command file path.

        Returns
        -------
        str
            Arguments to run the PhoSim.
        """

        # PhoSim parameters
        numPro = int(self._phosimCmptSettingFile.getSetting("numPro"))
        e2ADC = int(self._phosimCmptSettingFile.getSetting("e2ADC"))
        logFilePath = os.path.join(self.outputImgDir, logFileName)

        argString = self.tele.getPhoSimArgs(instFilePath,
                                            extraCommandFile=cmdFilePath,
                                            numPro=numPro,
                                            outputDir=self.outputImgDir,
                                            e2ADC=e2ADC,
                                            logFilePath=logFilePath)

        return argString

    def getComCamStarArgsAndFilesForPhoSim(
            self,
            extraObsId,
            intraObsId,
            skySim,
            simSeed=1000,
            cmdSettingFileName="starDefault.cmd",
            instSettingFileName="starSingleExp.inst"):
        """Get the star calculation arguments and files of ComCam for the
        PhoSim calculation.

        Parameters
        ----------
        extraObsId : int
            Extra-focal observation Id.
        intraObsId : int
            Intra-focal observation Id.
        skySim : SkySim
            Sky simulator
        simSeed : int, optional
            Random number seed. (the default is 1000.)
        cmdSettingFileName : str, optional
            Physical command setting file name. (the default is
            "starDefault.cmd".)
        instSettingFileName : str, optional
            Instance setting file name. (the default is "starSingleExp.inst".)

        Returns
        -------
        list[str]
            List of arguments to run the PhoSim.
        """

        # Set the intra- and extra-focal related information
        obsIdList = {"-1": extraObsId, "1": intraObsId}
        instFileNameList = {"-1": "starExtra.inst", "1": "starIntra.inst"}
        logFileNameList = {
            "-1": "starExtraPhoSim.log",
            "1": "starIntraPhoSim.log"
        }

        extraFocalDirName = self.getExtraFocalDirName()
        intraFocalDirName = self.getIntraFocalDirName()
        outImgDirNameList = {"-1": extraFocalDirName, "1": intraFocalDirName}

        # Write the instance and command files of defocal conditions
        cmdFileName = "star.cmd"
        onFocalDofInUm = self.getDofInUm()
        onFocalOutputImgDir = self.outputImgDir
        argStringList = []
        for ii in (-1, 1):

            # Set the observation ID
            self.setSurveyParam(obsId=obsIdList[str(ii)])

            # Camera piston (Change the unit from mm to um)
            pistonInUm = np.zeros(len(onFocalDofInUm))
            pistonInUm[5] = ii * self.tele.getDefocalDistInMm() * 1e3

            # Set the new DOF that considers the piston motion
            self.setDofInUm(onFocalDofInUm + pistonInUm)

            # Update the output image directory
            outputImgDir = os.path.join(onFocalOutputImgDir,
                                        outImgDirNameList[str(ii)])
            self.setOutputImgDir(outputImgDir)

            # Get the argument to run the phosim
            argString = self.getStarArgsAndFilesForPhoSim(
                skySim,
                cmdFileName=cmdFileName,
                instFileName=instFileNameList[str(ii)],
                logFileName=logFileNameList[str(ii)],
                simSeed=simSeed,
                cmdSettingFileName=cmdSettingFileName,
                instSettingFileName=instSettingFileName)
            argStringList.append(argString)

        # Put the internal state back to the focal plane condition
        self.setDofInUm(onFocalDofInUm)
        self.setOutputImgDir(onFocalOutputImgDir)

        return argStringList

    def getStarArgsAndFilesForPhoSim(self,
                                     skySim,
                                     cmdFileName="star.cmd",
                                     instFileName="star.inst",
                                     logFileName="starPhoSim.log",
                                     simSeed=1000,
                                     cmdSettingFileName="starDefault.cmd",
                                     instSettingFileName="starSingleExp.inst"):
        """Get the star calculation arguments and files for the PhoSim
        calculation.

        Parameters
        ----------
        skySim : SkySim
            Sky simulator
        cmdFileName : str, optional
            Physical command file name. (the default is "star.cmd".)
        instFileName : str, optional
            Star instance file name. (the default is "star.inst".)
        logFileName : str, optional
            Log file name. (the default is "starPhoSim.log".)
        simSeed : int, optional
            Random number seed. (the default is 1000)
        cmdSettingFileName : str, optional
            Physical command setting file name. (the default is
            "starDefault.cmd".)
        instSettingFileName : str, optional
            Instance setting file name. (the default is "starSingleExp.inst".)

        Returns
        -------
        str
            Arguments to run the PhoSim.
        """

        # Write the command file
        cmdFilePath = self._writePertAndCmdFiles(cmdSettingFileName,
                                                 cmdFileName)

        # Write the instance file
        instSettingFile = self._getInstSettingFilePath(instSettingFileName)
        instFilePath = self.tele.writeStarInstFile(
            self.outputDir,
            skySim,
            simSeed=simSeed,
            sedName="sed_flat.txt",
            instSettingFile=instSettingFile,
            instFileName=instFileName)

        # Get the argument to run the PhoSim
        argString = self._getPhoSimArgs(logFileName, instFilePath, cmdFilePath)

        return argString

    def analyzeComCamOpdData(self,
                             zkFileName="opd.zer",
                             rotOpdInDeg=0.0,
                             pssnFileName="PSSN.txt"):
        """Analyze the ComCam OPD data.

        Rotate OPD to simulate the output by rotated camera. When anaylzing the
        PSSN, the unrotated OPD is used.

        ComCam: Commissioning camera.
        OPD: Optical path difference.
        PSSN: Normalized point source sensitivity.

        Parameters
        ----------
        zkFileName : str, optional
            OPD in zk file name. (the default is "opd.zer".)
        rotOpdInDeg : float, optional
            Rotate OPD in degree in the counter-clockwise direction. (the
            default is 0.0.)
        pssnFileName : str, optional
            PSSN file name. (the default is "PSSN.txt".)
        """

        self._writeOpdZkFile(zkFileName, rotOpdInDeg)
        self._writeOpdPssnFile(pssnFileName)

    def _writeOpdZkFile(self, zkFileName, rotOpdInDeg):
        """Write the OPD in zk file.

        OPD: optical path difference.

        Parameters
        ----------
        zkFileName : str
            OPD in zk file name.
        rotOpdInDeg : float
            Rotate OPD in degree in the counter-clockwise direction.
        """

        filePath = os.path.join(self.outputImgDir, zkFileName)
        opdData = self._mapOpdToZk(rotOpdInDeg)
        header = "The followings are OPD in rotation angle of %.2f degree in um from z4 to z22:" % (
            rotOpdInDeg)
        np.savetxt(filePath, opdData, header=header)

    def _mapOpdToZk(self, rotOpdInDeg):
        """Map the OPD to the basis of annular Zernike polynomial (Zk).

        OPD: optical path difference.

        Parameters
        ----------
        rotOpdInDeg : float
            Rotate OPD in degree in the counter-clockwise direction.

        Returns
        -------
        numpy.ndarray
            Zk data from OPD. This is a 2D array. The row is the OPD index and
            the column is z4 to z22 in um. The order of OPD index is based on
            the file name.
        """

        # Get the sorted OPD file list
        opdFileList = self._getOpdFileInDir(self.outputImgDir)

        # Map the OPD to the Zk basis and do the collection
        numOfZk = self.getNumOfZk()
        opdData = np.zeros((len(opdFileList), numOfZk))
        for idx, opdFile in enumerate(opdFileList):
            opd = fits.getdata(opdFile)

            # Rotate OPD if needed
            if (rotOpdInDeg != 0):
                opdRot = ndimage.rotate(opd, rotOpdInDeg, reshape=False)
                opdRot[opd == 0] = 0
            else:
                opdRot = opd

            # z1 to z22 (22 terms)
            zk = self.metr.getZkFromOpd(opdMap=opdRot)[0]

            # Only need to collect z4 to z22
            initIdx = 3
            opdData[idx, :] = zk[initIdx:initIdx + numOfZk]

        return opdData

    def _getOpdFileInDir(self, opdDir):
        """Get the sorted OPD files in the directory.

        OPD: Optical path difference.

        Parameters
        ----------
        opdDir : str
            OPD file directory.

        Returns
        -------
        list
            List of sorted OPD files.
        """

        # Get the files
        opdFileList = []
        fileList = self._getFileInDir(opdDir)
        for file in fileList:
            fileName = os.path.basename(file)
            m = re.match(r"\Aopd_\d+_(\d+).fits.gz", fileName)
            if (m is not None):
                opdFileList.append(file)

        # Do the sorting of file name
        sortedOpdFileList = sortOpdFileList(opdFileList)

        return sortedOpdFileList

    def _getFileInDir(self, fileDir):
        """Get the files in the directory.

        Parameters
        ----------
        fileDir : str
            File directory.

        Returns
        -------
        list
            List of files.
        """

        fileList = []
        for name in os.listdir(fileDir):
            filePath = os.path.join(fileDir, name)
            if os.path.isfile(filePath):
                fileList.append(filePath)

        return fileList

    def _writeOpdPssnFile(self, pssnFileName):
        """Write the OPD PSSN in file.

        OPD: Optical path difference.
        PSSN: Normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.
        """

        filePath = os.path.join(self.outputImgDir, pssnFileName)

        # Calculate the PSSN
        pssnList, gqEffPssn = self._calcComCamOpdPssn()

        # Calculate the FWHM
        effFwhmList, gqEffFwhm = self._calcComCamOpdEffFwhm(pssnList)

        # Append the list to write the data into file
        pssnList.append(gqEffPssn)
        effFwhmList.append(gqEffFwhm)

        # Stack the data
        data = np.vstack((pssnList, effFwhmList))

        # Write to file
        header = "The followings are PSSN and FWHM (in arcsec) data. The final number is the GQ value."
        np.savetxt(filePath, data, header=header)

    def _calcComCamOpdPssn(self):
        """Calculate the ComCam PSSN of OPD.

        ComCam: Commissioning camera.
        OPD: Optical path difference.
        PSSN: Normalized point source sensitivity.
        GQ: Gaussian quadrature.

        Returns
        -------
        list
            PSSN list.
        float
            GQ effective PSSN.
        """

        opdFileList = self._getOpdFileInDir(self.outputImgDir)

        wavelengthInUm = self.tele.getRefWaveLength() * 1e-3
        pssnList = []
        for opdFile in opdFileList:
            pssn = self.metr.calcPSSN(wavelengthInUm, opdFitsFile=opdFile)
            pssnList.append(pssn)

        # Calculate the GQ effectice PSSN
        self._setComCamWgtRatio()
        gqEffPssn = self.metr.calcGQvalue(pssnList)

        return pssnList, gqEffPssn

    def _setComCamWgtRatio(self):
        """Set the ComCam weighting ratio.

        ComCam: Commissioning camera.
        """

        comcamWtRatio = np.ones(9)
        self.metr.setWeightingRatio(comcamWtRatio)

    def _calcComCamOpdEffFwhm(self, pssnList):
        """Calculate the ComCam effective FWHM of OPD.

        ComCam: Commissioning camera.
        FWHM: Full width and half maximum.
        PSSN: Normalized point source sensitivity.
        GQ: Gaussian quadrature.

        Parameters
        ----------
        pssnList : list
            List of PSSN.

        Returns
        -------
        list
            Effective FWHM list.
        float
            GQ effective FWHM of ComCam.
        """

        # Calculate the list of effective FWHM
        effFwhmList = []
        for pssn in pssnList:
            effFwhm = self.metr.calcFWHMeff(pssn)
            effFwhmList.append(effFwhm)

        # Calculate the GQ effectice FWHM
        self._setComCamWgtRatio()
        gqEffFwhm = self.metr.calcGQvalue(effFwhmList)

        return effFwhmList, gqEffFwhm

    def mapOpdDataToListOfWfErr(self, opdZkFileName, refSensorNameList):
        """Map the OPD data to the list of wavefront error.

        OPD: Optical path difference.

        Parameters
        ----------
        opdZkFileName : str
            OPD zk file name.
        refSensorNameList : list
            Reference sensor name list.

        Returns
        -------
        list [lsst.ts.wep.ctrlIntf.SensorWavefrontError]
            List of SensorWavefrontError object.
        """

        opdZk = self._getZkFromFile(opdZkFileName)

        mapSensorNameAndId = MapSensorNameAndId()
        sensorIdList = mapSensorNameAndId.mapSensorNameToId(refSensorNameList)

        listOfWfErr = []
        for sensorId, zk in zip(sensorIdList, opdZk):

            sensorWavefrontData = SensorWavefrontError(
                numOfZk=self.getNumOfZk())
            sensorWavefrontData.setSensorId(sensorId)
            sensorWavefrontData.setAnnularZernikePoly(zk)

            listOfWfErr.append(sensorWavefrontData)

        return listOfWfErr

    def _getZkFromFile(self, zkFileName):
        """Get the zk (z4-z22) from file.

        Parameters
        ----------
        zkFileName : str
            Zk file name.

        Returns
        -------
        numpy.ndarray
            zk matrix. The colunm is z4-z22. The raw is each data point.
        """

        filePath = os.path.join(self.outputImgDir, zkFileName)
        zk = np.loadtxt(filePath)

        return zk

    def getOpdPssnFromFile(self, pssnFileName):
        """Get the OPD PSSN from file.

        OPD: Optical path difference.
        PSSN: Normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.

        Returns
        -------
        numpy.ndarray
            PSSN.
        """

        data = self._getDataOfPssnFile(pssnFileName)
        pssn = data[0, :-1]

        return pssn

    def _getDataOfPssnFile(self, pssnFileName):
        """Get the data of the PSSN file.

        PSSN: Normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.

        Returns
        -------
        numpy.ndarray
            Data of the PSSN file.
        """

        filePath = os.path.join(self.outputImgDir, pssnFileName)
        data = np.loadtxt(filePath)

        return data

    def getOpdGqEffFwhmFromFile(self, pssnFileName):
        """Get the OPD GQ effective FWHM from file.

        OPD: Optical path difference.
        GQ: Gaussian quadrature.
        FWHM: Full width at half maximum.
        PSSN: Normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.

        Returns
        -------
        float
            OPD GQ effective FWHM.
        """

        data = self._getDataOfPssnFile(pssnFileName)
        gqEffFwhm = data[1, -1]

        return gqEffFwhm

    def getListOfFwhmSensorData(self, pssnFileName, refSensorNameList):
        """Get the list of FWHM sensor data based on the OPD PSSN file.

        FWHM: Full width at half maximum.
        OPD: Optical path difference.
        PSSN: Normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.
        refSensorNameList : list
            Reference sensor name list.

        Returns
        -------
        list [lsst.ts.ofc.ctrlIntf.FWHMSensorData]
            List of FWHMSensorData which contains the sensor Id and FWHM data.
        """

        # Get the FWHM data from the PSSN file
        # The first row is the PSSN and the second one is the FWHM
        # The final element in each row is the GQ value
        data = self._getDataOfPssnFile(pssnFileName)
        fwhmData = data[1, :-1]

        mapSensorNameAndId = MapSensorNameAndId()
        sensorIdList = mapSensorNameAndId.mapSensorNameToId(refSensorNameList)

        listOfFWHMSensorData = []
        for sensorId, fwhm in zip(sensorIdList, fwhmData):
            fwhmSensorData = FWHMSensorData(sensorId, np.array([fwhm]))
            listOfFWHMSensorData.append(fwhmSensorData)

        return listOfFWHMSensorData

    def repackageComCamAmpImgFromPhoSim(self):
        """Repackage the ComCam amplifier images from PhoSim to the single 16
        extension MEFs for processing.

        ComCam: commissioning camera.
        MEF: multi-extension frames.
        """

        self._repackageComCamImages(isEimg=False)

    def _repackageComCamImages(self, isEimg=False):
        """Repackage the ComCam images from PhoSim for processing.

        Parameters
        ----------
        isEimg : bool, optional
            Is eimage or not. (the default is False.)
        """

        # Make a temporary directory
        tmpDirPath = os.path.join(self.outputImgDir, "tmp")
        self._makeDir(tmpDirPath)

        intraFocalDirName = self.getIntraFocalDirName()
        extraFocalDirName = self.getExtraFocalDirName()
        for imgType in (intraFocalDirName, extraFocalDirName):

            # Repackage the images to the temporary directory
            command = "phosim_repackager.py"
            phosimImgDir = os.path.join(self.outputImgDir, imgType)
            argstring = "%s --out_dir=%s" % (phosimImgDir, tmpDirPath)
            if (isEimg):
                argstring += " --eimage"
            runProgram(command, argstring=argstring)

            # Remove the image data in the original directory
            argString = "-rf %s/*.fits*" % phosimImgDir
            runProgram("rm", argstring=argString)

            # Put the repackaged data into the image directory
            argstring = "%s/*.fits %s" % (tmpDirPath, phosimImgDir)
            runProgram("mv", argstring=argstring)

        # Remove the temporary directory
        shutil.rmtree(tmpDirPath)

    def repackageComCamEimgFromPhoSim(self):
        """Repackage the ComCam eimages from PhoSim for processing.

        ComCam: commissioning camera.
        """

        self._repackageComCamImages(isEimg=True)

    def reorderAndSaveWfErrFile(self,
                                listOfWfErr,
                                refSensorNameList,
                                zkFileName="wfs.zer"):
        """Reorder the wavefront error in the wavefront error list according to
        the reference sensor name list and save to a file.

        The unexisted wavefront error will be a numpy zero array. The unit is
        um.

        Parameters
        ----------
        listOfWfErr : list [lsst.ts.wep.ctrlIntf.SensorWavefrontData]
            List of SensorWavefrontData object.
        refSensorNameList : list
            Reference sensor name list.
        zkFileName : str, optional
            Wavefront error file name. (the default is "wfs.zer".)
        """

        # Get the sensor name that in the wavefront error map
        wfErrMap = self._transListOfWfErrToMap(listOfWfErr)
        nameListInWfErrMap = list(wfErrMap.keys())

        # Reorder the wavefront error map based on the reference sensor name
        # list.
        reorderedWfErrMap = dict()
        for sensorName in refSensorNameList:
            if sensorName in nameListInWfErrMap:
                wfErr = wfErrMap[sensorName]
            else:
                numOfZk = self.getNumOfZk()
                wfErr = np.zeros(numOfZk)
            reorderedWfErrMap[sensorName] = wfErr

        # Save the file
        filePath = os.path.join(self.outputImgDir, zkFileName)
        wfsData = self._getWfErrValuesAndStackToMatrix(reorderedWfErrMap)
        header = "The followings are ZK in um from z4 to z22:"
        np.savetxt(filePath, wfsData, header=header)

    def _transListOfWfErrToMap(self, listOfWfErr):
        """Transform the list of wavefront error to map.

        Parameters
        ----------
        listOfWfErr : list [lsst.ts.wep.ctrlIntf.SensorWavefrontData]
            List of SensorWavefrontData object.

        Returns
        -------
        dict
            Calculated wavefront error. The dictionary key [str] is the
            abbreviated sensor name (e.g. R22_S11). The dictionary item
            [numpy.ndarray] is the averaged wavefront error (z4-z22) in um.
        """

        mapSensorNameAndId = MapSensorNameAndId()

        wfErrMap = dict()
        for sensorWavefrontData in listOfWfErr:
            sensorId = sensorWavefrontData.getSensorId()
            sensorNameList = mapSensorNameAndId.mapSensorIdToName(sensorId)[0]
            sensorName = sensorNameList[0]

            avgErrInUm = sensorWavefrontData.getAnnularZernikePoly()

            wfErrMap[sensorName] = avgErrInUm

        return wfErrMap

    def _getWfErrValuesAndStackToMatrix(self, wfErrMap):
        """Get the wavefront errors and stack them to be a matrix.

        Parameters
        ----------
        wfErrMap : dict
            Calculated wavefront error. The dictionary key [str] is the
            abbreviated sensor name (e.g. R22_S11). The dictionary item
            [numpy.ndarray] is the averaged wavefront error (z4-z22) in um.

        Returns
        -------
        numpy.ndarray
            Wavefront errors as a matrix. The column is z4-z22 in um. The row
            is the individual sensor. The order is the same as the input of
            wfErrMap.
        """

        numOfZk = self.getNumOfZk()
        valueMatrix = np.empty((0, numOfZk))
        for wfErr in wfErrMap.values():
            valueMatrix = np.vstack((valueMatrix, wfErr))

        return valueMatrix
Beispiel #2
0
class TestOpdMetrology(unittest.TestCase):
    """ Test the OpdMetrology class."""
    def setUp(self):

        self.testDataDir = os.path.join(getModulePath(), "tests", "testData",
                                        "testOpdFunc")
        self.metr = OpdMetrology()

    def testSetWeightingRatio(self):

        wt = [1, 2]
        self.metr.setWeightingRatio(wt)
        self.assertEqual(len(self.metr.wt), len(wt))
        self.assertEqual(np.sum(self.metr.wt), 1)
        self.assertAlmostEqual(self.metr.wt[1] / self.metr.wt[0], 2)
        self.assertRaises(ValueError, self.metr.setWeightingRatio, [-1, 1])

    def testSetFieldXYinDeg(self):

        fieldXInDegree = 0.1
        fieldYInDegree = 0.2
        self.metr.setFieldXYinDeg(fieldXInDegree, fieldYInDegree)

        self.assertEqual(self.metr.fieldX, fieldXInDegree)
        self.assertEqual(self.metr.fieldY, fieldYInDegree)

    def testAddFieldXYbyDeg(self):

        fieldXInDegree = 0.1
        fieldYInDegree = 0.2
        self.metr.addFieldXYbyDeg(fieldXInDegree, fieldYInDegree)
        self.assertEqual(len(self.metr.fieldX), 1)

        self.metr.addFieldXYbyDeg(fieldXInDegree, fieldYInDegree)
        self.assertEqual(len(self.metr.fieldX), 2)

    def testSetDefaultLsstGQ(self):

        self.metr.setDefaultLsstGQ()
        self.assertEqual(len(self.metr.fieldX), 31)

    def testGetDefaultLsstWfsGQ(self):

        fieldWFSx, fieldWFSy = self.metr.getDefaultLsstWfsGQ()
        self.assertEqual(len(fieldWFSx), 4)

    def testSetDefaultComcamGQ(self):

        self.metr.setDefaultComcamGQ()
        self.assertEqual(len(self.metr.fieldX), 9)

    def testGetZkFromOpd(self):

        opdFilePath = self._getOpdFilePath()
        zk = self.metr.getZkFromOpd(opdFitsFile=opdFilePath)[0]

        ansOpdFileName = "sim6_iter0_opd.zer"
        ansOpdFilePath = os.path.join(self.testDataDir, ansOpdFileName)
        allOpdAns = np.loadtxt(ansOpdFilePath)
        self.assertLess(np.sum(np.abs(zk - allOpdAns[0, :])), 1e-10)

    def _getOpdFilePath(self):

        opdFileName = "sim6_iter0_opd0.fits.gz"
        opdFilePath = os.path.join(self.testDataDir, opdFileName)

        return opdFilePath

    def testRmPTTfromOPD(self):

        opdFilePath = self._getOpdFilePath()
        opdRmPTT, opdx, opdy = self.metr.rmPTTfromOPD(opdFitsFile=opdFilePath)

        zkRmPTT = self.metr.getZkFromOpd(opdMap=opdRmPTT)[0]
        self.assertLess(np.sum(np.abs(zkRmPTT[0:3])), 5e-2)

    def testAddFieldXYbyCamPos(self):

        sensorName = "R22_S11"
        xInpixel = 4000
        yInPixel = 4072
        self.metr.addFieldXYbyCamPos(sensorName, xInpixel, yInPixel,
                                     self.testDataDir)

        ansFieldXinDeg = 2000 * 0.2 / 3600
        ansFieldYinDeg = 2036 * 0.2 / 3600
        self.assertAlmostEqual((self.metr.fieldX[-1], self.metr.fieldY[-1]),
                               (ansFieldXinDeg, ansFieldYinDeg))

    def calcPSSN(self):

        pssn = self._calcPssn()
        allData = self._getMetroAllAnsData()
        self.assertAlmostEqual(pssn, allData[0, 0])

    def _calcPssn(self):

        wavelengthInUm = 0.5
        opdFilePath = self._getOpdFilePath()
        pssn = self.metr.calcPSSN(wavelengthInUm, opdFitsFile=opdFilePath)

        return pssn

    def _getMetroAllAnsData(self):

        ansAllDataFileName = "sim6_iter0_PSSN.txt"
        ansAllDataFilePath = os.path.join(self.testDataDir, ansAllDataFileName)
        allData = np.loadtxt(ansAllDataFilePath)

        return allData

    def testCalcFWHMeff(self):

        pssn = self._calcPssn()
        fwhm = self.metr.calcFWHMeff(pssn)

        allData = self._getMetroAllAnsData()
        self.assertAlmostEqual(fwhm, allData[1, 0])

    def testCalcDm5(self):

        pssn = self._calcPssn()
        dm5 = self.metr.calcDm5(pssn)

        allData = self._getMetroAllAnsData()
        self.assertAlmostEqual(dm5, allData[2, 0])

    def testCalcEllip(self):

        wavelengthInUm = 0.5
        opdFilePath = self._getOpdFilePath()
        elli = self.metr.calcEllip(wavelengthInUm, opdFitsFile=opdFilePath)

        ansElliFileName = "sim6_iter0_elli.txt"
        ansElliFilePath = os.path.join(self.testDataDir, ansElliFileName)
        allElli = np.loadtxt(ansElliFilePath)
        self.assertAlmostEqual(elli, allElli[0])

    def testCalcGQvalue(self):

        self.metr.setDefaultLsstGQ()
        allData = self._getMetroAllAnsData()
        valueList = allData[0, 0:31]

        GQvalue = self.metr.calcGQvalue(valueList)
        self.assertAlmostEqual(GQvalue, allData[0, -1])
Beispiel #3
0
class WepPhosimCmpt(object):

    NUM_OF_ZK = 19

    PISTON_INTRA_DIR_NAME = "intra"
    PISTON_EXTRA_DIR_NAME = "extra"

    def __init__(self, phosimDir):
        """Initialization of WEP PhoSim component class.

        WEP: wavefront estimation pipeline.

        Parameters
        ----------
        phosimDir : str
            PhoSim directory.
        """

        self.metr = OpdMetrology()
        self.tele = TeleFacade()

        self.outputDir = ""
        self.outputImgDir = ""
        self.seedNum = 0

        self.phosimParam = {"numPro": 1, "e2ADC": 1}

        self._config(phosimDir)

    def _config(self, phosimDir):
        """Do the configuration of WEP PhoSim component class.

        Parameters
        ----------
        phosimDir : str
            PhoSim directory.
        """

        # Configurate the TeleFacade class
        configDataPath = self._getConfigDataPath()
        teleConfigFilePath = os.path.join(configDataPath, "telescopeConfig",
                                          "GT.inst")
        self.tele.setConfigFile(teleConfigFilePath)

        camDataDir = os.path.join(configDataPath, "camera")
        M1M3dataDir = os.path.join(configDataPath, "M1M3")
        M2dataDir = os.path.join(configDataPath, "M2")
        self.tele.setSubSysConfigDir(camDataDir=camDataDir,
                                     M1M3dataDir=M1M3dataDir,
                                     M2dataDir=M2dataDir,
                                     phosimDir=phosimDir)

        self.tele.setSensorOn(sciSensorOn=True,
                              wfSensorOn=False,
                              guidSensorOn=False)

        # The lsst is used here because the obs_lsst only supports the lsst now
        self.tele.setInstName("lsst15")

    def _getConfigDataPath(self):
        """Get the configuration data path.

        Returns
        -------
        str
            Configuration data path
        """

        configDataPath = os.path.join(getModulePath(), "configData")

        return configDataPath

    def getOpdMetr(self):
        """Get the OPD metrology object.

        OPD: optical path difference.

        Returns
        -------
        OpdMetrology
            OPD metrology object.
        """

        return self.metr

    def setOutputDir(self, outputDir):
        """Set the output directory.

        The output directory will be constructed if there is no existed one.

        Parameters
        ----------
        outputDir : str
            Output directory.
        """

        self._makeDir(outputDir)
        self.outputDir = outputDir

    def _makeDir(self, newDir, exist_ok=True):
        """Make the new directory.

        Super-mkdir; create a leaf directory and all intermediate ones. Works
        like mkdir, except that any intermediate path segment (not just the
        rightmost) will be created if it does not exist.

        Parameters
        ----------
        newDir : str
            New directory.
        exist_ok : bool, optional
            If the target directory already exists, raise an OSError if
            exist_ok is False. Otherwise no exception is raised. (the default
            is True.)
        """

        os.makedirs(newDir, exist_ok=exist_ok)

    def getOutputDir(self):
        """Get the output directory.

        Returns
        -------
        str
            Output directory.
        """

        return self.outputDir

    def setOutputImgDir(self, outputImgDir):
        """Set the output image directory.

        The output image directory will be constructed if there is no existed
        one.

        Parameters
        ----------
        outputImgDir : str
            Output image directory
        """

        self._makeDir(outputImgDir)
        self.outputImgDir = outputImgDir

    def getOutputImgDir(self):
        """Get the output image directory.

        Returns
        -------
        str
            Output image directory
        """

        return self.outputImgDir

    def setSeedNum(self, seedNum):
        """Set the seed number for the M1M3 mirror surface purturbation.

        Parameters
        ----------
        seedNum : int
            Seed number.
        """

        self.seedNum = int(seedNum)

    def getSeedNum(self):
        """Get the seed number for the M1M3 random surface purturbation.

        Returns
        -------
        int or None
            Seed number. None means there is no random purturbation.
        """

        return self.seedNum

    def setPhosimParam(self, numPro=1, e2ADC=1):
        """Set the PhoSim simulation parameters.

        Parameters
        ----------
        numPro : int, optional
            Number of processors. The value should be greater than 1. (the
            default is 1.)
        e2ADC : int, optional
            Whether to generate amplifier images (1 = true, 0 = false) (the
            default is 1.)
        """

        if (numPro > 0):
            self.phosimParam["numPro"] = int(numPro)

        if e2ADC in (0, 1):
            self.phosimParam["e2ADC"] = int(e2ADC)

    def getPhosimParam(self):
        """Get the PhoSim simulation parameters.

        Returns
        -------
        dict
            PhoSim simulation parameters.
        """

        return self.phosimParam

    def setSurveyParam(self,
                       obsId=None,
                       filterType=None,
                       boresight=None,
                       zAngleInDeg=None,
                       rotAngInDeg=None,
                       mjd=None):
        """Set the survey parameters.

        Parameters
        ----------
        obsId : int, optional
            Observation Id. (the default is None.)
        filterType : FilterType, optional
            Active filter type. (the default is None.)
        boresight : tuple, optional
            Telescope boresight in (ra, decl). (the default is None.)
        zAngleInDeg : float, optional
            Zenith angle in degree. (the default is None.)
        rotAngInDeg : float, optional
            Camera rotation angle in degree between -90 and 90 degrees. (the
            default is None.)
        mjd : float, optional
            MJD of observation. (the default is None.)
        """

        self.tele.setSurveyParam(obsId=obsId,
                                 filterType=filterType,
                                 boresight=boresight,
                                 zAngleInDeg=zAngleInDeg,
                                 rotAngInDeg=rotAngInDeg,
                                 mjd=mjd)

    def addOpdFieldXYbyDeg(self, fieldXInDegree, fieldYInDegree):
        """Add the OPD new field X, Y in degree.

        OPD: optical path difference.

        Parameters
        ----------
        fieldXInDegree : float, list, or numpy.ndarray
            New field X in degree.
        fieldYInDegree : float, list, or numpy.ndarray
            New field Y in degree.
        """

        self.metr.addFieldXYbyDeg(fieldXInDegree, fieldYInDegree)

    def accDofInUm(self, dofInUm):
        """Accumulate the aggregated degree of freedom (DOF) in um.

        idx 0-4: M2 dz, dx, dy, rx, ry
        idx 5-9: Cam dz, dx, dy, rx, ry
        idx 10-29: M1M3 20 bending modes
        idx 30-49: M2 20 bending modes

        Parameters
        ----------
        dofInUm : list or numpy.ndarray
            DOF in um.
        """

        self.tele.accDofInUm(dofInUm)

    def setDofInUm(self, dofInUm):
        """Set the accumulated degree of freedom (DOF) in um.

        idx 0-4: M2 dz, dx, dy, rx, ry
        idx 5-9: Cam dz, dx, dy, rx, ry
        idx 10-29: M1M3 20 bending modes
        idx 30-49: M2 20 bending modes

        Parameters
        ----------
        dofInUm : list or numpy.ndarray
            DOF in um.
        """

        self.tele.setDofInUm(dofInUm)

    def getDofInUm(self):
        """Get the accumulated degree of freedom (DOF) in um.

        idx 0-4: M2 dz, dx, dy, rx, ry
        idx 5-9: Cam dz, dx, dy, rx, ry
        idx 10-29: M1M3 20 bending modes
        idx 30-49: M2 20 bending modes

        Returns
        -------
        numpy.ndarray
            DOF in um.
        """

        return self.tele.dofInUm

    def saveDofInUmFileForNextIter(self,
                                   dofInUm,
                                   dofInUmFileName="dofPertInNextIter.mat"):
        """Save the DOF in um data to file for the next iteration.

        DOF: degree of freedom.

        Parameters
        ----------
        dofInUm : list or numpy.ndarray
            DOF in um.
        dofInUmFileName : str, optional
            File name to save the DOF in um. (the default is
            "dofPertInNextIter.mat".)
        """

        filePath = os.path.join(self.outputDir, dofInUmFileName)
        header = "The followings are the DOF in um:"
        np.savetxt(filePath, np.transpose(dofInUm), header=header)

    def runPhoSim(self, argString):
        """Run the PhoSim program.

        Parameters
        ----------
        argString : str 
            Arguments for PhoSim.
        """

        self.tele.runPhoSim(argString)

    def getComCamOpdArgsAndFilesForPhoSim(
            self,
            cmdFileName="opd.cmd",
            instFileName="opd.inst",
            logFileName="opdPhoSim.log",
            cmdSettingFileName="opdDefault.cmd",
            instSettingFileName="opdDefault.inst"):
        """Get the OPD calculation arguments and files of ComCam for the PhoSim
        calculation.

        OPD: optical path difference.
        ComCam: commissioning camera.

        Parameters
        ----------
        cmdFileName : str, optional
            Physical command file name. (the default is "opd.cmd".)
        instFileName : str, optional
            OPD instance file name. (the default is "opd.inst".)
        logFileName : str, optional
            Log file name. (the default is "opdPhoSim.log".)
        cmdSettingFileName : str, optional
            Physical command setting file name. (the default is
            "opdDefault.cmd".)
        instSettingFileName : str, optional
            Instance setting file name. (the default is "opdDefault.inst".)

        Returns
        -------
        str
            Arguments to run the PhoSim.
        """

        # Set the default ComCam OPD field positions
        self.metr.setDefaultComcamGQ()

        argString = self._getOpdArgsAndFilesForPhoSim(
            cmdFileName=cmdFileName,
            instFileName=instFileName,
            logFileName=logFileName,
            cmdSettingFileName=cmdSettingFileName,
            instSettingFileName=instSettingFileName)

        return argString

    def _getOpdArgsAndFilesForPhoSim(self,
                                     cmdFileName="opd.cmd",
                                     instFileName="opd.inst",
                                     logFileName="opdPhoSim.log",
                                     cmdSettingFileName="opdDefault.cmd",
                                     instSettingFileName="opdDefault.inst"):
        """Get the OPD calculation arguments and files for the PhoSim
        calculation.

        OPD: optical path difference.

        Parameters
        ----------
        cmdFileName : str, optional
            Physical command file name. (the default is "opd.cmd".)
        instFileName : str, optional
            OPD instance file name. (the default is "opd.inst".)
        logFileName : str, optional
            Log file name. (the default is "opdPhoSim.log".)
        cmdSettingFileName : str, optional
            Physical command setting file name. (the default is
            "opdDefault.cmd".)
        instSettingFileName : str, optional
            Instance setting file name. (the default is "opdDefault.inst".)

        Returns
        -------
        str
            Arguments to run the PhoSim.
        """

        # Write the command file
        cmdFilePath = self._writePertAndCmdFiles(cmdSettingFileName,
                                                 cmdFileName)

        # Write the instance file
        instSettingFile = self._getInstSettingFilePath(instSettingFileName)
        instFilePath = self.tele.writeOpdInstFile(
            self.outputDir,
            self.metr,
            instSettingFile=instSettingFile,
            instFileName=instFileName)

        # Get the argument to run the PhoSim
        argString = self._getPhoSimArgs(logFileName, instFilePath, cmdFilePath)

        return argString

    def _writePertAndCmdFiles(self, cmdSettingFileName, cmdFileName):
        """Write the physical perturbation and command files.

        Parameters
        ----------
        cmdSettingFileName : str
            Physical command setting file name.
        cmdFileName : str
            Physical command file name.

        Returns
        -------
        str
            Command file path.
        """

        # Write the perturbation file
        pertCmdFileName = "pert.cmd"
        pertCmdFilePath = os.path.join(self.outputDir, pertCmdFileName)
        if (not os.path.exists(pertCmdFilePath)):
            self.tele.writePertBaseOnConfigFile(
                self.outputDir,
                seedNum=self.seedNum,
                saveResMapFig=True,
                pertCmdFileName=pertCmdFileName)

        # Write the physical command file
        configDataPath = self._getConfigDataPath()
        cmdSettingFile = os.path.join(configDataPath, "cmdFile",
                                      cmdSettingFileName)
        cmdFilePath = os.path.join(self.outputDir, cmdFileName)
        if (not os.path.exists(cmdFilePath)):
            self.tele.writeCmdFile(self.outputDir,
                                   cmdSettingFile=cmdSettingFile,
                                   pertFilePath=pertCmdFilePath,
                                   cmdFileName=cmdFileName)

        return cmdFilePath

    def _getInstSettingFilePath(self, instSettingFileName):
        """Get the instance setting file path.

        Parameters
        ----------
        instSettingFileName : str
            Instance setting file name.

        Returns
        -------
        str
            Instance setting file path.
        """

        configDataPath = self._getConfigDataPath()
        instSettingFile = os.path.join(configDataPath, "instFile",
                                       instSettingFileName)

        return instSettingFile

    def _getPhoSimArgs(self, logFileName, instFilePath, cmdFilePath):
        """Get the arguments needed to run the PhoSim.

        Parameters
        ----------
        logFileName : str
            Log file name.
        instFilePath: str
            Instance file path.
        cmdFilePath : str
            Physical command file path.

        Returns
        -------
        str
            Arguments to run the PhoSim.
        """

        numPro = self.phosimParam["numPro"]
        e2ADC = self.phosimParam["e2ADC"]
        logFilePath = os.path.join(self.outputImgDir, logFileName)

        argString = self.tele.getPhoSimArgs(instFilePath,
                                            extraCommandFile=cmdFilePath,
                                            numPro=numPro,
                                            outputDir=self.outputImgDir,
                                            e2ADC=e2ADC,
                                            logFilePath=logFilePath)

        return argString

    def getComCamStarArgsAndFilesForPhoSim(
            self,
            extraObsId,
            intraObsId,
            skySim,
            simSeed=1000,
            cmdSettingFileName="starDefault.cmd",
            instSettingFileName="starSingleExp.inst"):
        """Get the star calculation arguments and files of ComCam for the PhoSim
        calculation.

        Parameters
        ----------
        extraObsId : int
            Extra-focal observation Id.
        intraObsId : int
            Intra-focal observation Id.
        skySim : SkySim
            Sky simulator
        simSeed : int, optional
            Random number seed. (the default is 1000.)
        cmdSettingFileName : str, optional
            Physical command setting file name. (the default is
            "starDefault.cmd".)
        instSettingFileName : {str}, optional
            Instance setting file name. (the default is "starSingleExp.inst".)

        Returns
        -------
        list[str]
            List of arguments to run the PhoSim.
        """

        # Set the intra- and extra-focal related information
        obsIdList = {"-1": extraObsId, "1": intraObsId}
        instFileNameList = {"-1": "starExtra.inst", "1": "starIntra.inst"}
        logFileNameList = {
            "-1": "starExtraPhoSim.log",
            "1": "starIntraPhoSim.log"
        }
        outImgDirNameList = {
            "-1": self.PISTON_EXTRA_DIR_NAME,
            "1": self.PISTON_INTRA_DIR_NAME
        }

        # Write the instance and command files of defocal conditions
        cmdFileName = "star.cmd"
        onFocalDofInUm = self.getDofInUm()
        onFocalOutputImgDir = self.outputImgDir
        argStringList = []
        for ii in (-1, 1):

            # Set the observation ID
            self.setSurveyParam(obsId=obsIdList[str(ii)])

            # Camera piston (Change the unit from mm to um)
            pistonInUm = np.zeros(len(onFocalDofInUm))
            pistonInUm[5] = ii * self.tele.getDefocalDisInMm() * 1e3

            # Set the new DOF that considers the piston motion
            self.setDofInUm(onFocalDofInUm + pistonInUm)

            # Update the output image directory
            outputImgDir = os.path.join(onFocalOutputImgDir,
                                        outImgDirNameList[str(ii)])
            self.setOutputImgDir(outputImgDir)

            # Get the argument to run the phosim
            argString = self.getStarArgsAndFilesForPhoSim(
                skySim,
                cmdFileName=cmdFileName,
                instFileName=instFileNameList[str(ii)],
                logFileName=logFileNameList[str(ii)],
                simSeed=simSeed,
                cmdSettingFileName=cmdSettingFileName,
                instSettingFileName=instSettingFileName)
            argStringList.append(argString)

        # Put the internal state back to the focal plane condition
        self.setDofInUm(onFocalDofInUm)
        self.setOutputImgDir(onFocalOutputImgDir)

        return argStringList

    def getStarArgsAndFilesForPhoSim(self,
                                     skySim,
                                     cmdFileName="star.cmd",
                                     instFileName="star.inst",
                                     logFileName="starPhoSim.log",
                                     simSeed=1000,
                                     cmdSettingFileName="starDefault.cmd",
                                     instSettingFileName="starSingleExp.inst"):
        """Get the star calculation arguments and files for the PhoSim
        calculation.

        Parameters
        ----------
        skySim : SkySim
            Sky simulator
        cmdFileName : str, optional
            Physical command file name. (the default is "star.cmd".)
        instFileName : str, optional
            Star instance file name. (the default is "star.inst".)
        logFileName : str, optional
            Log file name. (the default is "starPhoSim.log".)
        simSeed : int, optional
            Random number seed. (the default is 1000)
        cmdSettingFileName : str, optional
            Physical command setting file name. (the default is
            "starDefault.cmd".)
        instSettingFileName : str, optional
            Instance setting file name. (the default is "starSingleExp.inst".)

        Returns
        -------
        str
            Arguments to run the PhoSim.
        """

        # Write the command file
        cmdFilePath = self._writePertAndCmdFiles(cmdSettingFileName,
                                                 cmdFileName)

        # Write the instance file
        instSettingFile = self._getInstSettingFilePath(instSettingFileName)
        instFilePath = self.tele.writeStarInstFile(
            self.outputDir,
            skySim,
            simSeed=simSeed,
            sedName="sed_flat.txt",
            instSettingFile=instSettingFile,
            instFileName=instFileName)

        # Get the argument to run the PhoSim
        argString = self._getPhoSimArgs(logFileName, instFilePath, cmdFilePath)

        return argString

    def analyzeComCamOpdData(self,
                             zkFileName="opd.zer",
                             pssnFileName="PSSN.txt"):
        """Analyze the ComCam OPD data.

        ComCam: Commissioning camera.
        OPD: optical path difference.
        PSSN: normalized point source sensitivity.

        Parameters
        ----------
        zkFileName : str, optional
            OPD in zk file name. (the default is "opd.zer".)
        pssnFileName : str, optional
            PSSN file name. (the default is "PSSN.txt".)
        """

        self._writeOpdZkFile(zkFileName)
        self._writeOpdPssnFile(pssnFileName)

    def _writeOpdZkFile(self, zkFileName):
        """Write the OPD in zk file.

        OPD: optical path difference.

        Parameters
        ----------
        zkFileName : str
            OPD in zk file name.
        """

        filePath = os.path.join(self.outputImgDir, zkFileName)
        opdData = self._mapOpdToZk()
        header = "The followings are OPD in um from z4 to z22:"
        np.savetxt(filePath, opdData, header=header)

    def _mapOpdToZk(self):
        """Map the OPD to the basis of annular Zernike polynomial (Zk).

        OPD: optical path difference.

        Returns
        -------
        numpy.ndarray
            Zk data from OPD. This is a 2D array. The row is the OPD index and
            the column is z4 to z22 in um. The order of OPD index is based on
            the file name.
        """

        # Get the OPD file list
        opdFileList = self._getOpdFileInDir(self.outputImgDir)

        # Map the OPD to the Zk basis and do the collection
        opdData = np.zeros((len(opdFileList), self.NUM_OF_ZK))
        for idx, opdFile in enumerate(opdFileList):

            # z1 to z22 (22 terms)
            zk = self.metr.getZkFromOpd(opdFitsFile=opdFile)[0]

            # Only need to collect z4 to z22
            initIdx = 3
            opdData[idx, :] = zk[initIdx:initIdx + self.NUM_OF_ZK]

        return opdData

    def _writeOpdPssnFile(self, pssnFileName):
        """Write the OPD PSSN in file.

        OPD: optical path difference.
        PSSN: normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.
        """

        filePath = os.path.join(self.outputImgDir, pssnFileName)

        # Calculate the PSSN
        pssnList, gqEffPssn = self._calcComCamOpdPssn()

        # Calculate the FWHM
        effFwhmList, gqEffFwhm = self._calcComCamOpdEffFwhm(pssnList)

        # Append the list to write the data into file
        pssnList.append(gqEffPssn)
        effFwhmList.append(gqEffFwhm)

        # Stack the data
        data = np.vstack((pssnList, effFwhmList))

        # Write to file
        header = "The followings are PSSN and FWHM (in arcsec) data. The final number is the GQ value."
        np.savetxt(filePath, data, header=header)

    def _calcComCamOpdPssn(self):
        """Calculate the ComCam PSSN of OPD.

        ComCam: commissioning camera.
        OPD: optical path difference.
        PSSN: normalized point source sensitivity.
        GQ: gaussian quadrature.

        Returns
        -------
        list
            PSSN list.
        float
            GQ effective PSSN.
        """

        opdFileList = self._getOpdFileInDir(self.outputImgDir)

        wavelengthInUm = self.tele.WAVELENGTH_IN_NM * 1e-3
        pssnList = []
        for opdFile in opdFileList:
            pssn = self.metr.calcPSSN(wavelengthInUm, opdFitsFile=opdFile)
            pssnList.append(pssn)

        # Calculate the GQ effectice PSSN
        self._setComCamWgtRatio()
        gqEffPssn = self.metr.calcGQvalue(pssnList)

        return pssnList, gqEffPssn

    def _getOpdFileInDir(self, opdDir):
        """Get the OPD files in the directory.

        OPD: optical path difference.

        Parameters
        ----------
        opdDir : str
            OPD file directory.

        Returns
        -------
        list
            List of OPD files.
        """

        opdFileList = []
        fileList = self._getFileInDir(opdDir)
        for file in fileList:
            fileName = os.path.basename(file)
            m = re.match(r"\Aopd_\d+_(\d+).fits.gz", fileName)
            if (m is not None):
                opdFileList.append(file)

        return opdFileList

    def _getFileInDir(self, fileDir):
        """Get the files in the directory.

        Parameters
        ----------
        fileDir : str
            File directory.

        Returns
        -------
        list
            List of files.
        """

        fileList = []
        for name in os.listdir(fileDir):
            filePath = os.path.join(fileDir, name)
            if os.path.isfile(filePath):
                fileList.append(filePath)

        return fileList

    def _setComCamWgtRatio(self):
        """Set the ComCam weighting ratio. 

        ComCam: Commissioning camera.
        """

        comcamWtRatio = np.ones(9)
        self.metr.setWeightingRatio(comcamWtRatio)

    def _calcComCamOpdEffFwhm(self, pssnList):
        """Calculate the ComCam effective FWHM of OPD.

        ComCam: commissioning camera.
        FWHM: full width and half maximum.
        PSSN: normalized point source sensitivity.
        GQ: Gaussian quadrature.

        Parameters
        ----------
        pssnList : list
            List of PSSN.

        Returns
        -------
        list
            Effective FWHM list.
        float
            GQ effective FWHM of ComCam.
        """

        # Calculate the list of effective FWHM
        effFwhmList = []
        for pssn in pssnList:
            effFwhm = self.metr.calcFWHMeff(pssn)
            effFwhmList.append(effFwhm)

        # Calculate the GQ effectice FWHM
        self._setComCamWgtRatio()
        gqEffFwhm = self.metr.calcGQvalue(effFwhmList)

        return effFwhmList, gqEffFwhm

    def getZkFromFile(self, zkFileName):
        """Get the zk (z4-z22) from file.

        Parameters
        ----------
        zkFileName : str
            Zk file name.

        Returns
        -------
        numpy.ndarray
            zk matrix. The colunm is z4-z22. The raw is each data point.
        """

        filePath = os.path.join(self.outputImgDir, zkFileName)
        zk = np.loadtxt(filePath)

        return zk

    def getOpdPssnFromFile(self, pssnFileName):
        """Get the OPD PSSN from file.

        OPD: optical path difference.
        PSSN: normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.

        Returns
        -------
        numpy.ndarray
            PSSN.
        """

        data = self._getDataOfPssnFile(pssnFileName)
        pssn = data[0, :-1]

        return pssn

    def getOpdGqEffFwhmFromFile(self, pssnFileName):
        """Get the OPD GQ effective FWHM from file.

        OPD: optical path difference.
        GQ: Gaussian quadrature.
        FWHM: full width at half maximum.
        PSSN: normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.

        Returns
        -------
        float
            OPD GQ effective FWHM.
        """

        data = self._getDataOfPssnFile(pssnFileName)
        gqEffFwhm = data[1, -1]

        return gqEffFwhm

    def _getDataOfPssnFile(self, pssnFileName):
        """Get the data of the PSSN file.

        PSSN: Normalized point source sensitivity.

        Parameters
        ----------
        pssnFileName : str
            PSSN file name.

        Returns
        -------
        numpy.ndarray
            Data of the PSSN file.
        """

        filePath = os.path.join(self.outputImgDir, pssnFileName)
        data = np.loadtxt(filePath)

        return data

    def repackageComCamImgFromPhoSim(self):
        """Repackage the ComCam amplifier images from PhoSim to the single 16
        extension MEFs for processing.

        ComCam: commissioning camera.
        MEF: multi-extension frames.
        """

        # Make a temp directory
        tmpDirPath = os.path.join(self.outputImgDir, "tmp")
        self._makeDir(tmpDirPath)

        for imgType in (self.PISTON_INTRA_DIR_NAME,
                        self.PISTON_EXTRA_DIR_NAME):

            # Repackage the images to that temp directory
            command = "phosim_repackager.py"
            phosimAmgImgDir = os.path.join(self.outputImgDir, imgType)
            argstring = "%s --out_dir=%s" % (phosimAmgImgDir, tmpDirPath)
            runProgram(command, argstring=argstring)

            # Remove the image data in the original directory
            argString = "-rf %s/lsst_a_*.fits.gz" % phosimAmgImgDir
            runProgram("rm", argstring=argString)

            # Put the repackaged data into the image directory
            argstring = "%s/*.fits %s" % (tmpDirPath, phosimAmgImgDir)
            runProgram("mv", argstring=argstring)

        # Remove the temp directory
        shutil.rmtree(tmpDirPath)

    def reorderAndSaveWfErrFile(self,
                                wfErrMap,
                                refSensorNameList,
                                zkFileName="wfs.zer"):
        """Reorder the wavefront error in the wavefront error map according to
        the reference sensor name list and save to a file.

        The unexisted wavefront error will be a numpy zero array. The unit is
        um.

        Parameters
        ----------
        wfErrMap : dict
            Calculated wavefront error. The dictionary key [str] is the
            abbreviated sensor name (e.g. R22_S11). The dictionary item
            [numpy.ndarray] is the averaged wavefront error (z4-z22) in nm.
        refSensorNameList : list
            Reference sensor name list
        zkFileName : str, optional
            Wavefront error file name. (the default is "wfs.zer".)
        """

        # Get the sensor name that in the wavefront error map
        nameListInWfErrMap = list(wfErrMap.keys())

        # Reorder the wavefront error map based on the reference sensor name
        # list. The wavefront error unit will change from nm to um.
        reorderedWfErrMap = dict()
        for sensorName in refSensorNameList:
            if sensorName in nameListInWfErrMap:
                # Change the unit from nm to um
                wfErr = wfErrMap[sensorName] * 1e-3
            else:
                wfErr = np.zeros(self.NUM_OF_ZK)
            reorderedWfErrMap[sensorName] = wfErr

        # Save the file
        filePath = os.path.join(self.outputImgDir, zkFileName)
        wfsData = self.getWfErrValuesAndStackToMatrix(reorderedWfErrMap)
        header = "The followings are ZK in um from z4 to z22:"
        np.savetxt(filePath, wfsData, header=header)

    def getWfErrValuesAndStackToMatrix(self, wfErrMap):
        """Get the wavefront errors and stack them to be a matrix.

        Parameters
        ----------
        wfErrMap : dict
            Calculated wavefront error. The dictionary key [str] is the
            abbreviated sensor name (e.g. R22_S11). The dictionary item
            [numpy.ndarray] is the averaged wavefront error (z4-z22) in nm. 

        Returns
        -------
        numpy.ndarray
            Wavefront errors as a matrix. The column is z4-z22 in nm. The row is
            the individual sensor. The order is the same as the input of
            wfErrMap.
        """

        valueMatrix = np.empty((0, self.NUM_OF_ZK))
        for wfErr in wfErrMap.values():
            valueMatrix = np.vstack((valueMatrix, wfErr))

        return valueMatrix