Skip to content

Commit

Permalink
Migrate Grism functionality to DispersiveTilt
Browse files Browse the repository at this point in the history
* New TiltInterface class for common Tilt logic
* Grism contructor now raises a DeprecationWarning
  • Loading branch information
andykee committed Nov 21, 2023
1 parent 5cc9bd5 commit 322e3ac
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 85 deletions.
4 changes: 2 additions & 2 deletions lentil/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
Pupil,
Image,
Detector,
DispersiveShift,
Tilt,
DispersiveTilt,
Grism,
LensletArray,
Tilt,
Rotate,
Flip
)
Expand Down
224 changes: 148 additions & 76 deletions lentil/plane.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
from warnings import warn

import numpy as np
from scipy import ndimage
Expand Down Expand Up @@ -612,38 +613,89 @@ class Detector(Image):
pass


class DispersivePhase(Plane):
class TiltInterfcace(Plane):
# Utility class for holding some common logic shared by
# classes that implement the Tilt interface

def multiply(self, wavefront):
# NOTE: we can handle wavelength-dependent phase terms here (e.g. chromatic
# aberrations). Since the phase will vary by wavelength, we can't fit out the
# tilt pre-propagation and apply the same tilt for each wavelength like we can
# with run of the mill tilt
raise NotImplementedError


class DispersiveShift(Plane):

def shift(self, wavelength, x0, y0, **kwargs):
raise NotImplementedError
def __init__(self, **kwargs):
# if ptype is provided as a kwarg use that, otherwise default
# to lentil.tilt
ptype = kwargs.pop('ptype', None)
if not ptype:
ptype = lentil.tilt
super().__init__(ptype=ptype, **kwargs)

def multiply(self, wavefront):
wavefront = super().multiply(wavefront)
for field in wavefront.data:
field.tilt.append(self)
return wavefront

def shift(self, wavelength, x0, y0, **kwargs):
raise NotImplementedError


class Grism(DispersiveShift):
r"""Class for representing a grism.
class Tilt(TiltInterfcace):
"""Object for representing tilt in terms of angle
A grism is an optical element that can be inserted into a collimated beam
to disperse incoming light according to its wavelength.
Parameters
----------
x : float
Radians of tilt about the x-axis
y : float
Radians of tilt about the y-axis
"""
def __init__(self, x, y, **kwargs):
super().__init__(**kwargs)
self.x = y # y tilt is about the x-axis.
self.y = x # x tilt is about the y-axis.

def shift(self, xs=0, ys=0, z=0, **kwargs):
"""Compute image plane shift due to this angular tilt
Parameters
----------
xs : float
Incoming x shift distance. Default is 0.
ys : float
Incoming y shift distance. Default is 0.
z : float
Propagation distance
Returns
-------
shift : tuple
Updated x and y shift terms
"""
x = xs - (z * self.x)
y = ys - (z * self.y)
return x, y


class DispersiveTilt(TiltInterfcace):
r"""Class for representing spectral dispersion that appears as a tilt.
Light is dispersed along a line called the the spectral trace. The position
along the trace is determined by the dispersion function of the grism. The
local origin of the spectral trace is anchored relative to the undispersed
position of the source. This basic geometry is illustrated in the figure
along the trace is determined by the dispersion function. The local origin
of the spectral trace is anchored relative to the undispersed position of
the source.
Parameters
----------
trace : array_like
Polynomial coefficients describing the spectral trace in decreasing
powers (i.e. trace[0] represents the highest order coefficient and
trace[-1] represents the lowest).
dispersion : array_like
Polynomial coefficients describing the dispersion in decreasing powers
(i.e. dispersion[0] represents the highest order coefficient and
dispersion[-1] represents the lowest.)
Notes
-----
The basic geometry of spectral dispersion is illustrated in the figure
below:
.. image:: /_static/img/grism_geometry.png
Expand All @@ -669,8 +721,6 @@ class Grism(DispersiveShift):
and should return units of meters of wavelength provided an input distance
along the spectral trace.
Note
----
Lentil supports trace and dispersion functions with any arbitrary polynomial
order. While a simple analytic solution exists for modeling first-order trace
and/or dispersion, there is no general solution for higher order functions.
Expand All @@ -681,23 +731,10 @@ class Grism(DispersiveShift):
dispersion functions. In cases where speed or accuracy are extremely important,
a custom solution may be required.
Parameters
----------
trace : array_like
Polynomial coefficients describing the spectral trace produced by the
grism in decreasing powers (i.e. trace[0] represents the highest order
coefficient and trace[-1] represents the lowest).
dispersion : array_like
Polynomial coefficients describing the dispersion produced by the grism
in decreasing powers (i.e. dispersion[0] represents the highest order
coefficient and dispersion[-1] represents the lowest.)
"""
def __init__(self, trace, dispersion, pixelscale=None, amplitude=1,
phase=0, mask=None):
super().__init__(pixelscale=pixelscale, amplitude=amplitude, phase=phase,
mask=mask)

def __init__(self, trace, dispersion, **kwargs):
super().__init__(**kwargs)

self.trace = np.asarray(trace)
self._trace_order = self.trace.size - 1
assert self._trace_order >= 1
Expand Down Expand Up @@ -799,56 +836,91 @@ def _arc_len(dist_func, a, b):
"""
return scipy.integrate.quad(dist_func, a, b)[0]


