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.
- checksums.yaml +4 -4
- data/README.md +23 -1
- data/Rakefile +84 -0
- data/VERSION +1 -1
- data/demo/car.rb +85 -72
- data/demo/gearbox.rb +4 -3
- data/demo/motor.rb +12 -10
- data/demo/mruby/car.rb +184 -0
- data/demo/mruby/motor.rb +140 -0
- 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 +14 -5
- data/lib/driving_physics/cli.rb +7 -35
- data/lib/driving_physics/cockpit.rb +167 -0
- data/lib/driving_physics/disk.rb +10 -30
- data/lib/driving_physics/environment.rb +2 -0
- data/lib/driving_physics/gearbox.rb +62 -32
- data/lib/driving_physics/motor.rb +218 -63
- data/lib/driving_physics/mruby.rb +48 -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/tire.rb +6 -6
- data/lib/driving_physics/vector_force.rb +23 -0
- data/lib/driving_physics.rb +28 -22
- data/test/disk.rb +23 -59
- 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 -53
- data/test/vector_force.rb +76 -37
- metadata +28 -4
data/demo/mruby/motor.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
|
2
|
+
# next fun idea: PID controller
|
3
|
+
|
4
|
+
include DrivingPhysics
|
5
|
+
|
6
|
+
env = Environment.new
|
7
|
+
motor = Motor.new(env)
|
8
|
+
puts env
|
9
|
+
puts motor
|
10
|
+
puts
|
11
|
+
|
12
|
+
puts "Rev it up!"
|
13
|
+
motor.throttle = 1.0
|
14
|
+
[:torque, :power].each { |run|
|
15
|
+
puts
|
16
|
+
puts run.to_s.upcase + ':'
|
17
|
+
|
18
|
+
800.upto(7000) { |rpm|
|
19
|
+
next unless rpm % 200 == 0
|
20
|
+
omega = DrivingPhysics.omega(rpm)
|
21
|
+
torque = motor.torque(rpm)
|
22
|
+
case run
|
23
|
+
when :torque
|
24
|
+
count = (torque.to_f / 10).round
|
25
|
+
char = 'T'
|
26
|
+
val = torque.round.to_s.rjust(5, ' ')
|
27
|
+
fmt = "%s Nm"
|
28
|
+
when :power
|
29
|
+
power = DrivingPhysics.power(torque, omega)
|
30
|
+
kw = power.to_f / 1000
|
31
|
+
count = (kw / 5).round
|
32
|
+
char = 'P'
|
33
|
+
val = kw.round(1).to_s.rjust(5, ' ')
|
34
|
+
fmt = "%s kW"
|
35
|
+
else
|
36
|
+
raise "unknown"
|
37
|
+
end
|
38
|
+
puts format("%s RPM: #{fmt}\t%s",
|
39
|
+
rpm.to_s.rjust(4, ' '),
|
40
|
+
val,
|
41
|
+
char * count)
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
puts
|
46
|
+
puts "Now, the simulation begins..."
|
47
|
+
puts "---"
|
48
|
+
puts "* Spin the motor up to #{motor.idle} RPM with the starter motor."
|
49
|
+
puts "* Rev it up with the throttle."
|
50
|
+
puts "* Let it die."
|
51
|
+
CLI.pause
|
52
|
+
|
53
|
+
alpha = 0.0
|
54
|
+
omega = 0.0
|
55
|
+
theta = 0.0
|
56
|
+
|
57
|
+
duration = 60
|
58
|
+
|
59
|
+
status = :ignition
|
60
|
+
rpm = 0
|
61
|
+
|
62
|
+
paused = 0.0
|
63
|
+
num_ticks = duration * env.hz + 1
|
64
|
+
start = Timer.now
|
65
|
+
|
66
|
+
num_ticks.times { |i|
|
67
|
+
# this is an input torque; alpha is determined after inertia and friction
|
68
|
+
torque = case status
|
69
|
+
when :ignition
|
70
|
+
motor.starter_torque
|
71
|
+
when :running, :off_throttle, :idling
|
72
|
+
motor.torque(rpm)
|
73
|
+
else
|
74
|
+
0
|
75
|
+
end
|
76
|
+
|
77
|
+
# Motor#alpha incorporates inertia and friction
|
78
|
+
alpha = motor.alpha(torque, omega: omega)
|
79
|
+
omega += alpha * env.tick
|
80
|
+
theta += omega * env.tick
|
81
|
+
|
82
|
+
# prevent silly oscillations due to ticks or tiny floating point errors
|
83
|
+
omega = 0 if omega < 0.0001
|
84
|
+
|
85
|
+
net_torque = motor.implied_torque(alpha)
|
86
|
+
rpm = DrivingPhysics.rpm(omega)
|
87
|
+
power = DrivingPhysics.power(net_torque, omega)
|
88
|
+
|
89
|
+
if rpm > motor.idle and status == :ignition
|
90
|
+
status = :running
|
91
|
+
flag = true
|
92
|
+
end
|
93
|
+
|
94
|
+
if rpm > 7000 and status == :running
|
95
|
+
status = :off_throttle
|
96
|
+
motor.throttle = 0.0
|
97
|
+
flag = true
|
98
|
+
end
|
99
|
+
|
100
|
+
if rpm < 1000 and status == :off_throttle
|
101
|
+
status = :idling
|
102
|
+
motor.throttle = 0.06
|
103
|
+
flag = true
|
104
|
+
end
|
105
|
+
|
106
|
+
# we don't need no PID control
|
107
|
+
if status == :idling
|
108
|
+
if rpm.round <= 999
|
109
|
+
motor.throttle += (1.0 - motor.throttle) * 0.005
|
110
|
+
elsif rpm.round == 1000
|
111
|
+
motor.throttle += (rand - 0.5) * 0.0005
|
112
|
+
else
|
113
|
+
motor.throttle -= motor.throttle * 0.005
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
if flag or
|
118
|
+
(i < 10) or
|
119
|
+
(i < 100 and i % 10 == 0) or
|
120
|
+
(i < 1000 and i % 100 == 0) or
|
121
|
+
(i < 10_000 and i % 500 == 0) or
|
122
|
+
(i % 5000 == 0) or
|
123
|
+
(status == :idling and i % 100 == 0)
|
124
|
+
puts Timer.display(ms: i)
|
125
|
+
puts format("Throttle: %.1f%%", motor.throttle * 100)
|
126
|
+
puts format("%d RPM %.1f Nm (%d Nm) %.1f kW Friction: %.1f Nm",
|
127
|
+
DrivingPhysics.rpm(omega),
|
128
|
+
net_torque,
|
129
|
+
torque,
|
130
|
+
power / 1000,
|
131
|
+
motor.spinner.rotating_friction(omega))
|
132
|
+
puts format("%.3f rad/s/s %.2f rad/s %.1f rad", alpha, omega, theta)
|
133
|
+
puts
|
134
|
+
|
135
|
+
paused += CLI.pause if flag
|
136
|
+
flag = false
|
137
|
+
end
|
138
|
+
}
|
139
|
+
|
140
|
+
puts Timer.summary(start, num_ticks, paused)
|
@@ -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)
|
@@ -103,7 +112,7 @@ module DrivingPhysics
|
|
103
112
|
end
|
104
113
|
|
105
114
|
def corner_mass
|
106
|
-
|
115
|
+
self.total_mass / @num_tires
|
107
116
|
end
|
108
117
|
|
109
118
|
# per tire
|
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
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'driving_physics/environment'
|
2
|
-
require 'driving_physics/vector_force'
|
2
|
+
#require 'driving_physics/vector_force'
|
3
3
|
|
4
4
|
module DrivingPhysics
|
5
5
|
# radius is always in meters
|
@@ -80,41 +80,16 @@ 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
|
|
111
86
|
def initialize(env)
|
112
87
|
@env = env
|
113
|
-
@radius =
|
114
|
-
@width =
|
88
|
+
@radius = 0.35
|
89
|
+
@width = 0.2
|
115
90
|
@density = DENSITY
|
116
|
-
@base_friction = 5/
|
117
|
-
@omega_friction = 5/
|
91
|
+
@base_friction = 5.0/100_000 # constant resistance to rotation
|
92
|
+
@omega_friction = 5.0/100_000 # scales with omega
|
118
93
|
yield self if block_given?
|
119
94
|
end
|
120
95
|
|
@@ -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
|