optixstuff 1.0.2__tar.gz → 1.2.0__tar.gz

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