L-Systems

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 length a at the current position”
  • 1 means “draw a line of length a at the current position”
  • [ means “store the current position, angle and length, turn by an angle alpha and reduce a by a factor l
  • ] means “retrieve latest position, angle and length, turn by an angle - alpha and reduce a by a factor l

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:

Last changed | authored by

Comments


← Back to Notes