driving_physics 0.0.1.1 → 0.0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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