Skip to content

Commit 122b07b

Browse files
authored
Added esis.optics.PrimaryMirror class. (#11)
1 parent bcc400a commit 122b07b

5 files changed

Lines changed: 268 additions & 0 deletions

File tree

esis/optics/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
from . import abc
66
from ._front_apertures import FrontAperture
77
from ._central_obscurations import CentralObscuration
8+
from ._primary_mirrors import PrimaryMirror
89

910
__all__ = [
1011
"abc",
1112
"FrontAperture",
1213
"CentralObscuration",
14+
"PrimaryMirror",
1315
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from ._primary_mirrors import (
2+
AbstractPrimaryMirror,
3+
PrimaryMirror,
4+
)
5+
6+
__all__ = [
7+
"AbstractPrimaryMirror",
8+
"PrimaryMirror",
9+
]
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
"AbstractPrimaryMirror",
10+
"PrimaryMirror",
11+
]
12+
13+
14+
@dataclasses.dataclass(eq=False, repr=False)
15+
class AbstractPrimaryMirror(
16+
optika.mixins.Printable,
17+
optika.mixins.Rollable,
18+
optika.mixins.Yawable,
19+
optika.mixins.Pitchable,
20+
optika.mixins.Translatable,
21+
):
22+
"""
23+
An interface describing the primary mirror of the instrument.
24+
"""
25+
26+
@property
27+
@abc.abstractmethod
28+
def sag(self) -> None | optika.sags.AbstractSag:
29+
"""
30+
The sag function of this primary mirror.
31+
"""
32+
33+
@property
34+
@abc.abstractmethod
35+
def num_folds(self) -> u.Quantity | na.AbstractScalar:
36+
"""
37+
The order of the rotational symmetry of the optical system.
38+
This is also the number of sides of the regular polygonal aperture.
39+
"""
40+
41+
@property
42+
@abc.abstractmethod
43+
def width_clear(self) -> u.Quantity | na.AbstractScalar:
44+
"""
45+
The width of the clear aperture from edge to edge.
46+
"""
47+
48+
@property
49+
def radius_clear(self) -> u.Quantity | na.AbstractScalar:
50+
"""
51+
The clear radius of the aperture from center to vertex.
52+
"""
53+
halfwidth_clear = self.width_clear / 2
54+
num_sides = self.num_folds
55+
if (num_sides % 2) != 0: # pragma: nocover
56+
raise ValueError("odd numbers of sides not supported")
57+
result = halfwidth_clear / np.cos(360 * u.deg / num_sides / 2)
58+
return result
59+
60+
@property
61+
@abc.abstractmethod
62+
def width_border(self) -> u.Quantity | na.AbstractScalar:
63+
"""
64+
The width of the border around the clear aperture.
65+
"""
66+
67+
@property
68+
def radius_mechanical(self) -> u.Quantity | na.AbstractScalar:
69+
"""
70+
The radius of the mechanical aperture from center to vertex.
71+
"""
72+
halfwidth_clear = self.width_clear / 2
73+
width_border = self.width_border
74+
halfwidth = halfwidth_clear + width_border
75+
num_sides = self.num_folds
76+
if (num_sides % 2) != 0: # pragma: nocover
77+
raise ValueError("odd numbers of sides not supported")
78+
result = halfwidth / np.cos(360 * u.deg / num_sides / 2)
79+
return result
80+
81+
@property
82+
@abc.abstractmethod
83+
def material(self) -> None | optika.materials.AbstractMaterial:
84+
"""
85+
The optics material composing this object.
86+
"""
87+
88+
@property
89+
def surface(self) -> optika.surfaces.Surface:
90+
"""
91+
Represent this object as an :mod:`optika` surface.
92+
"""
93+
return optika.surfaces.Surface(
94+
name="primary",
95+
sag=self.sag,
96+
material=self.material,
97+
aperture=optika.apertures.RegularPolygonalAperture(
98+
radius=self.radius_clear,
99+
num_vertices=self.num_folds,
100+
),
101+
aperture_mechanical=optika.apertures.RegularPolygonalAperture(
102+
radius=self.radius_mechanical,
103+
num_vertices=self.num_folds,
104+
),
105+
transformation=self.transformation,
106+
)
107+
108+
109+
@dataclasses.dataclass(eq=False, repr=False)
110+
class PrimaryMirror(
111+
AbstractPrimaryMirror,
112+
):
113+
"""
114+
A model of the primary mirror of the instrument.
115+
116+
This mirror collects light from the Sun and focuses it onto the field stop.
117+
"""
118+
119+
sag: None | optika.sags.AbstractSag = None
120+
"""
121+
The sag function of this primary mirror.
122+
"""
123+
124+
num_folds: int = 0
125+
"""
126+
The order of the rotational symmetry of the optical system.
127+
This is also the number of sides of the regular polygonal aperture.
128+
"""
129+
130+
width_clear: u.Quantity | na.AbstractScalar = 0 * u.mm
131+
"""
132+
The width of the clear aperture from edge to edge.
133+
"""
134+
135+
width_border: u.Quantity | na.AbstractScalar = 0 * u.mm
136+
"""
137+
The width of the border around the clear aperture.
138+
"""
139+
140+
material: None | optika.materials.AbstractMaterial = None
141+
"""
142+
The optical material composing this object.
143+
"""
144+
145+
translation: u.Quantity | na.AbstractCartesian3dVectorArray = 0 * u.mm
146+
"""
147+
A transformation which can arbitrarily translate this object.
148+
"""
149+
150+
pitch: u.Quantity | na.AbstractScalar = 0 * u.deg
151+
"""
152+
The pitch angle of this object.
153+
"""
154+
155+
yaw: u.Quantity | na.AbstractScalar = 0 * u.deg
156+
"""
157+
The yaw angle of this object.
158+
"""
159+
160+
roll: u.Quantity | na.AbstractScalar = 0 * u.deg
161+
"""
162+
The roll angle of this object
163+
"""
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import numpy as np
2+
import pytest
3+
import astropy.units as u
4+
import named_arrays as na
5+
import optika
6+
from optika._tests import test_mixins
7+
import esis
8+
9+
10+
class AbstractTestAbstractPrimaryMirror(
11+
test_mixins.AbstractTestPrintable,
12+
test_mixins.AbstractTestRollable,
13+
test_mixins.AbstractTestYawable,
14+
test_mixins.AbstractTestPitchable,
15+
test_mixins.AbstractTestTranslatable,
16+
):
17+
def test_sag(
18+
self,
19+
a: esis.optics.abc.AbstractPrimaryMirror,
20+
):
21+
assert isinstance(a.sag, optika.sags.AbstractSag)
22+
23+
def test_num_folds(
24+
self,
25+
a: esis.optics.abc.AbstractPrimaryMirror,
26+
):
27+
assert isinstance(a.num_folds, int)
28+
assert a.num_folds > 2
29+
30+
def test_width_clear(
31+
self,
32+
a: esis.optics.abc.AbstractPrimaryMirror,
33+
):
34+
assert isinstance(na.as_named_array(a.width_clear), na.AbstractScalar)
35+
assert na.unit_normalized(a.width_clear).is_equivalent(u.mm)
36+
37+
def test_radius_clear(
38+
self,
39+
a: esis.optics.abc.AbstractPrimaryMirror,
40+
):
41+
assert isinstance(na.as_named_array(a.radius_clear), na.AbstractScalar)
42+
assert na.unit_normalized(a.radius_clear).is_equivalent(u.mm)
43+
44+
def test_width_border(
45+
self,
46+
a: esis.optics.abc.AbstractPrimaryMirror,
47+
):
48+
assert isinstance(na.as_named_array(a.width_border), na.AbstractScalar)
49+
assert na.unit_normalized(a.width_border).is_equivalent(u.mm)
50+
51+
def test_radius_mechanical(
52+
self,
53+
a: esis.optics.abc.AbstractPrimaryMirror,
54+
):
55+
assert isinstance(na.as_named_array(a.radius_mechanical), na.AbstractScalar)
56+
assert na.unit_normalized(a.radius_mechanical).is_equivalent(u.mm)
57+
58+
def test_material(
59+
self,
60+
a: esis.optics.abc.AbstractPrimaryMirror,
61+
):
62+
assert isinstance(a.material, optika.materials.AbstractMaterial)
63+
64+
def test_translation(
65+
self,
66+
a: esis.optics.abc.AbstractPrimaryMirror,
67+
):
68+
result = a.translation
69+
assert np.issubdtype(na.get_dtype(result), float)
70+
assert na.unit_normalized(result).is_equivalent(u.mm)
71+
72+
def test_surface(
73+
self,
74+
a: esis.optics.abc.AbstractPrimaryMirror,
75+
):
76+
assert isinstance(a.surface, optika.surfaces.AbstractSurface)
77+
78+
79+
@pytest.mark.parametrize(
80+
argnames="a",
81+
argvalues=[
82+
esis.optics.PrimaryMirror(
83+
sag=optika.sags.ParabolicSag(1000 * u.mm),
84+
num_folds=8,
85+
material=optika.materials.Mirror(),
86+
),
87+
],
88+
)
89+
class TestPrimaryMirror(
90+
AbstractTestAbstractPrimaryMirror,
91+
):
92+
pass

esis/optics/abc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
from ._front_apertures import AbstractFrontAperture
66
from ._central_obscurations import AbstractCentralObscuration
7+
from ._primary_mirrors import AbstractPrimaryMirror
78

89
__all__ = [
910
"AbstractFrontAperture",
1011
"AbstractCentralObscuration",
12+
"AbstractPrimaryMirror",
1113
]

0 commit comments

Comments
 (0)