driving_physics 0.0.1.2 → 0.0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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