driving_physics 0.0.1.2 → 0.0.3.1

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.
@@ -13,22 +13,36 @@ module DrivingPhysics
13
13
  # Likewise, 1st gear is a _smaller_ gear ratio than 3rd
14
14
  class Gearbox
15
15
  class Disengaged < RuntimeError; end
16
+ class ClutchDisengage < Disengaged; end
16
17
 
17
18
  RATIOS = [1/5r, 2/5r, 5/9r, 5/7r, 1r, 5/4r]
18
19
  FINAL_DRIVE = 11/41r # 1/3.73
20
+ REVERSE = -1
21
+ REVERSE_RATIO = -1/10r
19
22
 
20
- attr_accessor :gear, :ratios, :final_drive, :spinner, :fixed_mass
23
+ attr_accessor :ratios, :final_drive, :spinner, :fixed_mass
24
+ attr_reader :gear, :clutch
25
+
26
+ def self.gear_interval!(gear, min: REVERSE, max:)
27
+ if gear < min or gear > max
28
+ raise(ArgumentError, format("gear %s should be between %d and %d",
29
+ gear.inspect, min, max))
30
+ end
31
+ raise(ArgumentError, "gear should be an integer") if !gear.is_a? Integer
32
+ gear
33
+ end
21
34
 
22
35
  def initialize(env)
23
36
  @ratios = RATIOS
24
37
  @final_drive = FINAL_DRIVE
25
- @gear = 0 # neutral
38
+ @gear = 0 # neutral
39
+ @clutch = 1.0 # fully engaged (clutch pedal out)
26
40
 
27
41
  # represent all rotating mass
28
42
  @spinner = Disk.new(env) { |m|
29
43
  m.radius = 0.15
30
- m.base_friction = 5/1000r
31
- m.omega_friction = 15/100_000r
44
+ m.base_friction = 5.0/1000
45
+ m.omega_friction = 15.0/100_000
32
46
  m.mass = 15
33
47
  }
34
48
  @fixed_mass = 30 # kg
@@ -36,6 +50,10 @@ module DrivingPhysics
36
50
  yield self if block_given?
37
51
  end
38
52
 
53
+ def clutch=(val)
54
+ @clutch = DrivingPhysics.unit_interval! val
55
+ end
56
+
39
57
  # given torque, apply friction, determine alpha
40
58
  def alpha(torque, omega: 0)
41
59
  @spinner.alpha(torque + @spinner.rotating_friction(omega))
@@ -53,57 +71,69 @@ module DrivingPhysics
53
71
  @fixed_mass + @spinner.mass
54
72
  end
55
73
 
74
+ def gear=(val)
75
+ @gear = self.class.gear_interval!(val, max: self.top_gear)
76
+ end
77
+
56
78
  def top_gear
57
79
  @ratios.length
58
80
  end
59
81
 
60
82
  def to_s
