mpb-1.4.2/ 0002755 0001754 0000144 00000000000 07631010734 006040 5 mpb-1.4.2/doc/ 0002755 0001754 0000144 00000000000 07631010727 006607 5 mpb-1.4.2/doc/acknowledgments.html 0000644 0001754 0000144 00000004367 07374654650 012627
This work was supported in part by the Materials Research Science and Engineering Center program of the National Science Foundation under Grant No. DMR-9400334, the U.S. Army Research Office under contract/grant DAAG55-97-1-0366, a National Defense Science and Engineering Fellowship, and an MIT Karl Taylor Compton Fellowship.
Clarendon Photonics, Inc., deserves special mention for funding development of the parallel version of MPB.
This project is also deeply indebted to the free software community for many invaluable tools and libraries, especially the GNU project (for Guile, gcc, autoconf, and other software, as well as its courageous leadership), the GNU/Linux operating system, the National Center for Supercomputing Applications at the University of Illinois (for HDF), and the LAPACK/BLAS developers.
S. G. Johnson thanks Dr. Matteo Frigo for his friendship, inspiration, and definition of "legacy code" as "any program written by a physicist." Dr. Shanhui Fan and Dr. Pierre R. Villeneuve endured endless interruptions by their group-mate and were generous with their patience and enthusiasm. Thanks to Dr. Douglas C. Allan of Corning for pestering (and bribing) me to bring the program to a usable state, and his colleague Karl Koch for being the first beta tester.
Robert D. Meade deserves credit for writing a predecessor to this program that was used in our group for many years. Although it does not share any code with MPB, his software blazed our algorithmic path and formed an invaluable baseline for testing.
This project would not exist without the tireless guidance, support, and encouragement of Prof. J. D. Joannopoulos of MIT. Thanks for letting me do things my way, John!
In the previous section, we focused on how to perform a calculation in MPB. Now, we'll give a brief tutorial on what you might do with the results of the calculations, and in particular how you might visualize the results. We'll focus on two systems, one two-dimensional and one three-dimensional.
First, we'll return to the two-dimensional triangular lattice of rods in
air from the tutorial. The control file for this calculation, which
can also be found in mpb-ctl/examples/tri-rods.ctl
, will
consist of:
(set! num-bands 8) (set! geometry-lattice (make lattice (size 1 1 no-size) (basis1 (/ (sqrt 3) 2) 0.5) (basis2 (/ (sqrt 3) 2) -0.5))) (set! geometry (list (make cylinder (center 0 0 0) (radius 0.2) (height infinity) (material (make dielectric (epsilon 12)))))) (set! k-points (list (vector3 0 0 0) ; Gamma (vector3 0 0.5 0) ; M (vector3 (/ -3) (/ 3) 0) ; K (vector3 0 0 0))) ; Gamma (set! k-points (interpolate 4 k-points)) (set! resolution 32) (run-tm (output-at-kpoint (vector3 (/ -3) (/ 3) 0) fix-efield-phase output-efield-z)) (run-te)
Notice that we're computing both TM and TE bands (where we expect a
gap in the TM bands), and are outputting the z component of the
electric field for the TM bands at the K point. (The
fix-efield-phase
will be explained below.)
Now, run the calculation, directing the output to a file, by entering the following command at the Unix prompt:
unix% mpb tri-rods.ctl >& tri-rods.out
It should finish after a minute or two.
In most cases, the first thing we'll want to do is to look at the
dielectric function, to make sure that we specified the correct
geometry. We can do this by looking at the epsilon.h5
output file.
The first thing that might come to mind would be to examine
epsilon.h5
directly, say by converting it to a PNG image with
h5topng
(from my free h5utils package),
magnifying it by 3:
unix% h5topng -S 3 epsilon.h5
The resulting image
(
epsilon.png
) is shown at right, and it initially seems
wrong! Why is the rod oval-shaped and not circular? Actually, the
dielectric function is correct, but the image is distorted because the
primitive cell of our lattice is a rhombus (with 60-degree acute
angles). Since the output grid of MPB is defined over the
non-orthogonal unit cell, while the image produced by
h5topng
(and most other plotting programs) is square, the
image is skewed.
We can fix the image in a variety of ways, but the best way is
probably to use the mpb-data
utility included (and
installed) with MPB. mpb-data
allows us to rearrange the
data into a rectangular cell (-r
) with the same
area/volume, expand the data to include multiple periods (-m
periods
), and change the resolution per unit distance in
each direction to a fixed value (-n resolution
).
man mpb-data
or run mpb-data -h
for more
options. In this case, we'll rectify the cell, expand it to three
periods in each direction, and fix the resolution to 32 pixels per
a:
unix% mpb-data -r -m 3 -n 32 epsilon.h5
It's important to use -n
when you use -r
,
as otherwise the non-square unit cell output by -r
will
have a different density of grid points in each direction, and appear
distorted. The output of mpb-data
is by default an
additional dataset within the input file, as we can see by running
h5ls
:
unix% h5ls epsilon.h5 data Dataset {32, 32} data-new Dataset {96, 83} description Dataset {SCALAR} lattice\ copies Dataset {3} lattice\ vectors Dataset {3, 3}
Here, the new dataset
output by
mpb-data
is the one called
data-new
. We can examine it by running
h5topng
again, this time explicitly specifying the name
of the dataset (and no longer magnifying):
unix% h5topng epsilon.h5:data-new
The new epsilon.png
output image is shown at right.
As you can see, the rods are now circular as desired, and they clearly
form a triangular lattice.
At this point, let's check for band gaps by picking out lines with
the word "Gap
" in them:
unix% grep Gap tri-rods.out Gap from band 1 (0.275065617068082) to band 2 (0.446289918847647), 47.4729292989213% Gap from band 3 (0.563582903703468) to band 4 (0.593059066215511), 5.0968516236891% Gap from band 4 (0.791161222813268) to band 5 (0.792042731370125), 0.111357548663006% Gap from band 5 (0.838730315053238) to band 6 (0.840305955160638), 0.187683867865441% Gap from band 6 (0.869285340346465) to band 7 (0.873496724070656), 0.483294361375001% Gap from band 4 (0.821658212109559) to band 5 (0.864454087942874), 5.07627823271133%
The first five gaps are for the TM bands (which we ran first), and the last gap is for the TE bands. Note, however that the < 1% gaps are probably false positives due to band crossings, as described in the user tutorial. There are no complete (overlapping TE/TM) gaps, and the largest gap is the 47% TM gap as expected. (To be absolutely sure of this and other band gaps, we would also check k-points within the interior of the Brillouin zone, but we'll omit that step here.)
Next, let's plot out the band structure. To do this, we'll first extract the TM and TE bands as comma-delimited text, which can then be imported and plotted in our favorite spreadsheet/plotting program.
unix% grep tmfreqs tri-rods.out > tri-rods.tm.dat unix% grep tefreqs tri-rods.out > tri-rods.te.dat
The TM and TE bands are both plotted below against the "k index" column of the data, with the special k-points labelled. TM bands are shown in blue (filled circles) with the gaps shaded light blue, while TE bands are shown in red (hollow circles) with the gaps shaded light red.
Note that we truncated the upper frequencies at a cutoff of 1.0 c/a. Although some of our bands go above that frequency, we didn't compute enough bands to fill in all of the states in that range. Besides, we only really care about the states around the gap(s), in most cases.
Now, let's actually examine the electric-field distributions for some of the bands (which were saved at the K point, remember). Besides looking neat, the field patterns will tell us about the characters of the modes and provide some hints regarding the origin of the band gap.
As before, we'll run mpb-data
on the field output
files (named e.k11.b*.z.tm.h5
), and then run
h5topng
to view the results:
unix% mpb-data -r -m 3 -n 32 e.k11.b*.z.tm.h5 unix% h5topng -C epsilon.h5:data-new -c bluered -Z -d z.r-new e.k11.b*.z.tm.h5
Here, we've used the -C
option to superimpose (crude)
black contours of the dielectric function over the fields, -c
bluered
to use a blue-white-red color table, -Z
to
center the color scale at zero (white), and -d
to specify
the dataset name for all of the files at once. man
h5topng
for more information. (There are plenty of
data-visualization programs available if you want more sophisticated
plotting capabilities than what h5topng
offers, of
course; you can use h5totxt
to convert the data to a
format suitable for import into e.g. spreadsheets.)
Note that the dataset name is z.r-new
, which is the
real part of the z component of the output of mpb-data
.
(Since these are TM fields, the z component is the only non-zero part
of the electric field.) The real and imaginary parts of the fields
correspond to what the fields look like at half-period intervals in
time, and in general they are different. However, at K they are
redundant, due to the inversion symmetry of that k-point (proof left
as an exercise for the reader). Usually, looking at the real parts
alone gives you a pretty good picture of the state, especially if you
use fix-efield-phase
(see below), which chooses the phase
to maximize the field energy in the real part. Sometimes, though, you
have to be careful: if the real part happens to be zero, what you'll
see is essentially numerical noise and you should switch to the
imaginary part.
The resulting field images are shown below:
TM band 1 | TM band 2 | TM band 3 | TM band 4 | TM band 5 | TM band 6 | TM band 7 | TM band 8 |
---|---|---|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Your images should look the same as the ones above. If we hadn't
included fix-efield-phase
before
output-efield-z
in the ctl file, on the other hand, yours
would have differed slightly (e.g. by a sign or a lattice shift),
because by default the phase is random.
When we look at the real parts of the fields, we are really looking
at the fields of the modes at a particular instant in time (and the
imaginary part is half a period later). The point in time (relative
to the periodic oscillation of the state) is determined by the phase
of the eigenstate. The fix-efield-phase
band function
picks a canonical phase for the eigenstate, giving us a deterministic
picture.
We can see several things from these plots:
First, the origin of the band gap is apparent. The lowest band is concentrated within the dielectric rods in order to minimize its frequency. The next bands, in order to be orthogonal, are forced to have a node within the rods, imposing a large "kinetic energy" (and/or "potential energy") cost and hence a gap. Successive bands have more and more complex nodal structures in order to maintain orthogonality. (The contrasting absence of a large TE gap has to do with boundary conditions. The perpendicular component of the displacement field must be continuous across the dielectric boundary, but the parallel component need not be.)
We can also see the deep impact of symmetry on the states. The K point has C3v symmetry (not quite the full C6v symmetry of the dielectric structure). This symmetry group has only one two-dimensional representation--that is what gives rise to the degenerate pairs of states (2/3, 4/5, and 7/8), all of which fall into this "p-like" category (where the states transform like two orthogonal dipole field patterns, essentially). The other two bands, 1 and 6, transform under the trivial "s-like" representation (with band 6 just a higher-order version of 1).
"Then were the entrances of this world made narrow, full of sorrow and travail: they are but few and evil, full of perils, and very painful." (Ezra 4:7)
Now, let us turn to a three-dimensional structure, a diamond lattice of dielectric spheres in air. The basic techniques to compute and analyze the modes of this structure are the same as in two dimensions, but of course, everything becomes more complicated in 3d. It's harder to find a structure with a complete gap, the modes are no longer polarized, the computations are far bigger, and visualization is much more difficult, for starters.
(The band gap of the diamond structure was first identified in: K. M. Ho, C. T. Chan, and C. M. Soukoulis, "Existence of a photonic gap in periodic dielectric structures," Phys. Rev. Lett. 65, 3152 (1990).)
The control file for this calculation, which can also be found in
mpb-ctl/examples/diamond.ctl
, consists of:
(set! geometry-lattice (make lattice (basis-size (sqrt 0.5) (sqrt 0.5) (sqrt 0.5)) (basis1 0 1 1) (basis2 1 0 1) (basis3 1 1 0))) ; Corners of the irreducible Brillouin zone for the fcc lattice, ; in a canonical order: (set! k-points (interpolate 4 (list (vector3 0 0.5 0.5) ; X (vector3 0 0.625 0.375) ; U (vector3 0 0.5 0) ; L (vector3 0 0 0) ; Gamma (vector3 0 0.5 0.5) ; X (vector3 0.25 0.75 0.5) ; W (vector3 0.375 0.75 0.375)))) ; K ; define a couple of parameters (which we can set from the command-line) (define-param eps 11.56) ; the dielectric constant of the spheres (define-param r 0.25) ; the radius of the spheres (define diel (make dielectric (epsilon eps))) ; A diamond lattice has two "atoms" per unit cell: (set! geometry (list (make sphere (center 0.125 0.125 0.125) (radius r) (material diel)) (make sphere (center -0.125 -0.125 -0.125) (radius r) (material diel)))) ; (A simple fcc lattice would have only one sphere/object at the origin.) (set-param! resolution 16) ; use a 16x16x16 grid (set-param! mesh-size 5) (set-param! num-bands 5) ; run calculation, outputting electric-field energy density at the U point: (run (output-at-kpoint (vector3 0 0.625 0.375) output-dpwr))
As before, run the calculation, directing the output to a file.
This will take a few minutes (2 minutes on our Pentium-II); we'll put
it in the background with nohup
so that it will finish
even if we log out:
unix% nohup mpb diamond.ctl >& diamond.out &
Note that, because we used define-param
and set-param!
to define/set some
variables (see the libctl
manual), we can change them from the command line. For example,
to use a radius of 0.3 and a resolution of 20, we can just type mpb
r=0.3 resolution=20 diamond.ctl
. This is an extremely useful feature,
because it allows you to use one generic control file for many
variations on the same structure.
As usual, all distances are in the "dimensionless" units determined by the length of the lattice vectors. We refer to these units as a, and frequencies are given in units of c/a. By default, the lattice/basis vectors are unit vectors, but in the case of fcc lattices this conflicts with the convention in the literature. In particular, the canonical a for fcc is the edge-length of a cubic supercell containing the lattice.
In order to follow this convention, we set the length of our basis
vectors appropriately using the basis-size
property of
geometry-lattice
. (The lattice vectors default to the
same length as the basis vectors.) If the cubic supercell edge has
unit length (a), then the fcc lattice vectors have length
sqrt(0.5), or (sqrt 0.5)
in Scheme.
The diamond lattice has a complete band gap:
unix% grep Gap diamond.out Gap from band 2 (0.396348703007373) to band 3 (0.440813418580596), 10.6227251392791%
We can also plot its band diagram, much as for the tri-rods case except that now we can't classify the bands by polarization.
unix% grep freqs diamond.out > diamond.dat
The resulting band diagram, with the complete band gap shaded yellow, is shown below. Note that we only computed 5 bands, so in reality the upper portion of the plot would contain a lot more bands (which are of less interest than the bands adjoining the gap).
Visualizing fields in a useful way for general three-dimensional
structures is fairly difficult, but we'll show you what we can with
the help of the free Vis5D
volumetric-visualization program, and the h5tov5d
conversion program from h5utils.
First, of course, we've got to rectangularize the unit cell using
mpb-data
, as before. We'll also expand it to two
periods in each direction.
unix% mpb-data -m 2 -r -n 32 epsilon.h5 dpwr.k06.b*.h5
Then, we'll use h5tov5d
to convert the resulting
datasets to Vis5D format, joining all the datasets into a single file
(diamond.v5d
) so that we can view them simultaneously if
we want to:
unix% h5tov5d -o diamond.v5d -d data-new epsilon.h5 dpwr.k06.b*.h5
Note that all of the datasets are named data-new
(from
the original datasets called data
) since we are looking
at scalar data (the time-averaged electric-field energy density). No
messy field components or real and imaginary parts this time; we have
enough to deal with already.
Now we can open the file with Vis5D and play around with various plots of the data:
unix% vis5d diamond.v5d &
If you stare at the dielectric function long enough from various angles, you can convince yourself that it is a diamond lattice:
The lowest two bands have their fields concentrated within the spheres as you might expect, flowing along more-or-less linear paths. The second band differs from the first mainly by the orientation of its field paths. The fields for the first band at U are depicted below, with the strongest fields (highest energy density) shown as the most opaque, blue pixels. Next to it is the same plot but with an isosurface at the boundary of the dielectric superimposed, so you can see that the energy is concentrated inside the dielectric.
The first band above the gap is band 3. Its field energy densities are depicted below in the same manner as above. The field patterns are considerably harder to make out than for the lower band, but they seem to be more diffuse and "clumpy," the latter likely indicating the expected field oscillations for orthogonality with the lower bands.
Here, we begin with a brief overview of what the program is computing, and then describe how the program and computation are broken up into different portions of the code.
Forgive the primitive math typography below; this will be rectified when MathML is supported in a decent browser.
This section provides a whirlwind tour of the mathematics of photonic band structure calculations and the algorithms that we employ. For more detailed information, see:
The MIT Photonic-Bands Package takes a periodic dielectric
structure and computes the eigenmodes of that structure,
which are the electromagnetic waves that can propagate through the
structure with a definite frequency. This corresponds to solving an
eigenvalue problem M h = (w/c)^2 h
, where h
is the magnetic field, w
is the frequency, and M is the
Maxwell operator curl 1/epsilon curl
. We also have an
additional constraint, that div h
be zero (the magnetic
field must be "transverse").
Since the structure is periodic, we can also invoke Bloch's theorem
to write the states in the form exp(i k*x)
times a
periodic function, where k
is the Bloch wavevector. So,
at each k-point (Bloch wavevector), we need to solve for a discrete
set of eigenstates, the photonic bands of the structure.
To solve for the eigenstates on a computer, we must expand the
magnetic field in some basis, where we truncate the basis to some
finite number of points to discretize the problem. For example, we
could use a traditional finite-element basis in which the field is
taken on a finite number of mesh points and linearly interpolated in
between. However, it is expensive to enforce the transversality
constraint in this basis. Instead, we use a Fourier (spectral) basis,
expanding the periodic part of the field in terms of exp(i
G*x)
planewaves. In this basis, the transversality constraint
is easy to maintain, as it merely implies that the planewave
amplitudes must be orthogonal to k + G
.
In order to find the eigenfunctions, we could compute the elements
of M
explicitly in our basis, and then call LAPACK or
some similar code to find the eigenvectors and eigenvalues. For a
three-dimensional calculation, this could mean finding the
eigenvectors of a matrix with hundreds of thousands of elements on a
side--daunting merely to store, much less compute. Fortunately, we
only want to know a few eigenvectors, not hundreds of thousands, so we
can use much less expensive iterative methods that don't
require us to store M
explicitly.
Iterative eigensolvers require only that one supply a routine to
operate M
on a vector (function). Starting with an
initial guess for the eigenvector, they then converge quickly to the
actual eigenvector, stopping when the desired tolerance is achieved.
There are many iterative eigensolver methods; we use a preconditioned
block minimization of the Rayleigh quotient which is further described
in the file src/matrices/eigensolver.c
. In the Fourier
basis, applying M
to a function is relatively easy: the
curls become cross products with i (k + G)
; the
multiplication by 1/epsilon
is performed by using an FFT
to transform to the spatial domain, multiplying, and then transforming
back with an inverse FFT. For more information and references on
iterative eigensolvers, see the paper cited above.
We also support a "targeted" eigensolver. A typical iterative
eigensolver finds the p
lowest eigenvalues and
eigenvectors. Instead, we can find the p
eigenvalues
closest to a given frequency w0
by solving for the
eigenvalues of (M - (w0/c)^2)^2
instead of
M
. This new operator has the same eigenvectors as
M
, but its eigenvalues have been shifted to make those
closest to w0
the smallest.
The eigensolver we use is preconditioned, which means that convergence
can be greatly improved by suppling a good preconditioner matrix.
Finding a good preconditioner involves making an approximate inverse
of M
, and is something of a black art with lots of trial
and error.
The initialization of the dielectric function deserves some additional discussion, both because it is crucial for good convergence, and because we use somewhat complicated algorithms for performance reasons.
To ameliorate the convergence problems caused in a planewave basis by a discontinuous dielectric function, the dielectric function is smoothed (averaged) at the resolution of the grid. Another way of thinking about it is that this brings the average dielectric constant (over the grid) closer to its true value. Since different polarizations of the field prefer different averaging methods, one has to construct an effective dielectric tensor at the boundaries between dielectrics, as described by the paper referenced above.
This averaging has two components. First, at each grid point the
dielectric constant (epsilon) and its inverse are averaged over a
uniform mesh extending halfway to the neighboring grid points. (The
mesh resolution is controlled by the mesh-size
user input
variable.) Second, for grid points on the boundary between two
dielectrics, we compute the vector normal to the dielectric interface;
this is done by averaging the "dipole moment" of the dielectric
function over a spherically-symmetric distribution of points. The
normal vector and the two averages of epsilon are then combined into
an effective dielectric tensor for the grid point.
All of this averaging is handled by a subroutine in
src/maxwell/
(see below) that takes as input a function
epsilon(r), which returns the dielectric constant for a given
position r. This epsilon function must be as efficient as
possible, because it is evaluated a large number of times: the size of
the grid multiplied by mesh-size
3 (in three
dimensions).
To specify the geometry, the user provides a list of geometric objects (blocks, spheres, cylinders and so on). These are parsed into an efficient data structure and are used to to provide the epsilon function described above. (All of this is handled by the libctlgeom component of libctl, described below.) At the heart of the epsilon function is a routine to return the geometric object enclosing a given point, taking into account the fact that the objects are periodic in the lattice vectors. Our first algorithm for doing this was a simple linear search through the list of objects and their translations by the lattice vectors, but this proved to be too slow, especially in supercell calculations where there are many objects. We addressed the performance problem in two ways. First, for each object we construct a bounding box, with which point inclusion can be tested rapidly. Second, we build a hierarchical tree of bounding boxes, recursively partitioning the set of objects in the cell. This allows us to search for the object containing a point in a time logarithmic in the number of objects (instead of linear as before).
The code is organized to keep the core computation independent of
the user interface, and to keep the eigensolver routines independent
of the operator they are computing the eigenvector of. The
computational code is located in the src/
directory, with
a few major subdirectories, described below. The Guile-based user
interface is completely contained within the mpb-ctl/
directory.
This directory contains the eigensolver, in
eigensolver.c
, to which you pass an operator and it
returns the eigenvectors. Eigenvectors are stored using the
evectmatrix
data structure, which holds p
eigenvectors of length n
, potentially distributed over
n
in MPI. See src/matrices/README
for more
information about the data structures. In particular, you should use
the supplied functions (create_evectmatrix
, etcetera) to
create and manipulate the data structures, where possible.
The type of the eigenvector elements is determined by
scalar.h
, which sets whether they are real or complex and
single or double precision. This is, in turn, controlled by the
--disable-complex
and --enable-single
parameters to the configure
script at install-time.
scalar.h
contains macros to make it easier to support
both real and complex numbers elsewhere in the code.
Also in this directory is blasglue.c
, a set of wrapper
routines to make it convienient to call BLAS and LAPACK routines from
C instead of Fortran.
As its name implies, this is simply a number of utility routines
for use elsewhere in the code. Of particular note is
check.h
, which defines a CHECK(condition,
error-message)
macro that is used extensively in the
code to improve robustness. There are also debugging versions of
malloc/free (which perform lots of paranoia tests, enabled by
--enable-debug-malloc
in configure
), and MPI
glue routines that allow the program to operate without the MPI
libraries.
This section contains code to abstract I/O for eigenvectors and similar matrices, providing a simpler layer on top of the HDF5 interface. This could be modified to support HDF4 or other I/O formats.
The maxwell/
directory contains all knowledge of
Maxwell's equations used by the program. It implements functions to
apply the Maxwell operator to a vector (in maxwell_op.c
)
and compute a good preconditioner (in maxwell_pre.c
).
These functions operate upon a representation of the fields in a
transverse Fourier basis.
In order to use these functions, one must first initialize a
maxwell_data
structure with
create_maxwell_data
(defined in maxwell.c
)
and specify a k point with update_maxwell_data_k
. One
must also initialize the dielectric function using
set_maxwell_dielectric
by supplying a function that
returns the dielectric constant for any given coordinate. You can
also restrict yourself to TE or TM polarizations in two dimensions by
calling set_maxwell_data_polarization
.
This directory also contains functions
maxwell_compute_dfield
, etcetera, to compute the
position-space fields from the Fourier-transform representation
returned by the eigensolver.
Here is the Guile-based user interface code for the eigensolver.
Instead of using Guile directly, this code is built on top of the
libctl
library as described in previous sections. This
means that the user-interface code (in mpb.c
) is fairly
short, consisting of a number of small functions that are callable by
the user from Guile.
The core of the user interface is the file mpb.scm
,
the specifications file for libctl as described in the libctl manual.
Actually, mpb.scm
is generated by configure
from mpb.scm.in
(in order to substitute in parameters
like the location of the libctl library); you should only edit
mpb.scm.in
directly. (You can regenerate
mpb.scm
simply by running ./config.status
instead of re-running configure
.)
The specifications file defines the data structures and subroutines
that are visible to the Guile user. It also defines a number of
Scheme subroutines for the user to call directly, like
(run)
. It is often simpler and more flexible to define
functions like this in Scheme rather than in C.
All of the code to handle the geometric objects resides in
libctlgeom, a set of Scheme and C utility functions included with
libctl (see the file utils/README
in the libctl package).
(These functions could also be useful in other programs, such as a
time-domain Maxwell's equation simulator.)