Instrument#

class esis.optics.Instrument(name='ESIS', axis_channel='channel', front_aperture=None, central_obscuration=None, primary_mirror=None, field_stop=None, grating=None, filter=None, camera=None, wavelength=None, field=None, pupil=None, pitch=<Quantity 0. deg>, yaw=<Quantity 0. deg>, roll=<Quantity 0. deg>, kwargs_plot=None)[source]#

Bases: AbstractInstrument

An object which represents the entire optical system.

A composition of the optical elements and a grid of input rays. Designed to resolve the optical elements into an instance of optika.systems.SequentialSystem for performance modeling.

Attributes

angle_grating_input

The angle between the grating normal and the direction of the incident light.

angle_grating_output

The angle between the grating normal and the direction of the diffracted light.

axes

The names of the axes of this object, tuple(self.shape).

axis_channel

The name of the logical axis corresponding to changing camera channel.

camera

A model of the camera and sensors.

central_obscuration

A model of the central obscuration.

field

A default grid of field positions to trace through the system.

field_stop

A model of the field stop.

filter

A model of the thin-film filters.

front_aperture

A model of the front aperture plate.

grating

A model of the diffraction grating array.

kwargs_plot

Extra keyword arguments used to plot the optical system.

name

The human-readable name of the instrument.

ndim

The number of dimensions of this object, len(self.shape).

pitch

The pitch angle of the instrument.

primary_mirror

A model of the primary mirror.

pupil

A default grid of pupil positions to trace through the system.

roll

The roll angle of the instrument.

shape

The broadcasted shape of every array-like field of this object.

size

The total number of elements in this object, the product of the values of shape.

system

Convert this model into an instance of optika.systems.SequentialSystem.

transformation

the coordinate transformation between the global coordinate system and this object's local coordinate system

wavelength

A default grid of wavelengths to trace through the system.

wavelength_max

The maximum wavelength permitted through the system.

wavelength_min

The minimum wavelength permitted through the system.

wavelength_physical

The value of wavelength converted to physical units if needed.

yaw

The yaw angle of the instrument.

Methods

__init__([name, axis_channel, ...])

align_grating([wavelength, position, field, ...])

Focus the gratings and then point them back at the sensor.

dispersion(wavelength[, delta])

Compute the wavelength interval imaged onto one pixel.

dispersion_doppler(wavelength[, delta])

Compute the Doppler velocity interval imaged onto one pixel.

focus_grating([wavelength, field, pupil, ...])

Move the gratings along the optic axis to their best focus.

isel(**item)

Index every array-like field of this object along named axes given as keyword arguments.

moved_grating([z, yaw])

Copy this instrument, putting its gratings somewhere else.

position_line([wavelength])

Where the center of the field of view lands on the sensor.

replace(**changes)

A copy of this object with the given fields replaced.

schematic_primary([ax, transformation, ...])

Plot a schematic of the primary mirror along with the beam footprint.

to_string([prefix])

Public-facing version of the __repr__ method that allows for defining a prefix string, which can be used to calculate how much whitespace to add to the beginning of each line of the result.

Inheritance Diagram

Inheritance diagram of esis.optics.Instrument
Parameters:
align_grating(wavelength=None, position=None, field=None, pupil=None, **kwargs)#

Focus the gratings and then point them back at the sensor.

focus_grating() moves each grating along the optic axis, which also moves the image along the sensor. An instrument which was aligned as well as focused puts the image where it belongs, and this method does both, leaving the line within a hundredth of a pixel of the position asked for.

Focusing alone comes closer than one might expect, because a grating whose radius is wrong both defocuses the image and displaces it, and moving the grating to correct the one largely corrects the other. For the as-built model it leaves a couple of pixels, which is still tens of kilometers per second of apparent Doppler shift.

The two are solved one after the other rather than together. Rotating a grating about \(y\) moves the image 915 microns per arcminute while changing the size of a spot by half a micron, so the rotation needed to place the line, of order ten arcseconds, does not disturb the focus. There is no trade to make between the two, and so no weighting between them to choose.

Parameters:
Return type:

Self

Examples

Align the as-built model and confirm the O V line lands where the sensor says it should.

import astropy.units as u
import named_arrays as na
import esis

instrument = esis.flights.f1.optics.as_built_unfocused(num_distribution=0)

aligned = instrument.align_grating()

error = aligned.position_line() - aligned.camera.sensor.position_image
na.nominal(error.length.to(u.um))
ScalarArray(
    ndarray=[0.55010484, 0.58132171, 0.58360151, 0.53344192] um,
    shape={'channel': 4},
)
dispersion(wavelength, delta=<Quantity 0.5 Angstrom>)#

Compute the wavelength interval imaged onto one pixel.

Measured from the raytrace: two wavelengths either side of the one asked for are traced along the optical axis, and the distance between where they land is divided into the width of a pixel.

