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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4b32478386d13b88eb67f9d2bb826c23dd06dd5100d807aa918cea6af962c6e3
4
+ data.tar.gz: 3ebcad787d058dac2d36718d46e5cf400dcc968f06427d2f65af4d1682f99166
5
+ SHA512:
6
+ metadata.gz: 4ce4c55bbb0e9e5f12146856c45df05860443b18d7fd54c173e8b3026e55e1c8ccdfa3e9d48a6974af5f28c79e6df04b487c9728d5a831ec88d657a7ce997db1
7
+ data.tar.gz: 28ea2e0361cc180a5a405ed66caffcced3d1669ec18b87ce14f3ab0d77d130de9bd92864a2b78bddbcee236fff7bd982d66272456c4ba81eec43b863691e044d
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ [![CI Status](https://github.com/rickhull/driving_physics/actions/workflows/ci.yaml/badge.svg)](https://github.com/rickhull/driving_physics/actions/workflows/ci.yaml)
2
+
3
+ # Driving Simulation
4
+
5
+ Physical simulation of how to make a car go around a track quickly, in pure
6
+ Ruby with minimal dependencies. Physics from first principles, often
7
+ using `Vector` class from `matrix.rb` in stdlib.
8
+
9
+ Note, this is very much a **Work In Progress**.
10
+
11
+ ## Rationale
12
+
13
+ This is a toy project intended to scratch a personal itch: I want to create
14
+ an environment to play with performance objectives under reasonable real-world
15
+ constraints:
16
+
17
+ Primarily, one must slow down in order to navigate a curve
18
+ quickly.
19
+
20
+ All of this is done with an eye towards handing the controls over to an AI,
21
+ likely a neural net, so that it can learn (and possibly teach) techniques
22
+ that increase performance via decreasing lap times.
23
+
24
+ ## Rough Notes, Thoughts, and Objectives
25
+
26
+ ### Constraints:
27
+
28
+ 1. cars must slow down to make tighter turns
29
+ 2. cars must manage grip levels
30
+ 3. equations to describe available grip
31
+ 4. grip depends on tires and road surface (coefficient of grip)
32
+ 5. grip "releases", whereby car controls lose effectiveness (sliding)
33
+ 6. higher speed means higher turning radius
34
+ 7. more mass (weight) means more grip, but also more force opposing that grip
35
+ 8. one clear downside of more mass is increased tire wear and tire heat
36
+ 9. tires get more grip with more heat up to a critical temp and then grip
37
+ falls off dramatically
38
+ 10. heat cycles (overheat and then cool down) reduce grip capacity over time
39
+ 11. sliding dramatically increases tire wear and decreases grip
40
+ 12. tire wear minimally affects grip (outside of heat cycling), up until tire
41
+ is completely worn
42
+ 13. sliding implies a gentle reduction of velocity over time, all things equal
43
+ 14. wheelspin may occur on driven wheels (different from brake- or
44
+ turn-induced sliding)
45
+ 15. wheelspin incurs similar wear and heat penalties to sliding (relative
46
+ velocity between tire and surface)
47
+
48
+ ### Given These Constraints:
49
+
50
+ 1. Car has enough power to create wheelspin (force applied to driven wheels
51
+ exceeds grip-reactive force)
52
+ 2. Car can achieve velocity that requires braking to successfully achieve a
53
+ given turn radius
54
+ 3. Grip level can be modeled as the ability to sustain acceleration around 1G
55
+ 4. Consider slip angles and modulated sliding (e.g. threshold braking, mild
56
+ wheelspin, limit-of-grip controlled slides)
57
+ 5. Fuel consumption reduces mass over time
58
+ 6. Driving outputs: gas pedal position (0-100), brake pedal position (0-100),
59
+ steering wheel position (-100 - +100)
60
+ 7. Car slows gently with 0 gas pedal
61
+ 8. Car consumes fuel gently with 0 gas pedal (idle)
62
+ 9. Car consumes fuel linearly with gas pedal position
63
+ 10. Brakes wear with brake usage (linear plus heat factor)
64
+ 11. Brakes can overheat
65
+ 12. Hot brakes wear faster
66
+ 13. Driving inputs: visual track position, fuel gauge, tire temp, brake temp
67
+
68
+ ### Examples
69
+
70
+ Given:
71
+ * 1.0 g lateral acceleration
72
+ * 100 ft radius turn
73
+
74
+ How fast can the car go around the turn?
75
+
76
+ ```
77
+ Vmax = sqrt(r) * sqrt(a) = sqrt(r*a)
78
+ Vmax = sqrt(100 ft * 32.2 ft / sec^2)
79
+ Vmax = sqrt(3220) ft/sec = 56.75 ft/sec
80
+
81
+ 56.75 ft/sec * 3600 sec/hr / 5280 ft/mile
82
+ 56.75 ft/sec * 3600/5280 = 38.7 mph
83
+ ```
data/Rakefile ADDED
@@ -0,0 +1,62 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new :test do |t|
4
+ t.pattern = "test/*.rb"
5
+ t.warning = true
6
+ end
7
+
8
+ desc "Run demo scripts"
9
+ task demo: [:test] do
10
+ Dir['demo/*.rb'].each { |filepath|
11
+ puts
12
+ sh "ruby -w -Ilib #{filepath}"
13
+ puts
14
+ }
15
+ end
16
+
17
+ task default: :test
18
+
19
+ #
20
+ # METRICS
21
+ #
22
+
23
+ begin
24
+ require 'flog_task'
25
+ FlogTask.new do |t|
26
+ t.threshold = 9000
27
+ t.dirs = ['lib']
28
+ t.verbose = true
29
+ end
30
+ rescue LoadError
31
+ warn 'flog_task unavailable'
32
+ end
33
+
34
+ begin
35
+ require 'flay_task'
36
+ FlayTask.new do |t|
37
+ t.dirs = ['lib']
38
+ t.verbose = true
39
+ end
40
+ rescue LoadError
41
+ warn 'flay_task unavailable'
42
+ end
43
+
44
+ begin
45
+ require 'roodi_task'
46
+ # RoodiTask.new config: '.roodi.yml', patterns: ['lib/**/*.rb']
47
+ RoodiTask.new patterns: ['lib/**/*.rb']
48
+ rescue LoadError
49
+ warn "roodi_task unavailable"
50
+ end
51
+
52
+ begin
53
+ require 'buildar'
54
+
55
+ Buildar.new do |b|
56
+ b.gemspec_file = 'driving_physics.gemspec'
57
+ b.version_file = 'VERSION'
58
+ b.use_git = true
59
+ end
60
+ rescue LoadError
61
+ warn "buildar tasks unavailable"
62
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0.2
data/demo/car.rb ADDED
@@ -0,0 +1,28 @@
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
+ }
@@ -0,0 +1,26 @@
1
+ require 'driving_physics/scalar_force'
2
+
3
+ DP = DrivingPhysics
4
+
5
+ pos = 0 # m
6
+ spd = 0 # m/s
7
+ mass = 1000 # kg
8
+ weight = mass * DP::G # N
9
+ drive_force = 7000 # N
10
+ duration = 100 # seconds
11
+ tick = 1.0 / DP::HZ
12
+
13
+ (duration * DP::HZ).times { |i|
14
+ nf = drive_force - DP::ScalarForce.all_resistance(spd, nf_mag: weight)
15
+
16
+ a = DP.a(nf, mass)
17
+ spd = DP.v(a, spd, dt: tick)
18
+ pos = DP.p(spd, pos, dt: tick)
19
+
20
+ if i % DP::HZ == 0
21
+ puts [i / DP::HZ,
22
+ format("%.2f m/s", spd),
23
+ format("%.2f m", pos),
24
+ ].join("\t")
25
+ end
26
+ }
data/demo/tire.rb ADDED
@@ -0,0 +1,169 @@
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
+ }
@@ -0,0 +1,26 @@
1
+ require 'driving_physics/vector_force'
2
+
3
+ DP = DrivingPhysics
4
+
5
+ p = Vector[0, 0] # m
6
+ v = Vector[0, 0] # m/s
7
+ mass = 1000 # kg
8
+ weight = mass * DP::G # N
9
+ duration = 100 # seconds
10
+ drive_force = DP.random_unit_vector * 7000 # N
11
+ tick = 1.0 / DP::HZ
12
+
13
+ (duration * DP::HZ).times { |i|
14
+ nf = drive_force + DP::VectorForce.all_resistance(v, dir: v, nf_mag: weight)
15
+
16
+ a = DP.a(nf, mass)
17
+ v = DP.v(a, v, dt: tick)
18
+ p = DP.p(v, p, dt: tick)
19
+
20
+ if i % DP::HZ == 0
21
+ puts [i / DP::HZ,
22
+ format("%.2f m/s", v.magnitude),
23
+ format("%.2f m", p.magnitude),
24
+ ].join("\t")
25
+ end
26
+ }
data/demo/wheel.rb ADDED
@@ -0,0 +1,84 @@
1
+ require 'driving_physics/wheel'
2
+
3
+ include DrivingPhysics
4
+
5
+ env = Environment.new
6
+ wheel = Wheel.new(env, mass: 25.0)
7
+
8
+ puts env
9
+ puts wheel
10
+
11
+ # 1000 kg car
12
+ # 4 tires
13
+ # 250 kg per tire plus tire mass
14
+
15
+ supported_mass = 1000 # kg
16
+ total_mass = supported_mass + 4 * wheel.mass
17
+ corner_mass = Rational(total_mass) / 4
18
+ normal_force = corner_mass * env.g
19
+ axle_torque = 1000 # N*m
20
+ friction_loss = 0.05 # 5% friction / hysteresis loss
21
+
22
+ puts [format("Corner mass: %d kg", corner_mass),
23
+ format("Normal force: %.1f N", normal_force),
24
+ format("Axle torque: %d Nm", axle_torque),
25
+ ].join("\n")
26
+ puts
27
+
28
+ traction = wheel.traction(normal_force)
29
+ drive_force = wheel.force(axle_torque)
30
+ inertial_loss = wheel.inertial_loss(axle_torque, supported_mass)
31
+ friction_loss *= axle_torque # 5% of the axle torque
32
+
33
+ # drive force = (axle torque - inertia - friction) limited by traction
34
+
35
+ net_axle_torque = axle_torque - inertial_loss - friction_loss
36
+ net_drive_force = wheel.force(net_axle_torque)
37
+ net_drive_force = traction if net_drive_force > traction # traction limited
38
+
39
+ acc = DrivingPhysics.acc(net_drive_force, supported_mass) # translational
40
+
41
+ puts [format("Traction: %.1f N", traction),
42
+ format("Drive force: %.1f N", drive_force),
43
+ format("Inertial loss: %.1f Nm", inertial_loss),
44
+ format("Friction loss: %.1f Nm", friction_loss),
45
+ format("Net Axle Torque: %.1f Nm", net_axle_torque),
46
+ format("Net Drive Force: %.1f N", net_drive_force),
47
+ format("Acceleration: %.1f m/s/s", acc),
48
+ format("Alpha: %.2f r/s/s", acc / wheel.radius_m),
49
+ ].join("\n")
50
+ puts
51
+
52
+ duration = 100 # sec
53
+
54
+ dist = 0.0 # meters
55
+ speed = 0.0 # meters/s
56
+
57
+ theta = 0.0 # radians
58
+ omega = 0.0 # radians/s
59
+
60
+ (duration * env.hz).times { |i|
61
+ # accumulate frictional losses with speed (omega)
62
+ omega_loss_cof = [wheel.omega_friction * omega, 1.0].min
63
+ slowed_acc = acc - acc * omega_loss_cof
64
+
65
+ # translational kinematics
66
+ speed += slowed_acc * env.tick
67
+ dist += speed * env.tick
68
+
69
+ # rotational kinematics
70
+ alpha = slowed_acc / wheel.radius_m
71
+ omega += alpha * env.tick
72
+ theta += omega * env.tick
73
+
74
+ if i < 10 or
75
+ (i < 10_000 and i%1000 == 0) or
76
+ (i % 10_000 == 0)
77
+ puts DrivingPhysics.elapsed_display(i)
78
+ puts format("Wheel: %.1f r %.2f r/s %.3f r/s^2", theta, omega, alpha)
79
+ puts format(" Car: %.1f m %.2f m/s %.3f m/s^2", dist, speed, slowed_acc)
80
+ puts format("Omega Frictional Loss: %.1f%%", omega_loss_cof * 100)
81
+ puts "Press [enter]"
82
+ gets
83
+ end
84
+ }
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'driving_physics'
3
+ s.summary = "WIP"
4
+ s.description = "WIP"
5
+ s.authors = ["Rick Hull"]
6
+ s.homepage = "https://github.com/rickhull/driving_physics"
7
+ s.license = "LGPL-3.0"
8
+
9
+ s.required_ruby_version = "> 2"
10
+
11
+ s.version = File.read(File.join(__dir__, 'VERSION')).chomp
12
+
13
+ s.files = %w[driving_physics.gemspec VERSION README.md Rakefile]
14
+ s.files += Dir['lib/**/*.rb']
15
+ s.files += Dir['test/**/*.rb']
16
+ s.files += Dir['demo/**/*.rb']
17
+ end