61
- [format("Ratios: %s", @ratios.inspect),
83
+ [self.inputs,
84
+ format("Ratios: %s", @ratios.inspect),
62
85
  format(" Final: %s Mass: %.1f kg Rotating: %.1f kg",
63
86
  @final_drive.inspect, self.mass, @spinner.mass),
64
87
  ].join("\n")
65
88
  end
66
89
 
90
+ def inputs
91
+ format("Gear: %d Clutch: %.1f%%", @gear, @clutch * 100)
92
+ end
93
+
67
94
  def ratio(gear = nil)
68
95
  gear ||= @gear
69
- raise(Disengaged, "Cannot determine gear ratio") if @gear == 0
70
- @ratios.fetch(gear - 1) * @final_drive
96
+ case gear
97
+ when REVERSE
98
+ REVERSE_RATIO * @final_drive
99
+ when 0
100
+ raise(Disengaged, "Cannot determine gear ratio")
101
+ else
102
+ @ratios.fetch(gear - 1) * @final_drive
103
+ end
71
104
  end
72
105
 
73
106
  def axle_torque(crank_torque)
74
- crank_torque / self.ratio
107
+ crank_torque * @clutch / self.ratio
75
108
  end
76
109
 
77
- def axle_omega(crank_rpm)
78
- DrivingPhysics.omega(crank_rpm) * self.ratio
110
+ def output_torque(crank_torque, crank_rpm, axle_omega: nil)
111
+ axle_alpha = self.alpha(self.axle_torque(crank_torque),
112
+ omega: self.axle_omega(crank_rpm,
113
+ axle_omega: axle_omega))
114
+ self.implied_torque(axle_alpha)
79
115
  end
80
116
 
81
- def crank_rpm(axle_omega)
82
- DrivingPhysics.rpm(axle_omega) / self.ratio
83
- end
84
-
85
- def match_rpms(old_rpm, new_rpm)
86
- diff = new_rpm - old_rpm
87
- proportion = diff.to_f / old_rpm
88
- if proportion.abs < 0.01
89
- [:ok, proportion]
90
- elsif proportion.abs < 0.1
91
- [:slip, proportion]
92
- elsif @gear == 1 and new_rpm < old_rpm and old_rpm <= 1500
93
- [:get_rolling, proportion]
94
- else
95
- [:mismatch, proportion]
117
+ # take into account the old axle_omega and @clutch
118
+ def axle_omega(crank_rpm, axle_omega: nil)
119
+ new_axle_omega = DrivingPhysics.omega(crank_rpm) * self.ratio
120
+ if axle_omega.nil?
121
+ raise(ClutchDisengage, "cannot determine axle omega") if @clutch != 1.0
122
+ return new_axle_omega
96
123
  end
124
+ diff = new_axle_omega - axle_omega
125
+ axle_omega + diff * @clutch
97
126
  end
98
127
 
99
- def next_gear(rpm, floor: 2500, ceiling: 6400)
100
- if rpm < floor and @gear > 1
101
- @gear - 1
102
- elsif rpm > ceiling and @gear < self.top_gear
103
- @gear + 1
104
- else
105
- @gear
128
+ # take into account the old crank_rpm and @clutch
129
+ # crank will tolerate mismatch more than axle
130
+ def crank_rpm(axle_omega, crank_rpm: nil)
131
+ new_crank_rpm = DrivingPhysics.rpm(axle_omega) / self.ratio
132
+ if crank_rpm.nil?
133
+ raise(ClutchDisengage, "cannot determine crank rpm") if @clutch != 1.0
134
+ return new_crank_rpm
106
135
  end
136
+ crank_rpm + (new_crank_rpm - crank_rpm) * @clutch
107
137
  end
108
138
  end
109
139
  end
@@ -1,103 +1,258 @@
1
1
  require 'driving_physics/disk'
2
2
 
3
3
  module DrivingPhysics
4
+ def self.interpolate(x, xs:, ys:)
5
+ raise("Xs size #{xs.size}; Ys size #{ys.size}") unless xs.size == ys.size
6
+ raise("#{x} out of range") if x < xs.first or x > xs.last
7
+ xs.each.with_index { |xi, i|
8
+ return ys[i] if x == xi
9
+ if i > 0
10
+ last_x, last_y = xs[i-1], ys[i-1]
11
+ raise("xs out of order (#{xi} <= #{last_x})") unless xi > last_x
12
+ if x <= xi
13
+ proportion = Rational(x - last_x) / (xi - last_x)
14
+ return last_y + (ys[i] - last_y) * proportion
15
+ end
16
+ end
17
+ }
18
+ raise("couldn't find #{x} in #{xs.inspect}") # sanity check
19
+ end
20
+
21
+ class TorqueCurve
22
+ RPMS = [500, 1000, 1500, 2000, 2500, 3500, 5000, 6000, 7000, 7100]
23
+ TORQUES = [ 0, 70, 130, 200, 250, 320, 330, 320, 260, 0]
24
+ RPM_IDX = {
25
+ min: 0,
26
+ idle: 1,
27
+ redline: -2,
28
+ max: -1,
29
+ }
30
+
31
+ def self.validate_rpms!(rpms)
32
+ raise("rpms should be positive") if rpms.any? { |r| r < 0 }
33
+ rpms.each.with_index { |r, i|
34
+ if i > 0 and r <= rpms[i-1]
35
+ raise("rpms #{rpms.inspect} should increase")
36
+ end
37
+ }
38
+ rpms
39
+ end
40
+
41
+ def self.validate_torques!(torques)
42
+ raise("first torque should be zero") unless torques.first == 0
43
+ raise("last torque should be zero") unless torques.last == 0
44
+ raise("torques should be positive") if torques.any? { |t| t < 0 }
45
+ torques
46
+ end
47
+
48
+ def initialize(rpms: RPMS, torques: TORQUES)
49
+ if rpms.size != torques.size
50
+ raise("RPMs size #{rpms.size}; Torques size #{torques.size}")
51
+ end
52
+ @rpms = self.class.validate_rpms! rpms
53
+ @torques = self.class.validate_torques! torques
54
+ peak_torque = 0
55
+ idx = 0
56
+ @torques.each.with_index { |t, i|
57
+ if t > peak_torque
58
+ peak_torque = t
59
+ idx = i
60
+ end
61
+ }
62
+ @peak = idx
63
+ end
64
+
65
+ def peak
66
+ [@rpms[@peak], @torques[@peak]]
67
+ end
68
+
69
+ def to_s
70
+ @rpms.map.with_index { |r, i|
71
+ format("%s RPM %s Nm",
72
+ r.to_s.rjust(5, ' '),
73
+ @torques[i].round(1).to_s.rjust(4, ' '))
74
+ }.join("\n")
75
+ end
76
+
77
+ RPM_IDX.each { |name, idx| define_method(name) do @rpms[idx] end }
78
+
79
+ # interpolate based on torque curve points
80
+ def torque(rpm)
81
+ DrivingPhysics.interpolate(rpm, xs: @rpms, ys: @torques)
82
+ end
83
+ end
84
+
85
+ # represent all rotating mass as one big flywheel
4
86
  class Motor
5
87
  class Stall < RuntimeError; end
6
88
  class OverRev < RuntimeError; end
7
- class SanityCheck < RuntimeError; end
8
89
 
9
- TORQUES = [ 0, 70, 130, 200, 250, 320, 330, 320, 260, 0]
10
- RPMS = [500, 1000, 1500, 2000, 2500, 3500, 5000, 6000, 7000, 7100]
11
- ENGINE_BRAKING = 0.2 # 20% of the torque at a given RPM
90
+ CLOSED_THROTTLE = 0.01 # threshold for engine braking
12
91
 
13
- attr_reader :env, :throttle
14
- attr_accessor :torques, :rpms, :fixed_mass,
15
- :spinner, :starter_torque, :idle_rpm
92
+ # What percentage of the nominal torque at a given RPM would slow the
93
+ # motor when off throttle? 0% at 0 RPM; 50% at 7000 RPM
94
+ def self.engine_braking(rpm)
95
+ (rpm / 14_000.0).clamp(0, 0.5)
96
+ end
16
97
 
17
- def initialize(env)
18
- @env = env
98
+ attr_reader :env, :torque_curve, :throttle
99
+ attr_accessor :fixed_mass, :spinner, :starter_torque
19
100
 
20
- @torques = TORQUES
21
- @rpms = RPMS
22
- @fixed_mass = 125
101
+ # Originally, torque_curve was a kwarg; but mruby currently has a bug
102
+ # where block_given? returns true in the presence of an unset default
103
+ # kwarg, or something like that.
104
+ # https://github.com/mruby/mruby/issues/5741
105
+ #
106
+ def initialize(env, torque_curve = nil)
107
+ @env = env
108
+ @torque_curve = torque_curve.nil? ? TorqueCurve.new : torque_curve
109
+ @throttle = 0.0 # 0.0 - 1.0 (0% - 100%)
23
110
 
24
- # represent all rotating mass as one big flywheel
25
- @spinner = Disk.new(@env) { |fly|
26
- fly.radius = 0.25 # m
27
- fly.mass = 75 # kg
28
- fly.base_friction = 5/1000r
29
- fly.omega_friction = 2/10_000r
111
+ @fixed_mass = 125
112
+ @flywheel = Disk.new(@env) { |fly|
113
+ fly.mass = 12
114
+ fly.radius = 0.20
115
+ fly.base_friction = 1.0 / 1_000
116
+ fly.omega_friction = 5.0 / 10_000
30
117
  }
118
+ @crankshaft = Disk.new(@env) { |crank|
119
+ crank.mass = 25
120
+ crank.radius = 0.04
121
+ crank.base_friction = 1.0 / 1_000
122
+ crank.omega_friction = 5.0 / 10_000
123
+ }
124
+
31
125
  @starter_torque = 500 # Nm
32
- @idle_rpm = 1000 # RPM
33
- @throttle = 0.0 # 0.0 - 1.0 (0% - 100%)
126
+
127
+ yield self if block_given?
128
+ end
129
+
130
+ def redline
131
+ @torque_curve.redline
132
+ end
133
+
134
+ def idle
135
+ @torque_curve.idle
34
136
  end
35
137
 
36
138
  def to_s
37
- ary = [format("Throttle: %.1f%%", @throttle * 100)]
38
- ary << "Torque:"
39
- @rpms.each_with_index { |r, i|
40
- ary << format("%s Nm %s RPM",
41
- @torques[i].round(1).to_s.rjust(4, ' '),
42
- r.to_s.rjust(5, ' '))
43
- }
44
- ary << format("Mass: %.1f kg Fixed: %d kg", self.mass, @fixed_mass)
45
- ary << format("Rotating: %s", @spinner)
46
- ary.join("\n")
139
+ peak_rpm, peak_tq = *@torque_curve.peak
140
+ [format("Peak Torque: %d Nm @ %d RPM Redline: %d",
141
+ peak_tq, peak_rpm, @torque_curve.redline),
142
+ format(" Throttle: %s Mass: %.1f kg (%d kg fixed)",
143
+ self.throttle_pct, self.mass, @fixed_mass),
144
+ format(" Crankshaft: %s", @crankshaft),
145
+ format(" Flywheel: %s", @flywheel),
146
+ ].join("\n")
147
+ end
148
+
149
+ def inertia
150
+ @crankshaft.rotational_inertia + @flywheel.rotational_inertia
151
+ end
152
+
153
+ def energy(omega)
154
+ @crankshaft.energy(omega) + @flywheel.energy(omega)
47
155
  end
48
156
 
49
- def rotational_inertia
50
- @spinner.rotational_inertia
157
+ def friction(omega)
158
+ @crankshaft.rotating_friction(omega) +
159
+ @flywheel.rotating_friction(omega)
51
160
  end
52
161
 
53
162
  def mass
54
- @spinner.mass + @fixed_mass
163
+ @fixed_mass + self.rotating_mass
164
+ end
165
+
166
+ def rotating_mass
167
+ @crankshaft.mass + @flywheel.mass
55
168
  end
56
169
 
57
170
  def throttle=(val)
58
- if val < 0.0 or val > 1.0
59
- raise(ArgumentError, "val #{val.inspect} should be between 0 and 1")
60
- end
61
- @throttle = val
171
+ @throttle = DrivingPhysics.unit_interval! val
62
172
  end
63
173
 
64
- # given torque, determine crank alpha after inertia and friction
174
+ def throttle_pct(places = 1)
175
+ format("%.#{places}f%%", @throttle * 100)
176
+ end
177
+
178
+ # given torque, determine crank alpha considering inertia and friction
65
179
  def alpha(torque, omega: 0)
66
- @spinner.alpha(torque + @spinner.rotating_friction(omega))
180
+ (torque + self.friction(omega)) / self.inertia
67
181
  end
68
182
 
69
183
  def implied_torque(alpha)
70
- @spinner.implied_torque(alpha)
184
+ alpha * self.inertia
71
185
  end
72
186
 
73
- # interpolate based on torque curve points
187
+ def output_torque(rpm)
188
+ self.torque(rpm) + self.friction(DrivingPhysics.omega(rpm))
189
+ end
190
+
191
+ # this is our "input torque" and it depends on @throttle
192
+ # here is where engine braking is implemented
74
193
  def torque(rpm)
75
- raise(Stall, "RPM #{rpm}") if rpm < @rpms.first
76
- raise(OverRev, "RPM #{rpm}") if rpm > @rpms.last
77
-
78
- last_rpm = 99999
79
- last_tq = -1
80
- torque = nil
81
-
82
- # ew; there must be a better way
83
- @rpms.each_with_index { |r, i|
84
- tq = @torques[i]
85
- if last_rpm <= rpm and rpm <= r
86
- proportion = Rational(rpm - last_rpm) / (r - last_rpm)
87
- torque = last_tq + (tq - last_tq) * proportion
88
- break
89
- end
90
- last_rpm = r
91
- last_tq = tq
92
- }
93
- raise(SanityCheck, "RPM #{rpm}") if torque.nil?
194
+ raise(Stall, "RPM #{rpm}") if rpm < @torque_curve.min
195
+ raise(OverRev, "RPM #{rpm}") if rpm > @torque_curve.max
196
+
197
+ # interpolate based on torque curve points
198
+ torque = @torque_curve.torque(rpm)
94
199
 
95
- if (@throttle <= 0.05) and (rpm > @idle_rpm * 1.5)
96
- # engine braking: 20% of torque
97
- -1 * torque * ENGINE_BRAKING
200
+ if (@throttle <= CLOSED_THROTTLE)
201
+ -1 * torque * self.class.engine_braking(rpm)
98
202
  else
99
203
  torque * @throttle
100
204
  end
101
205
  end
102
206
  end
103
207
  end
208
+
209
+
210
+ # TODO: starter motor
211
+ # Starter motor is power limited, not torque limited
212
+ # Consider:
213
+ # * 2.2 kW (3.75:1 gear reduction)
214
+ # * 1.8 kW (4.4:1 gear reduction)
215
+ # On a workbench, a starter will draw 80 to 90 amps. However, during actual
216
+ # start-up of an engine, a starter will draw 250 to 350 amps.
217
+ # from https://www.motortrend.com/how-to/because-theres-more-to-a-starter-than-you-realize/
218
+
219
+ # V - Potential, voltage
220
+ # I - Current, amperage
221
+ # R - Resistance, ohms
222
+ # P - Power, wattage
223
+
224
+ # Ohm's law: I = V / R (where R is held constant)
225
+ # P = I * V
226
+ # For resistors (where R is helod constant)
227
+ # = I^2 * R
228
+ # = V^2 / R
229
+
230
+
231
+ # torque proportional to A
232
+ # speed proportional to V
233
+
234
+
235
+ # batteries are rated in terms of CCA - cold cranking amps
236
+
237
+ # P = I * V
238
+ # V = 12 (up to 14ish)
239
+ # I = 300
240
+ # P = 3600 or 3.6 kW
241
+
242
+
243
+ # A starter rated at e.g. 2.2 kW will use more power on initial cranking
244
+ # Sometimes up to 500 amperes are required, and some batteries will provide
245
+ # over 600 cold cranking amps
246
+
247
+
248
+ # Consider:
249
+
250
+ # V = 12
251
+ # R = resistance of battery, wiring, starter motor
252
+ # L = inductance (approx 0)
253
+ # I = current through motor
254
+ # Vc = proportional to omega
255
+
256
+ # rated power - Vc * I
257
+ # input power - V * I
258
+ # input power that is unable to be converted to output power is wasted as heat
@@ -0,0 +1,48 @@
1
+ # This is only intended for use on mruby
2
+ # It is a workaround for Timer's use of Process in cli.rb
3
+ #
4
+ module DrivingPhysics
5
+ module CLI
6
+ # returns user input as a string
7
+ def self.prompt(msg = '')
8
+ print msg + ' ' unless msg.empty?
9
+ print '> '
10
+ $stdin.gets.chomp
11
+ end
12
+
13
+ # press Enter to continue, ignore input, return elapsed time
14
+ def self.pause(msg = '')
15
+ t = Timer.now
16
+ puts msg unless msg.empty?
17
+ puts ' [ Press Enter ]'
18
+ $stdin.gets
19
+ Timer.since(t)
20
+ end
21
+ end
22
+
23
+ module Timer
24
+ def self.now
25
+ Time.now
26
+ end
27
+
28
+ def self.since(t)
29
+ self.now - t
30
+ end
31
+
32
+ def self.elapsed(&work)
33
+ t = self.now
34
+ return yield, self.since(t)
35
+ end
36
+
37
+ # HH:MM:SS.mmm
38
+ def self.display(seconds: 0, ms: 0)
39
+ ms += (seconds * 1000).round if seconds > 0
40
+ DrivingPhysics.elapsed_display(ms)
41
+ end
42
+
43
+ def self.summary(start, num_ticks, paused = 0)
44
+ elapsed = self.since(start) - paused
45
+ format("%.3f s (%d ticks/s)", elapsed, num_ticks.to_f / elapsed)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,111 @@
1
+ module DrivingPhysics
2
+ # we will have a control loop
3
+ # SP - setpoint, this is the desired position
4
+ # PV(t) - process variable, this is the sensed position, varying over time
5
+ # e(t) - error, SP - PV
6
+ # CV(t) - control variable: the controller output
7
+
8
+ # for example, where to set the throttle to maintain 1000 RPM
9
+ # SP - 1000 RPM
10
+ # PV - current RPM
11
+ # CV - throttle position
12
+
13
+ class PIDController
14
+ HZ = 1000
15
+ TICK = Rational(1) / HZ
16
+
17
+ # Ziegler-Nichols method for tuning PID gain knobs
18
+ ZN = {
19
+ # Kp Ti Td Ki Kd
20
+ # Var: Ku Tu Tu Ku/Tu Ku*Tu
21
+ 'P' => [0.500],
22
+ 'PI' => [0.450, 0.800, nil, 0.540],
23
+ 'PD' => [0.800, nil, 0.125, nil, 0.100],
24
+ 'PID' => [0.600, 0.500, 0.125, 1.200, 0.075],
25
+ 'PIR' => [0.700, 0.400, 0.150, 1.750, 0.105],
26
+ # less overshoot than standard PID below
27
+ 'some' => [0.333, 0.500, 0.333, 0.666, 0.111],
28
+ 'none' => [0.200, 0.500, 0.333, 0.400, 0.066],
29
+ }
30
+
31
+ # ultimate gain, oscillation
32
+ def self.tune(type, ku, tu)
33
+ record = ZN[type.downcase] || ZN[type.upcase] || ZN.fetch(type)
34
+ kp, ti, td, ki, kd = *record
35
+ kp *= ku if kp
36
+ ti *= tu if ti
37
+ td *= tu if td
38
+ ki *= (ku / tu) if ki
39
+ kd *= (ku * tu) if kd
40
+ { kp: kp, ti: ti, td: td, ki: ki, kd: kd }
41
+ end
42
+
43
+ attr_accessor :kp, :ki, :kd, :dt, :setpoint,
44
+ :p_range, :i_range, :d_range, :o_range
45
+ attr_reader :measure, :error, :last_error, :sum_error
46
+
47
+ def initialize(setpoint, dt: TICK)
48
+ @setpoint, @dt, @measure = setpoint, dt, 0.0
49
+
50
+ # track error over time for integral and derivative
51
+ @error, @last_error, @sum_error = 0.0, 0.0, 0.0
52
+
53
+ # gain / multipliers for PID; tunables
54
+ @kp, @ki, @kd = 1.0, 1.0, 1.0
55
+
56
+ # optional clamps for PID terms and output
57
+ @p_range = (-Float::INFINITY..Float::INFINITY)
58
+ @i_range = (-Float::INFINITY..Float::INFINITY)
59
+ @d_range = (-Float::INFINITY..Float::INFINITY)
60
+ @o_range = (-Float::INFINITY..Float::INFINITY)
61
+
62
+ yield self if block_given?
63
+ end
64
+
65
+ def update(measure)
66
+ self.measure = measure
67
+ self.output
68
+ end
69
+
70
+ def measure=(val)
71
+ @measure = val
72
+ @last_error = @error
73
+ @error = @setpoint - @measure
74
+ if @error * @last_error <= 0 # zero crossing; reset the accumulated error
75
+ @sum_error = @error
76
+ else
77
+ @sum_error += @error
78
+ end
79
+ end
80
+
81
+ def output
82
+ (self.proportion +
83
+ self.integral +
84
+ self.derivative).clamp(@o_range.begin, @o_range.end)
85
+ end
86
+
87
+ def proportion
88
+ (@kp * @error).clamp(@p_range.begin, @p_range.end)
89
+ end
90
+
91
+ def integral
92
+ (@ki * @sum_error * @dt).clamp(@i_range.begin, @i_range.end)
93
+ end
94
+
95
+ def derivative
96
+ (@kd * (@error - @last_error) / @dt).clamp(@d_range.begin, @d_range.end)
97
+ end
98
+
99
+ def to_s
100
+ [format("Setpoint: %.3f Measure: %.3f",
101
+ @setpoint, @measure),
102
+ format("Error: %+.3f\tLast: %+.3f\tSum: %+.3f",
103
+ @error, @last_error, @sum_error),
104
+ format(" Gain:\t%.3f\t%.3f\t%.3f",
105
+ @kp, @ki, @kd),
106
+ format(" PID:\t%+.3f\t%+.3f\t%+.3f\t= %.5f",
107
+ self.proportion, self.integral, self.derivative, self.output),
108
+ ].join("\n")
109
+ end
110
+ end
111
+ end
@@ -10,7 +10,7 @@ module DrivingPhysics
10
10
  class Powertrain
11
11
  attr_reader :motor, :gearbox
12
12
 
13
- def initialize(motor, gearbox)
13
+ def initialize(motor:, gearbox:)
14
14
  @motor = motor
15
15
  @gearbox = gearbox
16
16
  end
@@ -29,22 +29,17 @@ module DrivingPhysics
29
29
  [t * o, t, o]
30
30
  end
31
31
 
32
- def axle_torque(rpm)
33
- crank_alpha = @motor.alpha(@motor.torque(rpm),
34
- omega: DrivingPhysics.omega(rpm))
35
- crank_torque = @motor.implied_torque(crank_alpha)
36
-
37
- axle_alpha = @gearbox.alpha(@gearbox.axle_torque(crank_torque),
38
- omega: @gearbox.axle_omega(rpm))
39
- @gearbox.implied_torque(axle_alpha)
32
+ def axle_torque(rpm, axle_omega: nil)
33
+ @gearbox.output_torque(@motor.output_torque(rpm), rpm,
34
+ axle_omega: axle_omega)
40
35
  end
41
36
 
42
- def axle_omega(rpm)
43
- @gearbox.axle_omega(rpm)
37
+ def axle_omega(rpm, axle_omega: nil)
38
+ @gearbox.axle_omega(rpm, axle_omega: axle_omega)
44
39
  end
45
40
 
46
- def crank_rpm(axle_omega)
47
- @gearbox.crank_rpm(axle_omega)
41
+ def crank_rpm(axle_omega, crank_rpm: nil)
42
+ @gearbox.crank_rpm(axle_omega, crank_rpm: crank_rpm)
48
43
  end
49
44
  end
50
45
  end
@@ -23,7 +23,8 @@ module DrivingPhysics
23
23
  frontal_area: FRONTAL_AREA,
24
24
  drag_cof: DRAG_COF,
25
25
  air_density: AIR_DENSITY)
26
- -1 * 0.5 * frontal_area * drag_cof * air_density * speed ** 2
26
+ (speed ** 2) * (speed > 0 ? -1 : 1) *
27
+ 0.5 * frontal_area * drag_cof * air_density
27
28
  end
28
29
 
29
30
  # opposes the direction of speed