driving_physics 0.0.0.2 → 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/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
|