Skip to content

Fractals

Fractals: escape-time sets, chaos-game attractors, and words and patches that grow by rewriting.

The rewriting classes are a worked example of a form defined outside the engine: elements and laws on the core bases, kernels, a compiler, a solver, and a visual on the engine bases.

A rewriting system runs as Simulation(system, compiler=RewritingCompiler()).

AffineSpec(scale=1.0, rotation=0.0, translation=(0.0, 0.0), reflect=False, matrix=None) dataclass

Bases: BaseSpec

Spec of an affine map of the plane: a reflection across the x axis, a rotation, a scale, and a translation.

The scale, the rotation, and the translation can be expressions of the parameters of the element whose laws hold the substitution, which are evaluated at every generation.

Parameters:

Name Type Description Default
scale float | Expression

The scale factor.

1.0
rotation float | Expression

The rotation in degrees.

0.0
translation Sequence[float | Expression]

The translation.

(0.0, 0.0)
reflect bool

Whether to reflect across the x axis first.

False
matrix ArrayLike | None

A homogeneous 3 by 3 matrix that replaces the other arguments.

None

Examples:

>>> from skcomplex.models.mathematics.fractals import AffineSpec
>>> AffineSpec(scale=0.5, translation=(1, 0)).check().scale
0.5

matrix_at(values)

Return the homogeneous matrix of the map at values of the parameters its expressions read.

The map reflects across the x axis, rotates, scales, and translates, in this order.

Parameters:

Name Type Description Default
values Mapping[object, float]

A mapping from parameter symbol to value.

required

Returns:

Name Type Description
matrix NDArray[float64]

The 3 by 3 matrix.

Examples:

>>> from skcomplex.models.mathematics.fractals import AffineSpec
>>> AffineSpec(scale=0.5, translation=(1, 0)).matrix_at({}).tolist()
[[0.5, 0.0, 1.0], [0.0, 0.5, 0.0], [0.0, 0.0, 1.0]]
Source code in src/skcomplex/models/mathematics/fractals/_affine_spec.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def matrix_at(self, values: Mapping[object, float]) -> npt.NDArray[np.float64]:
    """Return the homogeneous matrix of the map at values of the parameters its expressions read.

    The map reflects across the x axis, rotates, scales, and translates, in this order.

    Args:
        values:
            A mapping from parameter symbol to value.

    Returns:
        matrix:
            The 3 by 3 matrix.

    Examples:
        >>> from skcomplex.models.mathematics.fractals import AffineSpec
        >>> AffineSpec(scale=0.5, translation=(1, 0)).matrix_at({}).tolist()
        [[0.5, 0.0, 1.0], [0.0, 0.5, 0.0], [0.0, 0.0, 1.0]]
    """
    if self.matrix is not None:
        return np.asarray(self.matrix, dtype=float)
    scale, rotation, *translation = (
        float(item.subs(values)) if hasattr(item, 'subs') else float(item)
        for item in (self.scale, self.rotation, *self.translation)
    )
    angle = np.deg2rad(rotation)
    turn = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]])
    reflection = np.diag([1.0, -1.0]) if self.reflect else np.eye(2)
    matrix = np.eye(3)
    matrix[:2, :2] = scale * turn @ reflection
    matrix[:2, 2] = translation
    return matrix

CantorElement(label, tiles=None, *, ratio=1 / 3, prototiles=(), filled=False, max_size=10000000, bounds=None, visual=None)

Bases: PatchElement

Patch of intervals, each replaced by its first and its last part of the length ratio.

Parameters:

Name Type Description Default
label str

The label of the patch.

required
tiles Sequence[tuple[str, AffineSpec]] | None

Pairs of a prototile name and its placement at generation 0, or None for the first prototile in place.

None
ratio Annotated[float, ParameterSpec(domain=open(0, oo))]

The length of each kept part relative to its interval.

1 / 3
prototiles Sequence[PrototileSpec]

The prototiles of the patch.

()
filled bool

Whether the geometry visual draws polygons as a filled image instead of outlines.

False
max_size int

The largest number of tiles a generation may produce.

10000000
bounds Sequence[float] | None

The box (xmin, ymin, xmax, ymax) the geometry visual shows, or None for the box of the initial tiles.

None
visual VisualSpec | None

How the patch is drawn, or None for default_visual_.

None

Examples:

>>> from skcomplex.models.mathematics.fractals import CantorElement
>>> CantorElement('cantor', ratio=0.25).ratio
0.25
Source code in src/skcomplex/models/mathematics/fractals/_cantor_element.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(
    self,
    label: str,
    tiles: Sequence[tuple[str, AffineSpec]] | None = None,
    *,
    ratio: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1 / 3,
    prototiles: Sequence[PrototileSpec] = (),
    filled: bool = False,
    max_size: int = 10_000_000,
    bounds: Sequence[float] | None = None,
    visual: VisualSpec | None = None,
) -> None:
    super().__init__(
        label,
        tiles,
        prototiles=prototiles,
        filled=filled,
        max_size=max_size,
        bounds=bounds,
        visual=visual,
    )
    self.ratio = ratio

ChaosPointElement(label, visual=VisualSpec('points', {'position': 'p', 'radius': 0.004, 'states': ()}), *, p=0.0)

Bases: Element

Point of the plane whose position a random affine map moves at every step.

Parameters:

Name Type Description Default
label str

The label of the point or the group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('points', {'position': 'p', 'radius': 0.004, 'states': ()})
p Annotated[object, StateSpec(vector=True)]

The initial position.

0.0

Examples:

