Modeling the Behaviors of Hydrogen

Presenting Elliot’s Hydrogen Fusion Simulator, V1.0.0

It is fairly well accepted that the future of energy will be nuclear. The concept of a Dyson Sphere was first developed by Olaf Stapledon in his science fiction novel Star Maker (1937). The idea was further developed by the physicist Freeman Dyson in his 1960 paper “Search for Artificial Stellar Sources of Infrared Radiation“. Dyson speculated that such structures would be the logical consequence of the escalating energy needs of a technological civilization and would be a necessity for its long-term survival; the light signatures of such structures surrounding stars could be an indicator of extraterrestrial intelligence.

Other hypothetical mega structures include ring worlds, such as the one seen in in the film Elysium, and Alderson disks both of which would harness the energies of the sun.

While this is a possible path for an advanced civilization to take, it would undoubtedly have a negative effect on the on the home world, especially on a water world such as our own. Or, perhaps it could allow for the civilization to control the home world’s atmosphere and radiation levels at a more granular level, through containing and perhaps even sustaining the fusion processes of the Sun with powerfully reflective and absorbent magnetic fields.

proton_proton_reaction_chain
proton_proton_reaction_chain

Fusion is a gateway to the Stars

Harnessing the power of fusion is an obvious next step for humanity. As we progress scientifically, capturing the massive amounts of energy from the sun are becoming more and more of a priority. The concept of a Dyson sphere, or solar containment structure is all too wonderful; to be able to harness and manipulate the most powerful energy source we can currently explain. Black hole radiation is still poorly understood, as we cannot explain how a body that traps all light and energy could also release massive radiation, so we will set aside this possibility for a bit.

Understanding the Solar Environment

The first task for achieving progress towards any of these technological advancements we must first understand the nature of the Sun. The Sun creates energy through the fusion process known as the Proton-Proton chain where alpha particles are released and hydrogen fuses to become Deuterium, Tritium, and then Helium, which takes a tremendous amount of time. Since the conversion of hydrogen to helium is slow, the complete conversion of the hydrogen initially in the core of the Sun is calculated to take more than ten billion years.[5]

Every second the Sun releases about 3.8 x 10²⁶ joules. That means a second of energy from the Sun could power all of the electricity on planet Earth for 180 years or so. The Sun emits approximately 560 trillion times more energy in a year than Earth consumes. So obviously the Dyson sphere would be very useful for any type of advanced civilization.

However, it maybe be more practical; at least here on Earth, to create an energy source that is similar to the sun. The break-even point of such a reaction would be tremendous, as the plasma fusion process happens at millions of degrees. ITER’s Tokamak design is planned to achieve around 150 million degrees and will hopefully prove out that the concept is viable. There are other fusion reactor design that could also be possible, and more effective at energy production, such as the stellarator design, or spheromaks, but currently the Tokamak design is expected to reveal the technical limitations and bottlenecks for the fusion process.

ITER will demonstrate this through the magnetic confinement, inertial confinement, and advanced fuels necessary to achieve fusion. The magnetic confinement is very difficult and requires advanced materials.

Simulating Fusion Processes

In order to simulate fusion, the properties and behaviors of Hydrogen, the fuel of the sun, must be explored. Patterning the release of tritium, deuterium, ionized hydrogen particles and electrons will be a key to understand how to create a plasma wind that can create large amounts of energy through the fusion process.

Using Numpy, Numba and Pytorch, in a pygame environment, I have create a machine learning model for understand the particle behaviors in a fusion reactor. The simulation utilizes the concept of ideal confinement, that is to say that the magnetic repulsion barrier for the particles is 100% effective. Obviously in any real environment, especially those on Earth, this would be a far less efficient and would require containment physics, which are not present in this simulation (for obvious reasons once you see the size of the code base).

Imported libraries for the Sim

import numpy as np
import pygame
from numba import njit, prange
import sys
import torch
import torch.nn as nn
import torch.optim as optim
from scipy.interpolate import interp1d

Constants for the Hydrogen Plasma Simulator

