Welcome to Code Forum!

Join a community that supports you and your coding journey from day one. We strive to be a friendly, supportive community that empowers everyone to be better developers. By registering with us, you'll be able to discuss, share and private message with other members of our community.

SignUp Now!
  • Guest, before posting your code please take these rules into consideration:
    • It is required to use our BBCode feature to display your code. While within the editor click < / > or >_ and place your code within the BB Code prompt. This helps others with finding a solution by making it easier to read and easier to copy.
    • You can also use markdown to share your code. When using markdown your code will be automatically converted to BBCode. For help with markdown check out the markdown guide.
    • Don't share a wall of code. All we want is the problem area, the code related to your issue.

    GIF shows where to locate </> in the thread and or post editor toolbar.
    To learn more about how to use our BBCode feature, review our "How to post your code into threads" here.

    Thank you, Code Forum.

An early, rudimentary prototype for fractal growth, Diffusion-Limited Aggregation (DLA) with Python

ratwolf

Bronze Coder
Diffusion-Limited Aggregation (DLA) with Python

DLA_1.webp

Python:
import numpy as np
import matplotlib.pyplot as plt
from numba import njit, prange
from matplotlib.cm import ScalarMappable
from matplotlib.colors import Normalize

# Parameters
circle_radius = 100  # Maximum boundary of the circle
num_particles = 1000000  # Total number of particles
step_size = 1 # Size of each random walk step
max_attempts = 100000  # Maximum attempts per particle before discarding

# Initialize the seed at the center
seed = (0, 0)
aggregated_particles = [seed]

# Initialize the figure for visualization
plt.ion()
fig, ax = plt.subplots()
ax.set_xlim(-circle_radius, circle_radius)
ax.set_ylim(-circle_radius, circle_radius)
ax.set_aspect('equal')

# Set a dark background color
ax.set_facecolor('black')  # Dark background
fig.patch.set_facecolor('black')  # Dark background for the figure

# Create a colormap (e.g., 'hot' for heatmap effect)
cmap = plt.get_cmap('hot')
norm = Normalize(vmin=0, vmax=circle_radius)  # Normalize distances to [0, circle_radius]
scatter = ax.scatter(*zip(*aggregated_particles), c=[0], cmap=cmap, norm=norm, s=1)

# Add a colorbar for reference
cbar = plt.colorbar(ScalarMappable(norm=norm, cmap=cmap), ax=ax)
cbar.set_label('Distance from Center', color='white')  # White text for visibility
cbar.ax.yaxis.set_tick_params(color='white')  # White ticks for visibility
plt.setp(plt.getp(cbar.ax.axes, 'yticklabels'), color='white')  # White tick labels

# JIT-compiled functions for performance
@njit
def is_inside_circle(x, y, radius):
    """Check if a point is inside the circle."""
    return x**2 + y**2 < radius**2

@njit
def random_walk(x, y, step_size):
    """Perform a random walk in one of four cardinal directions."""
    direction_idx = np.random.randint(0, 4)  # Random index for direction
    directions = np.array([(1, 0), (-1, 0), (0, 1), (0, -1)], dtype=np.float64)
    direction = directions[direction_idx]
    return x + direction[0] * step_size, y + direction[1] * step_size

@njit
def is_near_aggregated(x, y, aggregated_particles_np, step_size):
    """Check if the particle is near any aggregated particle."""
    threshold = step_size**2 * 1.3  # Slightly larger threshold to ensure connection
    for particle in aggregated_particles_np:
        if (x - particle[0])**2 + (y - particle[1])**2 <= threshold:
            return True
    return False

# Function to compute the dynamic spawn radius
@njit
def compute_spawn_radius(aggregated_particles_np, margin=10):
    """Determine the spawn radius based on the furthest aggregated particle."""
    max_dist = 0.0
    for particle in aggregated_particles_np:
        dist = particle[0]**2 + particle[1]**2
        if dist > max_dist:
            max_dist = dist
    return np.sqrt(max_dist) + margin

# Convert aggregated_particles to a NumPy array for compatibility with Numba
aggregated_particles_np = np.array(aggregated_particles, dtype=np.float64)

# Main simulation loop
for _ in range(num_particles):
    # Dynamically compute the spawn radius
    spawn_radius = compute_spawn_radius(aggregated_particles_np)
    angle = np.random.uniform(0, 2 * np.pi)
    x, y = spawn_radius * np.cos(angle), spawn_radius * np.sin(angle)
 
    attempts = 0
    while attempts < max_attempts:
        # If the particle exits the boundary, discard it and spawn a new one
        if not is_inside_circle(x, y, circle_radius):
            break
 
        # Check if the particle is near the aggregation cluster
        if is_near_aggregated(x, y, aggregated_particles_np, step_size):
            # Add the particle to the aggregated list
            aggregated_particles_np = np.vstack((aggregated_particles_np, np.array([x, y])))
 
            # Update the visualization
            distances = np.sqrt(aggregated_particles_np[:, 0]**2 + aggregated_particles_np[:, 1]**2)
            scatter.set_offsets(aggregated_particles_np)
            scatter.set_array(distances)  # Update colors based on distance
            plt.draw()
            plt.pause(0.001)
            break
 
        # Perform the random walk
        x, y = random_walk(x, y, step_size)
        attempts += 1

