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/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