driving_physics 0.0.1.1 → 0.0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 102b8d70a0aa540ee6e56ca24987497c0f272473790180a9f8691418fd1557f6
4
- data.tar.gz: d82c3924641569610a538804e9b7cd9df040f8487ff50158e0b227cbf529f3dc
3
+ metadata.gz: ef486c39982798a7331f49f601e648d520a65977161180631cc4c5d3340a9011
4
+ data.tar.gz: 9e30b74ae34cb77a711d5d9631983b0c070872900123a1dde2d3dd03bdab854b
5
5
  SHA512:
6
- metadata.gz: '08e0488d1a152f1ca7c432fa60c3bbb06f63ef5674c12ec898de7505e8c054a0eac75280b0b2c78985b1cacf6f7fd2925130a0c48b0564c15cbaca5036e68e30'
7
- data.tar.gz: 1df9c64a0d59f48a4d9a13062166516456d685e18cf9f7bc684c0853bd9f71587460d98f14f4fc9c1f0e3a571ee325e09fe9538ab249ca4403071280b8a0eddb
6
+ metadata.gz: 46eaf512dad64ca39ec20451d7acce1b1819ecd64125ee858f92c26e5edd7b0ec0e7ef01af8f73daa0d37868e0ffd76bd3b29478093219cd906d8d9e38026bcd
7
+ data.tar.gz: ae2e01ae8e7986c9d2e659ff5a0934ed5359e20cf2757d7724118bb59e0fb0199180249815f2f28f55f48a2336511a0b43f0d6fab042ed5d20a5ab6a897baef1
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1.1
1
+ 0.0.1.2
data/demo/car.rb CHANGED
@@ -10,9 +10,9 @@ puts env
10
10
  tire = Tire.new(env)
11
11
  motor = Motor.new(env)
12
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
13
+ powertrain = Powertrain.new(motor, gearbox)
14
+ car = Car.new(tire: tire, powertrain: powertrain) { |c|
15
+ c.body_mass = 850.0
16
16
  c.frontal_area = 2.5
17
17
  c.cd = 0.5
18
18
  }
@@ -21,16 +21,20 @@ CLI.pause
21
21
 
22
22
  duration = 120
23
23
 
24
+ acc = 0.0
24
25
  speed = 0.0
25
26
  dist = 0.0
26
27
 
28
+ tire_alpha = 0.0
27
29
  tire_omega = 0.0
28
30
  tire_theta = 0.0
29
31
 
32
+ crank_alpha = 0.0
30
33
  crank_omega = 0.0
31
34
  crank_theta = 0.0
32
35
 
33
- t = Time.now
36
+ start = Timer.now
37
+ paused = 0.0
34
38
  num_ticks = duration * env.hz + 1
35
39
 
36
40
  clutch = :ok
@@ -55,15 +59,16 @@ num_ticks.times { |i|
55
59
  rpm = DrivingPhysics.rpm(crank_omega)
56
60
 
57
61
  if i % 100 == 0 or rpm > motor.idle_rpm
58
- puts DrivingPhysics.elapsed_display(i)
62
+ puts Timer.display(i)
59
63
  puts format("%d rad %d rad/s %d rad/s/s",
60
64
  crank_theta, crank_omega, crank_alpha)
61
- puts "RPM: #{rpm.round}"
65
+ puts format("%d RPM %d Nm starter torque", rpm, motor.starter_torque)
62
66
  puts
63
67
  end
64
68
 
65
69
  if rpm > motor.idle_rpm
66
- pt.select_gear(1)
70
+ car.gear = 1
71
+ car.throttle = 1.0
67
72
  phase = :running
68
73
 
69
74
  puts <<EOF
@@ -77,13 +82,16 @@ EOF
77
82
  elsif phase == :running
78
83
  # track crank_alpha/omega/theta
79
84
 
85
+ # cut throttle after 60 s
86
+ car.throttle = 0 if i > 60 * env.hz and car.throttle == 1.0
87
+
80
88
  ar = car.air_force(speed)
81
89
  rr = car.tire_rolling_force(tire_omega)
82
90
  rf = car.tire_rotational_force(tire_omega)
83
91
 
92
+ # note, this fails if we're in neutral
84
93
  force = car.drive_force(rpm) + ar + rr + rf
85
94
 
86
-
87
95
  ir = car.tire_inertial_force(force)
88
96
  force += ir
89
97
 
@@ -103,11 +111,11 @@ EOF
103
111
  crank_torque = car.powertrain.motor.torque(rpm)
104
112
 
105
113
  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))
114
+ puts Timer.display(i)
115
+ puts format(" Tire: %.1f r/s/s %.2f r/s %.3f r",
116
+ tire_alpha, tire_omega, tire_theta)
117
+ puts format(" Car: %.1f m/s/s %.2f m/s %.3f m (%.1f MPH)",
118
+ acc, speed, dist, Imperial.mph(speed))
111
119
  puts format(" Motor: %d RPM %.1f Nm", rpm, crank_torque)
112
120
  puts format(" Axle: %.1f Nm (%d N) Net Force: %.1f N",
113
121
  axle_torque, car.drive_force(rpm), force)
