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
-
#dist(r, t) ⇒ Object
Compute the distance traveled by an object moving at velocity
r
for an amount of timet
. -
#drop_melon(blist, dt) ⇒ Object
Repeatedly call the update_melon method until the
y
position of the melon is less than or equal to they
position of the surface of the earth. -
#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. -
#falling_bodies(n) ⇒ Object
:nodoc:.
-
#make_system(*args) ⇒ Object
Initialize a new n-body system, returning an array of body objects.
-
#position_melon(blist, height) ⇒ Object
Position the melon (the first body in the array
blist
) at a specified height above the earth. -
#random_bodies(n, big) ⇒ Object
:nodoc:.
-
#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.
-
#robot ⇒ Object
Return a reference to the Turtle object that represents the robot explorer (created when the user calls view_robot).
-
#save_system(b, fn) ⇒ Object
Write the mass, position, and velocity for each body in array
b
to a file namedfn
. -
#scale(vec, origin, sf) ⇒ Object
Map a simulation’s (x,y) coordinates to screen coordinates using origin and scale factor.
-
#set_flag(fx, fy) ⇒ Object
Plant a “flag” at location
fx
,fy
, which will serve as a reference point for calls torobot.orient
to reorient the robot. -
#setOrigin(type) ⇒ Object
Make a vector that defines the location of the origin (in pixels).
-
#setScale(blist, origin, scale) ⇒ Object
Set the scale factor.
-
#step_system(bodies, dt) ⇒ Object
Run one time step of a full n-body simulation.
-
#update_melon(blist, dt) ⇒ Object
Execute one time step of the two-body simulation involving the two objects in
blist
. -
#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.
-
#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. -
#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.
-
#view_robot(userOptions = {}) ⇒ Object
Initialize the RubyLabs Canvas for an experiment with the robot explorer.
-
#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
.
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.[:hmax] puts "Height must be between 0 and #{@@drawing.[: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 |
#robot ⇒ Object
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.[:dash] > 0 @@drawing.[:dashcount] = (@@drawing.[:dashcount] + 1) % @@drawing.[:dash] if @@drawing.[:dashcount] == 0 @@drawing.[:pendown] = @@drawing.[: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.[: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..has_key?(:dash) @@drawing.[:dashcount] = (@@drawing.[:dashcount] + 1) % @@drawing.[:dash] if @@drawing.[:dashcount] == 0 @@drawing.[:pendown] = @@drawing.[: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.[: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..has_key?(:dash) @@drawing.[:dashcount] = (@@drawing.[:dashcount] + 1) % @@drawing.[:dash] if @@drawing.[:dashcount] == 0 @@drawing.[:pendown] = @@drawing.[: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.[: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 = {}) = @@droppingOptions.merge(userOptions) [:dashcount] = 0 [:pendown] = :track edge = [:canvasSize] mxmin = [:mxmin] mymin = [:mymin] hmax = [: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, ) 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 = {}) = @@robotOptions.merge(userOptions) poly = [:polygon].clone edge = [: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 [:flag] turtle.set_flag( *[:flag] ) end if [:track] turtle.track( [:track] ) end class <<turtle def plant_flag set_flag( @position.x, @position.y ) end end @@drawing = TurtleView.new( turtle, ) 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 = @@viewerOptions.merge(userOptions) origin = setOrigin([:origin]) sf = setScale(blist, [:origin], [: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, ) if .has_key?(:dash) [:dashcount] = 0 [:pendown] = :track elsif .has_key?(:pendown) [:pendown] = :track end return true end |