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