@@ -127,7 +135,7 @@ EOF
127
135
  puts format("Clutch: [%s] %d RPM is %.1f%% from %d RPM",
128
136
  new_clutch, new_rpm, proportion * 100, rpm)
129
137
  clutch = new_clutch
130
- CLI.pause
138
+ paused += CLI.pause
131
139
  end
132
140
 
133
141
  case new_clutch
@@ -135,7 +143,9 @@ EOF
135
143
  rpm = new_rpm
136
144
  when :mismatch
137
145
  flag = true
138
- puts "LURCH!"
146
+ puts '#'
147
+ puts '# LURCH!'
148
+ puts '#'
139
149
  puts
140
150
  rpm = new_rpm
141
151
  end
@@ -143,11 +153,23 @@ EOF
143
153
  if next_gear != gearbox.gear
144
154
  flag = true
145
155
  puts "Gear Change: #{next_gear}"
146
- car.powertrain.select_gear(next_gear)
147
- CLI.pause
156
+ car.gear = next_gear
157
+ paused += CLI.pause
158
+ end
159
+
160
+ # maintain idle when revs drop
161
+ if car.throttle == 0 and rpm < motor.idle_rpm
162
+ phase = :idling
163
+ car.gear = 0
164
+ paused += CLI.pause
148
165
  end
166
+
167
+
168
+ elsif phase == :idling
169
+ # fake
170
+ rpm = motor.idle_rpm
171
+ break
149
172
  end
150
173
  }
151
174
 
152
- elapsed = Time.now - t
153
- puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
175
+ puts Timer.summary(start, num_ticks, paused)
data/demo/disk.rb CHANGED
@@ -19,6 +19,10 @@ puts [format("Axle torque: %.1f Nm", axle_torque),
19
19
  format("Drive force: %.1f N", drive_force),
20
20
  ].join("\n")
21
21
  puts
22
+
23
+ puts "* Spin up the disk with #{axle_torque} Nm of torque"
24
+ puts "* Cut the power at some point"
25
+ puts "* Observe"
22
26
  CLI.pause
23
27
 
24
28
  duration = 750 # sec
@@ -29,20 +33,20 @@ speed = 0.0 # meters/s
29
33
  theta = 0.0 # radians
30
34
  omega = 0.0 # radians/s
31
35
 
32
- t = Time.now
33
- elapsed = 0.0
34
- num_ticks = duration * env.hz
36
+ paused = 0.0 # seconds
37
+ num_ticks = duration * env.hz + 1
38
+ flag = false # to display current stats
39
+ start = Timer.now
35
40
 
36
41
  num_ticks.times { |i|
37
42
  # shut off the powah!
38
- if i == 19_000
39
- puts
40
- puts " ### CUT POWER ###"
43
+ if i == 18_950
44
+ flag = true
45
+ puts '#'
46
+ puts '# CUT POWER'
47
+ puts '#'
41
48
  puts
42
49
  axle_torque = 0
43
- elapsed += Time.now - t
44
- CLI.pause
45
- t = Time.now
46
50
  end
47
51
 
48
52
  rotating_friction = disk.rotating_friction(omega)
@@ -55,12 +59,12 @@ num_ticks.times { |i|
55
59
  omega = 0.0 if omega.abs < 0.0001
56
60
  theta += omega * env.tick
57
61
 
58
- if i < 10 or
62
+ if flag or i < 10 or
59
63
  (i < 20_000 and i%1000 == 0) or
60
64
  (i % 10_000 == 0) or
61
65
  i == duration * env.hz - 1
62
66
 
63
- puts DrivingPhysics.elapsed_display(i)
67
+ puts Timer.display(ms: i)
64
68
  puts format(" Torque: %.1f Nm (%d Nm) Friction: %.1f Nm",
65
69
  net_torque, axle_torque, rotating_friction)
66
70
  puts format("Radians: %.1f r %.2f r/s %.3f r/s^2", theta, omega, alpha)
@@ -69,8 +73,11 @@ num_ticks.times { |i|
69
73
  DrivingPhysics.revs(omega),
70
74
  DrivingPhysics.rpm(omega))
71
75
  puts
76
+ if flag
77
+ paused += CLI.pause
78
+ flag = false
79
+ end
72
80
  end
73
81
  }
74
82
 
75
- elapsed += Time.now - t
76
- puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
83
+ puts Timer.summary(start, num_ticks, paused)
data/demo/gearbox.rb CHANGED
@@ -21,6 +21,9 @@ alpha = 0.0
21
21
  omega = 0.0
22
22
  theta = 0.0
23
23
 
24
+ start = Timer.now
25
+ paused = 0
26
+
24
27
  (duration * env.hz + 1).times { |i|
25
28
  # just for info, not used in the simulation
26
29
  friction = gearbox.spinner.rotating_friction(omega)
@@ -38,10 +41,12 @@ theta = 0.0
38
41
  (i < 100 and i % 10 == 0) or
39
42
  (i < 1000 and i % 100 == 0) or
40
43
  i % 1000 == 0
41
- puts DrivingPhysics.elapsed_display(i)
44
+ puts Timer.display(i)
42
45
  puts format("RPM %d Torque: %.3f Nm (%d Nm) Friction: %.3f Nm",
43
46
  DrivingPhysics.rpm(omega), net_torque, torque, friction)
44
47
  puts format("%.1f rad %.1f rad/s %.1f rad/s/s", theta, omega, alpha)
45
48
  puts
46
49
  end
47
50
  }
51
+
52
+ puts Timer.summary(start, num_ticks, paused)
data/demo/motor.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  require 'driving_physics/motor'
2
2
  require 'driving_physics/cli'
3
+ require 'driving_physics/power'
3
4
 
4
- # fun idea for a different demo: keep increasing torque until idle is
5
- # maintained
5
+ # next fun idea: PID controller
6
6
 
7
7
  include DrivingPhysics
8
8
 
@@ -10,18 +10,44 @@ env = Environment.new
10
10
  motor = Motor.new(env)
11
11
  puts env
12
12
  puts motor
13
+ puts
13
14
 
14
15
  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)
