Skip to content

Sierpiński substitutions

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

  • Name: sierpinski_substitution
  • Field: mathematics/fractals
  • Formalism: rewriting system
  • References: Sierpiński 1915; Sierpiński 1916; Thurner, Hanel, and Klimek 2018; Trzaska 2023

Factory

@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)])

Components

class SierpinskiElement(PatchElement):
    """Patch of triangles or squares, each replaced by the scaled copies of the Sierpiński triangle or carpet.

    Args:
        label:
            The label of the patch.

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

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

        prototiles:
            The prototiles of the patch.

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

        max_size:
            The largest number of tiles a generation may produce.

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

        visual:
            How the patch is drawn, or `None` for `default_visual_`.

    Examples:
        >>> from skcomplex.models.mathematics.fractals import SierpinskiElement
        >>> len(SierpinskiElement('sierpinski', shape='carpet')._get_laws(None)[0].pieces)
        8
    """

    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

    def _get_laws(self, s: SimpleNamespace) -> list[SubstitutionLaw]:
        """Return the substitution of every polygon by its scaled copies.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The substitution of the patch.

        Raises:
            ValidationError:
                The shape is unknown.
        """
        del s
        prototile, pieces = SierpinskiElement._sierpinski_tiles(self.shape)
        return [SubstitutionLaw(prototile.name, pieces)]

    @staticmethod
    def _sierpinski_tiles(shape: str) -> tuple[PrototileSpec, list[tuple[str, AffineSpec]]]:
        """Return the prototile of a Sierpiński shape and the scaled copies that replace it."""
        if shape == 'triangle':
            height = np.sqrt(3) / 2
            corners = [(0.0, 0.0), (0.5, 0.0), (0.25, height / 2)]
            pieces = [('triangle', AffineSpec(scale=0.5, translation=corner)) for corner in corners]
            return PrototileSpec('triangle', [(0.0, 0.0), (1.0, 0.0), (0.5, height)]), pieces
        if shape == 'carpet':
            cells = [(column, row) for row in range(3) for column in range(3) if (column, row) != (1, 1)]
            pieces = [('square', AffineSpec(scale=1 / 3, translation=(column / 3, row / 3))) for column, row in cells]
            return PrototileSpec('square', [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]), pieces
        error_msg = f'Unknown shape {shape!r}. Use one of {", ".join(SHAPES)}.'
        raise ValidationError(error_msg)