Updated 2010-02-26 19:33:07 by AKgnome

This is a little toy I wrote for fun. It's a multibody gravity simulation. The most natural use for this is, of course, creating a solar system, so that's what the sample data is. Just a few planets though, no moons so far.


  • add some moons into the sample code.
  • add in some trojans (asteroids)
  • add ability to center on a particular body
  • optimize (with Critcl perhaps?)

using GL or whatever for 3-d would be sweet, but alas, not today.

The function "adddebris" is an attempt to dump small bodies into the L4/L5 points. Doesn't work too well.

In the function "addplanet", distance is in 1e6 km, mass is in 1e24 kg, and period is in earth years.

I hope the real solar system has more accurate numbers or math - the orbits tend to wander a fair amount after a while.

AM You solve the equations using the simplest of all schemes: Euler, first order. If you adapt it Runge-Kutta fourth order (see the Tcllib math::calculus module for an implementation) you will get much better results.

JR I've adapted this to use math::calculus (which interestingly I couldn't find any other uses of on this wiki), so switching beteeen euler and runge-kutta is easy. I don't notice huge differences.

AM No differences? Surprising. I know that I had, when I did something similar years ago (in FORTRAN using a DOS-based graphics package, so nothing that I would like to reanimate today :). Hm, worth looking into, I think.

