driving_physics 0.0.2.1 → 0.0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -14
- data/Rakefile +3 -3
- data/VERSION +1 -1
- data/demo/car.rb +83 -73
- data/demo/motor.rb +7 -4
- data/demo/mruby/car.rb +184 -0
- data/demo/mruby/motor.rb +7 -4
- data/demo/pid_controller.rb +143 -0
- data/demo/powertrain.rb +1 -1
- data/driving_physics.gemspec +2 -1
- data/lib/driving_physics/car.rb +13 -4
- data/lib/driving_physics/cli.rb +7 -35
- data/lib/driving_physics/cockpit.rb +167 -0
- data/lib/driving_physics/disk.rb +5 -25
- data/lib/driving_physics/environment.rb +2 -0
- data/lib/driving_physics/gearbox.rb +60 -30
- data/lib/driving_physics/motor.rb +166 -58
- data/lib/driving_physics/mruby.rb +3 -0
- data/lib/driving_physics/pid_controller.rb +111 -0
- data/lib/driving_physics/powertrain.rb +8 -13
- data/lib/driving_physics/scalar_force.rb +2 -1
- data/lib/driving_physics/timer.rb +34 -0
- data/lib/driving_physics/vector_force.rb +23 -0
- data/lib/driving_physics.rb +28 -22
- data/test/disk.rb +23 -62
- data/test/gearbox.rb +36 -0
- data/test/motor.rb +236 -0
- data/test/powertrain.rb +21 -0
- data/test/scalar_force.rb +3 -6
- data/test/tire.rb +25 -59
- data/test/vector_force.rb +76 -37
- metadata +26 -5
- data/demo/mruby/disk.rb +0 -81
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'device_control'
|
2
|
+
require 'driving_physics/motor'
|
3
|
+
require 'driving_physics/cli'
|
4
|
+
|
5
|
+
include DrivingPhysics
|
6
|
+
|
7
|
+
env = Environment.new { |e|
|
8
|
+
e.hz = CLI.prompt("What frequency? (Hz)", default: 1000).to_i
|
9
|
+
}
|
10
|
+
motor = Motor.new(env)
|
11
|
+
puts env
|
12
|
+
puts motor
|
13
|
+
puts
|
14
|
+
|
15
|
+
|
16
|
+
PID = DeviceControl::PIDController
|
17
|
+
models = PID::ZN.keys.join(' ')
|
18
|
+
|
19
|
+
puts "PID models: #{models}"
|
20
|
+
pid_model = CLI.prompt("What PID model?", default: 'none')
|
21
|
+
|
22
|
+
# Ku for 1000 RPM idle has been measured at 0.2
|
23
|
+
# Tu is 1000 Hz / 2
|
24
|
+
zn = PID.tune(pid_model, 0.2, 1/500r)
|
25
|
+
puts format("kp: %.3f\tki: %.1f\tkd: %.8f", zn[:kp], zn[:ki], zn[:kd])
|
26
|
+
puts
|
27
|
+
|
28
|
+
# maintain arbitrary RPM via throttle adjustment
|
29
|
+
pidc = DeviceControl::PIDController.new(motor.idle, dt: env.tick) { |p|
|
30
|
+
p.kp = zn[:kp]
|
31
|
+
p.ki = zn[:ki]
|
32
|
+
p.kd = zn[:kd]
|
33
|
+
p.p_range = (-1.00..1.00)
|
34
|
+
p.i_range = (-0.50..0.50)
|
35
|
+
p.d_range = (-0.25..0.25)
|
36
|
+
p.o_range = ( 0.00..1.00)
|
37
|
+
p.e_range = (-1.00..1.00)
|
38
|
+
}
|
39
|
+
# track the accumulated absolute error
|
40
|
+
pidc_error = 0.0
|
41
|
+
|
42
|
+
puts pidc
|
43
|
+
puts
|
44
|
+
|
45
|
+
duration = CLI.prompt("How long to run for? (seconds)", default: 10).to_f
|
46
|
+
CLI.pause
|
47
|
+
|
48
|
+
alpha = 0.0
|
49
|
+
omega = 0.0
|
50
|
+
|
51
|
+
rpm = 0
|
52
|
+
status = :ignition
|
53
|
+
|
54
|
+
(duration * env.hz + 1).to_i.times { |i|
|
55
|
+
puts Timer.display(seconds: i.to_f / env.hz)
|
56
|
+
|
57
|
+
# GIVEN:
|
58
|
+
# * rpm
|
59
|
+
# DETERMINE:
|
60
|
+
# * torque
|
61
|
+
# * new rpm
|
62
|
+
|
63
|
+
case status
|
64
|
+
when :ignition
|
65
|
+
torque = motor.starter_torque
|
66
|
+
puts "IGNITION: Starter torque %d Nm %d RPM" % [torque, rpm]
|
67
|
+
status = :running if rpm > motor.idle
|
68
|
+
when :running
|
69
|
+
torque = motor.torque(rpm)
|
70
|
+
puts format("RUNNING: %.1f Nm @ %d RPM Throttle: %s",
|
71
|
+
torque, rpm.round, motor.throttle_pct(3))
|
72
|
+
puts pidc
|
73
|
+
puts "Absolute error %.3f" % pidc_error
|
74
|
+
end
|
75
|
+
|
76
|
+
# update kinematics
|
77
|
+
alpha = motor.alpha(torque, omega: omega)
|
78
|
+
omega += alpha * env.tick
|
79
|
+
rpm = DrivingPhysics.rpm(omega)
|
80
|
+
|
81
|
+
puts format("Net Torque: %+.4f Nm Friction: %+.4f Nm",
|
82
|
+
motor.implied_torque(alpha),
|
83
|
+
motor.friction(omega))
|
84
|
+
|
85
|
+
case status
|
86
|
+
when :ignition
|
87
|
+
# ok
|
88
|
+
CLI.pause if i % 50 == 0
|
89
|
+
when :running
|
90
|
+
# Update throttle position and RPM
|
91
|
+
# Throttle position is based on PID controller
|
92
|
+
# PID controller looks at current RPM
|
93
|
+
# Update to the new RPM at the very end of the loop
|
94
|
+
|
95
|
+
motor.throttle = pidc.update(rpm)
|
96
|
+
pidc_error += pidc.error.abs
|
97
|
+
error_pct = pidc.error.abs / pidc.setpoint.to_f
|
98
|
+
|
99
|
+
# prompt every so often
|
100
|
+
if (error_pct < 0.001) or
|
101
|
+
(i < 10_000 and i % 100 == 0) or
|
102
|
+
(i % 1000 == 0)
|
103
|
+
# ask about PID tunables; loop until an acceptable answer
|
104
|
+
loop {
|
105
|
+
puts
|
106
|
+
puts format("rpm %.1f\tsetpoint %d\tkp %.3f\tki %.3f\tkd %.3f",
|
107
|
+
rpm, pidc.setpoint, pidc.kp, pidc.ki, pidc.kd)
|
108
|
+
str = CLI.prompt("Enter key and value, e.g. > setpoint 3500\n")
|
109
|
+
# empty answer is perfectly acceptable; exit the loop
|
110
|
+
break if str.empty?
|
111
|
+
|
112
|
+
# look for "key value" pairs
|
113
|
+
parts = str.split(' ').map(&:strip)
|
114
|
+
next unless parts.size == 2
|
115
|
+
begin
|
116
|
+
key, val = parts[0].downcase, parts[1].to_f
|
117
|
+
rescue => e
|
118
|
+
puts e
|
119
|
+
next
|
120
|
+
end
|
121
|
+
|
122
|
+
# update RPM or PID controller and exit the loop
|
123
|
+
if key == "rpm"
|
124
|
+
rpm = val
|
125
|
+
omega = DrivingPhysics.omega(rpm)
|
126
|
+
else
|
127
|
+
begin
|
128
|
+
pidc.send("#{key}=", val)
|
129
|
+
rescue => e
|
130
|
+
puts e
|
131
|
+
next
|
132
|
+
end
|
133
|
+
end
|
134
|
+
# reset error tracking on successful input
|
135
|
+
pidc_error = 0.0
|
136
|
+
pidc.sum_error = 0.0
|
137
|
+
puts
|
138
|
+
break
|
139
|
+
}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
puts
|
143
|
+
}
|
data/demo/powertrain.rb
CHANGED
@@ -8,7 +8,7 @@ include DrivingPhysics
|
|
8
8
|
env = Environment.new
|
9
9
|
motor = Motor.new(env)
|
10
10
|
gearbox = Gearbox.new(env)
|
11
|
-
powertrain = Powertrain.new(motor, gearbox)
|
11
|
+
powertrain = Powertrain.new(motor: motor, gearbox: gearbox)
|
12
12
|
motor.throttle = 1.0
|
13
13
|
puts env
|
14
14
|
puts powertrain
|
data/driving_physics.gemspec
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'driving_physics'
|
3
|
-
s.summary = "
|
3
|
+
s.summary = "Physics from first principles: mass, friction, rotation, etc"
|
4
4
|
s.description = "WIP"
|
5
5
|
s.authors = ["Rick Hull"]
|
6
6
|
s.homepage = "https://github.com/rickhull/driving_physics"
|
7
7
|
s.license = "LGPL-3.0"
|
8
8
|
|
9
9
|
s.required_ruby_version = "> 2"
|
10
|
+
s.add_runtime_dependency 'device_control', '~> 0.3'
|
10
11
|
|
11
12
|
s.version = File.read(File.join(__dir__, 'VERSION')).chomp
|
12
13
|
|
data/lib/driving_physics/car.rb
CHANGED
@@ -26,6 +26,14 @@ module DrivingPhysics
|
|
26
26
|
@powertrain.motor.throttle = val
|
27
27
|
end
|
28
28
|
|
29
|
+
def clutch
|
30
|
+
@powertrain.gearbox.clutch
|
31
|
+
end
|
32
|
+
|
33
|
+
def clutch=(val)
|
34
|
+
@powertrain.gearbox.clutch = val
|
35
|
+
end
|
36
|
+
|
29
37
|
def gear
|
30
38
|
@powertrain.gearbox.gear
|
31
39
|
end
|
@@ -38,9 +46,9 @@ module DrivingPhysics
|
|
38
46
|
@powertrain.gearbox.top_gear
|
39
47
|
end
|
40
48
|
|
41
|
-
# force opposing speed
|
49
|
+
# force opposing speed; depends on speed**2 but use speed and speed.abs
|
42
50
|
def air_force(speed)
|
43
|
-
-0.5 * @frontal_area * @cd * @env.air_density * speed
|
51
|
+
-0.5 * @frontal_area * @cd * @env.air_density * speed * speed.abs
|
44
52
|
end
|
45
53
|
|
46
54
|
# force of opposite sign to omega
|
@@ -60,6 +68,7 @@ module DrivingPhysics
|
|
60
68
|
# force of opposite sign to force
|
61
69
|
def tire_inertial_force(force)
|
62
70
|
mag = force.abs
|
71
|
+
return 0.0 if mag < 0.001
|
63
72
|
sign = force / mag
|
64
73
|
force_loss = 0
|
65
74
|
5.times {
|
@@ -86,8 +95,8 @@ module DrivingPhysics
|
|
86
95
|
].join("\n")
|
87
96
|
end
|
88
97
|
|
89
|
-
def drive_force(rpm)
|
90
|
-
@tire.force
|
98
|
+
def drive_force(rpm, axle_omega: nil)
|
99
|
+
@tire.force @powertrain.axle_torque(rpm, axle_omega: axle_omega)
|
91
100
|
end
|
92
101
|
|
93
102
|
def tire_speed(rpm)
|
data/lib/driving_physics/cli.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
|
+
require 'driving_physics/timer'
|
2
|
+
|
1
3
|
module DrivingPhysics
|
2
4
|
module CLI
|
3
5
|
# returns user input as a string
|
4
|
-
def self.prompt(msg = '')
|
5
|
-
print msg
|
6
|
+
def self.prompt(msg = '', default: nil)
|
7
|
+
print "#{msg} " unless msg.empty?
|
8
|
+
print "(#{default}) " unless default.nil?
|
6
9
|
print '> '
|
7
|
-
$stdin.gets.chomp
|
10
|
+
input = $stdin.gets.chomp
|
11
|
+
input.empty? ? default.to_s : input
|
8
12
|
end
|
9
13
|
|
10
14
|
# press Enter to continue, ignore input, return elapsed time
|
@@ -16,36 +20,4 @@ module DrivingPhysics
|
|
16
20
|
Timer.since(t)
|
17
21
|
end
|
18
22
|
end
|
19
|
-
|
20
|
-
module Timer
|
21
|
-
if defined? Process::CLOCK_MONOTONIC
|
22
|
-
def self.now
|
23
|
-
Process.clock_gettime Process::CLOCK_MONOTONIC
|
24
|
-
end
|
25
|
-
else
|
26
|
-
def self.now
|
27
|
-
Time.now
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.since(t)
|
32
|
-
self.now - t
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.elapsed(&work)
|
36
|
-
t = self.now
|
37
|
-
return yield, self.since(t)
|
38
|
-
end
|
39
|
-
|
40
|
-
# HH:MM:SS.mmm
|
41
|
-
def self.display(seconds: 0, ms: 0)
|
42
|
-
ms += (seconds * 1000).round if seconds > 0
|
43
|
-
DrivingPhysics.elapsed_display(ms)
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.summary(start, num_ticks, paused = 0)
|
47
|
-
elapsed = self.since(start) - paused
|
48
|
-
format("%.3f s (%d ticks/s)", elapsed, num_ticks.to_f / elapsed)
|
49
|
-
end
|
50
|
-
end
|
51
23
|
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'driving_physics/car'
|
2
|
+
|
3
|
+
module DrivingPhysics
|
4
|
+
class Cockpit
|
5
|
+
CLUTCH_MIN = 0.1
|
6
|
+
REV_MATCH = 0.02
|
7
|
+
REV_SLIP = 0.1
|
8
|
+
MODE = {
|
9
|
+
normal: 0,
|
10
|
+
sport: 1,
|
11
|
+
sport_plus: 2,
|
12
|
+
}
|
13
|
+
MODE_LABELS = MODE.invert
|
14
|
+
|
15
|
+
def self.mode_label(idx)
|
16
|
+
MODE_LABELS.fetch(idx)
|
17
|
+
end
|
18
|
+
|
19
|
+
# return [:status, recommended clutch, proportional difference]
|
20
|
+
def self.rev_match(old_rpm, new_rpm)
|
21
|
+
diff = new_rpm - old_rpm
|
22
|
+
proportion = diff.to_f / old_rpm
|
23
|
+
clutch = [1.0 - proportion.abs, CLUTCH_MIN].max
|
24
|
+
if proportion.abs <= REV_MATCH
|
25
|
+
[:match, 1.0, proportion]
|
26
|
+
elsif proportion.abs <= REV_SLIP
|
27
|
+
[:slip, clutch, proportion]
|
28
|
+
else
|
29
|
+
[:mismatch, clutch, proportion]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.generate
|
34
|
+
e = Environment.new
|
35
|
+
t = Tire.new(e)
|
36
|
+
m = Motor.new(e)
|
37
|
+
g = Gearbox.new(e)
|
38
|
+
p = Powertrain.new(motor: m, gearbox: g)
|
39
|
+
c = Car.new(tire: t, powertrain: p)
|
40
|
+
self.new(c)
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :car, :brake_pedal, :steering_wheel, :mode, :mode_label
|
44
|
+
attr_accessor :min_rpm, :max_rpm
|
45
|
+
|
46
|
+
def initialize(car)
|
47
|
+
@car = car
|
48
|
+
@brake_pedal = 0.0 # to 1.0
|
49
|
+
@steering_wheel = 0.0 # -1.0 to +1.0
|
50
|
+
@min_rpm = @car.powertrain.motor.idle + 1000
|
51
|
+
@max_rpm = @car.powertrain.motor.redline
|
52
|
+
|
53
|
+
self.mode = 0 # normal
|
54
|
+
|
55
|
+
yield self if block_given?
|
56
|
+
end
|
57
|
+
|
58
|
+
def clutch_pedal
|
59
|
+
1.0 - @car.clutch
|
60
|
+
end
|
61
|
+
|
62
|
+
def clutch_pct
|
63
|
+
self.clutch_pedal * 100
|
64
|
+
end
|
65
|
+
|
66
|
+
# clutch pedal has inverse relationship with the clutch itself
|
67
|
+
# clutch pedal DISENGAGES the clutch; pedal at 0.0 means clutch at 1.0
|
68
|
+
def clutch_pedal=(val)
|
69
|
+
@car.clutch = 1.0 - DrivingPhysics.unit_interval!(val)
|
70
|
+
end
|
71
|
+
|
72
|
+
def brake_pct
|
73
|
+
self.brake_pedal * 100
|
74
|
+
end
|
75
|
+
|
76
|
+
# TODO: implement @car.brake or something
|
77
|
+
def brake_pedal=(val)
|
78
|
+
@brake_pedal = DrivingPhysics.unit_interval!(val)
|
79
|
+
end
|
80
|
+
|
81
|
+
def throttle_pedal
|
82
|
+
@car.throttle
|
83
|
+
end
|
84
|
+
|
85
|
+
def throttle_pct
|
86
|
+
self.throttle_pedal * 100
|
87
|
+
end
|
88
|
+
|
89
|
+
def throttle_pedal=(val)
|
90
|
+
@car.throttle = DrivingPhysics.unit_interval!(val)
|
91
|
+
end
|
92
|
+
|
93
|
+
def steering_pct
|
94
|
+
self.steering_wheel * 100
|
95
|
+
end
|
96
|
+
|
97
|
+
# TODO: implement @car.steering_wheel
|
98
|
+
def steering_wheel=(val)
|
99
|
+
DrivingPhysics.unit_interval!(val.abs)
|
100
|
+
@steering_wheel = val
|
101
|
+
end
|
102
|
+
|
103
|
+
def gear
|
104
|
+
@car.gear
|
105
|
+
end
|
106
|
+
|
107
|
+
def gear=(val)
|
108
|
+
@car.gear = val
|
109
|
+
end
|
110
|
+
|
111
|
+
def mode=(val)
|
112
|
+
@mode = MODE_LABELS[val] ? val : MODE.fetch(val)
|
113
|
+
@mode_label = Cockpit.mode_label(@mode)
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_s
|
117
|
+
[format("Clutch: %d%% Brake: %d%% Throttle: %d%%",
|
118
|
+
self.clutch_pct, self.brake_pct, self.throttle_pct),
|
119
|
+
format("Wheel: %d%% Gear: %d Mode: [%d] %s",
|
120
|
+
self.steering_pct, self.gear, self.mode, self.mode_label)
|
121
|
+
].join("\n")
|
122
|
+
end
|
123
|
+
|
124
|
+
# return :normal, :sport, :sport_plus based on pedal positions
|
125
|
+
def pedal_mode
|
126
|
+
if self.brake_pedal < 0.4 and self.throttle_pedal < 0.5
|
127
|
+
MODE[:normal]
|
128
|
+
elsif self.brake_pedal < 0.7 and self.throttle_pedal < 0.8
|
129
|
+
MODE[:sport]
|
130
|
+
else
|
131
|
+
MODE[:sport_plus]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def rpm_range
|
136
|
+
if @mode < MODE[:sport_plus] and self.pedal_mode == MODE[:sport_plus]
|
137
|
+
mode = @mode + 1
|
138
|
+
elsif @mode == MODE[:sport_plus] and self.pedal_mode == MODE[:normal]
|
139
|
+
mode = MODE[:sport]
|
140
|
+
else
|
141
|
+
mode = @mode
|
142
|
+
end
|
143
|
+
|
144
|
+
case mode
|
145
|
+
when MODE[:normal]
|
146
|
+
[@min_rpm, [@min_rpm + 3000, @max_rpm - 1000].min]
|
147
|
+
when MODE[:sport]
|
148
|
+
[@min_rpm + 1000, @max_rpm - 1000]
|
149
|
+
when MODE[:sport_plus]
|
150
|
+
[@min_rpm + 1500, @max_rpm]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def choose_gear(rpm)
|
155
|
+
min, max = *self.rpm_range
|
156
|
+
gear = self.gear
|
157
|
+
|
158
|
+
if rpm < min and gear > 1
|
159
|
+
gear - 1
|
160
|
+
elsif rpm > max and gear < @car.top_gear
|
161
|
+
gear + 1
|
162
|
+
else
|
163
|
+
gear
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/driving_physics/disk.rb
CHANGED
@@ -80,31 +80,6 @@ module DrivingPhysics
|
|
80
80
|
tangential.to_f / radius
|
81
81
|
end
|
82
82
|
|
83
|
-
# vectors only
|
84
|
-
def self.torque_vector(force, radius)
|
85
|
-
if !force.is_a?(Vector) or force.size != 2
|
86
|
-
raise(ArgumentError, "force must be a 2D vector")
|
87
|
-
end
|
88
|
-
if !radius.is_a?(Vector) or radius.size != 2
|
89
|
-
raise(ArgumentError, "radius must be a 2D vector")
|
90
|
-
end
|
91
|
-
force = Vector[force[0], force[1], 0]
|
92
|
-
radius = Vector[radius[0], radius[1], 0]
|
93
|
-
force.cross(radius)
|
94
|
-
end
|
95
|
-
|
96
|
-
# vectors only
|
97
|
-
def self.force_vector(torque, radius)
|
98
|
-
if !torque.is_a?(Vector) or torque.size != 3
|
99
|
-
raise(ArgumentError, "torque must be a 3D vector")
|
100
|
-
end
|
101
|
-
if !radius.is_a?(Vector) or radius.size != 2
|
102
|
-
raise(ArgumentError, "radius must be a 2D vector")
|
103
|
-
end
|
104
|
-
radius = Vector[radius[0], radius[1], 0]
|
105
|
-
radius.cross(torque) / radius.dot(radius)
|
106
|
-
end
|
107
|
-
|
108
83
|
attr_reader :env
|
109
84
|
attr_accessor :radius, :width, :density, :base_friction, :omega_friction
|
110
85
|
|
@@ -130,6 +105,11 @@ module DrivingPhysics
|
|
130
105
|
@normal_force
|
131
106
|
end
|
132
107
|
|
108
|
+
# E = (1/2) (I) (omega^2)
|
109
|
+
def energy(omega)
|
110
|
+
0.5 * self.rotational_inertia * omega ** 2
|
111
|
+
end
|
112
|
+
|
133
113
|
def alpha(torque, omega: 0, normal_force: nil)
|
134
114
|
(torque - self.rotating_friction(omega, normal_force: normal_force)) /
|
135
115
|
self.rotational_inertia
|
@@ -13,16 +13,30 @@ 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 :
|
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
|
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|
|
@@ -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
|
-
[
|
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
|
-
|
70
|
-
|
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
|
78
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|