driving_physics 0.0.0.2 → 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: 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)