kerbaldyn 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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,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
@@ -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,11 @@
1
+ module KerbalDyn
2
+ module Part
3
+ # A class respresenting all liquid fuel tanks.
4
+ #
5
+ # Most of its methods are defined in the Mixin::FuelTank module.
6
+ class FuelTank < Base
7
+ include Mixin::FuelTank
8
+ end
9
+ end
10
+ end
11
+
@@ -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