>>> from skcomplex.models.mathematics.fractals import ChaosPointElement
>>> ChaosPointElement('point', p=(0.5, 0.5)).p
(0.5, 0.5)
Source code in src/skcomplex/models/mathematics/fractals/_chaos_game.py
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('points', {'position': 'p', 'radius': 0.004, 'states': ()}),
    *,
    p: Annotated[object, StateSpec(vector=True)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.p = p

GeometrySpec(angle=90.0, step=1.0, contraction=1.0, symbols=None, dimension=2, heading=0.0) dataclass

Bases: BaseSpec

Spec of the geometric interpretation of a word as line segments in two or three dimensions.

A cursor walks the word: symbols mean draw (forward with a segment), move (forward without one), left and right (turn by the angle), reverse (turn by 180 degrees), push and pop (save and restore the position and heading), and in three dimensions pitch_down, pitch_up, roll_left, and roll_right. This is the turtle interpretation of Prusinkiewicz and Lindenmayer (1990), whose symbols are the default meanings: F, f, +, -, |, [, ], &, ^, \, and /. A module's first parameter overrides the step of a move or the angle of a turn.

Parameters:

Name Type Description Default
angle float

The turning angle in degrees.

90.0
step float

The step at generation 0.

1.0
contraction float

The factor the step is multiplied by at every generation.

1.0
symbols Mapping[str, str] | None

Meanings of symbols that replace or extend the default ones of TURTLE_SYMBOLS.

None
dimension int

2 or 3.

2
heading float

The initial heading in degrees in two dimensions, measured from the x axis.

0.0

Examples:

>>> from skcomplex.models.mathematics.fractals import GeometrySpec
>>> GeometrySpec(angle=60.0, contraction=1 / 3).check().angle
60.0

GeometryVisual(filled=None, resolution=_RESOLUTION, *, path=None, recording=None)

Bases: BaseVisual

Visual that logs the structure of a rewritten element at every frame, in a frame fixed across generations.

Word segments, segment tiles, and polygon outlines are one batch of line strips, colored by branch depth or by prototile. A filled patch is an image of its raster instead. The counts of symbols or prototiles are logged as series under <path>/counts/<state>.

Parameters:

Name Type Description Default
filled bool | None

Whether to draw polygons as a filled image, or None to follow the patch.

None
resolution int

The side in pixels of the image of a filled patch.

_RESOLUTION
path str | None

The entity path of the element or agent group to draw, which also names it in the problem.

None
recording RecordingStream | None

The Rerun recording to log to, or None for the active recording of the thread.

None

Examples:

>>> from skcomplex.models.mathematics.fractals import GeometryVisual
>>> GeometryVisual(filled=True)
GeometryVisual(filled=True)
Source code in src/skcomplex/models/mathematics/fractals/_geometry_visual.py
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(
    self,
    filled: bool | None = None,
    resolution: int = _RESOLUTION,
    *,
    path: str | None = None,
    recording: rr.RecordingStream | None = None,
) -> None:
    self.filled = filled
    self.resolution = resolution
    self.path = path
    self.recording = recording

KochElement(label, axiom='', *, alphabet=None, ignore='', geometry=None, max_size=10000000, bounds=None, visual=None)

Bases: WordElement

Word rewritten by the production F → F+F--F+F of the Koch curve.

Parameters:

Name Type Description Default
label str

The label of the word.

required
axiom Annotated[Letters, StateSpec(domain=None)]

The word at generation 0.

''
geometry GeometrySpec | None

How the word is drawn as line segments, or None for no geometry.

None

Examples:

>>> from skcomplex.models.mathematics.fractals import KochElement
>>> KochElement('koch', axiom='F')._get_laws(None)[0].successor
'F+F--F+F'
Source code in src/skcomplex/models/mathematics/fractals/_word_element.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def __init__(
    self,
    label: str,
    axiom: Annotated[Letters, StateSpec(domain=None)] = '',
    *,
    alphabet: str | Mapping[str, int] | None = None,
    ignore: str | Sequence[str] = '',
    geometry: GeometrySpec | None = None,
    max_size: int = 10_000_000,
    bounds: Sequence[float] | None = None,
    visual: VisualSpec | None = None,
) -> None:
    super().__init__(label, visual=visual)
    self.axiom = axiom
    self.alphabet = alphabet
    self.ignore = ignore
    self.geometry = geometry
    self.max_size = max_size
    self.bounds = bounds

ModuleSpec(name, parameters=()) dataclass

Bases: BaseSpec

Spec of a symbol of a word with numeric or symbolic parameters, such as A(x) or F(1.0).

Parameters:

Name Type Description Default
name str

The name of the symbol.

required
parameters tuple[float | Expression, ...]

The parameters, numbers in a word and symbols or expressions in a production.

()

Examples:

>>> from skcomplex.models.mathematics.fractals import ModuleSpec
>>> ModuleSpec('F', (0.5,)).check().parameters
(0.5,)

PatchElement(label, tiles=None, *, prototiles=(), filled=False, max_size=10000000, bounds=None, visual=None)

Bases: Element

Element that holds tiles, each a prototile under an affine placement, replaced in parallel by substitutions.

A subclass returns the SubstitutionLaws of the patch from _get_laws. Its states are the count of every prototile and the number size of tiles.

Parameters:

Name Type Description Default
label str

The label of the element.

required
tiles Annotated[Sequence[tuple[str, AffineSpec]] | None, StateSpec(domain=None)]

Pairs of a prototile name and its placement at generation 0, or None for the first prototile in place.

None
prototiles Sequence[PrototileSpec]

The prototiles.

()
filled bool

Whether the geometry visual draws polygons as a filled image instead of outlines.

False
max_size int

The largest number of tiles a generation may produce.

10000000
bounds Sequence[float] | None

The box (xmin, ymin, xmax, ymax) the geometry visual shows, or None for the box of the initial tiles.

None
visual VisualSpec | None

How the patch is drawn, or None for default_visual_.

None

Examples:

>>> from skcomplex.models.mathematics.fractals import PatchElement
>>> from skcomplex.models.mathematics.fractals import PrototileSpec
>>> PatchElement('cantor', prototiles=[PrototileSpec('interval', [(0, 0), (1, 0)])]).check().filled
False
Source code in src/skcomplex/models/mathematics/fractals/_patch_element.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def __init__(
    self,
    label: str,
    tiles: Annotated[Sequence[tuple[str, AffineSpec]] | None, StateSpec(domain=None)] = None,
    *,
    prototiles: Sequence[PrototileSpec] = (),
    filled: bool = False,
    max_size: int = 10_000_000,
    bounds: Sequence[float] | None = None,
    visual: VisualSpec | None = None,
) -> None:
    super().__init__(label, visual=visual)
    self.tiles = tiles
    self.prototiles = prototiles
    self.filled = filled
    self.max_size = max_size
    self.bounds = bounds

PatchStructureSpec(prototiles, codes, matrices, generation) dataclass

Bases: BaseStructureSpec

Patch of tiles at one generation, each a prototile under a homogeneous affine matrix.

Parameters:

Name Type Description Default
prototiles tuple[PrototileSpec, ...]

The prototiles, indexed by code.

required
codes NDArray[int64]

The prototile code of every tile.

required
matrices NDArray[float64]

The placement of every tile, of shape (t, 3, 3).

required
generation int

The generation.

required

Examples:

>>> import numpy as np
>>> from skcomplex.models.mathematics.fractals import PatchStructureSpec
>>> from skcomplex.models.mathematics.fractals import PrototileSpec
>>> interval = PrototileSpec('interval', [(0, 0), (1, 0)])
>>> patch = PatchStructureSpec((interval,), np.array([0]), np.eye(3)[None], 0)
>>> patch.length()
1.0

has_geometry property

Whether the patch has a geometry, which it always has.

parts property

Number of tiles.

count(name)

Return the number of tiles of a prototile.

Parameters:

Name Type Description Default
name str

The name of the prototile.

required

Returns:

Name Type Description
count int

The number of tiles.

Source code in src/skcomplex/models/mathematics/fractals/_patch_structure_spec.py
58
59
60
61
62
63
64
65
66
67
68
69
70
def count(self, name: str) -> int:
    """Return the number of tiles of a prototile.

    Args:
        name:
            The name of the prototile.

    Returns:
        count:
            The number of tiles.
    """
    codes = [index for index, prototile in enumerate(self.prototiles) if prototile.name == name]
    return int(np.count_nonzero(np.isin(self.codes, codes)))

polygons()

Return the tiles of polygon prototiles, one coordinate array per prototile.

Returns:

Name Type Description
polygons AnyArray

An object array of arrays of shape (t, k, 2).

Source code in src/skcomplex/models/mathematics/fractals/_patch_structure_spec.py
 98
 99
100
101
102
103
104
105
106
107
108
def polygons(self) -> AnyArray:
    """Return the tiles of polygon prototiles, one coordinate array per prototile.

    Returns:
        polygons:
            An object array of arrays of shape `(t, k, 2)`.
    """
    parts = [self.vertices(code) for code, item in enumerate(self.prototiles) if not item.is_segment]
    result = np.empty(len(parts), dtype=object)
    result[:] = parts
    return result

segments()

Return the tiles of segment prototiles as segments.

Returns:

Name Type Description
segments AnyArray

The array of shape (k, 2, 2).

Source code in src/skcomplex/models/mathematics/fractals/_patch_structure_spec.py
88
89
90
91
92
93
94
95
96
def segments(self) -> AnyArray:
    """Return the tiles of segment prototiles as segments.

    Returns:
        segments:
            The array of shape `(k, 2, 2)`.
    """
    parts = [self.vertices(code) for code, item in enumerate(self.prototiles) if item.is_segment]
    return np.concatenate(parts) if parts else np.zeros((0, 2, 2))

vertices(code)

Return the vertices of the tiles of one prototile.

Parameters:

Name Type Description Default
code int

The code of the prototile.

required

Returns:

Name Type Description
vertices AnyArray

The array of shape (t, k, 2).

Source code in src/skcomplex/models/mathematics/fractals/_patch_structure_spec.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def vertices(self, code: int) -> AnyArray:
    """Return the vertices of the tiles of one prototile.

    Args:
        code:
            The code of the prototile.

    Returns:
        vertices:
            The array of shape `(t, k, 2)`.
    """
    points = np.asarray(self.prototiles[code].vertices, dtype=float)
    homogeneous = np.concatenate([points, np.ones((len(points), 1))], axis=1)
    matrices = self.matrices[self.codes == code]
    return np.einsum('tij,kj->tki', matrices, homogeneous)[..., :2]

ProductionLaw(predecessor, successor, *, left=None, right=None, condition=None, probability=None, label='')

Bases: BaseLaw

Production of a string rewriting system, which replaces every matching module of a word by a successor.

A word element returns its productions from _get_laws, and every generation applies all of them at once. The predecessor's parameters are symbols bound to the parameters of each matching module. The condition, the successor's parameters, and the probability are expressions of these symbols and of the parameters of the element. A production with a left or a right context matches only modules whose neighbours, skipping the ignored symbols of the word and its bracketed branches, spell the context.

Parameters:

Name Type Description Default
predecessor str | ModuleSpec

The symbol, or a module whose parameters are symbols.

required
successor Letters

The word that replaces the predecessor.

required
left Letters | None

The left context, or None.

None
right Letters | None

The right context, or None.

None
condition object

A symbolic condition of the predecessor's parameters, or None.

None
probability object

The probability of a stochastic production, or None for a deterministic one. The probabilities of the stochastic productions of one predecessor, context, and condition sum to one.

None
label str

The name of the law, or the empty string for a law without one.

''

Attributes:

Name Type Description
value_ list[ModuleSpec]

The modules of the successor, set by run.

Examples:

>>> from skcomplex.models.mathematics.fractals import ProductionLaw
>>> [module.name for module in ProductionLaw('F', 'F+F--F+F').check().prepare().run({}).value_]
['F', '+', 'F', '-', '-', 'F', '+', 'F']
Source code in src/skcomplex/models/mathematics/fractals/_production_law.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def __init__(
    self,
    predecessor: str | ModuleSpec,
    successor: Letters,
    *,
    left: Letters | None = None,
    right: Letters | None = None,
    condition: object = None,
    probability: object = None,
    label: str = '',
) -> None:
    super().__init__(label)
    self.predecessor = predecessor
    self.successor = successor
    self.left = left
    self.right = right
    self.condition = condition
    self.probability = probability

PrototileSpec(name, vertices) dataclass

Bases: BaseSpec

Spec of a shape that tiles of a patch are placed copies of, a segment or a polygon in its own frame.

Parameters:

Name Type Description Default
name str

The name of the prototile, which names its count state.

required
vertices Sequence[Sequence[float]]

The vertices in the plane, two for a segment and at least three for a polygon.

required

Examples:

>>> from skcomplex.models.mathematics.fractals import PrototileSpec
>>> len(PrototileSpec('interval', [(0, 0), (1, 0)]).check().vertices)
2

is_segment property

Whether the prototile is a segment, with two vertices, rather than a polygon.

QuadraticMapElement(label, visual=None, *, z=0.0, n=0.0, c=0.0, radius=2.0)

Bases: Element

Complex quadratic map z' = z^2 + c while |z| stays within the escape radius, with a count of its steps.

Parameters:

Name Type Description Default
label str

The label of the element.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
z Annotated[object, StateSpec(domain=CC)]

The initial point.

0.0
n Annotated[object, StateSpec(domain=NN)]

The initial number of steps within the escape radius.

0.0
c Annotated[object, ParameterSpec(domain=CC)]

The constant of the map.

0.0
radius Annotated[object, ParameterSpec(domain=open(0, oo))]

The escape radius.

2.0

Examples:

>>> from skcomplex.models.mathematics.fractals import QuadraticMapElement
>>> QuadraticMapElement('point', c=0.25).c
0.25
Source code in src/skcomplex/models/mathematics/fractals/_quadratic.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    z: Annotated[object, StateSpec(domain=CC)] = 0.0,
    n: Annotated[object, StateSpec(domain=NN)] = 0.0,
    c: Annotated[object, ParameterSpec(domain=CC)] = 0.0,
    radius: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
) -> None:
    super().__init__(label, visual=visual)
    self.z = z
    self.n = n
    self.c = c
    self.radius = radius

