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.
- checksums.yaml +4 -4
- data/README.md +31 -1
- data/Rakefile +84 -0
- data/VERSION +1 -1
- data/demo/car.rb +178 -0
- data/demo/disk.rb +83 -0
- data/demo/gearbox.rb +53 -0
- data/demo/motor.rb +140 -0
- data/demo/mruby/disk.rb +81 -0
- data/demo/mruby/motor.rb +137 -0
- data/demo/powertrain.rb +47 -0
- data/demo/scalar_force.rb +41 -15
- data/demo/tire.rb +87 -0
- data/demo/vector_force.rb +46 -16
- data/lib/driving_physics/car.rb +123 -0
- data/lib/driving_physics/cli.rb +51 -0
- data/lib/driving_physics/disk.rb +185 -0
- data/lib/driving_physics/gearbox.rb +109 -0
- data/lib/driving_physics/imperial.rb +6 -0
- data/lib/driving_physics/motor.rb +150 -0
- data/lib/driving_physics/mruby.rb +45 -0
- data/lib/driving_physics/power.rb +20 -0
- data/lib/driving_physics/powertrain.rb +50 -0
- data/lib/driving_physics/scalar_force.rb +6 -3
- data/lib/driving_physics/tire.rb +120 -0
- data/lib/driving_physics/vector_force.rb +15 -2
- data/lib/driving_physics.rb +2 -0
- data/test/disk.rb +132 -0
- data/test/scalar_force.rb +7 -5
- data/test/{wheel.rb → tire.rb} +65 -55
- data/test/vector_force.rb +7 -1
- metadata +20 -4
- data/demo/wheel.rb +0 -84
- data/lib/driving_physics/wheel.rb +0 -191
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'driving_physics/environment'
|
2
|
+
#require 'driving_physics/vector_force'
|
3
|
+
|
4
|
+
module DrivingPhysics
|
5
|
+
# radius is always in meters
|
6
|
+
# force in N
|
7
|
+
# torque in Nm
|
8
|
+
|
9
|
+
# Rotational complements to acc/vel/pos
|
10
|
+
# alpha - angular acceleration (radians / s / s)
|
11
|
+
# omega - angular velocity (radians / s)
|
12
|
+
# theta - radians
|
13
|
+
|
14
|
+
# convert radians to revolutions; works for alpha/omega/theta
|
15
|
+
def self.revs(rads)
|
16
|
+
rads / (2 * Math::PI)
|
17
|
+
end
|
18
|
+
|
19
|
+
# convert revs to rads; works for alpha/omega/theta
|
20
|
+
def self.rads(revs)
|
21
|
+
revs * 2 * Math::PI
|
22
|
+
end
|
23
|
+
|
24
|
+
# convert rpm to omega (rads / s)
|
25
|
+
def self.omega(rpm)
|
26
|
+
self.rads(rpm / 60.0)
|
27
|
+
end
|
28
|
+
|
29
|
+
# convert omega to RPM (revs per minute)
|
30
|
+
def self.rpm(omega)
|
31
|
+
self.revs(omega) * 60
|
32
|
+
end
|
33
|
+
|
34
|
+
class Disk
|
35
|
+
DENSITY = 1.0 # kg / L
|
36
|
+
|
37
|
+
# torque = force * distance
|
38
|
+
def self.force(axle_torque, radius)
|
39
|
+
axle_torque / radius.to_f
|
40
|
+
end
|
41
|
+
|
42
|
+
# in m^3
|
43
|
+
def self.volume(radius, width)
|
44
|
+
Math::PI * radius ** 2 * width
|
45
|
+
end
|
46
|
+
|
47
|
+
# in L
|
48
|
+
def self.volume_l(radius, width)
|
49
|
+
volume(radius, width) * 1000
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.density(mass, volume_l)
|
53
|
+
mass.to_f / volume_l
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.mass(radius, width, density)
|
57
|
+
volume_l(radius, width) * density
|
58
|
+
end
|
59
|
+
|
60
|
+
# I = 1/2 (m)(r^2) for a disk
|
61
|
+
def self.rotational_inertia(radius, mass)
|
62
|
+
mass * radius**2 / 2.0
|
63
|
+
end
|
64
|
+
class << self
|
65
|
+
alias_method(:moment_of_inertia, :rotational_inertia)
|
66
|
+
end
|
67
|
+
|
68
|
+
# angular acceleration
|
69
|
+
def self.alpha(torque, inertia)
|
70
|
+
torque / inertia
|
71
|
+
end
|
72
|
+
|
73
|
+
# convert alpha/omega/theta to acc/vel/pos
|
74
|
+
def self.tangential(rotational, radius)
|
75
|
+
rotational * radius
|
76
|
+
end
|
77
|
+
|
78
|
+
# convert acc/vel/pos to alpha/omega/theta
|
79
|
+
def self.rotational(tangential, radius)
|
80
|
+
tangential.to_f / radius
|
81
|
+
end
|
82
|
+
|
83
|
+
# vectors only
|
84
|
+
def self.torque_vector(force, radius)
|
85
|
+
if !force.is_a?(Vector) or force.size != 2
|
86
|
+
raise(ArgumentError, "force must be a 2D vector")
|
87
|
+
end
|
88
|
+
if !radius.is_a?(Vector) or radius.size != 2
|
89
|
+
raise(ArgumentError, "radius must be a 2D vector")
|
90
|
+
end
|
91
|
+
force = Vector[force[0], force[1], 0]
|
92
|
+
radius = Vector[radius[0], radius[1], 0]
|
93
|
+
force.cross(radius)
|
94
|
+
end
|
95
|
+
|
96
|
+
# vectors only
|
97
|
+
def self.force_vector(torque, radius)
|
98
|
+
if !torque.is_a?(Vector) or torque.size != 3
|
99
|
+
raise(ArgumentError, "torque must be a 3D vector")
|
100
|
+
end
|
101
|
+
if !radius.is_a?(Vector) or radius.size != 2
|
102
|
+
raise(ArgumentError, "radius must be a 2D vector")
|
103
|
+
end
|
104
|
+
radius = Vector[radius[0], radius[1], 0]
|
105
|
+
radius.cross(torque) / radius.dot(radius)
|
106
|
+
end
|
107
|
+
|
108
|
+
attr_reader :env
|
109
|
+
attr_accessor :radius, :width, :density, :base_friction, :omega_friction
|
110
|
+
|
111
|
+
def initialize(env)
|
112
|
+
@env = env
|
113
|
+
@radius = 0.35
|
114
|
+
@width = 0.2
|
115
|
+
@density = DENSITY
|
116
|
+
@base_friction = 5.0/100_000 # constant resistance to rotation
|
117
|
+
@omega_friction = 5.0/100_000 # scales with omega
|
118
|
+
yield self if block_given?
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_s
|
122
|
+
[[format("%d mm x %d mm (RxW)", @radius * 1000, @width * 1000),
|
123
|
+
format("%.1f kg %.2f kg/L", self.mass, @density),
|
124
|
+
].join(" | "),
|
125
|
+
].join("\n")
|
126
|
+
end
|
127
|
+
|
128
|
+
def normal_force
|
129
|
+
@normal_force ||= self.mass * @env.g
|
130
|
+
@normal_force
|
131
|
+
end
|
132
|
+
|
133
|
+
def alpha(torque, omega: 0, normal_force: nil)
|
134
|
+
(torque - self.rotating_friction(omega, normal_force: normal_force)) /
|
135
|
+
self.rotational_inertia
|
136
|
+
end
|
137
|
+
|
138
|
+
def implied_torque(alpha)
|
139
|
+
alpha * self.rotational_inertia
|
140
|
+
end
|
141
|
+
|
142
|
+
def mass
|
143
|
+
self.class.mass(@radius, @width, @density)
|
144
|
+
end
|
145
|
+
|
146
|
+
def mass=(val)
|
147
|
+
@density = self.class.density(val, self.volume_l)
|
148
|
+
@normal_force = nil # force update
|
149
|
+
end
|
150
|
+
|
151
|
+
# in m^3
|
152
|
+
def volume
|
153
|
+
self.class.volume(@radius, @width)
|
154
|
+
end
|
155
|
+
|
156
|
+
# in L
|
157
|
+
def volume_l
|
158
|
+
self.class.volume_l(@radius, @width)
|
159
|
+
end
|
160
|
+
|
161
|
+
def rotational_inertia
|
162
|
+
self.class.rotational_inertia(@radius, self.mass)
|
163
|
+
end
|
164
|
+
alias_method(:moment_of_inertia, :rotational_inertia)
|
165
|
+
|
166
|
+
def force(axle_torque)
|
167
|
+
self.class.force(axle_torque, @radius)
|
168
|
+
end
|
169
|
+
|
170
|
+
def tangential(rotational)
|
171
|
+
self.class.tangential(rotational, @radius)
|
172
|
+
end
|
173
|
+
|
174
|
+
# modeled as a tiny but increasing torque opposing omega
|
175
|
+
# also scales with normal force
|
176
|
+
# maybe not physically faithful but close enough
|
177
|
+
def rotating_friction(omega, normal_force: nil)
|
178
|
+
return omega if omega.zero?
|
179
|
+
normal_force = self.normal_force if normal_force.nil?
|
180
|
+
mag = omega.abs
|
181
|
+
sign = omega / mag
|
182
|
+
-1 * sign * normal_force * (@base_friction + mag * @omega_friction)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'driving_physics/disk'
|
2
|
+
|
3
|
+
module DrivingPhysics
|
4
|
+
# Technically speaking, the gear ratio goes up with speed and down with
|
5
|
+
# torque. But in the automotive world, it's customary to invert the
|
6
|
+
# relationship, where a bigger number means more torque and less speed.
|
7
|
+
# We'll store the truth, where the default final drive would be
|
8
|
+
# conventionally known as 3.73, but 1/3.73 will be stored.
|
9
|
+
|
10
|
+
# The gear ratio (above 1) multiplies speed and divides torque
|
11
|
+
# 5000 RPM is around 375 MPH with a standard wheel/tire
|
12
|
+
# 3.73 final drive reduces that to around 100 mph in e.g. 5th gear (1.0)
|
13
|
+
# Likewise, 1st gear is a _smaller_ gear ratio than 3rd
|
14
|
+
class Gearbox
|
15
|
+
class Disengaged < RuntimeError; end
|
16
|
+
|
17
|
+
RATIOS = [1/5r, 2/5r, 5/9r, 5/7r, 1r, 5/4r]
|
18
|
+
FINAL_DRIVE = 11/41r # 1/3.73
|
19
|
+
|
20
|
+
attr_accessor :gear, :ratios, :final_drive, :spinner, :fixed_mass
|
21
|
+
|
22
|
+
def initialize(env)
|
23
|
+
@ratios = RATIOS
|
24
|
+
@final_drive = FINAL_DRIVE
|
25
|
+
@gear = 0 # neutral
|
26
|
+
|
27
|
+
# represent all rotating mass
|
28
|
+
@spinner = Disk.new(env) { |m|
|
29
|
+
m.radius = 0.15
|
30
|
+
m.base_friction = 5.0/1000
|
31
|
+
m.omega_friction = 15.0/100_000
|
32
|
+
m.mass = 15
|
33
|
+
}
|
34
|
+
@fixed_mass = 30 # kg
|
35
|
+
|
36
|
+
yield self if block_given?
|
37
|
+
end
|
38
|
+
|
39
|
+
# given torque, apply friction, determine alpha
|
40
|
+
def alpha(torque, omega: 0)
|
41
|
+
@spinner.alpha(torque + @spinner.rotating_friction(omega))
|
42
|
+
end
|
43
|
+
|
44
|
+
def rotating_friction(omega)
|
45
|
+
@spinner.rotating_friction(omega)
|
46
|
+
end
|
47
|
+
|
48
|
+
def implied_torque(alpha)
|
49
|
+
@spinner.implied_torque(alpha)
|
50
|
+
end
|
51
|
+
|
52
|
+
def mass
|
53
|
+
@fixed_mass + @spinner.mass
|
54
|
+
end
|
55
|
+
|
56
|
+
def top_gear
|
57
|
+
@ratios.length
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
[format("Ratios: %s", @ratios.inspect),
|
62
|
+
format(" Final: %s Mass: %.1f kg Rotating: %.1f kg",
|
63
|
+
@final_drive.inspect, self.mass, @spinner.mass),
|
64
|
+
].join("\n")
|
65
|
+
end
|
66
|
+
|
67
|
+
def ratio(gear = nil)
|
68
|
+
gear ||= @gear
|
69
|
+
raise(Disengaged, "Cannot determine gear ratio") if @gear == 0
|
70
|
+
@ratios.fetch(gear - 1) * @final_drive
|
71
|
+
end
|
72
|
+
|
73
|
+
def axle_torque(crank_torque)
|
74
|
+
crank_torque / self.ratio
|
75
|
+
end
|
76
|
+
|
77
|
+
def axle_omega(crank_rpm)
|
78
|
+
DrivingPhysics.omega(crank_rpm) * self.ratio
|
79
|
+
end
|
80
|
+
|
81
|
+
def crank_rpm(axle_omega)
|
82
|
+
DrivingPhysics.rpm(axle_omega) / self.ratio
|
83
|
+
end
|
84
|
+
|
85
|
+
def match_rpms(old_rpm, new_rpm)
|
86
|
+
diff = new_rpm - old_rpm
|
87
|
+
proportion = diff.to_f / old_rpm
|
88
|
+
if proportion.abs < 0.01
|
89
|
+
[:ok, proportion]
|
90
|
+
elsif proportion.abs < 0.1
|
91
|
+
[:slip, proportion]
|
92
|
+
elsif @gear == 1 and new_rpm < old_rpm and old_rpm <= 1500
|
93
|
+
[:get_rolling, proportion]
|
94
|
+
else
|
95
|
+
[:mismatch, proportion]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def next_gear(rpm, floor: 2500, ceiling: 6400)
|
100
|
+
if rpm < floor and @gear > 1
|
101
|
+
@gear - 1
|
102
|
+
elsif rpm > ceiling and @gear < self.top_gear
|
103
|
+
@gear + 1
|
104
|
+
else
|
105
|
+
@gear
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
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
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'driving_physics/disk'
|
2
|
+
|
3
|
+
module DrivingPhysics
|
4
|
+
class Motor
|
5
|
+
class Stall < RuntimeError; end
|
6
|
+
class OverRev < RuntimeError; end
|
7
|
+
class SanityCheck < RuntimeError; end
|
8
|
+
|
9
|
+
TORQUES = [ 0, 70, 130, 200, 250, 320, 330, 320, 260, 0]
|
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
|
12
|
+
|
13
|
+
attr_reader :env, :throttle
|
14
|
+
attr_accessor :torques, :rpms, :fixed_mass,
|
15
|
+
:spinner, :starter_torque, :idle_rpm
|
16
|
+
|
17
|
+
def initialize(env)
|
18
|
+
@env = env
|
19
|
+
|
20
|
+
@torques = TORQUES
|
21
|
+
@rpms = RPMS
|
22
|
+
@fixed_mass = 125
|
23
|
+
|
24
|
+
# represent all rotating mass as one big flywheel
|
25
|
+
@spinner = Disk.new(@env) { |fly|
|
26
|
+
fly.radius = 0.25 # m
|
27
|
+
fly.mass = 75 # kg
|
28
|
+
fly.base_friction = 5.0/1000
|
29
|
+
fly.omega_friction = 2.0/10_000
|
30
|
+
}
|
31
|
+
@starter_torque = 500 # Nm
|
32
|
+
@idle_rpm = 1000 # RPM
|
33
|
+
@throttle = 0.0 # 0.0 - 1.0 (0% - 100%)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
ary = [format("Throttle: %.1f%%", @throttle * 100)]
|
38
|
+
ary << "Torque:"
|
39
|
+
@rpms.each_with_index { |r, i|
|
40
|
+
ary << format("%s Nm %s RPM",
|
41
|
+
@torques[i].round(1).to_s.rjust(4, ' '),
|
42
|
+
r.to_s.rjust(5, ' '))
|
43
|
+
}
|
44
|
+
ary << format("Mass: %.1f kg Fixed: %d kg", self.mass, @fixed_mass)
|
45
|
+
ary << format("Rotating: %s", @spinner)
|
46
|
+
ary.join("\n")
|
47
|
+
end
|
48
|
+
|
49
|
+
def rotational_inertia
|
50
|
+
@spinner.rotational_inertia
|
51
|
+
end
|
52
|
+
|
53
|
+
def mass
|
54
|
+
@spinner.mass + @fixed_mass
|
55
|
+
end
|
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
|
+
|
64
|
+
# given torque, determine crank alpha after inertia and friction
|
65
|
+
def alpha(torque, omega: 0)
|
66
|
+
@spinner.alpha(torque + @spinner.rotating_friction(omega))
|
67
|
+
end
|
68
|
+
|
69
|
+
def implied_torque(alpha)
|
70
|
+
@spinner.implied_torque(alpha)
|
71
|
+
end
|
72
|
+
|
73
|
+
# interpolate based on torque curve points
|
74
|
+
def torque(rpm)
|
75
|
+
raise(Stall, "RPM #{rpm}") if rpm < @rpms.first
|
76
|
+
raise(OverRev, "RPM #{rpm}") if rpm > @rpms.last
|
77
|
+
|
78
|
+
last_rpm, last_tq, torque = 99999, -1, nil
|
79
|
+
|
80
|
+
@rpms.each_with_index { |r, i|
|
81
|
+
tq = @torques[i]
|
82
|
+
if last_rpm <= rpm and rpm <= r
|
83
|
+
proportion = Rational(rpm - last_rpm) / (r - last_rpm)
|
84
|
+
torque = last_tq + (tq - last_tq) * proportion
|
85
|
+
break
|
86
|
+
end
|
87
|
+
last_rpm, last_tq = r, tq
|
88
|
+
}
|
89
|
+
raise(SanityCheck, "RPM #{rpm}") if torque.nil?
|
90
|
+
|
91
|
+
if (@throttle <= 0.05) and (rpm > @idle_rpm * 1.5)
|
92
|
+
# engine braking: 20% of torque
|
93
|
+
-1 * torque * ENGINE_BRAKING
|
94
|
+
else
|
95
|
+
torque * @throttle
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# TODO: starter motor
|
103
|
+
# Starter motor is power limited, not torque limited
|
104
|
+
# Consider:
|
105
|
+
# * 2.2 kW (3.75:1 gear reduction)
|
106
|
+
# * 1.8 kW (4.4:1 gear reduction)
|
107
|
+
# On a workbench, a starter will draw 80 to 90 amps. However, during actual
|
108
|
+
# start-up of an engine, a starter will draw 250 to 350 amps.
|
109
|
+
# from https://www.motortrend.com/how-to/because-theres-more-to-a-starter-than-you-realize/
|
110
|
+
|
111
|
+
# V - Potential, voltage
|
112
|
+
# I - Current, amperage
|
113
|
+
# R - Resistance, ohms
|
114
|
+
# P - Power, wattage
|
115
|
+
|
116
|
+
# Ohm's law: I = V / R (where R is held constant)
|
117
|
+
# P = I * V
|
118
|
+
# For resistors (where R is helod constant)
|
119
|
+
# = I^2 * R
|
120
|
+
# = V^2 / R
|
121
|
+
|
122
|
+
|
123
|
+
# torque proportional to A
|
124
|
+
# speed proportional to V
|
125
|
+
|
126
|
+
|
127
|
+
# batteries are rated in terms of CCA - cold cranking amps
|
128
|
+
|
129
|
+
# P = I * V
|
130
|
+
# V = 12 (up to 14ish)
|
131
|
+
# I = 300
|
132
|
+
# P = 3600 or 3.6 kW
|
133
|
+
|
134
|
+
|
135
|
+
# A starter rated at e.g. 2.2 kW will use more power on initial cranking
|
136
|
+
# Sometimes up to 500 amperes are required, and some batteries will provide
|
137
|
+
# over 600 cold cranking amps
|
138
|
+
|
139
|
+
|
140
|
+
# Consider:
|
141
|
+
|
142
|
+
# V = 12
|
143
|
+
# R = resistance of battery, wiring, starter motor
|
144
|
+
# L = inductance (approx 0)
|
145
|
+
# I = current through motor
|
146
|
+
# Vc = proportional to omega
|
147
|
+
|
148
|
+
# rated power - Vc * I
|
149
|
+
# input power - V * I
|
150
|
+
# input power that is unable to be converted to output power is wasted as heat
|
@@ -0,0 +1,45 @@
|
|
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
|
+
def self.now
|
22
|
+
Time.now
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.since(t)
|
26
|
+
self.now - t
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.elapsed(&work)
|
30
|
+
t = self.now
|
31
|
+
return yield, self.since(t)
|
32
|
+
end
|
33
|
+
|
34
|
+
# HH:MM:SS.mmm
|
35
|
+
def self.display(seconds: 0, ms: 0)
|
36
|
+
ms += (seconds * 1000).round if seconds > 0
|
37
|
+
DrivingPhysics.elapsed_display(ms)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.summary(start, num_ticks, paused = 0)
|
41
|
+
elapsed = self.since(start) - paused
|
42
|
+
format("%.3f s (%d ticks/s)", elapsed, num_ticks.to_f / elapsed)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'driving_physics'
|
2
|
+
|
3
|
+
# Work is Force * Distance (Torque * Theta)
|
4
|
+
# W = F * s (W = T * Th)
|
5
|
+
# W = T * Theta
|
6
|
+
|
7
|
+
# Power is Work / time
|
8
|
+
# P = W / dt
|
9
|
+
# P = T * Th / dt
|
10
|
+
# P = T * Omega
|
11
|
+
|
12
|
+
module DrivingPhysics
|
13
|
+
def self.work(force, displacement)
|
14
|
+
force * displacement
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.power(force, speed)
|
18
|
+
force * speed
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'driving_physics/motor'
|
2
|
+
require 'driving_physics/gearbox'
|
3
|
+
|
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
|
+
#
|
10
|
+
class Powertrain
|
11
|
+
attr_reader :motor, :gearbox
|
12
|
+
|
13
|
+
def initialize(motor, gearbox)
|
14
|
+
@motor = motor
|
15
|
+
@gearbox = gearbox
|
16
|
+
end
|
17
|
+
|
18
|
+
def mass
|
19
|
+
@motor.mass + @gearbox.mass
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
["\t[MOTOR]", @motor, "\t[GEARBOX]", @gearbox].join("\n")
|
24
|
+
end
|
25
|
+
|
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]
|
30
|
+
end
|
31
|
+
|
32
|
+
def axle_torque(rpm)
|
33
|
+
crank_alpha = @motor.alpha(@motor.torque(rpm),
|
34
|
+
omega: DrivingPhysics.omega(rpm))
|
35
|
+
crank_torque = @motor.implied_torque(crank_alpha)
|
36
|
+
|
37
|
+
axle_alpha = @gearbox.alpha(@gearbox.axle_torque(crank_torque),
|
38
|
+
omega: @gearbox.axle_omega(rpm))
|
39
|
+
@gearbox.implied_torque(axle_alpha)
|
40
|
+
end
|
41
|
+
|
42
|
+
def axle_omega(rpm)
|
43
|
+
@gearbox.axle_omega(rpm)
|
44
|
+
end
|
45
|
+
|
46
|
+
def crank_rpm(axle_omega)
|
47
|
+
@gearbox.crank_rpm(axle_omega)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -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
|
#
|