6. Experimenting with LensLab



6.1 Building a Black Hole

6.2 Using a Prism to Create Rainbows and More

6.3 Making an Angular Amplifier

6.4 Modeling a Cassegrain Telescope

Introduction to Chapter 6

In this chapter, we use LensLab for performing some advanced optical experiments. In Section 6.1, we construct a model of a Cassegrain telescope. Then, in Section 6.2, we use the Resonate genetic building block to construct a reflective optical tunnel. Later, in Section 6.3, we evaluate prism dispersion of white light. Finally in Section 6.4, we use refraction for building a novel angular amplifier.

Loading LensLab

Make sure that the LensLab package is located either in the home directory, or on a directory path recognized by Mathematica for packages. The LensLab package is named LensLab.m and located in the LensLab directory, and the LensLab package is loaded with the following expression.



LensLab version 1.3.2 is now loaded.

This loading process should only take a few seconds. In addition to being loaded as a package, the LensLab.m file is formatted as a Mathematica notebook. The LensLab source code is made accessible so that you can develop new functions of your own by studying LensLab's built-in functions. This is particularly helpful when you wish to model new component ideas in LensLab. However, you should receive permission from Optica Software before distributing any user-created functions that are derived from the LensLab source code. The unauthorized distribution of LensLab-derived code may be a LensLab license agreement violation or a copyright infringement.

Go to list of topics

6.1 Building a "Black Hole"

This example demonstrates how you can use LensLab to blend traditional mathematical functions with optics to create new and interesting optical components. Here we define a new component that acts as a reflective tapered tube, similar in nature to an optical fiber. To give this new component a tube shape, we use the hyperbolic function.


S[x_,y_] = 100/(x^2 + y^2);

A simple plot of 100/x^2 reveals its basic nature.




Note the singularity at x = 0. This singularity must be carefully treated when defining the new component in LensLab. Since our component will be three-dimensional, we next plot the three-dimensional hyperbolic function.




Having seen the basic nature of the hyperbolic function, we are nearly ready to fully define the component function. The pole in our function will be masked by putting a hole at its center. In addition, the option FunctionCenter->0 will be used to redefine the surface function at S[0,0] to be zero, instead of infinity. We give the new component function the name BlackHole for its geometric likeness to the gravitation field around a black hole in space. As discussed in Chapter 5, we define BlackHole by calling a series of genetic building blocks, each of which has a specific job in defining the behavior of the optical component. In this example we use Resonate to allow the same surface to be used for multiple interactions with the ray.

In addition to Resonate, we use CustomMirror for the basic mirror structure and Hole to put a hole in the component center. BlackHole takes arguments for the size of its mouth and the size of its hole at the center.



