Skip to content

geomotif.motifs.fractals

Fractals: the same shape at every scale, reached three different ways.

Three constructions share this page because they produce the same kind of object by very different means, and the difference is worth seeing:

  • Grammars. Koch, Hilbert, Gosper, the dragons and the Sierpinski curves are each an axiom, a rewrite rule and a turn angle, drawn with a turtle -- see :class:~geomotif.LSystemMotif. Every one of them below is four lines.
  • Recursion. :class:SierpinskiCarpet, :class:CantorSet, :class:PythagorasTree, :class:HTree and :class:ApollonianGasket are shapes that place smaller copies of themselves, which is a statement about squares and circles rather than about a path, so they are built directly.
  • Chance. :class:IFSAttractor and :class:BarnsleyFern play the chaos game: pick one contracting map at random, apply it, plot the point, repeat. The attractor appears without ever being drawn, and comes back as loose points rather than as a stroke.

A grammar fractal is sized by its :attr:~geomotif.LSystemMotif.step, which is the length of one turtle move -- so its overall size is that step times a power of the grammar's scale factor, and it changes when you change the depth. That is the nature of the construction rather than an oversight; call :meth:~geomotif.Design.fit when you need a fractal at a particular size. The recursive and chaos-game motifs are not built a step at a time and do take a plain size.

Classes:

Name Description
KochCurve

The original: replace the middle third of every segment with a spike.

KochSnowflake

Three Koch curves around a triangle: finite area, infinite perimeter.

KochAntisnowflake

The snowflake with its spikes turned inward, which is a different shape.

MinkowskiSausage

Koch's idea on a square grid: a battlement instead of a spike.

MinkowskiIsland

Four Minkowski sausages around a square: the quadratic Koch island.

SierpinskiTriangle

The gasket, drawn as one unbroken stroke that closes on itself.

SierpinskiArrowhead

The gasket approached along a curve instead of through a triangle.

DragonCurve

The Heighway dragon: fold a strip of paper in half, repeatedly, unfold.

TwinDragon

Two Heighway dragons back to back, enclosing a region that tiles.

Terdragon

The dragon done in thirds: one segment becomes three at 120 degrees.

LevyCCurve

Paul Levy's C curve: two half-size copies at right angles, forever.

HilbertCurve

The space-filling curve that keeps neighbours together.

MooreCurve

Hilbert's curve closed into a loop.

PeanoCurve

The first space-filling curve ever published, from 1890.

GosperCurve

The flowsnake: a space-filling curve on a hexagonal grid.

VicsekFractal

The box fractal: a plus sign made of plus signs.

SierpinskiCarpet

A square with its middle ninth removed, then again in each ninth.

CantorSet

Take out the middle third. Repeat. Stack the rounds to see it happen.

PythagorasTree

Squares on the legs of right triangles, all the way up.

HTree

An H whose serifs are smaller H's, and so on down.

ApollonianGasket

Circles packed into circles, filling every gap they leave.

IFSMap

One contracting map of an iterated function system, and how often to pick it.

IFSAttractor

The chaos game: pick a map at random, apply it, plot the point, repeat.

BarnsleyFern

Four affine maps and a coin, producing a fern.

KochCurve dataclass

