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/main.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main execution script for instrument response simulations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import warnings
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import dill
|
|
13
|
+
import yaml
|
|
14
|
+
import astropy.units as u
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
import gzip
|
|
17
|
+
import h5py
|
|
18
|
+
|
|
19
|
+
from .config import AluminiumFilter, Detector_SWC, Detector_EIS, Telescope_EUVST, Telescope_EIS, Simulation
|
|
20
|
+
from .data_processing import load_atmosphere, rebin_atmosphere
|
|
21
|
+
from .fitting import fit_cube_gauss
|
|
22
|
+
from .monte_carlo import monte_carlo
|
|
23
|
+
from .utils import parse_yaml_input, ensure_list, set_debug_mode, debug_break, debug_on_error
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def deduplicate_list(param_list, param_name):
|
|
28
|
+
"""
|
|
29
|
+
Remove duplicates from a parameter list and warn if duplicates were found.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
param_list : list
|
|
34
|
+
List of parameter values that may contain duplicates.
|
|
35
|
+
param_name : str
|
|
36
|
+
Name of the parameter for warning messages.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
list
|
|
41
|
+
List with duplicates removed, preserving original order.
|
|
42
|
+
"""
|
|
43
|
+
seen = set()
|
|
44
|
+
deduplicated = []
|
|
45
|
+
duplicates_found = False
|
|
46
|
+
|
|
47
|
+
for item in param_list:
|
|
48
|
+
# For quantities, compare values and units; for other types, compare directly
|
|
49
|
+
if hasattr(item, 'unit'):
|
|
50
|
+
# Create a comparable key from value and unit
|
|
51
|
+
key = (item.value, str(item.unit))
|
|
52
|
+
else:
|
|
53
|
+
key = item
|
|
54
|
+
|
|
55
|
+
if key not in seen:
|
|
56
|
+
seen.add(key)
|
|
57
|
+
deduplicated.append(item)
|
|
58
|
+
else:
|
|
59
|
+
duplicates_found = True
|
|
60
|
+
|
|
61
|
+
if duplicates_found:
|
|
62
|
+
warnings.warn(
|
|
63
|
+
f"Duplicate values found in '{param_name}' parameter list. "
|
|
64
|
+
f"Removed duplicates: {len(param_list)} -> {len(deduplicated)} unique values.",
|
|
65
|
+
UserWarning
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return deduplicated
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@debug_on_error
|
|
72
|
+
def main() -> None:
|
|
73
|
+
"""Main function for running instrument response simulations."""
|
|
74
|
+
|
|
75
|
+
# Suppress astropy warnings that clutter output
|
|
76
|
+
warnings.filterwarnings(
|
|
77
|
+
"ignore",
|
|
78
|
+
message="target cannot be converted to ICRS",
|
|
79
|
+
category=UserWarning,
|
|
80
|
+
)
|
|
81
|
+
warnings.filterwarnings(
|
|
82
|
+
"ignore",
|
|
83
|
+
message="target cannot be converted to ICRS, so will not be set on SpectralCoord",
|
|
84
|
+
category=UserWarning,
|
|
85
|
+
)
|
|
86
|
+
warnings.filterwarnings(
|
|
87
|
+
"ignore",
|
|
88
|
+
message="No observer defined on WCS, SpectralCoord will be converted without any velocity frame change",
|
|
89
|
+
category=UserWarning,
|
|
90
|
+
)
|
|
91
|
+
# Catch any astropy.wcs warnings about ICRS conversion
|
|
92
|
+
warnings.filterwarnings(
|
|
93
|
+
"ignore",
|
|
94
|
+
module="astropy.wcs.wcsapi.fitswcs",
|
|
95
|
+
category=UserWarning,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
parser = argparse.ArgumentParser()
|
|
99
|
+
parser.add_argument("--config", type=str, help="YAML config file", required=True)
|
|
100
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
|
101
|
+
args = parser.parse_args()
|
|
102
|
+
|
|
103
|
+
# Set debug mode globally
|
|
104
|
+
set_debug_mode(args.debug)
|
|
105
|
+
if args.debug:
|
|
106
|
+
print("Debug mode enabled - will break to IPython on errors")
|
|
107
|
+
|
|
108
|
+
# Check if config file exists
|
|
109
|
+
config_path = Path(args.config)
|
|
110
|
+
if not config_path.is_file():
|
|
111
|
+
raise FileNotFoundError(f"Config file not found: {args.config}")
|
|
112
|
+
|
|
113
|
+
# Load YAML config
|
|
114
|
+
with open(args.config, "r") as f:
|
|
115
|
+
config = yaml.safe_load(f)
|
|
116
|
+
|
|
117
|
+
# Set up instrument, detector, telescope, simulation from config
|
|
118
|
+
instrument = config.get("instrument", "SWC").upper()
|
|
119
|
+
psf_settings = ensure_list(config.get("psf", [False])) # Handle PSF as a list
|
|
120
|
+
|
|
121
|
+
# Synthesis file path - allow user to specify where the synthesised_spectra.pkl file is located
|
|
122
|
+
synthesis_file = config.get("synthesis_file", "./run/input/synthesised_spectra.pkl")
|
|
123
|
+
|
|
124
|
+
# Reference line for wavelength grid and metadata
|
|
125
|
+
reference_line = config.get("reference_line", "Fe12_195.1190")
|
|
126
|
+
|
|
127
|
+
# Check if synthesis file exists
|
|
128
|
+
synthesis_path = Path(synthesis_file)
|
|
129
|
+
if not synthesis_path.is_file():
|
|
130
|
+
raise FileNotFoundError(f"Synthesis file not found: {synthesis_file}. "
|
|
131
|
+
f"Please check the 'synthesis_file' path in your config file.")
|
|
132
|
+
psf_settings = deduplicate_list(psf_settings, "psf") # Remove duplicates
|
|
133
|
+
n_iter = config.get("n_iter", 25)
|
|
134
|
+
ncpu = config.get("ncpu", -1)
|
|
135
|
+
|
|
136
|
+
# Print PSF warnings for any True values in the list
|
|
137
|
+
if any(psf_settings):
|
|
138
|
+
if instrument == "SWC":
|
|
139
|
+
warnings.warn(
|
|
140
|
+
"The SWC PSF is the modelled PSF including simulations and some microroughness measurements. Final PSF will be measured before launch.",
|
|
141
|
+
UserWarning,
|
|
142
|
+
)
|
|
143
|
+
elif instrument == "EIS":
|
|
144
|
+
warnings.warn(
|
|
145
|
+
"The EIS PSF is not well understood. We use a symmetrical Voigt profile with a FWHM of 3 pixels from Ugarte-Urra (2016) EIS Software Note 2.",
|
|
146
|
+
UserWarning,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Parse configuration parameters - can be single values or lists
|
|
150
|
+
# Each parameter combination will be run independently, including exposure times
|
|
151
|
+
slit_widths = ensure_list(parse_yaml_input(config.get("slit_width", ['0.2 arcsec'])))
|
|
152
|
+
slit_widths = deduplicate_list(slit_widths, "slit_width")
|
|
153
|
+
|
|
154
|
+
# Handle instrument-specific parameters
|
|
155
|
+
if instrument == "SWC":
|
|
156
|
+
# SWC requires oxide, carbon, and aluminium thickness parameters
|
|
157
|
+
oxide_thicknesses = ensure_list(parse_yaml_input(config.get("oxide_thickness", ['95 angstrom'])))
|
|
158
|
+
oxide_thicknesses = deduplicate_list(oxide_thicknesses, "oxide_thickness")
|
|
159
|
+
c_thicknesses = ensure_list(parse_yaml_input(config.get("c_thickness", ['0 angstrom'])))
|
|
160
|
+
c_thicknesses = deduplicate_list(c_thicknesses, "c_thickness")
|
|
161
|
+
aluminium_thicknesses = ensure_list(parse_yaml_input(config.get("aluminium_thickness", ['1485 angstrom'])))
|
|
162
|
+
aluminium_thicknesses = deduplicate_list(aluminium_thicknesses, "aluminium_thickness")
|
|
163
|
+
elif instrument == "EIS":
|
|
164
|
+
# EIS doesn't use these parameters - check they weren't specified
|
|
165
|
+
if "oxide_thickness" in config:
|
|
166
|
+
raise ValueError("EIS does not support oxide thickness parameter. Remove 'oxide_thickness' from configuration.")
|
|
167
|
+
if "c_thickness" in config:
|
|
168
|
+
raise ValueError("EIS does not support carbon thickness parameter. Remove 'c_thickness' from configuration.")
|
|
169
|
+
if "aluminium_thickness" in config:
|
|
170
|
+
raise ValueError("EIS does not support custom aluminium thickness parameter. Remove 'aluminium_thickness' from configuration.")
|
|
171
|
+
|
|
172
|
+
# Set defaults for EIS (these won't be used but are needed for parameter combination logic)
|
|
173
|
+
oxide_thicknesses = [0 * u.nm]
|
|
174
|
+
c_thicknesses = [0 * u.nm]
|
|
175
|
+
aluminium_thicknesses = [1500 * u.angstrom]
|
|
176
|
+
|
|
177
|
+
ccd_temperatures = ensure_list(parse_yaml_input(config.get("ccd_temperature", ['-60 Celsius']))) # Temperature in Celsius
|
|
178
|
+
ccd_temperatures = deduplicate_list(ccd_temperatures, "ccd_temperature")
|
|
179
|
+
vis_sl_vals = ensure_list(parse_yaml_input(config.get("vis_sl", ['0 photon / (s * cm^2)'])))
|
|
180
|
+
vis_sl_vals = deduplicate_list(vis_sl_vals, "vis_sl")
|
|
181
|
+
exposures = ensure_list(parse_yaml_input(config.get("expos", ['1 s'])))
|
|
182
|
+
exposures = deduplicate_list(exposures, "expos")
|
|
183
|
+
|
|
184
|
+
# Parse pinhole parameters
|
|
185
|
+
enable_pinholes_vals = ensure_list(config.get("enable_pinholes", [False]))
|
|
186
|
+
enable_pinholes_vals = deduplicate_list(enable_pinholes_vals, "enable_pinholes")
|
|
187
|
+
|
|
188
|
+
# if pinholes are enabled, raise warning that this is only intended to be used by the instrument team
|
|
189
|
+
if any(enable_pinholes_vals):
|
|
190
|
+
warnings.warn(
|
|
191
|
+
"Pinhole effects are only intended for use by the instrument team. "
|
|
192
|
+
"Please contact MSSL for more information.",
|
|
193
|
+
UserWarning
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Parse pinhole sizes and positions (these are not swept - used together for multiple pinholes per simulation)
|
|
197
|
+
pinhole_sizes = []
|
|
198
|
+
pinhole_positions = []
|
|
199
|
+
|
|
200
|
+
if "pinhole_sizes" in config:
|
|
201
|
+
pinhole_sizes = ensure_list(parse_yaml_input(config["pinhole_sizes"]))
|
|
202
|
+
if "pinhole_positions" in config:
|
|
203
|
+
pinhole_positions = ensure_list(config["pinhole_positions"])
|
|
204
|
+
|
|
205
|
+
# Validate pinhole configuration
|
|
206
|
+
if len(pinhole_sizes) != len(pinhole_positions) and len(pinhole_sizes) > 0:
|
|
207
|
+
raise ValueError("pinhole_sizes and pinhole_positions must have the same length")
|
|
208
|
+
|
|
209
|
+
# Check if pinholes are enabled with EIS instrument
|
|
210
|
+
if instrument == "EIS" and any(enable_pinholes_vals):
|
|
211
|
+
raise ValueError("Pinhole effects are not supported for EIS instrument. Pinholes are only available for SWC.")
|
|
212
|
+
|
|
213
|
+
# Check if pinhole parameters are specified for EIS
|
|
214
|
+
if instrument == "EIS":
|
|
215
|
+
if "pinhole_sizes" in config:
|
|
216
|
+
raise ValueError("EIS does not support pinhole_sizes parameter. Remove 'pinhole_sizes' from configuration.")
|
|
217
|
+
if "pinhole_positions" in config:
|
|
218
|
+
raise ValueError("EIS does not support pinhole_positions parameter. Remove 'pinhole_positions' from configuration.")
|
|
219
|
+
if "enable_pinholes" in config and any(config.get("enable_pinholes", [False])):
|
|
220
|
+
raise ValueError("EIS does not support enable_pinholes parameter. Remove 'enable_pinholes' from configuration.")
|
|
221
|
+
|
|
222
|
+
if any(enable_pinholes_vals) and len(pinhole_sizes) == 0:
|
|
223
|
+
warnings.warn("enable_pinholes is True but no pinhole_sizes specified. Pinhole effects will be disabled.")
|
|
224
|
+
enable_pinholes_vals = [False]
|
|
225
|
+
|
|
226
|
+
# Load synthetic atmosphere cube
|
|
227
|
+
print("Loading atmosphere...")
|
|
228
|
+
print(f"Using '{reference_line}' as reference line for wavelength grid and metadata...")
|
|
229
|
+
cube_sim = load_atmosphere(synthesis_file, reference_line)
|
|
230
|
+
|
|
231
|
+
# Set up base detector configuration (doesn't change with parameters)
|
|
232
|
+
if instrument == "SWC":
|
|
233
|
+
DET = Detector_SWC()
|
|
234
|
+
elif instrument == "EIS":
|
|
235
|
+
DET = Detector_EIS()
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError(f"Unknown instrument: {instrument}")
|
|
238
|
+
|
|
239
|
+
# Create results structure for all parameter combinations
|
|
240
|
+
all_results = {}
|
|
241
|
+
|
|
242
|
+
# Loop over all parameter combinations
|
|
243
|
+
total_combinations = len(slit_widths) * len(oxide_thicknesses) * len(c_thicknesses) * len(aluminium_thicknesses) * len(ccd_temperatures) * len(vis_sl_vals) * len(exposures) * len(psf_settings) * len(enable_pinholes_vals)
|
|
244
|
+
print(f"Running {total_combinations} parameter combinations...")
|
|
245
|
+
|
|
246
|
+
combination_idx = 0
|
|
247
|
+
for slit_width in slit_widths:
|
|
248
|
+
# Rebin atmosphere only when slit width changes (expensive operation)
|
|
249
|
+
print(f"\nRebinning atmosphere cube for slit width {slit_width}...")
|
|
250
|
+
SIM_temp = Simulation(
|
|
251
|
+
expos=1.0 * u.s, # Temporary value for rebinning
|
|
252
|
+
n_iter=n_iter,
|
|
253
|
+
slit_width=slit_width,
|
|
254
|
+
ncpu=ncpu,
|
|
255
|
+
instrument=instrument,
|
|
256
|
+
psf=False, # Use False for rebinning
|
|
257
|
+
)
|
|
258
|
+
cube_reb = rebin_atmosphere(cube_sim, DET, SIM_temp)
|
|
259
|
+
|
|
260
|
+
print("Fitting ground truth cube...")
|
|
261
|
+
fit_truth_data, fit_truth_units = fit_cube_gauss(cube_reb, n_jobs=ncpu)
|
|
262
|
+
|
|
263
|
+
for oxide_thickness in oxide_thicknesses:
|
|
264
|
+
for c_thickness in c_thicknesses:
|
|
265
|
+
for aluminium_thickness in aluminium_thicknesses:
|
|
266
|
+
for ccd_temperature in ccd_temperatures:
|
|
267
|
+
for vis_sl in vis_sl_vals:
|
|
268
|
+
for exposure in exposures:
|
|
269
|
+
for psf in psf_settings:
|
|
270
|
+
for enable_pinholes in enable_pinholes_vals:
|
|
271
|
+
combination_idx += 1
|
|
272
|
+
print(f"--- Combination {combination_idx}/{total_combinations} ---")
|
|
273
|
+
print(f"Slit width: {slit_width}")
|
|
274
|
+
print(f"Oxide thickness: {oxide_thickness}")
|
|
275
|
+
print(f"Carbon thickness: {c_thickness}")
|
|
276
|
+
print(f"Aluminium thickness: {aluminium_thickness}")
|
|
277
|
+
print(f"CCD temperature: {ccd_temperature}")
|
|
278
|
+
print(f"Visible stray light (before filter): {vis_sl}")
|
|
279
|
+
print(f"Exposure time: {exposure}")
|
|
280
|
+
print(f"PSF enabled: {psf}")
|
|
281
|
+
print(f"Pinhole effects enabled: {enable_pinholes}")
|
|
282
|
+
if enable_pinholes and len(pinhole_sizes) > 0:
|
|
283
|
+
print(f"Pinhole sizes: {pinhole_sizes}")
|
|
284
|
+
print(f"Pinhole positions: {pinhole_positions}")
|
|
285
|
+
|
|
286
|
+
# Set up telescope configuration for this combination
|
|
287
|
+
if instrument == "SWC":
|
|
288
|
+
filter_obj = AluminiumFilter(
|
|
289
|
+
oxide_thickness=oxide_thickness,
|
|
290
|
+
c_thickness=c_thickness,
|
|
291
|
+
al_thickness=aluminium_thickness,
|
|
292
|
+
)
|
|
293
|
+
TEL = Telescope_EUVST(filter=filter_obj)
|
|
294
|
+
elif instrument == "EIS":
|
|
295
|
+
TEL = Telescope_EIS()
|
|
296
|
+
# EIS uses fixed filter configuration - no custom parameters needed
|
|
297
|
+
else:
|
|
298
|
+
raise ValueError(f"Unknown instrument: {instrument}")
|
|
299
|
+
|
|
300
|
+
# Set up detector configuration with calculated dark current
|
|
301
|
+
if instrument == "SWC":
|
|
302
|
+
# Create a detector with calculated dark current for this temperature
|
|
303
|
+
DET = Detector_SWC.with_temperature(ccd_temperature)
|
|
304
|
+
print(f"Calculated dark current: {DET.dark_current:.2e}")
|
|
305
|
+
elif instrument == "EIS":
|
|
306
|
+
DET = Detector_EIS.with_temperature(ccd_temperature)
|
|
307
|
+
print(f"Calculated dark current: {DET.dark_current:.2e}")
|
|
308
|
+
else:
|
|
309
|
+
raise ValueError(f"Unknown instrument: {instrument}")
|
|
310
|
+
|
|
311
|
+
# Create simulation object
|
|
312
|
+
SIM = Simulation(
|
|
313
|
+
expos=exposure, # Single exposure value
|
|
314
|
+
n_iter=n_iter,
|
|
315
|
+
slit_width=slit_width,
|
|
316
|
+
ncpu=ncpu,
|
|
317
|
+
instrument=instrument,
|
|
318
|
+
vis_sl=vis_sl,
|
|
319
|
+
psf=psf,
|
|
320
|
+
enable_pinholes=enable_pinholes,
|
|
321
|
+
pinhole_sizes=pinhole_sizes if enable_pinholes else [],
|
|
322
|
+
pinhole_positions=pinhole_positions if enable_pinholes else [],
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# # Debug breakpoint - inspect simulation parameters
|
|
326
|
+
# debug_break("Before Monte Carlo simulation", locals(), globals())
|
|
327
|
+
|
|
328
|
+
# Run Monte Carlo for this single parameter combination
|
|
329
|
+
first_dn_signal, dn_fit_stats, first_photon_signal, photon_fit_stats = monte_carlo(
|
|
330
|
+
cube_reb, exposure, DET, TEL, SIM, n_iter=SIM.n_iter
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Store results for this parameter combination
|
|
334
|
+
sec = exposure.to_value(u.s)
|
|
335
|
+
param_key = (
|
|
336
|
+
slit_width.to_value(u.arcsec),
|
|
337
|
+
oxide_thickness.to_value(u.nm) if oxide_thickness.unit.is_equivalent(u.nm) else oxide_thickness.to_value(u.AA),
|
|
338
|
+
c_thickness.to_value(u.nm) if c_thickness.unit.is_equivalent(u.nm) else c_thickness.to_value(u.AA),
|
|
339
|
+
aluminium_thickness.to_value(u.AA),
|
|
340
|
+
ccd_temperature.to_value(u.Celsius,equivalencies=u.temperature()),
|
|
341
|
+
vis_sl.to_value(u.photon / (u.s * u.cm**2)),
|
|
342
|
+
sec,
|
|
343
|
+
psf,
|
|
344
|
+
enable_pinholes
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Store fit_truth data and units separately
|
|
348
|
+
all_results[param_key] = {
|
|
349
|
+
"parameters": {
|
|
350
|
+
"slit_width": slit_width,
|
|
351
|
+
"oxide_thickness": oxide_thickness,
|
|
352
|
+
"c_thickness": c_thickness,
|
|
353
|
+
"aluminium_thickness": aluminium_thickness,
|
|
354
|
+
"ccd_temperature": ccd_temperature,
|
|
355
|
+
"vis_sl": vis_sl,
|
|
356
|
+
"exposure": exposure,
|
|
357
|
+
"psf": psf,
|
|
358
|
+
"enable_pinholes": enable_pinholes,
|
|
359
|
+
"pinhole_sizes": pinhole_sizes if enable_pinholes else [],
|
|
360
|
+
"pinhole_positions": pinhole_positions if enable_pinholes else [],
|
|
361
|
+
},
|
|
362
|
+
# Store signal data and units separately
|
|
363
|
+
"first_dn_signal_data": first_dn_signal.data,
|
|
364
|
+
"first_dn_signal_unit": first_dn_signal.unit,
|
|
365
|
+
"first_photon_signal_data": first_photon_signal.data,
|
|
366
|
+
"first_photon_signal_unit": first_photon_signal.unit,
|
|
367
|
+
"first_signal_wcs": first_dn_signal.wcs,
|
|
368
|
+
"dn_fit_stats": dn_fit_stats,
|
|
369
|
+
"photon_fit_stats": photon_fit_stats,
|
|
370
|
+
"ground_truth": {
|
|
371
|
+
"fit_truth_data": fit_truth_data,
|
|
372
|
+
"fit_truth_units": fit_truth_units,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# Clean up memory
|
|
377
|
+
del first_dn_signal, first_photon_signal, dn_fit_stats, photon_fit_stats
|
|
378
|
+
|
|
379
|
+
# Prepare final results structure
|
|
380
|
+
results = {
|
|
381
|
+
"all_combinations": all_results,
|
|
382
|
+
"parameter_ranges": {
|
|
383
|
+
"slit_widths": slit_widths,
|
|
384
|
+
"oxide_thicknesses": oxide_thicknesses,
|
|
385
|
+
"c_thicknesses": c_thicknesses,
|
|
386
|
+
"aluminium_thicknesses": aluminium_thicknesses,
|
|
387
|
+
"ccd_temperatures": ccd_temperatures,
|
|
388
|
+
"vis_sl_vals": vis_sl_vals,
|
|
389
|
+
"exposures": exposures,
|
|
390
|
+
"psf_settings": psf_settings,
|
|
391
|
+
"enable_pinholes_vals": enable_pinholes_vals,
|
|
392
|
+
"pinhole_sizes": pinhole_sizes,
|
|
393
|
+
"pinhole_positions": pinhole_positions,
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# Generate output filename based on config file
|
|
398
|
+
config_path = Path(args.config)
|
|
399
|
+
config_base = config_path.stem
|
|
400
|
+
output_file = Path(f"run/result/{config_base}.pkl")
|
|
401
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
402
|
+
|
|
403
|
+
print(f"\nSaving results to {output_file}")
|
|
404
|
+
|
|
405
|
+
# Prepare the data to save
|
|
406
|
+
save_data = {
|
|
407
|
+
"results": results,
|
|
408
|
+
"config": config,
|
|
409
|
+
"instrument": instrument,
|
|
410
|
+
"cube_sim": cube_sim,
|
|
411
|
+
"cube_reb": cube_reb,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
with open(output_file, "wb") as f:
|
|
415
|
+
dill.dump(save_data, f)
|
|
416
|
+
|
|
417
|
+
print(f"Saved results to {output_file} ({os.path.getsize(output_file) / 1e6:.1f} MB)")
|
|
418
|
+
|
|
419
|
+
print(f"Instrument response simulation complete!")
|
|
420
|
+
print(f"Total parameter combinations: {total_combinations}")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
if __name__ == "__main__":
|
|
424
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Monte Carlo simulation functions for instrument response analysis.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
import numpy as np
|
|
8
|
+
import astropy.units as u
|
|
9
|
+
from ndcube import NDCube
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
from .radiometric import (
|
|
12
|
+
apply_exposure_and_poisson, intensity_to_photons, add_telescope_throughput,
|
|
13
|
+
photons_to_pixel_counts, apply_focusing_optics_psf, to_electrons, add_visible_stray_light, to_dn,
|
|
14
|
+
add_pinhole_visible_light
|
|
15
|
+
)
|
|
16
|
+
from .pinhole_diffraction import apply_euv_pinhole_diffraction
|
|
17
|
+
from .fitting import fit_cube_gauss
|
|
18
|
+
from .utils import angle_to_distance
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def simulate_once(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim) -> Tuple[NDCube, ...]:
|
|
22
|
+
"""
|
|
23
|
+
Run a single Monte Carlo simulation of the instrument response.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
I_cube : NDCube
|
|
28
|
+
Input intensity cube
|
|
29
|
+
t_exp : u.Quantity
|
|
30
|
+
Exposure time
|
|
31
|
+
det : Detector_SWC or Detector_EIS
|
|
32
|
+
Detector configuration
|
|
33
|
+
tel : Telescope_EUVST or Telescope_EIS
|
|
34
|
+
Telescope configuration
|
|
35
|
+
sim : Simulation
|
|
36
|
+
Simulation configuration
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
tuple of NDCube
|
|
41
|
+
Signal cubes at each step of the radiometric pipeline:
|
|
42
|
+
(intensity_exp, photons_total, photons_throughput, photons_pixels,
|
|
43
|
+
photons_focused, photons_euv_pinholes, electrons, electrons_stray,
|
|
44
|
+
electrons_pinholes, dn)
|
|
45
|
+
"""
|
|
46
|
+
# Apply exposure time and Poisson noise
|
|
47
|
+
intensity_exp = apply_exposure_and_poisson(I_cube, t_exp)
|
|
48
|
+
|
|
49
|
+
# Convert to total photons
|
|
50
|
+
photons_total = intensity_to_photons(intensity_exp)
|
|
51
|
+
|
|
52
|
+
# Apply telescope optical throughput
|
|
53
|
+
photons_throughput = add_telescope_throughput(photons_total, tel)
|
|
54
|
+
|
|
55
|
+
# Convert to pixel counts
|
|
56
|
+
photons_pixels = photons_to_pixel_counts(photons_throughput, det.wvl_res, det.plate_scale_length, angle_to_distance(sim.slit_width))
|
|
57
|
+
|
|
58
|
+
# Apply focusing optics PSF (primary mirror + diffraction grating)
|
|
59
|
+
if sim.psf:
|
|
60
|
+
photons_focused = apply_focusing_optics_psf(photons_pixels, tel)
|
|
61
|
+
else:
|
|
62
|
+
photons_focused = photons_pixels
|
|
63
|
+
|
|
64
|
+
# Apply EUV pinhole diffraction effects (after focusing optics, if enabled)
|
|
65
|
+
if sim.enable_pinholes and len(sim.pinhole_sizes) > 0:
|
|
66
|
+
photons_euv_pinholes = apply_euv_pinhole_diffraction(photons_focused, det, sim, tel)
|
|
67
|
+
else:
|
|
68
|
+
photons_euv_pinholes = photons_focused
|
|
69
|
+
|
|
70
|
+
# Convert to electrons
|
|
71
|
+
electrons = to_electrons(photons_euv_pinholes, t_exp, det)
|
|
72
|
+
|
|
73
|
+
# Add visible stray light (with filter throughput)
|
|
74
|
+
electrons_stray = add_visible_stray_light(electrons, t_exp, det, sim, tel)
|
|
75
|
+
|
|
76
|
+
# Add visible light pinhole effects (if enabled)
|
|
77
|
+
if sim.enable_pinholes and len(sim.pinhole_sizes) > 0:
|
|
78
|
+
electrons_pinholes = add_pinhole_visible_light(electrons_stray, t_exp, det, sim, tel)
|
|
79
|
+
else:
|
|
80
|
+
electrons_pinholes = electrons_stray
|
|
81
|
+
|
|
82
|
+
# Convert to digital numbers
|
|
83
|
+
dn = to_dn(electrons_pinholes, det)
|
|
84
|
+
|
|
85
|
+
return (intensity_exp, photons_total, photons_throughput, photons_pixels,
|
|
86
|
+
photons_focused, photons_euv_pinholes, electrons, electrons_stray,
|
|
87
|
+
electrons_pinholes, dn)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def monte_carlo(I_cube: NDCube, t_exp: u.Quantity, det, tel, sim, n_iter: int = 5) -> Tuple[NDCube, dict, NDCube, dict]:
|
|
91
|
+
"""
|
|
92
|
+
Run Monte Carlo simulations and fit results.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
I_cube : NDCube
|
|
97
|
+
Input intensity cube
|
|
98
|
+
t_exp : u.Quantity
|
|
99
|
+
Exposure time
|
|
100
|
+
det : Detector_SWC or Detector_EIS
|
|
101
|
+
Detector configuration
|
|
102
|
+
tel : Telescope_EUVST or Telescope_EIS
|
|
103
|
+
Telescope configuration
|
|
104
|
+
sim : Simulation
|
|
105
|
+
Simulation configuration
|
|
106
|
+
n_iter : int
|
|
107
|
+
Number of Monte Carlo iterations
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
tuple
|
|
112
|
+
(first_dn_signal, dn_fit_results, first_photon_signal, photon_fit_results)
|
|
113
|
+
- first_dn_signal: First iteration DN signal (NDCube)
|
|
114
|
+
- dn_fit_results: Dict with fit data and units stored separately
|
|
115
|
+
- first_photon_signal: First iteration photon signal (NDCube)
|
|
116
|
+
- photon_fit_results: Dict with fit data and units stored separately
|
|
117
|
+
"""
|
|
118
|
+
first_dn_signal, first_photon_signal = None, None
|
|
119
|
+
dn_fit_values_list, photon_fit_values_list = [], []
|
|
120
|
+
|
|
121
|
+
for i in tqdm(range(n_iter), desc="Monte-Carlo", unit="iter", leave=False):
|
|
122
|
+
# Simulate one run
|
|
123
|
+
(intensity_exp, photons_total, photons_throughput, photons_pixels,
|
|
124
|
+
photons_focused, photons_euv_pinholes, electrons, electrons_stray,
|
|
125
|
+
electrons_pinholes, dn) = simulate_once(I_cube, t_exp, det, tel, sim)
|
|
126
|
+
|
|
127
|
+
# Store first iteration signals only
|
|
128
|
+
if i == 0:
|
|
129
|
+
first_dn_signal = dn
|
|
130
|
+
first_photon_signal = photons_euv_pinholes
|
|
131
|
+
|
|
132
|
+
# Fit DN signal
|
|
133
|
+
dn_fit_values, dn_fit_units = fit_cube_gauss(dn, n_jobs=sim.ncpu)
|
|
134
|
+
dn_fit_values_list.append(dn_fit_values)
|
|
135
|
+
|
|
136
|
+
# Fit photon signal
|
|
137
|
+
photon_fit_values, photon_fit_units = fit_cube_gauss(photons_euv_pinholes, n_jobs=sim.ncpu)
|
|
138
|
+
photon_fit_values_list.append(photon_fit_values)
|
|
139
|
+
|
|
140
|
+
# Stack fit results
|
|
141
|
+
dn_fits_values = np.stack(dn_fit_values_list)
|
|
142
|
+
photon_fits_values = np.stack(photon_fit_values_list)
|
|
143
|
+
|
|
144
|
+
# Compute statistics on stripped data
|
|
145
|
+
dn_fit_results = {
|
|
146
|
+
"first_fit_data": dn_fits_values[0],
|
|
147
|
+
"mean_data": dn_fits_values.mean(axis=0),
|
|
148
|
+
"std_data": dn_fits_values.std(axis=0),
|
|
149
|
+
"units": dn_fit_units,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
photon_fit_results = {
|
|
153
|
+
"first_fit_data": photon_fits_values[0],
|
|
154
|
+
"mean_data": photon_fits_values.mean(axis=0),
|
|
155
|
+
"std_data": photon_fits_values.std(axis=0),
|
|
156
|
+
"units": photon_fit_units,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return first_dn_signal, dn_fit_results, first_photon_signal, photon_fit_results
|