RandomAffineMapInteraction(label, *, target, state='p', maps=SIERPINSKI_MAPS, weights=(1.0, 1.0, 1.0))

Bases: MapInteraction

Map of a point of the plane by one affine map of an iterated function system, drawn anew at every step.

Map k takes (x, y) to (a x + b y + e, c x + d y + f) for its coefficients (a, b, c, d, e, f), and is drawn with a probability proportional to its weight, independently for every member of a bound group.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the element or group whose position the map sets.

required
state str

The name of the vector state of the target that holds the position.

'p'
maps Sequence[AffineMap]

The coefficients (a, b, c, d, e, f) of every affine map.

SIERPINSKI_MAPS
weights Sequence[float]

The weight of every map.

(1.0, 1.0, 1.0)

Raises:

Type Description
ValidationError

The number of weights differs from the number of maps, or a map does not have six coefficients.

Examples:

>>> from skcomplex.models.mathematics.fractals import RandomAffineMapInteraction
>>> game = RandomAffineMapInteraction('game', target='points', maps=[(0.5, 0, 0, 0.5, 0, 0)], weights=[1.0])
>>> game.check().maps
[(0.5, 0, 0, 0.5, 0, 0)]
Source code in src/skcomplex/models/mathematics/fractals/_random_affine_map.py
60
61
62
63
64
65
66
67
68
69
70
71
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    state: str = 'p',
    maps: Sequence[AffineMap] = SIERPINSKI_MAPS,
    weights: Sequence[float] = (1.0, 1.0, 1.0),
) -> None:
    super().__init__(label, target=target, state=state, function=self._get_image)
    self.maps = maps
    self.weights = weights

