rubylabs 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/spherelab.rb CHANGED
@@ -1,20 +1,22 @@
1
+ module RubyLabs
1
2
 
2
3
  =begin rdoc
3
4
 
4
5
  == SphereLab
5
6
 
6
- Definition of Vector and Body objects used for n-body simulations.
7
+ The SphereLab module has definitions of classes and methods used in the projects for Chapter 11
8
+ of <em>Explorations in Computing</em>. The module includes a simple "turtle graphics" module
9
+ for experiments with moving objects, definitions of
10
+ Vector and Body objects used for n-body simulations, and methods for updating a system of
11
+ bodies based on the gravitational forces acting between pairs of bodies.
7
12
 
8
13
  =end
9
14
 
10
- module RubyLabs
11
-
15
+
12
16
  module SphereLab
13
17
 
14
- =begin rdoc
15
- These class variables maintain the state of the display and other miscellaneous
16
- global values.
17
- =end
18
+ # These class variables maintain the state of the display and other miscellaneous
19
+ # global values.
18
20
 
19
21
  @@sphereDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'spheres')
20
22
 
@@ -45,72 +47,77 @@ module SphereLab
45
47
  MelonView = Struct.new(:bodies, :scale, :ground, :startloc, :options)
46
48
 
47
49
  =begin rdoc
48
- The constant G is the universal gravitational constant, assuming mass is
50
+ The universal gravitational constant, assuming mass is
49
51
  in units of kilograms, distances are in meters, and time is in seconds.
50
52
  =end
51
53
 
52
54
  G = 6.67E-11
53
55
 
54
56
  =begin rdoc
55
- A Vector is a 3-tuple of (x,y,z) coordinates. Operators defined for
56
- vector objects (where v is a vector and a is a scalar):
57
- v == v
58
- v + v
59
- v - v
60
- v * a
61
- v.add(v) (equivalent of v += v)
62
- v.sub(v) (equivalent of v -= v)
63
- v.scale(a) (equivalent of v *= a)
64
- v.norm
57
+
58
+ == Vector
59
+
60
+ A Vector is a 3-tuple of (x,y,z) coordinates. Operators defined for
61
+ vector objects (where +v+ is a vector and +a+ is a scalar) are:
62
+ v == v
63
+ v + v
64
+ v - v
65
+ v * a
66
+ v.add(v) (equivalent to v += v)
67
+ v.sub(v) (equivalent to v -= v)
68
+ v.scale(a) (equivalent to v *= a)
69
+ v.norm
70
+ Arithmetic methods are invoked for a vector <tt>v1</tt> when Ruby evaluates an expression of the
71
+ form <tt>v1</tt> <op> <tt>v2</tt> where <op> is +, -, or *. They create a a new vector containing the
72
+ result of the operation. + and - do element-wise addition or and subtraction, *
73
+ is a vector-scalar multiplication.
74
+
75
+ The add, sub, and scale methods modify a vector, e.g. a call to <tt>v1.add(v2)</tt> will update
76
+ <tt>v1</tt> by adding the corresponding components in <tt>v2</tt>.
65
77
  =end
66
78
 
67
79
  class Vector
68
80
  attr_accessor :x, :y, :z
69
81
 
70
- =begin rdoc
71
- Make a new vector with the specified x, y, and z components.
72
- =end
82
+ # Make a new vector with the specified +x+, +y+, and +z+ components.
73
83
 
74
84
  def initialize(*args)
75
85
  @x, @y, @z = args
76
86
  end
87
+
88
+ # Create a string that displays the +x+, +y+, and +z+ coordinates of the vector.
77
89
 
78
90
  def inspect
79
91
  pos = [@x,@y,@z].map { |x| sprintf "%.5g", x }
80
92
  return "(" + pos.join(",") + ")"
81
93
  end
82
94
 
83
- =begin rdoc
84
- v1 == v2 if the three components are the same
85
- =end
95
+ # Compare this vector with another vector +v+. Two vectors are the same if
96
+ # all three components are the same.
86
97
 
87
98
  def ==(v)
88
99
  return (@x == v.x) && (@y == v.y) && (@z == v.z)
89
100
  end
90
101
 
91
- =begin rdoc
92
- Arithmetic methods are invoked for a vector v1 when Ruby evaluates an expression of the
93
- form v1 <op> v2 where <op> is +, -, or *. They create a a new vector containing the
94
- result of the operation. + and - do element-wise addition or and subtraction, *
95
- is a vector-scalar multiplication.
96
- =end
97
-
102
+ # Create a new vector that is the sum of this vector and vector +v+.
103
+
98
104
  def +(v)
