driving_physics 0.0.0.2 → 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: 4b32478386d13b88eb67f9d2bb826c23dd06dd5100d807aa918cea6af962c6e3
4
- data.tar.gz: 3ebcad787d058dac2d36718d46e5cf400dcc968f06427d2f65af4d1682f99166
3
+ metadata.gz: ef486c39982798a7331f49f601e648d520a65977161180631cc4c5d3340a9011
4
+ data.tar.gz: 9e30b74ae34cb77a711d5d9631983b0c070872900123a1dde2d3dd03bdab854b
5
5
  SHA512:
6
- metadata.gz: 4ce4c55bbb0e9e5f12146856c45df05860443b18d7fd54c173e8b3026e55e1c8ccdfa3e9d48a6974af5f28c79e6df04b487c9728d5a831ec88d657a7ce997db1
7
- data.tar.gz: 28ea2e0361cc180a5a405ed66caffcced3d1669ec18b87ce14f3ab0d77d130de9bd92864a2b78bddbcee236fff7bd982d66272456c4ba81eec43b863691e044d
6
+ metadata.gz: 46eaf512dad64ca39ec20451d7acce1b1819ecd64125ee858f92c26e5edd7b0ec0e7ef01af8f73daa0d37868e0ffd76bd3b29478093219cd906d8d9e38026bcd
7
+ data.tar.gz: ae2e01ae8e7986c9d2e659ff5a0934ed5359e20cf2757d7724118bb59e0fb0199180249815f2f28f55f48a2336511a0b43f0d6fab042ed5d20a5ab6a897baef1
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/Rakefile CHANGED
@@ -32,10 +32,13 @@ rescue LoadError
32
32
  end
33
33
 
34
34
  begin
35
- require 'flay_task'
36
- FlayTask.new do |t|
37
- t.dirs = ['lib']
38
- t.verbose = true
35
+ # need to stop looking in old/ and also the scoring seems wack
36
+ if false
37
+ require 'flay_task'
38
+ FlayTask.new do |t|
39
+ t.dirs = ['lib']
40
+ t.verbose = true
41
+ end
39
42
  end
40
43
  rescue LoadError
41
44
  warn 'flay_task unavailable'
@@ -49,6 +52,10 @@ rescue LoadError
49
52
  warn "roodi_task unavailable"
50
53
  end
51
54
 
55
+ #
56
+ # GEM BUILD / PUBLISH
57
+ #
58
+
52
59
  begin
53
60
  require 'buildar'
54
61
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0.2
1
+ 0.0.1.2
data/demo/car.rb CHANGED
@@ -1,28 +1,175 @@
1
1
  require 'driving_physics/car'
2
+ require 'driving_physics/imperial'
3
+ require 'driving_physics/cli'
2
4
 
3
- DP = DrivingPhysics
4
-
5
- env = DP::Environment.new
6
- car = DP::Car.new(env)
7
- car.add_fuel 10
8
- duration = 120 # seconds
5
+ include DrivingPhysics
9
6
 
7
+ env = Environment.new
10
8
  puts env
11
- puts
9
+
10
+ tire = Tire.new(env)
11
+ motor = Motor.new(env)
12
+ gearbox = Gearbox.new(env)
13
+ powertrain = Powertrain.new(motor, gearbox)
14
+ car = Car.new(tire: tire, powertrain: powertrain) { |c|
15
+ c.body_mass = 850.0
16
+ c.frontal_area = 2.5
17
+ c.cd = 0.5
18
+ }
12
19
  puts car