16
+ motor.throttle = 1.0
17
+ [:torque, :power].each { |run|
18
+ puts
19
+ puts run.to_s.upcase + ':'
20
+
21
+ 800.upto(7000) { |rpm|
22
+ next unless rpm % 200 == 0
23
+ omega = DrivingPhysics.omega(rpm)
24
+ torque = motor.torque(rpm)
25
+ case run
26
+ when :torque
27
+ count = (torque.to_f / 10).round
28
+ char = 'T'
29
+ val = torque.round.to_s.rjust(5, ' ')
30
+ fmt = "%s Nm"
31
+ when :power
32
+ power = DrivingPhysics.power(torque, omega)
33
+ kw = power.to_f / 1000
34
+ count = (kw / 5).round
35
+ char = 'P'
36
+ val = kw.round(1).to_s.rjust(5, ' ')
37
+ fmt = "%s kW"
38
+ else
39
+ raise "unknown"
40
+ end
41
+ puts format("%s RPM: #{fmt}\t%s",
42
+ rpm.to_s.rjust(4, ' '),
43
+ val,
44
+ char * count)
45
+ }
22
46
  }
23
47
 
24
48
  puts
49
+ puts "Now, the simulation begins..."
50
+ puts "---"
25
51
  puts "* Spin the motor up to #{motor.idle_rpm} RPM with the starter motor."
26
52
  puts "* Rev it up with the throttle."
27
53
  puts "* Let it die."
@@ -36,12 +62,16 @@ duration = 60
36
62
  status = :ignition
37
63
  rpm = 0
38
64
 
39
- (duration * env.hz + 1).times { |i|
65
+ paused = 0.0
66
+ num_ticks = duration * env.hz + 1
67
+ start = Timer.now
68
+
69
+ num_ticks.times { |i|
40
70
  # this is an input torque; alpha is determined after inertia and friction
41
71
  torque = case status
42
72
  when :ignition
43
73
  motor.starter_torque
44
- when :running
74
+ when :running, :off_throttle, :idling
45
75
  motor.torque(rpm)
46
76
  else
47
77
  0
@@ -58,32 +88,54 @@ rpm = 0
58
88
  omega = 0 if omega < 0.00001
59
89
  rpm = DrivingPhysics.rpm(omega)
60
90
 
91
+ power = DrivingPhysics.power(net_torque, omega)
92
+
61
93
  if rpm > motor.idle_rpm and status == :ignition
62
94
  status = :running
63
95
  flag = true
64
96
  end
65
97
 
66
98
  if rpm > 7000 and status == :running
67
- status = :off
99
+ status = :off_throttle
100
+ motor.throttle = 0.0
68
101
  flag = true
69
102
  end
70
103
 
104
+ if rpm < 1000 and status == :off_throttle
105
+ status = :idling
106
+ motor.throttle = 0.06
107
+ flag = true
108
+ end
109
+
110
+ if status == :idling
111
+ if rpm < 999
112
+ motor.throttle += (1.0 - motor.throttle) * 0.005
113
+ elsif rpm > 1001
114
+ motor.throttle -= motor.throttle * 0.005
115
+ end
116
+ end
117
+
71
118
  if flag or
72
119
  (i < 10) or
73
120
  (i < 100 and i % 10 == 0) or
74
121
  (i < 1000 and i % 100 == 0) or
75
122
  (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",
123
+ (i % 5000 == 0) or
124
+ (status == :idling and i % 100 == 0)
125
+ puts Timer.display(i)
126
+ puts format("Throttle: %.1f%%", motor.throttle * 100)
127
+ puts format("%d RPM %.1f Nm (%d Nm) %.1f kW Friction: %.1f Nm",
79
128
  DrivingPhysics.rpm(omega),
80
129
  net_torque,
81
130
  torque,
131
+ power / 1000,
82
132
  motor.spinner.rotating_friction(omega))
83
133
  puts format("%d rad %.1f rad/s %.1f rad/s/s", theta, omega, alpha)
84
134
  puts
85
135
 
86
- CLI.pause if flag
136
+ paused += CLI.pause if flag
87
137
  flag = false
88
138
  end
89
139
  }
140
+
141
+ puts Timer.summary(start, num_ticks, paused)
data/demo/powertrain.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'driving_physics/powertrain'
2
2
  require 'driving_physics/imperial'
3
3
  require 'driving_physics/cli'
4
+ require 'driving_physics/power'
4
5
 
5
6
  include DrivingPhysics
6
7
 
@@ -8,6 +9,7 @@ env = Environment.new
8
9
  motor = Motor.new(env)
9
10
  gearbox = Gearbox.new(env)
10
11
  powertrain = Powertrain.new(motor, gearbox)
12
+ motor.throttle = 1.0
11
13
  puts env
12
14
  puts powertrain
13
15
  CLI.pause
@@ -18,23 +20,28 @@ crank_omega = 0.0
18
20
  axle_alpha = 0.0
19
21
  axle_omega = 0.0
20
22
 
21
-
22
23
  # Run through the gears
23
-
24
24
  1.upto(6) { |gear|
25
- powertrain.select_gear(gear)
25
+ gearbox.gear = gear
26
26
 
27
27
  puts <<EOF
28
28
 
29
- # GEAR #{gear} (#{powertrain.gearbox.ratio})
29
+ # GEAR #{gear} (#{gearbox.ratio})
30
30
  #
31
31
  EOF
32
32
 
33
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)
34
+ next unless rpm % 200 == 0
35
+
36
+ power, axle_torque, axle_omega = powertrain.output(rpm)
37
+ kw = power / 1000.to_f
38
+ mph = Imperial.mph(Disk.tangential(axle_omega, 0.32))
39
+ ps = Imperial.ps(kw)
40
+ puts format("%s RPM: %s Nm %s kW %s PS\t%s mph",
41
+ rpm.round.to_s.rjust(4, ' '),
42
+ axle_torque.round(1).to_s.rjust(5, ' '),
43
+ kw.round(1).to_s.rjust(5, ' '),
44
+ ps.round.to_s.rjust(4, ' '),
45
+ mph.round.to_s.rjust(3, ' '))
39
46
  }
40
47
  }
