driving_physics 0.0.1.1 → 0.0.1.2
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/VERSION +1 -1
- data/demo/car.rb +41 -19
- data/demo/disk.rb +20 -13
- data/demo/gearbox.rb +6 -1
- data/demo/motor.rb +68 -16
- data/demo/powertrain.rb +16 -9
- data/demo/scalar_force.rb +41 -15
- data/demo/tire.rb +38 -39
- data/demo/vector_force.rb +46 -16
- data/lib/driving_physics/car.rb +24 -4
- data/lib/driving_physics/cli.rb +35 -1
- data/lib/driving_physics/disk.rb +5 -4
- data/lib/driving_physics/imperial.rb +6 -0
- data/lib/driving_physics/motor.rb +24 -20
- data/lib/driving_physics/powertrain.rb +13 -2
- data/lib/driving_physics/scalar_force.rb +6 -3
- data/lib/driving_physics/tire.rb +1 -1
- data/lib/driving_physics.rb +1 -0
- data/test/scalar_force.rb +7 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef486c39982798a7331f49f601e648d520a65977161180631cc4c5d3340a9011
|
|
4
|
+
data.tar.gz: 9e30b74ae34cb77a711d5d9631983b0c070872900123a1dde2d3dd03bdab854b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46eaf512dad64ca39ec20451d7acce1b1819ecd64125ee858f92c26e5edd7b0ec0e7ef01af8f73daa0d37868e0ffd76bd3b29478093219cd906d8d9e38026bcd
|
|
7
|
+
data.tar.gz: ae2e01ae8e7986c9d2e659ff5a0934ed5359e20cf2757d7724118bb59e0fb0199180249815f2f28f55f48a2336511a0b43f0d6fab042ed5d20a5ab6a897baef1
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.1.
|
|
1
|
+
0.0.1.2
|
data/demo/car.rb
CHANGED
|
@@ -10,9 +10,9 @@ puts env
|
|
|
10
10
|
tire = Tire.new(env)
|
|
11
11
|
motor = Motor.new(env)
|
|
12
12
|
gearbox = Gearbox.new(env)
|
|
13
|
-
|
|
14
|
-
car = Car.new(tire: tire, powertrain:
|
|
15
|
-
c.
|
|
13
|
+
powertrain = Powertrain.new(motor, gearbox)
|
|
14
|
+
car = Car.new(tire: tire, powertrain: powertrain) { |c|
|
|
15
|
+
c.body_mass = 850.0
|
|
16
16
|
c.frontal_area = 2.5
|
|
17
17
|
c.cd = 0.5
|
|
18
18
|
}
|
|
@@ -21,16 +21,20 @@ CLI.pause
|
|
|
21
21
|
|
|
22
22
|
duration = 120
|
|
23
23
|
|
|
24
|
+
acc = 0.0
|
|
24
25
|
speed = 0.0
|
|
25
26
|
dist = 0.0
|
|
26
27
|
|
|
28
|
+
tire_alpha = 0.0
|
|
27
29
|
tire_omega = 0.0
|
|
28
30
|
tire_theta = 0.0
|
|
29
31
|
|
|
32
|
+
crank_alpha = 0.0
|
|
30
33
|
crank_omega = 0.0
|
|
31
34
|
crank_theta = 0.0
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
start = Timer.now
|
|
37
|
+
paused = 0.0
|
|
34
38
|
num_ticks = duration * env.hz + 1
|
|
35
39
|
|
|
36
40
|
clutch = :ok
|
|
@@ -55,15 +59,16 @@ num_ticks.times { |i|
|
|
|
55
59
|
rpm = DrivingPhysics.rpm(crank_omega)
|
|
56
60
|
|
|
57
61
|
if i % 100 == 0 or rpm > motor.idle_rpm
|
|
58
|
-
puts
|
|
62
|
+
puts Timer.display(i)
|
|
59
63
|
puts format("%d rad %d rad/s %d rad/s/s",
|
|
60
64
|
crank_theta, crank_omega, crank_alpha)
|
|
61
|
-
puts "RPM
|
|
65
|
+
puts format("%d RPM %d Nm starter torque", rpm, motor.starter_torque)
|
|
62
66
|
puts
|
|
63
67
|
end
|
|
64
68
|
|
|
65
69
|
if rpm > motor.idle_rpm
|
|
66
|
-
|
|
70
|
+
car.gear = 1
|
|
71
|
+
car.throttle = 1.0
|
|
67
72
|
phase = :running
|
|
68
73
|
|
|
69
74
|
puts <<EOF
|
|
@@ -77,13 +82,16 @@ EOF
|
|
|
77
82
|
elsif phase == :running
|
|
78
83
|
# track crank_alpha/omega/theta
|
|
79
84
|
|
|
85
|
+
# cut throttle after 60 s
|
|
86
|
+
car.throttle = 0 if i > 60 * env.hz and car.throttle == 1.0
|
|
87
|
+
|
|
80
88
|
ar = car.air_force(speed)
|
|
81
89
|
rr = car.tire_rolling_force(tire_omega)
|
|
82
90
|
rf = car.tire_rotational_force(tire_omega)
|
|
83
91
|
|
|
92
|
+
# note, this fails if we're in neutral
|
|
84
93
|
force = car.drive_force(rpm) + ar + rr + rf
|
|
85
94
|
|
|
86
|
-
|
|
87
95
|
ir = car.tire_inertial_force(force)
|
|
88
96
|
force += ir
|
|
89
97
|
|
|
@@ -103,11 +111,11 @@ EOF
|
|
|
103
111
|
crank_torque = car.powertrain.motor.torque(rpm)
|
|
104
112
|
|
|
105
113
|
if flag or (i < 5000 and i % 100 == 0) or (i % 1000 == 0)
|
|
106
|
-
puts
|
|
107
|
-
puts format(" Tire: %.1f r %.2f r/s %.3f r
|
|
108
|
-
|
|
109
|
-
puts format(" Car: %.1f m %.2f m/s %.3f m
|
|
110
|
-
|
|
114
|
+
puts Timer.display(i)
|
|
115
|
+
puts format(" Tire: %.1f r/s/s %.2f r/s %.3f r",
|
|
116
|
+
tire_alpha, tire_omega, tire_theta)
|
|
117
|
+
puts format(" Car: %.1f m/s/s %.2f m/s %.3f m (%.1f MPH)",
|
|
118
|
+
acc, speed, dist, Imperial.mph(speed))
|
|
111
119
|
puts format(" Motor: %d RPM %.1f Nm", rpm, crank_torque)
|
|
112
120
|
puts format(" Axle: %.1f Nm (%d N) Net Force: %.1f N",
|
|
113
121
|
axle_torque, car.drive_force(rpm), force)
|
|
@@ -127,7 +135,7 @@ EOF
|
|
|
127
135
|
puts format("Clutch: [%s] %d RPM is %.1f%% from %d RPM",
|
|
128
136
|
new_clutch, new_rpm, proportion * 100, rpm)
|
|
129
137
|
clutch = new_clutch
|
|
130
|
-
CLI.pause
|
|
138
|
+
paused += CLI.pause
|
|
131
139
|
end
|
|
132
140
|
|
|
133
141
|
case new_clutch
|
|
@@ -135,7 +143,9 @@ EOF
|
|
|
135
143
|
rpm = new_rpm
|
|
136
144
|
when :mismatch
|
|
137
145
|
flag = true
|
|
138
|
-
puts
|
|
146
|
+
puts '#'
|
|
147
|
+
puts '# LURCH!'
|
|
148
|
+
puts '#'
|
|
139
149
|
puts
|
|
140
150
|
rpm = new_rpm
|
|
141
151
|
end
|
|
@@ -143,11 +153,23 @@ EOF
|
|
|
143
153
|
if next_gear != gearbox.gear
|
|
144
154
|
flag = true
|
|
145
155
|
puts "Gear Change: #{next_gear}"
|
|
146
|
-
car.
|
|
147
|
-
CLI.pause
|
|
156
|
+
car.gear = next_gear
|
|
157
|
+
paused += CLI.pause
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# maintain idle when revs drop
|
|
161
|
+
if car.throttle == 0 and rpm < motor.idle_rpm
|
|
162
|
+
phase = :idling
|
|
163
|
+
car.gear = 0
|
|
164
|
+
paused += CLI.pause
|
|
148
165
|
end
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
elsif phase == :idling
|
|
169
|
+
# fake
|
|
170
|
+
rpm = motor.idle_rpm
|
|
171
|
+
break
|
|
149
172
|
end
|
|
150
173
|
}
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
|
|
175
|
+
puts Timer.summary(start, num_ticks, paused)
|
data/demo/disk.rb
CHANGED
|
@@ -19,6 +19,10 @@ puts [format("Axle torque: %.1f Nm", axle_torque),
|
|
|
19
19
|
format("Drive force: %.1f N", drive_force),
|
|
20
20
|
].join("\n")
|
|
21
21
|
puts
|
|
22
|
+
|
|
23
|
+
puts "* Spin up the disk with #{axle_torque} Nm of torque"
|
|
24
|
+
puts "* Cut the power at some point"
|
|
25
|
+
puts "* Observe"
|
|
22
26
|
CLI.pause
|
|
23
27
|
|
|
24
28
|
duration = 750 # sec
|
|
@@ -29,20 +33,20 @@ speed = 0.0 # meters/s
|
|
|
29
33
|
theta = 0.0 # radians
|
|
30
34
|
omega = 0.0 # radians/s
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
paused = 0.0 # seconds
|
|
37
|
+
num_ticks = duration * env.hz + 1
|
|
38
|
+
flag = false # to display current stats
|
|
39
|
+
start = Timer.now
|
|
35
40
|
|
|
36
41
|
num_ticks.times { |i|
|
|
37
42
|
# shut off the powah!
|
|
38
|
-
if i ==
|
|
39
|
-
|
|
40
|
-
puts
|
|
43
|
+
if i == 18_950
|
|
44
|
+
flag = true
|
|
45
|
+
puts '#'
|
|
46
|
+
puts '# CUT POWER'
|
|
47
|
+
puts '#'
|
|
41
48
|
puts
|
|
42
49
|
axle_torque = 0
|
|
43
|
-
elapsed += Time.now - t
|
|
44
|
-
CLI.pause
|
|
45
|
-
t = Time.now
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
rotating_friction = disk.rotating_friction(omega)
|
|
@@ -55,12 +59,12 @@ num_ticks.times { |i|
|
|
|
55
59
|
omega = 0.0 if omega.abs < 0.0001
|
|
56
60
|
theta += omega * env.tick
|
|
57
61
|
|
|
58
|
-
if i < 10 or
|
|
62
|
+
if flag or i < 10 or
|
|
59
63
|
(i < 20_000 and i%1000 == 0) or
|
|
60
64
|
(i % 10_000 == 0) or
|
|
61
65
|
i == duration * env.hz - 1
|
|
62
66
|
|
|
63
|
-
puts
|
|
67
|
+
puts Timer.display(ms: i)
|
|
64
68
|
puts format(" Torque: %.1f Nm (%d Nm) Friction: %.1f Nm",
|
|
65
69
|
net_torque, axle_torque, rotating_friction)
|
|
66
70
|
puts format("Radians: %.1f r %.2f r/s %.3f r/s^2", theta, omega, alpha)
|
|
@@ -69,8 +73,11 @@ num_ticks.times { |i|
|
|
|
69
73
|
DrivingPhysics.revs(omega),
|
|
70
74
|
DrivingPhysics.rpm(omega))
|
|
71
75
|
puts
|
|
76
|
+
if flag
|
|
77
|
+
paused += CLI.pause
|
|
78
|
+
flag = false
|
|
79
|
+
end
|
|
72
80
|
end
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
|
|
83
|
+
puts Timer.summary(start, num_ticks, paused)
|
data/demo/gearbox.rb
CHANGED
|
@@ -21,6 +21,9 @@ alpha = 0.0
|
|
|
21
21
|
omega = 0.0
|
|
22
22
|
theta = 0.0
|
|
23
23
|
|
|
24
|
+
start = Timer.now
|
|
25
|
+
paused = 0
|
|
26
|
+
|
|
24
27
|
(duration * env.hz + 1).times { |i|
|
|
25
28
|
# just for info, not used in the simulation
|
|
26
29
|
friction = gearbox.spinner.rotating_friction(omega)
|
|
@@ -38,10 +41,12 @@ theta = 0.0
|
|
|
38
41
|
(i < 100 and i % 10 == 0) or
|
|
39
42
|
(i < 1000 and i % 100 == 0) or
|
|
40
43
|
i % 1000 == 0
|
|
41
|
-
puts
|
|
44
|
+
puts Timer.display(i)
|
|
42
45
|
puts format("RPM %d Torque: %.3f Nm (%d Nm) Friction: %.3f Nm",
|
|
43
46
|
DrivingPhysics.rpm(omega), net_torque, torque, friction)
|
|
44
47
|
puts format("%.1f rad %.1f rad/s %.1f rad/s/s", theta, omega, alpha)
|
|
45
48
|
puts
|
|
46
49
|
end
|
|
47
50
|
}
|
|
51
|
+
|
|
52
|
+
puts Timer.summary(start, num_ticks, paused)
|
data/demo/motor.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require 'driving_physics/motor'
|
|
2
2
|
require 'driving_physics/cli'
|
|
3
|
+
require 'driving_physics/power'
|
|
3
4
|
|
|
4
|
-
# fun idea
|
|
5
|
-
# maintained
|
|
5
|
+
# next fun idea: PID controller
|
|
6
6
|
|
|
7
7
|
include DrivingPhysics
|
|
8
8
|
|
|
@@ -10,18 +10,44 @@ env = Environment.new
|
|
|
10
10
|
motor = Motor.new(env)
|
|
11
11
|
puts env
|
|
12
12
|
puts motor
|
|
13
|
+
puts
|
|
13
14
|
|
|
14
15
|
puts "Rev it up!"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
puts
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
motor.throttle = 1.0
|
|
17
|
+
[:torque, :power].each { |run|
|
|
18
|
+
puts
|
|
19
|
+
puts run.to_s.upcase + ':'
|
|
20
|
+
|
|
21
|
+
800.upto(7000) { |rpm|
|
|
22
|
+
next unless rpm % 200 == 0
|
|
23
|
+
omega = DrivingPhysics.omega(rpm)
|
|
24
|
+
torque = motor.torque(rpm)
|
|
25
|
+
case run
|
|
26
|
+
when :torque
|
|
27
|
+
count = (torque.to_f / 10).round
|
|
28
|
+
char = 'T'
|
|
29
|
+
val = torque.round.to_s.rjust(5, ' ')
|
|
30
|
+
fmt = "%s Nm"
|
|
31
|
+
when :power
|
|
32
|
+
power = DrivingPhysics.power(torque, omega)
|
|
33
|
+
kw = power.to_f / 1000
|
|
34
|
+
count = (kw / 5).round
|
|
35
|
+
char = 'P'
|
|
36
|
+
val = kw.round(1).to_s.rjust(5, ' ')
|
|
37
|
+
fmt = "%s kW"
|
|
38
|
+
else
|
|
39
|
+
raise "unknown"
|
|
40
|
+
end
|
|
41
|
+
puts format("%s RPM: #{fmt}\t%s",
|
|
42
|
+
rpm.to_s.rjust(4, ' '),
|
|
43
|
+
val,
|
|
44
|
+
char * count)
|
|
45
|
+
}
|
|
22
46
|
}
|
|
23
47
|
|
|
24
48
|
puts
|
|
49
|
+
puts "Now, the simulation begins..."
|
|
50
|
+
puts "---"
|
|
25
51
|
puts "* Spin the motor up to #{motor.idle_rpm} RPM with the starter motor."
|
|
26
52
|
puts "* Rev it up with the throttle."
|
|
27
53
|
puts "* Let it die."
|
|
@@ -36,12 +62,16 @@ duration = 60
|
|
|
36
62
|
status = :ignition
|
|
37
63
|
rpm = 0
|
|
38
64
|
|
|
39
|
-
|
|
65
|
+
paused = 0.0
|
|
66
|
+
num_ticks = duration * env.hz + 1
|
|
67
|
+
start = Timer.now
|
|
68
|
+
|
|
69
|
+
num_ticks.times { |i|
|
|
40
70
|
# this is an input torque; alpha is determined after inertia and friction
|
|
41
71
|
torque = case status
|
|
42
72
|
when :ignition
|
|
43
73
|
motor.starter_torque
|
|
44
|
-
when :running
|
|
74
|
+
when :running, :off_throttle, :idling
|
|
45
75
|
motor.torque(rpm)
|
|
46
76
|
else
|
|
47
77
|
0
|
|
@@ -58,32 +88,54 @@ rpm = 0
|
|
|
58
88
|
omega = 0 if omega < 0.00001
|
|
59
89
|
rpm = DrivingPhysics.rpm(omega)
|
|
60
90
|
|
|
91
|
+
power = DrivingPhysics.power(net_torque, omega)
|
|
92
|
+
|
|
61
93
|
if rpm > motor.idle_rpm and status == :ignition
|
|
62
94
|
status = :running
|
|
63
95
|
flag = true
|
|
64
96
|
end
|
|
65
97
|
|
|
66
98
|
if rpm > 7000 and status == :running
|
|
67
|
-
status = :
|
|
99
|
+
status = :off_throttle
|
|
100
|
+
motor.throttle = 0.0
|
|
68
101
|
flag = true
|
|
69
102
|
end
|
|
70
103
|
|
|
104
|
+
if rpm < 1000 and status == :off_throttle
|
|
105
|
+
status = :idling
|
|
106
|
+
motor.throttle = 0.06
|
|
107
|
+
flag = true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if status == :idling
|
|
111
|
+
if rpm < 999
|
|
112
|
+
motor.throttle += (1.0 - motor.throttle) * 0.005
|
|
113
|
+
elsif rpm > 1001
|
|
114
|
+
motor.throttle -= motor.throttle * 0.005
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
71
118
|
if flag or
|
|
72
119
|
(i < 10) or
|
|
73
120
|
(i < 100 and i % 10 == 0) or
|
|
74
121
|
(i < 1000 and i % 100 == 0) or
|
|
75
122
|
(i < 10_000 and i % 500 == 0) or
|
|
76
|
-
i % 5000 == 0
|
|
77
|
-
|
|
78
|
-
puts
|
|
123
|
+
(i % 5000 == 0) or
|
|
124
|
+
(status == :idling and i % 100 == 0)
|
|
125
|
+
puts Timer.display(i)
|
|
126
|
+
puts format("Throttle: %.1f%%", motor.throttle * 100)
|
|
127
|
+
puts format("%d RPM %.1f Nm (%d Nm) %.1f kW Friction: %.1f Nm",
|
|
79
128
|
DrivingPhysics.rpm(omega),
|
|
80
129
|
net_torque,
|
|
81
130
|
torque,
|
|
131
|
+
power / 1000,
|
|
82
132
|
motor.spinner.rotating_friction(omega))
|
|
83
133
|
puts format("%d rad %.1f rad/s %.1f rad/s/s", theta, omega, alpha)
|
|
84
134
|
puts
|
|
85
135
|
|
|
86
|
-
CLI.pause if flag
|
|
136
|
+
paused += CLI.pause if flag
|
|
87
137
|
flag = false
|
|
88
138
|
end
|
|
89
139
|
}
|
|
140
|
+
|
|
141
|
+
puts Timer.summary(start, num_ticks, paused)
|
data/demo/powertrain.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'driving_physics/powertrain'
|
|
2
2
|
require 'driving_physics/imperial'
|
|
3
3
|
require 'driving_physics/cli'
|
|
4
|
+
require 'driving_physics/power'
|
|
4
5
|
|
|
5
6
|
include DrivingPhysics
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ env = Environment.new
|
|
|
8
9
|
motor = Motor.new(env)
|
|
9
10
|
gearbox = Gearbox.new(env)
|
|
10
11
|
powertrain = Powertrain.new(motor, gearbox)
|
|
12
|
+
motor.throttle = 1.0
|
|
11
13
|
puts env
|
|
12
14
|
puts powertrain
|
|
13
15
|
CLI.pause
|
|
@@ -18,23 +20,28 @@ crank_omega = 0.0
|
|
|
18
20
|
axle_alpha = 0.0
|
|
19
21
|
axle_omega = 0.0
|
|
20
22
|
|
|
21
|
-
|
|
22
23
|
# Run through the gears
|
|
23
|
-
|
|
24
24
|
1.upto(6) { |gear|
|
|
25
|
-
|
|
25
|
+
gearbox.gear = gear
|
|
26
26
|
|
|
27
27
|
puts <<EOF
|
|
28
28
|
|
|
29
|
-
# GEAR #{gear} (#{
|
|
29
|
+
# GEAR #{gear} (#{gearbox.ratio})
|
|
30
30
|
#
|
|
31
31
|
EOF
|
|
32
32
|
|
|
33
33
|
800.upto(7000) { |rpm|
|
|
34
|
-
next unless rpm %
|
|
35
|
-
|
|
36
|
-
axle_torque = powertrain.
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
next unless rpm % 200 == 0
|
|
35
|
+
|
|
36
|
+
power, axle_torque, axle_omega = powertrain.output(rpm)
|
|
37
|
+
kw = power / 1000.to_f
|
|
38
|
+
mph = Imperial.mph(Disk.tangential(axle_omega, 0.32))
|
|
39
|
+
ps = Imperial.ps(kw)
|
|
40
|
+
puts format("%s RPM: %s Nm %s kW %s PS\t%s mph",
|
|
41
|
+
rpm.round.to_s.rjust(4, ' '),
|
|
42
|
+
axle_torque.round(1).to_s.rjust(5, ' '),
|
|
43
|
+
kw.round(1).to_s.rjust(5, ' '),
|
|
44
|
+
ps.round.to_s.rjust(4, ' '),
|
|
45
|
+
mph.round.to_s.rjust(3, ' '))
|
|
39
46
|
}
|
|
40
47
|
}
|
data/demo/scalar_force.rb
CHANGED
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
require 'driving_physics/scalar_force'
|
|
2
|
+
require 'driving_physics/environment'
|
|
3
|
+
require 'driving_physics/cli'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
include DrivingPhysics
|
|
6
|
+
|
|
7
|
+
env = Environment.new
|
|
8
|
+
puts env
|
|
4
9
|
|
|
5
|
-
pos = 0 # m
|
|
6
|
-
spd = 0 # m/s
|
|
7
10
|
mass = 1000 # kg
|
|
8
|
-
weight = mass *
|
|
11
|
+
weight = mass * env.g # N
|
|
9
12
|
drive_force = 7000 # N
|
|
10
13
|
duration = 100 # seconds
|
|
11
|
-
tick = 1.0 / DP::HZ
|
|
12
14
|
|
|
13
|
-
(
|
|
14
|
-
|
|
15
|
+
puts format("Force: %d N Mass: %d kg for %d seconds",
|
|
16
|
+
drive_force, mass, duration)
|
|
17
|
+
CLI.pause
|
|
18
|
+
|
|
19
|
+
spd = 0.0 # m/s
|
|
20
|
+
pos = 0.0 # m
|
|
21
|
+
num_ticks = duration * env.hz + 1
|
|
22
|
+
|
|
23
|
+
flag = false
|
|
24
|
+
phase = :accelerate
|
|
25
|
+
paused = 0.0
|
|
26
|
+
start = Timer.now
|
|
27
|
+
|
|
28
|
+
num_ticks.times { |i|
|
|
29
|
+
# TODO: make the resistance force negative
|
|
30
|
+
net_force = drive_force + ScalarForce.all_resistance(spd, nf_mag: weight)
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
spd
|
|
18
|
-
pos
|
|
32
|
+
acc = DrivingPhysics.acc(net_force, mass)
|
|
33
|
+
spd += acc * env.tick
|
|
34
|
+
pos += spd * env.tick
|
|
19
35
|
|
|
20
|
-
if
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
if phase == :accelerate and spd.magnitude > 100
|
|
37
|
+
flag = true
|
|
38
|
+
phase = :coast
|
|
39
|
+
drive_force = 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if flag or (i % 1000 == 0)
|
|
43
|
+
puts format("%d %.3f m/s/s %.2f m/s %.1f m",
|
|
44
|
+
i.to_f / env.hz, acc, spd, pos)
|
|
45
|
+
if flag
|
|
46
|
+
paused += CLI.pause
|
|
47
|
+
flag = false
|
|
48
|
+
end
|
|
25
49
|
end
|
|
26
50
|
}
|
|
51
|
+
|
|
52
|
+
puts Timer.summary(start, num_ticks, paused)
|
data/demo/tire.rb
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
require 'driving_physics/tire'
|
|
2
|
+
require 'driving_physics/imperial'
|
|
3
|
+
require 'driving_physics/cli'
|
|
2
4
|
|
|
3
5
|
include DrivingPhysics
|
|
4
6
|
|
|
5
7
|
env = Environment.new
|
|
6
8
|
tire = Tire.new(env)
|
|
7
|
-
|
|
8
9
|
puts env
|
|
9
10
|
puts tire
|
|
11
|
+
puts
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
|
|
13
|
+
duration = 100 # sec
|
|
14
|
+
axle_torque = 500 # N*m
|
|
15
|
+
supported_mass = 1500 # kg
|
|
16
|
+
|
|
17
|
+
puts "Given:"
|
|
18
|
+
puts "* #{axle_torque} Nm axle torque"
|
|
19
|
+
puts "* #{supported_mass} kg supported mass"
|
|
20
|
+
puts "* 4 tires"
|
|
21
|
+
puts "* #{duration} seconds"
|
|
22
|
+
puts
|
|
14
23
|
|
|
15
|
-
supported_mass = 1000 # kg
|
|
16
24
|
total_mass = supported_mass + 4 * tire.mass
|
|
17
25
|
corner_mass = Rational(total_mass) / 4
|
|
18
26
|
normal_force = corner_mass * env.g
|
|
19
|
-
axle_torque = 1000 # N*m
|
|
20
27
|
|
|
21
|
-
puts
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
puts "Therefore:"
|
|
29
|
+
puts format("* %.1f kg total mass", total_mass)
|
|
30
|
+
puts format("* %.1f kg per corner", corner_mass)
|
|
31
|
+
puts format("* %.1f N normal force", normal_force)
|
|
25
32
|
puts
|
|
26
33
|
|
|
27
34
|
traction = tire.traction(normal_force)
|
|
28
35
|
drive_force = tire.force(axle_torque)
|
|
29
|
-
inertial_loss = tire.inertial_loss(axle_torque, driven_mass: supported_mass)
|
|
30
|
-
net_axle_torque = axle_torque - inertial_loss
|
|
31
|
-
net_drive_force = tire.force(net_axle_torque)
|
|
32
|
-
acc = DrivingPhysics.acc(net_drive_force, supported_mass) # translational
|
|
33
|
-
alpha = acc / tire.radius
|
|
34
|
-
|
|
35
|
-
puts [format("Traction: %.1f N", traction),
|
|
36
|
-
format("Drive force: %.1f N", drive_force),
|
|
37
|
-
format("Inertial loss: %.1f Nm", inertial_loss),
|
|
38
|
-
format("Net Axle Torque: %.1f Nm", net_axle_torque),
|
|
39
|
-
format("Net Drive Force: %.1f N", net_drive_force),
|
|
40
|
-
format("Acceleration: %.1f m/s/s", acc),
|
|
41
|
-
format("Alpha: %.2f r/s/s", alpha),
|
|
42
|
-
].join("\n")
|
|
43
|
-
puts
|
|
44
36
|
|
|
45
|
-
|
|
37
|
+
puts "Tires:"
|
|
38
|
+
puts format("* %.1f N traction", traction)
|
|
39
|
+
puts format("* %.1f N drive force", drive_force)
|
|
46
40
|
|
|
47
|
-
|
|
41
|
+
CLI.pause
|
|
42
|
+
|
|
43
|
+
acc = 0.0 # meters/s/s
|
|
48
44
|
speed = 0.0 # meters/s
|
|
45
|
+
dist = 0.0 # meters
|
|
49
46
|
|
|
50
|
-
|
|
47
|
+
alpha = 0.0 # radians/s/s
|
|
51
48
|
omega = 0.0 # radians/s
|
|
49
|
+
theta = 0.0 # radians
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
start = Timer.now
|
|
52
|
+
paused = 0.0
|
|
53
|
+
num_ticks = duration * env.hz + 1
|
|
55
54
|
|
|
56
55
|
num_ticks.times { |i|
|
|
57
56
|
torque = tire.net_tractable_torque(axle_torque,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
mass: total_mass,
|
|
58
|
+
omega: omega,
|
|
59
|
+
normal_force: normal_force)
|
|
61
60
|
force = tire.force(torque)
|
|
62
61
|
|
|
63
62
|
# translational kinematics
|
|
64
|
-
acc = DrivingPhysics.acc(force,
|
|
63
|
+
acc = DrivingPhysics.acc(force, total_mass)
|
|
65
64
|
speed += acc * env.tick
|
|
66
65
|
dist += speed * env.tick
|
|
66
|
+
mph = Imperial.mph(speed)
|
|
67
67
|
|
|
68
68
|
# rotational kinematics
|
|
69
69
|
alpha = acc / tire.radius
|
|
@@ -72,17 +72,16 @@ num_ticks.times { |i|
|
|
|
72
72
|
|
|
73
73
|
if i < 10 or
|
|
74
74
|
(i < 20_000 and i%1000 == 0) or
|
|
75
|
-
(i % 10_000 == 0)
|
|
76
|
-
i == duration * env.hz - 1
|
|
75
|
+
(i % 10_000 == 0)
|
|
77
76
|
|
|
78
77
|
puts DrivingPhysics.elapsed_display(i)
|
|
79
78
|
puts format(" Tire: %.1f r %.2f r/s %.3f r/s^2", theta, omega, alpha)
|
|
80
|
-
puts format(" Car: %.1f m %.2f m/s %.3f m/s^2",
|
|
79
|
+
puts format(" Car: %.1f m %.2f m/s %.3f m/s^2 (%d mph)",
|
|
80
|
+
dist, speed, acc, mph)
|
|
81
81
|
puts format("Torque: %.1f Nm (%d N) Loss: %.1f%%",
|
|
82
82
|
torque, force, (1.0 - torque / axle_torque) * 100)
|
|
83
83
|
puts
|
|
84
84
|
end
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
|
|
87
|
+
puts Timer.summary(start, num_ticks, paused)
|
data/demo/vector_force.rb
CHANGED
|
@@ -1,26 +1,56 @@
|
|
|
1
1
|
require 'driving_physics/vector_force'
|
|
2
|
+
require 'driving_physics/environment'
|
|
3
|
+
require 'driving_physics/cli'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
include DrivingPhysics
|
|
6
|
+
|
|
7
|
+
env = Environment.new
|
|
8
|
+
puts env
|
|
4
9
|
|
|
5
|
-
p = Vector[0, 0] # m
|
|
6
|
-
v = Vector[0, 0] # m/s
|
|
7
10
|
mass = 1000 # kg
|
|
8
|
-
weight = mass *
|
|
11
|
+
weight = mass * env.g # N
|
|
12
|
+
drive_force = DrivingPhysics.random_unit_vector * 7000 # N
|
|
9
13
|
duration = 100 # seconds
|
|
10
|
-
drive_force = DP.random_unit_vector * 7000 # N
|
|
11
|
-
tick = 1.0 / DP::HZ
|
|
12
14
|
|
|
13
|
-
(
|
|
14
|
-
|
|
15
|
+
puts format("Force: %d N Dir: %s",
|
|
16
|
+
drive_force.magnitude,
|
|
17
|
+
DrivingPhysics.compass_dir(drive_force.normalize))
|
|
18
|
+
puts format("Mass: %d kg for %d seconds", mass, duration)
|
|
19
|
+
CLI.pause
|
|
20
|
+
|
|
21
|
+
acc = Vector[0, 0] # m/s/s
|
|
22
|
+
vel = Vector[0, 0] # m/s
|
|
23
|
+
pos = Vector[0, 0] # m
|
|
24
|
+
|
|
25
|
+
num_ticks = duration * env.hz + 1
|
|
26
|
+
|
|
27
|
+
flag = false
|
|
28
|
+
phase = :accelerate
|
|
29
|
+
paused = 0.0
|
|
30
|
+
start = Timer.now
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
num_ticks.times { |i|
|
|
33
|
+
net_force = drive_force +
|
|
34
|
+
VectorForce.all_resistance(vel, dir: vel, nf_mag: weight)
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
acc = DrivingPhysics.acc(net_force, mass)
|
|
37
|
+
vel += acc * env.tick
|
|
38
|
+
pos += vel * env.tick
|
|
39
|
+
|
|
40
|
+
if phase == :accelerate and vel.magnitude > 100
|
|
41
|
+
flag = true
|
|
42
|
+
phase = :coast
|
|
43
|
+
drive_force = Vector[0, 0]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if flag or (i % 1000 == 0)
|
|
47
|
+
puts format("%d %.3f m/s/s %.2f m/s %.1f m",
|
|
48
|
+
i.to_f / env.hz, acc.magnitude, vel.magnitude, pos.magnitude)
|
|
49
|
+
if flag
|
|
50
|
+
paused = CLI.pause
|
|
51
|
+
flag = false
|
|
52
|
+
end
|
|
25
53
|
end
|
|
26
54
|
}
|
|
55
|
+
|
|
56
|
+
puts Timer.summary(start, num_ticks, paused)
|
data/lib/driving_physics/car.rb
CHANGED
|
@@ -4,20 +4,40 @@ require 'driving_physics/powertrain'
|
|
|
4
4
|
module DrivingPhysics
|
|
5
5
|
class Car
|
|
6
6
|
attr_reader :tire, :powertrain, :env
|
|
7
|
-
attr_accessor :num_tires, :
|
|
7
|
+
attr_accessor :num_tires, :body_mass, :frontal_area, :cd
|
|
8
8
|
|
|
9
9
|
def initialize(tire:, powertrain:)
|
|
10
10
|
@num_tires = 4
|
|
11
11
|
@tire = tire
|
|
12
12
|
@env = @tire.env
|
|
13
13
|
@powertrain = powertrain
|
|
14
|
-
@
|
|
14
|
+
@body_mass = 1000.0
|
|
15
15
|
@frontal_area = DrivingPhysics::FRONTAL_AREA
|
|
16
16
|
@cd = DrivingPhysics::DRAG_COF
|
|
17
17
|
|
|
18
18
|
yield self if block_given?
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def throttle
|
|
22
|
+
@powertrain.motor.throttle
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def throttle=(val)
|
|
26
|
+
@powertrain.motor.throttle = val
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def gear
|
|
30
|
+
@powertrain.gearbox.gear
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def gear=(val)
|
|
34
|
+
@powertrain.gearbox.gear = val
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def top_gear
|
|
38
|
+
@powertrain.gearbox.top_gear
|
|
39
|
+
end
|
|
40
|
+
|
|
21
41
|
# force opposing speed
|
|
22
42
|
def air_force(speed)
|
|
23
43
|
-0.5 * @frontal_area * @cd * @env.air_density * speed ** 2
|
|
@@ -59,7 +79,7 @@ module DrivingPhysics
|
|
|
59
79
|
format("Fr.A: %.2f m^2", @frontal_area),
|
|
60
80
|
format("cD: %.2f", @cd),
|
|
61
81
|
].join(' | '),
|
|
62
|
-
format("
|
|
82
|
+
format("POWERTRAIN:\n%s", @powertrain),
|
|
63
83
|
format("Tires: %s", @tire),
|
|
64
84
|
format("Corner mass: %.1f kg | Normal force: %.1f N",
|
|
65
85
|
self.corner_mass, self.normal_force),
|
|
@@ -79,7 +99,7 @@ module DrivingPhysics
|
|
|
79
99
|
end
|
|
80
100
|
|
|
81
101
|
def total_mass
|
|
82
|
-
@mass + @tire.mass * @num_tires
|
|
102
|
+
@body_mass + @powertrain.mass + @tire.mass * @num_tires
|
|
83
103
|
end
|
|
84
104
|
|
|
85
105
|
def corner_mass
|
data/lib/driving_physics/cli.rb
CHANGED
|
@@ -7,11 +7,45 @@ module DrivingPhysics
|
|
|
7
7
|
$stdin.gets.chomp
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
# press Enter to continue
|
|
10
|
+
# press Enter to continue, ignore input, return elapsed time
|
|
11
11
|
def self.pause(msg = '')
|
|
12
|
+
t = Timer.now
|
|
12
13
|
puts msg unless msg.empty?
|
|
13
14
|
puts ' [ Press Enter ]'
|
|
14
15
|
$stdin.gets
|
|
16
|
+
Timer.since(t)
|
|
17
|
+
end
|
|
18
|
+
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)
|
|
15
49
|
end
|
|
16
50
|
end
|
|
17
51
|
end
|
data/lib/driving_physics/disk.rb
CHANGED
|
@@ -130,8 +130,9 @@ module DrivingPhysics
|
|
|
130
130
|
@normal_force
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
def alpha(torque)
|
|
134
|
-
torque
|
|
133
|
+
def alpha(torque, omega: 0, normal_force: nil)
|
|
134
|
+
(torque - self.rotating_friction(omega, normal_force: normal_force)) /
|
|
135
|
+
self.rotational_inertia
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
def implied_torque(alpha)
|
|
@@ -175,10 +176,10 @@ module DrivingPhysics
|
|
|
175
176
|
# maybe not physically faithful but close enough
|
|
176
177
|
def rotating_friction(omega, normal_force: nil)
|
|
177
178
|
return omega if omega.zero?
|
|
179
|
+
normal_force = self.normal_force if normal_force.nil?
|
|
178
180
|
mag = omega.abs
|
|
179
181
|
sign = omega / mag
|
|
180
|
-
-1 * sign *
|
|
181
|
-
(@base_friction + @omega_friction * mag)
|
|
182
|
+
-1 * sign * normal_force * (@base_friction + mag * @omega_friction)
|
|
182
183
|
end
|
|
183
184
|
end
|
|
184
185
|
end
|
|
@@ -7,6 +7,7 @@ module DrivingPhysics
|
|
|
7
7
|
MPH = (FEET_PER_METER / FEET_PER_MILE) * SECS_PER_HOUR
|
|
8
8
|
CI_PER_LITER = 61.024
|
|
9
9
|
GAL_PER_LITER = 0.264172
|
|
10
|
+
PS_PER_KW = 1.3596216173039
|
|
10
11
|
|
|
11
12
|
def self.feet(meters)
|
|
12
13
|
meters * FEET_PER_METER
|
|
@@ -36,6 +37,11 @@ module DrivingPhysics
|
|
|
36
37
|
DP::kph(mps(mph))
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
# convert kilowatts to horsepower
|
|
41
|
+
def self.ps(kw)
|
|
42
|
+
kw * PS_PER_KW
|
|
43
|
+
end
|
|
44
|
+
|
|
39
45
|
def self.deg_c(deg_f)
|
|
40
46
|
(deg_f - 32).to_f * 5 / 9
|
|
41
47
|
end
|
|
@@ -6,10 +6,11 @@ module DrivingPhysics
|
|
|
6
6
|
class OverRev < RuntimeError; end
|
|
7
7
|
class SanityCheck < RuntimeError; end
|
|
8
8
|
|
|
9
|
-
TORQUES = [ 0,
|
|
9
|
+
TORQUES = [ 0, 70, 130, 200, 250, 320, 330, 320, 260, 0]
|
|
10
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
|
|
11
12
|
|
|
12
|
-
attr_reader :env
|
|
13
|
+
attr_reader :env, :throttle
|
|
13
14
|
attr_accessor :torques, :rpms, :fixed_mass,
|
|
14
15
|
:spinner, :starter_torque, :idle_rpm
|
|
15
16
|
|
|
@@ -29,10 +30,12 @@ module DrivingPhysics
|
|
|
29
30
|
}
|
|
30
31
|
@starter_torque = 500 # Nm
|
|
31
32
|
@idle_rpm = 1000 # RPM
|
|
33
|
+
@throttle = 0.0 # 0.0 - 1.0 (0% - 100%)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def to_s
|
|
35
|
-
ary = ["
|
|
37
|
+
ary = [format("Throttle: %.1f%%", @throttle * 100)]
|
|
38
|
+
ary << "Torque:"
|
|
36
39
|
@rpms.each_with_index { |r, i|
|
|
37
40
|
ary << format("%s Nm %s RPM",
|
|
38
41
|
@torques[i].round(1).to_s.rjust(4, ' '),
|
|
@@ -51,6 +54,13 @@ module DrivingPhysics
|
|
|
51
54
|
@spinner.mass + @fixed_mass
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
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
|
|
62
|
+
end
|
|
63
|
+
|
|
54
64
|
# given torque, determine crank alpha after inertia and friction
|
|
55
65
|
def alpha(torque, omega: 0)
|
|
56
66
|
@spinner.alpha(torque + @spinner.rotating_friction(omega))
|
|
@@ -60,17 +70,6 @@ module DrivingPhysics
|
|
|
60
70
|
@spinner.implied_torque(alpha)
|
|
61
71
|
end
|
|
62
72
|
|
|
63
|
-
# How much torque is required to accelerate spinner up to alpha,
|
|
64
|
-
# overcoming both inertia and friction
|
|
65
|
-
# Presumably we have more input torque available, but this will be
|
|
66
|
-
# used to do more work than just spinning up the motor
|
|
67
|
-
#
|
|
68
|
-
#def resistance_torque(alpha, omega)
|
|
69
|
-
# # reverse sign on inertial_torque as it is not modeled as a resistance
|
|
70
|
-
# -1 * @spinner.inertial_torque(alpha) +
|
|
71
|
-
# @spinner.rotating_friction(omega)
|
|
72
|
-
#end
|
|
73
|
-
|
|
74
73
|
# interpolate based on torque curve points
|
|
75
74
|
def torque(rpm)
|
|
76
75
|
raise(Stall, "RPM #{rpm}") if rpm < @rpms.first
|
|
@@ -78,22 +77,27 @@ module DrivingPhysics
|
|
|
78
77
|
|
|
79
78
|
last_rpm = 99999
|
|
80
79
|
last_tq = -1
|
|
80
|
+
torque = nil
|
|
81
81
|
|
|
82
82
|
# ew; there must be a better way
|
|
83
83
|
@rpms.each_with_index { |r, i|
|
|
84
84
|
tq = @torques[i]
|
|
85
85
|
if last_rpm <= rpm and rpm <= r
|
|
86
86
|
proportion = Rational(rpm - last_rpm) / (r - last_rpm)
|
|
87
|
-
|
|
87
|
+
torque = last_tq + (tq - last_tq) * proportion
|
|
88
|
+
break
|
|
88
89
|
end
|
|
89
90
|
last_rpm = r
|
|
90
91
|
last_tq = tq
|
|
91
92
|
}
|
|
92
|
-
raise(SanityCheck, "RPM #{rpm}")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
raise(SanityCheck, "RPM #{rpm}") if torque.nil?
|
|
94
|
+
|
|
95
|
+
if (@throttle <= 0.05) and (rpm > @idle_rpm * 1.5)
|
|
96
|
+
# engine braking: 20% of torque
|
|
97
|
+
-1 * torque * ENGINE_BRAKING
|
|
98
|
+
else
|
|
99
|
+
torque * @throttle
|
|
100
|
+
end
|
|
97
101
|
end
|
|
98
102
|
end
|
|
99
103
|
end
|
|
@@ -2,6 +2,11 @@ require 'driving_physics/motor'
|
|
|
2
2
|
require 'driving_physics/gearbox'
|
|
3
3
|
|
|
4
4
|
module DrivingPhysics
|
|
5
|
+
# Powertrain right now is pretty simple. It combines the motor with the
|
|
6
|
+
# gearbox. It is focused on operations that require or involve both
|
|
7
|
+
# components. It does not pass through operations to the motor or gearbox.
|
|
8
|
+
# Instead, it provides direct access to each component.
|
|
9
|
+
#
|
|
5
10
|
class Powertrain
|
|
6
11
|
attr_reader :motor, :gearbox
|
|
7
12
|
|
|
@@ -10,12 +15,18 @@ module DrivingPhysics
|
|
|
10
15
|
@gearbox = gearbox
|
|
11
16
|
end
|
|
12
17
|
|
|
18
|
+
def mass
|
|
19
|
+
@motor.mass + @gearbox.mass
|
|
20
|
+
end
|
|
21
|
+
|
|
13
22
|
def to_s
|
|
14
23
|
["\t[MOTOR]", @motor, "\t[GEARBOX]", @gearbox].join("\n")
|
|
15
24
|
end
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
# returns [power, torque, omega]
|
|
27
|
+
def output(rpm)
|
|
28
|
+
t, o = self.axle_torque(rpm), self.axle_omega(rpm)
|
|
29
|
+
[t * o, t, o]
|
|
19
30
|
end
|
|
20
31
|
|
|
21
32
|
def axle_torque(rpm)
|
|
@@ -18,20 +18,23 @@ module DrivingPhysics
|
|
|
18
18
|
# Note: here we only consider speed; we're in a 1D world for now
|
|
19
19
|
#
|
|
20
20
|
|
|
21
|
+
# opposes the direction of speed
|
|
21
22
|
def self.air_resistance(speed,
|
|
22
23
|
frontal_area: FRONTAL_AREA,
|
|
23
24
|
drag_cof: DRAG_COF,
|
|
24
25
|
air_density: AIR_DENSITY)
|
|
25
|
-
0.5 * frontal_area * drag_cof * air_density * speed ** 2
|
|
26
|
+
-1 * 0.5 * frontal_area * drag_cof * air_density * speed ** 2
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
# opposes the direction of speed
|
|
28
30
|
def self.rotational_resistance(speed, rot_cof: ROT_COF)
|
|
29
|
-
speed * rot_cof
|
|
31
|
+
-1 * speed * rot_cof
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
# opposes the direction of speed
|
|
32
35
|
# normal force is not always mass * G, e.g. aero downforce
|
|
33
36
|
def self.rolling_resistance(normal_force, roll_cof: ROLL_COF)
|
|
34
|
-
normal_force * roll_cof
|
|
37
|
+
-1 * normal_force * roll_cof
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
#
|
data/lib/driving_physics/tire.rb
CHANGED
|
@@ -83,7 +83,7 @@ module DrivingPhysics
|
|
|
83
83
|
5.times {
|
|
84
84
|
acc = DrivingPhysics.acc(drive_force - force_loss, driven_mass)
|
|
85
85
|
alpha = acc / @radius
|
|
86
|
-
force_loss = self.
|
|
86
|
+
force_loss = self.implied_torque(alpha) / @radius
|
|
87
87
|
}
|
|
88
88
|
force_loss * @radius
|
|
89
89
|
end
|
data/lib/driving_physics.rb
CHANGED
data/test/scalar_force.rb
CHANGED
|
@@ -6,25 +6,27 @@ include DrivingPhysics
|
|
|
6
6
|
describe ScalarForce do
|
|
7
7
|
# i.e. multiply this number times speed^2 to approximate drag force
|
|
8
8
|
it "calculates a reasonable drag constant" do
|
|
9
|
-
expect(ScalarForce.air_resistance 1).must_be_within_epsilon DRAG
|
|
9
|
+
expect(ScalarForce.air_resistance 1).must_be_within_epsilon(-1 * DRAG)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# ROT_COF's value is from observing that rotational resistance
|
|
13
13
|
# matches air resistance at roughly 30 m/s in street cars
|
|
14
14
|
it "approximates a reasonable rotational resistance constant" do
|
|
15
|
-
expect(30 * ScalarForce.air_resistance(1)).
|
|
15
|
+
expect(30 * ScalarForce.air_resistance(1)).
|
|
16
|
+
must_be_within_epsilon(-1 * ROT_COF)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
it "approximates a positive drag force" do
|
|
19
|
-
expect(ScalarForce.air_resistance 30).must_be_within_epsilon
|
|
20
|
+
expect(ScalarForce.air_resistance 30).must_be_within_epsilon(-383.13)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
it "approximates a positive rotational resistance force" do
|
|
23
|
-
expect(ScalarForce.rotational_resistance 30).
|
|
24
|
+
expect(ScalarForce.rotational_resistance 30).
|
|
25
|
+
must_be_within_epsilon(-383.13)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
it "approximates a positive rolling resistance force" do
|
|
27
29
|
nf = 1000 * G
|
|
28
|
-
expect(ScalarForce.rolling_resistance nf).must_be_within_epsilon
|
|
30
|
+
expect(ScalarForce.rolling_resistance nf).must_be_within_epsilon(-98.0)
|
|
29
31
|
end
|
|
30
32
|
end
|