4. Customization

4.1 How to Specify Different Aperture Shapes

4.1.1 The Circular and Elliptical Apertures
4.1.2 The Rectangular Aperture
4.1.3 The Polygonal Aperture
4.1.4 Aperture Stops

4.2 Light Sources

4.2.1 Maintaining Constant Pupil Illumination

Constant Illumination with BirthPoint
Constant Illumination with ReadRays

4.2.2 Distant Sources, Lasers, and Beam Divergence
4.2.3 Laser Cavities and MoveAligned
4.2.4 Chromatic Sources

4.3 Custom Surface Shapes

4.3.1 Working from a Derived Symbolic Surface Equation
4.3.2 ModelSurfaceShape
4.3.3 SurfaceRayIntersections
4.3.4 Building a "Black Hole"
4.3.5 SurfaceApproximation and SurfacePoints
4.3.6 SurfaceApproximation -> {TriangleNet -> surfacefunction}
4.3.7 SurfaceApproximation -> PlanarShape
4.3.8 SurfaceApproximation -> {SphericalShape -> radius}
4.3.9 Interpolated Surface Functions
4.3.10 SurfaceIntersectionFunction

4.4 Holograms and Diffraction Gratings

4.4.1 Variable Pitch Grating
4.4.2 Holographic Optical Element

4.5 Generic Building Blocks: The Language of Components

4.6 CustomDeflections

4.6.1 Getting a Ray to Change Color
4.6.2 Using Information from a Ray Parameter
4.6.3 AddTo, TakeFrom, and ReplaceFor
4.6.4 Redirecting RayTilt with CustomDeflections
4.6.5 Optimizing CustomDeflections for TurboTrace
4.6.6 AffectedSurfaces

RemoveDeflection
LabelSurfaces

4.1 How to Specify Different Aperture Shapes

The edges of most lenses, mirrors, and baffles can be specified  as either circular, rectangular, elliptical, or polygonal, depending on the  type and number of elements used in the aperture parameter of the component function.  Rayica uses the same shape-pattern convention with both outside  aperture edges and inside aperture holes.  In this section, we examine how  these various types of edge shapes are specified.

4.1.1 The Circular and Elliptical Apertures

You can specify a circular aperture by using a single number in  the aperture parameter of the component function.  Here we denote a circular  aperture with a a diameter of 50 for a cylindrical lens.

AnalyzeSystem[BiConvexCylindricalLens[100,50,15],
Boxed->False];

[Graphics:HTMLFiles/index_1.gif]

A circular aperture shape.

You can indicate an elliptical shape instead by denoting the  major and minor ellipse dimensions with two negative numbers in the aperture parameter.  Here is an example using an elliptical shape of {-100, 50} for the  outer dimensions of PinHole with a circular hole of 30.

In[57]:=

AnalyzeSystem[PinHole[{-100,-50},30],Boxed->False];

[Graphics:HTMLFiles/index_2.gif]

An elliptical aperture shape with a circular hole.

Similarly, you can list two negative numbers to designate an  elliptical hole.  Here we use an elliptical hole of {-50, -10} in ParabolicMirror with the Hole option.  We also use the HoleOffset option to position the hole away from the surface center by a distance of {15, 20} units.

In[60]:=

AnalyzeSystem[
    ParabolicMirror[1000,100,30, Hole->{-30,-10}, HoleOffset->{15,20}],
        Boxed->False];

[Graphics:HTMLFiles/index_3.gif]

An elliptical hole that is placed offset from the center.

You can also specify polygon-edged holes by lising the corner coordinates of the polygon.  This is shown in Section 4.4.

Go to list of topics

4.1.2 The Rectangular Aperture

You can specify a rectangular shape by indicating the major and  minor dimensions with a list of two positive rather than negative numbers.  Here we use a  rectangular aperture of {100, 50} with SphericalMirror.

AnalyzeSystem[SphericalMirror[500,{100,50},30], Boxed->False];

[Graphics:HTMLFiles/index_4.gif]

A rectangular aperture.

Go to list of topics

4.1.3 The Polygonal Aperture

You can specify a polygon-edged component by using a list of coordinates that denote the corners of the polygon.  Here we create a triangular-edged lens that have vertices at {{-25,-25},{0,25}, {25,0}}.

AnalyzeSystem[PlanoConvexLens[100,{{-25,-25},{0,25},{25,0}},15],Boxed->False];

[Graphics:HTMLFiles/index_5.gif]

A triangular aperture.

You can also use Table to create a polygon-edged component.  Here we create a hexagon-shaped mirror.

AnalyzeSystem[
    Mirror[
        Table[50*N[{Sin[x Degree],Cos[x Degree]}],{x,0,300,60}],
        10
    ]
];

[Graphics:HTMLFiles/index_6.gif]

A regular polygon aperture.

You will learn more about using polygonal-shapes in the next section.

Go to list of topics

4.1.4 Aperture Stops

We have now examined the most basic forms of apertures supported by Rayica, but more complicated apertures are also possible.  Generally speaking, custom  apertures require a piecewise-linear definition.  In this section, we examine  one such custom aperture.

The Rayica function ApertureStop defines a planar stop with an aperture in the middle.  It is very  straightforward to define custom apertures with this function.  Here is an  array of two-dimensional points.  ApertureStop will accept these directly to create a custom aperture.

