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