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.
@@ -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.2.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
- Flux-level optical system abstractions for the HWO direct imaging simulation suite.
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 the **radiometric hardware layer** for HWO simulations. It operates on
65
- **flux** photon rates, throughput fractions, and detector-level noise — providing the
66
- shared interfaces that simulation tools and exposure time calculators build on top of.
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
- The same `OpticalPath` object drives both scalar ETC calculations (`jaxEDITH`) and full
69
- 2D image generation (`coronagraphoto`), ensuring that the hardware model is consistent
70
- across all downstream science products.
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
- `optixstuff` does not model diffraction, wavefront propagation, or E-field interference.
75
- That level of physical optics belongs to tools like [dLux](https://github.com/LouisDesdoigts/dLux)
76
- and [HCIPy](https://github.com/ehpor/hcipy), which generate PSFs from first principles.
77
-
78
- `optixstuff` and these wavefront tools are **complementary**: dLux/HCIPy compute the PSFs,
79
- which are delivered as yield input packages (YIPs). `optixstuff` consumes those PSFs as
80
- flux patterns (via [yippy](https://github.com/CoreySpohn/yippy)) and composes them with
81
- the rest of the observatory throughput chain, detector QE, noise — to produce
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`, `ConstantThroughputElement`,
92
- `SimpleDetector`
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
- Physical optics
106
- │ (dLux, HCIPy, PROPER) │
107
- │ E-fields PSFs │
108
- └──────────┬────────────────┘
109
- YIP
110
- ┌──────────▼────────────────┐
111
- yippy
112
- │ PSF interpolation │
113
- └──────────┬────────────────┘
114
- flux patterns
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).
@@ -26,6 +26,7 @@ docs = [
26
26
  "sphinx-book-theme",
27
27
  "sphinx-autoapi",
28
28
  "sphinx_autodoc_typehints",
29
+ "sphinxcontrib-mermaid",
29
30
  "ipython",
30
31
  "matplotlib",
31
32
  ]
@@ -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, AbstractScalarOnlyCoronagraph
4
+ from optixstuff.coronagraph import AbstractCoronagraph, AbstractScalarCoronagraph
5
5
  from optixstuff.detector import (
6
6
  AbstractDetector,
7
7
  Detector,
8
- SimpleDetector,
9
- simulate_cic,
10
- simulate_dark_current,
11
- simulate_read_noise,
8
+ IdealDetector,
9
+ clock_induced_charge,
10
+ dark_current,
11
+ read_noise,
12
12
  )
13
- from optixstuff.exposure import Exposure
13
+ from optixstuff.exposure import ExposureConfig
14
14
  from optixstuff.optical_elements import (
15
15
  AbstractOpticalElement,
16
16
  AbstractUniformElement,
17
- ConstantThroughputElement,
18
- LinearThroughputElement,
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
- "AbstractScalarOnlyCoronagraph",
29
+ "AbstractScalarCoronagraph",
31
30
  "AbstractUniformElement",
32
- "ConstantThroughputElement",
31
+ "ConstantThroughput",
33
32
  "Detector",
34
- "Exposure",
35
- "LinearThroughputElement",
36
- "OpticalFilter",
33
+ "ExposureConfig",
34
+ "IdealDetector",
37
35
  "OpticalPath",
38
- "SimpleDetector",
39
36
  "SimplePrimary",
37
+ "SpectralThroughput",
40
38
  "YippyCoronagraph",
41
39
  "__version__",
42
- "simulate_cic",
43
- "simulate_dark_current",
44
- "simulate_read_noise",
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.2.0'
22
- __version_tuple__ = version_tuple = (0, 2, 0)
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 AbstractScalarOnlyCoronagraph(AbstractCoronagraph):
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
- pixel_scale: AbstractVar[float]
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
- dark_current_rate: AbstractVar[float]
29
+ dark_current_rate_e_per_s: AbstractVar[float]
30
30
  """Dark current rate in electrons/pixel/second."""
31
31
 
32
- read_noise_electrons: AbstractVar[float]
32
+ read_noise_e: AbstractVar[float]
33
33
  """Read noise in electrons RMS per pixel per read."""
34
34
 
35
- cic_rate: AbstractVar[float]
35
+ clock_induced_charge_rate_e_per_frame: AbstractVar[float]
36
36
  """Clock-induced charge in electrons/pixel/frame."""
37
37
 
38
- frame_time: AbstractVar[float]
38
+ frame_time_s: AbstractVar[float]
39
39
  """Integration time per frame/read in seconds."""
40
40
 
41
- read_time: AbstractVar[float]
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
- exposure_time: ArrayLike,
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
- exposure_time: Exposure time in seconds.
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
- exposure_time: ArrayLike,
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
- exposure_time: Exposure time in seconds.
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 * exposure_time)
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
- exposure_time: ArrayLike,
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
- exposure_time: Exposure time in seconds.
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 * exposure_time * self.quantum_efficiency
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
- exposure_time: ArrayLike,
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
- exposure_time: Exposure time in seconds.
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 simulate_dark_current(
183
- dark_current_rate: float,
184
- exposure_time: ArrayLike,
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
- dark_current_rate: Dark current rate in electrons/s/pixel.
192
- exposure_time: Exposure time in seconds.
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, dark_current_rate * exposure_time, shape=shape)
199
+ return jax.random.poisson(prng_key, dark_current_rate_e_per_s * exposure_time_s, shape=shape)
200
200
 
201
201
 
202
- def simulate_cic(
203
- cic_rate: float,
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
- cic_rate: CIC rate in electrons/pixel/frame.
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, cic_rate * num_frames, shape=shape)
219
+ return jax.random.poisson(prng_key, clock_induced_charge_rate_e_per_frame * num_frames, shape=shape)
220
220
 
221
221
 
222
- def simulate_read_noise(
223
- read_noise: float,
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
- read_noise: Read noise in electrons/pixel/read.
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 = read_noise * jnp.sqrt(num_frames)
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 SimpleDetector(AbstractDetector):
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
- pixel_scale: float
256
+ pixel_scale_arcsec: float
257
257
  quantum_efficiency: float
258
- dark_current_rate: float
259
- read_noise_electrons: float
260
- cic_rate: float
261
- frame_time: float
262
- read_time: float
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
- pixel_scale: float,
268
+ pixel_scale_arcsec: float,
269
269
  shape: tuple[int, int],
270
270
  quantum_efficiency: float = 1.0,
271
- dark_current_rate: float = 0.0,
272
- read_noise_electrons: float = 0.0,
273
- cic_rate: float = 0.0,
274
- frame_time: float = 1.0,
275
- read_time: float = 0.05,
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.pixel_scale = pixel_scale
279
+ self.pixel_scale_arcsec = pixel_scale_arcsec
280
280
  self.shape = shape
281
281
  self.quantum_efficiency = quantum_efficiency
282
- self.dark_current_rate = dark_current_rate
283
- self.read_noise_electrons = read_noise_electrons
284
- self.cic_rate = cic_rate
285
- self.frame_time = frame_time
286
- self.read_time = read_time
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 (read_noise^2 * n_reads) / t_exp separately.
297
+ Callers add (read_noise_e^2 * n_reads) / t_exp separately.
298
298
  """
299
- dark_variance_rate = self.dark_current_rate * n_pix
300
- cic_variance_rate = self.cic_rate * n_pix / t_photon
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
- exposure_time: ArrayLike,
305
+ exposure_time_s: ArrayLike,
306
306
  prng_key: Array,
307
307
  ) -> Array:
308
- """Dark current only -- SimpleDetector has no CIC or read noise."""
309
- return simulate_dark_current(
310
- self.dark_current_rate, exposure_time, self.shape, prng_key
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
- exposure_time: ArrayLike,
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, exposure_time, key_src)
322
- noise = self.readout_noise_electrons(exposure_time, key_noise)
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"SimpleDetector({ny}x{nx} @ {self.pixel_scale:.3g} arcsec/px, "
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.dark_current_rate:.2g} e-/s/px)"
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(exposure_time / frame_time)`` is
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 exposure_time is traced.
345
+ ConcretizationTypeError when exposure_time_s is traced.
346
346
  """
347
347
 
348
- pixel_scale: float
348
+ pixel_scale_arcsec: float
349
349
  quantum_efficiency: float
350
- dark_current_rate: float
351
- read_noise_electrons: float
352
- cic_rate: float
353
- frame_time: float
354
- read_time: float
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
- pixel_scale: float,
360
+ pixel_scale_arcsec: float,
361
361
  shape: tuple[int, int],
362
362
  quantum_efficiency: float = 1.0,
363
- dark_current_rate: float = 0.0,
364
- read_noise_electrons: float = 0.0,
365
- cic_rate: float = 0.0,
366
- frame_time: float = 1.0,
367
- read_time: float = 0.05,
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.pixel_scale = pixel_scale
371
+ self.pixel_scale_arcsec = pixel_scale_arcsec
372
372
  self.shape = shape
373
373
  self.quantum_efficiency = quantum_efficiency
374
- self.dark_current_rate = dark_current_rate
375
- self.read_noise_electrons = read_noise_electrons
376
- self.cic_rate = cic_rate
377
- self.frame_time = frame_time
378
- self.read_time = read_time
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.dark_current_rate * n_pix
388
- cic_variance_rate = self.cic_rate * n_pix / t_photon
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
- exposure_time: ArrayLike,
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
- dark = simulate_dark_current(
399
- self.dark_current_rate, exposure_time, self.shape, key_dark
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(exposure_time / self.frame_time)
403
- cic = simulate_cic(self.cic_rate, num_frames, self.shape, key_cic)
404
- read = simulate_read_noise(
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
- return dark + cic + read
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
- exposure_time: ArrayLike,
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, exposure_time, key_src)
418
- noise = self.readout_noise_electrons(exposure_time, key_noise)
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.pixel_scale:.3g} arcsec/px, "
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.dark_current_rate:.2g} e-/s/px, "
428
- f"RN={self.read_noise_electrons:.2g} e-/read, "
429
- f"CIC={self.cic_rate:.2g} e-/frame, "
430
- f"frame_time={self.frame_time:.3g} s)"
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
- """Exposure: physical parameters defining a single detector integration.
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 Exposure(eqx.Module):
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 Exposure.
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 = Exposure.in_axes(central_wavelength_nm=0, bin_width_nm=0)
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 ConstantThroughputElement(AbstractUniformElement):
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"ConstantThroughputElement(name={self.name!r}, "
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 LinearThroughputElement(AbstractUniformElement):
89
- """An optical element with linearly interpolated wavelength-dependent throughput.
88
+ class SpectralThroughput(AbstractUniformElement):
89
+ """Wavelength-dependent throughput defined by sampled (wavelength, throughput) pairs.
90
90
 
91
- Throughput is specified at a set of wavelengths and linearly
92
- interpolated between them. Extrapolation returns zero outside the
93
- defined wavelength range.
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 wavelength/throughput pairs."""
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"LinearThroughputElement(wl={wl_min:.0f}-{wl_max:.0f} nm, "
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, SimpleDetector
12
+ from optixstuff.detector import AbstractDetector, IdealDetector
13
13
  from optixstuff.optical_elements import (
14
14
  AbstractOpticalElement,
15
- ConstantThroughputElement,
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
- dark_current_rate: float = 0.0,
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:`ConstantThroughputElement`). Default
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
- dark_current_rate: Default ``0.0`` e-/s/px (perfect detector;
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
- ConstantThroughputElement(
113
+ ConstantThroughput(
114
114
  throughput=attenuating_throughput, name="optics"
115
115
  ),
116
116
  ),
117
117
  coronagraph=coro,
118
- detector=SimpleDetector(
119
- pixel_scale=pixel_scale_arcsec,
118
+ detector=IdealDetector(
119
+ pixel_scale_arcsec=pixel_scale_arcsec,
120
120
  shape=detector_shape,
121
121
  quantum_efficiency=quantum_efficiency,
122
- dark_current_rate=dark_current_rate,
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"pixel_scale={float(self.pixel_scale_lod):.3g} lambda/D/px, "
200
+ f"pixel_scale_arcsec={float(self.pixel_scale_lod):.3g} lambda/D/px, "
201
201
  f"PSF {ny}x{nx})"
202
202
  )
@@ -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