driving_physics 0.0.0.2
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 +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
|