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
+ );
+ }
+
+}