Skip to content

Commit 7ce5700

Browse files
jacobdparkerclaude
andauthored
Make Instrument indexable via the na.Indexable mixin (#61)
* Make Instrument indexable via the na.Indexable mixin AbstractInstrument now inherits named_arrays.Indexable, giving every instrument a recursive named __getitem__/isel/shape that indexes its fields (rebuilding dataclasses via dataclasses.replace, so the result is a new object and cached properties such as `system` are dropped). A single channel can then be selected generically with instrument.isel(channel=...) (equivalently instrument[{axis_channel: ...}]), which extends to any future vectorized selection without a bespoke method per axis. Requires named-arrays >= 1.6 (na.Indexable, #180/#181); pin bumped accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Bumped `codecov-action` from v5 to v7 to fix the coverage upload step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c05a458 commit 7ce5700

4 files changed

Lines changed: 46 additions & 2 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
pytest --cov=. --cov-report=xml --cov-report=html
4747
4848
- name: Upload coverage to Codecov
49-
uses: codecov/codecov-action@v5
49+
uses: codecov/codecov-action@v7
5050
with:
5151
token: ${{ secrets.CODECOV_TOKEN }}
5252
files: coverage.xml

esis/optics/_instruments/_instruments.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
@dataclasses.dataclass(eq=False, repr=False)
2222
class AbstractInstrument(
23+
na.Indexable,
2324
optika.mixins.Printable,
2425
optika.mixins.Rollable,
2526
optika.mixins.Yawable,

esis/optics/_instruments/_instruments_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,46 @@ class TestInstrument(
119119
AbstractTestAbstractInstrument,
120120
):
121121
pass
122+
123+
124+
@pytest.mark.parametrize("channel", [0, 1, 2, 3])
125+
def test_isel(channel: int):
126+
"""Selecting a channel via the `na.Indexable` interface yields a valid system.
127+
128+
``AbstractInstrument`` inherits :class:`named_arrays.Indexable`, so a single
129+
channel can be selected with ``instrument.isel(channel=...)`` (equivalently
130+
``instrument[{instrument.axis_channel: ...}]``) without a bespoke method.
131+
"""
132+
instrument = esis.flights.f1.optics.design(num_distribution=0)
133+
axis = instrument.axis_channel
134+
135+
# make a grating orientation and a (dict-stored) ruling coefficient vary by
136+
# channel, mimicking a distortion-fit model
137+
yaw = na.ScalarArray(np.array([1.0, 2.0, 3.0, 4.0]) * u.deg, axes=axis)
138+
coefficient = na.ScalarArray(np.array([10.0, 20.0, 30.0, 40.0]) * u.um, axes=axis)
139+
instrument.grating.yaw = yaw
140+
instrument.grating.rulings.spacing.coefficients[0] = coefficient
141+
142+
result = instrument.isel(**{axis: channel})
143+
144+
# isel and the equivalent dict-indexing agree
145+
assert result.grating.yaw.ndarray == instrument[{axis: channel}].grating.yaw.ndarray
146+
147+
# the selected element is kept and the channel axis is removed
148+
assert result.grating.yaw.ndarray == yaw.ndarray[channel]
149+
assert axis not in result.grating.yaw.axes
150+
assert (
151+
result.grating.rulings.spacing.coefficients[0].ndarray
152+
== coefficient.ndarray[channel]
153+
)
154+
assert axis not in result.grating.rulings.spacing.coefficients[0].axes
155+
assert axis not in na.as_named_array(result.grating.azimuth).axes
156+
assert axis not in na.as_named_array(result.camera.channel).axes
157+
158+
# the original instrument is left unmodified (na.getitem rebuilds via replace)
159+
assert axis in instrument.grating.yaw.axes
160+
assert np.all(instrument.grating.yaw.ndarray == yaw.ndarray)
161+
162+
# the single-channel instrument still resolves into an optical system
163+
assert isinstance(result.system, optika.systems.AbstractSequentialSystem)
164+
assert result.system.surfaces

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dependencies = [
1919
"matplotlib",
2020
"astropy~=7.2",
2121
"joblib",
22-
"named-arrays~=1.0",
22+
"named-arrays~=1.6",
2323
"optika~=1.1",
2424
"msfc-ccd~=1.0",
2525
"solar-dynamics-observatory~=0.2",

0 commit comments

Comments
 (0)