RewritingCompiler(backend=None, solver=None, observables=())

Bases: BaseCompiler

Compiler of a system of words or patches into a problem of the rewriting form.

The backend, the solver, and the initial state do not change a problem of the rewriting form.

Examples:

>>> from skcomplex.core.system import System
>>> from skcomplex.models.mathematics.fractals import RewritingCompiler
>>> from skcomplex.models.biology.common.populations import PopulationElement
>>> RewritingCompiler().check().prepare(System(elements=[PopulationElement('rabbits')])).report_.ok
False
Source code in src/skcomplex/engine/compile/_base.py
84
85
86
87
88
89
90
91
92
def __init__(
    self,
    backend: BaseBackend | None = None,
    solver: object = None,
    observables: Sequence[ObservableSpec] = (),
) -> None:
    self.backend = backend
    self.solver = solver
    self.observables = observables

RewritingError

Bases: ValidationError

A rewriting system is malformed, or a generation would exceed its maximum size.

RewritingProblemSpec(form, states, parameters, initial_state, initial_parameters, time, rewriting=(), *, symbols=dict(), elements=(), interactions=(), system=None, agents=(), agent_space=None, companions=dict(), renewals=(), matrices=(), transfer_network=None, topology=None, observables=()) dataclass

Bases: BaseProblemSpec

Problem of the rewriting form, with the compiled rewriting system of every rewritten element.

Parameters:

Name Type Description Default
rewriting tuple[Compiled, ...]

The compiled rewriting systems of the words and patches.

()

RewritingSolver(*, random_state=None, until=math.inf)

Bases: BaseSolver[RewritingProblemSpec]

Solver of rewriting systems, which rewrites every structure in parallel once per generation.

The structures live next to the state vector, whose components are their symbol, prototile, or label counts and sizes. Stochastic productions draw from the simulation's random stream.

Examples:

>>> from skcomplex.core.spaces import DiscreteTime
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.mathematics.fractals import ProductionLaw
>>> from skcomplex.models.mathematics.fractals import RewritingSolver
>>> from skcomplex.models.mathematics.fractals import WordElement
>>> class AlgaeElement(WordElement):
...     def _get_laws(self, namespace):
...         return [ProductionLaw('A', 'AB'), ProductionLaw('B', 'A')]
>>> system = System(elements=[AlgaeElement('algae', axiom='A')], time=DiscreteTime())
>>> from skcomplex.models.mathematics.fractals import RewritingCompiler
>>> compiler = RewritingCompiler()
>>> simulation = Simulation(system, compiler=compiler, solver=RewritingSolver())
>>> simulation = simulation.check().prepare()
>>> trajectory = simulation.run(until=5).trajectory_
>>> trajectory.values('algae.size').tolist()
[1.0, 2.0, 3.0, 5.0, 8.0, 13.0]
Source code in src/skcomplex/engine/solvers/_base.py
87
88
89
def __init__(self, *, random_state: int | np.random.Generator | None = None, until: float = math.inf) -> None:
    self.random_state = random_state
    self.until = until

SierpinskiElement(label, tiles=None, *, shape='triangle', prototiles=(), filled=False, max_size=10000000, bounds=None, visual=None)