data/demo/scalar_force.rb CHANGED
@@ -1,26 +1,52 @@
1
1
  require 'driving_physics/scalar_force'
2
+ require 'driving_physics/environment'
3
+ require 'driving_physics/cli'
2
4
 
3
- DP = DrivingPhysics
5
+ include DrivingPhysics
6
+
7
+ env = Environment.new
8
+ puts env
4
9
 
5
- pos = 0 # m
6
- spd = 0 # m/s
7
10
  mass = 1000 # kg
8
- weight = mass * DP::G # N
11
+ weight = mass * env.g # N
9
12
  drive_force = 7000 # N
10
13
  duration = 100 # seconds
11
- tick = 1.0 / DP::HZ
12
14
 
13
- (duration * DP::HZ).times { |i|
14
- nf = drive_force - DP::ScalarForce.all_resistance(spd, nf_mag: weight)
15
+ puts format("Force: %d N Mass: %d kg for %d seconds",
16
+ drive_force, mass, duration)
17
+ CLI.pause
18
+
19
+ spd = 0.0 # m/s
20
+ pos = 0.0 # m
21
+ num_ticks = duration * env.hz + 1
22
+
23
+ flag = false
24
+ phase = :accelerate
25
+ paused = 0.0
26
+ start = Timer.now
27
+
28
+ num_ticks.times { |i|
29
+ # TODO: make the resistance force negative
30
+ net_force = drive_force + ScalarForce.all_resistance(spd, nf_mag: weight)
15
31
 
16
- a = DP.acc(nf, mass)
17
- spd = DP.vel(spd, a, dt: tick)
18
- pos = DP.pos(pos, spd, dt: tick)
32
+ acc = DrivingPhysics.acc(net_force, mass)
33
+ spd += acc * env.tick
34
+ pos += spd * env.tick
19
35
 
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")
36
+ if phase == :accelerate and spd.magnitude > 100
37
+ flag = true
38
+ phase = :coast
39
+ drive_force = 0
40
+ end
41
+
42
+ if flag or (i % 1000 == 0)
43
+ puts format("%d %.3f m/s/s %.2f m/s %.1f m",
44
+ i.to_f / env.hz, acc, spd, pos)
45
+ if flag
46
+ paused += CLI.pause
47
+ flag = false
48
+ end
25
49
  end
26
50
  }
51
+
52
+ puts Timer.summary(start, num_ticks, paused)
data/demo/tire.rb CHANGED
@@ -1,69 +1,69 @@
1
1
  require 'driving_physics/tire'
2
+ require 'driving_physics/imperial'
3
+ require 'driving_physics/cli'
2
4
 
3
5
  include DrivingPhysics
4
6
 
5
7
  env = Environment.new
6
8
  tire = Tire.new(env)
7
-
8
9
  puts env
9
10
  puts tire
11
+ puts
10
12
 
11
- # 1000 kg car
12
- # 4 tires
13
- # 250 kg per tire plus tire mass
13
+ duration = 100 # sec
14
+ axle_torque = 500 # N*m
15
+ supported_mass = 1500 # kg
16
+
17
+ puts "Given:"
18
+ puts "* #{axle_torque} Nm axle torque"
19
+ puts "* #{supported_mass} kg supported mass"
20
+ puts "* 4 tires"
21
+ puts "* #{duration} seconds"
22
+ puts
14
23
 
15
- supported_mass = 1000 # kg
16
24
  total_mass = supported_mass + 4 * tire.mass
17
25
  corner_mass = Rational(total_mass) / 4
18
26
  normal_force = corner_mass * env.g
19
- axle_torque = 1000 # N*m
20
27
 
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")
28
+ puts "Therefore:"
29
+ puts format("* %.1f kg total mass", total_mass)
30
+ puts format("* %.1f kg per corner", corner_mass)
31
+ puts format("* %.1f N normal force", normal_force)
25
32
  puts
26
33
 
27
34
  traction = tire.traction(normal_force)
28
35
  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
36
 
45
- duration = 100 # sec
37
+ puts "Tires:"
38
+ puts format("* %.1f N traction", traction)
39
+ puts format("* %.1f N drive force", drive_force)
46
40
 
47
- dist = 0.0 # meters
41
+ CLI.pause
42
+
43
+ acc = 0.0 # meters/s/s
48
44
  speed = 0.0 # meters/s
45
+ dist = 0.0 # meters
49
46
 
50
- theta = 0.0 # radians
47
+ alpha = 0.0 # radians/s/s
51
48
  omega = 0.0 # radians/s
49
+ theta = 0.0 # radians
52
50
 
53
- t = Time.now
54
- num_ticks = duration * env.hz
51
+ start = Timer.now
52
+ paused = 0.0
53
+ num_ticks = duration * env.hz + 1
55
54
 
56
55
  num_ticks.times { |i|
57
56
  torque = tire.net_tractable_torque(axle_torque,
58
- mass: total_mass,
59
- omega: omega,
60
- normal_force: normal_force)
57
+ mass: total_mass,
58
+ omega: omega,
59
+ normal_force: normal_force)
61
60
  force = tire.force(torque)
62
61
 
63
62
  # translational kinematics
64
- acc = DrivingPhysics.acc(force, supported_mass)
63
+ acc = DrivingPhysics.acc(force, total_mass)
65
64
  speed += acc * env.tick
66
65
  dist += speed * env.tick
66
+ mph = Imperial.mph(speed)
67
67
 
68
68
  # rotational kinematics
69
69
  alpha = acc / tire.radius
@@ -72,17 +72,16 @@ num_ticks.times { |i|
72
72
 
73
73
  if i < 10 or
74
74
  (i < 20_000 and i%1000 == 0) or
75
- (i % 10_000 == 0) or
76
- i == duration * env.hz - 1
75
+ (i % 10_000 == 0)
77
76
 
78
77
  puts DrivingPhysics.elapsed_display(i)
79
78
  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)
79
+ puts format(" Car: %.1f m %.2f m/s %.3f m/s^2 (%d mph)",
80
+ dist, speed, acc, mph)
81
81
  puts format("Torque: %.1f Nm (%d N) Loss: %.1f%%",
82
82
  torque, force, (1.0 - torque / axle_torque) * 100)
83
83
  puts
84
84
  end
85
85
  }
