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.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.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.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

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];

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];

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];

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.

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];

A rectangular aperture.

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];

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

]

];

A regular polygon aperture.

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

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.

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

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.

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.

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.

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]:=

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]:=

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.

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]:=

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]:=

Out[23]=

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]:=

Out[25]=

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

In[26]:=

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]:=

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]:=

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]:=

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.

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]:=

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]:=

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]:=

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

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.

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.

Out[432]=

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.

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.

Out[434]=

In[472]:=

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.

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.

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.

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.

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

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"}

];

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=

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"}

];

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.

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]=

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]=

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}];

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]=

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]=

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]=

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]=

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]}

];

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]=

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

];

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]=

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]=

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

In[5]:=

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

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]=

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]]

Out[22]=

In[15]:=

creationtime[2]/creationtime[1]

Out[15]=

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]=

In[16]:=

tracedtiming[2]/tracedtiming[1]

Out[16]=

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]=

In[21]:=

creationtime[3]/creationtime[1]

Out[21]=

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]=

In[20]:=

tracedtiming[3]/tracedtiming[1]

Out[20]=

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]=

In[26]:=

creationtime[4]/creationtime[1]

Out[26]=

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]=

In[29]:=

tracedtiming[4]/tracedtiming[1]

Out[29]=

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.

4.3.4 Building a "Black Hole"

In this next example, we will create 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.

In[7]:=

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

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

Plot[100./x^2,{x,-5,5},PlotRange->{0,100}];

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

In[13]:=

Plot3D[S[x,y], {x,-2,2}, {y,-2,2}, PlotRange->{0,1000}, PlotPoints->50];

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.

In addition to Resonate, we use CustomMirror 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.

In[44]:=

Clear[BlackHole];

BlackHole[aperture_,holeaperture_,opts___] :=

Block[{options},

options = Flatten[{opts,Options[CustomMirror]}];

Resonate[

CustomMirror[

Function[100./(#1^2 + #2^2)],

aperture,

FunctionCenter -> 0.,

Hole -> holeaperture,

options],

options

]

];

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.

In[19]:=

ShowSystem[BlackHole[5,2.5,GraphicDesign->Wire]];

Next we view the mouth of BlackHole.

In[20]:=

ShowSystem[%,PlotType->FrontView];

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

In[22]:=

sys =

AnalyzeSystem[{

Move[SingleRay[],{15,0},30],

BlackHole[5,2.5,GraphicDesign->Sketch],

Boundary[100, GraphicDesign->False]},

PlotType->TopView];

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 Rayica animations.

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

In[24]:=

Length[RaySelect[sys]]

Out[24]=

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

In[25]:=

Do[ShowSystem[

sys,

RayChoice->{IntersectionNumber->i},

PlotType->TopView,

PlotRange->{{-1,65},{-3,14}}],{i,1,63}];

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.

In[26]:=

sys =

AnalyzeSystem[

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

BlackHole[5,2,GraphicDesign->Sketch],

Boundary[{0,-100,-100},{120,100,100}, GraphicDesign->Off]},

PlotType->TopView];

This time all of the rays made it through the tube without turning around. Unfortunately, the ray trace has generated error messages. Here is a case where the default settings in Rayica has failed. This is a result of the default SurfaceRayIntersections -> Automatic option setting in CustomMirror. It is therefore necessary to choose a different setting. The problem is finally resolved with the SurfaceRayIntersections -> Solve setting.

In[46]:=

sys =

AnalyzeSystem[

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

BlackHole[5,2,GraphicDesign->Sketch, SurfaceRayIntersections -> Solve],

Boundary[{0,-100,-100},{120,100,100}, GraphicDesign->Off]},

PlotType->TopView];

4.3.5 SurfaceApproximation and SurfacePoints

In this section, we will examine the SurfaceApproximation and SurfacePoints options. These two options can greatly influence the behavior of iterative ray-surface intercept calculations in custom surfaces.

SurfaceApproximation -> method specifies the method used to determine an approximate starting point for numerical ray-surface intersection calculations.

SurfacePoints -> number specifies the number of points of a surface net used by SurfaceApproximation -> TriangleNet.

SurfaceApproximation becomes active for iterative settings of SurfaceRayIntersection. These include: SurfaceRayIntersection -> Automatic, Numeric, FindRoot, and FindMinimum. Note that the non-iterative settings of SurfaceRayIntersection such as Symbol, Solve, and NSolve do not depend on SurfaceApproximation at all! For many iterative techniques, having a reasonable initial estimate makes the difference between having a good trace or not. In particular, settings which involve iterative numerical methods for solving systems of equations first require an initial starting point to begin its iterative solution search. In such methods, it is critical that the initial starting point is sufficiently close to the global optimal solution in order for the correct solution to be determined. Otherwise the correct solution may be overlooked in favor of a local minimum that happens to lie closer to the initial starting point and corrupts the ray-surface calculation. When Rayica uses an iterative numeric procedure to calculate the ray-surface intersection point, it first uses a secondary procedure to get a rough estimate of the solution. It is the SurfaceApproximation option that specifies this secondary procedure. There are presently ten different methods available for SurfaceApproximation, shown below.

(1) SurfaceApproximation -> TriangleNet (or Automatic) constructs a triangular fishnet of starting points based on the original optical surfacefunction. For this, the SurfacePoints option specifies the number of fishnet elements.

(2) SurfaceApproximation -> {TriangleNet -> surfacefunction} denotes an alternative surface function to generate the fish-net of numerical starting points.

(3) SurfaceApproximation -> {Symbol -> surfacefunction} indicates an alternative surface function which has an analytic ray-surface intersection solution (determined by Rayica).

(4) SurfaceApproximation -> PlanarShape uses the intersection point with a plane for a starting condition.

(5) SurfaceApproximation -> {SphericalShape -> radius} uses the intersection point with a hemi-spherical surface that has a given radius.

(6) SurfaceApproximation -> {CylindricalShape -> radius} uses the intersection point with a half-cylinder that has a given radius.

(7) SurfaceApproximation -> {ParabolicShape -> focus} uses the intersection point with a paraboloid that has a given focus.

(8) SurfaceApproximation -> {SurfaceIntersectionFunction-> intersectionfunction} directly specifies an alternative ray-surface intersection solution. In this case, the intersectionfunction need not be analytic.

For SurfaceRayIntersection -> FindRoot/FindMinimum only, there are two additional settings available:

(9) SurfaceApproximation -> {Solve -> surfacefunction} dynamically uses the Solve function to determine an initial condition.

(10) SurfaceApproximation -> {NSolve -> surfacefunction} uses the NSolve function to dynamically determine an approximate intersection point for surfacefunction.

The ten different settings of SurfaceApproximation that find the approximate ray-surface intersection point.

Together, these ten different methods offer a nearly endless combination of settings. The optimal choice of setting is very much dependent on the particular surface function shape. However, since the SurfaceApproximation -> TriangleNet setting behaves robustly for nearly all surface shapes, it is usually invoked for the default SurfaceApproximation -> Automatic setting. As such, we will examine this setting in more detail. When this TriangleNet method is invoked, during the initial component creation process (when you first evaluate the component function), the optical surface function is first sampled across a grid of points that lie within the specified surface aperture. The values of these grid points are then stored in the component surface information for later use during the ray-trace. During the ray trace, the precalculated grid points are assembled into a model of triangular planar facets that span the optical surface. These facets are used to obtain the approximate location of the ray intercept point on the actual optical surface by finding the closest facet to the passing ray. With the default Interpolation -> True setting, the intercept estimate is further improved by interpolating the intercept value between the facet's three grid-point locations.