The dispersion of this instrument varies across its passband by about a percent, so it is reported at a wavelength rather than as a single number for the instrument.

Parameters:
  • wavelength (Quantity | AbstractScalar) – The wavelength to measure the dispersion at.

  • delta (Quantity) – The half-interval either side of wavelength to measure across.

Return type:

Quantity | AbstractScalar

Examples

The dispersion at the O V line.

import esis

instrument = esis.flights.f1.optics.design_single(num_distribution=0)

instrument.dispersion(esis.flights.f1.spectrum.O_V.wavelength)
ScalarArray(
    ndarray=36.67463684 mAngstrom / pix,
    shape={},
)
dispersion_doppler(wavelength, delta=<Quantity 0.5 Angstrom>)#

Compute the Doppler velocity interval imaged onto one pixel.

This is dispersion() expressed as a velocity, and it therefore varies across the passband much more than the dispersion itself does, since it is divided by the wavelength. Over the ESIS passband the dispersion changes by about a percent while its Doppler equivalent changes by about ten times that, so the line it is quoted at matters.

Parameters:
  • wavelength (Quantity | AbstractScalar) – The wavelength to measure the dispersion at.

  • delta (Quantity) – The half-interval either side of wavelength to measure across.

Return type:

Quantity | AbstractScalar

Examples

The Doppler dispersion at the O V line.

import esis

instrument = esis.flights.f1.optics.design_single(num_distribution=0)

instrument.dispersion_doppler(esis.flights.f1.spectrum.O_V.wavelength)
ScalarArray(
    ndarray=17.45945819 km / (pix s),
    shape={},
)
focus_grating(wavelength=None, field=None, pupil=None, bounds=(<Quantity -1. mm>, <Quantity 2. mm>), min_step_size=<Quantity 1. um>)#

Move the gratings along the optic axis to their best focus.

The gratings image the field stop onto the sensor, so a grating whose radius of curvature differs from the one it was placed for (a measured, as-built radius replacing the design radius, for example) images the field stop to a different distance and defocuses the system unless it is moved to compensate.

This method finds, independently for every element of the grating (every channel, for example), the translation along \(z\) which minimizes the root-mean-square radius of the spots imaged onto the sensor, and returns a copy of this instrument with the gratings moved there. Everything else, including the sensor, stays where it was. The search is a vectorized Brent minimization (named_arrays.optimize.minimum_brent()), so every channel is focused by the same handful of ray traces.

Parameters:
  • wavelength (None | Quantity | AbstractScalar) – The wavelengths at which to focus. If None (the default), wavelength_physical is used.

  • field (None | AbstractCartesian2dVectorArray) – The normalized field positions of the traced rays. If None (the default), a \(3 \times 3\) grid spanning the central half of the field of view is used.

  • pupil (None | AbstractCartesian2dVectorArray) – The normalized pupil positions of the traced rays. If None (the default), a \(21 \times 21\) grid is used.

  • bounds (tuple[Quantity, Quantity]) – The bracket of grating translations, relative to the current position, within which the best focus is sought.

  • min_step_size (Quantity) – The tolerance on the translation of the best focus.

Return type:

Self

Examples

Move the gratings of the ESIS-I as-built model, which carries the measured radii of curvature but the design positions, to their best focus in the O V line.

import astropy.units as u
import named_arrays as na
import esis

instrument = esis.flights.f1.optics.as_built_unfocused(num_distribution=0)

focused = instrument.focus_grating(wavelength=629.73 * u.AA)

na.nominal(focused.grating.translation.z - instrument.grating.translation.z)
ScalarArray(
    ndarray=[0.70506445, 0.57811216, 0.58559673, 0.47486535] mm,
    shape={'channel': 4},
)
isel(**item)#

Index every array-like field of this object along named axes given as keyword arguments.

This is a convenience wrapper around __getitem__(): a.isel(x=0) is equivalent to a[dict(x=0)].

Since keyword-argument names must be valid Python identifiers, axes whose names are not valid identifiers can only be indexed using the dict form, a[{...}].

Parameters:
Return type:

Self

moved_grating(z=None, yaw=None)#

Copy this instrument, putting its gratings somewhere else.

Built rather than copied and adjusted, so that the raytrace of the result is the one its gratings ask for. system is cached, and a copy carries the cache with it, so moving a grating on a copy moves nothing: the rays go on being traced through the geometry the cache was built from. A new instrument has nothing cached and cannot go stale in that way.

The copy is shallow, so everything except the grating is shared with this instrument. Take copy.deepcopy() of the result before changing anything else about it.

Parameters:
  • z (None | Quantity | AbstractScalar) – Where to put the gratings along the optic axis. If None (the default), they are left where they are.

  • yaw (None | Quantity | AbstractScalar) – How far to rotate the gratings about \(y\). If None (the default), they are left as they are.

Return type:

Self

position_line(wavelength=None)#