86
86
 
87
- elapsed = Time.now - t
88
- puts format("%.2f s (%d ticks / s)", elapsed, num_ticks / elapsed)
87
+ puts Timer.summary(start, num_ticks, paused)
data/demo/vector_force.rb CHANGED
@@ -1,26 +1,56 @@
1
1
  require 'driving_physics/vector_force'
2
+ require 'driving_physics/environment'
3
+ require 'driving_physics/cli'
2
4
 
3
- DP = DrivingPhysics
5
+ include DrivingPhysics
6
+
7
+ env = Environment.new
8
+ puts env
4
9
 
5
- p = Vector[0, 0] # m
6
- v = Vector[0, 0] # m/s
7
10
  mass = 1000 # kg
8
- weight = mass * DP::G # N
11
+ weight = mass * env.g # N
12
+ drive_force = DrivingPhysics.random_unit_vector * 7000 # N
9
13
  duration = 100 # seconds
10
- drive_force = DP.random_unit_vector * 7000 # N
11
- tick = 1.0 / DP::HZ
12
14
 
13
- (duration * DP::HZ).times { |i|
14
- nf = drive_force + DP::VectorForce.all_resistance(v, dir: v, nf_mag: weight)
15
+ puts format("Force: %d N Dir: %s",
16
+ drive_force.magnitude,
17
+ DrivingPhysics.compass_dir(drive_force.normalize))
18
+ puts format("Mass: %d kg for %d seconds", mass, duration)
19
+ CLI.pause
20
+
21
+ acc = Vector[0, 0] # m/s/s
22
+ vel = Vector[0, 0] # m/s
23
+ pos = Vector[0, 0] # m
24
+
25
+ num_ticks = duration * env.hz + 1
26
+
27
+ flag = false
28
+ phase = :accelerate
29
+ paused = 0.0
30
+ start = Timer.now
15
31
 
16
- a = DP.acc(nf, mass)
17
- v = DP.vel(v, a, dt: tick)
18
- p = DP.pos(p, v, dt: tick)
32
+ num_ticks.times { |i|
33
+ net_force = drive_force +
34
+ VectorForce.all_resistance(vel, dir: vel, nf_mag: weight)
19
35
 
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")
36
+ acc = DrivingPhysics.acc(net_force, mass)
37
+ vel += acc * env.tick
38
+ pos += vel * env.tick
39
+
40
+ if phase == :accelerate and vel.magnitude > 100
41
+ flag = true
42
+ phase = :coast
43
+ drive_force = Vector[0, 0]
44
+ end
45
+
46
+ if flag or (i % 1000 == 0)
47
+ puts format("%d %.3f m/s/s %.2f m/s %.1f m",
48
+ i.to_f / env.hz, acc.magnitude, vel.magnitude, pos.magnitude)
49
+ if flag
50
+ paused = CLI.pause
51
+ flag = false
52
+ end
25
53
  end
26
54
  }
55
+
56
+ puts Timer.summary(start, num_ticks, paused)
@@ -4,20 +4,40 @@ require 'driving_physics/powertrain'
4
4
  module DrivingPhysics
5
5
  class Car
6
6
  attr_reader :tire, :powertrain, :env
7
- attr_accessor :num_tires, :mass, :frontal_area, :cd
7
+ attr_accessor :num_tires, :body_mass, :frontal_area, :cd
8
8
 
9
9
  def initialize(tire:, powertrain:)
10
10
  @num_tires = 4
11
11
  @tire = tire
12
12
  @env = @tire.env
13
13
  @powertrain = powertrain
14
- @mass = Rational(1000)
14
+ @body_mass = 1000.0
15
15
  @frontal_area = DrivingPhysics::FRONTAL_AREA
16
16
  @cd = DrivingPhysics::DRAG_COF
17
17
 
18
18
  yield self if block_given?
19
19
  end
20
20
 
21
+ def throttle
22
+ @powertrain.motor.throttle
23
+ end
24
+
25
+ def throttle=(val)
26
+ @powertrain.motor.throttle = val
27
+ end
28
+
29
+ def gear
30
+ @powertrain.gearbox.gear
31
+ end
32
+
33
+ def gear=(val)
34
+ @powertrain.gearbox.gear = val
35
+ end
36
+
37
+ def top_gear
38
+ @powertrain.gearbox.top_gear
39
+ end
40
+
21
41
  # force opposing speed
22
42
  def air_force(speed)
23
43
  -0.5 * @frontal_area * @cd * @env.air_density * speed ** 2
@@ -59,7 +79,7 @@ module DrivingPhysics
59
79
  format("Fr.A: %.2f m^2", @frontal_area),
60
80
  format("cD: %.2f", @cd),
61
81
  ].join(' | '),
