driving_physics 0.0.0.3 → 0.0.1.1
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 +4 -4
- data/README.md +12 -1
- data/VERSION +1 -1
- data/demo/car.rb +153 -0
- data/demo/disk.rb +76 -0
- data/demo/gearbox.rb +47 -0
- data/demo/motor.rb +89 -0
- data/demo/powertrain.rb +40 -0
- data/demo/scalar_force.rb +3 -3
- data/demo/tire.rb +88 -0
- data/demo/vector_force.rb +3 -3
- data/lib/driving_physics/car.rb +103 -0
- data/lib/driving_physics/cli.rb +17 -0
- data/lib/driving_physics/disk.rb +184 -0
- data/lib/driving_physics/gearbox.rb +109 -0
- data/lib/driving_physics/motor.rb +99 -0
- data/lib/driving_physics/power.rb +20 -0
- data/lib/driving_physics/powertrain.rb +39 -0
- data/lib/driving_physics/tire.rb +120 -0
- data/lib/driving_physics/vector_force.rb +15 -2
- data/lib/driving_physics.rb +1 -0
- data/test/disk.rb +129 -0
- data/test/{wheel.rb → tire.rb} +59 -55
- data/test/vector_force.rb +7 -1
- metadata +17 -4
- data/demo/wheel.rb +0 -84
- data/lib/driving_physics/wheel.rb +0 -191
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 102b8d70a0aa540ee6e56ca24987497c0f272473790180a9f8691418fd1557f6
|
4
|
+
data.tar.gz: d82c3924641569610a538804e9b7cd9df040f8487ff50158e0b227cbf529f3dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '08e0488d1a152f1ca7c432fa60c3bbb06f63ef5674c12ec898de7505e8c054a0eac75280b0b2c78985b1cacf6f7fd2925130a0c48b0564c15cbaca5036e68e30'
|
7
|
+
data.tar.gz: 1df9c64a0d59f48a4d9a13062166516456d685e18cf9f7bc684c0853bd9f71587460d98f14f4fc9c1f0e3a571ee325e09fe9538ab249ca4403071280b8a0eddb
|
data/README.md
CHANGED
@@ -6,7 +6,18 @@ Physical simulation of how to make a car go around a track quickly, in pure
|
|
6
6
|
Ruby with minimal dependencies. Physics from first principles, often
|
7
7
|
using `Vector` class from `matrix.rb` in stdlib.
|
8
8
|
|
9
|
-
|
9
|
+
## Work In Progress
|
10
|
+
|
11
|
+
This is very much a **Work In Progress**. Implemented so far:
|
12
|
+
|
13
|
+
* Spinning: rotational inertia and friction / hysteresis
|
14
|
+
* Tires: traction force via friction and the normal force
|
15
|
+
* Vectors: 2D Vector forces and 3D Vector torques
|
16
|
+
* Motor: torque curves, rotating mass with friction
|
17
|
+
* Gearbox: gear ratios, final drive, rotating mass with friction
|
18
|
+
* Powertrain: combines motor and gearbox
|
19
|
+
* Car: 4 tires, powertrain, extra mass presents a load to the powertrain
|
20
|
+
* Acceleration: via drive traction from "first principles"
|
10
21
|
|
11
22
|
## Rationale
|
12
23
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.1.1
|
data/demo/car.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'driving_physics/car'
|
2
|
+
require 'driving_physics/imperial'
|
3
|
+
require 'driving_physics/cli'
|
4
|
+
|
5
|
+
include DrivingPhysics
|
6
|
+
|
7
|
+
env = Environment.new
|
8
|
+
puts env
|
9
|
+
|
10
|
+
tire = Tire.new(env)
|
11
|
+
motor = Motor.new(env)
|
12
|
+
gearbox = Gearbox.new(env)
|
13
|
+
pt = Powertrain.new(motor, gearbox)
|
14
|
+
car = Car.new(tire: tire, powertrain: pt) { |c|
|
15
|
+
c.mass = 1050.0
|
16
|
+
c.frontal_area = 2.5
|
17
|
+
c.cd = 0.5
|
18
|
+
}
|
19
|
+
puts car
|
20
|
+
CLI.pause
|
21
|
+
|
22
|
+
duration = 120
|
23
|
+
|
24
|
+
speed = 0.0
|
25
|
+
dist = 0.0
|
26
|
+
|
27
|
+
tire_omega = 0.0
|
28
|
+
tire_theta = 0.0
|
29
|
+
|
30
|
+
crank_omega = 0.0
|
31
|
+
crank_theta = 0.0
|
32
|
+
|
33
|
+
t = Time.now
|
34
|
+
num_ticks = duration * env.hz + 1
|
35
|
+
|
36
|
+
clutch = :ok
|
37
|
+
phase = :ignition
|
38
|
+
flag = false
|
39
|
+
rpm = 0
|
40
|
+
puts <<EOF
|
41
|
+
|
42
|
+
#
|
43
|
+
# IGNITION
|
44
|
+
#
|
45
|
+
|
46
|
+
EOF
|
47
|
+
|
48
|
+
num_ticks.times { |i|
|
49
|
+
if phase == :ignition
|
50
|
+
# ignition phase
|
51
|
+
crank_alpha = motor.alpha(motor.starter_torque, omega: crank_omega)
|
52
|
+
crank_omega += crank_alpha * env.tick
|
53
|
+
crank_theta += crank_omega * env.tick
|
54
|
+
|
55
|
+
rpm = DrivingPhysics.rpm(crank_omega)
|
56
|
+
|
57
|
+
if i % 100 == 0 or rpm > motor.idle_rpm
|
58
|
+
puts DrivingPhysics.elapsed_display(i)
|
59
|
+
puts format("%d rad %d rad/s %d rad/s/s",
|
60
|
+
crank_theta, crank_omega, crank_alpha)
|
61
|
+
puts "RPM: #{rpm.round}"
|
62
|
+
puts
|
63
|
+
end
|
64
|
+
|
65
|
+
if rpm > motor.idle_rpm
|
66
|
+
pt.select_gear(1)
|
67
|
+
phase = :running
|
68
|
+
|
69
|
+
puts <<EOF
|
70
|
+
|
71
|
+
#
|
72
|
+
# RUNNING
|
73
|
+
#
|
74
|
+
|
75
|
+
EOF
|
76
|
+
end
|
77
|
+
elsif phase == :running
|
78
|
+
# track crank_alpha/omega/theta
|
79
|
+
|
80
|
+
ar = car.air_force(speed)
|
81
|
+
rr = car.tire_rolling_force(tire_omega)
|
82
|
+
rf = car.tire_rotational_force(tire_omega)
|
83
|
+
|
84
|
+
force = car.drive_force(rpm) + ar + rr + rf
|
85
|
+
|
86
|
+
|
87
|
+
ir = car.tire_inertial_force(force)
|
88
|
+
force += ir
|
89
|
+
|
90
|
+
acc = DrivingPhysics.acc(force, car.total_mass)
|
91
|
+
speed += acc * env.tick
|
92
|
+
dist += speed * env.tick
|
93
|
+
|
94
|
+
tire_alpha = acc / car.tire.radius
|
95
|
+
tire_omega += tire_alpha * env.tick
|
96
|
+
tire_theta += tire_omega * env.tick
|
97
|
+
|
98
|
+
crank_alpha = tire_alpha / car.powertrain.gearbox.ratio
|
99
|
+
crank_omega += crank_alpha * env.tick
|
100
|
+
crank_theta += crank_omega * env.tick
|
101
|
+
|
102
|
+
axle_torque = car.powertrain.axle_torque(rpm)
|
103
|
+
crank_torque = car.powertrain.motor.torque(rpm)
|
104
|
+
|
105
|
+
if flag or (i < 5000 and i % 100 == 0) or (i % 1000 == 0)
|
106
|
+
puts DrivingPhysics.elapsed_display(i)
|
107
|
+
puts format(" Tire: %.1f r %.2f r/s %.3f r/s^2",
|
108
|
+
tire_theta, tire_omega, tire_alpha)
|
109
|
+
puts format(" Car: %.1f m %.2f m/s %.3f m/s^2 (%.1f MPH)",
|
110
|
+
dist, speed, acc, Imperial.mph(speed))
|
111
|
+
puts format(" Motor: %d RPM %.1f Nm", rpm, crank_torque)
|
112
|
+
puts format(" Axle: %.1f Nm (%d N) Net Force: %.1f N",
|
113
|
+
axle_torque, car.drive_force(rpm), force)
|
114
|
+
puts "Resist: " + format(%w[Air Roll Spin Inertial].map { |s|
|
115
|
+
"#{s}: %.1f N"
|
116
|
+
}.join(' '), ar, rr, rf, ir)
|
117
|
+
puts
|
118
|
+
flag = false
|
119
|
+
end
|
120
|
+
|
121
|
+
# tire_omega determines new rpm
|
122
|
+
new_rpm = car.powertrain.crank_rpm(tire_omega)
|
123
|
+
new_clutch, proportion = car.powertrain.gearbox.match_rpms(rpm, new_rpm)
|
124
|
+
|
125
|
+
if new_clutch != clutch
|
126
|
+
flag = true
|
127
|
+
puts format("Clutch: [%s] %d RPM is %.1f%% from %d RPM",
|
128
|
+
new_clutch, new_rpm, proportion * 100, rpm)
|
129
|
+
clutch = new_clutch
|
130
|
+
CLI.pause
|
131
|
+
end
|
132
|
+
|
133
|
+
case new_clutch
|
134
|
+
when :ok
|
135
|
+
rpm = new_rpm
|
136
|
+
when :mismatch
|
137
|
+
flag = true
|
138
|
+
puts "LURCH!"
|
139
|
+
puts
|
140
|
+
rpm = new_rpm
|
141
|
+
end
|
142
|
+
next_gear = car.powertrain.gearbox.next_gear(rpm)
|
143
|
+
if next_gear != gearbox.gear
|
144
|
+
flag = true
|
145
|
+
puts "Gear Change: #{next_gear}"
|
146
|
+
car.powertrain.select_gear(next_gear)
|
147
|
+
CLI.pause
|
148
|
+
end
|
149
|
+
end
|
150
|
+
}
|
151
|
+
|
152
|
+
elapsed = Time.now - t
|
153
|
+
puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
|
data/demo/disk.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'driving_physics/disk'
|
2
|
+
require 'driving_physics/cli'
|
3
|
+
|
4
|
+
include DrivingPhysics
|
5
|
+
|
6
|
+
env = Environment.new
|
7
|
+
disk = Disk.new(env)
|
8
|
+
|
9
|
+
puts env
|
10
|
+
puts disk
|
11
|
+
puts
|
12
|
+
|
13
|
+
axle_torque = 50
|
14
|
+
alpha = disk.alpha(axle_torque)
|
15
|
+
drive_force = disk.force(axle_torque)
|
16
|
+
|
17
|
+
puts [format("Axle torque: %.1f Nm", axle_torque),
|
18
|
+
format(" Alpha: %.1f rad/s/s", alpha),
|
19
|
+
format("Drive force: %.1f N", drive_force),
|
20
|
+
].join("\n")
|
21
|
+
puts
|
22
|
+
CLI.pause
|
23
|
+
|
24
|
+
duration = 750 # sec
|
25
|
+
|
26
|
+
dist = 0.0 # meters
|
27
|
+
speed = 0.0 # meters/s
|
28
|
+
|
29
|
+
theta = 0.0 # radians
|
30
|
+
omega = 0.0 # radians/s
|
31
|
+
|
32
|
+
t = Time.now
|
33
|
+
elapsed = 0.0
|
34
|
+
num_ticks = duration * env.hz
|
35
|
+
|
36
|
+
num_ticks.times { |i|
|
37
|
+
# shut off the powah!
|
38
|
+
if i == 19_000
|
39
|
+
puts
|
40
|
+
puts " ### CUT POWER ###"
|
41
|
+
puts
|
42
|
+
axle_torque = 0
|
43
|
+
elapsed += Time.now - t
|
44
|
+
CLI.pause
|
45
|
+
t = Time.now
|
46
|
+
end
|
47
|
+
|
48
|
+
rotating_friction = disk.rotating_friction(omega)
|
49
|
+
net_torque = axle_torque + rotating_friction
|
50
|
+
net_force = disk.force(net_torque)
|
51
|
+
|
52
|
+
# rotational kinematics
|
53
|
+
alpha = disk.alpha(net_torque)
|
54
|
+
omega += alpha * env.tick
|
55
|
+
omega = 0.0 if omega.abs < 0.0001
|
56
|
+
theta += omega * env.tick
|
57
|
+
|
58
|
+
if i < 10 or
|
59
|
+
(i < 20_000 and i%1000 == 0) or
|
60
|
+
(i % 10_000 == 0) or
|
61
|
+
i == duration * env.hz - 1
|
62
|
+
|
63
|
+
puts DrivingPhysics.elapsed_display(i)
|
64
|
+
puts format(" Torque: %.1f Nm (%d Nm) Friction: %.1f Nm",
|
65
|
+
net_torque, axle_torque, rotating_friction)
|
66
|
+
puts format("Radians: %.1f r %.2f r/s %.3f r/s^2", theta, omega, alpha)
|
67
|
+
puts format(" Revs: %d revs %d revs/s %d rpm",
|
68
|
+
DrivingPhysics.revs(theta),
|
69
|
+
DrivingPhysics.revs(omega),
|
70
|
+
DrivingPhysics.rpm(omega))
|
71
|
+
puts
|
72
|
+
end
|
73
|
+
}
|
74
|
+
|
75
|
+
elapsed += Time.now - t
|
76
|
+
puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
|
data/demo/gearbox.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'driving_physics/gearbox'
|
2
|
+
require 'driving_physics/cli'
|
3
|
+
|
4
|
+
include DrivingPhysics
|
5
|
+
|
6
|
+
env = Environment.new
|
7
|
+
gearbox = Gearbox.new(env)
|
8
|
+
|
9
|
+
torque = 30
|
10
|
+
duration = 30
|
11
|
+
|
12
|
+
puts env
|
13
|
+
puts gearbox
|
14
|
+
puts
|
15
|
+
puts "Spin up the gearbox with #{torque} Nm of input torque"
|
16
|
+
puts "How fast will it go in #{duration} seconds?"
|
17
|
+
CLI.pause
|
18
|
+
|
19
|
+
# rotational kinematics
|
20
|
+
alpha = 0.0
|
21
|
+
omega = 0.0
|
22
|
+
theta = 0.0
|
23
|
+
|
24
|
+
(duration * env.hz + 1).times { |i|
|
25
|
+
# just for info, not used in the simulation
|
26
|
+
friction = gearbox.spinner.rotating_friction(omega)
|
27
|
+
|
28
|
+
# update rotational kinematics
|
29
|
+
# gearbox.alpha incorporates friction and inertia
|
30
|
+
alpha = gearbox.alpha(torque, omega: omega)
|
31
|
+
omega += alpha * env.tick
|
32
|
+
theta += omega * env.tick
|
33
|
+
|
34
|
+
net_torque = gearbox.implied_torque(alpha)
|
35
|
+
|
36
|
+
# periodic output
|
37
|
+
if i < 10 or
|
38
|
+
(i < 100 and i % 10 == 0) or
|
39
|
+
(i < 1000 and i % 100 == 0) or
|
40
|
+
i % 1000 == 0
|
41
|
+
puts DrivingPhysics.elapsed_display(i)
|
42
|
+
puts format("RPM %d Torque: %.3f Nm (%d Nm) Friction: %.3f Nm",
|
43
|
+
DrivingPhysics.rpm(omega), net_torque, torque, friction)
|
44
|
+
puts format("%.1f rad %.1f rad/s %.1f rad/s/s", theta, omega, alpha)
|
45
|
+
puts
|
46
|
+
end
|
47
|
+
}
|
data/demo/motor.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'driving_physics/motor'
|
2
|
+
require 'driving_physics/cli'
|
3
|
+
|
4
|
+
# fun idea for a different demo: keep increasing torque until idle is
|
5
|
+
# maintained
|
6
|
+
|
7
|
+
include DrivingPhysics
|
8
|
+
|
9
|
+
env = Environment.new
|
10
|
+
motor = Motor.new(env)
|
11
|
+
puts env
|
12
|
+
puts motor
|
13
|
+
|
14
|
+
puts "Rev it up!"
|
15
|
+
800.upto(7000) { |rpm|
|
16
|
+
next unless rpm % 200 == 0
|
17
|
+
tq = motor.torque(rpm).to_f
|
18
|
+
puts format("%s RPM: %s Nm\t%s",
|
19
|
+
rpm.to_s.rjust(4, ' '),
|
20
|
+
tq.round(1).to_s.rjust(5, ' '),
|
21
|
+
'#' * (tq.to_f / 10).round)
|
22
|
+
}
|
23
|
+
|
24
|
+
puts
|
25
|
+
puts "* Spin the motor up to #{motor.idle_rpm} RPM with the starter motor."
|
26
|
+
puts "* Rev it up with the throttle."
|
27
|
+
puts "* Let it die."
|
28
|
+
CLI.pause
|
29
|
+
|
30
|
+
alpha = 0.0
|
31
|
+
omega = 0.0
|
32
|
+
theta = 0.0
|
33
|
+
|
34
|
+
duration = 60
|
35
|
+
|
36
|
+
status = :ignition
|
37
|
+
rpm = 0
|
38
|
+
|
39
|
+
(duration * env.hz + 1).times { |i|
|
40
|
+
# this is an input torque; alpha is determined after inertia and friction
|
41
|
+
torque = case status
|
42
|
+
when :ignition
|
43
|
+
motor.starter_torque
|
44
|
+
when :running
|
45
|
+
motor.torque(rpm)
|
46
|
+
else
|
47
|
+
0
|
48
|
+
end
|
49
|
+
|
50
|
+
# Motor#alpha incorporates inertia and friction
|
51
|
+
alpha = motor.alpha(torque, omega: omega)
|
52
|
+
omega += alpha * env.tick
|
53
|
+
theta += omega * env.tick
|
54
|
+
|
55
|
+
net_torque = motor.implied_torque(alpha)
|
56
|
+
|
57
|
+
# prevent silly oscillations due to tiny floating point errors
|
58
|
+
omega = 0 if omega < 0.00001
|
59
|
+
rpm = DrivingPhysics.rpm(omega)
|
60
|
+
|
61
|
+
if rpm > motor.idle_rpm and status == :ignition
|
62
|
+
status = :running
|
63
|
+
flag = true
|
64
|
+
end
|
65
|
+
|
66
|
+
if rpm > 7000 and status == :running
|
67
|
+
status = :off
|
68
|
+
flag = true
|
69
|
+
end
|
70
|
+
|
71
|
+
if flag or
|
72
|
+
(i < 10) or
|
73
|
+
(i < 100 and i % 10 == 0) or
|
74
|
+
(i < 1000 and i % 100 == 0) or
|
75
|
+
(i < 10_000 and i % 500 == 0) or
|
76
|
+
i % 5000 == 0
|
77
|
+
puts DrivingPhysics.elapsed_display(i)
|
78
|
+
puts format("%d RPM %.1f Nm (%d Nm) Friction: %.1f Nm",
|
79
|
+
DrivingPhysics.rpm(omega),
|
80
|
+
net_torque,
|
81
|
+
torque,
|
82
|
+
motor.spinner.rotating_friction(omega))
|
83
|
+
puts format("%d rad %.1f rad/s %.1f rad/s/s", theta, omega, alpha)
|
84
|
+
puts
|
85
|
+
|
86
|
+
CLI.pause if flag
|
87
|
+
flag = false
|
88
|
+
end
|
89
|
+
}
|
data/demo/powertrain.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'driving_physics/powertrain'
|
2
|
+
require 'driving_physics/imperial'
|
3
|
+
require 'driving_physics/cli'
|
4
|
+
|
5
|
+
include DrivingPhysics
|
6
|
+
|
7
|
+
env = Environment.new
|
8
|
+
motor = Motor.new(env)
|
9
|
+
gearbox = Gearbox.new(env)
|
10
|
+
powertrain = Powertrain.new(motor, gearbox)
|
11
|
+
puts env
|
12
|
+
puts powertrain
|
13
|
+
CLI.pause
|
14
|
+
|
15
|
+
crank_alpha = 0.0
|
16
|
+
crank_omega = 0.0
|
17
|
+
|
18
|
+
axle_alpha = 0.0
|
19
|
+
axle_omega = 0.0
|
20
|
+
|
21
|
+
|
22
|
+
# Run through the gears
|
23
|
+
|
24
|
+
1.upto(6) { |gear|
|
25
|
+
powertrain.select_gear(gear)
|
26
|
+
|
27
|
+
puts <<EOF
|
28
|
+
|
29
|
+
# GEAR #{gear} (#{powertrain.gearbox.ratio})
|
30
|
+
#
|
31
|
+
EOF
|
32
|
+
|
33
|
+
800.upto(7000) { |rpm|
|
34
|
+
next unless rpm % 500 == 0
|
35
|
+
|
36
|
+
axle_torque = powertrain.axle_torque(rpm)
|
37
|
+
mph = Imperial.mph(Disk.tangential(powertrain.axle_omega(rpm), 0.32))
|
38
|
+
puts format("%d RPM: %.1f Nm %d mph", rpm, axle_torque, mph)
|
39
|
+
}
|
40
|
+
}
|
data/demo/scalar_force.rb
CHANGED
@@ -13,9 +13,9 @@ tick = 1.0 / DP::HZ
|
|
13
13
|
(duration * DP::HZ).times { |i|
|
14
14
|
nf = drive_force - DP::ScalarForce.all_resistance(spd, nf_mag: weight)
|
15
15
|
|
16
|
-
a = DP.
|
17
|
-
spd = DP.
|
18
|
-
pos = DP.
|
16
|
+
a = DP.acc(nf, mass)
|
17
|
+
spd = DP.vel(spd, a, dt: tick)
|
18
|
+
pos = DP.pos(pos, spd, dt: tick)
|
19
19
|
|
20
20
|
if i % DP::HZ == 0
|
21
21
|
puts [i / DP::HZ,
|
data/demo/tire.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'driving_physics/tire'
|
2
|
+
|
3
|
+
include DrivingPhysics
|
4
|
+
|
5
|
+
env = Environment.new
|
6
|
+
tire = Tire.new(env)
|
7
|
+
|
8
|
+
puts env
|
9
|
+
puts tire
|
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 * tire.mass
|
17
|
+
corner_mass = Rational(total_mass) / 4
|
18
|
+
normal_force = corner_mass * env.g
|
19
|
+
axle_torque = 1000 # N*m
|
20
|
+
|
21
|
+
puts [format("Corner mass: %d kg", corner_mass),
|
22
|
+
format("Normal force: %.1f N", normal_force),
|
23
|
+
format("Axle torque: %d Nm", axle_torque),
|
24
|
+
].join("\n")
|
25
|
+
puts
|
26
|
+
|
27
|
+
traction = tire.traction(normal_force)
|
28
|
+
drive_force = tire.force(axle_torque)
|
29
|
+
inertial_loss = tire.inertial_loss(axle_torque, driven_mass: supported_mass)
|
30
|
+
net_axle_torque = axle_torque - inertial_loss
|
31
|
+
net_drive_force = tire.force(net_axle_torque)
|
32
|
+
acc = DrivingPhysics.acc(net_drive_force, supported_mass) # translational
|
33
|
+
alpha = acc / tire.radius
|
34
|
+
|
35
|
+
puts [format("Traction: %.1f N", traction),
|
36
|
+
format("Drive force: %.1f N", drive_force),
|
37
|
+
format("Inertial loss: %.1f Nm", inertial_loss),
|
38
|
+
format("Net Axle Torque: %.1f Nm", net_axle_torque),
|
39
|
+
format("Net Drive Force: %.1f N", net_drive_force),
|
40
|
+
format("Acceleration: %.1f m/s/s", acc),
|
41
|
+
format("Alpha: %.2f r/s/s", alpha),
|
42
|
+
].join("\n")
|
43
|
+
puts
|
44
|
+
|
45
|
+
duration = 100 # sec
|
46
|
+
|
47
|
+
dist = 0.0 # meters
|
48
|
+
speed = 0.0 # meters/s
|
49
|
+
|
50
|
+
theta = 0.0 # radians
|
51
|
+
omega = 0.0 # radians/s
|
52
|
+
|
53
|
+
t = Time.now
|
54
|
+
num_ticks = duration * env.hz
|
55
|
+
|
56
|
+
num_ticks.times { |i|
|
57
|
+
torque = tire.net_tractable_torque(axle_torque,
|
58
|
+
mass: total_mass,
|
59
|
+
omega: omega,
|
60
|
+
normal_force: normal_force)
|
61
|
+
force = tire.force(torque)
|
62
|
+
|
63
|
+
# translational kinematics
|
64
|
+
acc = DrivingPhysics.acc(force, supported_mass)
|
65
|
+
speed += acc * env.tick
|
66
|
+
dist += speed * env.tick
|
67
|
+
|
68
|
+
# rotational kinematics
|
69
|
+
alpha = acc / tire.radius
|
70
|
+
omega += alpha * env.tick
|
71
|
+
theta += omega * env.tick
|
72
|
+
|
73
|
+
if i < 10 or
|
74
|
+
(i < 20_000 and i%1000 == 0) or
|
75
|
+
(i % 10_000 == 0) or
|
76
|
+
i == duration * env.hz - 1
|
77
|
+
|
78
|
+
puts DrivingPhysics.elapsed_display(i)
|
79
|
+
puts format(" Tire: %.1f r %.2f r/s %.3f r/s^2", theta, omega, alpha)
|
80
|
+
puts format(" Car: %.1f m %.2f m/s %.3f m/s^2", dist, speed, acc)
|
81
|
+
puts format("Torque: %.1f Nm (%d N) Loss: %.1f%%",
|
82
|
+
torque, force, (1.0 - torque / axle_torque) * 100)
|
83
|
+
puts
|
84
|
+
end
|
85
|
+
}
|
86
|
+
|
87
|
+
elapsed = Time.now - t
|
88
|
+
puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
|
data/demo/vector_force.rb
CHANGED
@@ -13,9 +13,9 @@ tick = 1.0 / DP::HZ
|
|
13
13
|
(duration * DP::HZ).times { |i|
|
14
14
|
nf = drive_force + DP::VectorForce.all_resistance(v, dir: v, nf_mag: weight)
|
15
15
|
|
16
|
-
a = DP.
|
17
|
-
v = DP.v
|
18
|
-
p = DP.p
|
16
|
+
a = DP.acc(nf, mass)
|
17
|
+
v = DP.vel(v, a, dt: tick)
|
18
|
+
p = DP.pos(p, v, dt: tick)
|
19
19
|
|
20
20
|
if i % DP::HZ == 0
|
21
21
|
puts [i / DP::HZ,
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'driving_physics/tire'
|
2
|
+
require 'driving_physics/powertrain'
|
3
|
+
|
4
|
+
module DrivingPhysics
|
5
|
+
class Car
|
6
|
+
attr_reader :tire, :powertrain, :env
|
7
|
+
attr_accessor :num_tires, :mass, :frontal_area, :cd
|
8
|
+
|
9
|
+
def initialize(tire:, powertrain:)
|
10
|
+
@num_tires = 4
|
11
|
+
@tire = tire
|
12
|
+
@env = @tire.env
|
13
|
+
@powertrain = powertrain
|
14
|
+
@mass = Rational(1000)
|
15
|
+
@frontal_area = DrivingPhysics::FRONTAL_AREA
|
16
|
+
@cd = DrivingPhysics::DRAG_COF
|
17
|
+
|
18
|
+
yield self if block_given?
|
19
|
+
end
|
20
|
+
|
21
|
+
# force opposing speed
|
22
|
+
def air_force(speed)
|
23
|
+
-0.5 * @frontal_area * @cd * @env.air_density * speed ** 2
|
24
|
+
end
|
25
|
+
|
26
|
+
# force of opposite sign to omega
|
27
|
+
def tire_rolling_force(omega)
|
28
|
+
@num_tires *
|
29
|
+
@tire.rolling_friction(omega, normal_force: self.normal_force) /
|
30
|
+
@tire.radius
|
31
|
+
end
|
32
|
+
|
33
|
+
# force of opposite sign to omega
|
34
|
+
def tire_rotational_force(omega)
|
35
|
+
@num_tires *
|
36
|
+
@tire.rotating_friction(omega, normal_force: self.normal_force) /
|
37
|
+
@tire.radius
|
38
|
+
end
|
39
|
+
|
40
|
+
# force of opposite sign to force
|
41
|
+
def tire_inertial_force(force)
|
42
|
+
mag = force.abs
|
43
|
+
sign = force / mag
|
44
|
+
force_loss = 0
|
45
|
+
5.times {
|
46
|
+
# use magnitude, so we are subtracting only positive numbers
|
47
|
+
acc = DrivingPhysics.acc(mag - force_loss, self.total_mass)
|
48
|
+
alpha = acc / @tire.radius
|
49
|
+
# this will be a positive number
|
50
|
+
force_loss = @num_tires * @tire.implied_torque(alpha) /
|
51
|
+
@tire.radius
|
52
|
+
}
|
53
|
+
# oppose initial force
|
54
|
+
-1 * sign * force_loss
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
[[format("Mass: %.1f kg", self.total_mass),
|
59
|
+
format("Fr.A: %.2f m^2", @frontal_area),
|
60
|
+
format("cD: %.2f", @cd),
|
61
|
+
].join(' | '),
|
62
|
+
format("Powertrain: %s", @powertrain),
|
63
|
+
format("Tires: %s", @tire),
|
64
|
+
format("Corner mass: %.1f kg | Normal force: %.1f N",
|
65
|
+
self.corner_mass, self.normal_force),
|
66
|
+
].join("\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
def drive_force(rpm)
|
70
|
+
@tire.force(@powertrain.axle_torque(rpm))
|
71
|
+
end
|
72
|
+
|
73
|
+
def tire_speed(rpm)
|
74
|
+
@tire.tangential(@powertrain.axle_omega(rpm))
|
75
|
+
end
|
76
|
+
|
77
|
+
def motor_rpm(tire_speed)
|
78
|
+
@powertrain.gearbox.crank_rpm(tire_speed / @tire_radius)
|
79
|
+
end
|
80
|
+
|
81
|
+
def total_mass
|
82
|
+
@mass + @tire.mass * @num_tires
|
83
|
+
end
|
84
|
+
|
85
|
+
def corner_mass
|
86
|
+
Rational(self.total_mass) / @num_tires
|
87
|
+
end
|
88
|
+
|
89
|
+
# per tire
|
90
|
+
def normal_force
|
91
|
+
self.corner_mass * @env.g
|
92
|
+
end
|
93
|
+
|
94
|
+
# per tire
|
95
|
+
def tire_traction
|
96
|
+
@tire.traction(self.normal_force)
|
97
|
+
end
|
98
|
+
|
99
|
+
def total_normal_force
|
100
|
+
self.total_mass * env.g
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module DrivingPhysics
|
2
|
+
module CLI
|
3
|
+
# returns user input as a string
|
4
|
+
def self.prompt(msg = '')
|
5
|
+
print msg + ' ' unless msg.empty?
|
6
|
+
print '> '
|
7
|
+
$stdin.gets.chomp
|
8
|
+
end
|
9
|
+
|
10
|
+
# press Enter to continue
|
11
|
+
def self.pause(msg = '')
|
12
|
+
puts msg unless msg.empty?
|
13
|
+
puts ' [ Press Enter ]'
|
14
|
+
$stdin.gets
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|