The SurfacePoints option is used in conjunction with SurfaceApproximation -> TriangleNet. Here, the SurfacePoints setting can specify either a single integer or a pair of integer numbers. When two numbers are given, SurfacePoints -> {n, m}, each value separately specifies the number of points along each of the two parametric coordinate directions. Increasing the number helps to deal with steeply curved custom surfaces. In order to visualize the resulting SurfaceApproximation behavior, you can use option to SurfacePoints -> True|Fill|Mesh with ShowSystem and AnalyzeSystem to render the actual TriangleNet facets (Note that SurfacePoints does not work with settings other than TriangleNet). Here is an example that shows two views of a circular conic mirror. The bottom image shows a projection of the triangular surface facets present.

In[37]:=

ShowSystem[

Mirror[Function[Sqrt[#1^2 + #2^2]],50],

SurfacePoints -> Mesh

];

ShowSystem[

Mirror[Function[Sqrt[#1^2 + #2^2]],50, GraphicDesign -> False],

SurfacePoints -> Mesh,

PlotType->FrontView

];

Triangular facets given by TriangleNet for a circular component aperture.

For circular surface boundaries, the outer star-like facet protrusions are used to insure complete facet coverage near the surface edges. You can switch off these extra facets with the CircumscribeBoundary -> False option. In most cases, however, there is no reason to do this.

In[10]:=

ShowSystem[

Mirror[Function[Sqrt[#1^2 + #2^2]],50, CircumscribeBoundary -> False],

SurfacePoints -> Mesh

];

The facet arrangement is different for rectangular boundaries. Here are the facets of a square conic mirror.

In[31]:=

ShowSystem[

Mirror[Function[Sqrt[#1^2 + #2^2]],{50,50}],

SurfacePoints -> Mesh,

PlotPoints -> 50

];

ShowSystem[

Mirror[Function[Sqrt[#1^2 + #2^2]],{50,50}, GraphicDesign -> False],

SurfacePoints -> Mesh,

PlotType->FrontView

];

Triangular facets given by TriangleNet for a rectangular component aperture.

Sometimes, you can obtain a more accurate estimate of the ray-surface intercept if you increase the SurfacePoints value beyond its default setting, given by:

In[3]:=

SurfacePoints/.Options[CustomMirror]

Out[3]=

For example If we increase the SurfacePoints setting from 7 to 11, we obtain the following result.

In[4]:=

ShowSystem[

Mirror[Function[Sqrt[#1^2 + #2^2]],{50,50}, GraphicDesign -> False, SurfacePoints -> 11],

SurfacePoints -> Mesh,

PlotType->FrontView

];

You can also specify two numbers in the SurfacePoints setting in order to control the two sample dimensions independently. Here is the circular conic mirror again with a SurfacePoints -> {7, 11} setting. Note that the number of radial spokes has increased while the number of concentric lines has remained unchanged.

In[8]:=

ShowSystem[

Mirror[Function[Sqrt[#1^2 + #2^2]],50, GraphicDesign -> False, SurfacePoints -> {7, 11}],

SurfacePoints -> Mesh,

PlotType->FrontView

];

Here is a more complex custom mirror example with small-scale surface features.

In[5]:=

axiconMirrorWithRipples[outDiameter_, inDiameter_, bodyLength_, amplitude_, numberOfPeaks_, opts___] :=

(* for holes in materials, here is how to interpret the parameters :*)

(* outDiameter : the "entrance" hole diameter *)

(* inDiameter : the "exit" hole diameter *)

(* bodyLength : the "depth" of the hole *)

(* Preconditions : outDiameter > inDiameter *)

Module[{longLength = (bodyLength * outDiameter) / (outDiameter - inDiameter)},

(* local variable longLength represents the total length that the cone *)

(*would be if it was not truncated at its pointy end*)

Move[

Resonate[

(* List of components *)

Hole[

CustomMirror[

(* Surface function *)

Function[{s, t}, Evaluate[2 longLength/ outDiameter * Sqrt[s^2 + t^2] + amplitude *

Sin[ numberOfPeaks * π * (Sqrt[s^2 + t^2] - inDiameter/2)/

(1/2 (outDiameter - inDiameter))]]

],

outDiameter, opts

],

(* hole aperture diameter *)

inDiameter

]

],

longLength, 180]

];

In[77]:=

ShowSystem[

axiconMirrorWithRipples[80, 7.5, 200, 20., 4.],

PlotPoints -> 60

];

Here is a SurfacePoints plot of the mirror using the default SurfacePoints setting.

In[40]:=

ShowSystem[

axiconMirrorWithRipples[80, 7.5, 200, 20., 4.,GraphicDesign->False],

PlotPoints -> 60, SurfacePoints -> Mesh

];

The SurfacePoints plot has a better likeness to the actual surface function when we increase its value from 7 to 21.

In[7]:=

ShowSystem[

axiconMirrorWithRipples[80, 7.5, 200, 20., 4.,GraphicDesign->False, SurfacePoints -> 21],

PlotPoints -> 60, SurfacePoints -> Mesh

];

Unfortunately, there is a big tradeoff between the SurfacePoints number and the ray-trace calculation time. Therefore, it is best to use the lowest possible SurfacePoints setting that still gives the correct trace. In general, the SurfacePoints setting does not usually improve the numeric accuracy of the ray trace. It instead permits complex-shaped surfaces to be ray-traced without difficulty. In the present axiconMirrorWithRipples example, the default SurfacePoints setting appears to enable a good trace to occur. It is consequently unnecessary to increase the SurfacePoints value beyond the default setting.

In[78]:=

AnalyzeSystem[

{

Move[SingleRay[], {-10, 0}, 10],

axiconMirrorWithRipples[80, 7.5, 200, 20., 4.]

},

PlotType -> TopView,

PlotPoints -> 60

];

4.3.6 SurfaceApproximation -> {TriangleNet -> surfacefunction}

For some surface shapes, the SurfaceApproximation -> TriangleNet setting has difficulty working correctly regardless of the number of SurfacePoints chosen. This is particularly the case for enclosed surfaces that are steeply curved and where the ray must undergo multiple bounces within such a surface. The reason for this difficulty is that the triangular facets for this type of surface function can miss some rays entirely. Here is an example of such a surface that is in the shape of an elongated tube. Here the default setting of SurfaceApproximation produces triangular facets that inscribe the surface boundary.

In[9]:=

tubemirror = Mirror[Function[{#1,Cos[#2],Sin[#2]}],{10,2.0001*Pi}];

ShowSystem[tubemirror, PlotPoints->100, SurfacePoints->Mesh];

ShowSystem[tubemirror, PlotPoints->100, SurfacePoints->Mesh, PlotType->FrontView];

The facets given by the default TriangleNet inscribe the tube-shaped surface.

For most ray directions, this geometry still functions correctly.

In[49]:=

trace = AnalyzeSystem[{

Move[PointOfRays[{30,30}],0,30],

Move[Resonate[tubemirror],6],

Boundary[15,5]},

PlotType->TopView];

The trace works correctly.

However, the trace can fail in some situations. Here is one such example

In[24]:=

AnalyzeSystem[

{Move[SingleRay[],{0,0,-.9},{0,1,0}],

Resonate[tubemirror],

Move[Boundary[10,2, GraphicDesign-> Off],-5]},

PlotPoints->100, SurfacePoints->Mesh, PlotType->FrontView];

The trace has failed.

The problem is solved when a second, bigger surface function is supplied to SurfaceApproximation. The resulting TriangleNet facets now fully enclose the tube surface and the trace works correctly.

In[77]:=

tubemirror2 = Mirror[Function[{#1,Cos[#2],Sin[#2]}],{10,2.0001*Pi},

SurfaceApproximation -> {TriangleNet -> Function[{#1,1.15*Cos[#2],1.15*Sin[#2]}]}];

ShowSystem[tubemirror2, PlotPoints->100, SurfacePoints->Mesh];

AnalyzeSystem[

{Move[SingleRay[],{1,0,-.9},{0,1,0}],

Resonate[tubemirror2]},

PlotPoints->100, SurfacePoints->Mesh, PlotType->FrontView];

The facets given by the TriangleNet of a second surface function lie outside the tube-shaped surface. This produces in a more robust ray-trace.

4.3.7 SurfaceApproximation -> PlanarShape

In addition to the TriangleNet settings of SurfaceApproximation, you can use one of the built-in surface intercept solutions to construct the initial starting point for an interative procedure. If the custom surface is semi-flat then SurfaceApproximation -> PlanarShape can work very well. In this case, the ray intercept solution of a planar surface is used as an estimate for the curved surface solution. Here is one such example.

In[75]:=

sinusoidmirror = CustomMirror[Function[{Sin[Sqrt[#1^2+#2^2]],#1,#2}], {30,30},

SurfaceApproximation -> PlanarShape];

TurboPlot[{Move[GridOfRays[{25,25},NumberOfRays->8, MonteCarlo -> Stratified], 30, 180], sinusoidmirror, Boundary[40]}, PlotPoints->100];

SurfaceApproximation -> PlanarShape produces a very robust ray-trace for semi-flat surface shapes.

4.3.8 SurfaceApproximation -> {SphericalShape -> radius}

The built-in spherical shape can also be used as an ray-surface intercept estimate. This is accomplished with SurfaceApproximation -> {SphericalShape -> radius}. This is particularly useful to model the effects of imperfect manufacturing on spherical lenses. In what follows, we will model the effect from a sinusoidal perturbation of a spherical lens surface. Before examining a perturbed spherical lens, however, we will first review the behavior of a perfect spherical lens.

In[26]:=

sphericalLens = SphericalLens[Infinity, -50, {50,50}, 20, GraphicDesign->Solid];

sphericaltrace = TurboPlot[{

Move[GridOfRays[{45,45},NumberOfRays->16, MonteCarlo -> Stratified], 150, 180],

Move[sphericalLens,100],

Screen[50]}, PlotPoints->50];

FindFocus[sphericaltrace];

A perfect spherical lens and resulting focus.

In order to create a perturbed spherical surface function, we use ModelSurfaceShape to obtain the model of a perfect spherical shape.

In[51]:=

sphericalfunction = ModelSurfaceShape[SphericalShape,-50]

Out[51]=

We can now create a new surface function that combines the perfect spherical shape with a sinusoidal perturbation function.

In[52]:=

perturbedsphericalfunction = Function[Evaluate[First[sphericalfunction]+{.1*Sin[Sqrt[#1^2+#2^2]],0,0}]]

Out[52]=

We are now ready to build a CustomLens with this perturbed surface function. (When you wish to use a planar shape for one of the Custom surfaces, you can use Infinity for the desired planar surface parameter, as shown below.) Finally, we can model the ray-trace behavior of the perturbed spherical lens.

In[54]:=

perturbedSphericalLens = CustomLens[Infinity, perturbedsphericalfunction, {50,50}, 20, GraphicDesign->Solid,

SurfaceApproximation -> {SphericalShape -> -50}];

perturbedtrace = TurboPlot[{

Move[GridOfRays[{45,45},NumberOfRays->16, MonteCarlo -> Stratified], 150, 180],

Move[perturbedSphericalLens,100],

Screen[50]}, PlotPoints->100];

FindFocus[perturbedtrace];

A perturbed spherical lens trace and resulting focus.

In this case, the SurfaceApproximation is only used with the second surface since the first surface was a built-in planar shape. In the event that two spherical surfaces are involved, it is necessary to specify SurfaceApproximation -> {{SphericalShape -> radius1},{SphericalShape -> radius2}} instead. This two-input format can also be used with any of the other SurfaceApproximation methods as well.

4.3.9 Interpolated Surface Functions

When you need to build an optical surface shape from measured experimental data, you can use the ListInterpolation function of Mathematica to model this shape in Rayica.

In[78]:=

?ListInterpolation

In this example, we will simulate experimental data by sampling discrete values from an analytic function. In this case, we will sample the same perturbed spherical surface function used in the previous section. In practice, however, you can import a table of numbers from your experimental measurement.

In[40]:=

sphericalfunction = ModelSurfaceShape[SphericalShape,50];

perturbedsphericalfunction = Function[Evaluate[sphericalfunction[[1,1]]+.1*Sin[Sqrt[#1^2+#2^2]]]]

Out[41]=

In[42]:=

samplepoints = Table[ perturbedsphericalfunction[x,y], {x,-25, 25, 1}, {y,-25, 25, 1}];

Dimensions[samplepoints]

Out[43]=

You can use ListPlot3D to examine your experimental data points in Mathematica.

In[44]:=

ListPlot3D[samplepoints, PlotRange->All];

Finally, we define the surface function with ListInterpolation. Note that ListInterpolation requires that the data is sampled on an evenly spaced grid. As such, the sampled data is single-valued and does not contain coordinate information. Instead, the grid domain, given by {{-25,25},{-25,25}}, is passed in a separate arguement from the sample-data information.

In[45]:=

interpolationfunction = ListInterpolation[samplepoints,{{-25,25},{-25,25}}]

Out[45]=

Next, we define a CustomMirror with the interpolated surface function, interpolationfunction. This time, the grid domain size is the aperture size of the mirror, {50,50}.

In[46]:=

interpolatedmirror =

CustomMirror[interpolationfunction, {50,50}, SurfaceApproximation -> {SphericalShape -> 50}];

We can now perform a ray-trace on the interpolated mirror.

In[57]:=

perturbedtrace = TurboPlot[{

Move[PointOfRays[{30,30},NumberOfRays->5, MonteCarlo -> Stratified], 50, 180],

interpolatedmirror,

Boundary[100, 50]}, PlotPoints->30];

A interpolated mirror trace.

In the ray-traced graphics shown above, the rendered mirror surface does not depict all of the details present in the actual surface function. Nevertheless, the final ray-trace still faithfully follows the actual surface shape, since it does not depend on the graphical rendering in any way.

4.3.10 SurfaceIntersectionFunction

When a particular optical component is first defined, as part of the component creation process, Rayica stores within the component object a number of surface-specific functions that are used later on by PropagateSystem and TurboTrace to perform surface-specific ray-trace operations. Among these surface-specific functions are: SurfaceFunction, SurfaceNormalFunction, and SurfaceIntersectionFunction. These surface-specific functions are stored in the Surfaces parameter of the Component object. Here are the Surface results for the planar-shaped Screen.

In[7]:=

Surfaces/.Screen[50]

Out[7]=

Here is how the SurfaceIntersectionFunction is defined for Screen.

In[8]:=

InputForm[SurfaceIntersectionFunction/.(Surfaces/.Screen[50])]

Out[8]//InputForm=

{CompiledSurfaceIntersectionFunction[{_Real, _Real, _Real, _Real, _Real, _Real},

{{3, 0, 0}, {3, 0, 1}, {3, 0, 2}, {3, 0, 3}, {3, 0, 4}, {3, 0, 5}, {3, 2, 1}}, {5, 1, 10, 0, 4},

{{1, 4}, {5, 1.*^-10, 6}, {91, 14, 3, 0, 3, 3, 0, 6, 3, 0, 7}, {4, 0, 0}, {14, 0, 0, 6}, {47, 7, 6, 0, 0}, {57, 0, 1},

{43, 1, 50}, {36, 3, 7}, {29, 0, 7, 6}, {33, 6, 7}, {29, 0, 4, 6}, {36, 3, 8}, {29, 6, 8, 6}, {33, 6, 8}, {25, 1, 8, 6},

{29, 0, 5, 8}, {36, 3, 9}, {29, 8, 9, 8}, {33, 8, 9}, {25, 2, 9, 8}, {63, 7, 6, 8, 3, 0}, {4, 1, 0}, {66, 0, 0, 0, 0, 7},

{5, 1.*^-10, 6}, {91, 14, 3, 0, 7, 3, 0, 6, 3, 0, 8}, {5, 0., 7}, {53, 7, 8, 0}, {43, 0, 12}, {4, 2, 0},

{66, 0, 0, 0, 0, 8}, {29, 8, 8, 7}, {4, 3, 0}, {66, 0, 0, 0, 0, 8}, {29, 8, 8, 6}, {25, 7, 6, 7}, {5, 625., 6},

{53, 7, 6, 2}, {8, 2, 4}, {44, 3}, {3, False, 3}, {8, 3, 4}, {43, 4, 10}, {4, 1, 0}, {66, 0, 0, 0, 0, 7}, {4, 2, 0},

{66, 0, 0, 0, 0, 6}, {4, 3, 0}, {66, 0, 0, 0, 0, 8}, {63, 7, 6, 8, 3, 1}, {12, 1, 3}, {44, 3}, {7, {-1., 0., 0.}, 3, 3, 2},

{12, 2, 3}, {64, 3, 0}, {12, 0, 1}, {44, 3}, {7, {{-1., 0., 0.}}, 3, 1, 3, 3}, {12, 3, 1}, {2}},

Function[{RSx, RSy, RSz, RTx, RTy, RTz}, If[Chop[RTx] != 0,

{(If[N[Chop[#1[[1]]]] >= 0. && #1[[2]]^2 + #1[[3]]^2 <= 625., {#1[[1]], #1[[2]], #1[[3]]}, {-1., 0., 0.}] & )[

{-(RSx/RTx), RSy - (RSx*RTy)/RTx, RSz - (RSx*RTz)/RTx}]}, {{-1., 0., 0.}}]]]}

It is SurfaceIntersectionFunction that is later used during the ray-trace operation to calculate the surface-ray intersection point. Normally, the SurfaceIntersectionFunction is compiled in order to achieve maximum efficiency. In addition, the SurfaceIntersectionFunction has built-in checks to see if the ray has passed through the surface aperture boundary. In general, the previously discussed SurfaceRayIntersections, SurfaceApproximation, SurfacePoints options all help specify the format of SurfaceIntersectionFunction. With some custom-surface shape settings, the resulting SurfaceIntersectionFunction can get enormous.

4.4 Holograms and Diffraction Gratings

At the most fundamental level, the basic Rayica package performs geometric ray-trace calculations. Within these limits, you can model diffraction gratings in Rayica using the Grating function.

Grating[gratingfunction, aperture, thickness, label, options] and Grating[surfacefunction, gratingfunction, aperture, thickness, label, options] denote a diffractive component that splits incoming rays into multiple diffracted orders according to the gratingfunction parameter and the DiffractedOrders option. Here, the user-named label parameter is optional and can be omitted. The thickness parameter refers to the thickness dimension of the grating substrate. If no thickness value is given, the Grating component is assumed to have no substrate.

In Rayica, diffraction gratings work by treating rays as plane waves in the far-field. Here is a simple example that uses Grating.

In[8]:=

AnalyzeSystem[

{RainbowOfRays[{.4,.65},NumberOfRays->11],

Move[Grating[{0,500,0}&,50],50],

Boundary[150,100]}, PlotType->TopView];

A constant diffraction grating.

Here, the diffraction grating is behaves as a black box and the resulting rays contain the amplitude, polarization, and directional behavior of corresponding plane waves that diffracted through the grating. However, gratings in Rayica can only hold a single grating vector value for each surface position, although you can specify multiple diffraction orders. In the above example, we specified a three-dimensional grating vector that remained constant for all surface points. In the examples that follow, we will see how to specify a grating vector function that varies according to the surface position.

According to the GratingThickness option, the physical grating material may also have a physical thickness within a separate refractive emulsion layer. Note that the total thickness of the Grating is given by the substrate thickness in combination with GratingThickness. When DoubleSubstrate -> True is specified, the grating is sandwiched between two substrates of equal thickness. (Note that DoubleSubstrate only applies to flat-surfaced gratings).

The gratingfunction parameter specifies a grating vector either as a one-dimensional value, Gy, a two-dimensional list of values, {Gy, Gz}, a three-dimensional list of values, {Gx, Gy, Gz}, or a three-dimensional function, Function[{s,t},{Gx[s,t], Gy[s,t], Gz[s,t]}], in which {Gx, Gy, Gz} specify the indicated grating directions with the grating surface initially oriented parallel to the y-z plane. These vector quantities have units of spatial frequency and are given in line pairs per millimeter. A single number used in the grating parameter indicates a uniformly distributed grating vector pointing in the direction of the y axis. A list of two numbers designates a uniformly distributed grating vector whose direction lies within the surface plane.

If you compare results with calculations by hand, please keep track of the correspondence of the options VacuumFrequency and GratingMedium. Note that VacuumFrequency -> False assumes the grating function has already incorporated the refractive index of the grating material. However, if VacuumFrequency -> True is specified, then Rayica will include an additional refractive index factor, given by GratingMedium, to the grating magnitude.

4.4.1 Variable Pitch Grating

In[7]:=

Solve[fe==m*d+fc,m]

Out[7]=

In[8]:=

Simplify[-(fc-fe)/d * r +fc]

Out[8]=

In[14]:=

centerfrequency = 2000;

edgefrequency = 1200;

aperture = 25;

With[{fc = centerfrequency, fe = edgefrequency, d = aperture},

gratingfunction = Function[{s,t}, {0, fc + (-fc+fe)*Abs[s]/d, t}]

];

In[21]:=

AnalyzeSystem[{

LineOfRays[45, NumberOfRays -> 18],

Move[GratingMirror[gratingfunction, 50, DiffractedOrders -> {{1, 1},{0,1}}],100],

Boundary[{-50, -50, -50}, {200, 50, 50}]}, PlotType -> TopView];

In[23]:=

AnalyzeSystem[{

LineOfRays[45, NumberOfRays -> 18],

Move[CustomGratingMirror[

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

DiffractedOrders -> {{1, 1},{0,1}}],100, 180],

Boundary[{-50, -50, -50}, {200, 50, 50}]}, PlotType -> TopView];

4.4.2 Holographic Optical Element

In[77]:=

objectpoint = {-100,0,0};

gratingpoint = {0,s,t};

objectvector = (gratingpoint - objectpoint);

objectvector = objectvector/Sqrt[Dot[objectvector,objectvector]]

Out[80]=

In[81]:=

referencevector = {1,0,0};

In[82]:=

gratingvector = (objectvector - referencevector)/(.532*10^-3);

gratingfunction = Apply[Function,{{s,t},gratingvector}]

Out[83]=

In[104]:=

result = TurboPlot[{

Move[LineOfRays[45, NumberOfRays -> 11], -25],

Grating[gratingfunction, {50, 50}, DiffractedOrders -> {{1, 1},{0,0}}],

Move[Screen[100],50]},PlotType->TopView];

In[142]:=

{FocalPoint, SpotSize}/.FindFocus[result]

Out[142]=

In[96]:=

conjugateresult = AnalyzeSystem[{

Move[LineOfRays[45, NumberOfRays -> 11], -25],

Grating[gratingfunction, {50, 50}, DiffractedOrders -> {{-1, 1},{0,0}}],

Move[Screen[50],150]},PlotType->TopView];

In[143]:=

{FocalPoint, SpotSize}/.FindFocus[conjugateresult]

Out[143]=

In[144]:=

colorshiftedresult = AnalyzeSystem[{

Move[LineOfRays[45, NumberOfRays -> 11, WaveLength->.633], -25],

Grating[gratingfunction, {50, 50}, DiffractedOrders -> {{-1, 1},{0,0}}],

Move[Screen[{50,50}],150]},PlotType->TopView];

In[146]:=

{FocalPoint, SpotSize}/.FindFocus[colorshiftedresult]

Out[146]=

In[107]:=

?FindIntensity

In[122]:=

FindIntensity[{

Move[LineOfRays[45, NumberOfRays -> 1024, WaveLength->.633], -25],

Grating[gratingfunction, {50, 50}, DiffractedOrders -> {{-1, 1},{0,0}}],

Move[Screen[{.25,.25}],83.3673]}, SequentialTrace->True, SmoothKernelSize->3, ReportedSurfaces -> {1,2}]

Out[122]=

In[152]:=

TurboPlot[{

Move[GridOfRays[{45,45}, NumberOfRays -> 48, WaveLength->.633], -25],

Move[ApertureStop[{50,50},45],-20],

Grating[gratingfunction, {50, 50}, DiffractedOrders -> {{-1, 1},{0,0}}],

Move[Screen[{.25,.25}],83.3673]}, SequentialTrace->True, PlotType->Surface];

In[243]:=

FindIntensity[{

Move[GridOfRays[{45,45}, NumberOfRays -> 48, WaveLength->.633], -25],

Move[ApertureStop[{50,50},45],-20],

Grating[gratingfunction, {50, 50}, DiffractedOrders -> {{-1, 1},{0,0}}],

Move[Screen[{.25,.25}],83.3673]}, SequentialTrace->True, SmoothKernelSize->4, Full3D -> False, CalibrateEnergy -> True, ReportedSurfaces -> Last]

Out[243]=

In[244]:=

FindIntensity[{

Move[GridOfRays[{45,45}, NumberOfRays -> 48, WaveLength->.633], -25],

Move[ApertureStop[{50,50},45],-20],

Grating[gratingfunction, {50, 50}, DiffractedOrders -> {{-1, 1},{0,0}}],

Move[Screen[{.25,.25}],83.3673]}, SequentialTrace->True, SmoothKernelSize->.01, KernelScale->Absolute, Full3D -> False, ReportedSurfaces -> Last]

Out[244]=

4.5 Generic Building Blocks: The Language of Components

Rayica has its own special language called "generic building blocks" that are used by component functions to model different optical elements. Generic building blocks are a collection of functions that assign new behaviors and pass special attributes to the Component object, which holds the information about one or more optical surfaces that make up an element. Rayica presently has 30 principle generic building blocks. At any time that you wish while working with Rayica, you can access a listing of Rayica's positioning functions with the BuildingBlockFunctions command:

In[3]:=

BuildingBlockFunctions

Hyperlinks to Rayica's most important generic building blocks functions. Clicking on any name will give you a description of its use in Mathematica.

These various generic building blocks are called internally by each component function in order to construct a particular optical element. Normally, this activity happens transparently whenever you evaluate a component function, such as PlanoConvexLens. However, you can also call these generic building blocks directly. For example, the Resonate function is a generic building block that is frequently called by the user to invoke non-sequential ray-trace behavior within an optical system. Two other examples are Hole that places a hole in the element and Fresnel that creates a Fresnel lens (or mirror) out of an ordinary "thick" optic. You can see how generic building blocks are used within a particular component function with the BuildComponent -> True option. This option serves no purpose other than as a diagnostic tool that causes the created component to display its generic-building-block history. For example, here are the underlaying generic building blocks used by the PlanoConvexLens function.

In[3]:=

PlanoConvexLens[100, 50, 10, BuildComponent->True]

Out[3]=

From this, we can see that the PlanoConvexLens function has internally called Resonate, Refraction, ComponentRendering, and ComponentFoundation. Each of these generic functions have a specific purpose in fashioning the final plano-convex lens Component object. In particular, ComponentFoundation creates the initial Component object, while ComponentRendering instructs the Component object how it is graphically rendered. Refraction passes the trait for optical refraction to the Component object, and Resonate informs the Component to invoke nonsequential ray-tracing within its active surfaces. In general, generic building blocks completely define the behavior of a particular optical component. As a demonstration, to obtain the behavior of the PlanoConvexLens function, you can explicitly use the same generic building blocks directly.

In[21]:=

lens = Resonate[Refraction[ComponentRendering[ComponentFoundation[{51.872,∞},50.,10]]]];

AnalyzeSystem[{LineOfRays[45], Move[lens,50], Boundary[150,60]}, PlotType->TopView];

A plano-convex lens built from scratch.

Now, we will examine the Mirror function.

In[4]:=

Mirror[100, BuildComponent->True]

Out[4]=

Here you can see that the two inner-most generic functions, ComponentFoundation and ComponentRendering, have been used with both Mirror and PlanoConvexLens. In fact, these two functions are used with every component function in some combination (although non-rendered optical components would not require ComponentRendering). Therefore, only the Reflection function is new for Mirror, which passes the trait for optical reflection. Note, however, Resonate is not used with Mirror because only a single flat surface is involved. Next we examine the Screen function.

In[5]:=

Screen[100, BuildComponent->True]

Out[5]=

By comparing the results of Screen with those of Mirror, we can see that the Reflection function in Mirror has been exchanged by the Transmission function in Screen. Here of course, the Transmission function passes the trait for the transmission of rays (otherwise the optic would be totally invisible to the rays). The next example shows the BeamSplitter function, which uses both Reflection and Transmission generic traits.

In[13]:=

BeamSplitter[{50,50}, 100, BuildComponent->True]

Out[13]=

For component functions that use more than two surfaces, the generic building block structure can get more complicated. Here is the Prism function that contains five surfaces.

In[4]:=

Prism[{45, 50, 45}, 50, BuildComponent->True]

Out[4]=

In the instance of Prism, a new Refraction, ComponentRendering, and ComponentFoundation function was used for every surface present. However, it also would have been possible to apply a single Refraction and ComponentRendering function to the list of surfaces (created by ComponentFoundation) instead of repeating these at every surface. ComponentFoundation function, on the other hand, can not create more than two surfaces at a time (and only if the two surfaces are in-line with each other). Otherwise, a separate ComponentFoundation is required for every surface present. The following example illustrates this use of a single call to Refraction and ComponentRendering.

In[198]:=

strangeprism =

Resonate[Refraction[ComponentRendering[

{Move[ComponentFoundation[100,{50,50}],0,-45],

Move[ComponentFoundation[-100,{50,50}],{50,0},45],

Move[ComponentFoundation[100,{60,50}],{25,-35},90]},

GraphicDesign -> Wire, EdgeRendering -> Empty]]];

ShowSystem[strangeprism];

Only a single Refraction and ComponentRendering is used to generate this element.

Finally, we will examine how different surface shapes are handled by Rayica's generic building blocks. For this, we evaluate the ParabolicMirror function.

In[9]:=

ParabolicMirror[50, 100, BuildComponent->True]

Out[9]=

Here we can see that the ParabolicMirror has passed the SurfaceLabel -> ParabolicShape option to ComponentFoundation. It is therefore evident that the ComponentFoundation is responsible for establishing the component shape and the SurfaceLabel option is used to specify this information. In order to gain further insight into this process, lets evaluate a CustomMirror function.

In[25]:=

CustomMirror[Function[10], 100, BuildComponent->True]

Out[25]=

In this case, the custom surface function, 10 &, is the first parameter of ComponentFoundation and SurfaceLabel -> OtherShape indicates that an external surface function is being used.

4.6 CustomDeflections

In this section, we will examine the use of CustomDeflections. CustomDeflections is one of 30 principle generic building blocks that were listed at the start of the previous section. Unlike most generic building blocks, CustomDeflections is not used by any of the internal component functions. It is instead designed to give the user a way to create new ray-trace behaviors.

CustomDeflections[component, raydeflectfunction, functionlabel, options] allows the user to create custom ray-tracing functions. The functionlabel is optional.

CustomDeflections works equally well with both AnalyzeSystem and TurboTrace. We will see shortly that the raydeflectfunction of CustomDeflections operates on ray and surface parameters. When tracing with AnalyzeSystem, you can work with any ray parameter that is listed in Options[Ray]. However, TurboTrace uses a limited set of the most important ray parameters, shown below. (These parameters are listed in $TurboRaysRuleNames.)

RayStart | SurfaceNumber | RayTilt |

RayEnd | SurfaceCoordinates | |

RotationMatrix | SourceID | |

WaveLength | WaveFrontID | |

Intensity | RaySourceNumber | |

RayLength | RaySlot | |

RefractiveIndex | GenerationNumber | |

OpticalLength | IntersectionNumber | |

ComponentNumber | Polarization |

Ray parameters used by TurboTrace.

Note that the RayTilt parameter is in fact the same as the first three elements in RotationMatrix. In addition to the ray-specific parameters, the following optical surface parameters may be used: SurfaceCoordinates ( also given as a ray parameter), SurfaceNormalMatrix, and TransformationMatrix. When working exclusively with TurboTrace, the following additional parameters can also be used: RayAmplitude, RefractiveIndexBefore, and RefractiveIndexAfter.

Here, we can see the standard behavior of the unmodified Screen element.

AnalyzeSystem[{SingleRay[],Move[Screen[50],50],Boundary[100]},PlotType->TopView];

Default Screen behavior.

In the examples that follow, we will use CustomDeflections with Screen and see how to create different raydeflectfunction constructions that modify the ray-trace behavior in various ways.

4.6.1 Getting a Ray to Change Color

The simplest raydeflectfunction simply changes a ray parameter by a constant amount. In this first example, we will use CustomDeflections on Screen to alter the wavelength of any ray that passes through the screen surface. Here, the raydeflectfunction is given by Function[Ray[WaveLength->.45]].

In[7]:=

alterwavelength =

CustomDeflections[

Screen[50],

Function[Ray[WaveLength->.45]]

]

Out[7]=

When we trace a ray through the surface, we get the desired effect.

In[8]:=

AnalyzeSystem[{SingleRay[],Move[alterwavelength,50],Boundary[100]},PlotType->TopView];

CustomDeflections is used to modify WaveLength parameter. This changes both the wavelength and the displayed color of a ray.

4.6.2 Using Information from a Ray Parameter

The previous example did not use any existing ray parameter information. Instead, it simply forced a parameter change without interaction. We will now see how to use previously existing ray information in order to invoke a wanted change in the propagated ray. Lets try an example that uses the RayLength information to shift the transverse ray end position in space. We use ReplaceFor (written as /.) to get at the ray information. Here, an optional "shiftposition" label is used with CustomDeflections. Such labels were once required with the original Rayica package, but are no longer necessary.

In[3]:=

shiftposition =

CustomDeflections[Screen[50],

Function[Ray[RayEnd->({50,.5*RayLength,0}/.#)]],

"shiftposition"

]

Out[3]=

In[4]:=

AnalyzeSystem[{SingleRay[],Move[shiftposition,50],Boundary[100]},PlotType->TopView];

The position of the ray has been shifted.

You can also introduce intermediate variables inside our raydeflectfunction with either Block or Module. Here is the same raydeflectfunction, used in the previous example, with Block and two intermediate variables, rayend and raylength, included.

In[9]:=

shiftscreen2 =

CustomDeflections[Screen[50],

Function[

Block[{rayend,raylength},

rayend = RayEnd/.#;

raylength = RayLength/.#;

Ray[RayEnd->rayend + {0,.5*raylength,0}]

]

]

];

AnalyzeSystem[{SingleRay[],Move[shiftscreen2,50],Boundary[100]},PlotType->TopView];

Block is used to create intermediate variables.

You can also modify more than one ray parameter at a time in raydeflectfunction. In this example, we change the values of both RayEnd and WaveLength.

shiftscreen3 =

CustomDeflections[Screen[50],

Function[

Block[{rayend,raylength},

rayend = RayEnd/.#;

raylength = RayLength/.#;

Ray[RayEnd->rayend + {0,.5*raylength,0}, WaveLength->.65]

]

]

];

AnalyzeSystem[{SingleRay[],Move[shiftscreen3,50],Boundary[100]},PlotType->TopView];

Two ray parameters are modified at the same time.

Finally, you can to generate multiple rays in the output if you list several Ray objects in raydeflectfunction. In the following example, we list three different Ray objects to generate three new rays.

In[11]:=

shiftscreen4 =

CustomDeflections[Screen[50],

Function[

Block[{rayend,raylength},

rayend = RayEnd/.#;

raylength = RayLength/.#;

{Ray[RayEnd->rayend + {0,.5*raylength,0}, WaveLength->.45],

Ray[RayEnd->{50,10,0}],

Ray[WaveLength->.6]}

]

]

];

AnalyzeSystem[{SingleRay[],Move[shiftscreen4,50],Boundary[100]},PlotType->TopView];

Three new rays are generated.

Here is the same component traced with TurboPlot. Even though TurboTrace does not directly use Ray objects, the Ray notation of the raydeflectfunction is automatically converted by CustomDeflections into the TurboTrace format when TurboPlot is called.

In[72]:=

TurboPlot[{SingleRay[],Move[shiftscreen4,50],Boundary[100]},PlotType->TopView];

TurboPlot can also be used with CustomDeflections instead of AnalyzeSystem.

4.6.3 AddTo, TakeFrom, and ReplaceFor

CustomDeflections (and indeed all built-in deflection functions) use the options AddTo, TakeFrom, and sometimes ReplaceFor. These options control the flow of information to and from the ray deflection process.

AddTo -> objecttypes specifies the types of objects that receive the new parameter results.

TakeFrom -> objecttypes designates which types of objects are used as inputs to the ray-tracing function calculation.

ReplaceFor -> objecttypes indicates which types of objects are replaced by the new parameter results of the ray-tracing function calculation.

Three important options used by CustomDeflections and other generic building blocks.

Of these three options, AddTo is the most important to consider. The remaining two options, TakeFrom and ReplaceFor, are rarely altered and will not be considered further at this time. There are seven possible objecttypes that include: NewRay, ChangedRay, OriginalRay, CreatedRay, AllCreatedRays, ChangedSurface, and ChangedComponent. Of these seven objecttypes, only NewRay, ChangedRay, OriginalRay, and CreatedRay are commonly used by CustomDeflections. For the remainder of this section, we will examine these four objecttypes in more detail.

If we examine the options of CustomDeflections that modified the ray color, we can immediately see the effect of changing the AddTo value. By default, CustomDeflections uses AddTo -> CreatedRay:

In[16]:=

AddTo/.Options[CustomDeflections]

Out[16]=

In its default state, the new ray information is added to a newly created ray at the surface.

In[103]:=

colorscreen =

CustomDeflections[Screen[50],

Function[Ray[WaveLength->.45]]

];

AnalyzeSystem[{SingleRay[],Move[colorscreen,50],Boundary[100]},PlotType->TopView];

Default AddTo -> CreatedRay condition.

If we instead change this value to AddTo -> OriginalRay, we can change information in the ray that is incident on the surface.

In[150]:=

colorscreen =

CustomDeflections[Screen[50],

Function[Ray[WaveLength->.45]],

AddTo->OriginalRay

];

AnalyzeSystem[{SingleRay[],Move[colorscreen,50],Boundary[100]},PlotType->TopView];

AddTo -> OriginalRay alters the ray before the surface.

In this case, the color of the previous ray segment has been changed to blue and this attribute has then been passed on to the subsequent ray generations. On the other hand, if we use AddTo -> ChangedRay we get something even more astounding:

In[49]:=

colorscreen =

CustomDeflections[Screen[50],

Function[Ray[WaveLength->.45]],

AddTo->ChangedRay

];

AnalyzeSystem[{SingleRay[],Move[colorscreen,50],Boundary[100]},PlotType->TopView];

AddTo -> ChangedRay does not affect the ray after the surface.

This time, the newly created ray does not inherit the color change because the information gets passed to the previous ray segment after it has already passed its information onto the new ray generations.

You can use AddTo -> NewRay to create a new ray at the same surface. In the following case, two rays are generated at the screen surface that separately contain the new and the old wavelength information. Shown below, the Grating element is used to visually separate the two rays from each other.

In[160]:=

colorscreen =

CustomDeflections[Screen[50],

Function[Ray[WaveLength->.45]],

AddTo->NewRay

];

AnalyzeSystem[{

SingleRay[],

Move[colorscreen,50],

Move[Grating[1000,50,DiffractedOrders->{{1,1}}],75],

Boundary[100]},PlotType->TopView];

ReadRays[%, WaveLength, ComponentNumber -> 3]

Out[162]=

AddTo -> NewRay creates a new ray and preserves the old ray.

The AddTo -> CreatedRay setting becomes important with components that already generate multiple rays at its surface. This setting causes the new information to be included the previously created rays.

In[147]:=

colorsplitter =

CustomDeflections[BeamSplitter[{50,50},50],

Function[Ray[WaveLength->.45]],

AddTo->CreatedRay

];

AnalyzeSystem[{SingleRay[],Move[colorsplitter,50,45],Boundary[100]},PlotType->TopView];

ReadRays[%, WaveLength, ComponentNumber -> 2]

Out[149]=

AddTo -> CreatedRay conserves the total number of rays.

In contrast, the AddTo -> NewRay setting causes an additional ray to be generated by CustomDeflections for each ray already present. In this example, four rays are now created at the beamsplitter surface instead of two.

In[144]:=

colorsplitter =

CustomDeflections[BeamSplitter[{50,50},50],

Function[Ray[WaveLength->.45]],

AddTo->NewRay

];

TurboPlot[{SingleRay[],Move[colorsplitter,50,45],Boundary[100]},PlotType->TopView, ShowArrows->True];

ReadTurboRays[%, WaveLength, ComponentNumber -> 2]

Out[146]=

AddTo -> NewRay produces four rays with the beamsplitter.

4.6.4 Redirecting RayTilt with CustomDeflections

Of course, one of the most important actions in ray-tracing is the ability to change the direction of a ray at an optical surface. In Rayica, the direction of the ray is governed by the RayTilt parameter. The RayTilt parameter is a three-dimensional unit direction vector. In order to change the ray direction, you need to set the RayTilt of the ray to the desired direction. We can demonstrate this by creating a new type of optic that focusses the rays to a fixed point in space. First, let us consider the behavior of a normal mirror optic, illustrated below. In the case of a mirror, the rays continue to diverge after the mirror. In our new custom optic, we will cause the rays to focus after its surface.

AnalyzeSystem[{

WedgeOfRays[10],

Move[Mirror[50],100,-45],

Boundary[200]},PlotType->TopView];

A normal mirror optic.

In order to build this new optic, we first need to create a Component "base" for our CustomFunction to operate on. We can do this with the ComponentFoundation and ComponentRendering functions as shown.

In[3]:=

basecomponent = ComponentRendering[ComponentFoundation[Infinity,50]]

Out[3]=

This new basecomponent is not capable of interacting with rays until we give it a ray-trace deflection property with a generic function such as: Transmission, Reflection, Refraction, Diffraction, or, in this instance, CustomDeflection. If we try to trace rays through basecomponent without deflection property, the rays pass through the component without any interaction. The component is "invisible" to the rays.

In[4]:=

AnalyzeSystem[{

WedgeOfRays[10],

Move[basecomponent,100,-45],

Boundary[200]}, PlotType->TopView];

The rays pass through basecomponent without interaction.

We are now ready to prepare our CustomDeflections function and use it to create a new customcomponent optic. For this, we will cause the rays to focus at the fixed point of {100,100,0} in the ray-trace space. In order to create a new direction vector for the ray, we use the {100,100,0}-(RayEnd/.#), where the RayEnd parameter gives us the three-dimensional ray positions along the optical surface. Finally, the direction vector is normalized with (#/Sqrt[Dot[#,#]])&, where ( )& is a short-hand notation for Function[ ].

customcomponent = CustomDeflections[basecomponent,

Function[Ray[RayTilt->(#/Sqrt[Dot[#,#]])&[{100,100,0}-(RayEnd/.#)]]]];

AnalyzeSystem[{

WedgeOfRays[10],

Move[customcomponent,100,-45],

Boundary[200]}, PlotType->TopView];

The customcomponent optic focuses the rays to a fixed point in space.

Note that our particular CustomDeflections function causes the rays to focus through the same fixed point in space regardless of the customcomponent optic's position and orientation. This is shown below, with TurboPlot this time.

In[17]:=

TurboPlot[{

WedgeOfRays[10],

Move[customcomponent,150],

Boundary[200]},

PlotType->TopView, ShowArrows->True];

The rays focus to the same fixed point regardless of the customcomponent position.

In addition to using RayEnd, more elaborate RayTilt deflection functions may also use the SurfaceNormalMatrix surface parameter. See the Optica Software web-site (www.opticasoftware.com) and the on-line Library of Examples for further examples.

4.6.5 Optimizing CustomDeflections for TurboTrace

Because TurboTrace and PropagateSystem have different internal structures, the structural implementation of CustomDeflections is also different for TurboTrace and PropagateSystem. Consequently, there are two possible deflection function formats for CustomDeflections. Until now, the format that we have been using is actually the format for PropagateSystem. The PropagateSystem format is most convenient because it's structure can be automatically changed by Rayica into the TurboTrace format. However, it is also possible to directly specify the TurboTrace format in CustomDeflections. When only TurboTrace is required or with very complicated deflection functions (that require the extensive use of local variables), it is generally best to directly specify the TurboTrace format. Unfortunately, however, the TurboTrace format cannot be automatically changed into the PropagateSystem format. Consequently, it becomes necessary to directly specify both formats when working with both TurboTrace and PropagateSystem. This accomplished with the additional PropagateSystemDeflection option.

PropagateSystemDeflection is an option of CustomDeflections that specifies the deflection function to be used in PropagateSystem calculations.

When CustomDeflections uses an input deflection function parameter that is compatible with PropagateSystem calculations, then PropagateSystemDeflection is not utilized. Otherwise, the input deflection function parameter is only used for TurboTrace calculations and PropagateSystemDeflection is used with PropagateSystem. In the case where a separate deflection function is required with PropagateSystem, then you are use one of the built-in deflection functions with PropagateSystem. In particular, PropagateSystemDeflection -> Automatic/Transmission uses a Transmission-type deflection function and PropagateSystemDeflection -> Reflection uses a Reflection-type deflection function. In all other cases, the value of PropagateSystemDeflection is directly passed as the DeflectionFunction setting in PropagateSystem. In the following example, we directly specify both formats for TurboTrace and PropagateSystem.

In[6]:=

turboOptimized =

CustomDeflections[Screen[50],

{

Hold[{localrayend,_Real,1}, localraylength],

Hold[localrayend = {1.}, localraylength=1.],

Hold[

localrayend = RayEnd;

localraylength = RayLength;

AllCreatedRays = {

RayEnd = localrayend + {0,.5*localraylength,0}; Ray,

RayEnd = {50,10,0}; WaveLength =.45; Ray,

RayEnd = localrayend; WaveLength = .6; Ray}

]

},

MultipleCreatedRays -> 3,

PropagateSystemDeflection ->

Function[

Block[{rayend,raylength},

rayend = RayEnd/.#;

raylength = RayLength/.#;

{Ray[RayEnd->rayend + {0,.5*raylength,0}],

Ray[RayEnd->{50,10,0}, WaveLength->.45],

Ray[WaveLength->.6]}

]

]

];

The turboOptimized optic gives two separate formats for TurboTrace and PropagateSystem.

As demonstrated here, the TurboTrace format for CustomDeflections uses a list of three parameters that are each held (with Hold). Here, the first parameter declares the local variables (see Compile), the second parameter initializes the local variables, and the third parameter holds the actual deflection-function source code. Finally the PropagateSystem-formated deflection is passed in the PropagateSystemDeflection option as explained previously. In the TurboTrace format, the capitalized symbols denote placeholders in TurboTrace for important parameters. Many of these parameters have already been listed at the start of Section 4.6 as ray and surface parameters for TurboTrace. In addition, there are some other parameters that have not been discussed. In particular, AllCreatedRays holds the list of rays that are returned by the deflection function. Furthermore, Ray contains the entire ray parameter information.

Also demonstrated, we have specified a MultipleCreatedRays setting for the CustomDeflection function. MultipleCreatedRays is only used in TurboTrace calculations and helps to construct the most optimal ray-trace code.

MultipleCreatedRays -> True/False/numberofrays specifies how many rays are generated at an optical surface by a deflection function.

However, MultipleCreatedRays is not used for PropagateSystem calculations. In this example, MultipleCreatedRays -> 3 has indicated that 3 rays will be generated by our deflection function. When MultipleCreatedRays is not specified by the user, CustomDeflections will attempt to automatically determine the number of rays that will be generated and pass this information onto TurboTrace. However, it is always safest to provide MultipleCreatedRays information explicitly whenever more than one ray is to be generated by CustomDeflections. (Although we have been discussing MultipleCreatedRays for CustomDeflections in particular, it is worth noting that MultipleCreatedRays is actually used by all built-in deflection functions with TurboTrace. For most built-in deflection functions , however, this option is set automatically without further user involvement.)

Finally, we trace through this turboOptimized optic separately with TurboPlot and AnalyzeSystem.

In[7]:=

TurboPlot[{SingleRay[],Move[turboOptimized,50],Boundary[100]},PlotType->TopView];

The turboOptimized optic with TurboTrace.

AnalyzeSystem[{SingleRay[],Move[turboOptimized,50],Boundary[100]},PlotType->TopView];

The turboOptimized optic with PropagateSystem.

4.6.6 AffectedSurfaces

When an optical element contains more than one surface, you can use AffectedSurfaces -> {surface numbers} to specify which surfaces of the Component object will be affected by CustomDeflections. In particular, each surface element within the Component object is stored as an element within an ordered list. In general, the surface numbers given by AffectedSurfaces relate to the element positions in this list and identifies particular optical surfaces. These numbers also correspond with the SurfaceNumber parameter. For example, AffectedSurfaces -> {2} specifies that the second optical surface will inherit the new traits of CustomDeflections. In the following example, we will use AffectedSurfaces -> {2} to modify the wavelength of the ray at the second surface of a PlanoConvexLens component.

In[29]:=

specialLens =

CustomDeflections[

PlanoConvexLens[100,50,10],

Function[Ray[WaveLength->.45]],

AffectedSurfaces -> {2}

];

AnalyzeSystem[{LineOfRays[45],Move[specialLens,50],Boundary[100]},PlotType->TopView];

In this case, the wavelength of the rays are changed to blue at the right-most surface because this is internally recognized as the second surface element of the Component. We will next send the rays through the lens from the reverse direction.

In[7]:=

AnalyzeSystem[{Move[LineOfRays[45],100,180],Move[specialLens,50],Boundary[100]},PlotType->TopView];

Again the wavelength of the rays are changed to blue at the second surface, but this time the change happens as the ray enters the lens. This should not be surprising since the specified surface number is independent of the ray-trace order . If we had wished to make the wavelength change at the left-most surface, we would instead use AffectedSurfaces -> {1}.

RemoveDeflection

In addition to CustomDeflections, AffectedSurfaces is an option of many generic building blocks. In each case, you use AffectedSurfaces to affect a change at specific surfaces while leaving the other surfaces unchanged. As a more complex example, we will use Reflection to create mirror on the second surface of a plano-convex lens.

In[18]:=

reflectedLens =

Resonate[

Reflection[

RemoveDeflection[

PlanoConvexLens[100,50,10],

AffectedSurfaces -> {2}

],

AffectedSurfaces -> {2},

AddTo -> CreatedRay

],

AffectedSurfaces -> {2}

];

AnalyzeSystem[{LineOfRays[45],Move[reflectedLens,50],Boundary[100]},PlotType->TopView];

In this case, we needed to first remove the effect of Refraction at the second surface before we could add the Reflection trait. This was accomplished with RemoveDeflection.

RemoveDeflection[component, options] is a generic building block that is used to remove deflections from existing component surfaces.

RemoveDeflection represents a special class of generic functions that modify existing traits of optical components. Other members of this class include: RemoveSurface, InsertDeflection, and AddDeflection. (See the Rayica Reference Guide for further information on these functions.)

The previous example relied on several of the control mechanisms that we had examined earlier in this chapter. In particular, AffectedSurfaces -> {2} indicated that only the second surface is to be affected. In addition, AddTo -> CreatedRay was used with Reflection to combine its result with the existing ray and not to create extra rays at the surface for the new information. As evidenced here, both AffectSurfaces and AddTo are frequently used with many generic building blocks. Finally, the Resonate function was added to cause the component to trace rays non-sequentially between the first lens surface and the new reflective surface. It is interesting to note that the PlanoConvexLens object originally already had the Resonate attribute (see Section 4.5). Unfortunately, however, RemoveDeflection has removed not only the Refraction trait from the second surface, but also the Resonate trait. This is why we have reapplied Resonate to the second surface and not to the first surface (although a repeated application of Resonate to the first surface would not have hurt anything, it would have been unnecessary.)

In addition to AffectedSurfaces -> {surface numbers}, there exist several other ways to specify AffectedSurfaces. For example, AffectedSurfaces can operate with any the following word settings: AllDeflections, AllSurfaces, FirstSlot, NotFirstSlot, LastSlot, NotLastSlot, and IndependentSurfaces. Of these different forms, only AffectedSurfaces -> AllSurfaces is commonly employed since it is the default setting for most generic building blocks. AllSurfaces simply indicates that all surfaces within the Component object will be affected by the new generic trait. For most part, however, the other possible setting of AffectedSurfaces are rarely used and, in some instances, are not compatible with TurboTrace. As such, we will not examine any of the other AffectedSurfaces settings at this time. Please consult the Rayica Reference Guide for further information about AffectedSurfaces.

LabelSurfaces

When working with optical systems that contain multiple surface elements, it is sometimes difficult to find out how the surfaces are internally numbered in order to apply options such as AffectedSurfaces. You can use LabelSurfaces -> True with ShowSystem to determine this information.

LabelSurfaces -> True/False is an option of ShowSystem that displays the ComponentNumber and SurfaceNumber information for each component surface in an optical system.

When a single component element is present, the corresponding SurfaceNumber for each surface is displayed. In the PlanoConvexLens examples used previously, there are only two surfaces present and we simply guessed to discover the correct SurfaceNumber setting for AffectedSurfaces. If instead, we wished to work with a Prism that has five internal surfaces, we need to use the LabelSurfaces option with ShowSystem to easily determine the correct surface numbering:

In[9]:=

ShowSystem[Prism[{45,50,45},50], LabelSurfaces -> True, DefaultFont→{"Times",14}];

LabelSurfaces -> True shows the SurfaceNumber of each surface present.

For simple systems, you can immediately determine the corresponding ComponentNumber for each optical component by simply inspecting its order position in the component listing. However, when system definitions are more complex, it can become difficult to keep track of the corresponding ComponentNumber for every component of the system. In such cases, you can again use the LabelSurfaces option. When several components are present, a pair of numbers that correspond with {ComponentNumber, SurfaceNumber} is displayed by each component surface. As such, LabelSurfaces will tell you the internal surface order as well as how the different components are ordered. To illustrate this, here is a simple example that shows two components.

In[10]:=

ShowSystem[{PlanoConvexLens[100,50,10],Move[Prism[{45,50,45},50],50]},

PlotType->TopView, LabelSurfaces->True, DefaultFont→{"Times",14}];

LabelSurfaces -> True shows {ComponentNumber, SurfaceNumber} for multiple components.

In the case of multiple component entries, LabelSurfaces displays an ordered pair of numbers that show the {ComponentNumber, SurfaceNumber} of each optical surface.

Created by Mathematica (November 19, 2004)