Bases: PatchElement

Patch of triangles or squares, each replaced by the scaled copies of the Sierpiński triangle or carpet.

Parameters:

Name Type Description Default
label str

The label of the patch.

required
tiles Sequence[tuple[str, AffineSpec]] | None

Pairs of a prototile name and its placement at generation 0, or None for the first prototile in place.

None
shape str

'triangle' or 'carpet'.

'triangle'
prototiles Sequence[PrototileSpec]

The prototiles of the patch.

()
filled bool

Whether the geometry visual draws polygons as a filled image instead of outlines.

False
max_size int

The largest number of tiles a generation may produce.

10000000
bounds Sequence[float] | None

The box (xmin, ymin, xmax, ymax) the geometry visual shows, or None for the box of the initial tiles.

None
visual VisualSpec | None

How the patch is drawn, or None for default_visual_.

None

Examples:

>>> from skcomplex.models.mathematics.fractals import SierpinskiElement
>>> len(SierpinskiElement('sierpinski', shape='carpet')._get_laws(None)[0].pieces)
8
Source code in src/skcomplex/models/mathematics/fractals/_sierpinski_element.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def __init__(
    self,
    label: str,
    tiles: Sequence[tuple[str, AffineSpec]] | None = None,
    *,
    shape: str = 'triangle',
    prototiles: Sequence[PrototileSpec] = (),
    filled: bool = False,
    max_size: int = 10_000_000,
    bounds: Sequence[float] | None = None,
    visual: VisualSpec | None = None,
) -> None:
    super().__init__(
        label,
        tiles,
        prototiles=prototiles,
        filled=filled,
        max_size=max_size,
        bounds=bounds,
        visual=visual,
    )
    self.shape = shape

SubstitutionLaw(prototile, pieces, *, label='')

Bases: BaseLaw

Substitution that replaces every tile of a prototile by pieces placed relative to the tile.

A patch element returns its substitutions from _get_laws, and every generation applies all of them at once.

Parameters:

Name Type Description Default
prototile str

The name of the prototile replaced.

required
pieces Sequence[tuple[str, AffineSpec]]

Pairs of a prototile name and the affine map from the parent tile's frame.

required
label str

The name of the law, or the empty string for a law without one.

''

Attributes:

Name Type Description
value_ list[tuple[str, object]]

Pairs of a prototile name and the scale, the rotation, and the translation of its map, set by run.

Examples:

>>> from skcomplex.models.mathematics.fractals import SubstitutionLaw
>>> from skcomplex.models.mathematics.fractals import AffineSpec
>>> len(SubstitutionLaw('interval', [('interval', AffineSpec(scale=1 / 3))]).check().pieces)
1
Source code in src/skcomplex/models/mathematics/fractals/_substitution_law.py
45
46
47
48
def __init__(self, prototile: str, pieces: Sequence[tuple[str, AffineSpec]], *, label: str = '') -> None:
    super().__init__(label)
    self.prototile = prototile
    self.pieces = pieces

WordElement(label, axiom='', *, alphabet=None, ignore='', geometry=None, max_size=10000000, bounds=None, visual=None)

Bases: Element

Element that holds a word of modules rewritten in parallel by string productions.

A subclass returns the ProductionLaws of the word from _get_laws. Its states are the count of every symbol whose name is a word of letters, digits, and underscores, and the length size of the word.

Parameters:

Name Type Description Default
label str

The label of the element.

required
axiom Annotated[Letters, StateSpec(domain=None)]

The word at generation 0.

''
alphabet str | Mapping[str, int] | None

The symbols, as a string of one-character symbols or a mapping from name to number of parameters, or None to take them from the axiom and the productions.

None
ignore str | Sequence[str]

The symbols that contexts skip.

''
geometry GeometrySpec | None

The interpretation of the word as line segments, or None.

None
max_size int

The largest word a generation may produce.

10000000
bounds Sequence[float] | None

The box (xmin, ymin, xmax, ymax) the geometry visual shows, or None to fit the data.

None
visual VisualSpec | None

How the word is drawn, or None for default_visual_.

None

Examples:

>>> from skcomplex.models.mathematics.fractals import WordElement
>>> from skcomplex.models.mathematics.fractals import ProductionLaw
>>> class AlgaeElement(WordElement):
...     def _get_laws(self, namespace):
...         return [ProductionLaw('A', 'AB'), ProductionLaw('B', 'A')]
>>> AlgaeElement('algae', axiom='A').check().axiom
'A'
Source code in src/skcomplex/models/mathematics/fractals/_word_element.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def __init__(
    self,
    label: str,
    axiom: Annotated[Letters, StateSpec(domain=None)] = '',
    *,
    alphabet: str | Mapping[str, int] | None = None,
    ignore: str | Sequence[str] = '',
    geometry: GeometrySpec | None = None,
    max_size: int = 10_000_000,
    bounds: Sequence[float] | None = None,
    visual: VisualSpec | None = None,
) -> None:
    super().__init__(label, visual=visual)
    self.axiom = axiom
    self.alphabet = alphabet
    self.ignore = ignore
    self.geometry = geometry
    self.max_size = max_size
    self.bounds = bounds

WordStructureSpec(alphabet, codes, parameters, generation, *, strokes=None, depths=None) dataclass

Bases: BaseStructureSpec

Word of a string rewriting system at one generation, with its segments where a word geometry is declared.

Parameters:

Name Type Description Default
alphabet tuple[str, ...]

The symbol names, indexed by code.

required
codes NDArray[int64]

The symbol code of every module.

required
parameters NDArray[float64]

The parameters of every module, padded with NaN.

required
generation int

The generation.

required
strokes NDArray[float64] | None

The segments of shape (k, 2, d), or None without a word geometry.

None
depths NDArray[int64] | None

The branch depth of every segment, or None without a word geometry.

None

Examples:

>>> import numpy as np
>>> from skcomplex.models.mathematics.fractals import WordStructureSpec
>>> WordStructureSpec(('A', 'B'), np.array([0, 1, 0]), np.zeros((3, 0)), 2).text
'ABA'

has_geometry property

Whether a word geometry has drawn the word.

parts property

Number of modules.

text property

