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
data/lib/driving_physics/car.rb
CHANGED
@@ -1,294 +1,123 @@
|
|
1
|
-
require 'driving_physics/environment'
|
2
|
-
require 'driving_physics/vector_force'
|
3
1
|
require 'driving_physics/tire'
|
2
|
+
require 'driving_physics/powertrain'
|
4
3
|
|
5
4
|
module DrivingPhysics
|
6
|
-
# treat instances of this class as immutable
|
7
5
|
class Car
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@max_drive_force = 7000 # N - 1000kg car at 0.7g acceleration
|
19
|
-
@max_brake_clamp = 100 # N
|
20
|
-
@max_brake_force = 40_000 # N - 2000kg car at 2g braking
|
21
|
-
@fuel_capacity = 40 # L
|
22
|
-
@brake_pad_depth = 10 # mm
|
23
|
-
@driver_mass = 75 # kg
|
24
|
-
@fuel_consumption = 0.02 # L/s at full throttle
|
25
|
-
|
26
|
-
@frontal_area = DrivingPhysics::FRONTAL_AREA # m^2
|
6
|
+
attr_reader :tire, :powertrain, :env
|
7
|
+
attr_accessor :num_tires, :body_mass, :frontal_area, :cd
|
8
|
+
|
9
|
+
def initialize(tire:, powertrain:)
|
10
|
+
@num_tires = 4
|
11
|
+
@tire = tire
|
12
|
+
@env = @tire.env
|
13
|
+
@powertrain = powertrain
|
14
|
+
@body_mass = 1000.0
|
15
|
+
@frontal_area = DrivingPhysics::FRONTAL_AREA
|
27
16
|
@cd = DrivingPhysics::DRAG_COF
|
28
17
|
|
29
|
-
@tires = Tire.new
|
30
|
-
@controls = Controls.new
|
31
|
-
@condition = Condition.new(brake_temp: @environment.air_temp,
|
32
|
-
brake_pad_depth: @brake_pad_depth)
|
33
|
-
|
34
|
-
# consider downforce
|
35
|
-
# probably area * angle
|
36
|
-
# goes up with square of velocity
|
37
|
-
|
38
18
|
yield self if block_given?
|
39
19
|
end
|
40
20
|
|
41
|
-
def
|
42
|
-
|
43
|
-
format("Power: %.1f kN", @max_drive_force.to_f / 1000),
|
44
|
-
format("Brakes: %.1f kN", @max_brake_force.to_f / 1000),
|
45
|
-
format("Fr.A: %.2f m^2", @frontal_area),
|
46
|
-
format("cD: %.2f", @cd),
|
47
|
-
].join(' | '),
|
48
|
-
[format("Op: %d N", drive_force - brake_force),
|
49
|
-
format("Drive: %d N", drive_force),
|
50
|
-
format("Brake: %d N", brake_force),
|
51
|
-
].join(' | '),
|
52
|
-
[format("Net: %.1f N", sum_forces.magnitude),
|
53
|
-
format("Air: %.1f N", air_resistance.magnitude),
|
54
|
-
format("Rot: %.1f N", rotational_resistance.magnitude),
|
55
|
-
format("Roll: %.1f N", rolling_resistance.magnitude),
|
56
|
-
].join(' | '),
|
57
|
-
@controls, @condition, @tires,
|
58
|
-
].join("\n")
|
21
|
+
def throttle
|
22
|
+
@powertrain.motor.throttle
|
59
23
|
end
|
60
24
|
|
61
|
-
def
|
62
|
-
@
|
63
|
-
mass: total_mass,
|
64
|
-
tire: @tires,
|
65
|
-
env: @environment)
|
66
|
-
|
67
|
-
@condition.consume_fuel(@fuel_consumption *
|
68
|
-
@controls.drive_pedal *
|
69
|
-
@environment.tick)
|
25
|
+
def throttle=(val)
|
26
|
+
@powertrain.motor.throttle = val
|
70
27
|
end
|
71
28
|
|
72
|
-
def
|
73
|
-
@
|
29
|
+
def gear
|
30
|
+
@powertrain.gearbox.gear
|
74
31
|
end
|
75
32
|
|
76
|
-
def
|
77
|
-
@
|
33
|
+
def gear=(val)
|
34
|
+
@powertrain.gearbox.gear = val
|
78
35
|
end
|
79
36
|
|
80
|
-
def
|
81
|
-
@
|
37
|
+
def top_gear
|
38
|
+
@powertrain.gearbox.top_gear
|
82
39
|
end
|
83
40
|
|
84
|
-
|
85
|
-
|
41
|
+
# force opposing speed
|
42
|
+
def air_force(speed)
|
43
|
+
-0.5 * @frontal_area * @cd * @env.air_density * speed ** 2
|
86
44
|
end
|
87
45
|
|
88
|
-
|
89
|
-
|
46
|
+
# force of opposite sign to omega
|
47
|
+
def tire_rolling_force(omega)
|
48
|
+
@num_tires *
|
49
|
+
@tire.rolling_friction(omega, normal_force: self.normal_force) /
|
50
|
+
@tire.radius
|
90
51
|
end
|
91
52
|
|
92
|
-
|
93
|
-
|
53
|
+
# force of opposite sign to omega
|
54
|
+
def tire_rotational_force(omega)
|
55
|
+
@num_tires *
|
56
|
+
@tire.rotating_friction(omega, normal_force: self.normal_force) /
|
57
|
+
@tire.radius
|
94
58
|
end
|
95
59
|
|
96
|
-
|
97
|
-
|
60
|
+
# force of opposite sign to force
|
61
|
+
def tire_inertial_force(force)
|
62
|
+
mag = force.abs
|
63
|
+
sign = force / mag
|
64
|
+
force_loss = 0
|
65
|
+
5.times {
|
66
|
+
# use magnitude, so we are subtracting only positive numbers
|
67
|
+
acc = DrivingPhysics.acc(mag - force_loss, self.total_mass)
|
68
|
+
alpha = acc / @tire.radius
|
69
|
+
# this will be a positive number
|
70
|
+
force_loss = @num_tires * @tire.implied_torque(alpha) /
|
71
|
+
@tire.radius
|
72
|
+
}
|
73
|
+
# oppose initial force
|
74
|
+
-1 * sign * force_loss
|
98
75
|
end
|
99
76
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
77
|
+
def to_s
|
78
|
+
[[format("Mass: %.1f kg", self.total_mass),
|
79
|
+
format("Fr.A: %.2f m^2", @frontal_area),
|
80
|
+
format("cD: %.2f", @cd),
|
81
|
+
].join(' | '),
|
82
|
+
format("POWERTRAIN:\n%s", @powertrain),
|
83
|
+
format("Tires: %s", @tire),
|
84
|
+
format("Corner mass: %.1f kg | Normal force: %.1f N",
|
85
|
+
self.corner_mass, self.normal_force),
|
86
|
+
].join("\n")
|
105
87
|
end
|
106
88
|
|
107
|
-
def
|
108
|
-
|
109
|
-
VectorForce.air_resistance(@condition.vel,
|
110
|
-
frontal_area: @frontal_area,
|
111
|
-
drag_cof: @cd)
|
89
|
+
def drive_force(rpm)
|
90
|
+
@tire.force(@powertrain.axle_torque(rpm))
|
112
91
|
end
|
113
92
|
|
114
|
-
def
|
115
|
-
|
116
|
-
VectorForce.rotational_resistance(@condition.vel)
|
93
|
+
def tire_speed(rpm)
|
94
|
+
@tire.tangential(@powertrain.axle_omega(rpm))
|
117
95
|
end
|
118
96
|
|
119
|
-
def
|
120
|
-
|
121
|
-
VectorForce.rolling_resistance(weight,
|
122
|
-
dir: @condition.movement_dir,
|
123
|
-
roll_cof: @tires.roll_cof)
|
97
|
+
def motor_rpm(tire_speed)
|
98
|
+
@powertrain.gearbox.crank_rpm(tire_speed / @tire_radius)
|
124
99
|
end
|
125
100
|
|
126
|
-
def
|
127
|
-
|
101
|
+
def total_mass
|
102
|
+
@body_mass + @powertrain.mass + @tire.mass * @num_tires
|
128
103
|
end
|
129
104
|
|
130
|
-
def
|
131
|
-
|
105
|
+
def corner_mass
|
106
|
+
Rational(self.total_mass) / @num_tires
|
132
107
|
end
|
133
108
|
|
134
|
-
|
135
|
-
|
109
|
+
# per tire
|
110
|
+
def normal_force
|
111
|
+
self.corner_mass * @env.g
|
136
112
|
end
|
137
113
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
def initialize
|
142
|
-
@drive_pedal = 0.0 # up to 1.0
|
143
|
-
@brake_pedal = 0.0 # up to 1.0
|
144
|
-
@steering_wheel = 0.0 # -1.0 to 1.0
|
145
|
-
end
|
146
|
-
|
147
|
-
def drive_pedal=(flt)
|
148
|
-
@drive_pedal = flt.clamp(0.0, 1.0)
|
149
|
-
end
|
150
|
-
|
151
|
-
def brake_pedal=(flt)
|
152
|
-
@brake_pedal = flt.clamp(0.0, 1.0)
|
153
|
-
end
|
154
|
-
|
155
|
-
def steering_wheel=(flt)
|
156
|
-
@steering_wheel = steering_wheel.clamp(-1.0, 1.0)
|
157
|
-
end
|
158
|
-
|
159
|
-
def to_s
|
160
|
-
[format("Throttle: %d%%", @drive_pedal * 100),
|
161
|
-
format("Brake: %d%%", @brake_pedal * 100),
|
162
|
-
format("Steering: %d%%", @steering_wheel * 100),
|
163
|
-
].join(" | ")
|
164
|
-
end
|
114
|
+
# per tire
|
115
|
+
def tire_traction
|
116
|
+
@tire.traction(self.normal_force)
|
165
117
|
end
|
166
118
|
|
167
|
-
|
168
|
-
|
169
|
-
using VectorZeroBackport
|
170
|
-
end
|
171
|
-
|
172
|
-
attr_reader :dir, :pos, :vel, :acc, :fuel, :lat_g, :lon_g,
|
173
|
-
:wheelspeed, :brake_temp, :brake_pad_depth
|
174
|
-
|
175
|
-
def initialize(dir: DrivingPhysics.random_unit_vector,
|
176
|
-
brake_temp: AIR_TEMP,
|
177
|
-
brake_pad_depth: )
|
178
|
-
@dir = dir # maybe rename to @heading ?
|
179
|
-
@pos = Vector[0, 0]
|
180
|
-
@vel = Vector[0, 0]
|
181
|
-
@acc = Vector[0, 0]
|
182
|
-
@fuel = 0.0 # L
|
183
|
-
@lat_g = 0.0 # g
|
184
|
-
@lon_g = 0.0 # g
|
185
|
-
@wheelspeed = 0.0 # m/s (sliding when it differs from @speed)
|
186
|
-
@brake_temp = brake_temp
|
187
|
-
@brake_pad_depth = brake_pad_depth # mm
|
188
|
-
end
|
189
|
-
|
190
|
-
def to_s
|
191
|
-
[[compass,
|
192
|
-
format('P(%d, %d)', @pos[0], @pos[1]),
|
193
|
-
format('V(%.1f, %.1f)', @vel[0], @vel[1]),
|
194
|
-
format('A(%.2f, %.2f)', @acc[0], @acc[1]),
|
195
|
-
].join(' | '),
|
196
|
-
[format('%.1f m/s', speed),
|
197
|
-
format('LatG: %.2f', lat_g),
|
198
|
-
format('LonG: %.2f', lon_g),
|
199
|
-
format('Whl: %.1f m/s', @wheelspeed),
|
200
|
-
format('Slide: %.1f m/s', slide_speed),
|
201
|
-
].join(' | '),
|
202
|
-
[format('Brakes: %.1f C %.1f mm', @brake_temp, @brake_pad_depth),
|
203
|
-
format('Fuel: %.2f L', @fuel),
|
204
|
-
].join(' | ')
|
205
|
-
].join("\n")
|
206
|
-
end
|
207
|
-
|
208
|
-
# left is negative, right is positive
|
209
|
-
def lat_dir
|
210
|
-
DrivingPhysics.rot_90(@dir, clockwise: true)
|
211
|
-
end
|
212
|
-
|
213
|
-
# note, we might be moving backwards, so not always @dir
|
214
|
-
# and we can't normalize a zero vector if we're not moving
|
215
|
-
def movement_dir
|
216
|
-
(speed == 0.0) ? @dir : @vel.normalize
|
217
|
-
end
|
218
|
-
|
219
|
-
def tick!(force:, mass:, tire:, env:)
|
220
|
-
# take the longitudinal component of the force, relative to vel dir
|
221
|
-
vel_dir = @vel.zero? ? @dir : @vel.normalize
|
222
|
-
lon_force = force.dot(vel_dir)
|
223
|
-
@wheelspeed = nil
|
224
|
-
|
225
|
-
if lon_force < 0.0
|
226
|
-
is_stopping = true
|
227
|
-
if @vel.zero?
|
228
|
-
@acc = Vector[0,0]
|
229
|
-
@wheelspeed = 0.0
|
230
|
-
@lon_g = 0.0
|
231
|
-
|
232
|
-
# TODO: update any other physical vars
|
233
|
-
return
|
234
|
-
end
|
235
|
-
else
|
236
|
-
is_stopping = false
|
237
|
-
end
|
238
|
-
|
239
|
-
# now detect sliding
|
240
|
-
nominal_acc = DrivingPhysics.acc(force, mass)
|
241
|
-
init_v = @vel
|
242
|
-
|
243
|
-
if nominal_acc.magnitude > tire.max_g * env.g
|
244
|
-
nominal_v = DrivingPhysics.vel(@vel, nominal_acc, dt: env.tick)
|
245
|
-
|
246
|
-
# check for reversal of velocity; could be wheelspin while
|
247
|
-
# moving backwards, so can't just look at is_stopping
|
248
|
-
if nominal_v.dot(@vel) < 0 and is_stopping
|
249
|
-
@wheelspeed = 0.0
|
250
|
-
else
|
251
|
-
@wheelspeed = nominal_v.magnitude
|
252
|
-
end
|
253
|
-
@acc = nominal_acc.normalize * tire.max_g * env.g
|
254
|
-
else
|
255
|
-
@acc = nominal_acc
|
256
|
-
end
|
257
|
-
|
258
|
-
@vel = DrivingPhysics.vel(@vel, @acc, dt: env.tick)
|
259
|
-
@wheelspeed ||= @vel.magnitude
|
260
|
-
|
261
|
-
# finally, detect velocity reversal when stopping
|
262
|
-
if is_stopping and @vel.dot(init_v) < 0
|
263
|
-
puts "crossed zero; stopping!"
|
264
|
-
@vel = Vector[0, 0]
|
265
|
-
@wheelspeed = 0.0
|
266
|
-
@lon_g = 0.0
|
267
|
-
end
|
268
|
-
|
269
|
-
@lon_g = @acc.dot(@dir) / env.g
|
270
|
-
@pos = DrivingPhysics.pos(@pos, @vel, dt: env.tick)
|
271
|
-
end
|
272
|
-
|
273
|
-
def add_fuel(liters)
|
274
|
-
@fuel += liters
|
275
|
-
end
|
276
|
-
|
277
|
-
def consume_fuel(liters)
|
278
|
-
@fuel -= liters
|
279
|
-
end
|
280
|
-
|
281
|
-
def speed
|
282
|
-
@vel.magnitude
|
283
|
-
end
|
284
|
-
|
285
|
-
def slide_speed
|
286
|
-
(speed - @wheelspeed).abs
|
287
|
-
end
|
288
|
-
|
289
|
-
def compass
|
290
|
-
DrivingPhysics.compass_dir(@dir)
|
291
|
-
end
|
119
|
+
def total_normal_force
|
120
|
+
self.total_mass * env.g
|
292
121
|
end
|
293
122
|
end
|
294
123
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module DrivingPhysics
|
2
|
+
module CLI
|
3
|
+
# returns user input as a string
|
4
|
+
def self.prompt(msg = '')
|
5
|
+
print msg + ' ' unless msg.empty?
|
6
|
+
print '> '
|
7
|
+
$stdin.gets.chomp
|
8
|
+
end
|
9
|
+
|
10
|
+
# press Enter to continue, ignore input, return elapsed time
|
11
|
+
def self.pause(msg = '')
|
12
|
+
t = Timer.now
|
13
|
+
puts msg unless msg.empty?
|
14
|
+
puts ' [ Press Enter ]'
|
15
|
+
$stdin.gets
|
16
|
+
Timer.since(t)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Timer
|
21
|
+
if defined? Process::CLOCK_MONOTONIC
|
22
|
+
def self.now
|
23
|
+
Process.clock_gettime Process::CLOCK_MONOTONIC
|
24
|
+
end
|
25
|
+
else
|
26
|
+
def self.now
|
27
|
+
Time.now
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.since(t)
|
32
|
+
self.now - t
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.elapsed(&work)
|
36
|
+
t = self.now
|
37
|
+
return yield, self.since(t)
|
38
|
+
end
|
39
|
+
|
40
|
+
# HH:MM:SS.mmm
|
41
|
+
def self.display(seconds: 0, ms: 0)
|
42
|
+
ms += (seconds * 1000).round if seconds > 0
|
43
|
+
DrivingPhysics.elapsed_display(ms)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.summary(start, num_ticks, paused = 0)
|
47
|
+
elapsed = self.since(start) - paused
|
48
|
+
format("%.3f s (%d ticks/s)", elapsed, num_ticks.to_f / elapsed)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'driving_physics/environment'
|
2
|
+
require 'driving_physics/vector_force'
|
3
|
+
|
4
|
+
module DrivingPhysics
|
5
|
+
# radius is always in meters
|
6
|
+
# force in N
|
7
|
+
# torque in Nm
|
8
|
+
|
9
|
+
# Rotational complements to acc/vel/pos
|
10
|
+
# alpha - angular acceleration (radians / s / s)
|
11
|
+
# omega - angular velocity (radians / s)
|
12
|
+
# theta - radians
|
13
|
+
|
14
|
+
# convert radians to revolutions; works for alpha/omega/theta
|
15
|
+
def self.revs(rads)
|
16
|
+
rads / (2 * Math::PI)
|
17
|
+
end
|
18
|
+
|
19
|
+
# convert revs to rads; works for alpha/omega/theta
|
20
|
+
def self.rads(revs)
|
21
|
+
revs * 2 * Math::PI
|
22
|
+
end
|
23
|
+
|
24
|
+
# convert rpm to omega (rads / s)
|
25
|
+
def self.omega(rpm)
|
26
|
+
self.rads(rpm / 60.0)
|
27
|
+
end
|
28
|
+
|
29
|
+
# convert omega to RPM (revs per minute)
|
30
|
+
def self.rpm(omega)
|
31
|
+
self.revs(omega) * 60
|
32
|
+
end
|
33
|
+
|
34
|
+
class Disk
|
35
|
+
DENSITY = 1.0 # kg / L
|
36
|
+
|
37
|
+
# torque = force * distance
|
38
|
+
def self.force(axle_torque, radius)
|
39
|
+
axle_torque / radius.to_f
|
40
|
+
end
|
41
|
+
|
42
|
+
# in m^3
|
43
|
+
def self.volume(radius, width)
|
44
|
+
Math::PI * radius ** 2 * width
|
45
|
+
end
|
46
|
+
|
47
|
+
# in L
|
48
|
+
def self.volume_l(radius, width)
|
49
|
+
volume(radius, width) * 1000
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.density(mass, volume_l)
|
53
|
+
mass.to_f / volume_l
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.mass(radius, width, density)
|
57
|
+
volume_l(radius, width) * density
|
58
|
+
end
|
59
|
+
|
60
|
+
# I = 1/2 (m)(r^2) for a disk
|
61
|
+
def self.rotational_inertia(radius, mass)
|
62
|
+
mass * radius**2 / 2.0
|
63
|
+
end
|
64
|
+
class << self
|
65
|
+
alias_method(:moment_of_inertia, :rotational_inertia)
|
66
|
+
end
|
67
|
+
|
68
|
+
# angular acceleration
|
69
|
+
def self.alpha(torque, inertia)
|
70
|
+
torque / inertia
|
71
|
+
end
|
72
|
+
|
73
|
+
# convert alpha/omega/theta to acc/vel/pos
|
74
|
+
def self.tangential(rotational, radius)
|
75
|
+
rotational * radius
|
76
|
+
end
|
77
|
+
|
78
|
+
# convert acc/vel/pos to alpha/omega/theta
|
79
|
+
def self.rotational(tangential, radius)
|
80
|
+
tangential.to_f / radius
|
81
|
+
end
|
82
|
+
|
83
|
+
# vectors only
|
84
|
+
def self.torque_vector(force, radius)
|
85
|
+
if !force.is_a?(Vector) or force.size != 2
|
86
|
+
raise(ArgumentError, "force must be a 2D vector")
|
87
|
+
end
|
88
|
+
if !radius.is_a?(Vector) or radius.size != 2
|
89
|
+
raise(ArgumentError, "radius must be a 2D vector")
|
90
|
+
end
|
91
|
+
force = Vector[force[0], force[1], 0]
|
92
|
+
radius = Vector[radius[0], radius[1], 0]
|
93
|
+
force.cross(radius)
|
94
|
+
end
|
95
|
+
|
96
|
+
# vectors only
|
97
|
+
def self.force_vector(torque, radius)
|
98
|
+
if !torque.is_a?(Vector) or torque.size != 3
|
99
|
+
raise(ArgumentError, "torque must be a 3D vector")
|
100
|
+
end
|
101
|
+
if !radius.is_a?(Vector) or radius.size != 2
|
102
|
+
raise(ArgumentError, "radius must be a 2D vector")
|
103
|
+
end
|
104
|
+
radius = Vector[radius[0], radius[1], 0]
|
105
|
+
radius.cross(torque) / radius.dot(radius)
|
106
|
+
end
|
107
|
+
|
108
|
+
attr_reader :env
|
109
|
+
attr_accessor :radius, :width, :density, :base_friction, :omega_friction
|
110
|
+
|
111
|
+
def initialize(env)
|
112
|
+
@env = env
|
113
|
+
@radius = 350/1000r # m
|
114
|
+
@width = 200/1000r # m
|
115
|
+
@density = DENSITY
|
116
|
+
@base_friction = 5/100_000r # constant resistance to rotation
|
117
|
+
@omega_friction = 5/100_000r # scales with omega
|
118
|
+
yield self if block_given?
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_s
|
122
|
+
[[format("%d mm x %d mm (RxW)", @radius * 1000, @width * 1000),
|
123
|
+
format("%.1f kg %.2f kg/L", self.mass, @density),
|
124
|
+
].join(" | "),
|
125
|
+
].join("\n")
|
126
|
+
end
|
127
|
+
|
128
|
+
def normal_force
|
129
|
+
@normal_force ||= self.mass * @env.g
|
130
|
+
@normal_force
|
131
|
+
end
|
132
|
+
|
133
|
+
def alpha(torque, omega: 0, normal_force: nil)
|
134
|
+
(torque - self.rotating_friction(omega, normal_force: normal_force)) /
|
135
|
+
self.rotational_inertia
|
136
|
+
end
|
137
|
+
|
138
|
+
def implied_torque(alpha)
|
139
|
+
alpha * self.rotational_inertia
|
140
|
+
end
|
141
|
+
|
142
|
+
def mass
|
143
|
+
self.class.mass(@radius, @width, @density)
|
144
|
+
end
|
145
|
+
|
146
|
+
def mass=(val)
|
147
|
+
@density = self.class.density(val, self.volume_l)
|
148
|
+
@normal_force = nil # force update
|
149
|
+
end
|
150
|
+
|
151
|
+
# in m^3
|
152
|
+
def volume
|
153
|
+
self.class.volume(@radius, @width)
|
154
|
+
end
|
155
|
+
|
156
|
+
# in L
|
157
|
+
def volume_l
|
158
|
+
self.class.volume_l(@radius, @width)
|
159
|
+
end
|
160
|
+
|
161
|
+
def rotational_inertia
|
162
|
+
self.class.rotational_inertia(@radius, self.mass)
|
163
|
+
end
|
164
|
+
alias_method(:moment_of_inertia, :rotational_inertia)
|
165
|
+
|
166
|
+
def force(axle_torque)
|
167
|
+
self.class.force(axle_torque, @radius)
|
168
|
+
end
|
169
|
+
|
170
|
+
def tangential(rotational)
|
171
|
+
self.class.tangential(rotational, @radius)
|
172
|
+
end
|
173
|
+
|
174
|
+
# modeled as a tiny but increasing torque opposing omega
|
175
|
+
# also scales with normal force
|
176
|
+
# maybe not physically faithful but close enough
|
177
|
+
def rotating_friction(omega, normal_force: nil)
|
178
|
+
return omega if omega.zero?
|
179
|
+
normal_force = self.normal_force if normal_force.nil?
|
180
|
+
mag = omega.abs
|
181
|
+
sign = omega / mag
|
182
|
+
-1 * sign * normal_force * (@base_friction + mag * @omega_friction)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|