# Finalize the visualization
plt.ioff()
plt.show()

Update

DLA_1.webp


Python:
import numpy as np
import matplotlib.pyplot as plt
from numba import njit
from matplotlib.animation import FuncAnimation
import time
from tqdm import tqdm

# Parameters
radius = 150
grid_size = 2 * radius + 1
num_particles = 15000
max_attempts = 150000
spawn_radius_margin = 20

# Grid setup
dla_grid = np.full((grid_size, grid_size), np.nan, dtype=np.float32)
center = grid_size // 2
dla_grid[center, center] = 0  # Seed in the center

# Figure setup
fig, ax = plt.subplots()
ax.set_xticks([])
ax.set_yticks([])
ax.set_facecolor("black")
fig.patch.set_facecolor("black")

# Colormap
plasma_cmap = plt.get_cmap("plasma")
plasma_cmap.set_bad(color="black")  # NaN areas are black
im = ax.imshow(dla_grid, cmap=plasma_cmap, origin="lower", vmin=0, vmax=grid_size // 2)
cbar = plt.colorbar(im, ax=ax)
cbar.set_label("Euclidean Distance from Center", color="white")
cbar.ax.yaxis.set_tick_params(color="white")
plt.setp(plt.getp(cbar.ax.axes, "yticklabels"), color="white")

@njit(fastmath=True)
def clamp(value, min_value, max_value):
    return max(min_value, min(value, max_value))

@njit(fastmath=True)
def is_inside_circle(x, y, radius):
    return x * x + y * y < radius * radius

@njit(fastmath=True)
def random_walk(x, y, grid_size):
    # Precompute movement directions
    direction_table = np.array([
        (1, 0), (-1, 0), (0, 1), (0, -1),
        (1, 1), (-1, 1), (1, -1), (-1, -1)
    ], dtype=np.int8)
    dx, dy = direction_table[np.random.randint(8)]
    return clamp(x + dx, 0, grid_size - 1), clamp(y + dy, 0, grid_size - 1)

@njit
def is_near_aggregated(x, y, grid):
    """Vectorized check for neighboring aggregated pixels."""
    neighbors = grid[max(0, x - 1):min(grid.shape[0], x + 2),
                     max(0, y - 1):min(grid.shape[1], y + 2)]
    return np.any(neighbors >= 0)

@njit
def compute_spawn_radius(grid, center, margin):
    nonzero_x, nonzero_y = np.where(~np.isnan(grid))
    if len(nonzero_x) == 0:
        return margin
    max_dist_sq = np.max((nonzero_x - center) ** 2 + (nonzero_y - center) ** 2)
    return np.sqrt(max_dist_sq) + margin

@njit(fastmath=True)
def compute_euclidean_distance_sq(x, y, center):
    """Return squared Euclidean distance to avoid unnecessary sqrt calls."""
    return (x - center) ** 2 + (y - center) ** 2
    
@njit(fastmath=True)
def spawn_particle(center, spawn_radius, grid_size):
    """Spawn a particle near the computed radius."""
    angle = np.random.uniform(0, 2 * np.pi)
    x = int(center + spawn_radius * np.cos(angle))
    y = int(center + spawn_radius * np.sin(angle))
    return clamp(x, 0, grid_size - 1), clamp(y, 0, grid_size - 1)

@njit(fastmath=True)
def perform_random_walk(x, y, center, grid, max_attempts):
    """Perform a random walk until aggregation or max attempts reached."""
    attempts = 0
    while attempts < max_attempts:
        if not is_inside_circle(x - center, y - center, grid.shape[0] // 2):
            return None
        if is_near_aggregated(x, y, grid):
            return x, y
        x, y = random_walk(x, y, grid.shape[0])
        attempts += 1
    return None

# Initialize progress bar
pbar = tqdm(total=num_particles)

# Animation update function
def update(frame):
    # Compute dynamic spawn radius
    spawn_radius = compute_spawn_radius(dla_grid, center, spawn_radius_margin)
    spawn_radius = min(spawn_radius, grid_size // 2 - 1)

    # Spawn and simulate a particle
    x, y = spawn_particle(center, spawn_radius, grid_size)
    result = perform_random_walk(x, y, center, dla_grid, max_attempts)

    if result is not None:
        x, y = result
        color = np.sqrt(compute_euclidean_distance_sq(x, y, center))
        dla_grid[x, y] = color  # Update the data matrix

        # Directly update the pixel buffer instead of redrawing the whole image
        im_array = im.get_array()
        im_array[x, y] = color
        im.set_array(im_array)  # Apply the change

    # Update progress bar
    pbar.update(1)

    # Stop animation when complete
    if frame == num_particles - 1:
        pbar.close()
        print(f"Simulation complete! Time taken: {time.time() - start_time:.2f} seconds.")

start_time = time.time()

# Run animation
ani = FuncAnimation(fig, update, frames=num_particles, interval=1, repeat=False)
plt.show()
 
Last edited:

Buy us a coffee!

Buy me a coffee.
Back
Top Bottom