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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b32478386d13b88eb67f9d2bb826c23dd06dd5100d807aa918cea6af962c6e3
4
- data.tar.gz: 3ebcad787d058dac2d36718d46e5cf400dcc968f06427d2f65af4d1682f99166
3
+ metadata.gz: ec6ff08e7731aa8bcb419efee9ab1a96ef7642f3426c7b8a501dae764692d777
4
+ data.tar.gz: ae217c0e452a764392e5a4df9a0b3280369e21b92d6018ef8d8753c0a6f5f594
5
5
  SHA512:
6
- metadata.gz: 4ce4c55bbb0e9e5f12146856c45df05860443b18d7fd54c173e8b3026e55e1c8ccdfa3e9d48a6974af5f28c79e6df04b487c9728d5a831ec88d657a7ce997db1
7
- data.tar.gz: 28ea2e0361cc180a5a405ed66caffcced3d1669ec18b87ce14f3ab0d77d130de9bd92864a2b78bddbcee236fff7bd982d66272456c4ba81eec43b863691e044d
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
- require 'flay_task'
36
- FlayTask.new do |t|
37
- t.dirs = ['lib']
38
- t.verbose = true
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.2
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.2
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
- }
@@ -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
@@ -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