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.
- 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
|