driving_physics 0.0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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