- tags
- Complex Systems
L-systems are string re-writing systems. They operate on an alphabet of symbols, rewriting symbols or patterns of symbols into new patterns according to a set of rules.
Examples
Fractal tree
A simple binary tree can be constructed with a L-system, using the rule (1 → 11), (0 → 1[0]0)
and starting from a single 0
.
This could be then drawn by a turtle drawer, where :
0
means “draw a leaf line of lengtha
at the current position”1
means “draw a line of lengtha
at the current position”[
means “store the current position, angle and length, turn by an anglealpha
and reducea
by a factorl
”]
means “retrieve latest position, angle and length, turn by an angle- alpha
and reducea
by a factorl
”
We start by defining a few helper functions that will help us manipulate the lines and generate children lines from parent lines with the correct rotations.
This could be coded in a recursive way, but since Python is not very suitable for this kind of programming anyways, we program it non-recursively. The drawing is not done like a turtle but line by line which was more convinient to implement.
""" This code generates a L-System tree fractal with chosen parameters.
In the following, a line is represented as a numpy array:
[[x0, y0], [x1, y1]]
"""
import numpy as np
import matplotlib.pyplot as plt
def shorten(line: np.ndarray, length: float) -> np.ndarray:
""" Shorten a line by some fixed amount (length is a number between
0 and 1). Returns the shortened line.
"""
return np.array([line[0], (1 - length) * line[0] + length * line[1]])
def rotate(line: np.ndarray, alpha: float):
""" Rotate a line around its first point by an angle alpha (in radians).
Returns the rotated line.
"""
# Center line on origin
origin_line = line - line[0]
# Rotation matrix with angle alpha around origin
rot_matrix = np.array(
[[np.cos(alpha), -np.sin(alpha)],
[np.sin(alpha), np.cos(alpha)]]
)
# Rotate just the second point since the first is origin.
origin_line[1] = rot_matrix @ origin_line[1].T
return origin_line + line[0]
def noise_factor(noise: float) -> float:
""" Returns a factor of the form 1 + noise(). Noise amount can be
scaled.
"""
return 1 + noise * (2 * np.random.random() - 1)
def gen_new(line, alpha, length=0.5, noise=0.0):
""" Generate a new pair of line from a base one. Lines are rotated
and shortened according to `alpha` and `length`. Rotations and
shortenings can be perturbed with the `noise` parameter.
"""
short_line = (
shorten(line, length * noise_factor(noise)) - line[0]
)
rot1 = (
rotate(short_line, alpha * noise_factor(noise)) + line[1]
)
rot2 = (
rotate(short_line, -alpha * noise_factor(noise)) + line[1]
)
return [rot1, rot2]
Now we can generate a beautiful tree and play with the parameters:
# Base line. Vertical, centered on x=0.5
x0 = np.array([[0.5, 0], [0.5, 0.5]])
lines = [ [x0] ]
# The angle can be changed
alpha = np.pi / 5
plt.figure(figsize=(5, 5))
# Number of generations can be tuned. Warning, exponential
# time in the number of gens.
n_generations = 11
for g in range(n_generations):
new_lines = []
base = lines[-1]
for previous_line in base:
# Tune parameters
new_lines += gen_new(previous_line, alpha, 0.62, 0.5)
lines.append(new_lines)
# Plot all the lines with decreasing width
for n in range(len(lines)):
for l in lines[n]:
plt.plot(
*l.T,
color="#43A6C6",
linewidth=(0.92) ** n,
)
plt.tight_layout()
plt.axis("off")
plt.savefig("l-system.svg")
This results in the following image: