optixstuff 2.0.0__tar.gz → 3.0.0__tar.gz
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.
- {optixstuff-2.0.0 → optixstuff-3.0.0}/CHANGELOG.md +11 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/PKG-INFO +2 -1
- {optixstuff-2.0.0 → optixstuff-3.0.0}/pyproject.toml +1 -1
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/__init__.py +8 -1
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/_version.py +2 -2
- optixstuff-3.0.0/src/optixstuff/coronagraph.py +753 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/yippy_coronagraph.py +11 -8
- optixstuff-2.0.0/src/optixstuff/coronagraph.py +0 -193
- {optixstuff-2.0.0 → optixstuff-3.0.0}/.gitignore +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/.pre-commit-config.yaml +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/.readthedocs.yaml +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/.zenodo.json +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/CITATION.cff +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/LICENSE +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/README.md +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/_repr.py +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/detector.py +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/disperser.py +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/exposure.py +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/optical_elements.py +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/optical_path.py +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/primary.py +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/src/optixstuff/speckle.py +0 -0
- {optixstuff-2.0.0 → optixstuff-3.0.0}/tools/check_internal_refs.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.0.0](https://github.com/CoreySpohn/optixstuff/compare/v2.0.0...v3.0.0) (2026-07-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **coronagraph:** sampling-explicit image contract -- wavelength-explicit stellar_map/source_psfs/background_transmission/extended_scene, table SPI base, MultiBandCoronagraph
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **coronagraph:** sampling-explicit image contract -- wavelength-explicit stellar_map/source_psfs/background_transmission/extended_scene, table SPI base, MultiBandCoronagraph ([76c578b](https://github.com/CoreySpohn/optixstuff/commit/76c578be46ee79281dbb24ef44e3af4708fd7155))
|
|
13
|
+
|
|
3
14
|
## [2.0.0](https://github.com/CoreySpohn/optixstuff/compare/v1.5.0...v2.0.0) (2026-07-10)
|
|
4
15
|
|
|
5
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optixstuff
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Hardware abstractions for the HWO direct imaging simulation suite
|
|
5
5
|
Project-URL: Homepage, https://github.com/CoreySpohn/optixstuff
|
|
6
6
|
Project-URL: Issues, https://github.com/CoreySpohn/optixstuff/issues
|
|
@@ -34,6 +34,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
34
34
|
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
35
35
|
Requires-Python: >=3.11
|
|
36
36
|
Requires-Dist: equinox>=0.12.0
|
|
37
|
+
Requires-Dist: hwoutils
|
|
37
38
|
Requires-Dist: interpax
|
|
38
39
|
Requires-Dist: jax>=0.4.1
|
|
39
40
|
Requires-Dist: jaxlib>=0.4.1
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
17
17
|
"Topic :: Scientific/Engineering :: Astronomy",
|
|
18
18
|
]
|
|
19
|
-
dependencies = ["equinox>=0.12.0", "interpax", "jax>=0.4.1", "jaxlib>=0.4.1", "yippy"]
|
|
19
|
+
dependencies = ["equinox>=0.12.0", "hwoutils", "interpax", "jax>=0.4.1", "jaxlib>=0.4.1", "yippy"]
|
|
20
20
|
|
|
21
21
|
[project.optional-dependencies]
|
|
22
22
|
dev = ["pre-commit"]
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"""optixstuff -- Hardware abstractions for the HWO simulation suite."""
|
|
2
2
|
|
|
3
3
|
from optixstuff._version import __version__
|
|
4
|
-
from optixstuff.coronagraph import
|
|
4
|
+
from optixstuff.coronagraph import (
|
|
5
|
+
AbstractCoronagraph,
|
|
6
|
+
AbstractScalarCoronagraph,
|
|
7
|
+
AbstractTableCoronagraph,
|
|
8
|
+
MultiBandCoronagraph,
|
|
9
|
+
)
|
|
5
10
|
from optixstuff.detector import (
|
|
6
11
|
AbstractDetector,
|
|
7
12
|
Detector,
|
|
@@ -31,12 +36,14 @@ __all__ = [
|
|
|
31
36
|
"AbstractPrimary",
|
|
32
37
|
"AbstractScalarCoronagraph",
|
|
33
38
|
"AbstractSpeckleField",
|
|
39
|
+
"AbstractTableCoronagraph",
|
|
34
40
|
"AbstractUniformElement",
|
|
35
41
|
"ConstantThroughput",
|
|
36
42
|
"Detector",
|
|
37
43
|
"ExposureConfig",
|
|
38
44
|
"IdealDetector",
|
|
39
45
|
"LensletDisperser",
|
|
46
|
+
"MultiBandCoronagraph",
|
|
40
47
|
"OpticalPath",
|
|
41
48
|
"SegmentedPrimary",
|
|
42
49
|
"SimplePrimary",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '
|
|
22
|
-
__version_tuple__ = version_tuple = (
|
|
21
|
+
__version__ = version = '3.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 0, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
"""Coronagraph abstractions."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
|
|
5
|
+
import equinox as eqx
|
|
6
|
+
import jax
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
from equinox import AbstractVar
|
|
9
|
+
from hwoutils.transforms import resample_flux
|
|
10
|
+
from jax.typing import ArrayLike
|
|
11
|
+
from jaxtyping import Array
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _convolve_quadrants(flux, psf_datacube):
|
|
15
|
+
"""Convolve flux with a quarter-symmetric PSF datacube via fold-and-sum.
|
|
16
|
+
|
|
17
|
+
Handles padding dynamically to ensure all quadrants match the shape of the
|
|
18
|
+
first quadrant (which defines the PSF datacube shape).
|
|
19
|
+
"""
|
|
20
|
+
ny, nx = flux.shape
|
|
21
|
+
cy, cx = (ny - 1) // 2, (nx - 1) // 2
|
|
22
|
+
|
|
23
|
+
# Q1: top-right (includes center pixel and axes) -> reference shape
|
|
24
|
+
q1 = flux[cy:, cx:]
|
|
25
|
+
target_h, target_w = q1.shape
|
|
26
|
+
|
|
27
|
+
# Q2: top-left -- flip X, pad inner-left + outer-right to target width
|
|
28
|
+
q2_raw = flux[cy:, :cx]
|
|
29
|
+
q2_flipped = q2_raw[:, ::-1]
|
|
30
|
+
pad_q2_right = max(0, target_w - (q2_flipped.shape[1] + 1))
|
|
31
|
+
q2 = jnp.pad(q2_flipped, ((0, 0), (1, pad_q2_right)))
|
|
32
|
+
|
|
33
|
+
# Q3: bottom-left -- flip both, pad inner-top, inner-left, outer-bottom/right
|
|
34
|
+
q3_raw = flux[:cy, :cx]
|
|
35
|
+
q3_flipped = q3_raw[::-1, ::-1]
|
|
36
|
+
pad_q3_bottom = max(0, target_h - (q3_flipped.shape[0] + 1))
|
|
37
|
+
pad_q3_right = max(0, target_w - (q3_flipped.shape[1] + 1))
|
|
38
|
+
q3 = jnp.pad(q3_flipped, ((1, pad_q3_bottom), (1, pad_q3_right)))
|
|
39
|
+
|
|
40
|
+
# Q4: bottom-right -- flip Y, pad inner-top + outer-bottom
|
|
41
|
+
q4_raw = flux[:cy, cx:]
|
|
42
|
+
q4_flipped = q4_raw[::-1, :]
|
|
43
|
+
pad_q4_bottom = max(0, target_h - (q4_flipped.shape[0] + 1))
|
|
44
|
+
q4 = jnp.pad(q4_flipped, ((1, pad_q4_bottom), (0, 0)))
|
|
45
|
+
|
|
46
|
+
flux_stack = jnp.stack([q1, q2, q3, q4])
|
|
47
|
+
partial_images = jnp.einsum("qij,ijxy->qxy", flux_stack, psf_datacube)
|
|
48
|
+
|
|
49
|
+
img_q1 = partial_images[0]
|
|
50
|
+
img_q2 = jnp.fliplr(partial_images[1])
|
|
51
|
+
img_q3 = jnp.flipud(jnp.fliplr(partial_images[2]))
|
|
52
|
+
img_q4 = jnp.flipud(partial_images[3])
|
|
53
|
+
|
|
54
|
+
return img_q1 + img_q2 + img_q3 + img_q4
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AbstractCoronagraph(eqx.Module):
|
|
58
|
+
"""Abstract interface for coronagraph performance models.
|
|
59
|
+
|
|
60
|
+
Provides scalar performance curves (for ETC use), single-PSF
|
|
61
|
+
generation, and a sampling-explicit image contract (for image
|
|
62
|
+
simulation): ``stellar_map`` / ``source_psfs`` /
|
|
63
|
+
``background_transmission`` / ``extended_scene`` serve maps at the
|
|
64
|
+
caller's requested pixel scale and shape, so backend-internal grids
|
|
65
|
+
never leak downstream. Implementations can be backed by pre-computed
|
|
66
|
+
interpolation tables (subclass :class:`AbstractTableCoronagraph`,
|
|
67
|
+
which serves the image contract from native-grid tables), physical
|
|
68
|
+
wavefront propagation, or analytical models.
|
|
69
|
+
|
|
70
|
+
All wavelength arguments are in nanometres throughout.
|
|
71
|
+
All separations and pixel scales are in lambda/D units -- the
|
|
72
|
+
contract is dimensionless optics; angle conversions are the
|
|
73
|
+
telescope primary's business.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
pixel_scale_lod: AbstractVar[float]
|
|
77
|
+
"""Native pixel scale in lambda/D per pixel."""
|
|
78
|
+
|
|
79
|
+
IWA: AbstractVar[float]
|
|
80
|
+
"""Inner working angle in lambda/D."""
|
|
81
|
+
|
|
82
|
+
OWA: AbstractVar[float]
|
|
83
|
+
"""Outer working angle in lambda/D."""
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Scalar interface -- consumed by jaxEDITH and yield estimators
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
@abc.abstractmethod
|
|
90
|
+
def throughput(
|
|
91
|
+
self,
|
|
92
|
+
separation_lod: ArrayLike,
|
|
93
|
+
wavelength_nm: ArrayLike,
|
|
94
|
+
*,
|
|
95
|
+
time_s: ArrayLike = 0.0,
|
|
96
|
+
) -> ArrayLike:
|
|
97
|
+
"""Core (off-axis planet) throughput.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
separation_lod: Angular separation in lambda/D.
|
|
101
|
+
wavelength_nm: Wavelength in nanometres.
|
|
102
|
+
time_s: Time since mission start in seconds.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Fractional throughput in [0, 1].
|
|
106
|
+
"""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
@abc.abstractmethod
|
|
110
|
+
def core_area(
|
|
111
|
+
self,
|
|
112
|
+
separation_lod: ArrayLike,
|
|
113
|
+
wavelength_nm: ArrayLike,
|
|
114
|
+
*,
|
|
115
|
+
time_s: ArrayLike = 0.0,
|
|
116
|
+
) -> ArrayLike:
|
|
117
|
+
"""Photometric aperture area in (lambda/D)^2.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
separation_lod: Angular separation in lambda/D.
|
|
121
|
+
wavelength_nm: Wavelength in nanometres.
|
|
122
|
+
time_s: Time since mission start in seconds.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Core area in (lambda/D)^2.
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
@abc.abstractmethod
|
|
130
|
+
def core_mean_intensity(
|
|
131
|
+
self,
|
|
132
|
+
separation_lod: ArrayLike,
|
|
133
|
+
wavelength_nm: ArrayLike,
|
|
134
|
+
*,
|
|
135
|
+
time_s: ArrayLike = 0.0,
|
|
136
|
+
) -> ArrayLike:
|
|
137
|
+
"""Mean stellar intensity within the photometric aperture.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
separation_lod: Angular separation in lambda/D.
|
|
141
|
+
wavelength_nm: Wavelength in nanometres.
|
|
142
|
+
time_s: Time since mission start in seconds.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Mean stellar leakage intensity in (lambda/D)^-2.
|
|
146
|
+
"""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
@abc.abstractmethod
|
|
150
|
+
def occulter_transmission(
|
|
151
|
+
self,
|
|
152
|
+
separation_lod: ArrayLike,
|
|
153
|
+
wavelength_nm: ArrayLike,
|
|
154
|
+
*,
|
|
155
|
+
time_s: ArrayLike = 0.0,
|
|
156
|
+
) -> ArrayLike:
|
|
157
|
+
"""Off-axis (sky/zodi) transmission through the occulter.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
separation_lod: Angular separation in lambda/D.
|
|
161
|
+
wavelength_nm: Wavelength in nanometres.
|
|
162
|
+
time_s: Time since mission start in seconds.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Fractional sky transmission in [0, 1].
|
|
166
|
+
"""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# Single-PSF image interface
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
#
|
|
173
|
+
# Subsumed by the sampling-explicit contract below (``stellar_map``
|
|
174
|
+
# additionally carries the stellar diameter; ``source_psfs`` the 2D
|
|
175
|
+
# position) but retained for existing single-PSF consumers.
|
|
176
|
+
|
|
177
|
+
@abc.abstractmethod
|
|
178
|
+
def on_axis_psf(
|
|
179
|
+
self,
|
|
180
|
+
wavelength_nm: ArrayLike,
|
|
181
|
+
pixel_scale_rad: float,
|
|
182
|
+
npixels: int,
|
|
183
|
+
) -> Array:
|
|
184
|
+
"""On-axis (stellar leakage) PSF.
|
|
185
|
+
|
|
186
|
+
Returns the coronagraphic PSF for an on-axis point source,
|
|
187
|
+
normalized to unit stellar flux before the coronagraph. For a
|
|
188
|
+
finite stellar diameter use :meth:`stellar_map`, which carries
|
|
189
|
+
the diameter axis this signature lacks.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
wavelength_nm: Wavelength in nanometres.
|
|
193
|
+
pixel_scale_rad: Output pixel scale in radians/pixel.
|
|
194
|
+
npixels: Output array side length in pixels. Must be a
|
|
195
|
+
Python int (not a JAX array) as it determines the
|
|
196
|
+
output shape at compile time.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
2D float array of shape (npixels, npixels).
|
|
200
|
+
"""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
@abc.abstractmethod
|
|
204
|
+
def off_axis_psf(
|
|
205
|
+
self,
|
|
206
|
+
wavelength_nm: ArrayLike,
|
|
207
|
+
separation_lod: ArrayLike,
|
|
208
|
+
pixel_scale_rad: float,
|
|
209
|
+
npixels: int,
|
|
210
|
+
) -> Array:
|
|
211
|
+
"""Off-axis PSF at a given angular separation.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
wavelength_nm: Wavelength in nanometres.
|
|
215
|
+
separation_lod: Source separation in lambda/D.
|
|
216
|
+
pixel_scale_rad: Output pixel scale in radians/pixel.
|
|
217
|
+
npixels: Output array side length in pixels. Must be a
|
|
218
|
+
Python int (not a JAX array) as it determines the
|
|
219
|
+
output shape at compile time.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
2D float array of shape (npixels, npixels).
|
|
223
|
+
"""
|
|
224
|
+
...
|
|
225
|
+
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
# Sampling-explicit image contract -- consumed by coronagraphoto
|
|
228
|
+
# ------------------------------------------------------------------
|
|
229
|
+
#
|
|
230
|
+
# Maps are served AT the requested target sampling (in practice the
|
|
231
|
+
# detector grid), so callers never see a backend's internal grid.
|
|
232
|
+
|
|
233
|
+
@abc.abstractmethod
|
|
234
|
+
def stellar_map(
|
|
235
|
+
self,
|
|
236
|
+
wavelength_nm: ArrayLike,
|
|
237
|
+
stellar_diam_lod: ArrayLike,
|
|
238
|
+
*,
|
|
239
|
+
pixel_scale_lod: ArrayLike,
|
|
240
|
+
shape: tuple[int, int],
|
|
241
|
+
) -> Array:
|
|
242
|
+
"""On-axis stellar leakage map at the requested sampling.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
wavelength_nm: Wavelength in nanometres.
|
|
246
|
+
stellar_diam_lod: Stellar angular diameter in lambda/D.
|
|
247
|
+
pixel_scale_lod: Target pixel scale in lambda/D per pixel.
|
|
248
|
+
shape: Target array shape (ny, nx).
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Map of shape ``shape`` in fraction of pre-coronagraph
|
|
252
|
+
stellar photons per target pixel.
|
|
253
|
+
"""
|
|
254
|
+
...
|
|
255
|
+
|
|
256
|
+
@abc.abstractmethod
|
|
257
|
+
def source_psfs(
|
|
258
|
+
self,
|
|
259
|
+
wavelength_nm: ArrayLike,
|
|
260
|
+
x_lod: ArrayLike,
|
|
261
|
+
y_lod: ArrayLike,
|
|
262
|
+
*,
|
|
263
|
+
pixel_scale_lod: ArrayLike,
|
|
264
|
+
shape: tuple[int, int],
|
|
265
|
+
) -> Array:
|
|
266
|
+
"""Off-axis point-source PSFs at the requested sampling.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
wavelength_nm: Wavelength in nanometres.
|
|
270
|
+
x_lod: Source x-coordinates in lambda/D, shape (K,).
|
|
271
|
+
y_lod: Source y-coordinates in lambda/D, shape (K,).
|
|
272
|
+
pixel_scale_lod: Target pixel scale in lambda/D per pixel.
|
|
273
|
+
shape: Target array shape (ny, nx).
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Stack of shape (K, ny, nx) in fraction of source photons
|
|
277
|
+
per target pixel.
|
|
278
|
+
"""
|
|
279
|
+
...
|
|
280
|
+
|
|
281
|
+
@abc.abstractmethod
|
|
282
|
+
def background_transmission(
|
|
283
|
+
self,
|
|
284
|
+
wavelength_nm: ArrayLike,
|
|
285
|
+
*,
|
|
286
|
+
pixel_scale_lod: ArrayLike,
|
|
287
|
+
shape: tuple[int, int],
|
|
288
|
+
) -> Array:
|
|
289
|
+
"""Per-pixel transmission map for a spatially uniform background.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
wavelength_nm: Wavelength in nanometres.
|
|
293
|
+
pixel_scale_lod: Target pixel scale in lambda/D per pixel.
|
|
294
|
+
shape: Target array shape (ny, nx).
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Dimensionless transmission map of shape ``shape`` (value
|
|
298
|
+
semantics, not a flux).
|
|
299
|
+
"""
|
|
300
|
+
...
|
|
301
|
+
|
|
302
|
+
@abc.abstractmethod
|
|
303
|
+
def extended_scene(
|
|
304
|
+
self,
|
|
305
|
+
scene_map: Array,
|
|
306
|
+
map_pixel_scale_lod: ArrayLike,
|
|
307
|
+
wavelength_nm: ArrayLike,
|
|
308
|
+
*,
|
|
309
|
+
pixel_scale_lod: ArrayLike,
|
|
310
|
+
shape: tuple[int, int],
|
|
311
|
+
rotation_deg: ArrayLike = 0.0,
|
|
312
|
+
) -> Array:
|
|
313
|
+
"""Render an extended incoherent scene through the coronagraph.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
scene_map: Per-pixel source intensity in any units; the
|
|
317
|
+
output carries the same units per target pixel.
|
|
318
|
+
map_pixel_scale_lod: Pixel scale of ``scene_map`` in
|
|
319
|
+
lambda/D per pixel.
|
|
320
|
+
wavelength_nm: Wavelength in nanometres.
|
|
321
|
+
pixel_scale_lod: Target pixel scale in lambda/D per pixel.
|
|
322
|
+
shape: Target array shape (ny, nx).
|
|
323
|
+
rotation_deg: CCW rotation applied when mapping the scene
|
|
324
|
+
into the coronagraph frame (sky-to-detector roll).
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
The scene redistributed by the per-position off-axis PSFs,
|
|
328
|
+
shape ``shape``.
|
|
329
|
+
"""
|
|
330
|
+
...
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class AbstractTableCoronagraph(AbstractCoronagraph):
|
|
334
|
+
"""Base for coronagraphs backed by native-grid interpolation tables.
|
|
335
|
+
|
|
336
|
+
Declares the table SPI -- the native-grid maps a table backend holds
|
|
337
|
+
(``stellar_intens``, ``create_psfs``, ``sky_trans``, ``psf_shape``,
|
|
338
|
+
``psf_datacube``) -- and serves the sampling-explicit image contract
|
|
339
|
+
from it with flux-conserving resamples to the requested target grid.
|
|
340
|
+
Scalar performance curves remain the subclass's business (a table
|
|
341
|
+
backend interpolates its own curve tables).
|
|
342
|
+
|
|
343
|
+
The tables are monochromatic by construction (a YIP is a per-band
|
|
344
|
+
file), so the served maps ignore ``wavelength_nm`` beyond the
|
|
345
|
+
caller's own lambda/D sampling conversion; stack per-band instances
|
|
346
|
+
in a :class:`MultiBandCoronagraph` for a broadband model.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
# -- table SPI ---------------------------------------------------------
|
|
350
|
+
# AbstractVar members may be satisfied by a dataclass field or a
|
|
351
|
+
# property (a concrete base property would shadow subclass fields).
|
|
352
|
+
|
|
353
|
+
psf_shape: AbstractVar[tuple]
|
|
354
|
+
"""Native map shape (ny, nx) shared by the table members."""
|
|
355
|
+
|
|
356
|
+
sky_trans: AbstractVar[Array]
|
|
357
|
+
"""Native-grid transmission map for a spatially uniform background."""
|
|
358
|
+
|
|
359
|
+
psf_datacube: AbstractVar[Array | None]
|
|
360
|
+
"""Optional per-source-position PSF library for extended scenes.
|
|
361
|
+
|
|
362
|
+
``None`` means the table was built without one; :meth:`extended_scene`
|
|
363
|
+
then raises. Full ``(ny, nx, ny, nx)`` and quarter-symmetric
|
|
364
|
+
``(ny//2+1, nx//2+1, ny, nx)`` layouts are supported.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
@abc.abstractmethod
|
|
368
|
+
def stellar_intens(self, stellar_diam_lod: ArrayLike) -> Array:
|
|
369
|
+
"""Native-grid stellar leakage map for a stellar angular diameter.
|
|
370
|
+
|
|
371
|
+
The diameter axis is a genuine table capability (a YIP tabulates
|
|
372
|
+
finite-size stellar maps); implementations interpolate over it.
|
|
373
|
+
"""
|
|
374
|
+
...
|
|
375
|
+
|
|
376
|
+
@abc.abstractmethod
|
|
377
|
+
def create_psfs(self, x_lod: ArrayLike, y_lod: ArrayLike) -> Array:
|
|
378
|
+
"""Native-grid off-axis PSF stack at (x_lod, y_lod), shape (K, ny, nx)."""
|
|
379
|
+
...
|
|
380
|
+
|
|
381
|
+
# -- single-PSF interface served from the tables -----------------------
|
|
382
|
+
|
|
383
|
+
def on_axis_psf(
|
|
384
|
+
self,
|
|
385
|
+
wavelength_nm: ArrayLike,
|
|
386
|
+
pixel_scale_rad: float,
|
|
387
|
+
npixels: int,
|
|
388
|
+
) -> Array:
|
|
389
|
+
"""Point-source stellar leakage map on the native grid.
|
|
390
|
+
|
|
391
|
+
Table backends serve their native sampling; the requested
|
|
392
|
+
``pixel_scale_rad`` / ``npixels`` are accepted for interface
|
|
393
|
+
conformance only. Use :meth:`stellar_map` for target-sampled
|
|
394
|
+
maps and finite stellar diameters.
|
|
395
|
+
"""
|
|
396
|
+
return self.stellar_intens(0.0)
|
|
397
|
+
|
|
398
|
+
def off_axis_psf(
|
|
399
|
+
self,
|
|
400
|
+
wavelength_nm: ArrayLike,
|
|
401
|
+
separation_lod: ArrayLike,
|
|
402
|
+
pixel_scale_rad: float,
|
|
403
|
+
npixels: int,
|
|
404
|
+
) -> Array:
|
|
405
|
+
"""Off-axis PSF at +x separation on the native grid.
|
|
406
|
+
|
|
407
|
+
Sampling arguments are accepted for interface conformance only;
|
|
408
|
+
use :meth:`source_psfs` for target-sampled maps.
|
|
409
|
+
"""
|
|
410
|
+
return self.create_psfs(jnp.atleast_1d(separation_lod), jnp.zeros(1))[0]
|
|
411
|
+
|
|
412
|
+
# -- sampling-explicit contract served from the tables -----------------
|
|
413
|
+
|
|
414
|
+
def stellar_map(
|
|
415
|
+
self,
|
|
416
|
+
wavelength_nm: ArrayLike,
|
|
417
|
+
stellar_diam_lod: ArrayLike,
|
|
418
|
+
*,
|
|
419
|
+
pixel_scale_lod: ArrayLike,
|
|
420
|
+
shape: tuple[int, int],
|
|
421
|
+
) -> Array:
|
|
422
|
+
"""Native ``stellar_intens`` resampled (flux-conserving) to target."""
|
|
423
|
+
return resample_flux(
|
|
424
|
+
self.stellar_intens(stellar_diam_lod),
|
|
425
|
+
self.pixel_scale_lod,
|
|
426
|
+
pixel_scale_lod,
|
|
427
|
+
shape,
|
|
428
|
+
0.0,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
def source_psfs(
|
|
432
|
+
self,
|
|
433
|
+
wavelength_nm: ArrayLike,
|
|
434
|
+
x_lod: ArrayLike,
|
|
435
|
+
y_lod: ArrayLike,
|
|
436
|
+
*,
|
|
437
|
+
pixel_scale_lod: ArrayLike,
|
|
438
|
+
shape: tuple[int, int],
|
|
439
|
+
) -> Array:
|
|
440
|
+
"""Native ``create_psfs`` stack resampled (flux-conserving) to target."""
|
|
441
|
+
|
|
442
|
+
def _to_target(psf):
|
|
443
|
+
return resample_flux(psf, self.pixel_scale_lod, pixel_scale_lod, shape, 0.0)
|
|
444
|
+
|
|
445
|
+
return jax.vmap(_to_target)(self.create_psfs(x_lod, y_lod))
|
|
446
|
+
|
|
447
|
+
def background_transmission(
|
|
448
|
+
self,
|
|
449
|
+
wavelength_nm: ArrayLike,
|
|
450
|
+
*,
|
|
451
|
+
pixel_scale_lod: ArrayLike,
|
|
452
|
+
shape: tuple[int, int],
|
|
453
|
+
) -> Array:
|
|
454
|
+
"""Native ``sky_trans`` resampled to target with value semantics.
|
|
455
|
+
|
|
456
|
+
A transmission is a per-pixel value, not a flux, so the
|
|
457
|
+
pixel-area scaling of the flux-conserving resample is undone.
|
|
458
|
+
"""
|
|
459
|
+
resampled = resample_flux(
|
|
460
|
+
self.sky_trans, self.pixel_scale_lod, pixel_scale_lod, shape, 0.0
|
|
461
|
+
)
|
|
462
|
+
return resampled * (self.pixel_scale_lod / pixel_scale_lod) ** 2
|
|
463
|
+
|
|
464
|
+
def extended_scene(
|
|
465
|
+
self,
|
|
466
|
+
scene_map: Array,
|
|
467
|
+
map_pixel_scale_lod: ArrayLike,
|
|
468
|
+
wavelength_nm: ArrayLike,
|
|
469
|
+
*,
|
|
470
|
+
pixel_scale_lod: ArrayLike,
|
|
471
|
+
shape: tuple[int, int],
|
|
472
|
+
rotation_deg: ArrayLike = 0.0,
|
|
473
|
+
) -> Array:
|
|
474
|
+
"""Scene convolved with the ``psf_datacube`` PSF library.
|
|
475
|
+
|
|
476
|
+
Resamples the scene to the native source grid (applying the
|
|
477
|
+
sky-to-detector rotation), convolves via the full or
|
|
478
|
+
quarter-symmetric datacube, and resamples the result to the
|
|
479
|
+
target grid.
|
|
480
|
+
|
|
481
|
+
Raises:
|
|
482
|
+
ValueError: if :attr:`psf_datacube` is ``None``, or its
|
|
483
|
+
source grid matches neither the full nor the quarter
|
|
484
|
+
PSF shape.
|
|
485
|
+
"""
|
|
486
|
+
datacube = self.psf_datacube
|
|
487
|
+
if datacube is None:
|
|
488
|
+
raise ValueError(
|
|
489
|
+
"extended_scene requires a coronagraph with a PSF "
|
|
490
|
+
f"datacube; got {type(self).__name__}.psf_datacube=None. "
|
|
491
|
+
"The table implementation convolves the scene with the "
|
|
492
|
+
"per-source-position PSFs and cannot run without it."
|
|
493
|
+
)
|
|
494
|
+
ny, nx = self.psf_shape
|
|
495
|
+
src = resample_flux(
|
|
496
|
+
scene_map,
|
|
497
|
+
map_pixel_scale_lod,
|
|
498
|
+
self.pixel_scale_lod,
|
|
499
|
+
(ny, nx),
|
|
500
|
+
rotation_deg,
|
|
501
|
+
)
|
|
502
|
+
n_src_y, n_src_x = datacube.shape[:2]
|
|
503
|
+
if (n_src_y, n_src_x) == (ny, nx):
|
|
504
|
+
native = jnp.einsum("ij,ijxy->xy", src, datacube)
|
|
505
|
+
elif (n_src_y, n_src_x) == (ny // 2 + 1, nx // 2 + 1):
|
|
506
|
+
native = _convolve_quadrants(src, datacube)
|
|
507
|
+
else:
|
|
508
|
+
raise ValueError(
|
|
509
|
+
"extended_scene: psf_datacube source-grid shape "
|
|
510
|
+
f"({n_src_y}, {n_src_x}) does not match either the full "
|
|
511
|
+
f"PSF shape ({ny}, {nx}) or the quarter PSF shape "
|
|
512
|
+
f"({ny // 2 + 1}, {nx // 2 + 1}). Coronagraphs must "
|
|
513
|
+
"publish a full or quarter datacube."
|
|
514
|
+
)
|
|
515
|
+
return resample_flux(native, self.pixel_scale_lod, pixel_scale_lod, shape, 0.0)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class AbstractScalarCoronagraph(AbstractCoronagraph):
|
|
519
|
+
"""Base for ETC-only coronagraph models that lack 2D PSF generation.
|
|
520
|
+
|
|
521
|
+
Stubs out the single-PSF interface and the sampling-explicit image
|
|
522
|
+
contract with zero arrays so the class satisfies AbstractCoronagraph
|
|
523
|
+
without requiring a full optical model. Do NOT use this base for a
|
|
524
|
+
model that carries native-grid tables -- subclass
|
|
525
|
+
:class:`AbstractTableCoronagraph` instead, or the image pipeline
|
|
526
|
+
will silently render zeros.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
def on_axis_psf(
|
|
530
|
+
self,
|
|
531
|
+
wavelength_nm: ArrayLike,
|
|
532
|
+
pixel_scale_rad: float,
|
|
533
|
+
npixels: int,
|
|
534
|
+
) -> Array:
|
|
535
|
+
"""Return a zero PSF (not implemented for scalar-only models)."""
|
|
536
|
+
return jnp.zeros((npixels, npixels))
|
|
537
|
+
|
|
538
|
+
def off_axis_psf(
|
|
539
|
+
self,
|
|
540
|
+
wavelength_nm: ArrayLike,
|
|
541
|
+
separation_lod: ArrayLike,
|
|
542
|
+
pixel_scale_rad: float,
|
|
543
|
+
npixels: int,
|
|
544
|
+
) -> Array:
|
|
545
|
+
"""Return a zero PSF (not implemented for scalar-only models)."""
|
|
546
|
+
return jnp.zeros((npixels, npixels))
|
|
547
|
+
|
|
548
|
+
def stellar_map(
|
|
549
|
+
self,
|
|
550
|
+
wavelength_nm: ArrayLike,
|
|
551
|
+
stellar_diam_lod: ArrayLike,
|
|
552
|
+
*,
|
|
553
|
+
pixel_scale_lod: ArrayLike,
|
|
554
|
+
shape: tuple[int, int],
|
|
555
|
+
) -> Array:
|
|
556
|
+
"""Return a zero map (not implemented for scalar-only models)."""
|
|
557
|
+
return jnp.zeros(shape)
|
|
558
|
+
|
|
559
|
+
def source_psfs(
|
|
560
|
+
self,
|
|
561
|
+
wavelength_nm: ArrayLike,
|
|
562
|
+
x_lod: ArrayLike,
|
|
563
|
+
y_lod: ArrayLike,
|
|
564
|
+
*,
|
|
565
|
+
pixel_scale_lod: ArrayLike,
|
|
566
|
+
shape: tuple[int, int],
|
|
567
|
+
) -> Array:
|
|
568
|
+
"""Return zero PSFs (not implemented for scalar-only models)."""
|
|
569
|
+
k = jnp.atleast_1d(jnp.asarray(x_lod)).shape[0]
|
|
570
|
+
return jnp.zeros((k, *shape))
|
|
571
|
+
|
|
572
|
+
def background_transmission(
|
|
573
|
+
self,
|
|
574
|
+
wavelength_nm: ArrayLike,
|
|
575
|
+
*,
|
|
576
|
+
pixel_scale_lod: ArrayLike,
|
|
577
|
+
shape: tuple[int, int],
|
|
578
|
+
) -> Array:
|
|
579
|
+
"""Return a zero map (not implemented for scalar-only models)."""
|
|
580
|
+
return jnp.zeros(shape)
|
|
581
|
+
|
|
582
|
+
def extended_scene(
|
|
583
|
+
self,
|
|
584
|
+
scene_map: Array,
|
|
585
|
+
map_pixel_scale_lod: ArrayLike,
|
|
586
|
+
wavelength_nm: ArrayLike,
|
|
587
|
+
*,
|
|
588
|
+
pixel_scale_lod: ArrayLike,
|
|
589
|
+
shape: tuple[int, int],
|
|
590
|
+
rotation_deg: ArrayLike = 0.0,
|
|
591
|
+
) -> Array:
|
|
592
|
+
"""Return a zero map (not implemented for scalar-only models)."""
|
|
593
|
+
return jnp.zeros(shape)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class MultiBandCoronagraph(AbstractCoronagraph):
|
|
597
|
+
"""A broadband coronagraph as a stack of per-band models.
|
|
598
|
+
|
|
599
|
+
Every contract call dispatches to the band whose declared center is
|
|
600
|
+
nearest the requested ``wavelength_nm`` and forwards the call
|
|
601
|
+
unchanged. Because the contract is sampling-explicit in lambda/D,
|
|
602
|
+
chromatic magnification needs no handling here: the caller already
|
|
603
|
+
converts its detector grid to lambda/D at each wavelength.
|
|
604
|
+
|
|
605
|
+
The per-band models can be anything implementing
|
|
606
|
+
``AbstractCoronagraph`` -- per-band YIP tables
|
|
607
|
+
(:class:`optixstuff.YippyCoronagraph`) or per-wavelength compiled
|
|
608
|
+
views of a live propagation model -- and can be mixed.
|
|
609
|
+
|
|
610
|
+
``wavelength_nm`` must be concrete (a Python/NumPy scalar, not a
|
|
611
|
+
traced JAX value) at dispatch time: bands are distinct pytrees with
|
|
612
|
+
distinct shapes, so the selection is Python control flow. This
|
|
613
|
+
matches the per-bin structure of the rate pipeline, where the
|
|
614
|
+
wavelength is a static observation parameter.
|
|
615
|
+
|
|
616
|
+
Scalar metadata (``pixel_scale_lod`` / ``IWA`` / ``OWA``) is served
|
|
617
|
+
from the band nearest ``reference_wavelength_nm`` (default: the
|
|
618
|
+
first band).
|
|
619
|
+
"""
|
|
620
|
+
|
|
621
|
+
band_centers_nm: tuple = eqx.field(static=True)
|
|
622
|
+
bands: tuple
|
|
623
|
+
reference_wavelength_nm: float = eqx.field(static=True)
|
|
624
|
+
|
|
625
|
+
def __init__(self, band_centers_nm, bands, *, reference_wavelength_nm=None):
|
|
626
|
+
"""Build a multi-band stack.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
band_centers_nm: Band center wavelengths in nanometres, one
|
|
630
|
+
per band.
|
|
631
|
+
bands: The per-band ``AbstractCoronagraph`` models, aligned
|
|
632
|
+
with ``band_centers_nm``.
|
|
633
|
+
reference_wavelength_nm: Wavelength whose band serves the
|
|
634
|
+
scalar metadata. Default: the first band's center.
|
|
635
|
+
"""
|
|
636
|
+
centers = tuple(float(c) for c in band_centers_nm)
|
|
637
|
+
bands = tuple(bands)
|
|
638
|
+
if len(centers) != len(bands):
|
|
639
|
+
raise ValueError(
|
|
640
|
+
f"{len(centers)} band centers but {len(bands)} band models"
|
|
641
|
+
)
|
|
642
|
+
if not bands:
|
|
643
|
+
raise ValueError("MultiBandCoronagraph needs at least one band")
|
|
644
|
+
self.band_centers_nm = centers
|
|
645
|
+
self.bands = bands
|
|
646
|
+
self.reference_wavelength_nm = float(
|
|
647
|
+
centers[0] if reference_wavelength_nm is None else reference_wavelength_nm
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
def band(self, wavelength_nm) -> AbstractCoronagraph:
|
|
651
|
+
"""The band model whose center is nearest ``wavelength_nm``."""
|
|
652
|
+
wl = float(wavelength_nm)
|
|
653
|
+
index = min(
|
|
654
|
+
range(len(self.band_centers_nm)),
|
|
655
|
+
key=lambda i: abs(self.band_centers_nm[i] - wl),
|
|
656
|
+
)
|
|
657
|
+
return self.bands[index]
|
|
658
|
+
|
|
659
|
+
# -- scalar metadata from the reference band ---------------------------
|
|
660
|
+
|
|
661
|
+
@property
|
|
662
|
+
def pixel_scale_lod(self) -> float:
|
|
663
|
+
"""Native pixel scale of the reference band."""
|
|
664
|
+
return self.band(self.reference_wavelength_nm).pixel_scale_lod
|
|
665
|
+
|
|
666
|
+
@property
|
|
667
|
+
def IWA(self) -> float:
|
|
668
|
+
"""Inner working angle of the reference band."""
|
|
669
|
+
return self.band(self.reference_wavelength_nm).IWA
|
|
670
|
+
|
|
671
|
+
@property
|
|
672
|
+
def OWA(self) -> float:
|
|
673
|
+
"""Outer working angle of the reference band."""
|
|
674
|
+
return self.band(self.reference_wavelength_nm).OWA
|
|
675
|
+
|
|
676
|
+
# -- every interface dispatches on wavelength --------------------------
|
|
677
|
+
|
|
678
|
+
def throughput(self, separation_lod, wavelength_nm, *, time_s=0.0):
|
|
679
|
+
"""Core throughput from the nearest band."""
|
|
680
|
+
return self.band(wavelength_nm).throughput(
|
|
681
|
+
separation_lod, wavelength_nm, time_s=time_s
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
def core_area(self, separation_lod, wavelength_nm, *, time_s=0.0):
|
|
685
|
+
"""Photometric aperture area from the nearest band."""
|
|
686
|
+
return self.band(wavelength_nm).core_area(
|
|
687
|
+
separation_lod, wavelength_nm, time_s=time_s
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def core_mean_intensity(self, separation_lod, wavelength_nm, *, time_s=0.0):
|
|
691
|
+
"""Mean stellar leakage from the nearest band."""
|
|
692
|
+
return self.band(wavelength_nm).core_mean_intensity(
|
|
693
|
+
separation_lod, wavelength_nm, time_s=time_s
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
def occulter_transmission(self, separation_lod, wavelength_nm, *, time_s=0.0):
|
|
697
|
+
"""Sky transmission from the nearest band."""
|
|
698
|
+
return self.band(wavelength_nm).occulter_transmission(
|
|
699
|
+
separation_lod, wavelength_nm, time_s=time_s
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
def on_axis_psf(self, wavelength_nm, pixel_scale_rad, npixels):
|
|
703
|
+
"""On-axis PSF from the nearest band."""
|
|
704
|
+
return self.band(wavelength_nm).on_axis_psf(
|
|
705
|
+
wavelength_nm, pixel_scale_rad, npixels
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def off_axis_psf(self, wavelength_nm, separation_lod, pixel_scale_rad, npixels):
|
|
709
|
+
"""Off-axis PSF from the nearest band."""
|
|
710
|
+
return self.band(wavelength_nm).off_axis_psf(
|
|
711
|
+
wavelength_nm, separation_lod, pixel_scale_rad, npixels
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
def stellar_map(self, wavelength_nm, stellar_diam_lod, *, pixel_scale_lod, shape):
|
|
715
|
+
"""Stellar leakage map from the nearest band."""
|
|
716
|
+
return self.band(wavelength_nm).stellar_map(
|
|
717
|
+
wavelength_nm,
|
|
718
|
+
stellar_diam_lod,
|
|
719
|
+
pixel_scale_lod=pixel_scale_lod,
|
|
720
|
+
shape=shape,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
def source_psfs(self, wavelength_nm, x_lod, y_lod, *, pixel_scale_lod, shape):
|
|
724
|
+
"""Off-axis PSF stack from the nearest band."""
|
|
725
|
+
return self.band(wavelength_nm).source_psfs(
|
|
726
|
+
wavelength_nm, x_lod, y_lod, pixel_scale_lod=pixel_scale_lod, shape=shape
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
def background_transmission(self, wavelength_nm, *, pixel_scale_lod, shape):
|
|
730
|
+
"""Background transmission map from the nearest band."""
|
|
731
|
+
return self.band(wavelength_nm).background_transmission(
|
|
732
|
+
wavelength_nm, pixel_scale_lod=pixel_scale_lod, shape=shape
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
def extended_scene(
|
|
736
|
+
self,
|
|
737
|
+
scene_map,
|
|
738
|
+
map_pixel_scale_lod,
|
|
739
|
+
wavelength_nm,
|
|
740
|
+
*,
|
|
741
|
+
pixel_scale_lod,
|
|
742
|
+
shape,
|
|
743
|
+
rotation_deg=0.0,
|
|
744
|
+
):
|
|
745
|
+
"""Extended-scene render from the nearest band."""
|
|
746
|
+
return self.band(wavelength_nm).extended_scene(
|
|
747
|
+
scene_map,
|
|
748
|
+
map_pixel_scale_lod,
|
|
749
|
+
wavelength_nm,
|
|
750
|
+
pixel_scale_lod=pixel_scale_lod,
|
|
751
|
+
shape=shape,
|
|
752
|
+
rotation_deg=rotation_deg,
|
|
753
|
+
)
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
"""YippyCoronagraph -- AbstractCoronagraph backed by a yippy EqxCoronagraph."""
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
from typing import final
|
|
5
4
|
|
|
6
5
|
from jax.typing import ArrayLike
|
|
7
6
|
from jaxtyping import Array
|
|
8
7
|
from yippy import EqxCoronagraph
|
|
9
8
|
|
|
10
|
-
from optixstuff.coronagraph import
|
|
9
|
+
from optixstuff.coronagraph import AbstractTableCoronagraph
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
@final
|
|
14
|
-
class YippyCoronagraph(
|
|
13
|
+
class YippyCoronagraph(AbstractTableCoronagraph):
|
|
15
14
|
"""Coronagraph performance model backed by a yippy YIP interpolation table.
|
|
16
15
|
|
|
17
|
-
Wraps a yippy ``EqxCoronagraph`` via composition
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
``
|
|
16
|
+
Wraps a yippy ``EqxCoronagraph`` via composition: the scalar curves
|
|
17
|
+
adapt the backend's interpolation tables, and the native-grid table
|
|
18
|
+
SPI (``stellar_intens`` / ``create_psfs`` / ``sky_trans`` /
|
|
19
|
+
``psf_shape`` / ``psf_datacube``) delegates to the backend, from
|
|
20
|
+
which :class:`AbstractTableCoronagraph` serves the sampling-explicit
|
|
21
|
+
image contract. The ``_backend`` field is itself an ``eqx.Module``,
|
|
22
|
+
so its internal JAX arrays flow through ``filter_jit`` and
|
|
23
|
+
``filter_grad`` normally.
|
|
21
24
|
|
|
22
25
|
Construction mirrors ``EqxCoronagraph`` -- pass either a YIP path or
|
|
23
26
|
an existing ``EqxCoronagraph`` instance::
|
|
@@ -133,7 +136,7 @@ class YippyCoronagraph(AbstractCoronagraph):
|
|
|
133
136
|
"""
|
|
134
137
|
return self._backend.create_psf(separation_lod, 0.0, npixels)
|
|
135
138
|
|
|
136
|
-
# -- Convenience methods
|
|
139
|
+
# -- Convenience methods and the table SPI ----------------------------
|
|
137
140
|
|
|
138
141
|
def noise_floor_ayo(
|
|
139
142
|
self,
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
"""Coronagraph abstractions."""
|
|
2
|
-
|
|
3
|
-
import abc
|
|
4
|
-
|
|
5
|
-
import equinox as eqx
|
|
6
|
-
import jax.numpy as jnp
|
|
7
|
-
from equinox import AbstractVar
|
|
8
|
-
from jax.typing import ArrayLike
|
|
9
|
-
from jaxtyping import Array
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class AbstractCoronagraph(eqx.Module):
|
|
13
|
-
"""Abstract interface for coronagraph performance models.
|
|
14
|
-
|
|
15
|
-
Provides both scalar performance curves (for ETC use) and 2D PSF
|
|
16
|
-
generation (for image simulation). Implementations can be backed by
|
|
17
|
-
pre-computed interpolation tables (yippy), physical wavefront
|
|
18
|
-
propagation, or analytical models.
|
|
19
|
-
|
|
20
|
-
All wavelength arguments are in nanometres throughout.
|
|
21
|
-
All separations are in lambda/D units.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
pixel_scale_lod: AbstractVar[float]
|
|
25
|
-
"""Native pixel scale in lambda/D per pixel."""
|
|
26
|
-
|
|
27
|
-
IWA: AbstractVar[float]
|
|
28
|
-
"""Inner working angle in lambda/D."""
|
|
29
|
-
|
|
30
|
-
OWA: AbstractVar[float]
|
|
31
|
-
"""Outer working angle in lambda/D."""
|
|
32
|
-
|
|
33
|
-
# ------------------------------------------------------------------
|
|
34
|
-
# Scalar interface -- consumed by jaxEDITH and yield estimators
|
|
35
|
-
# ------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
@abc.abstractmethod
|
|
38
|
-
def throughput(
|
|
39
|
-
self,
|
|
40
|
-
separation_lod: ArrayLike,
|
|
41
|
-
wavelength_nm: ArrayLike,
|
|
42
|
-
*,
|
|
43
|
-
time_s: ArrayLike = 0.0,
|
|
44
|
-
) -> ArrayLike:
|
|
45
|
-
"""Core (off-axis planet) throughput.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
separation_lod: Angular separation in lambda/D.
|
|
49
|
-
wavelength_nm: Wavelength in nanometres.
|
|
50
|
-
time_s: Time since mission start in seconds.
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
Fractional throughput in [0, 1].
|
|
54
|
-
"""
|
|
55
|
-
...
|
|
56
|
-
|
|
57
|
-
@abc.abstractmethod
|
|
58
|
-
def core_area(
|
|
59
|
-
self,
|
|
60
|
-
separation_lod: ArrayLike,
|
|
61
|
-
wavelength_nm: ArrayLike,
|
|
62
|
-
*,
|
|
63
|
-
time_s: ArrayLike = 0.0,
|
|
64
|
-
) -> ArrayLike:
|
|
65
|
-
"""Photometric aperture area in (lambda/D)^2.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
separation_lod: Angular separation in lambda/D.
|
|
69
|
-
wavelength_nm: Wavelength in nanometres.
|
|
70
|
-
time_s: Time since mission start in seconds.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Core area in (lambda/D)^2.
|
|
74
|
-
"""
|
|
75
|
-
...
|
|
76
|
-
|
|
77
|
-
@abc.abstractmethod
|
|
78
|
-
def core_mean_intensity(
|
|
79
|
-
self,
|
|
80
|
-
separation_lod: ArrayLike,
|
|
81
|
-
wavelength_nm: ArrayLike,
|
|
82
|
-
*,
|
|
83
|
-
time_s: ArrayLike = 0.0,
|
|
84
|
-
) -> ArrayLike:
|
|
85
|
-
"""Mean stellar intensity within the photometric aperture.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
separation_lod: Angular separation in lambda/D.
|
|
89
|
-
wavelength_nm: Wavelength in nanometres.
|
|
90
|
-
time_s: Time since mission start in seconds.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
Mean stellar leakage intensity in (lambda/D)^-2.
|
|
94
|
-
"""
|
|
95
|
-
...
|
|
96
|
-
|
|
97
|
-
@abc.abstractmethod
|
|
98
|
-
def occulter_transmission(
|
|
99
|
-
self,
|
|
100
|
-
separation_lod: ArrayLike,
|
|
101
|
-
wavelength_nm: ArrayLike,
|
|
102
|
-
*,
|
|
103
|
-
time_s: ArrayLike = 0.0,
|
|
104
|
-
) -> ArrayLike:
|
|
105
|
-
"""Off-axis (sky/zodi) transmission through the occulter.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
separation_lod: Angular separation in lambda/D.
|
|
109
|
-
wavelength_nm: Wavelength in nanometres.
|
|
110
|
-
time_s: Time since mission start in seconds.
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
Fractional sky transmission in [0, 1].
|
|
114
|
-
"""
|
|
115
|
-
...
|
|
116
|
-
|
|
117
|
-
# ------------------------------------------------------------------
|
|
118
|
-
# Image interface -- consumed by coronagraphoto
|
|
119
|
-
# ------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
@abc.abstractmethod
|
|
122
|
-
def on_axis_psf(
|
|
123
|
-
self,
|
|
124
|
-
wavelength_nm: ArrayLike,
|
|
125
|
-
pixel_scale_rad: float,
|
|
126
|
-
npixels: int,
|
|
127
|
-
) -> Array:
|
|
128
|
-
"""On-axis (stellar leakage) PSF.
|
|
129
|
-
|
|
130
|
-
Returns the coronagraphic PSF for an on-axis point source,
|
|
131
|
-
normalized to unit stellar flux before the coronagraph.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
wavelength_nm: Wavelength in nanometres.
|
|
135
|
-
pixel_scale_rad: Output pixel scale in radians/pixel.
|
|
136
|
-
npixels: Output array side length in pixels. Must be a
|
|
137
|
-
Python int (not a JAX array) as it determines the
|
|
138
|
-
output shape at compile time.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
2D float array of shape (npixels, npixels).
|
|
142
|
-
"""
|
|
143
|
-
...
|
|
144
|
-
|
|
145
|
-
@abc.abstractmethod
|
|
146
|
-
def off_axis_psf(
|
|
147
|
-
self,
|
|
148
|
-
wavelength_nm: ArrayLike,
|
|
149
|
-
separation_lod: ArrayLike,
|
|
150
|
-
pixel_scale_rad: float,
|
|
151
|
-
npixels: int,
|
|
152
|
-
) -> Array:
|
|
153
|
-
"""Off-axis PSF at a given angular separation.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
wavelength_nm: Wavelength in nanometres.
|
|
157
|
-
separation_lod: Source separation in lambda/D.
|
|
158
|
-
pixel_scale_rad: Output pixel scale in radians/pixel.
|
|
159
|
-
npixels: Output array side length in pixels. Must be a
|
|
160
|
-
Python int (not a JAX array) as it determines the
|
|
161
|
-
output shape at compile time.
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
2D float array of shape (npixels, npixels).
|
|
165
|
-
"""
|
|
166
|
-
...
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class AbstractScalarCoronagraph(AbstractCoronagraph):
|
|
170
|
-
"""Base for ETC-only coronagraph models that lack 2D PSF generation.
|
|
171
|
-
|
|
172
|
-
Stubs out the image interface with zero arrays so the class satisfies
|
|
173
|
-
AbstractCoronagraph without requiring a full optical model.
|
|
174
|
-
"""
|
|
175
|
-
|
|
176
|
-
def on_axis_psf(
|
|
177
|
-
self,
|
|
178
|
-
wavelength_nm: ArrayLike,
|
|
179
|
-
pixel_scale_rad: float,
|
|
180
|
-
npixels: int,
|
|
181
|
-
) -> Array:
|
|
182
|
-
"""Return a zero PSF (not implemented for scalar-only models)."""
|
|
183
|
-
return jnp.zeros((npixels, npixels))
|
|
184
|
-
|
|
185
|
-
def off_axis_psf(
|
|
186
|
-
self,
|
|
187
|
-
wavelength_nm: ArrayLike,
|
|
188
|
-
separation_lod: ArrayLike,
|
|
189
|
-
pixel_scale_rad: float,
|
|
190
|
-
npixels: int,
|
|
191
|
-
) -> Array:
|
|
192
|
-
"""Return a zero PSF (not implemented for scalar-only models)."""
|
|
193
|
-
return jnp.zeros((npixels, npixels))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|