driving_physics 0.0.0.3 → 0.0.1.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 +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
|
#
|