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 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