class DispersivePhase(Plane):

class LensletArray(Plane):
pass
def multiply(self, wavefront):
# NOTE: we can handle wavelength-dependent phase terms here (e.g. chromatic
# aberrations). Since the phase will vary by wavelength, we can't fit out the
# tilt pre-propagation and apply the same tilt for each wavelength like we can
# with run of the mill tilt
raise NotImplementedError


class Tilt(Plane):
"""Object for representing tilt in terms of an angle
class Grism(DispersiveTilt):
r"""Class for representing a grism.
Parameters
----------
x : float
Radians of tilt about the x-axis
y : float
Radians of tilt about the y-axis
A grism is an optical element that can be inserted into a collimated beam
to disperse incoming light according to its wavelength.
"""
def __init__(self, x, y):
super().__init__(ptype=lentil.tilt)
Light is dispersed along a line called the the spectral trace. The position
along the trace is determined by the dispersion function of the grism. The
local origin of the spectral trace is anchored relative to the undispersed
position of the source. This basic geometry is illustrated in the figure
below:
self.x = y # y tilt is about the x-axis.
self.y = x # x tilt is about the y-axis.
.. image:: /_static/img/grism_geometry.png
:align: center
:width: 400px
def multiply(self, wavefront):
wavefront = super().multiply(wavefront)
for field in wavefront.data:
field.tilt.append(self)
return wavefront
The spectral trace is parameterized by a polynomial of the form
def shift(self, xs=0, ys=0, z=0, **kwargs):
"""Compute image plane shift due to this angular tilt
.. math::
Parameters
----------
xs : float
Incoming x shift distance. Default is 0.
ys : float
Incoming y shift distance. Default is 0.
z : float
Propagation distance
y = a_n x^n + \cdots + a_2 x^2 + a_1 x + a_0
Returns
-------
shift : tuple
Updated x and y shift terms
and should return units of meters on the focal plane provided an input
in meters on the focal plane.
"""
x = xs - (z * self.x)
y = ys - (z * self.y)
return x, y
Similarly, the wavelength along the trace is parameterized by a
polynomial of the form
.. math::
\lambda = a_n d^n + \cdots + a_2 d^2 + a_1 d + a_0
and should return units of meters of wavelength provided an input distance
along the spectral trace.
Note
----
Lentil supports trace and dispersion functions with any arbitrary polynomial
order. While a simple analytic solution exists for modeling first-order trace
and/or dispersion, there is no general solution for higher order functions.
As a result, trace and/or dispersion polynomials with order > 1 are evaluated
numerically. Although the effects are small, this approach impacts both the
speed and precision of modeling grisms with higher order trace and/or
dispersion functions. In cases where speed or accuracy are extremely important,
a custom solution may be required.
Parameters
----------
trace : array_like
Polynomial coefficients describing the spectral trace produced by the
grism in decreasing powers (i.e. trace[0] represents the highest order
coefficient and trace[-1] represents the lowest).
dispersion : array_like
Polynomial coefficients describing the dispersion produced by the grism
in decreasing powers (i.e. dispersion[0] represents the highest order
coefficient and dispersion[-1] represents the lowest.)
.. deprecated:: 1.0.0
`Grism` will be removed in Lentil v1.0.0, it is replaced by
`DispersiveTilt`.
"""
def __init__(self, trace, dispersion, **kwargs):
warn('lentil.Grism will be deprecated in v1.0.0, it is '
'replaced by lentil.DispersiveTilt.',
DeprecationWarning,
stacklevel=2)
super().__init__(trace=trace, dispersion=dispersion, **kwargs)


class LensletArray(Plane):
pass


class Rotate(Plane):
Expand Down
14 changes: 7 additions & 7 deletions tests/test_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,22 @@ def test_pupil_rescale_power():
assert math.isclose(amp_power, ampr_power, rel_tol=1e-3)


def test_grism_center():
def test_dispersive_tilt_center():
dispersion = [1, 650e-9]
trace = [2, 0]
grism = lentil.Grism(dispersion=dispersion, trace=trace)
dt = lentil.DispersiveTilt(dispersion=dispersion, trace=trace)

x0 = np.random.uniform(low=-5, high=5)
y0 = np.random.uniform(low=-5, high=5)
x, y = grism.shift(wavelength=650e-9, xs=x0, ys=y0)
x, y = dt.shift(wavelength=650e-9, xs=x0, ys=y0)
assert np.all((x == x0, y == y0))


def test_grism_shift():
grism = lentil.Grism(trace=[1,1], dispersion=[1,650e-9])
def test_dispersive_tilt_shift():
dt = lentil.DispersiveTilt(trace=[1,1], dispersion=[1,650e-9])
wave = 900e-9
x, y = grism.shift(wavelength=wave)
x, y = dt.shift(wavelength=wave)

assert x == (wave - grism.dispersion[1])/np.sqrt(2)
assert x == (wave - dt.dispersion[1])/np.sqrt(2)
assert y == 1+x

0 comments on commit 322e3ac

Please sign in to comment.