E_CHARGE = 1.60217662e-19       # Elementary charge (C)
E_MASS = 9.10938356e-31         # Electron mass (kg)
D_MASS = 3.343583719e-27        # Deuteron mass (kg)
T_MASS = 5.0073567446e-27       # Triton mass (kg)
HE_MASS = 6.644657230e-27       # Alpha particle mass (kg)
NEUTRON_MASS = 1.674927471e-27  # Neutron mass (kg)
EPS_0 = 8.854187817e-12         # Vacuum permittivity (F/m)
MU_0 = 4e-7 * np.pi             # Vacuum permeability (H/m)
K_B = 1.380649e-23              # Boltzmann constant (J/K)
EV_TO_J = 1.602176634e-19       # Electronvolt to joules conversion

Variables to be Adjusted depending on your Computer


# ------------------------
# Simulation Parameters
# ------------------------
INCLUDE_ELECTRONS = True

# Particle counts
NUM_DEUTERIUM = 50
NUM_TRITIUM = 50
NUM_ELECTRONS = 100 if INCLUDE_ELECTRONS else 0
NUM_PARTICLES = NUM_DEUTERIUM + NUM_TRITIUM + NUM_ELECTRONS

# Domain and grid
DOMAIN_SIZE = 1e-4             # Domain size (m)
GRID_SIZE = 100                # Number of grid cells in each dimension
DX = DOMAIN_SIZE / GRID_SIZE   # Grid spacing (m)

# Time parameters
DT = 1e-10                     # Time step (s)
NUM_STEPS = 5000               # Number of simulation steps

# Plasma parameters
TEMPERATURE = 1e6              # Initial plasma temperature (K)

# Magnetic field
B0 = 5.0                       # Magnetic field strength (T)

# Fusion parameters
FUSION_SCALING_FACTOR = 1e30    # Increased scaling factor for fusion probability

# Collision parameters
R_COLL = DOMAIN_SIZE / GRID_SIZE * 2.0  # Collision radius (m)
R_MAG = DOMAIN_SIZE / GRID_SIZE * 1.0   # Magnetic interaction radius (m)

# Visualization parameters
SCREEN_SIZE = 800              # Screen size in pixels
PARTICLE_SIZE = 4              # Particle radius in pixels
FPS = 60                       # Frames per second

# Runtime Warning Fix Parameters
MIN_DISTANCE = 1e-12           # Minimum distance to prevent division by zero
MAX_FORCE = 1e5                # Maximum allowed force magnitude

Key Features of the Simulation

There are many functions in this simulation, I was really pushing my M1 to its limits. The particle count could be scaled up as well as the fusion probabilities if I had say a nice and powerful GPU supported super-computer to test on.

  • Particle Behavior simulation within a magnetically confined environment
  • Particle Trails for understand the behavior of higher energy particles and to display velocities
  • Machine Learning for fusion event probabilities an associated energy conversions
  • Total Environment Charge calculations
  • Particle Velocity and Mass Computations
  • Particle Additions with clicks/keyboard Strokes
    • d key / Left Mouse click – add deuterium particle
    • t key / middle mouse click – add tritium particle
    • e key / right mouse click – add electron particle
  • Field and Convergence calculations
  • Spatial Particle Collision calculations
  • Alpha particle behaviors
  • Neutron particle behaviors
  • Fusion Event Counter

Boris Push Function –

a leapfrog scheme, where the particle is moved, then half of the energy changed is applied. After this, the momentum is rotated by the B field, and the rest of the energy change applied.