20
+ CLI.pause
21
+
22
+ duration = 120
23
+
24
+ acc = 0.0
25
+ speed = 0.0
26
+ dist = 0.0
27
+
28
+ tire_alpha = 0.0
29
+ tire_omega = 0.0
30
+ tire_theta = 0.0
31
+
32
+ crank_alpha = 0.0
33
+ crank_omega = 0.0
34
+ crank_theta = 0.0
35
+
36
+ start = Timer.now
37
+ paused = 0.0
38
+ num_ticks = duration * env.hz + 1
39
+
40
+ clutch = :ok
41
+ phase = :ignition
42
+ flag = false
43
+ rpm = 0
44
+ puts <<EOF
45
+
46
+ #
47
+ # IGNITION
48
+ #
49
+
50
+ EOF
51
+
52
+ num_ticks.times { |i|
53
+ if phase == :ignition
54
+ # ignition phase
55
+ crank_alpha = motor.alpha(motor.starter_torque, omega: crank_omega)
56
+ crank_omega += crank_alpha * env.tick
57
+ crank_theta += crank_omega * env.tick
58
+
59
+ rpm = DrivingPhysics.rpm(crank_omega)
60
+
61
+ if i % 100 == 0 or rpm > motor.idle_rpm
62
+ puts Timer.display(i)
63
+ puts format("%d rad %d rad/s %d rad/s/s",
64
+ crank_theta, crank_omega, crank_alpha)
65
+ puts format("%d RPM %d Nm starter torque", rpm, motor.starter_torque)
66
+ puts
67
+ end
68
+
69
+ if rpm > motor.idle_rpm
70
+ car.gear = 1
71
+ car.throttle = 1.0
72
+ phase = :running
73
+
74
+ puts <<EOF
75
+
76
+ #
77
+ # RUNNING
78
+ #
79
+
80
+ EOF
81
+ end
82
+ elsif phase == :running
83
+ # track crank_alpha/omega/theta
84
+
85
+ # cut throttle after 60 s
86
+ car.throttle = 0 if i > 60 * env.hz and car.throttle == 1.0
87
+
88
+ ar = car.air_force(speed)
89
+ rr = car.tire_rolling_force(tire_omega)
90
+ rf = car.tire_rotational_force(tire_omega)
91
+
92
+ # note, this fails if we're in neutral
93
+ force = car.drive_force(rpm) + ar + rr + rf
94
+
95
+ ir = car.tire_inertial_force(force)
96
+ force += ir
13
97
 
14
- car.controls.drive_pedal = 1.0
98
+ acc = DrivingPhysics.acc(force, car.total_mass)
99
+ speed += acc * env.tick
100
+ dist += speed * env.tick
15
101
 
16
- (duration * env.hz).times { |i|
17
- car.tick!
18
- if i % env.hz == 0
19
- if car.sum_forces.magnitude < 1
20
- car.controls.drive_pedal = 0.0
21
- car.controls.brake_pedal = 1.0
102
+ tire_alpha = acc / car.tire.radius
103
+ tire_omega += tire_alpha * env.tick
104
+ tire_theta += tire_omega * env.tick
105
+
106
+ crank_alpha = tire_alpha / car.powertrain.gearbox.ratio
107
+ crank_omega += crank_alpha * env.tick
108
+ crank_theta += crank_omega * env.tick
109
+
110
+ axle_torque = car.powertrain.axle_torque(rpm)
111
+ crank_torque = car.powertrain.motor.torque(rpm)
112
+
113
+ if flag or (i < 5000 and i % 100 == 0) or (i % 1000 == 0)
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))
119
+ puts format(" Motor: %d RPM %.1f Nm", rpm, crank_torque)
120
+ puts format(" Axle: %.1f Nm (%d N) Net Force: %.1f N",
121
+ axle_torque, car.drive_force(rpm), force)
122
+ puts "Resist: " + format(%w[Air Roll Spin Inertial].map { |s|
123
+ "#{s}: %.1f N"
124
+ }.join(' '), ar, rr, rf, ir)
125
+ puts
126
+ flag = false
22
127
  end
23
- puts
24
- puts "[t = #{i / env.hz}]"
25
- puts car
26
- gets if i % (env.hz * 10) == 0
128
+
129
+ # tire_omega determines new rpm
130
+ new_rpm = car.powertrain.crank_rpm(tire_omega)
131
+ new_clutch, proportion = car.powertrain.gearbox.match_rpms(rpm, new_rpm)
132
+
133
+ if new_clutch != clutch
134
+ flag = true
135
+ puts format("Clutch: [%s] %d RPM is %.1f%% from %d RPM",
136
+ new_clutch, new_rpm, proportion * 100, rpm)
137
+ clutch = new_clutch
138
+ paused += CLI.pause
139
+ end
140
+
141
+ case new_clutch
142
+ when :ok
143
+ rpm = new_rpm
144
+ when :mismatch
145
+ flag = true
146
+ puts '#'
147
+ puts '# LURCH!'
148
+ puts '#'
149
+ puts
150
+ rpm = new_rpm
151
+ end
152
+ next_gear = car.powertrain.gearbox.next_gear(rpm)
153
+ if next_gear != gearbox.gear
154
+ flag = true
155
+ puts "Gear Change: #{next_gear}"
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
165
+ end
166
+
167
+
168
+ elsif phase == :idling
169
+ # fake
170
+ rpm = motor.idle_rpm
171
+ break
27
172
  end
