Distortions

The raypier.distortions module contains objects representing distortions of a given face. The Distortion objects are intended to be used with the raypier.faces.DistortionFace class (part of the General Optic framework). Fundamentally, any 2D function can be implemented as a Distortion. At present, on a single type is implemented. I intend to implement a general Zernike polynomial distortion class. Other distortion functions are easy to add.

An example of their usage:



from raypier.tracer import RayTraceModel
from raypier.shapes import CircleShape
from raypier.faces import DistortionFace, PlanarFace, SphericalFace
from raypier.general_optic import GeneralLens
from raypier.materials import OpticalMaterial
from raypier.distortions import SimpleTestZernikeJ7, NullDistortion
from raypier.gausslet_sources import CollimatedGaussletSource
from raypier.fields import EFieldPlane
from raypier.probes import GaussletCapturePlane
from raypier.intensity_image import IntensityImageView
from raypier.intensity_surface import IntensitySurface

shape = CircleShape(radius=10.0)
f1 = SphericalFace(z_height=0.0, curvature=-25.0)
f2 = PlanarFace(z_height=5.0)

dist = SimpleTestZernikeJ7(unit_radius=10.0, amplitude=0.01)
#dist = NullDistortion()
f1 = DistortionFace(base_face=f1, distortion=dist)

mat = OpticalMaterial(glass_name="N-BK7")
lens = GeneralLens(shape=shape, surfaces=[f1,f2], materials=[mat])

src = CollimatedGaussletSource(radius=8.0, resolution=6, 
                               origin=(0,0,-15), direction=(0,0,1),
                               display="wires", opacity=0.2, show_normals=True)
src.max_ray_len=50.0


cap = GaussletCapturePlane(centre = (0,0,50),
                           direction= (0,0,1),
                           width=20,
                           height=20)

field = EFieldPlane(detector=cap,
                    align_detector=True,
                    size=100,
                    width=1,
                    height=1)

img = IntensityImageView(field_probe=field)
surf = IntensitySurface(field_probe=field)


model = RayTraceModel(optics=[lens], sources=[src], probes=[field,cap],
                      results=[img,surf])
model.configure_traits()

This example shows a high-amplitude distortion, for illustrative purposes.

_images/distortion_example.png

During the ray-tracing operation, the intersections with distorted faces are found using an iterative algorithm similar to Newton- Ralphson. Typically, the intersection is found with 2 to 3 calls to the intercept-method of the underlying face. Distortions are expected to be small deviations from the underlying face (maybe no more than a few wavelengths at most). If you make the amplitude of the distortion large, the under of iterations to converge will increase and the ray-tracing hit take a performance hit. For very large distortions, the intercept my fail altogether.

One could, in principle, wrap multiple DistortionFaces over other DistortionFaces. However, I would expect the performance penalty to be quite severe. In this case, A better plan would be to implement a specialised DistortionList object which can sum the distortion-values from a list of input Distortions. On my todo list …

In python scripting, one can simply evaluate any Distortion object given some x- and y-coordinates as numpy arrays. This is useful for testing. For example:

from raypier.distortions import SimpleTestZernikeJ7
import numpy

dist = SimpleTestZernikeJ7(unit_radius=10.0, amplitude=0,1)

x=y=nmupy.linspace(-10,10,500)
X,Y = numpy.meshgrid(x,y)

Z = dist.z_offset(X.ravel(),Y.ravel())
Z.shape = X.shape #restore the 2D shape of the Z array

Distortions have an additional method, Distortion.z_offset_and_gradient(). This returns a array of shape (N,3) where the input X and Y arrays have length N. The first two columns of this array contain the gradient of the distortion, dZ/dX and dZ/dY respectively. The third column simply contains Z. Returning both Z and it’s gradient turns out to be useful at the C-level during tracing. I.e.:

