Module: RubyLabs::SphereLab

Defined in:
lib/spherelab.rb

Defined Under Namespace

Classes: Body, MelonView, NBodyView, Turtle, TurtleView, Vector

Constant Summary collapse

G =

The universal gravitational constant, assuming mass is

in units of kilograms, distances are in meters, and time is in seconds.
6.67E-11
@@sphereDirectory =

These class variables maintain the state of the display and other miscellaneous global values.

File.join(File.dirname(__FILE__), '..', 'data', 'spheres')
@@viewerOptions =
{
  :dotColor => '#000080',
  :dotRadius => 1.0,
  :origin => :center,
}
@@droppingOptions =
{
  :canvasSize => 400,
  :mxmin => 100,
  :mymin => 50,
  :hmax => 100,
  :dash => 1,
}
@@robotOptions =
{
  :canvasSize => 400,
  :polygon => [4,0,0,10,4,8,8,10],    
}
@@drawing =
nil
@@delay =
0.1

Instance Method Summary collapse

Instance Method Details

#dist(r, t) ⇒ Object

Compute the distance traveled by an object moving at velocity r for an amount of time t. – :begin :dist



883
884
885
# File 'lib/spherelab.rb', line 883

def dist(r, t)
  return r * t
end

#drop_melon(blist, dt) ⇒ Object

Repeatedly call the update_melon method until the y position of the melon is less than or equal to the y position of the surface of the earth. The two objects representing the melon and earth are passed in the array blist, and dt is the size of the time step to use in calls to update_melon. The return value is the amount of simulated time it takes the melon to hit the earth; it will be the product of dt and the number of time steps (number of calls to update_melon).

Example – to run an experiment that drops the melon from 50 meters, using a time step size of .01 seconds:

>> position_melon(b, 50)
=> 50
>> drop_melon(b, 0.01)
=> 3.19

– :begin :drop_melon



868
869
870
871
872
873
874
875
876
# File 'lib/spherelab.rb', line 868

def drop_melon(blist, dt)
  count = 0
  loop do
    res = update_melon(blist, dt)
    break if res == "splat"
    count += 1
  end
  return count * dt
end

#falling(t) ⇒ Object

Compute the distance an object will fall when it is initially stationary and then falls for an amount of time dt, accelerating according to the force of the Earth’s gravity. – :begin :falling



893
894
895
# File 'lib/spherelab.rb', line 893

def falling(t)
  return 0.5 * 9.8 * t**2
end

#falling_bodies(n) ⇒ Object

:nodoc:



528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/spherelab.rb', line 528

def falling_bodies(n)   # :nodoc: 
  raise "n must be 5 or more" unless n >= 5
  a = random_bodies(n-1, n-1)
  b = Body.new(1e13, (a[0].position + a[1].position), Vector.new(0,0,0))
  # b = Body.new(1e13, (a[0].position + a[1].position)*0.85, Vector.new(0,0,0))
  # pos = a[0].position
  # (1..(n-2)).each { |i| pos.add( a[i].position ) }
  # b = Body.new(1e14, pos * (1.0 / n), Vector.new(0,0,0))
  b.name = "falling"
  b.size = 5
  b.color = 'red'
  a.insert(0, b)
  return a
end

#make_system(*args) ⇒ Object

Initialize a new n-body system, returning an array of body objects. If the argument is a symbol it is the name of a predefined system in the SphereLab data directory, otherwise it should be the name of a file in the local directory.

Example:

>> b = make_system(:melon)
=> [melon: 3 kg (0,6.371e+06,0) (0,0,0), earth: 5.97e+24 kg (0,0,0) (0,0,0)]
>> b = make_system(:solarsystem)
=> [sun: 1.99e+30 kg (0,0,0) (0,0,0), ... pluto: 1.31e+22 kg (-4.5511e+12,3.1753e+11,1.2822e+12) (636,-5762.1,440.88)]