62
- format("Powertrain: %s", @powertrain),
82
+ format("POWERTRAIN:\n%s", @powertrain),
63
83
  format("Tires: %s", @tire),
64
84
  format("Corner mass: %.1f kg | Normal force: %.1f N",
65
85
  self.corner_mass, self.normal_force),
@@ -79,7 +99,7 @@ module DrivingPhysics
79
99
  end
80
100
 
81
101
  def total_mass
82
- @mass + @tire.mass * @num_tires
102
+ @body_mass + @powertrain.mass + @tire.mass * @num_tires
83
103
  end
84
104
 
85
105
  def corner_mass
@@ -7,11 +7,45 @@ module DrivingPhysics
7
7
  $stdin.gets.chomp
8
8
  end
9
9
 
10
- # press Enter to continue
10
+ # press Enter to continue, ignore input, return elapsed time
11
11
  def self.pause(msg = '')
12
+ t = Timer.now
12
13
  puts msg unless msg.empty?
13
14
  puts ' [ Press Enter ]'
14
15
  $stdin.gets
16
+ Timer.since(t)
17
+ end
18
+ end
19
+
20
+ module Timer
21
+ if defined? Process::CLOCK_MONOTONIC
22
+ def self.now
23
+ Process.clock_gettime Process::CLOCK_MONOTONIC
24
+ end
25
+ else
26
+ def self.now
27
+ Time.now
28
+ end
29
+ end
30
+
31
+ def self.since(t)
32
+ self.now - t
33
+ end
34
+
35
+ def self.elapsed(&work)
36
+ t = self.now
37
+ return yield, self.since(t)
38
+ end
39
+
40
+ # HH:MM:SS.mmm
41
+ def self.display(seconds: 0, ms: 0)
42
+ ms += (seconds * 1000).round if seconds > 0
43
+ DrivingPhysics.elapsed_display(ms)
44
+ end
45
+
46
+ def self.summary(start, num_ticks, paused = 0)
47
+ elapsed = self.since(start) - paused
48
+ format("%.3f s (%d ticks/s)", elapsed, num_ticks.to_f / elapsed)
15
49
  end
16
50
  end
17
51
  end
@@ -130,8 +130,9 @@ module DrivingPhysics
130
130
  @normal_force
131
131
  end
132
132
 
133
- def alpha(torque)
134
- torque / self.rotational_inertia
133
+ def alpha(torque, omega: 0, normal_force: nil)
134
+ (torque - self.rotating_friction(omega, normal_force: normal_force)) /
135
+ self.rotational_inertia
135
136
  end
136
137
 
137
138
  def implied_torque(alpha)
@@ -175,10 +176,10 @@ module DrivingPhysics
175
176
  # maybe not physically faithful but close enough
176
177
  def rotating_friction(omega, normal_force: nil)
177
178
  return omega if omega.zero?
179
+ normal_force = self.normal_force if normal_force.nil?
178
180
  mag = omega.abs
179
181
  sign = omega / mag
