RCWA in Julia, and some thoughts on abstraction

I wrote an RCWA solver in Julia. It should be available on my GitHub under CasualRCWA.jl. It comes with documentation introducing RCWA, starting from the wave equations, developing the transfer-matrix method for layered media, and naturally leading to the S-matrix approach for RCWA. You can find the introduction in the associated GitHub Pages.

Now the point of this post isn’t to repeat this introduction. But for the sake of staying self-contained I’ll dedicate a single paragraph to it:

In brief, RCWA, or rigorous coupled-wave analysis, is a semi-analytical method for solving Maxwell’s equations. It considers structures that are periodic in the xy-plane and layered in the z-direction. Basically, you solve the Maxwell equations in the frequency domain in each layer, and then you simply connect up the eigenmodes in adjacent layers by enforcing the boundary condition demanding continuity of the $E$- and $H$-fields.

What I want to dwell on in the rest of the post is my motivation for writing this solver.

When convenience becomes constraint

I work at ASML. ASML makes photolithography machines. In other words, they make the machines that uses light to transfer patterns onto computer chips. Not surprisingly, people occasionally do optics simulations, and we need software for that. Now there are many companies dedicated to professional optics simulations, but we also have plenty of in-house optical simulation software.

One such piece of software is an RCWA solver that’s mainly intended for understanding the behaviour of light in computer chips. RCWA is a sensible choice because computer chips are made up of layers.

The original solver was fine, by engineering standards. It was a collection of (largely unstructured but occasionally documented) MATLAB scripts, which took some cognitive effort to disentangle — but once you understood its internals, it was entirely possible to describe a geometry, run the math, and obtain fields and diffraction efficiencies.

The solver eventually got promoted to the status of commercial software. It presumably got ported to C++ (to the best of my knowledge at least — the source code is not available to me). It received a GUI. It received some abstactions, and then some more, intended to ‘simplify’ the workflow and taylor it to the average semiconductor engineer.

The idea was well-intentioned. Instead of defining electromagnetic structures directly (scary!) you describe the semiconductor process steps. You start with a silicon wafer. You grow some oxide. You coat with nitride. You expose the active areas. Etch away the nitride. Oxidise. Resist and nitride get removed. Grow a thin oxide. Deposit some poly. Expose and etch again. CVD some more oxide. Sputter metal on top. And lo and behold, you got yourself an optical stack, through the mere clicking of some buttons.

Now let’s say my etch wasn’t quite as anisotropic as I wanted it to be. Then we implement a button that allows you to specify an oblique side-wall angle for the etched pattern. Perhaps I over-etched a bit. No worries, there’s another button for that. And what if my planarisation slightly tilted the top surface? Got you covered: the upcoming version will support that, too!

The problem that arises is that every geometry needs to be generated in some way or other by means of the finite set of processing steps that the GUI provides. And this is where things become painful. Let's say that I didn’t etch under a fixed oblique angle. Instead the side walls of my metal lines are a bit curved. Now what do I do? The simple answer was that it’s borderline impossible to realise moderately complex geometries with the provided interface.

By elevating a specific workflow to the primary interface, the software constrained what could be expressed. As long as your structure matched the assumptions encoded into the GUI, everything felt easy. The moment you wanted something slightly unconventional — a profile that did not correspond to a predefined sequence of deposition and etching operations — you hit a wall. The solver was theoretically general, but practically rigid.

Geometry in RCWA

RCWA is not restrictive. Any sufficiently complex grating profile can be represented by slicing the structure into uniform layers along the propagation direction. The mathematics does not care whether a shape arose from a cleanroom process, a parametric function, or a completely artificial construction. It only cares about the geometry.

Scientific software should follow a simple rule: every abstraction must compile down to a transparent, minimal representation that users can access directly. There must always be a lowest layer where the user can specify the raw inputs without interpretation or opinion from the software. In an RCWA solver, that means direct and total control over the permittivity distribution or its Fourier representation, independent of any higher-level workflow.

When writing CasualRCWA.jlNote 1, I tried to make the API as simple as I possibly could. To illustrate, here’s the code needed to simulate a 1D grating in silicon at the time of this writing.

using CasualRCWA

# Define incoming light
λ      = 0.6        # wavelength in µm
source = Source(λ)  # default normal incidence

# Refractive indices
n_air = 1.0
n_si  = 3.9 - 0.027im

# Define layers
air     = Layer(n_air)
grating = Layer([n_air n_si n_si n_air])
si      = Layer(n_si)

# Define layer stack
stack = Stack(
    [air, grating, si],  # layers
    [Inf, 0.5, Inf],     # thicknesses in µm
    1                    # period in µm
)

# Simulate up to 5th diffraction order
order = 5

# Set up simulation input
input = RCWAInput(source, stack, order)

# Perform simulaton
output = RCWA(input)

The output — of type RCWAOutput — then contains all the information necessary to understand the diffraction behaviour of this grating.

As can be seen from this example, geometry is specified layer by layer. Each layer is specified by a complex-valued matrix. Each entry in this matrix corresponds to the complex refractive index of an individual pixel. The resolution of the matrix is irrelevant. That’s why we only need four pixels to specify the grating layer. (In fact, if you don’t care about phases, two pixels would suffice, as Layer([n_si n_air]) is geometrically equivalent.)

It should hopefully be clear that any geometry can be encoded in this way. Now I don’t mean to imply that it’ll be easy — it takes some bookkeeping headache to accurately slice up a sufficiently complex 3D profile into layers — but at least it can be done. And who knows — maybe one day I’ll set up a GUI to help my process engineer colleagues with this tedious task! But such a GUI should sit on top of an accessible core, not replace it. The correct architecture is layered downward: convenience features map onto a general API, and that API maps onto the solver. Not the other way around.

Footnotes

  1. It carried the name RCWAForRetards.jl while using it to further my own understanding, as I felt too retarded to use existing RCWA software. This friendlier name was a suggestion of Copilot.