Printed and electronic copies of Modeling and Simulation in Python are available from No Starch Press and Bookshop.org and Amazon.
Torque#
This chapter is available as a Jupyter notebook where you can read the text, run the code, and work on the exercises. Click here to access the notebooks: https://allendowney.github.io/ModSimPy/.
In the previous chapter we modeled a system with constant angular velocity. In this chapter we take the next step, modeling a system with angular acceleration and deceleration.
Angular Acceleration#
Just as linear acceleration is the derivative of velocity, angular acceleration is the derivative of angular velocity. And just as linear acceleration is caused by force, angular acceleration is caused by the rotational version of force, torque. If you are not familiar with torque, you can read about it at http://modsimpy.com/torque.
In general, torque is a vector quantity, defined as the cross product of \(\vec{r}\) and \(\vec{F}\), where \(\vec{r}\) is the lever arm, a vector from the center of rotation to the point where the force is applied, and \(\vec{F}\) is the vector that represents the magnitude and direction of the force.
For the problems in this chapter, however, we only need the magnitude of torque; we don’t care about the direction. In that case, we can compute this product of scalar quantities:
where \(\tau\) is torque, \(r\) is the length of the lever arm, \(F\) is the magnitude of force, and \(\theta\) is the angle between \(\vec{r}\) and \(\vec{F}\).
Since torque is the product of a length and a force, it is expressed in newton meters (Nm).
Moment of Inertia#
In the same way that linear acceleration is related to force by Newton’s second law of motion, \(F=ma\), angular acceleration is related to torque by another form of Newton’s law:
where \(\alpha\) is angular acceleration and \(I\) is moment of inertia. Just as mass is what makes it hard to accelerate an object, moment of inertia is what makes it hard to spin an object.
In the most general case, a 3-D object rotating around an arbitrary axis, moment of inertia is a tensor, which is a function that takes a vector as a parameter and returns a vector as a result.
Fortunately, in a system where all rotation and torque happens around a single axis, we don’t have to deal with the most general case. We can treat moment of inertia as a scalar quantity.
For a small object with mass \(m\), rotating around a point at distance \(r\), the moment of inertia is \(I = m r^2\). For more complex objects, we can compute \(I\) by dividing the object into small masses, computing moments of inertia for each mass, and adding them up. For most simple shapes, people have already done the calculations; you can just look up the answers. For example, see http://modsimpy.com/moment.
Teapots and Turntables#
Tables in Chinese restaurants often have a rotating tray or turntable that makes it easy for customers to share dishes. These turntables are supported by low-friction bearings that allow them to turn easily and glide. However, they can be heavy, especially when they are loaded with food, so they have a high moment of inertia.
Suppose I am sitting at a table with a pot of tea on the turntable directly in front of me, and the person sitting directly opposite asks me to pass the tea. I push on the edge of the turntable with 2 N of force until it has turned 0.5 rad, then let go. The turntable glides until it comes to a stop 1.5 rad from the starting position. How much force should I apply for a second push so the teapot glides to a stop directly opposite me?
We’ll answer this question in these steps:
I’ll use the results from the first push to estimate the coefficient of friction for the turntable.
As an exercise, you’ll use that coefficient of friction to estimate the force needed to rotate the turntable through the remaining angle.
Our simulation will use the following parameters:
The radius of the turntable is 0.5 m, and its weight is 7 kg.
The teapot weights 0.3 kg, and it sits 0.4 m from the center of the turntable.
The following figure shows the scenario, where \(F\) is the force I apply to the turntable at the perimeter, perpendicular to the lever arm, \(r\), and \(\tau\) is the resulting torque. The circle near the bottom is the teapot.
Here are the parameters from the statement of the problem:
from numpy import pi
radius_disk = 0.5 # m
mass_disk = 7 # kg
radius_pot = 0.4 # m
mass_pot = 0.3 # kg
force = 2 # N
theta_push = 0.5 # radian
theta_test = 1.5 # radian
theta_target = pi # radian
theta_push
is the angle where I stop pushing on the turntable.
theta_test
is how far the table turns during my test push.
theta_target
is where we want the table to be after the second push.
We can use these parameters to compute the moment of inertia of the turntable, using the formula for a horizontal disk revolving around a vertical axis through its center:
I_disk = mass_disk * radius_disk**2 / 2
We can also compute the moment of inertia of the teapot, treating it as a point mass:
I_pot = mass_pot * radius_pot**2
The total moment of inertia is the sum of these parts:
I_total = I_disk + I_pot
Friction in the bearings probably depends on the weight of the turntable and its contents, but probably does not depend on angular velocity.
So we’ll assume that it is a constant.
We don’t know what it is, so I will start with a guess, and we will use root_scalar
to improve it.
torque_friction = 0.3 # N*m
For this problem we’ll treat friction as a torque.
The state variables we’ll use are theta
, which is the angle of the table in rad, and omega
, which is angular velocity in rad/s.
init = State(theta=0, omega=0)
Now we can make a System
with the initial state, init
, the maximum duration of the simulation, t_end
, and the parameters we are going to vary, force
and torque_friction
.
system = System(init=init,
force=force,
torque_friction=torque_friction,
t_end=20)
Here’s a slope function that takes the current state, which contains angle and angular velocity, and returns the derivatives, angular velocity and angular acceleration:
def slope_func(t, state, system):
theta, omega = state
force = system.force
torque_friction = system.torque_friction
torque = radius_disk * force - torque_friction
alpha = torque / I_total
return omega, alpha
In this scenario, the force I apply to the turntable is always perpendicular to the lever arm, so \(\sin \theta = 1\) and the torque due to force is \(\tau = r F\).
torque_friction
represents the torque due to friction. Because the
turntable is rotating in the direction of positive theta
, friction
acts in the direction of negative theta
.
We can test the slope function with the initial conditions:
slope_func(0, system.init, system)
(0, 0.7583965330444203)
We are almost ready to run the simulation, but first there’s a problem we have to address.
Two Phase Simulation#
When I stop pushing on the turntable, the angular acceleration changes
abruptly. We could implement the slope function with an if
statement
that checks the value of theta
and sets force
accordingly. And for a coarse model like this one, that might be fine. But a more robust approach is to simulate the system in two phases:
During the first phase, force is constant, and we run until
theta
is 0.5 radians.During the second phase, force is 0, and we run until
omega
is 0.
Then we can combine the results of the two phases into a single
TimeFrame
.
Phase 1#
Here’s the event function I’ll use for Phase 1; it stops the simulation when theta
reaches theta_push
, which is when I stop pushing:
def event_func1(t, state, system):
theta, omega = state
return theta - theta_push
We can test it with the initial conditions.
event_func1(0, system.init, system)
-0.5
And run the first phase of the simulation.
results1, details1 = run_solve_ivp(system, slope_func,
events=event_func1)
details1.message
'A termination event occurred.'
Here are the last few time steps.
results1.tail()
theta | omega | |
---|---|---|
1.102359 | 0.46080 | 0.836025 |
1.113842 | 0.47045 | 0.844734 |
1.125325 | 0.48020 | 0.853442 |
1.136808 | 0.49005 | 0.862151 |
1.148291 | 0.50000 | 0.870860 |
It takes a little more than a second for me to rotate the table 0.5 rad. When I release the table, the angular velocity is about 0.87 rad / s.
Before we run the second phase, we have to extract the final time and state of the first phase.
t_2 = results1.index[-1]
init2 = results1.iloc[-1]
Phase 2#
Now we can make a System
object for Phase 2 with the initial state
from Phase 1 and with force=0
.
system2 = system.set(t_0=t_2, init=init2, force=0)
For the second phase, we need an event function that stops when the turntable stops; that is, when angular velocity is 0.
def event_func2(t, state, system):
theta, omega = state
return omega
We’ll test it with the initial conditions for Phase 2.
event_func2(system2.t_0, system2.init, system2)
0.8708596517490182
The result is the angular velocity at the beginning of Phase 2, in rad/s.
Now we can run the second phase.
results2, details2 = run_solve_ivp(system2, slope_func,
events=event_func2)
details2.message
'A termination event occurred.'
Combining the Results#
Pandas provides a function called concat
, which makes a
DataFrame
with the rows from results1
followed by the rows from results2
.
results = pd.concat([results1, results2])
Here are the last few time steps.
results.tail()
theta | omega | |
---|---|---|
3.720462 | 1.664800 | 3.483439e-02 |
3.747255 | 1.665617 | 2.612579e-02 |
3.774049 | 1.666200 | 1.741719e-02 |
3.800842 | 1.666550 | 8.708597e-03 |
3.827636 | 1.666667 | -2.220446e-16 |
At the end, angular velocity is close to 0, and the total rotation is about 1.7 rad, a little farther than we were aiming for.
We can plot theta
for both phases.
results.theta.plot(label='theta')
decorate(xlabel='Time (s)',
ylabel='Angle (rad)')
And omega
.
results.omega.plot(label='omega', color='C1')
decorate(xlabel='Time (s)',
ylabel='Angular velocity (rad/s)')
Angular velocity, omega
, increases linearly while I am pushing, and decreases linearly after I let go. The angle, theta
, is the integral of angular velocity, so it forms a parabola during each phase.
In the next section, we’ll use this simulation to estimate the torque due to friction.
Estimating Friction#
Let’s take the code from the previous section and wrap it in a function.
def run_two_phases(force, torque_friction, system):
# put the specified parameters into the System object
system1 = system.set(force=force,
torque_friction=torque_friction)
# run phase 1
results1, details1 = run_solve_ivp(system1, slope_func,
events=event_func1)
# get the final state from phase 1
t_2 = results1.index[-1]
init2 = results1.iloc[-1]
# run phase 2
system2 = system1.set(t_0=t_2, init=init2, force=0)
results2, details2 = run_solve_ivp(system2, slope_func,
events=event_func2)
# combine and return the results
results = pd.concat([results1, results2])
return results
I’ll test it with the same parameters.
force = 2
torque_friction = 0.3
results = run_two_phases(force, torque_friction, system)
results.tail()
theta | omega | |
---|---|---|
3.720462 | 1.664800 | 3.483439e-02 |
3.747255 | 1.665617 | 2.612579e-02 |
3.774049 | 1.666200 | 1.741719e-02 |
3.800842 | 1.666550 | 8.708597e-03 |
3.827636 | 1.666667 | -2.220446e-16 |
These results are the same as in the previous section.
We can use run_two_phases
to write an error function we can use, with root_scalar
, to find the torque due to friction that yields the
observed results from the first push, a total rotation of 1.5 rad.
def error_func1(torque_friction, system):
force = system.force
results = run_two_phases(force, torque_friction, system)
theta_final = results.iloc[-1].theta
print(torque_friction, theta_final)
return theta_final - theta_test
This error function takes torque due to friction as an input.
It extracts force
from the System
object and runs the simulation.
From the results, it extracts the last value of theta
and returns the difference between the result of the simulation and the result of the experiment.
When this difference is 0, the value of torque_friction
is an estimate for the friction in the experiment.
To bracket the root, we need one value that’s too low and one that’s too high.
With torque_friction=0.3
, the table rotates a bit too far:
guess1 = 0.3
error_func1(guess1, system)
0.3 1.666666666666669
0.16666666666666896
With torque_friction=0.4
, it doesn’t go far enough.
guess2 = 0.4
error_func1(guess2, system)
0.4 1.2499999999999996
-0.25000000000000044
So we can use those two values as a bracket for root_scalar
.
res = root_scalar(error_func1, system, bracket=[guess1, guess2])
0.3 1.666666666666669
0.3 1.666666666666669
0.4 1.2499999999999996
0.3400000000000003 1.4705882352941169
0.3340000000000002 1.4970059880239517
0.3333320000000001 1.5000060000239976
0.3333486666010001 1.4999310034693254
The result is 0.333 N m, a little less than the initial guess.
actual_friction = res.root
actual_friction
0.3333320000000001
Now that we know the torque due to friction, we can compute the force needed to rotate the turntable through the remaining angle, that is, from 1.5 rad to 3.14 rad. You’ll have a chance to do that as an exercise, but first, let’s animate the results.
Animating the Turntable#
Here’s a function that takes the state of the system and draws it.
from matplotlib.patches import Circle
from matplotlib.pyplot import gca, axis
def draw_func(t, state):
theta, omega = state
# draw a circle for the table
circle1 = Circle([0, 0], radius_disk)
gca().add_patch(circle1)
# draw a circle for the teapot
center = pol2cart(theta, radius_pot)
circle2 = Circle(center, 0.05, color='C1')
gca().add_patch(circle2)
axis('equal')
This function uses a few features we have not seen before, but you can read about them in the Matplotlib documentation.
Here’s what the initial condition looks like.
state = results.iloc[0]
draw_func(0, state)
And here’s how we call it.
# animate(results, draw_func)
Summary#
The example in this chapter demonstrates the concepts of torque, angular acceleration, and moment of inertia. We used these concepts to simulate a turntable, using a hypothetical observation to estimate torque due to friction. As an exercise, you can finish off the example, estimating the force needed to rotate the table to a given target angle.
The next chapter describes several case studies you can work on to practice the tools from the last few chapters, including projectiles, rotating objects, root_scalar
, and maximize_scalar
.
Exercises#
This chapter is available as a Jupyter notebook where you can read the text, run the code, and work on the exercises. You can access the notebooks at https://allendowney.github.io/ModSimPy/.
Exercise 1#
Continuing the example from this chapter, estimate the force that delivers the teapot to the desired position.
Use this System
object, with the friction we computed in the previous section.
system3 = system.set(torque_friction=actual_friction)
Write an error function that takes force
and system
, simulates the system, and returns the difference between theta_final
and the remaining angle after the first push.
remaining_angle = theta_target - theta_test
remaining_angle
1.6415926535897931
Use your error function and root_scalar
to find the force needed for the second push.
Run the simulation with the force you computed and confirm that the table stops at the target angle after both pushes.
Show code cell content
# Solution
def error_func2(force, system):
"""Error function for root_scalar.
force: hypothetical value
returns: offset from target value
"""
results = run_two_phases(force, system.torque_friction, system)
theta_final = results.iloc[-1].theta
print(force, theta_final)
return theta_final - remaining_angle
Show code cell content
# Solution
guess1 = 2.0
error_func2(guess1, system3)
2.0 1.5000060000239976
-0.14158665356579547
Show code cell content
# Solution
guess2 = 3.0
error_func2(guess2, system3)
3.0 2.250009000035999
0.608416346446206
Show code cell content
# Solution
res = root_scalar(error_func2, system3, bracket=[guess1, guess2])
2.0 1.5000060000239976
2.0 1.5000060000239976
3.0 2.250009000035999
2.1887814496255746 1.641592653589789
2.188890888699056 1.6416747332232227
Show code cell content
# Solution
force = res.root
results = run_two_phases(force, actual_friction, system3)
theta_final = results.iloc[-1].theta
theta_final + theta_test
3.1415926535897887
Show code cell content
# Solution
theta_target
3.141592653589793