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