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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec6ff08e7731aa8bcb419efee9ab1a96ef7642f3426c7b8a501dae764692d777
4
- data.tar.gz: ae217c0e452a764392e5a4df9a0b3280369e21b92d6018ef8d8753c0a6f5f594
3
+ metadata.gz: 102b8d70a0aa540ee6e56ca24987497c0f272473790180a9f8691418fd1557f6
4
+ data.tar.gz: d82c3924641569610a538804e9b7cd9df040f8487ff50158e0b227cbf529f3dc
5
5
  SHA512:
6
- metadata.gz: b8844bf3f8c45aae742bc27cbfe8307b7f61f21a600d1eac7f34248aa9a980f9942f6e4ce51ec2b58834978e4ba1a06aa0153ba4323d4e3448b1f038c39495e2
7
- data.tar.gz: b41f09936f696ef4afe82f3be701409abddd6b92add4f39d5d1d42a1a412ce4204b08a411818c339bba6820858a0aae3d5324cdbabff76e2b2256e9228ae1120
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
- Note, this is very much a **Work In Progress**.
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.0.3
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
+ }
@@ -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.a(nf, mass)
17
- spd = DP.v(a, spd, dt: tick)
18
- pos = DP.p(spd, pos, dt: tick)
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.a(nf, mass)
17
- v = DP.v(a, v, dt: tick)
18
- p = DP.p(v, p, dt: tick)
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