Field lines and Ants Part II

Emergent Field Lines & Heuristic Force Carriers

Module ID: PHYS-ELEC-004
Core Concept: Agent-Based Field Mapping and Localized Momentum Exchange

Collaborative Attribution:
  • Concept & Physics Architecture: David J. Hoxie, PhD
  • Code Rework & Implementation: Gemini

Using an agent-based approach (where genetic algorithms, ant colony optimization, and slime mold pathfinding share a similar mathematical foundation), we can create a dynamic, heuristic model to visualize electric field lines.

While the macroscopic visualization is emergent, the underlying engine is built on rigorous physics. Each "ant" acts as a discrete test charge, calculating its local trajectory based on the exact $1/r^2$ Coulomb force exerted by the source charges in the space. This allows for real-time, interactive investigations into how different charge configurations interact, how equipotential gradients shift, and how field lines behave dynamically over time.

Part 1: The Global Field Map

In our first iteration, we model the test charges as our "ants." They spawn randomly across the entire canvas, evaluate the local electrostatic landscape, and trace a path along the steepest gradient.

Carefully note the trajectories of the test charges. Why is this visualization not exactly the same cohesive field map you calculate on paper in class?

Left-Click: + Charge | Shift+Click: - Charge

The Core Architecture

To achieve this, we do not program continuous lines. We program discrete particle intelligence. Here is the foundational source code for the global field engine, establishing the physics loop and the entropy management (respawning particles to maintain system equilibrium).

// The Physics Loop (The Student Free Body Diagram)
for (let i = particles.length - 1; i >= 0; i--) {
    let pt = particles[i];
    let force = p.createVector(0, 0);
    let hitSource = false;

    // Calculate the net pull from ALL sources on this single test particle
    for (let s of sources) {
        let dir = s.pos.copy().sub(pt.pos); 
        let d = dir.mag();
        
        // If the particle hits a source, it terminates
        if (d < 15) hitSource = true; 
        
        // Prevent infinite division singularities
        if (d < 5) d = 5; 

        // Apply F = k * (q1*q2) / r^2
        let strength = (k * s.charge) / (d * d); 
        dir.normalize();
        dir.mult(strength);
        
        force.add(dir);
    }

    // Apply F=ma (assuming mass = 1)
    pt.vel.add(force);
    pt.vel.limit(5); // Terminal velocity
    pt.pos.add(pt.vel);
    pt.life--;

    // Entropy Management: Respawn dead, out-of-bounds, or absorbed particles
    if (pt.life <= 0 || hitSource || outOfBounds(pt)) {
        particles.splice(i, 1);
        spawnParticle(); // Spawns randomly on the canvas
    }
}

Part 2: Localized Spawning & Temporal Dynamics

One issue with Part 1 is that because we are starting the test charges at completely random coordinates, we cannot readily observe the unbroken continuity of the field lines radiating specifically from the sources.

To fix this, we change the code to localize the test charges. By editing the spawnParticle() function, we change pos = random(x), random(y) to anchor around the sources: pos = source.x + random(offset), source.y + random(offset).

We also add the ability to drag and drop the charges. Now, we can see how movement physically alters the local field lines. As you drag a charge, watch how the directional vectors of the surrounding space update in real-time, illustrating temporal behavior in the fields without explicitly coding a vector potential.

Click & Drag charges to warp the field.

Part 3: The Heuristic Force Carrier

We now have electric field lines changing over time, but we still have an inherent physics problem: we don't have a true mechanism for force. The source charges are anchored in space, interacting via an omniscient, faster-than-light "force at a distance."

Is there a way to communicate the force between the macro-charges without actually calculating their distance from each other?

Let's make a tiny change. We won’t use rigorous classical integration, but rather a heuristic experiment. In our code, when a test charge hits a source charge, it is destroyed. What if, rather than just destroying the test charge, we destroyed it AND applied a tiny change in position to the source charge, effectively transferring momentum in the direction of the collision?

if (d < 15) {
    hitSource = true; 
    // Back-reaction: The test charge imparts its momentum onto the source!
    sources[S].pos.add(dir.mult(-1));
}

Try it. Watch what happens when you place multiple source charges near each other and let the swarm of test charges act as physical mediators of force.

Observe the autonomous movement driven entirely by particle collisions.

Pedagogical Inquiries

Intro-Level Questions:
  • What did we just accomplish physically?
  • Is this a valid model for photons mediating the electromagnetic force? Why or why not?
  • Is there another statistical model this simulation mimics? (Hint: search for Brownian motion).
