Skip to main content

Stabilising an Inverted Pendulum with LQR

· 4 min read
Oséias D. Farias
ML Engineer · MSc Electrical Eng. & Applied Computing · Researcher @ UFABC & UFPA

The inverted pendulum is the canonical benchmark of nonlinear control — unstable, simple enough to model analytically, yet rich enough to reveal the full LQR design workflow. This post derives the linearised model from physics and shows how to stabilise it with Synapsys in a few dozen lines of Python.

Physics and linearisation

A rigid rod of mass mm and length LL is hinged at a cart. Let θ\theta be the angle from the vertical upright. Linearising around θ=0\theta = 0 gives:

θ¨=gLθ1mL2u\ddot{\theta} = \frac{g}{L}\theta - \frac{1}{mL^2}u

Choosing the state vector x=[θ,  θ˙]x = [\theta,\; \dot{\theta}]^\top and the cart force uu:

x˙=[01g/L0]Ax+[01/(mL2)]Bu,y=[10]x\dot{x} = \underbrace{\begin{bmatrix}0 & 1 \\ g/L & 0\end{bmatrix}}_{A}\, x + \underbrace{\begin{bmatrix}0 \\ -1/(mL^2)\end{bmatrix}}_{B}\, u, \qquad y = \begin{bmatrix}1 & 0\end{bmatrix} x

For m=0.5m = 0.5 kg, L=0.3L = 0.3 m, g=9.81g = 9.81 m/s²:

import numpy as np
from synapsys.api import ss

m, L, g = 0.5, 0.3, 9.81

A = np.array([[0, 1 ],
[g/L, 0 ]])
B = np.array([[0 ],
[-1/(m*L**2) ]])
C = np.array([[1, 0]])
D = np.zeros((1, 1))

plant = ss(A, B, C, D)
print(plant) # StateSpace n_states=2 n_inputs=1 n_outputs=1
print(plant.is_stable()) # False — poles at ±√(g/L) = ±5.72 rad/s

The open-loop poles are ±g/L±5.72\pm\sqrt{g/L} \approx \pm 5.72 rad/s — one is in the right half-plane, confirming instability.


LQR design

LQR finds the state-feedback gain KK that minimises:

J=0(xQx+uRu)dtJ = \int_0^\infty \left(x^\top Q\, x + u^\top R\, u\right)\mathrm{d}t

The tuning intuition: large QQ penalises state error (fast, aggressive), large RR penalises control effort (slow, conservative). Here we want tight angle control with moderate actuation:

from synapsys.algorithms import lqr

Q = np.diag([100.0, 1.0]) # penalise angle heavily, velocity lightly
R = np.array([[0.1]]) # moderate actuator cost

K, P = lqr(A, B, Q, R)
print(f"K = {K}") # K = [[33.2 5.8]]

The closed-loop system matrix Acl=ABKA_{cl} = A - BK has eigenvalues that are all in the left half-plane — the pendulum is now stabilised.

A_cl = A - B @ K
print(np.linalg.eigvals(A_cl)) # e.g. [-4.1+2.3j -4.1-2.3j]

Simulation

from synapsys.api import c2d
import matplotlib.pyplot as plt

# Build closed-loop state-space
cl = ss(A_cl, B, C, D)

# Step response — initial condition θ₀ = 0.2 rad (≈ 11°)
t = np.linspace(0, 3, 600)
x0 = np.array([0.2, 0.0]) # [θ, θ_dot]

# simulate() accepts x0 for initial condition
u_zero = np.zeros((len(t), 1)) # no reference tracking, pure stabilisation
t_out, y = cl.simulate(t, u_zero, x0=x0)

plt.plot(t_out, y)
plt.xlabel("Time (s)")
plt.ylabel("θ (rad)")
plt.title("Inverted Pendulum — LQR stabilisation from θ₀ = 0.2 rad")
plt.grid(True)
plt.show()

The angle returns to zero in roughly 1.5 s without overshoot — the Q/RQ/R balance gave us a well-damped response.


Discretisation for embedded deployment

Real microcontrollers run discrete loops. ZOH discretisation at 100 Hz:

dt = 0.01  # 100 Hz
plant_d = c2d(plant, dt=dt)

print(plant_d) # StateSpace n_states=2 dt=0.01

# The same K works — LQR was designed in continuous time
# For production, redesign K in discrete time for better performance:
from synapsys.algorithms import lqr
from synapsys.api import c2d

# Discretise A and B for discrete-time LQR (dare)
# plant_d.A, plant_d.B are the ZOH-discretised matrices
K_d, _ = lqr(plant_d.A, plant_d.B, Q, R)

The control loop on the microcontroller reduces to:

x = np.zeros(2)
for tick in range(n_steps):
u = -K_d @ x
x, y = plant_d.evolve(x, u)

Key takeaways

StepSynapsys API
Modelss(A, B, C, D)
Stability checkplant.is_stable(), plant.poles()
LQR designlqr(A, B, Q, R) → returns (K, P)
Simulationcl.simulate(t, u, x0=x0)
Discretisationc2d(plant, dt=0.01)
Embedded loopplant_d.evolve(x, u)

The full notebook is available in examples/quickstart_en.ipynb.


Next in the series

The next post covers MIMO control of a quadrotor — extending these ideas to a 12-state system with coupled dynamics and a residual Neural-LQR that adds a learned correction layer on top of the classical solution.