99
105
  Vector.new(@x + v.x, @y + v.y, @z + v.z)
100
106
  end
107
+
108
+ # Create a new vector that is the difference between this vector and vector +v+.
101
109
 
102
110
  def -(v)
103
111
  Vector.new(@x - v.x, @y - v.y, @z - v.z)
104
112
  end
113
+
114
+ # Create a new vector that is the product of this vector and scalar +a+.
105
115
 
106
116
  def *(a)
107
117
  Vector.new(@x * a, @y * a, @z * a)
108
118
  end
109
119
 
110
- =begin rdoc
111
- v1.add(v2) adds the components of v2 to v1 -- would be v1 += v2 if Ruby
112
- allowed us to overload +=
113
- =end
120
+ # Add the components of vector +v+ to this vector.
114
121
 
115
122
  def add(v)
116
123
  @x += v.x
@@ -119,11 +126,8 @@ module SphereLab
119
126
  self
120
127
  end
121
128
 
122
- =begin rdoc
123
- v1.sub(v2) subtracts the components of v2 from v1 -- would be v1 -= v2 if Ruby
124
- allowed us to overload -=
125
- =end
126
-
129
+ # Subtract the components of vector +v+ from this vector.
130
+
127
131
  def sub(v)
128
132
  @x -= v.x
129
133
  @y -= v.y
@@ -131,9 +135,7 @@ module SphereLab
131
135
  self
132
136
  end
133
137
 
134
- =begin rdoc
135
- v.scale(a) -- multiply each element in v by scalar a
136
- =end
138
+ # Multiply each component of this vector by scalar +a+.
137
139
 
138
140
  def scale(a)
139
141
  @x *= a
@@ -141,31 +143,31 @@ module SphereLab
141
143
  @z *= a
142
144
  end
143
145
 
144
- =begin rdoc
145
- The magnitude of a vector is the Euclidean norm: sqrt(x**2 + y**2 + z**2)
146
- =end
146
+ # Compute the magnitude of this vector, which is the Euclidean norm
147
+ # defined by sqrt(x**2 + y**2 + z**2)
147
148
 
148
149
  def norm
149
150
  sqrt(@x*@x + @y*@y + @z*@z)
150
151
  end
151
152
 
152
- =begin rdoc
153
- Compute the angle between this vector and v -- used when drawing 2D vectors, so
154
- the z dimension is ignored
155
- =end
153
+ # Compute the angle between this vector and vector +v+. This method is
154
+ # only used when drawing 2D vectors, so the z dimension is ignored.
156
155
 
157
156
  def angle(v)
158
157
  acos((@x * v.x + @y * v.y) / (norm * v.norm))
159
158
  end
160
159
 
161
- =begin rdoc
162
- Accessor functions for the coordinates as a vector of three floats
163
- =end
164
-
160
+ # Return a vector of three Floats corresponding to the +x+, +y+, and +z+
161
+ # components of this vector.
162
+
165
163
  def coords
166
164
  [@x, @y, @z]
167
165
  end
168
166
 
167
+ # Set the three components of this vector to the values in the array +a+,
168
+ # which should be an array of three Floats (it can have more values, but
169
+ # only the first three are used).
170
+
169
171
  def coords=(a)
170
172
  @x = a[0]
171
173
  @y = a[1]
@@ -176,30 +178,34 @@ module SphereLab
176
178
 
177
179
 
178
180
  =begin rdoc
179
- A Body object represents the state of a celestial body. A body has mass (a scalar),
180
- position (a vector), and velocity (a vector). A third vector, named force, is used
181
- when calculating forces acting on a body. The size and color attributes are used by
182
- the visualization methods.
181
+
182
+ == Body
183
+
184
+ A Body object represents the state of a celestial body. A body has mass (a scalar),
185
+ position (a vector), and velocity (a vector). A third vector, named force, is used
186
+ when calculating forces acting on a body. The size and color attributes are used by
187
+ the visualization methods.
188
+
183
189
  =end
184
190
 
185
191
  class Body
186
192
  attr_accessor :mass, :position, :velocity, :force
187
193
  attr_accessor :name, :size, :color, :prevx, :prevy, :graphic