Upper-Level Undergrad Questions:
  • We are not currently modeling a magnetic field. Do we have to? Consider Poynting vectors, the electroweak force, vector potentials, and gauge transforms.
  • If this model has a relation to Brownian motion, how could this mathematical framework relate to t-SNE, CVAE, and other Helmholtz machines that rely on KL Divergence?
  • What is the strict physical equivalent of KL Divergence in this system?
  • If you watch the charges move, they sometimes repel and sometimes attract. When does this state change happen? (Hint: what is the limit that determines repulsion vs. collapse?)
Graduate-Level Considerations:
  • Why is it that like charges in this specific model may not always fly apart? (Hint: consider exactly when the test charges are generated and destroyed.)
  • What happens to the effective generation rate (density) of the test charges as the macro-sources move closer together or farther apart?
  • Why do the different charges eventually fly completely apart? (Hint: what conservation laws are we explicitly violating in this heuristic code? Consider test charge conservation and energy conservation as particles move off the screen.)

  

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>

<div id="emergent-field-container" style="display: flex; justify-content: center; width: 100%; padding: 20px;"></div>

<script>

const emergentFieldSim = (p) => {

let sources = [];

let particles = [];

let numParticles = 1000;

let k = 150; // The Coulomb constant multiplier

p.setup = () => {

let container = document.getElementById('emergent-field-container');

let w = container.offsetWidth > 0 ? container.offsetWidth : 800;

let canvas = p.createCanvas(w, 600);

canvas.parent('emergent-field-container');

‍ ‍

// Start with a standard Dipole setup

sources.push({ pos: p.createVector(p.width * 0.33, p.height / 2), charge: 1 });

sources.push({ pos: p.createVector(p.width * 0.66, p.height / 2), charge: -1 });

// Spawn the initial swarm

for (let i = 0; i < numParticles; i++) {

spawnParticle();

}

};

function spawnParticle() {

particles.push({

pos: p.createVector(p.random(p.width), p.random(p.height)),

vel: p.createVector(0, 0),

life: p.random(100, 300) // Gives them a finite lifespan so the screen doesn't clump

});

}

p.draw = () => {

// The Slime Mold trick: A slightly transparent background creates the visual trails

p.background(15, 23, 42, 20);

// 1. Draw the Source Charges

for (let s of sources) {

p.noStroke();

if (s.charge > 0) {

p.fill(239, 68, 68); // Red (+)

} else {

p.fill(59, 130, 246); // Blue (-)

}

‍ ‍p.circle(s.pos.x, s.pos.y, 25);

}

// 2. The Physics Loop (The Student Free Body Diagram)

for (let i = particles.length - 1; i >= 0; i--) {

let pt = particles[i];

let force = p.createVector(0, 0);

let hitSource = false;

// Calculate the net pull from ALL sources on this single test particle

for (let s of sources) {

// FIX: Use copy() to safely perform vector math in Instance Mode

let dir = s.pos.copy().sub(pt.pos);

let d = dir.mag();

‍ ‍

// If the particle hits a source, it terminates

if (d < 15) hitSource = true;

‍ ‍

// Prevent infinite division singularities

if (d < 5) d = 5;

// F = k (q1q2) / r^2

let strength = (k s.charge) / (d d);

dir.normalize();

dir.mult(strength);

‍ ‍

force.add(dir);

}

// Apply F=ma (assuming mass = 1)

pt.vel.add(force);

pt.vel.limit(5); // Terminal velocity so they don't break the spacetime continuum

pt.pos.add(pt.vel);

‍ ‍pt.life--;

// 3. Draw the Test Particle

p.stroke(148, 163, 184, 150);

p.strokeWeight(2);

p.point(pt.pos.x, pt.pos.y);

// 4. Entropy Management: Respawn dead, out-of-bounds, or absorbed particles

if (pt.life <= 0 || hitSource || pt.pos.x < 0 || pt.pos.x > p.width || pt.pos.y < 0 || pt.pos.y > p.height) {

particles.splice(i, 1);

spawnParticle();

}

}

};

p.mousePressed = () => {

// Only trigger if clicking inside the physical canvas

if (p.mouseX > 0 && p.mouseX < p.width && p.mouseY > 0 && p.mouseY < p.height) {

let chargeType = p.keyIsDown(p.SHIFT) ? -1 : 1;

sources.push({ pos: p.createVector(p.mouseX, p.mouseY), charge: chargeType });

}

};

‍ ‍

p.windowResized = () => {

let container = document.getElementById('emergent-field-container');

if(container.offsetWidth > 0) {

p.resizeCanvas(container.offsetWidth, 600);

sources[0].pos.set(p.width * 0.33, p.height / 2);

sources[1].pos.set(p.width * 0.66, p.height / 2);

}

};

};

new p5(emergentFieldSim);

</script>

Next
Next

Gradients: Motion and Learning