Inverted Pendulum Modeling and Control (Åström Ex. 2.2 & 9.6)
June 6, 2026 · Updated June 10, 2026
Why this tutorial exists
The Inverted Pendulum Simulator ships a one-click preset for Åström & Murray, Feedback Systems (2008), §2.2 Example 2.2 — the simplified pole-only model. Press the button and you’ll see the pole stay roughly upright, but the cart drifts indefinitely to one side. That’s not a bug; it’s the textbook lesson:
The Åström Example 2.2 model has no x-control and no friction. The cart has no equilibrium, so any practical controller needs to close the loop on all four states (cart position, pole angle, cart velocity, pole angular velocity), not just on θ and ω.
This page walks through the whole modeling and control design process, from Newton’s laws on the cart-pole to a state-feedback gain that actually keeps the cart where you put it. The Python code is copy-pastable; the simulator tie-in at the bottom shows you what gains to plug in to reproduce the result in the browser.
Prerequisites
- Linear algebra: matrix exponential, controllability
- Basic control: state-space form , eigenvalues
python-control≥ 0.10 andscipy- Helpful but not required: read Getting Started with PID Tuning first
1. The cart-pole, fully assembled
A cart of mass slides on a horizontal rail. A pole of mass and length (so the CoM is at distance from the pivot) is hinged on top of the cart. A horizontal force is applied to the cart. The pole angle is measured from the upright, positive in the fall-right direction. Viscous friction coefficients and resist motion in the cart and at the pivot.
Choosing as the configuration and writing Lagrange’s equations gives (Åström & Murray eq. 2.9, with and the rigid-body inertia term kept; we’ll take the point-mass limit in a moment):
For the rest of this tutorial we set (no friction) to match the textbook’s cleanest form, and use the point-mass moment of inertia (a uniform rod about its end would give , but the simpler form is what the simulator integrates). Pre-solving for and gives the decoupled form used in the simulator:
(See the simulator’s EOM section for the version with friction and the textbook citation. The two forms are algebraically identical.)
2. Linearize at the upright
The upright is an equilibrium: , , , . We want to know what happens for small perturbations. Substitute , , and drop the (quadratic in small quantities) terms:
Pick state and input . Solving for the accelerations:
The linearized state-space form :
For the canonical Åström values :
Open-loop eigenvalues
The rad/s eigenvalue is the falling-pole instability. Two zero eigenvalues correspond to the cart’s position being a free state (no spring pulling it back to zero) and the angle-rate being the derivative of the angle. With this single unstable mode, the reachability matrix has full rank — the system is controllable. Good.
3. State feedback design
The classical fix for a controllable linear system with one unstable mode is to put all four closed-loop poles in the open left half plane. Two standard ways:
3a. Pole placement (Ackermann)
Pick the four desired closed-loop poles. A reasonable starting point for “moderately damped, a few seconds settling time” is:
import numpy as np
from scipy.signal import place_poles
M, m, l, g = 2.0, 0.2, 0.5, 9.81
A = np.array([
[0, 0, 1, 0],
[0, 0, 0, 1],
[0, -m*g/M, 0, 0],
[0, g*(M+m)/(M*l), 0, 0],
])
B = np.array([[0], [0], [1/M], [-1/(M*l)]])
# Two slow poles for the (cart-position) integrator-like behavior
# and two faster poles for the (pole-angle) unstable mode.
desired = np.array([-1.5 + 0.5j, -1.5 - 0.5j, -3.0 + 1.5j, -3.0 - 1.5j])
K = place_poles(A, B, desired).gain_matrix
print(K)
# K = [[ -2.39, -56.16, -6.85, -9.94 ]]
The sign of each gain matters. The four entries of multiply — so means “if the cart is too far right, push it left” (regulate ), means “if the pole is leaning right, push the cart right to get under it” (regulate ), and so on.
3½. Same physics, different coordinates: Ex 2.2 vs Ex 9.6
The two presets on the simulator’s preset bar — Ex 2.2 and Ex 9.6 — look very different (different masses, different default gains, very different time scale) but they are the same cart-pole equation in two different unit systems. The simulator’s 4-state nonlinear EOM (Spong eq. 5.6–5.7, point-mass pole with ) is integrated in both cases. The presets only change the slider values of .
Linearized about the upright, frictionless, the theta-channel of either preset reduces to a single-input single-output plant:
with one open-loop pole on the right-half plane at
Plug in the two presets’ slider values and the open-loop pole is at very different rad/s:
| Preset | , , | Open-loop pole | ||
|---|---|---|---|---|
| Ex 2.2 | 0.5, 0.2, 0.3 | 0.15 | 6.867 | ±6.77 rad/s (≈ 1.1 Hz) |
| Ex 9.6 | 0.001, 1, 1 | 0.001 | 9.821 | ±99.1 rad/s (≈ 15.8 Hz) |
The Ex 9.6 plant is ~15× faster than Ex 2.2 in raw time. That’s the only intrinsic difference between the two as far as the linearized theta-channel goes.
Where does the 15× come from? It is a choice of time unit. Define a normalized time
i.e. measure time in units of the unstable-pole period. The plant becomes
so the pole in the -frame is at , independent of . This is the unit system Åström uses in §8.3 and §9.6 when he writes with the cart on a massless pivot. It is not a different physics — it is the same physics with the time axis rescaled.
Consequence for tuning. In the -frame, the closed-loop poles depend on three dimensionless gain ratios:
Two plants give the same closed-loop pole locations (in -units) iff these three ratios are equal. Concretely, if you have a working PID on one preset, the matching PID on the other preset is:
Numerically, mapping the current Ex 9.6 gains over to the Ex 2.2 plant () gives
Plug those into the Ex 2.2 preset and the closed-loop behavior (in real time) is exactly what you see on the Ex 9.6 preset — just ~15× slower, which is what “same physics in different units” means.
Why the same PID feels different on the two presets. If you load the current Ex 2.2 default gains (, , ) onto the Ex 9.6 plant, the dimensionless ratios are and — about 5–6× larger than the Ex 9.6 default’s . A high in the -frame pushes the closed-loop poles into a fast, lightly-damped region, which is why the Ex 2.2 gains can destabilize the Ex 9.6 plant even though the physical setup is “the same.”
Where the two presets are physically different:
-
Cart friction . This is the only physical difference that the -frame mapping above does not absorb. Ex 9.6 has (a strong damper on the cart’s runaway mode). Ex 2.2 has , so the cart position is a free integrator and the cart will drift indefinitely under any pole-only feedback. The scalar PD on cannot regulate the cart, only the pole — this is the lesson in §1.
The reason this matters in the simulator and not in the textbook -frame analysis: the textbook models the 2-state plant (theta, omega only). The simulator integrates the 4-state plant (cart x, cart v, pole theta, pole omega). With the linearized 4-state has eigenvalues rad/s — two of them are on the imaginary axis (the cart-position double integrator). The theta-only feedback closes the loop on the unstable pole pair but does not move the two imaginary-axis eigenvalues at all. Any tiny force from numerical noise eventually excites the double-integrator mode and the cart runs off to at growing speed.
Adding breaks the double-integrator degeneracy and replaces the pair with a damped mode (real, negative, finite). The cart is then bounded and the system is at least practical to work with under pole-only PD. The proper fix is a 4-state controller — see §8.
-
What the page’s plant-TF box shows. When Ex 9.6 is active, the displayed plant is switched to the textbook form in normalized units (pole at rad/s, which is s per natural second). The simulator below the math is still integrating the 4-state plant in raw SI units — the two views are in different unit systems. The page has a long note under the plant-TF box spelling this out.
Worked example: Ex 2.2 with the -mapped gains. Plug into the Ex 2.2 preset and press Start. Hold (the default) and watch for 20 s, sampling every second:
| t (s) | cart (m) | pole (deg) | peak (deg) |
|---|---|---|---|
| 0 | 0.000 | 5.7 | 5.7 |
| 1 | 1.290 | −326.1 | 326.1 |
| 6 | −464.710 | −310.3 | 338.2 |
| 15 | −3632.080 | −337.9 | 338.2 |
| 20 | (off-screen) | — | 338.2 |
The cart accelerates monotonically (peak m at s) and the pole has fully rotated within the first second. This is not a gain-mapping bug — it’s the unobservable cart mode I described above. The matching gains give the -frame behavior the mapping promised: a 338° peak angle is the same lightly-damped response as Ex 9.6, just on a 15× slower clock. The cart, however, is runaway because .
Now do the same with (matching Ex 9.6):
| peak (m) at s | peak (deg) | |
|---|---|---|
| 0 (Ex 2.2 default) | 3632 (runaway) | 338 |
| 0.1 | 3452 (runaway) | 382 |
| 0.5 | 399 (bounded, large) | 382 |
| 1.0 (matches Ex 9.6) | 506 (bounded) | 363 |
| 2.0 (slider max) | 283 (bounded) | 523 |
is the minimum friction that keeps the cart finite. The response is still lightly-damped (visible ringing), but it is the same -frame behavior as Ex 9.6 stretched out 15× in real time. To get a critically damped response, retune the -frame gains for the desired pole locations — see the recipe below.
Recipe: use the simulator with matched gains, step by step.
-
Click Åström Ex. 2.2 (loads the finite-mass cart plant with , ).
-
Set (cart friction slider). The default is 0 and the cart will run away regardless of PID gains — see the worked example above.
-
Set (the -mapped gains that match Ex 9.6’s ). The pole stays near upright; the cart oscillates m with a s period (lightly damped, ).
-
For a more conservative response, design the -frame pole locations first. Place 3 -frame poles at (well damped, 1 -rad/s bandwidth). The required -frame gains are , which map to the Ex 2.2 plant as . Plug those in and the pole settles in about 1.5 s with very little ringing; the cart reaches a steady offset of a few cm and stays there.
The same -frame poles map to on the Ex 9.6 plant — the is below the Ex 9.6 slider’s step size, so this recipe is really only practical on the Ex 2.2 plant.
Why “well-damped” still does not mean “cart stays at zero.” Even with and , the cart reaches a finite steady-state offset. The simulator’s PD on has no integral action on cart position, so any steady force on the cart (from a tilt, say) leaves a steady position error. The proper fix is a 4-state controller (see §8) or an integral term on cart position — neither is in the current simulator build. The page’s callout under the canvas flags this when you press Ex 2.2.
3b. The transfer-function form (Åström Ex. 9.6)
Chapters 2 and 5 of Åström work in state space; Chapter 8 introduces the transfer function as an alternative representation. The linearized cart-pole is a 2-input 1-output system in general, but if you make the cart massless () the cart’s position becomes a pure integrator and the dynamics collapse to a single-input single-output plant with the pole as the only state.
This is the normalized form Åström uses in §8.3 and §9.6: , , , , and the input is the cart’s acceleration (not force). Linearizing the pole EOM at the upright gives , i.e. the transfer function
This is the Example 9.6 plant. The unstable pole at corresponds to the falling-pole mode (same physical mode as the +4.65 rad/s eigenvalue in §2, just with a different time scaling).
Designing a PD controller. A PD compensator with zero at has the form . The loop transfer function is
The closed-loop characteristic polynomial is the denominator of :
By Routh–Hurwitz, this is stable iff and — i.e. . (The textbook quotes as the practical bound; the formal stability boundary is .) For the textbook’s figure value , the closed-loop poles are at , giving a natural frequency of and a damping ratio of (nice round numbers — this is why the textbook picks ).
Verify with python-control:
import control as ct
P = ct.tf([1], [1, 0, -1]) # P(s) = 1/(s^2 - 1)
C = ct.tf([2, 4], [1]) # C(s) = 2(s + 2) (i.e. k = 2)
L = C * P
T = ct.feedback(L, 1) # closed-loop
print(T.poles()) # [-1+1.414j, -1-1.414j]
# Gain and phase margin from the open loop
gm, pm, wg, wp = ct.margin(L)
print(f'gain margin = {gm:.2f}, phase margin = {pm*180/3.14159:.1f} deg')
Bode / Nyquist interpretation. The Nyquist plot of crosses the negative real axis at (for ), and . The curve makes one counterclockwise encirclement of the critical point , so . With (one open-loop RHP pole), the Nyquist criterion gives closed-loop RHP poles. Same conclusion as Routh-Hurwitz.
The page’s PD form corresponds to in the Laplace domain, so to get in the simulator you set and . For the textbook’s : , , . With (massless cart) the simulator’s “force” is numerically equal to the pivot acceleration , so the normalized form drops out automatically.
4. The control law in the simulator
The current simulator implements a PD on θ and ω only:
F = clamp(Kp · θ + Ki · ∫θ dt - Kd · d_f, -F_max, F_max)
with for the derivative filter. This is a scalar control law — it only sees the pole, not the cart. That’s why clicking Åström Ex. 2.2 leaves the cart drifting.
To use the full state-feedback gain from §3, the simulator needs a 4-input control law:
with the cart position actively regulated to zero. This extension is planned for the next simulator update; in the meantime, you can reproduce the behavior with the LQR gains above by running the Python code below against the same equations the simulator uses.
5. Verify with python-control
import control as ct
# Same A, B, K_lqr from §3b
A_cl = A - B @ K_lqr
sys_cl = ct.ss(A_cl, np.zeros((4, 1)), np.eye(4), np.zeros((4, 1)))
T = np.linspace(0, 5, 1000)
X0 = np.array([0, 0.1, 0, 0]) # pole at 0.1 rad, cart at rest
t, y = ct.initial_response(sys_cl, T, X0)
# y[:, 0] = p(t), y[:, 1] = θ(t), ...
You should see and both decay to zero with a couple of oscillations, with the cart excursion on the order of centimetres (not the runaway drift of the PD-only law).
6. Take it to the simulator
The simulator now ships two Åström textbook presets:
- Åström Ex. 2.2 — the full state-space form with scalar PD on θ, ω only. Pole stays upright, cart drifts (see §1).
- Åström Ex. 9.6 — the normalized transfer-function form with the massless-cart assumption. The controller is with , which gives the textbook’s closed-loop poles at (damping ratio ). Cart doesn’t drift because the controller is a true PD on (zero steady-state error) and the plant is linear.
To reproduce the LQR result from §3b in the browser, the simulator needs the 4-state gain (Kp_x, Kp_θ, Kd_x, Kd_ω) = (−4.5, −82.5, −10.1, −16.2). The current PID interface only exposes scalar , on , so the workaround is:
- Open the simulator
- Set the cart mass, pole mass, half-length, and friction to the Åström values (M=0.5, m=0.2, L=0.3, b_cart=b_pivot=0)
- Plug in , , set , and add cart-position feedback manually by leaving the cart undisturbed (the simulator currently has no term in the PID)
- Click Start. The pole will swing up; the cart will hold roughly still. The deviation from true LQR is the missing and terms.
For the Ex. 9.6 form, just click the button — the gains , are loaded and the normalized masses , , are set so the simulator’s “force” becomes the pivot acceleration.
7. What to take away
- Åström Example 2.2 is a teaching model, not a working controller. Its value is in showing the structure of the inverted-pendulum EOM, not in producing a deployable design.
- A real cart-pole controller closes the loop on all four states. Pole-only PD is enough to keep the pole upright for a while but cannot hold the cart still.
- The simulator is intentionally minimal. It exposes the scalar PID knobs (Kp, Ki, Kd, N) so you can build intuition. The state-feedback extension above is the next planned step.
- Ex 2.2 and Ex 9.6 are the same physics in different unit systems. A gain that works on one preset does not, in general, work on the other — see §3½ for the -frame mapping that tells you how to translate PID gains between presets.
8. Other methods (not in the simulator)
The methods below are conceptually important and are the standard
tooling for cart-pole control design, but the current simulator
implements only the scalar PID. They are documented here for
reference and to keep this tutorial complete; running them requires
either python-control in a notebook or a future simulator build.
8a. LQR (more robust than manual pole placement)
LQR finds the gain that minimizes for a chosen weighting. Pick to penalize what you care about (pole angle is usually the priority) and to penalize control effort:
from scipy.linalg import solve_continuous_are
Q = np.diag([1, 50, 1, 1]) # weight θ heavily
R = np.array([[0.05]]) # small control effort
P = solve_continuous_are(A, B, Q, R)
K_lqr = np.linalg.solve(R, B.T @ P)
print(K_lqr)
# K_lqr = [[ -4.47, -82.55, -10.10, -16.21 ]]
LQR guarantees closed-loop stability and a guaranteed gain/phase margin. It’s almost always a better starting point than pole placement. The simulator would need a 4-state input gain to use LQR; see §4 for the gap.
8b. Energy-based / swing-up control
For large initial angles (e.g. the pole hanging down at rad), a linear controller cannot stabilize — the linearized model is only valid near the upright. The classical fix is a two-mode controller: an energy-shaping swing-up law that drives the pole’s total mechanical energy to the upright value, and a switching rule to the linear balancing controller once is small enough that the linearization is valid (typically ).
The swing-up law for the cart-pole is
with the pole’s total energy. The sign term drives the cart back and forth to inject energy into the pole, building up amplitude. Once near upright the linear controller takes over.
8c. MPC / LQR with partial state observation
If only the pole angle and the cart position are measured (no rate sensors), you need an observer. The standard choice is a Kalman filter (LQE) running on the linearized to produce state estimates, which are then fed to an LQR controller. This is the LQG (Linear-Quadratic-Gaussian) design. It is robust to sensor noise and handles the situation where you can only measure position, not velocity.
The simulator currently assumes and are both available (the slider values for multiply directly), so an observer is not strictly needed for the experiments on the page. If you wanted to hook up a real sensor (e.g. an accelerometer on the cart, an encoder on the pivot), an observer would be the next step.
References
- Åström, K. J., & Murray, R. M. (2008). Feedback Systems: An Introduction for Scientists and Engineers. Princeton University Press. §2.2 Example 2.2, §6.3 State Feedback, Exercise 8.3 (linearized model), Exercise 8.13 (PD design).
- Spong, M. W., Hutchinson, S., & Vidyasagar, M. (2006). Robot Modeling and Control. Wiley. Ch. 5 (decoupled EOM derivation).
- Åström, K. J., & Hägglund, T. (2006). Advanced PID Control. ISA. (PID form with derivative filter.)