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