optixstuff 1.3.0__tar.gz → 1.5.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.3.0 → optixstuff-1.5.0}/CHANGELOG.md +15 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/CITATION.cff +1 -1
- {optixstuff-1.3.0 → optixstuff-1.5.0}/PKG-INFO +1 -1
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/__init__.py +2 -1
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/_version.py +2 -2
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/detector.py +35 -4
- optixstuff-1.5.0/src/optixstuff/primary.py +160 -0
- optixstuff-1.3.0/src/optixstuff/primary.py +0 -67
- {optixstuff-1.3.0 → optixstuff-1.5.0}/.gitignore +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/.pre-commit-config.yaml +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/.readthedocs.yaml +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/.zenodo.json +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/LICENSE +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/README.md +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/pyproject.toml +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/_repr.py +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/coronagraph.py +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/disperser.py +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/exposure.py +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/optical_elements.py +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/optical_path.py +0 -0
- {optixstuff-1.3.0 → optixstuff-1.5.0}/src/optixstuff/yippy_coronagraph.py +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0](https://github.com/CoreySpohn/optixstuff/compare/v1.4.0...v1.5.0) (2026-06-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **detector:** wavelength-dependent QE via optional qe_curve ([bf7bc6f](https://github.com/CoreySpohn/optixstuff/commit/bf7bc6f8272ff2d011551f3b1b78243c8ada5572))
|
|
9
|
+
* **primary:** add SegmentedPrimary carrying hex segment geometry ([fe7cab9](https://github.com/CoreySpohn/optixstuff/commit/fe7cab9bb6153874b029ead7d2cc888ca9e8709b))
|
|
10
|
+
|
|
11
|
+
## [1.4.0](https://github.com/CoreySpohn/optixstuff/compare/v1.3.0...v1.4.0) (2026-06-23)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Miscellaneous Chores
|
|
15
|
+
|
|
16
|
+
* release 1.4.0 ([39c1d25](https://github.com/CoreySpohn/optixstuff/commit/39c1d25741f3aaa47984d32e0e6c7917933a59ba))
|
|
17
|
+
|
|
3
18
|
## [1.3.0](https://github.com/CoreySpohn/optixstuff/compare/v1.2.0...v1.3.0) (2026-06-23)
|
|
4
19
|
|
|
5
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optixstuff
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.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
|
|
@@ -19,7 +19,7 @@ from optixstuff.optical_elements import (
|
|
|
19
19
|
SpectralThroughput,
|
|
20
20
|
)
|
|
21
21
|
from optixstuff.optical_path import OpticalPath
|
|
22
|
-
from optixstuff.primary import AbstractPrimary, SimplePrimary
|
|
22
|
+
from optixstuff.primary import AbstractPrimary, SegmentedPrimary, SimplePrimary
|
|
23
23
|
from optixstuff.yippy_coronagraph import YippyCoronagraph
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
@@ -36,6 +36,7 @@ __all__ = [
|
|
|
36
36
|
"IdealDetector",
|
|
37
37
|
"LensletDisperser",
|
|
38
38
|
"OpticalPath",
|
|
39
|
+
"SegmentedPrimary",
|
|
39
40
|
"SimplePrimary",
|
|
40
41
|
"SpectralThroughput",
|
|
41
42
|
"YippyCoronagraph",
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.5.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 5, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -4,6 +4,7 @@ import abc
|
|
|
4
4
|
from typing import final
|
|
5
5
|
|
|
6
6
|
import equinox as eqx
|
|
7
|
+
import interpax
|
|
7
8
|
import jax
|
|
8
9
|
import jax.numpy as jnp
|
|
9
10
|
from equinox import AbstractVar
|
|
@@ -392,6 +393,7 @@ class Detector(AbstractDetector):
|
|
|
392
393
|
read_time_s: float
|
|
393
394
|
dqe: float
|
|
394
395
|
shape: tuple[int, int] = eqx.field(static=True)
|
|
396
|
+
_qe_interp: interpax.Interpolator1D | None
|
|
395
397
|
|
|
396
398
|
def __init__(
|
|
397
399
|
self,
|
|
@@ -404,8 +406,16 @@ class Detector(AbstractDetector):
|
|
|
404
406
|
frame_time_s: float = 1.0,
|
|
405
407
|
read_time_s: float = 0.05,
|
|
406
408
|
dqe: float = 0.0,
|
|
409
|
+
qe_curve: tuple[ArrayLike, ArrayLike] | None = None,
|
|
407
410
|
) -> None:
|
|
408
|
-
"""Create a full detector with all noise sources.
|
|
411
|
+
"""Create a full detector with all noise sources.
|
|
412
|
+
|
|
413
|
+
Supplying ``qe_curve`` as a ``(wavelengths_nm, qe)`` pair makes
|
|
414
|
+
:meth:`get_qe` interpolate it (linear, zero outside the measured range)
|
|
415
|
+
for wavelength-dependent throughput/ETC use; the scalar
|
|
416
|
+
``quantum_efficiency`` remains the operating-point QE used by the
|
|
417
|
+
per-call (monochromatic) readout path.
|
|
418
|
+
"""
|
|
409
419
|
self.pixel_scale_arcsec = pixel_scale_arcsec
|
|
410
420
|
self.shape = shape
|
|
411
421
|
self.quantum_efficiency = quantum_efficiency
|
|
@@ -417,10 +427,26 @@ class Detector(AbstractDetector):
|
|
|
417
427
|
self.frame_time_s = frame_time_s
|
|
418
428
|
self.read_time_s = read_time_s
|
|
419
429
|
self.dqe = dqe
|
|
430
|
+
if qe_curve is None:
|
|
431
|
+
self._qe_interp = None
|
|
432
|
+
else:
|
|
433
|
+
wavelengths_nm, qe = qe_curve
|
|
434
|
+
self._qe_interp = interpax.Interpolator1D(
|
|
435
|
+
jnp.asarray(wavelengths_nm),
|
|
436
|
+
jnp.asarray(qe),
|
|
437
|
+
method="linear",
|
|
438
|
+
extrap=jnp.array([0.0, 0.0]),
|
|
439
|
+
)
|
|
420
440
|
|
|
421
441
|
def get_qe(self, wavelength_nm: ArrayLike) -> ArrayLike:
|
|
422
|
-
"""
|
|
423
|
-
|
|
442
|
+
"""Quantum efficiency at a wavelength.
|
|
443
|
+
|
|
444
|
+
Interpolates the ``qe_curve`` if one was supplied (linear, zero outside
|
|
445
|
+
the measured range); otherwise returns the scalar ``quantum_efficiency``.
|
|
446
|
+
"""
|
|
447
|
+
if self._qe_interp is None:
|
|
448
|
+
return self.quantum_efficiency
|
|
449
|
+
return self._qe_interp(wavelength_nm)
|
|
424
450
|
|
|
425
451
|
def scalar_noise_rate(self, n_pix: ArrayLike, t_photon: ArrayLike) -> ArrayLike:
|
|
426
452
|
"""Combined dark + CIC noise variance rate."""
|
|
@@ -463,9 +489,14 @@ class Detector(AbstractDetector):
|
|
|
463
489
|
def __repr__(self) -> str:
|
|
464
490
|
"""One-line summary of shape, plate scale, QE, and noise sources."""
|
|
465
491
|
ny, nx = self.shape
|
|
492
|
+
qe_str = (
|
|
493
|
+
"spectral"
|
|
494
|
+
if self._qe_interp is not None
|
|
495
|
+
else f"{self.quantum_efficiency:.2f}"
|
|
496
|
+
)
|
|
466
497
|
return (
|
|
467
498
|
f"Detector({ny}x{nx} @ {self.pixel_scale_arcsec:.3g} arcsec/px, "
|
|
468
|
-
f"QE={
|
|
499
|
+
f"QE={qe_str}, "
|
|
469
500
|
f"dark={self.dark_current_rate_e_per_s:.2g} e-/s/px, "
|
|
470
501
|
f"RN={self.read_noise_e:.2g} e-/read, "
|
|
471
502
|
f"CIC={self.clock_induced_charge_rate_e_per_frame:.2g} e-/frame, "
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Primary mirror abstractions."""
|
|
2
|
+
|
|
3
|
+
import equinox as eqx
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
from equinox import AbstractVar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AbstractPrimary(eqx.Module):
|
|
9
|
+
"""Abstract interface for a primary aperture.
|
|
10
|
+
|
|
11
|
+
Any concrete implementation must provide the diameter and collecting
|
|
12
|
+
area of the primary mirror as scalar values in SI units. These are
|
|
13
|
+
consumed by exposure time calculators and simulation tools alike.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
diameter_m: AbstractVar[float]
|
|
17
|
+
"""Primary mirror diameter in metres."""
|
|
18
|
+
|
|
19
|
+
area_m2: AbstractVar[float]
|
|
20
|
+
"""Effective collecting area in square metres."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SimplePrimary(AbstractPrimary):
|
|
24
|
+
"""A simple circular primary mirror with a central obscuration.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
diameter_m: Primary mirror diameter in metres.
|
|
28
|
+
obscuration: Linear obscuration fraction (0 = no obscuration).
|
|
29
|
+
shape_factor: Fraction of unobscured area that is collecting
|
|
30
|
+
(accounts for struts, segment gaps, etc.). Default 1.0.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_diameter_m: float
|
|
34
|
+
obscuration: float
|
|
35
|
+
shape_factor: float
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
diameter_m: float,
|
|
40
|
+
obscuration: float = 0.0,
|
|
41
|
+
shape_factor: float = 1.0,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Create a simple circular primary mirror."""
|
|
44
|
+
self._diameter_m = diameter_m
|
|
45
|
+
self.obscuration = obscuration
|
|
46
|
+
self.shape_factor = shape_factor
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def diameter_m(self) -> float:
|
|
50
|
+
"""Primary mirror diameter in metres."""
|
|
51
|
+
return self._diameter_m
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def area_m2(self) -> float:
|
|
55
|
+
"""Effective collecting area in square metres."""
|
|
56
|
+
r = self._diameter_m / 2.0
|
|
57
|
+
gross_area = jnp.pi * r**2
|
|
58
|
+
return gross_area * (1.0 - self.obscuration**2) * self.shape_factor
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
"""One-line summary of diameter, obscuration, and effective area."""
|
|
62
|
+
return (
|
|
63
|
+
f"SimplePrimary(D={self._diameter_m:.3g} m, "
|
|
64
|
+
f"obs={self.obscuration:.3g}, "
|
|
65
|
+
f"shape_factor={self.shape_factor:.3g}, "
|
|
66
|
+
f"area={float(self.area_m2):.3g} m^2)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _hex_axial_coords(n_rings: int) -> list[tuple[int, int]]:
|
|
71
|
+
"""Axial (q, r) centres of a hex-packed n-ring layout.
|
|
72
|
+
|
|
73
|
+
Yields ``1 + 3 n (n + 1)`` coordinates, centre first.
|
|
74
|
+
"""
|
|
75
|
+
coords = [(0, 0)]
|
|
76
|
+
for q in range(-n_rings, n_rings + 1):
|
|
77
|
+
for r in range(-n_rings, n_rings + 1):
|
|
78
|
+
if max(abs(q), abs(r), abs(q + r)) <= n_rings and not (q == 0 and r == 0):
|
|
79
|
+
coords.append((q, r))
|
|
80
|
+
return coords
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SegmentedPrimary(AbstractPrimary):
|
|
84
|
+
"""A segmented hex primary that carries pupil geometry, not just scalars.
|
|
85
|
+
|
|
86
|
+
Beyond the diameter and collecting area every primary provides, this
|
|
87
|
+
describes the segment layout -- ring count, segment count, gap, shape -- so a
|
|
88
|
+
diffraction backend (e.g. dLux) can build the pupil from it. optixstuff only
|
|
89
|
+
DESCRIBES the geometry; it does not propagate wavefronts.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
diameter_m: Circumscribing diameter in metres.
|
|
93
|
+
area_m2: Effective collecting area in square metres (gap/fill corrected).
|
|
94
|
+
n_rings: Number of segment rings around the centre segment.
|
|
95
|
+
n_segments: Total segment count (``1 + 3 n (n + 1)`` for a full layout).
|
|
96
|
+
segment_gap_m: Inter-segment optical gap in metres.
|
|
97
|
+
segment_shape: Segment shape; only ``"hexagon"`` is supported today.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
_diameter_m: float
|
|
101
|
+
_area_m2: float
|
|
102
|
+
segment_gap_m: float
|
|
103
|
+
n_rings: int = eqx.field(static=True)
|
|
104
|
+
n_segments: int = eqx.field(static=True)
|
|
105
|
+
segment_shape: str = eqx.field(static=True)
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
diameter_m: float,
|
|
110
|
+
area_m2: float,
|
|
111
|
+
n_rings: int,
|
|
112
|
+
n_segments: int,
|
|
113
|
+
segment_gap_m: float,
|
|
114
|
+
segment_shape: str = "hexagon",
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Create a segmented hex primary from its geometry."""
|
|
117
|
+
self._diameter_m = diameter_m
|
|
118
|
+
self._area_m2 = area_m2
|
|
119
|
+
self.segment_gap_m = segment_gap_m
|
|
120
|
+
self.n_rings = n_rings
|
|
121
|
+
self.n_segments = n_segments
|
|
122
|
+
self.segment_shape = segment_shape
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def diameter_m(self) -> float:
|
|
126
|
+
"""Circumscribing diameter in metres."""
|
|
127
|
+
return self._diameter_m
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def area_m2(self) -> float:
|
|
131
|
+
"""Effective collecting area in square metres."""
|
|
132
|
+
return self._area_m2
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def segment_flat_to_flat_m(self) -> float:
|
|
136
|
+
"""Segment flat-to-flat size in metres for the circumscribing layout."""
|
|
137
|
+
return self._diameter_m / (2 * self.n_rings + 1)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def segment_centres_m(self):
|
|
141
|
+
"""``(n_segments, 2)`` array of segment centre (x, y) positions in metres."""
|
|
142
|
+
if self.segment_shape != "hexagon":
|
|
143
|
+
msg = f"segment_centres_m undefined for shape {self.segment_shape!r}"
|
|
144
|
+
raise NotImplementedError(msg)
|
|
145
|
+
axial = _hex_axial_coords(self.n_rings)
|
|
146
|
+
spacing = self.segment_flat_to_flat_m + self.segment_gap_m
|
|
147
|
+
qs = jnp.array([q for q, _ in axial])
|
|
148
|
+
rs = jnp.array([r for _, r in axial])
|
|
149
|
+
xs = spacing * (qs + rs / 2.0)
|
|
150
|
+
ys = spacing * (jnp.sqrt(3.0) / 2.0) * rs
|
|
151
|
+
return jnp.stack([xs, ys], axis=-1)
|
|
152
|
+
|
|
153
|
+
def __repr__(self) -> str:
|
|
154
|
+
"""One-line summary of diameter, segment layout, and effective area."""
|
|
155
|
+
return (
|
|
156
|
+
f"SegmentedPrimary(D={self._diameter_m:.3g} m, "
|
|
157
|
+
f"{self.n_segments} {self.segment_shape} segs, "
|
|
158
|
+
f"{self.n_rings} rings, gap={self.segment_gap_m * 1e3:.3g} mm, "
|
|
159
|
+
f"area={float(self._area_m2):.3g} m^2)"
|
|
160
|
+
)
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
"""Primary mirror abstractions."""
|
|
2
|
-
|
|
3
|
-
import equinox as eqx
|
|
4
|
-
import jax.numpy as jnp
|
|
5
|
-
from equinox import AbstractVar
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AbstractPrimary(eqx.Module):
|
|
9
|
-
"""Abstract interface for a primary aperture.
|
|
10
|
-
|
|
11
|
-
Any concrete implementation must provide the diameter and collecting
|
|
12
|
-
area of the primary mirror as scalar values in SI units. These are
|
|
13
|
-
consumed by exposure time calculators and simulation tools alike.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
diameter_m: AbstractVar[float]
|
|
17
|
-
"""Primary mirror diameter in metres."""
|
|
18
|
-
|
|
19
|
-
area_m2: AbstractVar[float]
|
|
20
|
-
"""Effective collecting area in square metres."""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class SimplePrimary(AbstractPrimary):
|
|
24
|
-
"""A simple circular primary mirror with a central obscuration.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
diameter_m: Primary mirror diameter in metres.
|
|
28
|
-
obscuration: Linear obscuration fraction (0 = no obscuration).
|
|
29
|
-
shape_factor: Fraction of unobscured area that is collecting
|
|
30
|
-
(accounts for struts, segment gaps, etc.). Default 1.0.
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
_diameter_m: float
|
|
34
|
-
obscuration: float
|
|
35
|
-
shape_factor: float
|
|
36
|
-
|
|
37
|
-
def __init__(
|
|
38
|
-
self,
|
|
39
|
-
diameter_m: float,
|
|
40
|
-
obscuration: float = 0.0,
|
|
41
|
-
shape_factor: float = 1.0,
|
|
42
|
-
) -> None:
|
|
43
|
-
"""Create a simple circular primary mirror."""
|
|
44
|
-
self._diameter_m = diameter_m
|
|
45
|
-
self.obscuration = obscuration
|
|
46
|
-
self.shape_factor = shape_factor
|
|
47
|
-
|
|
48
|
-
@property
|
|
49
|
-
def diameter_m(self) -> float:
|
|
50
|
-
"""Primary mirror diameter in metres."""
|
|
51
|
-
return self._diameter_m
|
|
52
|
-
|
|
53
|
-
@property
|
|
54
|
-
def area_m2(self) -> float:
|
|
55
|
-
"""Effective collecting area in square metres."""
|
|
56
|
-
r = self._diameter_m / 2.0
|
|
57
|
-
gross_area = jnp.pi * r**2
|
|
58
|
-
return gross_area * (1.0 - self.obscuration**2) * self.shape_factor
|
|
59
|
-
|
|
60
|
-
def __repr__(self) -> str:
|
|
61
|
-
"""One-line summary of diameter, obscuration, and effective area."""
|
|
62
|
-
return (
|
|
63
|
-
f"SimplePrimary(D={self._diameter_m:.3g} m, "
|
|
64
|
-
f"obs={self.obscuration:.3g}, "
|
|
65
|
-
f"shape_factor={self.shape_factor:.3g}, "
|
|
66
|
-
f"area={float(self.area_m2):.3g} m^2)"
|
|
67
|
-
)
|
|
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
|