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