BlackHole[aperture_,holeaperture_,opts___] :=
    options = Flatten[{opts,Options[CustomMirror]}];
                Function[100./(#1^2 + #2^2)],
                SurfaceLabel -> OtherShape,
                FunctionCenter -> 0.,

Notice that we use the CustomMirror options to define BlackHole. This eliminates the need to create a special options listing for BlackHole. First let's just make a three-dimensional plot of BlackHole.




Next we view the mouth of BlackHole.




Finally we test out the effects of a single ray sent into the enlarged mouth of BlackHole.


sys =


For a better view, the image might need to be expanded using the mouse cursor to drag the corner of the graphics frame. We see that the ray bounced around inside the tube, and did not leave the other side. Instead, it seems to have turned around and bounced back out the entrance. We next see the process in better detail by creating an animation. The basic Mathematica code used below is typical for making LensLab animations. Be forewarned that limited computer memories may become taxed by the number of images required for animations.

To see how many frames we need, we check the number of Ray objects in the system.





We use RayChoice->{IntersectionNumber->i} to display a different ray segment in each frame.




In the previous ray tracing, we sent the ray into the component defined by BlackHole with an initial angle of 30 degrees. Let us now use a wedge of rays having angles shallower then 30 degrees. For more robust ray-tracing of some custom surfaces, you can use the SurfaceRayIntersections -> Solve option setting. In this case, the Solve function is used to find the symbolic solution of the surface intersection point while the default setting of SurfaceRayIntersections -> Automatic uses the FindRoot function. FindRoot can sometimes fail because sometime finds only a local minimum and may miss the global minimum for the surface intersection point.


sys =
    {Move[WedgeOfRays[30, NumberOfRays->6],{15,0,0}],


This time all of the rays made it through the tube without turning around. Next we create an animation for this system. First we must determine how many ray bounces occurred inside the tube. This time the number of frames is not simply given by the number of Ray objects divided by the number of rays. Instead, we use the following expression to check how many ray bounces occurred inside the tube.





Again, we use RayChoice->{IntersectionNumber->i} to display different ray segments in each frame.


        PlotRange->{{0,120},{-17,17}}], {i,1,21}];


Note that the shallower angles exit more quickly than the steeper angles. Next we use ReadRays to measure the total optical path length of the different rays.


ReadRays[sys, OpticalLength, ComponentNumber -> 2]


RowBox[{{, RowBox[{105.516, ,, 105.516, ,, 110.475, ,, 110.475, ,, 124.525, ,, 124.525}], }}]

We also measure the difference in optical path length using the built-in Max and Min functions of Mathematica.





This example only touches on the possibilities of building and analyzing novel optical components with LensLab.

Go to list of topics

6.2 Using a Prism to Create Rainbows and More

LensLab can be used to show the dispersive effects of prisms. Here we will do a simple experiment by sending a ray of "white" light through a typical prism (BK-7 glass). Our ray is actually six rays having wavelength distributions equally spaced between 400 nm and 700 nm. Since the index of refraction is a nonlinear function of wavelength, we can see the relationship between spatial distribution and wavelength on a flat optical surface intersecting with the rays.


    RainbowOfRays[{.4,.7}, NumberOfRays->6],





Next we look at where the rays intersect with the boundary.




Unfortunately, in this picture, the spot size of the rays is too small to see the relationship between wavelength and position. Let's put a screen in the system that has an aperture size slightly larger than the spot size. We can determine the appropriate screen dimensions and placement coordinates by using the cursor to take measurements on the above two pictures. This is done by first pointing to an image, pressing the mouse button, and at the same time pressing the Command key. We see that the intersection spot ranges from -139 to -155 in the horizontal. Using measurements from PlotType->TopView shown above, we place the screen at {296, -145}.


    RainbowOfRays[{.4,.7}, NumberOfRays->6],


We now see clearly how the positions are a nonlinear function of wavelength (which is why diffraction gratings are preferred over prisms for making careful spectroscopic measurements).

We next use Mathematica to make a plot of the horizontal component of the intersection points, and then fit a curve to these data points. First, let's trace through the system using 16 rays instead of 6.


colorsys = DrawSystem[{

We then use ReadRays to read out ray parameter values for wavelength and position at the ray/screen intersection. First, let's check out again how to use ReadRays. To do this, we use the built-in help function, ?ReadRays.



ReadRays[objectset, rayparameters, selectionproperties, options] is an advanced function that  ... a listing of the ray parameters available. See also: RaySelect for more about selectionproperties.

Next we must extract the horizontal component of each ray/screen intersection, creating a list of numbers for plotting. Before we do this, we need to review Options[Ray] to learn the parameter name for surface intersections.




RowBox[{{, RowBox[{RowBox[{BirthPoint, , RowBox[{{, RowBox[{0., ,, 0., ,, 0.}], }}]}], ... , ,, UnconfinedPositionUnconfinedPosition, ,, RowBox[{WaveLength, , 0.532}]}], }}]

SurfaceCoordinates seems to be the right parameter we want. To be sure, we again use the on-line help function ?SurfaceCoordinates.



SurfaceCoordinates is a rule of Ray that gives the parametric coordinate of the last surface function to intersect with the ray.

Finally, we use ReadRays with SurfaceCoordinates to extract the horizontal values. These numbers are measurements of distance along the screen object.


horizontalValues =
    ReadRays[colorsys,SurfaceCoordinates, ComponentNumber->2]


RowBox[{{, RowBox[{RowBox[{{, RowBox[{RowBox[{-, 1.94891}], ,, 0}], }}], ,, RowBox[{{, RowBox[ ...  }}], ,, RowBox[{{, RowBox[{5.38112, ,, 0}], }}], ,, RowBox[{{, RowBox[{5.6064, ,, 0}], }}]}], }}]

Since we are only concerned with the first surface coordinate, we use Map with First to extract the first values.


horizontalValues = Map[First[#]&,horizontalValues]


RowBox[{{, RowBox[{RowBox[{-, 1.94891}], ,, RowBox[{-, 0.866949}], ,, 0.0532683, ,, 0.84439, , ...  3.55484, ,, 3.93406, ,, 4.27731, ,, 4.58973, ,, 4.87555, ,, 5.13835, ,, 5.38112, ,, 5.6064}], }}]

We make the numbers start from 0.0 by subtracting the minimum offset from these values.


horizontalValues = horizontalValues-Min[horizontalValues]


RowBox[{{, RowBox[{0., ,, 1.08196, ,, 2.00218, ,, 2.7933, ,, 3.47994, ,, 4.081, ,, 4.61125, ,, ... 5.50375, ,, 5.88297, ,, 6.22622, ,, 6.53864, ,, 6.82446, ,, 7.08726, ,, 7.33003, ,, 7.55531}], }}]

Now we use ListPlot to generate a plot of these values.




Next, we create a second list, containing the wavelength of each intersecting ray. By multiplying by 1000, we get units of nanometer.


wavelengths =


RowBox[{{, RowBox[{400., ,, 420., ,, 440., ,, 460., ,, 480., ,, 500., ,, 520., ,, 540., ,, 560., ,, 580., ,, 600., ,, 620., ,, 640., ,, 660., ,, 680., ,, 700.}], }}]

Finally, we combine the two lists to make ordered pairs of numbers.


displacements = Transpose[{wavelengths,horizontalValues}]


RowBox[{{, RowBox[{RowBox[{{, RowBox[{400., ,, 0.}], }}], ,, RowBox[{{, RowBox[{420., ,, 1.081 ... , RowBox[{{, RowBox[{680., ,, 7.33003}], }}], ,, RowBox[{{, RowBox[{700., ,, 7.55531}], }}]}], }}]

Now we make a plot containing intersection distance as a function of wavelength. We make a solid curved line using PlotJoined->True.


measuredPlot =


We now take things a step further by using the Fit function to come up with a calibrated function for our physical system. Let's first check out how to use Fit.



Fit[data, funs, vars] finds a least-squares fit to a list of data as a linear combination of t ... . The argument funs can be any list of functions that depend only on the objects vars. More…

Now we do a quadratic fit.




RowBox[{RowBox[{-, 28.0265}], +, RowBox[{0.0975174,  , x}], -, RowBox[{0.0000670516,  , x^2}]}]

Now we plot the fitted function.


fittedPlot = Plot[%,{x,400,700}];


Finally, let's compare this graph with the graphed values "measured" using LensLab.




We see that our fitted function doesn't quite match the measured values. If we wanted to, we could go back and try a different fitting function, perhaps involving a sin term or a higher-order polynomial, until a better match was found.

Go to list of topics

6.3 Making an Angular Amplifier

Using LensLab, it is possible to quickly explore novel ideas. This example illustrates a possible method for making an angular amplifier that takes an input signal containing a small angular deviation, and produces an output signal having an amplified angular deviation. Using the properties of Snell's law, this amplifier works by sending a beam of light through a glass/air interface such that the input beam, coming from the high refractive index side, hits the interface very near the critical angle for the interface. The result is that the input angle gets amplified by the interface.


    Move[WedgeOfRays[2, OpticalMedium -> BK7, NumberOfRays->6],{0,-25},40],


The previous diagram demonstrates the basic system. We characterize the system more accurately by creating an input/output model of the system behavior. First of all, let's take more data points by increasing the number of rays used in our example. This time, we won't make a plot, but instead use PropagateSystem and store the results in system.


system = PropagateSystem[{
    Move[WedgeOfRays[2, OpticalMedium -> BK7, NumberOfRays->16],{0,-25},40],

We must extract from system only the input rays and output rays of the system. This is done using RaySelect.


inputrays = RaySelect[system,IntersectionNumber->1]


{Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray}


outputrays = RaySelect[system,IntersectionNumber->2]


{Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray, Ray}

Now we will extract the angle information from the input rays and output rays. For this we use the ray parameter RayTilt.

We extract the ray object angle by using Map and ReplaceAll, indicated by /. in the following expression. In addition ArcTan is employed for converting the direction vector into an angle. We store this result in outputangles.


outputangles = Map[(N[ArcTan[(RayTilt/.#)[[2]]/(RayTilt/.#)[[1]]]/Degree])&, outputrays]


RowBox[{{, RowBox[{72.823, ,, 73.3634, ,, 73.9204, ,, 74.4957, ,, 75.0915, ,, 75.7103, ,, 76.3 ... 77.7402, ,, 78.4917, ,, 79.2934, ,, 80.1577, ,, 81.1027, ,, 82.1574, ,, 83.3743, ,, 84.8673}], }}]

Note that we could get the same result by using ReadRays and taking more steps. Since we are interested in relative angular displacements, rather than absolute displacements, we now subtract off the angular minimum using Min.


outputangles = outputangles - Min[outputangles]


RowBox[{{, RowBox[{0., ,, 0.540416, ,, 1.09738, ,, 1.67269, ,, 2.26845, ,, 2.88723, ,, 3.53217 ... 4.91716, ,, 5.66867, ,, 6.47042, ,, 7.33467, ,, 8.27963, ,, 9.33442, ,, 10.5513, ,, 12.0443}], }}]

We now plot the angular displacements.




Similarly, we extract the input angular variation, storing the results in inputangles.


inputangles = Map[(N[ArcTan[(RayTilt/.#)[[2]]/(RayTilt/.#)[[1]]]/Degree])&, inputrays]


RowBox[{{, RowBox[{39., ,, 39.1333, ,, 39.2667, ,, 39.4, ,, 39.5333, ,, 39.6667, ,, 39.8, ,, 3 ... .9333, ,, 40.0667, ,, 40.2, ,, 40.3333, ,, 40.4667, ,, 40.6, ,, 40.7333, ,, 40.8667, ,, 41.}], }}]

Again, we subtract off the angular minimum.


inputangles = inputangles - Min[inputangles]


RowBox[{{, RowBox[{0., ,, 0.133333, ,, 0.266667, ,, 0.4, ,, 0.533333, ,, 0.666667, ,, 0.8, ,, 0.933333, ,, 1.06667, ,, 1.2, ,, 1.33333, ,, 1.46667, ,, 1.6, ,, 1.73333, ,, 1.86667, ,, 2.}], }}]

Next we combine the input and output variables into a list of ordered pairs.


transfercurve = Transpose[{inputangles,outputangles}]


RowBox[{{, RowBox[{RowBox[{{, RowBox[{0., ,, 0.}], }}], ,, RowBox[{{, RowBox[{0.133333, ,, 0.5 ...  RowBox[{{, RowBox[{1.86667, ,, 10.5513}], }}], ,, RowBox[{{, RowBox[{2., ,, 12.0443}], }}]}], }}]

We now plot the output angle as a function of the input angle.


measuredPlot = ListPlot[transfercurve,PlotJoined->True];


Next we create a model of this system. Initially, we apply a linear fit to the data points.


fittedfunction = Fit[transfercurve,{1,x},x]


RowBox[{RowBox[{-, 0.700074}], +, RowBox[{5.75045,  , x}]}]

Here we see that our amplifier has a gain of nearly 6.

Now we plot the fitted function.


fittedPlot = Plot[fittedfunction,{x,0,2}];


Finally, let's compare this graph with the "measured" results.




Clearly, our system is not terribly linear. Next we calculate and plot the error between the measured results and the fitted results. First we create a list of numbers using our fitted function.


fittedyvalues = Table[fittedfunction,{x,0,2,2./15}]


RowBox[{{, RowBox[{RowBox[{-, 0.700074}], ,, 0.0666524, ,, 0.833379, ,, 1.60011, ,, 2.36683, , ... 5.43374, ,, 6.20047, ,, 6.96719, ,, 7.73392, ,, 8.50065, ,, 9.26737, ,, 10.0341, ,, 10.8008}], }}]

Next, we use ListPlot to plot the difference between the fitted and actual values.


    (fittedyvalues - outputangles)}],PlotJoined->True];


As a further refinement to this example, we make a new Mathematica function that takes any optical system and returns a fitted function to describe the system gain. We will call this function ModelGain. This function is constructed by piecing together all of the steps performed separately above.


ModelGain[opticalsystem_, outputIntersectionNumber_,
    fittingfunction_, options___]:=
    inputangles,fittedfunction,    transfercurve,inputrange,
(* Propagate optical system, if system has not already
been propagated *)
system = PropagateSystem[opticalsystem];
inputrays = RaySelect[system,IntersectionNumber->1];
outputrays = RaySelect[system,
outputangles = Map[(N[ArcTan[(RayTilt/.#)[[2]]/
outputangles = outputangles - Min[outputangles];
inputangles = Map[(N[ArcTan[(RayTilt/.#)[[2]]/
inputangles = inputangles - Min[inputangles];
inputrange = Max[inputangles];
transfercurve = Transpose[{inputangles,outputangles}];
(* Now plot measured parameters *)
measuredplot = ListPlot[transfercurve,PlotJoined->True];
fittedfunction = Fit[transfercurve,fittingfunction,x];
(* Now plot fitted parameters *)
fittedPlot = Plot[fittedfunction,{x,0,inputrange}];
(* Show both plots together *)
(* Plot deviation between measured and fitted values *)
(* Finally, output the fitted function *)

Using ModelGain, we change parameters and then quickly test the system. For now, let's continue to use the same system and find a quadratic fit model for the system.








RowBox[{RowBox[{3.25716,  , x}], +, RowBox[{1.28209,  , x^2}]}]

Notice that our quadratic amplifier model more closely follows the measured parameters. In this example, we have examined a system behaving as an angular amplifier. In addition, we have seen how new Mathematica functions are quickly created for performing intricate tasks. We could conceivably build a multistage amplifier for higher, more linear gain.

Go to list of topics

6.4 Modeling a Cassegrain Telescope

In this section, we model a Cassegrain telescope with LensLab, including the primary and secondary mirror objectives. For this example, we combine several component functions into a special function that we call CassegrainTelescope. Within CassegrainTelescope, we use CylinderGraphic to create the telescope housing shroud. As inputs to CassegrainTelescope, we use the f/number of the system, the diameter of the primary mirror, the diameter of the secondary mirror, and the desired position in space of the eyepiece focal point. We now define CassegrainTelescope.



inch = 25.4;


bigmirrorthickness = bigdiameter/10;
smallmirrorthickness = smalldiameter/10;
focallength1 = fnumber bigdiameter;
position = N[smalldiameter focallength1 / bigdiameter];
gamma = N[(focallength1 - position + eyepieceposition +
focallength2 = -N[position gamma/(gamma - 1)];
holediameter = N[.75 smalldiameter];
{Move[ParabolicMirrorWithHole[focallength1, bigdiameter,holediameter,bigmirrorthickness, options],-focallength1,180],
Move[ParabolicMirror[focallength2,smalldiameter, smallmirrorthickness,options], -position],
Move[CylinderGraphic[bigdiameter,-N[.1 bigdiameter+.1 smalldiameter+focallength1-position],options], {-position + .05 bigdiameter,0,0}],
Boundary[{-N[1.25(focallength1+eyepieceposition)], -bigdiameter/2,-bigdiameter/2}, {1.5eyepieceposition,bigdiameter/2,bigdiameter/2}]}];

Next, we use DrawSystem to trace a circle of rays through CassegrainTelescope.


Move[{Move[CircleOfRays[15 inch, NumberOfRays->6],15 inch,180],
    CassegrainTelescope[2.5,20 inch,4 inch,10 inch, OpenSide->{-.25 Pi,1.3 Pi}, GraphicDesign->Solid]},


Finally, we use PlotType->Surface with RayChoice->{IntersectionNumber->2} to examine the ray intersection points on the secondary mirror surface.


ShowSystem[%,PlotType->Surface, RayChoice->{IntersectionNumber->2}];


In Chapter 7, you can learn more about designing optical systems using LensLab and Mathematica.

Go to list of topics

Copyright Statement

LensLab is a trademark of Optica Software.
Mathematica ® is a registered trademark of Wolfram Research, Inc. All other product names mentioned are trademarks of their producers. Mathematica is not associated with Mathematica Policy Research, Inc. or MathTech, Inc.
Copyright ©1995-2005 by Optica Software, Urbana, Illinois, Champaign, Illinois.

All rights reserved. No part of this document may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, electronic, mechanical, photocopying, recording or otherwise, without prior written permission of the author, Optica Software.

Optica Software is the holder of the copyright to the LensLab package software and documentation ("Product") described in this document, including without limitation such aspects of the Product as its code, structure, sequence, organization, "look and feel", programming language and compilation of command names. Use of the Product, unless pursuant to the terms of a license granted by Optica Software. or as otherwise authorized by law, is an infringement of the copyright.

Optica Software makes no representations, express or implied, with respect to this Product, including without limitations, any implied warranties of merchantability or fitness for a particular purpose, all of which are expressly disclaimed. Users should be aware that included in the terms and conditions under which Optica Software is willing to license the Product is a provision that the author, Optica Software and distribution licensees, distributors and dealers shall in no event be liable for any indirect, incidental or consequential damages, and that liability for direct damages shall be limited to the amount of the purchase price paid for the Product.

In addition to the foregoing, users should recognize that all complex software systems and their documentation contain errors and omissions. Optica Software shall not be responsible under any circumstances for providing information on or corrections to errors and omissions discovered at any time in this document or the package software it describes, whether or not they are aware of the errors or omissions. Optica Software does not recommend the use of the software described in this document for applications in which errors or omissions could threaten life, injury or significant loss.

Go to list of topics

Created by Mathematica  (November 4, 2005)