driving_physics 0.0.0.3 → 0.0.1.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 +12 -1
- data/VERSION +1 -1
- data/demo/car.rb +153 -0
- data/demo/disk.rb +76 -0
- data/demo/gearbox.rb +47 -0
- data/demo/motor.rb +89 -0
- data/demo/powertrain.rb +40 -0
- data/demo/scalar_force.rb +3 -3
- data/demo/tire.rb +88 -0
- data/demo/vector_force.rb +3 -3
- data/lib/driving_physics/car.rb +103 -0
- data/lib/driving_physics/cli.rb +17 -0
- data/lib/driving_physics/disk.rb +184 -0
- data/lib/driving_physics/gearbox.rb +109 -0
- data/lib/driving_physics/motor.rb +99 -0
- data/lib/driving_physics/power.rb +20 -0
- data/lib/driving_physics/powertrain.rb +39 -0
- data/lib/driving_physics/tire.rb +120 -0
- data/lib/driving_physics/vector_force.rb +15 -2
- data/lib/driving_physics.rb +1 -0
- data/test/disk.rb +129 -0
- data/test/{wheel.rb → tire.rb} +59 -55
- data/test/vector_force.rb +7 -1
- metadata +17 -4
- data/demo/wheel.rb +0 -84
- data/lib/driving_physics/wheel.rb +0 -191
@@ -0,0 +1,184 @@
|
|
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 = 350/1000r # m
|
114
|
+
@width = 200/1000r # m
|
115
|
+
@density = DENSITY
|
116
|
+
@base_friction = 5/100_000r # constant resistance to rotation
|
117
|
+
@omega_friction = 5/100_000r # 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)
|
134
|
+
torque / self.rotational_inertia
|
135
|
+
end
|
136
|
+
|
137
|
+
def implied_torque(alpha)
|
138
|
+
alpha * self.rotational_inertia
|
139
|
+
end
|
140
|
+
|
141
|
+
def mass
|
142
|
+
self.class.mass(@radius, @width, @density)
|
143
|
+
end
|
144
|
+
|
145
|
+
def mass=(val)
|
146
|
+
@density = self.class.density(val, self.volume_l)
|
147
|
+
@normal_force = nil # force update
|
148
|
+
end
|
149
|
+
|
150
|
+
# in m^3
|
151
|
+
def volume
|
152
|
+
self.class.volume(@radius, @width)
|
153
|
+
end
|
154
|
+
|
155
|
+
# in L
|
156
|
+
def volume_l
|
157
|
+
self.class.volume_l(@radius, @width)
|
158
|
+
end
|
159
|
+
|
160
|
+
def rotational_inertia
|
161
|
+
self.class.rotational_inertia(@radius, self.mass)
|
162
|
+
end
|
163
|
+
alias_method(:moment_of_inertia, :rotational_inertia)
|
164
|
+
|
165
|
+
def force(axle_torque)
|
166
|
+
self.class.force(axle_torque, @radius)
|
167
|
+
end
|
168
|
+
|
169
|
+
def tangential(rotational)
|
170
|
+
self.class.tangential(rotational, @radius)
|
171
|
+
end
|
172
|
+
|
173
|
+
# modeled as a tiny but increasing torque opposing omega
|
174
|
+
# also scales with normal force
|
175
|
+
# maybe not physically faithful but close enough
|
176
|
+
def rotating_friction(omega, normal_force: nil)
|
177
|
+
return omega if omega.zero?
|
178
|
+
mag = omega.abs
|
179
|
+
sign = omega / mag
|
180
|
+
-1 * sign * (normal_force || self.normal_force) *
|
181
|
+
(@base_friction + @omega_friction * mag)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
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/1000r
|
31
|
+
m.omega_friction = 15/100_000r
|
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
|
@@ -0,0 +1,99 @@
|
|
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, 50, 130, 200, 250, 320, 330, 320, 260, 0]
|
10
|
+
RPMS = [500, 1000, 1500, 2000, 2500, 3500, 5000, 6000, 7000, 7100]
|
11
|
+
|
12
|
+
attr_reader :env
|
13
|
+
attr_accessor :torques, :rpms, :fixed_mass,
|
14
|
+
:spinner, :starter_torque, :idle_rpm
|
15
|
+
|
16
|
+
def initialize(env)
|
17
|
+
@env = env
|
18
|
+
|
19
|
+
@torques = TORQUES
|
20
|
+
@rpms = RPMS
|
21
|
+
@fixed_mass = 125
|
22
|
+
|
23
|
+
# represent all rotating mass as one big flywheel
|
24
|
+
@spinner = Disk.new(@env) { |fly|
|
25
|
+
fly.radius = 0.25 # m
|
26
|
+
fly.mass = 75 # kg
|
27
|
+
fly.base_friction = 5/1000r
|
28
|
+
fly.omega_friction = 2/10_000r
|
29
|
+
}
|
30
|
+
@starter_torque = 500 # Nm
|
31
|
+
@idle_rpm = 1000 # RPM
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
ary = ["Torque:"]
|
36
|
+
@rpms.each_with_index { |r, i|
|
37
|
+
ary << format("%s Nm %s RPM",
|
38
|
+
@torques[i].round(1).to_s.rjust(4, ' '),
|
39
|
+
r.to_s.rjust(5, ' '))
|
40
|
+
}
|
41
|
+
ary << format("Mass: %.1f kg Fixed: %d kg", self.mass, @fixed_mass)
|
42
|
+
ary << format("Rotating: %s", @spinner)
|
43
|
+
ary.join("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
def rotational_inertia
|
47
|
+
@spinner.rotational_inertia
|
48
|
+
end
|
49
|
+
|
50
|
+
def mass
|
51
|
+
@spinner.mass + @fixed_mass
|
52
|
+
end
|
53
|
+
|
54
|
+
# given torque, determine crank alpha after inertia and friction
|
55
|
+
def alpha(torque, omega: 0)
|
56
|
+
@spinner.alpha(torque + @spinner.rotating_friction(omega))
|
57
|
+
end
|
58
|
+
|
59
|
+
def implied_torque(alpha)
|
60
|
+
@spinner.implied_torque(alpha)
|
61
|
+
end
|
62
|
+
|
63
|
+
# How much torque is required to accelerate spinner up to alpha,
|
64
|
+
# overcoming both inertia and friction
|
65
|
+
# Presumably we have more input torque available, but this will be
|
66
|
+
# used to do more work than just spinning up the motor
|
67
|
+
#
|
68
|
+
#def resistance_torque(alpha, omega)
|
69
|
+
# # reverse sign on inertial_torque as it is not modeled as a resistance
|
70
|
+
# -1 * @spinner.inertial_torque(alpha) +
|
71
|
+
# @spinner.rotating_friction(omega)
|
72
|
+
#end
|
73
|
+
|
74
|
+
# interpolate based on torque curve points
|
75
|
+
def torque(rpm)
|
76
|
+
raise(Stall, "RPM #{rpm}") if rpm < @rpms.first
|
77
|
+
raise(OverRev, "RPM #{rpm}") if rpm > @rpms.last
|
78
|
+
|
79
|
+
last_rpm = 99999
|
80
|
+
last_tq = -1
|
81
|
+
|
82
|
+
# ew; there must be a better way
|
83
|
+
@rpms.each_with_index { |r, i|
|
84
|
+
tq = @torques[i]
|
85
|
+
if last_rpm <= rpm and rpm <= r
|
86
|
+
proportion = Rational(rpm - last_rpm) / (r - last_rpm)
|
87
|
+
return last_tq + (tq - last_tq) * proportion
|
88
|
+
end
|
89
|
+
last_rpm = r
|
90
|
+
last_tq = tq
|
91
|
+
}
|
92
|
+
raise(SanityCheck, "RPM #{rpm}")
|
93
|
+
end
|
94
|
+
|
95
|
+
def net_torque(rpm, alpha: 0)
|
96
|
+
torque(rpm) + resistance_torque(alpha, DrivingPhysics.omega(rpm))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
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,39 @@
|
|
1
|
+
require 'driving_physics/motor'
|
2
|
+
require 'driving_physics/gearbox'
|
3
|
+
|
4
|
+
module DrivingPhysics
|
5
|
+
class Powertrain
|
6
|
+
attr_reader :motor, :gearbox
|
7
|
+
|
8
|
+
def initialize(motor, gearbox)
|
9
|
+
@motor = motor
|
10
|
+
@gearbox = gearbox
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
["\t[MOTOR]", @motor, "\t[GEARBOX]", @gearbox].join("\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
def select_gear(gear)
|
18
|
+
@gearbox.gear = gear
|
19
|
+
end
|
20
|
+
|
21
|
+
def axle_torque(rpm)
|
22
|
+
crank_alpha = @motor.alpha(@motor.torque(rpm),
|
23
|
+
omega: DrivingPhysics.omega(rpm))
|
24
|
+
crank_torque = @motor.implied_torque(crank_alpha)
|
25
|
+
|
26
|
+
axle_alpha = @gearbox.alpha(@gearbox.axle_torque(crank_torque),
|
27
|
+
omega: @gearbox.axle_omega(rpm))
|
28
|
+
@gearbox.implied_torque(axle_alpha)
|
29
|
+
end
|
30
|
+
|
31
|
+
def axle_omega(rpm)
|
32
|
+
@gearbox.axle_omega(rpm)
|
33
|
+
end
|
34
|
+
|
35
|
+
def crank_rpm(axle_omega)
|
36
|
+
@gearbox.crank_rpm(axle_omega)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'driving_physics/disk'
|
2
|
+
|
3
|
+
module DrivingPhysics
|
4
|
+
|
5
|
+
# a Tire is a Disk with lighter density and meaningful surface friction
|
6
|
+
|
7
|
+
class Tire < Disk
|
8
|
+
# Note, this is not the density of solid rubber. This density
|
9
|
+
# yields a sensible mass for a wheel / tire combo at common radius
|
10
|
+
# and width, assuming a uniform density
|
11
|
+
# e.g. 25kg at 350mm R x 200mm W
|
12
|
+
#
|
13
|
+
DENSITY = 0.325 # kg / L
|
14
|
+
|
15
|
+
# * the traction force opposes the axle torque / drive force
|
16
|
+
# thus, driving the car forward
|
17
|
+
# * if the drive force exceeds the traction force, slippage occurs
|
18
|
+
# * slippage reduces the available traction force further
|
19
|
+
# * if the drive force is not reduced, the slippage increases
|
20
|
+
# until resistance forces equal the drive force
|
21
|
+
def self.traction(normal_force, cof)
|
22
|
+
normal_force * cof
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_accessor :mu_s, :mu_k, :omega_friction, :base_friction, :roll_cof
|
26
|
+
|
27
|
+
def initialize(env)
|
28
|
+
@env = env
|
29
|
+
@radius = 350/1000r # m
|
30
|
+
@width = 200/1000r # m
|
31
|
+
@density = DENSITY
|
32
|
+
@temp = @env.air_temp
|
33
|
+
@mu_s = 11/10r # static friction
|
34
|
+
@mu_k = 7/10r # kinetic friction
|
35
|
+
@base_friction = 5/10_000r
|
36
|
+
@omega_friction = 5/100_000r # scales with speed
|
37
|
+
@roll_cof = DrivingPhysics::ROLL_COF
|
38
|
+
|
39
|
+
yield self if block_given?
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
[[format("%d mm x %d mm (RxW)", @radius * 1000, @width * 1000),
|
44
|
+
format("%.1f kg %.1f C", self.mass, @temp),
|
45
|
+
format("cF: %.1f / %.1f", @mu_s, @mu_k),
|
46
|
+
].join(" | "),
|
47
|
+
].join("\n")
|
48
|
+
end
|
49
|
+
|
50
|
+
def wear!(amount)
|
51
|
+
@radius -= amount
|
52
|
+
end
|
53
|
+
|
54
|
+
def heat!(amount_deg_c)
|
55
|
+
@temp += amount_deg_c
|
56
|
+
end
|
57
|
+
|
58
|
+
def traction(nf, static: true)
|
59
|
+
self.class.traction(nf, static ? @mu_s : @mu_k)
|
60
|
+
end
|
61
|
+
|
62
|
+
# require a normal_force to be be passed in
|
63
|
+
def rotating_friction(omega, normal_force:)
|
64
|
+
super(omega, normal_force: normal_force)
|
65
|
+
end
|
66
|
+
|
67
|
+
# rolling loss in terms of axle torque
|
68
|
+
def rolling_friction(omega, normal_force:)
|
69
|
+
return omega if omega.zero?
|
70
|
+
mag = omega.abs
|
71
|
+
sign = omega / mag
|
72
|
+
-1 * sign * (normal_force * @roll_cof) * @radius
|
73
|
+
end
|
74
|
+
|
75
|
+
# inertial loss in terms of axle torque when used as a drive wheel
|
76
|
+
def inertial_loss(axle_torque, driven_mass:)
|
77
|
+
drive_force = self.force(axle_torque)
|
78
|
+
force_loss = 0
|
79
|
+
# The force loss depends on the acceleration, but the acceleration
|
80
|
+
# depends on the force loss. Converge the value via 5 round trips.
|
81
|
+
# This is a rough way to compute an integral and should be accurate
|
82
|
+
# to 8+ digits.
|
83
|
+
5.times {
|
84
|
+
acc = DrivingPhysics.acc(drive_force - force_loss, driven_mass)
|
85
|
+
alpha = acc / @radius
|
86
|
+
force_loss = self.inertial_torque(alpha) / @radius
|
87
|
+
}
|
88
|
+
force_loss * @radius
|
89
|
+
end
|
90
|
+
|
91
|
+
def net_torque(axle_torque, mass:, omega:, normal_force:)
|
92
|
+
# friction forces oppose omega
|
93
|
+
net = axle_torque +
|
94
|
+
self.rolling_friction(omega, normal_force: normal_force) +
|
95
|
+
self.rotating_friction(omega, normal_force: normal_force)
|
96
|
+
|
97
|
+
# inertial loss has interdependencies; calculate last
|
98
|
+
# it opposes net torque, not omega
|
99
|
+
sign = net / net.abs
|
100
|
+
net - sign * self.inertial_loss(net, driven_mass: mass)
|
101
|
+
end
|
102
|
+
|
103
|
+
def net_tractable_torque(axle_torque,
|
104
|
+
mass:, omega:, normal_force:, static: true)
|
105
|
+
net = self.net_torque(axle_torque,
|
106
|
+
mass: mass,
|
107
|
+
omega: omega,
|
108
|
+
normal_force: normal_force)
|
109
|
+
tt = self.tractable_torque(normal_force, static: static)
|
110
|
+
net > tt ? tt : net
|
111
|
+
end
|
112
|
+
|
113
|
+
# this doesn't take inertial losses or internal frictional losses
|
114
|
+
# into account. input torque required to saturate traction will be
|
115
|
+
# higher than what this method returns
|
116
|
+
def tractable_torque(nf, static: true)
|
117
|
+
traction(nf, static: static) * @radius
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -84,12 +84,25 @@ module DrivingPhysics
|
|
84
84
|
frontal_area: FRONTAL_AREA,
|
85
85
|
drag_cof: DRAG_COF,
|
86
86
|
air_density: AIR_DENSITY)
|
87
|
+
return velocity if velocity.zero?
|
87
88
|
-1 * 0.5 * frontal_area * drag_cof * air_density *
|
88
89
|
velocity * velocity.magnitude
|
89
90
|
end
|
90
91
|
|
91
|
-
|
92
|
-
|
92
|
+
# return a force opposing velocity, representing friction / hysteresis
|
93
|
+
def self.rotational_resistance(velocity,
|
94
|
+
rot_const: ROT_CONST,
|
95
|
+
rot_cof: ROT_COF)
|
96
|
+
return velocity if velocity.zero?
|
97
|
+
-1 * velocity * rot_cof + -1 * velocity.normalize * rot_const
|
98
|
+
end
|
99
|
+
|
100
|
+
# return a torque opposing omega, representing friction / hysteresis
|
101
|
+
def self.omega_resistance(omega,
|
102
|
+
rot_const: ROT_TQ_CONST,
|
103
|
+
rot_cof: ROT_TQ_COF)
|
104
|
+
return 0 if omega == 0.0
|
105
|
+
omega * ROT_TQ_COF + ROT_TQ_CONST
|
93
106
|
end
|
94
107
|
|
95
108
|
# dir is drive_force vector or velocity vector; will be normalized
|
data/lib/driving_physics.rb
CHANGED
@@ -26,6 +26,7 @@ module DrivingPhysics
|
|
26
26
|
DRAG_COF = 0.3 # based roughly on 2000s-era Chevrolet Corvette
|
27
27
|
DRAG = 0.4257 # air_resistance at 1 m/s given above numbers
|
28
28
|
ROT_COF = 12.771 # if rotating resistance matches air resistance at 30 m/s
|
29
|
+
ROT_CONST = 0.05 # N opposing drive force / torque
|
29
30
|
ROLL_COF = 0.01 # roughly: street tires on concrete
|
30
31
|
|
31
32
|
#
|