L-Systems

Koch Snowflake — L-System

A fractal snowflake grown from a single line segment using string rewriting rules.

The Axiom

An L-system is a formal grammar — a set of rules for rewriting strings. At the heart of every L-system is a starting string called the axiom.

For the Koch snowflake, we begin with a triangle:

Axiom: F--F--F

Each F means “draw a line segment forward.” The -- means “turn right 120°.” Three sides, three 120° turns — a triangle.

Stage 0 is the raw axiom rendered: an equilateral triangle. Nothing fractal about it yet.

First Iteration

We apply the production rule once:

F → F+F--F+F

Every F in the axiom is replaced by F+F--F+F. The + means “turn left 60°.”

After substitution:

F+F--F+F -- F+F--F+F -- F+F--F+F

Each edge of the original triangle has sprouted a triangular bump. The perimeter has grown — each straight side is now four segments — but the overall shape is still recognisable as a triangle.

Second Iteration

Applying the rule again replaces every F in the stage-1 string:

Now each of the four segments from stage 1 has itself grown a bump. The snowflake silhouette starts to emerge. Notice that the perimeter length has grown by a factor of 4/3 again — but the area enclosed has grown only slightly.

Third Iteration and the Fractal Limit

At three iterations the Koch snowflake is clearly recognisable. The characteristic jagged, self-similar boundary repeats at every scale.

Mathematically this recursion continues to infinity. The Koch snowflake has an infinite perimeter — each iteration multiplies the perimeter by 4/3, and (4/3)ⁿ → ∞ as n → ∞. Yet the area it encloses converges to a finite value: exactly 8/5 of the original triangle’s area.

This is the paradox at the heart of fractal geometry: a finite region bounded by a curve of infinite length.

def l_system(axiom, rules, n):
    s = axiom
    for _ in range(n):
        s = "".join(rules.get(c, c) for c in s)
    return s

axiom = "F--F--F"
rules = {"F": "F+F--F+F"}

for i in range(4):
    result = l_system(axiom, rules, i)
    print(f"n={i}: {len(result)} characters")

Output:

n=0: 7 characters
n=1: 31 characters
n=2: 127 characters
n=3: 511 characters

Each iteration quadruples the string length (minus the constant -- connectors). By n=6 you have over 16,000 drawing commands — which is why we stop at 3 or 4 iterations for anything you’d want to render on screen.