– When random bodies are working again add this back in to the comment:

... or the symbol :random, which...


556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
# File 'lib/spherelab.rb', line 556

def make_system(*args)
  bodies = []
  begin
    raise "usage: make_system(id)" unless args.length > 0
    if args[0] == :random
      raise "usage:  make_system(:random, n, m)" unless args.length >= 2 && args[1].class == Fixnum
      return random_bodies(args[1], args[2])
    elsif args[0] == :falling
      raise "usage:  make_system(:falling, n)" unless args.length >= 1 && args[1].class == Fixnum
      return falling_bodies(args[1])
    end
    filename = args[0]
    if filename.class == Symbol
      filename = File.join(@@sphereDirectory, filename.to_s + ".txt")
    end
    File.open(filename).each do |line|
      line.strip!
      next if line.length == 0
      next if line[0] == ?#
      a = line.chomp.split
      for i in 1..7
        a[i] = a[i].to_f
      end
      b = Body.new( a[1], Vector.new(a[2],a[3],a[4]), Vector.new(a[5],a[6],a[7]), a[0] )
      b.size = a[-2].to_i
      b.color = a[-1]
      bodies << b
    end
    if args[0] == :melon
      class <<bodies[0]
        def height
          return 0 if prevy.nil?
          return position.y - prevy
        end
        # def height=(x)
        # end
      end       
    end
  rescue
    puts "error: #{$!}"
    return nil
  end
  return bodies
end

#position_melon(blist, height) ⇒ Object

Position the melon (the first body in the array blist) at a specified height above the earth.

Example:

>> position_melon(b, 50)
=> 50

– If no drawing, save the current position in prevy, but if drawing, get the earth’s surface from the drawing (this allows students to move the melon up or down in the middle of an experiment)



807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
# File 'lib/spherelab.rb', line 807

def position_melon(blist, height)
  melon = blist[0]
  if @@drawing && blist[0].graphic
    if height < 0 || height > @@drawing.options[:hmax]
      puts "Height must be between 0 and #{@@drawing.options[:hmax]} meters"
      return false
    end
    melon.prevy = @@drawing.ground
    melon.position.y = @@drawing.ground + height
    a = @@drawing.startloc.clone
    a[1] -= height * @@drawing.scale
    a[3] -= height * @@drawing.scale
    melon.graphic.coords = a
  else
    melon.prevy = melon.position.y
    melon.position.y += height
  end
  melon.velocity.y = 0.0
  return height
end

#random_bodies(n, big) ⇒ Object

:nodoc:



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/spherelab.rb', line 501

def random_bodies(n, big)   # :nodoc:
  big = 1 if big.nil?
  moving = true if moving.nil?
  res = []
  mm = 1e12          # average mass
  mr = 150          # average distance from origin
  bigm = (mm * 100) / big
  big.times do |i|
    r, v = random_vectors(mr/2, i, big)
    ms = (1 + (rand/2 - 0.25) )
    b = Body.new(bigm*ms, r, v)
    b.name = "b#{i}"
    b.color = '#0080ff'
    b.size = 10*ms
    res << b
  end
  (n-big).times do |i|
    r, v = random_vectors(mr, i, n-big)
    b = Body.new(mm, r, v)
    b.name = "b#{i+big}"
    b.color = '#0080ff'
    b.size = 5
    res << b
  end
  return res
end

#random_vectors(r, i, n) ⇒ Object

The methods that make random bodies need to be fixed – they need a more formal approach to define a system that “keeps together” longer. In the meantime, turn off documentation.…



489
490
491
492
493
494
495
496
497
498
499
# File 'lib/spherelab.rb', line 489

