kerbaldyn 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/Gemfile +11 -0
- data/README.rdoc +57 -0
- data/Rakefile +23 -0
- data/kerbaldyn.gemspec +30 -0
- data/lib/kerbaldyn.rb +14 -0
- data/lib/kerbaldyn/body.rb +48 -0
- data/lib/kerbaldyn/constants.rb +12 -0
- data/lib/kerbaldyn/data.rb +56 -0
- data/lib/kerbaldyn/data/planet_data.json +249 -0
- data/lib/kerbaldyn/mixin.rb +2 -0
- data/lib/kerbaldyn/mixin/options_processor.rb +17 -0
- data/lib/kerbaldyn/mixin/parameter_attributes.rb +38 -0
- data/lib/kerbaldyn/orbit.rb +379 -0
- data/lib/kerbaldyn/orbital_maneuver.rb +6 -0
- data/lib/kerbaldyn/orbital_maneuver/base.rb +159 -0
- data/lib/kerbaldyn/orbital_maneuver/bielliptic.rb +61 -0
- data/lib/kerbaldyn/orbital_maneuver/burn_event.rb +57 -0
- data/lib/kerbaldyn/orbital_maneuver/hohmann.rb +48 -0
- data/lib/kerbaldyn/orbital_maneuver/inclination_change.rb +0 -0
- data/lib/kerbaldyn/part.rb +15 -0
- data/lib/kerbaldyn/part/base.rb +154 -0
- data/lib/kerbaldyn/part/fuel_tank.rb +11 -0
- data/lib/kerbaldyn/part/generic.rb +10 -0
- data/lib/kerbaldyn/part/liquid_fuel_engine.rb +69 -0
- data/lib/kerbaldyn/part/mixin.rb +1 -0
- data/lib/kerbaldyn/part/mixin/fuel_tank.rb +35 -0
- data/lib/kerbaldyn/part/rcs_fuel_tank.rb +10 -0
- data/lib/kerbaldyn/part/solid_rocket.rb +30 -0
- data/lib/kerbaldyn/part_library.rb +55 -0
- data/lib/kerbaldyn/planetoid.rb +214 -0
- data/lib/kerbaldyn/version.rb +13 -0
- data/spec/bielliptic_orbital_maneuver_spec.rb +60 -0
- data/spec/constants_spec.rb +9 -0
- data/spec/hohmann_orbital_maneuver_spec.rb +385 -0
- data/spec/options_processor_spec.rb +33 -0
- data/spec/orbit_spec.rb +357 -0
- data/spec/orbital_maneuver_base_spec.rb +74 -0
- data/spec/parameter_attributes_spec.rb +82 -0
- data/spec/part_library_spec.rb +21 -0
- data/spec/part_spec.rb +218 -0
- data/spec/planetoid_spec.rb +117 -0
- data/spec/spec_helper.rb +110 -0
- data/spec/support/parts/RCSFuelTank/part.cfg +42 -0
- data/spec/support/parts/fuelTank/part.cfg +48 -0
- data/spec/support/parts/liquidEngine1/part.cfg +60 -0
- data/spec/support/parts/liquidEngine2/part.cfg +64 -0
- data/spec/support/parts/solidBooster/part.cfg +67 -0
- data/spec/support/planet_test_data.json +340 -0
- metadata +95 -0
@@ -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,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
|