RowBox[{RowBox[{holeaperture, =, RowBox[{{, , RowBox[{RowBox[{{, RowBox[{RowBox[{-, 26 ... 4248}], }}], ,, RowBox[{{, RowBox[{RowBox[{-, 29.6163}], ,, 13.7914}], }}]}], , }}]}], ;}]

ListPlot[holeaperture,Frame->True,Axes->False];

[Graphics:HTMLFiles/index_8.gif]

An array of two-dimensional points and their visualization with ListPlot.

These points may come from any number of sources.  For example, you could  read them in from a data file.  The points above define a piecewise-linear  approximation to a closed curve in the plane.  We now use them with ApertureStop and perform a test trace with GridOfRays as the source.

polygonAperture = ApertureStop[{100, 100}, holeaperture] ; output = AnalyzeSystem[{Gri ... ], Move[polygonAperture, 25], Boundary[{0, -50, -50}, {100, 50, 50}] }] ;

[Graphics:HTMLFiles/index_10.gif]

The custom ApertureStop at work.

We may examine the image of the light on the Boundary, which is ComponentNumber 2.  It matches the shape of the aperture.

ShowSystem[output, PlotTypeSurface, RayChoice {ComponentNumber2}] ;

[Graphics:HTMLFiles/index_12.gif]

The image at the boundary matches the shape of the custom aperture.

The example used the ApertureStop component to demonstrate a complex polygon hole opening, nevertheless,  the basic method works equally well for both the holes and edges of any  component aperture.

Go to list of topics

4.2 Light Sources

Some situations require light sources with special behaviors.   One such situation is when you want to move a light source but still maintain  full illumination of an entrance pupil.  Another is when the origin of some angular source lies far away from the rest of the system, but only light within a reasonable distance of the pupil is of interest.  Finally, some optical systems expect a chromatic source.  Rayica presents some options to handle these cases.

4.2.1 Maintaining Constant Pupil Illumination

Constant Illumination with BirthPoint

You often test an optical system by varying the vantage point of an input light source.  In general, however, only one vantage point will afford full illumination of the entrance pupil.  Typically this position is on the optic axis.  The BirthPoint option makes it possible to maintain illumination of the whole pupil regardless of vantage point.  This option reconfigures light source functions to interpret their input parameters in terms of entrance pupil requirements.  So, instead of distributing emission evenly across some geometry, a source will distribute rays evenly across the entrance pupil.  This behavioral change saves you from designing a unique light source for every new position just to hold constant pupil illumination.

The BirthPoint concept is easily visualized.  Following are two examples showing the effect of using BirthPoint.  The entrance pupil consists of a simple PlanoConvexLens.  The lens presents its planar side to the light sources.  This is the pupil.  The first example does not use BirthPoint.  Instead, the two sources are positioned with Move-related functions.

In[49]:=

RowBox[{RowBox[{AnalyzeSystem, [, RowBox[{{MoveDirected3D[PointOfRays[{10, 10}, RayLineRGB> ... 2754;, RowBox[{{, RowBox[{RowBox[{-, 1.708}], ,, RowBox[{-, 2.587}], ,, 1.356}], }}]}]}], ]}], ;}]

[Graphics:HTMLFiles/index_14.gif]

Uneven illumination caused by off-axis positioning of the corner source.  The sources are positioned with Move functions instead of BirthPoint.

The on-axis source spreads a nice grid of ray intercepts across the lens surface.  The pattern is symmetric with respect to the lens.  Yet the light from the corner source falls unevenly across the lens, and some of it misses altogether.  These stray light rays can be seen intercepting the outer Boundary object, where they terminate.

Now instead of Move functions, you employ BirthPoint.  When you position a source with BirthPoint, Move-related functions are unnecessary.

In[48]:=

RowBox[{RowBox[{AnalyzeSystem, [, RowBox[{{PointOfRays[{30, 30}, BirthPoint {-100, 100 ... 2754;, RowBox[{{, RowBox[{RowBox[{-, 1.708}], ,, RowBox[{-, 2.587}], ,, 1.356}], }}]}]}], ]}], ;}]

[Graphics:HTMLFiles/index_16.gif]

When you position a light source with BirthPoint, the distribution of light on planar entrance pupils is the same no matter where you put the source.

The light sources in each case have identical positions.  However, with BirthPoint, the corner source now distributes its light evenly across the lens.  You can see that the intercept points are the same as those for the on-axis source.

The caveat with BirthPoint is that it works well only for planar entrance pupils.  In both of the examples above, CurvatureDirection→ Back was used flip the PlanoConvexLens around.  The lens then presented its planar side to the light sources.  So you had a planar entrance pupil.

Go to list of topics

Constant Illumination with ReadRays

BirthPoint is designed for planar entrance pupils.  Curved pupils require a different approach.  Problems crop up when BirthPoint sources illuminate a curved surface, as the next example demonstrates.  This topic is somewhat advanced, and may be skipped without penalty.

BiConvexLens has equally curved surfaces on both sides.  Position two sources using BirthPoint: referenceSource on the optic axis, and offAxisSource at an off-axis point.

In[20]:=

offAxisSource = PointOfRays[{30, 30}, BirthPoint {-100, 100, 100}, RayLineRGBR ... 4;, RowBox[{{, RowBox[{RowBox[{-, 1.708}], ,, RowBox[{-, 2.587}], ,, 1.356}], }}]}]}], ]}]}], ;}]

[Graphics:HTMLFiles/index_18.gif]

Two standard sources illuminating a BiConvexLens from their respective BirthPoint positions.  The corner source fails to illuminate the lens evenly.

The illuminated surface is nonplanar, so offAxisSource fails to cover it evenly.  The lesson here is that the BirthPoint option is fairly easy to use, but from the standpoint of uniform illumination does not perform well in the presence of curvature.

A more advanced technique for maintaining illumination exploits actual ray trace data.  The idea is to perform an initial scout trace to glean surface intercept data, then employ that same data to define a light source.  This procedure is tantamount to selecting ray intercepts ahead of time and constraining a light source to produce them.

referenceSource has already produced a useful scout trace.  The strategy now will be to extract intercept data from the Ray objects created by this trace.  referenceSource illuminates the lens in a regular, full pattern.  You want to create an equivalent light source that reproduces the same pattern from the vantage of offAxisSource.  The main piece of information you need for this purpose is the location of the intercepts.  As is usual in such cases, ReadRays provides the needed information.

In[23]:=

rayendpts = ReadRays[sys, RayEnd, SourceID2, IntersectionNumber1]

Out[23]=

