optixstuff 0.2.0__tar.gz → 1.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-0.2.0 → optixstuff-1.0.0}/CHANGELOG.md +13 -0
- {optixstuff-0.2.0 → optixstuff-1.0.0}/PKG-INFO +35 -42
- optixstuff-1.0.0/README.md +71 -0
- {optixstuff-0.2.0 → optixstuff-1.0.0}/pyproject.toml +1 -0
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/__init__.py +16 -18
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/_version.py +2 -2
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/coronagraph.py +1 -1
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/detector.py +101 -99
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/exposure.py +4 -4
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/optical_elements.py +12 -49
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/optical_path.py +9 -9
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/yippy_coronagraph.py +1 -1
- optixstuff-0.2.0/README.md +0 -79
- {optixstuff-0.2.0 → optixstuff-1.0.0}/.gitignore +0 -0
- {optixstuff-0.2.0 → optixstuff-1.0.0}/LICENSE +0 -0
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/_repr.py +0 -0
- {optixstuff-0.2.0 → optixstuff-1.0.0}/src/optixstuff/primary.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0](https://github.com/CoreySpohn/optixstuff/compare/v0.2.0...v1.0.0) (2026-05-25)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* Docs and refactor to standardize function calls
|
|
9
|
+
* SimpleDetector -> IdealDetector (clarifies that the class ignores wavelength-dependent QE and noise contributions); ConstantThroughputElement -> ConstantThroughput and LinearThroughputElement -> LinearThroughput (the Element suffix was redundant with the inheritance from AbstractUniformElement); AbstractScalarOnlyCoronagraph -> AbstractScalarCoronagraph (the Only qualifier was redundant); Exposure -> ExposureConfig (settings-struct role explicit). README ecosystem diagram replaced with Mermaid and sphinxcontrib-mermaid added to the docs build.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* Docs and refactor to standardize function calls ([5e5ab2c](https://github.com/CoreySpohn/optixstuff/commit/5e5ab2c28ac3d4dc387ba7eee7407740f95648c3))
|
|
14
|
+
* rename detector/throughput classes and Exposure for naming consistency ([10beac7](https://github.com/CoreySpohn/optixstuff/commit/10beac7755e38c1743aa91931739453c9a5f50ed))
|
|
15
|
+
|
|
3
16
|
## [0.2.0](https://github.com/CoreySpohn/optixstuff/compare/v0.1.0...v0.2.0) (2026-05-24)
|
|
4
17
|
|
|
5
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optixstuff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.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
|
|
@@ -48,6 +48,7 @@ Requires-Dist: sphinx; extra == 'docs'
|
|
|
48
48
|
Requires-Dist: sphinx-autoapi; extra == 'docs'
|
|
49
49
|
Requires-Dist: sphinx-autodoc-typehints; extra == 'docs'
|
|
50
50
|
Requires-Dist: sphinx-book-theme; extra == 'docs'
|
|
51
|
+
Requires-Dist: sphinxcontrib-mermaid; extra == 'docs'
|
|
51
52
|
Provides-Extra: test
|
|
52
53
|
Requires-Dist: hypothesis; extra == 'test'
|
|
53
54
|
Requires-Dist: nox; extra == 'test'
|
|
@@ -57,29 +58,32 @@ Description-Content-Type: text/markdown
|
|
|
57
58
|
|
|
58
59
|
# optixstuff
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
Shared hardware objects — with standard values — for the HWO direct-imaging
|
|
62
|
+
simulation suite.
|
|
61
63
|
|
|
62
64
|
## What optixstuff is
|
|
63
65
|
|
|
64
|
-
`optixstuff` is
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
`optixstuff` is a **thin shared dependency** that defines the observatory's
|
|
67
|
+
hardware as composable JAX modules: primary aperture, throughput-affecting
|
|
68
|
+
elements, coronagraph backend, detector. Its job is to be the single source of
|
|
69
|
+
truth for the hardware configuration that downstream tools consume.
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
2D image
|
|
70
|
-
|
|
71
|
+
Both [coronagraphoto](https://github.com/CoreySpohn/coronagraphoto)
|
|
72
|
+
(2D image simulation) and [jaxEDITH](https://github.com/CoreySpohn/jaxedith)
|
|
73
|
+
(exposure-time and yield calculations) import the same `OpticalPath` class and
|
|
74
|
+
the same detector / throughput / primary types from optixstuff. Change a value
|
|
75
|
+
here and both downstream tools pick it up on the next import.
|
|
71
76
|
|
|
72
77
|
## What optixstuff is *not*
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
science-level outputs.
|
|
79
|
+
- **Not a wavefront tool.** Diffraction, E-field propagation, and PSFs-from-first-principles
|
|
80
|
+
belong to [dLux](https://github.com/LouisDesdoigts/dLux) and [HCIPy](https://github.com/ehpor/hcipy).
|
|
81
|
+
- **Not a PSF interpolator.** That's [yippy](https://github.com/CoreySpohn/yippy)'s job;
|
|
82
|
+
optixstuff wraps a PSF backend via `YippyCoronagraph` but does not compute PSFs.
|
|
83
|
+
- **Not a scene model.** Stars, planets, disks, and zodi live in
|
|
84
|
+
[skyscapes](https://github.com/CoreySpohn/skyscapes).
|
|
85
|
+
- **Not a simulator.** Downstream tools (coronagraphoto, jaxEDITH) consume an
|
|
86
|
+
`OpticalPath` to produce images or count rates.
|
|
83
87
|
|
|
84
88
|
## Architecture
|
|
85
89
|
|
|
@@ -88,8 +92,8 @@ Built on [JAX](https://github.com/google/jax) and
|
|
|
88
92
|
|
|
89
93
|
- **Abstract interfaces** — `AbstractPrimary`, `AbstractOpticalElement`,
|
|
90
94
|
`AbstractCoronagraph`, `AbstractDetector`
|
|
91
|
-
- **Concrete implementations** — `SimplePrimary`, `
|
|
92
|
-
`
|
|
95
|
+
- **Concrete implementations** — `SimplePrimary`, `ConstantThroughput`,
|
|
96
|
+
`IdealDetector`
|
|
93
97
|
- **Container** — `OpticalPath`, a composable hardware configuration passed to all
|
|
94
98
|
simulators
|
|
95
99
|
|
|
@@ -100,29 +104,18 @@ time-dependent detector degradation) can use them without breaking the interface
|
|
|
100
104
|
|
|
101
105
|
### Ecosystem position
|
|
102
106
|
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
┌──────────────────────▼────────────────────────┐
|
|
116
|
-
│ optixstuff │
|
|
117
|
-
│ Telescope • Coronagraph • Detector • OpticalPath │
|
|
118
|
-
│ Throughput chains • QE • Noise rates │
|
|
119
|
-
└────────┬──────────────────────┬───────────────┘
|
|
120
|
-
│ │
|
|
121
|
-
┌──────────▼──────────┐ ┌────────▼───────────────┐
|
|
122
|
-
│ jaxEDITH │ │ coronagraphoto │
|
|
123
|
-
│ Scalar count rates │ │ 2D image simulation │
|
|
124
|
-
│ Exposure times │ │ Multi-epoch scenes │
|
|
125
|
-
└─────────────────────┘ └────────────────────────┘
|
|
107
|
+
```mermaid
|
|
108
|
+
flowchart TB
|
|
109
|
+
physopt["<b>Physical optics</b><br/>dLux · HCIPy · PROPER<br/>E-fields → PSFs"]
|
|
110
|
+
yippy["<b>yippy</b><br/>PSF interpolation"]
|
|
111
|
+
optix["<b>optixstuff</b><br/>Telescope · Coronagraph · Detector · OpticalPath<br/>Throughput chains · QE · Noise rates"]
|
|
112
|
+
jaxedith["<b>jaxEDITH</b><br/>Scalar count rates<br/>Exposure-time calculations"]
|
|
113
|
+
corono["<b>coronagraphoto</b><br/>2D image simulation<br/>Multi-epoch scenes"]
|
|
114
|
+
|
|
115
|
+
physopt -- YIP --> yippy
|
|
116
|
+
yippy -- flux patterns --> optix
|
|
117
|
+
optix --> jaxedith
|
|
118
|
+
optix --> corono
|
|
126
119
|
```
|
|
127
120
|
|
|
128
121
|
## Installation
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# optixstuff
|
|
2
|
+
|
|
3
|
+
Shared hardware objects — with standard values — for the HWO direct-imaging
|
|
4
|
+
simulation suite.
|
|
5
|
+
|
|
6
|
+
## What optixstuff is
|
|
7
|
+
|
|
8
|
+
`optixstuff` is a **thin shared dependency** that defines the observatory's
|
|
9
|
+
hardware as composable JAX modules: primary aperture, throughput-affecting
|
|
10
|
+
elements, coronagraph backend, detector. Its job is to be the single source of
|
|
11
|
+
truth for the hardware configuration that downstream tools consume.
|
|
12
|
+
|
|
13
|
+
Both [coronagraphoto](https://github.com/CoreySpohn/coronagraphoto)
|
|
14
|
+
(2D image simulation) and [jaxEDITH](https://github.com/CoreySpohn/jaxedith)
|
|
15
|
+
(exposure-time and yield calculations) import the same `OpticalPath` class and
|
|
16
|
+
the same detector / throughput / primary types from optixstuff. Change a value
|
|
17
|
+
here and both downstream tools pick it up on the next import.
|
|
18
|
+
|
|
19
|
+
## What optixstuff is *not*
|
|
20
|
+
|
|
21
|
+
- **Not a wavefront tool.** Diffraction, E-field propagation, and PSFs-from-first-principles
|
|
22
|
+
belong to [dLux](https://github.com/LouisDesdoigts/dLux) and [HCIPy](https://github.com/ehpor/hcipy).
|
|
23
|
+
- **Not a PSF interpolator.** That's [yippy](https://github.com/CoreySpohn/yippy)'s job;
|
|
24
|
+
optixstuff wraps a PSF backend via `YippyCoronagraph` but does not compute PSFs.
|
|
25
|
+
- **Not a scene model.** Stars, planets, disks, and zodi live in
|
|
26
|
+
[skyscapes](https://github.com/CoreySpohn/skyscapes).
|
|
27
|
+
- **Not a simulator.** Downstream tools (coronagraphoto, jaxEDITH) consume an
|
|
28
|
+
`OpticalPath` to produce images or count rates.
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
Built on [JAX](https://github.com/google/jax) and
|
|
33
|
+
[Equinox](https://github.com/patrick-kidger/equinox), `optixstuff` provides:
|
|
34
|
+
|
|
35
|
+
- **Abstract interfaces** — `AbstractPrimary`, `AbstractOpticalElement`,
|
|
36
|
+
`AbstractCoronagraph`, `AbstractDetector`
|
|
37
|
+
- **Concrete implementations** — `SimplePrimary`, `ConstantThroughput`,
|
|
38
|
+
`IdealDetector`
|
|
39
|
+
- **Container** — `OpticalPath`, a composable hardware configuration passed to all
|
|
40
|
+
simulators
|
|
41
|
+
|
|
42
|
+
Every abstract method accepts three fidelity axes — **wavelength**, **position**, and
|
|
43
|
+
**time** — with defaults so that simple implementations can ignore unused axes while
|
|
44
|
+
future high-fidelity models (wavelength-dependent coatings, position-dependent vignetting,
|
|
45
|
+
time-dependent detector degradation) can use them without breaking the interface.
|
|
46
|
+
|
|
47
|
+
### Ecosystem position
|
|
48
|
+
|
|
49
|
+
```mermaid
|
|
50
|
+
flowchart TB
|
|
51
|
+
physopt["<b>Physical optics</b><br/>dLux · HCIPy · PROPER<br/>E-fields → PSFs"]
|
|
52
|
+
yippy["<b>yippy</b><br/>PSF interpolation"]
|
|
53
|
+
optix["<b>optixstuff</b><br/>Telescope · Coronagraph · Detector · OpticalPath<br/>Throughput chains · QE · Noise rates"]
|
|
54
|
+
jaxedith["<b>jaxEDITH</b><br/>Scalar count rates<br/>Exposure-time calculations"]
|
|
55
|
+
corono["<b>coronagraphoto</b><br/>2D image simulation<br/>Multi-epoch scenes"]
|
|
56
|
+
|
|
57
|
+
physopt -- YIP --> yippy
|
|
58
|
+
yippy -- flux patterns --> optix
|
|
59
|
+
optix --> jaxedith
|
|
60
|
+
optix --> corono
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install optixstuff
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Status
|
|
70
|
+
|
|
71
|
+
This package is in early development (pre-v0.1.0).
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
"""optixstuff -- Hardware abstractions for the HWO simulation suite."""
|
|
2
2
|
|
|
3
3
|
from optixstuff._version import __version__
|
|
4
|
-
from optixstuff.coronagraph import AbstractCoronagraph,
|
|
4
|
+
from optixstuff.coronagraph import AbstractCoronagraph, AbstractScalarCoronagraph
|
|
5
5
|
from optixstuff.detector import (
|
|
6
6
|
AbstractDetector,
|
|
7
7
|
Detector,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
IdealDetector,
|
|
9
|
+
clock_induced_charge,
|
|
10
|
+
dark_current,
|
|
11
|
+
read_noise,
|
|
12
12
|
)
|
|
13
|
-
from optixstuff.exposure import
|
|
13
|
+
from optixstuff.exposure import ExposureConfig
|
|
14
14
|
from optixstuff.optical_elements import (
|
|
15
15
|
AbstractOpticalElement,
|
|
16
16
|
AbstractUniformElement,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
OpticalFilter,
|
|
17
|
+
ConstantThroughput,
|
|
18
|
+
SpectralThroughput,
|
|
20
19
|
)
|
|
21
20
|
from optixstuff.optical_path import OpticalPath
|
|
22
21
|
from optixstuff.primary import AbstractPrimary, SimplePrimary
|
|
@@ -27,19 +26,18 @@ __all__ = [
|
|
|
27
26
|
"AbstractDetector",
|
|
28
27
|
"AbstractOpticalElement",
|
|
29
28
|
"AbstractPrimary",
|
|
30
|
-
"
|
|
29
|
+
"AbstractScalarCoronagraph",
|
|
31
30
|
"AbstractUniformElement",
|
|
32
|
-
"
|
|
31
|
+
"ConstantThroughput",
|
|
33
32
|
"Detector",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"OpticalFilter",
|
|
33
|
+
"ExposureConfig",
|
|
34
|
+
"IdealDetector",
|
|
37
35
|
"OpticalPath",
|
|
38
|
-
"SimpleDetector",
|
|
39
36
|
"SimplePrimary",
|
|
37
|
+
"SpectralThroughput",
|
|
40
38
|
"YippyCoronagraph",
|
|
41
39
|
"__version__",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
40
|
+
"clock_induced_charge",
|
|
41
|
+
"dark_current",
|
|
42
|
+
"read_noise",
|
|
45
43
|
]
|
|
@@ -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 = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (
|
|
21
|
+
__version__ = version = '1.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -166,7 +166,7 @@ class AbstractCoronagraph(eqx.Module):
|
|
|
166
166
|
...
|
|
167
167
|
|
|
168
168
|
|
|
169
|
-
class
|
|
169
|
+
class AbstractScalarCoronagraph(AbstractCoronagraph):
|
|
170
170
|
"""Base for ETC-only coronagraph models that lack 2D PSF generation.
|
|
171
171
|
|
|
172
172
|
Stubs out the image interface with zero arrays so the class satisfies
|
|
@@ -20,25 +20,25 @@ class AbstractDetector(eqx.Module):
|
|
|
20
20
|
must define the hardware parameters listed as ``AbstractVar`` fields.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
pixel_scale_arcsec: AbstractVar[float]
|
|
24
24
|
"""Detector plate scale in arcsec/pixel."""
|
|
25
25
|
|
|
26
26
|
quantum_efficiency: AbstractVar[float]
|
|
27
27
|
"""Baseline quantum efficiency as a fraction in [0, 1]."""
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
dark_current_rate_e_per_s: AbstractVar[float]
|
|
30
30
|
"""Dark current rate in electrons/pixel/second."""
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
read_noise_e: AbstractVar[float]
|
|
33
33
|
"""Read noise in electrons RMS per pixel per read."""
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
clock_induced_charge_rate_e_per_frame: AbstractVar[float]
|
|
36
36
|
"""Clock-induced charge in electrons/pixel/frame."""
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
frame_time_s: AbstractVar[float]
|
|
39
39
|
"""Integration time per frame/read in seconds."""
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
read_time_s: AbstractVar[float]
|
|
42
42
|
"""Time per read cycle in seconds (for RN^2/t_read in ETC)."""
|
|
43
43
|
|
|
44
44
|
dqe: AbstractVar[float]
|
|
@@ -79,7 +79,7 @@ class AbstractDetector(eqx.Module):
|
|
|
79
79
|
def readout(
|
|
80
80
|
self,
|
|
81
81
|
image_rate: Array,
|
|
82
|
-
|
|
82
|
+
exposure_time_s: ArrayLike,
|
|
83
83
|
prng_key: Array,
|
|
84
84
|
) -> Array:
|
|
85
85
|
"""Apply stochastic noise realization to a photon rate image.
|
|
@@ -89,7 +89,7 @@ class AbstractDetector(eqx.Module):
|
|
|
89
89
|
|
|
90
90
|
Args:
|
|
91
91
|
image_rate: Incident photon rate array in ph/s/pixel.
|
|
92
|
-
|
|
92
|
+
exposure_time_s: ExposureConfig time in seconds.
|
|
93
93
|
prng_key: JAX PRNG key (required, no default).
|
|
94
94
|
|
|
95
95
|
Returns:
|
|
@@ -100,7 +100,7 @@ class AbstractDetector(eqx.Module):
|
|
|
100
100
|
def readout_source_electrons(
|
|
101
101
|
self,
|
|
102
102
|
image_rate: Array,
|
|
103
|
-
|
|
103
|
+
exposure_time_s: ArrayLike,
|
|
104
104
|
prng_key: Array,
|
|
105
105
|
) -> Array:
|
|
106
106
|
"""Poisson-sample incident photons and convert to electrons via QE.
|
|
@@ -113,20 +113,20 @@ class AbstractDetector(eqx.Module):
|
|
|
113
113
|
|
|
114
114
|
Args:
|
|
115
115
|
image_rate: Incident photon rate array in ph/s/pixel.
|
|
116
|
-
|
|
116
|
+
exposure_time_s: ExposureConfig time in seconds.
|
|
117
117
|
prng_key: JAX PRNG key.
|
|
118
118
|
|
|
119
119
|
Returns:
|
|
120
120
|
Photo-electron counts, same shape as image_rate.
|
|
121
121
|
"""
|
|
122
122
|
key_phot, key_qe = jax.random.split(prng_key, 2)
|
|
123
|
-
inc_photons = jax.random.poisson(key_phot, image_rate *
|
|
123
|
+
inc_photons = jax.random.poisson(key_phot, image_rate * exposure_time_s)
|
|
124
124
|
return jax.random.binomial(key_qe, inc_photons, self.quantum_efficiency)
|
|
125
125
|
|
|
126
126
|
def readout_source_electrons_thinned(
|
|
127
127
|
self,
|
|
128
128
|
image_rate: Array,
|
|
129
|
-
|
|
129
|
+
exposure_time_s: ArrayLike,
|
|
130
130
|
prng_key: Array,
|
|
131
131
|
) -> Array:
|
|
132
132
|
"""Fast equivalent of :meth:`readout_source_electrons` via Poisson thinning.
|
|
@@ -144,20 +144,20 @@ class AbstractDetector(eqx.Module):
|
|
|
144
144
|
|
|
145
145
|
Args:
|
|
146
146
|
image_rate: Incident photon rate array in ph/s/pixel.
|
|
147
|
-
|
|
147
|
+
exposure_time_s: ExposureConfig time in seconds.
|
|
148
148
|
prng_key: JAX PRNG key.
|
|
149
149
|
|
|
150
150
|
Returns:
|
|
151
151
|
Photo-electron counts, same shape as image_rate.
|
|
152
152
|
"""
|
|
153
153
|
return jax.random.poisson(
|
|
154
|
-
prng_key, image_rate *
|
|
154
|
+
prng_key, image_rate * exposure_time_s * self.quantum_efficiency
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
@abc.abstractmethod
|
|
158
158
|
def readout_noise_electrons(
|
|
159
159
|
self,
|
|
160
|
-
|
|
160
|
+
exposure_time_s: ArrayLike,
|
|
161
161
|
prng_key: Array,
|
|
162
162
|
) -> Array:
|
|
163
163
|
"""Source-independent detector noise (dark + CIC + read).
|
|
@@ -167,7 +167,7 @@ class AbstractDetector(eqx.Module):
|
|
|
167
167
|
how many sources were co-added via :meth:`readout_source_electrons`.
|
|
168
168
|
|
|
169
169
|
Args:
|
|
170
|
-
|
|
170
|
+
exposure_time_s: ExposureConfig time in seconds.
|
|
171
171
|
prng_key: JAX PRNG key.
|
|
172
172
|
|
|
173
173
|
Returns:
|
|
@@ -179,28 +179,28 @@ class AbstractDetector(eqx.Module):
|
|
|
179
179
|
# -- Pure noise simulation functions -----------------------------------------
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
def dark_current(
|
|
183
|
+
dark_current_rate_e_per_s: float,
|
|
184
|
+
exposure_time_s: ArrayLike,
|
|
185
185
|
shape: tuple[int, int],
|
|
186
186
|
prng_key: Array,
|
|
187
187
|
) -> Array:
|
|
188
188
|
"""Draw dark current electrons from a Poisson distribution.
|
|
189
189
|
|
|
190
190
|
Args:
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
dark_current_rate_e_per_s: Dark current rate in electrons/s/pixel.
|
|
192
|
+
exposure_time_s: ExposureConfig time in seconds.
|
|
193
193
|
shape: Detector shape (ny, nx).
|
|
194
194
|
prng_key: PRNG key.
|
|
195
195
|
|
|
196
196
|
Returns:
|
|
197
197
|
Dark current electrons, shape (ny, nx).
|
|
198
198
|
"""
|
|
199
|
-
return jax.random.poisson(prng_key,
|
|
199
|
+
return jax.random.poisson(prng_key, dark_current_rate_e_per_s * exposure_time_s, shape=shape)
|
|
200
200
|
|
|
201
201
|
|
|
202
|
-
def
|
|
203
|
-
|
|
202
|
+
def clock_induced_charge(
|
|
203
|
+
clock_induced_charge_rate_e_per_frame: float,
|
|
204
204
|
num_frames: ArrayLike,
|
|
205
205
|
shape: tuple[int, int],
|
|
206
206
|
prng_key: Array,
|
|
@@ -208,7 +208,7 @@ def simulate_cic(
|
|
|
208
208
|
"""Draw clock-induced charge electrons from a Poisson distribution.
|
|
209
209
|
|
|
210
210
|
Args:
|
|
211
|
-
|
|
211
|
+
clock_induced_charge_rate_e_per_frame: CIC rate in electrons/pixel/frame.
|
|
212
212
|
num_frames: Number of frames (kept as float for JIT safety).
|
|
213
213
|
shape: Detector shape (ny, nx).
|
|
214
214
|
prng_key: PRNG key.
|
|
@@ -216,11 +216,11 @@ def simulate_cic(
|
|
|
216
216
|
Returns:
|
|
217
217
|
CIC electrons, shape (ny, nx).
|
|
218
218
|
"""
|
|
219
|
-
return jax.random.poisson(prng_key,
|
|
219
|
+
return jax.random.poisson(prng_key, clock_induced_charge_rate_e_per_frame * num_frames, shape=shape)
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
def
|
|
223
|
-
|
|
222
|
+
def read_noise(
|
|
223
|
+
read_noise_e: float,
|
|
224
224
|
num_frames: ArrayLike,
|
|
225
225
|
shape: tuple[int, int],
|
|
226
226
|
prng_key: Array,
|
|
@@ -230,7 +230,7 @@ def simulate_read_noise(
|
|
|
230
230
|
Total read noise sigma = sqrt(num_frames) * read_noise_per_read.
|
|
231
231
|
|
|
232
232
|
Args:
|
|
233
|
-
|
|
233
|
+
read_noise_e: Read noise in electrons/pixel/read.
|
|
234
234
|
num_frames: Number of frames.
|
|
235
235
|
shape: Detector shape (ny, nx).
|
|
236
236
|
prng_key: PRNG key.
|
|
@@ -238,7 +238,7 @@ def simulate_read_noise(
|
|
|
238
238
|
Returns:
|
|
239
239
|
Read noise electrons, shape (ny, nx).
|
|
240
240
|
"""
|
|
241
|
-
sigma =
|
|
241
|
+
sigma = read_noise_e * jnp.sqrt(num_frames)
|
|
242
242
|
return sigma * jax.random.normal(prng_key, shape=shape)
|
|
243
243
|
|
|
244
244
|
|
|
@@ -246,44 +246,44 @@ def simulate_read_noise(
|
|
|
246
246
|
|
|
247
247
|
|
|
248
248
|
@final
|
|
249
|
-
class
|
|
249
|
+
class IdealDetector(AbstractDetector):
|
|
250
250
|
"""Detector with constant QE and minimal noise sources.
|
|
251
251
|
|
|
252
252
|
Suitable for broadband imager studies where wavelength-dependent
|
|
253
253
|
QE variation is not important and CIC/read noise are negligible.
|
|
254
254
|
"""
|
|
255
255
|
|
|
256
|
-
|
|
256
|
+
pixel_scale_arcsec: float
|
|
257
257
|
quantum_efficiency: float
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
258
|
+
dark_current_rate_e_per_s: float
|
|
259
|
+
read_noise_e: float
|
|
260
|
+
clock_induced_charge_rate_e_per_frame: float
|
|
261
|
+
frame_time_s: float
|
|
262
|
+
read_time_s: float
|
|
263
263
|
dqe: float
|
|
264
264
|
shape: tuple[int, int] = eqx.field(static=True)
|
|
265
265
|
|
|
266
266
|
def __init__(
|
|
267
267
|
self,
|
|
268
|
-
|
|
268
|
+
pixel_scale_arcsec: float,
|
|
269
269
|
shape: tuple[int, int],
|
|
270
270
|
quantum_efficiency: float = 1.0,
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
dark_current_rate_e_per_s: float = 0.0,
|
|
272
|
+
read_noise_e: float = 0.0,
|
|
273
|
+
clock_induced_charge_rate_e_per_frame: float = 0.0,
|
|
274
|
+
frame_time_s: float = 1.0,
|
|
275
|
+
read_time_s: float = 0.05,
|
|
276
276
|
dqe: float = 0.0,
|
|
277
277
|
) -> None:
|
|
278
278
|
"""Create a simple constant-QE detector."""
|
|
279
|
-
self.
|
|
279
|
+
self.pixel_scale_arcsec = pixel_scale_arcsec
|
|
280
280
|
self.shape = shape
|
|
281
281
|
self.quantum_efficiency = quantum_efficiency
|
|
282
|
-
self.
|
|
283
|
-
self.
|
|
284
|
-
self.
|
|
285
|
-
self.
|
|
286
|
-
self.
|
|
282
|
+
self.dark_current_rate_e_per_s = dark_current_rate_e_per_s
|
|
283
|
+
self.read_noise_e = read_noise_e
|
|
284
|
+
self.clock_induced_charge_rate_e_per_frame = clock_induced_charge_rate_e_per_frame
|
|
285
|
+
self.frame_time_s = frame_time_s
|
|
286
|
+
self.read_time_s = read_time_s
|
|
287
287
|
self.dqe = dqe
|
|
288
288
|
|
|
289
289
|
def get_qe(self, wavelength_nm: ArrayLike) -> ArrayLike:
|
|
@@ -294,41 +294,41 @@ class SimpleDetector(AbstractDetector):
|
|
|
294
294
|
"""Combined dark + CIC noise variance rate.
|
|
295
295
|
|
|
296
296
|
Read noise is not included here as it scales per-read, not per-second.
|
|
297
|
-
Callers add (
|
|
297
|
+
Callers add (read_noise_e^2 * n_reads) / t_exp separately.
|
|
298
298
|
"""
|
|
299
|
-
dark_variance_rate = self.
|
|
300
|
-
cic_variance_rate = self.
|
|
299
|
+
dark_variance_rate = self.dark_current_rate_e_per_s * n_pix
|
|
300
|
+
cic_variance_rate = self.clock_induced_charge_rate_e_per_frame * n_pix / t_photon
|
|
301
301
|
return dark_variance_rate + cic_variance_rate
|
|
302
302
|
|
|
303
303
|
def readout_noise_electrons(
|
|
304
304
|
self,
|
|
305
|
-
|
|
305
|
+
exposure_time_s: ArrayLike,
|
|
306
306
|
prng_key: Array,
|
|
307
307
|
) -> Array:
|
|
308
|
-
"""Dark current only --
|
|
309
|
-
return
|
|
310
|
-
self.
|
|
308
|
+
"""Dark current only -- IdealDetector has no CIC or read noise."""
|
|
309
|
+
return dark_current(
|
|
310
|
+
self.dark_current_rate_e_per_s, exposure_time_s, self.shape, prng_key
|
|
311
311
|
)
|
|
312
312
|
|
|
313
313
|
def readout(
|
|
314
314
|
self,
|
|
315
315
|
image_rate: Array,
|
|
316
|
-
|
|
316
|
+
exposure_time_s: ArrayLike,
|
|
317
317
|
prng_key: Array,
|
|
318
318
|
) -> Array:
|
|
319
319
|
"""Full detector readout: source electrons + dark current."""
|
|
320
320
|
key_src, key_noise = jax.random.split(prng_key, 2)
|
|
321
|
-
source = self.readout_source_electrons(image_rate,
|
|
322
|
-
noise = self.readout_noise_electrons(
|
|
321
|
+
source = self.readout_source_electrons(image_rate, exposure_time_s, key_src)
|
|
322
|
+
noise = self.readout_noise_electrons(exposure_time_s, key_noise)
|
|
323
323
|
return source + noise
|
|
324
324
|
|
|
325
325
|
def __repr__(self) -> str:
|
|
326
326
|
"""One-line summary of shape, plate scale, QE, and dark current."""
|
|
327
327
|
ny, nx = self.shape
|
|
328
328
|
return (
|
|
329
|
-
f"
|
|
329
|
+
f"IdealDetector({ny}x{nx} @ {self.pixel_scale_arcsec:.3g} arcsec/px, "
|
|
330
330
|
f"QE={self.quantum_efficiency:.2f}, "
|
|
331
|
-
f"dark={self.
|
|
331
|
+
f"dark={self.dark_current_rate_e_per_s:.2g} e-/s/px)"
|
|
332
332
|
)
|
|
333
333
|
|
|
334
334
|
|
|
@@ -340,42 +340,42 @@ class Detector(AbstractDetector):
|
|
|
340
340
|
sources matter. Uses Poisson statistics for dark/CIC and Gaussian
|
|
341
341
|
for read noise, matching the coronagraphoto convention.
|
|
342
342
|
|
|
343
|
-
Warning: ``num_frames = jnp.ceil(
|
|
343
|
+
Warning: ``num_frames = jnp.ceil(exposure_time_s / frame_time_s)`` is
|
|
344
344
|
kept as a float. Never cast it to int inside JIT -- that triggers a
|
|
345
|
-
ConcretizationTypeError when
|
|
345
|
+
ConcretizationTypeError when exposure_time_s is traced.
|
|
346
346
|
"""
|
|
347
347
|
|
|
348
|
-
|
|
348
|
+
pixel_scale_arcsec: float
|
|
349
349
|
quantum_efficiency: float
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
350
|
+
dark_current_rate_e_per_s: float
|
|
351
|
+
read_noise_e: float
|
|
352
|
+
clock_induced_charge_rate_e_per_frame: float
|
|
353
|
+
frame_time_s: float
|
|
354
|
+
read_time_s: float
|
|
355
355
|
dqe: float
|
|
356
356
|
shape: tuple[int, int] = eqx.field(static=True)
|
|
357
357
|
|
|
358
358
|
def __init__(
|
|
359
359
|
self,
|
|
360
|
-
|
|
360
|
+
pixel_scale_arcsec: float,
|
|
361
361
|
shape: tuple[int, int],
|
|
362
362
|
quantum_efficiency: float = 1.0,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
363
|
+
dark_current_rate_e_per_s: float = 0.0,
|
|
364
|
+
read_noise_e: float = 0.0,
|
|
365
|
+
clock_induced_charge_rate_e_per_frame: float = 0.0,
|
|
366
|
+
frame_time_s: float = 1.0,
|
|
367
|
+
read_time_s: float = 0.05,
|
|
368
368
|
dqe: float = 0.0,
|
|
369
369
|
) -> None:
|
|
370
370
|
"""Create a full detector with all noise sources."""
|
|
371
|
-
self.
|
|
371
|
+
self.pixel_scale_arcsec = pixel_scale_arcsec
|
|
372
372
|
self.shape = shape
|
|
373
373
|
self.quantum_efficiency = quantum_efficiency
|
|
374
|
-
self.
|
|
375
|
-
self.
|
|
376
|
-
self.
|
|
377
|
-
self.
|
|
378
|
-
self.
|
|
374
|
+
self.dark_current_rate_e_per_s = dark_current_rate_e_per_s
|
|
375
|
+
self.read_noise_e = read_noise_e
|
|
376
|
+
self.clock_induced_charge_rate_e_per_frame = clock_induced_charge_rate_e_per_frame
|
|
377
|
+
self.frame_time_s = frame_time_s
|
|
378
|
+
self.read_time_s = read_time_s
|
|
379
379
|
self.dqe = dqe
|
|
380
380
|
|
|
381
381
|
def get_qe(self, wavelength_nm: ArrayLike) -> ArrayLike:
|
|
@@ -384,48 +384,50 @@ class Detector(AbstractDetector):
|
|
|
384
384
|
|
|
385
385
|
def scalar_noise_rate(self, n_pix: ArrayLike, t_photon: ArrayLike) -> ArrayLike:
|
|
386
386
|
"""Combined dark + CIC noise variance rate."""
|
|
387
|
-
dark_variance_rate = self.
|
|
388
|
-
cic_variance_rate = self.
|
|
387
|
+
dark_variance_rate = self.dark_current_rate_e_per_s * n_pix
|
|
388
|
+
cic_variance_rate = self.clock_induced_charge_rate_e_per_frame * n_pix / t_photon
|
|
389
389
|
return dark_variance_rate + cic_variance_rate
|
|
390
390
|
|
|
391
391
|
def readout_noise_electrons(
|
|
392
392
|
self,
|
|
393
|
-
|
|
393
|
+
exposure_time_s: ArrayLike,
|
|
394
394
|
prng_key: Array,
|
|
395
395
|
) -> Array:
|
|
396
396
|
"""Dark current + CIC + read noise (source-independent)."""
|
|
397
397
|
key_dark, key_cic, key_read = jax.random.split(prng_key, 3)
|
|
398
|
-
|
|
399
|
-
self.
|
|
398
|
+
dark_e = dark_current(
|
|
399
|
+
self.dark_current_rate_e_per_s, exposure_time_s, self.shape, key_dark
|
|
400
400
|
)
|
|
401
401
|
# num_frames stays as a traced float -- never cast to int
|
|
402
|
-
num_frames = jnp.ceil(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
self.read_noise_electrons, num_frames, self.shape, key_read
|
|
402
|
+
num_frames = jnp.ceil(exposure_time_s / self.frame_time_s)
|
|
403
|
+
cic_e = clock_induced_charge(
|
|
404
|
+
self.clock_induced_charge_rate_e_per_frame, num_frames, self.shape, key_cic
|
|
406
405
|
)
|
|
407
|
-
|
|
406
|
+
read_e = read_noise(
|
|
407
|
+
self.read_noise_e, num_frames, self.shape, key_read
|
|
408
|
+
)
|
|
409
|
+
return dark_e + cic_e + read_e
|
|
408
410
|
|
|
409
411
|
def readout(
|
|
410
412
|
self,
|
|
411
413
|
image_rate: Array,
|
|
412
|
-
|
|
414
|
+
exposure_time_s: ArrayLike,
|
|
413
415
|
prng_key: Array,
|
|
414
416
|
) -> Array:
|
|
415
417
|
"""Full detector readout: source electrons + all noise sources."""
|
|
416
418
|
key_src, key_noise = jax.random.split(prng_key, 2)
|
|
417
|
-
source = self.readout_source_electrons(image_rate,
|
|
418
|
-
noise = self.readout_noise_electrons(
|
|
419
|
+
source = self.readout_source_electrons(image_rate, exposure_time_s, key_src)
|
|
420
|
+
noise = self.readout_noise_electrons(exposure_time_s, key_noise)
|
|
419
421
|
return source + noise
|
|
420
422
|
|
|
421
423
|
def __repr__(self) -> str:
|
|
422
424
|
"""One-line summary of shape, plate scale, QE, and noise sources."""
|
|
423
425
|
ny, nx = self.shape
|
|
424
426
|
return (
|
|
425
|
-
f"Detector({ny}x{nx} @ {self.
|
|
427
|
+
f"Detector({ny}x{nx} @ {self.pixel_scale_arcsec:.3g} arcsec/px, "
|
|
426
428
|
f"QE={self.quantum_efficiency:.2f}, "
|
|
427
|
-
f"dark={self.
|
|
428
|
-
f"RN={self.
|
|
429
|
-
f"CIC={self.
|
|
430
|
-
f"
|
|
429
|
+
f"dark={self.dark_current_rate_e_per_s:.2g} e-/s/px, "
|
|
430
|
+
f"RN={self.read_noise_e:.2g} e-/read, "
|
|
431
|
+
f"CIC={self.clock_induced_charge_rate_e_per_frame:.2g} e-/frame, "
|
|
432
|
+
f"frame_time_s={self.frame_time_s:.3g} s)"
|
|
431
433
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""ExposureConfig: physical parameters defining a single detector integration.
|
|
2
2
|
|
|
3
3
|
Relocated from coronagraphoto so the same object can be consumed by
|
|
4
4
|
both image-level (coronagraphoto) and analytic (jaxedith) code paths.
|
|
@@ -10,7 +10,7 @@ import equinox as eqx
|
|
|
10
10
|
import jax.numpy as jnp
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class ExposureConfig(eqx.Module):
|
|
14
14
|
"""The physical parameters defining a single detector integration.
|
|
15
15
|
|
|
16
16
|
All fields can be scalars (for a single event) or vectors (for a
|
|
@@ -25,11 +25,11 @@ class Exposure(eqx.Module):
|
|
|
25
25
|
|
|
26
26
|
@classmethod
|
|
27
27
|
def in_axes(cls, **vectorized_axes):
|
|
28
|
-
"""Helper to generate in_axes structure for JAX vmap over an
|
|
28
|
+
"""Helper to generate in_axes structure for JAX vmap over an ExposureConfig.
|
|
29
29
|
|
|
30
30
|
Usage:
|
|
31
31
|
# Vectorize over wavelength (axis 0), keep time constant
|
|
32
|
-
in_axes =
|
|
32
|
+
in_axes = ExposureConfig.in_axes(central_wavelength_nm=0, bin_width_nm=0)
|
|
33
33
|
"""
|
|
34
34
|
spec_dict = {f.name: None for f in fields(cls)}
|
|
35
35
|
spec_dict.update(vectorized_axes)
|
|
@@ -62,7 +62,7 @@ class AbstractUniformElement(AbstractOpticalElement):
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
@final
|
|
65
|
-
class
|
|
65
|
+
class ConstantThroughput(AbstractUniformElement):
|
|
66
66
|
"""An optical element with wavelength-independent throughput.
|
|
67
67
|
|
|
68
68
|
Useful for modeling simple attenuators, beamsplitters, or as a
|
|
@@ -79,18 +79,21 @@ class ConstantThroughputElement(AbstractUniformElement):
|
|
|
79
79
|
def __repr__(self) -> str:
|
|
80
80
|
"""One-line summary of throughput value."""
|
|
81
81
|
return (
|
|
82
|
-
f"
|
|
82
|
+
f"ConstantThroughput(name={self.name!r}, "
|
|
83
83
|
f"throughput={self.throughput:.3g})"
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
@final
|
|
88
|
-
class
|
|
89
|
-
"""
|
|
88
|
+
class SpectralThroughput(AbstractUniformElement):
|
|
89
|
+
"""Wavelength-dependent throughput defined by sampled (wavelength, throughput) pairs.
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
Linear interpolation between samples; throughput is zero outside
|
|
92
|
+
the defined wavelength range.
|
|
93
|
+
|
|
94
|
+
Represents any tabulated wavelength-dependent throughput in the
|
|
95
|
+
optical path: bandpass filters, dichroics, mirror reflectivity,
|
|
96
|
+
coating losses, ADCs, blocking filters, etc.
|
|
94
97
|
"""
|
|
95
98
|
|
|
96
99
|
wavelengths_nm: Array
|
|
@@ -98,7 +101,7 @@ class LinearThroughputElement(AbstractUniformElement):
|
|
|
98
101
|
interp: interpax.Interpolator1D
|
|
99
102
|
|
|
100
103
|
def __init__(self, wavelengths_nm: Array, throughputs: Array) -> None:
|
|
101
|
-
"""Create a throughput element from sampled
|
|
104
|
+
"""Create a spectral throughput element from sampled pairs."""
|
|
102
105
|
self.wavelengths_nm = wavelengths_nm
|
|
103
106
|
self.throughputs = throughputs
|
|
104
107
|
self.interp = interpax.Interpolator1D(
|
|
@@ -117,46 +120,6 @@ class LinearThroughputElement(AbstractUniformElement):
|
|
|
117
120
|
t_min = float(self.throughputs.min())
|
|
118
121
|
t_max = float(self.throughputs.max())
|
|
119
122
|
return (
|
|
120
|
-
f"
|
|
123
|
+
f"SpectralThroughput(wl={wl_min:.0f}-{wl_max:.0f} nm, "
|
|
121
124
|
f"n={n}, throughput={t_min:.3g}-{t_max:.3g})"
|
|
122
125
|
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@final
|
|
126
|
-
class OpticalFilter(AbstractUniformElement):
|
|
127
|
-
"""A bandpass filter with linearly interpolated transmittance.
|
|
128
|
-
|
|
129
|
-
Structurally identical to LinearThroughputElement but semantically
|
|
130
|
-
distinct -- represents a spectral bandpass selection rather than a
|
|
131
|
-
reflective coating or attenuator.
|
|
132
|
-
"""
|
|
133
|
-
|
|
134
|
-
wavelengths_nm: Array
|
|
135
|
-
transmittances: Array
|
|
136
|
-
interp: interpax.Interpolator1D
|
|
137
|
-
|
|
138
|
-
def __init__(self, wavelengths_nm: Array, transmittances: Array) -> None:
|
|
139
|
-
"""Create an optical filter from sampled wavelength/transmittance pairs."""
|
|
140
|
-
self.wavelengths_nm = wavelengths_nm
|
|
141
|
-
self.transmittances = transmittances
|
|
142
|
-
self.interp = interpax.Interpolator1D(
|
|
143
|
-
wavelengths_nm,
|
|
144
|
-
transmittances,
|
|
145
|
-
method="linear",
|
|
146
|
-
extrap=jnp.array([0.0, 0.0]),
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
def get_throughput(self, wavelength_nm: ArrayLike) -> ArrayLike:
|
|
150
|
-
"""Interpolate filter transmittance at the requested wavelength."""
|
|
151
|
-
return self.interp(wavelength_nm)
|
|
152
|
-
|
|
153
|
-
def __repr__(self) -> str:
|
|
154
|
-
"""One-line summary of filter passband and peak transmittance."""
|
|
155
|
-
n = int(self.wavelengths_nm.shape[0])
|
|
156
|
-
wl_min = float(self.wavelengths_nm.min())
|
|
157
|
-
wl_max = float(self.wavelengths_nm.max())
|
|
158
|
-
t_peak = float(self.transmittances.max())
|
|
159
|
-
return (
|
|
160
|
-
f"OpticalFilter(wl={wl_min:.0f}-{wl_max:.0f} nm, "
|
|
161
|
-
f"n={n}, peak T={t_peak:.3g})"
|
|
162
|
-
)
|
|
@@ -9,10 +9,10 @@ import equinox as eqx
|
|
|
9
9
|
|
|
10
10
|
from optixstuff._repr import indent
|
|
11
11
|
from optixstuff.coronagraph import AbstractCoronagraph
|
|
12
|
-
from optixstuff.detector import AbstractDetector,
|
|
12
|
+
from optixstuff.detector import AbstractDetector, IdealDetector
|
|
13
13
|
from optixstuff.optical_elements import (
|
|
14
14
|
AbstractOpticalElement,
|
|
15
|
-
|
|
15
|
+
ConstantThroughput,
|
|
16
16
|
)
|
|
17
17
|
from optixstuff.primary import AbstractPrimary, SimplePrimary
|
|
18
18
|
|
|
@@ -56,7 +56,7 @@ class OpticalPath(eqx.Module):
|
|
|
56
56
|
detector_shape: tuple[int, int] = (512, 512),
|
|
57
57
|
pixel_scale_arcsec: float = 0.01,
|
|
58
58
|
quantum_efficiency: float = 0.9,
|
|
59
|
-
|
|
59
|
+
dark_current_rate_e_per_s: float = 0.0,
|
|
60
60
|
n_channels: float = 1.0,
|
|
61
61
|
npix_multiplier: float = 1.0,
|
|
62
62
|
) -> OpticalPath:
|
|
@@ -78,14 +78,14 @@ class OpticalPath(eqx.Module):
|
|
|
78
78
|
EAC1 baseline).
|
|
79
79
|
obscuration: Linear central-obscuration fraction. Default 0.
|
|
80
80
|
attenuating_throughput: Combined throughput of the optical
|
|
81
|
-
chain (one :class:`
|
|
81
|
+
chain (one :class:`ConstantThroughput`). Default
|
|
82
82
|
``1.0`` -- a perfect path; override for realistic studies.
|
|
83
83
|
detector_shape: Detector ``(ny, nx)`` in pixels. Default
|
|
84
84
|
``(512, 512)``.
|
|
85
85
|
pixel_scale_arcsec: Detector plate scale [arcsec/px]. Default
|
|
86
86
|
``0.01``.
|
|
87
87
|
quantum_efficiency: Default ``0.9``.
|
|
88
|
-
|
|
88
|
+
dark_current_rate_e_per_s: Default ``0.0`` e-/s/px (perfect detector;
|
|
89
89
|
callers add realistic noise when needed).
|
|
90
90
|
n_channels: AYO parallel-path multiplier. Default ``1.0``.
|
|
91
91
|
npix_multiplier: IFS signal-spread multiplier. Default ``1.0``.
|
|
@@ -110,16 +110,16 @@ class OpticalPath(eqx.Module):
|
|
|
110
110
|
return cls(
|
|
111
111
|
primary=SimplePrimary(diameter_m=diameter_m, obscuration=obscuration),
|
|
112
112
|
attenuating_elements=(
|
|
113
|
-
|
|
113
|
+
ConstantThroughput(
|
|
114
114
|
throughput=attenuating_throughput, name="optics"
|
|
115
115
|
),
|
|
116
116
|
),
|
|
117
117
|
coronagraph=coro,
|
|
118
|
-
detector=
|
|
119
|
-
|
|
118
|
+
detector=IdealDetector(
|
|
119
|
+
pixel_scale_arcsec=pixel_scale_arcsec,
|
|
120
120
|
shape=detector_shape,
|
|
121
121
|
quantum_efficiency=quantum_efficiency,
|
|
122
|
-
|
|
122
|
+
dark_current_rate_e_per_s=dark_current_rate_e_per_s,
|
|
123
123
|
),
|
|
124
124
|
n_channels=n_channels,
|
|
125
125
|
npix_multiplier=npix_multiplier,
|
|
@@ -197,6 +197,6 @@ class YippyCoronagraph(AbstractCoronagraph):
|
|
|
197
197
|
return (
|
|
198
198
|
f"YippyCoronagraph(IWA={float(self.IWA):.3g}, "
|
|
199
199
|
f"OWA={float(self.OWA):.3g} lambda/D, "
|
|
200
|
-
f"
|
|
200
|
+
f"pixel_scale_arcsec={float(self.pixel_scale_lod):.3g} lambda/D/px, "
|
|
201
201
|
f"PSF {ny}x{nx})"
|
|
202
202
|
)
|
optixstuff-0.2.0/README.md
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# optixstuff
|
|
2
|
-
|
|
3
|
-
Flux-level optical system abstractions for the HWO direct imaging simulation suite.
|
|
4
|
-
|
|
5
|
-
## What optixstuff is
|
|
6
|
-
|
|
7
|
-
`optixstuff` is the **radiometric hardware layer** for HWO simulations. It operates on
|
|
8
|
-
**flux** — photon rates, throughput fractions, and detector-level noise — providing the
|
|
9
|
-
shared interfaces that simulation tools and exposure time calculators build on top of.
|
|
10
|
-
|
|
11
|
-
The same `OpticalPath` object drives both scalar ETC calculations (`jaxEDITH`) and full
|
|
12
|
-
2D image generation (`coronagraphoto`), ensuring that the hardware model is consistent
|
|
13
|
-
across all downstream science products.
|
|
14
|
-
|
|
15
|
-
## What optixstuff is *not*
|
|
16
|
-
|
|
17
|
-
`optixstuff` does not model diffraction, wavefront propagation, or E-field interference.
|
|
18
|
-
That level of physical optics belongs to tools like [dLux](https://github.com/LouisDesdoigts/dLux)
|
|
19
|
-
and [HCIPy](https://github.com/ehpor/hcipy), which generate PSFs from first principles.
|
|
20
|
-
|
|
21
|
-
`optixstuff` and these wavefront tools are **complementary**: dLux/HCIPy compute the PSFs,
|
|
22
|
-
which are delivered as yield input packages (YIPs). `optixstuff` consumes those PSFs as
|
|
23
|
-
flux patterns (via [yippy](https://github.com/CoreySpohn/yippy)) and composes them with
|
|
24
|
-
the rest of the observatory — throughput chain, detector QE, noise — to produce
|
|
25
|
-
science-level outputs.
|
|
26
|
-
|
|
27
|
-
## Architecture
|
|
28
|
-
|
|
29
|
-
Built on [JAX](https://github.com/google/jax) and
|
|
30
|
-
[Equinox](https://github.com/patrick-kidger/equinox), `optixstuff` provides:
|
|
31
|
-
|
|
32
|
-
- **Abstract interfaces** — `AbstractPrimary`, `AbstractOpticalElement`,
|
|
33
|
-
`AbstractCoronagraph`, `AbstractDetector`
|
|
34
|
-
- **Concrete implementations** — `SimplePrimary`, `ConstantThroughputElement`,
|
|
35
|
-
`SimpleDetector`
|
|
36
|
-
- **Container** — `OpticalPath`, a composable hardware configuration passed to all
|
|
37
|
-
simulators
|
|
38
|
-
|
|
39
|
-
Every abstract method accepts three fidelity axes — **wavelength**, **position**, and
|
|
40
|
-
**time** — with defaults so that simple implementations can ignore unused axes while
|
|
41
|
-
future high-fidelity models (wavelength-dependent coatings, position-dependent vignetting,
|
|
42
|
-
time-dependent detector degradation) can use them without breaking the interface.
|
|
43
|
-
|
|
44
|
-
### Ecosystem position
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
┌───────────────────────────┐
|
|
48
|
-
│ Physical optics │
|
|
49
|
-
│ (dLux, HCIPy, PROPER) │
|
|
50
|
-
│ E-fields → PSFs │
|
|
51
|
-
└──────────┬────────────────┘
|
|
52
|
-
│ YIP
|
|
53
|
-
┌──────────▼────────────────┐
|
|
54
|
-
│ yippy │
|
|
55
|
-
│ PSF interpolation │
|
|
56
|
-
└──────────┬────────────────┘
|
|
57
|
-
│ flux patterns
|
|
58
|
-
┌──────────────────────▼────────────────────────┐
|
|
59
|
-
│ optixstuff │
|
|
60
|
-
│ Telescope • Coronagraph • Detector • OpticalPath │
|
|
61
|
-
│ Throughput chains • QE • Noise rates │
|
|
62
|
-
└────────┬──────────────────────┬───────────────┘
|
|
63
|
-
│ │
|
|
64
|
-
┌──────────▼──────────┐ ┌────────▼───────────────┐
|
|
65
|
-
│ jaxEDITH │ │ coronagraphoto │
|
|
66
|
-
│ Scalar count rates │ │ 2D image simulation │
|
|
67
|
-
│ Exposure times │ │ Multi-epoch scenes │
|
|
68
|
-
└─────────────────────┘ └────────────────────────┘
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Installation
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
pip install optixstuff
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Status
|
|
78
|
-
|
|
79
|
-
This package is in early development (pre-v0.1.0).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|