Python Forum
Rudimentary prototype for: Fractal Growth Model, Diffusion-Limited Aggregation (DLA)
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Rudimentary prototype for: Fractal Growth Model, Diffusion-Limited Aggregation (DLA)
#1
   

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

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()
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  a rudimentary caesar wheel mepyyeti 1 3,474 Apr-12-2018, 09:55 AM
Last Post: buran

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020