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.
- 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
|
#
|