Word as written, with modules separated by spaces when one has parameters or a long name.

count(name)

Return the number of modules of a symbol.

Parameters:

Name Type Description Default
name str

The symbol.

required

Returns:

Name Type Description
count int

The number of modules with that symbol.

Source code in src/skcomplex/models/mathematics/fractals/_word_structure_spec.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def count(self, name: str) -> int:
    """Return the number of modules of a symbol.

    Args:
        name:
            The symbol.

    Returns:
        count:
            The number of modules with that symbol.
    """
    if name not in self.alphabet:
        return 0
    return int(np.count_nonzero(self.codes == self.alphabet.index(name)))

modules()

Return the modules of the word with their parameters.

Returns:

Name Type Description
modules list[ModuleSpec]

The modules in order.

Source code in src/skcomplex/models/mathematics/fractals/_word_structure_spec.py
64
65
66
67
68
69
70
71
72
73
74
75
def modules(self) -> list[ModuleSpec]:
    """Return the modules of the word with their parameters.

    Returns:
        modules:
            The modules in order.
    """
    modules = []
    for code, row in zip(self.codes.tolist(), self.parameters, strict=True):
        values = [float(value) for value in row if not np.isnan(value)]
        modules.append(ModuleSpec(self.alphabet[code], tuple(values)))
    return modules

segments()

Return the segments of the word geometry.

Returns:

Name Type Description
segments AnyArray

The array of shape (k, 2, d), empty without a word geometry.

Source code in src/skcomplex/models/mathematics/fractals/_word_structure_spec.py
103
104
105
106
107
108
109
110
def segments(self) -> AnyArray:
    """Return the segments of the word geometry.

    Returns:
        segments:
            The array of shape `(k, 2, d)`, empty without a word geometry.
    """
    return np.zeros((0, 2, 2)) if self.strokes is None else self.strokes

cantor_set(ratio=1 / 3)

Return the Cantor set as the substitution of every interval by its two outer parts.

Every generation keeps the first and the last part of each interval, each ratio of its length, and removes the middle. With the ratio of one third, generation n has 2^n intervals of total length (2/3)^n, and the limit set has the similarity dimension log 2 / log 3, about 0.631.

Parameters:

Name Type Description Default
ratio float

The length of each kept part relative to its interval, below one half.

1 / 3

Returns:

Name Type Description
system System

The model, with the patch cantor and its number of intervals cantor.size.

Raises:

Type Description
ValidationError

The ratio is not between 0 and 1/2.

Examples:

>>> from skcomplex.models import run
>>> run('cantor_set', settings={'until': 3}).trajectory_.values('cantor.size').tolist()
[1.0, 2.0, 4.0, 8.0]
Source code in src/skcomplex/models/mathematics/fractals/_substitution.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@register_model(
    field='mathematics/fractals',
    tags=('fractal', 'substitution', 'rewriting system'),
    references=('Cantor 1883', 'Thurner, Hanel, and Klimek 2018', 'Trzaska 2023'),
    defaults={'compiler': RewritingCompiler(), 'until': 8, 'dt': 1},
)
def cantor_set(ratio: float = 1 / 3) -> System:
    """Return the Cantor set as the substitution of every interval by its two outer parts.

    Every generation keeps the first and the last part of each interval, each `ratio` of its length, and removes the
    middle. With the ratio of one third, generation `n` has `2^n` intervals of total length `(2/3)^n`, and the limit set
    has the similarity dimension `log 2 / log 3`, about 0.631.

    Args:
        ratio:
            The length of each kept part relative to its interval, below one half.

    Returns:
        system:
            The model, with the patch `cantor` and its number of intervals `cantor.size`.

    Raises:
        ValidationError:
            The ratio is not between 0 and 1/2.

    Examples:
        >>> from skcomplex.models import run
        >>> run('cantor_set', settings={'until': 3}).trajectory_.values('cantor.size').tolist()
        [1.0, 2.0, 4.0, 8.0]
    """
    if not 0 < ratio < LARGEST_RATIO:
        error_msg = f'ratio = {ratio!r} must lie between 0 and 1/2.'
        raise ValidationError(error_msg)
    interval = PrototileSpec('interval', [(0.0, 0.0), (1.0, 0.0)])
    return System(
        elements=[CantorElement('cantor', prototiles=[interval], bounds=(-0.03, -0.1, 1.03, 0.1), ratio=ratio)],
    )

chaos_game(preset='sierpinski', *, maps=None, weights=None, n_points=20000, random_state=0)

Return points that draw the attractor of an iterated function system by the chaos game.

At every step each point moves by one of the affine maps of the system, drawn with probabilities proportional to the weights. The maps contract, so after a few steps the cloud of points covers the attractor, the unique compact set that the union of the maps leaves unchanged. The Sierpiński triangle of three maps of ratio 1/2 has the box-counting dimension log 3 / log 2, and the Barnsley fern is the attractor of four maps.

Parameters:

Name Type Description Default
preset str

'sierpinski' or 'barnsley', used when maps is None.

'sierpinski'
maps Sequence[AffineMap] | None

The coefficients (a, b, c, d, e, f) of every map (x, y) -> (a x + b y + e, c x + d y + f), or None for the preset.

None
weights Sequence[float] | None

The weight of every map, or None for the weights of the preset, or equal weights for given maps.

None
n_points int

The number of points, started uniformly in the unit square.

20000
random_state int | Generator | None

The seed or generator of the initial points.

0

Returns:

Name Type Description
system System

The model, with the positions points.p.

Raises:

Type Description
ValidationError

The preset is unknown.

Examples:

