I wrote the scripts several days ago, they do exactly what I intended, but the performance is not very good, I am still stuck on for loops and don't know how to vectorize things, I wrote both scripts entirely by myself.

One script creates Fractal Canopy:

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

enter image description here

Step by step on how the coordinates are calculated:

First, you start with parameters iterations, span, branches, ratio=0.75, unit=1 and initial_branches=1.

iterations determines how many levels of branches to calculate, span determines the angular span of branches at each branching point, branches determines the number of branches that split from a branching point, ratio determines the scaling factor and 0 < ratio < 1, unit determines the length of the initial branches (it does not have any effect on the plot whatsoever, but setting it larger may allow more iterations before hitting arithmetic underflow), and initial_branches determines number of first level branches that spawns from origin.

You start at the origin (0, 0), then you construct initial_branches vectors of size unit from the origin, with each pair of branches forming $$\alpha = \frac{2\pi} {n}$$ (n = initial_branches, \_ doesn't work here)

Then all the vertices are:

$$\{(\upsilon \cdot cos(\frac{\pi} {2} + i \cdot \alpha), \upsilon \cdot sin(\frac{\pi} {2} + i \cdot \alpha)) | 0 <= i < n \ and \ i \in Z\}$$ $$\upsilon = unit$$.

For each vertex, collect the vertex and create a two element tuple with the origin as the first element and the point as the second element and put all of them in a stack.

The above is iteration 0 or the first iteration.

In each subsequent iteration, a new stack is created.

Then, the angle between each pair of subbranches is $$\beta = \frac{\sigma} {s - 1}$$

$$\sigma = span, s = branches$$

The angular difference between each subbranch and the parent branch is:

$$\Delta_\alpha = \{ \beta \cdot i - \frac{\sigma} {2} | 0 <= i < s \ and \ i \in Z \}$$

For each recursive iteration i:

length of vectors is:

$$\lambda = \upsilon * f ^ i, f = ratio$$

For each pair of points from the stack from the previous iteration, let the starting point be $$(x_1, y_1)$$ and the ending point be $$(x_2, y_2)$$, construct a vector from the ending point for each required angular difference and get its ending point:

$$\{(\frac{(x_2 - x_1) \cdot cos(\gamma) \cdot \lambda} {\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}} - \frac{(y_2 - y_1) \cdot sin(\gamma) \cdot \lambda} {\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}} + x_2, \frac{(y_2 - y_1) \cdot cos(\gamma) \cdot \lambda} {\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}} + \frac{(x_2 - x_1) \cdot sin(\gamma) \cdot \lambda} {\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}} + y_2) | \gamma \in \Delta_\alpha \}$$

For each point, let the point be $$(x_3, y_3)$$, collect the point and add $$((x_2, y_2), (x_3, y_3))$$ to the current stack.

At the end of the iteration, rebind the name that points to the previous stack to the current stack.

Do this iteratively, use the output of the previous iteration as input of current iteration.



import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection
from PIL import Image

def cos(d): return np.cos(np.radians(d))
def sin(d): return np.sin(np.radians(d))
def tan(d): return np.tan(np.radians(d))

def next_node(pos1, pos2, cosa, sina, length):
    x1, y1 = pos1
    x2, y2 = pos2
    dx = x2 - x1
    dy = y2 - y1
    r = (dx*dx + dy*dy)**.5
    L = length / r
    cosaL, sinaL = cosa*L, sina*L
    x3 = dx * cosaL - dy * sinaL + x2
    y3 = dy * cosaL + dx * sinaL + y2
    return (x3, y3)

