driving_physics 0.0.0.3 → 0.0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+
2
+ # next fun idea: PID controller
3
+
4
+ include DrivingPhysics
5
+
6
+ env = Environment.new
7
+ motor = Motor.new(env)
8
+ puts env
9
+ puts motor
10
+ puts
11
+
12
+ puts "Rev it up!"
13
+ motor.throttle = 1.0
14
+ [:torque, :power].each { |run|
15
+ puts
16
+ puts run.to_s.upcase + ':'
17
+
18
+ 800.upto(7000) { |rpm|
19
+ next unless rpm % 200 == 0
20
+ omega = DrivingPhysics.omega(rpm)
21
+ torque = motor.torque(rpm)
22
+ case run
23
+ when :torque
24
+ count = (torque.to_f / 10).round
25
+ char = 'T'
26
+ val = torque.round.to_s.rjust(5, ' ')
27
+ fmt = "%s Nm"
28
+ when :power
29
+ power = DrivingPhysics.power(torque, omega)
30
+ kw = power.to_f / 1000
31
+ count = (kw / 5).round
32
+ char = 'P'
33
+ val = kw.round(1).to_s.rjust(5, ' ')
34
+ fmt = "%s kW"
35
+ else
36
+ raise "unknown"
37
+ end
38
+ puts format("%s RPM: #{fmt}\t%s",
39
+ rpm.to_s.rjust(4, ' '),
40
+ val,
41
+ char * count)
42
+ }
43
+ }
44
+
45
+ puts
46
+ puts "Now, the simulation begins..."
47
+ puts "---"
48
+ puts "* Spin the motor up to #{motor.idle_rpm} RPM with the starter motor."
49
+ puts "* Rev it up with the throttle."
50
+ puts "* Let it die."
51
+ CLI.pause
52
+
53
+ alpha = 0.0
54
+ omega = 0.0
55
+ theta = 0.0
56
+
57
+ duration = 60
58
+
59
+ status = :ignition
60
+ rpm = 0
61
+
62
+ paused = 0.0
63
+ num_ticks = duration * env.hz + 1
64
+ start = Timer.now
65
+
66
+ num_ticks.times { |i|
67
+ # this is an input torque; alpha is determined after inertia and friction
68
+ torque = case status
69
+ when :ignition
70
+ motor.starter_torque
71
+ when :running, :off_throttle, :idling
72
+ motor.torque(rpm)
73
+ else
74
+ 0
75
+ end
76
+
77
+ # Motor#alpha incorporates inertia and friction
78
+ alpha = motor.alpha(torque, omega: omega)
79
+ omega += alpha * env.tick
80
+ theta += omega * env.tick
81
+
82
+ # prevent silly oscillations due to ticks or tiny floating point errors
83
+ omega = 0 if omega < 0.0001
84
+
85
+ net_torque = motor.implied_torque(alpha)
86
+ rpm = DrivingPhysics.rpm(omega)
87
+ power = DrivingPhysics.power(net_torque, omega)
88
+
89
+ if rpm > motor.idle_rpm and status == :ignition
90
+ status = :running
91
+ flag = true
92
+ end
93
+
94
+ if rpm > 7000 and status == :running
95
+ status = :off_throttle
96
+ motor.throttle = 0.0
97
+ flag = true
98
+ end
99
+
100
+ if rpm < 1000 and status == :off_throttle
101
+ status = :idling
102
+ motor.throttle = 0.06
103
+ flag = true
104
+ end
105
+
106
+ if status == :idling
107
+ if rpm < 999
108
+ motor.throttle += (1.0 - motor.throttle) * 0.005
109
+ elsif rpm > 1001
110
+ motor.throttle -= motor.throttle * 0.005
111
+ end
112
+ end
113
+
114
+ if flag or
115
+ (i < 10) or
116
+ (i < 100 and i % 10 == 0) or
117
+ (i < 1000 and i % 100 == 0) or
118
+ (i < 10_000 and i % 500 == 0) or
119
+ (i % 5000 == 0) or
120
+ (status == :idling and i % 100 == 0)
121
+ puts Timer.display(ms: i)
122
+ puts format("Throttle: %.1f%%", motor.throttle * 100)
123
+ puts format("%d RPM %.1f Nm (%d Nm) %.1f kW Friction: %.1f Nm",
124
+ DrivingPhysics.rpm(omega),
125
+ net_torque,
126
+ torque,
127
+ power / 1000,
128
+ motor.spinner.rotating_friction(omega))
129
+ puts format("%.3f rad/s/s %.2f rad/s %.1f rad", alpha, omega, theta)
130
+ puts
131
+
132
+ paused += CLI.pause if flag
133
+ flag = false
134
+ end
135
+ }
136
+
137
+ 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)
data/demo/tire.rb ADDED
@@ -0,0 +1,87 @@
1
+ require 'driving_physics/tire'
2
+ require 'driving_physics/imperial'
3
+ require 'driving_physics/cli'
4
+
5
+ include DrivingPhysics
6
+
7
+ env = Environment.new
8
+ tire = Tire.new(env)
9
+ puts env
10
+ puts tire
11
+ puts
12
+
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
23
+
24
+ total_mass = supported_mass + 4 * tire.mass
25
+ corner_mass = Rational(total_mass) / 4
26
+ normal_force = corner_mass * env.g
27
+
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)
32
+ puts
33
+
34
+ traction = tire.traction(normal_force)
35
+ drive_force = tire.force(axle_torque)
36
+
37
+ puts "Tires:"
38
+ puts format("* %.1f N traction", traction)
39
+ puts format("* %.1f N drive force", drive_force)
40
+
41
+ CLI.pause
42
+
43
+ acc = 0.0 # meters/s/s
44
+ speed = 0.0 # meters/s
45
+ dist = 0.0 # meters
46
+
47
+ alpha = 0.0 # radians/s/s
48
+ omega = 0.0 # radians/s
49
+ theta = 0.0 # radians
50
+
51
+ start = Timer.now
52
+ paused = 0.0
53
+ num_ticks = duration * env.hz + 1
54
+
55
+ num_ticks.times { |i|
56
+ torque = tire.net_tractable_torque(axle_torque,
57
+ mass: total_mass,
58
+ omega: omega,
59
+ normal_force: normal_force)
60
+ force = tire.force(torque)
61
+
62
+ # translational kinematics
63
+ acc = DrivingPhysics.acc(force, total_mass)
64
+ speed += acc * env.tick
65
+ dist += speed * env.tick
66
+ mph = Imperial.mph(speed)
67
+
68
+ # rotational kinematics
69
+ alpha = acc / tire.radius
70
+ omega += alpha * env.tick
71
+ theta += omega * env.tick
72
+
73
+ if i < 10 or
74
+ (i < 20_000 and i%1000 == 0) or
75
+ (i % 10_000 == 0)
76
+
77
+ puts DrivingPhysics.elapsed_display(i)
78
+ puts format(" Tire: %.1f r %.2f r/s %.3f r/s^2", theta, omega, alpha)
79
+ puts format(" Car: %.1f m %.2f m/s %.3f m/s^2 (%d mph)",
80
+ dist, speed, acc, mph)
81
+ puts format("Torque: %.1f Nm (%d N) Loss: %.1f%%",
82
+ torque, force, (1.0 - torque / axle_torque) * 100)
83
+ puts
84
+ end
85
+ }
86
+
87
+ 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.a(nf, mass)
17
- v = DP.v(a, v, dt: tick)
18
- p = DP.p(v, p, 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)
@@ -0,0 +1,123 @@
1
+ require 'driving_physics/tire'
2
+ require 'driving_physics/powertrain'
3
+
4
+ module DrivingPhysics
5
+ class Car
6
+ attr_reader :tire, :powertrain, :env
7
+ attr_accessor :num_tires, :body_mass, :frontal_area, :cd
8
+
9
+ def initialize(tire:, powertrain:)
10
+ @num_tires = 4
11
+ @tire = tire
12
+ @env = @tire.env
13
+ @powertrain = powertrain
14
+ @body_mass = 1000.0
15
+ @frontal_area = DrivingPhysics::FRONTAL_AREA
16
+ @cd = DrivingPhysics::DRAG_COF
17
+
18
+ yield self if block_given?
19
+ end
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
+
41
+ # force opposing speed
42
+ def air_force(speed)
43
+ -0.5 * @frontal_area * @cd * @env.air_density * speed ** 2
44
+ end
45
+
46
+ # force of opposite sign to omega
47
+ def tire_rolling_force(omega)
48
+ @num_tires *
49
+ @tire.rolling_friction(omega, normal_force: self.normal_force) /
50
+ @tire.radius
51
+ end
52
+
53
+ # force of opposite sign to omega
54
+ def tire_rotational_force(omega)
55
+ @num_tires *
56
+ @tire.rotating_friction(omega, normal_force: self.normal_force) /
57
+ @tire.radius
58
+ end
59
+
60
+ # force of opposite sign to force
61
+ def tire_inertial_force(force)
62
+ mag = force.abs
63
+ sign = force / mag
64
+ force_loss = 0
65
+ 5.times {
66
+ # use magnitude, so we are subtracting only positive numbers
67
+ acc = DrivingPhysics.acc(mag - force_loss, self.total_mass)
68
+ alpha = acc / @tire.radius
69
+ # this will be a positive number
70
+ force_loss = @num_tires * @tire.implied_torque(alpha) /
71
+ @tire.radius
72
+ }
73
+ # oppose initial force
74
+ -1 * sign * force_loss
75
+ end
76
+
77
+ def to_s
78
+ [[format("Mass: %.1f kg", self.total_mass),
79
+ format("Fr.A: %.2f m^2", @frontal_area),
80
+ format("cD: %.2f", @cd),
81
+ ].join(' | '),
82
+ format("POWERTRAIN:\n%s", @powertrain),
83
+ format("Tires: %s", @tire),
84
+ format("Corner mass: %.1f kg | Normal force: %.1f N",
85
+ self.corner_mass, self.normal_force),
86
+ ].join("\n")
87
+ end
88
+
89
+ def drive_force(rpm)
90
+ @tire.force(@powertrain.axle_torque(rpm))
91
+ end
92
+
93
+ def tire_speed(rpm)
94
+ @tire.tangential(@powertrain.axle_omega(rpm))
95
+ end
96
+
97
+ def motor_rpm(tire_speed)
98
+ @powertrain.gearbox.crank_rpm(tire_speed / @tire_radius)
99
+ end
100
+
101
+ def total_mass
102
+ @body_mass + @powertrain.mass + @tire.mass * @num_tires
103
+ end
104
+
105
+ def corner_mass
106
+ self.total_mass / @num_tires
107
+ end
108
+
109
+ # per tire
110
+ def normal_force
111
+ self.corner_mass * @env.g
112
+ end
113
+
114
+ # per tire
115
+ def tire_traction
116
+ @tire.traction(self.normal_force)
117
+ end
118
+
119
+ def total_normal_force
120
+ self.total_mass * env.g
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,51 @@
1
+ module DrivingPhysics
2
+ module CLI
3
+ # returns user input as a string
4
+ def self.prompt(msg = '')
5
+ print msg + ' ' unless msg.empty?
6
+ print '> '
7
+ $stdin.gets.chomp
8
+ end
9
+
10
+ # press Enter to continue, ignore input, return elapsed time
11
+ def self.pause(msg = '')
12
+ t = Timer.now
13
+ puts msg unless msg.empty?
14
+ puts ' [ Press Enter ]'
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)
49
+ end
50
+ end
51
+ end