6

I'm trying to make a simplified simulation of a 1-dimensional wave with a chain of harmonic oscillators. The differential equation for the position x[i] of the i-th oscillator (assuming equilibrium at x[i]=0) turns out to be - according to textbooks - this one:

m*x[i]''=-k(2*x[i]-x[i-1]-x[i+1])

(the derivative is wrt the time) so i tried to numerically compute the dynamics with the following algorithm. Here osc[i] is the i-th oscillator as an object with attributes loc (absolute location), vel (velocity), acc (acceleration) and bloc (equilibrium location), dt is the time increment:

for every i:
    osc[i].vel+=dt*osc[i].acc;
    osc[i].loc+=dt*osc[i].vel;
    osc[i].vel*=0.99;    
    osc[i].acc=-k*2*(osc[i].loc-osc[i].bloc);

    if(i!=0){
      osc[i].acc+=+k*(osc[i-1].loc-osc[i-1].bloc);
    }
    if(i!=N-1){
      osc[i].acc+=+k*(osc[i+1].loc-osc[i+1].bloc);
    }

You can see the algorithm in action here, just click to give an impulse to the 6-th oscillator. You can see that not only it doesn't generate a wave at all but it also generate a motion with growing total energy (even if I added the dampening!). What am I doing wrong?

tom10
  • 67,082
  • 10
  • 127
  • 137
Marco Disce
  • 234
  • 2
  • 13

4 Answers4

4

All given answers provide a (great) deal of useful information. What you must do is:

  • use ja72´s observation for performing the updates - you need to be coherent and compute the acceleration at a node based on the positions from the same iteration batch (i.e. do not mix $x⁽k⁾$ and $x⁽k+1⁾$ in the same expression of the acceleration as these are amounts belonging to different iteration steps)
  • forget about your original positions as tom10 said - you need to consider only relative positions - the behaviour of this wave equation is like a laplacian smoothing filter applied to a polygonal line - a point is relaxed using its two directly connected neighbours, being pulled towards the middle of the segment determined by those neighbours
  • finally, if you want your energy to be conserved (i.e. not have the water surface stop from vibrating), it's worth using a symplectic integrator. Symplectic/semi-implicit Euler will do. You don't need higher order integrators for this kind of simulation, but if you have the time, consider using Verlet or Ruth-Forest as they're both symplectic and of order 2 or 3 in terms of accuracy. Runge-Kutta will, like implicit Euler, draw energy (much slower) from the system, thus you'll get inherent damping. Explicit Euler will spell doom for you eventually - that's a bad choice - always (esp for stiff or undamped systems). There are tons of other integrators out there, so go ahead and experiment.

P.S. you did not implement true implicit Euler since that one requires simultaneously affecting all particles. For that, you must use a projected conjugate gradient method, derive Jacobians and solve sparse linear systems for your string of particles. Explicit or semi-implicit methods will get you the "follow the leader" behaviour you expect for an animation.

Now, if you want more, I implemented and tested this answer in SciLab (too lazy to program it in C++):

n=50;
for i=1:n
    x(i) = 1;
end

dt = 0.02;

k = 0.05;
x(20) = 1.1;
xold = x;
v = 0 * x;
plot(x, 'r');
plot(x*0,'r');
for iteration=1:10000
    for i = 1:n
        if (i>1 & i < n) then
            acc = k*(0.5*(xold(i-1) + xold(i+1)) - xold(i));
            v(i) = v(i) + dt * acc;
            x(i) = xold(i) + v(i) *dt;
        end
    end
    if (iteration/500  == round(iteration / 500) ) then
        plot(x - iteration/10000,'g');

    end
    xold = x;
end
plot(x,'b');

The wave evolution is seen in the stacked graphs below: enter image description here