def fractal_tree(iterations, span, branches, ratio=0.75, unit=1, initial_branches=1):
    assert branches >= 2
    assert initial_branches >= 1
    half = span / 2
    angles = [-half]
    diff = span / (branches - 1)
    for i in range(1, branches - 1):
        angles.append(diff * i - half)
    trigos = [(cos(angle), sin(angle)) for angle in angles]
    segments = [[(0, 0), (0, unit)]]
    if initial_branches == 2:
        segments.append([(0, 0), (0, -unit)])
    elif initial_branches > 2:
        rotation = 360 / initial_branches
        for i in range(1, initial_branches):
            segments.append([(0, 0), (unit*cos(90+i*rotation), unit*sin(90+i*rotation))])
    cur_level = segments.copy()
    levels = [cur_level]
    for i in range(1, iterations):
        next_level = []
        L = unit*ratio**i
        for pos1, pos2 in cur_level:
            for cosa, sina in trigos:
                pos3 = next_node(pos1, pos2, cosa, sina, L)
                segments.append([pos2, pos3])
                next_level.append([pos2, pos3])
        cur_level = next_level
    return {'segments': segments, 'levels': levels}

def plot_fractal_tree(iterations, span, branches, ratio=0.75, unit=1, initial_branches=1, width=1920, height=1080, show=True, random_colors=True, default_color="#875cff", control_width=False):
    fig = plt.figure(figsize=(width/100, height/100),
                     dpi=100, facecolor='black')
    ax = fig.add_subplot(111)
    segments, levels = fractal_tree(iterations, span, branches, ratio, unit, initial_branches).values()
    colors = default_color
    if random_colors:
        colors = np.random.random((len(segments), 3))
    if not control_width:
        collection = LineCollection(segments, edgecolors=colors)
        for i, level in enumerate(levels):
            collection = LineCollection(level, edgecolors=colors, lw=iterations-i)
    fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
    image = Image.frombytes(
        'RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
    if not show:
    return image

The other creates Pythagoras trees:

enter image description here

enter image description here

enter image description here

enter image description here

The general idea of the script is same, at iteration 0 you set some things, you create a stack, then at each subsequent iteration you create a stack, using the stack from the previous iteration as input, compute the result collect it and push the result into the stack, you then use the stack from current iteration as input to next iteration.

In this script the relevant parameter beside iterations is angle.

The angle determines the left acute angle (counter-clockwise) in the right triangle formed by the parent square and two child squares.

In iteration 0 you collect $$\{(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)\}$$ and put the last two points in stack.

In iterations from 1 on you compute the third vertex of the right triangle whose left acute angle is angle:

\begin{align} x_2 = (x_1 - x_0) \cdot cos(\alpha)^2 - (y_1 - y_0) \cdot cos(\alpha) \cdot sin(\alpha) + x_0 \\ y_2 = (y_1 - y_0) \cdot cos(\alpha)^2 + (x_1 - x_0) \cdot cos(\alpha) \cdot sin(\alpha) + y_0 \\ \alpha = angle \end{align}

In iteration 1:

$$(x_0, y_0) = (-0.5, 0.5), (x_1, y_1) = (0.5, 0.5)$$

Formulas to construct a square given two points as adjacent vertices counter-clockwise:

\begin{align} \delta_x = x_2 - x_1 \\ \delta_y = y_2 - y_1 \\ x_3 = x_1 - \delta_y \\ y_3 = y_1 + \delta_x \\ x_4 = x_3 + \delta_x \\ y_4 = y_3 + \delta_y \end{align}

The vertices are:

$$\{(x_1, y_1), (x_2, y_2), (x_4, y_4), (x_3, y_3)\}$$

(Order of the vertices are important)

In each iteration after iteration 0, create a new stack, for each pair of points from the previous stack, compute the third vertex, compute the square vertices for the first vertex and third vertex, collect the square, put the last two vertices in the square to the current stack, then do the same for the third vertex and second vertex.

At end of iteration, rebind names.



import math
import numpy as np
import random
import matplotlib.pyplot as plt
from matplotlib.collections import PolyCollection
from PIL import Image

def sin(d): return math.sin(math.radians(d))
def cos(d): return math.cos(math.radians(d))
def tan(d): return math.tan(math.radians(d))
def atan(d): return math.degrees(math.atan(d))

def spectrum_position(n, string=True):
    if not isinstance(n, int):
        raise TypeError('`n` should be an integer')
    if n < 0:
        raise ValueError('`n` must be non-negative')
    n %= 1530
    if 0 <= n < 255:
        return (255, n, 0) if not string else f'ff{n:02x}00'
    elif 255 <= n < 510:
        return (510-n, 255, 0) if not string else f'{510-n:02x}ff00'
    elif 510 <= n < 765:
        return (0, 255, n-510) if not string else f'00ff{n-510:02x}'
    elif 765 <= n < 1020:
        return (0, 1020-n, 255) if not string else f'00{1020-n:02x}ff'
    elif 1020 <= n < 1275:
        return (n-1020, 0, 255) if not string else f'{n-1020:02x}00ff'
    elif 1275 <= n < 1530:
        return (255, 0, 1530-n) if not string else f'ff00{1530-n:02x}'

def make_square(pos1, pos2):
    x1, y1 = pos1
    x2, y2 = pos2
    dx = x2 - x1
    dy = y2 - y1
    x3 = x1 - dy
    y3 = y1 + dx
    x4 = x3 + dx
    y4 = y3 + dy
    return [pos1, pos2, (x4, y4), (x3, y3)]

def pythagoras_tree(iterations, angle=30, unit=1, num_colors=12, color_start=None):
    assert 0 < angle < 90
    cosa = cos(angle)
    cosa2 = cosa * cosa
    cossina = cosa * sin(angle)
    def third_vertex(pos1, pos2):
        x1, y1 = pos1
        x2, y2 = pos2
        dx, dy = x2 - x1, y2 - y1
        x3 = dx * cosa2 - dy * cossina + x1
        y3 = dy * cosa2 + dx * cossina + y1
        return x3, y3
    if color_start is None:
        color_start = random.random() * 1530
    color_step = 1530 / num_colors
    color_values = ['#'+spectrum_position(round(color_start+color_step*i)) for i in range(num_colors)]
    colors = [color_values[0]]
    half = unit/2
    square = [(-half, half), (half, half), (half, -half), (-half, -half)]
    cur_vertices = [square[:2]]
    squares = [square]
    for i in range(1, iterations):
        next_vertices = []
        for pos1, pos2 in cur_vertices:
            other = third_vertex(pos1, pos2)
            square1 = make_square(pos1, other)
            square2 = make_square(other, pos2)
        cur_vertices = next_vertices
    return {'square': squares, 'colors': colors}

def plot_pythagoras_tree(iterations, angle=30, unit=1, num_colors=12, alpha=1, color_start=None, random_colors=False, width=1920, height=1080, show=True):
    fig = plt.figure(figsize=(width/100, height/100),
                     dpi=100, facecolor='black')
    ax = fig.add_subplot(111)
    squares, colors = pythagoras_tree(iterations, angle, unit, num_colors, color_start).values()
    colors = colors[::-1]
    if random_colors:
        colors = np.random.rand(len(squares), 3)
    collection = PolyCollection(squares[::-1], facecolors=colors, alpha=alpha)
    fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
    image = Image.frombytes(
        'RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
    if not show:
    return image

How can the scripts be improved, more specifically how do I vectorize it? Please do post answers, that is how I can improve my programming skills now, I am currently writing autobiographical blog posts (not currently posted, will post once complete), and I will prominently feature all my scripts in the blogs, maybe it will help me find jobs as a programmer.

I really do care about all my scripts, but I really can't improve my programming skills further any other way, my programming skills are way beyond what all those online simple and inefficient tutorials aimed at complete beginners teach, and I don't have money to attend programming courses and I doubt they will teach me much, but I know my skills are far from those of the best professional programmers, however I guess I am better than 75% percent of all programmers, but I really am the only one I personally know with my level of programming skills (or any knowledge about computers at all).

So please do help me improve.

  • \$\begingroup\$ I don't use plt.savefig because I use PIL to do image processing. \$\endgroup\$ Jun 16 at 10:42
  • 2
    \$\begingroup\$ I strongly recommend adding periods to your paragraph to create sentences. I also feel like the post might be more approachable split into two: one for each script, although I understand they're related. \$\endgroup\$
    – ggorlen
    Jun 16 at 14:23


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.