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