KochCurve(*, depth: int = 4, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The original: replace the middle third of every segment with a spike.

Helge von Koch's 1904 curve, published as an example of something continuous that has a tangent nowhere. Each round quadruples the segment count while thirding the length, so the curve between two fixed points grows without limit -- which is the whole point of it.

KochSnowflake dataclass

KochSnowflake(*, depth: int = 4, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

Three Koch curves around a triangle: finite area, infinite perimeter.

The perimeter multiplies by 4/3 every round and never stops; the area converges to exactly 8/5 of the starting triangle. Both facts are easy to check on the geometry this builds, and the test suite does.

KochAntisnowflake dataclass

KochAntisnowflake(*, depth: int = 4, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The snowflake with its spikes turned inward, which is a different shape.

One sign change in the rule, and the star becomes three bites taken out of a triangle. Worth having next to :class:KochSnowflake precisely because the grammars differ by so little and the results by so much.

MinkowskiSausage dataclass

MinkowskiSausage(*, depth: int = 3, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

Koch's idea on a square grid: a battlement instead of a spike.

Every segment becomes eight of a quarter the length, all of them axis aligned, which makes it the fractal of choice when the output has to look deliberate rather than organic. Eight pieces at a quarter scale puts its dimension at exactly three halves.

MinkowskiIsland dataclass

MinkowskiIsland(*, depth: int = 3, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

Four Minkowski sausages around a square: the quadratic Koch island.

What :class:KochSnowflake is to :class:KochCurve. The coastline never stops growing while the area it encloses stays exactly that of the starting square, which is the tidiest statement of the coastline paradox there is.

SierpinskiTriangle dataclass

SierpinskiTriangle(*, depth: int = 5, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The gasket, drawn as one unbroken stroke that closes on itself.

The triangle with its middle removed, then again, forever. Drawing it without lifting the pen is the trick the grammar performs: the naive construction is a pile of separate triangles, while this is a single closed path that happens to trace all of them.

The same set of points falls out of :class:IFSAttractor played as a chaos game -- one arrives as a stroke, the other as a cloud.

SierpinskiArrowhead dataclass

SierpinskiArrowhead(*, depth: int = 6, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The gasket approached along a curve instead of through a triangle.

Two rules that swap roles at every round, tracing an open path from one corner of the gasket to another. The limit is the same set as :class:SierpinskiTriangle, reached by a route that looks nothing like it at any finite depth.

DragonCurve dataclass

DragonCurve(*, depth: int = 10, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The Heighway dragon: fold a strip of paper in half, repeatedly, unfold.

Every crease is a right angle and the curve never crosses itself, which is far from obvious at any depth past about six. Four dragons fit together around a point with no gap, so the shape tiles the plane.

TwinDragon dataclass

TwinDragon(*, depth: int = 9, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

Two Heighway dragons back to back, enclosing a region that tiles.

The Davis-Knuth dragon. Joining the pair closes the curve, and the region inside it is the fundamental domain of the base -1+i number system -- the reason this shape turns up in radix arithmetic as well as in art.

Terdragon dataclass

Terdragon(*, depth: int = 6, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The dragon done in thirds: one segment becomes three at 120 degrees.

Threefold rather than twofold symmetry, and a much lacier result than the Heighway curve for the same number of segments.

LevyCCurve dataclass

LevyCCurve(*, depth: int = 10, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

Paul Levy's C curve: two half-size copies at right angles, forever.

It starts as a simple bracket and turns into a cauliflower. Unlike the dragons it overlaps itself freely, which is what gives the finished curve its dense, ruffled edge.

HilbertCurve dataclass

HilbertCurve(*, depth: int = 5, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The space-filling curve that keeps neighbours together.

Hilbert's curve visits every cell of a square grid, and two cells close along the curve are always close on the plane. That property is why it is the standard order for laying out image tiles, database keys and anything else where locality has to survive being flattened to one dimension.

X and Y drive the rewriting without ever drawing: the turtle only moves on F.

MooreCurve dataclass

MooreCurve(*, depth: int = 4, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

Hilbert's curve closed into a loop.

Four Hilbert curves arranged so the walk returns to where it began, which makes it the space-filling curve to use when the traversal has to be cyclic rather than to start and stop somewhere.

PeanoCurve dataclass

PeanoCurve(*, depth: int = 3, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The first space-filling curve ever published, from 1890.

Giuseppe Peano's construction predates Hilbert's by a year and divides the square into nine rather than four. It reads as a comb of combs, and it was the result that forced mathematics to take the idea of dimension seriously.

GosperCurve dataclass

GosperCurve(*, depth: int = 4, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The flowsnake: a space-filling curve on a hexagonal grid.

Bill Gosper's curve fills a region whose own boundary is fractal -- the Gosper island, which tiles the plane in sevens. The most beautiful thing in this module, and it is two rewrite rules.

VicsekFractal dataclass

VicsekFractal(*, depth: int = 3, step: float = 1.0, start_angle: float = 0.0)

Bases: LSystemMotif

The box fractal: a plus sign made of plus signs.

Tamas Vicsek's construction, which keeps only the center and the four edge-midpoints of each subdivided square. The saltire cross the outline traces is what stochastic versions of it are used to model -- percolation clusters and diffusion fronts.

SierpinskiCarpet dataclass

SierpinskiCarpet(depth: int = 4, size: float = 200.0, center: Point = (0.0, 0.0))

Bases: PolygonMotif

A square with its middle ninth removed, then again in each ninth.

The two-dimensional Cantor set, and the shape whose limit contains a copy of every possible one-dimensional curve. Drawn as outlines: the outer square plus every hole cut out of it, which is both what a plotter wants and what makes the construction legible.

Parameters:

Name Type Description Default
depth int

Rounds of subdivision. The hole count is (8**depth - 1) / 7, so it climbs fast.

4
size float

Side of the outer square.

200.0
center (float, float)

Middle of the carpet.

(0.0, 0.0)

Methods:

Name Description
hole_count

Return how many holes this carpet has at its depth.

hole_count

hole_count() -> int

Return how many holes this carpet has at its depth.

Source code in src/geomotif/motifs/fractals.py
def hole_count(self) -> int:
    """Return how many holes this carpet has at its depth."""
    return (_power(8, self.depth) - 1) // 7

CantorSet dataclass

CantorSet(depth: int = 6, width: float = 240.0, gap: float = 14.0, center: Point = (0.0, 0.0))

Bases: PolygonMotif

Take out the middle third. Repeat. Stack the rounds to see it happen.

The set that survives is uncountable and has length zero, which is the single most useful counterexample in analysis. Drawn the way it is always drawn: one row of bars per round, so the construction is visible rather than just its limit.

Parameters:

Name Type Description Default
depth int

Rounds to draw. Round zero is the single unbroken bar, so depth rows follow it and the bar count is 2**(depth + 1) - 1.

6
width float

Length of the first bar.

240.0
gap float

Vertical distance between consecutive rows.

14.0
center (float, float)

Middle of the whole stack.

(0.0, 0.0)

Methods:

Name Description
bar_count

Return how many bars this stack draws in total.

bar_count

bar_count() -> int

Return how many bars this stack draws in total.

Source code in src/geomotif/motifs/fractals.py
def bar_count(self) -> int:
    """Return how many bars this stack draws in total."""
    return _power(2, self.depth + 1) - 1

PythagorasTree dataclass

PythagorasTree(depth: int = 8, size: float = 60.0, lean: float = pi / 4.0, base: Point = (0.0, 0.0))

Bases: PolygonMotif

Squares on the legs of right triangles, all the way up.

Albert Bosman's 1942 construction, and a picture of the Pythagorean theorem rather than an illustration of it: the two child squares have the combined area of their parent at every level, whatever the lean, so the tree's total area grows by exactly one trunk per level.

lean is the angle at the left corner of the triangle sitting on each square. A quarter of a right angle makes the symmetric tree; anything else tips it, and values near zero or a right angle draw a fern-like frond.

Parameters:

Name Type Description Default
depth int

Branching rounds. The square count is 2**(depth + 1) - 1.

8
size float

Side of the trunk square.

60.0
lean float

Apex angle of the triangle, in radians. Must be strictly between zero and a right angle.

pi / 4.0
base (float, float)

Midpoint of the trunk's bottom edge -- where the tree stands.

(0.0, 0.0)

Methods:

Name Description
square_count

Return how many squares this tree draws in total.

square_count

square_count() -> int

Return how many squares this tree draws in total.

Source code in src/geomotif/motifs/fractals.py
def square_count(self) -> int:
    """Return how many squares this tree draws in total."""
    return _power(2, self.depth + 1) - 1

HTree dataclass

HTree(depth: int = 6, size: float = 220.0, center: Point = (0.0, 0.0))

Bases: PolygonMotif

An H whose serifs are smaller H's, and so on down.

Each round adds a perpendicular segment at both ends of every existing one, shortened by the square root of two so the arms of successive levels stay in proportion. It is the standard layout for a clock distribution network on a chip, because every leaf is exactly the same wire length from the root.

Parameters:

Name Type Description Default
depth int

Branching rounds. The segment count is 2**(depth + 1) - 1.

6
size float

Length of the first, longest bar.

220.0
center (float, float)

Middle of the tree, which is the middle of that first bar.

(0.0, 0.0)

Methods:

Name Description
segment_count

Return how many bars this tree draws in total.

segment_count

segment_count() -> int

Return how many bars this tree draws in total.

Source code in src/geomotif/motifs/fractals.py
def segment_count(self) -> int:
    """Return how many bars this tree draws in total."""
    return _power(2, self.depth + 1) - 1

ApollonianGasket dataclass

ApollonianGasket(depth: int = 5, radius: float = 150.0, min_radius: float = 0.004, center: Point = (0.0, 0.0))

Bases: Motif

Circles packed into circles, filling every gap they leave.

Start with three circles that touch, drop the largest circle that fits in the gap between them, and repeat on the three new gaps. Apollonius of Perga posed the underlying problem; Descartes' circle theorem solves it in one line, which is what this uses.

The default packing is the integral gasket (-1, 2, 2, 3): every circle in it has an integer curvature, which is a fact about this particular starting configuration rather than about gaskets in general.

Parameters:

Name Type Description Default
depth int

Rounds of gap-filling. Each round triples the number of gaps.

5
radius float

Radius of the outer circle.

150.0
min_radius float

Stop subdividing once a circle would be smaller than this fraction of the outer radius. Necessary as well as kind: the packing is infinite, and depth alone does not bound how small it gets.

0.004
center (float, float)

Middle of the outer circle.

(0.0, 0.0)

Methods:

Name Description
circles

Return every circle in the packing as a (center, radius) pair.

circles

circles() -> tuple[tuple[Point, float], ...]

Return every circle in the packing as a (center, radius) pair.

Source code in src/geomotif/motifs/fractals.py
def circles(self) -> tuple[tuple[Point, float], ...]:
    """Return every circle in the packing as a ``(center, radius)`` pair."""
    found = list(_GASKET_SEED)
    for excluded in range(len(_GASKET_SEED)):
        triple = tuple(c for i, c in enumerate(_GASKET_SEED) if i != excluded)
        self._fill(triple, _GASKET_SEED[excluded], self.depth, found)

    cx, cy = self.center
    out: list[tuple[Point, float]] = []
    for curvature, product in found:
        unit_radius = 1.0 / abs(curvature)
        middle = product / curvature
        out.append(
            (
                (cx + middle.real * self.radius, cy + middle.imag * self.radius),
                unit_radius * self.radius,
            )
        )
    return tuple(out)

IFSMap dataclass

IFSMap(transform: Affine, weight: float = 1.0)

One contracting map of an iterated function system, and how often to pick it.

Parameters:

Name Type Description Default
transform Affine

The map itself. It must contract -- shrink distances -- or the chaos game runs away instead of settling onto an attractor.

required
weight float

Relative probability of choosing this map. Weights are normalized, so only their ratios matter. Setting it near each map's area scale factor is what keeps the resulting cloud evenly dense.

1.0

IFSAttractor dataclass

IFSAttractor(maps: tuple[IFSMap, ...] = _GASKET_MAPS, count: int = 20000, size: float = 240.0, center: Point = (0.0, 0.0), *, seed: int = 0)

Bases: Motif

The chaos game: pick a map at random, apply it, plot the point, repeat.

Michael Barnsley's construction, and the most surprising thing in this module. A handful of contracting affine maps have exactly one compact set they leave unchanged, and iterating them at random converges onto it from any starting point whatsoever -- so the attractor draws itself without anyone ever computing where it is.

The default maps halve towards the corners of a triangle and produce the Sierpinski gasket, which is the same set :class:SierpinskiTriangle traces as a single stroke. Comparing the two is the cheapest way to see what a fractal actually is, as opposed to how one is drawn.

Produces loose points rather than a stroke: the order they arrive in is random, so joining them would draw noise.

Parameters:

Name Type Description Default
maps tuple of IFSMap

The system to iterate. Each map should contract; the weights are relative and get normalized.

_GASKET_MAPS
count int

Points to plot. More fills the attractor in more finely, and costs linearly.

20000
size float

Largest extent of the finished cloud. The attractor is measured and rescaled, since its own coordinates depend on the maps.

240.0
seed int

Seeds a private generator, so the same seed always gives the same cloud and the design stays reproducible from its metadata.

0
center (float, float)

Middle of the cloud's bounding box.

(0.0, 0.0)

BarnsleyFern dataclass

BarnsleyFern(count: int = 40000, size: float = 300.0, center: Point = (0.0, 0.0), *, seed: int = 0)

Bases: Motif

Four affine maps and a coin, producing a fern.

The canonical iterated function system, and still the most persuasive argument that a very short description can encode a very complicated shape: twenty-four numbers, listed in the source, and the result has a stem, fronds and leaflets that were never drawn.

The same engine with different numbers is :class:IFSAttractor. This is here because the fern is the example everybody arrives looking for.

Parameters:

Name Type Description Default
count int

Points to plot. The fern needs tens of thousands before its leaflets fill in.

40000
size float

Largest extent of the finished plant, which is its height.

300.0
seed int

Seeds a private generator, so the same seed always gives the same fern.

0
center (float, float)

Middle of the plant's bounding box.

(0.0, 0.0)