driving_physics 0.0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +83 -0
- data/Rakefile +62 -0
- data/VERSION +1 -0
- data/demo/car.rb +28 -0
- data/demo/scalar_force.rb +26 -0
- data/demo/tire.rb +169 -0
- data/demo/vector_force.rb +26 -0
- data/demo/wheel.rb +84 -0
- data/driving_physics.gemspec +17 -0
- data/lib/driving_physics/car.rb +294 -0
- data/lib/driving_physics/environment.rb +29 -0
- data/lib/driving_physics/imperial.rb +61 -0
- data/lib/driving_physics/scalar_force.rb +68 -0
- data/lib/driving_physics/tire.rb +288 -0
- data/lib/driving_physics/vector_force.rb +136 -0
- data/lib/driving_physics/wheel.rb +191 -0
- data/lib/driving_physics.rb +71 -0
- data/test/car.rb +156 -0
- data/test/driving_physics.rb +29 -0
- data/test/scalar_force.rb +30 -0
- data/test/tire.rb +125 -0
- data/test/vector_force.rb +90 -0
- data/test/wheel.rb +177 -0
- metadata +65 -0
@@ -0,0 +1,294 @@
|
|
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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'driving_physics'
|
2
|
+
|
3
|
+
module DrivingPhysics
|
4
|
+
class Environment
|
5
|
+
attr_reader :hz, :tick
|
6
|
+
attr_accessor :g, :air_temp, :air_density, :petrol_density
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
self.hz = HZ
|
10
|
+
@g = G
|
11
|
+
@air_temp = AIR_TEMP
|
12
|
+
@air_density = AIR_DENSITY
|
13
|
+
@petrol_density = PETROL_DENSITY
|
14
|
+
end
|
15
|
+
|
16
|
+
def hz=(int)
|
17
|
+
@hz = int
|
18
|
+
@tick = Rational(1) / @hz
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
[format("Tick: %d Hz", @hz),
|
23
|
+
format("G: %.2f m/s^2", @g),
|
24
|
+
format("Air: %.1f C %.2f kg/m^3", @air_temp, @air_density),
|
25
|
+
format("Petrol: %.2f kg/L", @petrol_density),
|
26
|
+
].join(" | ")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'driving_physics'
|
2
|
+
|
3
|
+
module DrivingPhysics
|
4
|
+
module Imperial
|
5
|
+
FEET_PER_METER = 3.28084
|
6
|
+
FEET_PER_MILE = 5280
|
7
|
+
MPH = (FEET_PER_METER / FEET_PER_MILE) * SECS_PER_HOUR
|
8
|
+
CI_PER_LITER = 61.024
|
9
|
+
GAL_PER_LITER = 0.264172
|
10
|
+
|
11
|
+
def self.feet(meters)
|
12
|
+
meters * FEET_PER_METER
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.meters(feet)
|
16
|
+
feet / FEET_PER_METER
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.miles(meters = nil, km: nil)
|
20
|
+
raise(ArgumentError, "argument missing") if meters.nil? and km.nil?
|
21
|
+
meters ||= km * 1000
|
22
|
+
meters * FEET_PER_METER / FEET_PER_MILE
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.mph(mps = nil, kph: nil)
|
26
|
+
raise(ArgumentError, "argument missing") if mps.nil? and kph.nil?
|
27
|
+
mps ||= kph.to_f * 1000 / SECS_PER_HOUR
|
28
|
+
MPH * mps
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.mps(mph)
|
32
|
+
mph / MPH
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.kph(mph)
|
36
|
+
DP::kph(mps(mph))
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.deg_c(deg_f)
|
40
|
+
(deg_f - 32).to_f * 5 / 9
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.deg_f(deg_c)
|
44
|
+
deg_c.to_f * 9 / 5 + 32
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.cubic_inches(liters)
|
48
|
+
liters * CI_PER_LITER
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.liters(ci = nil, gallons: nil)
|
52
|
+
raise(ArgumentError, "argument missing") if ci.nil? and gallons.nil?
|
53
|
+
return ci / CI_PER_LITER if gallons.nil?
|
54
|
+
gallons.to_f / GAL_PER_LITER
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.gallons(liters)
|
58
|
+
liters * GAL_PER_LITER
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'driving_physics'
|
2
|
+
|
3
|
+
module DrivingPhysics
|
4
|
+
module ScalarForce
|
5
|
+
#
|
6
|
+
# Resistance Forces
|
7
|
+
#
|
8
|
+
# 1. air resistance aka drag aka turbulent drag
|
9
|
+
# depends on v^2
|
10
|
+
# 2. "rotatational" resistance, e.g. bearings / axles / lubricating fluids
|
11
|
+
# aka viscous drag; linear with v
|
12
|
+
# 3. rolling resistance, e.g. tire and surface deformation
|
13
|
+
# constant with v, depends on normal force and tire/surface properties
|
14
|
+
# 4. braking resistance, supplied by operator, constant with v
|
15
|
+
# depends purely on operator choice and physical limits
|
16
|
+
# as such, it is not modeled here
|
17
|
+
#
|
18
|
+
# Note: here we only consider speed; we're in a 1D world for now
|
19
|
+
#
|
20
|
+
|
21
|
+
def self.air_resistance(speed,
|
22
|
+
frontal_area: FRONTAL_AREA,
|
23
|
+
drag_cof: DRAG_COF,
|
24
|
+
air_density: AIR_DENSITY)
|
25
|
+
0.5 * frontal_area * drag_cof * air_density * speed ** 2
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.rotational_resistance(speed, rot_cof: ROT_COF)
|
29
|
+
speed * rot_cof
|
30
|
+
end
|
31
|
+
|
32
|
+
# normal force is not always mass * G, e.g. aero downforce
|
33
|
+
def self.rolling_resistance(normal_force, roll_cof: ROLL_COF)
|
34
|
+
normal_force * roll_cof
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# convenience methods
|
39
|
+
#
|
40
|
+
|
41
|
+
def self.speed_resistance(speed,
|
42
|
+
frontal_area: FRONTAL_AREA,
|
43
|
+
drag_cof: DRAG_COF,
|
44
|
+
air_density: AIR_DENSITY,
|
45
|
+
rot_cof: ROT_COF)
|
46
|
+
air_resistance(speed,
|
47
|
+
frontal_area: frontal_area,
|
48
|
+
drag_cof: drag_cof,
|
49
|
+
air_density: air_density) +
|
50
|
+
rotational_resistance(speed, rot_cof: rot_cof)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.all_resistance(speed,
|
54
|
+
frontal_area: FRONTAL_AREA,
|
55
|
+
drag_cof: DRAG_COF,
|
56
|
+
air_density: AIR_DENSITY,
|
57
|
+
rot_cof: ROT_COF,
|
58
|
+
nf_mag:,
|
59
|
+
roll_cof: ROLL_COF)
|
60
|
+
speed_resistance(speed,
|
61
|
+
frontal_area: frontal_area,
|
62
|
+
drag_cof: drag_cof,
|
63
|
+
air_density: air_density,
|
64
|
+
rot_cof: rot_cof) +
|
65
|
+
rolling_resistance(nf_mag, roll_cof: roll_cof)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|