using LinearAlgebra
using JuMP, Ipopt, Zygote
using ControlSystemsBase
const Δt = 0.01 # Time step for the simulation
= [0.0, 0.0, 0.0, 0.0] # Initial state (cart at rest, pole hanging down)
x0 = [0.0, 0.0, π, 0.0] # Final state (cart at rest, pole upright)
xf
# Cart-pole dynamics function
function dynamics(x, p, t, u)
= p
mc, mp, l, g
= x[2]
dx₁ = 1 / (mc + mp * sin(x[3])^2) * (u[1] .- mp * sin(x[3]) * (l * x[4]^2 + g * cos(x[3])))
dx₂ = x[4]
dx₃ = 1 / (l * (mc + mp * sin(x[3])^2)) * (-u[1] * cos(x[3]) .- mp * l * x[4]^2 * sin(x[3]) * cos(x[3]) .- (mc + mp) * g * sin(x[3]))
dx₄
return [dx₁, dx₂, dx₃, dx₄]
end
# Wrapper with nominal parameters (You may find this useful for modeling the NLP)
function f(x, u)
= 0.5
mc = 0.2
mp = 0.3
l = 9.81
g
= [mc, mp, l, g]
p
return dynamics(x, p, 0.0, u)
end
# Function for trajectory planning (to be completed by the student)
function plan_cartpole_swingup()
= Model(Ipopt.Optimizer)
model
# TODO: Use JuMP to define the NLP model and Ipopt to solve it
# TODO: Extract the optimal trajectory from the solution (e.g. sample the trajectory at Δt intervals)
= 100 # Number of time steps (can be adjusted, e.g., computed from minimum time formulation)
N
# These will be filled with the planned trajectory
= zeros(4, N)
x_opt = zeros(1, N-1)
u_opt
return x_opt, u_opt
end
# Controller structure to store trajectory and any additional data (e.g. feedback gains)
mutable struct CartPoleController
::Matrix{Float64}
x_opt::Matrix{Float64}
u_opt# TODO: Add any other variables you may need (e.g. gain matrices, thresholds)
end
# Constructor for the controller
function CartPoleController(x_opt::Matrix{Float64}, u_opt::Matrix{Float64})
# TODO: Initialize and store anything else you need here
return CartPoleController(x_opt, u_opt)
end
# Evaluate the controller at state x and time step k
function step!(controller::CartPoleController, x::Vector{Float64}, k::Int64)
# Nominal state and input from the planned trajectory
= controller.x_opt[:, k]
x_nom = controller.u_opt[:, k]
u_nom
# TODO: Implement feedback to stabilize the trajectory.
# You may use time-varying LQR, constant gain, or another strategy.
#
# Consider switching to a simpler static controller (e.g., LQR around the upright equilibrium)
# once the system is sufficiently close to the target state.
#
# The input should not exceed the actuator limits of ± 3 N (you may clamp it if necessary).
# You may also update `controller` struct if needed (e.g., for gain scheduling)
= u_nom # Replace with your feedback control law
u
return u
end
Homework
Cart-pole swing-up
In this week’s homework, you will plan and stabilize a swing-up trajectory for a cart-pole system. The cart-pole is a classic control problem in which a pole is attached to a cart that moves along a horizontal track.
The system is show on the figure
The dynamics of the system are given by the equations \begin{align} \ddot{x} &= \frac{1}{m_\text{c} + m_\text{p}\sin^2{\theta}} \left[F_x + m_\text{p}\sin{\theta}\left(l\dot{\theta}^2+g\cos{\theta}\right)\right],\\ \ddot{\theta} &= \frac{1}{l(m_\text{c} + m_\text{p}\sin^2{\theta})} \left[-F_x\cos{\theta} - m_\text{p}l\dot{\theta}^2\sin{\theta}\cos{\theta} - (m_\text{c}+m_\text{p})g\sin{\theta}\right], \end{align} where
- x is the position of the cart,
- \dot{x} is the velocity of the cart,
- \theta is the angle of the pole (with \theta = 0 when the pole is hanging downward),
- \dot{\theta} is the angular velocity of the pole,
- m_\text{c} is the mass of the cart,
- m_\text{p} is the mass of the pole,
- l is the length of the pole,
- g is the gravitational acceleration.
You control the system by applying a horizontal force F_x to the cart.
Formally, let \mathbf{x} = (x, \dot{x}, \theta, \dot{\theta}) be the state of the system, and u = F_x be the control input. Your objective is to drive the system from the initial state {\mathbf{x}_\text{initial} = (0, 0, 0, 0)} (cart at rest at the origin, pole hanging down) to the final state {\mathbf{x}_\text{final} = (0, 0, \pi, 0)} (cart at rest at the origin, pole upright).
The cart is constrained to move within bounds, and the force is subject to actuator limits, i.e, {\lvert x \rvert \leq 1\,\text{m}} and {\lvert F_x \rvert \leq 3\,\text{N}}. You should get the system to the final state under 10 seconds.
Tasks
Base your implementation on the template provided below. Upload your solution as a single file named hw.jl
to the BRUTE system.
Your task are the following
1. Trajectory planning
Formulate the swing-up task as an optimal control problem (OCP), and numerically solve it. Specifically, you need to
Choose a suitable cost functional.
Formulate the OCP as a Nonlinear program (NLP) (e.g., using direct transcription, collocation, pseudo-spectral methods, etc.).
Model the NLP in
JuMP
and useIpopt
to solve it.The planned trajectory must reach the final state in under 10 seconds, and respect the state contraints and actuator limits.
Return the planned trajectory as
x_opt
(4 xN
matrix) for the state trajectory, andu_opt
(1 xN-1
matrix) for the control input trajectory. whereN
corresponds to the number of simulation steps (e.g.,N = 1001
for 10 seconds with time stepΔt = 0.01
).
Complete this part in the
plan_cartpole_swingup
function.
The OCP does not need to be “discretized” using the simulation time step Δt = 0.01
. If you use, for example, direct collocation you may solve the OCP with a different number or spacing of knot points. For global pseudo-spectral methods, time steps do not make sense at all, as you evalute the dynamics at the Chebyshev/Legendre nodes.
However, the resulting trajectory must be resampled to produce x_opt
and u_opt
on a uniform grid with time step Δt = 0.01
, as this is what the controller and simulator expect.
2. Trajectory stabilization
Your next task is to design a controller that tracks and stabilizes the planned trajectory. Specifically, complete the step!
function in the provided template.
You must
Implement feedback around the nominal trajectory
(x_opt, u_opt)
. The controller should correct for deviations from the planned path using the current state.You may use any stabilizing strategy. However, we recommend time-varying state feedback obtained by solving the LQR problem along the planned trajectory.
Near the target state, you should switch to a simpler static controller (e.g., LQR-based state feedback designed at the upright equilibrium). This improves stability and reduces sensitivity to trajectory mismatches.
The applied control input must respect the actuator limits of ±3 N. You may clamp the input if necessary.
The cart should also not exceed the bounds of ±1 m.
You may extend the CartPoleController
struct to store any data your feedback controller needs (e.g., gain matrices, thresholds, linearized models, etc.).
Your controller will be tested on a simulation with slightly perturbed system parameters (e.g., different masses or pole length) to reflect real-world model uncertainty. Your design should be robust enough to handle such variations and still stabilize the system.
3. Competition (optional)
To wrap up the assignment, your controllers will compete in a swing-up challenge.
Each submission will be evaluated in closed-loop on a perturbed version of the cart-pole system (i.e., with slightly different parameters than those used for planning).
The goal is simple—Swing the pole up and stabilize it in the shortest time possible.
Controllers will be ranked based on how quickly they reach a small neighborhood of the upright equilibrium and remain there without violating constraints.
The authors of the top three fastest controllers will receive +10% to the practical part of the exam.
You are free to tune everything — be it the NLP formulation, cost function, or feedback design.
Remarks
- Regarding the translation of the OCP into NLP, you may find find usefull Russ Tedrake’s lecture notes from his Underactuated Robotics course @ MIT, which can be found here. We also recommend Matthew Kelly’s tutorial paper dubbed An Introduction to Trajectory Optimization: How to Do Your Own Direct Collocation (available here).
- When designing the time-varying LQR state feedback controller, you will need to linearize the system dynamics around the planned trajectory. We discourage you from doing this by hand. Instead, use the
Zygote
package to compute the Jacobians of the system dynamics using automatic differentiation.
Template
Testing
Your planner and controller will be used in the BRUTE system in a similar way as the snippet below.
using OrdinaryDiffEq
= plan_cartpole_swingup()
x_opt, u_opt = CartPoleController(x_opt, u_opt)
controller
= 0:Δt:Δt*1000
ts_sim = zeros(4, length(ts_sim))
x_sim = zeros(1, length(ts_sim)-1)
u_sim
for (k, t) in enumerate(ts_sim[1:end-1])
:, k] = step!(controller, x_sim[:, k], k)
u_sim[
polecart(x, p, t) = dynamics(x, p, t, u_sim[:, k])
= ODEProblem(polecart, x_sim[:, k], (0, Δt), [0.50, 0.2, 0.3, 9.81])
prob = solve(prob, Tsit5())
sol
:, k+1] = sol[:, end]
x_sim[
end
The following code snippet shows how to visualize the results of your simulation. You can also edit this to visualize your planned trajectory.
using CairoMakie
= Figure( size = (600, 500))
fig
= Axis(fig[1, 1], title = "Cart-Pole Trajectory", xlabel = "Time (s)")
ax lines!(ax, ts_sim, x_sim[1, :], label = "x")
lines!(ax, ts_sim, x_sim[2, :], label = "ẋ")
lines!(ax, ts_sim, x_sim[3, :], label = "θ")
lines!(ax, ts_sim, x_sim[4, :], label = "θ̇")
axislegend(ax)
= Axis(fig[2, 1], title = "Control Input", xlabel = "Time", ylabel = "Force")
ax lines!(ax, ts_sim[1:end-1], u_sim[1, :], label = "Control Input")
fig
Last but not least, you can animate the cart-pole system using the following code snippet.
= 0.3 # pendulum length
l
= x_sim[1, :]
x = x_sim[3, :]
θ
= x .+ l .* sin.(θ)
px = .-l .* cos.(θ)
py
= Figure(resolution = (800, 300))
fig = Axis(fig[1, 1]; xlabel = "x", ylabel = "y", aspect = DataAspect())
ax
# Cart dimensions
= 0.2
cart_width = 0.1
cart_height
= Observable([x[1], px[1]])
x_line = Observable([cart_height, py[1]])
y_line
= Observable(px[1])
x_blob = Observable(py[1])
y_blob
# Initial shapes
= Observable(Rect(x[1] - cart_width/2, 0.0, cart_width, cart_height))
cart_obs
= lines!(ax, x_line, y_line, color=:black)
pendulum_line = scatter!(ax, x_blob, y_blob; markersize=15, color=:red)
pendulum_bob
# Cart patch
= poly!(ax, cart_obs, color = :blue)
cart_patch
# Set axis limits
xlims!(ax, -1.2, 1.2)
ylims!(ax, -0.5, 0.5)
# Animation parameters
= length(ts_sim)
frames = Int(round(frames / 10)) # approximate real-time
framerate
for i in 1:frames
# Update pendulum
= [x[i], px[i]]
x_line[] = [cart_height, py[i]]
y_line[] = px[i]
x_blob[] = py[i]
y_blob[] = Rect(x[i] - cart_width/2, 0.0, cart_width, cart_height)
cart_obs[]
display(fig)
sleep(1/framerate)
end