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