194
+
195
+ # Create a new Body object. If the argument list is empty, the attributes (mass,
196
+ # position, etc) are all set to 0. Otherwise the arguments should be +mass+ (a
197
+ # Float), +position+ (a vector), and +velocity+ (another vector). A fourth
198
+ # argument is an optional name for the body.
199
+ #
200
+ # Example:
201
+ # >> b = Body.new
202
+ # => : 0 kg (0,0,0) (0,0,0)
203
+ # >> b = Body.new(1e6, Vector.new(0,0,0), Vector.new(0,0,0), "test")
204
+ # => test: 1e+06 kg (0,0,0) (0,0,0)
188
205
 
189
206
  def initialize(*args)
190
- if args[1].class == Vector
207
+ if args.length > 0
191
208
  @mass, @position, @velocity, @name = args
192
- # elsif args[0] == :random
193
- # @mass = rand(10000) * 1e6
194
- # @position = Vector.new(rand(300)-150, rand(300)-150, 0)
195
- # @velocity = Vector.new(rand(20)-10, rand(10)-5, 0)
196
- # @name = "b" + self.object_id.to_s
197
- # elsif args[0].is_a?(Numeric) && args[1].is_a?(Numeric)
198
- # @mass = args[0]
199
- # @position = Vector.new(args[1], 0.0, 0.0)
200
- # @velocity = Vector.new(0.0, 0.0, 0.0)
201
- # @name = args[2]
202
- # @linear = true
203
209
  else
204
210
  @mass = 0.0
205
211
  @position = Vector.new(0.0, 0.0, 0.0)
@@ -209,6 +215,8 @@ module SphereLab
209
215
  @force = Vector.new(0.0, 0.0, 0.0)
210
216
  end
211
217
 
218
+ # Make a copy of this body.
219
+
212
220
  def clone
213
221
  copy = super
214
222
  if graphic
@@ -219,15 +227,16 @@ module SphereLab
219
227
  end
220
228
  return copy
221
229
  end
230
+
231
+ # Create a string that summarizes the state of this body.
222
232
 
223
233
  def inspect
224
234
  name = @name ? @name : ""
225
235
  return sprintf "%s: %.3g kg %s %s", name, @mass, @position.inspect, @velocity.inspect
226
236
  end
227
237
 
228
- =begin rdoc
229
- Reset the force vector to 0 (to get ready for a new round of interaction calculations).
230
- =end
238
+ # Reset the force vector to (0,0,0). Called by SphereLab#step_system at the start of each
239
+ # new round of interaction calculations.
231
240
 
232
241
  def clear_force
233
242
  @force.x = 0.0
@@ -235,9 +244,7 @@ module SphereLab
235
244
  @force.z = 0.0
236
245
  end
237
246
 
238
- =begin rdoc
239
- Compute force acting on this body wrt body b
240
- =end
247
+ # Compute the force exerted on this body by body +b+ and update this body's force vector.
241
248
 
242
249
  def add_force(b)
243
250
  r = @position - b.position
@@ -246,9 +253,7 @@ module SphereLab
246
253
  @force.add(r * mr)
247
254
  end
248
255
 
249
- =begin rdoc
250
- Move this body by applying current force vector for dt seconds
251
- =end
256
+ # Update this body's position by applying the current force vector for +dt+ seconds.
252
257
 
253
258
  def move(dt)
254
259
  acc = @force * G * -1.0
@@ -256,10 +261,10 @@ module SphereLab
256
261
  @position.add( @velocity * dt )
257
262
  end
258
263
 
259
- =begin rdoc
260
- Class method to compute the interaction between bodies b1 and b2 and update
261
- their force vectors.
262
- =end
264
+ # This class method will compute the interaction between bodies <tt>b1</tt> and <tt>b2</tt> and update
265
+ # their force vectors. Since the forces on the bodies are the same, but acting in the
266
+ # opposite direction, the force can be calculated just once and then used to update both
267
+ # bodies.
263
268
 
264
269
  def Body.interaction(b1, b2)
265
270
  r = b1.position - b2.position
@@ -271,9 +276,16 @@ module SphereLab
271
276
  end # Body
272
277
 
273
278
  =begin rdoc
274
- A class for rudimentary Turtle graphics. All we need is enough functionality to
275
- implement a robot that draws a circle by moving forward, then correcting its direction.
276
- Attributes: location and velocity vectors.
279
+
280
+ == Turtle
281
+
282
+ This class implements a rudimentary "turtle graphics" system. A turtle is an object that
283
+ moves around on a canvas, under direction from a user/program that tells it to advance, turn,
284
+ etc. A turtle also has a "pen" that can be raised or lowered. When the pen is down, the
285
+ turtle draws a line on the canvas as it moves.
286
+
287
+ A Turtle object is used in the robot explorer experiment, where the object is to get the robot
288
+ to move in as smooth a circle as possible about a central point.
277
289
  =end
