From 3807e0bae332d6a65d9b04d472e1e41c6dd0ce13 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sat, 12 Sep 2020 04:33:22 +0200 Subject: [PATCH] Added working particle physics simulation. --- pom.xml | 71 +++++++++- .../threadripper/ThreadRipperApplication.java | 86 +++++++++++- .../threadripper/engine/Constants.java | 15 ++ .../threadripper/engine/ParticleChamber.java | 130 ++++++++++++++++++ .../threadripper/engine/ParticleUpdate.java | 26 ++++ .../threadripper/engine/ParticleUpdater.java | 70 ++++++++++ .../andrewlalis/threadripper/engine/Vec2.java | 53 +++++++ .../threadripper/particle/Particle.java | 98 +++++++++++++ .../particle/ParticleFactory.java | 59 ++++++++ 9 files changed, 602 insertions(+), 6 deletions(-) create mode 100644 src/main/java/nl/andrewlalis/threadripper/engine/Constants.java create mode 100644 src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java create mode 100644 src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java create mode 100644 src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java create mode 100644 src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java create mode 100644 src/main/java/nl/andrewlalis/threadripper/particle/Particle.java create mode 100644 src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java diff --git a/pom.xml b/pom.xml index 792374e..39c3428 100644 --- a/pom.xml +++ b/pom.xml @@ -8,6 +8,10 @@ threadripper 0.0.1 + + UTF-8 + + @@ -15,19 +19,56 @@ maven-compiler-plugin 3.8.1 - 14 + 12 + + + + org.openjfx + javafx-maven-plugin + 0.0.4 + + 3.8.1 + launcher + nl.andrewlalis.threadripper.ThreadRipperApplication - + org.openjfx - javafx + javafx-controls + 15 + + + + + org.openjfx + javafx-graphics + 15 + + + + + org.openjfx + javafx-base + 15 + + + + + org.openjfx + javafx-media + 15 + + + + + org.openjfx + javafx-fxml 15 - pom @@ -45,5 +86,27 @@ 1.10.19 test + + + + org.slf4j + slf4j-api + 1.7.30 + + + + + org.slf4j + slf4j-jdk14 + 1.7.30 + + + + + org.projectlombok + lombok + 1.18.12 + provided + \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java b/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java index 65421bb..a039366 100644 --- a/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java +++ b/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java @@ -1,10 +1,92 @@ package nl.andrewlalis.threadripper; +import javafx.application.Application; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import lombok.extern.slf4j.Slf4j; +import nl.andrewlalis.threadripper.engine.ParticleChamber; +import nl.andrewlalis.threadripper.engine.Vec2; +import nl.andrewlalis.threadripper.particle.Particle; +import nl.andrewlalis.threadripper.particle.ParticleFactory; + /** * Main application starting point. */ -public class ThreadRipperApplication { +@Slf4j +public class ThreadRipperApplication extends Application { + + private final Canvas canvas; + private final ParticleChamber chamber; + + public ThreadRipperApplication() { + this.canvas = new Canvas(1000, 1000); + this.chamber = new ParticleChamber(canvas); + ParticleFactory factory = new ParticleFactory( + 10000.0, + 10000000.0, + 0, + 0, + new Vec2(350, 250), + new Vec2(450, 400), + new Vec2(10, -2), + new Vec2(25, 2) + ); + for (int i = 0; i < 100; i++) { + this.chamber.addParticle(factory.build()); + } + +// this.chamber.addParticle(new Particle(new Vec2(400, 298), new Vec2(19, 0), 100000.0, 0.0)); +// this.chamber.addParticle(new Particle(new Vec2(400, 300), new Vec2(20, 0), 10000000000.0, 0.0)); + this.chamber.addParticle(new Particle(new Vec2(400, 500), new Vec2(0, 0), 1000000000000000.0, 0.0)); + +// ParticleFactory factory1 = new ParticleFactory( +// 1000000000.0, +// 10000000000000.0, +// 0, +// 0, +// new Vec2(700, 700), +// new Vec2(900, 900), +// new Vec2(0, 0), +// new Vec2(0, 0) +// ); +// for (int i = 0; i < 500; i++) { +// this.chamber.addParticle(factory1.build()); +// } + + Thread particleChamberThread = new Thread(chamber); + particleChamberThread.setName("ParticleChamber"); + particleChamberThread.start(); + } + public static void main(String[] args) { - System.out.println("Hello world."); + log.info("Starting ThreadRipper application."); + Application.launch(args); + } + + @Override + public void start(Stage stage) throws Exception { + stage.setTitle("ThreadRipper"); + + GraphicsContext gc = this.canvas.getGraphicsContext2D(); + + gc.setFill(Color.GREEN); + gc.fillOval(10, 60, 30, 30); + + Group root = new Group(); + root.getChildren().add(this.canvas); + Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + + @Override + public void stop() throws Exception { + this.chamber.setRunning(false); + super.stop(); + System.exit(0); } } diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/Constants.java b/src/main/java/nl/andrewlalis/threadripper/engine/Constants.java new file mode 100644 index 0000000..a4514e9 --- /dev/null +++ b/src/main/java/nl/andrewlalis/threadripper/engine/Constants.java @@ -0,0 +1,15 @@ +package nl.andrewlalis.threadripper.engine; + +public class Constants { + /** + * Newton's Gravitational Constant + * https://en.wikipedia.org/wiki/Gravitational_constant + */ + public static final double G = 0.00000000006674; + + /** + * Coulomb's Constant + * https://en.wikipedia.org/wiki/Coulomb%27s_law + */ + public static final double Ke = 8990000000.0; +} diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java new file mode 100644 index 0000000..ee1d4ab --- /dev/null +++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java @@ -0,0 +1,130 @@ +package nl.andrewlalis.threadripper.engine; + +import javafx.application.Platform; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; +import lombok.extern.slf4j.Slf4j; +import nl.andrewlalis.threadripper.particle.Particle; + +import java.util.*; +import java.util.concurrent.*; + +@Slf4j +public class ParticleChamber implements Runnable { + private static final int DEFAULT_THREAD_POOL = 100; + private static final double DEFAULT_UPDATE_FPS = 60; + + private final Set particles; + private int threadCount; + private ExecutorService executorService; + private CompletionService particleUpdateService; + + private boolean running; + private double updateFps; + + private final Canvas canvas; + + public ParticleChamber(Canvas canvas) { + this.particles = new HashSet<>(); + + this.threadCount = DEFAULT_THREAD_POOL; + this.executorService = Executors.newFixedThreadPool(this.threadCount); + this.particleUpdateService = new ExecutorCompletionService<>(this.executorService); + + this.updateFps = DEFAULT_UPDATE_FPS; + + this.canvas = canvas; + } + + /** + * Adds one or more particles to the chamber. + * @param particles The particles to add. + */ + public void addParticle(Particle... particles) { + this.particles.addAll(Arrays.asList(particles)); + } + + public synchronized void setRunning(boolean running) { + this.running = running; + } + + @Override + public void run() { + this.running = true; + long previousTimeMilliseconds = System.currentTimeMillis(); + long millisecondsSinceLastUpdate = 0L; + + log.info("Starting particle chamber."); + while (this.running) { + final long currentTimeMilliseconds = System.currentTimeMillis(); + final long elapsedMilliseconds = currentTimeMilliseconds - previousTimeMilliseconds; + + millisecondsSinceLastUpdate += elapsedMilliseconds; + + final double millisecondsPerFrame = 1000.0 / this.updateFps; + + if (millisecondsSinceLastUpdate > millisecondsPerFrame) { + final double secondsSinceLastUpdate = millisecondsSinceLastUpdate / 1000.0; + millisecondsSinceLastUpdate = 0L; + //log.info("Updating particles after {} seconds elapsed.", secondsSinceLastUpdate); + this.updateParticles(secondsSinceLastUpdate); + this.drawParticles(); + } + + previousTimeMilliseconds = currentTimeMilliseconds; + } + log.info("Particle chamber stopped."); + } + + /** + * Updates all the particles in the simulation. + * @param deltaTime The amount of seconds that have passed since the last update. + */ + private void updateParticles(double deltaTime) { + // First submit a new callable task for each particle. + for (Particle particle : this.particles) { + this.particleUpdateService.submit(new ParticleUpdater(particle, this.particles)); + } + + int updatesReceived = 0; + boolean errorEncountered = false; + final List updates = new ArrayList<>(this.particles.size()); + + // Iterate until we've received the results of each particle updater's calculations. + while (updatesReceived < this.particles.size() && !errorEncountered) { + try { + Future updateFuture = this.particleUpdateService.take(); + updates.add(updateFuture.get()); + updatesReceived++; + } catch (Exception e) { + e.printStackTrace(); + errorEncountered = true; + } + } + + // Implement the updates for each particle. + for (ParticleUpdate update : updates) { + update.getFocusParticle().updateVelocity(update.getAcceleration(), deltaTime); + update.getFocusParticle().updatePosition(deltaTime); + //log.info("Particle updated: {}", update.getFocusParticle().toString()); + } + } + + /** + * Draws all particles on the canvas. + */ + private void drawParticles() { + Platform.runLater(() -> { + GraphicsContext gc = this.canvas.getGraphicsContext2D(); + gc.setFill(Color.WHITE); + gc.clearRect(0, 0, this.canvas.getWidth(), this.canvas.getHeight()); + gc.setFill(Color.BLACK); + gc.setStroke(Color.BLUE); + for (Particle particle : this.particles) { + Vec2 pos = particle.getPosition(); + gc.fillOval(pos.getX() - 3, pos.getY() - 3, 6, 6); + } + }); + } +} diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java new file mode 100644 index 0000000..7b117ee --- /dev/null +++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java @@ -0,0 +1,26 @@ +package nl.andrewlalis.threadripper.engine; + +import lombok.Getter; +import nl.andrewlalis.threadripper.particle.Particle; + +/** + * Describes an update to a particle's motion. + */ +@Getter +public class ParticleUpdate { + /** + * The particle to be updated. + */ + private final Particle focusParticle; + + /** + * The acceleration that the particle experiences, in meters per second + * squared. + */ + private final Vec2 acceleration; + + public ParticleUpdate(Particle focusParticle, Vec2 acceleration) { + this.focusParticle = focusParticle; + this.acceleration = acceleration; + } +} diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java new file mode 100644 index 0000000..17b7def --- /dev/null +++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java @@ -0,0 +1,70 @@ +package nl.andrewlalis.threadripper.engine; + +import lombok.extern.slf4j.Slf4j; +import nl.andrewlalis.threadripper.particle.Particle; + +import java.util.Set; +import java.util.concurrent.Callable; + +/** + * Callable which is dedicated to finding the net force which is applied to a + * particular particle of interest, with respect to all other particles. + */ +@Slf4j +public class ParticleUpdater implements Callable { + private final Particle focusParticle; + private final Set particles; + + public ParticleUpdater(Particle focusParticle, Set particles) { + this.focusParticle = focusParticle; + this.particles = particles; + } + + @Override + public ParticleUpdate call() throws Exception { + double accelerationX = 0L; + double accelerationY = 0L; + for (Particle particle : this.particles) { + if (!particle.equals(this.focusParticle)) { + Vec2 partialAcceleration = this.computeAcceleration(particle); + accelerationX += partialAcceleration.getX(); + accelerationY += partialAcceleration.getY(); + } + } + return new ParticleUpdate(this.focusParticle, new Vec2(accelerationX, accelerationY)); + } + + /** + * Computes the acceleration which another particle imparts on this updater's + * focus particle. + * @param other The other particle which is acting upon the focus particle. + * @return The acceleration, in m/s^2, which the focus particle experiences + * from this particle. + */ + private Vec2 computeAcceleration(Particle other) { + final double radius = this.focusParticle.getPosition().distance(other.getPosition()); + final double dY = other.getPosition().getY() - this.focusParticle.getPosition().getY(); + final double dX = other.getPosition().getX() - this.focusParticle.getPosition().getX(); + + final double angle = Math.atan2(dY, dX); + + final double gravityNewtons = Constants.G * (this.focusParticle.getMass() * other.getMass()) / Math.pow(radius, 2); + final double gravityAcceleration = gravityNewtons / this.focusParticle.getMass(); + final Vec2 gravityAccelerationVector = Vec2.fromPolar(gravityAcceleration, angle); +// log.info( +// "Force of gravity between particles {} and {} is {} Newtons.\nCauses {} m/s^2 acceleration on particle {}.", +// this.focusParticle.getId(), +// other.getId(), +// gravityNewtons, +// gravityAcceleration, +// this.focusParticle.getId() +// ); + + final double emNewtons = Constants.Ke * (this.focusParticle.getCharge() * other.getCharge()) / Math.pow(radius, 2); + final double emAcceleration = emNewtons / this.focusParticle.getMass(); + final Vec2 emAccelerationVector = Vec2.fromPolar(emAcceleration, angle); + + final Vec2 totalAcceleration = gravityAccelerationVector.add(emAccelerationVector); + return totalAcceleration; + } +} diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java b/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java new file mode 100644 index 0000000..d316d31 --- /dev/null +++ b/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java @@ -0,0 +1,53 @@ +package nl.andrewlalis.threadripper.engine; + +import lombok.Getter; + +/** + * Simple vector for two double values, X and Y. + */ +@Getter +public class Vec2 { + private final double x; + private final double y; + + public Vec2(double x, double y) { + this.x = x; + this.y = y; + } + + public static Vec2 fromPolar(double radius, double theta) { + return new Vec2( + radius * Math.cos(theta), + radius * Math.sin(theta) + ); + } + + public Vec2 add(Vec2 other) { + return new Vec2(this.getX() + other.getX(), this.getY() + other.getY()); + } + + public Vec2 multiply(double factor) { + return new Vec2(this.getX() * factor, this.getY() * factor); + } + + public double distance(Vec2 other) { + final double deltaX = this.getX() - other.getX(); + final double deltaY = this.getY() - other.getY(); + return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj instanceof Vec2) { + Vec2 other = (Vec2) obj; + return other.getX() == this.getX() && other.getY() == this.getY(); + } + return false; + } + + @Override + public String toString() { + return String.format("[%f, %f]", this.getX(), this.getY()); + } +} diff --git a/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java b/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java new file mode 100644 index 0000000..2bc3d6d --- /dev/null +++ b/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java @@ -0,0 +1,98 @@ +package nl.andrewlalis.threadripper.particle; + +import lombok.Getter; +import nl.andrewlalis.threadripper.engine.Vec2; + +import java.util.Objects; + +/** + * Represents a single particle that exists in a simulation. + */ +@Getter +public class Particle { + private static long NEXT_PARTICLE_ID = 1L; + + /** + * Unique id for this particle. + */ + private long id; + + /** + * The particle's position, in meters. + */ + private Vec2 position; + + /** + * The particle's velocity, in meters. + */ + private Vec2 velocity; + + /** + * The particle's mass, in kilograms. + */ + private double mass; + + /** + * The particle's charge, in coulombs. + */ + private double charge; + + public Particle(Vec2 position) { + this(position, new Vec2(0, 0), 1, 0); + } + + public Particle(Vec2 position, Vec2 velocity, double mass, double charge) { + this.position = position; + this.velocity = velocity; + this.mass = mass; + this.charge = charge; + + this.id = NEXT_PARTICLE_ID; + NEXT_PARTICLE_ID++; + } + + /** + * Updates this particle's velocity, according to a delta velocity value, + * @param acceleration Acceleration due to net force, in meters per second, + * squared. + * @param deltaTime The amount of time elapsed, in seconds. + */ + public void updateVelocity(Vec2 acceleration, double deltaTime) { + this.velocity = this.velocity.add(acceleration.multiply(deltaTime)); + } + + /** + * Updates this particle's position, according to its current velocity. + * @param deltaTime The amount of time that has elapsed, in seconds. + */ + public void updatePosition(double deltaTime) { + this.position = this.position.add(this.getVelocity().multiply(deltaTime)); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.id); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj instanceof Particle) { + Particle other = (Particle) obj; + return other.getId() == this.getId(); + } + return false; + } + + @Override + public String toString() { + return String.format( + "{id: %d, mass: %f, charge: %f, position: %s, velocity: %s}", + this.getId(), + this.getMass(), + this.getCharge(), + this.getPosition(), + this.getVelocity() + ); + } +} diff --git a/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java b/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java new file mode 100644 index 0000000..eb2a57e --- /dev/null +++ b/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java @@ -0,0 +1,59 @@ +package nl.andrewlalis.threadripper.particle; + +import nl.andrewlalis.threadripper.engine.Vec2; + +import java.util.concurrent.ThreadLocalRandom; + +public class ParticleFactory { + private final double minMass; + private final double maxMass; + private final double minCharge; + private final double maxCharge; + private final Vec2 minPosition; + private final Vec2 maxPosition; + private final Vec2 minVelocity; + private final Vec2 maxVelocity; + + public ParticleFactory( + double minMass, + double maxMass, + double minCharge, + double maxCharge, + Vec2 minPosition, + Vec2 maxPosition, + Vec2 minVelocity, + Vec2 maxVelocity + ) { + this.minMass = minMass; + this.maxMass = maxMass; + this.minCharge = minCharge; + this.maxCharge = maxCharge; + this.minPosition = minPosition; + this.maxPosition = maxPosition; + this.minVelocity = minVelocity; + this.maxVelocity = maxVelocity; + } + + public Particle build() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + final Vec2 position = new Vec2( + random.nextDouble(this.minPosition.getX(), this.maxPosition.getX()), + random.nextDouble(this.minPosition.getY(), this.maxPosition.getY()) + ); + final Vec2 velocity = new Vec2( + random.nextDouble(this.minVelocity.getX(), this.maxVelocity.getX()), + random.nextDouble(this.minVelocity.getY(), this.maxVelocity.getY()) + ); + final double mass = random.nextDouble(this.minMass, this.maxMass); +// final double charge = random.nextDouble(this.minCharge, this.maxCharge); + + final double charge = 0; + return new Particle( + position, + velocity, + mass, + charge + ); + } + +}