180
- -1 * sign * (normal_force || self.normal_force) *
181
- (@base_friction + @omega_friction * mag)
182
+ -1 * sign * normal_force * (@base_friction + mag * @omega_friction)
182
183
  end
183
184
  end
184
185
  end
@@ -7,6 +7,7 @@ module DrivingPhysics
7
7
  MPH = (FEET_PER_METER / FEET_PER_MILE) * SECS_PER_HOUR
8
8
  CI_PER_LITER = 61.024
9
9
  GAL_PER_LITER = 0.264172
10
+ PS_PER_KW = 1.3596216173039
10
11
 
11
12
  def self.feet(meters)
12
13
  meters * FEET_PER_METER
@@ -36,6 +37,11 @@ module DrivingPhysics
36
37
  DP::kph(mps(mph))
37
38
  end
38
39
 
40
+ # convert kilowatts to horsepower
41
+ def self.ps(kw)
42
+ kw * PS_PER_KW
43
+ end
44
+
39
45
  def self.deg_c(deg_f)
40
46
  (deg_f - 32).to_f * 5 / 9
41
47
  end
@@ -6,10 +6,11 @@ module DrivingPhysics
6
6
  class OverRev < RuntimeError; end
7
7
  class SanityCheck < RuntimeError; end
8
8
 
9
- TORQUES = [ 0, 50, 130, 200, 250, 320, 330, 320, 260, 0]
9
+ TORQUES = [ 0, 70, 130, 200, 250, 320, 330, 320, 260, 0]
10
10
  RPMS = [500, 1000, 1500, 2000, 2500, 3500, 5000, 6000, 7000, 7100]
11
+ ENGINE_BRAKING = 0.2 # 20% of the torque at a given RPM
11
12
 
12
- attr_reader :env
13
+ attr_reader :env, :throttle
13
14
  attr_accessor :torques, :rpms, :fixed_mass,
14
15
  :spinner, :starter_torque, :idle_rpm
15
16
 
@@ -29,10 +30,12 @@ module DrivingPhysics
29
30
  }
30
31
  @starter_torque = 500 # Nm
31
32
  @idle_rpm = 1000 # RPM
33
+ @throttle = 0.0 # 0.0 - 1.0 (0% - 100%)
32
34
  end
33
35
 
34
36
  def to_s
