driving_physics 0.0.1.2 → 0.0.3.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 +23 -1
- data/Rakefile +84 -0
- data/VERSION +1 -1
- data/demo/car.rb +85 -72
- data/demo/gearbox.rb +4 -3
- data/demo/motor.rb +12 -10
- data/demo/mruby/car.rb +184 -0
- data/demo/mruby/motor.rb +140 -0
- data/demo/pid_controller.rb +143 -0
- data/demo/powertrain.rb +1 -1
- data/driving_physics.gemspec +2 -1
- data/lib/driving_physics/car.rb +14 -5
- data/lib/driving_physics/cli.rb +7 -35
- data/lib/driving_physics/cockpit.rb +167 -0
- data/lib/driving_physics/disk.rb +10 -30
- data/lib/driving_physics/environment.rb +2 -0
- data/lib/driving_physics/gearbox.rb +62 -32
- data/lib/driving_physics/motor.rb +218 -63
- data/lib/driving_physics/mruby.rb +48 -0
- data/lib/driving_physics/pid_controller.rb +111 -0
- data/lib/driving_physics/powertrain.rb +8 -13
- data/lib/driving_physics/scalar_force.rb +2 -1
- data/lib/driving_physics/timer.rb +34 -0
- data/lib/driving_physics/tire.rb +6 -6
- data/lib/driving_physics/vector_force.rb +23 -0
- data/lib/driving_physics.rb +28 -22
- data/test/disk.rb +23 -59
- data/test/gearbox.rb +36 -0
- data/test/motor.rb +236 -0
- data/test/powertrain.rb +21 -0
- data/test/scalar_force.rb +3 -6
- data/test/tire.rb +25 -53
- data/test/vector_force.rb +76 -37
- metadata +28 -4
@@ -0,0 +1,34 @@
|
|
1
|
+
module DrivingPhysics
|
2
|
+
module Timer
|
3
|
+
# don't use `defined?` with mruby
|
4
|
+
if (Process::CLOCK_MONOTONIC rescue false)
|
5
|
+
def self.now
|
6
|
+
Process.clock_gettime Process::CLOCK_MONOTONIC
|
7
|
+
end
|
8
|
+
else
|
9
|
+
def self.now
|
10
|
+
Time.now
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.since(t)
|
15
|
+
self.now - t
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.elapsed(&work)
|
19
|
+
t = self.now
|
20
|
+
return yield, self.since(t)
|
21
|
+
end
|
22
|
+
|
23
|
+
# HH:MM:SS.mmm
|
24
|
+
def self.display(seconds: 0, ms: 0)
|
25
|
+
ms += (seconds * 1000).round if seconds > 0
|
26
|
+
DrivingPhysics.elapsed_display(ms)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.summary(start, num_ticks, paused = 0)
|
30
|
+
elapsed = self.since(start) - paused
|
31
|
+
format("%.3f s (%d ticks/s)", elapsed, num_ticks.to_f / elapsed)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/driving_physics/tire.rb
CHANGED
@@ -26,14 +26,14 @@ module DrivingPhysics
|
|
26
26
|
|
27
27
|
def initialize(env)
|
28
28
|
@env = env
|
29
|
-
@radius =
|
30
|
-
@width =
|
29
|
+
@radius = 0.35
|
30
|
+
@width = 0.2
|
31
31
|
@density = DENSITY
|
32
32
|
@temp = @env.air_temp
|
33
|
-
@mu_s =
|
34
|
-
@mu_k =
|
35
|
-
@base_friction = 5/
|
36
|
-
@omega_friction = 5/
|
33
|
+
@mu_s = 1.1 # static friction
|
34
|
+
@mu_k = 0.7 # kinetic friction
|
35
|
+
@base_friction = 5.0/10_000
|
36
|
+
@omega_friction = 5.0/100_000
|
37
37
|
@roll_cof = DrivingPhysics::ROLL_COF
|
38
38
|
|
39
39
|
yield self if block_given?
|
@@ -64,6 +64,29 @@ module DrivingPhysics
|
|
64
64
|
dir
|
65
65
|
end
|
66
66
|
|
67
|
+
def self.torque_vector(force, radius)
|
68
|
+
if !force.is_a?(Vector) or force.size != 2
|
69
|
+
raise(ArgumentError, "force must be a 2D vector")
|
70
|
+
end
|
71
|
+
if !radius.is_a?(Vector) or radius.size != 2
|
72
|
+
raise(ArgumentError, "radius must be a 2D vector")
|
73
|
+
end
|
74
|
+
force = Vector[force[0], force[1], 0]
|
75
|
+
radius = Vector[radius[0], radius[1], 0]
|
76
|
+
force.cross(radius)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.force_vector(torque, radius)
|
80
|
+
if !torque.is_a?(Vector) or torque.size != 3
|
81
|
+
raise(ArgumentError, "torque must be a 3D vector")
|
82
|
+
end
|
83
|
+
if !radius.is_a?(Vector) or radius.size != 2
|
84
|
+
raise(ArgumentError, "radius must be a 2D vector")
|
85
|
+
end
|
86
|
+
radius = Vector[radius[0], radius[1], 0]
|
87
|
+
radius.cross(torque) / radius.dot(radius)
|
88
|
+
end
|
89
|
+
|
67
90
|
module VectorForce
|
68
91
|
#
|
69
92
|
# Resistance Forces
|
data/lib/driving_physics.rb
CHANGED
@@ -1,37 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Units: metric
|
3
|
+
#
|
4
|
+
# distance: meter
|
5
|
+
# velocity: meter / sec
|
6
|
+
# acceleration: meter / sec^2
|
7
|
+
# volume: Liter
|
8
|
+
# temperature: Celsius
|
9
|
+
#
|
1
10
|
module DrivingPhysics
|
2
|
-
#
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
# acceleration: meter / sec^2
|
8
|
-
# volume: Liter
|
9
|
-
# temperature: Celsius
|
10
|
-
#
|
11
|
+
# runtime check; this returns false by default
|
12
|
+
# Vector is not currently/easily available in mruby
|
13
|
+
def self.has_vector?
|
14
|
+
Vector rescue false
|
15
|
+
end
|
11
16
|
|
12
|
-
#
|
13
17
|
# environmental defaults
|
14
|
-
#
|
15
18
|
HZ = 1000
|
16
19
|
TICK = Rational(1) / HZ
|
17
20
|
G = 9.8 # m/s^2
|
18
21
|
AIR_TEMP = 25 # deg c
|
19
22
|
AIR_DENSITY = 1.29 # kg / m^3
|
20
|
-
PETROL_DENSITY = 0.71 # kg / L
|
23
|
+
PETROL_DENSITY = 0.71 # kg / L
|
21
24
|
|
22
|
-
#
|
23
25
|
# defaults for resistance forces
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
ROLL_COF = 0.01 # roughly: street tires on concrete
|
26
|
+
FRONTAL_AREA = 2.2 # m^2, based roughly on 2000s-era Chevrolet Corvette
|
27
|
+
DRAG_COF = 0.3 # based roughly on 2000s-era Chevrolet Corvette
|
28
|
+
DRAG = 0.4257 # air_resistance at 1 m/s given above numbers
|
29
|
+
ROT_COF = 12.771 # if rot matches air at 30 m/s
|
30
|
+
ROT_CONST = 0.05 # N opposing drive force / torque
|
31
|
+
ROLL_COF = 0.01 # roughly: street tires on concrete
|
31
32
|
|
32
|
-
#
|
33
33
|
# constants
|
34
|
-
#
|
35
34
|
SECS_PER_MIN = 60
|
36
35
|
MINS_PER_HOUR = 60
|
37
36
|
SECS_PER_HOUR = SECS_PER_MIN * MINS_PER_HOUR
|
@@ -70,4 +69,11 @@ module DrivingPhysics
|
|
70
69
|
alias_method(:omega, :accum)
|
71
70
|
alias_method(:theta, :accum)
|
72
71
|
end
|
72
|
+
|
73
|
+
def self.unit_interval!(val)
|
74
|
+
if val < 0.0 or val > 1.0
|
75
|
+
raise(ArgumentError, "val #{val.inspect} should be between 0 and 1")
|
76
|
+
end
|
77
|
+
val
|
78
|
+
end
|
73
79
|
end
|
data/test/disk.rb
CHANGED
@@ -1,113 +1,78 @@
|
|
1
1
|
require 'minitest/autorun'
|
2
2
|
require 'driving_physics/disk'
|
3
|
+
require 'matrix'
|
3
4
|
|
4
|
-
|
5
|
+
include DrivingPhysics
|
5
6
|
|
6
|
-
describe
|
7
|
+
describe Disk do
|
7
8
|
describe "Disk.volume" do
|
8
9
|
it "calculates the volume (m^3) of disk given radius and width" do
|
9
|
-
cubic_m =
|
10
|
+
cubic_m = Disk.volume(1.0, 1.0)
|
10
11
|
expect(cubic_m).must_equal Math::PI
|
11
12
|
|
12
|
-
cubic_m =
|
13
|
+
cubic_m = Disk.volume(0.35, 0.2)
|
13
14
|
expect(cubic_m).must_be_within_epsilon 0.076969
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
18
|
describe "Disk.volume_l" do
|
18
19
|
it "calculates the volume (L) of a disk given radius and width" do
|
19
|
-
liters =
|
20
|
+
liters = Disk.volume_l(1.0, 1.0)
|
20
21
|
expect(liters).must_equal Math::PI * 1000
|
21
22
|
|
22
|
-
liters =
|
23
|
+
liters = Disk.volume_l(0.35, 0.2)
|
23
24
|
expect(liters).must_be_within_epsilon 76.96902
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
27
28
|
describe "Disk.density" do
|
28
29
|
it "calculates the density (kg/L) given mass and volume" do
|
29
|
-
expect(
|
30
|
-
expect(
|
30
|
+
expect(Disk.density(25.0, 25.0)).must_equal 1.0
|
31
|
+
expect(Disk.density(50.0, 25.0)).must_equal 2.0
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
35
|
describe "Disk.mass" do
|
35
36
|
it "calculates the mass (kg) of a disk given radius, width, and density" do
|
36
|
-
|
37
|
-
expect(D.mass(0.35, 0.2, D::DENSITY)).must_be_within_epsilon 25.015
|
37
|
+
expect(Disk.mass(0.35, 0.2, Disk::DENSITY)).must_be_within_epsilon 76.969
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
describe "Disk.rotational_inertia" do
|
42
42
|
it "calculates rotational inertia for a disk given radius and mass" do
|
43
|
-
expect(
|
43
|
+
expect(Disk.rotational_inertia(0.35, 25.0)).must_be_within_epsilon 1.53125
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
47
|
describe "Disk.alpha" do
|
48
48
|
it "calculates angular acceleration from torque and inertia" do
|
49
49
|
scalar_torque = 1000
|
50
|
-
inertia =
|
51
|
-
expect(
|
50
|
+
inertia = Disk.rotational_inertia(0.35, 25.0)
|
51
|
+
expect(Disk.alpha scalar_torque, inertia).must_be_within_epsilon 653.061
|
52
52
|
|
53
|
+
skip unless DrivingPhysics.has_vector?
|
53
54
|
vector_torque = Vector[0, 0, 1000]
|
54
|
-
vector_alpha =
|
55
|
+
vector_alpha = Disk.alpha vector_torque, inertia
|
55
56
|
expect(vector_alpha).must_be_instance_of Vector
|
56
57
|
expect(vector_alpha.size).must_equal 3
|
57
58
|
expect(vector_alpha[2]).must_be_within_epsilon 653.06
|
58
59
|
end
|
59
60
|
end
|
60
61
|
|
61
|
-
describe "Disk.torque_vector" do
|
62
|
-
it "calculates a torque in the 3rd dimension given 2D force and radius" do
|
63
|
-
force = Vector[1000, 0]
|
64
|
-
radius = Vector[0, 5]
|
65
|
-
torque = D.torque_vector(force, radius)
|
66
|
-
expect(torque).must_be_instance_of Vector
|
67
|
-
expect(torque.size).must_equal 3
|
68
|
-
expect(torque[2]).must_be_within_epsilon 5000.0
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
describe "Disk.force_vector" do
|
73
|
-
it "calculates a (3D) force given 3D torque and 2D radius" do
|
74
|
-
# let's invert the Disk.torque_vector case from above:
|
75
|
-
torque = Vector[0, 0, 5000]
|
76
|
-
radius = Vector[0, 5]
|
77
|
-
force = D.force_vector(torque, radius)
|
78
|
-
expect(force).must_be_instance_of Vector
|
79
|
-
expect(force.size).must_equal 3
|
80
|
-
expect(force[0]).must_be_within_epsilon 1000.0
|
81
|
-
|
82
|
-
# now let's rotate the radius into the x-dimension
|
83
|
-
# right hand rule, positive torque means thumb into screen, clockwise
|
84
|
-
# negative-x radius means positive-y force
|
85
|
-
torque = Vector[0, 0, 500]
|
86
|
-
radius = Vector[-5, 0]
|
87
|
-
force = D.force_vector(torque, radius)
|
88
|
-
expect(force).must_be_instance_of Vector
|
89
|
-
expect(force.size).must_equal 3
|
90
|
-
expect(force[1]).must_be_within_epsilon 100.0
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
62
|
describe "instance methods" do
|
95
63
|
before do
|
96
64
|
@env = DrivingPhysics::Environment.new
|
97
|
-
@disk =
|
65
|
+
@disk = Disk.new(@env)
|
98
66
|
end
|
99
67
|
|
100
68
|
it "initializes" do
|
101
|
-
|
102
|
-
expect(@disk).
|
103
|
-
expect(@disk.
|
104
|
-
|
105
|
-
|
106
|
-
with_mass = D.new(@env) { |w|
|
107
|
-
w.mass = 99.01
|
108
|
-
}
|
69
|
+
expect(@disk).must_be_instance_of Disk
|
70
|
+
expect(@disk.density).must_equal Disk::DENSITY # sanity check
|
71
|
+
expect(@disk.mass).must_be_within_epsilon 76.969
|
72
|
+
|
73
|
+
with_mass = Disk.new(@env) { |w| w.mass = 99.01 }
|
109
74
|
expect(with_mass.mass).must_equal 99.01
|
110
|
-
expect(with_mass.density).wont_equal
|
75
|
+
expect(with_mass.density).wont_equal Disk::DENSITY
|
111
76
|
end
|
112
77
|
|
113
78
|
it "has a string representation" do
|
@@ -122,8 +87,7 @@ describe D do
|
|
122
87
|
end
|
123
88
|
|
124
89
|
it "has inertia" do
|
125
|
-
|
126
|
-
expect(@disk.rotational_inertia).must_be_within_epsilon 1.5321
|
90
|
+
expect(@disk.rotational_inertia).must_be_within_epsilon 4.714
|
127
91
|
end
|
128
92
|
end
|
129
93
|
end
|
data/test/gearbox.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'driving_physics/gearbox'
|
3
|
+
|
4
|
+
include DrivingPhysics
|
5
|
+
|
6
|
+
describe Gearbox do
|
7
|
+
it "has gear ratios" do
|
8
|
+
end
|
9
|
+
|
10
|
+
it "has a final drive" do
|
11
|
+
end
|
12
|
+
|
13
|
+
it "has a neutral gear" do
|
14
|
+
end
|
15
|
+
|
16
|
+
it "has a clutch with state" do
|
17
|
+
end
|
18
|
+
|
19
|
+
it "has mass" do
|
20
|
+
end
|
21
|
+
|
22
|
+
it "has rotating mass" do
|
23
|
+
end
|
24
|
+
|
25
|
+
it "converts crank torque to axle torque" do
|
26
|
+
end
|
27
|
+
|
28
|
+
it "has losses due to inertia and friction" do
|
29
|
+
end
|
30
|
+
|
31
|
+
it "converts crank omega to axle omega (and vice versa)" do
|
32
|
+
end
|
33
|
+
|
34
|
+
it "varies torque delivery based on gear ratio and clutch" do
|
35
|
+
end
|
36
|
+
end
|
data/test/motor.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'driving_physics/motor'
|
3
|
+
|
4
|
+
include DrivingPhysics
|
5
|
+
|
6
|
+
describe "interpolate" do
|
7
|
+
it "validates xs and ys match in size" do
|
8
|
+
xs = [0, 5, 10, 15, 20]
|
9
|
+
ys = [10, 10, 10, 10, 5]
|
10
|
+
expect(DrivingPhysics.interpolate(1, xs: xs, ys: ys)).must_equal 10
|
11
|
+
expect { DrivingPhysics.interpolate(1, xs: xs + [10], ys: ys) }.must_raise
|
12
|
+
expect { DrivingPhysics.interpolate(1, xs: xs, ys: ys + [10]) }.must_raise
|
13
|
+
end
|
14
|
+
|
15
|
+
it "validates x is within the range of xs" do
|
16
|
+
xs = [0, 5, 10, 15, 20]
|
17
|
+
ys = [10, 10, 10, 10, 5]
|
18
|
+
expect(DrivingPhysics.interpolate(1, xs: xs, ys: ys)).must_equal 10
|
19
|
+
expect { DrivingPhysics.interpolate(-1, xs: xs, ys: ys) }.must_raise
|
20
|
+
expect { DrivingPhysics.interpolate(25, xs: xs, ys: ys) }.must_raise
|
21
|
+
end
|
22
|
+
|
23
|
+
it "validates the xs are in ascending order" do
|
24
|
+
xs = [0, 5, 10, 15, 20]
|
25
|
+
ys = [10, 10, 10, 10, 5]
|
26
|
+
expect(DrivingPhysics.interpolate(1, xs: xs, ys: ys)).must_equal 10
|
27
|
+
xs[1] = 15
|
28
|
+
expect {
|
29
|
+
DrivingPhysics.interpolate(16, xs: xs, ys: ys)
|
30
|
+
}.must_raise
|
31
|
+
end
|
32
|
+
|
33
|
+
it "performs linear interpolation to yield a y value for a valid x" do
|
34
|
+
xs = [0, 5, 10, 15, 20]
|
35
|
+
ys = [5, 5, 10, 10, 15]
|
36
|
+
|
37
|
+
expect(DrivingPhysics.interpolate(0, xs: xs, ys: ys)).must_equal 5
|
38
|
+
expect(DrivingPhysics.interpolate(1, xs: xs, ys: ys)).must_equal 5
|
39
|
+
expect(DrivingPhysics.interpolate(4, xs: xs, ys: ys)).must_equal 5
|
40
|
+
expect(DrivingPhysics.interpolate(5, xs: xs, ys: ys)).must_equal 5
|
41
|
+
expect(DrivingPhysics.interpolate(6, xs: xs, ys: ys)).must_equal 6
|
42
|
+
expect(DrivingPhysics.interpolate(7, xs: xs, ys: ys)).must_equal 7
|
43
|
+
expect(DrivingPhysics.interpolate(8, xs: xs, ys: ys)).must_equal 8
|
44
|
+
expect(DrivingPhysics.interpolate(9, xs: xs, ys: ys)).must_equal 9
|
45
|
+
expect(DrivingPhysics.interpolate(10, xs: xs, ys: ys)).must_equal 10
|
46
|
+
expect(DrivingPhysics.interpolate(11, xs: xs, ys: ys)).must_equal 10
|
47
|
+
expect(DrivingPhysics.interpolate(12, xs: xs, ys: ys)).must_equal 10
|
48
|
+
expect(DrivingPhysics.interpolate(14, xs: xs, ys: ys)).must_equal 10
|
49
|
+
expect(DrivingPhysics.interpolate(15, xs: xs, ys: ys)).must_equal 10
|
50
|
+
expect(DrivingPhysics.interpolate(16, xs: xs, ys: ys)).must_equal 11
|
51
|
+
expect(DrivingPhysics.interpolate(17, xs: xs, ys: ys)).must_equal 12
|
52
|
+
expect(DrivingPhysics.interpolate(18, xs: xs, ys: ys)).must_equal 13
|
53
|
+
expect(DrivingPhysics.interpolate(19, xs: xs, ys: ys)).must_equal 14
|
54
|
+
expect(DrivingPhysics.interpolate(20, xs: xs, ys: ys)).must_equal 15
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# shorthand
|
59
|
+
def tc(rpms, torques)
|
60
|
+
TorqueCurve.new(rpms: rpms, torques: torques)
|
61
|
+
end
|
62
|
+
|
63
|
+
describe TorqueCurve do
|
64
|
+
before do
|
65
|
+
@default = TorqueCurve.new
|
66
|
+
@rpms = [500, 1000, 3500, 6000, 7000, 7100]
|
67
|
+
@torques = [ 0, 100, 300, 300, 250, 0]
|
68
|
+
@minr = [0, 1]
|
69
|
+
@mint = [0, 0]
|
70
|
+
end
|
71
|
+
|
72
|
+
it "maps rpm values to torque values" do
|
73
|
+
expect(@default.torque(3500)).must_be(:>, 100)
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "initialize" do
|
77
|
+
it "accepts two arrays: rpms, and the corresponding torques" do
|
78
|
+
expect(tc(@rpms, @torques)).must_be_kind_of(TorqueCurve)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "has defaults for rpms and torques" do
|
82
|
+
expect(@default).must_be_kind_of(TorqueCurve)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "validates size match for rpms and torques" do
|
86
|
+
expect( tc( @minr, @mint) ).must_be_kind_of TorqueCurve
|
87
|
+
expect { tc( [0], @mint) }.must_raise
|
88
|
+
expect { tc([0,1,2], @mint) }.must_raise
|
89
|
+
expect { tc(@minr, [0]) }.must_raise
|
90
|
+
expect { tc(@minr, [0,1,0]) }.must_raise
|
91
|
+
|
92
|
+
rpms = [100, 2000, 30_000]
|
93
|
+
torques = [0, 500, 0]
|
94
|
+
|
95
|
+
expect( tc( rpms, torques) ).must_be_kind_of TorqueCurve
|
96
|
+
expect { tc( rpms, [0, 0]) }.must_raise
|
97
|
+
expect { tc([1,2], torques) }.must_raise
|
98
|
+
expect { tc([1,2,3,4], torques) }.must_raise
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
describe "rpms" do
|
103
|
+
it "validates rpms are positive" do
|
104
|
+
expect( tc( @minr, @mint) ).must_be_kind_of TorqueCurve
|
105
|
+
expect { tc([-10, 10], @mint) }.must_raise
|
106
|
+
end
|
107
|
+
|
108
|
+
it "validates rpms are in ascending order" do
|
109
|
+
expect( tc( @minr, @mint) ).must_be_kind_of TorqueCurve
|
110
|
+
expect { tc([0, 0], @mint) }.must_raise
|
111
|
+
expect( tc([99, 100], @mint) ).must_be_kind_of TorqueCurve
|
112
|
+
expect { tc([100, 99], @mint) }.must_raise
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "torques" do
|
117
|
+
it "validates that torques start and end at zero" do
|
118
|
+
expect( tc(@minr, @mint) ).must_be_kind_of TorqueCurve
|
119
|
+
expect { tc(@minr, [1,0]) }.must_raise
|
120
|
+
expect { tc(@minr, [0,1]) }.must_raise
|
121
|
+
expect { tc(@minr, [1,1]) }.must_raise
|
122
|
+
|
123
|
+
expect( tc([1,2,3], [0,500,0]) ).must_be_kind_of TorqueCurve
|
124
|
+
expect { tc([1,2,3], [1,500,0]) }.must_raise
|
125
|
+
expect { tc([1,2,3], [0,500,1]) }.must_raise
|
126
|
+
end
|
127
|
+
|
128
|
+
it "validates that torques are positive" do
|
129
|
+
expect( tc([1,2,3], [0, 5,0]) ).must_be_kind_of TorqueCurve
|
130
|
+
expect { tc([1,2,3], [0,-5,0]) }.must_raise
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it "finds the peak torque and corresponding RPM" do
|
136
|
+
rpm, torque = *@default.peak
|
137
|
+
expect(rpm).must_be(:>, 1000)
|
138
|
+
expect(rpm).must_be(:<, 10_000)
|
139
|
+
expect(torque).must_be(:>, 0)
|
140
|
+
expect(torque).must_be(:<, 10_000)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "interpolates to find the torque for a valid RPM" do
|
144
|
+
tc = tc(@rpms, @torques)
|
145
|
+
r1 = @rpms[1]
|
146
|
+
r2 = @rpms[2]
|
147
|
+
|
148
|
+
expect(tc.torque(r1)).must_equal @torques[1]
|
149
|
+
expect(tc.torque(r2)).must_equal @torques[2]
|
150
|
+
|
151
|
+
r3 = (r1 + r2) / 2
|
152
|
+
t3 = tc.torque(r3)
|
153
|
+
t3_expect = (@torques[1] + @torques[2]) / 2.0
|
154
|
+
|
155
|
+
expect(t3).must_be :>, @torques[1]
|
156
|
+
expect(t3).must_be :<, @torques[2]
|
157
|
+
expect(t3).must_be :>=, t3_expect.floor
|
158
|
+
expect(t3).must_be :<=, t3_expect.ceil
|
159
|
+
end
|
160
|
+
|
161
|
+
it "tracks min, idle, redline, and max rpms" do
|
162
|
+
expect(@default.min).must_be :>, 0
|
163
|
+
expect(@default.min).must_be :<, 1000
|
164
|
+
expect(@default.idle).must_be :>, @default.min
|
165
|
+
expect(@default.idle).must_be :<, 3000
|
166
|
+
expect(@default.redline).must_be :>, @default.idle
|
167
|
+
expect(@default.redline).must_be :<, 20_000
|
168
|
+
expect(@default.max).must_be :>, @default.redline
|
169
|
+
expect(@default.max).must_be :<, 20_000
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe Motor do
|
174
|
+
before do
|
175
|
+
@default = Motor.new(Environment.new)
|
176
|
+
end
|
177
|
+
|
178
|
+
it "has a throttle with state" do
|
179
|
+
expect(@default.throttle).must_equal 0.0
|
180
|
+
@default.throttle = 0.5
|
181
|
+
expect(@default.throttle).must_equal 0.5
|
182
|
+
expect(@default.throttle_pct).must_equal "50.0%"
|
183
|
+
|
184
|
+
expect { @default.throttle = 1.5 }.must_raise
|
185
|
+
end
|
186
|
+
|
187
|
+
it "has mass" do
|
188
|
+
expect(@default.mass).must_be :>, 0
|
189
|
+
expect(@default.fixed_mass).must_be :>, 0
|
190
|
+
expect(@default.rotating_mass).must_be :>, 0
|
191
|
+
end
|
192
|
+
|
193
|
+
it "has a torque curve" do
|
194
|
+
expect(@default.torque_curve).must_be_kind_of TorqueCurve
|
195
|
+
end
|
196
|
+
|
197
|
+
it "has idle RPM and redline RPM" do
|
198
|
+
expect(@default.idle).must_be :>, 500
|
199
|
+
expect(@default.idle).must_be :<, 1500
|
200
|
+
|
201
|
+
expect(@default.redline).must_be :>, @default.idle
|
202
|
+
expect(@default.redline).must_be :<, 10_000
|
203
|
+
end
|
204
|
+
|
205
|
+
it "has inertia and energy" do
|
206
|
+
expect(@default.inertia).must_be :>, 0
|
207
|
+
expect(@default.energy(99)).must_be :>, 0
|
208
|
+
end
|
209
|
+
|
210
|
+
it "determines crank alpha from torque" do
|
211
|
+
expect(@default.alpha(50)).must_be :>, 0
|
212
|
+
end
|
213
|
+
|
214
|
+
it "determines torque from crank alpha" do
|
215
|
+
a = @default.alpha(50, omega: 20)
|
216
|
+
t = @default.implied_torque(a)
|
217
|
+
# frictional losses
|
218
|
+
expect(t).must_be :>, 40
|
219
|
+
expect(t).must_be :<, 50
|
220
|
+
end
|
221
|
+
|
222
|
+
it "determines torque based on torque curve, RPM and throttle" do
|
223
|
+
@default.throttle = 1.0
|
224
|
+
expect(@default.torque(1000)).must_be :>, 0
|
225
|
+
expect(@default.torque(3500)).must_be :>, @default.torque(1000)
|
226
|
+
end
|
227
|
+
|
228
|
+
it "has an engine braking effect when the throttle is closed" do
|
229
|
+
@default.throttle = 0
|
230
|
+
expect(@default.torque(3500)).must_be :<, 0
|
231
|
+
end
|
232
|
+
|
233
|
+
it "has a starter motor to get running" do
|
234
|
+
expect(@default.starter_torque).must_be :>, 0
|
235
|
+
end
|
236
|
+
end
|
data/test/powertrain.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'driving_physics/powertrain'
|
3
|
+
|
4
|
+
include DrivingPhysics
|
5
|
+
|
6
|
+
describe Powertrain do
|
7
|
+
it "has a motor and a gearbox" do
|
8
|
+
end
|
9
|
+
|
10
|
+
it "has mass" do
|
11
|
+
end
|
12
|
+
|
13
|
+
it "coverts rpm to axle torque" do
|
14
|
+
end
|
15
|
+
|
16
|
+
it "converts rpm to axle omega" do
|
17
|
+
end
|
18
|
+
|
19
|
+
it "determines crank rpm from axle_omega" do
|
20
|
+
end
|
21
|
+
end
|
data/test/scalar_force.rb
CHANGED
@@ -12,8 +12,7 @@ describe ScalarForce do
|
|
12
12
|
# ROT_COF's value is from observing that rotational resistance
|
13
13
|
# matches air resistance at roughly 30 m/s in street cars
|
14
14
|
it "approximates a reasonable rotational resistance constant" do
|
15
|
-
|
16
|
-
must_be_within_epsilon(-1 * ROT_COF)
|
15
|
+
_(30 * ScalarForce.air_resistance(1)).must_be_within_epsilon(-1 * ROT_COF)
|
17
16
|
end
|
18
17
|
|
19
18
|
it "approximates a positive drag force" do
|
@@ -21,12 +20,10 @@ describe ScalarForce do
|
|
21
20
|
end
|
22
21
|
|
23
22
|
it "approximates a positive rotational resistance force" do
|
24
|
-
|
25
|
-
must_be_within_epsilon(-383.13)
|
23
|
+
_(ScalarForce.rotational_resistance 30).must_be_within_epsilon(-383.13)
|
26
24
|
end
|
27
25
|
|
28
26
|
it "approximates a positive rolling resistance force" do
|
29
|
-
|
30
|
-
expect(ScalarForce.rolling_resistance nf).must_be_within_epsilon(-98.0)
|
27
|
+
_(ScalarForce.rolling_resistance(1000 * G)).must_be_within_epsilon(-98.0)
|
31
28
|
end
|
32
29
|
end
|