Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions furst_optics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from . import feed_optics
from . import gratings
from . import detectors
from . import spectrographs

__all__ = [
"typevars",
Expand All @@ -18,4 +19,5 @@
"feed_optics",
"gratings",
"detectors",
"spectrographs",
]
2 changes: 1 addition & 1 deletion furst_optics/sources/_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def surface(self) -> optika.surfaces.Surface:
return optika.surfaces.Surface(
name="solar disk",
aperture=optika.apertures.CircularAperture(
radius=np.cos(self.radius),
radius=np.sin(self.radius),
),
is_field_stop=True,
transformation=self.transformation,
Expand Down
9 changes: 9 additions & 0 deletions furst_optics/spectrographs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
End-to-end models of the FURST optical system.
"""

from ._spectrographs import Spectrograph

__all__ = [
"Spectrograph",
]
68 changes: 68 additions & 0 deletions furst_optics/spectrographs/_design.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import astropy.units as u
import named_arrays as na
import optika
import furst_optics

def design():

rowland_radius = 1 * u.m

result = furst_optics.spectrographs.Spectrograph(
name="furst",
source=furst_optics.sources.SolarDisk(),
front_aperture=furst_optics.apertures.FrontAperture(),
feed_optic=furst_optics.feed_optics.FeedOptic(
radius=3 * u.mm,
aperture_subtent=1 * u.deg,
aperture_height=10 * u.mm,
material=optika.materials.Mirror(),
rowland_radius=rowland_radius,
# rowland_azimuth=na.linspace(10, 15, axis="angle", num=2) * u.deg,
rowland_azimuth=10 * u.deg,
),
grating=furst_optics.gratings.Grating(
sag=optika.sags.SphericalSag(
radius=-2 * rowland_radius,
),
width_clear=100 * u.mm,
material=optika.materials.Mirror(),
rulings=optika.rulings.Rulings(
spacing=1 * u.um,
diffraction_order=1,
),
rowland_radius=rowland_radius,
rowland_azimuth=175 * u.deg,
),
detector=furst_optics.detectors.Detector(
width_pixel=15 * u.um,
axis_pixel=na.Cartesian2dVectorArray(
x="detector_x",
y="detector_y",
),
num_pixel=na.Cartesian2dVectorArray(
x=2048,
y=1024,
),
rowland_radius=rowland_radius,
rowland_azimuth=5 * u.deg,
),
grid_input=optika.vectors.ObjectVectorArray(
wavelength=na.linspace(-1, 1, axis="wavelength", num=1, centers=True),
field=na.Cartesian2dVectorLinearSpace(
start=-1,
stop=1,
axis=na.Cartesian2dVectorArray("fx", "fy"),
num=1,
centers=True,
),
pupil=na.Cartesian2dVectorLinearSpace(
start=-1,
stop=1,
axis=na.Cartesian2dVectorArray("px", "py"),
num=1,
centers=True,
)
)
)

return result
206 changes: 206 additions & 0 deletions furst_optics/spectrographs/_spectrographs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import dataclasses
import numpy as np
import astropy.units as u
import named_arrays as na
import optika
import furst_optics

__all__ = [
"Spectrograph",
]


@dataclasses.dataclass(eq=False, repr=False)
class Spectrograph(
optika.mixins.Printable,
optika.mixins.Rollable,
optika.mixins.Yawable,
optika.mixins.Pitchable,
):
"""
A generic model of the FURST instrument,
which includes the feed optics, grating, visible-blind filter,
and detector.
"""

name: str
"""
The human-readable name of this optics model.
"""

source: furst_optics.sources.AbstractSource
"""
The light source being observed by this instrument.
"""

front_aperture: furst_optics.apertures.FrontAperture
"""
A model of the front aperture plate of the instrument.
"""

feed_optic: furst_optics.feed_optics.FeedOptic
"""
A model of the feed optic array.
"""

grating: furst_optics.gratings.Grating
"""
A model of the diffraction grating used to disperse light.
"""

detector: furst_optics.detectors.Detector
"""
A model of the imaging sensor used to measure light
at the end of the optical system.
"""

grid_input: optika.vectors.ObjectVectorArray
"""
A grid of samples in wavelength, pupil, and field position used
to trace rays through the optical system.
"""

pitch: u.Quantity | na.AbstractScalar = 0 * u.deg
"""
Rotation about the vector perpendicular to the optic axis and the
vector normal to the optical table.
"""

yaw: u.Quantity | na.AbstractScalar = 0 * u.deg
"""
Rotation of the instrument about the vector normal to
the FURST optical table.
"""

roll: u.Quantity | na.AbstractScalar = 0 * u.deg
"""
Rotation of the instrument about the optic axis.
"""

def angle_grating_input(self) -> na.AbstractScalar:
"""
The angle between the grating normal and the vector
pointing from the center of the grating
to the virtual image of the source inside the feed optic.
"""
feed_optic = self.feed_optic
grating = self.grating
position = na.Cartesian3dVectorArray() * u.mm
normal_surface = grating.sag.normal(position)
normal_rulings = grating.rulings.spacing_(position).normalized
transformation_grating = grating.transformation.inverse
transformation_feed_optic = feed_optic.transformation_image
transformation = transformation_grating @ transformation_feed_optic
x = na.Cartesian3dVectorArray() * u.mm
x = transformation(x)
return np.arctan2(
x @ normal_rulings,
x @ normal_surface,
)

def angle_grating_output(self, axis: str):
"""
The angles between the grating normal and the vectors
pointing from the center of the grating
to the edges of the detector.

Parameters
----------
axis
An axis name for the array of points
around the edges of the detector.
"""
detector = self.detector.surface
grating = self.grating.surface
position = na.Cartesian3dVectorArray() * u.mm
normal_surface = grating.sag.normal(position)
normal_rulings = grating.rulings.spacing_(position).normalized
transformation = grating.transformation.inverse @ detector.transformation
wire = np.moveaxis(
a=detector.aperture.wire(),
source="wire",
destination=axis,
)
wire = transformation(wire)
return np.arctan2(
wire @ normal_rulings,
wire @ normal_surface,
)

def _wavelength_test_grid(
self,
axis_output: str,
) -> na.AbstractScalar:
"""
A grid of test wavelengths used to find the wavelength
range for each channel of FURST.

Parameters
----------
axis_output
An axis name for the array of points
around the edges of the detector.
"""
position = na.Cartesian3dVectorArray() * u.mm
grating = self.grating.surface
m = grating.rulings.diffraction_order
d = grating.rulings.spacing_(position).length
a = self.angle_grating_input()
b = self.angle_grating_output(axis_output)
result = np.abs((np.sin(a) + np.sin(b)) * d / m)
return result.to(u.AA)

@property
def wavelength_min(self) -> u.Quantity | na.AbstractScalar:
"""The minimum wavelength for each channel."""
axis = "_dummy"
wavelength = self._wavelength_test_grid(axis)
return wavelength.min(axis)

@property
def wavelength_max(self) -> u.Quantity | na.AbstractScalar:
"""The maximum wavelength for each channel."""
axis = "_dummy"
wavelength = self._wavelength_test_grid(axis)
return wavelength.max(axis)

def wavelength_physical(
self,
wavelength_normalized: u.Quantity | na.AbstractScalar,
) -> na.AbstractScalar:
"""
Convert wavelength in normalized units (between -1 and +1),
to wavelength in physical units using :attr:`wavelength_min`
and :attr:`wavelength_max`.

Parameters
----------
wavelength_normalized
An array of wavelengths in normalized units to convert.
"""
wavelength_min = self.wavelength_min
wavelength_max = self.wavelength_max
wavelength_range = wavelength_max - wavelength_min
result = wavelength_range * (wavelength_normalized + 1) / 2 + wavelength_min
return result

@property
def system(self) -> optika.systems.SequentialSystem:
"""
This spectrograph expressed as an instance of
:class:`optika.systems.SequentialSystem`.
"""
grid = self.grid_input.explicit.copy_shallow()
grid.wavelength = self.wavelength_physical(grid.wavelength)

return optika.systems.SequentialSystem(
surfaces=[
self.front_aperture.surface,
self.feed_optic.surface,
self.grating.surface,
],
object=self.source.surface,
sensor=self.detector.surface,
grid_input=grid,
transformation=self.transformation,
)
Loading