grad = dist.z_offset_and_gradient(X.ravel(), Y.ravel()).reshape(X.shape[0], X.shape[1],3)
dZdX = grad[...,0]
dZdY = grad[...,1]
Z = grad[...,2]

Zernike Polymonial Distortions

More general distortions can be applied using the raypier.distortions.ZernikeSeries class.

As previously, instances of this object are passed to a raypier.faces.DistortionFace , along with the base-surface to which the distortion is to be applied.

An example of the this class in action can be seen here:



from raypier.tracer import RayTraceModel
from raypier.shapes import CircleShape
from raypier.faces import DistortionFace, PlanarFace, SphericalFace
from raypier.general_optic import GeneralLens
from raypier.materials import OpticalMaterial
from raypier.distortions import SimpleTestZernikeJ7, NullDistortion, ZernikeSeries
from raypier.gausslet_sources import CollimatedGaussletSource
from raypier.fields import EFieldPlane
from raypier.probes import GaussletCapturePlane
from raypier.intensity_image import IntensityImageView
from raypier.intensity_surface import IntensitySurface
from raypier.api import Constraint

from traits.api import Range, observe
from traitsui.api import View, Item, VGroup


shape = CircleShape(radius=10.0)

#f1 = SphericalFace(z_height=0.0, curvature=-25.0)

f1 = PlanarFace(z_height=0.0)
f2 = PlanarFace(z_height=5.0)

dist = ZernikeSeries(unit_radius=10.0, coefficients=[(i,0.0) for i in range(12)])
f1 = DistortionFace(base_face=f1, distortion=dist)


class Sliders(Constraint):
    """Make a Constrain object just to give us a more convenient UI for adjusting Zernike coefficients.
    """
    J0 = Range(-1.0,1.0,0.0)
    J1 = Range(-1.0,1.0,0.0)
    J2 = Range(-1.0,1.0,0.0)
    J3 = Range(-1.0,1.0,0.0)
    J4 = Range(-1.0,1.0,0.0)
    J5 = Range(-1.0,1.0,0.0)
    J6 = Range(-1.0,1.0,0.0)
    J7 = Range(-1.0,1.0,0.0)
    J8 = Range(-1.0,1.0,0.0)
    
    traits_view = View(VGroup(
            Item("J0", style="custom"),
            Item("J1", style="custom"),
            Item("J2", style="custom"),
            Item("J3", style="custom"),
            Item("J4", style="custom"),
            Item("J5", style="custom"),
            Item("J6", style="custom"),
            Item("J7", style="custom"),
            Item("J8", style="custom"),
        ),
        resizable=True)
    
    def _anytrait_changed(self):
        dist.coefficients = list(enumerate([self.J0, self.J1, self.J2, self.J3, self.J4, 
                                            self.J5, self.J6, self.J7, self.J8]))


mat = OpticalMaterial(glass_name="N-BK7")
lens = GeneralLens(shape=shape, surfaces=[f1,f2], materials=[mat])

src = CollimatedGaussletSource(radius=9.0, resolution=20, 
                               origin=(0,0,-15), direction=(0,0,1),
                               display="wires", opacity=0.02,
                               wavelength=1.0, 
                               beam_waist=10.0,
                               show_normals=True)
src.max_ray_len=50.0


cap = GaussletCapturePlane(centre = (0,0,50),
                           direction= (0,0,1),
                           width=20,
                           height=20)

field = EFieldPlane(centre=(0,0,30),
                    detector=cap,
                    align_detector=True,
                    size=100,
                    width=20,
                    height=20)

img = IntensityImageView(field_probe=field)
surf = IntensitySurface(field_probe=field)


model = RayTraceModel(optics=[lens], sources=[src], probes=[field,cap],
                      results=[img,surf], constraints=[Sliders()])
model.configure_traits()

Here’s what the model looks like in the UI.

_images/zernike_distortions_example.png

This example also demonstrates the use of a Constraints object to provide some UI controls for easier adjustment of the relevant model parameters.