>>> from skcomplex.models.mathematics.fractals import chaos_game
>>> len(chaos_game('barnsley', n_points=10).interactions[0].maps)
4
Source code in src/skcomplex/models/mathematics/fractals/_chaos_game.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
@register_model(
    field='mathematics/fractals',
    tags=('fractal', 'iterated function system', 'chaos game', 'discrete map'),
    references=('Barnsley 1988', 'Dilão 2023'),
    defaults={'until': 30, 'dt': 1},
)
def chaos_game(
    preset: str = 'sierpinski',
    *,
    maps: Sequence[AffineMap] | None = None,
    weights: Sequence[float] | None = None,
    n_points: int = 20000,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return points that draw the attractor of an iterated function system by the chaos game.

    At every step each point moves by one of the affine maps of the system, drawn with probabilities proportional to
    the weights. The maps contract, so after a few steps the cloud of points covers the attractor, the unique compact
    set that the union of the maps leaves unchanged. The Sierpiński triangle of three maps of ratio 1/2 has the
    box-counting dimension `log 3 / log 2`, and the Barnsley fern is the attractor of four maps.

    Args:
        preset:
            `'sierpinski'` or `'barnsley'`, used when `maps` is `None`.

        maps:
            The coefficients `(a, b, c, d, e, f)` of every map `(x, y) -> (a x + b y + e, c x + d y + f)`, or `None` for
            the preset.

        weights:
            The weight of every map, or `None` for the weights of the preset, or equal weights for given maps.

        n_points:
            The number of points, started uniformly in the unit square.

        random_state:
            The seed or generator of the initial points.

    Returns:
        system:
            The model, with the positions `points.p`.

    Raises:
        ValidationError:
            The preset is unknown.

    Examples:
        >>> from skcomplex.models.mathematics.fractals import chaos_game
        >>> len(chaos_game('barnsley', n_points=10).interactions[0].maps)
        4
    """
    if maps is None and preset not in PRESETS:
        error_msg = f'Unknown chaos game preset {preset!r}. Use one of {", ".join(PRESETS)}, or give the maps.'
        raise ValidationError(error_msg)
    chosen_maps, preset_weights = PRESETS[preset] if maps is None else (maps, (1.0,) * len(maps))
    return _system_of(chosen_maps, weights if weights is not None else preset_weights, n_points, random_state)

julia(c=-0.8 + 0.156j, shape=(240, 320), *, window=JULIA_WINDOW, radius=2.0)

Return the filled Julia set of z -> z^2 + c, the starting points whose orbit stays bounded (Dilão 2023 §5.5).

Every site holds one starting point z of the window and iterates the map with the same constant c, and n counts the steps the orbit stays within the escape radius. For c in the Mandelbrot set the filled Julia set is connected, and outside it the set breaks into dust.

Parameters:

Name Type Description Default
c complex

The constant of the map.

-0.8 + 0.156j
shape Sequence[int]

The number of sites along the imaginary and the real axis.

(240, 320)
window Window

The smallest and largest real part, then the smallest and largest imaginary part, of the starting points.

JULIA_WINDOW
radius float

The escape radius, at least 2 and at least |c|, so escaped orbits never return.

2.0

Returns:

Name Type Description
system System

The model, with the field plane, its orbits plane.z, and its escape times plane.n.

Examples:

>>> from skcomplex.models.mathematics.fractals import julia
>>> julia(0.0, (3, 3), window=(-1, 1, -1, 1)).elements[0].values['z'][0].tolist()
[(-1+1j), 1j, (1+1j)]
Source code in src/skcomplex/models/mathematics/fractals/_quadratic.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
@register_model(
    field='mathematics/fractals',
    tags=('fractal', 'complex', 'map', 'lattice', 'escape time'),
    references=('Dilão 2023',),
    defaults={'until': 100, 'dt': 1},
)
def julia(
    c: complex = -0.8 + 0.156j,
    shape: Sequence[int] = (240, 320),
    *,
    window: Window = JULIA_WINDOW,
    radius: float = 2.0,
) -> System:
    """Return the filled Julia set of `z -> z^2 + c`, the starting points whose orbit stays bounded (Dilão 2023 §5.5).

    Every site holds one starting point `z` of the window and iterates the map with the same constant `c`, and `n`
    counts the steps the orbit stays within the escape radius. For `c` in the Mandelbrot set the filled Julia set is
    connected, and outside it the set breaks into dust.

    Args:
        c:
            The constant of the map.

        shape:
            The number of sites along the imaginary and the real axis.

        window:
            The smallest and largest real part, then the smallest and largest imaginary part, of the starting points.

        radius:
            The escape radius, at least 2 and at least `|c|`, so escaped orbits never return.

    Returns:
        system:
            The model, with the field `plane`, its orbits `plane.z`, and its escape times `plane.n`.

    Examples:
        >>> from skcomplex.models.mathematics.fractals import julia
        >>> julia(0.0, (3, 3), window=(-1, 1, -1, 1)).elements[0].values['z'][0].tolist()
        [(-1+1j), 1j, (1+1j)]
    """
    points = _plane(shape, window)
    return _escape_field(shape, points, complex(c), radius)

koch_curve(angle=60.0)

Return the Koch curve as the L-system F → F+F--F+F drawn by a turtle.

Every generation replaces each segment by four segments of a third of its length, with a bump of the turning angle in the middle. With the angle of 60 degrees the curve keeps its endpoints at (0, 0) and (1, 0), its length is (4/3)^n at generation n, and its similarity dimension is log 4 / log 3, about 1.262.

Parameters:

Name Type Description Default
angle float

The turning angle of the turtle in degrees.

60.0

Returns:

Name Type Description
system System

The model, with the word koch, its symbol counts koch.F, and its size koch.size.

Examples:

>>> from skcomplex.models import run
>>> run('koch_curve', settings={'until': 3}).trajectory_.values('koch.F').tolist()
[1.0, 4.0, 16.0, 64.0]
Source code in src/skcomplex/models/mathematics/fractals/_substitution.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@register_model(
    field='mathematics/fractals',
    tags=('fractal', 'L-system', 'rewriting system', 'turtle'),
    references=('Prusinkiewicz and Lindenmayer 1990', 'Thurner, Hanel, and Klimek 2018', 'Trzaska 2023'),
    defaults={'compiler': RewritingCompiler(), 'until': 5, 'dt': 1},
)
def koch_curve(angle: float = 60.0) -> System:
    """Return the Koch curve as the L-system `F → F+F--F+F` drawn by a turtle.

    Every generation replaces each segment by four segments of a third of its length, with a bump of the turning angle
    in the middle. With the angle of 60 degrees the curve keeps its endpoints at `(0, 0)` and `(1, 0)`, its length is
    `(4/3)^n` at generation `n`, and its similarity dimension is `log 4 / log 3`, about 1.262.

    Args:
        angle:
            The turning angle of the turtle in degrees.

    Returns:
        system:
            The model, with the word `koch`, its symbol counts `koch.F`, and its size `koch.size`.

    Examples:
        >>> from skcomplex.models import run
        >>> run('koch_curve', settings={'until': 3}).trajectory_.values('koch.F').tolist()
        [1.0, 4.0, 16.0, 64.0]
    """
    word = KochElement(
        'koch',
        axiom='F',
        alphabet='F+-',
        geometry=GeometrySpec(angle=angle, step=1.0, contraction=1 / 3),
        bounds=(-0.03, -0.05, 1.03, 0.35),
    )
    return System(elements=[word])

mandelbrot(shape=(240, 300), *, window=MANDELBROT_WINDOW, radius=2.0)

Return the Mandelbrot set, the points c whose orbit of z -> z^2 + c from 0 stays bounded (Dilão 2023 §5.5).

Every site holds one point c of the window and iterates the map, and n counts the steps the orbit stays within the escape radius. The points of the set never escape, so their count grows with every step, while the count of an escaped point stops at its escape time and colors the bands around the set.

Parameters:

Name Type Description Default
shape Sequence[int]

The number of sites along the imaginary and the real axis.

(240, 300)
window Window

The smallest and largest real part, then the smallest and largest imaginary part, of the points.

MANDELBROT_WINDOW
radius float

The escape radius, 2 for the quadratic map.

2.0

Returns:

Name Type Description
system System

The model, with the field plane, its orbits plane.z, and its escape times plane.n.

Examples:

>>> from skcomplex.models.mathematics.fractals import mandelbrot
>>> mandelbrot((3, 3), window=(-1, 1, -1, 1)).elements[0].values['c'][1].tolist()
[(-1+0j), 0j, (1+0j)]
Source code in src/skcomplex/models/mathematics/fractals/_quadratic.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@register_model(
    field='mathematics/fractals',
    tags=('fractal', 'complex', 'map', 'lattice', 'escape time'),
    references=('Dilão 2023',),
    defaults={'until': 100, 'dt': 1},
)
def mandelbrot(
    shape: Sequence[int] = (240, 300),
    *,
    window: Window = MANDELBROT_WINDOW,
    radius: float = 2.0,
) -> System:
    """Return the Mandelbrot set, the points `c` whose orbit of `z -> z^2 + c` from 0 stays bounded (Dilão 2023 §5.5).

    Every site holds one point `c` of the window and iterates the map, and `n` counts the steps the orbit stays within
    the escape radius. The points of the set never escape, so their count grows with every step, while the count of an
    escaped point stops at its escape time and colors the bands around the set.

    Args:
        shape:
            The number of sites along the imaginary and the real axis.

        window:
            The smallest and largest real part, then the smallest and largest imaginary part, of the points.

        radius:
            The escape radius, 2 for the quadratic map.

    Returns:
        system:
            The model, with the field `plane`, its orbits `plane.z`, and its escape times `plane.n`.

    Examples:
        >>> from skcomplex.models.mathematics.fractals import mandelbrot
        >>> mandelbrot((3, 3), window=(-1, 1, -1, 1)).elements[0].values['c'][1].tolist()
        [(-1+0j), 0j, (1+0j)]
    """
    points = _plane(shape, window)
    return _escape_field(shape, np.zeros_like(points), points, radius)

sierpinski_substitution(shape='triangle', *, filled=True)

Return the Sierpiński triangle or carpet as the substitution of a polygon by scaled copies of itself.

The triangle is replaced by its three corner copies of half its side, so generation n has 3^n triangles of total area (3/4)^n times the first, and the gasket has the similarity dimension log 3 / log 2, about 1.585. The square is replaced by the eight copies of a third of its side around its middle, so generation n has 8^n squares of area (8/9)^n, and the carpet has the dimension log 8 / log 3, about 1.893.

Parameters:

Name Type Description Default
shape str

'triangle' or 'carpet'.

'triangle'
filled bool

Whether the geometry visual draws the polygons as a filled image instead of outlines.

True

Returns:

Name Type Description
system System

The model, with the patch sierpinski and its number of polygons sierpinski.size.

Raises:

Type Description
ValidationError

The shape is unknown.

Examples:

>>> from skcomplex.models import run
>>> run('sierpinski_substitution', settings={'until': 2}).trajectory_.values('sierpinski.size').tolist()
[1.0, 3.0, 9.0]
Source code in src/skcomplex/models/mathematics/fractals/_substitution.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
@register_model(
    field='mathematics/fractals',
    tags=('fractal', 'substitution', 'rewriting system'),
    references=('Sierpiński 1915', 'Sierpiński 1916', 'Thurner, Hanel, and Klimek 2018', 'Trzaska 2023'),
    defaults={'compiler': RewritingCompiler(), 'until': 6, 'dt': 1},
)
def sierpinski_substitution(shape: str = 'triangle', *, filled: bool = True) -> System:
    """Return the Sierpiński triangle or carpet as the substitution of a polygon by scaled copies of itself.

    The triangle is replaced by its three corner copies of half its side, so generation `n` has `3^n` triangles of
    total area `(3/4)^n` times the first, and the gasket has the similarity dimension `log 3 / log 2`, about 1.585. The
    square is replaced by the eight copies of a third of its side around its middle, so generation `n` has `8^n`
    squares of area `(8/9)^n`, and the carpet has the dimension `log 8 / log 3`, about 1.893.

    Args:
        shape:
            `'triangle'` or `'carpet'`.

        filled:
            Whether the geometry visual draws the polygons as a filled image instead of outlines.

    Returns:
        system:
            The model, with the patch `sierpinski` and its number of polygons `sierpinski.size`.

    Raises:
        ValidationError:
            The shape is unknown.

    Examples:
        >>> from skcomplex.models import run
        >>> run('sierpinski_substitution', settings={'until': 2}).trajectory_.values('sierpinski.size').tolist()
        [1.0, 3.0, 9.0]
    """
    prototile, _ = SierpinskiElement._sierpinski_tiles(shape)
    return System(elements=[SierpinskiElement('sierpinski', shape=shape, prototiles=[prototile], filled=filled)])