driving_physics 0.0.0.2 → 0.0.1.2
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/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
|
#
|