def random_vectors(r, i, n)   # :nodoc:
  theta = (2 * PI / n) * i + (PI * rand / n)
  # radius = r + (r/3)*(rand-0.5)
  radius = r + r * (rand-0.5)
  x = radius * cos(theta)
  y = radius * sin(theta)
  vtheta = (PI - theta) * -1 + PI * (rand-0.5)
  vx = radius/20 * cos(vtheta)
  vy = radius/20 * sin(vtheta)
  return Vector.new(x, y, 0), Vector.new(vx, vy, 0)
end

#robotObject

Return a reference to the Turtle object that represents the robot explorer (created when the user calls view_robot). In experiments, users combine a call to this method with a call to a method that controls the robot.

Example:

>> robot
=> #<SphereLab::Turtle x: 40 y: 200 heading: 360 speed: 10.00>
>> robot.heading
=> 360.0
>> robot.turn(30)
=> 30


476
477
478
479
480
481
482
483
# File 'lib/spherelab.rb', line 476

def robot
  if @@drawing.nil?
    puts "No robot; call view_robot to initialize"
    return nil
  else
    return @@drawing.turtle
  end
end

#save_system(b, fn) ⇒ Object

Write the mass, position, and velocity for each body in array b to a file named fn. The data in the file can later be read back in by passing the file name to make_system.

– Not intended to be used by students, but to save interesting data sets they can load and use.



606
607
608
609
610
611
612
613
614
615
616
617
# File 'lib/spherelab.rb', line 606

def save_system(b, fn)
  raise "file exists" if File.exists?(fn)
  File.open(fn, "w") do |f|
    b.each do |x|
      f.printf "%s %g %g %g %g %g %g %g %d %s\n", 
        x.name, x.mass, 
        x.position.x, x.position.y, x.position.z, 
        x.velocity.x, x.velocity.y, x.velocity.z, 
        x.size, x.color
    end
  end
end

#scale(vec, origin, sf) ⇒ Object

Map a simulation’s (x,y) coordinates to screen coordinates using origin and scale factor



726
727
728
729
730
731
# File 'lib/spherelab.rb', line 726

def scale(vec, origin, sf)  # :nodoc:
  loc = vec.clone
  loc.scale(sf)
  loc.add(origin)
  return loc.x, loc.y
end

#set_flag(fx, fy) ⇒ Object

Plant a “flag” at location fx, fy, which will serve as a reference point for calls to robot.orient to reorient the robot. The flag will be shown as a circle on the canvas at the specified position.



457
458
459
460
461
# File 'lib/spherelab.rb', line 457

def set_flag(fx, fy)
  r = 3.0
  Canvas::Circle.new( fx + r/2, fy + r/2, r, :fill => 'darkblue' )
  @reference = [ fx, fy ]          
end

#setOrigin(type) ⇒ Object

Make a vector that defines the location of the origin (in pixels).



735
736
737
738
739
740
741
742
# File 'lib/spherelab.rb', line 735

def setOrigin(type)   # :nodoc:
  case type
  when :center
    Vector.new( Canvas.width/2, Canvas.height/2, 0)
  else
    Vector.new(0,0,0)
  end
end

#setScale(blist, origin, scale) ⇒ Object

Set the scale factor. Use the parameter passed by the user, or find the largest coordinate in a list of bodies. Add 20% for a margin



747
748
749
750
751
752
753
754
755
756
757
758
# File 'lib/spherelab.rb', line 747

def setScale(blist, origin, scale)  # :nodoc:
  if scale == nil
    dmax = 0.0
    blist.each do |b|
      b.position.coords.each { |val| dmax = val.abs if val.abs > dmax } 
    end
  else
    dmax = scale
  end
  sf = (origin == :center) ? (Canvas.width / 2.0) : Canvas.width 
  return (sf / dmax) * 0.8
end

#step_system(bodies, dt) ⇒ Object

Run one time step of a full n-body simulation. Compute the pairwise interactions of all bodies in the system, update their force vectors, and then move them an amount determined by the time step size dt. – :begin :step_system



685
686
687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/spherelab.rb', line 685