RowBox[{{, RowBox[{RowBox[{{, RowBox[{5.51259, ,, RowBox[{-, 15.8269}], ,, RowBox[{-, 15.8269} ... 914, ,, 0., ,, 15.3779}], }}], ,, RowBox[{{, RowBox[{5.51259, ,, 15.8269, ,, 15.8269}], }}]}], }}]

The variable rayendpts holds the return value from ReadRays.  This return value is a list of 3D coordinate triplets.  The arguments to ReadRays are sys, which holds the OpticalSystem that was previously traced; RayEnd, the ray data which you seek; SourceID, constraining your query to those rays emanating from a particular source; and IntersectionNumber, further stipulating only those rays that hit the first surface.  This final constraint is required because the system has many other rays traveling between other surfaces.  In this system there are two other surfaces, namely the other side of the lens and the Boundary itself.  You do not want RayEnd data from these other rays.

Now you have a starting and an ending point for all the rays you need.  The whole set of rays must start at a common point, the position of offAxisSource, and terminate at various locations given by rayendpts.  However, the Rayica function that will construct the new source requires a slightly different  form of input.  CustomRays cannot define a light source by means of starting and ending points.  Instead, CustomRays requires a starting point and a direction vector for each ray.  So first, construct a set of direction vectors.

This construction involves simple vector subtraction and normalization.  The Mathematica function Map applies a Function to each element in a list.  Function here defined is a so-called pure function, which, in our case, performs the vector subtraction.  The other Map/Function combination on the second line normalizes the direction vectors.

In[24]:=

raydirvecs = Map[Function[#1 - {-100, 100, 100}], rayendpts] ; raydirvecs = Map[Function[#1/Sqrt[Dot[#1, #1]]], raydirvecs]

Out[25]=

RowBox[{{, RowBox[{RowBox[{{, RowBox[{0.54152, ,, RowBox[{-, 0.594456}], ,, RowBox[{-, 0.59445 ... , ,, RowBox[{{, RowBox[{0.663311, ,, RowBox[{-, 0.529159}], ,, RowBox[{-, 0.529159}]}], }}]}], }}]

Armed with the starting point and direction vector for each ray, you can define a new light source.

In[26]:=

customSource = CustomRays[{ {RayTilt, raydirvecs}, {RayStart, Table[{-100, 100, 100},  ... s]}]}}, RayLineRGBRed, RayPointRGBRed, RayPointSize4, SourceID1] ;

CustomRays is the only Rayica light source function which allows free-form light source shaping.  Essentially, we are instructing CustomRays to accept the given parameter lists for the RayStart and RayTilt attributes of the Ray objects that it will generate.  We used Table inside CustomRays to generate a list of as many starting points as there are vectors in raydirvecs.  This construction conforms our input to the format expected by CustomRays.  Note that BirthPoint does not appear in our definition of customSource.  In fact, CustomRays does not accept the BirthPoint option.

Now let us analyze the modified system.

In[27]:=

RowBox[{RowBox[{newsys, =, RowBox[{AnalyzeSystem, [, RowBox[{{customSource, referenceSource, B ... 54;, RowBox[{{, RowBox[{RowBox[{-, 1.708}], ,, RowBox[{-, 2.587}], ,, 1.356}], }}]}]}], ]}]}], ;}]

[Graphics:HTMLFiles/index_25.gif]

Custom light source modifications enforce uniform illumination from two positions.

A new light source has been defined, customSource, to replace offAxisSource.  This new source intercepts the entrance pupil at the same points as the onAxisSource.  Thus both sources illuminate the pupil evenly.

This new source works for one fixed position, {-100,100,100}.  The question remains as to what happens if you move customSource.  At that stage, the direction vectors will no longer point in the right direction, so customSource will break it is moved.

Arbitrary positioning requires consolidation of all the previous steps into a Mathematica function.  This function, which is entitled FixedIlluminationSource, accepts the position as an input parameter.

In[28]:=

Clear[FixedIlluminationSource] ; (* good practice *)<br />FixedIlluminationSource[newbirthpoin ... Rays[{{RayTilt, raydirvecs}, {RayStart, Table[newbirthpoint, {Length[raydirvecs]}]}}, options]]] ;

A custom light source function providing fixed illumination of a particular entrance pupil from any position.

A simple test of FixedIlluminationSource will show that it performs as specified.

In[29]:=

RowBox[{RowBox[{multisys, =, RowBox[{AnalyzeSystem, [, RowBox[{{Table[FixedIlluminationSource[ ... 54;, RowBox[{{, RowBox[{RowBox[{-, 1.708}], ,, RowBox[{-, 2.587}], ,, 1.356}], }}]}]}], ]}]}], ;}]

[Graphics:HTMLFiles/index_28.gif]

A demonstration of the FixedIlluminationSource function at work.  Rays from this custom source always intercept the same points on the lens surface.

No matter where you position a FixedIlluminationSource, the rays always intercept the same points on the surface of the lens.  This function equips you to test with uniform illumination any system having  this entrance pupil.

In fact, the function is more general because it has defined endpts as an input parameter.  To apply this function to a different system, illuminate the system with the pattern you want to hold constant, then call ReadRays to find the surface intercepts.  You then use these intercepts as the endpts argument to FixedIlluminationSource.

Go to list of topics

4.2.2 Distant Sources, Lasers, and Beam Divergence

If you consider a source as being at infinity, then, as far as your system is concerned, its rays are all parallel.  Rayica presents a suite of parallel ray sources for such cases: GridOfRays, CircleOfRays, LineOfRays, and so on.

Sometimes the source is distant, but not at infinity.  In these situations you need a mechanism to express the divergence of the light as it enters your system, without including the entire path from its point of origin.  You care only about the local divergence near the entrance of your system.

Lasers present a similar dilemma.  Laser light exhibits beam divergence.  In the far field, laser light appears to emerge from a virtual point.  This virtual source point is the center of the "beam waist" in the lasing cavity.  However, you normally want to model laser light with a certain divergence only as it approaches your system.  The task of modeling the beam waist would require an intimate knowledge of the laser.  It would also introduce unwanted complexity into your project. In many instances, you can simply use the built-in GaussianBeam source to model such divergence issues for lasers. However, GaussianBeam may not be appropriate for all applications.

As an alternative to GaussianBeam you can instead model distant, diverging sources with the use of the BirthPoint and StartAtBirthPoint options in tandem for any of the built-in source functions.  When you define a BirthPoint source with StartAtBirthPoint→False, Rayica still positions the source at the BirthPoint.  However, Rayica handles the rays differently.  Rayica considers the light only in the vicinity of the origin.  More precisely, the rays do not start at the BirthPoint but from the principal coordinate plane orthogonal to the optic axis.  The modified ray trajectories are colinear with the old but originate from this plane.  (Therefore when you set StartAtBirthPoint→False, you should position the BirthPoint behind this plane.)  You may view this technique as a way to turn far-away point sources into near-field planar sources with characteristic divergence.  Once configured in this way, the diverging source can be moved and rotated arbitrarily like any other source.

Here is a standard light source shooting rays from its BirthPoint because StartAtBirthPoint→True has been set.  This divSource is a point source.  The eventual system aperture is represented, for the time being, by a simple Baffle placed 50 units away from the origin.  There is then a total path length of 100 units from the source to the baffle.

In[55]:=

divSource = WedgeOfRays[10, BirthPoint {-50, 0, 0}, StartAtBirthPointTrue, Num ... e, Move[Baffle[{20, 20}], 50]}, BoxedFalse, PlotTypeTopView, FrameTrue] ;

[Graphics:HTMLFiles/index_30.gif]

With the option setting StartAtBirthPoint→True, analysis and graphics encompass a large area that includes the source of the light.

The BirthPoint at {-50,0,0} is far from the intended system input plane.  The source indeed exhibits the divergence you need at the input plane, but incorporates a long optical path that you do not.  To remedy this situation, set StartAtBirthPoint → False.

In[57]:=

divSource = WedgeOfRays[10, BirthPoint {-50, 0, 0}, StartAtBirthPointFalse, Nu ... e, Move[Baffle[{20, 20}], 50]}, BoxedFalse, PlotTypeTopView, FrameTrue] ;

[Graphics:HTMLFiles/index_32.gif]

With the option setting StartAtBirthPoint→False, analysis and graphics need only include the vicinity of your system.

Rayica now recognizes that the system begins at the origin instead of {-50,0,0}.  For all intents and purposes, divSource now represents a broad, planar source with characteristic divergence.  In fact you can orient divSource in 3-space just like any other source.

Here is a diverging beam source spotlighting a cube from two positions.  Use a ConeOfRays with StartAtBirthPoint→False.

In[14]:=

beamSource = ConeOfRays[10, NumberOfRays15, BirthPoint {-30, 0, 30}, StartAtBi ... 754;, RowBox[{{, RowBox[{RowBox[{-, 1.479}], ,, RowBox[{-, 2.959}], ,, 0.713}], }}]}]}], ]}], ;}]

