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
eclipse/sampling.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Laying a scene onto detector pixels.
|
|
3
|
+
|
|
4
|
+
A pixel records the mean over its extent of the cells' radiance, each cell
|
|
5
|
+
uniform, blurred by the optics. The weight of cell b in pixel p is
|
|
6
|
+
|
|
7
|
+
W[p, b] = (1 / |p|) int_p dx int_b dx' h(x - x')
|
|
8
|
+
|
|
9
|
+
for blur h, along each axis in turn. For a Gaussian blur, alone or
|
|
10
|
+
convolved with a rectangle such as a slit's image, these integrals have
|
|
11
|
+
closed forms, so the weights are exact for any cell, pixel and blur sizes.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from scipy import sparse
|
|
17
|
+
from scipy.special import erfc
|
|
18
|
+
|
|
19
|
+
# A Gaussian's weight beyond this many sigma is below 1e-30 of its total.
|
|
20
|
+
_REACH_IN_SIGMA = 12.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _antiderivative(u, sigma, order: int) -> np.ndarray:
|
|
24
|
+
"""
|
|
25
|
+
Antiderivative of order `order` of a unit Gaussian of standard deviation
|
|
26
|
+
`sigma`, zero at minus infinity.
|
|
27
|
+
|
|
28
|
+
`order` is 1 (the cumulative distribution), 2 or 3. Where `sigma` is 0
|
|
29
|
+
the Gaussian is a unit spike. `sigma` is one value or one per element of
|
|
30
|
+
`u`.
|
|
31
|
+
"""
|
|
32
|
+
u = np.asarray(u, dtype=float)
|
|
33
|
+
sigma = np.broadcast_to(np.asarray(sigma, dtype=float), u.shape)
|
|
34
|
+
spike = sigma == 0.0
|
|
35
|
+
s = np.where(spike, 1.0, sigma)
|
|
36
|
+
cdf = 0.5 * erfc(-u / (np.sqrt(2.0) * s))
|
|
37
|
+
if order == 1:
|
|
38
|
+
smooth = cdf
|
|
39
|
+
sharp = np.where(u > 0, 1.0, np.where(u == 0, 0.5, 0.0))
|
|
40
|
+
else:
|
|
41
|
+
density = np.exp(-0.5 * (u / s) ** 2) / (np.sqrt(2.0 * np.pi) * s)
|
|
42
|
+
positive = np.maximum(u, 0.0)
|
|
43
|
+
if order == 2:
|
|
44
|
+
smooth = u * cdf + s**2 * density
|
|
45
|
+
sharp = positive
|
|
46
|
+
else:
|
|
47
|
+
smooth = (u**2 + s**2) / 2 * cdf + s**2 * u / 2 * density
|
|
48
|
+
sharp = positive**2 / 2
|
|
49
|
+
return np.where(spike, sharp, smooth) if spike.any() else smooth
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _blurred_antiderivative(u, sigma, width, order: int) -> np.ndarray:
|
|
53
|
+
"""
|
|
54
|
+
:func:`_antiderivative` of the Gaussian convolved with a rectangle
|
|
55
|
+
`width` wide, such as a slit's image, where `width` is above 0. `sigma`
|
|
56
|
+
and `width` are each one value or one per element of `u`.
|
|
57
|
+
"""
|
|
58
|
+
u = np.asarray(u, dtype=float)
|
|
59
|
+
width = np.broadcast_to(np.asarray(width, dtype=float), u.shape)
|
|
60
|
+
plain = _antiderivative(u, sigma, order)
|
|
61
|
+
boxed = width > 0
|
|
62
|
+
if not boxed.any():
|
|
63
|
+
return plain
|
|
64
|
+
w = np.where(boxed, width, 1.0)
|
|
65
|
+
within = (_antiderivative(u + w / 2, sigma, order + 1)
|
|
66
|
+
- _antiderivative(u - w / 2, sigma, order + 1)) / w
|
|
67
|
+
return np.where(boxed, within, plain)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def light_onto_pixels(cell_low, cell_high, pixel_edges, sigma=0.0,
|
|
71
|
+
width=0.0) -> sparse.csr_matrix:
|
|
72
|
+
"""
|
|
73
|
+
Share of each cell's blurred light recorded by each pixel.
|
|
74
|
+
|
|
75
|
+
Each cell's light is spread evenly from `cell_low` to `cell_high` and
|
|
76
|
+
blurred by a Gaussian of `sigma` convolved with a rectangle `width`
|
|
77
|
+
wide. The shares are exact integrals of the blur over the cell and the
|
|
78
|
+
pixel, computed only for the pixels the blur reaches from each cell.
|
|
79
|
+
Cells may overlap and leave gaps; pixels are contiguous.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
cell_low, cell_high : array
|
|
84
|
+
Ends of each cell, in one unit.
|
|
85
|
+
pixel_edges : array
|
|
86
|
+
Pixel edges, increasing, in the same unit.
|
|
87
|
+
sigma, width : float or array
|
|
88
|
+
Blur, one value or one per cell, in the same unit; 0 for none.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
scipy.sparse.csr_matrix
|
|
93
|
+
Shape `(n_pixels, n_cells)`. The shares of a cell whose light all
|
|
94
|
+
lands within the pixels add up to 1.
|
|
95
|
+
"""
|
|
96
|
+
low = np.atleast_1d(np.asarray(cell_low, dtype=float))
|
|
97
|
+
high = np.atleast_1d(np.asarray(cell_high, dtype=float))
|
|
98
|
+
edges = np.asarray(pixel_edges, dtype=float)
|
|
99
|
+
sigma = np.broadcast_to(np.asarray(sigma, dtype=float), low.shape)
|
|
100
|
+
width = np.broadcast_to(np.asarray(width, dtype=float), low.shape)
|
|
101
|
+
reach = _REACH_IN_SIGMA * sigma + width
|
|
102
|
+
# Pixels each cell's blurred light reaches: from the first pixel ending
|
|
103
|
+
# above where it starts, to just before the first starting above where
|
|
104
|
+
# it ends.
|
|
105
|
+
first = np.searchsorted(edges[1:], low - reach, side="right")
|
|
106
|
+
last = np.searchsorted(edges[:-1], high + reach, side="left")
|
|
107
|
+
count = np.maximum(last - first, 0)
|
|
108
|
+
cell = np.repeat(np.arange(low.size), count)
|
|
109
|
+
pixel = first[cell] + np.arange(count.sum()) - np.repeat(np.cumsum(count) - count, count)
|
|
110
|
+
p_low, p_high = edges[pixel], edges[pixel + 1]
|
|
111
|
+
b_low, b_high = low[cell], high[cell]
|
|
112
|
+
s, w = sigma[cell], width[cell]
|
|
113
|
+
|
|
114
|
+
def corner(p, b):
|
|
115
|
+
return _blurred_antiderivative(p - b, s, w, 2)
|
|
116
|
+
|
|
117
|
+
total = (corner(p_high, b_low) - corner(p_high, b_high)
|
|
118
|
+
- corner(p_low, b_low) + corner(p_low, b_high))
|
|
119
|
+
# The closed forms cancel to rounding error where the true share is 0.
|
|
120
|
+
share = np.maximum(total, 0.0) / (b_high - b_low)
|
|
121
|
+
return sparse.csr_matrix((share, (pixel, cell)), shape=(edges.size - 1, low.size))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def points_onto_pixels(position, pixel_edges, sigma, width=0.0) -> sparse.csr_matrix:
|
|
125
|
+
"""
|
|
126
|
+
Share of each point's blurred light recorded by each pixel.
|
|
127
|
+
|
|
128
|
+
This is :func:`light_onto_pixels` for light all at one place, such as a
|
|
129
|
+
line at its centre blurred by its own width.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
position : array
|
|
134
|
+
Position of each point, in one unit.
|
|
135
|
+
pixel_edges : array
|
|
136
|
+
Pixel edges, increasing, in the same unit.
|
|
137
|
+
sigma, width : float or array
|
|
138
|
+
Blur, a Gaussian of `sigma` convolved with a rectangle `width` wide,
|
|
139
|
+
one value or one per point, in the same unit. `sigma` is above 0
|
|
140
|
+
where `width` is 0.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
scipy.sparse.csr_matrix
|
|
145
|
+
Shape `(n_pixels, n_points)`.
|
|
146
|
+
"""
|
|
147
|
+
position = np.atleast_1d(np.asarray(position, dtype=float))
|
|
148
|
+
edges = np.asarray(pixel_edges, dtype=float)
|
|
149
|
+
sigma = np.broadcast_to(np.asarray(sigma, dtype=float), position.shape)
|
|
150
|
+
width = np.broadcast_to(np.asarray(width, dtype=float), position.shape)
|
|
151
|
+
reach = _REACH_IN_SIGMA * sigma + width
|
|
152
|
+
first = np.searchsorted(edges[1:], position - reach, side="right")
|
|
153
|
+
last = np.searchsorted(edges[:-1], position + reach, side="left")
|
|
154
|
+
count = np.maximum(last - first, 0)
|
|
155
|
+
point = np.repeat(np.arange(position.size), count)
|
|
156
|
+
pixel = first[point] + np.arange(count.sum()) - np.repeat(np.cumsum(count) - count, count)
|
|
157
|
+
x, s, w = position[point], sigma[point], width[point]
|
|
158
|
+
share = (_blurred_antiderivative(edges[pixel + 1] - x, s, w, 1)
|
|
159
|
+
- _blurred_antiderivative(edges[pixel] - x, s, w, 1))
|
|
160
|
+
return sparse.csr_matrix((np.maximum(share, 0.0), (pixel, point)),
|
|
161
|
+
shape=(edges.size - 1, position.size))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def photons_onto_pixels(share: sparse.spmatrix, photons: np.ndarray,
|
|
165
|
+
wavelength: np.ndarray) -> tuple:
|
|
166
|
+
"""
|
|
167
|
+
Photons laid onto pixels, with the wavelengths of their mean and root mean square energies.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
share : scipy.sparse matrix
|
|
172
|
+
Each cell's share in each pixel, from :func:`light_onto_pixels`.
|
|
173
|
+
photons : np.ndarray
|
|
174
|
+
Photons in each cell, any number of spectra with the cells on the
|
|
175
|
+
last axis.
|
|
176
|
+
wavelength : np.ndarray
|
|
177
|
+
Wavelength of each cell's photons.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
tuple
|
|
182
|
+
`(on_pixels, photon_wavelength, rms_wavelength)`, all with the pixels
|
|
183
|
+
on the last axis. A photon frees electrons in a detector by its
|
|
184
|
+
energy, so `photon_wavelength` is the wavelength of the mean energy
|
|
185
|
+
of the photons a pixel receives, and `rms_wavelength` that of their
|
|
186
|
+
root mean square energy, which sets how far their electrons spread.
|
|
187
|
+
Both are in the unit of `wavelength`, and NaN where no photons
|
|
188
|
+
arrive, or only photons below the smallest normal float.
|
|
189
|
+
"""
|
|
190
|
+
photons = np.asarray(photons, dtype=float)
|
|
191
|
+
flat = photons.reshape(-1, photons.shape[-1])
|
|
192
|
+
wavelength = np.asarray(wavelength, dtype=float)
|
|
193
|
+
on, photon_wavelength, rms_wavelength = _carry_photon_wavelengths(
|
|
194
|
+
lambda cells: np.asarray(share @ cells.T).T, flat, wavelength, wavelength, np.nan)
|
|
195
|
+
shape = photons.shape[:-1] + (share.shape[0],)
|
|
196
|
+
return on.reshape(shape), photon_wavelength.reshape(shape), rms_wavelength.reshape(shape)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _carry_photon_wavelengths(carry, photons, mean, rms, fill) -> tuple:
|
|
200
|
+
"""
|
|
201
|
+
Photons moved by `carry`, with the wavelengths of the mean and root mean
|
|
202
|
+
square energies of the photons where they land.
|
|
203
|
+
|
|
204
|
+
A photon's energy goes as one over its wavelength. Photons whose mean
|
|
205
|
+
energy is that of wavelength `mean`, and whose root mean square energy
|
|
206
|
+
is that of `rms`, hold an energy that goes as photons / mean and a
|
|
207
|
+
squared energy that goes as photons / rms**2. Both add up like the
|
|
208
|
+
photons, so any linear map `carry` moves them as it moves the photons.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
carry : callable
|
|
213
|
+
Linear map from an array shaped as `photons` to where they land.
|
|
214
|
+
photons : np.ndarray
|
|
215
|
+
Photons.
|
|
216
|
+
mean, rms : np.ndarray
|
|
217
|
+
Wavelengths of the mean and root mean square energies of `photons`,
|
|
218
|
+
broadcasting against them.
|
|
219
|
+
fill : float or np.ndarray
|
|
220
|
+
Wavelength for both where no photons land, broadcasting against
|
|
221
|
+
where they land. Also used where the photons that land are below
|
|
222
|
+
the smallest normal float, as their ratios then have too few
|
|
223
|
+
significant bits to mean anything.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
tuple
|
|
228
|
+
`(moved, mean, rms)`.
|
|
229
|
+
"""
|
|
230
|
+
photons = np.asarray(photons, dtype=float)
|
|
231
|
+
mean = np.broadcast_to(np.asarray(mean, dtype=float), photons.shape)
|
|
232
|
+
rms = np.broadcast_to(np.asarray(rms, dtype=float), photons.shape)
|
|
233
|
+
# The wavelengths are taken relative to the largest, so the energies
|
|
234
|
+
# carried are about the size of the photons. Divided by wavelengths in
|
|
235
|
+
# cm they would be up to 1e12 times larger, and could stay above zero
|
|
236
|
+
# where the photons round to zero.
|
|
237
|
+
scale = np.max(mean, initial=0.0, where=np.isfinite(mean))
|
|
238
|
+
scale = scale if scale > 0 else 1.0
|
|
239
|
+
moved = carry(photons)
|
|
240
|
+
energy = carry(photons * np.divide(scale, mean, out=np.zeros(photons.shape), where=mean > 0))
|
|
241
|
+
square = carry(photons * np.divide(scale, rms, out=np.zeros(photons.shape), where=rms > 0)**2)
|
|
242
|
+
empty = np.broadcast_to(np.asarray(fill, dtype=float), moved.shape)
|
|
243
|
+
tiny = np.finfo(float).tiny
|
|
244
|
+
with_mean = (moved >= tiny) & (energy >= tiny)
|
|
245
|
+
with_rms = (moved >= tiny) & (square >= tiny)
|
|
246
|
+
ratio = np.divide(moved, energy, out=np.ones(moved.shape), where=with_mean)
|
|
247
|
+
moved_mean = np.where(with_mean, scale * ratio, empty)
|
|
248
|
+
ratio = np.divide(moved, square, out=np.ones(moved.shape), where=with_rms)
|
|
249
|
+
moved_rms = np.where(with_rms, scale * np.sqrt(ratio), empty)
|
|
250
|
+
return moved, moved_mean, moved_rms
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def pixel_weights(cell_edges, pixel_edges, sigma: float = 0.0, width: float = 0.0,
|
|
254
|
+
extend: bool = False) -> np.ndarray:
|
|
255
|
+
"""
|
|
256
|
+
Weight of each cell in each pixel: the mean over the pixel of the cell's
|
|
257
|
+
light, uniform over the cell, after blurring.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
cell_edges, pixel_edges : array
|
|
262
|
+
Cell and pixel edges along one axis, increasing, in one unit.
|
|
263
|
+
sigma : float
|
|
264
|
+
Standard deviation of the Gaussian blur, in the same unit; 0 for
|
|
265
|
+
none.
|
|
266
|
+
width : float
|
|
267
|
+
Width of a rectangle convolved with the Gaussian, such as a slit's
|
|
268
|
+
image, in the same unit; 0 for none.
|
|
269
|
+
extend : bool
|
|
270
|
+
Extend the first and last cells outward as far as the blur reaches,
|
|
271
|
+
for a scene that continues past its edges.
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
np.ndarray
|
|
276
|
+
Shape `(n_pixels, n_cells)`. With no blur each row is the fraction
|
|
277
|
+
of the pixel each cell covers.
|
|
278
|
+
"""
|
|
279
|
+
cells = np.asarray(cell_edges, dtype=float)
|
|
280
|
+
pixels = np.asarray(pixel_edges, dtype=float)
|
|
281
|
+
reach = _REACH_IN_SIGMA * sigma + width
|
|
282
|
+
if extend and reach > 0:
|
|
283
|
+
beyond = reach + (pixels[-1] - pixels[0]) + (cells[-1] - cells[0])
|
|
284
|
+
cells = cells.copy()
|
|
285
|
+
cells[0] = min(cells[0], pixels[0]) - beyond
|
|
286
|
+
cells[-1] = max(cells[-1], pixels[-1]) + beyond
|
|
287
|
+
|
|
288
|
+
# Each cell's share of its light in a pixel, turned into the mean over
|
|
289
|
+
# the pixel of the cell's light per unit length.
|
|
290
|
+
share = light_onto_pixels(cells[:-1], cells[1:], pixels, sigma, width)
|
|
291
|
+
weights = sparse.diags(1.0 / np.diff(pixels)) @ share @ sparse.diags(np.diff(cells))
|
|
292
|
+
return weights.toarray()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def pixel_weights_through(cell_edges, pixel_edges, openings, sigma: float = 0.0,
|
|
296
|
+
extend: bool = False) -> np.ndarray:
|
|
297
|
+
"""
|
|
298
|
+
Pixel weights for light passing only through `openings`.
|
|
299
|
+
|
|
300
|
+
As :func:`pixel_weights`, for a slit with gaps in it. The blur comes before the openings,
|
|
301
|
+
as the optics in front of a slit blur the image the slit then cuts. Each
|
|
302
|
+
pixel still holds the mean over its whole extent, so a pixel half
|
|
303
|
+
covered by a gap holds half the light, and a pixel inside a gap none.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
cell_edges, pixel_edges, sigma, extend
|
|
308
|
+
As for :func:`pixel_weights`.
|
|
309
|
+
openings : array
|
|
310
|
+
`(n, 2)` start and end of each opening, in the unit of the edges,
|
|
311
|
+
increasing and not overlapping.
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
np.ndarray
|
|
316
|
+
Weights, `(n_pixels, n_cells)`.
|
|
317
|
+
"""
|
|
318
|
+
pixels = np.asarray(pixel_edges, dtype=float)
|
|
319
|
+
openings = np.asarray(openings, dtype=float).reshape(-1, 2)
|
|
320
|
+
if np.any(openings[:, 1] <= openings[:, 0]) or np.any(openings[1:, 0] < openings[:-1, 1]):
|
|
321
|
+
raise ValueError("The openings must each end after they start, in increasing "
|
|
322
|
+
"order, without overlapping.")
|
|
323
|
+
# Cut the pixels at every opening edge inside them, so each piece is wholly
|
|
324
|
+
# open or closed, then average the open pieces' weights over their pixel.
|
|
325
|
+
cuts = openings[(openings > pixels[0]) & (openings < pixels[-1])]
|
|
326
|
+
pieces = np.union1d(pixels, cuts)
|
|
327
|
+
middles = (pieces[:-1] + pieces[1:]) / 2
|
|
328
|
+
is_open = np.any((middles[:, np.newaxis] > openings[:, 0])
|
|
329
|
+
& (middles[:, np.newaxis] < openings[:, 1]), axis=1)
|
|
330
|
+
weights = pixel_weights(cell_edges, pieces, sigma, extend=extend)
|
|
331
|
+
pixel = np.searchsorted(pixels, middles, side="right") - 1
|
|
332
|
+
out = np.zeros((pixels.size - 1, weights.shape[1]))
|
|
333
|
+
np.add.at(out, pixel, weights * (np.diff(pieces) * is_open)[:, np.newaxis])
|
|
334
|
+
return out / np.diff(pixels)[:, np.newaxis]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def centred_edges(n: int, pitch: float, centre: float = 0.0) -> np.ndarray:
|
|
338
|
+
"""Edges of `n` pixels of width `pitch`, centred on `centre`."""
|
|
339
|
+
return centre + (np.arange(n + 1) - n / 2) * pitch
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def onto_pixels(data: np.ndarray, rows: np.ndarray, columns: np.ndarray,
|
|
343
|
+
wavelengths: np.ndarray) -> np.ndarray:
|
|
344
|
+
"""
|
|
345
|
+
`data`, shaped `(y, x, wavelength)`, laid onto pixels by the weights
|
|
346
|
+
along each axis from :func:`pixel_weights`.
|
|
347
|
+
"""
|
|
348
|
+
out = np.tensordot(data, wavelengths, axes=([2], [1])) # (y, x, l)
|
|
349
|
+
out = np.tensordot(out, columns, axes=([1], [1])) # (y, l, q)
|
|
350
|
+
out = np.tensordot(rows, out, axes=([1], [0])) # (p, l, q)
|
|
351
|
+
return np.ascontiguousarray(np.moveaxis(out, 2, 1)) # (p, q, l)
|