def step_system(bodies, dt)
  nb = bodies.length

  for i in 0..(nb-1)      # compute all pairwise interactions
    for j in (i+1)..(nb-1)
      Body.interaction( bodies[i], bodies[j] )
    end
  end

  bodies.each do |b|
    b.move(dt)            # apply the accumulated forces
    b.clear_force         # reset force to 0 for next round
  end
end

#update_melon(blist, dt) ⇒ Object

Execute one time step of the two-body simulation involving the two objects in blist. Similar to the general purpose method update_system, except the drawing position of the earth is not updated.



832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
# File 'lib/spherelab.rb', line 832

def update_melon(blist, dt)
  melon = blist[0]
  mx = melon.position.x
  my = melon.position.y
  return "splat" if melon.prevy.nil? || my < melon.prevy
  step_system(blist, dt)
  if @@drawing && blist[0].graphic
    if @@drawing.options[:dash] > 0
      @@drawing.options[:dashcount] = (@@drawing.options[:dashcount] + 1) % @@drawing.options[:dash]
      if @@drawing.options[:dashcount] == 0
        @@drawing.options[:pendown] = @@drawing.options[:pendown].nil? ? :track : nil
      end
    end     
    dx = (melon.position.x - mx) * @@drawing.scale
    dy = (my - melon.position.y) * @@drawing.scale
    Canvas.move(melon.graphic, dx, dy, @@drawing.options[:pendown])
  end
  return blist[0].height
end

#update_one(falling, stationary, time) ⇒ Object

Run one step of a simulation of an n-body system where only one body is allowed to move and all the others are fixed in place. The first argument is a reference to the moving body, the second is any array containing references to the other bodies in the system, and the third is the time step size.



657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'lib/spherelab.rb', line 657

def update_one(falling, stationary, time)
  if falling.graphic.nil?
    puts "display the system with view_system"
    return nil
  end
  stationary.each do |x|
    Body.interaction( falling, x )
  end
  falling.move(time)
  if @@drawing.options.has_key?(:dash)
    @@drawing.options[:dashcount] = (@@drawing.options[:dashcount] + 1) % @@drawing.options[:dash]
    if @@drawing.options[:dashcount] == 0
      @@drawing.options[:pendown] = @@drawing.options[:pendown].nil? ? :track : nil
    end        
  end
  newx, newy = scale(falling.position, @@drawing.origin, @@drawing.scale)
  Canvas.move(falling.graphic, newx-falling.prevx, newy-falling.prevy, @@drawing.options[:pendown])
  falling.prevx = newx
  falling.prevy = newy
  falling.clear_force
  return true
end

#update_system(bodies, dt) ⇒ Object

This method will call step_system to simulate the motion of a set of bodies for the specified amount of time dt and then update their positions on the canvas.



704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
# File 'lib/spherelab.rb', line 704

def update_system(bodies, dt)
  return false unless @@drawing
  step_system(bodies, dt)
  if @@drawing.options.has_key?(:dash)
    @@drawing.options[:dashcount] = (@@drawing.options[:dashcount] + 1) % @@drawing.options[:dash]
    if @@drawing.options[:dashcount] == 0
      @@drawing.options[:pendown] = @@drawing.options[:pendown].nil? ? :track : nil
    end        
  end
  bodies.each do |b|
    next unless b.graphic
    newx, newy = scale(b.position, @@drawing.origin, @@drawing.scale)
    Canvas.move(b.graphic, newx-b.prevx, newy-b.prevy, @@drawing.options[:pendown])
    b.prevx = newx
    b.prevy = newy
  end
  return true
end

#view_melon(blist, userOptions = {}) ⇒ Object

Initialize the RubyLabs Canvas to show a drawing of a “2-body system” with a small circle to represent a watermelon and a much larger partial circle for the earth. The two Body objects representing the melon and earth should be passed in the array blist. Additonal optional arguments are viewing options, which have the following defaults:

:canvasSize => 400
:mxmin => 100         maximum melon x position
:mymin => 50,         maximum melon y position
:hmax => 100,         maximum melon height
:dash => 1,           size of dashes drawn as melon falls

Example:

>> b = make_system(:melon)
=> [melon: 3 kg (0,6.371e+06,0) (0,0,0), earth: 5.97e+24 kg (0,0,0) (0,0,0)]
>> view_melon(b)
=> true


777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
# File 'lib/spherelab.rb', line 777

def view_melon(blist, userOptions = {})
  options = @@droppingOptions.merge(userOptions)
  options[:dashcount] = 0
  options[:pendown] = :track
  edge = options[:canvasSize]
  mxmin = options[:mxmin]
  mymin = options[:mymin]
  hmax = options[:hmax]
  Canvas.init(edge, edge, "SphereLab::Melon")
  earth = Canvas::Circle.new(200, 2150, 1800, :fill => blist[1].color)
  blist[1].graphic = earth
  mymax = earth.coords[1]
  melon = Canvas::Circle.new(mxmin, mymax, 5, :fill => blist[0].color)
  blist[0].graphic = melon
  scale = (mymax-mymin) / hmax.to_f
  @@drawing = MelonView.new(blist, scale, blist[0].position.y, melon.coords, options)
  return true
end

#view_robot(userOptions = {}) ⇒ Object

Initialize the RubyLabs Canvas for an experiment with the robot explorer. The canvas will show a map of an area 400 x 400 meters and the robot (represented by a Turtle object) on the west edge, facing north. The two options that can be passed specify the canvas size and a polygon that determines its shape:

:canvasSize => 400,
:polygon => [4,0,0,10,4,8,8,10],


427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/spherelab.rb', line 427

def view_robot(userOptions = {})
  options = @@robotOptions.merge(userOptions)
  poly = options[:polygon].clone
  edge = options[:canvasSize]
  (0...poly.length).step(2) do |i| 
    poly[i] += edge/10
    poly[i+1] += edge/2
  end
  Canvas.init(edge, edge, "SphereLab::Robot")
  turtle = Turtle.new( :x => edge/10, :y => edge/2, :heading => 0, :speed => 10 )
  turtle.graphic = Canvas::Polygon.new(poly, :outline => 'black', :fill => '#00ff88')
  if options[:flag]
    turtle.set_flag( *options[:flag] )
  end
  if options[:track]
    turtle.track( options[:track] )
  end
  class <<turtle
    def plant_flag
      set_flag( @position.x, @position.y )
    end
  end
  @@drawing = TurtleView.new( turtle, options )
  return true
end

#view_system(blist, userOptions = {}) ⇒ Object

Initialize the RubyLabs Canvas to show the motion of a set of bodies in an n-body simulation and draw a circle for each body in blist.

Example – make a drawing with the Sun and the inner three planets:

>> b = make_system(:solarsystem)
=> [sun: 1.99e+30 kg (0,0,0) (0,0,0), ... ]
>> view_system(b[0..3])
=> true


628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
# File 'lib/spherelab.rb', line 628

def view_system(blist, userOptions = {})
  Canvas.init(700, 700, "SphereLab::N-Body")
  if prev = @@drawing
    prev.bodies.each { |x| x.graphic = nil }
  end
  options = @@viewerOptions.merge(userOptions)
  origin = setOrigin(options[:origin])
  sf = setScale(blist, options[:origin], options[:scale])
  blist.each do |b|
    x, y = scale(b.position, origin, sf)
    b.graphic = Canvas::Circle.new(x, y, b.size, :fill => b.color)
    b.prevx = x
    b.prevy = y
  end
  @@drawing = NBodyView.new(blist, origin, sf, options)
  if options.has_key?(:dash)
    options[:dashcount] = 0
    options[:pendown] = :track
  elsif options.has_key?(:pendown)
    options[:pendown] = :track
  end
  return true  
end