def boris_push(positions, velocities, charges, masses, Ex, Ey, Bz, dt, grid_size, domain_size):
    num_particles = positions.shape[0]
    for i in prange(num_particles):
        # Get particle's grid indices
        x_idx = int(positions[i, 0] / domain_size * grid_size)
        y_idx = int(positions[i, 1] / domain_size * grid_size)
        
        # Clamp indices to grid boundaries
        if x_idx >= grid_size:
            x_idx = grid_size - 1
        elif x_idx < 0:
            x_idx = 0
        if y_idx >= grid_size:
            y_idx = grid_size - 1
        elif y_idx < 0:
            y_idx = 0
        
        # Get electric and magnetic fields at particle's position
        E_particle = np.array([Ex[x_idx, y_idx], Ey[x_idx, y_idx]])
        B_particle_z = Bz[x_idx, y_idx]  # Magnetic field in z-direction
        
        # Charge-to-mass ratio
        qm = charges[i] / masses[i]
        
        # Half acceleration due to electric field
        velocities[i] += qm * E_particle * (dt / 2.0)
        
        # Rotation due to magnetic field (B only in z)
        t = qm * B_particle_z * (dt / 2.0)
        s = 2.0 * t / (1.0 + t * t)
        v_minus = velocities[i]
        
        # Cross product in 2D (magnetic field only in z, velocities in x and y)
        v_prime_x = v_minus[0] + v_minus[1] * t
        v_prime_y = v_minus[1] - v_minus[0] * t
        v_prime = np.array([v_prime_x, v_prime_y])
        
        velocities[i] = v_minus + s * v_prime
        
        # Half acceleration due to electric field
        velocities[i] += qm * E_particle * (dt / 2.0)
        
        # Update positions
        positions[i] += velocities[i] * dt
        
        # Reflective boundary conditions with clamping
        for dim in range(2):  # x and y
            if positions[i, dim] < 0:
                positions[i, dim] = -positions[i, dim]
                velocities[i, dim] = -velocities[i, dim]
            elif positions[i, dim] > domain_size:
                positions[i, dim] = 2 * domain_size - positions[i, dim]
                velocities[i, dim] = -velocities[i, dim]
        
        # **Explicit Clamping (Additional Safeguard)**
        positions[i, 0] = min(max(positions[i, 0], 0.0), domain_size)
        positions[i, 1] = min(max(positions[i, 1], 0.0), domain_size)
        
    return positions, velocities

Particle Pairing through Magnetic Forces

def compute_pairwise_magnetic_forces(positions, velocities, charges, masses, R_mag, mu_0, dt):
    """
    Compute pairwise magnetic forces between charged particles.
    Simplified 2D magnetic force calculation.
    Returns:
        F_mag: Array of shape (num_particles, 2) representing magnetic forces.
    """
    num_particles = positions.shape[0]
    F_mag = np.zeros((num_particles, 2), dtype=np.float64)
    for i in prange(num_particles):
        for j in range(i + 1, num_particles):
            r_vec = positions[j] - positions[i]
            distance = np.linalg.norm(r_vec)
            if distance < R_mag and distance > MIN_DISTANCE:
                # Simplified magnetic force calculation
                v1 = velocities[i]
                v2 = velocities[j]
                v_rel = v1 - v2
                r_hat = r_vec / distance
                # Relative velocity perpendicular component
                v_rel_perp = v_rel - np.dot(v_rel, r_hat) * r_hat
                # Force magnitude
                F = (mu_0 / (4 * np.pi)) * (charges[i] * charges[j] * np.linalg.norm(v_rel_perp)) / (distance ** 2)
                
                # Clamp force to prevent overflow
                if F > MAX_FORCE:
                    F = MAX_FORCE
                elif F < -MAX_FORCE:
                    F = -MAX_FORCE
                
                # Direction: perpendicular to r_vec
                perp_dir = np.array([-r_hat[1], r_hat[0]])
                F_vector = F * perp_dir * dt
                F_mag[i] += F_vector
                F_mag[j] -= F_vector  # Newton's third law
    return F_mag

def identify_significant_pairs(F_mag, positions, velocities, charges, species, threshold=1e-25):
    """
    Identify particle pairs experiencing significant magnetic forces.
    Args:
        F_mag (np.ndarray): Array of shape (num_particles, 2) representing magnetic forces.
        positions (np.ndarray): Array of shape (num_particles, 2) representing positions.
        velocities (np.ndarray): Array of shape (num_particles, 2) representing velocities.
        charges (np.ndarray): Array of shape (num_particles,) representing charges.
        species (np.ndarray): Array of shape (num_particles,) representing species.
        threshold (float): Threshold for force magnitude to consider significant.
    Returns:
        significant_pairs (list of tuples): List of particle index pairs with significant forces.
    """
    significant_pairs = []
    num_particles = positions.shape[0]
    for i in range(num_particles):
        for j in range(i + 1, num_particles):
            # Compute force magnitude between particles i and j
            r_vec = positions[j] - positions[i]
            distance = np.linalg.norm(r_vec)
            if distance < R_MAG and distance > MIN_DISTANCE:
                # Simplified magnetic force calculation (same as in Numba function)
                v_rel = velocities[i] - velocities[j]
                if distance > 0:
                    r_hat = r_vec / distance
                else:
                    r_hat = np.array([0.0, 0.0])
                v_rel_perp = v_rel - np.dot(v_rel, r_hat) * r_hat
                F = (MU_0 / (4 * np.pi)) * (charges[i] * charges[j] * np.linalg.norm(v_rel_perp)) / (distance ** 2)
                if F > threshold:
                    significant_pairs.append((i, j))
    return significant_pairs

