kerbaldyn 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. data/.rspec +2 -0
  2. data/Gemfile +11 -0
  3. data/README.rdoc +57 -0
  4. data/Rakefile +23 -0
  5. data/kerbaldyn.gemspec +30 -0
  6. data/lib/kerbaldyn.rb +14 -0
  7. data/lib/kerbaldyn/body.rb +48 -0
  8. data/lib/kerbaldyn/constants.rb +12 -0
  9. data/lib/kerbaldyn/data.rb +56 -0
  10. data/lib/kerbaldyn/data/planet_data.json +249 -0
  11. data/lib/kerbaldyn/mixin.rb +2 -0
  12. data/lib/kerbaldyn/mixin/options_processor.rb +17 -0
  13. data/lib/kerbaldyn/mixin/parameter_attributes.rb +38 -0
  14. data/lib/kerbaldyn/orbit.rb +379 -0
  15. data/lib/kerbaldyn/orbital_maneuver.rb +6 -0
  16. data/lib/kerbaldyn/orbital_maneuver/base.rb +159 -0
  17. data/lib/kerbaldyn/orbital_maneuver/bielliptic.rb +61 -0
  18. data/lib/kerbaldyn/orbital_maneuver/burn_event.rb +57 -0
  19. data/lib/kerbaldyn/orbital_maneuver/hohmann.rb +48 -0
  20. data/lib/kerbaldyn/orbital_maneuver/inclination_change.rb +0 -0
  21. data/lib/kerbaldyn/part.rb +15 -0
  22. data/lib/kerbaldyn/part/base.rb +154 -0
  23. data/lib/kerbaldyn/part/fuel_tank.rb +11 -0
  24. data/lib/kerbaldyn/part/generic.rb +10 -0
  25. data/lib/kerbaldyn/part/liquid_fuel_engine.rb +69 -0
  26. data/lib/kerbaldyn/part/mixin.rb +1 -0
  27. data/lib/kerbaldyn/part/mixin/fuel_tank.rb +35 -0
  28. data/lib/kerbaldyn/part/rcs_fuel_tank.rb +10 -0
  29. data/lib/kerbaldyn/part/solid_rocket.rb +30 -0
  30. data/lib/kerbaldyn/part_library.rb +55 -0
  31. data/lib/kerbaldyn/planetoid.rb +214 -0
  32. data/lib/kerbaldyn/version.rb +13 -0
  33. data/spec/bielliptic_orbital_maneuver_spec.rb +60 -0
  34. data/spec/constants_spec.rb +9 -0
  35. data/spec/hohmann_orbital_maneuver_spec.rb +385 -0
  36. data/spec/options_processor_spec.rb +33 -0
  37. data/spec/orbit_spec.rb +357 -0
  38. data/spec/orbital_maneuver_base_spec.rb +74 -0
  39. data/spec/parameter_attributes_spec.rb +82 -0
  40. data/spec/part_library_spec.rb +21 -0
  41. data/spec/part_spec.rb +218 -0
  42. data/spec/planetoid_spec.rb +117 -0
  43. data/spec/spec_helper.rb +110 -0
  44. data/spec/support/parts/RCSFuelTank/part.cfg +42 -0
  45. data/spec/support/parts/fuelTank/part.cfg +48 -0
  46. data/spec/support/parts/liquidEngine1/part.cfg +60 -0
  47. data/spec/support/parts/liquidEngine2/part.cfg +64 -0
  48. data/spec/support/parts/solidBooster/part.cfg +67 -0
  49. data/spec/support/planet_test_data.json +340 -0
  50. metadata +95 -0
@@ -0,0 +1,2 @@
1
+ require_relative 'mixin/parameter_attributes'
2
+ require_relative 'mixin/options_processor'
@@ -0,0 +1,17 @@
1
+ module KerbalDyn
2
+ module Mixin
3
+ module OptionsProcessor
4
+
5
+ private
6
+
7
+ # Iterates over all the key/value pairs in the options, calling the relevant
8
+ # accessor. Uses the passed block to get defaults for given keys.
9
+ def process_options(options, defaults={})
10
+ defaults.merge(options).each do |key,value|
11
+ self.send("#{key}=", value)
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ module KerbalDyn
2
+ module Mixin
3
+ # See ClassMethods for methods added by this module.
4
+ module ParameterAttributes
5
+
6
+ def self.included(mod)
7
+ mod.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Metaprogramming method for setting physical parameters, which are
13
+ # always of float type.
14
+ def attr_parameter(*params)
15
+ params.each do |param|
16
+ attr_reader param
17
+
18
+ setter_line = __LINE__ + 1
19
+ setter = <<-METHOD
20
+ def #{param}=(value)
21
+ @#{param} = value && value.to_f
22
+ end
23
+ METHOD
24
+ class_eval(setter, __FILE__, setter_line)
25
+ end
26
+ end
27
+
28
+ # Alias a parameter getter and setter methods.
29
+ def alias_parameter(to, from)
30
+ alias_method to, from
31
+ alias_method "#{to}=", "#{from}="
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,379 @@
1
+ module KerbalDyn
2
+ # The primary class for encapsulating an orbit around a primary body.
3
+ # At this time orbits are simplified to only consider the mass of the primary
4
+ # body, thus the orbit of the secondary body is around the center-of-mass of
5
+ # the primary body. This approximation works well for the Kerbal universe.
6
+ class Orbit
7
+ include Mixin::ParameterAttributes
8
+ include Mixin::OptionsProcessor
9
+
10
+ # A list of the independent parameters that define an orbit for this class.
11
+ BASE_PARAMETERS = [:periapsis, :periapsis_velocity, :inclination, :longitude_of_ascending_node, :argument_of_periapsis, :mean_anomaly, :epoch]
12
+
13
+ # A map of default values for initialization parameters.
14
+ DEFAULT_OPTIONS = BASE_PARAMETERS.inject({}) {|opts,param| opts[param] = 0.0; opts}.merge(:secondary_body => Body::TEST_PARTICLE)
15
+
16
+ # A quick-reference system structure data structure. Each parent body has a key with an array of children. Subsequent hits are necessary for additional lookups.
17
+ #SYSTEM_STRUCTURE = {:kerbol => [:moho, :eve, :kerbin, :duna, :jool], :eve => [:gilly], :kerbin => [:mun, :minmus], :duna => [:ike], :jool => [:laythe, :vall, :tylo, :bop]}.each do |primary, secondaries|
18
+
19
+ # For data read in from data files, this private method DRYs the process.
20
+ def self.make(planet_ref)
21
+ # Get the data and dup it so we can muck with it.
22
+ data = Data.fetch(:planet_data)[planet_ref][:orbit].dup
23
+ # Get the primary/secondary body refs from it and then lookup to get the values.
24
+ primary_body = Planetoid.send( data.delete(:primary_body) )
25
+ secondary_body = Planetoid.send( data.delete(:secondary_body) )
26
+ # Now construct the object.
27
+ return self.new( primary_body, data.merge(:secondary_body => secondary_body) ).freeze
28
+ end
29
+
30
+ class << self
31
+ private :make
32
+ end
33
+
34
+ # :category: Library Methods
35
+ def self.kerbin
36
+ return @kerbin ||= make(__method__)
37
+ end
38
+
39
+ # :category: Library Methods
40
+ def self.mun
41
+ return @mun ||= make(__method__)
42
+ end
43
+
44
+ # :category: Library Methods
45
+ def self.minmus
46
+ return @minmus ||= make(__method__)
47
+ end
48
+
49
+ # :category: Library Methods
50
+ def self.moho
51
+ return @moho ||= make(__method__)
52
+ end
53
+
54
+ # :category: Library Methods
55
+ def self.eve
56
+ return @eve ||= make(__method__)
57
+ end
58
+
59
+ # :category: Library Methods
60
+ def self.gilly
61
+ return @gilly ||= make(__method__)
62
+ end
63
+
64
+ # :category: Library Methods
65
+ def self.duna
66
+ return @duna ||= make(__method__)
67
+ end
68
+
69
+ # :category: Library Methods
70
+ def self.ike
71
+ return @ike ||= make(__method__)
72
+ end
73
+
74
+ # :category: Library Methods
75
+ def self.jool
76
+ return @jool ||= make(__method__)
77
+ end
78
+
79
+ # :category: Library Methods
80
+ def self.laythe
81
+ return @laythe ||= make(__method__)
82
+ end
83
+
84
+ # :category: Library Methods
85
+ def self.vall
86
+ return @vall ||= make(__method__)
87
+ end
88
+
89
+ # :category: Library Methods
90
+ def self.tylo
91
+ return @tylo ||= make(__method__)
92
+ end
93
+
94
+ # :category: Library Methods
95
+ def self.bop
96
+ return @bop ||= make(__method__)
97
+ end
98
+
99
+ # Convenience method for creating a circular orbit about the given body.
100
+ # This is redundant to calling new with the option of :radius.
101
+ def self.circular_orbit(primary_body, radius)
102
+ return self.new(primary_body, :radius => radius)
103
+ end
104
+
105
+ # Convenience method for creating a circular orbit of a given period around
106
+ # a given body.
107
+ def self.circular_orbit_of_period(primary_body, period)
108
+ planetoid_angular_velocity = 2.0 * Math::PI / period
109
+ radius = (primary_body.gravitational_parameter / planetoid_angular_velocity**2)**(1.0/3.0)
110
+ return self.circular_orbit(primary_body, radius)
111
+ end
112
+
113
+ # Convenience method for creating a geostationary orbit around the given body.
114
+ def self.geostationary_orbit(primary_body)
115
+ return self.circular_orbit_of_period(primary_body, primary_body.rotational_period)
116
+ end
117
+
118
+ # Convenience method for creating an escape orbit around the given body.
119
+ def self.escape_orbit(primary_body, periapsis)
120
+ periapsis_escape_velocity = Math.sqrt(2.0 * primary_body.gravitational_parameter / periapsis)
121
+ return self.new(primary_body, :periapsis => periapsis, :periapsis_velocity => periapsis_escape_velocity)
122
+ end
123
+
124
+ # Create a new orbit.
125
+ #
126
+ # The first argument should be the Body (usually a Planetoid) that is being orbited.
127
+ #
128
+ # The second argument is a list of parameters which should include a size
129
+ # and eccentricity specification, and a may include other optional parameters.
130
+ #
131
+ # To specify an orbit, at least one of the following pairs must be given:
132
+ # * periapsis, periapsis_velocity
133
+ # * periapsis, apoapsis
134
+ # * semimajor_axis, eccentricity
135
+ # * radius (assumes circular orbit)
136
+ #
137
+ # The following additional parameters may be given:
138
+ # * inclination
139
+ # * longitude_of_ascending_node
140
+ # * argument_of_periapsis
141
+ # * mean_anomaly
142
+ # * epoch
143
+ def initialize(primary_body, options={})
144
+ # Set the primary planetoid
145
+ self.primary_body = primary_body
146
+
147
+ # Set the periapsis and periapsis_velocity from the options
148
+ replaced_options = replace_orbital_parameters(options)
149
+
150
+ # Default all the base parameters to zero if not given.
151
+ process_options(replaced_options, DEFAULT_OPTIONS)
152
+ end
153
+
154
+ attr_parameter *BASE_PARAMETERS
155
+
156
+ # The body being orbited (required)
157
+ # Expected to be an instance of Planetoid.
158
+ attr_accessor :primary_body
159
+
160
+ # The body in orbit (optional)
161
+ # Expected to be an instance of Planetoid.
162
+ attr_accessor :secondary_body
163
+
164
+ # Returns the sphere of influence (SOI) for the primary body in the context
165
+ # of the two-body system.
166
+ #
167
+ # This is NOT the KSP SOI, for it, use +kerbal_sphere_of_influence+
168
+ def primary_body_sphere_of_influence
169
+ return self.semimajor_axis * (self.primary_body.mass / self.secondary_body.mass)**(0.4)
170
+ end
171
+
172
+ # Returns the sphere of influence (SOI) for the secondary body in the context
173
+ # of the two-body system.
174
+ #
175
+ # This is NOT the KSP SOI, for it, use +kerbal_sphere_of_influence+
176
+ def secondary_body_sphere_of_influence
177
+ return self.semimajor_axis * (self.secondary_body.mass / self.primary_body.mass)**(0.4)
178
+ end
179
+ alias_method :sphere_of_influence, :secondary_body_sphere_of_influence
180
+
181
+ # The Hill Sphere radius.
182
+ #
183
+ # This is NOT the KSP SOI, for it, use +kerbal_sphere_of_influence+
184
+ def hill_sphere_radius
185
+ return self.periapsis * (self.secondary_body.mass / (3.0*self.primary_body.mass))**(2.0/3.0)
186
+ end
187
+
188
+
189
+ # KSP uses this alternate hill sphere radius I found on Wikipedia.
190
+ def kerbal_sphere_of_influence
191
+ return self.periapsis * (self.secondary_body.mass / self.primary_body.mass)**(1.0/3.0)
192
+ end
193
+ alias_method :kerbal_soi, :kerbal_sphere_of_influence
194
+
195
+
196
+ # Orbit classification, returns one of :subelliptical, :circular, :elliptical,
197
+ # :parabolic, or :hyperbolic.
198
+ #
199
+ # Because of how floats work, a delta is given for evaluation of "critical"
200
+ # points such as circular and elliptical orbits. The default value is 1e-9,
201
+ # however other values may be given.
202
+ def classification(delta=0.000000001)
203
+ e = self.eccentricity
204
+ if( e.abs < delta )
205
+ return :circular
206
+ elsif( (e-1.0).abs < delta )
207
+ return :parabolic
208
+ elsif( e > 0.0 && e < 1.0 )
209
+ return :elliptical
210
+ elsif( e > 1.0 )
211
+ return :hyperbolic
212
+ elsif( e < 0.0 )
213
+ return :subelliptical
214
+ end
215
+ end
216
+ alias_method :kind, :classification
217
+
218
+ # Returns true if the orbit is a closed orbit (eccentricity < 1)
219
+ def closed?
220
+ return self.eccentricity < 1.0
221
+ end
222
+
223
+ # Returns false if the orbit is closed.
224
+ def open?
225
+ return !self.closed?
226
+ end
227
+
228
+ # Returns the gravitational parameter for this orbit.
229
+ # Note that this currently is G*M rather than G*(M+m).
230
+ def gravitational_parameter
231
+ return self.primary_body.gravitational_parameter
232
+ end
233
+
234
+ # The specific potential energy for any given orbit radius.
235
+ def specific_potential_energy(r)
236
+ return -self.gravitational_parameter / r
237
+ end
238
+
239
+ # The specific kinetic energy for any given velocity.
240
+ def specific_kinetic_energy(v)
241
+ return 0.5 * v**2
242
+ end
243
+
244
+ # The total specific energyy of the orbit; this is constant over the entire
245
+ # orbit.
246
+ def specific_energy
247
+ return self.specific_potential_energy(self.periapsis) + self.specific_kinetic_energy(self.periapsis_velocity)
248
+ end
249
+ alias_method :vis_viva_energy, :specific_energy
250
+
251
+ # The specific angular momentum for this orbit; this is constant over the
252
+ # entire orbit.
253
+ def specific_angular_momentum
254
+ return self.periapsis * self.periapsis_velocity
255
+ end
256
+ alias_method :angular_momentum, :specific_angular_momentum
257
+
258
+ # The orbit eccentricity.
259
+ def eccentricity
260
+ return (self.periapsis * self.periapsis_velocity**2 / self.gravitational_parameter) - 1
261
+ end
262
+
263
+ # The orbit semimajor-axis.
264
+ def semimajor_axis
265
+ if self.closed?
266
+ return self.periapsis / (1.0 - self.eccentricity)
267
+ else
268
+ return self.periapsis / (self.eccentricity - 1.0)
269
+ end
270
+ end
271
+
272
+ # The orbit semiminor_axis, if the eccentricity is less than one.
273
+ def semiminor_axis
274
+ if self.closed?
275
+ return self.semimajor_axis * Math.sqrt(1.0 - self.eccentricity**2)
276
+ else
277
+ return nil
278
+ end
279
+ end
280
+
281
+ # The orbital period, if the eccentricity is less than one.
282
+ def period
283
+ if self.closed?
284
+ return 2.0 * Math::PI * Math.sqrt( self.semimajor_axis**3 / self.gravitational_parameter )
285
+ else
286
+ return nil
287
+ end
288
+ end
289
+
290
+ # This is the mean angular velocity (angle change per unit time) for this orbit.
291
+ def mean_angular_velocity
292
+ if self.closed?
293
+ return 2.0 * Math::PI / self.period
294
+ else
295
+ return nil
296
+ end
297
+ end
298
+
299
+ # This is the mean angular velocity (angle change per unit time) for this orbit.
300
+ #
301
+ # This simply calls +mean_angular_velocity+ on self.
302
+ def mean_motion
303
+ return self.mean_angular_velocity
304
+ end
305
+
306
+ # This is the velocity associated with the mean motion at the semimajor axis,
307
+ # This works out to be the velocity of an object in a circular orbit with the same period.
308
+ def mean_velocity
309
+ return self.mean_motion * self.semimajor_axis
310
+ end
311
+
312
+ # The apoapsis radius, if the eccentricity is less than one.
313
+ def apoapsis
314
+ if self.closed?
315
+ return self.semimajor_axis * (1 + self.eccentricity)
316
+ else
317
+ return nil
318
+ end
319
+ end
320
+
321
+ # The apoapsis velocity, if the eccentricity is less than one.
322
+ def apoapsis_velocity
323
+ if self.closed?
324
+ e = self.eccentricity
325
+ k = self.gravitational_parameter / self.semimajor_axis
326
+ return Math.sqrt( (1-e)/(1+e) * k )
327
+ else
328
+ return nil
329
+ end
330
+ end
331
+
332
+ # Instantiates a new orbit with the same period and semimajor axis, zero
333
+ # inclination, and is circular.
334
+ #
335
+ # This is useful for idealizing orbits for rough calculations.
336
+ #--
337
+ # TODO: Figure out how to translate parameters such as mean anomaly and LAN.
338
+ #++
339
+ def circularize
340
+ return self.class.new(primary_body, {
341
+ :secondary_body => self.secondary_body,
342
+ :radius => self.semimajor_axis,
343
+ :inclination => 0.0
344
+ })
345
+ end
346
+
347
+ private
348
+
349
+ # This takes one several sets of base parameters, and derives the periapsis
350
+ # and periapsis_velocity, and then replaces the original keys with them.
351
+ #
352
+ # This is helper method for initialization.
353
+ def replace_orbital_parameters(options)
354
+ # We don't want to mutate the options hash they passed in.
355
+ opts = options.dup
356
+
357
+ if( opts.include?(:periapsis) && opts.include?(:periapsis_velocity) )
358
+ return opts
359
+ elsif( opts.include?(:periapsis) && opts.include?(:apoapsis) )
360
+ rp = opts[:periapsis]
361
+ ra = opts.delete(:apoapsis)
362
+ opts[:periapsis_velocity] = Math.sqrt( (2.0 * ra)/(ra+rp) * (self.gravitational_parameter / rp) )
363
+ elsif( opts.include?(:eccentricity) && opts.include?(:semimajor_axis) )
364
+ e = opts.delete(:eccentricity)
365
+ a = opts.delete(:semimajor_axis)
366
+ mu = self.gravitational_parameter
367
+ opts[:periapsis] = rp = a*(1-e)
368
+ opts[:periapsis_velocity] = Math.sqrt( (e+1)*mu/rp )
369
+ elsif( opts.include?(:radius) )
370
+ opts[:periapsis] = opts.delete(:radius)
371
+ # This is a special case of the equation used for apoapsis/periapsis configuration, where they are equal.
372
+ opts[:periapsis_velocity] = Math.sqrt( self.gravitational_parameter / opts[:periapsis] )
373
+ end
374
+
375
+ return opts
376
+ end
377
+
378
+ end
379
+ end
@@ -0,0 +1,6 @@
1
+ require_relative 'orbital_maneuver/burn_event'
2
+
3
+ require_relative 'orbital_maneuver/base'
4
+ require_relative 'orbital_maneuver/hohmann'
5
+ require_relative 'orbital_maneuver/bielliptic'
6
+ require_relative 'orbital_maneuver/inclination_change'
@@ -0,0 +1,159 @@
1
+ module KerbalDyn
2
+ # The namespace for all orbital maneuvers
3
+ module OrbitalManeuver
4
+ # The base-class for all orbital maneuvers.
5
+ class Base
6
+ include Mixin::OptionsProcessor
7
+ include Mixin::ParameterAttributes
8
+
9
+ def initialize(initial_orbit, final_orbit, options={})
10
+ # For now this is a safe thing to do, *way* in the future we may have to differentiate
11
+ # between complex maneuvers to other systems and maneuvers within the system.
12
+ raise ArgumentError, "Expected the initial and final orbit to orbit the same body." unless initial_orbit.primary_body == final_orbit.primary_body
13
+ self.initial_orbit = initial_orbit
14
+ self.final_orbit = final_orbit
15
+
16
+ process_options(options)
17
+ end
18
+
19
+ # The orbit you are starting at.
20
+ attr_accessor :initial_orbit
21
+
22
+ # The orbit you are going to maneuver into.
23
+ attr_accessor :final_orbit
24
+
25
+
26
+ # Returns an array of the orbits, including the initial and final orbit.
27
+ #
28
+ # Subclasses should override this method.
29
+ def orbits
30
+ return [initial_orbit, final_orbit]
31
+ end
32
+
33
+ # Returns the array of burn events.
34
+ #
35
+ # Subclasses should override this method, as most other values are derived
36
+ # from it.
37
+ def burn_events
38
+ return @burn_events ||= []
39
+ end
40
+
41
+ # Returns true if the final orbit has a larger semimajor axis than the
42
+ # initial orbit.
43
+ def moving_to_higher_orbit?
44
+ return final_orbit.semimajor_axis > initial_orbit.semimajor_axis
45
+ end
46
+
47
+ # Returns an array of the before/after burn even velocity pairs for each burn event.
48
+ def velocities
49
+ return self.burn_events.map {|be| [be.initial_velocity, be.final_velocity]}
50
+ end
51
+
52
+ # Returns an array of the velocity changes for each burn event.
53
+ def delta_velocities
54
+ return self.burn_events.map {|be| be.delta_velocity}
55
+ end
56
+
57
+ # Returns an array of the effective impulse times of each maneuver.
58
+ def times
59
+ return self.burn_events.map {|be| be.time}
60
+ end
61
+
62
+ # Returns an array of the orbital radii for the burn events.
63
+ def orbital_radii
64
+ return self.burn_events.map {|be| be.orbital_radius}
65
+ end
66
+
67
+ # Returns an array of the mean anomaly at each maneuver.
68
+ def mean_anomalies
69
+ return self.burn_events.map {|be| be.mean_anomaly}
70
+ end
71
+
72
+ # Calculates the total delta-v for this maneuver.
73
+ #
74
+ # This is always a positive quantity that relates the total amount of
75
+ # velocity change necessary, and hence a sense of the total amount of
76
+ # fuel used during the maneuver.
77
+ #
78
+ # The baseclass implementation is to sum the absolute values of the
79
+ # deltas in the velocity list +delta_velocities+.
80
+ def delta_velocity
81
+ return self.delta_velocities.reduce(0) {|a,b| a.abs + b.abs}
82
+ end
83
+ alias_method :delta_v, :delta_velocity
84
+
85
+ # Calculates the total time for completion.
86
+ #
87
+ # For some kinds of idealized maneuvers this may be meaningless, in which
88
+ # case nil is returned.
89
+ #
90
+ # Subclasses should override this.
91
+ def delta_time
92
+ return self.times.last - self.times.first
93
+ end
94
+
95
+ # An alias to delta_time.
96
+ def delta_t
97
+ return self.delta_time
98
+ end
99
+
100
+ # Calculates the total mean anomaly covered during the maneuver.
101
+ def delta_mean_anomaly
102
+ return self.mean_anomalies.last - self.mean_anomalies.first
103
+ end
104
+
105
+ # Calculates the mean lead-angle for a given maneuver.
106
+ #
107
+ # For a target in a higher orbit, this is the mean anomaly ahead of you
108
+ # that the target should be located at.
109
+ def mean_lead_angle
110
+ target_delta_mean_anomaly = self.delta_time * self.final_orbit.mean_angular_velocity
111
+ return self.delta_mean_anomaly - target_delta_mean_anomaly
112
+ end
113
+
114
+ # The time elapsed such that--if started when the target is directly radial
115
+ # from you (same true anomaly)--you would start your transfer orbit in order
116
+ # to intercept.
117
+ def mean_lead_time
118
+ # theta_f1 = theta_01 + omega_1*t
119
+ # theta_f2 = theta_02 + omega_2*t
120
+ # theta_f2 - theta_f1 = theta_lead (After time t, we want to achieve lead angle)
121
+ # theta_02 - theta_01 = 0 (When we start the clock, we want no separation)
122
+ #
123
+ # Thefefore: theta_lead = (omega_2 - omega_1) * t
124
+ #
125
+ # We also have to find the offset (n*2*pi) to the lead angle that will lead to the
126
+ # first positive time. This depends on wether delta_omega is positive or
127
+ # negative.
128
+ delta_omega = self.final_orbit.mean_angular_velocity - self.initial_orbit.mean_angular_velocity
129
+
130
+ two_pi = 2.0*Math::PI
131
+ theta_lead = (self.mean_lead_angle % two_pi)
132
+ theta_lead = (delta_omega>0.0) ? theta_lead : (theta_lead-two_pi)
133
+
134
+ return theta_lead / delta_omega
135
+ end
136
+
137
+ # For every full cycle of the initial orbit (2pi radians), the final orbit
138
+ # covers either less than 2pi radians (if it is bigger) or more than 2pi radians
139
+ # (if it is smaller) in the same amount of time. The relative_delta_anomaly
140
+ # is the change difference covered.
141
+ #
142
+ # If it is positive, then the initial orbit is slower,
143
+ # If it is zero, then they are in lock-step,
144
+ # If it is negative, then the initial orbit is faster.
145
+ #
146
+ # Note that for a large orbit ratio, going from the lower to higher orbit
147
+ # will have a very negative anomaly close to -2pi (the target didn't move much
148
+ # much , while you did your rotation), while going from a higher to lower
149
+ # orbit has a high positive number (the target did a lot of laps while you
150
+ # were waiting for it.
151
+ def relative_anomaly_delta
152
+ initial_orbit_anomaly = 2.0 * Math::PI
153
+ final_orbit_anomaly = 2.0 * Math::PI * (self.initial_orbit.period / self.final_orbit.period)
154
+ return final_orbit_anomaly - initial_orbit_anomaly
155
+ end
156
+
157
+ end
158
+ end
159
+ end