mirror of
https://github.com/TheAlgorithms/Python.git
synced 2026-09-28 21:45:27 +08:00
* Clean up the fractals directory Add a directory README plus two new, fully doctested fractal generators. - README.md: overview table of every fractal, how to run them, and further reading. Distinguishes the visual (turtle/matplotlib) demos from the pure-computation ones that run under doctest in CI. - sierpinski_carpet.py: integer-arithmetic Sierpinski carpet. is_filled(), generate_carpet() and count_filled_cells() are all doctested; no display needed. - barnsley_fern.py: the Barnsley fern iterated function system. transform(), choose_transformation() and generate_fern() are doctested and made deterministic via an optional seed; matplotlib plotting is optional and guarded behind __main__. * fractals: inline demo code under __main__ (address keeper-bot doctest note)
148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
"""
|
|
The Barnsley fern is a fractal that resembles the black spleenwort fern. It was
|
|
described by the British mathematician Michael Barnsley in his 1988 book
|
|
*Fractals Everywhere* and is a classic example of an iterated function system
|
|
(IFS).
|
|
|
|
An IFS builds a fractal by repeatedly applying a small set of affine
|
|
transformations, each chosen at random with a fixed probability. Starting from
|
|
the point ``(0, 0)`` the fern uses four transformations:
|
|
|
|
=============== =========================================== ============
|
|
Transformation Effect Probability
|
|
=============== =========================================== ============
|
|
Stem collapse onto the y-axis 1%
|
|
Successive leaf the main self-similar copy of the fern 85%
|
|
Left leaflet a smaller rotated/reflected copy 7%
|
|
Right leaflet another smaller rotated/reflected copy 7%
|
|
=============== =========================================== ============
|
|
|
|
Because the whole picture is produced by chance the doctests below seed Python's
|
|
random generator so that the results are reproducible. Plotting the points with
|
|
matplotlib is optional and only happens when the module is run directly.
|
|
|
|
Reference: https://en.wikipedia.org/wiki/Barnsley_fern
|
|
"""
|
|
|
|
import random
|
|
|
|
# Each row is (a, b, c, d, e, f) for the affine map
|
|
# x' = a*x + b*y + e
|
|
# y' = c*x + d*y + f
|
|
# and the running cumulative probabilities used to pick a transformation.
|
|
TRANSFORMATIONS: tuple[tuple[float, float, float, float, float, float], ...] = (
|
|
(0.00, 0.00, 0.00, 0.16, 0.00, 0.00), # stem
|
|
(0.85, 0.04, -0.04, 0.85, 0.00, 1.60), # successive smaller leaflets
|
|
(0.20, -0.26, 0.23, 0.22, 0.00, 1.60), # left-hand leaflet
|
|
(-0.15, 0.28, 0.26, 0.24, 0.00, 0.44), # right-hand leaflet
|
|
)
|
|
CUMULATIVE_PROBABILITIES: tuple[float, ...] = (0.01, 0.86, 0.93, 1.00)
|
|
|
|
|
|
def transform(point: tuple[float, float], index: int) -> tuple[float, float]:
|
|
"""
|
|
Apply the affine transformation ``index`` to ``point`` and return the image.
|
|
|
|
>>> transform((0.0, 0.0), 0)
|
|
(0.0, 0.0)
|
|
>>> transform((1.0, 1.0), 1)
|
|
(0.89, 2.41)
|
|
>>> transform((2.0, 3.0), 3)
|
|
(0.54, 1.68)
|
|
>>> transform((0.0, 0.0), 4)
|
|
Traceback (most recent call last):
|
|
...
|
|
IndexError: index must be in range 0..3, got 4
|
|
"""
|
|
if not 0 <= index < len(TRANSFORMATIONS):
|
|
msg = f"index must be in range 0..3, got {index}"
|
|
raise IndexError(msg)
|
|
a, b, c, d, e, f = TRANSFORMATIONS[index]
|
|
x, y = point
|
|
new_x = round(a * x + b * y + e, 12)
|
|
new_y = round(c * x + d * y + f, 12)
|
|
return (new_x, new_y)
|
|
|
|
|
|
def choose_transformation(sample: float) -> int:
|
|
"""
|
|
Map a value ``sample`` from ``[0, 1)`` to a transformation index using the
|
|
cumulative probabilities of the fern.
|
|
|
|
>>> choose_transformation(0.0)
|
|
0
|
|
>>> choose_transformation(0.5)
|
|
1
|
|
>>> choose_transformation(0.9)
|
|
2
|
|
>>> choose_transformation(0.97)
|
|
3
|
|
"""
|
|
for index, threshold in enumerate(CUMULATIVE_PROBABILITIES):
|
|
if sample < threshold:
|
|
return index
|
|
return len(CUMULATIVE_PROBABILITIES) - 1
|
|
|
|
|
|
def generate_fern(
|
|
iterations: int, seed: int | None = None
|
|
) -> list[tuple[float, float]]:
|
|
"""
|
|
Generate ``iterations`` points of the Barnsley fern, starting at ``(0, 0)``.
|
|
|
|
Passing a ``seed`` makes the (otherwise random) output reproducible, which is
|
|
what keeps the doctests deterministic.
|
|
|
|
>>> points = generate_fern(5, seed=0)
|
|
>>> len(points)
|
|
5
|
|
>>> points[0]
|
|
(0.0, 0.0)
|
|
>>> points # doctest: +NORMALIZE_WHITESPACE
|
|
[(0.0, 0.0), (0.0, 1.6), (0.064, 2.96),
|
|
(0.1728, 4.11344), (0.3114176, 5.089512)]
|
|
|
|
Every fern point lives inside the well known bounding box.
|
|
|
|
>>> cloud = generate_fern(2000, seed=42)
|
|
>>> all(-2.182 <= x <= 2.6558 for x, _ in cloud)
|
|
True
|
|
>>> all(0.0 <= y <= 9.9984 for _, y in cloud)
|
|
True
|
|
>>> generate_fern(0)
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: iterations must be positive, got 0
|
|
"""
|
|
if iterations <= 0:
|
|
msg = f"iterations must be positive, got {iterations}"
|
|
raise ValueError(msg)
|
|
rng = random.Random(seed)
|
|
point = (0.0, 0.0)
|
|
points = [point]
|
|
for _ in range(iterations - 1):
|
|
index = choose_transformation(rng.random())
|
|
point = transform(point, index)
|
|
points.append(point)
|
|
return points
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
|
|
doctest.testmod()
|
|
|
|
try:
|
|
import matplotlib.pyplot as plt
|
|
except ImportError:
|
|
print("matplotlib is required to plot the fern (pip install matplotlib).")
|
|
else:
|
|
fern_points = generate_fern(100_000, seed=0)
|
|
xs = [x for x, _ in fern_points]
|
|
ys = [y for _, y in fern_points]
|
|
plt.figure(figsize=(4, 8))
|
|
plt.scatter(xs, ys, s=0.2, color="forestgreen")
|
|
plt.axis("off")
|
|
plt.title("Barnsley fern")
|
|
plt.show()
|