278
290
 
279
291
  class Turtle
@@ -289,6 +301,19 @@ module SphereLab
289
301
  }
290
302
 
291
303
  @@north = Vector.new(0, 10, 0)
304
+
305
+ # Create a new Turtle object. The arguments (all optional) specify the turtle's starting
306
+ # position and orientation. The possible options and their default values:
307
+ # :x => 20
308
+ # :y => 100
309
+ # :heading => 0
310
+ # :speed => 10
311
+ # :track => :off
312
+ # Example:
313
+ # >> t = Turtle.new
314
+ # => #<SphereLab::Turtle x: 20 y: 100 heading: 360 speed: 10.00>
315
+ # >> t = Turtle.new( :x => 100, :y => 100, :speed => 5 )
316
+ # => #<SphereLab::Turtle x: 100 y: 100 heading: 360 speed: 5.00>
292
317
 
293
318
  def initialize(args = nil)
294
319
  args = { } if args.nil?
@@ -296,29 +321,44 @@ module SphereLab
296
321
  options = @@turtleOptions.merge(args)
297
322
  alpha = Canvas.radians(options[:heading] + 90.0)
298
323
  @position = Vector.new(options[:x], options[:y], 0.0)
299
- @velocity = Vector.new(-1.0 * options[:speed] * cos(alpha), options[:speed] * sin(alpha), 0.0)
324
+ if options[:speed] > 0
325
+ @velocity = Vector.new(-1.0 * options[:speed] * cos(alpha), options[:speed] * sin(alpha), 0.0)
326
+ else
327
+ raise "Turtle.new: speed must be greater than 0"
328
+ end
300
329
  @graphic = nil
301
330
  @reference = nil
302
331
  @tracking = options[:track]
303
332
  end
304
333
 
334
+ # Create a string that summarizes the status of this turtle object.
335
+
305
336
  def inspect
306
337
  sprintf "#<SphereLab::Turtle x: %d y: %d heading: %d speed: %.2f>", @position.x, @position.y, heading, speed
307
338
  end
308
339
 
340
+ # Return an array of two numbers representing the +x+ and +y+ coordinates of this object.
341
+
309
342
  def location
310
343
  return [@position.x, @position.y]
311
344
  end
312
345
 
346
+ # Return the current heading, in compass degrees, of this turtle object. The heading is a number between 0 and 360,
347
+ # where 0 is north, 90 is east, etc.
348
+
313
349
  def heading
314
350
  d = Canvas.degrees( @velocity.angle(@@north) )
315
351
  return (@velocity.x < 0) ? (360 - d) : d
316
352
  end
317
353
 
354
+ # Return the current speed (in meters per second) of this turtle object.
355
+
318
356
  def speed
319
357
  @velocity.norm
320
358
  end
321
359
 
360
+ # Reorient the turtle by telling it to turn clockwise by +alpha+ degrees.
361
+
322
362
  def turn(alpha)
323
363
  theta = -1.0 * Canvas.radians(alpha)
324
364
  x = @velocity.x * cos(theta) - @velocity.y * sin(theta)
@@ -326,11 +366,13 @@ module SphereLab
326
366
  @velocity.x = x
327
367
  @velocity.y = y
328
368
  if @graphic
329
- Canvas.rotate(@graphic,alpha)
369
+ @graphic.rotate(alpha)
330
370
  end
331
371
  return alpha
332
372
  end
333
373
 
374
+ # Tell the turtle to move straight ahead from its current position for +dt+ seconds.
375
+
334
376
  def advance(dt)
335
377
  prevx = @position.x
336
378
  prevy = @position.y
@@ -341,6 +383,10 @@ module SphereLab
341
383
  return dt
342
384
  end
343
385
 
386
+ # Tell the turtle to rotate until its heading is orthogonal to a reference point.
387
+ # See the Lab Manual for details about setting a reference point and how the
388
+ # orientation is calculated.
389
+
344
390
  def orient
345
391
  if @reference.nil?
346
392
  puts "no reference point"
@@ -355,6 +401,9 @@ module SphereLab
355
401
  return alpha
356
402
  end
357
403
 
404
+ # Turn tracking <tt>:on</tt> or <tt>:off</tt>. When tracking is <tt>:on</tt> the
405
+ # turtle draws a line on the canvas as it moves.
406
+
358
407
  def track(val)
359
408
  case val
360
409
  when :off : @tracking = :off
