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