Particle Fusion Simulation Parameters

def attempt_fusion_spatial(positions, velocities, charges, masses, species, dt, grid_size, domain_size):
    fusion_events = []
    
    # Calculate cell indices for all particles
    cell_indices_x = (positions[:, 0] / domain_size * grid_size).astype(np.int32)
    cell_indices_y = (positions[:, 1] / domain_size * grid_size).astype(np.int32)
    
    # Clamp indices to grid boundaries
    cell_indices_x = np.clip(cell_indices_x, 0, grid_size - 1)
    cell_indices_y = np.clip(cell_indices_y, 0, grid_size - 1)
    
    # Spatial grid for efficient collision detection
    grid = {}
    for idx in range(len(positions)):
        key = (cell_indices_x[idx], cell_indices_y[idx])
        if key in grid:
            grid[key].append(idx)
        else:
            grid[key] = [idx]
    
    # Iterate through grid cells and their neighbors for potential fusion
    for key, particle_indices in grid.items():
        neighbors = []
        x, y = key
        for dx_cell in [-1, 0, 1]:
            for dy_cell in [-1, 0, 1]:
                neighbor_key = (x + dx_cell, y + dy_cell)
                if neighbor_key in grid:
                    neighbors.extend(grid[neighbor_key])
        for i in particle_indices:
            for j in neighbors:
                if j <= i:
                    continue  # Avoid duplicate checks
                # Check if one is deuterium and the other is tritium
                if ((species[i] == 0 and species[j] == 1) or
                    (species[i] == 1 and species[j] == 0)):
                    # Calculate distance
                    distance = np.linalg.norm(positions[i] - positions[j])
                    if distance > R_COLL:
                        continue  # Beyond collision radius
                    # Calculate relative kinetic energy (J)
                    relative_velocity = compute_relative_velocity(velocities[i], velocities[j])
                    kinetic_energy_J = 0.5 * masses[i] * relative_velocity**2  # Approximation
                    
                    # Get fusion cross-section (m^2)
                    sigma = get_fusion_cross_section(kinetic_energy_J)
                    
                    # Calculate fusion probability
                    P_fusion = sigma * relative_velocity * dt
                    P_fusion *= FUSION_SCALING_FACTOR  # Artificial scaling
                    
                    # Clamp probability to [0,1]
                    P_fusion = min(P_fusion, 1.0)
                    
                    # Determine if fusion occurs
                    if np.random.rand() < P_fusion:
                        fusion_events.append((i, j))
    
    print(f"Fusion Events This Step: {len(fusion_events)}")
    return fusion_events

