Code to resemble a rowing stroke - python

I'm working on a small program displaying moving rowing boats. The following shows a simple sample code (Python 2.x):
import time
class Boat:
def __init__(self, pace, spm):
self.pace = pace #velocity of the boat in m/s
self.spm = spm #strokes per minute
self.distance = 0 #distance travelled
def move(self, deltaT):
self.distance = self.distance + (self.pace * deltaT)
boat1 = Boat(3.33, 20)
while True:
boat1.move(0.1)
print boat1.distance
time.sleep(0.1)
As you can see a boat has a pace and rows with a number of strokes per minute. Everytime the method move(deltaT) is called it moves a certain distance according to the pace.
The above boat just travels at a constant pace which is not realistic. A real rowing boat accelerates at the beginning of a stroke and then decelerates after the rowing blades left the water. There are many graphs online which show a typical rowing curve (force shown here, velocity looks similar):
Source: highperformancerowing.net
The pace should be constant over time, but it should change during the stroke.
What is the best way to change the constant velocity into a curve which (at least basically) resembles a more realistic rowing stroke?
Note: Any ideas on how to tag this question better? Is it an algorithm-problem?

If your goal is to simply come up with something visually plausible and not to do a full physical simulation, you can simply add a sine wave to the position.
class Boat:
def __init__(self, pace, spm, var=0.5):
self.pace = pace #average velocity of the boat in m/s
self.sps = spm/60.0 #strokes per second
self.var = var #variation in speed from 0-1
self.totalT = 0 #total time
self.distance = 0 #distance traveled
def move(self, deltaT):
self.totalT += deltaT
self.distance = self.pace * (self.totalT + self.var * math.sin(self.totalT * self.sps * 2*math.pi)
You need to be careful with the variation var, if it gets too high the boat might go backwards and destroy the illusion.

You can convert a curve like this into a polynomial equation for velocity.
A description/example of how to do this can be found at:
python numpy/scipy curve fitting
This shows you how to take a set of x,y coordinates (which you can get by inspection of your existing plot or from actual data) and create a polynomial function.
If you are using the same curve for each Boat object, you could just hard code it into your program. But you could also have a separate polynomial equation for each Boat object as well, assuming each rower or boat has a different profile.

You can perform simple integration of the differential equation of motion. (This is what you are already doing to get space as a function of time, with constant speed, x' = x + V.dt.)
Assume a simple model with a constant force during the stroke and no force during the glide, and drag proportional to the speed.
So the acceleration is a = P - D.v during stroke, and - D.v during glide (deceleration).
The speed is approximated with v' = v + a.dt.
The space is approximated with x' = x + v.dt.
If dt is sufficiently small, this motion should look realistic. You can refine the model with a more accurate force law and better integration techniques like Runge-Kutta, but I am not sure it is worth it.
Below an example plot of speed and space vs time using this technique. It shows speed oscillations quickly establishing a periodic regime, and quasi-linear displacement with undulations.

Related

Python: How do I properly use integrate.solve_bvp for the 2-D projectile motion of an object with air resistance

I'm fairly new to programming and am trying to write a program using the inbuilt function 'integrate.solve_bvp' to determinine the trajectory of a projectile subject to boundary conditions.
I'm not by any means a programmer, so my knowledge and understanding is extremely limited. Please explain like I'm 5.
I need to be able to determine the launch and final velocity of a projectile, given its launch angle, and the time taken for it to return to the ground while considering drag.
I've written some code, but it doesn't work and I don't know why. I've tried reading the documentation, but it all goes over my head.
I started by considering the 1-D case (launch angle of 90 degrees), which seemed failly simple. I used SUVAT to gain a guess of the launch velocity (ie, the launch velocity if ignoring drag) and wrote the following code:
import numpy as np
from scipy import integrate
import matplotlib.pyplot as plt
drag_coef = 0.47 # average drag coefficient of a golf ball
air_density = 1.293
area = 1.45*10**-3 # cross sectionl area of a golf ball
mass = 45.9*10**-3 # mass of a golf ball
g = -9.80665
def function(time,height):
drag_factor = drag_coef * air_density * area / (2*mass)
return height[1], (g-drag_factor*height[1]*np.abs(height[1]))
def boundary_conditions(height_0,height_end):
return height_0[0], height_end[0]
time_scale = 10 # time at which you want projectile to hit the ground
velocity_0_guess = 49 # initial velocity guess (t=0)
time = np.linspace(0, time_scale, time_scale*1000+1)
height_0 = np.zeros(len(time))
velocity_0 = velocity_0_guess * np.ones(len(time))
height = np.array((height_0,velocity_0))
res = integrate.solve_bvp(function, boundary_conditions, time, height, max_nodes = time_scale*1000+1)
print(res.y[1] [0]) # calculated V_0
print(res.y[1] [time_scale*1000]) # Calculated V_end
print(res)
plt.plot(time, res.y[0], label="S_z") # caculated S_z
plt.xlabel("time [s]")
plt.ylabel("displacement [m]")
plt.show()
plt.plot(time, res.y[1], label="V_z") # calcuted V_z
plt.xlabel("time [s]")
plt.ylabel("velocity [m/s]")
plt.show()
However, even for this seemingly simple case, when I "print(res)"; I get a success result of 'False', along with the following statement:
message: 'The maximum number of mesh nodes is exceeded.'
And I don't know why as I think I've defined the number of nodes to equal the number of points in time that are being considered.
Clearly however this isn't the case as when I halve my 'time_scale' and 'velocity_0_guess' to 5 and 24.5 respecitvely, I get a successful result, even though both of these should equally valid:
message: 'The algorithm converged to the desired accuracy.'
I've tried to google the issue, but I haven't found anything that's been able to help me. I've looked through Reddit, and StackOverflow with no success. And I've even tried using ChatGPT to help fix my code, but that too was to no avail. So my step is posting a question.
I don't know if this is relevant, but I've been writing this program via the website: repl.it
The solver works iteratively. It alternates between computing an approximate solution of the DE on a fixed time grid and refining the time grid using an error estimate along the approximate solution.
The general idea is to start with an initial guess that roughly gives the shape of the desired solution. This usually should have no more points than necessary to define the shape. The solver then fills in the grid.
The resulting solution is found either in the function table res.x, res.y or with the "dense output" interpolation res.sol.
For your constant guess it is completely sufficient to have a minimal grid of two points
time = [0,time_scale]
height = [[0.0]*2, [velocity_0_guess]*2]
This finishes without complaint, giving res.y[1,[0,-1]] = [ 95.93681148, -30.32436139] and the number of grid points as len(res.x) = 27. Visibly, the time grid is no longer that of time, so you need to use res.x in the plots.
You can get a denser grid and more accurate solution by setting the error tolerance tol lower than its default value 1e-3
res = integrate.solve_bvp(function, boundary_conditions, time, height, tol=1e-6);
giving len(res.x) = 186 and res.y[1,[0,-1]] = [ 95.93702666, -30.32440457]

Random Walk of a Photon Through the Sun

For a project, I am trying to determine the time it would take for a photon to leave the Sun. However, I am having trouble with my code (found below).
More specifically, I set up a for loop with an if statement, and if some randomly generated probability is less than the probability of collision, that means the photon collides and it changes direction.
What I am having trouble with is setting up a condition where the for loop stops if the photon escapes (when distance > Sun radius). The one I have set up already doesn't appear to work.
I use a very scaled down measurement of the Sun's radius because if I didn't it would take a long time for the photon to escape in my simulation.
from numpy.random import random as rng # we want them random numbers
import numpy as np # for the math functions
import matplotlib.pyplot as plt # to make pretty pretty class
mass_proton = 1.67e-27
mass_electron = 9.11e-31
Thompson_cross = 6.65e-29
Sun_density = 150000
Sun_radius = .005
Mean_Free = (mass_proton + mass_electron)/(Thompson_cross*Sun_density*np.sqrt(2))
time_step= 10**-13 # Used this specifically in order for the path length to be < Mean free Path
path_length = (3e8)*time_step
Probability = 1-np.exp(-path_length/Mean_Free) # Probability of the photon colliding
def Random_walk():
x = 0 # Start at origin (0,0)
y = 0
N = 1000
m=0 # This is a counter I have set up for the number of collisions
for i in range(1,N+1):
prand = rng(N+1) # Randomly generated probability
if prand[i] < Probability: # If my prand is less than the probability
# of collision, the photon collides and changes
# direction
x += Mean_Free*np.cos(2*np.pi*prand)
y += Mean_Free*np.sin(2*np.pi*prand)
m += 1 # Everytime a collision occurs 1 is added to my collision counter
distance = np.sqrt(x**2 + y**2) # Final distance the photon covers
if np.all(distance) > Sun_radius: # if the distance the photon travels
break # is greater than the Radius of the Sun,
# the for loop stops, meaning the
#photon escaped
print(m)
return x,y,distance
x,y,d = Random_walk()
plt.plot(x,y, '--')
plt.plot(x[-1], y[-1], 'ro')
Any criticisms of my code are welcome, this is for a grade and I do want to learn how to do this correctly, so please tell me if you notice any other errors.
I don't understand the motivation for the formulas you've implemented. I'll explain my own motivation here, but if your instructor told you to do something else, I guess you should listen to them instead.
If I were going to do this, I would generate a sequence of movements of a photon, stopping when distance of the photon to the center of the sun is greater than the solar radius. Each movement is a sample from a distribution which has two components: one for the distance, and one for the direction. I will assume that these are independent (this may be questioned in a more careful simulation).
It seems plausible that the distribution of distance is an exponential distribution with parameter 1/(mean free path). Then the density is p(d) = (1/MFP) exp(-d/MFP). Its cdf is 1 - exp(-d/MFP) and the inverse of the cdf is -MFP log(1 - p) where p = cdf(d). Now you can sample from the distribution of distances: let p = rand(0, 1) where rand = uniform random and plug it into the inverse cdf to get d. This is called the inverse cdf method of sampling; a web search will find more info about it.
As for the direction, you can let angle = rand(0, 2*pi) and then (x, y) = (cos(angle), sin(angle)).
Now you can construct the series of positions. From an initial location, let the new location = previous + d*(x, y). Stop when distance of location to center is greater than radius.
Looks like a great problem! Good luck and have fun. Let me know if you have any questions.
Here is a way of thinking about the problem that you may find helpful. At each moment, the photon has a position (x, y) and a direction (dx, dy). The (dx, dy) variables are coefficients of the unit vector, so sqrt(dx**2 + dy**2) = 1.0. The distance traveled by the photon during one step is path_length * direction.
At each step you do 4 things:
calculate the photon's new position
figure out if the photon has left the sun by computing its distance from the center point
determine, with a single random number, whether or not the photon collides. If it does you randomly generate a new direction.
Append the photon's current position to a list. You might want to do this as a function of distance from the center rather than x,y.
At the end, you plot the list you have built up.
You should also choose a random direction at the very start.
I don't know how you will terminate the loop, for the photon isn't ever guaranteed to leave the sun - just like in the real world. In principle the program might run forever (or until the sun burns out).
There is a slight inaccuracy in that the photon can collide at any instant, not just at the end of one step. But since the steps are small, so is the error introduced by this simplification.
I will point out that you do not need numpy for any of this except perhaps the final plot. The standard Python library has all the math functions you need. Numpy is of course great for manipulating arrays of data, but the only array you will have here is the one you build, a step at a time, of photon position versus time.
As I pointed out in one of my comments, you are modeling the sun as a 2-dimensional object. If you want to do this calculation in three dimensions, you don't need to change this basic approach.

Particle-particle collision in box with periodic boundaries

The Task
For a class in molecular dynamics, I have to simulate 100 particles in a box with periodic boundaries. I have to take particle-particle collisions into account, since the walls are 'transparent' those interactions can be happen across the boundaries. Since the simulation should cover 50000 steps, and I'm expecting more and more additional tasks in the future, I want my code as efficient as possible (I have to use python, despite the long run time).
The Setting
The system consists of
100 Particles
Box with x = 10, y = 5
Mass = 2
Radius = 0.2
Velocity |v| = 0.5 per step
Simulation of 50000 steps
What I've done so fare
I have found this example for particles in a box with particle-particle collision. Since the author used a efficient implementation, I took his approach.
My relevant code parts are (in strong resemblance to the linked site):
class particlesInPeriodicBox:
# define the particle properties
def __init__(self,
initialState = [[0,0,0,0]], # state with form [x, y, vx, vy] for each particle
boundaries = [0, 10, 0, 5], # box boundaries with form [xmin, xmax, ymin, ymax] in nm
radius = 0.2, # particle radius in nm
mass = 2): # mass in g/mol. Here a parameter instead of a global variable
self.initialState = np.asarray(initialState, dtype=float)
self.state = self.initialState.copy()
self.radius = radius
self.time = 0 # keep count of time, if time, i want to implement a 'clock'
self.mass = mass # mass
self.boundaries = boundaries
def collision(self):
"""
now, one has to check for collisions. I do this by distance check and will solve this problems below.
To minimize the simulation time I then will only consider the particles that colided.
"""
dist = squareform(pdist(self.state[:, :2])) # direct distance
colPart1, colPart2 = np.where(dist < 2 * self.radius) # define collision partners 1 and 2 as those where the distance between the centeres are smaller than two times the radius
# resolve self-self collissions
unique = (colPart1 < colPart2)
colPart1 = colPart1[unique]
colPart2 = colPart2[unique]
"""
The following loop resolves the collisions. I zip the lists of collisionpartners to one aray,
where one entry contains both colisionpartners.
"""
for cp1, cp2 in zip(colPart1, colPart2): # cp1/cp2 are the two particles colliding in one collision.
# masses could be different in future...
m1 = self.mass[cp1]
m2 = self.mass[cp2]
# take the position (x,y) tuples for the two particles
r1 = self.state[cp1, :2]
r2 = self.state[cp2, :2]
# same with velocities
v1 = self.state[cp1, 2:]
v2 = self.state[cp2, 2:]
# get relative parameters
r = r1-r2
v = r2-r1
# center of mass velocity:
vcm = (m1 * v1 + m2 * v2) / (m1 + m2)
"""
This is the part with the elastic collision
"""
dotrr = np.dot(r, r) # the dot product of the relative position with itself
dotvr = np.dot(v, r) # the dot product of the relative velocity with the relative position
v = 2 * r * dotvr / dotrr - v # new relative velocity
"""
In this center of mass frame, the velocities 'reflect' on the center of mass in oposite directions
"""
self.state[cp1, 2:] = vcm + v * m2/(m1 + m2) # new velocity of particle 1 still considering possible different masses
self.state[cp2, 2:] = vcm - v * m1/(m1 + m2) # new velocity of particle 2
As I understand it, this technique of handling the operations to the whole arrays is more efficient than manually looping through it every time. Moving the particles 'trough' the wall is easy, I just subtract or add the dimension of the box, respectively. But:
The Problem
For now the algorithm only sees collision inside the box, but not across the boundaries. I thought about this problem a while now, and come up with the following Ideas:
I could make a total of 9 copies of this system in a 3x3 grid, and only considering the middle one, can so look into the neighboring cells for the nearest neighbor search. BUT i can't think of a effective way to implement this despite the fact, that this approach seams to be the standard way
Every other idea has some hand waving use of modulo, and im almost sure, that this is not the way to go.
If I had to boil it down, I guess my key questions are:
How do I take periodic boundaries into account when calculating
the distance between particles?
the actual particle-particle collision (elastic) and resulting directions?
For the first problem it might be possible to use techniques like in Calculation of Contact/Coordination number with Periodic Boundary Conditions, but Im not sure if that is the most efficient way.
Thank you!
Modulus is likely as quick an operation as you're going to get. In any self-respecting run-time system, this will attach directly to the on-chip floating-divide operations, which may well be faster than a tedious set of "if-subtract" pairs.
I think your 9-cell solution is overkill. Use 4 cells in a 2x2 matrix and check two regions: the original cell, and the same dimensions centered on the "four corners" point (middle of the 2x2). For any pair of points, the proper distance is the lesser of these two. Note that this method also gives you a frame in which you can easily calculate the momentum changes.
A third possible approach is to double the dimensions (ala the 2x2 above), but give each particle four sets of coordinates, one in each box. Alter your algorithms to consider all four when computing distance. If you have good vectorization packages and parallelism, this might be the preferred solution.

Uncontrollable Simulation Oscillation

This is likely a math problem as much as it is a programming problem, but I seem to be encountering severe oscillations in temperature in my class method "update()" when warp is set for a high value (1000+) in the code below. All temperatures are in Kelvin for simplicity.
(I am not a programmer by profession. This formatting is likely unpleasant.)
import math
#Critical to the Stefan-Boltzmann equation. Otherwise known as Sigma
BOLTZMANN_CONSTANT = 5.67e-8
class GeneratorObject(object):
"""Create a new object to run thermal simulation on."""
def __init__(self, mass, emissivity, surfaceArea, material, temp=0, power=5000, warp=1):
self.tK = temp #Temperature of the object.
self.mass = mass #Mass of the object.
self.emissivity = emissivity #Emissivity of the object. Always between 0 and 1.
self.surfaceArea = surfaceArea #Emissive surface area of the object.
self.material = material #Store the material name for some reason.
self.specificHeat = (0.45*1000)*self.mass #Get the specific heat of the object in J/kg (Iron: 0.45*1000=450J/kg)
self.power = power #Joules/Second (Watts) input. This is for heating the object.
self.warp = warp #Warp Multiplier. This pertains to how KSP's warp multiplier works.
def update(self):
"""Update the object's temperature according to it's properties."""
#This method updates the object's temperature according to heat losses and other factors.
self.tK -= (((self.emissivity * BOLTZMANN_CONSTANT * self.surfaceArea * (math.pow(self.tK,4) - math.pow(30+273.15,4))) / self.specificHeat) - (self.power / self.specificHeat)) * self.warp
The law used is the Stefan-Boltzmann law for calculating black-body heat losses:
Temp -= (Emissivity*Sigma*SurfaceArea*(Temp^4-Amb^4))/SpecificHeat)
This was ported from a KSP plugin for quicker debugging. Object.update() is called 50 times per second.
Would there be a solution to preventing these extreme oscillations that doesn't involve executing the code multiple times per step?
Your integration scheme is bad as already hinted by #Beta and #tom10. The integration timestep is self.warp units of time, i.e. self.warp seconds since your work with physical units. This is not the way things are done. You should first convert the equation to a dimensionless format by expressing each term in some sort of computational units. For example, the Stefan-Boltzmann constant and the self.power could be measured in units, in which the constant is 1. Then you should determine the characteristic time for the object, e.g. the time by which the temperature reaches to a certain degree the equilibrium one. If there are many such objects, you should find the smallest of all characteristic times and use it as unit of measurement for the time. Then the integration timestep should be about an order of magnitude less than the characteristic time, otherwise you completely miss the correct solution to the differential equation and end up with wild oscillations.
Example of what happens now: Let's take an 1 kg iron sphere. With surface area of 3,05.10^(-3) m^2 the radiative heating/cooling power is up to 1,73.10^(-10) W/K^4. With self.power equal to 5 kW, the radiative power equates the internal one when the temperature reaches 2319 K and that's the equilibrium temperature. At low temperatures the radiative heating/cooling is negligible and with the internal heating only you end up with temperature rate of 11,1 K/s. If warp is 1000+, your first integration step results in temperature of 11100 K or more, which overshoots the equilibrium one 5 times. Now the radiative energy is orders of magnitude higher than the internal heating and it leads to huge cool-down rate - multiply by 1000+ and you end up with negative temperature. And then the cycle repeats with higher and higher absolute temperatures until you hit outside the range of the floating-point arithmetic.
Here is a hint for you: if self.power is kept constant, then the equation has an analytical solution. Find it (or use a tool like Maple or Mathematica to find it for you) and then plot the solution. See how your timestep of 1000+ units compares to the timescale of the solution, i.e. the time it takes for the system to reach an almost equilibrium state.
I guess KSP = Kerbal Space Platform, so I gather this is a problem in game physics. If so maybe an approximation with the same qualitative behavior is sufficient. Maybe an exponential curve which starts at the initial temperature and falls to the ambient temperature is enough. Pick the decay constant by matching the heat transfer at the initial time.
Sometimes an approximation is good enough. I don't know if this is one of those situations.

What is wrong with my gravity simulation?

As per advice given to me in this answer, I have implemented a Runge-Kutta integrator in my gravity simulator.
However, after I simulate one year of the solar system, the positions are still off by cca 110 000 kilometers, which isn't acceptable.
My initial data was provided by NASA's HORIZONS system. Through it, I obtained position and velocity vectors of the planets, Pluto, the Moon, Deimos and Phobos at a specific point in time.
These vectors were 3D, however, some people told me that I could ignore the third dimension as the planets aligned themselves in a plate around the Sun, and so I did. I merely copied the x-y coordinates into my files.
This is the code of my improved update method:
"""
Measurement units:
[time] = s
[distance] = m
[mass] = kg
[velocity] = ms^-1
[acceleration] = ms^-2
"""
class Uni:
def Fg(self, b1, b2):
"""Returns the gravitational force acting between two bodies as a Vector2."""
a = abs(b1.position.x - b2.position.x) #Distance on the x axis
b = abs(b1.position.y - b2.position.y) #Distance on the y axis
r = math.sqrt(a*a + b*b)
fg = (self.G * b1.m * b2.m) / pow(r, 2)
return Vector2(a/r * fg, b/r * fg)
#After this is ran, all bodies have the correct accelerations:
def updateAccel(self):
#For every combination of two bodies (b1 and b2) out of all bodies:
for b1, b2 in combinations(self.bodies.values(), 2):
fg = self.Fg(b1, b2) #Calculate the gravitational force between them
#Add this force to the current force vector of the body:
if b1.position.x > b2.position.x:
b1.force.x -= fg.x
b2.force.x += fg.x
else:
b1.force.x += fg.x
b2.force.x -= fg.x
if b1.position.y > b2.position.y:
b1.force.y -= fg.y
b2.force.y += fg.y
else:
b1.force.y += fg.y
b2.force.y -= fg.y
#For body (b) in all bodies (self.bodies.itervalues()):
for b in self.bodies.itervalues():
b.acceleration.x = b.force.x/b.m
b.acceleration.y = b.force.y/b.m
b.force.null() #Reset the force as it's not needed anymore.
def RK4(self, dt, stage):
#For body (b) in all bodies (self.bodies.itervalues()):
for b in self.bodies.itervalues():
rd = b.rk4data #rk4data is an object where the integrator stores its intermediate data
if stage == 1:
rd.px[0] = b.position.x
rd.py[0] = b.position.y
rd.vx[0] = b.velocity.x
rd.vy[0] = b.velocity.y
rd.ax[0] = b.acceleration.x
rd.ay[0] = b.acceleration.y
if stage == 2:
rd.px[1] = rd.px[0] + 0.5*rd.vx[0]*dt
rd.py[1] = rd.py[0] + 0.5*rd.vy[0]*dt
rd.vx[1] = rd.vx[0] + 0.5*rd.ax[0]*dt
rd.vy[1] = rd.vy[0] + 0.5*rd.ay[0]*dt
rd.ax[1] = b.acceleration.x
rd.ay[1] = b.acceleration.y
if stage == 3:
rd.px[2] = rd.px[0] + 0.5*rd.vx[1]*dt
rd.py[2] = rd.py[0] + 0.5*rd.vy[1]*dt
rd.vx[2] = rd.vx[0] + 0.5*rd.ax[1]*dt
rd.vy[2] = rd.vy[0] + 0.5*rd.ay[1]*dt
rd.ax[2] = b.acceleration.x
rd.ay[2] = b.acceleration.y
if stage == 4:
rd.px[3] = rd.px[0] + rd.vx[2]*dt
rd.py[3] = rd.py[0] + rd.vy[2]*dt
rd.vx[3] = rd.vx[0] + rd.ax[2]*dt
rd.vy[3] = rd.vy[0] + rd.ay[2]*dt
rd.ax[3] = b.acceleration.x
rd.ay[3] = b.acceleration.y
b.position.x = rd.px[stage-1]
b.position.y = rd.py[stage-1]
def update (self, dt):
"""Pushes the uni 'dt' seconds forward in time."""
#Repeat four times:
for i in range(1, 5, 1):
self.updateAccel() #Calculate the current acceleration of all bodies
self.RK4(dt, i) #ith Runge-Kutta step
#Set the results of the Runge-Kutta algorithm to the bodies:
for b in self.bodies.itervalues():
rd = b.rk4data
b.position.x = b.rk4data.px[0] + (dt/6.0)*(rd.vx[0] + 2*rd.vx[1] + 2*rd.vx[2] + rd.vx[3]) #original_x + delta_x
b.position.y = b.rk4data.py[0] + (dt/6.0)*(rd.vy[0] + 2*rd.vy[1] + 2*rd.vy[2] + rd.vy[3])
b.velocity.x = b.rk4data.vx[0] + (dt/6.0)*(rd.ax[0] + 2*rd.ax[1] + 2*rd.ax[2] + rd.ax[3])
b.velocity.y = b.rk4data.vy[0] + (dt/6.0)*(rd.ay[0] + 2*rd.ay[1] + 2*rd.ay[2] + rd.ay[3])
self.time += dt #Internal time variable
The algorithm is as follows:
Update the accelerations of all bodies in the system
RK4(first step)
goto 1
RK4(second)
goto 1
RK4(third)
goto 1
RK4(fourth)
Did I mess something up with my RK4 implementation? Or did I just start with corrupted data (too few important bodies and ignoring the 3rd dimension)?
How can this be fixed?
Explanation of my data etc...
All of my coordinates are relative to the Sun (i.e. the Sun is at (0, 0)).
./my_simulator 1yr
Earth position: (-1.47589927462e+11, 18668756050.4)
HORIZONS (NASA):
Earth position: (-1.474760457316177E+11, 1.900200786726017E+10)
I got the 110 000 km error by subtracting the Earth's x coordinate given by NASA from the one predicted by my simulator.
relative error = (my_x_coordinate - nasa_x_coordinate) / nasa_x_coordinate * 100
= (-1.47589927462e+11 + 1.474760457316177E+11) / -1.474760457316177E+11 * 100
= 0.077%
The relative error seems miniscule, but that's simply because Earth is really far away from the Sun both in my simulation and in NASA's. The distance is still huge and renders my simulator useless.
110 000 km absolute error means what relative error?
I got the 110 000 km value by subtracting my predicted Earth's x
coordinate with NASA's Earth x coordinate.
I'm not sure what you're calculating here or what you mean by "NASA's Earth x coordinate". That's a distance from what origin, in what coordinate system, at what time? (As far as I know, the earth moves in orbit around the sun, so its x-coordinate w.r.t. a coordinate system centered at the sun is changing all the time.)
In any case, you calculated an absolute error of 110,000 km by subtracting your calculated value from "NASA's Earth x coordinate". You seem to think this is a bad answer. What's your expectation? To hit it spot on? To be within a meter? One km? What's acceptable to you and why?
You get a relative error by dividing your error difference by "NASA's Earth x coordinate". Think of it as a percentage. What value do you get? If it's 1% or less, congratulate yourself. That would be quite good.
You should know that floating point numbers aren't exact on computers. (You can't represent 0.1 exactly in binary any more than you can represent 1/3 exactly in decimal.) There are going to be errors. Your job as a simulator is to understand the errors and minimize them as best you can.
You could have a stepsize problem. Try reducing your time step size by half and see if you do better. If you do, it says that your results have not converged. Reduce by half again until you achieve acceptable error.
Your equations might be poorly conditioned. Small initial errors will be amplified over time if that's true.
I'd suggest that you non-dimensionalize your equations and calculate the stability limit step size. Your intuition about what a "small enough" step size should be might surprise you.
I'd also read a bit more about the many body problem. It's subtle.
You might also try a numerical integration library instead of your integration scheme. You'll program your equations and give them to an industrial strength integrator. It could give some insight into whether or not it's your implementation or the physics that causes the problem.
Personally, I don't like your implementation. It'd be a better solution if you'd done it with mathematical vectors in mind. The "if" test for the relative positions leaves me cold. Vector mechanics would make the signs come out naturally.
UPDATE:
OK, your relative errors are pretty small.
Of course the absolute error does matter - depending on your requirements. If you're landing a vehicle on a planet you don't want to be off by that much.
So you need to stop making assumptions about what constitutes too small a step size and do what you must to drive the errors to an acceptable level.
Are all the quantities in your calculation 64-bit IEEE floating point numbers? If not, you'll never get there.
A 64 bit floating point number has about 16 digits of accuracy. If you need more than that, you'll have to use an infinite precision object like Java's BigDecimal or - wait for it - rescale your equations to use a length unit other than kilometers. If you scale all your distances by something meaningful for your problem (e.g., the diameter of the earth or the length of the major/minor axis of the earth's orbit) you might do better.
To do a RK4 integration of the solar system you need a very good precision or your solution will diverge quickly. Assuming you have implemented everything correctly, you may be seeing the drawbacks with RK for this sort of simulation.
To verify if this is the case: try a different integration algorithm. I found that using Verlet integration a solar system simulation will be much less chaotic. Verlet is much simpler to implement than RK4 as well.
The reason Verlet (and derived methods) are often better than RK4 for long term prediction (like full orbits) is that they are symplectic, that is, conserve momentum which RK4 does not. Thus Verlet will give a better behavior even after it diverges (a realistic simulation but with an error in it) whereas RK will give a non-physical behavior once it diverges.
Also: make sure you are using floating point as good as you can. Don't use distances in meters in the solar system, since the precision of floating point numbers is much better in the 0..1 interval. Using AU or some other normalized scale is much better than using meters. As suggested on the other topic, ensure you use an epoch for the time to avoid accumulating errors when adding time steps.
Such simulations are notoriously unreliable. Rounding errors accumulate and introduce instability. Increasing precision doesn't help much; the problem is that you (have to) use a finite step size and nature uses a zero step size.
You can reduce the problem by reducing the step size, so it takes longer for the errors to become apparent. If you are not doing this in real time, you can use a dynamic step size which reduces if two or more bodies are very close.
One thing I do with these kinds of simulations is "re-normalise" after each step to make the total energy the same. The sum of gravitational plus kinetic energy for the system as a whole should be a constant (conservation of energy). Work out the total energy after each step, and then scale all the object speeds by a constant amount to keep the total energy a constant. This at least keeps the output looking more plausible. Without this scaling, a tiny amount of energy is either added to or removed from the system after each step, and orbits tend to blow up to infinity or spiral into the sun.
Very simple changes that will improve things (proper usage of floating point values)
Change the unit system, to use as much mantissa bits as possible. Using meters, you're doing it wrong... Use AU, as suggested above. Even better, scale things so that the solar system fits in a 1x1x1 box
Already said in an other post : your time, compute it as time = epoch_count * time_step, not by adding ! This way, you avoid accumulating errors.
When doing a summation of several values, use a high precision sum algorithm, like Kahan summmation. In python, math.fsum does it for you.
Shouldn't the force decomposition be
th = atan(b, a);
return Vector2(cos(th) * fg, sin(th) * fg)
(see http://www.physicsclassroom.com/class/vectors/Lesson-3/Resolution-of-Forces or https://fiftyexamples.readthedocs.org/en/latest/gravity.html#solution)
BTW: you take the square root to calculate the distance, but you actually need the squared distance...
Why not simplify
r = math.sqrt(a*a + b*b)
fg = (self.G * b1.m * b2.m) / pow(r, 2)
to
r2 = a*a + b*b
fg = (self.G * b1.m * b2.m) / r2
I am not sure about python, but in some cases you get more precise calculations for intermediate results (Intel CPUs support 80 bit floats, but when assigning to variables, they get truncated to 64 bit):
Floating point computation changes if stored in intermediate "double" variable
It is not quite clear in what frame of reference you have your planet coordinates and velocities. If it is in heliocentric frame (the frame of reference is tied to the Sun), then you have two options: (i) perform calculations in non-inertial frame of reference (the Sun is not motionless), (ii) convert positions and velocities into the inertial barycentric frame of reference. If your coordinates and velocities are in barycentric frame of reference, you must have coordinates and velocities for the Sun as well.

Categories