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
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
|