35
- ary = ["Torque:"]
37
+ ary = [format("Throttle: %.1f%%", @throttle * 100)]
38
+ ary << "Torque:"
36
39
  @rpms.each_with_index { |r, i|
37
40
  ary << format("%s Nm %s RPM",
38
41
  @torques[i].round(1).to_s.rjust(4, ' '),
@@ -51,6 +54,13 @@ module DrivingPhysics
51
54
  @spinner.mass + @fixed_mass
52
55
  end
53
56
 
57
+ def throttle=(val)
58
+ if val < 0.0 or val > 1.0
59
+ raise(ArgumentError, "val #{val.inspect} should be between 0 and 1")
60
+ end
61
+ @throttle = val
62
+ end
63
+
54
64
  # given torque, determine crank alpha after inertia and friction
55
65
  def alpha(torque, omega: 0)
56
66
  @spinner.alpha(torque + @spinner.rotating_friction(omega))
@@ -60,17 +70,6 @@ module DrivingPhysics
60
70
  @spinner.implied_torque(alpha)
61
71
  end
62
72
 
63
- # How much torque is required to accelerate spinner up to alpha,
64
- # overcoming both inertia and friction
65
- # Presumably we have more input torque available, but this will be
66
- # used to do more work than just spinning up the motor
67
- #
68
- #def resistance_torque(alpha, omega)
69
- # # reverse sign on inertial_torque as it is not modeled as a resistance
70
- # -1 * @spinner.inertial_torque(alpha) +
71
- # @spinner.rotating_friction(omega)
72
- #end
73
-
74
73
  # interpolate based on torque curve points
75
74
  def torque(rpm)
76
75
  raise(Stall, "RPM #{rpm}") if rpm < @rpms.first
@@ -78,22 +77,27 @@ module DrivingPhysics
78
77
 
79
78
  last_rpm = 99999
80
79
  last_tq = -1
80
+ torque = nil
81
81
 
82
82
  # ew; there must be a better way
83
83
  @rpms.each_with_index { |r, i|
84
84
  tq = @torques[i]
85
85
  if last_rpm <= rpm and rpm <= r
86
86
  proportion = Rational(rpm - last_rpm) / (r - last_rpm)
87
- return last_tq + (tq - last_tq) * proportion
87
+ torque = last_tq + (tq - last_tq) * proportion
88
+ break
88
89
  end
89
90
  last_rpm = r
90
91
  last_tq = tq
91
92
  }
92
- raise(SanityCheck, "RPM #{rpm}")
93
- end
94
-
95
- def net_torque(rpm, alpha: 0)
96
- torque(rpm) + resistance_torque(alpha, DrivingPhysics.omega(rpm))
93
+ raise(SanityCheck, "RPM #{rpm}") if torque.nil?
94
+
95
+ if (@throttle <= 0.05) and (rpm > @idle_rpm * 1.5)
96
+ # engine braking: 20% of torque
97
+ -1 * torque * ENGINE_BRAKING
98
+ else
99
+ torque * @throttle
100
+ end
97
101
  end
98
102
  end
99
103
  end
@@ -2,6 +2,11 @@ require 'driving_physics/motor'
2
2
  require 'driving_physics/gearbox'
3
3
 
4
4
  module DrivingPhysics
5
+ # Powertrain right now is pretty simple. It combines the motor with the
6
+ # gearbox. It is focused on operations that require or involve both
7
+ # components. It does not pass through operations to the motor or gearbox.
8
+ # Instead, it provides direct access to each component.
9
+ #
5
10
  class Powertrain
6
11
  attr_reader :motor, :gearbox
7
12
 
@@ -10,12 +15,18 @@ module DrivingPhysics
10
15
  @gearbox = gearbox
11
16
  end
12
17
 
18
+ def mass
19
+ @motor.mass + @gearbox.mass
20
+ end
21
+
13
22
  def to_s
14
23
  ["\t[MOTOR]", @motor, "\t[GEARBOX]", @gearbox].join("\n")
15
24
  end
16
25
 
17
- def select_gear(gear)
18
- @gearbox.gear = gear
26
+ # returns [power, torque, omega]
27
+ def output(rpm)
28
+ t, o = self.axle_torque(rpm), self.axle_omega(rpm)
29
+ [t * o, t, o]
19
30
  end
20
31
 
21
32
  def axle_torque(rpm)
@@ -18,20 +18,23 @@ module DrivingPhysics
18
18
  # Note: here we only consider speed; we're in a 1D world for now
19
19
  #
20
20
 
21
+ # opposes the direction of speed
21
22
  def self.air_resistance(speed,
22
23
  frontal_area: FRONTAL_AREA,
23
24
  drag_cof: DRAG_COF,
24
25
  air_density: AIR_DENSITY)
25
- 0.5 * frontal_area * drag_cof * air_density * speed ** 2
26
+ -1 * 0.5 * frontal_area * drag_cof * air_density * speed ** 2
26
27
  end
27
28
 
29
+ # opposes the direction of speed
28
30
  def self.rotational_resistance(speed, rot_cof: ROT_COF)
29
- speed * rot_cof
31
+ -1 * speed * rot_cof
30
32
  end
31
33
 
34
+ # opposes the direction of speed
32
35
  # normal force is not always mass * G, e.g. aero downforce
33
36
  def self.rolling_resistance(normal_force, roll_cof: ROLL_COF)
34
- normal_force * roll_cof
37
+ -1 * normal_force * roll_cof
35
38
  end
36
39
 
37
40
  #
@@ -83,7 +83,7 @@ module DrivingPhysics
83
83
  5.times {
84
84
  acc = DrivingPhysics.acc(drive_force - force_loss, driven_mass)
85
85
  alpha = acc / @radius
86
- force_loss = self.inertial_torque(alpha) / @radius
86
+ force_loss = self.implied_torque(alpha) / @radius
87
87
  }
88
88
  force_loss * @radius
89
89
  end
@@ -36,6 +36,7 @@ module DrivingPhysics
36
36
  MINS_PER_HOUR = 60
37
37
  SECS_PER_HOUR = SECS_PER_MIN * MINS_PER_HOUR
38
38
 
39
+ # HH::MM::SS.mmm
39
40
  def self.elapsed_display(elapsed_ms)
40
41
  elapsed_s, ms = elapsed_ms.divmod 1000
41
42
 
data/test/scalar_force.rb CHANGED
@@ -6,25 +6,27 @@ include DrivingPhysics
6
6
  describe ScalarForce do
7
7
  # i.e. multiply this number times speed^2 to approximate drag force
8
8
  it "calculates a reasonable drag constant" do
9
- expect(ScalarForce.air_resistance 1).must_be_within_epsilon DRAG
9
+ expect(ScalarForce.air_resistance 1).must_be_within_epsilon(-1 * DRAG)
10
10
  end
11
11
 
12
12
  # ROT_COF's value is from observing that rotational resistance
13
13
  # matches air resistance at roughly 30 m/s in street cars
14
14
  it "approximates a reasonable rotational resistance constant" do
15
- expect(30 * ScalarForce.air_resistance(1)).must_be_within_epsilon ROT_COF
15
+ expect(30 * ScalarForce.air_resistance(1)).
16
+ must_be_within_epsilon(-1 * ROT_COF)
16
17
  end
17
18
 
18
19
  it "approximates a positive drag force" do
19
- expect(ScalarForce.air_resistance 30).must_be_within_epsilon 383.13
20
+ expect(ScalarForce.air_resistance 30).must_be_within_epsilon(-383.13)
20
21
  end
21
22
 
22
23
  it "approximates a positive rotational resistance force" do
23
- expect(ScalarForce.rotational_resistance 30).must_be_within_epsilon 383.13
24
+ expect(ScalarForce.rotational_resistance 30).
25
+ must_be_within_epsilon(-383.13)
24
26
  end
25
27
 
26
28
  it "approximates a positive rolling resistance force" do
27
29
  nf = 1000 * G
28
- expect(ScalarForce.rolling_resistance nf).must_be_within_epsilon 98.0
30
+ expect(ScalarForce.rolling_resistance nf).must_be_within_epsilon(-98.0)
29
31
  end
30
32
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: driving_physics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.1
4
+ version: 0.0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rick Hull