[Graphics:HTMLFiles/index_34.gif]

Divergent beams created by the option setting StartAtBirthPoint→False.  Rayica omits the light source birth points from its analysis and graphics.

Go to list of topics

4.2.3 Laser Cavities and MoveAligned

The MoveAligned function, introduced previously in Section 2.4, is well-suited for designing laser cavities.  The prism elements reroute the light path in unpredictable ways.  MoveAligned tracks these zig-zags.

The following example demonstrates laser cavity design; it also shows a way to employ units in Rayica.  The base unit in this example is the millimeter, so the variable mm is defined as 1.0.  Other units are then scaled relative to this value.  Note that all of the Move directives in the following effect only orientation changes.  The translation terms are zero.

Clear[mm, cm, m1, m2, m3, m4, r1, r2, r3, r4, theta1, theta2, p1, p2, p3, p4, system] ; RowBox ... ] ; BRF = Move[RhomboidPrism[Automatic, 1 cm, 2 cm, "BRF"], 0, TwistAngle180] ;

Definitions for a laser cavity design.

MoveAligned will place every component except the final Tweeter.  The spacings between components are defined as separate variables.  These might change several times during design iterations.

s1 = 30 cm ; s2 = 20 cm ; s3 = 4 cm ; s4 = 5 cm ; s5 = 50 cm ; s6 = 5cm ; s7 = 35 cm ; partial ... ed[Move[SingleRay[], 15 cm], {s1, BRF, s2 , OC, s3, Rhomb, s4, Diode, s5, UFM, s6, AC, s7 , LFM}]

Out[432]=

RowBox[{{, RowBox[{TagBox[RowBox[{Move, [, RowBox[{BRF, ,, RowBox[{{, RowBox[{450., ,, 0, ,, 0 ... [{{, RowBox[{429.728, ,, RowBox[{-, 67.4776}]}], }}], ,, RowBox[{-, 20.}]}], ]}], HoldForm]}], }}]

Rayica echoes the Move equivalents for this usage of MoveAligned.

Now propagate a SingleRay through the partialcavity to determine where the Tweeter belongs.  This component, a spherical mirror, will complete the cavity.

system = AnalyzeSystem[Flatten[{SingleRay[], partialcavity, Boundary[75 cm, 30 cm, GraphicDesignOff]}], PlotTypeTopView, FrameTrue] ;

[Graphics:HTMLFiles/index_39.gif]

Pilot trace for the laser cavity design.

Tweeter must be placed at the geometric crossover between the input to BRF and the output from LFM.  It will have to reflect toward both directions.  MoveReflected is the perfect directive for this purpose.

Unfortunately, Rayica has no built-in call to obtain the ideal placement.  You must guess it from screen coordinates.  The guess will be reasonable, however.  Mathematica features a keystroke mechanism that enables you to obtain plot coordinates from mouse movements.  In the present case, put Tweeter at {178.95,0}.  Using Append, a standard Mathematica function, construct the final system and trace it.

RowBox[{finalcavity, =, RowBox[{Append, [, , RowBox[{partialcavity, ,, , RowBo ... 67.4776}]}], }}], ,, RowBox[{{, RowBox[{178.95, ,, 0}], }}], ,, {450, 0}}], ]}]}], , ]}]}]

Out[434]=

