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)
def testGetMatContentWithDefaultSetting(self): paramReader = ParamReader() matInYamlFile = paramReader.getMatContent() self.assertTrue(isinstance(matInYamlFile, np.ndarray)) self.assertEqual(len(matInYamlFile), 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
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
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 __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)
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
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 __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 __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
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 __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
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 __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([])
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 testGetAbsPath(self): filePath = "README.md" self.assertFalse(os.path.isabs(filePath)) filePathAbs = ParamReader.getAbsPath(filePath, getModulePath()) self.assertTrue(os.path.isabs(filePathAbs))
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 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 _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
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
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)
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 __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 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 __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 __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 __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")
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")
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)
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
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
def testGetContentWithDefaultSetting(self): paramReader = ParamReader() content = paramReader.getContent() self.assertTrue(isinstance(content, dict))
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
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)
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
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
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)
def testNonexistentFile(self): with self.assertWarns(UserWarning): paramReader = ParamReader(filePath="thisFileDoesntExists") self.assertEqual(len(paramReader.getContent().keys()), 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)
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
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()
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
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)]