Where the center of the field of view lands on the sensor.

The ray from the middle of the field of view, through the middle of the pupil, at the wavelength asked for. This is the position a spectral line is at, in the sense that esis.optics.Sensor.position_image means it.

It is taken from the one ray rather than from the centroid of the whole image because the centroid depends on how much of each beam survives to the sensor. With the primary aperture stop removed, three quarters of the rays are lost, and asymmetrically enough to drag the centroid of the O V image 0.42 mm from where its central ray lands. That is a real effect and it is what the flight data show, but it is not a property of the alignment, and it would move under any change to the model of the apertures.

Parameters:

wavelength (None | Quantity | AbstractScalar) – The wavelength of the line. If None (the default), wavelength_physical is used.

Return type:

AbstractCartesian2dVectorArray

replace(**changes)#

A copy of this object with the given fields replaced.

A method version of dataclasses.replace(), matching named_arrays.AbstractArray.replace(), so that a nested change reads the same at every level:

system.replace(
    surface=system.surface.replace(
        sag=system.surface.sag.replace(radius=r),
    ),
)

The copy is shallow, as dataclasses.replace() is: every field which is not named is shared with the original, and mutating one through the copy is mutating it in both. Take copy.deepcopy() of the result where that matters.

Parameters:

changes – The fields to overwrite in the copy.

Return type:

Self

schematic_primary(ax=None, transformation=None, footprint=True, color='black', kwargs_footprint=None, **kwargs)#

Plot a schematic of the primary mirror along with the beam footprint.

Parameters:
  • ax (None | Axes) – The matplotlib axes on which to plot this schematic. If None (the default), the schematic is plotted on the current axes.

  • transformation (None | AbstractTransformation) – An additional transformation to apply to the coordinates before plotting the schematic.

  • footprint (bool) – Whether to plot the footprint of the rays on the mirror surface.

  • color (str) – The color of the primary mirror.

  • kwargs_footprint (None | dict[str, Any]) – Additional kwargs for plotting the footprint of the beam.

  • kwargs – Additional kwargs for plotting the primary mirror.

Return type:

None

to_string(prefix=None)#

Public-facing version of the __repr__ method that allows for defining a prefix string, which can be used to calculate how much whitespace to add to the beginning of each line of the result.

Parameters:

prefix (None | str) – an optional string, the length of which is used to calculate how much whitespace to add to the result.

Return type:

str

property angle_grating_input: AbstractScalar#

The angle between the grating normal and the direction of the incident light.

This is the incidence angle \(theta_i\) in the diffraction grating equation.

property angle_grating_output: AbstractScalar#

The angle between the grating normal and the direction of the diffracted light.

This is an analogue to the diffracted angle in the diffraction grating equation.

property axes: tuple[str, ...]#

The names of the axes of this object, tuple(self.shape).

axis_channel: str = 'channel'#

The name of the logical axis corresponding to changing camera channel.

camera: None | Camera = None#

A model of the camera and sensors.

central_obscuration: None | CentralObscuration = None#

A model of the central obscuration.

field: None | AbstractCartesian2dVectorArray = None#

A default grid of field positions to trace through the system.

field_stop: None | FieldStop = None#

A model of the field stop.

filter: None | Filter = None#

A model of the thin-film filters.

front_aperture: None | FrontAperture = None#

A model of the front aperture plate.

grating: None | Grating = None#

A model of the diffraction grating array.

kwargs_plot: None | dict = None#

Extra keyword arguments used to plot the optical system.

name: str = 'ESIS'#

The human-readable name of the instrument.

property ndim: int#

The number of dimensions of this object, len(self.shape).

pitch: Quantity | AbstractScalar = <Quantity 0. deg>#

The pitch angle of the instrument.

primary_mirror: None | PrimaryMirror = None#

A model of the primary mirror.

pupil: None | AbstractCartesian2dVectorArray = None#

A default grid of pupil positions to trace through the system.

roll: Quantity | AbstractScalar = <Quantity 0. deg>#

The roll angle of the instrument.

property shape: dict[str, int]#

The broadcasted shape of every array-like field of this object.

property size: int#

The total number of elements in this object, the product of the values of shape.

property system: SequentialSystem#

Convert this model into an instance of optika.systems.SequentialSystem.

This is a cached property that is only computed once.

property transformation: AbstractTransformation#

the coordinate transformation between the global coordinate system and this object’s local coordinate system

wavelength: None | Quantity | AbstractScalar = None#

A default grid of wavelengths to trace through the system.

property wavelength_max: Quantity | AbstractScalar#

The maximum wavelength permitted through the system.

property wavelength_min: Quantity | AbstractScalar#

The minimum wavelength permitted through the system.

property wavelength_physical: ScalarArray#

The value of wavelength converted to physical units if needed.

yaw: Quantity | AbstractScalar = <Quantity 0. deg>#

The yaw angle of the instrument.