@@ -368,13 +417,13 @@ module SphereLab
368
417
 
369
418
  end # class Turtle
370
419
 
371
- =begin rdoc
372
- Set up an experiment with a robot/turtle. Initialize the canvas, make the turtle,
373
- assign it a graphic. These methods are all defined with respect to a canvas, and
374
- they will print an error message if the experiment has not been set up with a call
375
- to init_robot (which sets @@drawing).
376
- =end
377
-
420
+ # Initialize the RubyLabs Canvas for an experiment with the robot explorer. The
421
+ # canvas will show a map of an area 400 x 400 meters and the robot (represented by
422
+ # a Turtle object) on the west edge, facing north. The two options that can be
423
+ # passed specify the canvas size and a polygon that determines its shape:
424
+ # :canvasSize => 400,
425
+ # :polygon => [4,0,0,10,4,8,8,10],
426
+
378
427
  def view_robot(userOptions = {})
379
428
  options = @@robotOptions.merge(userOptions)
380
429
  poly = options[:polygon].clone
@@ -401,12 +450,29 @@ module SphereLab
401
450
  return true
402
451
  end
403
452
 
453
+ # Plant a "flag" at location +fx+, +fy+, which will serve as a reference point for
454
+ # calls to <tt>robot.orient</tt> to reorient the robot. The flag will be shown as
455
+ # a circle on the canvas at the specified position.
456
+
404
457
  def set_flag(fx, fy)
405
458
  r = 3.0
406
459
  Canvas::Circle.new( fx + r/2, fy + r/2, r, :fill => 'darkblue' )
407
460
  @reference = [ fx, fy ]
408
461
  end
409
-
462
+
463
+ # Return a reference to the Turtle object that represents the robot explorer
464
+ # (created when the user calls view_robot).
465
+ # In experiments, users combine a call to this method with a call to a method that
466
+ # controls the robot.
467
+ #
468
+ # Example:
469
+ # >> robot
470
+ # => #<SphereLab::Turtle x: 40 y: 200 heading: 360 speed: 10.00>
471
+ # >> robot.heading
472
+ # => 360.0
473
+ # >> robot.turn(30)
474
+ # => 30
475
+
410
476
  def robot
411
477
  if @@drawing.nil?
412
478
  puts "No robot; call view_robot to initialize"
@@ -415,24 +481,12 @@ module SphereLab
415
481
  return @@drawing.turtle
416
482
  end
417
483
  end
418
-
419
- # :begin :make_circle
420
- def make_circle(nseg, time, angle)
421
- view_robot
422
- robot.turn( angle/2 )
423
- nseg.times { robot.advance(time); robot.turn(angle) }
424
- robot.turn( -angle/2 )
425
- return true
426
- end
427
- # :end :make_circle
428
-
429
484
 
430
- =begin rdoc
431
- These methods create a set of bodies with random size, location, and initial
432
- velocity.
433
- =end
434
-
435
- def random_vectors(r, i, n)
485
+ # The methods that make random bodies need to be fixed -- they need a more formal
486
+ # approach to define a system that "keeps together" longer. In the meantime, turn
487
+ # off documentation....
488
+
489
+ def random_vectors(r, i, n) # :nodoc:
436
490
  theta = (2 * PI / n) * i + (PI * rand / n)
437
491
  # radius = r + (r/3)*(rand-0.5)
438
492
  radius = r + r * (rand-0.5)
@@ -444,13 +498,7 @@ module SphereLab
444
498
  return Vector.new(x, y, 0), Vector.new(vx, vy, 0)
445
499
  end
446
500
 
447
- def random_velocity(v, r)
448
- res = Vector.new(-r * cos)
449
- end
450
-
451
- # todo -- put mm, mr in global options
452
-
453
- def random_bodies(n, big)
501
+ def random_bodies(n, big) # :nodoc:
454
502
  big = 1 if big.nil?
455
503
  moving = true if moving.nil?
456
504
  res = []
@@ -477,7 +525,7 @@ module SphereLab
477
525
  return res
478
526
  end
479
527
 
480
- def falling_bodies(n)
528
+ def falling_bodies(n) # :nodoc:
481
529
  raise "n must be 5 or more" unless n >= 5
482
530
  a = random_bodies(n-1, n-1)
483
531
  b = Body.new(1e13, (a[0].position + a[1].position), Vector.new(0,0,0))
@@ -492,26 +540,33 @@ module SphereLab
492
540
  return a
493
541
  end
494
542
 
