7

This morning I felt like writing a useless program and I ended up with this extremely minimal astronomic simulator in processing. MCVE version of code attached at the end of the post.

Then I added some planets, sit back and got ready to enjoy watching some planets orbiting around each other. However there turned out to be a problem.

The problem is, two originally static planets that getting too close to each other tend to fling each other to super ultra high speed and they will both disappear out of the screen forever. This is obviously against principle of momentum. Planets don't do this. I start to doubt there is something wrong with my Newton's Law implementation. But some other times the planets work just fine, provided that they keep a 'safe' distance.

If you wanna look into this problem, I have carefully included two setups for you. The first one, adds two planets and is beautifully stable. (You can do this by commenting out every line that has 'Three' in it.) You can test the second one by just input the default code. This one is where planets go crazy.

This issue seems like a mystery that origins from some computer science domain I have never explored(I am a noob though). For the sake of my hair, I would greatly appreciate it if someone could explain.

Start of code:

PVector planetOneLocation = new PVector(300, 200);
PVector planetOneSpeed = new PVector(0, -.1);
float planetOneMass = 1;

PVector planetTwoLocation = new PVector(100, 200);
PVector planetTwoSpeed = new PVector(0, .1);
float planetTwoMass = 1;

PVector planetThreeLocation = new PVector(200, 200);
PVector planetThreeSpeed = new PVector(0, 0);
float planetThreeMass = 10;

float g = 5;

void setup() {
  size(500, 500);
}

void draw() {

  updatePlanetOne();
  updatePlanetTwo();
  updatePlanetThree();

  planetOneLocation.add(planetOneSpeed);
  planetTwoLocation.add(planetTwoSpeed);
  planetThreeLocation.add(planetThreeSpeed);

  background(0);

  ellipse(planetOneLocation.x, planetOneLocation.y, 10*planetOneMass, 10*planetOneMass);
  ellipse(planetTwoLocation.x, planetTwoLocation.y, 10*planetTwoMass, 10*planetTwoMass);
  ellipse(planetThreeLocation.x, planetThreeLocation.y, 10*planetThreeMass, 10*planetThreeMass);
}

void updatePlanetOne() {
  PVector accDir = PVector.sub(planetTwoLocation, planetOneLocation);
  float a = g * planetTwoMass / (accDir.mag() * accDir.mag());
  accDir.normalize();
  PVector acceleration = accDir.mult(a);
  planetOneSpeed.add(acceleration);

  accDir = PVector.sub(planetThreeLocation, planetOneLocation);
  a = g * planetThreeMass / (accDir.mag() * accDir.mag());
  accDir.normalize();
  acceleration = accDir.mult(a);
  planetOneSpeed.add(acceleration);
}

void updatePlanetTwo() {
  PVector accDir = PVector.sub(planetOneLocation, planetTwoLocation);
  float a = g * planetOneMass / (accDir.mag() * accDir.mag());
  accDir.normalize();
  PVector acceleration = accDir.mult(a);
  planetTwoSpeed.add(acceleration);

  accDir = PVector.sub(planetThreeLocation, planetTwoLocation);
  a = g * planetThreeMass / (accDir.mag() * accDir.mag());
  accDir.normalize();
  acceleration = accDir.mult(a);
  planetTwoSpeed.add(acceleration);
}

void updatePlanetThree() {
  PVector accDir = PVector.sub(planetOneLocation, planetThreeLocation);
  float a = g * planetOneMass / (accDir.mag() * accDir.mag());
  accDir.normalize();
  PVector acceleration = accDir.mult(a);
  planetThreeSpeed.add(acceleration);

  accDir = PVector.sub(planetTwoLocation, planetThreeLocation);
  a = g * planetTwoMass / (accDir.mag() * accDir.mag());
  accDir.normalize();
  acceleration = accDir.mult(a);
  planetThreeSpeed.add(acceleration);
}

Update: After some effort I change float variables to double. But my planets are stilling bouncing out of the screen. I think besides the double/float problem, actually there is some issues with the resolution. I didn't define any time step, and that also contribute to inaccurate behaviour especially when speed is high.

Update 2: Setting up time-step helps a lot. Some setups that went crazy without a time-step now works fine. But as long as there is the possibility that the center of two planets will be extremely close, there will be chances that the system go wild again. To solve this, a better integrator is needed.

Updata 3: In respond to @kevin-workman I ported his beautiful code here to replace the original project code. The same third planet in original post is added and the corresponding Newton math is updated. Contradictory to his tests, it looks even if mv.add(p.speed.mult(p.mass)); is commented out, the third planet still go crazy(Now since I have changed the demonstration code to a minimal version, there is no such line anymore). I think the error introduced by mult()do contribute, but concretely the unstable integrator also plays a major role.

CodeMouse92
  • 6,840
  • 14
  • 73
  • 130
