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:
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.
Code:
fractal_tree.py
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)
angles.append(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])
levels.append(next_level)
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)
ax.set_axis_off()
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)
ax.add_collection(collection)
else:
for i, level in enumerate(levels):
collection = LineCollection(level, edgecolors=colors, lw=iterations-i)
ax.add_collection(collection)
plt.axis('scaled')
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
fig.canvas.draw()
image = Image.frombytes(
'RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
if not show:
plt.close(fig)
else:
plt.show()
return image
The other creates Pythagoras trees:
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.
Code:
pythagorean_tree.py
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)
squares.append(square1)
squares.append(square2)
colors.append(color_values[i%num_colors])
colors.append(color_values[i%num_colors])
next_vertices.append(square1[-2:][::-1])
next_vertices.append(square2[-2:][::-1])
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)
ax.set_axis_off()
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)
ax.add_collection(collection)
plt.axis('scaled')
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
fig.canvas.draw()
image = Image.frombytes(
'RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
if not show:
plt.close(fig)
else:
plt.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.
plt.savefig
because I usePIL
to do image processing. \$\endgroup\$