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,61 @@
|
|
1
|
+
module KerbalDyn
|
2
|
+
module OrbitalManeuver
|
3
|
+
class Bielliptic < Base
|
4
|
+
|
5
|
+
# A special 3-burn orbital maneuver between two circular orbits through
|
6
|
+
# the transfer_radius options.
|
7
|
+
#
|
8
|
+
# Note that all orbits are circularized before being used.
|
9
|
+
#
|
10
|
+
# The first burn moves from the initial orbit to the transfer radius,
|
11
|
+
# the second burn (at the transfer radius) moves the periapsis of the
|
12
|
+
# transfer orbit to the final orbit. The thrid burn circularizes
|
13
|
+
# to the final orbit.
|
14
|
+
def initialize(initial_orbit, final_orbit, options={})
|
15
|
+
options = {:transfer_radius => final_orbit.semimajor_axis}.merge(options)
|
16
|
+
super(initial_orbit.circularize, final_orbit.circularize, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_accessor :transfer_radius
|
20
|
+
private :transfer_radius=
|
21
|
+
|
22
|
+
def initial_transfer_orbit
|
23
|
+
r1 = self.initial_orbit.periapsis
|
24
|
+
rt = self.transfer_radius
|
25
|
+
return Orbit.new(self.initial_orbit.primary_body, :periapsis => [r1,rt].min, :apoapsis => [r1,rt].max)
|
26
|
+
end
|
27
|
+
|
28
|
+
def final_transfer_orbit
|
29
|
+
r2 = self.final_orbit.periapsis
|
30
|
+
rt = self.transfer_radius
|
31
|
+
return Orbit.new(self.initial_orbit.primary_body, :periapsis => [r2,rt].min, :apoapsis => [r2,rt].max)
|
32
|
+
end
|
33
|
+
|
34
|
+
def orbits
|
35
|
+
return [self.initial_orbit, self.initial_transfer_orbit, self.final_transfer_orbit.second, self.final_orbit]
|
36
|
+
end
|
37
|
+
|
38
|
+
def burn_events
|
39
|
+
r1 = self.initial_orbit.periapsis
|
40
|
+
r2 = self.final_orbit.periapsis
|
41
|
+
rt = self.transfer_radius
|
42
|
+
|
43
|
+
ito = self.initial_transfer_orbit
|
44
|
+
v11, v12 = (rt >= r1) ? [ito.periapsis_velocity, ito.apoapsis_velocity] : [ito.apoapsis_velocity, ito.periapsis_velocity]
|
45
|
+
|
46
|
+
fto = self.final_transfer_orbit
|
47
|
+
v21, v22 = (rt < r2) ? [fto.periapsis_velocity, fto.apoapsis_velocity] : [fto.apoapsis_velocity, fto.periapsis_velocity]
|
48
|
+
|
49
|
+
t1 = self.initial_transfer_orbit.period / 2.0
|
50
|
+
t2 = self.final_transfer_orbit.period / 2.0
|
51
|
+
|
52
|
+
return [
|
53
|
+
BurnEvent.new(:initial_velocity => self.initial_orbit.mean_velocity, :final_velocity => v11, :time => 0.0, :orbital_radius => self.initial_orbit.semimajor_axis, :mean_anomaly => 0.0),
|
54
|
+
BurnEvent.new(:initial_velocity => v12, :final_velocity => v21, :time => t1, :orbital_radius => self.transfer_radius, :mean_anomaly => Math::PI),
|
55
|
+
BurnEvent.new(:initial_velocity => v22, :final_velocity => self.final_orbit.mean_velocity, :time => t1+t2, :mean_anomaly => 2.0 * Math::PI)
|
56
|
+
]
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module KerbalDyn
|
2
|
+
module OrbitalManeuver
|
3
|
+
# Encapsulates information about a burn event.
|
4
|
+
class BurnEvent
|
5
|
+
include Mixin::ParameterAttributes
|
6
|
+
include Mixin::OptionsProcessor
|
7
|
+
|
8
|
+
# Create a new burn event.
|
9
|
+
#
|
10
|
+
# The following parameters are expected to be given:
|
11
|
+
# [initial_velocity] The velocity before the burn.
|
12
|
+
# [final_velocity] The velocity after the burn.
|
13
|
+
# [time] The time of the burn.
|
14
|
+
# [orbital_radius] The orbital radius at the time of the burn.
|
15
|
+
# [mean_anomaly] The mean anomaly at the time of the burn.
|
16
|
+
#
|
17
|
+
# The following parameters are optional.
|
18
|
+
# [epoch] Used to offset the time.
|
19
|
+
def initialize(options={})
|
20
|
+
process_options(options, :epoch => 0.0)
|
21
|
+
end
|
22
|
+
|
23
|
+
# The velocity before burn.
|
24
|
+
attr_parameter :initial_velocity
|
25
|
+
|
26
|
+
# The velocity after the burn.
|
27
|
+
attr_parameter :final_velocity
|
28
|
+
|
29
|
+
# The time for the burn.
|
30
|
+
attr_parameter :time
|
31
|
+
|
32
|
+
# The epoch for when time is zero. (optional)
|
33
|
+
attr_parameter :epoch
|
34
|
+
|
35
|
+
# The orbital radius at the time of the burn.
|
36
|
+
attr_parameter :orbital_radius
|
37
|
+
|
38
|
+
# The mean anomaly at the time of the burn.
|
39
|
+
attr_parameter :mean_anomaly
|
40
|
+
|
41
|
+
# Returns the change in velocity for this maneuver.
|
42
|
+
#
|
43
|
+
# Note that the sign may be meaningful to the maneuver. For example,
|
44
|
+
# a retrograde burn is usually negative.
|
45
|
+
def delta_velocity
|
46
|
+
return self.final_velocity - self.initial_velocity
|
47
|
+
end
|
48
|
+
alias_method :delta_v, :delta_velocity
|
49
|
+
|
50
|
+
# Gives the time of this event in epoch time if epoch was set.
|
51
|
+
def epoch_time
|
52
|
+
return time + epoch
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module KerbalDyn
|
2
|
+
module OrbitalManeuver
|
3
|
+
# A special 2-burn orbital maneuver between two circular orbits.
|
4
|
+
#
|
5
|
+
# Note that all orbits are circulairzed before being used.
|
6
|
+
#
|
7
|
+
# The first burn moves the opposite side of the orbit to the radius of
|
8
|
+
# the destination orbit, and the second burn--done when reaching the destination
|
9
|
+
# radius--moves the opposite side (where you started) to circularize your
|
10
|
+
# orbit.
|
11
|
+
#
|
12
|
+
#--
|
13
|
+
# TODO: To facilitate elliptical orbits, assume coplanar/coaxial, and take options to use periapsis, semimajor_axis, or apoapsis for each orbit.
|
14
|
+
# TODO: ALWAYS use semimajor axis here, so that we can assume circular on lead angle and time; make another class for elliptics and eventually replace this as a subclass with default args.
|
15
|
+
#++
|
16
|
+
class Hohmann < Base
|
17
|
+
|
18
|
+
def initialize(initial_orbit, final_orbit, options={})
|
19
|
+
super(initial_orbit.circularize, final_orbit.circularize, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
# The elliptical orbit used to transfer from the initial_orbit to the
|
23
|
+
# final_orbit.
|
24
|
+
def transfer_orbit
|
25
|
+
r1 = initial_orbit.periapsis
|
26
|
+
r2 = final_orbit.apoapsis
|
27
|
+
# TODO: It should be the Orbit's job to min/max periapsis and apoapsis, and then set angles appropriately.
|
28
|
+
return @transfer_orbit ||= Orbit.new(self.initial_orbit.primary_body, :periapsis => [r1,r2].min, :apoapsis => [r1,r2].max)
|
29
|
+
end
|
30
|
+
|
31
|
+
def orbits
|
32
|
+
return [self.initial_orbit, self.transfer_orbit, self.final_orbit]
|
33
|
+
end
|
34
|
+
|
35
|
+
def burn_events
|
36
|
+
vs = [self.transfer_orbit.periapsis_velocity, self.transfer_orbit.apoapsis_velocity]
|
37
|
+
vs.reverse! if( initial_orbit.semimajor_axis > final_orbit.semimajor_axis )
|
38
|
+
v1,v2 = vs
|
39
|
+
|
40
|
+
return [
|
41
|
+
BurnEvent.new(:initial_velocity => self.initial_orbit.mean_velocity, :final_velocity => v1, :time => 0.0, :orbital_radius => self.initial_orbit.semimajor_axis, :mean_anomaly => 0.0),
|
42
|
+
BurnEvent.new(:initial_velocity => v2, :final_velocity => self.final_orbit.mean_velocity, :time => self.transfer_orbit.period/2.0, :orbital_radius => self.final_orbit.semimajor_axis, :mean_anomaly => Math::PI)
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module KerbalDyn
|
2
|
+
module Part
|
3
|
+
CATEGORIES = {'Propulsion' => 0, 'Command & Control' => 1, 'Structural & Aerodynamic' => 2, 'Utility & Scientific' => 3, 'Decals' => 4, 'Crew' => 5}.freeze
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
require_relative 'part/mixin'
|
9
|
+
|
10
|
+
require_relative 'part/base'
|
11
|
+
require_relative 'part/generic'
|
12
|
+
require_relative 'part/fuel_tank'
|
13
|
+
require_relative 'part/liquid_fuel_engine'
|
14
|
+
require_relative 'part/rcs_fuel_tank'
|
15
|
+
require_relative 'part/solid_rocket'
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module KerbalDyn
|
5
|
+
module Part
|
6
|
+
# The base-class for all rocket parts. Instances are always of another
|
7
|
+
# type, which is determined by the +module+ attribute. For parts that
|
8
|
+
# don't have special implementations, the Generic part class is used.
|
9
|
+
class Base
|
10
|
+
|
11
|
+
# Load the part from a given part directory. This will automatically
|
12
|
+
# instantiate the correct subclass.
|
13
|
+
def self.load_part(directory)
|
14
|
+
# Process the argument.
|
15
|
+
dir = Pathname.new(directory)
|
16
|
+
return nil unless dir.directory?
|
17
|
+
|
18
|
+
# Get a handle on the spec file.
|
19
|
+
spec_file = dir + 'part.cfg'
|
20
|
+
return nil unless spec_file.file?
|
21
|
+
|
22
|
+
# Initialize the attributes container.
|
23
|
+
attributes = {}
|
24
|
+
line_count = 0
|
25
|
+
|
26
|
+
# Parse the lines.
|
27
|
+
spec_file.read.each_line do |line|
|
28
|
+
line_count += 1
|
29
|
+
line.chomp!
|
30
|
+
line = line.encode('ASCII-8BIT', :invalid => :replace, :replace => '?') unless line.valid_encoding?
|
31
|
+
|
32
|
+
case line
|
33
|
+
when /^\s*$/
|
34
|
+
# Blank
|
35
|
+
when /^\s*\/\//
|
36
|
+
# Comments
|
37
|
+
when /^(.*)=(.*)/
|
38
|
+
key,value = line.split('=', 2).map {|s| s.strip}
|
39
|
+
attributes[key] = value
|
40
|
+
else
|
41
|
+
STDERR.puts "Unhandled line in #{spec_file.to_s}:#{line_count}: #{line.inspect}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Now instantiate the right kind of part.
|
46
|
+
return self.module_class(attributes['module']).new(attributes)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return the class to instantiate for a given +module+ attribute.
|
50
|
+
def self.module_class(module_name)
|
51
|
+
ref_mod = Module.nesting[1]
|
52
|
+
if( ref_mod.constants.include?(module_name.to_sym) )
|
53
|
+
return ref_mod.const_get(module_name.to_sym)
|
54
|
+
else
|
55
|
+
return ref_mod.const_get(:Generic)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Initialize the part from the hash. Note that this does NOT auto-select
|
60
|
+
# the subclass.
|
61
|
+
def initialize(attributes)
|
62
|
+
@attributes = attributes.dup
|
63
|
+
end
|
64
|
+
|
65
|
+
# Return the raw attributes hash.
|
66
|
+
#
|
67
|
+
# Generally speaking it is better to use to_hash to keep from accidentally
|
68
|
+
# altering the part by altering the attributes hash by reference. That
|
69
|
+
# being said, this is provided for special/power use cases.
|
70
|
+
attr_reader :attributes
|
71
|
+
|
72
|
+
# Return the raw attribute value by string or symbol.
|
73
|
+
#
|
74
|
+
# It is generally preferrable to use the accessor method.
|
75
|
+
def [](attr)
|
76
|
+
return self.attributes[attr.to_s]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return a the part parameters as a hash.
|
80
|
+
#
|
81
|
+
# Currently this is implemented as a raw dump of the attributes hash,
|
82
|
+
# but in the future it is planned to convert numeric types appropriately.
|
83
|
+
def to_hash
|
84
|
+
return attributes.dup
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns a JSON encoded form of the to_hash result.
|
88
|
+
def to_json
|
89
|
+
return self.to_hash.to_json
|
90
|
+
end
|
91
|
+
|
92
|
+
def name
|
93
|
+
return self['name']
|
94
|
+
end
|
95
|
+
|
96
|
+
def title
|
97
|
+
return self['title']
|
98
|
+
end
|
99
|
+
|
100
|
+
def description
|
101
|
+
return self['description']
|
102
|
+
end
|
103
|
+
|
104
|
+
def category
|
105
|
+
return self['category'].to_i
|
106
|
+
end
|
107
|
+
|
108
|
+
def category_name
|
109
|
+
return CATEGORIES.invert[self.category]
|
110
|
+
end
|
111
|
+
|
112
|
+
def module
|
113
|
+
return self['module']
|
114
|
+
end
|
115
|
+
|
116
|
+
def module_class
|
117
|
+
return self.class.module_class(self.module)
|
118
|
+
end
|
119
|
+
|
120
|
+
def mass
|
121
|
+
return self['mass'].to_f
|
122
|
+
end
|
123
|
+
|
124
|
+
def maximum_drag
|
125
|
+
return self['maximum_drag'] && self['maximum_drag'].to_f
|
126
|
+
end
|
127
|
+
|
128
|
+
def drag
|
129
|
+
return self.maximum_drag
|
130
|
+
end
|
131
|
+
|
132
|
+
def minimum_drag
|
133
|
+
return self['minimum_drag'] && self['minimum_drag'].to_f
|
134
|
+
end
|
135
|
+
|
136
|
+
def max_temp
|
137
|
+
return self['maxTemp'].to_f
|
138
|
+
end
|
139
|
+
|
140
|
+
def crash_tolerance
|
141
|
+
return self['crashTolerance'].to_f
|
142
|
+
end
|
143
|
+
|
144
|
+
def impact_tolerance
|
145
|
+
return self.crash_tolerance
|
146
|
+
end
|
147
|
+
|
148
|
+
def cost
|
149
|
+
return self['cost'].to_i
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module KerbalDyn
|
2
|
+
module Part
|
3
|
+
# Parts without specific subclasses are defined as being of type Generic.
|
4
|
+
#
|
5
|
+
# At this time Generic parts are functionally no different than Base part,
|
6
|
+
# however this is reserved to change in the future.
|
7
|
+
class Generic < Base
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module KerbalDyn
|
2
|
+
module Part
|
3
|
+
class LiquidFuelEngine < Base
|
4
|
+
# The surface gravity, as used for Isp calculations; this was determined experimentally.
|
5
|
+
IspSurfaceGravity = 9.8072
|
6
|
+
|
7
|
+
def max_thrust
|
8
|
+
return self['maxThrust'].to_f
|
9
|
+
end
|
10
|
+
alias_method :thrust, :max_thrust
|
11
|
+
|
12
|
+
def min_thrust
|
13
|
+
return self['minThrust'].to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def isp
|
17
|
+
return self['Isp'].to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
def vac_isp
|
21
|
+
return self['vacIsp'].to_f
|
22
|
+
end
|
23
|
+
|
24
|
+
def heat_production
|
25
|
+
return self['heatProduction'].to_f
|
26
|
+
end
|
27
|
+
|
28
|
+
# Calculated mass fuel flow.
|
29
|
+
#
|
30
|
+
# To calculate the fuel flow in liters, one must multiply by 1000.0 and
|
31
|
+
# divide by the fuel tank density
|
32
|
+
def mass_flow_rate
|
33
|
+
return self.max_thrust / (self.isp * IspSurfaceGravity)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Calculated mass fuel flow.
|
37
|
+
def vac_mass_flow_rate
|
38
|
+
return self.max_thrust / (self.vac_isp * IspSurfaceGravity)
|
39
|
+
end
|
40
|
+
|
41
|
+
# This is the volume-wise fuel flow. Multiply by 1000.0 to get liters/s
|
42
|
+
# instead of m^3/s.
|
43
|
+
#
|
44
|
+
# It needs a fuel tank to calculate from, as fuel densities vary by
|
45
|
+
# tank.
|
46
|
+
def fuel_consumption(tank)
|
47
|
+
return self.mass_flow_rate / tank.fuel_density
|
48
|
+
end
|
49
|
+
|
50
|
+
# This is the volume-wise fuel flow. Multiply by 1000.0 to get liters/s
|
51
|
+
# instead of m^3/s.
|
52
|
+
#
|
53
|
+
# It needs a fuel tank to calculate from, as fuel densities vary by
|
54
|
+
# tank.
|
55
|
+
def vac_fuel_consumption(tank)
|
56
|
+
return self.vac_mass_flow_rate / tank.fuel_density
|
57
|
+
end
|
58
|
+
|
59
|
+
def thrust_vectored?
|
60
|
+
return self['thrustVectoringCapable'].to_s.downcase == 'true'
|
61
|
+
end
|
62
|
+
|
63
|
+
def gimbal_range
|
64
|
+
return self['gimbal_range'].to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'mixin/fuel_tank'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module KerbalDyn
|
2
|
+
module Part
|
3
|
+
module Mixin
|
4
|
+
module FuelTank
|
5
|
+
|
6
|
+
# Fuel capacity in m^3 to match mks requirement,
|
7
|
+
# even though the game seems to display liters.
|
8
|
+
#
|
9
|
+
# Note that 1 m^3 = 1000 liters
|
10
|
+
def fuel
|
11
|
+
return (self['fuel'] || self['internalFuel']).to_f / 1000.0
|
12
|
+
end
|
13
|
+
alias_method :internal_fuel, :fuel
|
14
|
+
alias_method :capacity, :fuel
|
15
|
+
|
16
|
+
def dry_mass
|
17
|
+
return self['dryMass'].to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
# The mass of the fuel.
|
21
|
+
def fuel_mass
|
22
|
+
return self.mass - self.dry_mass
|
23
|
+
end
|
24
|
+
|
25
|
+
# Calculated density in kg/m^3.
|
26
|
+
#
|
27
|
+
# Note that 1 m^3 = 1000 liters
|
28
|
+
def fuel_density
|
29
|
+
return self.fuel_mass / self.capacity
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|