driving_physics 0.0.0.2 → 0.0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +11 -4
- data/VERSION +1 -1
- metadata +1 -7
- data/demo/car.rb +0 -28
- data/demo/tire.rb +0 -169
- data/lib/driving_physics/car.rb +0 -294
- data/lib/driving_physics/tire.rb +0 -288
- data/test/car.rb +0 -156
- data/test/tire.rb +0 -125
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec6ff08e7731aa8bcb419efee9ab1a96ef7642f3426c7b8a501dae764692d777
|
4
|
+
data.tar.gz: ae217c0e452a764392e5a4df9a0b3280369e21b92d6018ef8d8753c0a6f5f594
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b8844bf3f8c45aae742bc27cbfe8307b7f61f21a600d1eac7f34248aa9a980f9942f6e4ce51ec2b58834978e4ba1a06aa0153ba4323d4e3448b1f038c39495e2
|
7
|
+
data.tar.gz: b41f09936f696ef4afe82f3be701409abddd6b92add4f39d5d1d42a1a412ce4204b08a411818c339bba6820858a0aae3d5324cdbabff76e2b2256e9228ae1120
|
data/Rakefile
CHANGED
@@ -32,10 +32,13 @@ rescue LoadError
|
|
32
32
|
end
|
33
33
|
|
34
34
|
begin
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
# need to stop looking in old/ and also the scoring seems wack
|
36
|
+
if false
|
37
|
+
require 'flay_task'
|
38
|
+
FlayTask.new do |t|
|
39
|
+
t.dirs = ['lib']
|
40
|
+
t.verbose = true
|
41
|
+
end
|
39
42
|
end
|
40
43
|
rescue LoadError
|
41
44
|
warn 'flay_task unavailable'
|
@@ -49,6 +52,10 @@ rescue LoadError
|
|
49
52
|
warn "roodi_task unavailable"
|
50
53
|
end
|
51
54
|
|
55
|
+
#
|
56
|
+
# GEM BUILD / PUBLISH
|
57
|
+
#
|
58
|
+
|
52
59
|
begin
|
53
60
|
require 'buildar'
|
54
61
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.0.
|
1
|
+
0.0.0.3
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: driving_physics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.0.
|
4
|
+
version: 0.0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rick Hull
|
@@ -19,24 +19,18 @@ files:
|
|
19
19
|
- README.md
|
20
20
|
- Rakefile
|
21
21
|
- VERSION
|
22
|
-
- demo/car.rb
|
23
22
|
- demo/scalar_force.rb
|
24
|
-
- demo/tire.rb
|
25
23
|
- demo/vector_force.rb
|
26
24
|
- demo/wheel.rb
|
27
25
|
- driving_physics.gemspec
|
28
26
|
- lib/driving_physics.rb
|
29
|
-
- lib/driving_physics/car.rb
|
30
27
|
- lib/driving_physics/environment.rb
|
31
28
|
- lib/driving_physics/imperial.rb
|
32
29
|
- lib/driving_physics/scalar_force.rb
|
33
|
-
- lib/driving_physics/tire.rb
|
34
30
|
- lib/driving_physics/vector_force.rb
|
35
31
|
- lib/driving_physics/wheel.rb
|
36
|
-
- test/car.rb
|
37
32
|
- test/driving_physics.rb
|
38
33
|
- test/scalar_force.rb
|
39
|
-
- test/tire.rb
|
40
34
|
- test/vector_force.rb
|
41
35
|
- test/wheel.rb
|
42
36
|
homepage: https://github.com/rickhull/driving_physics
|
data/demo/car.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
require 'driving_physics/car'
|
2
|
-
|
3
|
-
DP = DrivingPhysics
|
4
|
-
|
5
|
-
env = DP::Environment.new
|
6
|
-
car = DP::Car.new(env)
|
7
|
-
car.add_fuel 10
|
8
|
-
duration = 120 # seconds
|
9
|
-
|
10
|
-
puts env
|
11
|
-
puts
|
12
|
-
puts car
|
13
|
-
|
14
|
-
car.controls.drive_pedal = 1.0
|
15
|
-
|
16
|
-
(duration * env.hz).times { |i|
|
17
|
-
car.tick!
|
18
|
-
if i % env.hz == 0
|
19
|
-
if car.sum_forces.magnitude < 1
|
20
|
-
car.controls.drive_pedal = 0.0
|
21
|
-
car.controls.brake_pedal = 1.0
|
22
|
-
end
|
23
|
-
puts
|
24
|
-
puts "[t = #{i / env.hz}]"
|
25
|
-
puts car
|
26
|
-
gets if i % (env.hz * 10) == 0
|
27
|
-
end
|
28
|
-
}
|
data/demo/tire.rb
DELETED
@@ -1,169 +0,0 @@
|
|
1
|
-
require 'driving_physics/tire'
|
2
|
-
|
3
|
-
include DrivingPhysics
|
4
|
-
|
5
|
-
t = Tire.new
|
6
|
-
t.condition.debug_temp = false
|
7
|
-
|
8
|
-
# drive time
|
9
|
-
mins = 45
|
10
|
-
|
11
|
-
drive_map = {
|
12
|
-
acc: {
|
13
|
-
label: "ACCELERATING",
|
14
|
-
g: 0.5,
|
15
|
-
fuel: 0.0002, # kg consumed / tick
|
16
|
-
small_slide: 1, # m/s of wheelspin
|
17
|
-
big_slide: 10,
|
18
|
-
seconds: 1,
|
19
|
-
next_sector: :brake,
|
20
|
-
},
|
21
|
-
brake: {
|
22
|
-
label: "BRAKING",
|
23
|
-
g: 1.0,
|
24
|
-
fuel: 0.00001,
|
25
|
-
small_slide: 0.5,
|
26
|
-
big_slide: 2,
|
27
|
-
seconds: 1,
|
28
|
-
next_sector: :corner,
|
29
|
-
},
|
30
|
-
corner: {
|
31
|
-
label: "CORNERING",
|
32
|
-
g: 0.8,
|
33
|
-
fuel: 0.0001,
|
34
|
-
small_slide: 0.5,
|
35
|
-
big_slide: 5,
|
36
|
-
seconds: 2,
|
37
|
-
next_sector: :acc,
|
38
|
-
},
|
39
|
-
}
|
40
|
-
|
41
|
-
slide_speed = 0
|
42
|
-
sliding = false
|
43
|
-
mass = 900.0
|
44
|
-
ambient_temp = 25
|
45
|
-
critical_temp = 100
|
46
|
-
|
47
|
-
drive_time = 0 # how many ticks elapsed in the current sector
|
48
|
-
sector_map = drive_map[:acc]
|
49
|
-
puts "ACCELERATING"
|
50
|
-
puts "---"
|
51
|
-
|
52
|
-
cooldown = false
|
53
|
-
pushing = false
|
54
|
-
|
55
|
-
# 100 ticks / sec
|
56
|
-
(mins * 60 * 100).times { |i|
|
57
|
-
drive_time += 1
|
58
|
-
|
59
|
-
dynamic_g = sector_map[:g]
|
60
|
-
|
61
|
-
if t.condition.temp_c <= 80 and cooldown
|
62
|
-
puts "ENDING COOLDOWN"
|
63
|
-
cooldown = false
|
64
|
-
elsif t.condition.temp_c >= 110 and Random.rand(100) >= 90
|
65
|
-
puts "COOLING DOWN"
|
66
|
-
cooldown = true
|
67
|
-
end
|
68
|
-
dynamic_g -= 0.2 if cooldown
|
69
|
-
|
70
|
-
if pushing
|
71
|
-
# stop pushing at very high temp
|
72
|
-
# 1/1000 chance to stop pushing
|
73
|
-
if cooldown
|
74
|
-
puts "ENDING PUSH BECAUSE COOLDOWN"
|
75
|
-
pushing = false
|
76
|
-
else
|
77
|
-
if t.condition.temp_c >= critical_temp and Random.rand(1000) >= 999
|
78
|
-
puts "ENDING PUSH"
|
79
|
-
pushing = false
|
80
|
-
else
|
81
|
-
dynamic_g += 0.2
|
82
|
-
end
|
83
|
-
end
|
84
|
-
else
|
85
|
-
if !cooldown and
|
86
|
-
t.condition.temp_c <= critical_temp and
|
87
|
-
Random.rand(1000) >= 999
|
88
|
-
|
89
|
-
puts "PUSHING!"
|
90
|
-
pushing = true
|
91
|
-
dynamic_g += 0.2
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
if sliding
|
96
|
-
# 5% chance to end the slide
|
97
|
-
if Random.rand(100) >= 95
|
98
|
-
puts " -= CAUGHT THE SLIDE! =-"
|
99
|
-
sliding = false
|
100
|
-
slide_speed = 0
|
101
|
-
end
|
102
|
-
else
|
103
|
-
# 1% chance to start a small slide
|
104
|
-
# 0.1% chance to start a big slide
|
105
|
-
if Random.rand(100) >= 99
|
106
|
-
puts " -= SMALL SLIDE! =-"
|
107
|
-
sliding = true
|
108
|
-
slide_speed = sector_map[:small_slide]
|
109
|
-
elsif Random.rand(1000) >= 999
|
110
|
-
puts " -= BIG SLIDE! =-"
|
111
|
-
sliding = true
|
112
|
-
slide_speed = sector_map[:big_slide]
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
# fuel consumption
|
117
|
-
# 5L of fuel should last 5 minutes
|
118
|
-
# ~3.5 kg of fuel consumption
|
119
|
-
# 1.2e-4 kg / tick
|
120
|
-
mass -= sector_map[:fuel]
|
121
|
-
|
122
|
-
begin
|
123
|
-
t.condition.tick!(ambient_temp: ambient_temp, g: dynamic_g,
|
124
|
-
slide_speed: slide_speed,
|
125
|
-
mass: mass, tire_mass: 12, critical_temp: critical_temp)
|
126
|
-
|
127
|
-
if i % 10 == 0
|
128
|
-
condition = if pushing
|
129
|
-
"Pushing"
|
130
|
-
elsif cooldown
|
131
|
-
"Cooldown"
|
132
|
-
else
|
133
|
-
"Normal"
|
134
|
-
end
|
135
|
-
|
136
|
-
puts [sector_map[:label].ljust(12, ' '),
|
137
|
-
'%.2f' % dynamic_g,
|
138
|
-
'%.1f' % slide_speed,
|
139
|
-
'%.3f' % t.condition.temp_c,
|
140
|
-
].join(' ')
|
141
|
-
end
|
142
|
-
if i % 600 == 0
|
143
|
-
puts
|
144
|
-
puts "Condition: #{condition}"
|
145
|
-
puts "Mass: #{'%.2f' % mass} kg"
|
146
|
-
if t.condition.tread_mm > 0
|
147
|
-
puts "Tread remaining: #{'%.3f' % t.condition.tread_mm}"
|
148
|
-
else
|
149
|
-
puts "Cords remaining: #{'%.3f' % t.condition.cords_mm}"
|
150
|
-
end
|
151
|
-
puts "Heat cycles: #{t.condition.heat_cycles}"
|
152
|
-
puts DrivingPhysics.elapsed_display(i * 10)
|
153
|
-
puts "[Enter] to continue"
|
154
|
-
gets
|
155
|
-
end
|
156
|
-
rescue Tire::Condition::Error => e
|
157
|
-
puts "FATAL:"
|
158
|
-
puts [e.class, e.message].join(': ')
|
159
|
-
break
|
160
|
-
end
|
161
|
-
|
162
|
-
if drive_time > sector_map[:seconds] * 100
|
163
|
-
sector_map = drive_map[sector_map[:next_sector]]
|
164
|
-
drive_time = 0
|
165
|
-
puts
|
166
|
-
puts sector_map[:label]
|
167
|
-
puts '---'
|
168
|
-
end
|
169
|
-
}
|
data/lib/driving_physics/car.rb
DELETED
@@ -1,294 +0,0 @@
|
|
1
|
-
require 'driving_physics/environment'
|
2
|
-
require 'driving_physics/vector_force'
|
3
|
-
require 'driving_physics/tire'
|
4
|
-
|
5
|
-
module DrivingPhysics
|
6
|
-
# treat instances of this class as immutable
|
7
|
-
class Car
|
8
|
-
attr_accessor :mass, :min_turn_radius,
|
9
|
-
:max_drive_force, :max_brake_clamp, :max_brake_force,
|
10
|
-
:fuel_capacity, :brake_pad_depth, :driver_mass,
|
11
|
-
:frontal_area, :cd, :fuel_consumption,
|
12
|
-
:tires, :controls, :condition
|
13
|
-
|
14
|
-
def initialize(environment)
|
15
|
-
@environment = environment
|
16
|
-
@mass = 1000 # kg, without fuel or driver
|
17
|
-
@min_turn_radius = 10 # meters
|
18
|
-
@max_drive_force = 7000 # N - 1000kg car at 0.7g acceleration
|
19
|
-
@max_brake_clamp = 100 # N
|
20
|
-
@max_brake_force = 40_000 # N - 2000kg car at 2g braking
|
21
|
-
@fuel_capacity = 40 # L
|
22
|
-
@brake_pad_depth = 10 # mm
|
23
|
-
@driver_mass = 75 # kg
|
24
|
-
@fuel_consumption = 0.02 # L/s at full throttle
|
25
|
-
|
26
|
-
@frontal_area = DrivingPhysics::FRONTAL_AREA # m^2
|
27
|
-
@cd = DrivingPhysics::DRAG_COF
|
28
|
-
|
29
|
-
@tires = Tire.new
|
30
|
-
@controls = Controls.new
|
31
|
-
@condition = Condition.new(brake_temp: @environment.air_temp,
|
32
|
-
brake_pad_depth: @brake_pad_depth)
|
33
|
-
|
34
|
-
# consider downforce
|
35
|
-
# probably area * angle
|
36
|
-
# goes up with square of velocity
|
37
|
-
|
38
|
-
yield self if block_given?
|
39
|
-
end
|
40
|
-
|
41
|
-
def to_s
|
42
|
-
[[format("Mass: %.1f kg", total_mass),
|
43
|
-
format("Power: %.1f kN", @max_drive_force.to_f / 1000),
|
44
|
-
format("Brakes: %.1f kN", @max_brake_force.to_f / 1000),
|
45
|
-
format("Fr.A: %.2f m^2", @frontal_area),
|
46
|
-
format("cD: %.2f", @cd),
|
47
|
-
].join(' | '),
|
48
|
-
[format("Op: %d N", drive_force - brake_force),
|
49
|
-
format("Drive: %d N", drive_force),
|
50
|
-
format("Brake: %d N", brake_force),
|
51
|
-
].join(' | '),
|
52
|
-
[format("Net: %.1f N", sum_forces.magnitude),
|
53
|
-
format("Air: %.1f N", air_resistance.magnitude),
|
54
|
-
format("Rot: %.1f N", rotational_resistance.magnitude),
|
55
|
-
format("Roll: %.1f N", rolling_resistance.magnitude),
|
56
|
-
].join(' | '),
|
57
|
-
@controls, @condition, @tires,
|
58
|
-
].join("\n")
|
59
|
-
end
|
60
|
-
|
61
|
-
def tick!
|
62
|
-
@condition.tick!(force: sum_forces,
|
63
|
-
mass: total_mass,
|
64
|
-
tire: @tires,
|
65
|
-
env: @environment)
|
66
|
-
|
67
|
-
@condition.consume_fuel(@fuel_consumption *
|
68
|
-
@controls.drive_pedal *
|
69
|
-
@environment.tick)
|
70
|
-
end
|
71
|
-
|
72
|
-
def drive_force
|
73
|
-
@condition.fuel > 0.0 ? (@max_drive_force * @controls.drive_pedal) : 0.0
|
74
|
-
end
|
75
|
-
|
76
|
-
def drive_force_vector
|
77
|
-
@condition.dir * drive_force
|
78
|
-
end
|
79
|
-
|
80
|
-
def brake_force
|
81
|
-
@max_brake_force * @controls.brake_pedal
|
82
|
-
end
|
83
|
-
|
84
|
-
def brake_force_vector
|
85
|
-
-1 * @condition.movement_dir * brake_force
|
86
|
-
end
|
87
|
-
|
88
|
-
def fuel_mass
|
89
|
-
@condition.fuel * @environment.petrol_density
|
90
|
-
end
|
91
|
-
|
92
|
-
def total_mass
|
93
|
-
@mass + fuel_mass + @driver_mass
|
94
|
-
end
|
95
|
-
|
96
|
-
def weight
|
97
|
-
total_mass * @environment.g
|
98
|
-
end
|
99
|
-
|
100
|
-
def add_fuel(liters)
|
101
|
-
sum = @condition.fuel + liters
|
102
|
-
overflow = sum > @fuel_capacity ? sum - @fuel_capacity : 0
|
103
|
-
@condition.add_fuel(liters - overflow)
|
104
|
-
overflow
|
105
|
-
end
|
106
|
-
|
107
|
-
def air_resistance
|
108
|
-
# use default air density for now
|
109
|
-
VectorForce.air_resistance(@condition.vel,
|
110
|
-
frontal_area: @frontal_area,
|
111
|
-
drag_cof: @cd)
|
112
|
-
end
|
113
|
-
|
114
|
-
def rotational_resistance
|
115
|
-
# uses default ROT_COF
|
116
|
-
VectorForce.rotational_resistance(@condition.vel)
|
117
|
-
end
|
118
|
-
|
119
|
-
def rolling_resistance
|
120
|
-
# TODO: downforce
|
121
|
-
VectorForce.rolling_resistance(weight,
|
122
|
-
dir: @condition.movement_dir,
|
123
|
-
roll_cof: @tires.roll_cof)
|
124
|
-
end
|
125
|
-
|
126
|
-
def applied_force
|
127
|
-
drive_force_vector + brake_force_vector
|
128
|
-
end
|
129
|
-
|
130
|
-
def natural_force
|
131
|
-
air_resistance + rotational_resistance + rolling_resistance
|
132
|
-
end
|
133
|
-
|
134
|
-
def sum_forces
|
135
|
-
applied_force + natural_force
|
136
|
-
end
|
137
|
-
|
138
|
-
class Controls
|
139
|
-
attr_reader :drive_pedal, :brake_pedal, :steering_wheel
|
140
|
-
|
141
|
-
def initialize
|
142
|
-
@drive_pedal = 0.0 # up to 1.0
|
143
|
-
@brake_pedal = 0.0 # up to 1.0
|
144
|
-
@steering_wheel = 0.0 # -1.0 to 1.0
|
145
|
-
end
|
146
|
-
|
147
|
-
def drive_pedal=(flt)
|
148
|
-
@drive_pedal = flt.clamp(0.0, 1.0)
|
149
|
-
end
|
150
|
-
|
151
|
-
def brake_pedal=(flt)
|
152
|
-
@brake_pedal = flt.clamp(0.0, 1.0)
|
153
|
-
end
|
154
|
-
|
155
|
-
def steering_wheel=(flt)
|
156
|
-
@steering_wheel = steering_wheel.clamp(-1.0, 1.0)
|
157
|
-
end
|
158
|
-
|
159
|
-
def to_s
|
160
|
-
[format("Throttle: %d%%", @drive_pedal * 100),
|
161
|
-
format("Brake: %d%%", @brake_pedal * 100),
|
162
|
-
format("Steering: %d%%", @steering_wheel * 100),
|
163
|
-
].join(" | ")
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
class Condition
|
168
|
-
unless Vector.method_defined?(:zero?)
|
169
|
-
using VectorZeroBackport
|
170
|
-
end
|
171
|
-
|
172
|
-
attr_reader :dir, :pos, :vel, :acc, :fuel, :lat_g, :lon_g,
|
173
|
-
:wheelspeed, :brake_temp, :brake_pad_depth
|
174
|
-
|
175
|
-
def initialize(dir: DrivingPhysics.random_unit_vector,
|
176
|
-
brake_temp: AIR_TEMP,
|
177
|
-
brake_pad_depth: )
|
178
|
-
@dir = dir # maybe rename to @heading ?
|
179
|
-
@pos = Vector[0, 0]
|
180
|
-
@vel = Vector[0, 0]
|
181
|
-
@acc = Vector[0, 0]
|
182
|
-
@fuel = 0.0 # L
|
183
|
-
@lat_g = 0.0 # g
|
184
|
-
@lon_g = 0.0 # g
|
185
|
-
@wheelspeed = 0.0 # m/s (sliding when it differs from @speed)
|
186
|
-
@brake_temp = brake_temp
|
187
|
-
@brake_pad_depth = brake_pad_depth # mm
|
188
|
-
end
|
189
|
-
|
190
|
-
def to_s
|
191
|
-
[[compass,
|
192
|
-
format('P(%d, %d)', @pos[0], @pos[1]),
|
193
|
-
format('V(%.1f, %.1f)', @vel[0], @vel[1]),
|
194
|
-
format('A(%.2f, %.2f)', @acc[0], @acc[1]),
|
195
|
-
].join(' | '),
|
196
|
-
[format('%.1f m/s', speed),
|
197
|
-
format('LatG: %.2f', lat_g),
|
198
|
-
format('LonG: %.2f', lon_g),
|
199
|
-
format('Whl: %.1f m/s', @wheelspeed),
|
200
|
-
format('Slide: %.1f m/s', slide_speed),
|
201
|
-
].join(' | '),
|
202
|
-
[format('Brakes: %.1f C %.1f mm', @brake_temp, @brake_pad_depth),
|
203
|
-
format('Fuel: %.2f L', @fuel),
|
204
|
-
].join(' | ')
|
205
|
-
].join("\n")
|
206
|
-
end
|
207
|
-
|
208
|
-
# left is negative, right is positive
|
209
|
-
def lat_dir
|
210
|
-
DrivingPhysics.rot_90(@dir, clockwise: true)
|
211
|
-
end
|
212
|
-
|
213
|
-
# note, we might be moving backwards, so not always @dir
|
214
|
-
# and we can't normalize a zero vector if we're not moving
|
215
|
-
def movement_dir
|
216
|
-
(speed == 0.0) ? @dir : @vel.normalize
|
217
|
-
end
|
218
|
-
|
219
|
-
def tick!(force:, mass:, tire:, env:)
|
220
|
-
# take the longitudinal component of the force, relative to vel dir
|
221
|
-
vel_dir = @vel.zero? ? @dir : @vel.normalize
|
222
|
-
lon_force = force.dot(vel_dir)
|
223
|
-
@wheelspeed = nil
|
224
|
-
|
225
|
-
if lon_force < 0.0
|
226
|
-
is_stopping = true
|
227
|
-
if @vel.zero?
|
228
|
-
@acc = Vector[0,0]
|
229
|
-
@wheelspeed = 0.0
|
230
|
-
@lon_g = 0.0
|
231
|
-
|
232
|
-
# TODO: update any other physical vars
|
233
|
-
return
|
234
|
-
end
|
235
|
-
else
|
236
|
-
is_stopping = false
|
237
|
-
end
|
238
|
-
|
239
|
-
# now detect sliding
|
240
|
-
nominal_acc = DrivingPhysics.acc(force, mass)
|
241
|
-
init_v = @vel
|
242
|
-
|
243
|
-
if nominal_acc.magnitude > tire.max_g * env.g
|
244
|
-
nominal_v = DrivingPhysics.vel(@vel, nominal_acc, dt: env.tick)
|
245
|
-
|
246
|
-
# check for reversal of velocity; could be wheelspin while
|
247
|
-
# moving backwards, so can't just look at is_stopping
|
248
|
-
if nominal_v.dot(@vel) < 0 and is_stopping
|
249
|
-
@wheelspeed = 0.0
|
250
|
-
else
|
251
|
-
@wheelspeed = nominal_v.magnitude
|
252
|
-
end
|
253
|
-
@acc = nominal_acc.normalize * tire.max_g * env.g
|
254
|
-
else
|
255
|
-
@acc = nominal_acc
|
256
|
-
end
|
257
|
-
|
258
|
-
@vel = DrivingPhysics.vel(@vel, @acc, dt: env.tick)
|
259
|
-
@wheelspeed ||= @vel.magnitude
|
260
|
-
|
261
|
-
# finally, detect velocity reversal when stopping
|
262
|
-
if is_stopping and @vel.dot(init_v) < 0
|
263
|
-
puts "crossed zero; stopping!"
|
264
|
-
@vel = Vector[0, 0]
|
265
|
-
@wheelspeed = 0.0
|
266
|
-
@lon_g = 0.0
|
267
|
-
end
|
268
|
-
|
269
|
-
@lon_g = @acc.dot(@dir) / env.g
|
270
|
-
@pos = DrivingPhysics.pos(@pos, @vel, dt: env.tick)
|
271
|
-
end
|
272
|
-
|
273
|
-
def add_fuel(liters)
|
274
|
-
@fuel += liters
|
275
|
-
end
|
276
|
-
|
277
|
-
def consume_fuel(liters)
|
278
|
-
@fuel -= liters
|
279
|
-
end
|
280
|
-
|
281
|
-
def speed
|
282
|
-
@vel.magnitude
|
283
|
-
end
|
284
|
-
|
285
|
-
def slide_speed
|
286
|
-
(speed - @wheelspeed).abs
|
287
|
-
end
|
288
|
-
|
289
|
-
def compass
|
290
|
-
DrivingPhysics.compass_dir(@dir)
|
291
|
-
end
|
292
|
-
end
|
293
|
-
end
|
294
|
-
end
|
data/lib/driving_physics/tire.rb
DELETED
@@ -1,288 +0,0 @@
|
|
1
|
-
# NOTE: This entire file will likely be replaced or removed.
|
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
|
-
#
|
6
|
-
|
7
|
-
require 'driving_physics'
|
8
|
-
|
9
|
-
# treat instances of this class as immutable
|
10
|
-
# Tire::Condition has mutable attributes
|
11
|
-
#
|
12
|
-
module DrivingPhysics
|
13
|
-
class Tire
|
14
|
-
class Error < RuntimeError; end
|
15
|
-
|
16
|
-
attr_accessor :roll_cof,
|
17
|
-
:tread_mm,
|
18
|
-
:cords_mm,
|
19
|
-
:radius_mm,
|
20
|
-
:g_factor,
|
21
|
-
:max_heat_cycles,
|
22
|
-
:temp_profile,
|
23
|
-
:condition
|
24
|
-
|
25
|
-
def initialize
|
26
|
-
@roll_cof = ROLL_COF
|
27
|
-
@tread_mm = 10
|
28
|
-
@cords_mm = 1
|
29
|
-
@radius_mm = 350
|
30
|
-
@g_factor = 1.0
|
31
|
-
@max_heat_cycles = 50
|
32
|
-
@temp_profile = TemperatureProfile.new
|
33
|
-
|
34
|
-
yield self if block_given?
|
35
|
-
@condition = Condition.new(tread_mm: @tread_mm, cords_mm: @cords_mm)
|
36
|
-
end
|
37
|
-
|
38
|
-
def to_s
|
39
|
-
[[format("Grip: %.2f / %.1f G", max_g, @g_factor),
|
40
|
-
format("Radius: %d mm", @radius_mm),
|
41
|
-
format("cRR: %.3f", @roll_cof),
|
42
|
-
].join(' | '),
|
43
|
-
@condition,
|
44
|
-
].join("\n")
|
45
|
-
end
|
46
|
-
|
47
|
-
def tread_left?
|
48
|
-
@condition.tread_mm > 0.0
|
49
|
-
end
|
50
|
-
|
51
|
-
# cords give half the traction as tread
|
52
|
-
def tread_factor
|
53
|
-
tread_left? ? 1.0 : 0.5 * @condition.cords_mm / @cords_mm
|
54
|
-
end
|
55
|
-
|
56
|
-
# up to max_heat_cycles, the grip decays down to 80%
|
57
|
-
# beyond max_heat_cycles, the grip decay plateaus at 80%
|
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
|
62
|
-
end
|
63
|
-
|
64
|
-
def temp_factor
|
65
|
-
@temp_profile.grip_factor(@condition.temp_c)
|
66
|
-
end
|
67
|
-
|
68
|
-
def max_g
|
69
|
-
@g_factor * temp_factor * heat_cycle_factor * tread_factor
|
70
|
-
end
|
71
|
-
|
72
|
-
# treat instances of this class as immutable
|
73
|
-
class TemperatureProfile
|
74
|
-
class Error < DrivingPhysics::Tire::Error; end
|
75
|
-
|
76
|
-
TEMPS = [-100, 0, 25, 50, 75, 80, 85, 95, 100, 105, 110, 120, 130, 150]
|
77
|
-
GRIPS = [0.1, 0.5, 0.75, 0.8, 0.9, 0.95, 1.0,
|
78
|
-
0.95, 0.9, 0.75, 0.5, 0.25, 0.1, 0.05]
|
79
|
-
MIN_GRIP = 0.01
|
80
|
-
|
81
|
-
attr_reader :critical_temp
|
82
|
-
|
83
|
-
def initialize(deg_ary = TEMPS, grip_ary = GRIPS)
|
84
|
-
if !deg_ary.is_a?(Array) or !grip_ary.is_a?(Array)
|
85
|
-
raise(ArgumentError, "two arrays are required")
|
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
|
129
|
-
end
|
130
|
-
|
131
|
-
# treat attributes of this class as *mutable*
|
132
|
-
class Condition
|
133
|
-
class Error < DrivingPhysics::Tire::Error; end
|
134
|
-
class Destroyed < Error; end
|
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
|
186
|
-
|
187
|
-
slide_factor = slide_speed * 5
|
188
|
-
target_slide_temp = target_g_temp + slide_factor
|
189
|
-
|
190
|
-
puts "target_slide_temp=#{target_slide_temp}" if @debug_temp
|
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
|
269
|
-
|
270
|
-
wt = wear_tick(g: g,
|
271
|
-
slide_speed: slide_speed,
|
272
|
-
mass: mass,
|
273
|
-
critical_temp: critical_temp)
|
274
|
-
|
275
|
-
if @tread_mm > 0
|
276
|
-
@tread_mm -= wt / 18000
|
277
|
-
@tread_mm = 0 if @tread_mm < 0
|
278
|
-
else
|
279
|
-
# cords wear 2x faster
|
280
|
-
@cords_mm -= wt * 2 / 18000
|
281
|
-
if @cords_mm <= 0
|
282
|
-
raise(Destroyed, "no more cords")
|
283
|
-
end
|
284
|
-
end
|
285
|
-
end
|
286
|
-
end
|
287
|
-
end
|
288
|
-
end
|
data/test/car.rb
DELETED
@@ -1,156 +0,0 @@
|
|
1
|
-
require 'minitest/autorun'
|
2
|
-
require 'driving_physics/car'
|
3
|
-
|
4
|
-
C = DrivingPhysics::Car
|
5
|
-
|
6
|
-
describe C do
|
7
|
-
before do
|
8
|
-
@env = DrivingPhysics::Environment.new
|
9
|
-
@c = C.new(@env)
|
10
|
-
end
|
11
|
-
|
12
|
-
def get_moving
|
13
|
-
@c.controls.brake_pedal = 0.0
|
14
|
-
@c.controls.drive_pedal = 1.0
|
15
|
-
@c.add_fuel 10
|
16
|
-
50.times { @c.tick! }
|
17
|
-
expect(@c.condition.speed).must_be :>, 0.0
|
18
|
-
end
|
19
|
-
|
20
|
-
|
21
|
-
it "initializes" do
|
22
|
-
expect(@c).must_be_instance_of C
|
23
|
-
end
|
24
|
-
|
25
|
-
it "has a string representation" do
|
26
|
-
str = @c.to_s
|
27
|
-
expect(str).must_be_instance_of String
|
28
|
-
expect(str.length).must_be(:>, 5)
|
29
|
-
end
|
30
|
-
|
31
|
-
it "adds fuel and reports overflow" do
|
32
|
-
expect(@c.condition.fuel).must_equal 0.0
|
33
|
-
@c.add_fuel 10.0
|
34
|
-
expect(@c.condition.fuel).must_equal 10.0
|
35
|
-
overflow = @c.add_fuel @c.fuel_capacity
|
36
|
-
expect(@c.condition.fuel).must_equal @c.fuel_capacity
|
37
|
-
expect(overflow).must_equal 10.0
|
38
|
-
end
|
39
|
-
|
40
|
-
it "varies drive_force based on drive_pedal and available fuel" do
|
41
|
-
expect(@c.drive_force).must_equal 0.0 # no pedal
|
42
|
-
@c.controls.drive_pedal = 1.0
|
43
|
-
expect(@c.drive_force).must_equal 0.0 # no fuel
|
44
|
-
@c.add_fuel 10
|
45
|
-
|
46
|
-
expect(@c.drive_force).must_equal @c.max_drive_force # vroom!
|
47
|
-
end
|
48
|
-
|
49
|
-
it "has a drive vector in direction of @dir" do
|
50
|
-
@c.add_fuel 10
|
51
|
-
@c.controls.drive_pedal = 1.0
|
52
|
-
dv = @c.drive_force_vector
|
53
|
-
expect(dv).must_be_instance_of Vector
|
54
|
-
dvn = dv.normalize
|
55
|
-
[0,1].each { |dim|
|
56
|
-
expect(dvn[dim]).must_be_within_epsilon @c.condition.dir[dim]
|
57
|
-
}
|
58
|
-
end
|
59
|
-
|
60
|
-
it "varies brake_force based on brake_pedal" do
|
61
|
-
expect(@c.brake_force).must_equal 0.0 # no pedal
|
62
|
-
@c.controls.brake_pedal = 1.0
|
63
|
-
expect(@c.brake_force).must_equal @c.max_brake_force
|
64
|
-
end
|
65
|
-
|
66
|
-
it "has a brake vector opposing movement or @dir" do
|
67
|
-
# hmm, no good way to go in reverse
|
68
|
-
# just test against forward movement for now
|
69
|
-
@c.controls.brake_pedal = 1.0
|
70
|
-
bv = @c.brake_force_vector
|
71
|
-
expect(bv).must_be_instance_of Vector
|
72
|
-
bvn = bv.normalize
|
73
|
-
[0,1].each { |dim|
|
74
|
-
expect(bvn[dim]).must_be_within_epsilon @c.condition.dir[dim] * -1
|
75
|
-
}
|
76
|
-
|
77
|
-
get_moving
|
78
|
-
|
79
|
-
@c.controls.drive_pedal = 0.0
|
80
|
-
@c.controls.brake_pedal = 1.0
|
81
|
-
bdir = @c.brake_force_vector.normalize
|
82
|
-
vdir = @c.condition.vel.normalize
|
83
|
-
[0,1].each { |dim|
|
84
|
-
expect(bdir[dim]).must_be_within_epsilon vdir[dim] * -1
|
85
|
-
}
|
86
|
-
end
|
87
|
-
|
88
|
-
it "tracks the mass of remaining fuel" do
|
89
|
-
expect(@c.fuel_mass).must_equal 0.0
|
90
|
-
@c.add_fuel 10
|
91
|
-
expect(@c.fuel_mass).must_be_within_epsilon 7.1
|
92
|
-
end
|
93
|
-
|
94
|
-
it "tracks total_mass including fuel and driver" do
|
95
|
-
expect(@c.total_mass).must_equal @c.mass + @c.driver_mass
|
96
|
-
@c.add_fuel 10
|
97
|
-
expect(@c.total_mass).must_equal @c.mass + @c.fuel_mass + @c.driver_mass
|
98
|
-
end
|
99
|
-
|
100
|
-
it "computes the total weight based on G" do
|
101
|
-
expect(@c.weight).must_be_within_epsilon 10535.0
|
102
|
-
end
|
103
|
-
|
104
|
-
it "computes resistance forces based on instance variables" do
|
105
|
-
air = @c.air_resistance
|
106
|
-
expect(air).must_be_kind_of Vector
|
107
|
-
expect(air.magnitude).must_equal 0.0
|
108
|
-
|
109
|
-
rot = @c.rotational_resistance
|
110
|
-
expect(rot).must_be_kind_of Vector
|
111
|
-
expect(rot.magnitude).must_equal 0.0
|
112
|
-
|
113
|
-
roll = @c.rolling_resistance
|
114
|
-
expect(roll).must_be_kind_of Vector
|
115
|
-
expect(roll.magnitude).must_be :>, 0
|
116
|
-
end
|
117
|
-
|
118
|
-
describe C::Condition do
|
119
|
-
before do
|
120
|
-
@cond = @c.condition
|
121
|
-
end
|
122
|
-
|
123
|
-
it "intializes" do
|
124
|
-
expect(@cond).must_be_kind_of C::Condition
|
125
|
-
end
|
126
|
-
|
127
|
-
it "has a string representation" do
|
128
|
-
str = @cond.to_s
|
129
|
-
expect(str).must_be_kind_of String
|
130
|
-
expect(str.length).must_be :>, 5
|
131
|
-
end
|
132
|
-
|
133
|
-
it "has a lateral direction clockwise from @dir" do
|
134
|
-
lat = @cond.lat_dir
|
135
|
-
expect(lat).must_be_kind_of Vector
|
136
|
-
expect(lat.magnitude).must_be_within_epsilon 1.0
|
137
|
-
expect(lat.independent?(@cond.dir)).must_equal true
|
138
|
-
end
|
139
|
-
|
140
|
-
it "has a movement_dir based on velocity, or @dir when stopped" do
|
141
|
-
md = @cond.movement_dir
|
142
|
-
expect(md).must_be_kind_of Vector
|
143
|
-
expect(md.magnitude).must_be_within_epsilon 1.0
|
144
|
-
expect(md).must_equal @cond.dir
|
145
|
-
|
146
|
-
get_moving
|
147
|
-
md = @cond.movement_dir
|
148
|
-
expect(md).must_be_kind_of Vector
|
149
|
-
expect(md.magnitude).must_be_within_epsilon 1.0
|
150
|
-
vd = @cond.vel.normalize
|
151
|
-
[0,1].each { |dim|
|
152
|
-
expect(md[dim]).must_be_within_epsilon vd[dim]
|
153
|
-
}
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
data/test/tire.rb
DELETED
@@ -1,125 +0,0 @@
|
|
1
|
-
require 'driving_physics/tire'
|
2
|
-
require 'minitest/autorun'
|
3
|
-
|
4
|
-
include DrivingPhysics
|
5
|
-
|
6
|
-
describe Tire do
|
7
|
-
TP = Tire::TemperatureProfile
|
8
|
-
|
9
|
-
describe TP do
|
10
|
-
before do
|
11
|
-
@tp = TP.new
|
12
|
-
end
|
13
|
-
|
14
|
-
it "initializes with two same-sized arrays" do
|
15
|
-
expect(@tp).must_be_kind_of TP
|
16
|
-
expect(TP.new([0,1,2,3], [0,1.0,0.7,0.4])).wont_be_nil
|
17
|
-
expect { TP.new('', '') }.must_raise ArgumentError
|
18
|
-
expect { TP.new([]) }.must_raise ArgumentError
|
19
|
-
expect { TP.new([0], []) }.must_raise ArgumentError
|
20
|
-
expect { TP.new([], [0.0]) }.must_raise ArgumentError
|
21
|
-
end
|
22
|
-
|
23
|
-
it "determines a grip number from a temp number" do
|
24
|
-
{ -500 => TP::MIN_GRIP,
|
25
|
-
-100 => 0.1,
|
26
|
-
-0.0001 => 0.1,
|
27
|
-
0.0 => 0.5
|
28
|
-
}.each { |temp, gf| expect(@tp.grip_factor(temp)).must_equal gf }
|
29
|
-
end
|
30
|
-
|
31
|
-
it "has a critical_temp above the temp for 100%" do
|
32
|
-
expect(@tp.critical_temp).must_be(:>, 90)
|
33
|
-
expect(@tp.critical_temp).must_equal 105
|
34
|
-
end
|
35
|
-
|
36
|
-
it "has a map that increases to 100% and decreases below 80%" do
|
37
|
-
expect {
|
38
|
-
TP.new([0,1,2,3,4], [0.0,0.1,0.2,0.3,0.4])
|
39
|
-
}.must_raise TP::Error
|
40
|
-
|
41
|
-
expect {
|
42
|
-
TP.new([0,1,2,3,4], [0.0, 1.0, 0.99, 0.98, 0.97])
|
43
|
-
}.must_raise TP::Error
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
before do
|
48
|
-
@t = Tire.new
|
49
|
-
end
|
50
|
-
|
51
|
-
it "initializes with default values without a block" do
|
52
|
-
expect(@t).must_be_kind_of Tire
|
53
|
-
end
|
54
|
-
|
55
|
-
it "accepts a block to initialize with custom values" do
|
56
|
-
t = Tire.new { |x|
|
57
|
-
x.tread_mm = 9999
|
58
|
-
x.g_factor = -0.1234
|
59
|
-
}
|
60
|
-
|
61
|
-
expect(t.tread_mm).must_equal 9999
|
62
|
-
expect(t.g_factor).must_equal(-0.1234)
|
63
|
-
end
|
64
|
-
|
65
|
-
it "knows when the tread is gone" do
|
66
|
-
expect(@t.tread_left?).must_equal true
|
67
|
-
|
68
|
-
@t.condition.tread_mm = 0.00001
|
69
|
-
expect(@t.tread_left?).must_equal true
|
70
|
-
|
71
|
-
@t.condition.tread_mm = 0.0
|
72
|
-
expect(@t.tread_left?).must_equal false
|
73
|
-
end
|
74
|
-
|
75
|
-
it "has 50% grip when down to the cords" do
|
76
|
-
expect(@t.tread_factor).must_equal 1.0
|
77
|
-
|
78
|
-
@t.condition.tread_mm = 0.0
|
79
|
-
expect(@t.tread_factor).must_be_within_epsilon 0.5
|
80
|
-
end
|
81
|
-
|
82
|
-
it "has less than 10% tread factor when the cords start to wear" do
|
83
|
-
expect(@t.tread_factor).must_equal 1.0
|
84
|
-
|
85
|
-
@t.condition.tread_mm = 5.0
|
86
|
-
expect(@t.tread_factor).must_equal 1.0
|
87
|
-
|
88
|
-
@t.condition.tread_mm = 0.0
|
89
|
-
expect(@t.tread_factor).must_be_within_epsilon 0.5
|
90
|
-
|
91
|
-
@t.condition.cords_mm = 0.9
|
92
|
-
expect(@t.tread_factor).must_be_within_epsilon 0.45
|
93
|
-
end
|
94
|
-
|
95
|
-
it "has decreasing heat cycle factor" do
|
96
|
-
expect(@t.condition.heat_cycles).must_equal 0
|
97
|
-
expect(@t.heat_cycle_factor).must_equal 1.0
|
98
|
-
|
99
|
-
@t.condition.heat_cycles = 20
|
100
|
-
expect(@t.heat_cycle_factor).must_be(:<, 1.0)
|
101
|
-
end
|
102
|
-
|
103
|
-
it "has a temp factor according to temperature profile" do
|
104
|
-
expect(@t.condition.temp_c).must_equal 25
|
105
|
-
expect(@t.temp_factor).must_equal 0.75
|
106
|
-
|
107
|
-
@t.condition.temp_c = 90
|
108
|
-
expect(@t.temp_factor).must_equal 1.0
|
109
|
-
end
|
110
|
-
|
111
|
-
it "incorporates temp, heat_cycles, and tread depth into available grip" do
|
112
|
-
@t.condition.temp_c = 60
|
113
|
-
expect(@t.temp_factor).must_be_within_epsilon 0.8
|
114
|
-
|
115
|
-
@t.condition.heat_cycles = 10
|
116
|
-
expect(@t.heat_cycle_factor).must_be_within_epsilon 0.96
|
117
|
-
|
118
|
-
@t.condition.tread_mm = 5.0
|
119
|
-
expect(@t.tread_factor).must_equal 1.0
|
120
|
-
expect(@t.max_g).must_be_within_epsilon 0.768
|
121
|
-
|
122
|
-
@t.condition.tread_mm = 0.0
|
123
|
-
expect(@t.max_g).must_be_within_epsilon 0.384
|
124
|
-
end
|
125
|
-
end
|