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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pinhole diffraction effects for aluminum filter modeling.
|
|
3
|
+
|
|
4
|
+
This module calculates the diffraction patterns from pinholes in the aluminum filter,
|
|
5
|
+
including both EUV and visible light contributions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import numpy as np
|
|
10
|
+
import astropy.units as u
|
|
11
|
+
import astropy.constants as const
|
|
12
|
+
from scipy.special import j1
|
|
13
|
+
from ndcube import NDCube
|
|
14
|
+
from typing import List, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def airy_disk_pattern(r: np.ndarray, wavelength: u.Quantity, pinhole_diameter: u.Quantity,
|
|
18
|
+
distance: u.Quantity) -> np.ndarray:
|
|
19
|
+
"""
|
|
20
|
+
Calculate the Airy disk diffraction pattern for a circular pinhole.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
r : np.ndarray
|
|
25
|
+
Radial distances from optical axis (in detector plane) in meters
|
|
26
|
+
wavelength : u.Quantity
|
|
27
|
+
Wavelength of light
|
|
28
|
+
pinhole_diameter : u.Quantity
|
|
29
|
+
Diameter of the pinhole
|
|
30
|
+
distance : u.Quantity
|
|
31
|
+
Distance from pinhole to detector
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
np.ndarray
|
|
36
|
+
Normalized intensity pattern (peak = 1.0)
|
|
37
|
+
"""
|
|
38
|
+
# Calculate the exact sine of the diffraction angle
|
|
39
|
+
# sin(theta) = r / sqrt(r^2 + distance^2)
|
|
40
|
+
distance_m = distance.to(u.m).value
|
|
41
|
+
sin_theta = r / np.sqrt(r**2 + distance_m**2)
|
|
42
|
+
|
|
43
|
+
# Airy disk parameter
|
|
44
|
+
# beta = (pi * D * sin(theta)) / lambda
|
|
45
|
+
beta = (np.pi * pinhole_diameter.to(u.m).value * sin_theta) / wavelength.to(u.m).value
|
|
46
|
+
|
|
47
|
+
# Avoid division by zero at center
|
|
48
|
+
beta = np.where(beta == 0, 1e-10, beta)
|
|
49
|
+
|
|
50
|
+
# Airy disk intensity pattern: I(beta) = (2*J1(beta)/beta)^2
|
|
51
|
+
# where J1 is the first-order Bessel function
|
|
52
|
+
intensity = (2 * j1(beta) / beta) ** 2
|
|
53
|
+
|
|
54
|
+
return intensity
|
|
55
|
+
|
|
56
|
+
def calculate_pinhole_diffraction_pattern(
|
|
57
|
+
detector_shape: Tuple[int, int],
|
|
58
|
+
pixel_size: u.Quantity,
|
|
59
|
+
pinhole_diameter: u.Quantity,
|
|
60
|
+
pinhole_position_slit: float,
|
|
61
|
+
slit_width: u.Quantity,
|
|
62
|
+
plate_scale: u.Quantity,
|
|
63
|
+
distance: u.Quantity,
|
|
64
|
+
wavelength: u.Quantity
|
|
65
|
+
) -> np.ndarray:
|
|
66
|
+
"""
|
|
67
|
+
Calculate the diffraction pattern from a single pinhole on the detector.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
detector_shape : tuple of int
|
|
72
|
+
(n_slit, n_spectral) shape of detector
|
|
73
|
+
pixel_size : u.Quantity
|
|
74
|
+
Physical size of detector pixels
|
|
75
|
+
pinhole_diameter : u.Quantity
|
|
76
|
+
Diameter of the pinhole
|
|
77
|
+
pinhole_position_slit : float
|
|
78
|
+
Position along slit as fraction (0.0 to 1.0)
|
|
79
|
+
slit_width : u.Quantity
|
|
80
|
+
Width of the slit
|
|
81
|
+
plate_scale : u.Quantity
|
|
82
|
+
Angular plate scale (arcsec/pixel)
|
|
83
|
+
distance : u.Quantity
|
|
84
|
+
Distance from pinhole to detector
|
|
85
|
+
wavelength : u.Quantity
|
|
86
|
+
Wavelength of light
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
np.ndarray
|
|
91
|
+
2D diffraction pattern normalized to peak intensity of 1.0
|
|
92
|
+
"""
|
|
93
|
+
n_slit, n_spectral = detector_shape
|
|
94
|
+
|
|
95
|
+
# Create coordinate grids for detector
|
|
96
|
+
slit_pixels = np.arange(n_slit)
|
|
97
|
+
spectral_pixels = np.arange(n_spectral)
|
|
98
|
+
|
|
99
|
+
# Convert pinhole position from slit fraction to pixel coordinate
|
|
100
|
+
pinhole_pixel_slit = pinhole_position_slit * (n_slit - 1)
|
|
101
|
+
|
|
102
|
+
# Calculate distances from pinhole position on detector
|
|
103
|
+
# Assuming pinhole projects to center of spectral direction
|
|
104
|
+
pinhole_pixel_spectral = n_spectral // 2
|
|
105
|
+
|
|
106
|
+
# Create 2D coordinate arrays
|
|
107
|
+
slit_grid, spectral_grid = np.meshgrid(slit_pixels, spectral_pixels, indexing='ij')
|
|
108
|
+
|
|
109
|
+
# Calculate distances from pinhole center in detector plane
|
|
110
|
+
dy_pixels = slit_grid - pinhole_pixel_slit
|
|
111
|
+
dx_pixels = spectral_grid - pinhole_pixel_spectral
|
|
112
|
+
|
|
113
|
+
# Convert to physical distances
|
|
114
|
+
dy_physical = dy_pixels * pixel_size.to(u.m).value
|
|
115
|
+
dx_physical = dx_pixels * pixel_size.to(u.m).value
|
|
116
|
+
|
|
117
|
+
# Radial distance from pinhole center
|
|
118
|
+
r_physical = np.sqrt(dx_physical**2 + dy_physical**2)
|
|
119
|
+
|
|
120
|
+
# Calculate Airy disk pattern
|
|
121
|
+
pattern = airy_disk_pattern(r_physical, wavelength, pinhole_diameter, distance)
|
|
122
|
+
|
|
123
|
+
return pattern
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def apply_euv_pinhole_diffraction(
|
|
127
|
+
photon_counts: NDCube,
|
|
128
|
+
det,
|
|
129
|
+
sim,
|
|
130
|
+
tel
|
|
131
|
+
) -> NDCube:
|
|
132
|
+
"""
|
|
133
|
+
Apply EUV pinhole diffraction effects to photon counts.
|
|
134
|
+
|
|
135
|
+
This adds EUV light that bypasses the aluminum filter through pinholes
|
|
136
|
+
and creates diffraction patterns. This should be applied after the
|
|
137
|
+
focusing optics PSF (primary mirror + grating) since the filter is
|
|
138
|
+
positioned after these optical elements.
|
|
139
|
+
|
|
140
|
+
This function correctly handles the physics by:
|
|
141
|
+
1. Subtracting the filtered EUV signal in pinhole regions
|
|
142
|
+
2. Adding the unattenuated EUV signal through pinholes
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
photon_counts : NDCube
|
|
147
|
+
EUV photon counts per pixel (shape: n_scan, n_slit, n_spectral)
|
|
148
|
+
These should already have filter throughput applied.
|
|
149
|
+
det : Detector_SWC
|
|
150
|
+
Detector configuration
|
|
151
|
+
sim : Simulation
|
|
152
|
+
Simulation configuration containing pinhole parameters
|
|
153
|
+
tel : Telescope_EUVST
|
|
154
|
+
Telescope configuration (needed to calculate filter throughput)
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
NDCube
|
|
159
|
+
Modified photon counts with EUV pinhole contributions added
|
|
160
|
+
"""
|
|
161
|
+
if not (sim.enable_pinholes and len(sim.pinhole_sizes) > 0):
|
|
162
|
+
return photon_counts # No pinholes enabled
|
|
163
|
+
|
|
164
|
+
# Get detector and data properties
|
|
165
|
+
data_shape = photon_counts.data.shape # (n_scan, n_slit, n_spectral)
|
|
166
|
+
n_scan, n_slit, n_spectral = data_shape
|
|
167
|
+
|
|
168
|
+
# Get rest wavelength for EUV calculations
|
|
169
|
+
rest_wavelength = photon_counts.meta['rest_wav']
|
|
170
|
+
|
|
171
|
+
# Calculate pixel area
|
|
172
|
+
pixel_area = (det.pix_size*1*u.pix)**2
|
|
173
|
+
|
|
174
|
+
# Initialize additional photon contributions
|
|
175
|
+
additional_photons = np.zeros_like(photon_counts.data)
|
|
176
|
+
|
|
177
|
+
# Get the wavelength axis and calculate filter throughput for EUV
|
|
178
|
+
wl_axis = photon_counts.axis_world_coords(2)[0]
|
|
179
|
+
|
|
180
|
+
# Calculate filter throughput at each wavelength
|
|
181
|
+
filter_throughput_spectrum = np.array([tel.filter.total_throughput(wl) for wl in wl_axis])
|
|
182
|
+
|
|
183
|
+
for pinhole_diameter, pinhole_position in zip(sim.pinhole_sizes, sim.pinhole_positions):
|
|
184
|
+
# Calculate pinhole area
|
|
185
|
+
pinhole_area = np.pi * (pinhole_diameter / 2)**2
|
|
186
|
+
|
|
187
|
+
# === Physics Correction for EUV ===
|
|
188
|
+
# Current photon_counts already have filter attenuation applied
|
|
189
|
+
# We need to:
|
|
190
|
+
# 1. Back-calculate what the unfiltered signal would be
|
|
191
|
+
# 2. Apply pinhole diffraction to that unfiltered signal
|
|
192
|
+
# 3. Subtract the over-counted filtered signal in pinhole regions
|
|
193
|
+
|
|
194
|
+
area_ratio = (pinhole_area / pixel_area).to(u.dimensionless_unscaled).value
|
|
195
|
+
|
|
196
|
+
# Calculate theoretical diffraction size for validation
|
|
197
|
+
# First Airy minimum: r = 1.22 * lambda * distance / diameter
|
|
198
|
+
theoretical_radius = (1.22 * rest_wavelength * det.filter_distance / pinhole_diameter).to(u.m)
|
|
199
|
+
theoretical_radius_pixels = (theoretical_radius / (det.pix_size*1*u.pix)).to(u.dimensionless_unscaled).value
|
|
200
|
+
|
|
201
|
+
# Calculate EUV diffraction pattern
|
|
202
|
+
euv_pattern = calculate_pinhole_diffraction_pattern(
|
|
203
|
+
detector_shape=(n_slit, n_spectral),
|
|
204
|
+
pixel_size=det.pix_size*u.pix,
|
|
205
|
+
pinhole_diameter=pinhole_diameter,
|
|
206
|
+
pinhole_position_slit=pinhole_position,
|
|
207
|
+
slit_width=sim.slit_width,
|
|
208
|
+
plate_scale=det.plate_scale_angle,
|
|
209
|
+
distance=det.filter_distance,
|
|
210
|
+
wavelength=rest_wavelength
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# For EUV, the diffraction pattern is much smaller than visible light
|
|
214
|
+
# Normalize the pattern to ensure total integrated intensity equals 1.0
|
|
215
|
+
# This is crucial for proper photon conservation
|
|
216
|
+
pattern_total = np.sum(euv_pattern)
|
|
217
|
+
if pattern_total > 0:
|
|
218
|
+
euv_pattern_normalized = euv_pattern / pattern_total
|
|
219
|
+
else:
|
|
220
|
+
euv_pattern_normalized = euv_pattern
|
|
221
|
+
|
|
222
|
+
# Process each scan position
|
|
223
|
+
for i in range(n_scan):
|
|
224
|
+
# Current filtered signal at this scan position
|
|
225
|
+
filtered_signal = photon_counts.data[i, :, :] # Shape: (n_slit, n_spectral)
|
|
226
|
+
|
|
227
|
+
# Back-calculate unfiltered signal (before filter attenuation)
|
|
228
|
+
# filtered_signal = unfiltered_signal * filter_throughput
|
|
229
|
+
# So: unfiltered_signal = filtered_signal / filter_throughput
|
|
230
|
+
unfiltered_signal = filtered_signal / filter_throughput_spectrum[np.newaxis, :]
|
|
231
|
+
|
|
232
|
+
# Calculate what would come through pinhole (unattenuated)
|
|
233
|
+
# Use normalized pattern to ensure proper photon conservation
|
|
234
|
+
pinhole_signal = unfiltered_signal * area_ratio * euv_pattern_normalized
|
|
235
|
+
|
|
236
|
+
# Calculate what we incorrectly have from filter in pinhole regions
|
|
237
|
+
# (filtered signal weighted by diffraction pattern and area ratio)
|
|
238
|
+
overcounted_filtered = filtered_signal * area_ratio * euv_pattern_normalized
|
|
239
|
+
|
|
240
|
+
# Net correction: add unfiltered pinhole signal, subtract overcounted filtered signal
|
|
241
|
+
# This simplifies to: filtered_signal * area_ratio * pattern * (1/filter_throughput - 1)
|
|
242
|
+
# Physical meaning:
|
|
243
|
+
# - unfiltered * area_ratio * pattern = total light through pinhole
|
|
244
|
+
# - filtered * area_ratio * pattern = incorrectly counted filtered light
|
|
245
|
+
# - difference = net additional light from pinhole
|
|
246
|
+
correction = (pinhole_signal - overcounted_filtered)
|
|
247
|
+
|
|
248
|
+
# Equivalent simplified form (more efficient):
|
|
249
|
+
# correction = filtered_signal * area_ratio * euv_pattern * (1/filter_throughput_spectrum[np.newaxis, :] - 1)
|
|
250
|
+
additional_photons[i, :, :] += correction
|
|
251
|
+
|
|
252
|
+
# Create new photon counts with EUV pinhole contributions
|
|
253
|
+
new_data = photon_counts.data + additional_photons
|
|
254
|
+
|
|
255
|
+
return NDCube(
|
|
256
|
+
data=new_data,
|
|
257
|
+
wcs=photon_counts.wcs.deepcopy(),
|
|
258
|
+
unit=photon_counts.unit,
|
|
259
|
+
meta=photon_counts.meta,
|
|
260
|
+
)
|
euvst_response/psf.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PSF handling functions for telescope and detector point spread functions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import numpy as np
|
|
8
|
+
import astropy.units as u
|
|
9
|
+
from scipy.ndimage import zoom
|
|
10
|
+
from scipy.signal import convolve2d
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_psf_ascii(fname: Path, skip: int) -> np.ndarray:
|
|
14
|
+
"""Load PSF data from ASCII file."""
|
|
15
|
+
return np.loadtxt(fname, skiprows=skip, encoding="utf-16 LE")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _resample_psf(psf: np.ndarray, res_in: u.Quantity, res_out: u.Quantity) -> np.ndarray:
|
|
19
|
+
"""Resample PSF to different pixel scale."""
|
|
20
|
+
factor = (res_in / res_out).decompose().value
|
|
21
|
+
return zoom(psf, factor, order=1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _combine_psfs(psf_focus: np.ndarray, psf_mesh: np.ndarray, crop: float = 0.99, size: int | None = None) -> np.ndarray:
|
|
25
|
+
"""Convolve focus and mesh PSF and crop to given energy or size."""
|
|
26
|
+
psf = convolve2d(psf_focus, psf_mesh, mode="same")
|
|
27
|
+
if size is not None:
|
|
28
|
+
if size % 2 == 0:
|
|
29
|
+
size += 1
|
|
30
|
+
r0, c0 = np.array(psf.shape) // 2
|
|
31
|
+
half = size // 2
|
|
32
|
+
psf = psf[r0 - half : r0 + half + 1, c0 - half : c0 + half + 1]
|
|
33
|
+
else:
|
|
34
|
+
flat = psf.ravel()
|
|
35
|
+
idx = flat.argsort()[::-1]
|
|
36
|
+
csum = flat[idx].cumsum()
|
|
37
|
+
thr = flat[idx[np.searchsorted(csum, flat.sum() * crop)]]
|
|
38
|
+
flat[flat < thr] = 0
|
|
39
|
+
rows, cols = np.where(flat.reshape(psf.shape))
|
|
40
|
+
r0, r1 = rows.min(), rows.max()
|
|
41
|
+
c0, c1 = cols.min(), cols.max()
|
|
42
|
+
side = max(r1 - r0, c1 - c0) + 1
|
|
43
|
+
r0 = (r0 + r1) // 2 - side // 2
|
|
44
|
+
c0 = (c0 + c1) // 2 - side // 2
|
|
45
|
+
psf = psf[r0 : r0 + side, c0 : c0 + side]
|
|
46
|
+
return psf / psf.sum()
|