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.
@@ -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
  #