shi
  • 286
  • 1
  • 9
  • 1
    Please put the relevant code into the question itself. Nobody wants to chase code through a link. – cf- May 13 '16 at 08:27
  • Okay. Code is attached :D – shi May 13 '16 at 08:35
  • You are sure that this is Java? I doubt that... – Uwe Allner May 13 '16 at 09:00
  • @UweAllner - Actually, the file(.pde) is of processing. But, basic java is allowed in processing. – Am_I_Helpful May 13 '16 at 09:02
  • @UweAllner Although it is written in a processing framework but I think the problem itself is more of a Java thingy. – shi May 13 '16 at 09:03
  • What data type are you using to store your position and velocity vectors? Float? Double? If Float try switching to Double and see if the behaviour changes. – Tim B May 13 '16 at 09:11
  • Extract your newton-calculation and debug it alone or better make a Unit-Test. There is probably an error in it. – hinneLinks May 13 '16 at 09:12
  • Something tell me you made the same mistake as NASA (can't remember the mission name). Using float is not precise, so you are going to have mathematics errors in some specific case. You can try with BigDecimal in Java to prevent these errors. – AxelH May 13 '16 at 09:14
  • So my thoughts follow Tim B's be aware that Floats have a limited accuracy, as do Doubles. So after a number of iterations it's quite likely you will start to see errors creep in. As per hinneLinks suggestion set up a test framework and watch what happens over time. – TJA May 13 '16 at 09:18
  • @AxelH The "very close together" makes me think that too. At that point limitations in float accuracy start becoming more and more significant and rounding errors more likely to expand. I'm not sure BigDecimal is a great idea though. 1 it's a lot slower and 2. you will actually need to do some rounding or your big decimal sizes will explode out massively. – Tim B May 13 '16 at 09:23
  • I agree with you guys about the float accuracy. In fact when distance is really small, the acceleration tends to be infinite accord to $a = \frac{km}{r^2}$. But can this explain everything? Think about two phase: 1. they get close and they accelerate at max float value 2. they apart and before leaving 'danger range' they decelerate at max float value. So why they always end up at high speed? – shi May 13 '16 at 09:28
  • Also since very big value of acceleration is guaranteed to happen, is there any good solution to it? – shi May 13 '16 at 09:31
  • @TimB I know, but Double will end up with the same problem, at some point. The only permanent solution is to use non-primitives value for SOME calculation, does where every decimal is usefull. But this will cost a lot in perf and memory... – AxelH May 13 '16 at 09:34
  • After some effort I change float variables to double. This solves the crazy momentum. But my planets are stilling bouncing out of the screen. I think besides the double/float problem, actually there is some issues with the resolution. I didn't define any time step, and that also contribute to inaccurate behaviour especially when speed is high. – shi May 13 '16 at 12:26
  • Your integrator is very unstable. Search for higher order integrators, e.g. Velocity Verlet or some predictor-corrector based method. – Hristo Iliev May 13 '16 at 13:18
  • To be more exact, the integrator is one of the symplectic Euler variants. Since that results in the exact integration of a Hamiltonion modified by an O(dt) term, large step sizes as dt=1 may lead to rather unphysical solutions. – Lutz Lehmann May 13 '16 at 15:14
  • To avoid the singularity, change that line to something like `a = g * p.mass / (r*r + accDir.mag() * accDir.mag());` where `r` is some value on the scale of a typical planet size, with your setup you might use `r=1` or `r=10`. This is still a conservative system. – Lutz Lehmann May 13 '16 at 15:20
  • Did you ever get this sorted out? – Kevin Workman Oct 24 '16 at 17:28
  • @KevinWorkman Yeah actually I founded out that it was because my integrator was not stable enough. It worked after I change to verlot – shi Nov 13 '16 at 12:41

2 Answers2

10

This problem has nothing to do with float vs double accuracy. Float has more than enough accuracy for this, and in fact Processing uses float for everything by default, so any double values you try to use will just get lost anyway.

All of your problems are caused by this line:

for (Planet p: planets) {
    mv.add(p.speed.mult(p.mass));
}

Particularly this bit:

p.speed.mult(p.mass)

This line multiplies each planet's speed by its mass.

You might be thinking that p.speed.mult(p.mass) will leave the original p.speed PVector unchanged, but this isn't the case. PVector is not immutable, so calling the mult() function changes the underlying PVector instance.

Your first two planets have a mass of 1, so this line doesn't really affect them. But your third planet has a mass of 10, which means you're multiplying its speed by 10 every single frame.

You can test this by simply changing the mass of one of your original planets to 10 or even 2, or by changing the mass of the third planet to 1.

So, to fix your main problem, just get rid of this line, or change it so that it doesn't modify the p.speed PVector.

More info can be found in the Processing reference for PVector#mult():

Multiplies a vector by a scalar. The version of the method that uses a float acts directly on the vector upon which it is called (as in the first example above). The versions that receive both a PVector and a float as arguments are static methods, and each returns a new PVector that is the result of the multiplication operation.

Basically, you could change that line to this:

PVector.mult(p.speed, p.mass)

That will leave p.speed unchanged and return a copy with the multiplied value.

Taking a step back, you're going to have another issue because your planets can get arbitrarily close to each other. In other words, their distance can approach (or equal) zero. This doesn't happen in real life, and if it does, you can bet things are going to go crazy. So you're going to have crazy "gravity assists" if things get too close. You might consider limiting their distances.

If you have further questions, it'll be easier to help you if you work from this MCVE instead of posting your whole project:

PVector planetOneLocation = new PVector(300, 200);
PVector planetOneSpeed = new PVector(0, -.1);
float planetOneMass = 1;

PVector planetTwoLocation = new PVector(100, 200);
PVector planetTwoSpeed = new PVector(0, .1);
float planetTwoMass = 10;

float g = 5;

void setup() {
  size(500, 500);
}

void draw() {

  updatePlanetOne();
  updatePlanetTwo();

  planetOneLocation.add(planetOneSpeed);
  planetTwoLocation.add(planetTwoSpeed);

  background(0);

  ellipse(planetOneLocation.x, planetOneLocation.y, 10*planetOneMass, 10*planetOneMass);
  ellipse(planetTwoLocation.x, planetTwoLocation.y, 10*planetTwoMass, 10*planetTwoMass);
}

void updatePlanetOne() {
  PVector accDir = PVector.sub(planetTwoLocation, planetOneLocation);
  float a = g * planetOneMass / (accDir.mag() * accDir.mag());
  accDir.normalize();
  PVector acceleration = accDir.mult(a);
  planetOneSpeed.add(acceleration);
}

void updatePlanetTwo() {
  PVector accDir = PVector.sub(planetOneLocation, planetTwoLocation);
  float a = g * planetTwoMass / (accDir.mag() * accDir.mag());
  accDir.normalize();
  PVector acceleration = accDir.mult(a);
  planetTwoSpeed.add(acceleration);
}
Community
  • 1
  • 1
Kevin Workman
  • 41,537
  • 9
  • 68
  • 107
  • Hey Kevin thanks for the great post! This explains some of the bug! Nicely done. However I must say that if you set the initial speed of both planets to 0 they still go crazy(also 0.01). Just wait one moment before you say 'Hey I have mentioned the distance thing and they are sure to go crazy', think about this model where two rigi singular mass interact in close distance in real life, no matter how they move, they end up following the newton's law(we are not going into more advanced astronomic here because that's not our model). And that is not the case your/my script reflect. – shi May 13 '16 at 16:39
  • By reading other's comment I know that my integration engine is not so stable. So there is something still need to be done to make this script work properly. That's why I up-voted your post but I did not accept it. P.S. Thanks for the tip of MCVE. Will do next time :D – shi May 13 '16 at 16:39
  • This answers the question of why your third planet goes crazy. If you have another question about two planets that are close together going crazy, please create a second post that focuses on just that problem. Make sure to include a [mcve]. – Kevin Workman May 13 '16 at 16:44
  • Sorry but I am afraid it did not answer the question completely. I comment out `mv.add(p.speed.mult(p.mass));` and I still see them rocketing away. – shi May 13 '16 at 16:59
  • @shi When I comment out that line, your code works fine for me. It sounds like you might have added another line that does something similar. Please post an updated [mcve] with the latest code you're running. – Kevin Workman May 13 '16 at 17:02
  • The code is uploaded in the question post. I am still wondering why we are getting different results. – shi May 14 '16 at 01:32
  • @shi It's very confusing to keep editing your question and then editing my answer. That makes it very hard for other people (especially people with similar problems in the future) to follow. Please ask a new question, and I'll be happy to help. I actually have an answer ready. But Stack Overlow isn't really designed for conversations, it's single questions and single answers. See also: http://meta.stackexchange.com/questions/156900/whats-the-policy-on-follow-up-questions and http://meta.stackoverflow.com/questions/266767/what-is-the-the-best-way-to-ask-follow-up-questions – Kevin Workman May 14 '16 at 04:21
0

The problem is in the integrating part. The one used is an Euler Integrator and it's not very accurate. Verlet integration is normally used for physics simulation.

Quoting Ilmari Karonen's Answer, verlet can be implemented as:

acceleration = force(time, position) / mass;
time += timestep;
position += timestep * (velocity + timestep * acceleration / 2);
newAcceleration = force(time, position) / mass;
velocity += timestep * (acceleration + newAcceleration) / 2;
Community
  • 1
  • 1
shi
  • 286
  • 1
  • 9