495
- =begin rdoc
496
- Initialize a new n-body system, returning an array of body objects. The
497
- parameter is the name of a predefined system, the name of a file, or the
498
- symbol :random.
499
- =end
500
-
501
- def make_system(*args)
543
+ # Initialize a new n-body system, returning an array of body objects. If the argument
544
+ # is a symbol it is the name of a predefined system in the SphereLab data directory,
545
+ # otherwise it should be the name of a file in the local directory.
546
+ #
547
+ # Example:
548
+ # >> b = make_system(:melon)
549
+ # => [melon: 3 kg (0,6.371e+06,0) (0,0,0), earth: 5.97e+24 kg (0,0,0) (0,0,0)]
550
+ # >> b = make_system(:solarsystem)
551
+ # => [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)]
552
+ #--
553
+ # When random bodies are working again add this back in to the comment:
554
+ # ... or the symbol :random, which...
555
+
556
+ def make_system(*args)
502
557
  bodies = []
503
- begin
504
- raise "usage: make_system(id)" unless args.length > 0
505
- if args[0] == :random
506
- raise "usage: make_system(:random, n, m)" unless args.length >= 2 && args[1].class == Fixnum
507
- return random_bodies(args[1], args[2])
508
- elsif args[0] == :falling
509
- raise "usage: make_system(:falling, n)" unless args.length >= 1 && args[1].class == Fixnum
510
- return falling_bodies(args[1])
511
- end
512
- filename = args[0]
513
- if filename.class == Symbol
514
- filename = File.join(@@sphereDirectory, filename.to_s + ".txt")
558
+ begin
559
+ raise "usage: make_system(id)" unless args.length > 0
560
+ if args[0] == :random
561
+ raise "usage: make_system(:random, n, m)" unless args.length >= 2 && args[1].class == Fixnum
562
+ return random_bodies(args[1], args[2])
563
+ elsif args[0] == :falling
564
+ raise "usage: make_system(:falling, n)" unless args.length >= 1 && args[1].class == Fixnum
565
+ return falling_bodies(args[1])
566
+ end
567
+ filename = args[0]
568
+ if filename.class == Symbol
569
+ filename = File.join(@@sphereDirectory, filename.to_s + ".txt")
515
570
  end
516
571
  File.open(filename).each do |line|
517
572
  line.strip!
@@ -536,37 +591,39 @@ module SphereLab
536
591
  # end
537
592
  end
538
593
  end
539
- rescue
540
- puts "error: #{$!}"
541
- return nil
542
- end
543
- return bodies
544
- end
545
-
546
- =begin rdoc
547
- Write the mass, position, and velocity for each body to a file. Not intended to
548
- be used by students, but used to save interesting data sets they can load and use.
549
- =end
550
-
551
- # d 4.5e16 300 -75 0 -5 2 0 6 #ff6666
552
-
553
-
554
- def save_system(b, fn)
555
- raise "file exists" if File.exists?(fn)
556
- File.open(fn, "w") do |f|
557
- b.each do |x|
558
- f.printf "%s %g %g %g %g %g %g %g %d %s\n",
559
- x.name, x.mass,
560
- x.position.x, x.position.y, x.position.z,
561
- x.velocity.x, x.velocity.y, x.velocity.z,
562
- x.size, x.color
563
- end
564
- end
565
- end
566
-
567
- =begin rdoc
568
- Initialize the drawing canvas by drawing a circle for each body in list b.
569
- =end
594
+ rescue
595
+ puts "error: #{$!}"
596
+ return nil
597
+ end
598
+ return bodies
599
+ end
600
+
601
+ # Write the mass, position, and velocity for each body in array +b+ to a file named +fn+.
602
+ # The data in the file can later be read back in by passing the file name to make_system.
603
+ #--
604
+ # Not intended to be used by students, but to save interesting data sets they can load and use.
605
+
606
+ def save_system(b, fn)
607
+ raise "file exists" if File.exists?(fn)
608
+ File.open(fn, "w") do |f|
609
+ b.each do |x|
610
+ f.printf "%s %g %g %g %g %g %g %g %d %s\n",
611
+ x.name, x.mass,
612
+ x.position.x, x.position.y, x.position.z,
613
+ x.velocity.x, x.velocity.y, x.velocity.z,
614
+ x.size, x.color
615
+ end
616
+ end
617
+ end
618
+
619
+ # Initialize the RubyLabs Canvas to show the motion of a set of bodies in an
620
+ # n-body simulation and draw a circle for each body in +blist+.
621
+ #
622
+ # Example -- make a drawing with the Sun and the inner three planets:
623
+ # >> b = make_system(:solarsystem)
624
+ # => [sun: 1.99e+30 kg (0,0,0) (0,0,0), ... ]
625
+ # >> view_system(b[0..3])
626
+ # => true
570
627
 
