eclipse 0.13.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.
- eclipse/__init__.py +95 -0
- eclipse/analysis.py +853 -0
- eclipse/atmosphere.py +859 -0
- eclipse/band.py +477 -0
- eclipse/cli.py +138 -0
- eclipse/config.py +1710 -0
- eclipse/continuum.py +413 -0
- eclipse/data/eis_response/EIS_EffArea_A.004 +61 -0
- eclipse/data/eis_response/EIS_EffArea_B.004 +60 -0
- eclipse/data/eis_response/EIS_EffArea_B.005 +64 -0
- eclipse/data/eis_response/README_dz2025 +50 -0
- eclipse/data/eis_response/fit_eis_ea_2023-05-04_smooth.sav +0 -0
- eclipse/data/eis_response/source.txt +33 -0
- eclipse/data/science_cases.yaml +365 -0
- eclipse/data/throughput/grating_reflection_efficiency.dat +25 -0
- eclipse/data/throughput/primary_mirror_coating_reflectance.dat +25 -0
- eclipse/data/throughput/source.txt +6 -0
- eclipse/data/throughput/throughput_aluminium_1000_angstrom.dat +503 -0
- eclipse/data/throughput/throughput_aluminium_oxide_1000_angstrom.dat +503 -0
- eclipse/data/throughput/throughput_carbon_1000_angstrom.dat +503 -0
- eclipse/data_processing.py +904 -0
- eclipse/deprecated.py +161 -0
- eclipse/eis_calibration.py +573 -0
- eclipse/extern/__init__.py +0 -0
- eclipse/extern/mpfit.py +2388 -0
- eclipse/fitting.py +1734 -0
- eclipse/frame.py +594 -0
- eclipse/full_frame.py +944 -0
- eclipse/main.py +1646 -0
- eclipse/monte_carlo.py +660 -0
- eclipse/pinhole_diffraction.py +409 -0
- eclipse/psf.py +6 -0
- eclipse/radiometric.py +1328 -0
- eclipse/raster.py +869 -0
- eclipse/readout.py +1002 -0
- eclipse/resolution_budget.py +108 -0
- eclipse/results_file.py +1226 -0
- eclipse/sampling.py +351 -0
- eclipse/science_cases.py +364 -0
- eclipse/synthesis.py +2492 -0
- eclipse/synthesis_cli.py +12 -0
- eclipse/synthesis_file.py +882 -0
- eclipse/utils.py +1145 -0
- eclipse-0.13.0.dist-info/METADATA +70 -0
- eclipse-0.13.0.dist-info/RECORD +50 -0
- eclipse-0.13.0.dist-info/WHEEL +5 -0
- eclipse-0.13.0.dist-info/entry_points.txt +7 -0
- eclipse-0.13.0.dist-info/licenses/LICENSE +3 -0
- eclipse-0.13.0.dist-info/top_level.txt +2 -0
- euvst_response/__init__.py +129 -0
|
@@ -0,0 +1,409 @@
|
|
|
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
|
+
Airy pattern of a circular pinhole on a screen some distance behind it.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
r : np.ndarray
|
|
25
|
+
Distances from the pattern centre on the screen, in metres.
|
|
26
|
+
wavelength : u.Quantity
|
|
27
|
+
Wavelength of the light.
|
|
28
|
+
pinhole_diameter : u.Quantity
|
|
29
|
+
Pinhole diameter.
|
|
30
|
+
distance : u.Quantity
|
|
31
|
+
Distance from the pinhole to the screen.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
np.ndarray
|
|
36
|
+
Brightness at each distance, 1 at the centre.
|
|
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
|
+
|
|
57
|
+
def airy_peak_fraction_per_pixel(
|
|
58
|
+
pinhole_diameter: u.Quantity,
|
|
59
|
+
distance: u.Quantity,
|
|
60
|
+
wavelength: u.Quantity,
|
|
61
|
+
pixel_size: u.Quantity,
|
|
62
|
+
) -> float:
|
|
63
|
+
"""
|
|
64
|
+
Fraction of a pinhole's transmitted photons in the brightest detector pixel.
|
|
65
|
+
|
|
66
|
+
This is the absolute normalisation of the Airy pattern, which
|
|
67
|
+
`calculate_pinhole_diffraction_pattern` leaves out (its pattern peaks at
|
|
68
|
+
1.0). For a circular aperture of area A at distance L, the on-axis
|
|
69
|
+
irradiance is
|
|
70
|
+
|
|
71
|
+
E_0 = P_total * A / (lambda^2 L^2)
|
|
72
|
+
|
|
73
|
+
(integrating (2*J1(u)/u)^2 over the plane gives 4*lambda^2*L^2/(pi*D^2),
|
|
74
|
+
which recovers P_total), so the fraction of the transmitted power on one
|
|
75
|
+
pixel of area a is `A * a / (lambda L)^2`.
|
|
76
|
+
|
|
77
|
+
Normalising the pattern by its sum over the detector would force every
|
|
78
|
+
pinhole photon onto the detector. For a small pinhole the Airy disc is
|
|
79
|
+
far larger than the detector: a 1 micron hole at 250 mm has its first
|
|
80
|
+
minimum at 183 mm, against a detector tens of mm across, so most of the
|
|
81
|
+
light misses the detector and must not be redistributed onto it. Use
|
|
82
|
+
this function when absolute photon numbers matter, e.g. for a pinhole
|
|
83
|
+
budget.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
float
|
|
88
|
+
Fraction of transmitted photons in the brightest pixel, capped at
|
|
89
|
+
1.0. The cap only binds for holes so large that the geometric image
|
|
90
|
+
is smaller than a pixel, where Fraunhofer diffraction no longer
|
|
91
|
+
applies.
|
|
92
|
+
"""
|
|
93
|
+
area = np.pi * (pinhole_diameter.to(u.m).value / 2.0) ** 2
|
|
94
|
+
pix = pixel_size.to(u.m).value
|
|
95
|
+
lam = wavelength.to(u.m).value
|
|
96
|
+
dist = distance.to(u.m).value
|
|
97
|
+
return float(min(1.0, area * pix ** 2 / (lam * dist) ** 2))
|
|
98
|
+
|
|
99
|
+
def calculate_pinhole_diffraction_pattern(
|
|
100
|
+
detector_shape: Tuple[int, int],
|
|
101
|
+
pixel_size: u.Quantity,
|
|
102
|
+
pinhole_diameter: u.Quantity,
|
|
103
|
+
pinhole_position_slit: float,
|
|
104
|
+
slit_width: u.Quantity,
|
|
105
|
+
plate_scale: u.Quantity,
|
|
106
|
+
distance: u.Quantity,
|
|
107
|
+
wavelength: u.Quantity,
|
|
108
|
+
pinhole_position_spectral: float | None = None,
|
|
109
|
+
) -> np.ndarray:
|
|
110
|
+
"""
|
|
111
|
+
Calculate the diffraction pattern from a single pinhole on the detector.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
detector_shape : tuple of int
|
|
116
|
+
(n_slit, n_spectral) shape of detector
|
|
117
|
+
pixel_size : u.Quantity
|
|
118
|
+
Physical size of detector pixels
|
|
119
|
+
pinhole_diameter : u.Quantity
|
|
120
|
+
Diameter of the pinhole
|
|
121
|
+
pinhole_position_slit : float
|
|
122
|
+
Position along slit as fraction (0.0 to 1.0)
|
|
123
|
+
slit_width : u.Quantity
|
|
124
|
+
Width of the slit
|
|
125
|
+
plate_scale : u.Quantity
|
|
126
|
+
Angular plate scale (arcsec/pixel)
|
|
127
|
+
distance : u.Quantity
|
|
128
|
+
Distance from pinhole to detector
|
|
129
|
+
wavelength : u.Quantity
|
|
130
|
+
Wavelength of light
|
|
131
|
+
pinhole_position_spectral : float, optional
|
|
132
|
+
Position along the spectral axis as a fraction (0.0 to 1.0) of the
|
|
133
|
+
detector width. The default, None, puts every pinhole at the centre
|
|
134
|
+
of the spectral window. On a slit-scan spectrograph the spectral
|
|
135
|
+
axis is wavelength, so this sets which emission lines a pinhole
|
|
136
|
+
contaminates.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
np.ndarray
|
|
141
|
+
2D diffraction pattern normalized to a peak of 1.0, with no absolute
|
|
142
|
+
normalisation; see `airy_peak_fraction_per_pixel` when photon
|
|
143
|
+
numbers matter.
|
|
144
|
+
"""
|
|
145
|
+
n_slit, n_spectral = detector_shape
|
|
146
|
+
|
|
147
|
+
# Create coordinate grids for detector
|
|
148
|
+
slit_pixels = np.arange(n_slit)
|
|
149
|
+
spectral_pixels = np.arange(n_spectral)
|
|
150
|
+
|
|
151
|
+
# Convert pinhole position from slit fraction to pixel coordinate
|
|
152
|
+
pinhole_pixel_slit = pinhole_position_slit * (n_slit - 1)
|
|
153
|
+
|
|
154
|
+
# Pinhole position on the detector. Without a spectral position, use the
|
|
155
|
+
# centre of the spectral window.
|
|
156
|
+
if pinhole_position_spectral is None:
|
|
157
|
+
pinhole_pixel_spectral = n_spectral // 2
|
|
158
|
+
else:
|
|
159
|
+
if not 0.0 <= pinhole_position_spectral <= 1.0:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"pinhole_position_spectral is a fraction of the detector "
|
|
162
|
+
f"width and must lie in [0, 1], got "
|
|
163
|
+
f"{pinhole_position_spectral}. Out of range it would place "
|
|
164
|
+
"the pinhole off the detector, where it looks like a valid "
|
|
165
|
+
"pinhole whose light merely happens to be missing."
|
|
166
|
+
)
|
|
167
|
+
pinhole_pixel_spectral = pinhole_position_spectral * (n_spectral - 1)
|
|
168
|
+
|
|
169
|
+
# Create 2D coordinate arrays
|
|
170
|
+
slit_grid, spectral_grid = np.meshgrid(slit_pixels, spectral_pixels, indexing='ij')
|
|
171
|
+
|
|
172
|
+
# Calculate distances from pinhole center in detector plane
|
|
173
|
+
dy_pixels = slit_grid - pinhole_pixel_slit
|
|
174
|
+
dx_pixels = spectral_grid - pinhole_pixel_spectral
|
|
175
|
+
|
|
176
|
+
# Convert to physical distances
|
|
177
|
+
dy_physical = dy_pixels * pixel_size.to(u.m).value
|
|
178
|
+
dx_physical = dx_pixels * pixel_size.to(u.m).value
|
|
179
|
+
|
|
180
|
+
# Radial distance from pinhole center
|
|
181
|
+
r_physical = np.sqrt(dx_physical**2 + dy_physical**2)
|
|
182
|
+
|
|
183
|
+
# Calculate Airy disk pattern
|
|
184
|
+
pattern = airy_disk_pattern(r_physical, wavelength, pinhole_diameter, distance)
|
|
185
|
+
|
|
186
|
+
return pattern
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _pinholes_on(sim) -> bool:
|
|
190
|
+
"""Whether `sim` has pinholes enabled and at least one given."""
|
|
191
|
+
return bool(sim.enable_pinholes and len(sim.pinhole_sizes) > 0)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _filter_transmission(tel, wavelength: u.Quantity) -> np.ndarray:
|
|
195
|
+
"""
|
|
196
|
+
Share of the EUV the filter passes at each `wavelength`, for the pinholes.
|
|
197
|
+
|
|
198
|
+
The pinholes work out the light the filter blocks from the light it
|
|
199
|
+
passes, which cannot be done where it passes none. A filter that passes
|
|
200
|
+
no EUV at a wavelength its tables cover is therefore refused.
|
|
201
|
+
"""
|
|
202
|
+
wavelength = u.Quantity(wavelength)
|
|
203
|
+
passed = tel.filter.total_throughput(wavelength).to_value(u.dimensionless_unscaled)
|
|
204
|
+
opaque = np.isfinite(passed) & (passed <= 0)
|
|
205
|
+
if opaque.any():
|
|
206
|
+
where = np.broadcast_to(wavelength, passed.shape, subok=True)[opaque].to(u.AA)
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"The filter passes no EUV from {where.min():.3f} to {where.max():.3f}, so the "
|
|
209
|
+
f"light the pinholes let through there cannot be worked out from the light it "
|
|
210
|
+
f"passes. Model the pinholes with a filter that passes some EUV at every "
|
|
211
|
+
f"wavelength.")
|
|
212
|
+
return passed
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _with_filter_transmission(photon_counts: NDCube, tel) -> NDCube:
|
|
216
|
+
"""
|
|
217
|
+
`photon_counts` with the `meta` that :func:`apply_euv_pinhole_diffraction`
|
|
218
|
+
needs, for photons counted at each pixel's own wavelength: the share of
|
|
219
|
+
their light the filter passed there, and the light it blocked, at the
|
|
220
|
+
same wavelength.
|
|
221
|
+
"""
|
|
222
|
+
meta = dict(photon_counts.meta or {})
|
|
223
|
+
own = photon_counts.axis_world_coords_values(2)[0]
|
|
224
|
+
shape = photon_counts.data.shape
|
|
225
|
+
meta["photon_filter_transmission"] = np.broadcast_to(_filter_transmission(tel, own), shape)
|
|
226
|
+
meta["blocked_photon_wavelength"] = meta.get("photon_wavelength",
|
|
227
|
+
np.broadcast_to(own, shape, subok=True))
|
|
228
|
+
meta["blocked_photon_rms_wavelength"] = meta.get("photon_rms_wavelength",
|
|
229
|
+
meta["blocked_photon_wavelength"])
|
|
230
|
+
return NDCube(photon_counts.data, wcs=photon_counts.wcs, unit=photon_counts.unit, meta=meta)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def apply_euv_pinhole_diffraction(
|
|
234
|
+
photon_counts: NDCube,
|
|
235
|
+
det,
|
|
236
|
+
sim,
|
|
237
|
+
tel
|
|
238
|
+
) -> NDCube:
|
|
239
|
+
"""
|
|
240
|
+
Add the unattenuated EUV light that passes through pinholes in the filter.
|
|
241
|
+
|
|
242
|
+
At each pinhole the filtered light is replaced by the unattenuated light
|
|
243
|
+
through the pinhole, spread in its diffraction pattern. The filter is
|
|
244
|
+
behind the mirror and the grating, so this comes after the PSF. It does
|
|
245
|
+
nothing unless `sim.enable_pinholes` is set and there are pinholes.
|
|
246
|
+
|
|
247
|
+
The light the filter blocked is worked out at each photon's own
|
|
248
|
+
wavelength, from `photon_filter_transmission` in the cube's `meta`, the
|
|
249
|
+
share of the photons' light the filter passed, which `rebin_atmosphere`
|
|
250
|
+
and `rebin_spectra` set with the pinholes on. Without it, each pixel's
|
|
251
|
+
photons are taken to have passed the filter at the pixel's own
|
|
252
|
+
wavelength, as photons counted there have. The pinholes' photons have
|
|
253
|
+
the energies of the blocked light, and the wavelengths of the mean and
|
|
254
|
+
RMS photon energies in `meta` include them.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
photon_counts : NDCube
|
|
259
|
+
EUV photons in each pixel, after the filter and the PSF.
|
|
260
|
+
det : Detector_SWC
|
|
261
|
+
EUVST-SW detector, whose `filter_distance` sets the diffraction.
|
|
262
|
+
sim : Simulation
|
|
263
|
+
Simulation, with the pinholes.
|
|
264
|
+
tel : Telescope_EUVST
|
|
265
|
+
Telescope, whose filter transmission is taken out where the pinholes
|
|
266
|
+
are.
|
|
267
|
+
|
|
268
|
+
Returns
|
|
269
|
+
-------
|
|
270
|
+
NDCube
|
|
271
|
+
Photons in each pixel, including the pinholes' light.
|
|
272
|
+
"""
|
|
273
|
+
if not (sim.enable_pinholes and len(sim.pinhole_sizes) > 0):
|
|
274
|
+
return photon_counts # No pinholes enabled
|
|
275
|
+
|
|
276
|
+
# Get detector and data properties
|
|
277
|
+
data_shape = photon_counts.data.shape # (n_slit, n_scan, n_spectral)
|
|
278
|
+
n_slit, n_scan, n_spectral = data_shape
|
|
279
|
+
|
|
280
|
+
# Get rest wavelength for EUV calculations
|
|
281
|
+
rest_wavelength = photon_counts.meta['rest_wav']
|
|
282
|
+
|
|
283
|
+
# Calculate pixel area
|
|
284
|
+
pixel_area = (det.pix_size*1*u.pix)**2
|
|
285
|
+
|
|
286
|
+
# Initialize additional photon contributions
|
|
287
|
+
additional_photons = np.zeros_like(photon_counts.data)
|
|
288
|
+
|
|
289
|
+
# Light the filter blocked from each pixel's photons, at each photon's
|
|
290
|
+
# own wavelength.
|
|
291
|
+
counts = (photon_counts if "photon_filter_transmission" in (photon_counts.meta or {})
|
|
292
|
+
else _with_filter_transmission(photon_counts, tel))
|
|
293
|
+
data = np.asarray(photon_counts.data, dtype=float)
|
|
294
|
+
passed = np.asarray(counts.meta["photon_filter_transmission"], dtype=float)
|
|
295
|
+
blocked = data * np.divide(1.0 - passed, passed, out=np.zeros(data.shape), where=passed > 0)
|
|
296
|
+
|
|
297
|
+
# Spectral positions are optional, as in the visible path: without them
|
|
298
|
+
# every pinhole projects to the centre of the spectral window.
|
|
299
|
+
spectral_positions = (list(sim.pinhole_positions_spectral)
|
|
300
|
+
or [None] * len(sim.pinhole_sizes))
|
|
301
|
+
|
|
302
|
+
for pinhole_diameter, pinhole_position, pinhole_spectral in zip(
|
|
303
|
+
sim.pinhole_sizes, sim.pinhole_positions, spectral_positions):
|
|
304
|
+
# Calculate pinhole area
|
|
305
|
+
pinhole_area = np.pi * (pinhole_diameter / 2)**2
|
|
306
|
+
|
|
307
|
+
# photon_counts already have the filter attenuation, so:
|
|
308
|
+
# 1. Work out the unfiltered signal.
|
|
309
|
+
# 2. Apply the pinhole diffraction to it.
|
|
310
|
+
# 3. Subtract the filtered signal already counted in the pinhole regions.
|
|
311
|
+
|
|
312
|
+
area_ratio = (pinhole_area / pixel_area).to(u.dimensionless_unscaled).value
|
|
313
|
+
|
|
314
|
+
# Calculate theoretical diffraction size for validation
|
|
315
|
+
# First Airy minimum: r = 1.22 * lambda * distance / diameter
|
|
316
|
+
theoretical_radius = (1.22 * rest_wavelength * det.filter_distance / pinhole_diameter).to(u.m)
|
|
317
|
+
theoretical_radius_pixels = (theoretical_radius / (det.pix_size*1*u.pix)).to(u.dimensionless_unscaled).value
|
|
318
|
+
|
|
319
|
+
# Calculate EUV diffraction pattern
|
|
320
|
+
euv_pattern = calculate_pinhole_diffraction_pattern(
|
|
321
|
+
detector_shape=(n_slit, n_spectral),
|
|
322
|
+
pixel_size=det.pix_size*u.pix,
|
|
323
|
+
pinhole_diameter=pinhole_diameter,
|
|
324
|
+
pinhole_position_slit=pinhole_position,
|
|
325
|
+
slit_width=sim.slit_width,
|
|
326
|
+
plate_scale=det.plate_scale_angle,
|
|
327
|
+
distance=det.filter_distance,
|
|
328
|
+
wavelength=rest_wavelength,
|
|
329
|
+
pinhole_position_spectral=pinhole_spectral,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Scale the pattern by its absolute normalisation, as the visible path
|
|
333
|
+
# does. Dividing by its sum over the detector would force every photon
|
|
334
|
+
# through the pinhole onto the detector, whatever the geometry. The EUV
|
|
335
|
+
# Airy pattern is about thirty times smaller than the visible one at
|
|
336
|
+
# the same diameter, but a 1 micron hole at 195 Angstrom still has its
|
|
337
|
+
# first minimum 5.95 mm out, against a detector a few mm across, so
|
|
338
|
+
# most of its light misses and must be lost, not redistributed.
|
|
339
|
+
# euv_pattern peaks at 1.0, so multiplying by the peak per-pixel
|
|
340
|
+
# fraction gives the right fraction everywhere, summing to less than 1
|
|
341
|
+
# when light falls off the detector.
|
|
342
|
+
peak_fraction = airy_peak_fraction_per_pixel(
|
|
343
|
+
pinhole_diameter=pinhole_diameter,
|
|
344
|
+
distance=det.filter_distance,
|
|
345
|
+
wavelength=rest_wavelength,
|
|
346
|
+
pixel_size=det.pix_size * u.pix,
|
|
347
|
+
)
|
|
348
|
+
euv_pattern_normalized = euv_pattern * peak_fraction
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# Process each scan position
|
|
352
|
+
for i in range(n_scan):
|
|
353
|
+
# Current filtered signal at this scan position
|
|
354
|
+
filtered_signal = photon_counts.data[:, i, :] # Shape: (n_slit, n_spectral)
|
|
355
|
+
|
|
356
|
+
# Unfiltered signal: the filtered signal plus the light the filter
|
|
357
|
+
# blocked from it.
|
|
358
|
+
unfiltered_signal = filtered_signal + blocked[:, i, :]
|
|
359
|
+
|
|
360
|
+
# Unattenuated light through the pinhole. unfiltered_signal *
|
|
361
|
+
# area_ratio is the light collected over the pinhole's area, and
|
|
362
|
+
# the scaled pattern gives the fraction of it reaching each pixel,
|
|
363
|
+
# which need not sum to 1.
|
|
364
|
+
pinhole_signal = unfiltered_signal * area_ratio * euv_pattern_normalized
|
|
365
|
+
|
|
366
|
+
# The filtered light already counted in the pinhole regions
|
|
367
|
+
# (filtered signal weighted by diffraction pattern and area ratio)
|
|
368
|
+
overcounted_filtered = filtered_signal * area_ratio * euv_pattern_normalized
|
|
369
|
+
|
|
370
|
+
# Net extra light from the pinhole: its total light less the
|
|
371
|
+
# filtered light already counted there, which is
|
|
372
|
+
# blocked * area_ratio * pattern.
|
|
373
|
+
correction = (pinhole_signal - overcounted_filtered)
|
|
374
|
+
additional_photons[:, i, :] += correction
|
|
375
|
+
|
|
376
|
+
# Create new photon counts with EUV pinhole contributions
|
|
377
|
+
new_data = photon_counts.data + additional_photons
|
|
378
|
+
|
|
379
|
+
# The pinholes' photons have the energies of the blocked light. The
|
|
380
|
+
# filter entries in meta no longer hold once they are added, so drop them.
|
|
381
|
+
meta = {key: value for key, value in counts.meta.items() if key not in _FILTER_KEYS}
|
|
382
|
+
if "photon_wavelength" in meta:
|
|
383
|
+
mean = u.Quantity(meta["photon_wavelength"])
|
|
384
|
+
unit = mean.unit
|
|
385
|
+
rms = u.Quantity(meta.get("photon_rms_wavelength", mean)).to_value(unit)
|
|
386
|
+
blocked_mean = u.Quantity(counts.meta["blocked_photon_wavelength"]).to_value(unit)
|
|
387
|
+
blocked_rms = u.Quantity(counts.meta["blocked_photon_rms_wavelength"]).to_value(unit)
|
|
388
|
+
|
|
389
|
+
def over(photons, wavelength):
|
|
390
|
+
return np.divide(photons, wavelength, out=np.zeros(data.shape), where=wavelength > 0)
|
|
391
|
+
|
|
392
|
+
energy = over(data, mean.value) + over(additional_photons, blocked_mean)
|
|
393
|
+
square = over(data, rms**2) + over(additional_photons, blocked_rms**2)
|
|
394
|
+
meta["photon_wavelength"] = np.divide(new_data, energy, out=np.array(mean.value, dtype=float),
|
|
395
|
+
where=energy > 0) * unit
|
|
396
|
+
meta["photon_rms_wavelength"] = np.sqrt(np.divide(
|
|
397
|
+
new_data, square, out=np.array(rms, dtype=float) ** 2, where=square > 0)) * unit
|
|
398
|
+
|
|
399
|
+
return NDCube(
|
|
400
|
+
data=new_data,
|
|
401
|
+
wcs=photon_counts.wcs.deepcopy(),
|
|
402
|
+
unit=photon_counts.unit,
|
|
403
|
+
meta=meta,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# Filter entries in a cube's meta, which the pinholes need.
|
|
408
|
+
_FILTER_KEYS = ("photon_filter_transmission", "blocked_photon_wavelength",
|
|
409
|
+
"blocked_photon_rms_wavelength")
|