teodron
  • 1,410
  • 1
  • 20
  • 41
  • Imlemented 2nd order Runge-Kutta and it seems to be the best even with very low dampening and high dt. The result can be seen [here](http://pokipsy.0fees.net/sandbox.html). Thank you very much to you and to the other people answering. – Marco Disce Sep 06 '13 at 07:57
2

The growing amplitude over time is likely an artifact of the simple Euler integration you're using. You can try using a shorter timestep, or try switching to semi-implicit Euler, aka symplectic Euler, which has better energy conservation properties.

As for the odd propagation behavior (where the wave seems to propagate very slowly), it might be that you have too low of a spring constant (k) relative to the particle mass.

Also, the equation you posted looks slightly wrong as it should involve k/m, not k*m. That is, the equation should be

x[i]''=-k/m(2*x[i]-x[i-1]-x[i+1])

(c.f. this Wikipedia page). However this only affects the overall speed of propagation.

Nathan Reed
  • 3,583
  • 1
  • 26
  • 33
  • Thanks for your advices, I implemented implicit Euler and now I can see the wave ([look here](http://pokipsy.0fees.net/sandbox.html)). I have also reduced dt and increased k but the energy still grows (you can see the amplitude of the wave exploding after a while). – Marco Disce Sep 03 '13 at 21:47
  • Welcome to the real world of modelling physical systems. Nature operates using zero step sizes; when we model numerically we are using an approximation - usually linear segments between data points. And invariably these non-zero step sizes end up adding energy to the system (almost never removing energy). Reducing the step size dt will delay but not eliminate the problem. A kludge I use which eliminates the problem is renormalising after every step to ensure the total energy remains constant when moving from t to t+dt. It is not mathematically correct, but it does solve the problem. – Peter Webb Sep 05 '13 at 09:31
2

You've expressed the initial equation incorrectly in your code in two important ways:

First, note that the equation only expresses relative distortions; that is, in the equation, if x[i]==x[i-1]==x[i+1] then x"[i]=0 regardless of how far x[i] is from zero. In your code, you're measuring the absolute distance from an equilibrium for each particle. That is, the equation fixes the row of oscillators only at the boundary, like a single spring held at the ends, but you're simulating a whole set of little springs, each fixed at some "equilibrium" point.

Second, your terms like osc[i].acc+=+k*(osc[i-1].loc-osc[i-1].bloc); don't make much sense. Here you're setting osc[i].acc based solely on the absolute position of the particle next to it, and not on the relative position between the two.

This second problem is probably why you're gaining energy, but that's only a side effect of doing an incorrect simulation, not necessarily implying that your simulation is producing errors. Currently there's no evidence that you need to change from the simple Euler simulation (as Nathan suggested). First get the equation correct, and then get fancy with the simulation method if you need to.

Instead, write the equation for relative displacements, ie, with terms like osc.loc[i]-osc.loc[i-1].

comment: I rarely like to comment on others' answers to a question I've answered, but both Nathan and ja72 are focusing on things that just aren't the main issue. First get your simulation equations correct, then, maybe, worry about fancier approaches than Euler, and the order of updating your terms, etc, if you ever need to. For a simple, linear, first order equation, especially with a little damping, the direct Euler method works fine if the time step is small enough, so get it working like this first.

tom10
  • 67,082
  • 10
  • 127
  • 137
  • Wait: combining the three code lines `osc[i].acc=-k*2*(osc[i].loc-osc[i].bloc)`, `osc[i].acc+=+k*(osc[i-1].loc-osc[i-1].bloc)` and `osc[i].acc+=+k*(osc[i+1].loc-osc[i+1].bloc)` actually is equivalent to `x[i]''=-k/m(2*x[i]-x[i-1]-x[i+1])` which is equivalent to `x[i]''=-k/m(x[i]-x[i-1])-k/m(x[i+1]-x[i])`, so I am actually considering relative positions. – Marco Disce Sep 04 '13 at 06:25
  • @Marco: provided the `bloc` positions are all equal – teodron Sep 04 '13 at 08:03
  • @teodron Even if they are not equal the difference `osc[i].loc-osc[i].bloc` is `x[i]`. – Marco Disce Sep 04 '13 at 09:52
  • 1
    @Marco I'll check the equations myself later on (maybe you'll succeed by then by yourself :) ). What I'd do is to consider vertical lines to constrain particle movements and attach springs between the particles. The springs are themselves 2D, but only their _vertical_ acceleration component will be able to act on the particles due to the constraints. If I get the same as you did, then it's strange indeed - still, it shouldn't be the integrator that you have to blame first. – teodron Sep 04 '13 at 09:56
  • @Marco: I guess you could think of `2*osc[i].block -osc[i-1].block-osc[i+1].bloc` as a weird way to express some type of equilibrium factor for `osc[i]`. But they aren't in the initial equation, and it's difficult to imagine the physical system that you're simulating with them in there, and I'm doubt you're simulating what you think you're simulating, so that the first step. – tom10 Sep 04 '13 at 14:24
  • Try this: set all the `bloc=0` and at the end of the timestep cycle, set `osc.loc[0]=0` and `osc.loc[N-1]=0`. Watching your simulation, the end points move a lot. I would've thought they'd be fixed. In fact, after watching for awhile, the whole thing sort-of drifts downward to make a ramp. This should not happen with fixed endpoints, and although the bloc idea doesn't make sense to me, I'd guess these should make this not happen too. – tom10 Sep 04 '13 at 14:44
  • @tom10 the wave equation involves a second derivative (not first) with respect to space, and the term on the right side here is a numerical approximation of it; see https://en.wikipedia.org/wiki/Wave_equation#From_Hooke.27s_law – Nathan Reed Sep 04 '13 at 16:25
  • @Marco: Yes, I know that, and certainly know what the wave equation is, but it's not my point at all. You're initial equation is correct (your approx to the wave equation), but your code doesn't express the correct physics. Diff EQ sims usually have two things: 1) the dynamics, and 2) the boundary conditions. It's the second that is messed up (which you can see by the drift in your simulation), and the whole bloc not expressed correctly. Just try my suggestion and you'll see (bloc=0, loc[0]=loc[N-1]=0`). I'm offline for awhile, btw... just give it a try. – tom10 Sep 04 '13 at 17:05
2

For one your acceleration osc[i].acc here depends on the updated position osc[i-1].loc and the not-updated yet position osc[i+1].loc.

You need to calculate all the accelerations first for all the nodes, and then in a separate loop update the positions and velocities.

It is best to create a function returning the accelerations given time, positions and velocities.

// calculate accelerations
// each spring has length L
// work out deflections of previous and next spring
for(i=1..N)
{
   prev_pos = if(i>1, pos[i-1], 0)
   next_pos = if(i<N, pos[i+1], i*L)
   prev_del = (pos[i]-prev_pos)-L
   next_del = (next_pos-pos[i])-L
   acc[i] = -(k/m)*(prev_del-next_del)
}

// calculate next step, with semi-implicit Euler
// step size is h
for(i=1..N)
{
    vel[i] = vel[i] + h*acc[i]
    pos[i] = pos[i] + h*vel[i]
}

You might need to use a higher order integrator scheme. Start by an 2nd order implicit Runge-Kutta.

John Alexiou
  • 28,472
  • 11
  • 77
  • 133