571
628
  def view_system(blist, userOptions = {})
572
629
  Canvas.init(700, 700, "SphereLab::N-Body")
@@ -592,10 +649,11 @@ module SphereLab
592
649
  return true
593
650
  end
594
651
 
595
- =begin rdoc
596
- Demonstrate adding force vectors by moving only one body
597
- =end
598
-
652
+ # Run one step of a simulation of an n-body system where only one body is allowed
653
+ # to move and all the others are fixed in place. The first argument is a reference
654
+ # to the moving body, the second is any array containing references to the other
655
+ # bodies in the system, and the third is the time step size.
656
+
599
657
  def update_one(falling, stationary, time)
600
658
  if falling.graphic.nil?
601
659
  puts "display the system with view_system"
@@ -619,31 +677,29 @@ module SphereLab
619
677
  return true
620
678
  end
621
679
 
622
- =begin rdoc
623
- Call SphereLab::step(bodies, time) to calculate the pairwise interactions between all
624
- Body objects in the array +bodies+ and then compute their new positions after +time+ seconds.
625
- =end
626
-
627
- # :begin :step_system
680
+ # Run one time step of a full n-body simulation. Compute the pairwise interactions of all
681
+ # bodies in the system, update their force vectors, and then move them an amount determined
682
+ # by the time step size +dt+.
683
+ #--
684
+ # :begin :step_system
628
685
  def step_system(bodies, dt)
629
686
  nb = bodies.length
630
687
 
631
- for i in 0..(nb-1) # compute all pairwise interactions
688
+ for i in 0..(nb-1) # compute all pairwise interactions
632
689
  for j in (i+1)..(nb-1)
633
690
  Body.interaction( bodies[i], bodies[j] )
634
691
  end
635
692
  end
636
693
 
637
694
  bodies.each do |b|
638
- b.move(dt) # apply the accumulated forces
695
+ b.move(dt) # apply the accumulated forces
639
696
  b.clear_force # reset force to 0 for next round
640
697
  end
641
698
  end
642
- # :end :step_system
699
+ # :end :step_system
643
700
 
644
- =begin rdoc
645
- After making a canvas with view_system, call this method to move the bodies on the canvas.
646
- =end
701
+ # This method will call step_system to simulate the motion of a set of bodies for the
702
+ # specified amount of time +dt+ and then update their positions on the canvas.
647
703
 
648
704
  def update_system(bodies, dt)
649
705
  return false unless @@drawing
@@ -664,23 +720,19 @@ module SphereLab
664
720
  return true
665
721
  end
666
722
 
667
- =begin rdoc
668
- Map a simulation's (x,y) coordinates to screen coordinates using origin
669
- and scale factor
670
- =end
723
+ # Map a simulation's (x,y) coordinates to screen coordinates using origin
724
+ # and scale factor
671
725
 
672
- def scale(vec, origin, sf)
726
+ def scale(vec, origin, sf) # :nodoc:
673
727
  loc = vec.clone
674
728
  loc.scale(sf)
675
729
  loc.add(origin)
676
730
  return loc.x, loc.y
677
731
  end
678
732
 
679
- =begin rdoc
680
- Make a vector that defines the location of the origin (in pixels).
681
- =end
733
+ # Make a vector that defines the location of the origin (in pixels).
682
734
 
683
- def setOrigin(type)
735
+ def setOrigin(type) # :nodoc:
684
736
  case type
685
737
  when :center
686
738
  Vector.new( Canvas.width/2, Canvas.height/2, 0)
@@ -689,12 +741,10 @@ module SphereLab
689
741
  end
690
742
  end
691
743
 
692
- =begin rdoc
693
- Set the scale factor. Use the parameter passed by the user, or find the
694
- largest coordinate in a list of bodies. Add 20% for a margin
695
- =end
744
+ # Set the scale factor. Use the parameter passed by the user, or find the
745
+ # largest coordinate in a list of bodies. Add 20% for a margin
696
746
 
697
- def setScale(blist, origin, scale)
747
+ def setScale(blist, origin, scale) # :nodoc:
698
748
  if scale == nil
699
749
  dmax = 0.0
700
750
  blist.each do |b|
@@ -707,10 +757,22 @@ module SphereLab
707
757
  return (sf / dmax) * 0.8
708
758
  end
709
759
 
