I have this program that simulates a closed particle system. Closed in this context means that the sum of all energies is constant. My primary concern is code itself, yet I would like to hear comments regarding physics as well. Here is my code:
Configuration.java
package net.coderodde.simulation;
public final class Configuration {
/**
* Defines the drawing scale. A distance of one unit length corresponds to
* the length of 100 pixels.
*/
public static final int PIXELS_PER_UNIT_LENGTH = 10;
/**
* The rejection force constant.
*/
public static final double FORCE_CONSTANT = 1000.0;
}
Particle.java
package net.coderodde.simulation;
import java.awt.Color;
import java.awt.Graphics;
import java.util.Objects;
import static net.coderodde.simulation.Configuration.FORCE_CONSTANT;
import static net.coderodde.simulation.Configuration.PIXELS_PER_UNIT_LENGTH;
import static net.coderodde.simulation.Utils.checkNonInfinite;
import static net.coderodde.simulation.Utils.checkNonNaN;
import static net.coderodde.simulation.Utils.checkNonNegative;
/**
* This class defines a particle in the simulation. The entire weight of a
* particle is considered to be fully focused in the center of this particle.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Sep 2, 2017)
*/
public final class Particle {
/**
* The mass of this particle.
*/
private final double mass;
/**
* The radius of the graphical representation of this particle.
*/
private final int radius;
/**
* The color of the graphical representation of this particle.
*/
private final Color color;
/**
* The current x-coordinate of this particle.
*/
private double x;
/**
* The current y-coordinate of this particle.
*/
private double y;
/**
* The current velocity to the right. May be negative when the particle
* moves to the left.
*/
private double velocityX;
/**
* The current velocity downwards. May be negative when the particle moves
* upwards.
*/
private double velocityY;
/**
* Constructs a new particle.
*
* @param mass the weight of the new particle.
* @param radius the radius of the new particle.
* @param color the color of the new particle.
*/
public Particle(double mass, int radius, Color color) {
this.mass = checkMass(mass);
this.radius = checkRadius(radius);
this.color = Objects.requireNonNull(color,
"The particle color is null.");
}
/**
* Copy-constructs a new particle.
*
* @param other the other particle to copy.
*/
public Particle(Particle other) {
this.mass = other.mass;
this.radius = other.radius;
this.color = other.color;
this.x = other.x;
this.y = other.y;
this.velocityX = other.velocityX;
this.velocityY = other.velocityY;
}
public Vector getVelocityVector() {
return new Vector(velocityX, velocityY);
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getVelocityX() {
return velocityX;
}
public double getVelocityY() {
return velocityY;
}
public double getMass() {
return mass;
}
public void setX(double x) {
this.x = checkX(x);
}
public void setY(double y) {
this.y = checkY(y);
}
public void setVelocityX(double velocityX) {
this.velocityX = checkVelocityX(velocityX);
}
public void setVelocityY(double velocityY) {
this.velocityY = checkVelocityY(velocityY);
}
/**
* Returns the current speed of this particle.
*
* @return the current speed.
*/
public double getSpeed() {
double vxSquared = velocityX * velocityX;
double vySquared = velocityY * velocityY;
return Math.sqrt(vxSquared + vySquared);
}
/**
* Returns the distance between this particle and {@code other}.
*
* @param other the other particle.
* @return the distance between two particles.
*/
public double getDistance(Particle other) {
double dx = x - other.x;
double dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Computes the kinetic energy of this particle.
*
* @return the kinetic energy.
*/
public double getKineticEnergy() {
double speed = getSpeed();
return 0.5 * mass * speed * speed;
}
/**
* Computes the rejection force between this and {@code other} particles.
*
* @param other the other particle.
* @return the rejection force.
*/
public double getRejectionForce(Particle other) {
double distance = getDistance(other);
return FORCE_CONSTANT * mass * other.getMass() / (distance * distance);
}
/**
* Computes the potential energy between this and {@code other} particle.
*
* @param other the other particle.
* @return potential energy.
*/
public double getPotentialEnergy(Particle other) {
return FORCE_CONSTANT * mass * other.getMass() / getDistance(other);
}
/**
* Draws this particle on a canvas.
*
* @param g the graphics context.
*/
public void draw(Graphics g) {
int effectiveX = (int)(x * PIXELS_PER_UNIT_LENGTH);
int effectiveY = (int)(y * PIXELS_PER_UNIT_LENGTH);
g.setColor(color);
g.fillOval(effectiveX - radius,
effectiveY - radius,
2 * radius,
2 * radius);
}
@Override
public String toString() {
return "[x=" + x + ", y=" + y + ", velocityX=" + velocityX +
", velocityY=" + velocityY + "]";
}
private double checkMass(double mass) {
checkNonNaN(mass, "The particle mass is NaN.");
checkNonNegative(mass, "The particle mass is non-positive.");
checkNonInfinite(mass, "The particle mass is infinite.");
return mass;
}
private int checkRadius(int radius) {
if (radius <= 0) {
throw new IllegalArgumentException(
"The particle radius is non-positive: " + radius);
}
return radius;
}
private double checkCoordinate(double coordinate,
String errorMessageNaN,
String errorMessageInfinite) {
checkNonNaN(coordinate, errorMessageNaN);
checkNonInfinite(coordinate, errorMessageInfinite);
return coordinate;
}
private double checkX(double x) {
checkCoordinate(x,
"The x-coordinate is NaN.",
"The x-coordinate is infinite.");
return x;
}
private double checkY(double y) {
checkCoordinate(y,
"The y-coordinate is NaN.",
"The y-coordinate is infinite.");
return y;
}
private double checkVelocityX(double velocityX) {
checkCoordinate(velocityX,
"The x-velocity is NaN.",
"The x-velocity is infinite.");
return velocityX;
}
private double checkVelocityY(double velocityY) {
checkCoordinate(velocityY,
"The y-velocity is NaN.",
"The y-velocity is infinite.");
return velocityY;
}
}
SimulationApp.java
package net.coderodde.simulation;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static net.coderodde.simulation.Configuration.PIXELS_PER_UNIT_LENGTH;
/**
* This class implements the entire simulation program.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Sep 2, 2017)
*/
public final class SimulationApp {
/**
* The minimum particle mass.
*/
private static final double MINIMUM_MASS = 15.0;
/**
* The maximum particle mass.
*/
private static final double MAXIMUM_MASS = 30.0;
/**
* Reserve the number of pixels for the title bar.
*/
private static final int TITLE_BAR_RESERVED_HEIGHT = 50;
/**
* The default number of particles in the simulation.
*/
private static final int DEFAULT_PARTICLES = 6;
/**
* The time step.
*/
private static final double TIME_STEP = 0.01;
/**
* The number of milliseconds spent between two consecutive time quants.
*/
private static final int SLEEP_TIME = 20;
/**
* Used for randomly generating the color components.
*/
private static final int COLOR_CHANNEL_MAX = 256;
/**
* The maximum initial velocity horizontally and/or vertically.
*/
private static final double MAX_INITIAL_VELOCITY = 40.0;
/**
* Defines the entry point of the program.
*
* @param args the command line arguments.
*/
public static void main(String[] args) {
Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize();
screenDimension.height -= TITLE_BAR_RESERVED_HEIGHT;
double worldWidth = (1.0 * screenDimension.width)
/ PIXELS_PER_UNIT_LENGTH;
double worldHeight = (1.0 * screenDimension.height)
/ PIXELS_PER_UNIT_LENGTH;
long seed = System.currentTimeMillis();
Random random = new Random(seed);
System.out.println("Seed = " + seed);
List<Particle> particles = getParticles(DEFAULT_PARTICLES,
worldWidth,
worldHeight,
random);
SimulationCanvas simulationCanvas = new SimulationCanvas();
Simulator simulator = new Simulator(particles,
simulationCanvas,
worldWidth,
worldHeight,
TIME_STEP,
SLEEP_TIME);
simulationCanvas.setSimulator(simulator);
SimulationFrame simulationFrame =
new SimulationFrame(simulationCanvas,
screenDimension.width,
screenDimension.height);
SimulationFrameKeyListener keyListener =
new SimulationFrameKeyListener(simulator);
simulationFrame.addKeyListener(keyListener);
simulator.run();
}
private static List<Particle> getParticles(int particles,
double worldWidth,
double worldHeight,
Random random) {
List<Particle> particleList = new ArrayList<>(particles);
for (int i = 0; i < particles; ++i) {
particleList.add(createRandomParticle(random,
worldWidth,
worldHeight));
}
return particleList;
}
private static Particle createRandomParticle(Random random,
double worldWidth,
double worldHeight) {
double mass = MINIMUM_MASS +
(MAXIMUM_MASS - MINIMUM_MASS) * random.nextDouble();
int radius = (int) mass;
Color color = new Color(random.nextInt(COLOR_CHANNEL_MAX),
random.nextInt(COLOR_CHANNEL_MAX),
random.nextInt(COLOR_CHANNEL_MAX));
Particle particle = new Particle(mass, radius, color);
particle.setX(worldWidth * random.nextDouble());
particle.setY(worldHeight * random.nextDouble());
particle.setVelocityX(MAX_INITIAL_VELOCITY * random.nextDouble());
particle.setVelocityY(MAX_INITIAL_VELOCITY * random.nextDouble());
return particle;
}
}
SimulationCanvas.java
package net.coderodde.simulation;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.util.List;
import java.util.Objects;
/**
* This class implements a simple canvas for drawing the simulated particle
* system.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Sep 2, 2017)
*/
public final class SimulationCanvas extends Canvas {
/**
* The list of particles.
*/
private List<Particle> particles;
/**
* The simulation engine.
*/
private Simulator simulator;
@Override
public void paint(Graphics g) {
update(g);
}
@Override
public void update(Graphics g) {
double totalEnergy = simulator.computeTotalEnergy();
String totalEnergyString = "Total energy: " + totalEnergy;
g.setColor(getBackground());
g.clearRect(0, 0, getWidth(), getHeight());
for (Particle particle : particles) {
particle.draw(g);
}
g.setColor(Color.WHITE);
g.drawChars(totalEnergyString.toCharArray(),
0,
totalEnergyString.length(),
0,
20);
}
void setParticles(List<Particle> particles) {
this.particles = Objects.requireNonNull(
particles,
"The particle list is null.");
}
void setSimulator(Simulator simulator) {
this.simulator = simulator;
}
}
SimulationFrame.java
package net.coderodde.simulation;
import java.awt.Color;
import java.util.Objects;
import javax.swing.JFrame;
public final class SimulationFrame extends JFrame {
private static final String FRAME_TITLE = "Closed system simulation";
public SimulationFrame(SimulationCanvas simulationCanvas,
int width,
int height) {
super(FRAME_TITLE);
Objects.requireNonNull(simulationCanvas, "The input canvas is null.");
setSize(width, height);
simulationCanvas.setSize(width, height);
setLocation(0, 0);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
getContentPane().add(simulationCanvas);
simulationCanvas.setBackground(Color.BLACK);
setResizable(false);
setVisible(true);
}
}
SimulationFrameKeyListener.java
package net.coderodde.simulation;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Objects;
public final class SimulationFrameKeyListener implements KeyListener {
private final Simulator simulator;
public SimulationFrameKeyListener(Simulator simulator) {
this.simulator = Objects.requireNonNull(simulator,
"The simulator is null.");
}
@Override
public void keyTyped(KeyEvent e) {
simulator.togglePause();
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}
}
Simulator.java
package net.coderodde.simulation;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static net.coderodde.simulation.Utils.checkNonInfinite;
import static net.coderodde.simulation.Utils.checkNonNaN;
import static net.coderodde.simulation.Utils.checkNonNegative;
/**
* This class implements the actual simulator.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Sep 2, 2017)
*/
public final class Simulator {
/**
* The list of particles.
*/
private final List<Particle> particles = new ArrayList<>();
/**
* Holds the canvas for drawing the system.
*/
private final SimulationCanvas simulationCanvas;
/**
* The time quant.
*/
private final double timeStep;
/**
* The total energy of the simulated system.
*/
private final double totalEnergy;
/**
* The width of the system.
*/
private final double worldWidth;
/**
* The height of the system.
*/
private final double worldHeight;
/**
* Number of milliseconds between two time quants.
*/
private final int sleepTime;
/**
* The exit flag
*/
private volatile boolean exit = false;
/**
* The pause flag.
*/
private volatile boolean pause = true;
/**
* Used for mapping the particles to their respective force vectors.
*/
private final Map<Particle, Vector> particleToForceVectorMap =
new HashMap<>();
public Simulator(List<Particle> particles,
SimulationCanvas simulationCanvas,
double worldWidth,
double worldHeight,
double timeStep,
int sleepTime) {
Objects.requireNonNull(particles, "The particle list is null.");
this.simulationCanvas =
Objects.requireNonNull(
simulationCanvas,
"The simulation canvas is null.");
checkNotEmpty(particles);
copy(particles);
checkParticlesDoNotOverlap();
this.worldWidth = checkWorldWidth(worldWidth);
this.worldHeight = checkWorldHeight(worldHeight);
this.timeStep = checkTimeStep(timeStep);
this.sleepTime = checkSleepTime(sleepTime);
totalEnergy = computeTotalEnergy();
simulationCanvas.setParticles(this.particles);
}
public void togglePause() {
pause = !pause;
}
public void run() {
while (!exit) {
if (!pause) {
performStep();
simulationCanvas.repaint();
}
sleep(sleepTime);
}
}
List<Particle> getParticles() {
return Collections.<Particle>unmodifiableList(particles);
}
/**
* Checks that the particle list is not empty.
*
* @param particles the particles list.
*/
private void checkNotEmpty(List<Particle> particles) {
if (particles.isEmpty()) {
throw new IllegalArgumentException("No particles given.");
}
}
/**
* Makes internal copies of all the particles so that client programmer
* cannot interfere.
*
* @param particles the particle list.
*/
private void copy(List<Particle> particles) {
for (Particle particle : particles) {
this.particles.add(new Particle(particle));
}
}
/**
* Performs one simulation step.
*/
private void performStep() {
// Compute the force vectors of all partices:
computeForceVectors();
updateParticleVelocities();
moveParticles();
resolveWorldBorderCollisions();
normalizeVelocityVectors();
particleToForceVectorMap.clear();
}
/**
* Computes all the repelling force vectors for each particle.
*/
private void computeForceVectors() {
for (Particle particle : particles) {
Vector vector = new Vector();
for (Particle other : particles) {
if (particle == other) {
// Do not compute the force from and to itself.
continue;
}
Vector aux = computeForceVector(particle, other);
vector = vector.plus(aux);
}
particleToForceVectorMap.put(particle, vector);
}
}
/**
* Computes a repelling force vector from {@code other} to {@code target}.
*
* @param target the target particle.
* @param other the particle exerting repelling force towards
* {@code target}.
* @return the force vector.
*/
private Vector computeForceVector(Particle target, Particle other) {
double vectorLength = target.getRejectionForce(other);
double dx = target.getX() - other.getX();
double dy = target.getY() - other.getY();
double angle = Math.atan2(dy, dx);
double xComponent = vectorLength * Math.cos(angle);
double yComponent = vectorLength * Math.sin(angle);
return new Vector(xComponent, yComponent);
}
/**
* Updates the velocities of each particle.
*/
private void updateParticleVelocities() {
for (Map.Entry<Particle, Vector> e
: particleToForceVectorMap.entrySet()) {
Particle particle = e.getKey();
Vector vector = e.getValue();
// Make the force 'vector' a acceleration vector:
vector = vector.multiply(1.0 / particle.getMass());
// Update the velocity components:
particle.setVelocityX(
particle.getVelocityX() + vector.getX() * timeStep);
particle.setVelocityY(
particle.getVelocityY() + vector.getY() * timeStep);
}
}
/**
* Moves all the particles.
*/
private void moveParticles() {
for (Particle particle : particles) {
particle.setX(particle.getX() + particle.getVelocityX() * timeStep);
particle.setY(particle.getY() + particle.getVelocityY() * timeStep);
}
}
/**
* Resolves all the border collisions.
*/
private void resolveWorldBorderCollisions() {
for (Particle particle : particles) {
if (particle.getY() <= 0.0 || particle.getY() >= worldHeight) {
particle.setVelocityY(-particle.getVelocityY());
}
if (particle.getX() <= 0.0 || particle.getX() >= worldWidth) {
particle.setVelocityX(-particle.getVelocityX());
}
}
}
/**
* Normalizes the current velocity vectors such that the total energy of the
* system remains constant.
*/
private void normalizeVelocityVectors() {
double totalEnergyDelta = computeTotalEnergyDelta();
double factor = getNormalizationConstant(totalEnergyDelta);
for (Particle particle : particles) {
particle.setVelocityX(factor * particle.getVelocityX());
particle.setVelocityY(factor * particle.getVelocityY());
}
}
/**
* Computes the difference between initial total energy and current total
* energy.
*
* @return the total energy difference.
*/
private double computeTotalEnergyDelta() {
double currentTotalEnergy = computeTotalEnergy();
double totalEnergyDelta = totalEnergy - currentTotalEnergy;
return totalEnergyDelta;
}
/**
* Computes such a velocity normalization constant, that the total energy of
* the system remains constant.
*
* @param totalEnergyDelta the difference of initial and current total
* energies.
* @return the velocity normalization constant.
*/
private double getNormalizationConstant(double totalEnergyDelta) {
double aux = totalEnergyDelta / computeTotalKineticEnergy() + 1;
if (aux < 0.0) {
return 1.0;
}
return Math.sqrt(aux);
}
/**
* Computes the sum of kinetic energies of all the particles.
*
* @return the sum of kinetic energies.
*/
private double computeTotalKineticEnergy() {
double kineticEnergy = 0.0;
for (Particle particle : particles) {
kineticEnergy += particle.getKineticEnergy();
}
return kineticEnergy;
}
/**
* Computes the current total energy.
*
* @return the current total energy.
*/
public double computeTotalEnergy() {
double totalEnergy = 0.0;
for (Particle particle : particles) {
totalEnergy += particle.getKineticEnergy();
}
for (int i = 0; i < particles.size(); ++i) {
Particle particle1 = particles.get(i);
for (int j = i + 1; j < particles.size(); ++j) {
Particle particle2 = particles.get(j);
totalEnergy += particle1.getPotentialEnergy(particle2);
}
}
return totalEnergy;
}
/**
* Checks that there is no two different particles on the same spot.
*/
private void checkParticlesDoNotOverlap() {
for (int i = 0; i < particles.size(); ++i) {
Particle particle1 = particles.get(i);
for (int j = i + 1; j < particles.size(); ++j) {
Particle particle2 = particles.get(j);
if (particle1.getX() == particle2.getX()
&& particle1.getY() == particle2.getY()) {
throw new IllegalStateException(
"Two particles occupy the same spot.");
}
}
}
}
private double checkTimeStep(double timeStep) {
checkNonNaN(timeStep, "The time step is NaN.");
checkNonNegative(timeStep,
"The time step is non-positive: " + timeStep + ".");
checkNonInfinite(timeStep, "The time step is infinite.");
return timeStep;
}
private double checkWorldWidth(double worldWidth) {
return checkWorldDimension(
worldWidth,
"The world width is NaN.",
"The world width is non-positive: " + worldWidth,
"The world width is infinite.");
}
private double checkWorldHeight(double worldHeight) {
return checkWorldDimension(
worldHeight,
"The world height is NaN.",
"The world height is non-positive: " + worldHeight,
"The world height is infinite.");
}
private double checkWorldDimension(double dimension,
String errorMessageNaN,
String errorMessageNonPositive,
String errorMessageInfinite) {
checkNonNaN(dimension, errorMessageNaN);
checkNonNegative(dimension, errorMessageNonPositive);
checkNonInfinite(dimension, errorMessageInfinite);
return dimension;
}
private int checkSleepTime(int sleepTime) {
if (sleepTime < 1) {
throw new IllegalArgumentException(
"The sleep time is non-positive: " + sleepTime + ".");
}
return sleepTime;
}
private static void sleep(int milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException ex) {
}
}
}
Utils.java
package net.coderodde.simulation;
public final class Utils {
private Utils() {}
static void checkNonNaN(double value, String errorMessage) {
if (Double.isNaN(value)) {
throw new IllegalArgumentException(errorMessage);
}
}
static void checkNonNegative(double value, String errorMessage) {
if (value < 0.0) {
throw new IllegalArgumentException(errorMessage);
}
}
static void checkNonInfinite(double value, String errorMessage) {
if (Double.isInfinite(value)) {
throw new IllegalArgumentException(errorMessage);
}
}
}
Vector.java
package net.coderodde.simulation;
import static net.coderodde.simulation.Utils.checkNonInfinite;
import static net.coderodde.simulation.Utils.checkNonNaN;
/**
* This class implements a two-dimensional vector.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Sep 2, 2017)
*/
public final class Vector {
/**
* The x-component of this vector.
*/
private final double x;
/**
* The y-component of this vector.
*/
private final double y;
public Vector(double x, double y) {
this.x = checkX(x);
this.y = checkY(y);
}
public Vector() {
this(0.0, 0.0);
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public Vector plus(Vector other) {
return new Vector(x + other.x, y + other.y);
}
public Vector multiply(double factor) {
return new Vector(x * factor, y * factor);
}
public double dotProduct(Vector other) {
return x * other.x + y * other.y;
}
@Override
public String toString() {
return "(x=" + x + ", y=" + y + ")";
}
private double checkX(double x) {
return check(x,
"The x-component is NaN.",
"The x-component is infinite.");
}
private double checkY(double y) {
return check(y,
"The y-component is NaN.",
"The y-component is infinite.");
}
private double check(double value,
String errorMessageNaN,
String errorMessageInfinite) {
checkNonNaN(value, errorMessageNaN);
checkNonInfinite(value, errorMessageInfinite);
return value;
}
}
Pressing any key toggles a pause on or off. If it does not work, first click the window so that it gets the focus.
Windows If you suffer from the flickering, check this repo.