driving_physics 0.0.0.3 → 0.0.2.1

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