diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..105ce2da2d6447d11dfe32bfb846c3d5b199fc99
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..a9b5e744c0d05f2592bc82552323c2044a5bfbce
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 CSST-L1
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index f9a5328c3c39539219a075366937c977d253077e..aed373c9603307c3bddd511cc45de92fabdf830a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Ifs
+# csst_ifs_common
@@ -15,14 +15,14 @@ Already a pro? Just edit this README.md and make it your own. Want to make it ea
```
cd existing_repo
-git remote add origin https://csst-tb.bao.ac.cn/code/shaosim/ifs.git
+git remote add origin https://csst-tb.bao.ac.cn/code/csst-l1/ifs/csst_ifs_common.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
-- [ ] [Set up project integrations](https://csst-tb.bao.ac.cn/code/shaosim/ifs/-/settings/integrations)
+- [ ] [Set up project integrations](http://10.3.10.28/code/csst-l1/ifs/csst_ifs_common/-/settings/integrations)
## Collaborate with your team
diff --git a/csst_ifs_sim/CTI/CTI.py b/csst_ifs_sim/CTI/CTI.py
new file mode 100644
index 0000000000000000000000000000000000000000..48c87cb8aee4207a0b4d764e32a0cea44a3d6987
--- /dev/null
+++ b/csst_ifs_sim/CTI/CTI.py
@@ -0,0 +1,495 @@
+"""
+Charge Transfer Inefficiency
+============================
+
+This file contains a simple class to run a CDM03 CTI model developed by Alex Short (ESA).
+
+This now contains both the official CDM03 and a new version that allows different trap
+parameters in parallel and serial direction.
+
+:requires: NumPy
+:requires: CDM03 (FORTRAN code, f2py -c -m cdm03bidir cdm03bidir.f90)
+
+:version: 0.35
+"""
+import numpy as np
+
+# try:
+# import cdm03bidir
+# #import cdm03bidirTest as cdm03bidir #for testing purposes only
+# except ImportError:
+# print('import CTI module')
+# #print ('No CDM03bidir module available, please compile it: f2py -c -m cdm03bidir cdm03bidir.f90')
+
+
+
+#CDM03bidir
+class CDM03bidir():
+ """
+ Class to run CDM03 CTI model, class Fortran routine to perform the actual CDM03 calculations.
+
+ :param settings: input parameters
+ :type settings: dict
+ :param data: input data to be radiated
+ :type data: ndarray
+ :param log: instance to Python logging
+ :type log: logging instance
+ """
+ def __init__(self, settings, data, log=None):
+ """
+ Class constructor.
+
+ :param settings: input parameters
+ :type settings: dict
+ :param data: input data to be radiated
+ :type data: ndarray
+ :param log: instance to Python logging
+ :type log: logging instance
+ """
+ self.data = data
+ self.values = dict(quads=(0,1,2,3), xsize=2048, ysize=2066, dob=0.0, rdose=8.0e9)
+ self.values.update(settings)
+ self.log = log
+ self._setupLogger()
+
+ #default CDM03 settings
+ self.params = dict(beta_p=0.6, beta_s=0.6, fwc=200000., vth=1.168e7, vg=6.e-11, t=20.48e-3,
+ sfwc=730000., svg=1.0e-10, st=5.0e-6, parallel=1., serial=1.)
+ #update with inputs
+ self.params.update(self.values)
+
+ #read in trap information
+ trapdata = np.loadtxt(self.values['parallelTrapfile'])
+ if trapdata.ndim > 1:
+ self.nt_p = trapdata[:, 0]
+ self.sigma_p = trapdata[:, 1]
+ self.taur_p = trapdata[:, 2]
+ else:
+ #only one trap species
+ self.nt_p = [trapdata[0],]
+ self.sigma_p = [trapdata[1],]
+ self.taur_p = [trapdata[2],]
+
+ trapdata = np.loadtxt(self.values['serialTrapfile'])
+ if trapdata.ndim > 1:
+ self.nt_s = trapdata[:, 0]
+ self.sigma_s = trapdata[:, 1]
+ self.taur_s = trapdata[:, 2]
+ else:
+ #only one trap species
+ self.nt_s = [trapdata[0],]
+ self.sigma_s = [trapdata[1],]
+ self.taur_s = [trapdata[2],]
+
+ #scale thibaut's values
+ if 'thibaut' in self.values['parallelTrapfile']:
+ self.nt_p /= 0.576 #thibaut's values traps / pixel
+ self.sigma_p *= 1.e4 #thibaut's values in m**2
+ if 'thibaut' in self.values['serialTrapfile']:
+ self.nt_s *= 0.576 #thibaut's values traps / pixel #should be division?
+ self.sigma_s *= 1.e4 #thibaut's values in m**2
+
+
+ def _setupLogger(self):
+ """
+ Set up the logger.
+ """
+ self.logger = True
+ if self.log is None:
+ self.logger = False
+
+
+ def radiateFullCCD(self):
+ """
+ This routine allows the whole CCD to be run through a radiation damage mode.
+ The routine takes into account the fact that the amplifiers are in the corners
+ of the CCD. The routine assumes that the CCD is using four amplifiers.
+
+ There is an excess of .copy() calls, which should probably be cleaned up. However,
+ given that I had problem with the Fortran code, I have kept the calls. If memory
+ becomes an issue then this should be cleaned.
+
+ :return: radiation damaged image
+ :rtype: ndarray
+ """
+ ydim, xdim = self.data.shape
+ out = np.zeros((xdim, ydim))
+
+ #transpose the data, because Python has different convention than Fortran
+ data = self.data.transpose().copy()
+
+ for quad in self.values['quads']:
+ if self.logger:
+ self.log.info('Adding CTI to Q%i' % quad)
+
+ if quad == 0:
+ d = data[0:self.values['xsize'], 0:self.values['ysize']].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[0:self.values['xsize'], 0:self.values['ysize']] = tmp
+ elif quad == 1:
+ d = data[self.values['xsize']:, :self.values['ysize']].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[self.values['xsize']:, :self.values['ysize']] = tmp
+ elif quad == 2:
+ d = data[:self.values['xsize'], self.values['ysize']:].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[:self.values['xsize'], self.values['ysize']:] = tmp
+ elif quad == 3:
+ d = data[self.values['xsize']:, self.values['ysize']:].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[self.values['xsize']:, self.values['ysize']:] = tmp
+ else:
+ print( 'ERROR -- too many quadrants!!' )
+ self.log.error('Too many quadrants! This method allows only four quadrants.')
+
+ return out.transpose()
+
+
+ def radiateFullCCD2(self):
+ """
+ This routine allows the whole CCD to be run through a radiation damage mode.
+ The routine takes into account the fact that the amplifiers are in the corners
+ of the CCD. The routine assumes that the CCD is using four amplifiers.
+
+ There is an excess of .copy() calls, which should probably be cleaned up. However,
+ given that I had problem with the Fortran code, I have kept the calls. If memory
+ becomes an issue then this should be cleaned.
+
+ :return: radiation damaged image
+ :rtype: ndarray
+ """
+ ydim, xdim = self.data.shape
+ out = np.empty((ydim, xdim))
+
+ #transpose the data, because Python has different convention than Fortran
+ data = self.data.copy()
+
+ for quad in self.values['quads']:
+ if self.logger:
+ self.log.info('Adding CTI to Q%i' % quad)
+
+ if quad == 0:
+ d = data[:self.values['ysize'], :self.values['xsize']].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[:self.values['ysize'], :self.values['xsize']] = tmp
+ elif quad == 1:
+ d = data[:self.values['ysize'], self.values['xsize']:].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[:self.values['ysize'], self.values['xsize']:] = tmp
+ elif quad == 2:
+ d = data[self.values['ysize']:, :self.values['xsize']].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[self.values['ysize']:, :self.values['xsize']] = tmp
+ elif quad == 3:
+ d = data[self.values['ysize']:, self.values['xsize']:].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[self.values['ysize']:, self.values['xsize']:] = tmp
+ else:
+ print( 'ERROR -- too many quadrants!!')
+ self.log.error('Too many quadrants! This method allows only four quadrants.')
+
+ return out
+
+
+ def applyRadiationDamage(self, data, iquadrant=0):
+ """
+ Apply radian damage based on FORTRAN CDM03 model. The method assumes that
+ input data covers only a single quadrant defined by the iquadrant integer.
+
+ :param data: imaging data to which the CDM03 model will be applied to.
+ :type data: ndarray
+
+ :param iquandrant: number of the quadrant to process
+ :type iquandrant: int
+
+ cdm03 - Function signature::
+
+ sout = cdm03(sinp,iflip,jflip,dob,rdose,in_nt,in_sigma,in_tr,[xdim,ydim,zdim])
+ Required arguments:
+ sinp : input rank-2 array('d') with bounds (xdim,ydim)
+ iflip : input int
+ jflip : input int
+ dob : input float
+ rdose : input float
+ in_nt : input rank-1 array('d') with bounds (zdim)
+ in_sigma : input rank-1 array('d') with bounds (zdim)
+ in_tr : input rank-1 array('d') with bounds (zdim)
+ Optional arguments:
+ xdim := shape(sinp,0) input int
+ ydim := shape(sinp,1) input int
+ zdim := len(in_nt) input int
+ Return objects:
+ sout : rank-2 array('d') with bounds (xdim,ydim)
+
+ .. Note:: Because Python/NumPy arrays are different row/column based, one needs
+ to be extra careful here. NumPy.asfortranarray will be called to get
+ an array laid out in Fortran order in memory. Before returning the
+ array will be laid out in memory in C-style (row-major order).
+
+ :return: image that has been run through the CDM03 model
+ :rtype: ndarray
+ """""
+ #return data
+
+ iflip = iquadrant / 2
+ jflip = iquadrant % 2
+
+ params = [self.params['beta_p'], self.params['beta_s'], self.params['fwc'], self.params['vth'],
+ self.params['vg'], self.params['t'], self.params['sfwc'], self.params['svg'], self.params['st'],
+ self.params['parallel'], self.params['serial']]
+
+ if self.logger:
+ self.log.info('nt_p=' + str(self.nt_p))
+ self.log.info('nt_s=' + str(self.nt_s))
+ self.log.info('sigma_p= ' + str(self.sigma_p))
+ self.log.info('sigma_s= ' + str(self.sigma_s))
+ self.log.info('taur_p= ' + str(self.taur_p))
+ self.log.info('taur_s= ' + str(self.taur_s))
+ self.log.info('dob=%f' % self.values['dob'])
+ self.log.info('rdose=%e' % self.values['rdose'])
+ self.log.info('xsize=%i' % data.shape[1])
+ self.log.info('ysize=%i' % data.shape[0])
+ self.log.info('quadrant=%i' % iquadrant)
+ self.log.info('iflip=%i' % iflip)
+ self.log.info('jflip=%i' % jflip)
+
+#################################################################################
+###modify
+ import sys
+ #sys.path.append('../so')
+ from ifs_so import cdm03bidir
+ # from ifs_so.cdm03.cpython-38-x86_64-linux-gnu import cdm03bidir
+ # import cdm03bidir
+ CTIed = cdm03bidir.cdm03(np.asfortranarray(data),
+ jflip, iflip,
+ self.values['dob'], self.values['rdose'],
+ self.nt_p, self.sigma_p, self.taur_p,
+ self.nt_s, self.sigma_s, self.taur_s,
+ params,
+ [data.shape[0], data.shape[1], len(self.nt_p), len(self.nt_s), len(self.params)])
+
+
+ return np.asanyarray(CTIed)
+
+#################################################################################################################
+
+
+
+class CDM03():
+ """
+ Class to run CDM03 CTI model, class Fortran routine to perform the actual CDM03 calculations.
+
+ :param data: input data to be radiated
+ :type data: ndarray
+ :param input: input parameters
+ :type input: dictionary
+ :param log: instance to Python logging
+ :type log: logging instance
+ """
+ def __init__(self, input, data, log=None):
+ """
+ Class constructor.
+
+ :param data: input data to be radiated
+ :type data: ndarray
+ :param input: input parameters
+ :type input: dictionary
+ :param log: instance to Python logging
+ :type log: logging instance
+ """
+ try:
+ import cdm03
+ except ImportError:
+ print( 'No CDM03 module available, please compile it: f2py -c -m cdm03 cdm03.f90')
+
+ self.data = data
+ self.values = dict(quads=(0,1,2,3), xsize=2048, ysize=2066, dob=0.0, rdose=8.0e9)
+ self.values.update(input)
+ self.log = log
+ self._setupLogger()
+
+
+ def _setupLogger(self):
+ """
+ Set up the logger.
+ """
+ self.logger = True
+ if self.log is None:
+ self.logger = False
+
+
+ def radiateFullCCD(self):
+ """
+ This routine allows the whole CCD to be run through a radiation damage mode.
+ The routine takes into account the fact that the amplifiers are in the corners
+ of the CCD. The routine assumes that the CCD is using four amplifiers.
+
+ There is an excess of .copy() calls, which should probably be cleaned up. However,
+ given that I had problem with the Fortran code, I have kept the calls. If memory
+ becomes an issue then this should be cleaned.
+
+ :return: radiation damaged image
+ :rtype: ndarray
+ """
+ ydim, xdim = self.data.shape
+ out = np.zeros((xdim, ydim))
+
+ #transpose the data, because Python has different convention than Fortran
+ data = self.data.transpose().copy()
+
+ for quad in self.values['quads']:
+ if self.logger:
+ self.log.info('Adding CTI to Q%i' % quad)
+
+ if quad == 0:
+ d = data[0:self.values['xsize'], 0:self.values['ysize']].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[0:self.values['xsize'], 0:self.values['ysize']] = tmp
+ elif quad == 1:
+ d = data[self.values['xsize']:, :self.values['ysize']].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[self.values['xsize']:, :self.values['ysize']] = tmp
+ elif quad == 2:
+ d = data[:self.values['xsize'], self.values['ysize']:].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[:self.values['xsize'], self.values['ysize']:] = tmp
+ elif quad == 3:
+ d = data[self.values['xsize']:, self.values['ysize']:].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[self.values['xsize']:, self.values['ysize']:] = tmp
+ else:
+ print ('ERROR -- too many quadrants!!')
+ self.log.error('Too many quadrants! This method allows only four quadrants.')
+
+ return out.transpose()
+
+
+ def radiateFullCCD2(self):
+ """
+ This routine allows the whole CCD to be run through a radiation damage mode.
+ The routine takes into account the fact that the amplifiers are in the corners
+ of the CCD. The routine assumes that the CCD is using four amplifiers.
+
+ There is an excess of .copy() calls, which should probably be cleaned up. However,
+ given that I had problem with the Fortran code, I have kept the calls. If memory
+ becomes an issue then this should be cleaned.
+
+ :return: radiation damaged image
+ :rtype: ndarray
+ """
+ ydim, xdim = self.data.shape
+ out = np.empty((ydim, xdim))
+
+ #transpose the data, because Python has different convention than Fortran
+ data = self.data.copy()
+
+ for quad in self.values['quads']:
+ if self.logger:
+ self.log.info('Adding CTI to Q%i' % quad)
+
+ if quad == 0:
+ d = data[:self.values['ysize'], :self.values['xsize']].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[:self.values['ysize'], :self.values['xsize']] = tmp
+ elif quad == 1:
+ d = data[:self.values['ysize'], self.values['xsize']:].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[:self.values['ysize'], self.values['xsize']:] = tmp
+ elif quad == 2:
+ d = data[self.values['ysize']:, :self.values['xsize']].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[self.values['ysize']:, :self.values['xsize']] = tmp
+ elif quad == 3:
+ d = data[self.values['ysize']:, self.values['xsize']:].copy()
+ tmp = self.applyRadiationDamage(d, iquadrant=quad).copy()
+ out[self.values['ysize']:, self.values['xsize']:] = tmp
+ else:
+ print ('ERROR -- too many quadrants!!')
+ self.log.error('Too many quadrants! This method allows only four quadrants.')
+
+ return out
+
+
+ def applyRadiationDamage(self, data, iquadrant=0):
+ """
+ Apply radian damage based on FORTRAN CDM03 model. The method assumes that
+ input data covers only a single quadrant defined by the iquadrant integer.
+
+ :param data: imaging data to which the CDM03 model will be applied to.
+ :type data: ndarray
+
+ :param iquandrant: number of the quadrant to process
+ :type iquandrant: int
+
+ cdm03 - Function signature::
+
+ sout = cdm03(sinp,iflip,jflip,dob,rdose,in_nt,in_sigma,in_tr,[xdim,ydim,zdim])
+ Required arguments:
+ sinp : input rank-2 array('d') with bounds (xdim,ydim)
+ iflip : input int
+ jflip : input int
+ dob : input float
+ rdose : input float
+ in_nt : input rank-1 array('d') with bounds (zdim)
+ in_sigma : input rank-1 array('d') with bounds (zdim)
+ in_tr : input rank-1 array('d') with bounds (zdim)
+ Optional arguments:
+ xdim := shape(sinp,0) input int
+ ydim := shape(sinp,1) input int
+ zdim := len(in_nt) input int
+ Return objects:
+ sout : rank-2 array('d') with bounds (xdim,ydim)
+
+ .. Note:: Because Python/NumPy arrays are different row/column based, one needs
+ to be extra careful here. NumPy.asfortranarray will be called to get
+ an array laid out in Fortran order in memory. Before returning the
+ array will be laid out in memory in C-style (row-major order).
+
+ :return: image that has been run through the CDM03 model
+ :rtype: ndarray
+ """
+ #read in trap information
+ trapdata = np.loadtxt(self.values['trapfile'])
+ nt = trapdata[:, 0]
+ sigma = trapdata[:, 1]
+ taur = trapdata[:, 2]
+
+ iflip = iquadrant / 2
+ jflip = iquadrant % 2
+
+ if self.logger:
+ self.log.info('nt=' + str(nt))
+ self.log.info('sigma= ' + str(sigma))
+ self.log.info('taur= ' + str(taur))
+ self.log.info('dob=%f' % self.values['dob'])
+ self.log.info('rdose=%e' % self.values['rdose'])
+ self.log.info('xsize=%i' % data.shape[1])
+ self.log.info('ysize=%i' % data.shape[0])
+ self.log.info('quadrant=%i' % iquadrant)
+ self.log.info('iflip=%i' % iflip)
+ self.log.info('jflip=%i' % jflip)
+
+
+ # #call Fortran routine
+ # CTIed = cdm03.cdm03(np.asfortranarray(data),
+ # iflip, jflip,
+ # self.values['dob'], self.values['rdose'],
+ # nt, sigma, taur)
+ ###modify
+ import sys
+ sys.path.append('../CTI')
+ import cdm03
+
+ #################################################################################
+
+ CTIed = cdm03.cdm03(np.asfortranarray(data),
+ jflip, iflip,
+ self.values['dob'], self.values['rdose'],
+ nt,sigma,taur )
+
+
+ return np.asanyarray(CTIed)
+
+
+#################################################################################################################
+
diff --git a/csst_ifs_sim/CTI/__init__.py b/csst_ifs_sim/CTI/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/csst_ifs_sim/__init__.py b/csst_ifs_sim/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/csst_ifs_sim/csst_ifs_sim.py b/csst_ifs_sim/csst_ifs_sim.py
new file mode 100644
index 0000000000000000000000000000000000000000..1dbd918a2cdaf4cbe59781b2edfd439be1e427a8
--- /dev/null
+++ b/csst_ifs_sim/csst_ifs_sim.py
@@ -0,0 +1,2624 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+
+@author: yan
+"""
+
+"""
+The CSST IFS Image Simulator
+=============================================
+
+This file contains an image simulator for the CSST IFS.
+
+The approximate sequence of events in the simulator is as follows:
+
+ #. Read in a configuration file, which defines for example,
+ detector characteristics (bias, dark and readout noise, gain,
+ plate scale and pixel scale, oversampling factor, exposure time etc.).
+ #. Read in another file containing charge trap definitions (for CTI modelling).
+ #. Read in a file defining the cosmic rays (trail lengths and cumulative distributions).
+
+ #. Load the wavefront aberration data used to calculate PSF with defined wavelength and field of view.
+
+ #. Apply calibration unit flux to mimic flat field exposures [optional].
+ #. Apply a multiplicative flat-field map to emulate pixel-to-pixel non-uniformity [optional].
+
+ #. Add cosmic ray tracks onto the CCD with random positions but known distribution [optional].
+ #. Apply detector charge bleeding in column direction [optional].
+ #. Add constant dark current and background light from Zodiacal light [optional].
+ #. Add photon (Poisson) noise [optional]
+ #. Add cosmetic defects from an input file [optional].
+ #. Add overscan regions in the serial direction [optional].
+ #. Apply the CDM03 radiation damage model [optional].
+ #. Apply non-linearity model to the pixel data [optional].
+ #. Add readout noise selected from a Gaussian distribution [optional].
+ #. Convert from electrons to ADUs using a given gain factor.
+ #. Add a given bias level and discretise the counts (the output is going to be in 16bit unsigned integers).
+ #. Finally the simulated image is converted to a FITS file.
+
+:version: 2023.04
+
+
+Future Work
+-----------
+
+.. todo::
+
+ #.
+ #.
+ #. ......
+
+Contact Information: zhaojunyan@shao.ac.cn
+
+-------
+
+"""
+
+from scipy.integrate import simps
+import pandas as pd
+from datetime import datetime, timedelta
+import julian
+from astropy.coordinates import get_sun
+from astropy.time import Time
+from astropy import units as u
+
+from astropy.coordinates import SkyCoord, EarthLocation
+
+from scipy import interpolate
+
+import numpy as np
+
+import scipy.io as sio
+
+from astropy.io import fits
+
+from scipy.signal import fftconvolve
+
+import galsim
+
+from scipy import ndimage
+
+import cmath
+
+import os, sys
+
+import configparser as ConfigParser
+
+from optparse import OptionParser
+
+sys.path.append('../')
+
+from CTI import CTI
+from support import logger as lg
+from support import cosmicrays
+from support import IFSinstrumentModel
+
+###############################################################################
+
+def str2time(strTime):
+ if len(strTime)>20:#
+ msec=int(float('0.'+strTime[20:])*1000000) #
+ str2=strTime[0:19]+' '+str(msec)
+ return datetime.strptime(str2,'%Y %m %d %H %M %S %f')
+
+#datetime to mjd
+def time2mjd(dateT):
+ t0=datetime(1858,11,17,0,0,0,0)#
+ mjd=(dateT-t0).days
+ mjd_s=dateT.hour*3600.0+dateT.minute*60.0+dateT.second+dateT.microsecond/1000000.0
+ return mjd+mjd_s/86400.0
+
+def time2jd(dateT):
+ t0=datetime(1858,11,17,0,0,0,0)#
+ mjd=(dateT-t0).days
+ mjd_s=dateT.hour*3600.0+dateT.minute*60.0+dateT.second+dateT.microsecond/1000000.0
+ return mjd+mjd_s/86400.0++2400000.5
+
+#mjd to datetime
+def mjd2time(mjd):
+ t0=datetime(1858,11,17,0,0,0,0)#
+ return t0+timedelta(days=mjd)
+
+def jd2time(jd):
+ mjd=jd2mjd(jd)
+ return mjd2time(mjd)
+
+#mjd to jd
+def mjd2jd(mjd):
+ return mjd+2400000.5
+
+def jd2mjd(jd):
+ return jd-2400000.5
+
+def dt2hmd(dt):
+ ## dt is datetime
+ hour=dt.hour
+ minute=dt.minute
+ second=dt.second
+ if hour<10:
+ str_h='0'+str(hour)
+ else:
+ str_h=str(hour)
+
+ if minute<10:
+ str_m='0'+str(minute)
+ else:
+ str_m=str(minute)
+
+ if second<10:
+ str_d='0'+str(second+dt.microsecond*1e-6)
+ else:
+ str_d=str(second+dt.microsecond*1e-6)
+
+ return str_h+':'+str_m+':'+str_d
+
+###############################################################################
+
+###############################################################################
+
+def deg2HMS(ra, dec, rou=False):
+ '''convert deg to ra's HMS or dec's DHS'''
+ RA, DEC, rs, ds = '000000', '000000', '', ''
+ if dec:
+ if str(dec)[0] == '-':
+ ds, dec = '-', abs(dec)
+ deg = int(dec)
+ decM = abs(int((dec-deg)*60))
+
+ if rou:
+ decS = round((abs((dec-deg)*60)-decM)*60,1)
+ else:
+ decS = int((abs((dec-deg)*60)-decM)*60)
+
+ if deg ==0:
+ deg ="00"
+ elif deg <10:
+ deg = "0%s"%deg
+
+ if decM ==0:
+ decM ="00"
+ elif decM <10:
+ decM = "0%s"%decM
+
+ if decS ==0:
+ decS ="00"
+ elif decS <10:
+ decS = "0%s"%decS
+
+ DEC = '{0}{1}{2}{3}'.format(ds, deg, decM, decS)
+
+ if ra:
+ if str(ra)[0] == '-':
+ rs, ra = '-', abs(ra)
+ raH = int(ra/15)
+ raM = int(((ra/15)-raH)*60)
+
+ if rou:
+ raS = round(((((ra/15)-raH)*60)-raM)*60,1)
+ else:
+ raS = int(((((ra/15)-raH)*60)-raM)*60)
+
+ if raH ==0:
+ raH = "00"
+
+ elif raH <10:
+ raH = "0%s"%raH
+
+ if raM ==0:
+ raM = "00"
+ elif raM <10:
+ raM = "0%s"%raM
+
+ if raS ==0:
+ raS = "00"
+
+ elif raS <10:
+ raS = "0%s"%raS
+
+ RA = '{0}{1}{2}{3}'.format(rs, raH, raM, raS)
+
+ if ds=='-':
+ return RA+DEC
+ else:
+ return RA+'+'+DEC
+###############################################################################
+
+###########################################################
+def beta_angle( x_sat, y_sat, z_sat, vx_sat, vy_sat, vz_sat, ra_obj, dec_obj):
+
+ # get the vector for next motion
+ x_sat2 = x_sat + vx_sat
+ y_sat2 = y_sat + vy_sat
+ z_sat2 = z_sat + vz_sat
+
+ vector1 = np.stack((x_sat, y_sat, z_sat), axis=0)
+ vector2 = np.stack((x_sat2, y_sat2, z_sat2), axis=0)
+ vector_normal = np.cross(vector1, vector2)
+
+ location_normal = EarthLocation.from_geocentric(vector_normal[0],
+ vector_normal[1],
+ vector_normal[2],
+ unit=u.km)
+ radec_normal = SkyCoord(ra=location_normal.lon,
+ dec=location_normal.lat,
+ frame='gcrs')
+ lb_normal = radec_normal.transform_to('geocentrictrueecliptic')
+
+
+ radec_sun = SkyCoord(ra=ra_obj*u.degree, dec=dec_obj*u.degree, frame='gcrs')
+ lb_sun = radec_sun.transform_to('geocentrictrueecliptic')
+
+ # get the angle between normal and solar
+ angle = 90 - lb_normal.separation(lb_sun).degree
+
+ return angle
+
+##########################################################
+##########################################################
+def LSR_velocity(ra,dec,velocity,Obstime):
+
+ local = EarthLocation.from_geodetic(lat=22.349368*u.deg, lon=113.584068*u.deg, height=10*u.m)
+ ### convert ra and dec to
+ source = SkyCoord(ra*u.deg, dec*u.deg,frame='icrs', unit=(u.hourangle,u.deg) )
+ l = source.galactic.l.deg
+ b = source.galactic.b.deg
+ c = SkyCoord(l=l*u.degree, b=b*u.degree, frame='galactic')
+ c_icrs = c.transform_to('icrs')
+ barycorr = c_icrs.radial_velocity_correction(obstime=Time(Obstime), location=local)
+ velocity = velocity + barycorr.value/1000
+ #print(barycorr.value/1000)
+ l = l * np.pi / 180
+ b = b * np.pi / 180
+ return velocity + 9 * np.cos(l) * np.cos(b) + 12 * np.sin(l) * np.cos(b) + 7 * np.sin(b)
+
+###############################################################################
+def rotation_yan(x0,y0,x1,y1,angle):
+ #% point A (x0,y0)
+ #% point B(x1,y1)
+ #% roate angle ,point B rotate with point A
+ alpha=angle/180*3.1415926 #% in radians
+ x2=(x1-x0)*np.cos(alpha)-(y1-y0)*np.sin(alpha)+x0
+ y2=(x1-x0)*np.sin(alpha)+(y1-y0)*np.cos(alpha)+y0
+ return x2, y2
+######################################################################
+def centroid(data):
+ h,w = np.shape(data)
+ x = np.arange(0,w)
+ y = np.arange(0,h)
+ x1=np.ones((1,h))
+ y1=np.ones((w,1))
+ cx=(np.dot(np.dot(x1,data),y)) /(np.dot(np.dot(x1,data),y1))
+ cy=(np.dot(np.dot(x ,data),y1)) /(np.dot(np.dot(x1,data),y1))
+ return np.float64(cx),np.float64(cy)
+####################################################################
+def SpecCube2photon(data,wave):
+ # calcutle photons from original spec cube data,
+ # data: input data, two-dimentional, unit : 1e-17 erg/s/A/cm^2
+ # wave: the relative wavefront, unit in nm;
+ planckh= 6.62620*10**-27 # % erg s;
+ cc=2.99792458*10**17 # in nm/s
+ # fov2=0.01 # in arcsec^2
+ # telarea=3.1415926*100*100 # in cm^2,
+ fluxlam=1e-17*data # convert original unit to unit of erg/s/A/cm^2
+ lam=wave # wave in nm ;;
+ ephoton=planckh*cc/lam # in erg/photon,
+ Nphoton =fluxlam/ephoton # in unit of photons/cm2/s/A
+ return Nphoton
+
+######################################################################
+
+######################################################################
+def zero_pad(image, shape, position='corner'):
+ """
+ Extends image to a certain size with zeros
+ Parameters
+ ----------
+ image: real 2d `np.ndarray`
+ Input image
+ shape: tuple of int
+ Desired output shape of the image
+ position : str, optional
+ The position of the input image in the output one:
+ * 'corner'
+ top-left corner (default)
+ * 'center'
+ centered
+ Returns
+ -------
+ padded_img: real `np.ndarray`
+ The zero-padded image
+ """
+ shape = np.asarray(shape, dtype=int)
+ imshape = np.asarray(image.shape, dtype=int)
+
+ if np.alltrue(imshape == shape):
+ return image
+
+ if np.any(shape <= 0):
+ raise ValueError("ZERO_PAD: null or negative shape given")
+
+ dshape = shape - imshape
+ if np.any(dshape < 0):
+ raise ValueError("ZERO_PAD: target size smaller than source one")
+
+ pad_img = np.zeros(shape, dtype=image.dtype)
+
+ idx, idy = np.indices(imshape)
+
+ if position == 'center':
+ if np.any(dshape % 2 != 0):
+ raise ValueError("ZERO_PAD: source and target shapes "
+ "have different parity.")
+ offx, offy = dshape // 2
+ else:
+ offx, offy = (0, 0)
+
+ pad_img[idx + offx, idy + offy] = image
+
+ return pad_img
+
+def anySampledPSF(wavefront, pupil , Q , sizeout):
+ '''
+ written on 2020.12.04 by Yan @Shao
+ % wavefront sampled in the united circle;
+ % pupil function samped as wavefront;
+ % sample ratio Q=lambda/D/pixel;
+ % lambda is wavelength;
+ % D is diameter;
+ % pixel is in arcsec;
+ % or sample ratio Q=lambda*f/D/pixelsize
+ % f is focal length;
+ % pixelsize is the actural size of the detector;
+ % make sure all the varia have the same unit;
+ % the returned PSF has sum value of 1
+ '''
+
+ m,n = np.shape(wavefront)
+
+ phase=2.0*np.pi*wavefront #% the phase of the input wavefront;
+
+ ####generalized pupil function of channel1;
+ if Q>=1:
+ pk1=zero_pad(pupil*np.exp(cmath.sqrt(-1)*(phase)),( int(np.floor(m*Q)),int(np.floor(m*Q))), position='corner')
+ psf=abs(np.fft.fftshift(np.fft.fft2(pk1)))**2
+ #psf=psf/psf.sum()
+ else: #%%% case: Q<1
+ # %fft method
+ if Q>=0.5: #% in this case Q<1 and Q>=0.5
+ pk1=zero_pad(pupil*np.exp(cmath.sqrt(-1)*(phase)), (int(2*np.floor(m*Q)),int(2*np.floor(m*Q))), position='corner')
+ mypsf=np.fft.fft2(pk1);
+ t=mypsf[0::2,0::2];
+ psf=abs(np.fft.fftshift(t))**2
+ #psf=mypsf3/mypsf3.sum();
+
+ else:
+ if Q>=0.25:
+ pk1=zero_pad(pupil*np.exp(cmath.sqrt(-1)*(phase)),( int(4*np.floor(m*Q)),int(4*np.floor(m*Q))), position='corner')
+ mypsf=np.fft.fft2(pk1);
+ t=mypsf[0::4,0::4];
+ psf=abs(np.fft.fftshift(t))**2
+ #psf=mypsf3/mypsf3.sum();
+ else:
+ print('---- Q<0.25 , stop running')
+ sys.exit(0)
+ ########################################################################################
+ mm,nn=np.shape(psf)
+ if np.mod(sizeout,2)==0:
+ Nx=sizeout/2-1
+ else:
+ Nx=(sizeout+1)/2-1
+
+
+ if max(mm,nn)3*sigma:
+
+ SpmOut=1.0/20000
+ else:
+
+ column=np.where(d==min(d))
+ SpmOut=Qt[column[0]]*np.exp(-(lam-wave[column[0]])**2/2/sigma**2)
+
+ return SpmOut
+
+#####################################################################################
+####################################################################################################################
+
+def processArgs(printHelp=False):
+ """
+ Processes command line arguments.
+ """
+ parser = OptionParser()
+
+ parser.add_option('-c', '--configfile', dest='configfile',
+ help="Name of the configuration file", metavar="string")
+ parser.add_option('-s', '--section', dest='section',
+ help="Name of the section of the config file [SCIENCE]", metavar="string")
+ parser.add_option('-q', '--quadrant', dest='quadrant', help='CCD quadrant to simulate [0, 1, 2, 3]',
+ metavar='int')
+ parser.add_option('-x', '--xCCD', dest='xCCD', help='CCD number in X-direction within the FPA matrix',
+ metavar='int')
+ parser.add_option('-y', '--yCCD', dest='yCCD', help='CCD number in Y-direction within the FPA matrix',
+ metavar='int')
+ parser.add_option('-d', '--debug', dest='debug', action='store_true',
+ help='Debugging mode on')
+ parser.add_option('-t', '--test', dest='test', action='store_true',
+ help='Run unittest')
+ parser.add_option('-f', '--fixed', dest='fixed', help='Use a fixed seed for the random number generators',
+ metavar='int')
+ if printHelp:
+ parser.print_help()
+ else:
+ return parser.parse_args()
+##############################################################################################
+class IFSsimulator():
+ """
+ CSST IFS Simulator
+
+ The image that is being build is in::
+
+ self.image
+ self.image_b blue channel
+ self.image_r red channel
+
+ :param opts: OptionParser instance
+ :type opts: OptionParser instance
+ """
+
+ def __init__(self, opts):
+ """
+ Class Constructor.
+
+ :param opts: OptionParser instance
+ :type opts: OptionParser instance
+
+ """
+
+ ####################################
+
+ self.configfile = opts.configfile
+
+ if opts.section is None:
+ ####self.section = 'DEFAULT'
+ self.section = 'TEST' #####simulation section;
+
+ else:
+ self.section = opts.section
+
+ #load instrument model, these values are also stored in the FITS header
+ self.information = IFSinstrumentModel.IFSinformation()
+
+ #update settings with defaults
+ self.information.update(dict(quadrant=int(0),
+ ccdx=int(0),
+ ccdy=int(0),
+ psfoversampling=1.0,
+ ccdxgap=1.643,
+ ccdygap=8.116,
+ fullwellcapacity=90000,
+ readouttime=4,
+ rdose=8.0e9,
+ coveringfraction=0.5 ))
+
+ ######################################################################
+ self.configure() #print the configfile name and path;
+
+ self.information.update(dict(
+
+ cosmicraylengths =self.information['indata_path']+'/cdf_cr_length.dat',
+ cosmicraydistance=self.information['indata_path']+'/cdf_cr_total.dat',
+ parallelTrapfile =self.information['indata_path']+'/cdm_euclid_parallel.dat',
+ serialTrapfile =self.information['indata_path']+'/cdm_euclid_serial.dat',
+ cosmeticsfile_b =self.information['indata_path']+'/Cosmetics_b.txt',
+ cosmeticsfile_r =self.information['indata_path']+'/Cosmetics_r.txt' ))
+
+ # public system information
+ self.pixelscale=0.1 # the pixel size is 0.1 arcsec
+ self.pixelsize=15 # the pixel physical size is 15 micron
+ self.Fnum=15.469875 # the f number= focal_length/D;
+ self.telD=2 # tht telescope size is 2 meter, diamter size;
+ self.telarea=3.1415926*(self.telD/2)**2*100*100 # telescope square in cm^2
+ self.fov2=0.01 # the pixel square
+ self.planckh= 6.62620*1e-27 # % erg s;
+ self.cc=2.99792458*1e17 # in nm/s
+ self.light_FWHM=0.175
+ self.HgArsigma=self.light_FWHM/2.35; ## sigma value of the light source;
+
+ #### load system optical and CCD efficiency data
+ matfn0=self.information['indata_path']+'/TotalQ200923.mat'
+ da0=sio.loadmat(matfn0)
+ self.optical_blue_Q=da0['opticalQ_1'] # optical efficiency of blue channel
+ self.optical_red_Q =da0['opticalQ_2'] # optical efficiency of red channel
+ self.CCD_Qe=da0['ccdQE'] # red channel
+
+ ############################################################# load all useful data;
+ ############### load wavefront data;
+ matfn2=self.information['indata_path']+'/opd_638nm.mat'
+ da2=sio.loadmat(matfn2)
+ opd0=da2['opd'] # opd unit is in meter
+ self.opd0=opd0*1e9 # convert opd to nm
+ self.pupil=abs(opd0)>0.0
+ ####################
+
+ da=fits.open(self.information['indata_path']+'/opd1.fits')
+ self.opd1=da[0].data
+
+ da=fits.open(self.information['indata_path']+'/opd2.fits')
+ self.opd2=da[0].data
+
+ da=fits.open(self.information['indata_path']+'/opd3.fits')
+ self.opd3=da[0].data
+
+
+ ################################################################################
+ ## slice information; the slice is put in vertial direction;
+ # the silce long size is 64 pixels
+ slice_blue=dict()
+ slice_red =dict()
+
+ randRedpos=np.array([0.52835362, 1.1105926 , -0.81667794, 0.88860884, -2.78092636,
+ -0.15810022, -1.56726852, -0.71601855, -1.31768647, 1.73107581,
+ 0.4933876 , 2.83673377, 0.22226286, -0.02634998, 0.35539383,
+ -0.91989574, -2.44856212, 0.91020484, -3.03097852, -1.11638071,
+ 1.28360669, -0.12521128, -0.3907698 , 0.70183575, 1.00578099,
+ 1.67339662, 0.18067182, -0.56303075, 0.40959616, 1.45518379,
+ -0.93194744, 0.41492972])
+
+ randBluepos=np.array([ 0.97449008, -0.21371406, -1.62513338, -3.06938604, 1.72615283,
+ 0.73333374, 0.80923919, -0.9418576 , -0.16806578, -1.04416631,
+ 2.20155068, -0.0900156 , 0.07597706, 0.76208373, 0.29426592,
+ -0.89434703, 0.34017826, 1.16936499, 0.10977829, -1.31179539,
+ -0.50859787, -1.01891651, -0.95791581, -1.53018041, 0.88358827,
+ 0.07837641, -0.86157157, -1.18070438, 0.53970599, 1.4913733 ,
+ 2.10938775, 1.23213412])
+
+ #####################
+ for i in range(32):
+ if i==0:
+ slice_blue['py']=np.zeros(32)
+ slice_blue['px']=np.zeros(32)
+
+ slice_red['py']=np.zeros(32)
+ slice_red['px']=np.zeros(32)
+
+ if i<16:
+ slice_blue['py'][i]=50+randBluepos[i]*4
+ slice_blue['px'][i]=3.55/0.015*i+166.0
+
+
+ slice_red['py'][i]=50+randBluepos[i]*4
+ slice_red['px'][i]=3.55/0.015*i+1190.0
+
+ else:
+ slice_blue['py'][i]=50+250+randBluepos[i]*4
+ slice_blue['px'][i]=3.55/0.015*(i-16)+166.0+118
+
+
+ slice_red['py'][i]=50+250+randBluepos[i]*4
+ slice_red['px'][i]=3.55/0.015*(i-16)+1190.0+118
+ #######
+
+ self.slice_blue=slice_blue
+ self.slice_red=slice_red
+
+ ###############################################################################
+ maskSlice=dict()
+ maskSlit=dict()
+ sizeout=100
+ for k in range(32):
+ maskSlice[str(k)]=np.zeros((sizeout,sizeout))
+ maskSlice[str(k)][2*k+18:2*k+20, int(sizeout/2)-32:int(sizeout/2)+32]=1
+
+ maskSlit[str(k)]=np.zeros((sizeout,sizeout))
+ maskSlit[str(k)][2*k+18:2*k+20, int(sizeout/2)-37:int(sizeout/2)+37]=1
+
+ self.maskSlice=maskSlice
+ self.maskSlit =maskSlit
+ ################################################################################################
+
+
+
+
+############################################################################3
+ def readConfigs(self):
+ """
+ Reads the config file information using configParser and sets up a logger.
+ """
+ self.config = ConfigParser.RawConfigParser()
+
+ self.config.read_file(open(self.configfile))
+
+
+###############################################################################
+
+
+ def processConfigs(self):
+ """
+ Processes configuration information and save the information to a dictionary self.information.
+
+ The configuration file may look as follows::
+
+ [TEST]
+
+
+ For explanation of each field, see /data/test.config. Note that if an input field does not exist,
+ then the values are taken from the default instrument model as described in
+ support.IFSinstrumentModel.VISinformation(). Any of the defaults can be overwritten by providing
+ a config file with a correct field name.
+ """
+ #parse options and update the information dictionary
+ options = self.config.options(self.section)
+
+ settings = {}
+
+ for option in options:
+ try:
+ settings[option] = self.config.getint(self.section, option)
+ except ValueError:
+ try:
+ settings[option] = self.config.getfloat(self.section, option)
+ except ValueError:
+ settings[option] = self.config.get(self.section, option)
+
+ self.information.update(settings)
+
+ self.cosmicRays = self.config.getboolean(self.section, 'cosmicRays')
+ self.darknoise = self.config.getboolean(self.section, 'darknoise')
+ self.cosmetics = self.config.getboolean(self.section, 'cosmetics')
+ self.radiationDamage = self.config.getboolean(self.section, 'radiationDamage')
+
+ self.bleeding = self.config.getboolean(self.section, 'bleeding')
+
+ self.skyback = self.config.getboolean(self.section, 'skyback')
+
+
+ #these don't need to be in the config file
+
+ try:
+ self.nonlinearity = self.config.getboolean(self.section, 'nonlinearity')
+ except:
+ self.nonlinearity = False
+
+ try:
+ self.flatfieldM = self.config.getboolean(self.section, 'flatfieldM')
+ except:
+ self.flatfieldM = False
+
+ try:
+ self.readoutNoise = self.config.getboolean(self.section, 'readoutNoise')
+ except:
+ self.readoutNoise = True
+
+ ####################################################################
+ self.booleans = dict(nonlinearity=self.nonlinearity,
+ flatfieldM=self.flatfieldM,
+ cosmicRays=self.cosmicRays,
+ darknoise=self.darknoise,
+ cosmetics=self.cosmetics,
+ radiationDamage=self.radiationDamage,
+ bleeding=self.bleeding,
+ skyback =self.skyback)
+ #####################################################################
+
+ now=datetime.now()
+
+ result_day=now.strftime("%Y-%m-%d")
+
+ self.result_path=self.information['result_path']+'/'+result_day
+
+ if os.path.isdir(self.result_path)==False:
+ os.mkdir(self.result_path)
+ os.mkdir(self.result_path+'/log_file')
+ os.mkdir(self.result_path+'/sky_Data')
+
+ now=datetime.now()
+
+ data_time=now.strftime("%Y-%m-%d-%H-%M-%S")
+
+ self.log = lg.setUpLogger(self.result_path+'/log_file/IFS_'+'_'+data_time+'.log')
+
+ self.log.info('STARTING A NEW SIMULATION')
+
+ self.log.info(self.information)
+
+ return
+ #########################################################################################
+
+#######################################################################
+
+
+ def readCosmicRayInformation(self):
+ """
+ Reads in the cosmic ray track information from two input files.
+
+ Stores the information to a dictionary called cr.
+ """
+ self.log.info('Reading in cosmic ray information from %s and %s' % (self.information['cosmicraylengths'],
+ self.information['cosmicraydistance']))
+
+ crLengths = np.loadtxt(self.information['cosmicraylengths'])
+ crDists = np.loadtxt(self.information['cosmicraydistance'])
+
+ self.cr = dict(cr_u=crLengths[:, 0], cr_cdf=crLengths[:, 1], cr_cdfn=np.shape(crLengths)[0],
+ cr_v=crDists[:, 0], cr_cde=crDists[:, 1], cr_cden=np.shape(crDists)[0])
+
+##############################################################################################
+
+
+ def configure(self):
+ """
+ Configures the simulator with input information and creates and empty array to which the final image will
+ be build on.
+ """
+ self.readConfigs()
+
+ self.processConfigs()
+
+ self.log.info('Read in the configuration files and created an empty array')
+
+#################################################################################################################
+
+ def MakeFlatMatrix(self, img, seed):
+ ####
+ ysize, xsize=img.shape
+ np.random.seed(seed)
+ r1,r2,r3,r4 = np.random.random(4)
+ a1 = -0.5 + 0.2*r1
+ a2 = -0.5 + 0.2*r2
+ a3 = r3+5
+ a4 = r4+5
+ xmin,xmax,ymin,ymax = 0, xsize,0, ysize
+ Flty, Fltx = np.mgrid[ymin:ymax, xmin:xmax]
+ np.random.seed(seed)
+ p1,p2,bg=np.random.poisson(1000, 3)
+ Fltz = 1e-6*(a1 * (Fltx-p1) ** 2 + a2 * (Flty-p2) ** 2 - a3*Fltx - a4*Flty) + bg*20
+ FlatMat = Fltz/np.mean(Fltz)
+
+ return FlatMat
+
+ #########################################################################################################
+ def addLampFlux(self):
+ """
+ Include flux from the calibration source.
+ """
+
+
+ self.image_b += fits.getdata(self.information['flatflux'])
+
+ self.image_r += fits.getdata(self.information['flatflux'])
+
+ self.log.info('Flux from the calibration unit included (%s)' % self.information['flatflux'])
+
+
+ def applyflatfield(self):
+ """
+ Applies multiplicative flat field to emulate pixel-to-pixel non-uniformity.
+
+ Because the pixel-to-pixel non-uniformity effect (i.e. multiplicative) flat fielding takes place
+ before CTI and other effects, the flat field file must be the same size as the pixels that see
+ the sky.
+ """
+
+ ###
+ flat_b=self.MakeFlatMatrix(self.image_b, 100)
+
+ flat_r=self.MakeFlatMatrix(self.image_r, 200)
+
+ self.image_b *= flat_b
+
+ self.image_r *= flat_r
+
+ self.log.info('Applied flatfield to images.')
+
+ return
+
+ ########################################################
+
+###############################################################################
+ def addCosmicRays(self):
+ """
+ Add cosmic rays to the arrays based on a power-law intensity distribution for tracks.
+ Cosmic ray properties (such as location and angle) are chosen from random Uniform distribution.
+ For details, see the documentation for the cosmicrays class in the support package.
+ """
+ self.readCosmicRayInformation()
+ self.cr['exptime'] = self.information['exptime'] #to scale the number of cosmics with exposure time
+
+ #cosmic ray image
+ crImage_b = np.zeros((2048, 4096), dtype=np.float64)
+
+ crImage_r = np.zeros((3072, 6144), dtype=np.float64)
+
+ #cosmic ray instance
+ cosmics_b = cosmicrays.cosmicrays(self.log, crImage_b, crInfo=self.cr)
+ cosmics_r = cosmicrays.cosmicrays(self.log, crImage_r, crInfo=self.cr)
+
+
+ #add cosmic rays up to the covering fraction
+ #CCD_cr = cosmics.addUpToFraction(self.information['coveringFraction'], limit=None)
+
+ CCD_cr_b = cosmics_b.addUpToFraction(self.information['coveringfraction'], limit=None)
+ CCD_cr_r = cosmics_r.addUpToFraction(self.information['coveringfraction'], limit=None)
+
+
+ #paste the information
+ self.image_b += CCD_cr_b
+ self.image_r += CCD_cr_r
+
+
+ #count the covering factor
+ area_cr_b = np.count_nonzero(CCD_cr_b)
+ area_cr_r = np.count_nonzero(CCD_cr_r)
+
+ #self.log.info('The cosmic ray covering factor is %i pixels ' % area_cr)
+ self.log.info('The cosmic ray in blue channel covering factor is %i pixels ' % area_cr_b)
+ self.log.info('The cosmic ray in red channel covering factor is %i pixels ' % area_cr_r)
+
+ #########################################################
+
+#########################################################################
+
+#########################################################################
+
+
+ def applyDarkCurrent(self):
+ """
+ Apply dark current. Scales the dark with the exposure time.
+
+ Additionally saves the image without noise to a FITS file.
+ """
+
+ self.log.info('Added dark current to bule and red channel' )
+
+ ########## blue zone 1
+ self.image_b[0:1024,0:2048] += self.information['exptime'] * self.information['dark1_b']
+
+ ########## zone 4 #################
+ self.image_b[1024:2048,0:2048] += self.information['exptime'] * self.information['dark4_b']
+
+ ########## zone 2 ###################
+ self.image_b[0:1024,2048:4096] += self.information['exptime'] * self.information['dark2_b']
+
+ ########## zone 3
+ self.image_b[1024:2048,2048:4096]+= self.information['exptime'] * self.information['dark3_b']
+
+
+
+ ########## red zone 1
+ self.image_r[0:1536, 0:3072] += self.information['exptime'] * self.information['dark1_r']
+ ########## zone 4 #################
+
+ self.image_r[1536:3712,0:3072] += self.information['exptime'] * self.information['dark4_r']
+
+ ########## zone 2 ###################
+ self.image_r[0:1536,3072:6144] += self.information['exptime'] * self.information['dark2_r']
+
+ ########## zone 3
+ self.image_r[1536:3072,3072:6144] += self.information['exptime'] * self.information['dark3_r']
+
+
+#######################################################################################################3
+
+ def applyCosmicBackground(self):
+ """
+ Apply dark the cosmic background. Scales the background with the exposure time.
+
+ Additionally saves the image without noise to a FITS file.
+ """
+
+ #add background
+ bcgr = self.information['exptime'] * self.information['cosmic_bkgd']
+ #self.image += bcgr
+ self.image_b += bcgr
+ self.image_r += bcgr
+
+ self.log.info('Added cosmic background = %f' % bcgr)
+
+ if self.cosmicRays:
+ #self.imagenoCR += bcgr
+ self.imagenoCR_b += bcgr
+ self.imagenoCR_r += bcgr
+
+ ##########################################################################################
+
+
+
+##############################################################################
+ def applyPoissonNoise(self):
+ """
+ Add Poisson noise to the image.
+ """
+
+ rounded = np.rint(self.image_b) ### round to
+ residual = self.image_b.copy() - rounded #ugly workaround for multiple rounding operations...
+ rounded[rounded < 0.0] = 0.0
+
+ np.random.seed()
+ self.image_b = np.random.poisson(rounded).astype(np.float64)
+ self.log.info('Added Poisson noise on channel blue')
+ self.image_b += residual
+
+ rounded = np.rint(self.image_r) ### round to
+ residual = self.image_r.copy() - rounded #ugly workaround for multiple rounding operations...
+ rounded[rounded < 0.0] = 0.0
+
+ np.random.seed()
+ self.image_r = np.random.poisson(rounded).astype(np.float64)
+ self.log.info('Added Poisson noise on channel red')
+ self.image_r += residual
+
+
+###################################################################################################################
+
+ def applyCosmetics(self):
+ """
+ Apply cosmetic defects described in the input file.
+
+ Warning:: This method does not work if the input file has exactly one line.
+ """
+ cosmetics = np.loadtxt(self.information['cosmeticsfile_b'])
+
+ x = np.round(cosmetics[:, 0]).astype(int)
+ y = np.round(cosmetics[:, 1]).astype(int)
+ value = cosmetics[:, 2]
+
+ cosmetics_b=np.zeros((3712,6784))
+ cosmetics_r=np.zeros((3712,6784))
+ self.log.info('Adding cosmetic defects to blue channel:' )
+ for xc, yc, val in zip(x, y, value):
+ if 0 <= xc <= 6784 and 0 <= yc <= 3712:
+ #self.image[yc, xc] = val
+ self.image_b[yc, xc] = val
+ cosmetics_b[yc,xc]=val
+ self.log.info('x=%i, y=%i, value=%f' % (xc, yc, val))
+######################################################################################################
+
+ cosmetics = np.loadtxt(self.information['cosmeticsfile_r'])
+
+ x = np.round(cosmetics[:, 0]).astype(int)
+ y = np.round(cosmetics[:, 1]).astype(int)
+ value = cosmetics[:, 2]
+
+
+ self.log.info('Adding cosmetic defects to red channel:' )
+
+ for xc, yc, val in zip(x, y, value):
+ if 0 <= xc <= 6784 and 0 <= yc <= 3712:
+ #self.image[yc, xc] = val
+ self.image_r[yc, xc] = val
+ cosmetics_r[yc,xc]=val
+ self.log.info('x=%i, y=%i, value=%f' % (xc, yc, val))
+
+
+################################################################################
+
+
+ def applyRadiationDamage(self):
+ """
+ Applies CDM03 radiation model to the image being constructed.
+
+ .. seealso:: Class :`CDM03`
+ """
+
+
+ self.log.debug('Starting to apply radiation damage model...')
+ #at this point we can give fake data...
+ cti = CTI.CDM03bidir(self.information, [], log=self.log)
+ #here we need the right input data
+ self.image_b = cti.applyRadiationDamage(self.image_b.copy().transpose(), iquadrant=self.information['quadrant']).transpose()
+ self.log.info('Radiation damage added.')
+
+
+ self.log.debug('Starting to apply radiation damage model...')
+ #at this point we can give fake data...
+ cti = CTI.CDM03bidir(self.information, [], log=self.log)
+ #here we need the right input data
+ self.image_r = cti.applyRadiationDamage(self.image_r.copy().transpose(), iquadrant=self.information['quadrant']).transpose()
+ self.log.info('Radiation damage added.')
+##################################################################################
+
+
+ def applyNonlinearity(self):
+ """
+ Applies a CCD273 non-linearity model to the image being constructed.
+ """
+
+ self.log.debug('Starting to apply non-linearity model...')
+ self.image_b = IFSinstrumentModel.CCDnonLinearityModel(self.image_b.copy())
+
+ self.log.info('Non-linearity effects included.')
+
+
+ self.log.debug('Starting to apply non-linearity model...')
+ self.image_r = IFSinstrumentModel.CCDnonLinearityModel(self.image_r.copy())
+
+ self.log.info('Non-linearity effects included.')
+
+#####################################################################################
+ def applyReadoutNoise(self):
+ """
+ Applies readout noise to the image being constructed.
+
+ The noise is drawn from a Normal (Gaussian) distribution with average=0.0 and std=readout noise.
+ """
+
+
+ self.log.info('readnoise added in blue channel')
+ ########## blue zone 1
+ np.random.seed()
+ self.image_b[0:1856,0:3392] += np.random.normal(loc=0.0, scale=self.information['rn1_b'], size=(1856,3392))
+
+ ########## zone 4 #################
+ np.random.seed()
+ self.image_b[1856:3712,0:3392] += np.random.normal(loc=0.0, scale=self.information['rn4_b'], size=(1856,3392))
+
+ ########## zone 2 ###################
+ np.random.seed()
+ self.image_b[0:1856,3392:6784] += np.random.normal(loc=0.0, scale=self.information['rn2_b'], size=(1856,3392))
+
+ ########## zone 3
+ np.random.seed()
+ self.image_b[1856:3712,3392:6784]+= np.random.normal(loc=0.0, scale=self.information['rn3_b'], size=(1856,3392))
+
+
+ ############################################################################
+
+ self.log.info('readnoise added in blue channel')
+
+
+ ########## red zone 1
+ np.random.seed()
+ self.image_r[0:1856, 0:3392] += np.random.normal(loc=0.0, scale=self.information['rn1_r'], size=(1856,3392))
+
+ ########## zone 4 #################
+ np.random.seed()
+ self.image_r[1856:3712,0:3392] += np.random.normal(loc=0.0, scale=self.information['rn4_r'], size=(1856,3392))
+
+ ########## zone 2 ###################
+ np.random.seed()
+ self.image_r[0:1856,3392:6784] += np.random.normal(loc=0.0, scale=self.information['rn2_r'], size=(1856,3392))
+
+ ########## zone 3
+ np.random.seed()
+ self.image_r[1856:3712,3392:6784]+= np.random.normal(loc=0.0, scale=self.information['rn3_r'], size=(1856,3392))
+
+##########################################################################################
+
+ def electrons2ADU(self):
+ """
+ Convert from electrons to ADUs using the value read from the configuration file.
+ """
+
+
+ ###############################################################
+
+ self.log.info('Converting from electrons to ADUs using a factor of gain' )
+
+ ########## blue zone 1
+ self.image_b[0:1856,0:3392] /= self.information['gain1_b']
+
+ ########## zone 4 #################
+ self.image_b[1856:3712,0:3392] /= self.information['gain4_b']
+
+ ########## zone 2 ###################
+ self.image_b[0:1856,3392:6784] /= self.information['gain2_b']
+
+ ########## zone 3
+ self.image_b[1856:3712,3392:6784]/= self.information['gain3_b']
+
+ ############################################################################
+
+ ########## red zone 1
+ self.image_r[0:1856, 0:3392] /= self.information['gain1_r']
+
+ ########## zone 4 #################
+ self.image_r[1856:3712,0:3392] /= self.information['gain4_r']
+
+ ########## zone 2 ###################
+ self.image_r[0:1856,3392:6784] /= self.information['gain2_r']
+
+ ########## zone 3
+ self.image_r[1856:3712,3392:6784] /= self.information['gain3_r']
+
+ ##########################################################################3
+
+
+ ####################################################################################
+
+ def applyBias(self):
+ """
+ Adds a bias level to the image being constructed.
+
+ The value of bias is read from the configure file and stored
+ in the information dictionary (key bias).
+ """
+
+
+ ########## blue zone 1
+ self.image_b[0:1856,0:3392] += self.information['bias1_b']
+
+ ########## zone 4 #################
+ self.image_b[1856:3712,0:3392] += self.information['bias4_b']
+
+ ########## zone 2 ###################
+ self.image_b[0:1856,3392:6784] += self.information['bias2_b']
+
+ ########## zone 3
+ self.image_b[1856:3712,3392:6784] += self.information['bias3_b']
+
+ self.log.info('Bias counts were added to the blue image' )
+
+ ############################################################################
+
+ ########## red zone 1
+ self.image_r[0:1856, 0:3392] += self.information['bias1_r']
+
+ ########## zone 4 #################
+ self.image_r[1856:3712,0:3392] += self.information['bias4_r']
+
+ ########## zone 2 ###################
+ self.image_r[0:1856,3392:6784] += self.information['bias2_r']
+
+ ########## zone 3
+ self.image_r[1856:3712,3392:6784] += self.information['bias3_r']
+
+ ##########################################################################
+
+ self.log.info('Bias counts were added to the red image' )
+
+###############################################################################
+
+###############################################################################
+
+ def applyBleeding_yan(self):
+ """
+ Apply bleeding along the CCD columns if the number of electrons in a pixel exceeds the full-well capacity.
+
+ Bleeding is modelled in the parallel direction only, because the CCD273s are assumed not to bleed in
+ serial direction.
+
+ :return: None
+ """
+
+
+ if self.image_b.max()>self.information['fullwellcapacity']:
+
+ self.log.info('Applying column bleeding to blue CCD image...')
+
+ #loop over each column, as bleeding is modelled column-wise
+ for i, column in enumerate(self.image_b.T):
+ sum = 0.
+ for j, value in enumerate(column):
+ #first round - from bottom to top (need to half the bleeding)
+ overload = value - self.information['fullwellcapacity']
+ if overload > 0.:
+ overload /= 2.
+ #self.image[j, i] -= overload
+ self.image_b[j, i] -= overload
+
+ sum += overload
+
+ elif sum > 0.:
+ if -overload > sum:
+ overload = -sum
+
+ self.image_b[j, i] -= overload
+ sum += overload
+
+ for i, column in enumerate(self.image_b.T):
+ sum = 0.
+ for j, value in enumerate(column[::-1]):
+ #second round - from top to bottom (bleeding was half'd already, so now full)
+ overload = value - self.information['fullwellcapacity']
+ if overload > 0.:
+ #self.image[-j-1, i] -= overload
+ self.image_b[-j-1, i] -= overload
+
+ sum += overload
+ elif sum > 0.:
+ if -overload > sum:
+ overload = -sum
+ #self.image[-j-1, i] -= overload
+ self.image_b[-j-1, i] -= overload
+
+
+ sum += overload
+ print('Applying column bleeding to blue image finished.......')
+
+
+ ######################################################################
+ if self.image_r.max()>self.information['fullwellcapacity']:
+
+ self.log.info('Applying column bleeding to red CCD image...')
+
+ for i, column in enumerate(self.image_r.T):
+ sum = 0.
+ for j, value in enumerate(column):
+ #first round - from bottom to top (need to half the bleeding)
+ overload = value - self.information['fullwellcapacity']
+ if overload > 0.:
+ overload /= 2.
+ #self.image[j, i] -= overload
+ self.image_r[j, i] -= overload
+
+ sum += overload
+ elif sum > 0.:
+ if -overload > sum:
+ overload = -sum
+ #self.image[j, i] -= overload
+ self.image_r[j, i] -= overload
+
+
+ sum += overload
+
+ for i, column in enumerate(self.image_r.T):
+ sum = 0.
+ for j, value in enumerate(column[::-1]):
+ #second round - from top to bottom (bleeding was half'd already, so now full)
+ overload = value - self.information['fullwellcapacity']
+ if overload > 0.:
+ #self.image[-j-1, i] -= overload
+ self.image_r[-j-1, i] -= overload
+
+ sum += overload
+ elif sum > 0.:
+ if -overload > sum:
+ overload = -sum
+ #self.image[-j-1, i] -= overload
+ self.image_r[-j-1, i] -= overload
+
+
+ sum += overload
+ print('Applying column bleeding to red image finished.......')
+
+ ############################################################################
+
+ ############################################################################
+
+ def discretise(self, max=2**16-1):
+ """
+ Converts a floating point image array (self.image) to an integer array with max values
+ defined by the argument max.
+
+ :param max: maximum value the the integer array may contain [default 65k]
+ :type max: float
+
+ :return: None
+ """
+ #avoid negative numbers in case bias level was not added
+ self.image_b[self.image_b < 0.0] = 0.
+ #cut of the values larger than max
+ self.image_b[self.image_b > max] = max
+
+ self.image_b = np.rint(self.image_b).astype(int)
+ self.log.info('Maximum and total values of the image are %i and %i, respectively' % (np.max(self.image_b),
+ np.sum(self.image_b)))
+
+ #avoid negative numbers in case bias level was not added
+ self.image_r[self.image_r < 0.0] = 0.
+ #cut of the values larger than max
+ self.image_r[self.image_r > max] = max
+
+ self.image_r = np.rint(self.image_r).astype(int)
+ self.log.info('Maximum and total values of the image are %i and %i, respectively' % (np.max(self.image_r),
+ np.sum(self.image_r)))
+
+ ##################################################################################################
+
+ def applyImageShift(self):
+
+ np.random.seed()
+ ud= np.random.random() # Choose a random rotation
+ dx= 2* (ud-0.5) * self.information['shiftmax']
+
+ np.random.seed()
+ ud= np.random.random() # Choose a random rotation
+ dy= 2* (ud-0.5) * self.information['shiftmax']
+
+ self.image_b= ndimage.shift(self.image_b.copy(), [dy+self.information['shift_b_y'] , dx+self.information['shift_b_x']], order=0, mode='nearest')
+ self.image_r= ndimage.shift(self.image_r.copy(), [dy+self.information['shift_r_y'] , dx+self.information['shift_r_x']], order=0, mode='nearest')
+
+ self.log.info('Applied image shifting to g r i channels.')
+ self.information['ra'] = dx*self.information['pixel_size']
+ self.information['dec'] = dy*self.information['pixel_size']
+
+
+######################################################################################################33
+
+ def applyImageRotate(self ):
+ np.random.seed()
+ ud = np.random.random() # Choose a random rotation
+ angle = 2* (ud-0.5) * self.information['tel_rotmax']
+
+ inputimg=self.image_b.copy()
+ rotimg=ndimage.rotate(inputimg,angle+self.information['rotate_b'],order=1,reshape=False) # here we choose reshape=False, the rotated image will
+ self.image_b=rotimg
+
+ inputimg=self.image_r.copy()
+ rotimg=ndimage.rotate(inputimg,angle+self.information['rotate_r'],order=1,reshape=False) # here we choose reshape=False, the rotated image will
+ self.image_r=rotimg
+
+ self.information['Tel_rot']=angle
+
+ self.log.info('Applied telescope rotation with angle (in degree)= %f.', angle)
+###############################################################################
+
+ def CCDreadout(self):
+
+ imgb=self.image_b.copy()
+ temp=np.zeros((3712,6784))
+ ########## zone 1
+ x1=0
+ x2=x1+1024
+
+ y1=0
+ y2=y1+2048
+ temp[x1:x2,y1:y2]=imgb[0:1024,0:2048]
+ ########## zone 4 #################
+ x1=2688
+ x2=x1+1024
+
+ y1=0
+ y2=y1+2048
+ temp[x1:x2,y1:y2]=imgb[1024:2048,0:2048]
+ ########## zone 2 ###################
+ x1=0
+ x2=x1+1024
+
+ y1=6784-2048
+ y2=y1+2048
+ temp[x1:x2,y1:y2]=imgb[0:1024,2048:4096]
+ ########## zone 3
+ x1=2688
+ x2=x1+1024
+
+ y1=6784-2048
+ y2=y1+2048
+ temp[x1:x2,y1:y2]=imgb[1024:2048,2048:4096]
+
+ self.image_b=temp
+
+ ##############################################################################
+ imgr=self.image_r.copy()
+ temp=np.zeros((3712,6784))
+ ########## zone 1
+ x1=0
+ x2=x1+1536
+
+ y1=0
+ y2=y1+3072
+ temp[x1:x2,y1:y2]=imgr[0:1536, 0:3072]
+ ########## zone 4 #################
+ x1=2176
+ x2=x1+1536
+
+ y1=0
+ y2=y1+3072
+ temp[x1:x2,y1:y2]=imgr[1536:3712,0:3072]
+ ########## zone 2 ###################
+ x1=0
+ x2=x1+1536
+
+ y1=6784-3072
+ y2=y1+3072
+ temp[x1:x2,y1:y2]=imgr[0:1536,3072:6144]
+ ########## zone 3
+ x1=2176
+ x2=x1+1536
+
+ y1=6784-3072
+ y2=y1+3072
+ temp[x1:x2,y1:y2]=imgr[1536:3072,3072:6144]
+
+ self.image_r=temp
+
+ return
+##############################################################################
+
+ def writeOutputs(self):
+ """
+ Writes out a FITS file using PyFITS and converts the image array to 16bit unsigned integer as
+ appropriate for VIS.
+
+ Updates header with the input values and flags used during simulation.
+ """
+
+ ## Readout information
+ self.source='sci'
+ now=datetime.now()
+ data_time=now.strftime("%Y-%m-%d %H:%M:%S")
+ exp_endtime=now.strftime("%Y%m%d%H%M%S")
+ start=now-timedelta(seconds=self.information['exptime'])
+ exp_starttime=start.strftime("%Y%m%d%H%M%S")
+
+ #write the actual file
+ obsid=300000000+1
+
+ data_time=self.dt.strftime("%Y-%m-%d %H:%M:%S")
+
+ exp_starttime=self.dt.strftime("%Y%m%d%H%M%S")
+ ### exposure end time is t2 ;
+ t2=self.dt+timedelta(seconds=self.information['exptime'])
+ exp_endtime=t2.strftime("%Y%m%d%H%M%S")
+ t3=self.dt+timedelta(seconds=self.information['exptime'])+timedelta(seconds=self.information['readouttime'])
+
+ filename_b='CSST_IFS_B_'+self.source+'_'+exp_starttime+'_'+exp_endtime+'_'+str(obsid)+'_X_L0_VER_'+self.information['img_ver']+'.fits'
+ file_b=self.result_path+'/sky_Data/'+filename_b
+
+ filename_r='CSST_IFS_R_'+self.source+'_'+exp_starttime+'_'+exp_endtime+'_'+str(obsid)+'_X_L0_VER_'+self.information['img_ver']+'.fits'
+ file_r=self.result_path+'/sky_Data/'+filename_r
+
+
+ #create a new FITS file, using HDUList instance
+ ofd_b = fits.PrimaryHDU()
+
+ ofd_b.header['GROUPS']=( bool(False), 'always F')
+ ofd_b.header['DATE'] =( data_time, 'date this file was written' )
+
+ ofd_b.header['FILENAME']=(filename_b, ' file name C48 ')
+ ofd_b.header['OBSTYPE'] =( self.source, 'observation type raw,flt, mask, bias, dark, sci')
+ ofd_b.header['TELESCOP']=('CSST', 'always CSST')
+ ofd_b.header['INSTRUME']=( 'IFS', ' ')
+ ofd_b.header['RADECSYS']=('ICRS', ' always ICRS ')
+ ofd_b.header['EQUINOX'] =( float(2000.0), 'always 2000.0')
+ ofd_b.header['FITSCREA']=( '4.2.1', 'FITS create software version')
+
+ ######### Object information #############
+
+ ofd_b.header['OBJECT']=( self.information['name_obj'], 'object name')
+ ofd_b.header['TARGET']=( (self.information['target']), 'target name, hhmmss+ddmmss')
+ ofd_b.header['OBJ_RA'] =(np.float64(self.information['ra_obj']) , 'RA of the object in deg')
+ ofd_b.header['OBJ_DEC']=(np.float64(self.information['dec_obj']) , 'DEC of the object in deg')
+
+ ofd_b.header['RA_PNT0']=( np.float64(self.information['ra_pnt0']) , 'RA of the pointing (degrees) at EXPSTART')
+ ofd_b.header['DEC_PNT0']=(np.float64(self.information['dec_pnt0']) , 'DEC of the pointing (degrees) at EXPSTART')
+
+
+
+ ##############
+ ofd_b.header['OBSID'] =(str(obsid) , 'observation ID, 3+8bit')
+
+ ######## Telescope information ###############
+ # ofd_b.header['COMMENT'] ='=========================================================='
+ # ofd_b.header['COMMENT'] ='Telescope information'
+ # ofd_b.header['COMMENT'] ='=========================================================='
+
+ ofd_b.header['REFFRAME']=('CSSTGSC-1.0' , 'guide star catalog version')
+ ofd_b.header['DATE-OBS']=(data_time , 'date of the observation (yyyy-mm-dd hh:mm:ss)')
+
+ ofd_b.header['EXPSTART']=(np.float64(time2jd(self.dt)), 'exposure start time')
+ ofd_b.header['SUNANGL0']=(np.float32(0.0) , 'angle between sun and optical axis at EXPSTART')
+ ofd_b.header['MOONANG0']=(np.float32(0.0) , 'angle between moon and optical axis at EXPSTART')
+ ofd_b.header['POS_ANG0']=(np.float64(0.0), 'angle between optical axis and the North Pole at EXPSTART in arcsec')
+ ofd_b.header['TEL_ALT0']=(np.float64(0.0), 'angle between optical axis and the ground- piston at EXPSTART in deg')
+
+ ofd_b.header['HOODSTA0']=(np.float32(0.0) , 'lens hood altitude at EXPSTART')
+ ofd_b.header['HOODANG0']=(np.float32(0.0), 'lens hood azimuth at EXPSTART')
+ ofd_b.header['POSI0_X'] =(np.float64(self.information['POSI0_X']) , 'the orbital position of CSST in X direction at EXPSTART')
+ ofd_b.header['POSI0_Y'] =(np.float64(self.information['POSI0_Y']) , 'the orbital position of CSST in Y direction at EXPSTART')
+ ofd_b.header['POSI0_Z'] =(np.float64(self.information['POSI0_Z']) , 'the orbital position of CSST in Z direction at EXPSTART')
+ ofd_b.header['VELO0_X']=( np.float64(self.information['VELO0_X']) , 'the orbital velocity of CSST in X direction at EXPSTART')
+ ofd_b.header['VELO0_Y']=( np.float64(self.information['VELO0_Y']) , 'the orbital velocity of CSST in Y direction at EXPSTART')
+ ofd_b.header['VELO0_Z']=( np.float64(self.information['VELO0_Z']) , 'the orbital velocity of CSST in Z direction at EXPSTART')
+
+ ofd_b.header['Euler0_1']=( np.float64(0.0), 'Euler angle 1 at EXPSTART')
+ ofd_b.header['Euler0_2']=( np.float64(0.0), 'Euler angle 2 at EXPSTART')
+ ofd_b.header['Euler0_3']=( np.float64(0.0), 'Euler angle 3 at EXPSTART')
+
+
+ ofd_b.header['EXPEND'] =(np.float64(time2jd(t2)) , 'exposure end time')
+
+ ofd_b.header['SUNANGL1']=(np.float32(0.0), 'angle between sun and optical axis at EXPEND')
+ ofd_b.header['MOONANG1']=(np.float32(0.0) , 'angle between moon and optical axis at EXPEND ')
+ ofd_b.header['POS_ANG1']=(np.float64(0.0) , 'angle between optical axis and the North Pole at EXPEND in arcsec')
+ ofd_b.header['TEL_ALT1']=(np.float64(0.0) , 'angle between optical axis and the ground- piston at EXPEND in deg ')
+ ofd_b.header['HOODSTA1']=(np.float32(0.0), 'lens hood altitude at EXPEND ')
+ ofd_b.header['HOODANG1']=(np.float32(0.0), 'lens hood azimuth at EXPEND ')
+
+ ofd_b.header['POSI1_X'] =(np.float64(self.information['POSI1_X']) , 'the orbital position of CSST in X direction at EXPEND')
+ ofd_b.header['POSI1_Y'] =(np.float64(self.information['POSI1_Y']) , 'the orbital position of CSST in Y direction at EXPEND')
+ ofd_b.header['POSI1_Z'] =(np.float64(self.information['POSI1_Z']) , 'the orbital position of CSST in Z direction at EXPEND')
+
+ ofd_b.header['VELO1_X']=(np.float64(self.information['VELO1_X']) , 'the orbital velocity of CSST in X direction at EXPEND')
+ ofd_b.header['VELO1_Y']=(np.float64(self.information['VELO1_Y']) , 'the orbital velocity of CSST in Y direction at EXPEND')
+ ofd_b.header['VELO1_Z']=(np.float64(self.information['VELO1_Z']) , 'the orbital velocity of CSST in Z direction at EXPEND')
+
+
+ ofd_b.header['Euler1_1']=( np.float64(0.0), 'Euler angle 1 at EXPEND')
+ ofd_b.header['Euler1_2']=( np.float64(0.0), 'Euler angle 2 at EXPEND')
+ ofd_b.header['Euler1_3']=( np.float64(0.0), 'Euler angle 3 at EXPEND')
+
+ ofd_b.header['RA_PNT1']=(np.float64(ofd_b.header['RA_PNT0']), 'RA of the pointing (degrees) at EXPEND in deg')
+ ofd_b.header['DEC_PNT1']=(np.float64(ofd_b.header['DEC_PNT0']), 'DEC of the pointing (degrees) at EXPEND in deg')
+
+ ofd_b.header['EXPTIME']=(self.information['exptime'], 'exposure duration')
+ ofd_b.header['EPOCH'] =(np.float32(0.0), 'coordinate epoch')
+ ofd_b.header['CHECKSUM']=( 0 , 'hdu-checksum')
+
+ ########## finish header for 0 layer
+ #############################################################################3
+ ##### header
+
+ b1= self.image_b[1856:3712,0:3392] #b4
+ b2= self.image_b[1856:3712,3392:6784] #b3
+ b3= self.image_b[0:1856,0:3392] #b1
+ b4= self.image_b[0:1856,3392:6784] #b2
+
+ ####### do Flip the b2 b2 and b4 array in the up/down or left/right direction.
+ b2=np.fliplr(b2) ## left to right
+ b3=np.flipud(b3) ## down to up
+
+ b4=np.fliplr(b4) ## left to right and down to up
+ b4=np.flipud(b4)
+
+
+ bb=np.hstack((b1,b2,b3,b4))
+
+
+ #new image HDU, blue channel, layer 1
+ hdu_b =fits.ImageHDU(data=np.uint16(bb))
+
+ ######### instrument information ######
+ #####
+ hdu_b.header['PMIRRPOS']=(bool(False), 'FSM pointing,T: to MCI, F: not to MCI')
+
+ if self.source =='sci':
+ hdu_b.header['CMIRRPOS']=(bool(False), 'position of calibration switch mirror,T: for calibration, F: not')
+ else:
+ hdu_b.header['CMIRRPOS']=(bool(True), 'position of calibration switch mirror,T: for calibration, F: not')
+
+ if self.source=='flat':
+ hdu_b.header['FLAMP'] =(int(1), 'status of flat lamp,0: off, 1: , 2: ')
+ else:
+ hdu_b.header['FLAMP'] =(int(0), 'status of flat lamp,0: off, 1: , 2: ')
+
+
+ if self.source=='lamp':
+ hdu_b.header['ALAMP'] =(int(1),'status of atomic emission line lamp,0: off, 1: , 2: ')
+ else:
+ hdu_b.header['ALAMP'] =(int(0),'status of atomic emission line lamp,0: off, 1: , 2: ')
+
+ #############
+ hdu_b.header['IFSMODE'] =(int(0), 'IFS working mode')
+ hdu_b.header['IFSTEMP'] =(float(0.0), 'IFS components temperature in degC')
+ hdu_b.header['IFSSTAT'] =(int(0), 'IFS components status parameter')
+ ##############################################################################
+ ################### detector information#############################
+ # hdu_b.header['COMMENT'] ='=========================================================='
+ # hdu_b.header['COMMENT'] ='Detector information'
+ # hdu_b.header['COMMENT'] ='=========================================================='
+
+ hdu_b.header['CAMERA'] =('Blue','camera of IFS')
+ hdu_b.header['DETNAM'] =('CCD231-c4','detector name')
+ hdu_b.header['DETSIZE'] =('', 'detector size')
+ hdu_b.header['DATASEC'] =('', 'data section')
+ hdu_b.header['PIXSCAL1']=(1856, 'pixel scale for axis 1')
+ hdu_b.header['PIXSCAL2']=(3392*4, 'pixel scale for axis 2')
+ hdu_b.header['PIXSIZE1']=(15, 'pixel size in um')
+ hdu_b.header['PIXSIZE2']=(15, 'pixel size in um')
+ hdu_b.header['NCHAN'] =(4, 'number of readout channels')
+ hdu_b.header['NCHAN1'] =(2, 'number of horizontal channels')
+ hdu_b.header['NCHAN2'] =(2, 'number of verticalchannels')
+ hdu_b.header['PSCAN1'] =(0, 'horizontal prescan width, per readout channel')
+ hdu_b.header['PSCAN2'] =(0, 'vertical prescan width, per readout channel')
+ hdu_b.header['OSCAN1'] =(0,' horizontal overscan width, per readout channel')
+ hdu_b.header['OSCAN2'] =(0, 'vertical overscan width, per readout channel')
+
+ ## Readout information
+ # hdu_b.header['COMMENT'] ='============================================================='
+ # hdu_b.header['COMMENT'] ='Readout information'
+ # hdu_b.header['COMMENT'] ='============================================================='
+
+
+ hdu_b.header['READT0'] =(np.float64(time2jd(t2)),'read start time (UTC)')
+ hdu_b.header['READT1'] =(np.float64(time2jd(t3)), 'read end time (UTC)')
+
+ hdu_b.header['DETTEMP0']=(np.float32(0.0), 'detector temperature at READT0')
+ hdu_b.header['DETTEMP1']=(np.float32(0.0), 'detector temperature at READT1')
+ hdu_b.header['BIN_X'] =(0, 'bin number in X (wavelength)')
+ hdu_b.header['BIN_Y'] =(0, 'bin number in Y (spatial)')
+
+ hdu_b.header['GAIN1'] =(self.information['gain4_b'],'CCD gain (channel 1)')
+ hdu_b.header['GAIN2'] =(self.information['gain3_b'],'CCD gain (channel 2)')
+ hdu_b.header['GAIN3'] =(self.information['gain1_b'],'CCD gain (channel 3)')
+ hdu_b.header['GAIN4'] =(self.information['gain2_b'],'CCD gain (channel 4)')
+
+ hdu_b.header['DARK1'] =(self.information['dark4_b'],'CCD dark (channel 1)')
+ hdu_b.header['DARK2'] =(self.information['dark3_b'],'CCD dark (channel 2)')
+ hdu_b.header['DARK3'] =(self.information['dark1_b'],'CCD dark (channel 3)')
+ hdu_b.header['DARK4'] =(self.information['dark2_b'],'CCD dark (channel 4)')
+
+
+ hdu_b.header['RDNOIS1'] =(self.information['rn4_b'],'read noise (channel 1')
+ hdu_b.header['RDNOIS2'] =(self.information['rn3_b'],'read noise (channel 2')
+ hdu_b.header['RDNOIS3'] =(self.information['rn1_b'],'read noise (channel 3')
+ hdu_b.header['RDNOIS4'] =(self.information['rn2_b'],'read noise (channel 4')
+
+ hdu_b.header['DETBIA1'] =(self.information['bias4_b'],'amplifier bias voltage (channel1)')
+ hdu_b.header['DETBIA2'] =(self.information['bias3_b'],'amplifier bias voltage (channel2)')
+ hdu_b.header['DETBIA3'] =(self.information['bias1_b'],'amplifier bias voltage (channel3)')
+ hdu_b.header['DETBIA4'] =(self.information['bias2_b'],'amplifier bias voltage (channel4)')
+
+ hdu_b.header['RDSPEED'] =(100,'read speed (in MHz)')
+
+ hdu_b.header['EXPTIME'] =(self.information['exptime'],'exposure time in seconds')
+
+ hdu_b.header['Img_Ver'] =(self.information['img_ver'], 'IFS CCD image Version')
+
+ hdu_b.header['sky_obj'] =(self.skyfilepath,'input sky fits filepath')
+
+
+ ##########################################################
+ #################### red camera ######################
+
+
+ #create a new FITS file, using HDUList instance
+ ofd_r = fits.PrimaryHDU()
+
+ ofd_r.header['GROUPS']=( bool(False), 'always F')
+ ofd_r.header['DATE'] =( data_time, 'date this file was written' )
+
+ ofd_r.header['FILENAME']=(filename_r, ' file name C48 ')
+
+ ofd_r.header['OBSTYPE'] =( self.source, 'observation type raw,flt, mask, bias, dark, sci')
+
+ ofd_r.header['TELESCOP']=('CSST' , 'always CSST')
+ ofd_r.header['INSTRUME']=( 'IFS', ' ')
+ ofd_r.header['RADECSYS']=('ICRS', ' always ICRS ')
+ ofd_r.header['EQUINOX'] =( float(2000.0), 'always 2000.0')
+ ofd_r.header['FITSCREA']=( '4.2.1' , 'FITS create software version')
+ ######### Object information #############
+
+ ######### Object information #############
+ # ofd_r.header['COMMENT']='======================================================================='
+ # ofd_r.header['COMMENT']='Object information'
+ # ofd_r.header['COMMENT']='======================================================================='
+
+ ofd_r.header['OBJECT']=( self.information['name_obj'], 'object name')
+ ofd_r.header['TARGET']=( (self.information['target']), 'target name, hhmmss+ddmmss')
+ ofd_r.header['OBJ_RA'] =(np.float64(self.information['ra_obj']) , 'RA of the object in deg')
+ ofd_r.header['OBJ_DEC']=(np.float64(self.information['dec_obj']) , 'DEC of the object in deg')
+
+ ofd_r.header['RA_PNT0']=( np.float64(self.information['ra_pnt0']) , 'RA of the pointing (degrees) at EXPSTART')
+ ofd_r.header['DEC_PNT0']=(np.float64(self.information['dec_pnt0']) , 'DEC of the pointing (degrees) at EXPSTART')
+
+ ofd_r.header['OBSID'] =(str(obsid) , 'observation ID, 3+8bit ')
+
+ ######## Telescope information ###############
+ # ofd_r.header['COMMENT']='======================================================================='
+ # ofd_r.header['COMMENT']='Telescope information'
+ # ofd_r.header['COMMENT']='======================================================================='
+
+ ofd_r.header['REFFRAME']=('CSSTGSC-1.0' , 'guide star catalog version ')
+ ofd_r.header['DATE-OBS']=(data_time , 'date of the observation (yyyy-mm-dd hh:mm:ss)')
+
+ ofd_r.header['EXPSTART']=(np.float64(exp_starttime), 'exposure start time ')
+ ofd_r.header['SUNANGL0']=(np.float32(0.0) , 'angle between sun and optical axis at EXPSTART ')
+ ofd_r.header['MOONANG0']=(np.float32(0.0) , 'angle between moon and optical axis at EXPSTART ')
+ ofd_r.header['POS_ANG0']=(np.float64(0.0), 'angle between optical axis and the North Pole at EXPSTART in arcsec ')
+ ofd_r.header['TEL_ALT0']=(np.float64(0.0), 'angle between optical axis and the ground- piston at EXPSTART in deg ')
+ ofd_r.header['HOODSTA0']=(np.float32(0.0) , 'lens hood altitude at EXPSTART ')
+ ofd_r.header['HOODANG0']=(np.float32(0.0), 'lens hood azimuth at EXPSTART ')
+ ofd_r.header['POSI0_X'] =(np.float64(self.information['POSI0_X']) , 'the orbital position of CSST in X direction at EXPSTART')
+ ofd_r.header['POSI0_Y'] =(np.float64(self.information['POSI0_Y']) , 'the orbital position of CSST in Y direction at EXPSTART')
+ ofd_r.header['POSI0_Z'] =(np.float64(self.information['POSI0_Z']) , 'the orbital position of CSST in Z direction at EXPSTART')
+ ofd_r.header['VELO0_X']=( np.float64(self.information['VELO0_X']) , 'the orbital velocity of CSST in X direction at EXPSTART')
+ ofd_r.header['VELO0_Y']=( np.float64(self.information['VELO0_Y']) , 'the orbital velocity of CSST in Y direction at EXPSTART')
+ ofd_r.header['VELO0_Z']=( np.float64(self.information['VELO0_Z']) , 'the orbital velocity of CSST in Z direction at EXPSTART')
+
+ ofd_r.header['Euler0_1']=( np.float64(0.0), 'Euler angle 1 at EXPSTART')
+ ofd_r.header['Euler0_2']=( np.float64(0.0), 'Euler angle 2 at EXPSTART')
+ ofd_r.header['Euler0_3']=( np.float64(0.0), 'Euler angle 3 at EXPSTART')
+
+ ofd_r.header['EXPEND'] =(np.float64(exp_endtime) , 'exposure end time')
+
+ ofd_r.header['SUNANGL1']=(np.float32(0.0), 'angle between sun and optical axis at EXPEND')
+ ofd_r.header['MOONANG1']=(np.float32(0.0) , 'angle between moon and optical axis at EXPEND ')
+ ofd_r.header['POS_ANG1']=(np.float64(0.0) , 'angle between optical axis and the North Pole at EXPEND in arcsec')
+ ofd_r.header['TEL_ALT1']=(np.float64(0.0) , 'angle between optical axis and the ground- piston at EXPEND in deg ')
+ ofd_r.header['HOODSTA1']=(np.float32(0.0), 'lens hood altitude at EXPEND ')
+ ofd_r.header['HOODANG1']=(np.float32(0.0), 'lens hood azimuth at EXPEND ')
+
+ ofd_r.header['POSI1_X'] =(np.float64(self.information['POSI1_X']) , 'the orbital position of CSST in X direction at EXPEND')
+ ofd_r.header['POSI1_Y'] =(np.float64(self.information['POSI1_Y']) , 'the orbital position of CSST in Y direction at EXPEND')
+ ofd_r.header['POSI1_Z'] =(np.float64(self.information['POSI1_Z']) , 'the orbital position of CSST in Z direction at EXPEND')
+
+ ofd_r.header['VELO1_X']=(np.float64(self.information['VELO1_X']) , 'the orbital velocity of CSST in X direction at EXPEND')
+ ofd_r.header['VELO1_Y']=(np.float64(self.information['VELO1_Y']) , 'the orbital velocity of CSST in Y direction at EXPEND')
+ ofd_r.header['VELO1_Z']=(np.float64(self.information['VELO1_Z']) , 'the orbital velocity of CSST in Z direction at EXPEND')
+
+ ofd_r.header['Euler1_1']=( np.float64(0.0), 'Euler angle 1 at EXPEND')
+ ofd_r.header['Euler1_2']=( np.float64(0.0), 'Euler angle 2 at EXPEND')
+ ofd_r.header['Euler1_3']=( np.float64(0.0), 'Euler angle 3 at EXPEND')
+
+ ofd_r.header['RA_PNT1']=(np.float64(ofd_r.header['RA_PNT0']), 'RA of the pointing (degrees) at EXPEND in deg')
+ ofd_r.header['DEC_PNT1']=(np.float64(ofd_r.header['DEC_PNT0']), 'DEC of the pointing (degrees) at EXPEND in deg')
+
+ ofd_r.header['EXPTIME']=(self.information['exptime'], 'exposure duration')
+ ofd_r.header['EPOCH'] =(np.float32(0.0), 'coordinate epoch')
+ ofd_r.header['CHECKSUM']=( 0 , 'hdu-checksum')
+
+ ### finish 0 layer header
+
+ ########## finish header for 0 layer
+
+ # ########## blue zone 1--to--3
+ # self.image_r[0:1856,0:3392] += self.information['bias1_r']
+
+ # ########## zone 4 --to---1 #################
+ # self.image_r[1856:3712,0:3392] += self.information['bias4_r']
+
+ # ########## zone 2 ----to----4 ###################
+ # self.image_r[0:1856,3392:6784] += self.information['bias2_r']
+
+ # ########## zone 3 ---to------2
+ # self.image_r[1856:3712,3392:6784] += self.information['bias3_r']
+ #############################################################################3
+ ##### header
+
+ b1= self.image_r[1856:3712,0:3392]
+ b2= self.image_r[1856:3712,3392:6784]
+ b3= self.image_r[0:1856,0:3392]
+ b4= self.image_r[0:1856,3392:6784]
+
+
+ ####### do Flip the b2 b2 and b4 array in the up/down or left/right direction.
+ b2=np.fliplr(b2) ## left to right
+ b3=np.flipud(b3) ## down to up
+
+ b4=np.fliplr(b4) ## left to right and down to up
+ b4=np.flipud(b4)
+
+
+
+ rr=np.hstack((b1,b2,b3,b4))
+ #new image HDU, blue channel, layer 1
+ hdu_r =fits.ImageHDU(data=np.uint16(rr))
+
+
+ #########################################
+ ######### instrument information ######
+ hdu_r.header['PMIRRPOS']=(bool(False), 'FSM pointing,T: to MCI, F: not to MCI')
+
+
+ hdu_r.header['CMIRRPOS']=(bool(False), 'position of calibration switch mirror,T: for calibration, F: not')
+
+ hdu_r.header['FLAMP'] =(int(0), 'status of flat lamp,0: off, 1: , 2: ')
+
+ hdu_r.header['ALAMP'] =(int(0),'status of atomic emission line lamp,0: off, 1: , 2: ')
+
+ hdu_r.header['IFSMODE'] =(int(0), 'IFS working mode')
+ hdu_r.header['IFSTEMP'] =(float(0.0), 'IFS components temperature in degC')
+ hdu_r.header['IFSSTAT'] =(int(0), 'IFS components status parameter')
+
+ ################### detector information#############################
+ # hdu_r.header['COMMENT']='======================================================================='
+ # hdu_r.header['COMMENT']='Detector information'
+ # hdu_r.header['COMMENT']='======================================================================='
+
+ hdu_r.header['CAMERA'] =('Red','camera of IFS')
+ hdu_r.header['DETNAM'] =('CCD231-c4','detector name')
+ hdu_r.header['DETSIZE'] =('', 'detector size')
+ hdu_r.header['DATASEC'] =('', 'data section')
+ hdu_r.header['PIXSCAL1']=(1856, 'pixel scale for axis 1')
+ hdu_r.header['PIXSCAL2']=(3392*4, 'pixel scale for axis 2')
+ hdu_r.header['PIXSIZE1']=(15, 'pixel size in um')
+ hdu_r.header['PIXSIZE2']=(15, 'pixel size in um')
+ hdu_r.header['NCHAN'] =(4, 'number of readout channels')
+ hdu_r.header['NCHAN1'] =(2, 'number of horizontal channels')
+ hdu_r.header['NCHAN2'] =(2, 'number of verticalchannels')
+ hdu_r.header['PSCAN1'] =(0, 'horizontal prescan width, per readout channel')
+ hdu_r.header['PSCAN2'] =(0, 'vertical prescan width, per readout channel')
+ hdu_r.header['OSCAN1'] =(0,' horizontal overscan width, per readout channel')
+ hdu_r.header['OSCAN2'] =(0, 'vertical overscan width, per readout channel')
+
+#####################################################################################################
+ ## Readout information
+ # hdu_r.header['COMMENT']='======================================================================='
+ # hdu_r.header['COMMENT']='Readout information'
+ # hdu_r.header['COMMENT']='======================================================================='
+
+
+
+ hdu_r.header['READT0'] =(np.float64(time2jd(t2)),'read start time (UTC)')
+ hdu_r.header['READT1'] =(np.float64(time2jd(t3)), 'read end time (UTC)')
+
+ hdu_r.header['DETTEMP0']=(np.float32(0.0), 'detector temperature at READT0')
+ hdu_r.header['DETTEMP1']=(np.float32(0.0), 'detector temperature at READT1')
+ hdu_r.header['BIN_X'] =(0, 'bin number in X (wavelength)')
+ hdu_r.header['BIN_Y'] =(0, 'bin number in Y (spatial)')
+
+ hdu_r.header['GAIN1'] =(self.information['gain4_r'],'CCD gain (channel 1)')
+ hdu_r.header['GAIN2'] =(self.information['gain3_r'],'CCD gain (channel 2)')
+ hdu_r.header['GAIN3'] =(self.information['gain1_r'],'CCD gain (channel 3)')
+ hdu_r.header['GAIN4'] =(self.information['gain2_r'],'CCD gain (channel 4)')
+
+
+ hdu_r.header['DARK1'] =(self.information['dark4_r'],'CCD dark (channel 1)')
+ hdu_r.header['DARK2'] =(self.information['dark3_r'],'CCD dark (channel 2)')
+ hdu_r.header['DARK3'] =(self.information['dark1_r'],'CCD dark (channel 3)')
+ hdu_r.header['DARK4'] =(self.information['dark2_r'],'CCD dark (channel 4)')
+
+
+ hdu_r.header['RDNOIS1'] =(self.information['rn4_r'],'read noise (channel 1')
+ hdu_r.header['RDNOIS2'] =(self.information['rn3_r'],'read noise (channel 2')
+ hdu_r.header['RDNOIS3'] =(self.information['rn1_r'],'read noise (channel 3')
+ hdu_r.header['RDNOIS4'] =(self.information['rn2_r'],'read noise (channel 4')
+
+ hdu_r.header['DETBIA1'] =(self.information['bias4_r'],'amplifier bias voltage (channel1)')
+ hdu_r.header['DETBIA2'] =(self.information['bias3_r'],'amplifier bias voltage (channel2)')
+ hdu_r.header['DETBIA3'] =(self.information['bias1_r'],'amplifier bias voltage (channel3)')
+ hdu_r.header['DETBIA4'] =(self.information['bias2_r'],'amplifier bias voltage (channel4)')
+
+ hdu_r.header['RDSPEED'] =(100,'read speed (in MHz)')
+
+ hdu_r.header['EXPTIME'] =(self.information['exptime'],'exposure time in seconds')
+
+ hdu_r.header['Img_Ver'] =(self.information['img_ver'], 'IFS CCD image Version')
+
+ hdu_r.header['sky_obj'] =(self.skyfilepath,'input sky fits filename')
+
+
+
+ hdulist_b=fits.HDUList([ofd_b, hdu_b])
+ hdulist_b.writeto(file_b, overwrite=True)
+ #print('IFS_b.fits is created ')
+
+ hdulist_r=fits.HDUList([ofd_r, hdu_r])
+ hdulist_r.writeto(file_r, overwrite=True)
+ #print('IFS_r.fits is created ')
+ ##################################################################################
+
+ def earthshine(self,theta):
+ """
+ For given theta angle, return the earth-shine spectrum.
+
+ :param theta: angle (in degree) from the target to earth limb.
+ :return: the scaled solar spectrum
+ template_wave: unit in A
+ template_flux: unit in erg/s/cm^2/A/arcsec^2
+
+ """
+
+ # read solar template
+ solar_template = pd.read_csv(self.information['indata_path']+'/refs/solar_spec.dat', sep='\s+',
+ header=None, comment='#')
+ template_wave = solar_template[0].values
+ template_flux = solar_template[1].values
+
+ # read earth shine surface brightness
+ earthshine_curve = pd.read_csv(self.information['indata_path']+'/refs/earthshine.dat',
+ header=None, comment='#')
+ angle = earthshine_curve[0].values
+ surface_brightness = earthshine_curve[1].values
+
+ # read V-band throughtput
+ cat_filter_V = pd.read_csv(self.information['indata_path']+'/refs/filter_Bessell_V.dat', sep='\s+',
+ header=None, comment='#')
+ filter_wave = cat_filter_V[0].values
+ filter_response = cat_filter_V[1].values
+
+ # interplate to the target wavelength in V-band
+ ind_filter = (template_wave >= np.min(filter_wave)) & (template_wave <= np.max(filter_wave))
+ filter_wave_interp = template_wave[ind_filter]
+ filter_response_interp = np.interp(filter_wave_interp, filter_wave, filter_response)
+
+ filter_constant = simps(filter_response_interp * filter_wave_interp, filter_wave_interp)
+ template_constant = simps(filter_response_interp * template_wave[ind_filter] * template_flux[ind_filter],
+ template_wave[ind_filter])
+ dwave = filter_wave_interp[1:] - filter_wave_interp[:-1]
+ wave_eff = np.nansum(dwave * filter_wave_interp[1:] * filter_response_interp[1:]) / \
+ np.nansum(dwave * filter_response_interp[1:])
+
+ # get the normalized value at theta.
+ u0 = np.interp(theta, angle, surface_brightness) # mag/arcsec^2
+ u0 = 10**((u0 + 48.6)/(-2.5)) # target flux in erg/s/cm^2/Hz unit
+ u0 = u0 * 3e18 / wave_eff**2 # erg/s/cm^2/A/arcsec^2
+
+ factor = u0 * filter_constant / template_constant
+ norm_flux = template_flux * factor # erg/s/cm^2/A/arcsec^2
+
+ self.earthshine_wave=template_wave # A
+ self.earthshine_flux=norm_flux
+
+ return
+
+
+########################################################################################################################################################################################################################################################
+
+
+ def zodiacal(self, ra, dec, time):
+ """
+ For given RA, DEC and TIME, return the interpolated zodical spectrum in Leinert-1998.
+
+ :param ra: RA in unit of degree, ICRS frame
+ :param dec: DEC in unit of degree, ICRS frame
+ :param time: the specified string that in ISO format i.e., yyyy-mm-dd.
+ :return:
+ wave_A: wavelength of the zodical spectrum
+ spec_mjy: flux of the zodical spectrum, in unit of MJy/sr
+ spec_erg: flux of the zodical spectrum, in unit of erg/s/cm^2/A/sr
+
+ """
+
+ # get solar position
+ dt = datetime.fromisoformat(time)
+ jd = julian.to_jd(dt, fmt='jd')
+ t = Time(jd, format='jd', scale='utc')
+
+ astro_sun = get_sun(t)
+ ra_sun, dec_sun = astro_sun.gcrs.ra.deg, astro_sun.gcrs.dec.deg
+
+ radec_sun = SkyCoord(ra=ra_sun*u.degree, dec=dec_sun*u.degree, frame='gcrs')
+ lb_sun = radec_sun.transform_to('geocentrictrueecliptic')
+
+ # get offsets between the target and sun.
+ radec_obj = SkyCoord(ra=ra*u.degree, dec=dec*u.degree, frame='icrs')
+ lb_obj = radec_obj.transform_to('geocentrictrueecliptic')
+
+ beta = abs(lb_obj.lat.degree)
+ lamda = abs(lb_obj.lon.degree - lb_sun.lon.degree)
+
+ # interpolated zodical surface brightness at 0.5 um
+ zodi = pd.read_csv(self.information['indata_path']+'/refs/zodi_map.dat', sep='\s+', header=None, comment='#')
+ beta_angle = np.array([0, 5, 10, 15, 20, 25, 30, 45, 60, 75])
+ lamda_angle = np.array([0, 5, 10, 15, 20, 25, 30, 35, 40, 45,
+ 60, 75, 90, 105, 120, 135, 150, 165, 180])
+ xx, yy = np.meshgrid(beta_angle, lamda_angle)
+ f = interpolate.interp2d(xx, yy, zodi, kind='linear')
+ zodi_obj = f(beta, lamda) # 10^−8 W m−2 sr−1 um−1
+
+ # read the zodical spectrum in the ecliptic
+ cat_spec = pd.read_csv(self.information['indata_path']+'/refs/solar_spec.dat', sep='\s+', header=None, comment='#')
+ wave = cat_spec[0].values # A
+ spec0 = cat_spec[1].values # 10^-8 W m^−2 sr^−1 μm^−1
+ zodi_norm = 252 # 10^-8 W m^−2 sr^−1 μm^−1
+
+ spec = spec0 * (zodi_obj / zodi_norm) * 1e-8 # W m^−2 sr^−1 μm^−1
+
+ # convert to the commonly used unit of MJy/sr, erg/s/cm^2/A/sr
+ wave_A = wave # A
+ spec_mjy = spec * 0.1 * wave_A**2 / 3e18 * 1e23 * 1e-6 # MJy/sr
+ spec_erg = spec * 0.1 # erg/s/cm^2/A/sr
+ spec_erg2 = spec_erg / 4.25452e10 # erg/s/cm^2/A/arcsec^2
+
+ self.zodiacal_wave=wave_A # in A
+ self.zodiacal_flux=spec_erg2
+
+ return
+ ###################################################################################
+ ##################################################################################
+ def CalskyNoise(self, lam):
+
+
+ # calculate sky noise;
+ planckh= 6.62620*10**-27 # erg s;
+ cc=2.99792458*10**17 # in nm/s
+ fov2=0.01 # arcsec^2
+
+ # lam is input wavelength in nm
+
+ ##########################################
+ self.earthshine_wave # A
+ self.earthshine_flux # erg/s/cm^2/A/arcsec^2
+ earthshine_flux=np.interp(lam*10.0, self.earthshine_wave,self.earthshine_flux) # flux from zodiacal
+
+ self.zodiacal_wave # in A
+ self.zodiacal_flux # erg/s/cm^2/A/arcsec^2
+ zodiacal_flux=np.interp(lam*10.0, self.zodiacal_wave,self.zodiacal_flux) # flux from zodiacal
+ fluxlam_sky=(earthshine_flux+zodiacal_flux)*fov2 # erg/s/cm2/A
+ ###############
+
+ ephoton=planckh*cc/lam # in erg/photon, cc与lambda单位需要一致;
+ Ns_skynoise=fluxlam_sky/ephoton # in unit of photons/cm2/s/A
+
+
+ return Ns_skynoise
+
+
+ #################################################################################
+ def sim_sky_img(self,skyfitsfilename, skyRa_shift, skyDec_shift, sky_rot, exposuretime):
+
+
+ ############################################################################
+ ### load fits file
+ indatafile=skyfitsfilename
+ a=fits.open(indatafile)
+ #######################################
+ self.information['name_obj']=a[0].header['OBJECT']
+ self.information['ra_obj'] =a[0].header['RA'] ### in degree
+ self.information['dec_obj'] =a[0].header['DEC'] ### in degree
+
+ disRa =(skyRa_shift)/3600.0 ##convert unit of degree to arcsec
+ disDec=(skyDec_shift)/3600.0 ##convert unit of degree to arcsec
+
+ self.information['ra_pnt0'] =a[0].header['RA'] + disRa/np.cos(a[0].header['DEC']/180.0*np.pi)
+ self.information['dec_pnt0']=a[0].header['DEC']+ disDec
+
+ self.earthshine(self.earthshine_theta)
+
+ self.zodiacal(self.information['ra_obj'], self.information['dec_obj'], self.zodiacal_time)
+
+ self.information['target']=deg2HMS(self.information['ra_obj'], self.information['dec_obj'])
+
+
+ ### main input data
+ SpecCube=a[1].data ## spectrum data cube;
+
+ Wave=0.1*a[2].data # the relatived wavelength which is converted from Unit A to nm
+ #print('Wave data header', hdr)
+
+ ######################################################################################
+
+ exptime=self.information['exptime'] #exposure time
+
+ dis_dx=disRa # image shift Ra in arcsec
+ dis_dy=disDec # image shift Dec in arcsec
+
+ sizeout= len(SpecCube[:,0,0])
+
+ blue_img=galsim.Image(np.zeros((2048,4096)),copy=True)
+ blue_img.scale=self.pixelscale
+ blue_img.setOrigin=(0,0)
+
+ red_img=galsim.Image(np.zeros((3072,6144)),copy=True)
+ red_img.scale=self.pixelscale
+ red_img.setOrigin=(0,0)
+
+ blue_sensor=galsim.Sensor()
+
+ red_sensor=galsim.Sensor()
+
+ deltalam=np.mean(np.diff(Wave))
+
+ energy=0.0
+
+ energy_blue=0.0
+ energy_red=0.0
+
+ width_blue=0
+ ################################
+ ############## doppler effect to photons.wavelength #############
+
+ #self.orbit_pars
+ x_sat=float(self.orbit_pars[self.orbit_exp_num,1])
+ y_sat=float(self.orbit_pars[self.orbit_exp_num,2])
+ z_sat=float(self.orbit_pars[self.orbit_exp_num,3])
+ vx_sat=float(self.orbit_pars[self.orbit_exp_num,4])
+ vy_sat=float(self.orbit_pars[self.orbit_exp_num,5])
+ vz_sat=float(self.orbit_pars[self.orbit_exp_num,6])
+
+ self.information['POSI0_X']=x_sat
+ self.information['POSI0_Y']=y_sat
+ self.information['POSI0_Z']=z_sat
+ self.information['VELO0_X']=vx_sat
+ self.information['VELO0_Y']=vy_sat
+ self.information['VELO0_Z']=vz_sat
+
+
+ theta1=beta_angle( x_sat,y_sat,z_sat,vx_sat,vy_sat,vz_sat, self.information['ra_obj'], self.information['dec_obj'])
+
+ v1=np.sqrt(vx_sat**2+vy_sat**2+vz_sat**2)*np.cos(theta1/180.0*np.pi) # velocity at stat exposure time
+ vv1=LSR_velocity(self.information['ra_obj'],self.information['dec_obj'],v1, self.TianCe_day)
+
+
+ #################################################
+
+ ### exposure end time is t2 ;
+ t2=self.dt+timedelta(seconds=self.information['exptime'])
+
+ t2jd=time2jd(t2)
+
+ if self.orbit_pars[-1,0]1e-3)
+ energy=energy+sum(photons.flux[idx0]) ### totla energy for slice image
+ p_num=len(idx0[0])
+
+ ###############################################################################################
+ ############### find photons for blue channel, and make the flux multiple the optical and CCD efficiency
+
+ np.random.seed()
+ wavesample=lam1+(lam2-lam1)*np.random.rand(p_num)
+
+
+ if (lam>=350.0 and lam<=650.0):
+ ## bulue channel
+ photons_blue=galsim.PhotonArray(N=p_num, x=photons.x[idx0], y=photons.y[idx0], flux = Qe_blue*photons.flux[idx0],wavelength=wavesample)
+
+ dx_blue, dy_blue = get_dx_dy_blue(wavesample)
+
+ photons_blue.x=photons_blue.x/self.pixelscale+dx_blue+self.slice_blue['px'][k]
+
+ photons_blue.y=photons_blue.y/self.pixelscale+dy_blue+self.slice_blue['py'][k]
+
+ blue_sensor.accumulate(photons_blue, blue_img)
+
+ energy_blue=energy_blue+sum(photons_blue.flux)
+
+ width_blue=width_blue+deltalam/32.0
+
+
+
+ if (lam>=560.0) & (lam<=1000.0 ):
+ ## red channel
+ photons_red=galsim.PhotonArray(N=p_num,x=photons.x[idx0],y=photons.y[idx0], flux = Qe_red*photons.flux[idx0],wavelength=wavesample)
+
+ dx_red, dy_red = get_dx_dy_red(wavesample)
+
+ photons_red.x=photons_red.x/self.pixelscale+dx_red+self.slice_red['px'][k]
+
+ photons_red.y=photons_red.y/self.pixelscale+dy_red+self.slice_red['py'][k]
+
+ red_sensor.accumulate(photons_red, red_img)
+
+ energy_red=energy_red+sum(photons_red.flux)
+
+ ####################################################################################
+ ## stray light will cover 2% of input total light;
+ blue_img.array[:,:]=blue_img.array[:,:]+0.01*energy/2048/4096
+
+ red_img.array[:,:] =red_img.array[:,:]+ 0.01*energy/3072/6144
+
+ self.image_b=blue_img.array
+ self.image_r=red_img.array
+
+ return
+
+#################################################################################################
+
+#################################################################################################
+
+ def simulate(self, skyfitsin, skyRa_shift, skyDec_shift,sky_rot, exptime):
+
+
+ """
+ Create a single simulated image of a quadrant defined by the configuration file.
+
+
+ """
+
+ #self.configure() #print the configfile name and path;
+
+ self.dt=datetime.now()
+
+ self.information['exptime']=exptime
+
+ self.skyfilepath=skyfitsin
+
+ np.random.seed()
+ ud = np.random.random() # Choose a random
+ self.earthshine_theta=ud * 60 # in degree
+
+ ##################################################################
+ #### load orbit parameters #####
+ flag=0
+ for k in range(1,50,1):
+
+ fn=self.information['indata_path']+'/refs/orbit20160925/'+str(k)+'.txt';
+ d=np.loadtxt(fn);
+ self.dt_num=int((self.information['exptime']+self.information['readouttime']+125)/120)
+ now_dt=datetime.utcnow()
+ now_jd=time2jd(now_dt)
+ for kk in range(len(d[:,0])):
+ if now_jd-d[kk,0]<=0:
+ flag=1
+ break
+ if flag==1:
+ break
+ #####################end for
+ self.orbit_pars=d
+ self.orbit_file_num=k
+ self.orbit_exp_num =kk
+
+ exptime_start_jd=d[kk,0] #### jd time, utc format
+
+ self.dt=julian.from_jd(exptime_start_jd, fmt='jd')
+
+ self.TianCe_day=self.dt.strftime("%Y-%m-%d") ###str(self.dt.year)+'-'+str(self.dt.month)+'-'+str(self.dt.day)
+
+ self.TianCe_exp_start=dt2hmd(self.dt)
+
+ self.zodiacal_time=self.TianCe_day
+
+ #######################################################################
+
+ self.sim_sky_img(skyfitsin, skyRa_shift, skyDec_shift, sky_rot, self.information['exptime'])
+
+ self.information['sky_rot']=sky_rot
+ self.information['skyRa_shift']=skyRa_shift
+ self.information['skyRa_shift']=skyDec_shift
+
+###############################################################################
+############ add some effect to images #######################################
+
+
+ if self.flatfieldM:
+ self.applyflatfield()
+ #print('Applying flatfieldM finished.......')
+
+
+ if self.darknoise:
+ self.applyDarkCurrent()
+
+
+ if self.cosmicRays:
+ self.addCosmicRays()
+ #print('Applying cosmicRays finished.......')
+
+ if self.bleeding:
+ self.applyBleeding_yan()
+ #print('Applying bleeding finished.......')
+
+ self.applyPoissonNoise()
+
+
+ if self.nonlinearity:
+ self.applyNonlinearity()
+ #print('Applying nonlinearity finished.......')
+
+
+ if self.radiationDamage:
+ self.applyRadiationDamage()
+ #print('Applying radiationDamage finished.......')
+
+
+
+ ##### cut original CCD image to four parts by four read out channels and zones
+ self.CCDreadout()
+
+ if self.readoutNoise:
+ self.applyReadoutNoise()
+ #print('Applying readoutNoise finished.......')
+
+ self.electrons2ADU()
+
+ self.applyBias()
+
+ if self.cosmetics:
+ self.applyCosmetics()
+ #print('Applying cosmetics finished.......')
+
+ self.discretise()
+
+ self.writeOutputs()
+
+ self.log.info('Using the following input values:')
+
+ for key, value in self.information.items():
+ self.log.info('%s = %s' % (key, value))
+
+ self.log.info('Using the following booleans:')
+
+ for key, value in self.booleans.items():
+ self.log.info('%s = %s' % (key, value))
+
+ self.log.info('Finished the simulation.')
+
+##############################################################################################
+##############################################################################################
+def runIFSsim(configfile):
+
+
+ opts, args = processArgs()
+
+ opts.configfile=configfile
+
+ simulate= IFSsimulator(opts)
+
+ skyfitsin=simulate.information['skyfitsin']
+
+ exptime=simulate.information['exptime']
+
+ sky_ra_dis=simulate.information['sky_ra_dis']
+
+ sky_dec_dis=simulate.information['sky_dec_dis']
+
+ sky_angle_dis=simulate.information['sky_angle_dis']
+
+
+
+ simulate.simulate(skyfitsin, sky_ra_dis, sky_dec_dis, sky_angle_dis, exptime)
+
+
+########################## begin main fucntion #######################################
+##############################################################################################
+##############################################################################################
+
+if __name__ == "__main__":
+
+
+
+ if len(sys.argv[:]) <2:
+
+ configfile='./ifs_data/ifs_sim_example.config'
+
+ ###########################################################################################
+
+ if len(sys.argv[:]) >=2:
+ configfile=sys.argv[1]
+ if not os.path.exists(configfile):
+ print('The given input configfile path is wrong......')
+ sys.exit(1)
+
+ ################################################
+
+ runIFSsim(configfile)
+
+ print('---The CSST-IFS simulation is successful!---')
+
+ #'/home/yan/IFS_FabuCode/InputData/IFS_sim_Fabu.config'
+ ############################################################
+
+
+
diff --git a/csst_ifs_sim/ifs_so/__init__.py b/csst_ifs_sim/ifs_so/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/csst_ifs_sim/ifs_so/cdm03.cpython-37m-x86_64-linux-gnu.so b/csst_ifs_sim/ifs_so/cdm03.cpython-37m-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..85a4cfc31920218e8b634b8d53cf9db62547f63f
Binary files /dev/null and b/csst_ifs_sim/ifs_so/cdm03.cpython-37m-x86_64-linux-gnu.so differ
diff --git a/csst_ifs_sim/ifs_so/cdm03.cpython-38-x86_64-linux-gnu.so b/csst_ifs_sim/ifs_so/cdm03.cpython-38-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..7bfa02f3f33ab5d5a16e4d35ac622d8bf7a49ed1
Binary files /dev/null and b/csst_ifs_sim/ifs_so/cdm03.cpython-38-x86_64-linux-gnu.so differ
diff --git a/csst_ifs_sim/ifs_so/cdm03bidir.cpython-37m-x86_64-linux-gnu.so b/csst_ifs_sim/ifs_so/cdm03bidir.cpython-37m-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..a63494c33d3e3f2c0dd42085e549d38415c682c6
Binary files /dev/null and b/csst_ifs_sim/ifs_so/cdm03bidir.cpython-37m-x86_64-linux-gnu.so differ
diff --git a/csst_ifs_sim/ifs_so/cdm03bidir.cpython-38-x86_64-linux-gnu.so b/csst_ifs_sim/ifs_so/cdm03bidir.cpython-38-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..4ddf6dd453c91a1ed8b3448956bc127596d748f8
Binary files /dev/null and b/csst_ifs_sim/ifs_so/cdm03bidir.cpython-38-x86_64-linux-gnu.so differ
diff --git a/csst_ifs_sim/support/IFSinstrumentModel.py b/csst_ifs_sim/support/IFSinstrumentModel.py
new file mode 100644
index 0000000000000000000000000000000000000000..52550f9722521599ed5fa9948184efa96b98936b
--- /dev/null
+++ b/csst_ifs_sim/support/IFSinstrumentModel.py
@@ -0,0 +1,55 @@
+"""
+VIS Instrument Model
+====================
+
+The file provides a function that returns VIS related information such as pixel
+size, dark current, gain, zeropoint, and sky background.
+
+:requires: NumPy
+:requires: numexpr
+
+:version: 0.7
+"""
+
+# import datetime, math
+# import numpy as np
+# import numexpr as ne
+
+
+def IFSinformation():
+ """
+ Returns a dictionary describing VIS. The following information is provided (id: value - reference)::
+
+
+
+ :return: instrument model parameters
+ :rtype: dict
+ """
+
+ #########################################################################################################
+ #out = dict(readnoise=4, pixel_size=0.1, dark=0.0008333, fullwellcapacity=90000, bluesize=4000, redsize=6000, readtime=300.)
+ out=dict()
+ out.update({'dob' : 0, 'rdose' : 8.0e9,
+ 'parallelTrapfile' : 'cdm_euclid_parallel.dat', 'serialTrapfile' : 'cdm_euclid_serial.dat',
+ 'beta_s' : 0.6, 'beta_p': 0.6, 'fwc' : 90000, 'vth' : 1.168e7, 't' : 20.48e-3, 'vg' : 6.e-11,
+ 'st' : 5.0e-6, 'sfwc' : 730000., 'svg' : 1.0e-10})
+ return out
+
+
+def CCDnonLinearityModel(data, beta=6e-7):
+ """
+
+ The non-linearity is modelled based on the results presented.
+ :param data: data to which the non-linearity model is being applied to
+ :type data: ndarray
+
+ :return: input data after conversion with the non-linearity model
+ :rtype: float or ndarray
+ """
+ out = data-beta*data**2
+
+ return out
+
+
+if __name__ == '__main__':
+ print('IFSinstrument')
diff --git a/csst_ifs_sim/support/__init__.py b/csst_ifs_sim/support/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/csst_ifs_sim/support/cosmicrays.py b/csst_ifs_sim/support/cosmicrays.py
new file mode 100644
index 0000000000000000000000000000000000000000..e6c7464b7529deab9442071625632a9a7a7547fa
--- /dev/null
+++ b/csst_ifs_sim/support/cosmicrays.py
@@ -0,0 +1,424 @@
+"""
+Cosmic Rays
+===========
+
+This simple class can be used to include cosmic ray events to an image.
+By default the cosmic ray events are drawn from distributions describing
+the length and energy of the events. Such distributions can be generated
+for example using Stardust code (http://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=04636917).
+The energy of the cosmic ray events can also be set to constant for
+testing purposes. The class can be used to draw a single cosmic ray
+event or up to a covering fraction.
+
+:requires: NumPy
+:requires: SciPy
+
+:version: 0.2
+
+"""
+import numpy as np
+from scipy.interpolate import InterpolatedUnivariateSpline
+
+
+class cosmicrays():
+ """
+ Cosmic ray generation class. Can either draw events from distributions or
+ set the energy of the events to a constant.
+
+ :param log: logger instance
+ :param image: image to which cosmic rays are added to (a copy is made not to change the original numpy array)
+ :param crInfo: column information (cosmic ray file)
+ :param information: cosmic ray track information (file containing track length and energy information) and
+ exposure time.
+ """
+ def __init__(self, log, image, crInfo=None, information=None):
+ """
+ Cosmic ray generation class. Can either draw events from distributions or
+ set the energy of the events to a constant.
+
+ :param log: logger instance
+ :param image: image to which cosmic rays are added to (a copy is made not to change the original numpy array)
+ :param crInfo: column information (cosmic ray file)
+ :param information: cosmic ray track information (file containing track length and energy information) and
+ exposure time.
+ """
+ #setup logger
+ self.log = log
+
+ #image and size
+ self.image = image.copy()
+ self.ysize, self.xsize = self.image.shape
+
+ #set up the information dictionary, first with defaults and then overwrite with inputs if given
+ self.information = (dict(cosmicraylengths='/home/yan/csst-master/data/cdf_cr_length.dat',
+ cosmicraydistance='/home/yan/csst-master/data/cdf_cr_total.dat',
+ exptime=565))
+ if information is not None:
+ self.information.update(information)
+
+ if crInfo is not None:
+ self.cr = crInfo
+ else:
+ self._readCosmicrayInformation()
+
+
+ def _readCosmicrayInformation(self):
+ self.log.info('Reading in cosmic ray information from %s and %s' % (self.information['cosmicraylengths'],
+ self.information['cosmicraydistance']))
+ #read in the information from the files
+ crLengths = np.loadtxt(self.information['cosmicraylengths'])
+ crDists = np.loadtxt(self.information['cosmicraydistance'])
+
+ #set up the cosmic ray information dictionary
+ self.cr = dict(cr_u=crLengths[:, 0], cr_cdf=crLengths[:, 1], cr_cdfn=np.shape(crLengths)[0],
+ cr_v=crDists[:, 0], cr_cde=crDists[:, 1], cr_cden=np.shape(crDists)[0])
+
+ return self.cr
+
+
+ def _cosmicRayIntercepts(self, lum, x0, y0, l, phi):
+ """
+ Derive cosmic ray streak intercept points.
+
+ :param lum: luminosities of the cosmic ray tracks
+ :param x0: central positions of the cosmic ray tracks in x-direction
+ :param y0: central positions of the cosmic ray tracks in y-direction
+ :param l: lengths of the cosmic ray tracks
+ :param phi: orientation angles of the cosmic ray tracks
+
+ :return: cosmic ray map (image)
+ :rtype: nd-array
+ """
+ #create empty array
+ crImage = np.zeros((self.ysize, self.xsize), dtype=np.float64)
+
+ #x and y shifts
+ dx = l * np.cos(phi) / 2.
+ dy = l * np.sin(phi) / 2.
+ mskdx = np.abs(dx) < 1e-8
+ mskdy = np.abs(dy) < 1e-8
+ dx[mskdx] = 0.
+ dy[mskdy] = 0.
+
+ #pixels in x-direction
+ ilo = np.round(x0.copy() - dx)
+ msk = ilo < 0.
+ ilo[msk] = 0
+ ilo = ilo.astype(np.int)
+
+ ihi = 1 + np.round(x0.copy() + dx)
+ msk = ihi > self.xsize
+ ihi[msk] = self.xsize
+ ihi = ihi.astype(np.int)
+
+ #pixels in y-directions
+ jlo = np.round(y0.copy() - dy)
+ msk = jlo < 0.
+ jlo[msk] = 0
+ jlo = jlo.astype(np.int)
+
+ jhi = 1 + np.round(y0.copy() + dy)
+ msk = jhi > self.ysize
+ jhi[msk] = self.ysize
+ jhi = jhi.astype(np.int)
+
+ #loop over the individual events
+ for i, luminosity in enumerate(lum):
+ n = 0 # count the intercepts
+
+ u = []
+ x = []
+ y = []
+
+ #Compute X intercepts on the pixel grid
+ if ilo[i] < ihi[i]:
+ for xcoord in range(ilo[i], ihi[i]):
+ ok = (xcoord - x0[i]) / dx[i]
+ if np.abs(ok) <= 0.5:
+ n += 1
+ u.append(ok)
+ x.append(xcoord)
+ y.append(y0[i] + ok * dy[i])
+ else:
+ for xcoord in range(ihi[i], ilo[i]):
+ ok = (xcoord - x0[i]) / dx[i]
+ if np.abs(ok) <= 0.5:
+ n += 1
+ u.append(ok)
+ x.append(xcoord)
+ y.append(y0[i] + ok * dy[i])
+
+ #Compute Y intercepts on the pixel grid
+ if jlo[i] < jhi[i]:
+ for ycoord in range(jlo[i], jhi[i]):
+ ok = (ycoord - y0[i]) / dy[i]
+ if np.abs(ok) <= 0.5:
+ n += 1
+ u.append(ok)
+ x.append(x0[i] + ok * dx[i])
+ y.append(ycoord)
+ else:
+ for ycoord in range(jhi[i], jlo[i]):
+ ok = (ycoord - y0[i]) / dy[i]
+ if np.abs(ok) <= 0.5:
+ n += 1
+ u.append(ok)
+ x.append(x0[i] + ok * dx[i])
+ y.append(ycoord)
+
+ #check if no intercepts were found
+ if n < 1:
+ xc = int(np.floor(x0[i]))
+ yc = int(np.floor(y0[i]))
+ crImage[yc, xc] += luminosity
+
+ #Find the arguments that sort the intersections along the track
+ u = np.asarray(u)
+ x = np.asarray(x)
+ y = np.asarray(y)
+
+ args = np.argsort(u)
+
+ u = u[args]
+ x = x[args]
+ y = y[args]
+
+ #Decide which cell each interval traverses, and the path length
+ for i in range(1, n - 1):
+ w = u[i + 1] - u[i]
+ cx = int(1 + np.floor((x[i + 1] + x[i]) / 2.))
+ cy = int(1 + np.floor((y[i + 1] + y[i]) / 2.))
+
+ if 0 <= cx < self.xsize and 0 <= cy < self.ysize:
+ crImage[cy, cx] += (w * luminosity)
+
+ return crImage
+
+
+ def _drawCosmicRays(self, limit=None):
+ """
+ Add cosmic rays to the arrays based on a power-law intensity distribution for tracks.
+
+ Cosmic ray properties (such as location and angle) are chosen from random Uniform distribution.
+ """
+ #estimate the number of cosmics
+ cr_n = self.xsize * self.ysize * 0.014 / 43.263316 * 2.
+ #scale with exposure time, the above numbers are for the nominal 565s exposure
+ cr_n *= (self.information['exptime'] / 565.0)
+
+ #assume a power-law intensity distribution for tracks
+ fit = dict(cr_lo=1.0e3, cr_hi=1.0e5, cr_q=2.0e0)
+ fit['q1'] = 1.0e0 - fit['cr_q']
+ fit['en1'] = fit['cr_lo'] ** fit['q1']
+ fit['en2'] = fit['cr_hi'] ** fit['q1']
+
+ #pseudo-random numbers taken from a uniform distribution between 0 and 1
+ np.random.seed()
+ luck = np.random.rand(int(np.floor(cr_n)))
+
+ #draw the length of the tracks
+ if self.cr['cr_cdfn'] > 1:
+ ius = InterpolatedUnivariateSpline(self.cr['cr_cdf'], self.cr['cr_u'])
+ self.cr['cr_l'] = ius(luck)
+ else:
+ self.cr['cr_l'] = np.sqrt(1.0 - luck ** 2) / luck
+
+ #draw the energy of the tracks
+ if self.cr['cr_cden'] > 1:
+ ius = InterpolatedUnivariateSpline(self.cr['cr_cde'], self.cr['cr_v'])
+ self.cr['cr_e'] = ius(luck)
+ else:
+ np.random.seed()
+ self.cr['cr_e'] = (fit['en1'] + (fit['en2'] - fit['en1']) *
+ np.random.rand(int(np.floor(cr_n)))) ** (1.0 / fit['q1'])
+
+ #Choose the properties such as positions and an angle from a random Uniform dist
+ np.random.seed()
+ cr_x = self.xsize * np.random.rand(int(np.floor(cr_n)))
+
+ np.random.seed()
+ cr_y = self.ysize * np.random.rand(int(np.floor(cr_n)))
+
+ np.random.seed()
+ cr_phi = np.pi * np.random.rand(int(np.floor(cr_n)))
+
+ #find the intercepts
+ if limit is None:
+ self.cosmicrayMap = self._cosmicRayIntercepts(self.cr['cr_e'], cr_x, cr_y, self.cr['cr_l'], cr_phi)
+ print ('Number of cosmic ray events:', len(self.cr['cr_e']))
+ else:
+ #limit to electron levels < limit
+ msk = self.cr['cr_e'] < limit
+ print ('Number of cosmic ray events: %i / %i' % (len(self.cr['cr_e'][msk]), int(np.floor(cr_n))))
+ self.cosmicrayMap = self._cosmicRayIntercepts(self.cr['cr_e'][msk], cr_x[msk], cr_y[msk],
+ self.cr['cr_l'][msk], cr_phi[msk])
+
+ #count the covering factor
+ area_cr = np.count_nonzero(self.cosmicrayMap)
+ text = 'The cosmic ray covering factor is %i pixels i.e. %.3f per cent' \
+ % (area_cr, 100.*area_cr / (self.xsize*self.ysize))
+ self.log.info(text)
+ print (text)
+
+
+ def _drawSingleEvent(self, limit=1000, cr_n=1):
+ """
+ Generate a single cosmic ray event and include it to a cosmic ray map (self.cosmicrayMap).
+
+ :param limit: limiting energy for the cosmic ray event
+ :type limit: float
+ :param cr_n: number of cosmic ray events to include
+ :type cr_n: int
+
+ :return: None
+ """
+ #pseudo-random numbers taken from a uniform distribution between 0 and 1
+ np.random.seed()
+ luck = np.random.rand(cr_n)
+
+ #draw the length of the tracks
+ ius = InterpolatedUnivariateSpline(self.cr['cr_cdf'], self.cr['cr_u'])
+ self.cr['cr_l'] = ius(luck)
+
+ #set the energy directly to the limit
+ self.cr['cr_e'] = np.asarray([limit, ]*cr_n)
+
+ #Choose the properties such as positions and an angle from a random Uniform dist
+ np.random.seed()
+ cr_x = self.xsize * np.random.rand(int(np.floor(cr_n)))
+
+ np.random.seed()
+ cr_y = self.ysize * np.random.rand(int(np.floor(cr_n)))
+
+ np.random.seed()
+ cr_phi = np.pi * np.random.rand(int(np.floor(cr_n)))
+
+ #find the intercepts
+ self.cosmicrayMap = self._cosmicRayIntercepts(self.cr['cr_e'], cr_x, cr_y, self.cr['cr_l'], cr_phi)
+
+ #count the covering factor
+ area_cr = np.count_nonzero(self.cosmicrayMap)
+ text = 'The cosmic ray covering factor is %i pixels i.e. %.3f per cent' \
+ % (area_cr, 100.*area_cr / (self.xsize*self.ysize))
+ self.log.info(text)
+ print( text)
+
+
+ def _drawEventsToCoveringFactor(self, coveringFraction=3.0, limit=1000, verbose=False):
+ """
+ Generate cosmic ray events up to a covering fraction and include it to a cosmic ray map (self.cosmicrayMap).
+
+ :param coveringFraction: covering fraction of cosmic rya events in per cent of total number of pixels
+ :type coveringFraction: float
+ :param limit: limiting energy for the cosmic ray event [None = draw from distribution]
+ :type limit: None or float
+ :param verbose: print out information to stdout
+ :type verbose: bool
+
+
+ :return: None
+ """
+ self.cosmicrayMap = np.zeros((self.ysize, self.xsize))
+
+ #how many events to draw at once, too large number leads to exceeding the covering fraction
+ cr_n = int(295 * self.information['exptime'] / 565. * coveringFraction / 1.4)
+
+ covering = 0.0
+
+ while covering < coveringFraction:
+ #pseudo-random numbers taken from a uniform distribution between 0 and 1
+ np.random.seed()
+ luck = np.random.rand(cr_n)
+
+ #draw the length of the tracks
+ ius = InterpolatedUnivariateSpline(self.cr['cr_cdf'], self.cr['cr_u'])
+ self.cr['cr_l'] = ius(luck)
+
+ if limit is None:
+ ius = InterpolatedUnivariateSpline(self.cr['cr_cde'], self.cr['cr_v'])
+ self.cr['cr_e'] = ius(luck)
+ else:
+ #set the energy directly to the limit
+ self.cr['cr_e'] = np.asarray([limit,])
+
+ #Choose the properties such as positions and an angle from a random Uniform dist
+ np.random.seed()
+ cr_x = self.xsize * np.random.rand(int(np.floor(cr_n)))
+
+ np.random.seed()
+ cr_y = self.ysize * np.random.rand(int(np.floor(cr_n)))
+
+ np.random.seed()
+ cr_phi = np.pi * np.random.rand(int(np.floor(cr_n)))
+
+ #find the intercepts
+ self.cosmicrayMap += self._cosmicRayIntercepts(self.cr['cr_e'], cr_x, cr_y, self.cr['cr_l'], cr_phi)
+
+ #count the covering factor
+ area_cr = np.count_nonzero(self.cosmicrayMap)
+ covering = 100.*area_cr / (self.xsize*self.ysize)
+
+ text = 'The cosmic ray covering factor is %i pixels i.e. %.3f per cent' % (area_cr, covering)
+ self.log.info(text)
+
+ if verbose:
+ print( text)
+
+
+ def addCosmicRays(self, limit=None):
+ """
+ Include cosmic rays to the image given.
+
+ :return: image with cosmic rays
+ :rtype: ndarray
+ """
+ self._drawCosmicRays(limit=limit)
+
+ #paste cosmic rays
+ self.image += self.cosmicrayMap
+
+ return self.image
+
+
+ def addSingleEvent(self, limit=None):
+ """
+ Include a single cosmic ray event to the image given.
+
+ :return: image with cosmic rays
+ :rtype: ndarray
+ """
+ self._drawSingleEvent(limit=limit)
+
+ #paste cosmic rays
+ self.image += self.cosmicrayMap
+
+ return self.image
+
+
+ def addUpToFraction(self, coveringFraction, limit=None, verbose=False):
+ """
+ Add cosmic ray events up to the covering Fraction.
+
+ :param coveringFraction: covering fraction of cosmic rya events in per cent of total number of pixels
+ :type coveringFraction: float
+ :param limit: limiting energy for the cosmic ray event [None = draw from distribution]
+ :type limit: None or float
+ :param verbose: print out information to stdout
+ :type verbose: bool
+
+ :return: image with cosmic rays
+ :rtype: ndarray
+ """
+ self._drawEventsToCoveringFactor(coveringFraction, limit=limit, verbose=verbose)
+
+ #paste cosmic rays
+ self.image += self.cosmicrayMap
+
+ return self.image
+
+
+if __name__ == "__main__":
+
+
+ print()
+
diff --git a/csst_ifs_sim/support/logger.py b/csst_ifs_sim/support/logger.py
new file mode 100644
index 0000000000000000000000000000000000000000..b09ef822fe697c173e3d8d4b1970fe6af1e6f39a
--- /dev/null
+++ b/csst_ifs_sim/support/logger.py
@@ -0,0 +1,40 @@
+"""
+These functions can be used for logging information.
+
+.. Warning:: logger is not multiprocessing safe.
+
+:version: 0.3
+"""
+import logging
+import logging.handlers
+
+
+def setUpLogger(log_filename, loggername='logger'):
+ """
+ Sets up a logger.
+
+ :param: log_filename: name of the file to save the log.
+ :param: loggername: name of the logger
+
+ :return: logger instance
+ """
+ # create logger
+ logger = logging.getLogger(loggername)
+ logger.setLevel(logging.DEBUG)
+ # Add the log message handler to the logger
+ handler = logging.handlers.RotatingFileHandler(log_filename)
+ #maxBytes=20, backupCount=5)
+ # create formatter
+ formatter = logging.Formatter('%(asctime)s - %(module)s - %(funcName)s - %(levelname)s - %(message)s')
+ # add formatter to ch
+ handler.setFormatter(formatter)
+ # add handler to logger
+
+ if (logger.hasHandlers()):
+ logger.handlers.clear()
+
+ logger.addHandler(handler)
+
+ return logger
+
+
diff --git a/ifs_data/__init__.py b/ifs_data/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..f65e2b384bdc95954408133449d4fdc1562b5262
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,36 @@
+import setuptools
+
+with open("README.md", "r") as fh:
+ long_description = fh.read()
+
+setuptools.setup(
+ name='csst_ifs_sim',
+ version='2.0.0-IFS1.0.0',
+ author='CSST Team',
+ author_email='zhaojunyan@shao.ac.cn',
+ description='The CSST - ifs - sim', # short description
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url='https://csst-tb.bao.ac.cn/',
+ # project_urls={
+ # 'Source': 'https://csst-tb.bao.ac.cn/code/csst-l1/ifs/csst_ifs_sim',
+ # },
+ packages=setuptools.find_packages(),
+ license='MIT',
+ classifiers=["Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3.8",
+ "Topic :: Scientific/Engineering :: Physics",
+ "Topic :: Scientific/Engineering :: Astronomy"],
+ package_dir={'csst_ifs_sim': 'csst_ifs_sim'},
+ # include_package_data=True,
+ package_data={"": ["LICENSE", "README.md"],
+ "csst_ifs_sim": ["ifs_so/*", "ifs_data/*", "ifs_data/refs/*", "ifs_data/refs/orbit20160925/*"]},
+ # install_requires=['sphinx',
+ # 'numpy',
+ # 'scipy', 'matplotlib',
+ # 'astropy', 'healpy', 'ccdproc', 'deepCR', 'photutils'],
+ python_requires='>=3.8',
+)