|
| 1 | +""" |
| 2 | +Description |
| 3 | + The Koch snowflake is a fractal curve and one of the earliest fractals to |
| 4 | + have been described. The Koch snowflake can be built up iteratively, in a |
| 5 | + sequence of stages. The first stage is an equilateral triangle, and each |
| 6 | + successive stage is formed by adding outward bends to each side of the |
| 7 | + previous stage, making smaller equilateral triangles. |
| 8 | + This can be achieved through the following steps for each line: |
| 9 | + 1. divide the line segment into three segments of equal length. |
| 10 | + 2. draw an equilateral triangle that has the middle segment from step 1 |
| 11 | + as its base and points outward. |
| 12 | + 3. remove the line segment that is the base of the triangle from step 2. |
| 13 | + (description adapted from https://en.wikipedia.org/wiki/Koch_snowflake ) |
| 14 | + (for a more detailed explanation and an implementation in the |
| 15 | + Processing language, see https://natureofcode.com/book/chapter-8-fractals/ |
| 16 | + #84-the-koch-curve-and-the-arraylist-technique ) |
| 17 | +
|
| 18 | +Requirements (pip): |
| 19 | + - matplotlib |
| 20 | + - numpy |
| 21 | +""" |
| 22 | + |
| 23 | + |
| 24 | +from __future__ import annotations |
| 25 | + |
| 26 | +import matplotlib.pyplot as plt # type: ignore |
| 27 | +import numpy |
| 28 | + |
| 29 | +# initial triangle of Koch snowflake |
| 30 | +VECTOR_1 = numpy.array([0, 0]) |
| 31 | +VECTOR_2 = numpy.array([0.5, 0.8660254]) |
| 32 | +VECTOR_3 = numpy.array([1, 0]) |
| 33 | +INITIAL_VECTORS = [VECTOR_1, VECTOR_2, VECTOR_3, VECTOR_1] |
| 34 | + |
| 35 | +# uncomment for simple Koch curve instead of Koch snowflake |
| 36 | +# INITIAL_VECTORS = [VECTOR_1, VECTOR_3] |
| 37 | + |
| 38 | + |
| 39 | +def iterate(initial_vectors: list[numpy.ndarray], steps: int) -> list[numpy.ndarray]: |
| 40 | + """ |
| 41 | + Go through the number of iterations determined by the argument "steps". |
| 42 | + Be careful with high values (above 5) since the time to calculate increases |
| 43 | + exponentially. |
| 44 | + >>> iterate([numpy.array([0, 0]), numpy.array([1, 0])], 1) |
| 45 | + [array([0, 0]), array([0.33333333, 0. ]), array([0.5 , \ |
| 46 | +0.28867513]), array([0.66666667, 0. ]), array([1, 0])] |
| 47 | + """ |
| 48 | + vectors = initial_vectors |
| 49 | + for i in range(steps): |
| 50 | + vectors = iteration_step(vectors) |
| 51 | + return vectors |
| 52 | + |
| 53 | + |
| 54 | +def iteration_step(vectors: list[numpy.ndarray]) -> list[numpy.ndarray]: |
| 55 | + """ |
| 56 | + Loops through each pair of adjacent vectors. Each line between two adjacent |
| 57 | + vectors is divided into 4 segments by adding 3 additional vectors in-between |
| 58 | + the original two vectors. The vector in the middle is constructed through a |
| 59 | + 60 degree rotation so it is bent outwards. |
| 60 | + >>> iteration_step([numpy.array([0, 0]), numpy.array([1, 0])]) |
| 61 | + [array([0, 0]), array([0.33333333, 0. ]), array([0.5 , \ |
| 62 | +0.28867513]), array([0.66666667, 0. ]), array([1, 0])] |
| 63 | + """ |
| 64 | + new_vectors = [] |
| 65 | + for i, start_vector in enumerate(vectors[:-1]): |
| 66 | + end_vector = vectors[i + 1] |
| 67 | + new_vectors.append(start_vector) |
| 68 | + difference_vector = end_vector - start_vector |
| 69 | + new_vectors.append(start_vector + difference_vector / 3) |
| 70 | + new_vectors.append( |
| 71 | + start_vector + difference_vector / 3 + rotate(difference_vector / 3, 60) |
| 72 | + ) |
| 73 | + new_vectors.append(start_vector + difference_vector * 2 / 3) |
| 74 | + new_vectors.append(vectors[-1]) |
| 75 | + return new_vectors |
| 76 | + |
| 77 | + |
| 78 | +def rotate(vector: numpy.ndarray, angle_in_degrees: float) -> numpy.ndarray: |
| 79 | + """ |
| 80 | + Standard rotation of a 2D vector with a rotation matrix |
| 81 | + (see https://en.wikipedia.org/wiki/Rotation_matrix ) |
| 82 | + >>> rotate(numpy.array([1, 0]), 60) |
| 83 | + array([0.5 , 0.8660254]) |
| 84 | + >>> rotate(numpy.array([1, 0]), 90) |
| 85 | + array([6.123234e-17, 1.000000e+00]) |
| 86 | + """ |
| 87 | + theta = numpy.radians(angle_in_degrees) |
| 88 | + c, s = numpy.cos(theta), numpy.sin(theta) |
| 89 | + rotation_matrix = numpy.array(((c, -s), (s, c))) |
| 90 | + return numpy.dot(rotation_matrix, vector) |
| 91 | + |
| 92 | + |
| 93 | +def plot(vectors: list[numpy.ndarray]) -> None: |
| 94 | + """ |
| 95 | + Utility function to plot the vectors using matplotlib.pyplot |
| 96 | + No doctest was implemented since this function does not have a return value |
| 97 | + """ |
| 98 | + # avoid stretched display of graph |
| 99 | + axes = plt.gca() |
| 100 | + axes.set_aspect("equal") |
| 101 | + |
| 102 | + # matplotlib.pyplot.plot takes a list of all x-coordinates and a list of all |
| 103 | + # y-coordinates as inputs, which are constructed from the vector-list using |
| 104 | + # zip() |
| 105 | + x_coordinates, y_coordinates = zip(*vectors) |
| 106 | + plt.plot(x_coordinates, y_coordinates) |
| 107 | + plt.show() |
| 108 | + |
| 109 | + |
| 110 | +if __name__ == "__main__": |
| 111 | + import doctest |
| 112 | + |
| 113 | + doctest.testmod() |
| 114 | + |
| 115 | + processed_vectors = iterate(INITIAL_VECTORS, 5) |
| 116 | + plot(processed_vectors) |
0 commit comments