Example #1
0
    def __init__(self,
                 settingFileName="default.yaml",
                 focalPlaneFileName="focalplanelayout.txt"):
        """Initialize the SourceProcessor class.

        Parameters
        ----------
        settingFileName : str, optional
            Setting file name. (the default is "default.yaml".)
        focalPlaneFileName : str, optional
            Focal plane file name used in the PhoSim instrument directory. (the
            default is "focalplanelayout.txt".)
        """

        self.sensorName = ""

        configDir = getConfigDir()
        settingFilePath = os.path.join(configDir, settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        self.sensorFocaPlaneInDeg = dict()
        self.sensorFocaPlaneInUm = dict()
        self.sensorDimList = dict()
        self.sensorEulerRot = dict()
        self._readFocalPlane(configDir, focalPlaneFileName)

        # Deblending donut algorithm to use
        deblendDonutType = self._getDeblendDonutTypeInSetting()
        self.deblend = DeblendDonutFactory.createDeblendDonut(deblendDonutType)
Example #2
0
    def testGetMatContentWithDefaultSetting(self):

        paramReader = ParamReader()
        matInYamlFile = paramReader.getMatContent()

        self.assertTrue(isinstance(matInYamlFile, np.ndarray))
        self.assertEqual(len(matInYamlFile), 0)
Example #3
0
    def testGetMatContentWithDefaultSetting(self):

        paramReader = ParamReader()
        matInYamlFile = paramReader.getMatContent()

        self.assertTrue(isinstance(matInYamlFile, np.ndarray))
        self.assertEqual(len(matInYamlFile), 0)
Example #4
0
    def _writeMatToFile(self):

        mat = np.random.rand(3, 4, 5)
        filePath = os.path.join(self.testTempDir.name, "temp.yaml")
        ParamReader.writeMatToFile(mat, filePath)

        return mat, filePath
Example #5
0
    def __init__(self, innerRinM, outerRinM, mirrorDataDir):
        """Initiate the mirror simulator class.

        Parameters
        ----------
        innerRinM : float or tuple
            Mirror inner radius in m.
        outerRinM : float or tuple
            Mirror outer radius in m.
        mirrorDataDir : str
            Mirror data directory.
        """

        # Mirror inner radius
        self.radiusInner = innerRinM

        # Mirror outer radius
        self.radiusOuter = outerRinM

        # Configuration data directory
        self.mirrorDataDir = mirrorDataDir

        # Mirror actuator force
        self._actForceFile = ParamReader()

        # Look-up table (LUT) file
        self._lutFile = ParamReader()

        # Mirror surface
        self._surf = np.array([])

        # Number of Zernike terms to fit.
        self._numTerms = 0
Example #6
0
    def __init__(self, camType, bscDbType, settingFileName="default.yaml"):
        """Initialize the source selector class.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        bscDbType : enum 'BscDbType'
            Bright star catalog (BSC) database type.
        settingFileName : str, optional
            Setting file name (the default is "default.yaml".)
        """

        self.camera = CamFactory.createCam(camType)
        self.db = DatabaseFactory.createDb(bscDbType)
        self.filter = Filter()

        self.maxDistance = 0.0
        self.maxNeighboringStar = 0

        settingFilePath = os.path.join(getConfigDir(), settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        # Configurate the criteria of neighboring stars
        starRadiusInPixel = self.settingFile.getSetting("starRadiusInPixel")
        spacingCoefficient = self.settingFile.getSetting("spacingCoef")
        maxNeighboringStar = self.settingFile.getSetting("maxNumOfNbrStar")
        self.configNbrCriteria(starRadiusInPixel,
                               spacingCoefficient,
                               maxNeighboringStar=maxNeighboringStar)
Example #7
0
    def __init__(self, camTBinDegC=6.5650, camRotInRad=0):
        """Initialization of camera simulator class.

        This class is used to correct the camera distortion.

        Parameters
        ----------
        camTBinDegC : float, optional
            Camera body temperature in degree C. (the default is 6.5650.)
        camRotInRad : float, optional
            Camera rotation angle in radian. (the default is 0.)
        """

        # Configuration directory
        self.configDir = os.path.join(getConfigDir(), "camera")

        # Camera setting file
        settingFilePath = os.path.join(self.configDir, "camSetting.yaml")
        self._camSettingFile = ParamReader(filePath=settingFilePath)

        # Camera body temperature in degree C
        self.camTBinDegC = self.setBodyTempInDegC(camTBinDegC)

        # Camera rotation angle in radian
        self.camRotInRad = self.setRotAngInRad(camRotInRad)
Example #8
0
    def getCamDistortionInMm(self, zAngleInRad, camDistType):
        """Get the camera distortion correction in mm.

        Parameters
        ----------
        zAngleInRad : float
            Zenith angle in radian.
        camDistType : enum 'CamDistType'
            Camera distortion type.

        Returns
        -------
        numpy.ndarray
            Camera distortion in mm.
        """

        # Get the distortion data
        distType = camDistType.name
        dataFilePath = os.path.join(self.configDir, (distType + ".yaml"))
        paramReader = ParamReader(filePath=dataFilePath)

        data = paramReader.getMatContent()

        # Calculate the gravity and temperature distortions
        distortion = self._calcGravityDist(data, zAngleInRad) + \
            self._calcTempDist(data)

        # Reorder the index of Zernike corrections to match the PhoSim use
        zIdx = self._camSettingFile.getSetting("zIdxMapping")

        # The index of python begins from 0.
        distortion = distortion[[x - 1 for x in zIdx]]

        return distortion
Example #9
0
    def _writeMatToFile(self):

        mat = np.random.rand(3, 4, 5)
        filePath = os.path.join(self.testTempDir, "temp.yaml")
        ParamReader.writeMatToFile(mat, filePath)

        return mat, filePath
Example #10
0
    def __init__(self, algoDir):
        """Initialize the Algorithm class.

        Algorithm used to solve the transport of intensity equation to get
        normal/ annular Zernike polynomials.

        Parameters
        ----------
        algoDir : str
            Algorithm configuration directory.
        """

        self.algoDir = algoDir
        self.algoParamFile = ParamReader()

        self._inst = Instrument("")

        # Show the calculation message based on this value
        # 0 means no message will be showed
        self.debugLevel = 0

        # Image has the problem or not from the over-compensation
        self.caustic = False

        # Record the Zk coefficients in each outer-loop iteration
        # The actual total outer-loop iteration time is Num_of_outer_itr + 1
        self.converge = np.array([])

        # Current number of outer-loop iteration
        self.currentItr = 0

        # Record the coefficients of normal/ annular Zernike polynomials after
        # z4 in unit of nm
        self.zer4UpNm = np.array([])

        # Converged wavefront.
        self.wcomp = np.array([])

        # Calculated wavefront in previous outer-loop iteration.
        self.West = np.array([])

        # Converged Zk coefficients
        self.zcomp = np.array([])

        # Calculated Zk coefficients in previous outer-loop iteration
        self.zc = np.array([])

        # Padded mask for use at the offset planes
        self.pMask = None

        # Non-padded mask corresponding to aperture
        self.cMask = None

        # Change the dimension of mask for fft to use
        self.pMaskPad = None
        self.cMaskPad = None
Example #11
0
    def __init__(self):
        """Initialize the filter class."""

        # Configuration file of the limit of star's magnitude
        pathMagLimitStar = os.path.join(getConfigDir(), "bsc",
                                        "magLimitStar.yaml")
        self._fileMagLimitStar = ParamReader(filePath=pathMagLimitStar)

        # Filter type in use
        self.filter = FilterType.U
Example #12
0
    def setUp(self):

        testDir = os.path.join(getModulePath(), "tests")
        self.configDir = os.path.join(testDir, "testData")
        self.fileName = "testConfigFile.yaml"

        filePath = os.path.join(self.configDir, self.fileName)
        self.paramReader = ParamReader(filePath=filePath)

        self.testTempDir = tempfile.TemporaryDirectory(dir=testDir)
Example #13
0
    def __init__(self, algoDir):

        self.algoDir = algoDir
        self.algoParamFile = ParamReader()

        self._inst = Instrument("")

        # Show the calculation message based on this value
        # 0 means no message will be showed
        self.debugLevel = 0

        # Image has the problem or not from the over-compensation
        self.caustic = False

        # Record the Zk coefficients in each outer-loop iteration
        # The actual total outer-loop iteration time is Num_of_outer_itr + 1
        self.converge = np.array([])

        # Current number of outer-loop iteration
        self.currentItr = 0

        # Record the coefficients of normal/ annular Zernike polynomials after
        # z4 in unit of nm
        self.zer4UpNm = np.array([])

        # Converged wavefront.
        self.wcomp = np.array([])

        # Calculated wavefront in previous outer-loop iteration.
        self.West = np.array([])

        # Converged Zk coefficients
        self.zcomp = np.array([])

        # Calculated Zk coefficients in previous outer-loop iteration
        self.zc = np.array([])

        # Padded mask for use at the offset planes
        self.mask_comp = None

        # Non-padded mask corresponding to aperture
        self.mask_pupil = None

        # Change the dimension of mask for fft to use
        self.mask_comp_pad = None
        self.mask_pupil_pad = None

        # Cache annular Zernike evaluations
        self._zk = None

        # Cache evaluations of X and Y annular Zernike gradients
        self._dzkdx = None
        self._dzkdy = None
Example #14
0
    def __init__(self, sensorNameToIdFileName="sensorNameToId.yaml"):
        """Construct a MapSensorNameAndId object.

        Parameters
        ----------
        sensorNameToIdFileName : str, optional
            Configuration file name to map sensor name and Id. (the default is
            "sensorNameToId.yaml".)
        """

        sensorNameToIdFilePath = os.path.join(getConfigDir(),
                                              sensorNameToIdFileName)
        self._sensorNameToIdFile = ParamReader(filePath=sensorNameToIdFilePath)
Example #15
0
    def __init__(self, instDir):

        self.instDir = instDir
        self.instName = ""
        self.dimOfDonutImg = 0
        self.announcedDefocalDisInMm = 0.0

        self.instParamFile = ParamReader()
        self.maskParamFile = ParamReader()

        self.xSensor = np.array([])
        self.ySensor = np.array([])

        self.xoSensor = np.array([])
        self.yoSensor = np.array([])
Example #16
0
    def __init__(self, settingFileName="default.yaml",
                 focalPlaneFileName="focalplanelayout.txt"):
        """Initialize the SourceProcessor class.

        Parameters
        ----------
        settingFileName : str, optional
            Setting file name. (the default is "default.yaml".)
        focalPlaneFileName : str, optional
            Focal plane file name used in the PhoSim instrument directory. (the
            default is "focalplanelayout.txt".)
        """

        self.sensorName = ""
        self.blendedImageDecorator = BlendedImageDecorator()

        configDir = getConfigDir()
        settingFilePath = os.path.join(configDir, settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        self.sensorFocaPlaneInDeg = dict()
        self.sensorFocaPlaneInUm = dict()
        self.sensorDimList = dict()
        self.sensorEulerRot = dict()
        self._readFocalPlane(configDir, focalPlaneFileName)
Example #17
0
    def __init__(self, camType, bscDbType, settingFileName="default.yaml"):
        """Initialize the source selector class.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        bscDbType : enum 'BscDbType'
            Bright star catalog (BSC) database type.
        settingFileName : str, optional
            Setting file name (the default is "default.yaml".)
        """

        self.camera = CamFactory.createCam(camType)
        self.db = DatabaseFactory.createDb(bscDbType)
        self.filter = Filter()

        self.maxDistance = 0.0
        self.maxNeighboringStar = 0

        settingFilePath = os.path.join(getConfigDir(), settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        # Configurate the criteria of neighboring stars
        starRadiusInPixel = self.settingFile.getSetting("starRadiusInPixel")
        spacingCoefficient = self.settingFile.getSetting("spacingCoef")
        maxNeighboringStar = self.settingFile.getSetting("maxNumOfNbrStar")
        self.configNbrCriteria(starRadiusInPixel, spacingCoefficient,
                               maxNeighboringStar=maxNeighboringStar)
Example #18
0
    def testGetAbsPath(self):

        filePath = "README.md"
        self.assertFalse(os.path.isabs(filePath))

        filePathAbs = ParamReader.getAbsPath(filePath, getModulePath())
        self.assertTrue(os.path.isabs(filePathAbs))
Example #19
0
    def __init__(self, algoDir):
        """Initialize the Algorithm class.

        Algorithm used to solve the transport of intensity equation to get
        normal/ annular Zernike polynomials.

        Parameters
        ----------
        algoDir : str
            Algorithm configuration directory.
        """

        self.algoDir = algoDir
        self.algoParamFile = ParamReader()

        self._inst = Instrument("")

        # Show the calculation message based on this value
        # 0 means no message will be showed
        self.debugLevel = 0

        # Image has the problem or not from the over-compensation
        self.caustic = False

        # Record the Zk coefficients in each outer-loop iteration
        # The actual total outer-loop iteration time is Num_of_outer_itr + 1
        self.converge = np.array([])

        # Current number of outer-loop iteration
        self.currentItr = 0

        # Record the coefficients of normal/ annular Zernike polynomials after
        # z4 in unit of nm
        self.zer4UpNm = np.array([])

        # Converged wavefront.
        self.wcomp = np.array([])

        # Calculated wavefront in previous outer-loop iteration.
        self.West = np.array([])

        # Converged Zk coefficients
        self.zcomp = np.array([])

        # Calculated Zk coefficients in previous outer-loop iteration
        self.zc = np.array([])

        # Padded mask for use at the offset planes
        self.pMask = None

        # Non-padded mask corresponding to aperture
        self.cMask = None

        # Change the dimension of mask for fft to use
        self.pMaskPad = None
        self.cMaskPad = None
Example #20
0
    def __init__(self):
        """Initialization of telescope facade class.

        This class uses the facade pattern that the high level class telescope
        helps to write the perturbations of camera, M1M3, and M2 into the
        PhoSim by the interface to PhoSim.
        """

        # Telescope setting file
        settingFilePath = os.path.join(getConfigDir(), "teleSetting.yaml")
        self._teleSettingFile = ParamReader(filePath=settingFilePath)

        # Camera subsystem
        self.cam = None

        # M1M3 subsystem
        self.m1m3 = None

        # M2 subsystem
        self.m2 = None

        # PhoSim communication
        self.phoSimCommu = PhosimCommu()

        # Telescope aggregated DOF in um
        # This value will be put back to PhoSim to do the perturbation
        numOfDof = self.getNumOfDof()
        self.dofInUm = np.zeros(numOfDof)

        # Telescopt survery parameters
        defocalDist = self.getDefaultDefocalDist()
        self.surveyParam = {"instName": "lsst",
                            "defocalDistInMm": defocalDist,
                            "obsId": 9999,
                            "filterType": FilterType.REF,
                            "boresight": (0, 0),
                            "zAngleInDeg": 0,
                            "rotAngInDeg": 0}

        # Sensors on the telescope
        self.sensorOn = {"sciSensorOn": True,
                         "wfSensorOn": True,
                         "guidSensorOn": False}
Example #21
0
    def setUp(self):

        testDir = os.path.join(getModulePath(), "tests")
        self.configDir = os.path.join(testDir, "testData")
        self.fileName = "testConfigFile.yaml"

        filePath = os.path.join(self.configDir, self.fileName)
        self.paramReader = ParamReader(filePath=filePath)

        self.testTempDir = os.path.join(testDir, "tmp")
        self._makeDir(self.testTempDir)
Example #22
0
    def _getOffAxisCorrSingle(self, confFile):
        """Get the image-related parameters for the off-axis distortion by the
        linear approximation with a series of fitted parameters with LSST
        ZEMAX model.

        Parameters
        ----------
        confFile : str
            Path of configuration file.

        Returns
        -------
        numpy.ndarray
            Coefficients for the off-axis distortion based on the linear
            response.
        float
            Defocal distance in m.
        """

        fieldDist = self._getFieldDistFromOrigin(minDist=0.0)

        # Read the configuration file
        paramReader = ParamReader()
        paramReader.setFilePath(confFile)
        cdata = paramReader.getMatContent()

        # Record the offset (defocal distance)
        offset = cdata[0, 0]

        # Take the reference parameters
        c = cdata[:, 1:]

        # Get the ruler, which is the distance to center
        # ruler is between 1.51 and 1.84 degree here
        ruler = np.sqrt(c[:, 0]**2 + c[:, 1]**2)

        # Get the fitted parameters for off-axis correction by linear
        # approximation
        corr_coeff = self._linearApprox(fieldDist, ruler, c[:, 2:])

        return corr_coeff, offset
Example #23
0
    def _getOffAxisCorrSingle(self, confFile):
        """Get the image-related pamameters for the off-axis distortion by the
        linear approximation with a series of fitted parameters with LSST
        ZEMAX model.

        Parameters
        ----------
        confFile : str
            Path of configuration file.

        Returns
        -------
        numpy.ndarray
            Coefficients for the off-axis distortion based on the linear
            response.
        float
            Defocal distance in m.
        """

        fieldDist = self._getFieldDistFromOrigin(minDist=0.0)

        # Read the configuration file
        paramReader = ParamReader()
        paramReader.setFilePath(confFile)
        cdata = paramReader.getMatContent()

        # Record the offset (defocal distance)
        offset = cdata[0, 0]

        # Take the reference parameters
        c = cdata[:, 1:]

        # Get the ruler, which is the distance to center
        # ruler is between 1.51 and 1.84 degree here
        ruler = np.sqrt(c[:, 0]**2 + c[:, 1]**2)

        # Get the fitted parameters for off-axis correction by linear
        # approximation
        corr_coeff = self._linearApprox(fieldDist, ruler, c[:, 2:])

        return corr_coeff, offset
Example #24
0
    def __init__(self, astWcsSol, camType, isrDir, settingFileName="default.yaml"):
        """Construct an WEP calculation object.

        Parameters
        ----------
        astWcsSol : AstWcsSol
            AST world coordinate system (WCS) solution.
        camType : enum 'CamType'
            Camera type.
        isrDir : str
            Instrument signature remocal (ISR) directory. This directory will
            have the input and output that the data butler needs.
        settingFileName : str, optional
            Setting file name. (the default is "default.yaml".)
        """

        super().__init__()

        # This attribute is just a stakeholder here since there is no detail of
        # AST WCS solution yet
        self.astWcsSol = astWcsSol

        # ISR directory that the data butler uses
        self.isrDir = isrDir

        # Boresight infomation
        self.raInDeg = 0.0
        self.decInDeg = 0.0

        # Sky rotation angle
        self.rotSkyPos = 0.0

        # Sky information file for the temporary use
        self.skyFile = ""

        # Default setting file
        settingFilePath = os.path.join(getConfigDir(), settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        # Configure the WEP controller
        self.wepCntlr = self._configWepController(camType, settingFileName)
Example #25
0
    def __init__(self, instDir):
        """Instrument class for wavefront estimation.

        Parameters
        ----------
        instDir : str
            Instrument configuration directory.
        """

        self.instDir = instDir
        self.instName = ""
        self.dimOfDonutImg = 0
        self.announcedDefocalDisInMm = 0.0

        self.instParamFile = ParamReader()
        self.maskParamFile = ParamReader()

        self.xSensor = np.array([])
        self.ySensor = np.array([])

        self.xoSensor = np.array([])
        self.yoSensor = np.array([])
Example #26
0
    def __init__(self, instDir):
        """Instrument class for wavefront estimation.

        Parameters
        ----------
        instDir : str
            Instrument configuration directory.
        """

        self.instDir = instDir
        self.instName = ""
        self.dimOfDonut = 0
        self.announcedDefocalDisInMm = 0.0

        self.instParamFile = ParamReader()
        self.maskParamFile = ParamReader()

        self.xSensor = np.array([])
        self.ySensor = np.array([])

        self.xoSensor = np.array([])
        self.yoSensor = np.array([])
Example #27
0
    def __init__(self, sensorNameToIdFileName="sensorNameToId.yaml"):
        """Construct a MapSensorNameAndId object.

        Parameters
        ----------
        sensorNameToIdFileName : str, optional
            Configuration file name to map sensor name and Id. (the default is
            "sensorNameToId.yaml".)
        """

        sensorNameToIdFilePath = os.path.join(getConfigDir(),
                                              sensorNameToIdFileName)
        self._sensorNameToIdFile = ParamReader(filePath=sensorNameToIdFilePath)
Example #28
0
    def testSaveSettingWithoutFilePath(self):

        filePath = self._saveSettingFile()
        paramReader = ParamReader(filePath=filePath)

        paramReader.saveSetting()
        self.assertEqual(paramReader.getFilePath(), filePath)

        # Check the values are saved actually
        self.assertEqual(paramReader.getSetting("znmax"), 22)

        keysInContent = paramReader.getContent().keys()
        self.assertTrue("dofIdx" in keysInContent)
        self.assertTrue("zn3Idx" in keysInContent)

        self.assertEqual(paramReader.getSetting("zn3Idx"), [1] * 19)
Example #29
0
    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
Example #30
0
    def __init__(self, astWcsSol, camType, isrDir,
                 settingFileName="default.yaml"):
        """Construct an WEP calculation object.

        Parameters
        ----------
        astWcsSol : AstWcsSol
            AST world coordinate system (WCS) solution.
        camType : enum 'CamType'
            Camera type.
        isrDir : str
            Instrument signature remocal (ISR) directory. This directory will
            have the input and output that the data butler needs.
        settingFileName : str, optional
            Setting file name. (the default is "default.yaml".)
        """

        super().__init__()

        # This attribute is just a stakeholder here since there is no detail of
        # AST WCS solution yet
        self.astWcsSol = astWcsSol

        # ISR directory that the data butler uses
        self.isrDir = isrDir

        # Number of processors for WEP to use
        # This is just a stakeholder at this moment
        self.numOfProc = 1

        # Boresight infomation
        self.raInDeg = 0.0
        self.decInDeg = 0.0

        # Sky rotation angle
        self.rotSkyPos = 0.0

        # Sky information file for the temporary use
        self.skyFile = ""

        # Default setting file
        settingFilePath = os.path.join(getConfigDir(), settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        # Configure the WEP controller
        self.wepCntlr = self._configWepController(camType, settingFileName)
Example #31
0
    def __init__(self):
        """Initiate the M2 simulator class."""

        # M2 setting file
        configDir = os.path.join(getConfigDir(), "M2")
        settingFilePath = os.path.join(configDir, "m2Setting.yaml")
        self._m2SettingFile = ParamReader(filePath=settingFilePath)

        # Inner and outer radius of M2 mirror in m
        radiusInner = self._m2SettingFile.getSetting("radiusInner")
        radiusOuter = self._m2SettingFile.getSetting("radiusOuter")

        super(M2Sim, self).__init__(radiusInner, radiusOuter, configDir)

        # Mirror surface bending mode grid file
        self._gridFile = ParamReader()

        # Mirror FEA model with gradient temperature data
        self._feaFile = ParamReader()

        self._config("M2_1um_force.yaml", "", "M2_1um_grid.yaml",
                     "M2_GT_FEA.yaml")
Example #32
0
    def __init__(self):
        """Initiate the M1M3 simulator class."""

        # M2 setting file
        configDir = os.path.join(getConfigDir(), "M1M3")
        settingFilePath = os.path.join(configDir, "m1m3Setting.yaml")
        self._m1m3SettingFile = ParamReader(filePath=settingFilePath)

        # Inner and outer radius of M1 mirror in m
        radiusM1Inner = self._m1m3SettingFile.getSetting("radiusM1Inner")
        radiusM1Outer = self._m1m3SettingFile.getSetting("radiusM1Outer")

        # Inner and outer radius of M3 mirror in m
        radiusM3Inner = self._m1m3SettingFile.getSetting("radiusM3Inner")
        radiusM3Outer = self._m1m3SettingFile.getSetting("radiusM3Outer")

        super(M1M3Sim,
              self).__init__((radiusM1Inner, radiusM3Inner),
                             (radiusM1Outer, radiusM3Outer), configDir)

        # Mirror surface bending mode grid file
        self._gridFile = ParamReader()

        # FEA model file
        self._feaFile = ParamReader()

        # FEA model data in zenith angle
        self._feaZenFile = ParamReader()

        # FEA model data in horizontal angle
        self._feaHorFile = ParamReader()

        # Actuator forces along zenith direction
        self._forceZenFile = ParamReader()

        # Actuator forces along horizon direction
        self._forceHorFile = ParamReader()

        # Influence matrix of actuator forces
        self._forceInflFile = ParamReader()

        self._config("M1M3_1um_156_force.yaml", "M1M3_LUT.yaml",
                     "M1M3_1um_156_grid.yaml", "M1M3_thermal_FEA.yaml",
                     "M1M3_dxdydz_zenith.yaml", "M1M3_dxdydz_horizon.yaml",
                     "M1M3_force_zenith.yaml", "M1M3_force_horizon.yaml",
                     "M1M3_influence_256.yaml")
Example #33
0
class TeleFacade(object):

    def __init__(self):
        """Initialization of telescope facade class.

        This class uses the facade pattern that the high level class telescope
        helps to write the perturbations of camera, M1M3, and M2 into the
        PhoSim by the interface to PhoSim.
        """

        # Telescope setting file
        settingFilePath = os.path.join(getConfigDir(), "teleSetting.yaml")
        self._teleSettingFile = ParamReader(filePath=settingFilePath)

        # Camera subsystem
        self.cam = None

        # M1M3 subsystem
        self.m1m3 = None

        # M2 subsystem
        self.m2 = None

        # PhoSim communication
        self.phoSimCommu = PhosimCommu()

        # Telescope aggregated DOF in um
        # This value will be put back to PhoSim to do the perturbation
        numOfDof = self.getNumOfDof()
        self.dofInUm = np.zeros(numOfDof)

        # Telescopt survery parameters
        defocalDist = self.getDefaultDefocalDist()
        self.surveyParam = {"instName": "lsst",
                            "defocalDistInMm": defocalDist,
                            "obsId": 9999,
                            "filterType": FilterType.REF,
                            "boresight": (0, 0),
                            "zAngleInDeg": 0,
                            "rotAngInDeg": 0}

        # Sensors on the telescope
        self.sensorOn = {"sciSensorOn": True,
                         "wfSensorOn": True,
                         "guidSensorOn": False}

    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.dofInUm

    def getNumOfDof(self):
        """Get the number of DOF in the setting file.

        DOF: Degree of freedom.

        Returns
        -------
        int
            Number of DOF.
        """

        return int(self._teleSettingFile.getSetting("numOfDof"))

    def getDefaultDefocalDist(self):
        """Get the default defocal distance in mm.

        Returns
        -------
        float
            Default defocal distance in mm.
        """

        return self._teleSettingFile.getSetting("defocalDist")

    def getSurfGridN(self):
        """Get the number of grid of surface

        Returns
        -------
        int
            Number of grid of surface.
        """

        return int(self._teleSettingFile.getSetting("surfaceGridN"))

    def getCamMjd(self):
        """Get the camera MJD.

        Returns
        -------
        float
            Camera MJD.
        """

        return self._teleSettingFile.getSetting("cameraMJD")

    def getRefWaveLength(self):
        """Get the reference wavelength in nm.

        Returns
        -------
        int
            Reference wavelength in nm.
        """

        return self._teleSettingFile.getSetting("wavelengthInNm")

    def getDefocalDistInMm(self):
        """Get the defocal distance in mm.

        Returns
        -------
        float
            defocal distance in mm.
        """

        return self.surveyParam["defocalDistInMm"]

    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._setSurveyParamItem("obsId", obsId, int)
        self._setSurveyParamItem("filterType", filterType, FilterType)
        self._setSurveyParamItem("boresight", boresight, tuple)
        self._setSurveyParamItem("zAngleInDeg", zAngleInDeg, (int, float))
        self._setSurveyParamItem("rotAngInDeg", rotAngInDeg, (int, float))

    def _setSurveyParamItem(self, dictKeyName, varValue, varType):
        """Set the item in the dictionary of survey parameters.

        Parameters
        ----------
        dictKeyName : str
            Name of dictionary key.
        varValue : int, tuple, float, or enum 'FilterType'
            Variable value.
        varType: int, tuple, float, or enum 'FilterType'
            Variable type.
        """

        if (isinstance(varValue, varType)):
            self.surveyParam[dictKeyName] = varValue

    def setSensorOn(self, sciSensorOn=True, wfSensorOn=True,
                    guidSensorOn=False):
        """Set the sensor on.

        Parameters
        ----------
        sciSensorOn : bool, optional
            Scientific sensors are on. (the default is True.)
        wfSensorOn : bool, optional
            Wavefront sensors are on. (the default is True.)
        guidSensorOn : bool, optional
            Guider sensors are on. (the default is False.)
        """

        self.sensorOn["sciSensorOn"] = sciSensorOn
        self.sensorOn["wfSensorOn"] = wfSensorOn
        self.sensorOn["guidSensorOn"] = guidSensorOn

    def runPhoSim(self, argString):
        """

        Run the PhoSim program.

        Arguments:
            argString {[str]} -- Arguments for PhoSim.
        """

        self.phoSimCommu.runPhoSim(argstring=argString)

    def getPhoSimArgs(self, instFilePath, extraCommandFile=None, numPro=1,
                      numThread=1, outputDir=None, sensorName=None,
                      e2ADC=1, logFilePath=None):
        """Get the arguments needed to run the PhoSim.

        Parameters
        ----------
        instanceFile : str
            Instance catalog file.
        extraCommandFile : str, optional
            Command file to modify the default physics. (the default is None.)
        numProc : int, optional
             Number of processors. (the default is 1.)
        numThread : int, optional
            Number of threads. (the default is 1.)
        outputDir : str, optional
            Output image directory. (the default is None.)
        sensorName : str, optional
            Sensor chip specification (e.g., all, R22_S11, "R22_S11|R22_S12").
            (the default is None.)
        e2ADC : int, optional
            Whether to generate amplifier images (1 = true, 0 = false). (the
            default is 1.)
        logFilePath : str, optional
            Log file path of PhoSim calculation. (the default is None.)

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

        instName = self.surveyParam["instName"]
        argString = self.phoSimCommu.getPhoSimArgs(
            instFilePath, extraCommandFile=extraCommandFile, numProc=numPro,
            numThread=numThread, outputDir=outputDir, instrument=instName,
            sensorName=sensorName, e2ADC=e2ADC, logFilePath=logFilePath)

        return argString

    def setInstName(self, camType, defocalDist=None):
        """Set the instrument name.

        Parameters
        ----------
        camType : enum 'CamType' in lsst.ts.wep.Utility
            Camera type.
        defocalDist : float, optional
            Defocal distance in mm. If None, the default value will be used.
            (the default is None.)

        Raises
        ------
        ValueError
            Defocal distance can not be <= 0.
        """

        if (defocalDist is None):
            defocalDist = self.getDefaultDefocalDist()

        if (defocalDist <= 0):
            raise ValueError("Defocal distance can not be <= 0.")

        self.surveyParam["defocalDistInMm"] = defocalDist

        self._setInstNameBasedOnCamType(camType)

    def _setInstNameBasedOnCamType(self, camType):
        """Set the instrument name based on the camera type.

        Parameters
        ----------
        camType : enum 'CamType' in lsst.ts.wep.Utility
            Camera type.

        Raises
        ------
        ValueError
            This camera type is not supported yet.
        """

        if camType in (CamType.LsstCam, CamType.LsstFamCam):
            instName = "lsst"
        elif (camType == CamType.ComCam):
            instName = "lsst"
            warnings.warn("Use 'lsst' instead of 'comcam' in PhoSim.",
                          category=UserWarning)
        else:
            raise ValueError("This camera type (%s) is not supported yet."
                             % camType)

        self.surveyParam["instName"] = instName

    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._checkNumOfDof(dofInUm)
        self.dofInUm = np.array(dofInUm, dtype=float)

    def _checkNumOfDof(self, dof):
        """Check the number of DOF:

        DOF: Degree of freedom.

        Parameters
        ----------
        dof : list or numpy.ndarray
            DOF.

        Raises
        ------
        ValueError
            The size of DOF should be 50.
        """

        numOfDof = self.getNumOfDof()

        if (len(dof) != numOfDof):
            raise ValueError("The size of DOF should be %d." % numOfDof)

    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._checkNumOfDof(dofInUm)
        self.dofInUm += np.array(dofInUm, dtype=float)

    def addSubSys(self, addCam=False, addM1M3=False, addM2=False):
        """Add the sub systems to do the perturbation.

        Parameters
        ----------
        addCam : bool, optional
            Add the camera. (the default is False.)
        addM1M3 : bool, optional
            Add the M1M3 mirror. (the default is False.)
        addM2 : bool, optional
            Add the M2 mirror. (the default is False.)
        """

        if (addCam):
            self.cam = CamSim()

        if (addM1M3):
            self.m1m3 = M1M3Sim()

        if (addM2):
            self.m2 = M2Sim()

    def setPhoSimDir(self, phosimDir):
        """Set the directory of PhoSim.

        Parameters
        ----------
        phosimDir : str
            Directory of PhoSim.
        """

        self.phoSimCommu.setPhoSimDir(phosimDir)

    def writeAccDofFile(self, outputFileDir, dofFileName="pert.mat"):
        """Write the accumulated degree of freedom (DOF) in um to file.

        Parameters
        ----------
        outputFileDir : str
            Output file directory.
        dofFileName : str, optional
            DOF file name. (the default is "pert.mat".)

        Returns
        -------
        str
            DOF file path.
        """

        dofFilePath = os.path.join(outputFileDir, dofFileName)
        np.savetxt(dofFilePath, self.dofInUm)

        return dofFilePath

    def writeCmdFile(self, cmdFileDir, cmdSettingFile=None, pertFilePath=None,
                     cmdFileName="taskPert.cmd"):
        """Write the physical command file.

        OPD: Optical path difference.

        Parameters
        ----------
        cmdFileDir : str
            Directory to the OPD command file.
        cmdSettingFile : str, optional
            Physical command setting file path. (the default is None.)
        pertFilePath : str, optional
            Subsystem perturbation command file path. (the default is None.)
        cmdFileName : str, optional
            Command file name. (the default is "taskPert.cmd".)

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

        # Command file path
        cmdFilePath = os.path.join(cmdFileDir, cmdFileName)
        self.phoSimCommu.writeToFile(cmdFilePath, content="", mode="w")

        # Write the physical setting
        if (cmdSettingFile is not None):
            self.phoSimCommu.writeToFile(cmdFilePath,
                                         sourceFile=cmdSettingFile)

        # Add the subsystem perturbation
        if (pertFilePath is not None):
            self.phoSimCommu.writeToFile(cmdFilePath, sourceFile=pertFilePath)

        return cmdFilePath

    def writeStarInstFile(self, instFileDir, skySim, simSeed=1000,
                          sedName="sed_flat.txt", instSettingFile=None,
                          instFileName="star.inst"):
        """Write the star instance file.

        Parameters
        ----------
        instFileDir : str
            Directory to instance file.
        skySim : SkySim
            SkySim object.
        simSeed : int, optional
            Random number seed. (the default is 1000.)
        sedName : str, optional
            The name of the SED file with a file path that is relative to the
            data directory in PhoSim. (the default is "sed_flat.txt".)
        instSettingFile : str optional
            Instance setting file. (the default is None.)
        instFileName : str, optional
            Star instance file name. (the default is "star.inst".)

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

        # Instance file path
        instFilePath = os.path.join(instFileDir, instFileName)

        # Get the filter ID in PhoSim
        aFilterId = self._getFilterIdInPhoSim()

        # Write the default instance setting
        obsId = self.surveyParam["obsId"]

        boresight = self.surveyParam["boresight"]
        ra = boresight[0]
        dec = boresight[1]

        rot = self.surveyParam["rotAngInDeg"]
        mjd = self.getCamMjd()

        self.phoSimCommu.getStarInstance(
            obsId, aFilterId, ra=ra, dec=dec, rot=rot, mjd=mjd,
            simSeed=simSeed, filePath=instFilePath)

        if (instFilePath is not None):
            self.phoSimCommu.writeToFile(instFilePath,
                                         sourceFile=instSettingFile)

        # Write the telescope accumulated degree of freedom (DOF)
        content = self.phoSimCommu.doDofPert(self.dofInUm)

        # Set the camera configuration
        sciSensorOn = self.sensorOn["sciSensorOn"]
        wfSensorOn = self.sensorOn["wfSensorOn"]
        guidSensorOn = self.sensorOn["guidSensorOn"]
        content += self.phoSimCommu.doCameraConfig(
            sciSensorOn=sciSensorOn, wfSensorOn=wfSensorOn,
            guidSensorOn=guidSensorOn)

        # Use the SED file of single wavelendth if the reference filter is
        # used.
        filterType = self.surveyParam["filterType"]
        if (filterType == FilterType.REF):
            self._writeSedFileIfPhoSimDirSet()
            sedName = "sed_%s.txt" % int(self.getRefWaveLength())

        # Write the star source
        starId = skySim.getStarId()
        ra, decl = skySim.getRaDecInDeg()
        mag = skySim.getStarMag()
        for idx in range(len(starId)):
            content += self.phoSimCommu.generateStar(
                starId[idx], ra[idx], decl[idx], mag[idx], sedName)
        self.phoSimCommu.writeToFile(instFilePath, content=content)

        return instFilePath

    def _getFilterIdInPhoSim(self):
        """Get the active filter Id used in PhoSim.

        Returns
        -------
        int
            Active filter ID in PhoSim.
        """

        filterType = self.surveyParam["filterType"]
        mappedFilterType = mapFilterRefToG(filterType)

        return self.phoSimCommu.getFilterId(mappedFilterType)

    def _writeSedFileIfPhoSimDirSet(self):
        """Write the SED file if the PhoSim directory is set.

        SED: Spectral energy distribution.
        """

        if os.path.isdir(self.phoSimCommu.getPhoSimDir()):
            self.phoSimCommu.writeSedFile(self.getRefWaveLength())
        else:
            warnings.warn("No inspection of SED file for no PhoSim path.",
                          category=UserWarning)

    def writeOpdInstFile(self, instFileDir, opdMetr, instSettingFile=None,
                         instFileName="opd.inst"):
        """Write the optical path difference (OPD) instance file.

        Parameters
        ----------
        instFileDir : str
            Directory to instance file.
        opdMetr : OpdMetrology
            OpdMetrology object.
        instSettingFile : str, optional
            Instance setting file. (the default is None.)
        instFileName : str, optional
            OPD instance file name. (the default is "opd.inst".)

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

        # Instance file path
        instFilePath = os.path.join(instFileDir, instFileName)

        # Get the observation ID
        obsId = self.surveyParam["obsId"]

        # Get the filter ID in PhoSim
        aFilterId = self._getFilterIdInPhoSim()

        # Add the sky information
        boresight = self.surveyParam["boresight"]
        ra = boresight[0]
        dec = boresight[1]

        rot = self.surveyParam["rotAngInDeg"]

        # Write the default instance setting
        self.phoSimCommu.getOpdInstance(obsId, aFilterId, ra=ra, dec=dec,
                                        rot=rot, filePath=instFilePath)
        if (instSettingFile is not None):
            self.phoSimCommu.writeToFile(instFilePath,
                                         sourceFile=instSettingFile)

        # Write the telescope accumulated degree of freedom (DOF)
        content = self.phoSimCommu.doDofPert(self.dofInUm)

        # Write the OPD source
        fieldX, fieldY = opdMetr.getFieldXY()
        for idx in range(len(fieldX)):
            content += self.phoSimCommu.generateOpd(
                idx, fieldX[idx], fieldY[idx], self.getRefWaveLength())
        self.phoSimCommu.writeToFile(instFilePath, content=content)

        # Write the OPD SED file if necessary
        self._writeSedFileIfPhoSimDirSet()

        return instFilePath

    def writePertBaseOnConfigFile(self, pertCmdFileDir, seedNum=None,
                                  m1m3ForceError=0.05, saveResMapFig=False,
                                  pertCmdFileName="pert.cmd"):
        """Write the perturbation command file based on the telescope
        configuration file.

        Parameters
        ----------
        pertCmdFileDir : str
            Directory to the pertubation command file.
        seedNum : int, optional
            Random seed number. If the value is not None, the M1M3 mirror
            will generate a random surface error. (the default is None.)
        m1m3ForceError : float, optional
            Ratio of actuator force error. (the default is 0.05.)
        saveResMapFig : bool, optional
            Save the mirror surface residue map or not. (the default is False.)
        pertCmdFileName : str, optional
            Perturbation command file name. (the default is "pert.cmd".)

        Returns
        -------
        str
            Perturbation command file path.
        """

        # Get the perturbation of subsystems
        content = ""
        if (self.m1m3 is not None):
            m1ResFileName = "M1res.txt"
            m3ResFileName = "M3res.txt"
            m1m3ZcFileName = "M1M3zlist.txt"

            m1ResFilePath = os.path.join(pertCmdFileDir, m1ResFileName)
            m3ResFilePath = os.path.join(pertCmdFileDir, m3ResFileName)
            m1m3ZcFilePath = os.path.join(pertCmdFileDir, m1m3ZcFileName)

            content = self._addPertM1M3(m1ResFilePath, m3ResFilePath,
                                        m1m3ZcFilePath, content,
                                        m1m3ForceError, seedNum=seedNum)

        if (self.m2 is not None):
            m2ResFileName = "M2res.txt"
            m2ZcFileName = "M2zlist.txt"

            m2ResFilePath = os.path.join(pertCmdFileDir, m2ResFileName)
            m2ZcFilePath = os.path.join(pertCmdFileDir, m2ZcFileName)

            content = self._addPertM2(m2ResFilePath, m2ZcFilePath, content)

        if (self.cam is not None):
            content = self._addPertCam(content)

        # Write the perturbation command to file
        pertCmdFilePath = os.path.join(pertCmdFileDir, pertCmdFileName)
        self.phoSimCommu.writeToFile(pertCmdFilePath, content=content,
                                     mode="w")

        # Save the mirror residue map if necessary
        if (saveResMapFig):

            if (self.m1m3 is not None):
                self._saveM1M3ResMapFig(m1ResFilePath, m3ResFilePath)

            if (self.m2 is not None):
                self._saveM2ResMapFig(m2ResFilePath)

        return pertCmdFilePath

    def _addPertM1M3(self, m1ResFilePath, m3ResFilePath, m1m3ZcFilePath,
                     content, m1m3ForceError, seedNum=None):
        """Add the perturbation of M1M3.

        Parameters
        ----------
        m1ResFilePath : str
            M1 residue file path.
        m3ResFilePath : str
            M3 residue file path.
        m1m3ZcFilePath : str
            M1M3 fitted zk file path.
        content : str
            Perturbation without M1M3.
        m1m3ForceError : float
            Ratio of actuator force error.
        seedNum : int, optional
            Random seed number. If the value is not None, the M1M3 mirror
            will generate a random surface error. (the default is None.)

        Returns
        -------
        str
            Perturbation with M1M3.
        """

        # Do the gravity correction
        zAngleInRad = self._getZenAngleInRad()
        printthzInM = self.m1m3.getPrintthz(zAngleInRad)

        # Add the surface error if necessary
        randSurfInM = None
        if (seedNum is not None):
            randSurfInM = self.m1m3.genMirSurfRandErr(
                zAngleInRad, m1m3ForceError=m1m3ForceError,
                seedNum=seedNum)

        # Do the temperature correction
        m1m3TBulk = self._teleSettingFile.getSetting("m1m3TBulk")
        m1m3TxGrad = self._teleSettingFile.getSetting("m1m3TxGrad")
        m1m3TyGrad = self._teleSettingFile.getSetting("m1m3TyGrad")
        m1m3TzGrad = self._teleSettingFile.getSetting("m1m3TzGrad")
        m1m3TrGrad = self._teleSettingFile.getSetting("m1m3TrGrad")
        tempCorrInUm = self.m1m3.getTempCorr(m1m3TBulk, m1m3TxGrad,
                                             m1m3TyGrad, m1m3TzGrad,
                                             m1m3TrGrad)

        # Set the mirror surface in mm
        if (randSurfInM is not None):
            mirrorSurfInUm = (printthzInM + randSurfInM) * 1e6 + \
                tempCorrInUm
        else:
            mirrorSurfInUm = printthzInM * 1e6 + tempCorrInUm
        self.m1m3.setSurfAlongZ(mirrorSurfInUm)

        resFile = [m1ResFilePath, m3ResFilePath]
        surfaceGridN = self.getSurfGridN()
        self.m1m3.writeMirZkAndGridResInZemax(
            resFile=resFile, surfaceGridN=surfaceGridN,
            writeZcInMnToFilePath=m1m3ZcFilePath)

        # Get the Zk in mm
        zkInMm = np.loadtxt(m1m3ZcFilePath)

        # Do the surface perturbation
        surfList = [SurfaceType.M1, SurfaceType.M3]
        surfIdList = []

        contentWithPert = content
        for ii in range(len(surfList)):
            surf = surfList[ii]
            surfId = self.phoSimCommu.getSurfaceId(surf)
            contentWithPert += self.phoSimCommu.doSurfPert(surfId, zkInMm)

            # Collect the surface ID
            surfIdList.append(surfId)

            # Do the surface residue map perturbation
            contentWithPert += self.phoSimCommu.doSurfMapPert(surfId,
                                                              resFile[ii], 1)

        # Do the surface linkage
        contentWithPert += self.phoSimCommu.doSurfLink(surfIdList[1],
                                                       surfIdList[0])

        return contentWithPert

    def _getZenAngleInRad(self):
        """Get the zenith angle in radian.

        Returns
        -------
        float
            Zenith angle in radian.
        """

        zAngleInDeg = self.surveyParam["zAngleInDeg"]
        zAngleInRad = np.deg2rad(zAngleInDeg)

        return zAngleInRad

    def _addPertM2(self, m2ResFilePath, m2ZcFilePath, content):
        """Add the perturbation of M2.

        Parameters
        ----------
        m2ResFilePath : str
            M2 residue file path.
        m2ZcFilePath : str
            M2 fitted zk file path.
        content : str
            Perturbation without M2.

        Returns
        -------
        str
            Perturbation with M2.
        """

        # Do the gravity correction
        zAngleInRad = self._getZenAngleInRad()
        printthzInUm = self.m2.getPrintthz(zAngleInRad)

        # Do the temperature correction
        m2TzGrad = self._teleSettingFile.getSetting("m2TzGrad")
        m2TrGrad = self._teleSettingFile.getSetting("m2TrGrad")
        tempCorrInUm = self.m2.getTempCorr(m2TzGrad, m2TrGrad)

        # Set the mirror surface in mm
        mirrorSurfInUm = printthzInUm + tempCorrInUm
        self.m2.setSurfAlongZ(mirrorSurfInUm)

        surfaceGridN = self.getSurfGridN()
        self.m2.writeMirZkAndGridResInZemax(
            resFile=m2ResFilePath, surfaceGridN=surfaceGridN,
            writeZcInMnToFilePath=m2ZcFilePath)

        # Get the Zk in mm
        zkInMm = np.loadtxt(m2ZcFilePath)

        # Do the surface perturbation
        surfId = self.phoSimCommu.getSurfaceId(SurfaceType.M2)

        contentWithPert = content
        contentWithPert += self.phoSimCommu.doSurfPert(surfId, zkInMm)

        # Do the surface residue map perturbation
        contentWithPert += self.phoSimCommu.doSurfMapPert(
            surfId, m2ResFilePath, 1)

        return contentWithPert

    def _addPertCam(self, content):
        """Add the perturbation of camera.

        Parameters
        ----------
        content : str
            Perturbation without camera.

        Returns
        -------
        str
            Perturbation with camera.
        """

        # Set the camera rotation angle
        rotAngInDeg = self.surveyParam["rotAngInDeg"]
        self.cam.setRotAngInDeg(rotAngInDeg)

        # Set the temperature information
        tempInDegC = self._teleSettingFile.getSetting("camTB")
        self.cam.setBodyTempInDegC(tempInDegC)

        # Add the perturbation of camera
        zAngleInRad = self._getZenAngleInRad()
        contentWithPert = content
        for distType in CamDistType:
            # Get the surface ID
            surfaceType = self._getPhoSimCamSurf(distType.name)
            surfId = self.phoSimCommu.getSurfaceId(surfaceType)

            # Do the perturbation
            zkInMm = self.cam.getCamDistortionInMm(zAngleInRad, distType)
            contentWithPert += self.phoSimCommu.doSurfPert(surfId, zkInMm)

        return contentWithPert

    def _saveM1M3ResMapFig(self, m1ResFilePath, m3ResFilePath):
        """Save the figure of M1M3 residue map.

        Parameters
        ----------
        m1ResFilePath : str
            M1 residue file path.
        m3ResFilePath : str
            M3 residue file path.
        """

        resFile = [m1ResFilePath, m3ResFilePath]

        writeToResMapFilePath1 = self._getImgPathFromDataFilePath(
            m1ResFilePath)
        writeToResMapFilePath3 = self._getImgPathFromDataFilePath(
            m3ResFilePath)
        writeToResMapFilePath = [writeToResMapFilePath1,
                                 writeToResMapFilePath3]

        self.m1m3.showMirResMap(
            resFile=resFile,
            writeToResMapFilePath=writeToResMapFilePath)

    def _getImgPathFromDataFilePath(self, dataFilePath, imgType=".png"):
        """Get the image path from the data file path.

        Parameters
        ----------
        dataFilePath : str
            Data file path.
        imgType : str, optional
            Image type (the default is ".png".)

        Returns
        -------
        str
            Image path.
        """

        imgPath = os.path.splitext(dataFilePath)[0] + imgType

        return imgPath

    def _saveM2ResMapFig(self, m2ResFilePath):
        """Save the figure of M2 residue map.

        Parameters
        ----------
        m2ResFilePath : str
            M2 residue file path.
        """

        writeToResMapFilePath = self._getImgPathFromDataFilePath(
            m2ResFilePath)

        self.m2.showMirResMap(
            resFile=m2ResFilePath,
            writeToResMapFilePath=writeToResMapFilePath)

    def _getPhoSimCamSurf(self, camSurfName):
        """Get the camera surface used in PhoSim.

        Parameters
        ----------
        camSurfName : str
            Camera surface name (e.g. L1S1zer).

        Returns
        -------
        enum 'SurfaceType'
            Camera surface type.

        Raises
        ------
        ValueError
            Can not get the camera surface name.
        """

        # Get the camera surface name
        m = re.match(r"(\AL\d)S(\d)zer", camSurfName)
        if (m is None):
            raise ValueError("Cannot get the camera surface name: %s."
                             % camSurfName)

        # Get the surface name used in PhoSim
        camPhoFaceDict = {"1": "F", "2": "B"}
        surfName = m.groups()[0] + camPhoFaceDict[m.groups()[1]]

        return mapSurfNameToEnum(surfName)
Example #34
0
class Instrument(object):

    def __init__(self, instDir):
        """Instrument class for wavefront estimation.

        Parameters
        ----------
        instDir : str
            Instrument configuration directory.
        """

        self.instDir = instDir
        self.instName = ""
        self.dimOfDonut = 0
        self.announcedDefocalDisInMm = 0.0

        self.instParamFile = ParamReader()
        self.maskParamFile = ParamReader()

        self.xSensor = np.array([])
        self.ySensor = np.array([])

        self.xoSensor = np.array([])
        self.yoSensor = np.array([])

    def config(self, camType, dimOfDonutOnSensor,
               announcedDefocalDisInMm=1.5,
               instParamFileName="instParam.yaml",
               maskMigrateFileName="maskMigrate.yaml"):
        """Do the configuration of Instrument.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        dimOfDonutOnSensor : int
            Dimension of image on sensor in pixel.
        announcedDefocalDisInMm : float
            Announced defocal distance in mm. It is noted that the defocal
            distance offset used in calculation might be different from this
            value. (the default is 1.5.)
        instParamFileName : str, optional
            Instrument parameter file name. (the default is "instParam.yaml".)
        maskMigrateFileName : str, optional
            Mask migration (off-axis correction) file name. (the default is
            "maskMigrate.yaml".)
        """

        self.instName = self._getInstName(camType)
        self.dimOfDonut = int(dimOfDonutOnSensor)
        self.announcedDefocalDisInMm = announcedDefocalDisInMm

        # Path of instrument param file
        instFileDir = self.getInstFileDir()
        instParamFilePath = os.path.join(instFileDir, instParamFileName)
        self.instParamFile.setFilePath(instParamFilePath)

        # Path of mask off-axis correction file
        maskParamFilePath = os.path.join(instFileDir, maskMigrateFileName)
        self.maskParamFile.setFilePath(maskParamFilePath)

        self._setSensorCoor()
        self._setSensorCoorAnnular()

    def _getInstName(self, camType):
        """Get the instrument name.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.

        Returns
        -------
        str
            Instrument name.

        Raises
        ------
        ValueError
            Camera type is not supported.
        """

        if (camType == CamType.LsstCam):
            return "lsst"
        elif (camType == CamType.ComCam):
            return "comcam"
        else:
            raise ValueError("Camera type (%s) is not supported." % camType)

    def getInstFileDir(self):
        """Get the instrument parameter file directory.

        Returns
        -------
        str
            Instrument parameter file directory.
        """

        return os.path.join(self.instDir, self.instName)

    def _setSensorCoor(self):
        """Set the sensor coordinate."""

        ySensorGrid, xSensorGrid = np.mgrid[
            -(self.dimOfDonut/2-0.5):(self.dimOfDonut/2 + 0.5),
            -(self.dimOfDonut/2-0.5):(self.dimOfDonut/2 + 0.5)]

        sensorFactor = self.getSensorFactor()
        denominator = self.dimOfDonut / 2 / sensorFactor

        self.xSensor = xSensorGrid / denominator
        self.ySensor = ySensorGrid / denominator

    def _setSensorCoorAnnular(self):
        """Set the sensor coordinate with the annular aperature."""

        self.xoSensor = self.xSensor.copy()
        self.yoSensor = self.ySensor.copy()

        # Get the position index that is out of annular aperature range
        obscuration = self.getObscuration()
        r2Sensor = self.xSensor**2 + self.ySensor**2
        idx = (r2Sensor > 1) | (r2Sensor < obscuration**2)

        # Define the value to be NaN if it is not in pupul
        self.xoSensor[idx] = np.nan
        self.yoSensor[idx] = np.nan

    def setAnnDefocalDisInMm(self, annDefocalDisInMm):
        """Set the announced defocal distance in mm.

        Parameters
        ----------
        annDefocalDisInMm : float
            Announced defocal distance in mm.
        """

        self.announcedDefocalDisInMm = annDefocalDisInMm

    def getAnnDefocalDisInMm(self):
        """Get the announced defocal distance in mm.

        Returns
        -------
        float
            Announced defocal distance in mm.
        """

        return self.announcedDefocalDisInMm

    def getInstFilePath(self):
        """Get the instrument parameter file path.

        Returns
        -------
        str
            Instrument parameter file path.
        """

        return self.instParamFile.getFilePath()

    def getMaskOffAxisCorr(self):
        """Get the mask off-axis correction.

        Returns
        -------
        numpy.ndarray
            Mask off-axis correction.
        """

        return self.maskParamFile.getMatContent()

    def getDimOfDonutOnSensor(self):
        """Get the dimension of donut's size on sensor in pixel.

        Returns
        -------
        int
            Dimension of donut's size on sensor in pixel.
        """

        return self.dimOfDonut

    def getObscuration(self):
        """Get the obscuration.

        Returns
        -------
        float
            Obscuration.
        """

        return self.instParamFile.getSetting("obscuration")

    def getFocalLength(self):
        """Get the focal length of telescope in meter.

        Returns
        -------
        float
            Focal length of telescope in meter.
        """

        return self.instParamFile.getSetting("focalLength")

    def getApertureDiameter(self):
        """Get the aperture diameter in meter.

        Returns
        -------
        float
            Aperture diameter in meter.
        """

        return self.instParamFile.getSetting("apertureDiameter")

    def getDefocalDisOffset(self):
        """Get the defocal distance offset in meter.

        Returns
        -------
        float
            Defocal distance offset in meter.
        """

        offset = self.instParamFile.getSetting("offset")
        defocalDisInMm = "%.1fmm" % self.announcedDefocalDisInMm

        return offset[defocalDisInMm]

    def getCamPixelSize(self):
        """Get the camera pixel size in meter.

        Returns
        -------
        float
            Camera pixel size in meter.
        """

        return self.instParamFile.getSetting("pixelSize")

    def getMarginalFocalLength(self):
        """Get the marginal focal length in meter.

        Marginal_focal_length = sqrt(f^2 - (D/2)^2)

        Returns
        -------
        float
            Marginal focal length in meter.
        """

        focalLength = self.getFocalLength()
        apertureDiameter = self.getApertureDiameter()
        marginalFL = np.sqrt(focalLength**2 - (apertureDiameter/2)**2)

        return marginalFL

    def getSensorFactor(self):
        """Get the sensor factor.

        Returns
        -------
        float
            Sensor factor.
        """

        offset = self.getDefocalDisOffset()
        apertureDiameter = self.getApertureDiameter()
        focalLength = self.getFocalLength()
        pixelSize = self.getCamPixelSize()
        sensorFactor = self.dimOfDonut / (
            offset * apertureDiameter / focalLength / pixelSize)

        return sensorFactor

    def getSensorCoor(self):
        """Get the sensor coordinate.

        Returns
        -------
        numpy.ndarray
            X coordinate.
        numpy.ndarray
            Y coordinate.
        """

        return self.xSensor, self.ySensor

    def getSensorCoorAnnular(self):
        """Get the sensor coordinate with the annular aperature.

        Returns
        -------
        numpy.ndarray
            X coordinate.
        numpy.ndarray
            Y coordinate.
        """

        return self.xoSensor, self.yoSensor
Example #35
0
class M1M3Sim(MirrorSim):
    def __init__(self):
        """Initiate the M1M3 simulator class."""

        # M2 setting file
        configDir = os.path.join(getConfigDir(), "M1M3")
        settingFilePath = os.path.join(configDir, "m1m3Setting.yaml")
        self._m1m3SettingFile = ParamReader(filePath=settingFilePath)

        # Inner and outer radius of M1 mirror in m
        radiusM1Inner = self._m1m3SettingFile.getSetting("radiusM1Inner")
        radiusM1Outer = self._m1m3SettingFile.getSetting("radiusM1Outer")

        # Inner and outer radius of M3 mirror in m
        radiusM3Inner = self._m1m3SettingFile.getSetting("radiusM3Inner")
        radiusM3Outer = self._m1m3SettingFile.getSetting("radiusM3Outer")

        super(M1M3Sim,
              self).__init__((radiusM1Inner, radiusM3Inner),
                             (radiusM1Outer, radiusM3Outer), configDir)

        # Mirror surface bending mode grid file
        self._gridFile = ParamReader()

        # FEA model file
        self._feaFile = ParamReader()

        # FEA model data in zenith angle
        self._feaZenFile = ParamReader()

        # FEA model data in horizontal angle
        self._feaHorFile = ParamReader()

        # Actuator forces along zenith direction
        self._forceZenFile = ParamReader()

        # Actuator forces along horizon direction
        self._forceHorFile = ParamReader()

        # Influence matrix of actuator forces
        self._forceInflFile = ParamReader()

        self._config("M1M3_1um_156_force.yaml", "M1M3_LUT.yaml",
                     "M1M3_1um_156_grid.yaml", "M1M3_thermal_FEA.yaml",
                     "M1M3_dxdydz_zenith.yaml", "M1M3_dxdydz_horizon.yaml",
                     "M1M3_force_zenith.yaml", "M1M3_force_horizon.yaml",
                     "M1M3_influence_256.yaml")

    def _config(self, actForceFileName, lutFileName, gridFileName, feaFileName,
                feaZenFileName, feaHorFileName, forceZenFileName,
                forceHorFileName, forceInflFileName):
        """Do the configuration.

        LUT: Look-up table.
        FEA: Finite element analysis.

        Parameters
        ----------
        actForceFileName : str
            Actuator force file name.
        lutFileName : str
            LUT file name.
        gridFileName : str
            File name of bending mode data.
        feaFileName : str
            FEA model data file name.
        feaZenFileName : str
            FEA model data file name in zenith angle.
        feaHorFileName : str
            FEA model data file name in horizontal angle.
        forceZenFileName : str
            File name of actuator forces along zenith direction.
        forceHorFileName : str
            File name of actuator forces along horizon direction.
        forceInflFileName : str
            Influence matrix of actuator forces.
        """

        numTerms = self._m1m3SettingFile.getSetting("numTerms")

        super(M1M3Sim, self).config(numTerms=numTerms,
                                    actForceFileName=actForceFileName,
                                    lutFileName=lutFileName)

        mirrorDataDir = self.getMirrorDataDir()

        gridFilePath = os.path.join(mirrorDataDir, gridFileName)
        self._gridFile.setFilePath(gridFilePath)

        feaFilePath = os.path.join(mirrorDataDir, feaFileName)
        self._feaFile.setFilePath(feaFilePath)

        feaZenFilePath = os.path.join(mirrorDataDir, feaZenFileName)
        self._feaZenFile.setFilePath(feaZenFilePath)

        feaHorFilePath = os.path.join(mirrorDataDir, feaHorFileName)
        self._feaHorFile.setFilePath(feaHorFilePath)

        forceZenFilePath = os.path.join(mirrorDataDir, forceZenFileName)
        self._forceZenFile.setFilePath(forceZenFilePath)

        forceHorFilePath = os.path.join(mirrorDataDir, forceHorFileName)
        self._forceHorFile.setFilePath(forceHorFilePath)

        forceInflFilePath = os.path.join(mirrorDataDir, forceInflFileName)
        self._forceInflFile.setFilePath(forceInflFilePath)

    def getPrintthz(self, zAngleInRadian, preCompElevInRadian=0):
        """Get the mirror print in m along z direction in specific zenith
        angle.

        FEA: Finite element analysis.

        Parameters
        ----------
        zAngleInRadian : float
            Zenith angle in radian.
        preCompElevInRadian : float, optional
            Pre-compensation elevation angle in radian. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Corrected projection in m along z direction.
        """

        # Data needed to determine gravitational print through
        data = self._feaZenFile.getMatContent()
        zdx = data[:, 0]
        zdy = data[:, 1]
        zdz = data[:, 2]

        data = self._feaHorFile.getMatContent()
        hdx = data[:, 0]
        hdy = data[:, 1]
        hdz = data[:, 2]

        # Do the M1M3 gravitational correction.
        # Map the changes of dx, dy, and dz on a plane for certain zenith angle
        printthxInM = zdx * np.cos(zAngleInRadian) + \
            hdx * np.sin(zAngleInRadian)
        printthyInM = zdy * np.cos(zAngleInRadian) + \
            hdy * np.sin(zAngleInRadian)
        printthzInM = zdz * np.cos(zAngleInRadian) + \
            hdz * np.sin(zAngleInRadian)

        # Get the bending mode information
        idx1, idx3, bx, by, bz = self._getMirCoor()

        # Calcualte the mirror ideal shape
        zRef = self._calcIdealShape(bx * 1000, by * 1000, idx1, idx3) / 1000

        # Calcualte the mirror ideal shape with the displacement
        zpRef = self._calcIdealShape(
            (bx + printthxInM) * 1000,
            (by + printthyInM) * 1000, idx1, idx3) / 1000

        # Convert printthz into surface sag to get the estimated wavefront
        # error.
        # Do the zenith angle correction by the linear approximation with the
        # ideal shape.
        printthzInM = printthzInM - (zpRef - zRef)

        # Normalize the coordinate
        Ri = self.getInnerRinM()[0]
        R = self.getOuterRinM()[0]

        normX = bx / R
        normY = by / R
        obs = Ri / R

        # Fit the annular Zernike polynomials z0-z2 (piton, x-tilt, y-tilt)
        zc = ZernikeAnnularFit(printthzInM, normX, normY, 3, obs)

        # Do the estimated wavefront error correction for the mirror projection
        printthzInM -= ZernikeAnnularEval(zc, normX, normY, obs)

        return printthzInM

    def _getMirCoor(self):
        """Get the mirror coordinate and node.

        Returns
        -------
        numpy.ndarray[int]
            M1 node.
        numpy.ndarray[int]
            M3 node.
        numpy.ndarray
            x coordinate.
        numpy.ndarray
            y coordinate.
        numpy.ndarray
            z coordinate.
        """

        # Get the bending mode information
        data = self._gridFile.getMatContent()

        nodeID = data[:, 0].astype("int")
        nodeM1 = (nodeID == 1)
        nodeM3 = (nodeID == 3)

        bx = data[:, 1]
        by = data[:, 2]
        bz = data[:, 3:]

        return nodeM1, nodeM3, bx, by, bz

    def _calcIdealShape(self,
                        xInMm,
                        yInMm,
                        idxM1,
                        idxM3,
                        dr1=0,
                        dr3=0,
                        dk1=0,
                        dk3=0):
        """Calculate the ideal shape of mirror along z direction.

        This is described by a series of cylindrically-symmetric aspheric
        surfaces.

        Parameters
        ----------
        xInMm : numpy.ndarray
            Coordinate x in 1D array in mm.
        yInMm : numpy.ndarray
            Coordinate y in 1D array in mm.
        idxM1 : numpy.ndarray [int]
            M1 node.
        idxM3 : numpy.ndarray [int]
            M3 node.
        dr1 : float, optional
            Displacement of r in mirror 1. (the default is 0.)
        dr3 : float, optional
            Displacement of r in mirror 3. (the default is 0.)
        dk1 : float, optional
            Displacement of kappa (k) in mirror 1. (the default is 0.)
        dk3 : float, optional
            Displacement of kappa (k) in mirror 3. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Ideal mirror surface along z direction.

        Raises
        ------
        ValueError
            X is unequal to y.
        """

        # M1 optical design
        r1 = -1.9835e4
        k1 = -1.215
        alpha1 = np.zeros((8, 1))
        alpha1[2] = 1.38e-24

        # M3 optical design
        r3 = -8344.5
        k3 = 0.155
        alpha3 = np.zeros((8, 1))
        alpha3[2] = -4.5e-22
        alpha3[3] = -8.15e-30

        # Get the dimension of input xInMm, yInMm
        nr = xInMm.shape
        mr = yInMm.shape
        if (nr != mr):
            raise ValueError("X[%d] is unequal to y[%d]." % (nr, mr))

        # Calculation the curvature (c) and conic constant (kappa)

        # Mirror 1 (M1)
        c1 = 1 / (r1 + dr1)
        k1 = k1 + dk1

        # Mirror 3 (M3)
        c3 = 1 / (r3 + dr3)
        k3 = k3 + dk3

        # Construct the curvature, kappa, and alpha matrixes for the ideal
        # shape calculation
        cMat = np.zeros(nr)
        cMat[idxM1] = c1
        cMat[idxM3] = c3

        kMat = np.zeros(nr)
        kMat[idxM1] = k1
        kMat[idxM3] = k3

        alphaMat = np.tile(np.zeros(nr), (8, 1))
        for ii in range(8):
            alphaMat[ii, idxM1] = alpha1[ii]
            alphaMat[ii, idxM3] = alpha3[ii]

        # Calculate the radius
        r2 = xInMm**2 + yInMm**2

        # Calculate the ideal surface

        # The optical elements of telescopes can often be described by a
        # series of cylindrically-symmetric aspheric surfaces:
        # z(r) = c * r^2/[ 1 + sqrt( 1-(1+k) * c^2 * r^2 ) ] +
        # sum(ai * r^(2*i)) + sum(Aj * Zj)
        # where i = 1-8, j = 1-N

        z0 = cMat * r2 / (1 + np.sqrt(1 - (1 + kMat) * cMat**2 * r2))
        for ii in range(8):
            z0 += alphaMat[ii, :] * r2**(ii + 1)

        # M3 vertex offset from M1 vertex, values from Zemax model
        # M3voffset = (233.8 - 233.8 - 900 - 3910.701 - 1345.500 + 1725.701
        # + 3530.500 + 900 + 233.800)
        M3voffset = 233.8

        # Add the M3 offset (sum(Aj * Zj), j = 1 - N)
        z0[idxM3] = z0[idxM3] + M3voffset

        # In Zemax, z axis points from M1M3 to M2. the reversed direction
        # (z0>0) is needed. That means the direction of M2 to M1M3.
        return -z0

    def getTempCorr(self, m1m3TBulk, m1m3TxGrad, m1m3TyGrad, m1m3TzGrad,
                    m1m3TrGrad):
        """Get the mirror print correction along z direction for certain
        temperature gradient.

        FEA: Finite element analysis.

        Parameters
        ----------
        m1m3TBulk : float
            Bulk temperature in degree C (+/-2sigma spans +/-0.8C).
        m1m3TxGrad : float
            Temperature gradient along x direction in degree C (+/-2sigma
            spans 0.4C).
        m1m3TyGrad : float
            Temperature gradient along y direction in degree C (+/-2sigma
            spans 0.4C).
        m1m3TzGrad : float
            Temperature gradient along z direction in degree C (+/-2sigma
            spans 0.1C).
        m1m3TrGrad : float
            Temperature gradient along r direction in degree C (+/-2sigma
            spans 0.1C).

        Returns
        -------
        numpy.ndarray
            Corrected projection in um along z direction.
        """

        # Data needed to determine thermal deformation
        data = self._feaFile.getMatContent()

        # These are the normalized coordinates

        # In the original XLS file (thermal FEA model), max(x)=164.6060 in,
        # while 4.18m = 164.5669 in for real mirror. These two numbers do
        # not match.
        # The data here has normalized the dimension (x, y) already.
        # n.b. these may not have been normalized correctly, b/c max(tx)=1.0
        tx = data[:, 0]
        ty = data[:, 1]

        # Below are in M1M3 coordinate system, and in micron

        # Do the fitting in the normalized coordinate
        bx, by = self._getMirCoor()[2:4]
        R = self.getOuterRinM()[0]
        normX = bx / R
        normY = by / R

        # Fit the bulk
        tbdz = self._fitData(tx, ty, data[:, 2], normX, normY)

        # Fit the x-grad
        txdz = self._fitData(tx, ty, data[:, 3], normX, normY)

        # Fit the y-grad
        tydz = self._fitData(tx, ty, data[:, 4], normX, normY)

        # Fit the z-grad
        tzdz = self._fitData(tx, ty, data[:, 5], normX, normY)

        # Fit the r-gradß
        trdz = self._fitData(tx, ty, data[:, 6], normX, normY)

        # Get the temprature correction
        tempCorrInUm = m1m3TBulk*tbdz + m1m3TxGrad*txdz + m1m3TyGrad*tydz + \
            m1m3TzGrad*tzdz + m1m3TrGrad*trdz

        return tempCorrInUm

    def _fitData(self, dataX, dataY, data, x, y):
        """Fit the data by radial basis function.

        Parameters
        ----------
        dataX : numpy.ndarray
            Data x.
        dataY : numpy.ndarray
            Data y.
        data : numpy.ndarray
            Data to fit.
        x : numpy.ndarray
            x coordinate.
        y : numpy.ndarray
            y coordinate.

        Returns
        -------
        numpy.ndarray
            Fitted data.
        """

        # Construct the fitting model
        rbfi = Rbf(dataX, dataY, data)

        # Return the fitted data
        return rbfi(x, y)

    def getMirrorResInMmInZemax(self, writeZcInMnToFilePath=None):
        """Get the residue of surface (mirror print along z-axis) in mm under
        the Zemax coordinate.

        This value is after the fitting with spherical Zernike polynomials
        (zk).

        Parameters
        ----------
        writeZcInMnToFilePath : str, optional
            File path to write the fitted zk in mm. (the default is None.)

        Returns
        ------
        numpy.ndarray
            Fitted residue in mm after removing the fitted zk terms in Zemax
            coordinate.
        numpy.ndarray
            X position in mm in Zemax coordinate.
        numpy.ndarray
            Y position in mm in Zemax coordinate.
        numpy.ndarray
            Fitted zk in mm in Zemax coordinate.
        """

        # Get the bending mode information
        bx, by, bz = self._getMirCoor()[2:5]

        # Transform the M1M3 coordinate to Zemax coordinate
        bxInZemax, byInZemax, surfInZemax = opt2ZemaxCoorTrans(
            bx, by, self.getSurfAlongZ())

        # Get the mirror residue and zk in um
        RinM = self.getOuterRinM()[0]
        resInUmInZemax, zcInUmInZemax = self._getMirrorResInNormalizedCoor(
            surfInZemax, bxInZemax / RinM, byInZemax / RinM)

        # Change the unit to mm
        resInMmInZemax = resInUmInZemax * 1e-3
        bxInMmInZemax = bxInZemax * 1e3
        byInMmInZemax = byInZemax * 1e3
        zcInMmInZemax = zcInUmInZemax * 1e-3

        # Save the file of fitted Zk
        if (writeZcInMnToFilePath is not None):
            np.savetxt(writeZcInMnToFilePath, zcInMmInZemax)

        return resInMmInZemax, bxInMmInZemax, byInMmInZemax, zcInMmInZemax

    def writeMirZkAndGridResInZemax(self,
                                    resFile=[],
                                    surfaceGridN=200,
                                    writeZcInMnToFilePath=None):
        """Write the grid residue in mm of mirror surface after the fitting
        with Zk under the Zemax coordinate.

        Parameters
        ----------
        resFile : list, optional
            File path to save the grid surface residue map. (the default
            is [].)
        surfaceGridN : {number}, optional
            Surface grid number. (the default is 200.)
        writeZcInMnToFilePath : str, optional
            File path to write the fitted zk in mm. (the default is None.)

        Returns
        -------
        str
            Grid residue map related data of M1.
        str
            Grid residue map related data of M3.
        """

        # Get the residure map
        resInMmInZemax, bxInMmInZemax, byInMmInZemax = \
            self.getMirrorResInMmInZemax(
                writeZcInMnToFilePath=writeZcInMnToFilePath)[0:3]

        # Get the mirror node
        idx1, idx3 = self._getMirCoor()[0:2]

        # Grid sample map for M1 and M3
        for ii, idx in zip((0, 1), (idx1, idx3)):

            # Change the unit from m to mm
            innerRinMm = self.getInnerRinM()[ii] * 1e3
            outerRinMm = self.getOuterRinM()[ii] * 1e3

            # Get the residue map used in Zemax
            # Content header: (NUM_X_PIXELS, NUM_Y_PIXELS, delta x, delta y)
            # Content: (z, dx, dy, dxdy)
            content = self._gridSampInMnInZemax(resInMmInZemax[idx],
                                                bxInMmInZemax[idx],
                                                byInMmInZemax[idx],
                                                innerRinMm,
                                                outerRinMm,
                                                surfaceGridN,
                                                surfaceGridN,
                                                resFile=resFile[ii])
            if (ii == 0):
                contentM1 = content
            elif (ii == 1):
                contentM3 = content

        return contentM1, contentM3

    def showMirResMap(self, resFile, writeToResMapFilePath=[]):
        """Show the mirror residue map.

        Parameters
        ----------
        resFile : list
            File path of the grid surface residue map.
        writeToResMapFilePath : list, optional
            File path to save the residue map. (the default is [].)
        """

        # Get the residure map
        resInMmInZemax, bxInMmInZemax, byInMmInZemax = \
            self.getMirrorResInMmInZemax()[0:3]

        # Get the mirror node
        idx1, idx3 = self._getMirCoor()[0:2]

        # Show the mirror maps
        RinMtuple = self.getOuterRinM()

        for ii, RinM, idx in zip((0, 1), RinMtuple, (idx1, idx3)):
            outerRinMm = RinM * 1e3
            plotResMap(resInMmInZemax[idx],
                       bxInMmInZemax[idx],
                       byInMmInZemax[idx],
                       outerRinMm,
                       resFile=resFile[ii],
                       writeToResMapFilePath=writeToResMapFilePath[ii])

    def genMirSurfRandErr(self,
                          zAngleInRadian,
                          m1m3ForceError=0.05,
                          seedNum=0):
        """Generate the mirror surface random error.

        LUT: Loop-up table.

        Parameters
        ----------
        zAngleInRadian : float
            Zenith angle in radian.
        m1m3ForceError : float, optional
            Ratio of actuator force error. (the default is 0.05.)
        seedNum : int, optional
            Random seed number. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Generated mirror surface random error in m.
        """

        # Get the actuator forces in N of M1M3 based on the look-up table (LUT)
        zangleInDeg = np.rad2deg(zAngleInRadian)
        LUTforce = self.getLUTforce(zangleInDeg)

        # Assume the m1m3ForceError=0.05
        # Add 5% force error to the original actuator forces
        # This means from -5% to +5% of original actuator's force.
        np.random.seed(int(seedNum))
        nActuator = len(LUTforce)
        myu = (1 + 2 *
               (np.random.rand(nActuator) - 0.5) * m1m3ForceError) * LUTforce

        # Balance forces along z-axis
        # This statement is intentionally to make the force balance.
        nzActuator = int(self._m1m3SettingFile.getSetting("numActuatorInZ"))
        myu[nzActuator-1] = np.sum(LUTforce[:nzActuator]) - \
            np.sum(myu[:nzActuator-1])

        # Balance forces along y-axis
        # This statement is intentionally to make the force balance.
        myu[nActuator-1] = np.sum(LUTforce[nzActuator:]) - \
            np.sum(myu[nzActuator:-1])

        # Get the net force along the z-axis
        zf = self._forceZenFile.getMatContent()
        hf = self._forceHorFile.getMatContent()
        u0 = zf * np.cos(zAngleInRadian) + hf * np.sin(zAngleInRadian)

        # Calculate the random surface
        G = self._forceInflFile.getMatContent()
        randSurfInM = G.dot(myu - u0)

        return randSurfInM
Example #36
0
    def testGetContentWithDefaultSetting(self):

        paramReader = ParamReader()

        content = paramReader.getContent()
        self.assertTrue(isinstance(content, dict))
Example #37
0
class SourceSelector(object):
    def __init__(self, camType, bscDbType, settingFileName="default.yaml"):
        """Initialize the source selector class.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        bscDbType : enum 'BscDbType'
            Bright star catalog (BSC) database type.
        settingFileName : str, optional
            Setting file name (the default is "default.yaml".)
        """

        self.camera = CamFactory.createCam(camType)
        self.db = DatabaseFactory.createDb(bscDbType)
        self.filter = Filter()

        self.maxDistance = 0.0
        self.maxNeighboringStar = 0

        settingFilePath = os.path.join(getConfigDir(), settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        # Configurate the criteria of neighboring stars
        starRadiusInPixel = self.settingFile.getSetting("starRadiusInPixel")
        spacingCoefficient = self.settingFile.getSetting("spacingCoef")
        maxNeighboringStar = self.settingFile.getSetting("maxNumOfNbrStar")
        self.configNbrCriteria(starRadiusInPixel,
                               spacingCoefficient,
                               maxNeighboringStar=maxNeighboringStar)

    def configNbrCriteria(self,
                          starRadiusInPixel,
                          spacingCoefficient,
                          maxNeighboringStar=0):
        """Set the neighboring star criteria to decide the scientific target.

        Parameters
        ----------
        starRadiusInPixel : float, optional
            Diameter of star. For the defocus = 1.5 mm, the star's radius is
            63 pixel.
        spacingCoefficient : float, optional
            Maximum distance in units of radius one donut must be considered
            as a neighbor.
        maxNeighboringStar : int, optional
            Maximum number of neighboring stars. (the default is 0.)
        """

        self.maxDistance = starRadiusInPixel * spacingCoefficient
        self.maxNeighboringStar = int(maxNeighboringStar)

    def connect(self, *args):
        """Connect the database.

        Parameters
        ----------
        *args : str or *list
            Information to connect to the database.
        """

        self.db.connect(*args)

    def disconnect(self):
        """Disconnect the database."""

        self.db.disconnect()

    def setFilter(self, filterType):
        """Set the filter type.

        Parameters
        ----------
        filterType : FilterType
            Filter type.
        """

        self.filter.setFilter(filterType)

    def getFilter(self):
        """Get the filter type.

        Returns
        -------
        FilterType
            Filter type.
        """

        return self.filter.getFilter()

    def setObsMetaData(self, ra, dec, rotSkyPos):
        """Set the observation meta data.

        Parameters
        ----------
        ra : float
            Pointing ra in degree.
        dec : float
            Pointing decl in degree.
        rotSkyPos : float
            The orientation of the telescope in degrees.
        """

        self.camera.setObsMetaData(ra, dec, rotSkyPos)

    def getTargetStar(self, offset=0):
        """Get the target stars by querying the database.

        Parameters
        ----------
        offset : float, optional
            Offset to the dimension of camera. If the detector dimension is 10
            (assume 1-D), the star's position between -offset and 10+offset
            will be seem to be on the detector. (the default is 0.)

        Returns
        -------
        dict
            Information of neighboring stars and candidate stars with the name
            of sensor as a dictionary.
        dict
            Information of stars with the name of sensor as a dictionary.
        dict
            (ra, dec) of four corners of each sensor with the name
            of sensor as a list. The dictionary key is the sensor name.
        """

        wavefrontSensors = self.camera.getWavefrontSensor()
        lowMagnitude, highMagnitude = self.filter.getMagBoundary()

        # Map the reference filter to the G filter
        filterType = self.getFilter()
        mappedFilterType = mapFilterRefToG(filterType)

        # Query the star database
        starMap = dict()
        neighborStarMap = dict()
        for detector, wavefrontSensor in wavefrontSensors.items():

            # Get stars in this wavefront sensor for this observation field
            stars = self.db.query(
                mappedFilterType,
                wavefrontSensor[0],
                wavefrontSensor[1],
                wavefrontSensor[2],
                wavefrontSensor[3],
            )

            # Set the detector information for the stars
            stars.setDetector(detector)

            # Populate pixel information for stars
            populatedStar = self.camera.populatePixelFromRADecl(stars)

            # Get the stars that are on the detector
            starsOnDet = self.camera.getStarsOnDetector(populatedStar, offset)
            starMap[detector] = starsOnDet

            # Check the candidate of bright stars based on the magnitude
            indexCandidate = starsOnDet.checkCandidateStars(
                mappedFilterType, lowMagnitude, highMagnitude)

            # Determine the neighboring stars based on the distance and
            # allowed number of neighboring stars
            neighborStar = starsOnDet.getNeighboringStar(
                indexCandidate,
                self.maxDistance,
                mappedFilterType,
                self.maxNeighboringStar,
            )
            neighborStarMap[detector] = neighborStar

        # Remove the data that has no bright star
        self._rmDataWithoutBrightStar(neighborStarMap, starMap,
                                      wavefrontSensors)

        return neighborStarMap, starMap, wavefrontSensors

    def _rmDataWithoutBrightStar(self, neighborStarMap, starMap,
                                 wavefrontSensors):
        """Remove the data that has no bright stars on the detector.

        The data in inputs will be changed directly.

        Parameters
        ----------
        neighborStarMap : dict
            Information of neighboring stars and candidate stars with the name
            of sensor as a dictionary.
        starMap : dict
            Information of stars with the name of sensor as a dictionary.
        wavefrontSensors : dict
            (ra, dec) of four corners of each sensor with the name
            of sensor as a list. The dictionary key is the sensor name.
        """

        # Collect the sensor list without the bright star
        noStarSensorList = []
        for detector, stars in neighborStarMap.items():
            if len(stars.getId()) == 0:
                noStarSensorList.append(detector)

        # Remove the data in map
        for detector in noStarSensorList:
            neighborStarMap.pop(detector)
            starMap.pop(detector)
            wavefrontSensors.pop(detector)

    def getTargetStarByFile(self, skyFilePath, offset=0):
        """Get the target stars by querying the star file.

        This function is only for the test. This shall be removed in the final.

        Parameters
        ----------
        skyFilePath : str
            Sky data file path.
        offset : float, optional
            Offset to the dimension of camera. If the detector dimension is 10
            (assume 1-D), the star's position between -offset and 10+offset
            will be seem to be on the detector. (the default is 0.)

        Returns
        -------
        dict
            Information of neighboring stars and candidate stars with the name
            of sensor as a dictionary.
        dict
            Information of stars with the name of sensor as a dictionary.
        dict
            (ra, dec) of four corners of each sensor with the name
            of sensor as a list. The dictionary key is the sensor name.

        Raises
        ------
        TypeError
            The database type is incorrect.
        """

        if not isinstance(self.db, LocalDatabaseForStarFile):
            raise TypeError("The database type is incorrect.")

        # Map the reference filter to the G filter
        filterType = self.getFilter()
        mappedFilterType = mapFilterRefToG(filterType)

        # Write the sky data into the temporary table
        self.db.createTable(mappedFilterType)
        self.db.insertDataByFile(skyFilePath, mappedFilterType, skiprows=1)
        neighborStarMap, starMap, wavefrontSensors = self.getTargetStar(
            offset=offset)

        # Delete the table
        self.db.deleteTable(mappedFilterType)

        return neighborStarMap, starMap, wavefrontSensors
Example #38
0
class TestParamReader(unittest.TestCase):
    """Test the ParamReaderYaml class."""

    def setUp(self):

        testDir = os.path.join(getModulePath(), "tests")
        self.configDir = os.path.join(testDir, "testData")
        self.fileName = "testConfigFile.yaml"

        filePath = os.path.join(self.configDir, self.fileName)
        self.paramReader = ParamReader(filePath=filePath)

        self.testTempDir = os.path.join(testDir, "tmp")
        self._makeDir(self.testTempDir)

    def _makeDir(self, directory):

        if (not os.path.exists(directory)):
            os.makedirs(directory)

    def tearDown(self):

        shutil.rmtree(self.testTempDir)

    def testGetSetting(self):

        znmax = self.paramReader.getSetting("znmax")
        self.assertEqual(znmax, 22)

    def testGetFilePath(self):

        ansFilePath = os.path.join(self.configDir, self.fileName)
        self.assertEqual(self.paramReader.getFilePath(), ansFilePath)

    def testSetFilePath(self):

        fileName = "test.yaml"
        filePath = os.path.join(self.configDir, fileName)
        self.paramReader.setFilePath(filePath)

        self.assertEqual(self.paramReader.getFilePath(), filePath)

    def testGetContent(self):

        content = self.paramReader.getContent()
        self.assertTrue(isinstance(content, dict))

    def testGetContentWithDefaultSetting(self):

        paramReader = ParamReader()

        content = paramReader.getContent()
        self.assertTrue(isinstance(content, dict))

    def testWriteMatToFile(self):

        self._writeMatToFile()

        numOfFile = self._getNumOfFileInFolder(self.testTempDir)
        self.assertEqual(numOfFile, 1)

    def _writeMatToFile(self):

        mat = np.random.rand(3, 4, 5)
        filePath = os.path.join(self.testTempDir, "temp.yaml")
        ParamReader.writeMatToFile(mat, filePath)

        return mat, filePath

    def _getNumOfFileInFolder(self, folder):

        return len([name for name in os.listdir(folder)
                   if os.path.isfile(os.path.join(folder, name))])

    def testWriteMatToFileWithWrongFileFormat(self):

        wrongFilePath = os.path.join(self.testTempDir, "temp.txt")
        self.assertRaises(ValueError, ParamReader.writeMatToFile,
                          np.ones(4), wrongFilePath)

    def testGetMatContent(self):

        mat, filePath = self._writeMatToFile()

        self.paramReader.setFilePath(filePath)
        matInYamlFile = self.paramReader.getMatContent()

        delta = np.sum(np.abs(matInYamlFile - mat))
        self.assertLess(delta, 1e-10)

    def testGetMatContentWithDefaultSetting(self):

        paramReader = ParamReader()
        matInYamlFile = paramReader.getMatContent()

        self.assertTrue(isinstance(matInYamlFile, np.ndarray))
        self.assertEqual(len(matInYamlFile), 0)
Example #39
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
Example #40
0
class SourceProcessor(object):

    def __init__(self, settingFileName="default.yaml",
                 focalPlaneFileName="focalplanelayout.txt"):
        """Initialize the SourceProcessor class.

        Parameters
        ----------
        settingFileName : str, optional
            Setting file name. (the default is "default.yaml".)
        focalPlaneFileName : str, optional
            Focal plane file name used in the PhoSim instrument directory. (the
            default is "focalplanelayout.txt".)
        """

        self.sensorName = ""
        self.blendedImageDecorator = BlendedImageDecorator()

        configDir = getConfigDir()
        settingFilePath = os.path.join(configDir, settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        self.sensorFocaPlaneInDeg = dict()
        self.sensorFocaPlaneInUm = dict()
        self.sensorDimList = dict()
        self.sensorEulerRot = dict()
        self._readFocalPlane(configDir, focalPlaneFileName)

    def _readFocalPlane(self, folderPath, focalPlaneFileName):
        """Read the focal plane data used in PhoSim to get the ccd dimension
        and fieldXY in chip center.

        Parameters
        ----------
        folderPath : str
            Directory of focal plane file.
        focalPlaneFileName : str
            Focal plane file name used in the PhoSim instrument directory.
        """

        # Read the focal plane data by the delegation
        ccdData = readPhoSimSettingData(folderPath, focalPlaneFileName,
                                        "fieldCenter")

        # Collect the focal plane data
        sensorFocaPlaneInDeg = dict()
        sensorFocaPlaneInUm = dict()
        sensorDimList = dict()
        for sensorName, data in ccdData.items():

            # Consider the x-translation in corner wavefront sensors
            self._shiftCenterWfs(sensorName, data)

            # Change the unit from um to degree
            xInUm = float(data[0])
            yInUm = float(data[1])
            pixelSizeInUm = float(data[2])
            sizeXinPixel = int(data[3])
            sizeYinPixel = int(data[4])

            # 1 degree = 3600 arcsec
            pixelToArcsec = self.settingFile.getSetting("pixelToArcsec")
            fieldX = xInUm / pixelSizeInUm * pixelToArcsec / 3600
            fieldY = yInUm / pixelSizeInUm * pixelToArcsec / 3600

            # Get the data
            sensorFocaPlaneInDeg[sensorName] = (fieldX, fieldY)
            sensorFocaPlaneInUm[sensorName] = (xInUm, yInUm)
            sensorDimList[sensorName] = (sizeXinPixel, sizeYinPixel)

        # Assign the values
        self.sensorDimList = sensorDimList
        self.sensorFocaPlaneInDeg = sensorFocaPlaneInDeg
        self.sensorFocaPlaneInUm = sensorFocaPlaneInUm
        self.sensorEulerRot = readPhoSimSettingData(
            folderPath, focalPlaneFileName, "eulerRot")

    def _shiftCenterWfs(self, sensorName, focalPlaneData):
        """Shift the fieldXY of center of wavefront sensors.

        The input data is the center of combined chips (C0+C1). The input data
        will be updated directly.

        Parameters
        ----------
        sensorName : str
            Abbreviated sensor name.
        focalPlaneData : list
            Data of focal plane: [x position (microns), y position (microns),
            pixel size (microns), number of x pixels, number of y pixels].
        """

        # The layout is shown in the following:

        # R04_S20              R44_S00
        # --------           -----------       /\ +y
        # |  C0  |           |    |    |        |
        # |------|           | C1 | C0 |        |
        # |  C1  |           |    |    |        |
        # --------           -----------        -----> +x

        # R00_S22              R40_S02
        # -----------          --------
        # |    |    |          |  C1  |
        # | C0 | C1 |          |------|
        # |    |    |          |  C0  |
        # -----------          --------

        xInUm = float(focalPlaneData[0])
        yInUm = float(focalPlaneData[1])
        pixelSizeInUm = float(focalPlaneData[2])
        sizeXinPixel = float(focalPlaneData[3])

        # Consider the x-translation in corner wavefront sensors
        tempX = None
        tempY = None

        if sensorName in ("R44_S00_C0", "R00_S22_C1"):
            # Shift center to +x direction
            tempX = xInUm + sizeXinPixel / 2 * pixelSizeInUm
        elif sensorName in ("R44_S00_C1", "R00_S22_C0"):
            # Shift center to -x direction
            tempX = xInUm - sizeXinPixel / 2 * pixelSizeInUm
        elif sensorName in ("R04_S20_C1", "R40_S02_C0"):
            # Shift center to -y direction
            tempY = yInUm - sizeXinPixel / 2 * pixelSizeInUm
        elif sensorName in ("R04_S20_C0", "R40_S02_C1"):
            # Shift center to +y direction
            tempY = yInUm + sizeXinPixel / 2 * pixelSizeInUm

        # Replace the value by the shifted one
        if (tempX is not None):
            focalPlaneData[0] = str(tempX)
        elif (tempY is not None):
            focalPlaneData[1] = str(tempY)

    def config(self, sensorName=None):
        """Do the configuration.

        Parameters
        ----------
        sensorName : str, optional
            Abbreviated sensor name. (the default is None.)
        """

        if (sensorName is not None):
            self.sensorName = sensorName

    def getEulerZinDeg(self, sensorName):
        """Get the Euler Z angle of sensor in degree.

        Parameters
        ----------
        sensorName : str
            Abbreviated sensor name.

        Returns
        -------
        float
            Euler Z angle in degree.
        """

        return float(self.sensorEulerRot[sensorName][0])

    def camXYtoFieldXY(self, pixelX, pixelY):
        """Get the field X, Y from the pixel x, y position on CCD.

        Parameters
        ----------
        pixelX : float
            Pixel x on camera coordinate.
        pixelY : float
            Pixel y on camera coordinate.

        Returns
        -------
        float
            Field x in degree.
        float
            Field y in degree.
        """

        # The wavefront sensors will do the counter-clockwise rotation as the
        # following based on the euler angle:

        # R04_S20              R44_S00
        # O-------           -----O----O       /\ +y
        # |  C0  |           |    |    |        |
        # O------|           | C1 | C0 |        |
        # |  C1  |           |    |    |        |
        # --------           -----------        O----> +x

        # R00_S22              R40_S02
        # -----------          --------
        # |    |    |          |  C1  |
        # | C0 | C1 |          |------O
        # |    |    |          |  C0  |
        # O----O-----          -------O

        # Get the field X, Y of sensor's center
        fieldXc, fieldYc = self.sensorFocaPlaneInDeg[self.sensorName]

        # Get the center pixel position
        pixelXc, pixelYc = self.sensorDimList[self.sensorName]
        pixelXc = pixelXc / 2
        pixelYc = pixelYc / 2

        # Calculate the delta x and y in degree
        # 1 degree = 3600 arcsec
        pixelToArcsec = self.settingFile.getSetting("pixelToArcsec")
        deltaX = (pixelX - pixelXc) * pixelToArcsec / 3600.0
        deltaY = (pixelY - pixelYc) * pixelToArcsec / 3600.0

        # Calculate the transformed coordinate in degree.
        fieldX, fieldY = self._rotCam2FocalPlane(
            self.sensorName, fieldXc, fieldYc, deltaX, deltaY)

        return fieldX, fieldY

    def _rotCam2FocalPlane(self, sensorName, centerX, centerY, deltaX, deltaY,
                           clockWise=False):
        """Do the rotation from camera coordinate to focal plane coordinate or
        vice versa.

        Parameters
        ----------
        sensorName : str
            Abbreviated sensor name.
        centerX : float
            CCD center x.
        centerY : float
            CCD center y.
        deltaX : float
            Delta x from the CCD's center.
        deltaY : float
            Delta y from the CCD's center.
        clockWise : bool, optional
            Rotation direction (True: clockwise, False: counter-clockwise).
            (the default is False.)

        Returns
        -------
        float
            Transformed x position.
        float
            Transformed y position.
        """

        # Get the euler angle in z direction (only consider the z rotatioin at
        # this moment)
        eulerZ = round(self.getEulerZinDeg(sensorName))
        eulerZinRad = np.deg2rad(eulerZ)

        # Counter-clockwise or clockwise rotation
        if (clockWise):
            eulerZinRad = -eulerZinRad

        # Calculate the new x, y by the rotation. This is important for
        # wavefront sensor.
        newX = centerX + np.cos(eulerZinRad) * deltaX - \
            np.sin(eulerZinRad) * deltaY
        newY = centerY + np.sin(eulerZinRad) * deltaX + \
            np.cos(eulerZinRad)*deltaY

        return newX, newY

    def dmXY2CamXY(self, pixelDmX, pixelDmY):
        """Transform the pixel x, y from DM library to camera to use.

        Parameters
        ----------
        pixelDmX : float
            Pixel x defined in DM coordinate.
        pixelDmY : float
            Pixel y defined in DM coordinate.

        Returns
        -------
        float
            Pixel x defined in camera coordinate based on LCA-13381.
        float
            Pixel y defined in camera coordinate based on LCA-13381.
        """

        # Camera coordinate is defined in LCA-13381. Define camera coordinate
        # (x, y) and DM coordinate (x', y'), then the relation is:
        # Camera team +y = DM team +x'
        # Camera team +x = DM team -y'

        #  O---->y
        #  |
        #  |   ----------------------
        #  \/ |                      |   (x', y') = (200, 500) =>
        #  x  |                      |   (x, y) = (-500, 200) -> (3500, 200)
        #     |4000                  |
        # y'  |                      |
        #  /\ |       4072           |
        #  |  |----------------------
        #  |
        #  O-----> x'

        # Get the CCD dimension
        dimX, dimY = self.sensorDimList[self.sensorName]

        # Calculate the transformed coordinate
        pixelCamX = dimX - pixelDmY
        pixelCamY = pixelDmX

        return pixelCamX, pixelCamY

    def camXY2DmXY(self, pixelCamX, pixelCamY):
        """Transform the pixel x, y from camera coordinate to DM coordinate.

        Parameters
        ----------
        pixelCamX : float
            Pixel x defined in Camera coordinate based on LCA-13381.
        pixelCamY : float
            Pixel y defined in Camera coordinate based on LCA-13381.

        Returns
        -------
        float
            Pixel x defined in DM coordinate.
        float
            Pixel y defined in DM coordinate.
        """

        # Check the comment in dmXY2CamXY() for the details of coordinate
        # systems of DM and camera teams.

        # Get the CCD dimension
        dimX, dimY = self.sensorDimList[self.sensorName]

        # Calculate the transformed coordinate
        pixelDmX = pixelCamY
        pixelDmY = dimX - pixelCamX

        return pixelDmX, pixelDmY

    def isVignette(self, fieldX, fieldY):
        """The donut is vignetted or not by calculating the donut's distance to
        center.

        Parameters
        ----------
        fieldX : float
            Field x in degree.
        fieldY : float
            Field y in degree.

        Returns
        -------
        bool
            True if the donut is vignette.
        """

        # Calculate the distance to center in degree to judge the donut is
        # vignetted or not.
        fldr = np.sqrt(fieldX**2 + fieldY**2)
        distVignette = self.settingFile.getSetting("distVignette")
        if (fldr >= distVignette):
            return True
        else:
            return False

    def simulateImg(self, imageFolderPath, defocalDis, nbrStar, filterType,
                    noiseRatio=0.01):
        """Simulate the defocal CCD images with the neighboring star map.

        This function is only for the test use.

        Parameters
        ----------
        imageFolderPath : str
            Path to image directory.
        defocalDis : float
            Defocal distance in mm.
        nbrStar : NbrStar
            Neighboring star on single detector.
        filterType : FilterType
            Filter type.
        noiseRatio : float, optional
            The noise ratio. (the default is 0.01.)

        Returns
        -------
        numpy.ndarray
            Simulated intra-focal images.
        numpy.ndarray
            Simulated extra-focal images.

        Raises
        -------
        ValueError
            No available donut images.
        ValueError
            The numbers of intra- and extra-focal images are different.
        """

        # Generate the intra- and extra-focal ccd images
        d1, d2 = self.sensorDimList[self.sensorName]
        ccdImgIntra = np.random.random([d2, d1])*noiseRatio
        ccdImgExtra = ccdImgIntra.copy()

        # Get all files in the image directory in a sorted order
        fileList = sorted(os.listdir(imageFolderPath))

        # Redefine the format of defocal distance
        defocalDis = "%.2f" % defocalDis

        # Get the available donut files
        intraFileList = []
        extraFileList = []
        for afile in fileList:

            # Get the file name
            fileName, fileExtension = os.path.splitext(afile)

            # Split the file name for the analysis
            fileNameStr = fileName.split("_")

            # Find the file name with the correct defocal distance
            if (len(fileNameStr) == 3 and fileNameStr[1] == defocalDis):

                # Collect the file name based on the defocal type
                if (fileNameStr[-1] == "intra"):
                    intraFileList.append(afile)
                elif (fileNameStr[-1] == "extra"):
                    extraFileList.append(afile)

        # Get the number of available files
        numFile = len(intraFileList)
        if (numFile == 0):
            raise ValueError("No available donut images.")

        # Check the numbers of intra- and extra-focal images should be the same
        if (numFile != len(extraFileList)):
            raise ValueError(
                "The numbers of intra- and extra-focal images are different.")

        # Get the magnitude of stars
        mappedFilterType = mapFilterRefToG(filterType)
        starMag = nbrStar.getMag(mappedFilterType)

        # Based on the nbrStar to reconstruct the image
        for brightStar, neighboringStar in nbrStar.getId().items():

            # Generate a random number
            randNum = np.random.randint(0, high=numFile)

            # Choose a random donut image from the file
            donutImageIntra = self._getDonutImgFromFile(
                imageFolderPath, intraFileList[randNum])
            donutImageExtra = self._getDonutImgFromFile(
                imageFolderPath, extraFileList[randNum])

            # Get the bright star magnitude
            magBS = starMag[brightStar]

            # Combine the bright star and neighboring stars. Put the bright
            # star in the first one.
            allStars = neighboringStar[:]
            allStars.insert(0, brightStar)

            # Add the donut image
            for star in allStars:

                # Get the brigtstar pixel x, y
                starX, starY = nbrStar.getRaDeclInPixel()[star]
                magStar = starMag[star]

                # Transform the coordiante from DM team to camera team
                starX, starY = self.dmXY2CamXY(starX, starY)

                # Ratio of magnitude between donuts (If the magnitudes of stars
                # differs by 5, the brightness differs by 100.)
                # (Magnitude difference shoulbe be >= 1.)
                magDiff = magStar - magBS
                magRatio = 1 / 100 ** (magDiff / 5.0)

                # Add the donut image
                self._addDonutImage(magRatio * donutImageIntra, starX, starY,
                                    ccdImgIntra)
                self._addDonutImage(magRatio * donutImageExtra, starX, starY,
                                    ccdImgExtra)

        return ccdImgIntra, ccdImgExtra

    def _getDonutImgFromFile(self, imageFolderPath, fileName):
        """Read the donut image from the file.

        Parameters
        ----------
        imageFolderPath : str
            Path to image directory.
        fileName : str
            File name.

        Returns
        -------
        numpy.ndarray
            Donut image.
        """

        # Get the donut image from the file by the delegation
        self.blendedImageDecorator.setImg(
            imageFile=os.path.join(imageFolderPath, fileName))

        return self.blendedImageDecorator.getImg().copy()

    def _addDonutImage(self, donutImage, starX, starY, ccdImg):
        """Add the donut image to simulated CCD image frame.

        Parameters
        ----------
        donutImage : numpy.ndarray
            Donut image.
        starX : float
            Star position in pixel x.
        starY : float
            Star position in pixel y.
        ccdImg : numpy.ndarray
            CCD image.
        """

        # Get the dimension of donut image
        d1, d2 = donutImage.shape

        # Get the interger of position to use as the index
        y = int(starY)
        x = int(starX)

        # Add the donut image on the CCD image
        ccdImg[y-int(d1/2):y-int(d1/2)+d1, x-int(d2/2):x-int(d2/2)+d2] += \
            donutImage

    def getSingleTargetImage(self, ccdImg, nbrStar, index, filterType):
        """Get the image of single scientific target and related neighboring
        stars.

        Parameters
        ----------
        ccdImg : numpy.ndarray
            CCD image.
        nbrStar : NbrStar
            Neighboring star on single detector.
        index : int
            Index of science target star in neighboring star.
        filterType : FilterType
            Filter type.

        Returns
        -------
        numpy.ndarray
            Ccd image of target stars.
        numpy.ndarray
            Star x-positions. The arange is [neighboring stars, bright star].
        numpy.ndarray
            Star y-positions. The arange is [neighboring stars, bright star].
        numpy.ndarray
            Star magnitude ratio compared with the bright star. The arange is
            [neighboring stars, bright star].
        float
            Offset x from the origin of target star image to the origin of CCD
            image.
        float
            Offset y from the origin of target star image to the origin of CCD
            image.

        Raises
        ------
        ValueError
            Index is higher than the length of star map.
        """

        # Get the target star position
        nbrStarId = nbrStar.getId()
        if (index >= len(nbrStarId)):
            raise ValueError("Index is higher than the length of star map.")

        # Get the star SimobjID
        brightStar = list(nbrStarId)[index]
        neighboringStar = nbrStarId[brightStar]

        # Get all star SimobjID list
        allStar = neighboringStar[:]
        allStar.append(brightStar)

        # Get the pixel positions
        raDeclInPixel = nbrStar.getRaDeclInPixel()
        allStarPosX = []
        allStarPosY = []
        for star in allStar:

            # Get the star pixel position
            starX, starY = raDeclInPixel[star]

            # Transform the coordiante from DM team to camera team
            starX, starY = self.dmXY2CamXY(starX, starY)

            allStarPosX.append(starX)
            allStarPosY.append(starY)

        # Check the ccd image dimenstion
        ccdD1, ccdD2 = ccdImg.shape

        # Define the range of image
        # Get min/ max of x, y
        minX = int(min(allStarPosX))
        maxX = int(max(allStarPosX))

        minY = int(min(allStarPosY))
        maxY = int(max(allStarPosY))

        # Get the central point
        cenX = int(np.mean([minX, maxX]))
        cenY = int(np.mean([minY, maxY]))

        # Get the image dimension
        starRadiusInPixel = self.settingFile.getSetting("starRadiusInPixel")
        d1 = (maxY - minY) + 4 * starRadiusInPixel
        d2 = (maxX - minX) + 4 * starRadiusInPixel

        # Make d1 and d2 to be symmetric and even
        d = max(d1, d2)
        if (d%2 == 1):
            # Use d-1 instead of d+1 to avoid the boundary touch
            d = d-1

        # If central x or y plus d/2 will over the boundary, shift the
        # central x, y values
        cenY = self._shiftCenter(cenY, ccdD1, d / 2)
        cenY = self._shiftCenter(cenY, 0, d / 2)

        cenX = self._shiftCenter(cenX, ccdD2, d / 2)
        cenX = self._shiftCenter(cenX, 0, d / 2)

        # Get the bright star and neighboring stas image
        offsetX = cenX - d / 2
        offsetY = cenY - d / 2
        singleSciNeiImg = \
            ccdImg[int(offsetY):int(cenY + d / 2), int(offsetX):int(cenX + d / 2)]

        # Get the stars position in the new coordinate system
        # The final one is the bright star
        allStarPosX = np.array(allStarPosX) - offsetX
        allStarPosY = np.array(allStarPosY) - offsetY

        # Get the star magnitude
        mappedFilterType = mapFilterRefToG(filterType)
        magList = nbrStar.getMag(mappedFilterType)

        # Get the list of magnitude
        magRatio = np.array([])
        for star in allStar:
            neiMag = magList[star]
            magRatio = np.append(magRatio, neiMag)

        # Calculate the magnitude ratio
        magRatio = 1 / 100 ** ((magRatio - magRatio[-1]) / 5.0)

        return singleSciNeiImg, allStarPosX, allStarPosY, magRatio, offsetX, offsetY

    def _shiftCenter(self, center, boundary, distance):
        """Shift the center if its distance to boundary is less than required.

        Parameters
        ----------
        center : float
            Center point.
        boundary : float
            Boundary point.
        distance : float
            Required distance.

        Returns
        -------
        float
            Shifted center.
        """

        # Distance between the center and boundary
        delta = boundary - center

        # Shift the center if needed
        if (abs(delta) < distance):
            return boundary - np.sign(delta)*distance
        else:
            return center

    def doDeblending(self, blendedImg, allStarPosX, allStarPosY, magRatio):
        """Do the deblending.

        It is noted that the algorithm now is only for one bright star and one
        neighboring star.

        Parameters
        ----------
        blendedImg : numpy.ndarray
            Blended image.
        allStarPosX : list or numpy.ndarray
            Star's position x in pixel. The arange is [neighboring star,
            bright star].
        allStarPosY : list or numpy.ndarray
            Star's position y in pixel. The arange is [neighboring star,
            bright star].
        magRatio : list or numpy.ndarray
            Star magnitude ratio compared with the bright star. The arange is
            [neighboring stars, bright star].

        Returns
        -------
        numpy.ndarray
            Deblended image.
        float
            Pixel x of bright star.
        float
            Pixel y of bright star.

        Raises
        ------
        ValueError
            Only one neighboring star allowed.
        """

        # Check there is only one bright star and one neighboring star.
        # This is the limit of deblending algorithm now.
        if (len(magRatio) != 2):
            raise ValueError("Only one neighboring star allowed.")

        # Set the image for the deblending
        self.blendedImageDecorator.setImg(image=blendedImg)

        # Do the deblending
        imgDeblend, realcx, realcy = \
            self.blendedImageDecorator.deblendDonut((allStarPosX[0],
                                                     allStarPosY[0]))

        return imgDeblend, realcx, realcy
Example #41
0
    def testGetContentWithDefaultSetting(self):

        paramReader = ParamReader()

        content = paramReader.getContent()
        self.assertTrue(isinstance(content, dict))
Example #42
0
class TestParamReader(unittest.TestCase):
    """Test the ParamReaderYaml class."""

    def setUp(self):

        testDir = os.path.join(getModulePath(), "tests")
        self.configDir = os.path.join(testDir, "testData")
        self.fileName = "testConfigFile.yaml"

        filePath = os.path.join(self.configDir, self.fileName)
        self.paramReader = ParamReader(filePath=filePath)

        self.testTempDir = tempfile.TemporaryDirectory(dir=testDir)

    def tearDown(self):

        self.testTempDir.cleanup()

    def testGetSetting(self):

        znmax = self.paramReader.getSetting("znmax")
        self.assertEqual(znmax, 22)

    def testGetSettingWithWrongParam(self):

        self.assertRaises(ValueError, self.paramReader.getSetting, "wrongParam")

    def testGetFilePath(self):

        ansFilePath = os.path.join(self.configDir, self.fileName)
        self.assertEqual(self.paramReader.getFilePath(), ansFilePath)

    def testSetFilePath(self):

        fileName = "test.yaml"
        filePath = os.path.join(self.configDir, fileName)
        with self.assertWarns(UserWarning):
            self.paramReader.setFilePath(filePath)

        self.assertEqual(self.paramReader.getFilePath(), filePath)

    def testGetContent(self):

        content = self.paramReader.getContent()
        self.assertTrue(isinstance(content, dict))

    def testGetContentWithDefaultSetting(self):

        paramReader = ParamReader()

        content = paramReader.getContent()
        self.assertTrue(isinstance(content, dict))

    def testWriteMatToFile(self):

        self._writeMatToFile()

        numOfFile = self._getNumOfFileInFolder(self.testTempDir.name)
        self.assertEqual(numOfFile, 1)

    def _writeMatToFile(self):

        mat = np.random.rand(3, 4, 5)
        filePath = os.path.join(self.testTempDir.name, "temp.yaml")
        ParamReader.writeMatToFile(mat, filePath)

        return mat, filePath

    def _getNumOfFileInFolder(self, folder):

        return len(
            [
                name
                for name in os.listdir(folder)
                if os.path.isfile(os.path.join(folder, name))
            ]
        )

    def testWriteMatToFileWithWrongFileFormat(self):

        wrongFilePath = os.path.join(self.testTempDir.name, "temp.txt")
        self.assertRaises(
            ValueError, ParamReader.writeMatToFile, np.ones(4), wrongFilePath
        )

    def testGetMatContent(self):

        mat, filePath = self._writeMatToFile()

        self.paramReader.setFilePath(filePath)
        matInYamlFile = self.paramReader.getMatContent()

        delta = np.sum(np.abs(matInYamlFile - mat))
        self.assertLess(delta, 1e-10)

    def testGetMatContentWithDefaultSetting(self):

        paramReader = ParamReader()
        matInYamlFile = paramReader.getMatContent()

        self.assertTrue(isinstance(matInYamlFile, np.ndarray))
        self.assertEqual(len(matInYamlFile), 0)

    def testUpdateSettingSeries(self):

        znmaxValue = 20
        zn3IdxValue = [1, 2, 3]
        settingSeries = {"znmax": znmaxValue, "zn3Idx": zn3IdxValue}
        self.paramReader.updateSettingSeries(settingSeries)

        self.assertEqual(self.paramReader.getSetting("znmax"), znmaxValue)
        self.assertEqual(self.paramReader.getSetting("zn3Idx"), zn3IdxValue)

    def testUpdateSetting(self):

        value = 10
        param = "znmax"
        self.paramReader.updateSetting(param, value)

        self.assertEqual(self.paramReader.getSetting(param), value)

    def testUpdateSettingWithWrongParam(self):

        self.assertRaises(ValueError, self.paramReader.updateSetting, "wrongParam", -1)

    def testSaveSettingWithFilePath(self):

        filePath = self._saveSettingFile()

        self.assertTrue(os.path.exists(filePath))
        self.assertEqual(self.paramReader.getFilePath(), filePath)

    def _saveSettingFile(self):

        filePath = os.path.join(self.testTempDir.name, "newConfigFile.yaml")
        self.paramReader.saveSetting(filePath=filePath)

        return filePath

    def testSaveSettingWithoutFilePath(self):

        filePath = self._saveSettingFile()
        paramReader = ParamReader(filePath=filePath)

        paramReader.saveSetting()
        self.assertEqual(paramReader.getFilePath(), filePath)

        # Check the values are saved actually
        self.assertEqual(paramReader.getSetting("znmax"), 22)

        keysInContent = paramReader.getContent().keys()
        self.assertTrue("dofIdx" in keysInContent)
        self.assertTrue("zn3Idx" in keysInContent)

        self.assertEqual(paramReader.getSetting("zn3Idx"), [1] * 19)

    def testGetAbsPathNotExist(self):

        self.assertRaises(
            ValueError, self.paramReader.getAbsPath, "testFile.txt", getModulePath()
        )

    def testGetAbsPath(self):

        filePath = "README.md"
        self.assertFalse(os.path.isabs(filePath))

        filePathAbs = ParamReader.getAbsPath(filePath, getModulePath())
        self.assertTrue(os.path.isabs(filePathAbs))

    def testNonexistentFile(self):

        with self.assertWarns(UserWarning):
            paramReader = ParamReader(filePath="thisFileDoesntExists")
        self.assertEqual(len(paramReader.getContent().keys()), 0)
Example #43
0
    def testNonexistentFile(self):

        with self.assertWarns(UserWarning):
            paramReader = ParamReader(filePath="thisFileDoesntExists")
        self.assertEqual(len(paramReader.getContent().keys()), 0)
Example #44
0
class WEPCalculation(object):
    """Base class for converting the wavefront images into wavefront errors.

    There will be different implementations of this for different
    types of CCDs (normal, full array mode, comcam, cmos, shwfs).
    """

    def __init__(self, astWcsSol, camType, isrDir,
                 settingFileName="default.yaml"):
        """Construct an WEP calculation object.

        Parameters
        ----------
        astWcsSol : AstWcsSol
            AST world coordinate system (WCS) solution.
        camType : enum 'CamType'
            Camera type.
        isrDir : str
            Instrument signature remocal (ISR) directory. This directory will
            have the input and output that the data butler needs.
        settingFileName : str, optional
            Setting file name. (the default is "default.yaml".)
        """

        super().__init__()

        # This attribute is just a stakeholder here since there is no detail of
        # AST WCS solution yet
        self.astWcsSol = astWcsSol

        # ISR directory that the data butler uses
        self.isrDir = isrDir

        # Number of processors for WEP to use
        # This is just a stakeholder at this moment
        self.numOfProc = 1

        # Boresight infomation
        self.raInDeg = 0.0
        self.decInDeg = 0.0

        # Sky rotation angle
        self.rotSkyPos = 0.0

        # Sky information file for the temporary use
        self.skyFile = ""

        # Default setting file
        settingFilePath = os.path.join(getConfigDir(), settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        # Configure the WEP controller
        self.wepCntlr = self._configWepController(camType, settingFileName)

    def _configWepController(self, camType, settingFileName):
        """Configure the WEP controller.

        WEP: wavefront estimation pipeline.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        settingFileName : str
            Setting file name.

        Returns
        -------
        WepController
            Configured WEP controller.
        """

        dataCollector = CamDataCollector(self.isrDir)
        isrWrapper = CamIsrWrapper(self.isrDir)

        bscDbType = self._getBscDbType()
        sourSelc = self._configSourceSelector(camType, bscDbType,
                                              settingFileName)

        sourProc = SourceProcessor(settingFileName=settingFileName)
        wfsEsti = self._configWfEstimator(camType)

        wepCntlr = WepController(dataCollector, isrWrapper, sourSelc,
                                 sourProc, wfsEsti)

        return wepCntlr

    def _getBscDbType(self):
        """Get the bright star catalog (BSC) database type.

        Returns
        -------
        enum 'BscDbType'
            BSC database type.

        Raises
        ------
        ValueError
            The bscDb is not supported.
        """

        bscDb = self.settingFile.getSetting("bscDb")
        if (bscDb == "localDb"):
            return BscDbType.LocalDb
        elif (bscDb == "file"):
            return BscDbType.LocalDbForStarFile
        else:
            raise ValueError("The bscDb (%s) is not supported." % bscDb)

    def _configSourceSelector(self, camType, bscDbType, settingFileName):
        """Configue the source selector.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        bscDbType : enum 'BscDbType'
            Bright star catalog (BSC) database type.
        settingFileName : str
            Setting file name.

        Returns
        -------
        SourceSelector
            Configured source selector.

        Raises
        ------
        ValueError
            WEPCalculation does not support this bscDbType yet.
        """

        sourSelc = SourceSelector(camType, bscDbType,
                                  settingFileName=settingFileName)
        sourSelc.setFilter(FilterType.REF)

        if (bscDbType == BscDbType.LocalDbForStarFile):
            dbAdress = os.path.join(getModulePath(), "tests", "testData",
                                    "bsc.db3")
            sourSelc.connect(dbAdress)
        else:
            raise ValueError("WEPCalculation does not support %s yet." % bscDbType)

        return sourSelc

    def _configWfEstimator(self, camType):
        """Configure the wavefront estimator.

        Returns
        -------
        WfEstimator
            Configured wavefront estimator.
        """

        configDir = getConfigDir()
        instDir = os.path.join(configDir, "cwfs", "instData")
        algoDir = os.path.join(configDir, "cwfs", "algo")
        wfsEsti = WfEstimator(instDir, algoDir)

        solver = self.settingFile.getSetting("poissonSolver")
        opticalModel = self.settingFile.getSetting("opticalModel")
        defocalDisInMm = self.settingFile.getSetting("dofocalDistInMm")
        donutImgSizeInPixel = self.settingFile.getSetting("donutImgSizeInPixel")
        wfsEsti.config(solver=solver, camType=camType,
                       opticalModel=opticalModel,
                       defocalDisInMm=defocalDisInMm,
                       sizeInPix=donutImgSizeInPixel)

        return wfsEsti

    def getWepCntlr(self):
        """Get the configured WEP controller.

        Returns
        -------
        WepController
            Configured WEP controller.
        """

        return self.wepCntlr

    def disconnect(self):
        """Disconnect the database."""

        sourSelc = self.wepCntlr.getSourSelc()
        sourSelc.disconnect()

    def getIsrDir(self):
        """Get the instrument signature removal (ISR) directory.

        This directory will have the input and output that the data butler
        needs.

        Returns
        -------
        str
            ISR directory.
        """

        return self.isrDir

    def setSkyFile(self, skyFile):
        """Set the sky information file.

        This is a temporary function to set the star file generated by
        ts_tcs_wep_phosim module to do the query. This function will be removed
        after we begin to integrate the bright star catalog database.

        Parameters
        ----------
        skyFile : str
            Sky information file.
        """

        self.skyFile = skyFile

    def getSkyFile(self):
        """Get the sky information file.

        Returns
        -------
        str
            Sky information file.
        """

        return self.skyFile

    def setWcsData(self, wcsData):
        """Set the WCS data.

        Parameters
        ----------
        wcsData : WcsData
            WCS data used in the WCS solution.
        """

        self.astWcsSol.setWcsData(wcsData)

    def setFilter(self, filterType):
        """Set the current filter.

        Parameters
        ----------
        filterType : enum 'FilterType'
            The new filter configuration to use for WEP data processing.
        """

        sourSelc = self.wepCntlr.getSourSelc()
        sourSelc.setFilter(filterType)

    def getFilter(self):
        """Get the current filter.

        Returns
        -------
        enum 'FilterType'
            The current filter configuration to use for WEP data processing.
        """

        sourSelc = self.wepCntlr.getSourSelc()
        return sourSelc.getFilter()

    def setBoresight(self, raInDeg, decInDeg):
        """Set the boresight (ra, dec) in degree from the pointing component.

        The cooridinate system of pointing component is the international
        cannabinoid research society (ICRS).

        Parameters
        ----------
        raInDeg : float
            Right ascension in degree. The value should be in (0, 360).
        decInDeg : float
            Declination in degree. The value should be in (-90, 90).
        """

        self.raInDeg = raInDeg
        self.decInDeg = decInDeg

    def getBoresight(self):
        """Get the boresight (ra, dec) defined in the international
        cannabinoid research society (ICRS).

        Returns
        -------
        raInDeg : float
            Right ascension in degree. The value should be in (0, 360).
        decInDeg : float
            Declination in degree. The value should be in (-90, 90).
        """

        return self.raInDeg, self.decInDeg

    def setRotAng(self, rotAngInDeg):
        """Set the camera rotation angle in degree from the camera rotator
        control system.

        Parameters
        ----------
        rotAngInDeg : float
            The camera rotation angle in degree (-90 to 90).
        """

        # In the WCS solution provided by SIMS team, the input angle is sky
        # rotation angle. We do not know its relationship with the camera
        # rotation angle yet.

        self.rotSkyPos = rotAngInDeg

    def getRotAng(self):
        """Get the camera rotation angle in degree defined in the camera
        rotator control system.

        Returns
        -------
        float
            The camera rotation angle in degree.
        """

        # In the WCS solution provided by SIMS team, the input angle is sky
        # rotation angle. We do not know its relationship with the camera
        # rotation angle yet.

        return self.rotSkyPos

    def setNumOfProc(self, numOfProc):
        """Set the number of processor

        Parameters
        ----------
        numOfProc : int
            Number of processor.

        Raises
        ------
        ValueError
            Number of processor should be >=1.
        """

        # Discuss with Chris that we put this into the configuration file or
        # not

        if (numOfProc < 1):
            raise ValueError("Number of processor should be >=1.")

        self.numOfProc = numOfProc

    def calculateWavefrontErrors(self, rawExpData, extraRawExpData=None):
        """Calculate the wavefront errors.

        Parameters
        ----------
        rawExpData : RawExpData
            Raw exposure data for the corner wavefront sensor. If the input of
            extraRawExpData is not None, this input will be the intra-focal raw
            exposure data.
        extraRawExpData : RawExpData, optional
            This is the extra-focal raw exposure data if not None. (the default
            is None.)

        Returns
        -------
        list[SensorWavefrontData]
            List of SensorWavefrontData object.

        Raises
        ------
        ValueError
            Corner WFS is not supported yet.
        ValueError
            Only single visit is allowed at this time.
        """

        if (extraRawExpData is None):
            raise ValueError("Corner WFS is not supported yet.")

        if (len(rawExpData.getVisit()) != 1):
            raise ValueError("Only single visit is allowed at this time.")

        # Ingest the exposure data and do the ISR
        self._ingestImg(rawExpData)
        if (extraRawExpData is not None):
            self._ingestImg(extraRawExpData)

        self._doIsr(isrConfigfileName="isr_config.py")

        # Set the butler inputs path to get the post-ISR CCD
        rerunName = self._getIsrRerunName()
        postIsrCcdDir = os.path.join(self.isrDir, "rerun", rerunName)
        self.wepCntlr.setPostIsrCcdInputs(postIsrCcdDir)

        # Get the target stars map neighboring stars
        neighborStarMap = self._getTargetStar()

        # Calculate the wavefront error
        intraObsIdList = rawExpData.getVisit()
        intraObsId = intraObsIdList[0]
        if (extraRawExpData is None):
            obsIdList = [intraObsId]
        else:
            extraObsIdList = extraRawExpData.getVisit()
            extraObsId = extraObsIdList[0]
            obsIdList = [intraObsId, extraObsId]

        donutMap = self._calcWfErr(neighborStarMap, obsIdList)

        listOfWfErr = self._populateListOfSensorWavefrontData(donutMap)

        return listOfWfErr

    def _ingestImg(self, rawExpData):
        """Ingest the images.

        Parameters
        ----------
        rawExpData : RawExpData
            Raw exposure data.
        """

        dataCollector = self.wepCntlr.getDataCollector()

        rawExpDirList = rawExpData.getRawExpDir()
        for rawExpDir in rawExpDirList:
            rawImgFiles = os.path.join(rawExpDir, "*.fits")
            dataCollector.ingestImages(rawImgFiles)

    def _doIsr(self, isrConfigfileName):
        """Do the instrument signature removal (ISR).

        Parameters
        ----------
        isrConfigfileName : str
            ISR configuration file name.
        """

        isrWrapper = self.wepCntlr.getIsrWrapper()
        isrWrapper.config(doFlat=True, fileName=isrConfigfileName)

        rerunName = self._getIsrRerunName()
        isrWrapper.doISR(self.isrDir, rerunName=rerunName)

    def _getIsrRerunName(self):
        """Get the instrument signature removal (ISR) rerun name.

        Returns
        -------
        str
            ISR rerun name.
        """

        return self.settingFile.getSetting("rerunName")

    def _getTargetStar(self):
        """Get the target stars

        Returns
        -------
        dict
            Information of neighboring stars and candidate stars with the name
            of sensor as a dictionary.

        Raises
        ------
        ValueError
            BSC database is not supported.
        """

        sourSelc = self.wepCntlr.getSourSelc()
        sourSelc.setObsMetaData(self.raInDeg, self.decInDeg, self.rotSkyPos)

        camDimOffset = self.settingFile.getSetting("camDimOffset")

        bscDbType = self._getBscDbType()
        if (bscDbType == BscDbType.LocalDb):
            return sourSelc.getTargetStar(offset=camDimOffset)[0]
        elif (bscDbType == BscDbType.LocalDbForStarFile):
            return sourSelc.getTargetStarByFile(self.skyFile,
                                                offset=camDimOffset)[0]
        else:
            raise ValueError("BSC database (%s) is not supported." % bscDbType)

    def _calcWfErr(self, neighborStarMap, obsIdList):
        """Calculate the wavefront error.

        Only consider one intra-focal and one extra-focal images at this
        moment

        Parameters
        ----------
        neighborStarMap : dict
            Information of neighboring stars and candidate stars with the name
            of sensor as a dictionary.
        obsIdList : list[int]
            Observation Id list in [intraObsId, extraObsId]. If the input is
            [intraObsId], this means the corner WFS.

        Returns
        -------
        dict
            Donut image map with the calculated wavefront error. The dictionary
            key is the sensor name. The dictionary item is the donut image
            (type: DonutImage).
        """

        sensorNameList = list(neighborStarMap)
        wfsImgMap = self.wepCntlr.getPostIsrImgMapByPistonDefocal(
            sensorNameList, obsIdList)

        doDeblending = self.settingFile.getSetting("doDeblending")
        donutMap = self.wepCntlr.getDonutMap(
            neighborStarMap, wfsImgMap, self.getFilter(),
            doDeblending=doDeblending)

        donutMap = self.wepCntlr.calcWfErr(donutMap)

        return donutMap

    def _populateListOfSensorWavefrontData(self, donutMap):
        """Populate the list of sensor wavefront data.

        Parameters
        ----------
        donutMap : dict
            Donut image map with the calculated wavefront error. The dictionary
            key is the sensor name. The dictionary item is the donut image
            (type: DonutImage).

        Returns
        -------
        list[SensorWavefrontData]
            List of SensorWavefrontData object.
        """

        mapSensorNameAndId = MapSensorNameAndId()
        listOfWfErr = []
        for sensor, donutList in donutMap.items():

            sensorWavefrontData = SensorWavefrontData()

            # Set the sensor Id
            abbrevSensor = abbrevDectectorName(sensor)
            sensorIdList = mapSensorNameAndId.mapSensorNameToId(abbrevSensor)
            sensorId = sensorIdList[0]
            sensorWavefrontData.setSensorId(sensorId)

            sensorWavefrontData.setListOfDonut(donutList)

            # Set the average zk in um
            avgErrInNm = self.wepCntlr.calcAvgWfErrOnSglCcd(donutList)
            avgErrInUm = avgErrInNm * 1e-3
            sensorWavefrontData.setAnnularZernikePoly(avgErrInUm)

            listOfWfErr.append(sensorWavefrontData)

        return listOfWfErr

    def ingestCalibs(self, calibsDir):
        """Ingest the calibration products.

        Parameters
        ----------
        calibsDir : str
            Calibration products directory.
        """

        self._genCamMapperIfNeed()

        dataCollector = self.wepCntlr.getDataCollector()

        calibFiles = os.path.join(calibsDir, "*")
        dataCollector.ingestCalibs(calibFiles)

    def _genCamMapperIfNeed(self):
        """Generate the camera mapper file if it is needed.

        The mapper file is used by the data butler.

        Raises
        ------
        ValueError
            Mapper is not supported yet.
        """

        mapperFile = os.path.join(self.isrDir, "_mapper")
        if (not os.path.exists(mapperFile)):

            dataCollector = self.wepCntlr.getDataCollector()

            camMapper = self.settingFile.getSetting("camMapper")
            if (camMapper == "phosim"):
                dataCollector.genPhoSimMapper()
            else:
                raise ValueError("Mapper (%s) is not supported yet." % camMapper)
Example #45
0
class SourceSelector(object):

    def __init__(self, camType, bscDbType, settingFileName="default.yaml"):
        """Initialize the source selector class.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        bscDbType : enum 'BscDbType'
            Bright star catalog (BSC) database type.
        settingFileName : str, optional
            Setting file name (the default is "default.yaml".)
        """

        self.camera = CamFactory.createCam(camType)
        self.db = DatabaseFactory.createDb(bscDbType)
        self.filter = Filter()

        self.maxDistance = 0.0
        self.maxNeighboringStar = 0

        settingFilePath = os.path.join(getConfigDir(), settingFileName)
        self.settingFile = ParamReader(filePath=settingFilePath)

        # Configurate the criteria of neighboring stars
        starRadiusInPixel = self.settingFile.getSetting("starRadiusInPixel")
        spacingCoefficient = self.settingFile.getSetting("spacingCoef")
        maxNeighboringStar = self.settingFile.getSetting("maxNumOfNbrStar")
        self.configNbrCriteria(starRadiusInPixel, spacingCoefficient,
                               maxNeighboringStar=maxNeighboringStar)

    def configNbrCriteria(self, starRadiusInPixel, spacingCoefficient,
                          maxNeighboringStar=0):
        """Set the neighboring star criteria to decide the scientific target.

        Parameters
        ----------
        starRadiusInPixel : float, optional
            Diameter of star. For the defocus = 1.5 mm, the star's radius is
            63 pixel.
        spacingCoefficient : float, optional
            Maximum distance in units of radius one donut must be considered
            as a neighbor.
        maxNeighboringStar : int, optional
            Maximum number of neighboring stars. (the default is 0.)
        """

        self.maxDistance = starRadiusInPixel * spacingCoefficient
        self.maxNeighboringStar = int(maxNeighboringStar)

    def connect(self, *kwargs):
        """Connect the database.

        Parameters
        ----------
        *kwargs : str or *list
            Information to connect to the database.
        """

        self.db.connect(*kwargs)

    def disconnect(self):
        """Disconnect the database."""

        self.db.disconnect()

    def setFilter(self, filterType):
        """Set the filter type.

        Parameters
        ----------
        filterType : FilterType
            Filter type.
        """

        self.filter.setFilter(filterType)

    def getFilter(self):
        """Get the filter type.

        Returns
        -------
        FilterType
            Filter type.
        """

        return self.filter.getFilter()

    def setObsMetaData(self, ra, dec, rotSkyPos):
        """Set the observation meta data.

        Parameters
        ----------
        ra : float
            Pointing ra in degree.
        dec : float
            Pointing decl in degree.
        rotSkyPos : float
            The orientation of the telescope in degrees.
        """

        mjd = self.settingFile.getSetting("cameraMJD")
        self.camera.setObsMetaData(ra, dec, rotSkyPos, mjd=mjd)

    def getTargetStar(self, offset=0):
        """Get the target stars by querying the database.

        Parameters
        ----------
        offset : float, optional
            Offset to the dimension of camera. If the detector dimension is 10
            (assume 1-D), the star's position between -offset and 10+offset
            will be seem to be on the detector. (the default is 0.)

        Returns
        -------
        dict
            Information of neighboring stars and candidate stars with the name
            of sensor as a dictionary.
        dict
            Information of stars with the name of sensor as a dictionary.
        dict
            (ra, dec) of four corners of each sensor with the name
            of sensor as a list. The dictionary key is the sensor name.
        """

        wavefrontSensors = self.camera.getWavefrontSensor()
        lowMagnitude, highMagnitude = self.filter.getMagBoundary()

        # Map the reference filter to the G filter
        filterType = self.getFilter()
        mappedFilterType = mapFilterRefToG(filterType)

        # Query the star database
        starMap = dict()
        neighborStarMap = dict()
        for detector, wavefrontSensor in wavefrontSensors.items():

            # Get stars in this wavefront sensor for this observation field
            stars = self.db.query(mappedFilterType, wavefrontSensor[0],
                                  wavefrontSensor[1], wavefrontSensor[2],
                                  wavefrontSensor[3])

            # Set the detector information for the stars
            stars.setDetector(detector)

            # Populate pixel information for stars
            populatedStar = self.camera.populatePixelFromRADecl(stars)

            # Get the stars that are on the detector
            starsOnDet = self.camera.getStarsOnDetector(populatedStar, offset)
            starMap[detector] = starsOnDet

            # Check the candidate of bright stars based on the magnitude
            indexCandidate = starsOnDet.checkCandidateStars(
                mappedFilterType, lowMagnitude, highMagnitude)

            # Determine the neighboring stars based on the distance and
            # allowed number of neighboring stars
            neighborStar = starsOnDet.getNeighboringStar(
                indexCandidate, self.maxDistance, mappedFilterType,
                self.maxNeighboringStar)
            neighborStarMap[detector] = neighborStar

        # Remove the data that has no bright star
        self._rmDataWithoutBrightStar(neighborStarMap, starMap,
                                      wavefrontSensors)

        return neighborStarMap, starMap, wavefrontSensors

    def _rmDataWithoutBrightStar(self, neighborStarMap, starMap,
                                 wavefrontSensors):
        """Remove the data that has no bright stars on the detector.

        The data in inputs will be changed directly.

        Parameters
        ----------
        neighborStarMap : dict
            Information of neighboring stars and candidate stars with the name
            of sensor as a dictionary.
        starMap : dict
            Information of stars with the name of sensor as a dictionary.
        wavefrontSensors : dict
            (ra, dec) of four corners of each sensor with the name
            of sensor as a list. The dictionary key is the sensor name.
        """

        # Collect the sensor list without the bright star
        noStarSensorList = []
        for detector, stars in neighborStarMap.items():
            if (len(stars.getId()) == 0):
                noStarSensorList.append(detector)

        # Remove the data in map
        for detector in noStarSensorList:
            neighborStarMap.pop(detector)
            starMap.pop(detector)
            wavefrontSensors.pop(detector)

    def getTargetStarByFile(self, skyFilePath, offset=0):
        """Get the target stars by querying the star file.

        This function is only for the test. This shall be removed in the final.

        Parameters
        ----------
        skyFilePath : str
            Sky data file path.
        offset : float, optional
            Offset to the dimension of camera. If the detector dimension is 10
            (assume 1-D), the star's position between -offset and 10+offset
            will be seem to be on the detector. (the default is 0.)

        Returns
        -------
        dict
            Information of neighboring stars and candidate stars with the name
            of sensor as a dictionary.
        dict
            Information of stars with the name of sensor as a dictionary.
        dict
            (ra, dec) of four corners of each sensor with the name
            of sensor as a list. The dictionary key is the sensor name.

        Raises
        ------
        TypeError
            The database type is incorrect.
        """

        if (not isinstance(self.db, LocalDatabaseForStarFile)):
            raise TypeError("The database type is incorrect.")

        # Map the reference filter to the G filter
        filterType = self.getFilter()
        mappedFilterType = mapFilterRefToG(filterType)

        # Write the sky data into the temporary table
        self.db.createTable(mappedFilterType)
        self.db.insertDataByFile(skyFilePath, mappedFilterType, skiprows=1)
        neighborStarMap, starMap, wavefrontSensors = self.getTargetStar(offset=offset)

        # Delete the table
        self.db.deleteTable(mappedFilterType)

        return neighborStarMap, starMap, wavefrontSensors
Example #46
0
class Algorithm(object):

    def __init__(self, algoDir):
        """Initialize the Algorithm class.

        Algorithm used to solve the transport of intensity equation to get
        normal/ annular Zernike polynomials.

        Parameters
        ----------
        algoDir : str
            Algorithm configuration directory.
        """

        self.algoDir = algoDir
        self.algoParamFile = ParamReader()

        self._inst = Instrument("")

        # Show the calculation message based on this value
        # 0 means no message will be showed
        self.debugLevel = 0

        # Image has the problem or not from the over-compensation
        self.caustic = False

        # Record the Zk coefficients in each outer-loop iteration
        # The actual total outer-loop iteration time is Num_of_outer_itr + 1
        self.converge = np.array([])

        # Current number of outer-loop iteration
        self.currentItr = 0

        # Record the coefficients of normal/ annular Zernike polynomials after
        # z4 in unit of nm
        self.zer4UpNm = np.array([])

        # Converged wavefront.
        self.wcomp = np.array([])

        # Calculated wavefront in previous outer-loop iteration.
        self.West = np.array([])

        # Converged Zk coefficients
        self.zcomp = np.array([])

        # Calculated Zk coefficients in previous outer-loop iteration
        self.zc = np.array([])

        # Padded mask for use at the offset planes
        self.pMask = None

        # Non-padded mask corresponding to aperture
        self.cMask = None

        # Change the dimension of mask for fft to use
        self.pMaskPad = None
        self.cMaskPad = None

    def reset(self):
        """Reset the calculation for the new input images with the same
        algorithm settings."""

        self.caustic = False
        self.converge = np.zeros(self.converge.shape)
        self.currentItr = 0
        self.zer4UpNm = np.zeros(self.zer4UpNm.shape)

        self.wcomp = np.zeros(self.wcomp.shape)
        self.West = np.zeros(self.West.shape)

        self.zcomp = np.zeros(self.zcomp.shape)
        self.zc = np.zeros(self.zc.shape)

        self.pMask = None
        self.cMask = None

        self.pMaskPad = None
        self.cMaskPad = None

    def config(self, algoName, inst, debugLevel=0):
        """Configure the algorithm to solve TIE.

        Parameters
        ----------
        algoName : str
            Algorithm configuration file to solve the Poisson's equation in the
            transport of intensity equation (TIE). It can be "fft" or "exp"
            here.
        inst : Instrument
            Instrument to use.
        debugLevel : int, optional
            Show the information under the running. If the value is higher, the
            information shows more. It can be 0, 1, 2, or 3. (the default is
            0.)
        """

        algoParamFilePath = os.path.join(self.algoDir, "%s.yaml" % algoName)
        self.algoParamFile.setFilePath(algoParamFilePath)

        self._inst = inst
        self.debugLevel = debugLevel

        self.caustic = False

        numTerms = self.getNumOfZernikes()
        outerItr = self.getNumOfOuterItr()
        self.converge = np.zeros((numTerms, outerItr + 1))

        self.currentItr = 0

        self.zer4UpNm = np.zeros(numTerms - 3)

        # Wavefront related parameters
        dimOfDonut = self._inst.getDimOfDonutOnSensor()
        self.wcomp = np.zeros((dimOfDonut, dimOfDonut))
        self.West = self.wcomp.copy()

        # Used in model basis ("zer").
        self.zcomp = np.zeros(numTerms)
        self.zc = self.zcomp.copy()

        # Mask related variables
        self.pMask = None
        self.cMask = None
        self.pMaskPad = None
        self.cMaskPad = None

    def setDebugLevel(self, debugLevel):
        """Set the debug level.

        If the value is higher, the information shows more. It can be 0, 1, 2,
        or 3.

        Parameters
        ----------
        debugLevel : int
            Show the information under the running.
        """

        self.debugLevel = int(debugLevel)

    def getDebugLevel(self):
        """Get the debug level.

        If the value is higher, the information shows more. It can be 0, 1, 2,
        or 3.

        Returns
        -------
        int
            Debug level.
        """

        return self.debugLevel

    def getZer4UpInNm(self):
        """Get the coefficients of Zernike polynomials of z4-zn in nm.

        Returns
        -------
        numpy.ndarray
            Zernike polynomials of z4-zn in nm.
        """

        return self.zer4UpNm

    def getPoissonSolverName(self):
        """Get the method name to solve the Poisson equation.

        Returns
        -------
        str
            Method name to solve the Poisson equation.
        """

        return self.algoParamFile.getSetting("poissonSolver")

    def getNumOfZernikes(self):
        """Get the maximum number of Zernike polynomials supported.

        Returns
        -------
        int
            Maximum number of Zernike polynomials supported.
        """

        return int(self.algoParamFile.getSetting("numOfZernikes"))

    def getZernikeTerms(self):
        """Get the Zernike terms in using.

        Returns
        -------
        numpy.ndarray
            Zernkie terms in using.
        """

        numTerms = self.getNumOfZernikes()
        zTerms = np.arange(numTerms) + 1

        return zTerms

    def getObsOfZernikes(self):
        """Get the obscuration of annular Zernike polynomials.

        Returns
        -------
        float
            Obscuration of annular Zernike polynomials
        """

        zobsR = self.algoParamFile.getSetting("obsOfZernikes")
        if (zobsR == 1):
            zobsR = self._inst.getObscuration()

        return float(zobsR)

    def getNumOfOuterItr(self):
        """Get the number of outer loop iteration.

        Returns
        -------
        int
            Number of outer loop iteration.
        """

        return int(self.algoParamFile.getSetting("numOfOuterItr"))

    def getNumOfInnerItr(self):
        """Get the number of inner loop iteration.

        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        int
            Number of inner loop iteration.
        """

        return int(self.algoParamFile.getSetting("numOfInnerItr"))

    def getFeedbackGain(self):
        """Get the gain value used in the outer loop iteration.

        Returns
        -------
        float
            Gain value used in the outer loop iteration.
        """

        return self.algoParamFile.getSetting("feedbackGain")

    def getOffAxisPolyOrder(self):
        """Get the number of polynomial order supported in off-axis correction.

        Returns
        -------
        int
            Number of polynomial order supported in off-axis correction.
        """

        return int(self.algoParamFile.getSetting("offAxisPolyOrder"))

    def getCompensatorMode(self):
        """Get the method name to compensate the wavefront by wavefront error.

        Returns
        -------
        str
            Method name to compensate the wavefront by wavefront error.
        """

        return self.algoParamFile.getSetting("compensatorMode")

    def getCompSequence(self):
        """Get the compensated sequence of Zernike order for each iteration.

        Returns
        -------
        numpy.ndarray[int]
            Compensated sequence of Zernike order for each iteration.
        """

        compSequenceFromFile = self.algoParamFile.getSetting("compSequence")
        compSequence = np.array(compSequenceFromFile, dtype=int)

        # If outerItr is large, and compSequence is too small,
        # the rest in compSequence will be filled.
        # This is used in the "zer" method.
        outerItr = self.getNumOfOuterItr()
        compSequence = self._extend1dArray(compSequence, outerItr)
        compSequence = compSequence.astype(int)

        return compSequence

    def _extend1dArray(self, origArray, targetLength):
        """Extend the 1D original array to the taget length.

        The extended value will be the final element of original array. Nothing
        will be done if the input array is not 1D or its length is less than
        the target.

        Parameters
        ----------
        origArray : numpy.ndarray
            Original array with 1 dimension.
        targetLength : int
            Target length of new extended array.

        Returns
        -------
        numpy.ndarray
            Extended 1D array.
        """

        if (len(origArray) < targetLength) and (origArray.ndim == 1):
            leftOver = np.ones(targetLength - len(origArray))
            extendArray = np.append(origArray, origArray[-1] * leftOver)
        else:
            extendArray = origArray

        return extendArray

    def getBoundaryThickness(self):
        """Get the boundary thickness that the computation mask extends beyond
        the pupil mask.

        It is noted that in Fast Fourier transform (FFT) algorithm, it is also
        the width of Neuman boundary where the derivative of the wavefront is
        set to zero

        Returns
        -------
        int
            Boundary thickness.
        """

        return int(self.algoParamFile.getSetting("boundaryThickness"))

    def getFftDimension(self):
        """Get the FFT pad dimension in pixel.

        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        int
            FFT pad dimention.
        """

        fftDim = int(self.algoParamFile.getSetting("fftDimension"))

        # Make sure the dimension is the order of multiple of 2
        if (fftDim == 999):
            dimToFit = self._inst.getDimOfDonutOnSensor()
        else:
            dimToFit = fftDim

        padDim = int(2**np.ceil(np.log2(dimToFit)))

        return padDim

    def getSignalClipSequence(self):
        """Get the signal clip sequence.

        The number of values should be the number of compensation plus 1.
        This is for the fast Fourier transform (FFT) solver only.

        Returns
        -------
        numpy.ndarray
            Signal clip sequence.
        """

        sumclipSequenceFromFile = self.algoParamFile.getSetting("signalClipSequence")
        sumclipSequence = np.array(sumclipSequenceFromFile)

        # If outerItr is large, and sumclipSequence is too small, the rest in
        # sumclipSequence will be filled.
        # This is used in the "zer" method.
        targetLength = self.getNumOfOuterItr() + 1
        sumclipSequence = self._extend1dArray(sumclipSequence, targetLength)

        return sumclipSequence

    def getMaskScalingFactor(self):
        """Get the mask scaling factor for fast beam.

        Returns
        -------
        float
            Mask scaling factor for fast beam.
        """

        # m = R'*f/(l*R), R': radius of the no-aberration image
        focalLength = self._inst.getFocalLength()
        marginalFL = self._inst.getMarginalFocalLength()
        maskScalingFactor = focalLength / marginalFL

        return maskScalingFactor

    def itr0(self, I1, I2, model):
        """Calculate the wavefront and coefficients of normal/ annular Zernike
        polynomials in the first iteration time.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".
        """

        # Reset the iteration time of outer loop and decide to reset the
        # defocal images or not
        self._reset(I1, I2)

        # Solve the transport of intensity equation (TIE)
        self._singleItr(I1, I2, model)

    def runIt(self, I1, I2, model, tol=1e-3):
        """Calculate the wavefront error by solving the transport of intensity
        equation (TIE).

        The inner (for fft algorithm) and outer loops are used. The inner loop
        is to solve the Poisson's equation. The outer loop is to compensate the
        intra- and extra-focal images to mitigate the calculation of wavefront
        (e.g. S = -1/(delta Z) * (I1 - I2)/ (I1 + I2)).

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".
        tol : float, optional
            Tolerance of difference of coefficients of Zk polynomials compared
            with the previours iteration. (the default is 1e-3.)
        """

        # To have the iteration time initiated from global variable is to
        # distinguish the manually and automatically iteration processes.
        itr = self.currentItr
        while (itr <= self.getNumOfOuterItr()):
            stopItr = self._singleItr(I1, I2, model, tol)

            # Stop the iteration of outer loop if converged
            if (stopItr):
                break

            itr += 1

    def nextItr(self, I1, I2, model, nItr=1):
        """Run the outer loop iteration with the specific time defined in nItr.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".
        nItr : int, optional
            Outer loop iteration time. (the default is 1.)
        """

        #  Do the iteration
        ii = 0
        while (ii < nItr):
            self._singleItr(I1, I2, model)
            ii += 1

    def _singleItr(self, I1, I2, model, tol=1e-3):
        """Run the outer-loop with single iteration to solve the transport of
        intensity equation (TIE).

        This is to compensate the approximation of wavefront:
        S = -1/(delta Z) * (I1 - I2)/ (I1 + I2)).

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        model : str
            Optical model. It can be "paraxial", "onAxis", or "offAxis".
        tol : float, optional
            Tolerance of difference of coefficients of Zk polynomials compared
            with the previours iteration. (the default is 1e-3.)

        Returns
        -------
        bool
            Status of iteration.
        """

        # Use the zonal mode ("zer")
        compMode = self.getCompensatorMode()

        # Define the gain of feedbackGain
        feedbackGain = self.getFeedbackGain()

        # Set the pre-condition
        if (self.currentItr == 0):

            # Check this is the first time of running iteration or not
            if (I1.getImgInit() is None or I2.getImgInit() is None):

                # Check the image dimension
                if (I1.getImg().shape != I2.getImg().shape):
                    print("Error: The intra and extra image stamps need to be of same size.")
                    sys.exit()

                # Calculate the pupil mask (binary matrix) and related
                # parameters
                boundaryT = self.getBoundaryThickness()
                I1.makeMask(self._inst, model, boundaryT, 1)
                I2.makeMask(self._inst, model, boundaryT, 1)
                self._makeMasterMask(I1, I2, self.getPoissonSolverName())

                # Load the offAxis correction coefficients
                if (model == "offAxis"):
                    offAxisPolyOrder = self.getOffAxisPolyOrder()
                    I1.setOffAxisCorr(self._inst, offAxisPolyOrder)
                    I2.setOffAxisCorr(self._inst, offAxisPolyOrder)

                # Cocenter the images to the center referenced to fieldX and
                # fieldY. Need to check the availability of this.
                I1.imageCoCenter(self._inst, debugLevel=self.debugLevel)
                I2.imageCoCenter(self._inst, debugLevel=self.debugLevel)

                # Update the self-initial image
                I1.updateImgInit()
                I2.updateImgInit()

            # Initialize the variables used in the iteration.
            self.zcomp = np.zeros(self.getNumOfZernikes())
            self.zc = self.zcomp.copy()

            dimOfDonut = self._inst.getDimOfDonutOnSensor()
            self.wcomp = np.zeros((dimOfDonut, dimOfDonut))
            self.West = self.wcomp.copy()

            self.caustic = False

        # Rename this index (currentItr) for the simplification
        jj = self.currentItr

        # Solve the transport of intensity equation (TIE)
        if (not self.caustic):

            # Reset the images before the compensation
            I1.updateImage(I1.getImgInit().copy())
            I2.updateImage(I2.getImgInit().copy())

            if (compMode == "zer"):

                # Zk coefficient from the previous iteration
                ztmp = self.zc

                # Do the feedback of Zk from the lower terms first based on the
                # sequence defined in compSequence
                if (jj != 0):
                    compSequence = self.getCompSequence()
                    ztmp[int(compSequence[jj - 1]):] = 0

                # Add partial feedback of residual estimated wavefront in Zk
                self.zcomp = self.zcomp + ztmp*feedbackGain

                # Remove the image distortion if the optical model is not
                # "paraxial"
                # Only the optical model of "onAxis" or "offAxis" is considered
                # here
                I1.compensate(self._inst, self, self.zcomp, model)
                I2.compensate(self._inst, self, self.zcomp, model)

            # Check the image condition. If there is the problem, done with
            # this _singleItr().
            if (I1.isCaustic() is True) or (I2.isCaustic() is True):
                self.converge[:, jj] = self.converge[:, jj - 1]
                self.caustic = True
                return

            # Correct the defocal images if I1 and I2 are belong to different
            # sources, which is determined by the (fieldX, field Y)
            I1, I2 = self._applyI1I2pMask(I1, I2)

            # Solve the Poisson's equation
            self.zc, self.West = self._solvePoissonEq(I1, I2, jj)

            # Record/ calculate the Zk coefficient and wavefront
            if (compMode == "zer"):
                self.converge[:, jj] = self.zcomp + self.zc

                xoSensor, yoSensor = self._inst.getSensorCoorAnnular()
                self.wcomp = self.West + ZernikeAnnularEval(
                    np.concatenate(([0, 0, 0], self.zcomp[3:])), xoSensor,
                    yoSensor, self.getObsOfZernikes())

        else:
            # Once we run into caustic, stop here, results may be close to real
            # aberration.
            # Continuation may lead to disatrous results.
            self.converge[:, jj] = self.converge[:, jj - 1]

        # Record the coefficients of normal/ annular Zernike polynomials after
        # z4 in unit of nm
        self.zer4UpNm = self.converge[3:, jj]*1e9

        # Status of iteration
        stopItr = False

        # Calculate the difference
        if (jj > 0):
            diffZk = np.sum(np.abs(self.converge[:, jj]-self.converge[:, jj-1]))*1e9

            # Check the Status of iteration
            if (diffZk < tol):
                stopItr = True

        # Update the current iteration time
        self.currentItr += 1

        # Show the Zk coefficients in interger in each iteration
        if (self.debugLevel >= 2):
            tmp = self.zer4UpNm
            print("itr = %d, z4-z%d" % (jj, self.getNumOfZernikes()))
            print(np.rint(tmp))

        return stopItr

    def _solvePoissonEq(self, I1, I2, iOutItr=0):
        """Solve the Poisson's equation by Fourier transform (differential) or
        serial expansion (integration).

        There is no convergence for fft actually. Need to add the difference
        comparison and X-alpha method. Need to discuss further for this.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        iOutItr : int, optional
            ith number of outer loop iteration which is important in "fft"
            algorithm. (the default is 0.)

        Returns
        -------
        numpy.ndarray
            Coefficients of normal/ annular Zernike polynomials.
        numpy.ndarray
            Estimated wavefront.
        """

        # Calculate the aperature pixel size
        apertureDiameter = self._inst.getApertureDiameter()
        sensorFactor = self._inst.getSensorFactor()
        dimOfDonut = self._inst.getDimOfDonutOnSensor()
        aperturePixelSize = apertureDiameter*sensorFactor/dimOfDonut

        # Calculate the differential Omega
        dOmega = aperturePixelSize**2

        # Solve the Poisson's equation based on the type of algorithm
        numTerms = self.getNumOfZernikes()
        zobsR = self.getObsOfZernikes()
        PoissonSolver = self.getPoissonSolverName()
        if (PoissonSolver == "fft"):

            # Use the differential method by fft to solve the Poisson's
            # equation

            # Parameter to determine the threshold of calculating I0.
            sumclipSequence = self.getSignalClipSequence()
            cliplevel = sumclipSequence[iOutItr]

            # Generate the v, u-coordinates on pupil plane
            padDim = self.getFftDimension()
            v, u = np.mgrid[
                -0.5/aperturePixelSize: 0.5/aperturePixelSize: 1./padDim/aperturePixelSize,
                -0.5/aperturePixelSize: 0.5/aperturePixelSize: 1./padDim/aperturePixelSize]

            # Show the threshold and pupil coordinate information
            if (self.debugLevel >= 3):
                print("iOuter=%d, cliplevel=%4.2f" % (iOutItr, cliplevel))
                print(v.shape)

            # Calculate the const of fft:
            # FT{Delta W} = -4*pi^2*(u^2+v^2) * FT{W}
            u2v2 = -4 * (np.pi**2) * (u*u + v*v)

            # Set origin to Inf to result in 0 at origin after filtering
            ctrIdx = int(np.floor(padDim/2.0))
            u2v2[ctrIdx, ctrIdx] = np.inf

            # Calculate the wavefront signal
            Sini = self._createSignal(I1, I2, cliplevel)

            # Find the just-outside and just-inside indices of a ring in pixels
            # This is for the use in setting dWdn = 0
            boundaryT = self.getBoundaryThickness()

            struct = generate_binary_structure(2, 1)
            struct = iterate_structure(struct, boundaryT)

            ApringOut = np.logical_xor(binary_dilation(self.pMask, structure=struct),
                                       self.pMask).astype(int)
            ApringIn = np.logical_xor(binary_erosion(self.pMask, structure=struct),
                                      self.pMask).astype(int)

            bordery, borderx = np.nonzero(ApringOut)

            # Put the signal in boundary (since there's no existing Sestimate,
            # S just equals self.S as the initial condition of SCF
            S = Sini.copy()
            for jj in range(self.getNumOfInnerItr()):

                # Calculate FT{S}
                SFFT = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(S)))

                # Calculate W by W=IFT{ FT{S}/(-4*pi^2*(u^2+v^2)) }
                W = np.fft.fftshift(np.fft.irfft2(np.fft.fftshift(SFFT/u2v2), s=S.shape))

                # Estimate the wavefront (includes zeroing offset & masking to
                # the aperture size)

                # Take the estimated wavefront
                West = extractArray(W, dimOfDonut)

                # Calculate the offset
                offset = West[self.pMask == 1].mean()
                West = West - offset
                West[self.pMask == 0] = 0

                # Set dWestimate/dn = 0 around boundary
                WestdWdn0 = West.copy()

                # Do a 3x3 average around each border pixel, including only
                # those pixels inside the aperture
                for ii in range(len(borderx)):
                    reg = West[borderx[ii] - boundaryT:
                               borderx[ii] + boundaryT + 1,
                               bordery[ii] - boundaryT:
                               bordery[ii] + boundaryT + 1]

                    intersectIdx = ApringIn[borderx[ii] - boundaryT:
                                            borderx[ii] + boundaryT + 1,
                                            bordery[ii] - boundaryT:
                                            bordery[ii] + boundaryT + 1]

                    WestdWdn0[borderx[ii], bordery[ii]] = \
                        reg[np.nonzero(intersectIdx)].mean()

                # Take Laplacian to find sensor signal estimate (Delta W = S)
                del2W = laplace(WestdWdn0)/dOmega

                # Extend the dimension of signal to the order of 2 for "fft" to
                # use
                Sest = padArray(del2W, padDim)

                # Put signal back inside boundary, leaving the rest of
                # Sestimate
                Sest[self.pMaskPad == 1] = Sini[self.pMaskPad == 1]

                # Need to recheck this condition
                S = Sest

            # Define the estimated wavefront
            # self.West = West.copy()

            # Calculate the coefficient of normal/ annular Zernike polynomials
            if (self.getCompensatorMode() == "zer"):
                xSensor, ySensor = self._inst.getSensorCoor()
                zc = ZernikeMaskedFit(West, xSensor, ySensor, numTerms,
                                      self.pMask, zobsR)
            else:
                zc = np.zeros(numTerms)

        elif (PoissonSolver == "exp"):

            # Use the integration method by serial expansion to solve the
            # Poisson's equation

            # Calculate I0 and dI
            I0, dI = self._getdIandI(I1, I2)

            # Get the x, y coordinate in mask. The element outside mask is 0.
            xSensor, ySensor = self._inst.getSensorCoor()
            xSensor = xSensor * self.cMask
            ySensor = ySensor * self.cMask

            # Create the F matrix and Zernike-related matrixes
            F = np.zeros(numTerms)
            dZidx = np.zeros((numTerms, dimOfDonut, dimOfDonut))
            dZidy = dZidx.copy()

            zcCol = np.zeros(numTerms)
            for ii in range(int(numTerms)):

                # Calculate the matrix for each Zk related component
                # Set the specific Zk cofficient to be 1 for the calculation
                zcCol[ii] = 1

                F[ii] = np.sum(dI*ZernikeAnnularEval(zcCol, xSensor, ySensor, zobsR))*dOmega
                dZidx[ii, :, :] = ZernikeAnnularGrad(zcCol, xSensor, ySensor, zobsR, "dx")
                dZidy[ii, :, :] = ZernikeAnnularGrad(zcCol, xSensor, ySensor, zobsR, "dy")

                # Set the specific Zk cofficient back to 0 to avoid interfering
                # other Zk's calculation
                zcCol[ii] = 0

            # Calculate Mij matrix, need to check the stability of integration
            # and symmetry later
            Mij = np.zeros([numTerms, numTerms])
            for ii in range(numTerms):
                for jj in range(numTerms):
                    Mij[ii, jj] = np.sum(I0*(dZidx[ii, :, :].squeeze()*dZidx[jj, :, :].squeeze() +
                                             dZidy[ii, :, :].squeeze()*dZidy[jj, :, :].squeeze()))
            Mij = dOmega/(apertureDiameter/2.)**2 * Mij

            # Calculate dz
            focalLength = self._inst.getFocalLength()
            offset = self._inst.getDefocalDisOffset()
            dz = 2*focalLength*(focalLength-offset)/offset

            # Define zc
            zc = np.zeros(numTerms)

            # Consider specific Zk terms only
            idx = (self.getZernikeTerms() - 1).tolist()

            # Solve the equation: M*W = F => W = M^(-1)*F
            zc_tmp = np.linalg.lstsq(Mij[:, idx][idx], F[idx], rcond=None)[0]/dz
            zc[idx] = zc_tmp

            # Estimate the wavefront surface based on z4 - z22
            # z0 - z3 are set to be 0 instead
            West = ZernikeAnnularEval(np.concatenate(([0, 0, 0], zc[3:])),
                                      xSensor, ySensor, zobsR)

        return zc, West

    def _createSignal(self, I1, I2, cliplevel):
        """Calculate the wavefront singal for "fft" to use in solving the
        Poisson's equation.

        Need to discuss the method to define threshold and discuss to use
        np.median() instead.
        Need to discuss why the calculation of I0 is different from "exp".

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        cliplevel : float
            Parameter to determine the threshold of calculating I0.

        Returns
        -------
        numpy.ndarray
            Approximated wavefront signal.
        """

        # Check the condition of images
        I1image, I2image = self._checkImageDim(I1, I2)

        # Wavefront signal S=-(1/I0)*(dI/dz) is approximated to be
        # -(1/delta z)*(I1-I2)/(I1+I2)
        num = I1image - I2image
        den = I1image + I2image

        # Define the effective minimum central signal element by the threshold
        # ( I0=(I1+I2)/2 )

        # Calculate the threshold
        pixelList = den * self.cMask
        pixelList = pixelList[pixelList != 0]

        low = pixelList.min()
        high = pixelList.max()
        medianThreshold = (high-low)/2. + low

        # Define the effective minimum central signal element
        den[den < medianThreshold*cliplevel] = 1.5*medianThreshold

        # Calculate delta z = f(f-l)/l, f: focal length, l: defocus distance of
        # the image planes
        focalLength = self._inst.getFocalLength()
        offset = self._inst.getDefocalDisOffset()
        deltaZ = focalLength*(focalLength-offset)/offset

        # Calculate the wavefront signal. Enforce the element outside the mask
        # to be 0.
        den[den == 0] = np.inf

        # Calculate the wavefront signal
        S = num/den/deltaZ

        # Extend the dimension of signal to the order of 2 for "fft" to use
        padDim = self.getFftDimension()
        Sout = padArray(S, padDim)*self.cMaskPad

        return Sout

    def _getdIandI(self, I1, I2):
        """Calculate the central image and differential image to be used in the
        serial expansion method.

        It is noted that the images are assumed to be co-center already. And
        the intra-/ extra-focal image can overlap with one another after the
        rotation of 180 degree.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            Image data of I0.
        numpy.ndarray
            Differential image (dI) of I0.
        """

        # Check the condition of images
        I1image, I2image = self._checkImageDim(I1, I2)

        # Calculate the central image and differential iamge
        I0 = (I1image+I2image)/2
        dI = I2image-I1image

        return I0, dI

    def _checkImageDim(self, I1, I2):
        """Check the dimension of images.

        It is noted that the I2 image is rotated by 180 degree.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            I1 defocal image.
        numpy.ndarray
            I2 defocal image. It is noted that the I2 image is rotated by 180
            degree.

        Raises
        ------
        Exception
            Check the dimension of images is n by n or not.
        Exception
            Check two defocal images have the same size or not.
        """

        # Check the condition of images
        m1, n1 = I1.getImg().shape
        m2, n2 = I2.getImg().shape

        if (m1 != n1 or m2 != n2):
            raise Exception("Image is not square.")

        if (m1 != m2 or n1 != n2):
            raise Exception("Images do not have the same size.")

        # Define I1
        I1image = I1.getImg()

        # Rotate the image by 180 degree through rotating two times of 90
        # degree
        I2image = np.rot90(I2.getImg(), k=2)

        return I1image, I2image

    def _makeMasterMask(self, I1, I2, poissonSolver=None):
        """Calculate the common mask of defocal images.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        poissonSolver : str, optional
            Algorithm to solve the Poisson's equation. If the "fft" is used,
            the mask dimension will be extended to the order of 2 for the "fft"
            to use. (the default is None.)
        """

        # Get the overlap region of mask for intra- and extra-focal images.
        # This is to avoid the anormalous signal due to difference in
        # vignetting.
        self.pMask = I1.getPaddedMask() * I2.getPaddedMask()
        self.cMask = I1.getNonPaddedMask() * I2.getNonPaddedMask()

        # Change the dimension of image for fft to use
        if (poissonSolver == "fft"):
            padDim = self.getFftDimension()
            self.pMaskPad = padArray(self.pMask, padDim)
            self.cMaskPad = padArray(self.cMask, padDim)

    def _applyI1I2pMask(self, I1, I2):
        """Correct the defocal images if I1 and I2 are belong to different
        sources.

        (There is a problem for this actually. If I1 and I2 come from different
        sources, what should the correction of TIE be? At this moment, the
        fieldX and fieldY of I1 and I2 should be different. And the sources are
        different also.)

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.

        Returns
        -------
        numpy.ndarray
            Corrected I1 image.
        numpy.ndarray
            Corrected I2 image.
        """

        # Get the overlap region of images and do the normalization.
        if (I1.fieldX != I2.fieldX or I1.fieldY != I2.fieldY):

            # Get the overlap region of image
            I1.updateImage(I1.getImg()*self.pMask)

            # Rotate the image by 180 degree through rotating two times of 90
            # degree
            I2.updateImage(I2.getImg()*np.rot90(self.pMask, 2))

            # Do the normalization of image.
            I1.updateImage(I1.getImg()/np.sum(I1.getImg()))
            I2.updateImage(I2.getImg()/np.sum(I2.getImg()))

        # Return the correct images. It is noted that there is no need of
        # vignetting correction.
        # This is after masking already in _singleItr() or itr0().
        return I1, I2

    def _reset(self, I1, I2):
        """Reset the iteration time of outer loop and defocal images.

        Parameters
        ----------
        I1 : Image
            Intra- or extra-focal image.
        I2 : Image
            Intra- or extra-focal image.
        """

        # Reset the current iteration time to 0
        self.currentItr = 0

        # Show the reset information
        if (self.debugLevel >= 3):
            print("Resetting images: I1 and I2")

        # Determine to reset the images or not based on the existence of
        # the attribute: Image.image0. Only after the first run of
        # inner loop, this attribute will exist.
        try:
            # Reset the images to the first beginning
            I1.updateImage(I1.getImgInit().copy())
            I2.updateImage(I2.getImgInit().copy())

            # Show the information of resetting image
            if (self.debugLevel >= 3):
                print("Resetting images in inside.")

        except AttributeError:
            # Show the information of no image0
            if (self.debugLevel >= 3):
                print("Image0 = None. This is the first time to run the code.")

            pass

    def outZer4Up(self, unit="nm", filename=None, showPlot=False):
        """Put the coefficients of normal/ annular Zernike polynomials on
        terminal or file ande show the image if it is needed.

        Parameters
        ----------
        unit : str, optional
            Unit of the coefficients of normal/ annular Zernike polynomials. It
            can be m, nm, or um. (the default is "nm".)
        filename : str, optional
            Name of output file. (the default is None.)
        showPlot : bool, optional
            Decide to show the plot or not. (the default is False.)
        """

        # List of Zn,m
        Znm = ["Z0,0", "Z1,1", "Z1,-1", "Z2,0", "Z2,-2", "Z2,2", "Z3,-1",
               "Z3,1", "Z3,-3", "Z3,3", "Z4,0", "Z4,2", "Z4,-2", "Z4,4",
               "Z4,-4", "Z5,1", "Z5,-1", "Z5,3", "Z5,-3", "Z5,5", "Z5,-5",
               "Z6,0"]

        # Decide the format of z based on the input unit (m, nm, or um)
        if (unit == "m"):
            z = self.zer4UpNm*1e-9
        elif (unit == "nm"):
            z = self.zer4UpNm
        elif (unit == "um"):
            z = self.zer4UpNm*1e-3
        else:
            print("Unknown unit: %s" % unit)
            print("Unit options are: m, nm, um")
            return

        # Write the coefficients into a file if needed.
        if (filename is not None):
            f = open(filename, "w")
        else:
            f = sys.stdout

        for ii in range(4, len(z)+4):
            f.write("Z%d (%s)\t %8.3f\n" % (ii, Znm[ii-1], z[ii-4]))

        # Close the file
        if (filename is not None):
            f.close()

        # Show the plot
        if (showPlot):
            plt.figure()

            x = range(4, len(z) + 4)
            plt.plot(x, z, marker="o", color="r", markersize=10)
            plt.xlabel("Zernike Index")
            plt.ylabel("Zernike coefficient (%s)" % unit)
            plt.grid()
            plt.show()
Example #47
0
class Instrument(object):
    def __init__(self, instDir):
        """Instrument class for wavefront estimation.

        Parameters
        ----------
        instDir : str
            Instrument configuration directory.
        """

        self.instDir = instDir
        self.instName = ""
        self.dimOfDonutImg = 0
        self.announcedDefocalDisInMm = 0.0

        self.instParamFile = ParamReader()
        self.maskParamFile = ParamReader()

        self.xSensor = np.array([])
        self.ySensor = np.array([])

        self.xoSensor = np.array([])
        self.yoSensor = np.array([])

    def config(
        self,
        camType,
        dimOfDonutImgOnSensor,
        announcedDefocalDisInMm=1.5,
        instParamFileName="instParam.yaml",
        maskMigrateFileName="maskMigrate.yaml",
    ):
        """Do the configuration of Instrument.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.
        dimOfDonutImgOnSensor : int
            Dimension of donut image on sensor in pixel.
        announcedDefocalDisInMm : float
            Announced defocal distance in mm. It is noted that the defocal
            distance offset used in calculation might be different from this
            value. (the default is 1.5.)
        instParamFileName : str, optional
            Instrument parameter file name. (the default is "instParam.yaml".)
        maskMigrateFileName : str, optional
            Mask migration (off-axis correction) file name. (the default is
            "maskMigrate.yaml".)
        """

        self.instName = self._getInstName(camType)
        self.dimOfDonutImg = int(dimOfDonutImgOnSensor)
        self.announcedDefocalDisInMm = announcedDefocalDisInMm

        # Path of instrument param file
        instFileDir = self.getInstFileDir()
        instParamFilePath = os.path.join(instFileDir, instParamFileName)
        self.instParamFile.setFilePath(instParamFilePath)

        # Path of mask off-axis correction file
        # There is no such file for auxiliary telescope
        maskParamFilePath = os.path.join(instFileDir, maskMigrateFileName)
        if os.path.exists(maskParamFilePath):
            self.maskParamFile.setFilePath(maskParamFilePath)

        self._setSensorCoor()
        self._setSensorCoorAnnular()

    def _getInstName(self, camType):
        """Get the instrument name.

        Parameters
        ----------
        camType : enum 'CamType'
            Camera type.

        Returns
        -------
        str
            Instrument name.

        Raises
        ------
        ValueError
            Camera type is not supported.
        """

        if camType == CamType.LsstCam:
            return "lsst"
        elif camType == CamType.LsstFamCam:
            return "lsstfam"
        elif camType == CamType.ComCam:
            return "comcam"
        elif camType == CamType.AuxTel:
            return "auxTel"
        else:
            raise ValueError("Camera type (%s) is not supported." % camType)

    def getInstFileDir(self):
        """Get the instrument parameter file directory.

        Returns
        -------
        str
            Instrument parameter file directory.
        """

        return os.path.join(self.instDir, self.instName)

    def _setSensorCoor(self):
        """Set the sensor coordinate."""

        # 0.5 is the half of single pixel
        ySensorGrid, xSensorGrid = np.mgrid[-(self.dimOfDonutImg / 2 - 0.5):(
            self.dimOfDonutImg / 2 +
            0.5), -(self.dimOfDonutImg / 2 - 0.5):(self.dimOfDonutImg / 2 +
                                                   0.5), ]

        sensorFactor = self.getSensorFactor()
        denominator = self.dimOfDonutImg / 2 / sensorFactor

        self.xSensor = xSensorGrid / denominator
        self.ySensor = ySensorGrid / denominator

    def _setSensorCoorAnnular(self):
        """Set the sensor coordinate with the annular aperature."""

        self.xoSensor = self.xSensor.copy()
        self.yoSensor = self.ySensor.copy()

        # Get the position index that is out of annular aperature range
        obscuration = self.getObscuration()
        r2Sensor = self.xSensor**2 + self.ySensor**2
        idx = (r2Sensor > 1) | (r2Sensor < obscuration**2)

        # Define the value to be NaN if it is not in pupul
        self.xoSensor[idx] = np.nan
        self.yoSensor[idx] = np.nan

    def setAnnDefocalDisInMm(self, annDefocalDisInMm):
        """Set the announced defocal distance in mm.

        Parameters
        ----------
        annDefocalDisInMm : float
            Announced defocal distance in mm.
        """

        self.announcedDefocalDisInMm = annDefocalDisInMm

    def getAnnDefocalDisInMm(self):
        """Get the announced defocal distance in mm.

        Returns
        -------
        float
            Announced defocal distance in mm.
        """

        return self.announcedDefocalDisInMm

    def getInstFilePath(self):
        """Get the instrument parameter file path.

        Returns
        -------
        str
            Instrument parameter file path.
        """

        return self.instParamFile.getFilePath()

    def getMaskOffAxisCorr(self):
        """Get the mask off-axis correction.

        Returns
        -------
        numpy.ndarray
            Mask off-axis correction.
        """

        return self.maskParamFile.getMatContent()

    def getDimOfDonutOnSensor(self):
        """Get the dimension of donut image size on sensor in pixel.

        Returns
        -------
        int
            Dimension of donut's size on sensor in pixel.
        """

        return self.dimOfDonutImg

    def getObscuration(self):
        """Get the obscuration.

        Returns
        -------
        float
            Obscuration.
        """

        return self.instParamFile.getSetting("obscuration")

    def getFocalLength(self):
        """Get the focal length of telescope in meter.

        Returns
        -------
        float
            Focal length of telescope in meter.
        """

        return self.instParamFile.getSetting("focalLength")

    def getApertureDiameter(self):
        """Get the aperture diameter in meter.

        Returns
        -------
        float
            Aperture diameter in meter.
        """

        return self.instParamFile.getSetting("apertureDiameter")

    def getDefocalDisOffset(self):
        """Get the defocal distance offset in meter.

        Returns
        -------
        float
            Defocal distance offset in meter.
        """

        # Use this way to differentiate the announced defocal distance
        # and the used defocal distance in the fitting of parameters by ZEMAX
        # For example, in the ComCam's instParam.yaml file, they are a little
        # different
        offset = self.instParamFile.getSetting("offset")
        defocalDisInMm = "%.1fmm" % self.announcedDefocalDisInMm

        return offset[defocalDisInMm]

    def getCamPixelSize(self):
        """Get the camera pixel size in meter.

        Returns
        -------
        float
            Camera pixel size in meter.
        """

        return self.instParamFile.getSetting("pixelSize")

    def getMarginalFocalLength(self):
        """Get the marginal focal length in meter.

        Marginal_focal_length = sqrt(f^2 - (D/2)^2)

        Returns
        -------
        float
            Marginal focal length in meter.
        """

        focalLength = self.getFocalLength()
        apertureDiameter = self.getApertureDiameter()
        marginalFL = np.sqrt(focalLength**2 - (apertureDiameter / 2)**2)

        return marginalFL

    def getSensorFactor(self):
        """Get the sensor factor.

        Returns
        -------
        float
            Sensor factor.
        """

        offset = self.getDefocalDisOffset()
        apertureDiameter = self.getApertureDiameter()
        focalLength = self.getFocalLength()
        pixelSize = self.getCamPixelSize()
        sensorFactor = self.dimOfDonutImg / (offset * apertureDiameter /
                                             focalLength / pixelSize)

        return sensorFactor

    def getSensorCoor(self):
        """Get the sensor coordinate.

        Returns
        -------
        numpy.ndarray
            X coordinate.
        numpy.ndarray
            Y coordinate.
        """

        return self.xSensor, self.ySensor

    def getSensorCoorAnnular(self):
        """Get the sensor coordinate with the annular aperature.

        Returns
        -------
        numpy.ndarray
            X coordinate.
        numpy.ndarray
            Y coordinate.
        """

        return self.xoSensor, self.yoSensor

    def calcSizeOfDonutExpected(self):
        """Calculate the size of expected donut (diameter).

        Returns
        -------
        float
            Size of expected donut (diameter) in pixel.
        """

        offset = self.getDefocalDisOffset()
        fNumber = self.getFocalLength() / self.getApertureDiameter()
        pixelSize = self.getCamPixelSize()

        return offset / fNumber / pixelSize
Example #48
0
class MapSensorNameAndId(object):

    def __init__(self, sensorNameToIdFileName="sensorNameToId.yaml"):
        """Construct a MapSensorNameAndId object.

        Parameters
        ----------
        sensorNameToIdFileName : str, optional
            Configuration file name to map sensor name and Id. (the default is
            "sensorNameToId.yaml".)
        """

        sensorNameToIdFilePath = os.path.join(getConfigDir(),
                                              sensorNameToIdFileName)
        self._sensorNameToIdFile = ParamReader(filePath=sensorNameToIdFilePath)

    def mapSensorNameToId(self, sensorName):
        """Map the sensor name to sensor Id.

        Parameters
        ----------
        sensorName : list[str] or str
            List or string of abbreviated sensor names.

        Returns
        -------
        list[int]
            List of sensor Id.
        """

        sensorNameList = self._changeToListIfNeed(sensorName)

        sensorIdList = []
        for sensor in sensorNameList:
            sensorId = self._sensorNameToIdFile.getSetting(sensor)
            sensorIdList.append(sensorId)

        return sensorIdList

    def _changeToListIfNeed(self, inputArg):
        """Change the input argument to list type if needed.

        Parameters
        ----------
        inputArg : obj
            Input argument.

        Returns
        -------
        list
            Input argument as the list type.
        """

        if (not isinstance(inputArg, list)):
            inputArg = [inputArg]

        return inputArg

    def mapSensorIdToName(self, sensorId):
        """Map the sensor Id to sensor name.

        If no sensor name is found for a specific Id, there will be no returned
        value.

        Parameters
        ----------
        sensorId : list[int] or int
            List or integer of sensor Id.

        Returns
        -------
        list
            List of abbreviated sensor names.
        int
            Number of sensors.
        """

        sensorIdList = self._changeToListIfNeed(sensorId)

        sensorNameList = []
        content = self._sensorNameToIdFile.getContent()
        for sensor in sensorIdList:
            try:
                sensorName = self._getKeyFromValueInDict(content, sensor)
                sensorNameList.append(sensorName)
            except ValueError:
                pass

        return sensorNameList, len(sensorNameList)

    def _getKeyFromValueInDict(self, aDict, value):
        """Get the key from value in a dictionary object.

        Parameters
        ----------
        aDict : dict
            Dictionary object.
        value : str or int
            Value in the dictionary.

        Returns
        -------
        str
            Dictionary key.
        """

        return list(aDict.keys())[list(aDict.values()).index(value)]