def process_fusion_events(fusion_events, positions, velocities, charges, masses, species, particle_trails):
    if not fusion_events:
        return positions, velocities, charges, masses, species, particle_trails, 0

    fusion_count = 0
    removed_indices = set()
    new_positions = []
    new_velocities = []
    new_charges = []
    new_masses = []
    new_species = []
    new_trails = []

    for (i, j) in fusion_events:
        if i in removed_indices or j in removed_indices:
            continue  # Skip if already processed

        # Mark reactants for removal
        removed_indices.add(i)
        removed_indices.add(j)

        # Calculate the midpoint position for fusion products
        mid_pos = (positions[i] + positions[j]) / 2.0

        # Calculate total momentum before fusion
        total_momentum = masses[i] * velocities[i] + masses[j] * velocities[j]

        # Energy distribution (simplified)
        energy_alpha = 8.8e6 * EV_TO_J    # Energy to alpha particle
        energy_neutron = 8.8e6 * EV_TO_J  # Energy to neutron

        # Velocity calculations based on kinetic energy
        v_alpha = np.sqrt(2 * energy_alpha / HE_MASS)
        v_neutron = np.sqrt(2 * energy_neutron / NEUTRON_MASS)

        # Assign random direction for alpha particle
        theta = np.random.uniform(0, 2 * np.pi)
        alpha_velocity = v_alpha * np.array([np.cos(theta), np.sin(theta)])

        # Neutron velocity ensuring momentum conservation
        neutron_velocity = (total_momentum - HE_MASS * alpha_velocity) / NEUTRON_MASS

        # Ensure mid_pos is within domain
        mid_pos = np.clip(mid_pos, 0.0, DOMAIN_SIZE)

        # Append new fusion products
        # Alpha Particle: Cyan (0,255,255)
        new_positions.append(mid_pos)
        new_velocities.append(alpha_velocity)
        new_charges.append(2 * E_CHARGE)      # Alpha particle charge
        new_masses.append(HE_MASS)
        new_species.append(3)                  # Alpha Particle
        new_trails.append([])                  # Initialize trail for new particle

        # Neutron: Magenta (255,0,255)
        new_positions.append(mid_pos)
        new_velocities.append(neutron_velocity)
        new_charges.append(0.0)               # Neutron charge
        new_masses.append(NEUTRON_MASS)
        new_species.append(4)                  # Neutron
        new_trails.append([])                  # Initialize trail for new particle

        fusion_count += 1

    # Create a boolean mask to keep particles not in removed_indices
    if removed_indices:
        mask = np.ones(len(positions), dtype=bool)
        mask[list(removed_indices)] = False

        # Apply mask to all relevant arrays
        positions = positions[mask]
        velocities = velocities[mask]
        charges = charges[mask]
        masses = masses[mask]
        species = species[mask]

        # Update particle_trails by removing trails of fused particles
        particle_trails = [trail for idx, trail in enumerate(particle_trails) if mask[idx]]

    # Add new fusion products to arrays
    if new_positions:
        # Convert lists to NumPy arrays
        new_positions = np.array(new_positions)
        new_velocities = np.array(new_velocities)
        new_charges = np.array(new_charges)
        new_masses = np.array(new_masses)
        new_species = np.array(new_species)

        # Append to existing arrays
        positions = np.vstack([positions, new_positions])
        velocities = np.vstack([velocities, new_velocities])
        charges = np.hstack([charges, new_charges])
        masses = np.hstack([masses, new_masses])
        species = np.hstack([species, new_species])

        # Append new trails
        particle_trails.extend(new_trails)

    return positions, velocities, charges, masses, species, particle_trails, fusion_count

Main Particle Fusion Simulation Loop

# Main Simulation Loop
# ------------------------
running_simulation = True
step = 0
fusion_count_total = 0  # To keep track of total fusion events

# UI Variables
current_particle_type = 'deuterium'  # Default particle type

# End State Variables
MAX_FUSION_EVENTS = 100
neutrality_steps_required = 100
neutrality_counter = 0
previous_energy = None  # To be initialized after first energy computation

