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/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()
@@ -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
@@ -0,0 +1,3 @@
1
+ Aluminium, aluminium oxide, and carbon throughputs retrieved from:
2
+ https://henke.lbl.gov/optical_constants/filter2.html
3
+ 11/08/2025