710
- =begin rdoc
711
- Initialize the drawing for a "2-body system" with a small object and the
712
- earth. The parameters are the list of Body objects and viewing options.
713
- =end
760
+ # Initialize the RubyLabs Canvas to show a drawing of a "2-body system" with
761
+ # a small circle to represent a watermelon and a much larger partial circle for the
762
+ # earth. The two Body objects representing the melon and earth should be passed
763
+ # in the array +blist+. Additonal optional arguments are viewing options, which have
764
+ # the following defaults:
765
+ # :canvasSize => 400
766
+ # :mxmin => 100 maximum melon x position
767
+ # :mymin => 50, maximum melon y position
768
+ # :hmax => 100, maximum melon height
769
+ # :dash => 1, size of dashes drawn as melon falls
770
+ #
771
+ # Example:
772
+ # >> b = make_system(:melon)
773
+ # => [melon: 3 kg (0,6.371e+06,0) (0,0,0), earth: 5.97e+24 kg (0,0,0) (0,0,0)]
774
+ # >> view_melon(b)
775
+ # => true
714
776
 
715
777
  def view_melon(blist, userOptions = {})
716
778
  options = @@droppingOptions.merge(userOptions)
@@ -731,11 +793,16 @@ module SphereLab
731
793
  return true
732
794
  end
733
795
 
734
- =begin rdoc
735
- Position the melon (body 0) at a specified height. If no drawing, save the current
736
- position in prevy, but if drawing, get the earth's surface from the drawing (this allows
737
- students to move the melon up or down in the middle of an experiment)
738
- =end
796
+ # Position the melon (the first body in the array +blist+) at a specified height
797
+ # above the earth.
798
+ #
799
+ # Example:
800
+ # >> position_melon(b, 50)
801
+ # => 50
802
+ #--
803
+ # If no drawing, save the current position in prevy, but if drawing, get the earth's
804
+ # surface from the drawing (this allows students to move the melon up or down in the
805
+ # middle of an experiment)
739
806
 
740
807
  def position_melon(blist, height)
741
808
  melon = blist[0]
@@ -758,6 +825,10 @@ module SphereLab
758
825
  return height
759
826
  end
760
827
 
828
+ # Execute one time step of the two-body simulation involving the two objects in
829
+ # +blist+. Similar to the general purpose method update_system, except the
830
+ # drawing position of the earth is not updated.
831
+
761
832
  def update_melon(blist, dt)
762
833
  melon = blist[0]
763
834
  mx = melon.position.x
@@ -778,6 +849,21 @@ module SphereLab
778
849
  return blist[0].height
779
850
  end
780
851
 
852
+ # Repeatedly call the update_melon method until the +y+ position of the melon
853
+ # is less than or equal to the +y+ position of the surface of the earth. The
854
+ # two objects representing the melon and earth are passed in the array +blist+,
855
+ # and +dt+ is the size of the time step to use in calls to update_melon.
856
+ # The return value is the amount of simulated time it takes the melon to hit
857
+ # the earth; it will be the product of +dt+ and the number of time steps (number of
858
+ # calls to update_melon).
859
+ #
860
+ # Example -- to run an experiment that drops the melon from 50 meters, using a
861
+ # time step size of .01 seconds:
862
+ # >> position_melon(b, 50)
863
+ # => 50
864
+ # >> drop_melon(b, 0.01)
865
+ # => 3.19
866
+ #--
781
867
  # :begin :drop_melon
782
868
  def drop_melon(blist, dt)
783
869
  count = 0
@@ -790,22 +876,24 @@ module SphereLab
790
876
  end
791
877
  # :end :drop_melon
792
878
 
793
-
794
- =begin rdoc
795
- Methods used in IRB sessions.
796
- =end
797
-
798
- # :begin :dist
879
+ # Compute the distance traveled by an object moving at velocity +r+ for
880
+ # an amount of time +t+.
881
+ #--
882
+ # :begin :dist
799
883
  def dist(r, t)
800
884
  return r * t
801
885
  end
802
- # :end :dist
803
-
804
- # :begin :falling
886
+ # :end :dist
887
+
888
+ # Compute the distance an object will fall when it is initially stationary
889
+ # and then falls for an amount of time +dt+, accelerating
890
+ # according to the force of the Earth's gravity.
891
+ #--
892
+ # :begin :falling
805
893
  def falling(t)
806
894
  return 0.5 * 9.8 * t**2
807
895
  end
808
- # :end :falling
896
+ # :end :falling
809
897
 
810
898
  end # SphereLab
811
899