As for the use of math::calculus: only few Wiki pages explicitly use external packages. Most simply are standalone. But I welcome the opportunity to see it used here.
 package require math::calculus

 # gravitational constant
 set G .00067
 set pi [expr atan(1)*4]

 proc grav {o1 o2} {
   global objs G pi
   # objects are list: {X Y mass id dx dy color}
   # returns a vector representing the force on o1
   foreach {o1x o1y o1m} $o1 {o2x o2y o2m} $o2 {break}
   set dx [expr {$o2x-$o1x}]
   set dy [expr {$o2y-$o1y}]
   set r2 [expr {$dx*$dx+$dy*$dy}]
   if ($r2==0) {set r2 1e-10}
   set F [expr {($G*($o1m*$o2m)/$r2)/$o1m}]
   if {$dx == 0} {
        if {$dy > 0} {
          set theta [expr {$pi/2}]
        } else {
          set theta [expr {-$pi/2}]
   } elseif {$dx < 0} {
     set theta [expr {$pi+atan($dy/$dx)}]
   } else {
     set theta [expr {atan($dy/$dx)}]
   set Fx [expr $F*cos($theta)]
   set Fy [expr $F*sin($theta)]
   return [list $Fx $Fy]

 proc sumgrav {o} {
   # returns the vector sum of all objects on O
   global objs
   set fv {}
   set mo [lindex $objs $o]
   foreach obj $objs {
     if {[lindex $mo 3] == [lindex $obj 3]} continue
     lappend fv [grav $mo $obj]
   set fx {}
   set fy {}
   foreach f $fv {
     lappend fx [lindex $f 0]
     lappend fy [lindex $f 1]
   return [list [expr [join $fx +]] [expr [join $fy +]]]

 proc accelerate {t vec} {
   foreach {xv yv xa ya} $vec {break}
   return [list $xa $ya 0 0]

 proc translate {t vec} {
   foreach {x y xv yv} $vec {break}
   return [list $xv $yv 0 0]

 set ts 3
 set trimdist 5000
 set time 0
 proc move {} {
   global objs ts trimdist time
   set oobjs {}

   for {set o 0} {$o < [llength $objs]} {incr o} {
     set ob [lindex $objs $o]
     set tr [sumgrav $o]
     # current x, y; mass, name; current x,y velocity;
     # x,y acceleration
     foreach {cx cy mass name cxv cyv} $ob  {xa ya} $tr {break}

     set avec [list $cxv $cyv $xa $ya]
     set res [math::calculus::rungeKuttaStep $time $ts $avec accelerate]
     foreach {nxv nyv xa ya} $res {break}
     set tvec [list $cx $cy $nxv $nyv]
     set res [math::calculus::rungeKuttaStep $time $ts $tvec translate]
     foreach {nx ny nxv nyv} $res {break}

     if {sqrt($nx*$nx+$ny*$ny) < $trimdist} {
       set ob [concat $nx $ny [lrange $ob 2 3] $nxv $nyv [lrange $ob 6 end]]
       lappend oobjs $ob
   set objs $oobjs

 set scale 0.2
 set trails 0
 set okeep 1000
 proc animate {} {
   global trails okeep
   if {!$trails} {
     .c delete all
   } else {
        set all [.c find all]
     if {[llength $all] > $okeep} {
          eval .c delete [lrange $all 0 [expr [llength $all]-1000]]
   global objs scale
   set ccx [expr [winfo width .c]/2]
   set ccy [expr [winfo height .c]/2]
   foreach o $objs {
     set cx [expr [lindex $o 0]*$scale+$ccx]
        set cy [expr [lindex $o 1]*$scale+$ccy]
        set cr [expr [lindex $o 2]*$scale/100000]
     .c create oval [expr $cx-$cr] [expr $cy-$cr] [expr $cx+$cr] [expr $cy+$cr] -outline [lindex $o 6]

 set playing 1
 set delay 25
 proc playing {} {
   global delay
   global playing
   if {$playing} {after $delay playing}

 proc float {number} {
   expr {$number+0.0}

 proc addplanet {name distance mass period color} {
   # distance is in 1e6 km
   # mass is in 1e24 kg
   # period is in earth years
   global objs
   set speed [expr $distance*1000*2*3.14/($period*365*846)]
   lappend objs [list 0 [float $distance] [float $mass] $name $speed 0 $color]

 proc adddebris {tag} {
   global objs
   set ob [lindex $objs [lsearch $objs *$tag*]]
   foreach {ox oy} $ob {sx sy} [lindex $objs 0] {break}
   set d 10
   set dx [expr $ox-$sx]
   set dy [expr $oy-$sy]
   set th [expr atan($dy/($dx+1e-10))]
   set dist [expr sqrt($dx*$dx+$dy*$dy)]
   for {set c 0} {$c < 5} {incr c} {
          lappend objs [list [expr cos($th+1.047)*$dist+$d*rand()] \
             [expr sin($th+1.047)*$dist+$d*rand()] 1e-10 trojan 0 0 gray]
          lappend objs [list [expr cos($th-1.047)*$dist+$d*rand()] \
             [expr sin($th-1.047)*$dist+$d*rand()] 1e-10 trojan 0 0 gray]

 proc sysmom {} {
   global objs
   set mx 0
   set my 0
   foreach o $objs {
     set mx [expr $mx + [lindex $o 2]*[lindex $o 4]]
     set my [expr $my + [lindex $o 2]*[lindex $o 5]]
   list $mx $my

 proc recenter {} {
   global objs
   set sun [lindex $objs 0]
   set sx [lindex $sun 0]
   set sy [lindex $sun 1]
   set nobjs {}
   foreach o $objs {
     lappend nobjs [concat [expr [lindex $o 0]-$sx] [expr [lindex $o 1]-$sy] [lrange $o 2 end]]
   set objs $nobjs

 proc setup {} {
   canvas .c -background black -width 400 -height 400
   frame .panel
   pack .c -expand t -fill both
   pack .panel
   # controls
   label .zooml -text "Zoom"
   scale .zoom -resolution .005 -orient horizontal \
     -from .001 -to 2 -variable scale
   grid .zooml .zoom -in .panel  -sticky s

   label .speedl -text "Delay"
   scale .speed -resolution 1 -orient horizontal -from 1 -to 100 -variable delay
   grid .speedl .speed -in .panel -sticky s

   label .timel -text "Time step"
   scale .time -resolution 0.5 -orient horizontal -from 0.5 -to 50 -variable ts
   grid .timel .time -in .panel -sticky s

   label .playl -text "Animate"
   checkbutton .play -command playing -variable playing
   grid .playl .play -in .panel

   label .traill -text "Show Trails"
   checkbutton .trail -variable trails
   grid .traill .trail -in .panel

   button .clear -command ".c delete all" -text Clear

   button .center -command "recenter" -text Recenter
   grid .clear .center -in .panel


 # solar system
 # the strange number for the sun's velocity is to try to keep the overall
 # momentum of the system to zero
 set objs { {0 0     1989000  sun   -0.00163355465017 0 yellow} }
 addplanet mercury 57.9 .33 .23 red
 addplanet venus 108.2 4.86 .613 green
 addplanet earth 149 5.97 1 blue
 addplanet mars 227 .64 2 red
 addplanet jupiter 778 1900.0 11.868 green
 addplanet saturn 1429 568.0 29.47 yellow
 addplanet uranus 2870 86.8 84.06 blue
 addplanet neptune 4504 102.47 164.90 green
 addplanet pluto 5913 .012 248.08 grey


schlenk Hmm, may i suggest OpenGL support for 3D?

TV Nice, though a bit depressing after a while.. It could be a good idea to have a course screen resolution as a test, I had that when I was a teenager doing this sort of thing, first order Euler or not (or backward, with prediction) then it is clear where roundoffs come from, or (energy) compensate by.

Consodering the attraction falls of with a square as a function of distance, it can be considered that using complicated or higher order approximations (unless they are for this purpose) may leave you with more stable (not usually the case with higher order polynomials, though 4 is not that high) or more accurate solutions, they also may not take non-linearity better into account than a lower order solution with a smaller timestep, and in extreme cases, like when planets sheer just past eachother, may be wrong.

I didn't look at the code, but would it be doable to make longer, and maybe fading trails ?

JR - longer trails is a cinch, just change the value of "okeep". Increasing it too much will eventually bog down the canvas widget tho. Fading trails would be somewhat more work and significantly slower (it would need to run through the whole list of objects on the canvas with each or every few passes)

GWM If you need more accuracy you could try the Runge-Kutta method.

See also TclPlanets