RowBox[{{, RowBox[{TagBox[RowBox[{Move, [, RowBox[{BRF, ,, RowBox[{{, RowBox[{450., ,, 0, ,, 0 ...  [, RowBox[{Tweeter, ,, RowBox[{{, RowBox[{178.95, ,, 0}], }}], ,, 172.47}], ]}], HoldForm]}], }}]

In[472]:=

system = AnalyzeSystem[Flatten[{Move[SingleRay[], 250], Resonate[final ... 54;Off] }], PlotTypeTopView, QuickTraceFalse, FrameTrue] ;

[Graphics:HTMLFiles/index_43.gif]

The source SingleRay shifted past the Tweeter to trace the system.  In the pilot trace, the SingleRay emerged from {0,0}, but here it emerges from {250,0} and resonates in the laser cavity.  The Resonate function instructs Rayica that this is a resonating cavity.  System output emerges from the beam-splitter OC at right.

The lesson of this example is how MoveAligned simplified the design.  All that had to be specified was the spacing from one component to the next.  MoveAligned handled the task of keeping the components aligned with the light path inside the cavity.

Go to list of topics

4.2.4 Chromatic Sources

Often you need a light source with certain chromatic characteristics.  The most common type of chromatic source is white light.  White light contains all visible wavelengths equally.  Rayica can approximate white light with colinear rays of different wavelengths.  Multiple ray objects follow the same path, but each has a distinct wavelength.

RainbowOfRays creates a white light source.  You must supply two numbers, in microns, delimiting the bandwidth of the white light. The bandwidth divides evenly across the NumberOfRays specified.  Each ray has only one wavelength; the more rays, the better RainbowOfRays populates the bandwidth.

The following is a RainbowOfRays passing through a diffraction grating.  The rays start out colinear, but diffract differently according to their different wavelengths.  By default,  Rayica renders each ray with a screen color matching its wavelength.

rainbowSource = RainbowOfRays[{.4, .65}, NumberOfRays8] ; AnalyzeSystem[{rainb ... 800, {20, 20}, 1, GraphicDesignSolid], 20], Move[Screen[{85, 20}], {75, 35}] }] ;

[Graphics:HTMLFiles/index_45.gif]

RainbowOfRays diffracted by a Grating.  The color separation is even across the screen.

If you need to test specific wavelengths, use CustomRays instead.  The WaveLength specifier allows you to create a set of colinear rays with arbitrary wavelength settings.  You need not rely on the even-distribution principle of  RainbowOfRays, but you may instead define your own distribution.

whiteLightSource = CustomRays[{WaveLength, {.446, .476, .512, .530, .532, .590, .632, .650}}]  ... 800, {20, 20}, 1, GraphicDesignSolid], 20], Move[Screen[{85, 20}], {75, 35}] }] ;

[Graphics:HTMLFiles/index_47.gif]

CustomRays diffracted by a Grating.  The color separation is uneven because the wavelengths were manually defined.

CustomRays acts effectively like a single, multichromatic ray.  Sometimes you need more than one of these multichromatic rays.  Since light sources can be nested, the task is not hard.  Use CustomRays as the inner light source.

Define a CustomRays nested inside a PointOfRays. Set not only the wavelength of each ray, but also its Intensity.

pointWhiteLightSource = PointOfRays[CustomRays[{ {WaveLength, {.430, . ... Move[Grating[700, {20, 20}, 1, GraphicDesignSolid], 20], Boundary[75]}] ;

[Graphics:HTMLFiles/index_49.gif]

Nested light source with multichromatic rays having different energies at each wavelength.

Go to list of topics

4.3 Custom Surface Shapes

With Rayica you can blend traditional mathematical functions with your optics to create new forms of optical component shapes. In order to accomplish this you can either define a new surface model using ModelSurfaceShape or you can directly specify the new surface function in a custom component. In particular, with every category of optical component in Rayica, there exists a "Custom" version of it. These are listed in the following table.

CustomBaffle CustomBeamSplitter CustomBirefringentLensSurface
CustomBranchingSurface CustomConjugateBeamSplitter CustomConjugateMirror
CustomDiffuser CustomDiffuserMirror CustomFiber
CustomFiberMirror CustomGrating CustomGratingMirror
CustomIntrinsicSurface CustomJonesMatrixOptic CustomLens
CustomLensSurface CustomLinearPolarizer CustomMirror
CustomPrism CustomRetardationPlate CustomScreen

Custom component function names.

With the exceptions of CustomPrism, CustomFiber, and CustomFiberMirror, the surface shapes for all of these component functions work essentially in the same fashion. Therefore, in the following examples, CustomLens and CustomMirror will be used to illustrate the different methods employed. (Demonstrations of CustomPrism, CustomFiber and CustomFiberMirror are presented in the Library of Examples portion of Rayica's User's Guide.)

CustomLens[f1, f2, aperture, thickness, label, options] designates a double-surfaced refractive component having user-defined surface functions. The surfaces are given by f1 and f2, where f1 and f2 are user-defined surface functions, and an optional user-named objectlabel.
CustomMirror[
surfacefunction, aperture, label, options] and CustomMirror[surfacefunction, aperture, thickness, label, options] designate a mirror that has a user-defined surfacefunction, and an optional user-named label.

In order to specify a new surface shape in Rayica, you must provide a surfacefunction in one of the three following formats:

(1) Function[{s,t}, fx[s,t]]
(2) Function[{s,t}, {
fx[s,t], fy[s,t], fz[s,t]}]
(3) {Function[{s,t},
fx[s,t]], Function[{s,t}, fy[s,t]], Function[{s,t}, fz[s,t]]}

Three different surface function formats that describe a surface shape.  {fx, fy, fz} are user-specified functions that depend on parameters {s,t}.

As indicated, the surfacefunction must always have a Function head. If the surface shape is very simple, then only the single-valued, non-parametric function format, shown in (1), can be given. The vector-parametric format, shown in (2), is the most general way to specify a geometric three-dimensional shape since it permits enclosed surfaces with multiple "folds". This format is a three-dimensional vector function of two parametric coordinates. Internally, Rayica always uses this vector parametric format. In fact, regardless of how the function was originally inputted by the user, it is always converted internally into this form. The last format, listed in (3), is an obsolete format that was previously used by Rayica version 1, although this format is still recognized.

Very often, the two parametric coordinates correspond with y and z coordinate axes of three-dimensional Cartesian space. In such cases, the surfacefunction output has a single-value that specifies the x-axis component. For the such systems, the position of the unperturbed optical surface has a local coordinate system that matches the global coordinate system of the optical space. In the conic lens example given below, the two parametric variables correspond with the y and z axes of the optical coordinate system and the function result corresponds with the x-axis (optical axis). Once a component surface has been spatially reoriented, however, the surface function's local reference coordinates become different from the global coordinate space.

In[43]:=

AnalyzeSystem[
    {
    Move[GridOfRays[40, NumberOfRays -> 6], {-5,0,5}],
    CustomLens[ Function[{y,z}, Sqrt[y^2 + z^2]], Function[0], 50, 25, GraphicDesign -> Solid],
    Boundary[45,GraphicDesign->Off]
    },
    Axes -> True,
    AxesLabel -> {"x-axis","y-axis","z-axis"}
];

[Graphics:HTMLFiles/index_50.gif]

Single-valued surface function format of a conic-shaped lens.

Based on the previous, surfacefunction format notation, this conic lens example specifies Sqrt[y^2 + z^2]for fx and {y,z} for {s,t}. In general, there is nothing sacred about the naming of parametric variables in your surface function. As shown in the table below, you can also use {#1, #2} instead of alphabetic symbols.

(1) Function[fx[#1,#2]]
(2) Function[{
fx[#1,#2], fy[#1,#2], fz[#1,#2]}]

The two principle surface function formats shown in slot variable notation.

These "slot" variables have an advantage in that they do not require any declaration before they can be used in the Function statement. For this reason, slot variables are sometimes called "pure" variables and are a kind of short-hand notation for working with Function statements. They are known as slot variables because, internally, they are represented by the Slot head:

In[20]:=

FullForm[#1]

Out[20]//FullForm=

Slot[1]

For some surface models, the two parametric variables do not have any correspondence with the three-dimensional coordinates of ordinary Cartesian space. In such cases, the surface function must provide a vector quantity that designates all three components of space. This is demonstrated in the spherical mirror example given below. Here, the two parametric variables have angular dimensions instead of rectangular spatial dimensions. In this case, the aperture parameter of CustomMirror has units of radians.

In[22]:=

AnalyzeSystem[
    {Move[GridOfRays[1.5,NumberOfRays->5],2,180],
    CustomMirror[ Function[{Sin[#1], Cos[#1]*Sin[#2], Cos[#1]*Cos[#2]}], {2.001*Pi,1.001*Pi}],
    Boundary[2]},
    PlotPoints -> 60,
    Axes -> True,
    AxesLabel -> {"x-axis","y-axis","z-axis"}
];

[Graphics:HTMLFiles/index_52.gif]

Vector parametric function format of an enclosed spherical mirror.

Although such vector surface functions do not have parametric correspondence with the Cartesian space coordinates, the resulting ray trace is still reported in rectangular coordinates.

Go to list of topics

4.3.1 Working from a Derived Symbolic Surface Equation

Sometimes, you may want to use a derived symbolic equation in Rayica. As an example, we can derive the equation for an ellipse and then create a mirror with it. First we need to determine the general mathematical form for the ellipsoid surface. We can start with the three-dimensional ellipse equation (x/a)^2 + (y/b)^2 + (z/c)^2 = r^2 and solve for z.

In[23]:=

solution = Solve[(x/a)^2 + (y/b)^2 + (z/c)^2 == r^2,z]

Out[23]=

{{z -c (r^2 - x^2/a^2 - y^2/b^2)^(1/2)}, {zc (r^2 - x^2/a^2 - y^2/b^2)^(1/2)}}

Notice that there are two solutions: one positive valued and the other negative valued. Such solutions from Solve are always reported as sets of replacement rules in the form of z -> solution. As such, we need to extract the positive solution and discard the other solution. In addition, we will use ReplaceFor (/.) in order to extract the desired solution. The final result is finally stored in the ellipse3D variable.

In[24]:=

ellipse3D[a_,b_,c_,r_,x_,y_] = z/.solution[[2]]

Out[24]=

c (r^2 - x^2/a^2 - y^2/b^2)^(1/2)

Before using the equation to define an optical surface, we need to test appropriate numeric values for the constants a, b, c, and r. We can use Plot3D to visually verify our chosen parameter settings.

In[34]:=

Plot3D[ellipse3D[10,10,10,20,s,t], {s,-100,100},{t,-100,100}];

[Graphics:HTMLFiles/index_55.gif]

In order to create an elliptical mirror, we can directly pass this elliptical equation to CustomMirror. Before doing this, however, the elliptical equation needs to be expressed as one of the three Function formats described in the previous section. In this case, the elliptical function is single-valued and dependent on two rectangular parameters {s, t}. As such, we can use the single-valued Function format of (1). However, before we can implement this format, we first need to learn more about the use of the Function structure. In particular, Function has the HoldAll attribute. You can check this with the Attributes command.

In[47]:=

Attributes[Function]

Out[47]=

{HoldAll, Protected}

As a result of this HoldAll attribute, all of the arguments for Function are maintained in an unevaluated form. Therefore, if we directly pass ellipse3D to Function, it does not get converted into the underlaying equation, but instead remains in an unevaluated state:

In[49]:=

Function[{s,t},ellipse3D[10,10,10,20,s,t]]

Out[49]=

Function[{s, t}, ellipse3D[10, 10, 10, 20, s, t]]

This becomes a problem for Rayica because it expects to receive the actual surface equation and not an unevaluated intermediate expression. Therefore, we must use the Evaluate command to evaluate ellipse3D variable and directly express the surface equation within the Function:

In[25]:=

Function[{s,t}, Evaluate[ellipse3D[10,10,10,20,s,t]] ]

Out[25]=

Function[{s, t}, 10 (400 - s^2/100 - t^2/100)^(1/2)]

With this, we can now define a CustomMirror component.

In[79]:=

custommirror =
    CustomMirror[
        Function[{s,t}, Evaluate[ellipse3D[10,10,10,20,s,t]] ],
        {100,100}
    ]

Out[79]=

CustomMirror[Function[{s, t}, 10 (400 - s^2/100 - t^2/100)^(1/2)], {100, 100}]

We can now immediately use this new optic for ray tracing.

In[66]:=

AnalyzeSystem[{
    Move[WedgeOfRays[20],30,5],
    custommirror,
    Move[Boundary[300,150],-50]}
];

[Graphics:HTMLFiles/index_60.gif]

Go to list of topics

4.3.2 ModelSurfaceShape

ModelSurfaceShape can be used to define new forms of optical surfaces. This is an alternative to directly passing the surface function to custom components such as CustomMirror. In particular, ModelSurfaceShape allows you to set-up a generalized surface function with symbolic entries. As you will see shortly, you can even set up your own generic component function with your own built-in surface shapes.

ModelSurfaceShape[surfacelabel, value] = surfacefunction is a global function that defines a surfacefunction with the surfacelabel as a reference tag.

Using the same ellipse equation result from the previous section, we will next create a new ModelSurfaceShape definition using ellipticalshape for the surfacelabel parameter of ModelSurfaceShape. Here we use lower case letters to distinguish ellipticalshape from a potential built-in EllipticalShape label.  Before the user can define new ModelSurfaceShape functions, the ModelSurfaceShape must first be unprotected.

In[32]:=

Unprotect[ModelSurfaceShape];

In[36]:=

ModelSurfaceShape[ellipticalshape,{a_,b_,c_,r_}] = Function[Evaluate[ellipse3D[a,b,c,r,#1,#2]]]

Out[36]=

c (r^2 - #1^2/a^2 - #2^2/b^2)^(1/2) &

In[37]:=

Protect[ModelSurfaceShape];

Now we can create a new ellipticalMirror function that uses this new surface model. For this, we use CustomMirror together with SurfaceLabel->ellipticalshape.

In[37]:=

ellipticalMirror[{a_,b_,c_,r_}, aperture_, options___] :=
    CustomMirror[{a, b, c, r}, aperture, SurfaceLabel->ellipticalshape, options]

Here is a ray trace that uses our new ellipticalMirror function.

In[35]:=

AnalyzeSystem[{
    Move[WedgeOfRays[20],30,5],
    ellipticalMirror[{10.,10.,10.,20.},{100,100}],
    Move[Boundary[300,150],-50]},
    PlotType->TopView
];

[Graphics:HTMLFiles/index_62.gif]

Go to list of topics

4.3.3 SurfaceRayIntersections

Rayica offers many alternative methods for calculating the ray-surface intersection point. You can use the SurfaceRayIntersections option to choose a particular method to work with custom surfaces. Note, however, that all of Rayica's built-in surface shapes (PlanarShape, SphericalShape, and ParabolicShape) already have built-in solutions that cannot be altered by the SurfaceRayIntersections setting.

SurfaceRayIntersections -> method is an option of all component functions that specifies the method for finding the surface-ray intersection point.

Currently, there are seven different method settings available for SurfaceRayIntersections: Symbol, Numeric, FindRoot, FindMinimum, Solve, NSolve, and Automatic. In general, the optimal choice of method depends on the particular ray-trace circumstances. While all of these settings work transparently within Rayica, only Symbol and Numeric can be compiled together with the compiled ray-trace code generated by TurboTrace. Therefore, these two settings are the fastest with TurboTrace. Nevertheless, the other settings can still be used with TurboTrace although they will run less efficiently. Symbol constructs an analytic, closed-form solution for the intersection point and exhibits the fastest ray-trace response. Symbol is also used by all of Rayica's built-in surface shapes. Unfortunately, Symbol does not work with surface functions that lack a closed-form inverse solution and the build-time of Symbol is much longer than the other methods.

SurfaceRayIntersections -> Numeric constructs a numerical solution that can be directly compiled by Rayica. The Numeric setting invokes numerical algorithms that are specifically built for use with Rayica, whereas the FindRoot and FindMinimum settings use the standard built-in FindRoot and FindMinimum functions of Mathematica. This is why the Numeric setting can be directly compiled into the Rayica source code whereas FindRoot and FindMinimum must be internally maintained as external functions to Rayica-compiled code. Although Numeric is designed to have the same functional capabilities of both FindRoot and FindMinimum, the internal algorithms are not shared so these two settings may outperform Numeric in some instances.

In cases involving interpolated surfacefunctions or when Symbol and Numeric fail to perform, one of the remaining methods can work more effectively. SurfaceRayIntersections -> FindRoot uses FindRoot internally and is usually the most robust numerical ray-trace method. For this reason, SurfaceRayIntersections -> Automatic generally invokes the FindRoot setting internally.  SurfaceRayIntersections -> FindMinimum uses FindMinimum internally and, in special cases, can work better than FindRoot (although not in most cases). SurfaceRayIntersections -> Solve uses Solve internally to dynamically find the analytic ray-surface solution. SurfaceRayIntersections -> NSolve uses NSolve internally and works exclusively with polynomial surface equations. For qualified surface functions, Solve and NSolve will always find the globally correct intersection point. For further information, see the Mathematica book on FindRoot, FindMinimum, Solve, and NSolve.

Next, we will examine the effects of five different SurfaceRayIntersections settings on the component creation and ray-trace process, namely: Automatic/FindRoot, Symbol, Numeric, and Solve. In particular, we shall see how each of these settings affect both the component creation time as well as the ray-trace calculation time in different ways. For this, we will use a custom mirror that is shaped to form an elliptical tube. To build this customized shape into a mirror, we again use the CustomMirror function. To give this new component a tube shape, we pass the parametric vector function {10*Cos[s], 5*Sin[s], t} (for {fx, fy, fz}) to the surfacefunction in CustomMirror. By default, CustomMirror uses the SurfaceRayIntersections -> Automatic setting:

In[3]:=

SurfaceRayIntersections/.Options[CustomMirror]

Out[3]=

Automatic

We will first create the CustomMirror with this default setting. However, we will store its component creation time in a variable for future comparison with the other methods.

In[4]:=

creationtime[1] =
Timing[
    ellipticalmirror[1] = CustomMirror[ Function[{s,t}, {10*Cos[s], 5*Sin[s], t}], {2*Pi,10}]
][[1]]

Out[4]=

RowBox[{3.05,  , Second}]

Here is a plot of the mirror's three-dimensional shape.

In[5]:=

ShowSystem[ellipticalmirror[1], PlotPoints->60];

[Graphics:HTMLFiles/index_65.gif]

By tracing a SingleRay within the ellipticalmirror that has the Resonate feature added, we can observe its resulting the caustic caused by repeated bounces around the mirror's interior. We will store the total ray-trace time in a variable for future comparison with other settings.

In[6]:=

tracedtiming[1] =
Timing[sys = PropagateSystem[{
    Move[SingleRay[],{0,2}],
    Resonate[ellipticalmirror[1]]}];][[1]]
ShowSystem[sys,PlotType->TopView, PlotPoints->60];

Out[6]=

RowBox[{23.7,  , Second}]

[Graphics:HTMLFiles/index_67.gif]

SurfaceRayIntersections -> Automatic is in fact not a unique method of its own, but, rather, enables Rayica to internally select one of the six other methods. The advantage of the Automatic setting is that it gives Rayica the opportunity to choose different settings according to the character of the specified surface function. This frees the user from having to manipulate the ray-trace operation details at this low level. However, this has a disadvantage in that the user does not have control over Rayica's final selection. This becomes a problem if Rayica happens to make a poor internal choice. In practise, SurfaceRayIntersections -> Automatic usually invokes the FindRoot setting since it is the most robust and handles a widest range of custom surface functions. Unfortunately, the FindRoot setting can be sigificantly slower at tracing than some other selections such as Symbol. However, Symbol requires a closed-form symbolic solution to exist and, as such, can only be used with a limited range of surface functions. In addition, when a closed-form solution does exist, the component creation time is much longer with Symbol. Here is the same mirror system with SurfaceRayIntersections -> Symbol instead of SurfaceRayIntersections -> Automatic.

In[8]:=

Off[Solve::ifun];

In[22]:=

creationtime[2] =
Timing[
    ellipticalmirror[2] =
    CustomMirror[ Function[{s,t}, {10*Cos[s], 5*Sin[s], t}], {2*Pi,10}, SurfaceRayIntersections->Symbol]
][[1]]

Please be advised that one or more singularities have been isolated in the SurfaceIntersectionFunction .

No further action is necessary.

Out[22]=

RowBox[{20.6333,  , Second}]

In[15]:=

creationtime[2]/creationtime[1]

Out[15]=

6.89617

In this case, the mirror creation time was nearly seven time longer than before. For some custom surface functions, the component creation time for Symbol can consume unreasonable amounts of time (and memory). For this reason, the Symbol setting must be used with care. In cases where it can successfully be employed, however, the Symbol setting often works the best, both in terms of time and accuracy.

In[13]:=

tracedtiming[2] =
Timing[sys = PropagateSystem[{
    Move[SingleRay[],{0,2}],
    Resonate[ellipticalmirror[2]]}];][[1]]
ShowSystem[sys,PlotType->TopView, PlotPoints->60];

Out[13]=

RowBox[{14.9333,  , Second}]

[Graphics:HTMLFiles/index_73.gif]

In[16]:=

tracedtiming[2]/tracedtiming[1]

Out[16]=

0.630098

In this setting, the ray-trace time was sixty percent of the Automatic setting. Next, we will use the SurfaceRayIntersections -> Numeric setting on the same system.

In[25]:=

creationtime[3] =
Timing[
    ellipticalmirror[3] =
    CustomMirror[ Function[{s,t}, {10*Cos[s], 5*Sin[s], t}], {2*Pi,10}, SurfaceRayIntersections->Numeric]
][[1]]

Out[25]=

RowBox[{3.55,  , Second}]

In[21]:=

creationtime[3]/creationtime[1]

Out[21]=

1.13115

Here we see that the Numeric setting took slightly longer than the Automatic (FindRoot) setting for the component creation.

In[18]:=

tracedtiming[3] =
Timing[sys = PropagateSystem[{
    Move[SingleRay[],{0,2}],
    Resonate[ellipticalmirror[3]]}];][[1]]
ShowSystem[sys,PlotType->TopView, PlotPoints->60];

Out[18]=

RowBox[{22.3,  , Second}]

[Graphics:HTMLFiles/index_78.gif]

In[20]:=

tracedtiming[3]/tracedtiming[1]

Out[20]=

0.940928

The resulting ray-trace is slightly faster than the Automatic setting. Finally, we will test the Solve setting.

In[24]:=

creationtime[4] =
Timing[
    ellipticalmirror[4] =
    CustomMirror[ Function[{s,t}, {10*Cos[s], 5*Sin[s], t}], {2*Pi,10}, SurfaceRayIntersections -> Solve]
][[1]]

Out[24]=

RowBox[{1.33333,  , Second}]

In[26]:=

creationtime[4]/creationtime[1]

Out[26]=

0.437158

For this SurfaceRayIntersections -> Solve setting, the component creation time was the fastest of all previous tests. We will now test the ray-trace time for Solve.

In[27]:=

tracedtiming[4] =
Timing[sys = PropagateSystem[{
    Move[SingleRay[],{0,2}],
    Resonate[ellipticalmirror[4]]}];][[1]]
ShowSystem[sys,PlotType->TopView, PlotPoints->60];

Out[27]=

RowBox[{31.8333,  , Second}]

[Graphics:HTMLFiles/index_83.gif]

In[29]:=

tracedtiming[4]/tracedtiming[1]

Out[29]=

1.34318

The trace time for this setting, however, was the slowest of all previous settings. In general, both Solve and Symbol find the closed-form solution to a ray-surface intersection point. Symbol operates by finding the generalized solution only once during the component creation and then storing this solution inside the component prior to the ray-trace run time. Solve, on the other hand, works by symbolically solving the ray-surface equations at each new intersection point during the actual ray-trace process. Because of this, the Solve ray-trace operates much more slowly than Symbol, but Solve can handle far more complex surface functions than Symbol since the run-time symbolic equations presented to Solve can be simplified to a much greater extent than the equations presented to Symbol. In fact, SurfaceRayIntersections -> Solve is one of the most robust settings of all. Unfortunately, it is also the slowest.

Go to