solarc-eclipse 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euvst_response/__init__.py +52 -0
- euvst_response/analysis.py +680 -0
- euvst_response/cli.py +113 -0
- euvst_response/config.py +396 -0
- euvst_response/data/throughput/grating_reflection_efficiency.dat +25 -0
- euvst_response/data/throughput/primary_mirror_coating_reflectance.dat +25 -0
- euvst_response/data/throughput/source.txt +3 -0
- euvst_response/data/throughput/throughput_aluminium_1000_angstrom.dat +503 -0
- euvst_response/data/throughput/throughput_aluminium_oxide_1000_angstrom.dat +503 -0
- euvst_response/data/throughput/throughput_carbon_1000_angstrom.dat +503 -0
- euvst_response/data_processing.py +269 -0
- euvst_response/fitting.py +144 -0
- euvst_response/main.py +424 -0
- euvst_response/monte_carlo.py +159 -0
- euvst_response/pinhole_diffraction.py +260 -0
- euvst_response/psf.py +46 -0
- euvst_response/radiometric.py +512 -0
- euvst_response/synthesis.py +911 -0
- euvst_response/synthesis_cli.py +12 -0
- euvst_response/utils.py +176 -0
- solarc_eclipse-0.5.0.dist-info/METADATA +354 -0
- solarc_eclipse-0.5.0.dist-info/RECORD +26 -0
- solarc_eclipse-0.5.0.dist-info/WHEEL +5 -0
- solarc_eclipse-0.5.0.dist-info/entry_points.txt +5 -0
- solarc_eclipse-0.5.0.dist-info/licenses/LICENSE +1 -0
- solarc_eclipse-0.5.0.dist-info/top_level.txt +1 -0
euvst_response/cli.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command line interface for ECLIPSE.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .main import main as run_simulation
|
|
12
|
+
from .synthesis import main as run_synthesis
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ASCII_LOGO = """
|
|
16
|
+
______ _____ _ _____ _____ _____ ______
|
|
17
|
+
| ____/ ____| | |_ _| __ \ / ____| ____|
|
|
18
|
+
| |__ | | | | | | | |__) | (___ | |__
|
|
19
|
+
| __|| | | | | | | ___/ \___ \| __|
|
|
20
|
+
| |___| |____| |____ _| |_| | ____) | |____
|
|
21
|
+
|______\_____|______|_____|_| |_____/|______|
|
|
22
|
+
|
|
23
|
+
ECLIPSE: Emission Calculation and Line Intensity Prediction for SOLAR-C EUVST
|
|
24
|
+
|
|
25
|
+
Contact: James McKevitt (jm2@mssl.ucl.ac.uk). License: Contact for permission to use.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def print_logo():
|
|
30
|
+
"""Print the ECLIPSE ASCII logo and info."""
|
|
31
|
+
print(ASCII_LOGO)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
"""Main CLI entry point."""
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
prog="eclipse",
|
|
38
|
+
description="ECLIPSE: Emission Calculation and Line Intensity Prediction for SOLAR-C EUVST",
|
|
39
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--version",
|
|
44
|
+
action="version",
|
|
45
|
+
version=f"ECLIPSE {__version__}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--config",
|
|
50
|
+
type=str,
|
|
51
|
+
help="YAML config file for instrument response simulation",
|
|
52
|
+
required=False
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--logo-only",
|
|
57
|
+
action="store_true",
|
|
58
|
+
help="Just print the logo and exit"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--debug",
|
|
63
|
+
action="store_true",
|
|
64
|
+
help="Enable debug mode - drops to IPython on errors"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
args = parser.parse_args()
|
|
68
|
+
|
|
69
|
+
# Always print the logo first
|
|
70
|
+
print_logo()
|
|
71
|
+
|
|
72
|
+
if args.logo_only:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if not args.config:
|
|
76
|
+
print("Usage: eclipse --config <config.yaml>")
|
|
77
|
+
print("\nTo run instrument response simulation, provide a YAML config file.")
|
|
78
|
+
print("Example config files can be found in the run/input/ directory.")
|
|
79
|
+
print("\nFor more help: eclipse --help")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Validate config file exists
|
|
83
|
+
config_path = Path(args.config)
|
|
84
|
+
if not config_path.exists():
|
|
85
|
+
print(f"Error: Config file '{args.config}' not found.")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
print(f"Running instrument response simulation with config: {args.config}")
|
|
89
|
+
print("-" * 60)
|
|
90
|
+
|
|
91
|
+
# Set up sys.argv for the main function (it expects argparse format)
|
|
92
|
+
if args.debug:
|
|
93
|
+
sys.argv = ["eclipse", "--config", args.config, "--debug"]
|
|
94
|
+
else:
|
|
95
|
+
sys.argv = ["eclipse", "--config", args.config]
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
run_simulation()
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
print("\nSimulation interrupted by user.")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"Error during simulation: {e}")
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def synthesis_main():
|
|
112
|
+
"""Entry point for the synthesis command line script."""
|
|
113
|
+
run_synthesis()
|
euvst_response/config.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration classes for instruments, detectors, and simulation parameters.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
import numpy as np
|
|
10
|
+
import astropy.units as u
|
|
11
|
+
import scipy.interpolate
|
|
12
|
+
from .utils import angle_to_distance
|
|
13
|
+
from importlib.resources import files
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ------------------------------------------------------------------
|
|
17
|
+
# Detector materials and shared properties
|
|
18
|
+
# ------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
# Material properties (Fano factors)
|
|
21
|
+
DETECTOR_MATERIALS = {
|
|
22
|
+
"silicon": {
|
|
23
|
+
"fano_factor": 0.115,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def calculate_dark_current(temp: u.Quantity, q_d0_293k: u.Quantity, ccd_type: str = "NIMO") -> u.Quantity:
|
|
28
|
+
"""
|
|
29
|
+
Calculate dark current based on CCD temperature and type.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
temp : u.Quantity
|
|
34
|
+
CCD temperature with units (e.g., -60 * u.deg_C)
|
|
35
|
+
q_d0_293k : u.Quantity
|
|
36
|
+
Dark current at 293K in electrons per pixel per second
|
|
37
|
+
ccd_type : str
|
|
38
|
+
"NIMO" (non-inverted mode), "AIMO" (advanced inverted mode)
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
dark_current : u.Quantity
|
|
43
|
+
Dark current in electrons per pixel per second
|
|
44
|
+
|
|
45
|
+
Raises
|
|
46
|
+
------
|
|
47
|
+
ValueError
|
|
48
|
+
If temperature is above 300K (27 deg C) or unknown CCD type
|
|
49
|
+
"""
|
|
50
|
+
temp_kelvin = temp.to(u.Kelvin, equivalencies=u.temperature())
|
|
51
|
+
max_temp = 300 * u.K
|
|
52
|
+
min_temp = 230 * u.K
|
|
53
|
+
|
|
54
|
+
# Check temperature limits
|
|
55
|
+
if temp_kelvin > max_temp:
|
|
56
|
+
raise ValueError(f"Cannot calculate dark current at {temp_kelvin}. "
|
|
57
|
+
f"Maximum temperature is {max_temp}")
|
|
58
|
+
|
|
59
|
+
# Apply minimum temperature limit (clamp to 230K)
|
|
60
|
+
if temp_kelvin < min_temp:
|
|
61
|
+
temp_kelvin = min_temp
|
|
62
|
+
|
|
63
|
+
Q_d0 = q_d0_293k.to_value(u.electron / (u.pixel * u.s))
|
|
64
|
+
T = temp_kelvin.value
|
|
65
|
+
|
|
66
|
+
if ccd_type.upper() == "NIMO":
|
|
67
|
+
# Q_d = Q_d0 * 122 * T^3 * exp(-6400/T)
|
|
68
|
+
dark_current = Q_d0 * 122 * T**3 * np.exp(-6400/T)
|
|
69
|
+
elif ccd_type.upper() == "AIMO":
|
|
70
|
+
# Q_d = Qd0 * 1.14e6 * T^3 * exp(-9080/T)
|
|
71
|
+
dark_current = Q_d0 * 1.14e6 * T**3 * np.exp(-9080/T)
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"Unknown CCD type: {ccd_type}. Must be 'NIMO' or 'AIMO'.")
|
|
74
|
+
|
|
75
|
+
return dark_current * u.electron / (u.pixel * u.s)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
# Throughput helpers & AluminiumFilter
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
def _load_throughput_table(path) -> tuple[u.Quantity, np.ndarray]:
|
|
82
|
+
"""Return (lambda, T) arrays from a 2-col ASCII table (skip comments). lambda is in nm."""
|
|
83
|
+
content = path.read_text()
|
|
84
|
+
lines = content.strip().split('\n')[2:] # Skip first 2 lines
|
|
85
|
+
data = []
|
|
86
|
+
for line in lines:
|
|
87
|
+
if line.strip() and not line.strip().startswith('#'):
|
|
88
|
+
data.append([float(x) for x in line.split()])
|
|
89
|
+
arr = np.array(data)
|
|
90
|
+
wl = arr[:, 0] * u.nm
|
|
91
|
+
tr = arr[:, 1]
|
|
92
|
+
return wl, tr
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _interp_tr(wavelength_nm: float, wl_tab: np.ndarray, tr_tab: np.ndarray) -> float:
|
|
96
|
+
"""Linear interpolation."""
|
|
97
|
+
f = scipy.interpolate.interp1d(wl_tab, tr_tab, bounds_error=False, fill_value=np.nan)
|
|
98
|
+
return float(f(wavelength_nm))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class AluminiumFilter:
|
|
103
|
+
"""Multi-layer EUV filter (Al + Al2O3 + C) in front of SWC detector."""
|
|
104
|
+
al_thickness: u.Quantity = 1485 * u.angstrom
|
|
105
|
+
oxide_thickness: u.Quantity = 95 * u.angstrom
|
|
106
|
+
c_thickness: u.Quantity = 0 * u.angstrom
|
|
107
|
+
mesh_throughput: float = 0.8
|
|
108
|
+
al_table: Path = field(default_factory=lambda: files('euvst_response') / 'data' / 'throughput' / 'throughput_aluminium_1000_angstrom.dat')
|
|
109
|
+
oxide_table: Path = field(default_factory=lambda: files('euvst_response') / 'data' / 'throughput' / 'throughput_aluminium_oxide_1000_angstrom.dat')
|
|
110
|
+
c_table: Path = field(default_factory=lambda: files('euvst_response') / 'data' / 'throughput' / 'throughput_carbon_1000_angstrom.dat')
|
|
111
|
+
table_thickness: u.Quantity = 1000 * u.angstrom
|
|
112
|
+
|
|
113
|
+
def total_throughput(self, wl0: u.Quantity) -> float:
|
|
114
|
+
"""Calculate throughput at a given central wavelength (wl0, astropy Quantity)."""
|
|
115
|
+
wl_nm = wl0.to_value(u.nm)
|
|
116
|
+
wl_al, tr_al = _load_throughput_table(self.al_table)
|
|
117
|
+
wl_ox, tr_ox = _load_throughput_table(self.oxide_table)
|
|
118
|
+
wl_c, tr_c = _load_throughput_table(self.c_table)
|
|
119
|
+
t_al = _interp_tr(wl_nm, wl_al, tr_al) ** (self.al_thickness.cgs / self.table_thickness.cgs)
|
|
120
|
+
t_ox = _interp_tr(wl_nm, wl_ox, tr_ox) ** (self.oxide_thickness.cgs / self.table_thickness.cgs)
|
|
121
|
+
t_c = _interp_tr(wl_nm, wl_c, tr_c) ** (self.c_thickness.cgs / self.table_thickness.cgs)
|
|
122
|
+
return t_al * t_ox * t_c * self.mesh_throughput
|
|
123
|
+
|
|
124
|
+
def visible_light_throughput(self) -> float:
|
|
125
|
+
"""Calculate visible light throughput reduction due to aluminum filter (For every 170 Angstrom of aluminum, throughput is reduced by factor of 10)."""
|
|
126
|
+
thickness_aa = self.al_thickness.to(u.angstrom).value
|
|
127
|
+
layers = thickness_aa / 170.0
|
|
128
|
+
return 10.0 ** (-layers) * self.mesh_throughput
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# -----------------------------------------------------------------------------
|
|
132
|
+
# Configuration objects
|
|
133
|
+
# -----------------------------------------------------------------------------
|
|
134
|
+
@dataclass
|
|
135
|
+
class Detector_SWC:
|
|
136
|
+
"""Solar-C/EUVST SWC detector configuration."""
|
|
137
|
+
qe_vis: float = 1.0
|
|
138
|
+
qe_euv: float = 0.76
|
|
139
|
+
read_noise_rms: u.Quantity = 10.0 * u.electron / u.pixel
|
|
140
|
+
dark_current: u.Quantity = 21.0 * u.electron / (u.pixel * u.s) # Default value, will be overridden
|
|
141
|
+
_dark_current_293k: u.Quantity = 20000.0 * u.electron / (u.pixel * u.s) # Q_d0 at 293K
|
|
142
|
+
gain_e_per_dn: u.Quantity = 2.0 * u.electron / u.DN
|
|
143
|
+
max_dn: u.Quantity = 65535 * u.DN / u.pixel
|
|
144
|
+
pix_size: u.Quantity = (13.5 * u.um).cgs / u.pixel
|
|
145
|
+
wvl_res: u.Quantity = (16.9 * u.mAA).cgs / u.pixel
|
|
146
|
+
plate_scale_angle: u.Quantity = 0.159 * u.arcsec / u.pixel
|
|
147
|
+
material: str = "silicon"
|
|
148
|
+
filter_distance: u.Quantity = 250 * u.mm # Distance from filter to detector for pinhole diffraction
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def si_fano(self) -> float:
|
|
152
|
+
"""Get Fano factor for the detector material."""
|
|
153
|
+
return DETECTOR_MATERIALS[self.material]["fano_factor"]
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def calculate_dark_current(temp: u.Quantity) -> u.Quantity:
|
|
157
|
+
"""Calculate dark current for SWC (NIMO) CCD."""
|
|
158
|
+
return calculate_dark_current(temp, Detector_SWC._dark_current_293k, ccd_type="NIMO")
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def with_temperature(cls, temp: u.Quantity):
|
|
162
|
+
"""
|
|
163
|
+
Create a detector instance with dark current calculated from temperature.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
temp : u.Quantity
|
|
168
|
+
CCD temperature with units (e.g., -60 * u.deg_C)
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
detector : Detector_SWC
|
|
173
|
+
Detector instance with calculated dark current and stored temperature
|
|
174
|
+
"""
|
|
175
|
+
from dataclasses import replace
|
|
176
|
+
dark_current = cls.calculate_dark_current(temp)
|
|
177
|
+
|
|
178
|
+
detector = replace(cls(), dark_current=dark_current)
|
|
179
|
+
detector._ccd_temperature = temp # Store original temperature with units
|
|
180
|
+
return detector
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def plate_scale_length(self) -> u.Quantity:
|
|
184
|
+
return angle_to_distance(self.plate_scale_angle * 1*u.pix) / u.pixel
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class Detector_EIS:
|
|
189
|
+
"""Hinode/EIS detector configuration for comparison."""
|
|
190
|
+
qe_euv: float = 0.64 # EIS SW Note 2
|
|
191
|
+
qe_vis: float = 0.65 # MSSL engineering test report
|
|
192
|
+
read_noise_rms: u.Quantity = 5.0 * u.electron / u.pixel
|
|
193
|
+
dark_current: u.Quantity = 21.0 * u.electron / (u.pixel * u.s) # Default value, will be overridden
|
|
194
|
+
_dark_current_293k: u.Quantity = 250.0 * u.electron / (u.pixel * u.s) # Q_d0 at 293K for EIS
|
|
195
|
+
gain_e_per_dn: u.Quantity = 6.3 * u.electron / u.DN
|
|
196
|
+
max_dn: u.Quantity = 65535 * u.DN / u.pixel
|
|
197
|
+
pix_size: u.Quantity = (13.5 * u.um).cgs / u.pixel
|
|
198
|
+
wvl_res: u.Quantity = (22.3 * u.mAA).cgs / u.pixel
|
|
199
|
+
plate_scale_angle: u.Quantity = 1 * u.arcsec / u.pixel
|
|
200
|
+
material: str = "silicon"
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def si_fano(self) -> float:
|
|
204
|
+
"""Get Fano factor for the detector material."""
|
|
205
|
+
return DETECTOR_MATERIALS[self.material]["fano_factor"]
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def plate_scale_length(self) -> u.Quantity:
|
|
209
|
+
return angle_to_distance(self.plate_scale_angle * 1*u.pix) / u.pixel
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def calculate_dark_current(temp: u.Quantity) -> u.Quantity:
|
|
213
|
+
"""Calculate dark current for EIS (AIMO) CCD."""
|
|
214
|
+
return calculate_dark_current(temp, Detector_EIS._dark_current_293k, ccd_type="AIMO")
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def with_temperature(cls, temp: u.Quantity):
|
|
218
|
+
"""
|
|
219
|
+
Create a detector instance with dark current calculated from temperature.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
temp : u.Quantity
|
|
224
|
+
CCD temperature with units (e.g., -60 * u.deg_C)
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
detector : Detector_EIS
|
|
229
|
+
Detector instance with calculated dark current and stored temperature
|
|
230
|
+
"""
|
|
231
|
+
from dataclasses import replace
|
|
232
|
+
dark_current = cls.calculate_dark_current(temp)
|
|
233
|
+
|
|
234
|
+
detector = replace(cls(), dark_current=dark_current)
|
|
235
|
+
detector._ccd_temperature = temp # Store original temperature with units
|
|
236
|
+
return detector
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class Telescope_EUVST:
|
|
241
|
+
"""Solar-C/EUVST telescope configuration."""
|
|
242
|
+
D_ap: u.Quantity = 0.28 * u.m
|
|
243
|
+
microroughness_sigma: u.Quantity = 0.3 * u.nm # RMS microroughness for primary mirror
|
|
244
|
+
filter: AluminiumFilter = field(default_factory=AluminiumFilter)
|
|
245
|
+
psf_type: str = "gaussian"
|
|
246
|
+
psf_params: list = field(default_factory=lambda: [0.343 * u.pixel]) # FWHM of 0.805 pix from 0.128 arcsec from optical design RSC-2022021B in sigma
|
|
247
|
+
|
|
248
|
+
# Wavelength-dependent efficiency tables
|
|
249
|
+
pm_table: Path = field(default_factory=lambda: files('euvst_response') / 'data' / 'throughput' / 'primary_mirror_coating_reflectance.dat')
|
|
250
|
+
grating_table: Path = field(default_factory=lambda: files('euvst_response') / 'data' / 'throughput' / 'grating_reflection_efficiency.dat')
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def collecting_area(self) -> u.Quantity:
|
|
254
|
+
return 0.5 * np.pi * (self.D_ap / 2) ** 2
|
|
255
|
+
|
|
256
|
+
def primary_mirror_efficiency(self, wl0: u.Quantity) -> float:
|
|
257
|
+
"""
|
|
258
|
+
Calculate wavelength-dependent primary mirror efficiency.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
wl0 : u.Quantity
|
|
263
|
+
Wavelength
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
float
|
|
268
|
+
Primary mirror efficiency (dimensionless)
|
|
269
|
+
"""
|
|
270
|
+
wl_nm = wl0.to_value(u.nm)
|
|
271
|
+
wl_pm, eff_pm = _load_throughput_table(self.pm_table)
|
|
272
|
+
return _interp_tr(wl_nm, wl_pm, eff_pm)
|
|
273
|
+
|
|
274
|
+
def grating_efficiency(self, wl0: u.Quantity) -> float:
|
|
275
|
+
"""
|
|
276
|
+
Calculate wavelength-dependent grating efficiency.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
wl0 : u.Quantity
|
|
281
|
+
Wavelength
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
float
|
|
286
|
+
Grating efficiency (dimensionless)
|
|
287
|
+
"""
|
|
288
|
+
wl_nm = wl0.to_value(u.nm)
|
|
289
|
+
wl_grat, eff_grat = _load_throughput_table(self.grating_table)
|
|
290
|
+
return _interp_tr(wl_nm, wl_grat, eff_grat)
|
|
291
|
+
|
|
292
|
+
def microroughness_efficiency(self, wl0: u.Quantity) -> float:
|
|
293
|
+
"""
|
|
294
|
+
Calculate the efficiency reduction due to primary mirror microroughness.
|
|
295
|
+
|
|
296
|
+
Formula: 1 - (4*pi*sigma/lambda)^2
|
|
297
|
+
where sigma is the RMS microroughness and lambda is the wavelength.
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
wl0 : u.Quantity
|
|
302
|
+
Wavelength
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
float
|
|
307
|
+
Microroughness efficiency factor (dimensionless)
|
|
308
|
+
"""
|
|
309
|
+
# Convert both wavelength and sigma to the same units (nm for convenience)
|
|
310
|
+
wl_nm = wl0.to(u.nm)
|
|
311
|
+
sigma_nm = self.microroughness_sigma.to(u.nm)
|
|
312
|
+
|
|
313
|
+
# Calculate (4*pi*sigma/lambda)^2
|
|
314
|
+
roughness_term = (4 * np.pi * sigma_nm / wl_nm) ** 2
|
|
315
|
+
|
|
316
|
+
# Return 1 - (4*pi*sigma/lambda)^2
|
|
317
|
+
return 1.0 - roughness_term.value
|
|
318
|
+
|
|
319
|
+
def throughput(self, wl0: u.Quantity) -> float:
|
|
320
|
+
"""
|
|
321
|
+
Calculate total telescope throughput including wavelength-dependent efficiencies.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
wl0 : u.Quantity
|
|
326
|
+
Wavelength
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
float
|
|
331
|
+
Total telescope throughput (dimensionless)
|
|
332
|
+
"""
|
|
333
|
+
# Get wavelength-dependent efficiencies
|
|
334
|
+
pm_eff_wl = self.primary_mirror_efficiency(wl0)
|
|
335
|
+
grat_eff_wl = self.grating_efficiency(wl0)
|
|
336
|
+
|
|
337
|
+
# Apply microroughness efficiency to primary mirror efficiency
|
|
338
|
+
pm_eff_with_roughness = pm_eff_wl * self.microroughness_efficiency(wl0)
|
|
339
|
+
|
|
340
|
+
# Calculate total throughput
|
|
341
|
+
return pm_eff_with_roughness * grat_eff_wl * self.filter.total_throughput(wl0)
|
|
342
|
+
|
|
343
|
+
def ea_and_throughput(self, wl0: u.Quantity) -> u.Quantity:
|
|
344
|
+
return self.collecting_area * self.throughput(wl0)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass
|
|
348
|
+
class Telescope_EIS:
|
|
349
|
+
"""Hinode/EIS telescope configuration for comparison."""
|
|
350
|
+
psf_type: str = "gaussian"
|
|
351
|
+
psf_params: list = field(default_factory=lambda: [1.28 * u.pixel]) # FWHM of 3 in sigma
|
|
352
|
+
|
|
353
|
+
def ea_and_throughput(self, wl0: u.Quantity) -> u.Quantity:
|
|
354
|
+
# Effective area including detector QE is 0.23 cm2
|
|
355
|
+
# https://hinode.nao.ac.jp/en/for-researchers/instruments/eis/fact-sheet/
|
|
356
|
+
# https://solarb.mssl.ucl.ac.uk/SolarB/eis_docs/eis_notes/02_RADIOMETRIC_CALIBRATION/eis_swnote_02.pdf
|
|
357
|
+
# Returning the throughput (without the QE):
|
|
358
|
+
eis_detector = Detector_EIS()
|
|
359
|
+
return (0.23 * u.cm**2) / eis_detector.qe_euv
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@dataclass
|
|
363
|
+
class Simulation:
|
|
364
|
+
"""
|
|
365
|
+
Simulation configuration and parameters.
|
|
366
|
+
|
|
367
|
+
The expos parameter is a single exposure time for this simulation.
|
|
368
|
+
"""
|
|
369
|
+
expos: u.Quantity = 1.0 * u.s # Single exposure time
|
|
370
|
+
n_iter: int = 10
|
|
371
|
+
slit_width: u.Quantity = 0.2 * u.arcsec
|
|
372
|
+
ncpu: int = -1
|
|
373
|
+
instrument: str = "SWC"
|
|
374
|
+
vis_sl: u.Quantity = 0 * u.photon / (u.s * u.cm**2) # Visible stray light flux before filter
|
|
375
|
+
psf: bool = False
|
|
376
|
+
enable_pinholes: bool = False
|
|
377
|
+
pinhole_sizes: List[u.Quantity] = field(default_factory=list)
|
|
378
|
+
pinhole_positions: List[float] = field(default_factory=list)
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def slit_scan_step(self) -> u.Quantity:
|
|
382
|
+
return self.slit_width
|
|
383
|
+
|
|
384
|
+
def __post_init__(self):
|
|
385
|
+
allowed_slits = {
|
|
386
|
+
"EIS": [1, 2, 4],
|
|
387
|
+
"SWC": [0.2, 0.4, 0.8, 1.6],
|
|
388
|
+
}
|
|
389
|
+
inst = self.instrument.upper()
|
|
390
|
+
slit_val = self.slit_width.to_value(u.arcsec)
|
|
391
|
+
if inst == "EIS":
|
|
392
|
+
if slit_val not in allowed_slits["EIS"]:
|
|
393
|
+
raise ValueError("For EIS, slit_width must be 1, 2, or 4 arcsec.")
|
|
394
|
+
elif inst in ("SWC"):
|
|
395
|
+
if slit_val not in allowed_slits["SWC"]:
|
|
396
|
+
raise ValueError("For SWC, slit_width must be 0.2, 0.4, 0.8, or 1.6 arcsec.")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
RSC-2022035A_EUVSTSensitivityBudget_20241213.pdf
|
|
2
|
+
Wavelength (nm), Grating reflection efficiency
|
|
3
|
+
17.000 0.04000
|
|
4
|
+
17.200 0.04460
|
|
5
|
+
17.400 0.05090
|
|
6
|
+
17.600 0.05590
|
|
7
|
+
17.800 0.05890
|
|
8
|
+
18.000 0.06010
|
|
9
|
+
18.200 0.05940
|
|
10
|
+
18.400 0.05780
|
|
11
|
+
18.600 0.05640
|
|
12
|
+
18.800 0.05550
|
|
13
|
+
19.000 0.05620
|
|
14
|
+
19.200 0.05800
|
|
15
|
+
19.400 0.06060
|
|
16
|
+
19.600 0.06390
|
|
17
|
+
19.800 0.06680
|
|
18
|
+
20.000 0.06960
|
|
19
|
+
20.200 0.07070
|
|
20
|
+
20.400 0.07080
|
|
21
|
+
20.600 0.06920
|
|
22
|
+
20.800 0.06590
|
|
23
|
+
21.000 0.06170
|
|
24
|
+
21.200 0.05570
|
|
25
|
+
21.400 0.04960
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
RSC-2022035A_EUVSTSensitivityBudget_20241213.pdf
|
|
2
|
+
Wavelength (nm), Primary mirror costing reflectance
|
|
3
|
+
17.000 0.09700
|
|
4
|
+
17.200 0.10300
|
|
5
|
+
17.400 0.09900
|
|
6
|
+
17.600 0.10200
|
|
7
|
+
17.800 0.10600
|
|
8
|
+
18.000 0.10800
|
|
9
|
+
18.200 0.11500
|
|
10
|
+
18.400 0.12800
|
|
11
|
+
18.600 0.13600
|
|
12
|
+
18.800 0.13600
|
|
13
|
+
19.000 0.13900
|
|
14
|
+
19.200 0.14900
|
|
15
|
+
19.400 0.16100
|
|
16
|
+
19.600 0.17300
|
|
17
|
+
19.800 0.17700
|
|
18
|
+
20.000 0.17800
|
|
19
|
+
20.200 0.18400
|
|
20
|
+
20.400 0.18500
|
|
21
|
+
20.600 0.18100
|
|
22
|
+
20.800 0.17400
|
|
23
|
+
21.000 0.15800
|
|
24
|
+
21.200 0.13400
|
|
25
|
+
21.400 0.10700
|