while running_simulation and step < NUM_STEPS:
    # Event handling for Pygame
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running_simulation = False
            break
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_q:
                running_simulation = False
                print("Simulation terminated by user.")
                break
            elif event.key == pygame.K_d:
                current_particle_type = 'deuterium'
                print("Selected particle type: Deuterium")
            elif event.key == pygame.K_t:
                current_particle_type = 'tritium'
                print("Selected particle type: Tritium")
            elif event.key == pygame.K_e:
                current_particle_type = 'electron'
                print("Selected particle type: Electron")
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            if event.button == 1:  # Left-click
                positions, velocities, charges, masses, species, particle_trails = add_particle(
                    mouse_pos, 'deuterium', positions, velocities, charges, masses, species, particle_trails
                )
                print(f"Added Deuterium at {mouse_pos}")
            elif event.button == 3:  # Right-click
                positions, velocities, charges, masses, species, particle_trails = add_particle(
                    mouse_pos, 'tritium', positions, velocities, charges, masses, species, particle_trails
                )
                print(f"Added Tritium at {mouse_pos}")
            elif event.button == 2:  # Middle-click
                positions, velocities, charges, masses, species, particle_trails = add_particle(
                    mouse_pos, 'electron', positions, velocities, charges, masses, species, particle_trails
                )
                print(f"Added Electron at {mouse_pos}")
    
    # Deposit charge
    rho = deposit_charge(positions, charges, GRID_SIZE, DOMAIN_SIZE)

    # Calculate potential phi with convergence
    phi = calculate_phi(rho, DX, EPS_0)

    # Calculate electric fields Ex and Ey outside Numba
    Ex_padded, Ey_padded = calculate_E_fields(phi, DX)

    # Push particles
    positions, velocities = boris_push(positions, velocities, charges, masses, Ex_padded, Ey_padded, Bz, DT, GRID_SIZE, DOMAIN_SIZE)

    # Compute and apply pairwise magnetic forces
    F_mag = compute_pairwise_magnetic_forces(positions, velocities, charges, masses, R_mag=R_MAG, mu_0=MU_0, dt=DT)
    velocities += F_mag  # Update velocities based on magnetic forces

    # Check for invalid values in positions and velocities
    valid_mask = validate_particles(charges, velocities)
    if not np.all(valid_mask):
        num_invalid = np.sum(~valid_mask)
        print(f"Step {step}: Invalid Particles Detected = {num_invalid}. Removing them.")
        positions = positions[valid_mask]
        velocities = velocities[valid_mask]
        charges = charges[valid_mask]
        masses = masses[valid_mask]
        species = species[valid_mask]
        # Remove trails for invalid particles
        particle_trails = [trail for idx, trail in enumerate(particle_trails) if valid_mask[idx]]

    # Identify significant_pairs after ensuring all positions are valid
    significant_pairs = identify_significant_pairs(F_mag, positions, velocities, charges, species, threshold=1e-25)

    # Attempt fusion with spatial collision detection
    fusion_events = attempt_fusion_spatial(positions, velocities, charges, masses, species, DT, GRID_SIZE, DOMAIN_SIZE)

    # Process fusion events
    if fusion_events:
        positions, velocities, charges, masses, species, particle_trails, fusion_count = process_fusion_events(
            fusion_events, positions, velocities, charges, masses, species, particle_trails
        )
        fusion_count_total += fusion_count

        # Collect data for machine learning (optional)
        for (i, j) in fusion_events:
            # Ensure indices are still valid after fusion processing
            if i < len(positions) and j < len(positions):
                # Features: [x_i, y_i, vx_i, vy_i, x_j, y_j, vx_j, vy_j]
                feature = np.concatenate([positions[i], velocities[i], positions[j], velocities[j]])
                features.append(feature)
                labels.append(1)  # Fusion occurred

    # Update particle trails
    for i in range(len(positions)):
        if len(particle_trails[i]) >= trail_length:
            particle_trails[i].pop(0)  # Remove the oldest position
        trail_pos = (
            int(positions[i, 0] / DOMAIN_SIZE * SCREEN_SIZE),
            int(positions[i, 1] / DOMAIN_SIZE * SCREEN_SIZE)
        )
        particle_trails[i].append(trail_pos)

    # Compute total charge
    total_charge = compute_total_charge(rho)

    # Compute total energy and check for equilibrium
    current_energy = compute_total_energy(rho, phi, velocities, masses, Bz)
    if previous_energy is not None:
        energy_change = abs(current_energy - previous_energy)
        if energy_change < 1e-20:
            print("Energy equilibrium reached. Stopping simulation.")
            running_simulation = False
    previous_energy = current_energy

    # Check charge neutrality and monitor velocities every 100 steps
    if step % 100 == 0:
        if abs(total_charge) < 1e-12:
            neutrality_counter += 1
            if neutrality_counter >= neutrality_steps_required:
                print("Charge neutrality achieved and stable. Stopping simulation.")
                running_simulation = False
        else:
            neutrality_counter = 0
            # Optionally enforce charge neutrality
            charges = enforce_charge_neutrality(charges)
        
        avg_velocity = np.mean(np.linalg.norm(velocities, axis=1))
        max_velocity = monitor_velocities(velocities)
        print(f"Step {step}: Avg Velocity = {avg_velocity:.2e} m/s, Max Velocity = {max_velocity:.2e} m/s, Total Charge = {total_charge:.2e} C")

    # Visualization
    screen.fill((0, 0, 0))  # Clear screen with black

    # Draw boundary walls
    boundary_color = (255, 255, 255)  # White
    boundary_thickness = 5
    pygame.draw.rect(screen, boundary_color, pygame.Rect(0, 0, SCREEN_SIZE, SCREEN_SIZE), boundary_thickness)

    # Optional: Draw magnetic force lines
    for (i, j) in significant_pairs:
        x1 = int(positions[i, 0] / DOMAIN_SIZE * SCREEN_SIZE)
        y1 = int(positions[i, 1] / DOMAIN_SIZE * SCREEN_SIZE)
        x2 = int(positions[j, 0] / DOMAIN_SIZE * SCREEN_SIZE)
        y2 = int(positions[j, 1] / DOMAIN_SIZE * SCREEN_SIZE)
        pygame.draw.line(screen, (0, 0, 255), (x1, y1), (x2, y2), 1)  # Blue lines for magnetic forces

    # Draw particles and their trails
    for i in range(len(positions)):
        x = int(positions[i, 0] / DOMAIN_SIZE * SCREEN_SIZE)
        y = int(positions[i, 1] / DOMAIN_SIZE * SCREEN_SIZE)

        # Ensure particles are within screen bounds
        x = min(max(x, 0), SCREEN_SIZE - 1)
        y = min(max(y, 0), SCREEN_SIZE - 1)

        # Assign colors based on species
        if species[i] == 0:
            color = (0, 0, 255)          # Deuterium: Blue
        elif species[i] == 1:
            color = (0, 255, 0)          # Tritium: Green
        elif species[i] == 2:
            color = (255, 255, 255)      # Electrons: White
        elif species[i] == 3:
            color = (0, 255, 255)        # Alpha Particle: Cyan (combined color)
        elif species[i] == 4:
            color = (255, 0, 255)        # Neutron: Magenta (combined color)
        else:
            color = (255, 255, 255)      # Undefined species: White

        # Draw trails
        if len(particle_trails[i]) > 1:
            pygame.draw.lines(screen, color, False, particle_trails[i], 1)
        
        # Draw the particle
        pygame.draw.circle(screen, color, (x, y), PARTICLE_SIZE)

    # Display Fusion Count, Step, Total Charge, and Current Particle Type
    font = pygame.font.SysFont(None, 24)
    fusion_text = font.render(f'Fusion Events: {fusion_count_total}', True, (255, 255, 255))
    step_text = font.render(f'Step: {step}', True, (255, 255, 255))
    charge_color = (0, 255, 0) if abs(total_charge) < 1e-12 else (255, 0, 0)
    charge_text = font.render(f'Total Charge: {total_charge:.2e} C', True, charge_color)
    particle_type_text = font.render(f'Current Particle: {current_particle_type.capitalize()}', True, (255, 255, 255))
    screen.blit(fusion_text, (10, 10))
    screen.blit(step_text, (10, 30))
    screen.blit(charge_text, (10, 50))
    screen.blit(particle_type_text, (10, 70))

    # Update display
    pygame.display.flip()
    clock.tick(FPS)
    step += 1

# Print total fusion events upon completion
print(f"Total Fusion Events: {fusion_count_total}")

# Quit Pygame
pygame.quit()
sys.exit()

Fusion Particle Simulation Code available on GitHub.

Feel free to pull the branch on Github and experiment with the code yourself! Due to the complexity of the simulation environment and the fact that it is coded in Python, I can’t easily host the simulator.

Anaconda is probably necessary to run the code, I developed this in tandem with Chat GPT using Jupyter Notebooks (which are a fantastic way to control version and safely utilize your machine for advanced computational analysis). Peer review and critical analysis is more than welcome.

Please feel free to reach out through any medium to contact me about the simulation. I will be advancing this over time, but for now it is an interesting tool to understand how to optimize fusion environments, and to visualize the interactions between particles and their fusion probabilities, which a highly scaled for the computational environment. Enjoy!

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.