optixstuff 1.0.2__tar.gz → 1.2.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-1.2.0/.pre-commit-config.yaml +20 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/CHANGELOG.md +17 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/PKG-INFO +13 -9
- {optixstuff-1.0.2 → optixstuff-1.2.0}/README.md +12 -8
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/__init__.py +3 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/_version.py +2 -2
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/detector.py +50 -10
- optixstuff-1.2.0/src/optixstuff/disperser.py +114 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/optical_path.py +4 -3
- {optixstuff-1.0.2 → optixstuff-1.2.0}/.gitignore +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/.readthedocs.yaml +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/LICENSE +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/pyproject.toml +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/_repr.py +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/coronagraph.py +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/exposure.py +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/optical_elements.py +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/primary.py +0 -0
- {optixstuff-1.0.2 → optixstuff-1.2.0}/src/optixstuff/yippy_coronagraph.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: "v6.0.0"
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: name-tests-test
|
|
7
|
+
args: [--pytest-test-first]
|
|
8
|
+
- id: end-of-file-fixer
|
|
9
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
10
|
+
rev: v0.15.2
|
|
11
|
+
hooks:
|
|
12
|
+
- id: ruff-check
|
|
13
|
+
args: [--fix]
|
|
14
|
+
- id: ruff-format
|
|
15
|
+
- repo: https://github.com/compilerla/conventional-pre-commit
|
|
16
|
+
rev: v4.3.0
|
|
17
|
+
hooks:
|
|
18
|
+
- id: conventional-pre-commit
|
|
19
|
+
stages: [commit-msg]
|
|
20
|
+
args: []
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0](https://github.com/CoreySpohn/optixstuff/compare/v1.1.0...v1.2.0) (2026-06-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Add reference wavelength for the psflets ([a2fe241](https://github.com/CoreySpohn/optixstuff/commit/a2fe241ee778ef54c4832e68950fe5268f8a8b1c))
|
|
9
|
+
* **disperser:** wavelength-dependent throughput via composed optical element ([c902c88](https://github.com/CoreySpohn/optixstuff/commit/c902c88ae08447e5dfbc868cbcc51a254098877e))
|
|
10
|
+
|
|
11
|
+
## [1.1.0](https://github.com/CoreySpohn/optixstuff/compare/v1.0.2...v1.1.0) (2026-05-30)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* add AbstractDisperser and LensletDisperser descriptors ([6e9cd21](https://github.com/CoreySpohn/optixstuff/commit/6e9cd210c3b6f9b8af3b4b8a72cc85e412288c39))
|
|
17
|
+
* **detector:** add per-pixel noise_variance (shot + dark + CIC + read) ([f983b0d](https://github.com/CoreySpohn/optixstuff/commit/f983b0dc628e6a2ef8f6489a396c18ff2fff173a))
|
|
18
|
+
* **optical_path:** add optional disperser descriptor field ([06e486e](https://github.com/CoreySpohn/optixstuff/commit/06e486e31daaa15773d91f2b586ad7ec16359db5))
|
|
19
|
+
|
|
3
20
|
## [1.0.2](https://github.com/CoreySpohn/optixstuff/compare/v1.0.1...v1.0.2) (2026-05-25)
|
|
4
21
|
|
|
5
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optixstuff
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.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
|
|
@@ -68,11 +68,13 @@ hardware as composable JAX modules: primary aperture, throughput-affecting
|
|
|
68
68
|
elements, coronagraph backend, detector. Its job is to be the single source of
|
|
69
69
|
truth for the hardware configuration that downstream tools consume.
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
(2D image simulation)
|
|
71
|
+
[coronagraphoto](https://github.com/CoreySpohn/coronagraphoto)
|
|
72
|
+
(2D image simulation), [coronachrome](https://github.com/CoreySpohn/coronachrome)
|
|
73
|
+
(lenslet-IFS dispersion + spectral extraction), and
|
|
74
|
+
[jaxEDITH](https://github.com/CoreySpohn/jaxedith)
|
|
73
75
|
(exposure-time and yield calculations) import the same `OpticalPath` class and
|
|
74
|
-
the same detector / throughput / primary types from optixstuff.
|
|
75
|
-
here and
|
|
76
|
+
the same detector / throughput / primary / disperser types from optixstuff.
|
|
77
|
+
Change a value here and every downstream tool picks it up on the next import.
|
|
76
78
|
|
|
77
79
|
## What optixstuff is *not*
|
|
78
80
|
|
|
@@ -82,8 +84,8 @@ here and both downstream tools pick it up on the next import.
|
|
|
82
84
|
optixstuff wraps a PSF backend via `YippyCoronagraph` but does not compute PSFs.
|
|
83
85
|
- **Not a scene model.** Stars, planets, disks, and zodi live in
|
|
84
86
|
[skyscapes](https://github.com/CoreySpohn/skyscapes).
|
|
85
|
-
- **Not a simulator.** Downstream tools (coronagraphoto, jaxEDITH)
|
|
86
|
-
`OpticalPath` to produce images or count rates.
|
|
87
|
+
- **Not a simulator.** Downstream tools (coronagraphoto, coronachrome, jaxEDITH)
|
|
88
|
+
consume an `OpticalPath` to produce images, dispersed IFS frames, or count rates.
|
|
87
89
|
|
|
88
90
|
## Architecture
|
|
89
91
|
|
|
@@ -91,9 +93,9 @@ Built on [JAX](https://github.com/google/jax) and
|
|
|
91
93
|
[Equinox](https://github.com/patrick-kidger/equinox), `optixstuff` provides:
|
|
92
94
|
|
|
93
95
|
- **Abstract interfaces** — `AbstractPrimary`, `AbstractOpticalElement`,
|
|
94
|
-
`AbstractCoronagraph`, `AbstractDetector`
|
|
96
|
+
`AbstractCoronagraph`, `AbstractDetector`, `AbstractDisperser`
|
|
95
97
|
- **Concrete implementations** — `SimplePrimary`, `ConstantThroughput`,
|
|
96
|
-
`IdealDetector`
|
|
98
|
+
`IdealDetector`, `Detector`, `LensletDisperser`
|
|
97
99
|
- **Container** — `OpticalPath`, a composable hardware configuration passed to all
|
|
98
100
|
simulators
|
|
99
101
|
|
|
@@ -111,11 +113,13 @@ flowchart TB
|
|
|
111
113
|
optix["<b>optixstuff</b><br/>Telescope · Coronagraph · Detector · OpticalPath<br/>Throughput chains · QE · Noise rates"]
|
|
112
114
|
jaxedith["<b>jaxEDITH</b><br/>Scalar count rates<br/>Exposure-time calculations"]
|
|
113
115
|
corono["<b>coronagraphoto</b><br/>2D image simulation<br/>Multi-epoch scenes"]
|
|
116
|
+
coronachrome["<b>coronachrome</b><br/>Lenslet-IFS dispersion<br/>Spectral extraction"]
|
|
114
117
|
|
|
115
118
|
physopt -- YIP --> yippy
|
|
116
119
|
yippy -- flux patterns --> optix
|
|
117
120
|
optix --> jaxedith
|
|
118
121
|
optix --> corono
|
|
122
|
+
optix --> coronachrome
|
|
119
123
|
```
|
|
120
124
|
|
|
121
125
|
## Installation
|
|
@@ -10,11 +10,13 @@ hardware as composable JAX modules: primary aperture, throughput-affecting
|
|
|
10
10
|
elements, coronagraph backend, detector. Its job is to be the single source of
|
|
11
11
|
truth for the hardware configuration that downstream tools consume.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
(2D image simulation)
|
|
13
|
+
[coronagraphoto](https://github.com/CoreySpohn/coronagraphoto)
|
|
14
|
+
(2D image simulation), [coronachrome](https://github.com/CoreySpohn/coronachrome)
|
|
15
|
+
(lenslet-IFS dispersion + spectral extraction), and
|
|
16
|
+
[jaxEDITH](https://github.com/CoreySpohn/jaxedith)
|
|
15
17
|
(exposure-time and yield calculations) import the same `OpticalPath` class and
|
|
16
|
-
the same detector / throughput / primary types from optixstuff.
|
|
17
|
-
here and
|
|
18
|
+
the same detector / throughput / primary / disperser types from optixstuff.
|
|
19
|
+
Change a value here and every downstream tool picks it up on the next import.
|
|
18
20
|
|
|
19
21
|
## What optixstuff is *not*
|
|
20
22
|
|
|
@@ -24,8 +26,8 @@ here and both downstream tools pick it up on the next import.
|
|
|
24
26
|
optixstuff wraps a PSF backend via `YippyCoronagraph` but does not compute PSFs.
|
|
25
27
|
- **Not a scene model.** Stars, planets, disks, and zodi live in
|
|
26
28
|
[skyscapes](https://github.com/CoreySpohn/skyscapes).
|
|
27
|
-
- **Not a simulator.** Downstream tools (coronagraphoto, jaxEDITH)
|
|
28
|
-
`OpticalPath` to produce images or count rates.
|
|
29
|
+
- **Not a simulator.** Downstream tools (coronagraphoto, coronachrome, jaxEDITH)
|
|
30
|
+
consume an `OpticalPath` to produce images, dispersed IFS frames, or count rates.
|
|
29
31
|
|
|
30
32
|
## Architecture
|
|
31
33
|
|
|
@@ -33,9 +35,9 @@ Built on [JAX](https://github.com/google/jax) and
|
|
|
33
35
|
[Equinox](https://github.com/patrick-kidger/equinox), `optixstuff` provides:
|
|
34
36
|
|
|
35
37
|
- **Abstract interfaces** — `AbstractPrimary`, `AbstractOpticalElement`,
|
|
36
|
-
`AbstractCoronagraph`, `AbstractDetector`
|
|
38
|
+
`AbstractCoronagraph`, `AbstractDetector`, `AbstractDisperser`
|
|
37
39
|
- **Concrete implementations** — `SimplePrimary`, `ConstantThroughput`,
|
|
38
|
-
`IdealDetector`
|
|
40
|
+
`IdealDetector`, `Detector`, `LensletDisperser`
|
|
39
41
|
- **Container** — `OpticalPath`, a composable hardware configuration passed to all
|
|
40
42
|
simulators
|
|
41
43
|
|
|
@@ -53,11 +55,13 @@ flowchart TB
|
|
|
53
55
|
optix["<b>optixstuff</b><br/>Telescope · Coronagraph · Detector · OpticalPath<br/>Throughput chains · QE · Noise rates"]
|
|
54
56
|
jaxedith["<b>jaxEDITH</b><br/>Scalar count rates<br/>Exposure-time calculations"]
|
|
55
57
|
corono["<b>coronagraphoto</b><br/>2D image simulation<br/>Multi-epoch scenes"]
|
|
58
|
+
coronachrome["<b>coronachrome</b><br/>Lenslet-IFS dispersion<br/>Spectral extraction"]
|
|
56
59
|
|
|
57
60
|
physopt -- YIP --> yippy
|
|
58
61
|
yippy -- flux patterns --> optix
|
|
59
62
|
optix --> jaxedith
|
|
60
63
|
optix --> corono
|
|
64
|
+
optix --> coronachrome
|
|
61
65
|
```
|
|
62
66
|
|
|
63
67
|
## Installation
|
|
@@ -10,6 +10,7 @@ from optixstuff.detector import (
|
|
|
10
10
|
dark_current,
|
|
11
11
|
read_noise,
|
|
12
12
|
)
|
|
13
|
+
from optixstuff.disperser import AbstractDisperser, LensletDisperser
|
|
13
14
|
from optixstuff.exposure import ExposureConfig
|
|
14
15
|
from optixstuff.optical_elements import (
|
|
15
16
|
AbstractOpticalElement,
|
|
@@ -24,6 +25,7 @@ from optixstuff.yippy_coronagraph import YippyCoronagraph
|
|
|
24
25
|
__all__ = [
|
|
25
26
|
"AbstractCoronagraph",
|
|
26
27
|
"AbstractDetector",
|
|
28
|
+
"AbstractDisperser",
|
|
27
29
|
"AbstractOpticalElement",
|
|
28
30
|
"AbstractPrimary",
|
|
29
31
|
"AbstractScalarCoronagraph",
|
|
@@ -32,6 +34,7 @@ __all__ = [
|
|
|
32
34
|
"Detector",
|
|
33
35
|
"ExposureConfig",
|
|
34
36
|
"IdealDetector",
|
|
37
|
+
"LensletDisperser",
|
|
35
38
|
"OpticalPath",
|
|
36
39
|
"SimplePrimary",
|
|
37
40
|
"SpectralThroughput",
|
|
@@ -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 = '1.0
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Detector abstractions and concrete implementations."""
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
import abc
|
|
5
4
|
from typing import final
|
|
6
5
|
|
|
@@ -154,6 +153,37 @@ class AbstractDetector(eqx.Module):
|
|
|
154
153
|
prng_key, image_rate * exposure_time_s * self.quantum_efficiency
|
|
155
154
|
)
|
|
156
155
|
|
|
156
|
+
def noise_variance(self, image_rate: Array, exposure_time_s: ArrayLike) -> Array:
|
|
157
|
+
"""Deterministic per-pixel total noise variance in electrons^2.
|
|
158
|
+
|
|
159
|
+
The expected variance at each pixel for an incident photon-rate image
|
|
160
|
+
-- the deterministic companion to :meth:`readout`. Combines source shot
|
|
161
|
+
noise (Poisson on detected electrons) with the source-independent floor
|
|
162
|
+
(dark current, clock-induced charge, read noise)::
|
|
163
|
+
|
|
164
|
+
N = QE * rate * t
|
|
165
|
+
+ dark_rate * t
|
|
166
|
+
+ CIC_rate * n_frames
|
|
167
|
+
+ read_noise^2 * n_frames
|
|
168
|
+
|
|
169
|
+
with ``n_frames = ceil(t / frame_time_s)``. Use ``1 / noise_variance(...)``
|
|
170
|
+
as inverse-variance weights for least-squares spectral extraction or its
|
|
171
|
+
GLS covariance, where the shot term carries the wavelength dependence.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
image_rate: Incident photon rate [ph/s/pixel], any shape.
|
|
175
|
+
exposure_time_s: Exposure time in seconds.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Per-pixel noise variance [electrons^2], same shape as image_rate.
|
|
179
|
+
"""
|
|
180
|
+
n_frames = jnp.ceil(exposure_time_s / self.frame_time_s)
|
|
181
|
+
shot = self.quantum_efficiency * image_rate * exposure_time_s
|
|
182
|
+
dark = self.dark_current_rate_e_per_s * exposure_time_s
|
|
183
|
+
cic = self.clock_induced_charge_rate_e_per_frame * n_frames
|
|
184
|
+
read = self.read_noise_e**2 * n_frames
|
|
185
|
+
return shot + dark + cic + read
|
|
186
|
+
|
|
157
187
|
@abc.abstractmethod
|
|
158
188
|
def readout_noise_electrons(
|
|
159
189
|
self,
|
|
@@ -196,7 +226,9 @@ def dark_current(
|
|
|
196
226
|
Returns:
|
|
197
227
|
Dark current electrons, shape (ny, nx).
|
|
198
228
|
"""
|
|
199
|
-
return jax.random.poisson(
|
|
229
|
+
return jax.random.poisson(
|
|
230
|
+
prng_key, dark_current_rate_e_per_s * exposure_time_s, shape=shape
|
|
231
|
+
)
|
|
200
232
|
|
|
201
233
|
|
|
202
234
|
def clock_induced_charge(
|
|
@@ -216,7 +248,9 @@ def clock_induced_charge(
|
|
|
216
248
|
Returns:
|
|
217
249
|
CIC electrons, shape (ny, nx).
|
|
218
250
|
"""
|
|
219
|
-
return jax.random.poisson(
|
|
251
|
+
return jax.random.poisson(
|
|
252
|
+
prng_key, clock_induced_charge_rate_e_per_frame * num_frames, shape=shape
|
|
253
|
+
)
|
|
220
254
|
|
|
221
255
|
|
|
222
256
|
def read_noise(
|
|
@@ -281,7 +315,9 @@ class IdealDetector(AbstractDetector):
|
|
|
281
315
|
self.quantum_efficiency = quantum_efficiency
|
|
282
316
|
self.dark_current_rate_e_per_s = dark_current_rate_e_per_s
|
|
283
317
|
self.read_noise_e = read_noise_e
|
|
284
|
-
self.clock_induced_charge_rate_e_per_frame =
|
|
318
|
+
self.clock_induced_charge_rate_e_per_frame = (
|
|
319
|
+
clock_induced_charge_rate_e_per_frame
|
|
320
|
+
)
|
|
285
321
|
self.frame_time_s = frame_time_s
|
|
286
322
|
self.read_time_s = read_time_s
|
|
287
323
|
self.dqe = dqe
|
|
@@ -297,7 +333,9 @@ class IdealDetector(AbstractDetector):
|
|
|
297
333
|
Callers add (read_noise_e^2 * n_reads) / t_exp separately.
|
|
298
334
|
"""
|
|
299
335
|
dark_variance_rate = self.dark_current_rate_e_per_s * n_pix
|
|
300
|
-
cic_variance_rate =
|
|
336
|
+
cic_variance_rate = (
|
|
337
|
+
self.clock_induced_charge_rate_e_per_frame * n_pix / t_photon
|
|
338
|
+
)
|
|
301
339
|
return dark_variance_rate + cic_variance_rate
|
|
302
340
|
|
|
303
341
|
def readout_noise_electrons(
|
|
@@ -373,7 +411,9 @@ class Detector(AbstractDetector):
|
|
|
373
411
|
self.quantum_efficiency = quantum_efficiency
|
|
374
412
|
self.dark_current_rate_e_per_s = dark_current_rate_e_per_s
|
|
375
413
|
self.read_noise_e = read_noise_e
|
|
376
|
-
self.clock_induced_charge_rate_e_per_frame =
|
|
414
|
+
self.clock_induced_charge_rate_e_per_frame = (
|
|
415
|
+
clock_induced_charge_rate_e_per_frame
|
|
416
|
+
)
|
|
377
417
|
self.frame_time_s = frame_time_s
|
|
378
418
|
self.read_time_s = read_time_s
|
|
379
419
|
self.dqe = dqe
|
|
@@ -385,7 +425,9 @@ class Detector(AbstractDetector):
|
|
|
385
425
|
def scalar_noise_rate(self, n_pix: ArrayLike, t_photon: ArrayLike) -> ArrayLike:
|
|
386
426
|
"""Combined dark + CIC noise variance rate."""
|
|
387
427
|
dark_variance_rate = self.dark_current_rate_e_per_s * n_pix
|
|
388
|
-
cic_variance_rate =
|
|
428
|
+
cic_variance_rate = (
|
|
429
|
+
self.clock_induced_charge_rate_e_per_frame * n_pix / t_photon
|
|
430
|
+
)
|
|
389
431
|
return dark_variance_rate + cic_variance_rate
|
|
390
432
|
|
|
391
433
|
def readout_noise_electrons(
|
|
@@ -403,9 +445,7 @@ class Detector(AbstractDetector):
|
|
|
403
445
|
cic_e = clock_induced_charge(
|
|
404
446
|
self.clock_induced_charge_rate_e_per_frame, num_frames, self.shape, key_cic
|
|
405
447
|
)
|
|
406
|
-
read_e = read_noise(
|
|
407
|
-
self.read_noise_e, num_frames, self.shape, key_read
|
|
408
|
-
)
|
|
448
|
+
read_e = read_noise(self.read_noise_e, num_frames, self.shape, key_read)
|
|
409
449
|
return dark_e + cic_e + read_e
|
|
410
450
|
|
|
411
451
|
def readout(
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Disperser hardware descriptors for integral field spectrographs.
|
|
2
|
+
|
|
3
|
+
optixstuff owns the descriptor: the interface plus a cheap closed-form
|
|
4
|
+
scalar/ETC face. The heavy render logic (building the forward operator) lives
|
|
5
|
+
in coronachrome. This mirrors the coronagraph split, so jaxedith and yield
|
|
6
|
+
tools can read IFS hardware info without importing the render engine.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import abc
|
|
10
|
+
|
|
11
|
+
import equinox as eqx
|
|
12
|
+
import jax.numpy as jnp
|
|
13
|
+
from jax import Array
|
|
14
|
+
|
|
15
|
+
from optixstuff.optical_elements import AbstractOpticalElement, ConstantThroughput
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AbstractDisperser(eqx.Module):
|
|
19
|
+
"""Interface for a dispersing IFS element (lenslet array, slicer, MSA).
|
|
20
|
+
|
|
21
|
+
Only the scalar/ETC face is defined here. Render geometry lives in
|
|
22
|
+
coronachrome and dispatches on the concrete descriptor type.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@abc.abstractmethod
|
|
26
|
+
def spectral_resolution(self, wavelength_nm):
|
|
27
|
+
"""Resolving power R = lambda / dlambda at the given wavelength."""
|
|
28
|
+
|
|
29
|
+
@abc.abstractmethod
|
|
30
|
+
def spectral_sampling(self):
|
|
31
|
+
"""Detector pixels per resolution element."""
|
|
32
|
+
|
|
33
|
+
@abc.abstractmethod
|
|
34
|
+
def n_pix_spread(self, wavelength_min_nm, wavelength_max_nm):
|
|
35
|
+
"""Detector pixels a single spaxel spectrum spans across a band."""
|
|
36
|
+
|
|
37
|
+
@abc.abstractmethod
|
|
38
|
+
def throughput(self, wavelength_nm):
|
|
39
|
+
"""Disperser optical throughput in [0, 1] at the given wavelength."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _polyval_deriv(coeffs, x):
|
|
43
|
+
"""Evaluate the derivative of a descending-order polynomial at x."""
|
|
44
|
+
n = coeffs.shape[0]
|
|
45
|
+
if n <= 1:
|
|
46
|
+
return jnp.zeros_like(jnp.asarray(x, dtype=float))
|
|
47
|
+
powers = jnp.arange(n - 1, 0, -1)
|
|
48
|
+
return jnp.polyval(coeffs[:-1] * powers, x)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LensletDisperser(AbstractDisperser):
|
|
52
|
+
"""Lenslet-array IFS disperser (CRISPY heritage).
|
|
53
|
+
|
|
54
|
+
Config only. The render geometry (IR build) is performed by coronachrome,
|
|
55
|
+
which reads these fields. Scalar/ETC methods derive from
|
|
56
|
+
``dispersion_coeffs`` + ``pix_per_reselt`` so the dispersion model is the
|
|
57
|
+
single source of truth.
|
|
58
|
+
|
|
59
|
+
``psflet_params[0]`` is the PSFlet core width in detector pixels (Gaussian
|
|
60
|
+
``sigma`` or Moffat ``alpha``); any trailing entries are dimensionless shape
|
|
61
|
+
parameters (e.g. Moffat ``beta``). ``psflet_ref_nm`` is the wavelength at
|
|
62
|
+
which that core width is specified: a diffraction-limited spot scales as
|
|
63
|
+
``lambda f / D``, so at fixed pixel scale coronachrome scales the core width
|
|
64
|
+
by ``lambda / psflet_ref_nm`` per wavelength (the shape parameters do not
|
|
65
|
+
scale).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
pitch_m: float
|
|
69
|
+
pixsize_m: float
|
|
70
|
+
angle_rad: float
|
|
71
|
+
lam_ref_nm: float
|
|
72
|
+
pix_per_reselt: float
|
|
73
|
+
dispersion_coeffs: Array = eqx.field(converter=jnp.asarray)
|
|
74
|
+
psflet_params: Array = eqx.field(converter=jnp.asarray)
|
|
75
|
+
psflet_ref_nm: float
|
|
76
|
+
grid_kind: str = eqx.field(static=True)
|
|
77
|
+
n_lenslets: int = eqx.field(static=True)
|
|
78
|
+
psflet_kind: str = eqx.field(static=True)
|
|
79
|
+
detector_shape: tuple[int, int] = eqx.field(static=True)
|
|
80
|
+
throughput_element: AbstractOpticalElement = ConstantThroughput(1.0)
|
|
81
|
+
|
|
82
|
+
def _dispersion_px(self, wavelength_nm):
|
|
83
|
+
"""Spectral-axis detector offset [px] for the wavelength(s)."""
|
|
84
|
+
u = jnp.log(jnp.asarray(wavelength_nm, dtype=float) / self.lam_ref_nm)
|
|
85
|
+
return jnp.polyval(self.dispersion_coeffs, u)
|
|
86
|
+
|
|
87
|
+
def spectral_resolution(self, wavelength_nm):
|
|
88
|
+
"""R = (local px per unit log-lambda) / pixels-per-resolution-element."""
|
|
89
|
+
u = jnp.log(jnp.asarray(wavelength_nm, dtype=float) / self.lam_ref_nm)
|
|
90
|
+
local = jnp.abs(_polyval_deriv(self.dispersion_coeffs, u))
|
|
91
|
+
return local / self.pix_per_reselt
|
|
92
|
+
|
|
93
|
+
def spectral_sampling(self):
|
|
94
|
+
"""Detector pixels per resolution element."""
|
|
95
|
+
return self.pix_per_reselt
|
|
96
|
+
|
|
97
|
+
def n_pix_spread(self, wavelength_min_nm, wavelength_max_nm):
|
|
98
|
+
"""Spectral trace length [px] across a band, plus a PSFlet-width margin."""
|
|
99
|
+
span = jnp.abs(
|
|
100
|
+
self._dispersion_px(wavelength_max_nm)
|
|
101
|
+
- self._dispersion_px(wavelength_min_nm)
|
|
102
|
+
)
|
|
103
|
+
return span + self.psflet_params[0]
|
|
104
|
+
|
|
105
|
+
def throughput(self, wavelength_nm):
|
|
106
|
+
"""Disperser optical throughput in [0, 1], shaped like wavelength_nm.
|
|
107
|
+
|
|
108
|
+
Delegates to the composed throughput element (ConstantThroughput by
|
|
109
|
+
default; SpectralThroughput for a tabulated blaze/transmission curve) and
|
|
110
|
+
broadcasts to the wavelength shape so the output shape is canonical
|
|
111
|
+
regardless of the backing element.
|
|
112
|
+
"""
|
|
113
|
+
w = jnp.asarray(wavelength_nm, dtype=float)
|
|
114
|
+
return self.throughput_element.get_throughput(w) * jnp.ones_like(w)
|
|
@@ -10,6 +10,7 @@ import equinox as eqx
|
|
|
10
10
|
from optixstuff._repr import indent
|
|
11
11
|
from optixstuff.coronagraph import AbstractCoronagraph
|
|
12
12
|
from optixstuff.detector import AbstractDetector, IdealDetector
|
|
13
|
+
from optixstuff.disperser import AbstractDisperser
|
|
13
14
|
from optixstuff.optical_elements import (
|
|
14
15
|
AbstractOpticalElement,
|
|
15
16
|
ConstantThroughput,
|
|
@@ -31,6 +32,7 @@ class OpticalPath(eqx.Module):
|
|
|
31
32
|
between the primary and coronagraph (mirrors, filters, etc.).
|
|
32
33
|
coronagraph: Coronagraph performance model.
|
|
33
34
|
detector: Focal-plane detector model.
|
|
35
|
+
disperser: Optional IFS disperser descriptor; None for imaging mode.
|
|
34
36
|
n_channels: Number of parallel identical optical-path copies
|
|
35
37
|
(AYO shorthand, multiplicative factor on count rates;
|
|
36
38
|
not a spectral channel count). Default 1.0.
|
|
@@ -42,6 +44,7 @@ class OpticalPath(eqx.Module):
|
|
|
42
44
|
attenuating_elements: tuple[AbstractOpticalElement, ...]
|
|
43
45
|
coronagraph: AbstractCoronagraph
|
|
44
46
|
detector: AbstractDetector
|
|
47
|
+
disperser: AbstractDisperser | None = None
|
|
45
48
|
n_channels: float = 1.0
|
|
46
49
|
npix_multiplier: float = 1.0
|
|
47
50
|
|
|
@@ -110,9 +113,7 @@ class OpticalPath(eqx.Module):
|
|
|
110
113
|
return cls(
|
|
111
114
|
primary=SimplePrimary(diameter_m=diameter_m, obscuration=obscuration),
|
|
112
115
|
attenuating_elements=(
|
|
113
|
-
ConstantThroughput(
|
|
114
|
-
throughput=attenuating_throughput, name="optics"
|
|
115
|
-
),
|
|
116
|
+
ConstantThroughput(throughput=attenuating_throughput, name="optics"),
|
|
116
117
|
),
|
|
117
118
|
coronagraph=coro,
|
|
118
119
|
detector=IdealDetector(
|
|
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
|