import os
import warnings
from astropy.units import Quantity
from glob import glob
from mejiro.instruments.instrument import Instrument
from mejiro.utils import roman_util, util
IMAGING_PATH = 'data/WideFieldInstrument/Imaging/'
ZEROPOINT_PATH = IMAGING_PATH + 'ZeroPoints/Roman_zeropoints_*.ecsv'
ZODIACAL_LIGHT_PATH = IMAGING_PATH + 'ZodiacalLight/zodiacal_light.ecsv'
THERMAL_BACKGROUND_PATH = IMAGING_PATH + 'Backgrounds/internal_thermal_backgrounds.ecsv'
FILTER_PARAMS_PATH = IMAGING_PATH + 'FiltersSummary/filter_parameters.ecsv'
[docs]
class Roman(Instrument):
"""
Roman Space Telescope (Roman) Wide Field Instrument (WFI) class
Attributes
----------
roman_technical_information_path : str
Path to the roman-technical-information directory.
pixel_scale : Quantity
Pixel scale of the instrument in arcsec/pix.
gain : float
Gain of the instrument.
stray_light_fraction : float
Fraction of stray light.
zeropoints : QTable or None
Zeropoints table, initialized on demand.
thermal_background : QTable or None
Thermal background table, initialized on demand.
minimum_zodiacal_light : QTable or None
Minimum zodiacal light table, initialized on demand.
psf_fwhm : QTable or None
PSF FWHM table, initialized on demand.
"""
def __init__(self):
name = 'Roman'
bands = ['F062', 'F087', 'F106', 'F129', 'F158', 'F184', 'F213', 'F146']
num_detectors = 18
engines = ['galsim', 'romanisim']
super().__init__(
name,
bands,
num_detectors,
engines
)
# get path to roman-technical-information from environment variable
self.roman_technical_information_path = os.getenv("ROMAN_TECHNICAL_INFORMATION_PATH")
if self.roman_technical_information_path is None:
raise EnvironmentError("Environment variable 'ROMAN_TECHNICAL_INFORMATION_PATH' is not set.")
# record version of roman-technical-documentation
try:
version_path = os.path.join(self.roman_technical_information_path, 'VERSION.md')
with open(version_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
if len(lines) == 1:
self.versions['roman-technical-information'] = lines[0].strip()
else:
raise IndexError(f"Error reading version number from VERSION.md due to unexpected format: {lines}.")
except FileNotFoundError:
raise FileNotFoundError(f"VERSION.md not found in {self.roman_technical_information_path}.")
# set attributes
self.pixel_scale = Quantity(0.11, 'arcsec / pix')
self.gain = 1.
self.stray_light_fraction = 0.1
# fields to initialize: these can be retrieved on demand
self.zeropoints = None
self.thermal_background = None
self.minimum_zodiacal_light = None
self.psf_fwhm = None
[docs]
def get_minimum_zodiacal_light(self, band):
"""
Get the minimum zodiacal light at high galactic latitudes for a given band in ct/pix.
Parameters
----------
band : str
The name of the band, e.g., 'F129'.
Returns
-------
Astropy.units.Quantity
Minimum zodiacal light in ct/pix.
"""
if self.minimum_zodiacal_light is None:
self.minimum_zodiacal_light = util.return_qtable(os.path.join(self.roman_technical_information_path, ZODIACAL_LIGHT_PATH))
return self.minimum_zodiacal_light[self.minimum_zodiacal_light['filter'] == band]['rate']
# implement abstract methods
[docs]
def get_pixel_scale(self, band=None):
return self.pixel_scale
[docs]
def get_psf_fwhm(self, band):
if self.psf_fwhm is None:
self.psf_fwhm = util.return_qtable(os.path.join(self.roman_technical_information_path, FILTER_PARAMS_PATH))
return self.psf_fwhm[self.psf_fwhm['filter'] == band]['PSF_FWHM']
[docs]
def get_psf_kwargs(self, band, detector=1, detector_position=(2044, 2044), oversample=5, num_pix=101, check_cache=True, psf_cache_dir=None, require_cached=False):
from mejiro.engines.stpsf_engine import STPSFEngine
return STPSFEngine.get_roman_psf_kwargs(band, roman_util.get_sca_int(detector), detector_position, oversample, num_pix, check_cache, psf_cache_dir, require_cached=require_cached)
[docs]
def get_thermal_background(self, band):
if self.thermal_background is None:
self.thermal_background = util.return_qtable(os.path.join(self.roman_technical_information_path, THERMAL_BACKGROUND_PATH))
return self.thermal_background[self.thermal_background['filter'] == band]['rate']
[docs]
def get_gain(self, band):
return self.gain
[docs]
def get_dark_current(self, band):
raise NotImplementedError("Dark current is not yet implemented for Roman.")
[docs]
def get_read_noise(self, band):
raise NotImplementedError("Read noise is not yet implemented for Roman.")
[docs]
def get_sky_level(self, band):
raise NotImplementedError("Sky level is not yet implemented for Roman.")
[docs]
def get_zeropoint_magnitude(self, band, detector=1):
sca_number = roman_util.get_sca_int(detector)
if self.zeropoints is None:
self.zeropoints = util.return_qtable(os.path.join(self.roman_technical_information_path, ZEROPOINT_PATH))
return self.zeropoints[(self.zeropoints['element'] == band) & (self.zeropoints['detector'] == f'WFI{str(sca_number).zfill(2)}')]['ABMag']
[docs]
@staticmethod
def load_speclite_filters(detector='sca01'):
detector_string = roman_util.get_sca_string(detector)
import mejiro
module_path = os.path.dirname(mejiro.__file__)
filters = sorted(glob(os.path.join(module_path, 'data', 'roman_filter_response', f'Roman{detector_string}-*.ecsv')))
from speclite.filters import load_filters
return load_filters(*filters[:8])
[docs]
@staticmethod
def default_params():
return {
'detector': 1,
'detector_position': (2048, 2048)
}
[docs]
@staticmethod
def validate_instrument_params(params):
if 'detector' in params:
detector = roman_util.get_sca_int(params['detector'])
if detector not in range(1, 19):
raise ValueError('Detector number must be an integer between 1 and 18.')
else:
default_detector = Roman.default_params()['detector']
warnings.warn(f'No detector number provided. Defaulting to detector {default_detector}.')
params['detector'] = default_detector
if 'detector_position' in params:
if not (isinstance(params['detector_position'], tuple) and len(params['detector_position']) == 2 and all(isinstance(x, int) for x in params['detector_position'])):
raise ValueError('The detector_position parameter must be an (x,y) coordinate tuple.')
if not (params['detector_position'][0] in range(4, 4092 + 1) and params['detector_position'][1] in range(4, 4092 + 1)):
raise ValueError('Choose a valid pixel position on the range 4-4092.')
else:
default_detector_position = Roman.default_params()['detector_position']
warnings.warn(f'No detector position provided. Defaulting to {default_detector_position}')
params['detector_position'] = default_detector_position
return params