driving_physics 0.0.0.2 → 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/README.md +12 -1
- data/Rakefile +11 -4
- data/VERSION +1 -1
- data/demo/car.rb +165 -18
- data/demo/disk.rb +83 -0
- data/demo/gearbox.rb +52 -0
- data/demo/motor.rb +141 -0
- data/demo/powertrain.rb +47 -0
- data/demo/scalar_force.rb +41 -15
- data/demo/tire.rb +80 -162
- data/demo/vector_force.rb +46 -16
- data/lib/driving_physics/car.rb +77 -248
- data/lib/driving_physics/cli.rb +51 -0
- data/lib/driving_physics/disk.rb +185 -0
- data/lib/driving_physics/gearbox.rb +109 -0
- data/lib/driving_physics/imperial.rb +6 -0
- data/lib/driving_physics/motor.rb +103 -0
- data/lib/driving_physics/power.rb +20 -0
- data/lib/driving_physics/powertrain.rb +50 -0
- data/lib/driving_physics/scalar_force.rb +6 -3
- data/lib/driving_physics/tire.rb +90 -258
- data/lib/driving_physics/vector_force.rb +15 -2
- data/lib/driving_physics.rb +2 -0
- data/test/disk.rb +129 -0
- data/test/scalar_force.rb +7 -5
- data/test/tire.rb +144 -88
- data/test/vector_force.rb +7 -1
- metadata +12 -5
- data/demo/wheel.rb +0 -84
- data/lib/driving_physics/wheel.rb +0 -191
- data/test/car.rb +0 -156
- data/test/wheel.rb +0 -177
data/lib/driving_physics/tire.rb
CHANGED
@@ -1,288 +1,120 @@
|
|
1
|
-
|
2
|
-
# It was created early on, before taking a true physics / formulaic
|
3
|
-
# approach. Just treat it as a toy / PoC for now. It does not
|
4
|
-
# currently behave as desired.
|
5
|
-
#
|
1
|
+
require 'driving_physics/disk'
|
6
2
|
|
7
|
-
require 'driving_physics'
|
8
|
-
|
9
|
-
# treat instances of this class as immutable
|
10
|
-
# Tire::Condition has mutable attributes
|
11
|
-
#
|
12
3
|
module DrivingPhysics
|
13
|
-
class Tire
|
14
|
-
class Error < RuntimeError; end
|
15
4
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
5
|
+
# a Tire is a Disk with lighter density and meaningful surface friction
|
6
|
+
|
7
|
+
class Tire < Disk
|
8
|
+
# Note, this is not the density of solid rubber. This density
|
9
|
+
# yields a sensible mass for a wheel / tire combo at common radius
|
10
|
+
# and width, assuming a uniform density
|
11
|
+
# e.g. 25kg at 350mm R x 200mm W
|
12
|
+
#
|
13
|
+
DENSITY = 0.325 # kg / L
|
14
|
+
|
15
|
+
# * the traction force opposes the axle torque / drive force
|
16
|
+
# thus, driving the car forward
|
17
|
+
# * if the drive force exceeds the traction force, slippage occurs
|
18
|
+
# * slippage reduces the available traction force further
|
19
|
+
# * if the drive force is not reduced, the slippage increases
|
20
|
+
# until resistance forces equal the drive force
|
21
|
+
def self.traction(normal_force, cof)
|
22
|
+
normal_force * cof
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_accessor :mu_s, :mu_k, :omega_friction, :base_friction, :roll_cof
|
24
26
|
|
25
|
-
def initialize
|
26
|
-
@
|
27
|
-
@
|
28
|
-
@
|
29
|
-
@
|
30
|
-
@
|
31
|
-
@
|
32
|
-
@
|
27
|
+
def initialize(env)
|
28
|
+
@env = env
|
29
|
+
@radius = 350/1000r # m
|
30
|
+
@width = 200/1000r # m
|
31
|
+
@density = DENSITY
|
32
|
+
@temp = @env.air_temp
|
33
|
+
@mu_s = 11/10r # static friction
|
34
|
+
@mu_k = 7/10r # kinetic friction
|
35
|
+
@base_friction = 5/10_000r
|
36
|
+
@omega_friction = 5/100_000r # scales with speed
|
37
|
+
@roll_cof = DrivingPhysics::ROLL_COF
|
33
38
|
|
34
39
|
yield self if block_given?
|
35
|
-
@condition = Condition.new(tread_mm: @tread_mm, cords_mm: @cords_mm)
|
36
40
|
end
|
37
41
|
|
38
42
|
def to_s
|
39
|
-
[[format("
|
40
|
-
format("
|
41
|
-
format("
|
42
|
-
].join(
|
43
|
-
@condition,
|
43
|
+
[[format("%d mm x %d mm (RxW)", @radius * 1000, @width * 1000),
|
44
|
+
format("%.1f kg %.1f C", self.mass, @temp),
|
45
|
+
format("cF: %.1f / %.1f", @mu_s, @mu_k),
|
46
|
+
].join(" | "),
|
44
47
|
].join("\n")
|
45
48
|
end
|
46
49
|
|
47
|
-
def
|
48
|
-
@
|
50
|
+
def wear!(amount)
|
51
|
+
@radius -= amount
|
49
52
|
end
|
50
53
|
|
51
|
-
|
52
|
-
|
53
|
-
tread_left? ? 1.0 : 0.5 * @condition.cords_mm / @cords_mm
|
54
|
+
def heat!(amount_deg_c)
|
55
|
+
@temp += amount_deg_c
|
54
56
|
end
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
def heat_cycle_factor
|
59
|
-
heat_cycles = [@condition.heat_cycles, @max_heat_cycles].min.to_f
|
60
|
-
heat_pct = heat_cycles / @max_heat_cycles
|
61
|
-
1.0 - 0.2 * heat_pct
|
58
|
+
def traction(nf, static: true)
|
59
|
+
self.class.traction(nf, static ? @mu_s : @mu_k)
|
62
60
|
end
|
63
61
|
|
64
|
-
|
65
|
-
|
62
|
+
# require a normal_force to be be passed in
|
63
|
+
def rotating_friction(omega, normal_force:)
|
64
|
+
super(omega, normal_force: normal_force)
|
66
65
|
end
|
67
66
|
|
68
|
-
|
69
|
-
|
67
|
+
# rolling loss in terms of axle torque
|
68
|
+
def rolling_friction(omega, normal_force:)
|
69
|
+
return omega if omega.zero?
|
70
|
+
mag = omega.abs
|
71
|
+
sign = omega / mag
|
72
|
+
-1 * sign * (normal_force * @roll_cof) * @radius
|
70
73
|
end
|
71
74
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
end
|
87
|
-
if deg_ary.count != grip_ary.count
|
88
|
-
raise(ArgumentError, "arrays don't match")
|
89
|
-
end
|
90
|
-
@deg_c = deg_ary
|
91
|
-
@grip_pct = grip_ary
|
92
|
-
@critical_temp = nil
|
93
|
-
determine_critical_temp!
|
94
|
-
end
|
95
|
-
|
96
|
-
def grip_factor(temp_c)
|
97
|
-
pct = MIN_GRIP
|
98
|
-
@deg_c.each_with_index { |deg, i|
|
99
|
-
pct = @grip_pct[i] if temp_c >= deg
|
100
|
-
break if temp_c < deg
|
101
|
-
}
|
102
|
-
pct
|
103
|
-
end
|
104
|
-
|
105
|
-
def to_s
|
106
|
-
lines = []
|
107
|
-
@deg_c.each_with_index { |deg, i|
|
108
|
-
lines << [deg, @grip_pct[i]].join("\t")
|
109
|
-
}
|
110
|
-
lines.join("\n")
|
111
|
-
end
|
112
|
-
|
113
|
-
private
|
114
|
-
def determine_critical_temp!
|
115
|
-
return @critical_temp if @critical_temp
|
116
|
-
reached_100 = false
|
117
|
-
# go up to 100% grip, then back down to less than 80%
|
118
|
-
@deg_c.each_with_index { |deg, i|
|
119
|
-
next if !reached_100 and @grip_pct[i] < 1.0
|
120
|
-
reached_100 = true if @grip_pct[i] == 1.0
|
121
|
-
if reached_100 and @grip_pct[i] <= 0.8
|
122
|
-
@critical_temp = deg
|
123
|
-
break
|
124
|
-
end
|
125
|
-
}
|
126
|
-
raise(Error, "bad profile, can't find 100% grip") unless reached_100
|
127
|
-
raise(Error, "bad profile, no critical temp") unless @critical_temp
|
128
|
-
end
|
75
|
+
# inertial loss in terms of axle torque when used as a drive wheel
|
76
|
+
def inertial_loss(axle_torque, driven_mass:)
|
77
|
+
drive_force = self.force(axle_torque)
|
78
|
+
force_loss = 0
|
79
|
+
# The force loss depends on the acceleration, but the acceleration
|
80
|
+
# depends on the force loss. Converge the value via 5 round trips.
|
81
|
+
# This is a rough way to compute an integral and should be accurate
|
82
|
+
# to 8+ digits.
|
83
|
+
5.times {
|
84
|
+
acc = DrivingPhysics.acc(drive_force - force_loss, driven_mass)
|
85
|
+
alpha = acc / @radius
|
86
|
+
force_loss = self.implied_torque(alpha) / @radius
|
87
|
+
}
|
88
|
+
force_loss * @radius
|
129
89
|
end
|
130
90
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
attr_accessor :tread_mm,
|
137
|
-
:cords_mm,
|
138
|
-
:temp_c,
|
139
|
-
:heat_cycles,
|
140
|
-
:debug_temp,
|
141
|
-
:debug_wear
|
142
|
-
|
143
|
-
def initialize(temp_c: AIR_TEMP, tread_mm:, cords_mm:)
|
144
|
-
@tread_mm = tread_mm.to_f
|
145
|
-
@cords_mm = cords_mm.to_f
|
146
|
-
@temp_c = temp_c.to_f
|
147
|
-
@heat_cycles = 0
|
148
|
-
@hottest_temp = @temp_c
|
149
|
-
@debug_temp = false
|
150
|
-
@debug_wear = false
|
151
|
-
end
|
152
|
-
|
153
|
-
def to_s
|
154
|
-
[format("Temp: %.1f C", @temp_c),
|
155
|
-
format("Tread: %.2f (%.1f) mm", @tread_mm, @cords_mm),
|
156
|
-
format("Cycles: %d", @heat_cycles),
|
157
|
-
].join(' | ')
|
158
|
-
end
|
159
|
-
|
160
|
-
def temp_tick(ambient_temp:, g:, slide_speed:,
|
161
|
-
mass:, tire_mass:, critical_temp:)
|
162
|
-
# env:
|
163
|
-
# * mass (kg) (e.g. 1000 kg)
|
164
|
-
# * tire_mass (kg) (e.g. 10 kg)
|
165
|
-
# * critical temp (c) (e.g. 100c)
|
166
|
-
# * g (e.g. 1.0 g)
|
167
|
-
# * slide_speed (m/s) (typically 0.1, up to 1 or 10 or 50)
|
168
|
-
# * ambient_temp (c) (e.g. 30c)
|
169
|
-
|
170
|
-
# g gives a target temp between 25 and 100
|
171
|
-
# at 0g, tire tends to ambient temp
|
172
|
-
# at 1g, tire tends to 100 c
|
173
|
-
# that 100c upper target also gets adjusted due to ambient temps
|
174
|
-
|
175
|
-
target_hot = critical_temp + 5
|
176
|
-
ambient_diff = AIR_TEMP - ambient_temp
|
177
|
-
target_hot -= (ambient_diff / 2)
|
178
|
-
puts "target_hot=#{target_hot}" if @debug_temp
|
179
|
-
|
180
|
-
if slide_speed <= 0.1
|
181
|
-
target_g_temp = ambient_temp + (target_hot - ambient_temp) * g
|
182
|
-
else
|
183
|
-
target_g_temp = target_hot
|
184
|
-
end
|
185
|
-
puts "target_g_temp=#{target_g_temp}" if @debug_temp
|
91
|
+
def net_torque(axle_torque, mass:, omega:, normal_force:)
|
92
|
+
# friction forces oppose omega
|
93
|
+
net = axle_torque +
|
94
|
+
self.rolling_friction(omega, normal_force: normal_force) +
|
95
|
+
self.rotating_friction(omega, normal_force: normal_force)
|
186
96
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
# temp_tick is presumed to be +1.0 or -1.0 (100th of a degree)
|
193
|
-
# more mass biases towards heat
|
194
|
-
# more tire mass biases for smaller tick
|
195
|
-
|
196
|
-
tick = @temp_c < target_slide_temp ? 1.0 : -1.0
|
197
|
-
tick += slide_speed / 10
|
198
|
-
puts "base tick: #{tick}" if @debug_temp
|
199
|
-
|
200
|
-
mass_factor = (mass - 1000).to_f / 1000
|
201
|
-
if mass_factor < 0
|
202
|
-
# lighter car cools quicker; heats slower
|
203
|
-
tick += mass_factor
|
204
|
-
else
|
205
|
-
# heavier car cools slower, heats quicker
|
206
|
-
tick += mass_factor / 10
|
207
|
-
end
|
208
|
-
puts "mass tick: #{tick}" if @debug_temp
|
209
|
-
|
210
|
-
tire_mass_factor = (tire_mass - 10).to_f / 10
|
211
|
-
if tire_mass_factor < 0
|
212
|
-
# lighter tire has bigger tick
|
213
|
-
tick -= tire_mass_factor
|
214
|
-
else
|
215
|
-
# heavier tire has smaller tick
|
216
|
-
tire_mass_factor = (tire_mass - 10).to_f / 100
|
217
|
-
tick -= tire_mass_factor
|
218
|
-
end
|
219
|
-
puts "tire mass tick: #{tick}" if @debug_temp
|
220
|
-
puts if @debug_temp
|
221
|
-
|
222
|
-
tick
|
223
|
-
end
|
224
|
-
|
225
|
-
def wear_tick(g:, slide_speed:, mass:, critical_temp:)
|
226
|
-
# cold tires wear less
|
227
|
-
tick = [0, @temp_c.to_f / critical_temp].max
|
228
|
-
puts "wear tick: #{tick}" if @debug_wear
|
229
|
-
|
230
|
-
# lower gs reduce wear in the absence of sliding
|
231
|
-
tick *= g if slide_speed <= 0.1
|
232
|
-
puts "g wear tick: #{tick}" if @debug_wear
|
233
|
-
|
234
|
-
# slide wear
|
235
|
-
tick += slide_speed
|
236
|
-
puts "slide wear tick: #{tick}" if @debug_wear
|
237
|
-
puts if @debug_wear
|
238
|
-
tick
|
239
|
-
end
|
240
|
-
|
241
|
-
|
242
|
-
def tick!(ambient_temp:, g:, slide_speed:,
|
243
|
-
mass:, tire_mass:, critical_temp:)
|
244
|
-
|
245
|
-
# heat cycle:
|
246
|
-
# when the tire goes above the critical temp and then
|
247
|
-
# cools significantly below the critical temp
|
248
|
-
# track @hottest_temp
|
249
|
-
|
250
|
-
@temp_c += temp_tick(ambient_temp: ambient_temp,
|
251
|
-
g: g,
|
252
|
-
slide_speed: slide_speed,
|
253
|
-
mass: mass,
|
254
|
-
tire_mass: tire_mass,
|
255
|
-
critical_temp: critical_temp) / 100
|
256
|
-
@hottest_temp = @temp_c if @temp_c > @hottest_temp
|
257
|
-
if @hottest_temp > critical_temp and @temp_c < critical_temp * 0.8
|
258
|
-
@heat_cycles += 1
|
259
|
-
@hottest_temp = @temp_c
|
260
|
-
end
|
261
|
-
|
262
|
-
# a tire should last for 30 minutes at 1g sustained
|
263
|
-
# with minimal sliding
|
264
|
-
# 100 ticks / sec
|
265
|
-
# 6000 ticks / min
|
266
|
-
# 180_000 ticks / 30 min
|
267
|
-
# 10mm = 180_000 ticks
|
268
|
-
# wear_tick is nominally 1 / 18_000 mm
|
97
|
+
# inertial loss has interdependencies; calculate last
|
98
|
+
# it opposes net torque, not omega
|
99
|
+
sign = net / net.abs
|
100
|
+
net - sign * self.inertial_loss(net, driven_mass: mass)
|
101
|
+
end
|
269
102
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
103
|
+
def net_tractable_torque(axle_torque,
|
104
|
+
mass:, omega:, normal_force:, static: true)
|
105
|
+
net = self.net_torque(axle_torque,
|
106
|
+
mass: mass,
|
107
|
+
omega: omega,
|
108
|
+
normal_force: normal_force)
|
109
|
+
tt = self.tractable_torque(normal_force, static: static)
|
110
|
+
net > tt ? tt : net
|
111
|
+
end
|
274
112
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
@cords_mm -= wt * 2 / 18000
|
281
|
-
if @cords_mm <= 0
|
282
|
-
raise(Destroyed, "no more cords")
|
283
|
-
end
|
284
|
-
end
|
285
|
-
end
|
113
|
+
# this doesn't take inertial losses or internal frictional losses
|
114
|
+
# into account. input torque required to saturate traction will be
|
115
|
+
# higher than what this method returns
|
116
|
+
def tractable_torque(nf, static: true)
|
117
|
+
traction(nf, static: static) * @radius
|
286
118
|
end
|
287
119
|
end
|
288
120
|
end
|
@@ -84,12 +84,25 @@ module DrivingPhysics
|
|
84
84
|
frontal_area: FRONTAL_AREA,
|
85
85
|
drag_cof: DRAG_COF,
|
86
86
|
air_density: AIR_DENSITY)
|
87
|
+
return velocity if velocity.zero?
|
87
88
|
-1 * 0.5 * frontal_area * drag_cof * air_density *
|
88
89
|
velocity * velocity.magnitude
|
89
90
|
end
|
90
91
|
|
91
|
-
|
92
|
-
|
92
|
+
# return a force opposing velocity, representing friction / hysteresis
|
93
|
+
def self.rotational_resistance(velocity,
|
94
|
+
rot_const: ROT_CONST,
|
95
|
+
rot_cof: ROT_COF)
|
96
|
+
return velocity if velocity.zero?
|
97
|
+
-1 * velocity * rot_cof + -1 * velocity.normalize * rot_const
|
98
|
+
end
|
99
|
+
|
100
|
+
# return a torque opposing omega, representing friction / hysteresis
|
101
|
+
def self.omega_resistance(omega,
|
102
|
+
rot_const: ROT_TQ_CONST,
|
103
|
+
rot_cof: ROT_TQ_COF)
|
104
|
+
return 0 if omega == 0.0
|
105
|
+
omega * ROT_TQ_COF + ROT_TQ_CONST
|
93
106
|
end
|
94
107
|
|
95
108
|
# dir is drive_force vector or velocity vector; will be normalized
|
data/lib/driving_physics.rb
CHANGED
@@ -26,6 +26,7 @@ module DrivingPhysics
|
|
26
26
|
DRAG_COF = 0.3 # based roughly on 2000s-era Chevrolet Corvette
|
27
27
|
DRAG = 0.4257 # air_resistance at 1 m/s given above numbers
|
28
28
|
ROT_COF = 12.771 # if rotating resistance matches air resistance at 30 m/s
|
29
|
+
ROT_CONST = 0.05 # N opposing drive force / torque
|
29
30
|
ROLL_COF = 0.01 # roughly: street tires on concrete
|
30
31
|
|
31
32
|
#
|
@@ -35,6 +36,7 @@ module DrivingPhysics
|
|
35
36
|
MINS_PER_HOUR = 60
|
36
37
|
SECS_PER_HOUR = SECS_PER_MIN * MINS_PER_HOUR
|
37
38
|
|
39
|
+
# HH::MM::SS.mmm
|
38
40
|
def self.elapsed_display(elapsed_ms)
|
39
41
|
elapsed_s, ms = elapsed_ms.divmod 1000
|
40
42
|
|
data/test/disk.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'driving_physics/disk'
|
3
|
+
|
4
|
+
D = DrivingPhysics::Disk
|
5
|
+
|
6
|
+
describe D do
|
7
|
+
describe "Disk.volume" do
|
8
|
+
it "calculates the volume (m^3) of disk given radius and width" do
|
9
|
+
cubic_m = D.volume(1.0, 1.0)
|
10
|
+
expect(cubic_m).must_equal Math::PI
|
11
|
+
|
12
|
+
cubic_m = D.volume(0.35, 0.2)
|
13
|
+
expect(cubic_m).must_be_within_epsilon 0.076969
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "Disk.volume_l" do
|
18
|
+
it "calculates the volume (L) of a disk given radius and width" do
|
19
|
+
liters = D.volume_l(1.0, 1.0)
|
20
|
+
expect(liters).must_equal Math::PI * 1000
|
21
|
+
|
22
|
+
liters = D.volume_l(0.35, 0.2)
|
23
|
+
expect(liters).must_be_within_epsilon 76.96902
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "Disk.density" do
|
28
|
+
it "calculates the density (kg/L) given mass and volume" do
|
29
|
+
expect(D.density(25.0, 25.0)).must_equal 1.0
|
30
|
+
expect(D.density(50.0, 25.0)).must_equal 2.0
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "Disk.mass" do
|
35
|
+
it "calculates the mass (kg) of a disk given radius, width, and density" do
|
36
|
+
skip
|
37
|
+
expect(D.mass(0.35, 0.2, D::DENSITY)).must_be_within_epsilon 25.015
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "Disk.rotational_inertia" do
|
42
|
+
it "calculates rotational inertia for a disk given radius and mass" do
|
43
|
+
expect(D.rotational_inertia(0.35, 25.0)).must_be_within_epsilon 1.53125
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "Disk.alpha" do
|
48
|
+
it "calculates angular acceleration from torque and inertia" do
|
49
|
+
scalar_torque = 1000
|
50
|
+
inertia = D.rotational_inertia(0.35, 25.0)
|
51
|
+
expect(D.alpha scalar_torque, inertia).must_be_within_epsilon 653.061
|
52
|
+
|
53
|
+
vector_torque = Vector[0, 0, 1000]
|
54
|
+
vector_alpha = D.alpha vector_torque, inertia
|
55
|
+
expect(vector_alpha).must_be_instance_of Vector
|
56
|
+
expect(vector_alpha.size).must_equal 3
|
57
|
+
expect(vector_alpha[2]).must_be_within_epsilon 653.06
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "Disk.torque_vector" do
|
62
|
+
it "calculates a torque in the 3rd dimension given 2D force and radius" do
|
63
|
+
force = Vector[1000, 0]
|
64
|
+
radius = Vector[0, 5]
|
65
|
+
torque = D.torque_vector(force, radius)
|
66
|
+
expect(torque).must_be_instance_of Vector
|
67
|
+
expect(torque.size).must_equal 3
|
68
|
+
expect(torque[2]).must_be_within_epsilon 5000.0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "Disk.force_vector" do
|
73
|
+
it "calculates a (3D) force given 3D torque and 2D radius" do
|
74
|
+
# let's invert the Disk.torque_vector case from above:
|
75
|
+
torque = Vector[0, 0, 5000]
|
76
|
+
radius = Vector[0, 5]
|
77
|
+
force = D.force_vector(torque, radius)
|
78
|
+
expect(force).must_be_instance_of Vector
|
79
|
+
expect(force.size).must_equal 3
|
80
|
+
expect(force[0]).must_be_within_epsilon 1000.0
|
81
|
+
|
82
|
+
# now let's rotate the radius into the x-dimension
|
83
|
+
# right hand rule, positive torque means thumb into screen, clockwise
|
84
|
+
# negative-x radius means positive-y force
|
85
|
+
torque = Vector[0, 0, 500]
|
86
|
+
radius = Vector[-5, 0]
|
87
|
+
force = D.force_vector(torque, radius)
|
88
|
+
expect(force).must_be_instance_of Vector
|
89
|
+
expect(force.size).must_equal 3
|
90
|
+
expect(force[1]).must_be_within_epsilon 100.0
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "instance methods" do
|
95
|
+
before do
|
96
|
+
@env = DrivingPhysics::Environment.new
|
97
|
+
@disk = D.new(@env)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "initializes" do
|
101
|
+
skip
|
102
|
+
expect(@disk).must_be_instance_of D
|
103
|
+
expect(@disk.density).must_equal D::DENSITY # sanity check
|
104
|
+
expect(@disk.mass).must_be_within_epsilon 25.01
|
105
|
+
|
106
|
+
with_mass = D.new(@env) { |w|
|
107
|
+
w.mass = 99.01
|
108
|
+
}
|
109
|
+
expect(with_mass.mass).must_equal 99.01
|
110
|
+
expect(with_mass.density).wont_equal D::DENSITY
|
111
|
+
end
|
112
|
+
|
113
|
+
it "has a string representation" do
|
114
|
+
str = @disk.to_s
|
115
|
+
expect(str).must_be_instance_of String
|
116
|
+
expect(str.length).must_be(:>, 5)
|
117
|
+
end
|
118
|
+
|
119
|
+
it "has volume" do
|
120
|
+
expect(@disk.volume).must_be_within_epsilon 0.07697
|
121
|
+
expect(@disk.volume_l).must_be_within_epsilon 76.96902
|
122
|
+
end
|
123
|
+
|
124
|
+
it "has inertia" do
|
125
|
+
skip
|
126
|
+
expect(@disk.rotational_inertia).must_be_within_epsilon 1.5321
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
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
|