import numpy as np
import astropy.units as u
import named_arrays as na
import optika
import esis
from .. import primaries
from .. import gratings
from .. import filters
__all__ = [
"design_full",
"design",
"design_single",
"as_built",
]
[docs]
def design_full(
grid: None | optika.vectors.ObjectVectorArray = None,
axis_channel: str = "channel",
num_distribution: int = 11,
) -> esis.optics.Instrument:
"""
Load the entire optical design including the inactive channels.
This instance includes all six channels instead of the four active channels
included in :func:`design`.
Parameters
----------
grid
sampling of wavelength, field, and pupil positions that will be used to
characterize the optical system.
axis_channel
The name of the logical axis corresponding to changing camera channel.
num_distribution
number of Monte Carlo samples to draw when computing uncertainties
"""
num_folds = 8
num_channels = 6
name_channel = na.arange(0, num_channels, axis=axis_channel)
angle_per_channel = (360 * u.deg) / num_folds
cos_per_channel = np.cos(angle_per_channel / 2)
angle_channel_offset = -angle_per_channel / 2
angle_channel = na.linspace(
start=0 * u.deg,
stop=num_channels * angle_per_channel,
axis=axis_channel,
num=num_channels,
endpoint=False,
)
angle_channel = angle_channel + angle_channel_offset
# dashstyle = (0, (1, 3))
# dashstyle_channels = na.ScalarArray(
# ndarray=np.array(
# object=[dashstyle, "solid", "solid", "solid", "solid", dashstyle],
# dtype=object,
# ),
# axes="channel",
# )
# alpha_channels = na.ScalarArray(np.array([0, 1, 1, 1, 1, 0]), axes="channel")
radius_primary_clear = 77.9 * u.mm
primary = esis.optics.PrimaryMirror(
sag=optika.sags.ParabolicSag(
focal_length=-1000 * u.mm,
parameters_slope_error=optika.metrology.SlopeErrorParameters(
step_size=4 * u.mm,
kernel_size=2 * u.mm,
),
parameters_roughness=optika.metrology.RoughnessParameters(
period_min=0.06 * u.mm,
period_max=6 * u.mm,
),
parameters_microroughness=optika.metrology.RoughnessParameters(
period_min=1.6 * u.um,
period_max=70 * u.um,
),
),
num_folds=8,
width_clear=2 * radius_primary_clear * cos_per_channel,
width_border=(83.7 * u.mm - radius_primary_clear) * cos_per_channel,
material=primaries.materials.multilayer_design(),
translation=na.Cartesian3dVectorArray(
x=na.UniformUncertainScalarArray(
nominal=0 * u.mm,
width=1 * u.mm,
num_distribution=num_distribution,
),
y=na.UniformUncertainScalarArray(
nominal=0 * u.mm,
width=1 * u.mm,
num_distribution=num_distribution,
),
z=0 * u.mm,
),
)
front_aperture = esis.optics.FrontAperture(
translation=na.Cartesian3dVectorArray(
x=0 * u.mm,
y=0 * u.mm,
z=primary.sag.focal_length - 500 * u.mm,
),
)
point_tuffet_1 = na.Cartesian2dVectorArray(2.54, 37.1707) * u.mm
point_tuffet_2 = na.Cartesian2dVectorArray(24.4876, 28.0797) * u.mm
difference_tuffet = point_tuffet_2 - point_tuffet_1
slope_tuffet = difference_tuffet.y / difference_tuffet.x
radius_tuffet = point_tuffet_1.y - slope_tuffet * point_tuffet_1.x
central_obscuration = esis.optics.CentralObscuration(
num_folds=num_folds,
halfwidth=radius_tuffet * cos_per_channel,
remove_last_vertex=True,
translation=na.Cartesian3dVectorArray(z=-1404.270) * u.mm,
)
field_stop = esis.optics.FieldStop(
num_folds=num_folds,
radius_clear=1.82 * u.mm,
radius_mechanical=2.81 * u.mm,
translation=na.Cartesian3dVectorArray(
x=primary.translation.x.copy(),
y=primary.translation.y.copy(),
z=primary.sag.focal_length,
),
)
radius_grating = 597.830 * u.mm
error_radius_grating = 0.4 * u.percent
width_grating_border = 2 * u.mm
width_grating_border_inner = 4.58 * u.mm
var_grating_z_single = np.square(2.5e-5 * u.m)
var_grating_z_systematic = np.square(5e-6 * u.m)
var_grating_z = var_grating_z_single / 3 + var_grating_z_systematic
error_grating_z = np.sqrt(var_grating_z)
grating = esis.optics.Grating(
angle_input=1.301 * u.deg,
angle_output=8.057 * u.deg,
sag=optika.sags.SphericalSag(
radius=na.UniformUncertainScalarArray(
nominal=-radius_grating,
width=radius_grating * error_radius_grating,
num_distribution=num_distribution,
),
parameters_slope_error=optika.metrology.SlopeErrorParameters(
step_size=2 * u.mm,
kernel_size=1 * u.mm,
),
parameters_roughness=optika.metrology.RoughnessParameters(
period_min=0.024 * u.mm,
period_max=2.4 * u.mm,
),
parameters_microroughness=optika.metrology.RoughnessParameters(
period_min=0.02 * u.um,
period_max=2 * u.um,
),
),
material=gratings.materials.multilayer_design(),
rulings=gratings.rulings.ruling_design(
num_distribution=num_distribution,
),
num_folds=num_folds,
halfwidth_inner=13.02 * u.mm - width_grating_border_inner,
halfwidth_outer=10.49 * u.mm - width_grating_border,
width_border=width_grating_border,
width_border_inner=width_grating_border_inner,
clearance=1.25 * u.mm,
distance_radial=2.074999998438000e1 * u.mm,
azimuth=angle_channel.copy(),
translation=na.Cartesian3dVectorArray(
x=na.UniformUncertainScalarArray(
nominal=0 * u.mm,
width=1 * u.mm,
num_distribution=num_distribution,
),
y=na.UniformUncertainScalarArray(
nominal=0 * u.mm,
width=1 * u.mm,
num_distribution=num_distribution,
),
z=na.UniformUncertainScalarArray(
nominal=primary.sag.focal_length - 374.7 * u.mm,
width=error_grating_z,
num_distribution=num_distribution,
),
),
yaw=-4.469567242792327 * u.deg,
roll=na.UniformUncertainScalarArray(
nominal=0 * u.deg,
width=1.3e-2 * u.rad,
num_distribution=num_distribution,
),
)
filter = esis.optics.Filter(
material=filters.materials.thin_film_design(),
radius_clear=15 * u.mm,
width_border=0 * u.mm,
distance_radial=95.9 * u.mm,
azimuth=angle_channel.copy(),
translation=na.Cartesian3dVectorArray(
x=0 * u.mm,
y=0 * u.mm,
z=grating.translation.z.nominal + 1.301661998854058 * u.m,
),
yaw=-3.45 * u.deg,
roll=45 * u.deg,
)
sensor = esis.optics.Sensor(
temperature=-55 * u.deg_C,
distance_radial=108 * u.mm,
azimuth=angle_channel.copy(),
translation=na.Cartesian3dVectorArray(
x=0 * u.mm,
y=0 * u.mm,
z=filter.translation.z + 200 * u.mm,
),
yaw=-12.252 * u.deg,
)
camera = esis.optics.Camera(
sensor=sensor,
gain=2.5 * u.electron / u.DN,
channel=name_channel,
channel_trigger=1,
timedelta_sync=1 * u.ms,
)
if grid is None:
grid = optika.vectors.ObjectVectorArray(
wavelength=629.77 * u.AA,
field=na.Cartesian2dVectorLinearSpace(
start=-1,
stop=1,
axis=na.Cartesian2dVectorArray("field_x", "field_y"),
num=11,
centers=True,
),
pupil=na.Cartesian2dVectorLinearSpace(
start=-1,
stop=1,
axis=na.Cartesian2dVectorArray("pupil_x", "pupil_y"),
num=11,
centers=True,
),
)
return esis.optics.Instrument(
name="ESIS 1 final design (all channels)",
axis_channel=axis_channel,
front_aperture=front_aperture,
central_obscuration=central_obscuration,
primary_mirror=primary,
field_stop=field_stop,
grating=grating,
filter=filter,
camera=camera,
wavelength=grid.wavelength,
field=grid.field,
pupil=grid.pupil,
)
[docs]
def design(
grid: None | optika.vectors.ObjectVectorArray = None,
axis_channel: str = "channel",
num_distribution: int = 11,
) -> esis.optics.Instrument:
"""
Load the final optical design prepared by Charles Kankelborg and Hans Courrier.
Parameters
----------
grid
sampling of wavelength, field, and pupil positions that will be used to
characterize the optical system.
axis_channel
The name of the logical axis corresponding to changing camera channel.
num_distribution
number of Monte Carlo samples to draw when computing uncertainties
"""
result = design_full(
grid=grid,
axis_channel=axis_channel,
num_distribution=num_distribution,
)
slice_active = {axis_channel: slice(1, 5)}
result.grating.azimuth = result.grating.azimuth[slice_active]
result.filter.azimuth = result.filter.azimuth[slice_active]
result.camera.channel = result.camera.channel[slice_active]
result.camera.sensor.azimuth = result.camera.sensor.azimuth[slice_active]
return result
[docs]
def design_single(
grid: None | optika.vectors.ObjectVectorArray = None,
axis_channel: str = "channel",
num_distribution: int = 11,
) -> esis.optics.Instrument:
"""
Load only a single channel of the optical design.
Since the system is rotationally symmetric, sometimes it's nice to model
only one channel
Parameters
----------
grid
sampling of wavelength, field, and pupil positions that will be used to
characterize the optical system.
axis_channel
The name of the logical axis corresponding to changing camera channel.
num_distribution
number of Monte Carlo samples to draw when computing uncertainties
"""
result = design(
grid=grid,
axis_channel=axis_channel,
num_distribution=num_distribution,
)
index = {axis_channel: 0}
result.grating.azimuth = result.grating.azimuth[index]
result.filter.azimuth = result.filter.azimuth[index]
result.camera.channel = result.camera.channel[index]
result.camera.sensor.azimuth = result.camera.sensor.azimuth[index]
result.roll = -result.grating.azimuth
return result
[docs]
def as_built(
grid: None | optika.vectors.ObjectVectorArray = None,
axis_channel: str = "channel",
num_distribution: int = 11,
) -> esis.optics.Instrument:
"""
Load the as-built optical model.
Based on :func:`design`, but includes efficiency and figure measurements of the
primary mirror and gratings, as well as gain measurements of the sensor.
Parameters
----------
grid
sampling of wavelength, field, and pupil positions that will be used to
characterize the optical system.
axis_channel
The name of the logical axis corresponding to changing camera channel.
num_distribution
number of Monte Carlo samples to draw when computing uncertainties
Examples
--------
Load the as-built optical model and print its parameters.
.. jupyter-execute::
import esis
esis.flights.f1.optics.as_built()
"""
result = design(
grid=grid,
axis_channel=axis_channel,
num_distribution=num_distribution,
)
result.primary_mirror.material = primaries.materials.multilayer_fit()
result.grating.serial_number = na.stack(
arrays=[
"89025",
"89024",
"89026",
"89027",
],
axis=axis_channel,
)
result.grating.manufacturing_number = na.stack(
arrays=[
"UBO-16-024",
"UBO-16-017",
"UBO-16-019",
"UBO-16-014",
],
axis=axis_channel,
)
radius_014 = [597.170, 597.210, 597.195] * u.mm
radius_017 = [597.065, 597.045, 597.050] * u.mm
radius_019 = [597.055, 597.045, 597.030] * u.mm
radius_024 = [596.890, 596.870, 596.880] * u.mm
result.grating.sag.radius = na.stack(
arrays=[
radius_024.mean(),
radius_017.mean(),
radius_019.mean(),
radius_014.mean(),
],
axis=axis_channel,
)
result.grating.material = gratings.materials.multilayer_fit()
result.grating.rulings = gratings.rulings.ruling_measurement(
num_distribution=num_distribution,
)
result.camera.sensor.serial_number = na.stack(
arrays=[
"SN6",
"SN7",
"SN9",
"SN10",
],
axis=axis_channel,
)
axis_tap_x = result.camera.axis_tap_x
axis_tap_y = result.camera.axis_tap_y
# Results from Laurel Rachmeler presented on 2017-07-06 and 2017-07-12.
result.camera.gain = na.ScalarArray(
ndarray=[
[
[2.55, 2.63],
[2.57, 2.57],
],
[
[2.57, 2.53],
[2.50, 2.52],
],
[
[2.57, 2.59],
[2.53, 2.52],
],
[
[2.60, 2.58],
[2.60, 2.54],
],
]
* u.electron
/ u.DN,
axes=(axis_channel, axis_tap_y, axis_tap_x),
)
result.camera.sensor.readout_noise = 6 * u.electron
return result