Ollin

Dielectric breakdown

Dielectric breakdown grows lightning. The model holds the discharge at one voltage and the surroundings at another, then solves the electric field between them over a lattice. Every empty cell touching the discharge is a candidate. One candidate grows per step, with a probability proportional to the local field raised to eta. That one exponent picks the regime. A low eta grows a furry, even bush. Near 2 you get the sparse, jagged branches of lightning, and the Lichtenberg figures burned into wood and acrylic.

Two panels: left, a young lattice discharge inside a violet wash of its solved field, its frontier dotted in orange with the dots large at the tips and small in the crevices; right, a sparse jagged discharge with its main channels drawn thick

DielectricBreakdown is a stateful stepper you hold on to, like the other growth models. step() adds one site, step(_:) adds a batch per frame, and grow() runs until the arc connects. The model is seeded, so the same seed grows the same figure.

let bolt = DielectricBreakdown(seeds: [center], in: bounds, seed: 7)

override func draw() {
    bolt.step(6)
    background(.black)
    stroke(.white)
    for (a, b) in bolt.segments { drawLine(a, b) }
}

Contents

Building a discharge

DielectricBreakdown(seeds: [Vector2], in bounds: Rectangle, resolution: Int = 140,
                    eta: Double = 1.7, ground: Ground = .border,
                    maxSites: Int = 3000, seed: Int = 0)

seeds are the points where the discharge starts, snapped to lattice cells. One center point grows a radial Lichtenberg figure, and a row along the top grows lightning that reaches down. ground is the far electrode the discharge grows toward. .border puts ground on the rim of bounds, which gives the classic radial figure. .points(_:) instead holds specific cells at full potential, such as a wire, a plate, or one far point, and the rim then insulates. resolution is the lattice's cell count across the width, so a finer lattice gives finer filaments and costs more per step. eta is the character parameter. At 1 you get the aggregation regime, around 2 the lightning regime, and higher values approach a single channel.

Growing it

step() does the solving for you. Each step relaxes the field a little, weighs the candidates by field^eta, and grows one of them. step(_:) grows a batch per frame, and a handful per frame looks like the discharge feeling its way. grow() runs until the figure arcs to ground, fills its room, or hits maxSites, and isFinished says which end you reached. Sites arrive in growth order and carry a parent link.

Drawing it

bolt.positions                                  // [Vector2], for drawCircles
bolt.segments                                   // [(Vector2, Vector2)], site to parent
bolt.branches()                                 // [Contour], whole channels, tip to fork
bolt.thicknesses(tipWidth: 1.2, exponent: 2.0)  // [Double], indexed like sites

segments gives you the plain skeleton. branches() returns whole channels as polylines. Each channel runs from a seed or a fork out to a tip, and every edge lands in exactly one channel. A channel strokes as one continuous filament, and it takes smoothed(iterations:) well for the plotter path. thicknesses(tipWidth:exponent:) follows the pipe model. Tips carry tipWidth, and channels thicken toward the seed, the way a real discharge brightens its main channel. The Patterns/Lichtenberg example draws those widths under an additive halo.

Reading the field

bolt.potential(at: point) -> Double   // 0 on the discharge, 1 at ground

The solved field is worth drawing on its own. Sample it over a grid for a glow that follows the figure. Feed it to isolines for equipotential contour lines, or use it to drive color. A candidate's growth weight is exactly this value raised to eta. A picture of the field is therefore also a picture of where the discharge will strike next.


Related: DiffusionLimitedAggregation builds the same kind of figure from random walkers, and eta: 1 grows the same family of shapes. See also SpaceColonization, which grows toward targets you declare rather than a solved field, and CrackGrowth, which grows by collision.

↑↓ move ↩ open