driving_physics 0.0.0.2 → 0.0.1.2
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/Rakefile +11 -4
- data/VERSION +1 -1
- data/demo/car.rb +165 -18
- data/demo/disk.rb +83 -0
- data/demo/gearbox.rb +52 -0
- data/demo/motor.rb +141 -0
- data/demo/powertrain.rb +47 -0
- data/demo/scalar_force.rb +41 -15
- data/demo/tire.rb +80 -162
- data/demo/vector_force.rb +46 -16
- data/lib/driving_physics/car.rb +77 -248
- 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 +103 -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 +90 -258
- data/lib/driving_physics/vector_force.rb +15 -2
- data/lib/driving_physics.rb +2 -0
- data/test/disk.rb +129 -0
- data/test/scalar_force.rb +7 -5
- data/test/tire.rb +144 -88
- data/test/vector_force.rb +7 -1
- metadata +12 -5
- data/demo/wheel.rb +0 -84
- data/lib/driving_physics/wheel.rb +0 -191
- data/test/car.rb +0 -156
- data/test/wheel.rb +0 -177
@@ -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
|
@@ -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,103 @@
|
|
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/1000r
|
29
|
+
fly.omega_friction = 2/10_000r
|
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 = 99999
|
79
|
+
last_tq = -1
|
80
|
+
torque = nil
|
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
|
+
torque = last_tq + (tq - last_tq) * proportion
|
88
|
+
break
|
89
|
+
end
|
90
|
+
last_rpm = r
|
91
|
+
last_tq = tq
|
92
|
+
}
|
93
|
+
raise(SanityCheck, "RPM #{rpm}") if torque.nil?
|
94
|
+
|
95
|
+
if (@throttle <= 0.05) and (rpm > @idle_rpm * 1.5)
|
96
|
+
# engine braking: 20% of torque
|
97
|
+
-1 * torque * ENGINE_BRAKING
|
98
|
+
else
|
99
|
+
torque * @throttle
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
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
|
#
|