Skip to content

Commit 6b670f2

Browse files
authored
Added esis.optics.FieldStop class. (#12)
1 parent 122b07b commit 6b670f2

5 files changed

Lines changed: 200 additions & 0 deletions

File tree

esis/optics/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
from ._front_apertures import FrontAperture
77
from ._central_obscurations import CentralObscuration
88
from ._primary_mirrors import PrimaryMirror
9+
from ._field_stops import FieldStop
910

1011
__all__ = [
1112
"abc",
1213
"FrontAperture",
1314
"CentralObscuration",
1415
"PrimaryMirror",
16+
"FieldStop",
1517
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from ._field_stops import (
2+
AbstractFieldStop,
3+
FieldStop,
4+
)
5+
6+
__all__ = [
7+
"AbstractFieldStop",
8+
"FieldStop",
9+
]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import abc
2+
import dataclasses
3+
import numpy as np
4+
import astropy.units as u
5+
import named_arrays as na
6+
import optika
7+
8+
__all__ = [
9+
"FieldStop",
10+
]
11+
12+
13+
@dataclasses.dataclass(eq=False, repr=False)
14+
class AbstractFieldStop(
15+
optika.mixins.Printable,
16+
optika.mixins.Translatable,
17+
):
18+
"""
19+
An interface describing the field stop of the instrument.
20+
"""
21+
22+
@property
23+
@abc.abstractmethod
24+
def num_folds(self) -> int:
25+
"""
26+
The order of the rotational symmetry of the optical system.
27+
"""
28+
29+
@property
30+
def num_sides(self) -> int:
31+
"""
32+
The number of sides of the field stop's aperture.
33+
"""
34+
return self.num_folds
35+
36+
@property
37+
@abc.abstractmethod
38+
def radius_clear(self) -> u.Quantity | na.AbstractScalar:
39+
"""
40+
The distance from the center to a vertex of the clear aperture.
41+
"""
42+
43+
@property
44+
def width_clear(self) -> u.Quantity:
45+
"""
46+
The width of the clear aperture from edge to edge.
47+
"""
48+
return 2 * self.radius_clear * np.cos(360 * u.deg / self.num_sides / 2)
49+
50+
@property
51+
@abc.abstractmethod
52+
def radius_mechanical(self) -> u.Quantity | na.AbstractScalar:
53+
"""
54+
The radius of the exterior edge of the field stop.
55+
"""
56+
57+
@property
58+
def surface(self) -> optika.surfaces.Surface:
59+
"""
60+
Represent this object as an :mod:`optika` surface.
61+
"""
62+
return optika.surfaces.Surface(
63+
name="field stop",
64+
aperture=optika.apertures.RegularPolygonalAperture(
65+
radius=self.radius_clear,
66+
num_vertices=self.num_sides,
67+
),
68+
aperture_mechanical=optika.apertures.CircularAperture(
69+
radius=self.radius_mechanical,
70+
),
71+
is_field_stop=True,
72+
transformation=self.transformation,
73+
)
74+
75+
76+
@dataclasses.dataclass(eq=False, repr=False)
77+
class FieldStop(
78+
AbstractFieldStop,
79+
):
80+
"""
81+
A model of the field stop of the instrument.
82+
83+
This element restricts the field of view of the spectrograph to simplify
84+
the inversion process.
85+
"""
86+
87+
num_folds: int = 0
88+
"""
89+
The order of the rotational symmetry of the optical system.
90+
"""
91+
92+
radius_clear: u.Quantity | na.AbstractScalar = 0 * u.mm
93+
"""
94+
The distance from the center to a vertex of the clear aperture.
95+
"""
96+
97+
radius_mechanical: u.Quantity | na.AbstractScalar = 0 * u.mm
98+
"""
99+
The radius of the exterior edge of the field stop.
100+
"""
101+
102+
translation: u.Quantity | na.AbstractCartesian3dVectorArray = 0 * u.mm
103+
"""
104+
A transformation which can arbitrarily translate this object.
105+
"""
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pytest
2+
import numpy as np
3+
import astropy.units as u
4+
import named_arrays as na
5+
import optika
6+
import optika._tests.test_mixins
7+
import esis
8+
9+
10+
class AbstractTestAbstractFieldStop(
11+
optika._tests.test_mixins.AbstractTestPrintable,
12+
optika._tests.test_mixins.AbstractTestTranslatable,
13+
):
14+
def test_num_folds(
15+
self,
16+
a: esis.optics.abc.AbstractFieldStop,
17+
):
18+
result = a.num_folds
19+
assert isinstance(result, int)
20+
assert result >= 0
21+
22+
def test_num_sides(
23+
self,
24+
a: esis.optics.abc.AbstractFieldStop,
25+
):
26+
result = a.num_sides
27+
assert isinstance(result, int)
28+
assert result >= 0
29+
30+
def test_radius_clear(
31+
self,
32+
a: esis.optics.abc.AbstractFieldStop,
33+
):
34+
result = a.radius_clear
35+
assert isinstance(na.as_named_array(result), na.AbstractScalar)
36+
assert na.unit_normalized(result).is_equivalent(u.mm)
37+
assert np.all(result >= 0)
38+
39+
def test_width_clear(
40+
self,
41+
a: esis.optics.abc.AbstractFieldStop,
42+
):
43+
result = a.width_clear
44+
assert isinstance(na.as_named_array(result), na.AbstractScalar)
45+
assert na.unit_normalized(result).is_equivalent(u.mm)
46+
47+
def test_radius_mechanical(
48+
self,
49+
a: esis.optics.abc.AbstractFieldStop,
50+
):
51+
result = a.radius_mechanical
52+
assert isinstance(na.as_named_array(result), na.AbstractScalar)
53+
assert na.unit_normalized(result).is_equivalent(u.mm)
54+
assert np.all(result >= 0)
55+
56+
def test_surface(
57+
self,
58+
a: esis.optics.abc.AbstractFieldStop,
59+
):
60+
result = a.surface
61+
assert isinstance(result, optika.surfaces.Surface)
62+
assert result.is_field_stop
63+
assert result.aperture is not None
64+
assert result.aperture_mechanical is not None
65+
66+
67+
@pytest.mark.parametrize(
68+
argnames="a",
69+
argvalues=[
70+
esis.optics.FieldStop(),
71+
esis.optics.FieldStop(
72+
num_folds=8,
73+
radius_clear=5 * u.mm,
74+
radius_mechanical=10 * u.mm,
75+
translation=na.Cartesian3dVectorArray(z=200) * u.mm,
76+
),
77+
],
78+
)
79+
class TestFieldStop(
80+
AbstractTestAbstractFieldStop,
81+
):
82+
pass

esis/optics/abc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
from ._front_apertures import AbstractFrontAperture
66
from ._central_obscurations import AbstractCentralObscuration
77
from ._primary_mirrors import AbstractPrimaryMirror
8+
from ._field_stops import AbstractFieldStop
89

910
__all__ = [
1011
"AbstractFrontAperture",
1112
"AbstractCentralObscuration",
1213
"AbstractPrimaryMirror",
14+
"AbstractFieldStop",
1315
]

0 commit comments

Comments
 (0)