28
173
  }
174
+
175
+ puts Timer.summary(start, num_ticks, paused)
data/demo/disk.rb ADDED
@@ -0,0 +1,83 @@
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
+
23
+ puts "* Spin up the disk with #{axle_torque} Nm of torque"
24
+ puts "* Cut the power at some point"
25
+ puts "* Observe"
26
+ CLI.pause
27
+
28
+ duration = 750 # sec
29
+
30
+ dist = 0.0 # meters
31
+ speed = 0.0 # meters/s
32
+
33
+ theta = 0.0 # radians
34
+ omega = 0.0 # radians/s
35
+
36
+ paused = 0.0 # seconds
37
+ num_ticks = duration * env.hz + 1
38
+ flag = false # to display current stats
39
+ start = Timer.now
40
+
41
+ num_ticks.times { |i|
42
+ # shut off the powah!
43
+ if i == 18_950
44
+ flag = true
45
+ puts '#'
46
+ puts '# CUT POWER'
47
+ puts '#'
48
+ puts
49
+ axle_torque = 0
50
+ end
51
+
52
+ rotating_friction = disk.rotating_friction(omega)
53
+ net_torque = axle_torque + rotating_friction
54
+ net_force = disk.force(net_torque)
55
+
56
+ # rotational kinematics
57
+ alpha = disk.alpha(net_torque)
58
+ omega += alpha * env.tick
59
+ omega = 0.0 if omega.abs < 0.0001
60
+ theta += omega * env.tick
61
+
62
+ if flag or i < 10 or
63
+ (i < 20_000 and i%1000 == 0) or
64
+ (i % 10_000 == 0) or
65
+ i == duration * env.hz - 1
66
+
67
+ puts Timer.display(ms: i)
68
+ puts format(" Torque: %.1f Nm (%d Nm) Friction: %.1f Nm",
69
+ net_torque, axle_torque, rotating_friction)
70
+ puts format("Radians: %.1f r %.2f r/s %.3f r/s^2", theta, omega, alpha)
71
+ puts format(" Revs: %d revs %d revs/s %d rpm",
72
+ DrivingPhysics.revs(theta),
73
+ DrivingPhysics.revs(omega),
74
+ DrivingPhysics.rpm(omega))
75
+ puts
76
+ if flag
77
+ paused += CLI.pause
78
+ flag = false
79
+ end
80
+ end
81
+ }
82
+
83
+ puts Timer.summary(start, num_ticks, paused)
data/demo/gearbox.rb ADDED
@@ -0,0 +1,52 @@
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
+ start = Timer.now
25
+ paused = 0
26
+
27
+ (duration * env.hz + 1).times { |i|
28
+ # just for info, not used in the simulation
29
+ friction = gearbox.spinner.rotating_friction(omega)
30
+
31
+ # update rotational kinematics
32
+ # gearbox.alpha incorporates friction and inertia
33
+ alpha = gearbox.alpha(torque, omega: omega)
34
+ omega += alpha * env.tick
35
+ theta += omega * env.tick
36
+
37
+ net_torque = gearbox.implied_torque(alpha)
38
+
39
+ # periodic output
40
+ if i < 10 or
41
+ (i < 100 and i % 10 == 0) or
42
+ (i < 1000 and i % 100 == 0) or
43
+ i % 1000 == 0
44
+ puts Timer.display(i)
45
+ puts format("RPM %d Torque: %.3f Nm (%d Nm) Friction: %.3f Nm",
46
+ DrivingPhysics.rpm(omega), net_torque, torque, friction)
47
+ puts format("%.1f rad %.1f rad/s %.1f rad/s/s", theta, omega, alpha)
48
+ puts
49
+ end
50
+ }
51
+
52
+ puts Timer.summary(start, num_ticks, paused)
data/demo/motor.rb ADDED
@@ -0,0 +1,141 @@
1
+ require 'driving_physics/motor'
2
+ require 'driving_physics/cli'
3
+ require 'driving_physics/power'
4
+
5
+ # next fun idea: PID controller
6
+
7
+ include DrivingPhysics
8
+
9
+ env = Environment.new
10
+ motor = Motor.new(env)
11
+ puts env
12
+ puts motor
13
+ puts
14
+
15
+ puts "Rev it up!"
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
+ }
46
+ }
47
+
48
+ puts
49
+ puts "Now, the simulation begins..."
50
+ puts "---"
51
+ puts "* Spin the motor up to #{motor.idle_rpm} RPM with the starter motor."
52
+ puts "* Rev it up with the throttle."
53
+ puts "* Let it die."
54
+ CLI.pause
55
+
56
+ alpha = 0.0
57
+ omega = 0.0
58
+ theta = 0.0
59
+
60
+ duration = 60
61
+
62
+ status = :ignition
63
+ rpm = 0
64
+
65
+ paused = 0.0
66
+ num_ticks = duration * env.hz + 1
67
+ start = Timer.now
68
+
69
+ num_ticks.times { |i|
70
+ # this is an input torque; alpha is determined after inertia and friction
71
+ torque = case status
72
+ when :ignition
73
+ motor.starter_torque
74
+ when :running, :off_throttle, :idling
75
+ motor.torque(rpm)
76
+ else
77
+ 0
78
+ end
79
+
80
+ # Motor#alpha incorporates inertia and friction
81
+ alpha = motor.alpha(torque, omega: omega)
82
+ omega += alpha * env.tick
83
+ theta += omega * env.tick
84
+
85
+ net_torque = motor.implied_torque(alpha)
86
+
87
+ # prevent silly oscillations due to tiny floating point errors
88
+ omega = 0 if omega < 0.00001
89
+ rpm = DrivingPhysics.rpm(omega)
90
+
91
+ power = DrivingPhysics.power(net_torque, omega)
92
+
93
+ if rpm > motor.idle_rpm and status == :ignition
94
+ status = :running
95
+ flag = true
96
+ end
97
+
98
+ if rpm > 7000 and status == :running
99
+ status = :off_throttle
100
+ motor.throttle = 0.0
101
+ flag = true
102
+ end
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
+
118
+ if flag or
119
+ (i < 10) or
120
+ (i < 100 and i % 10 == 0) or
121
+ (i < 1000 and i % 100 == 0) or
122
+ (i < 10_000 and i % 500 == 0) or
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",
128
+ DrivingPhysics.rpm(omega),
129
+ net_torque,
130
+ torque,
131
+ power / 1000,
132
+ motor.spinner.rotating_friction(omega))
133
+ puts format("%d rad %.1f rad/s %.1f rad/s/s", theta, omega, alpha)
134
+ puts
135
+
136
+ paused += CLI.pause if flag
137
+ flag = false
138
+ end
139
+ }
140
+
141
+ puts Timer.summary(start, num_ticks, paused)
@@ -0,0 +1,47 @@
1
+ require 'driving_physics/powertrain'
2
+ require 'driving_physics/imperial'
3
+ require 'driving_physics/cli'
4
+ require 'driving_physics/power'
5
+
6
+ include DrivingPhysics
7
+
8
+ env = Environment.new
9
+ motor = Motor.new(env)
10
+ gearbox = Gearbox.new(env)
11
+ powertrain = Powertrain.new(motor, gearbox)
12
+ motor.throttle = 1.0
13
+ puts env
14
+ puts powertrain
15
+ CLI.pause
16
+
17
+ crank_alpha = 0.0
18
+ crank_omega = 0.0
19
+
20
+ axle_alpha = 0.0
21
+ axle_omega = 0.0
22
+
23
+ # Run through the gears
24
+ 1.upto(6) { |gear|
25
+ gearbox.gear = gear
26
+
27
+ puts <<EOF
28
+
29
+ # GEAR #{gear} (#{gearbox.ratio})
30
+ #
31
+ EOF
32
+
33
+ 800.upto(7000) { |rpm|
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, ' '))
46
+ }
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.a(nf, mass)
17
- spd = DP.v(a, spd, dt: tick)
18
- pos = DP.p(spd, pos, 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)