Cart-pole: stabilizing an unstable equilibrium#

The model is the declared cart-pole from models/cart_pole.py: an inverted pendulum on a cart, four states, one force input, starting from a 40-degree tilt. The upright target is an unstable equilibrium, the regime where short-horizon NMPC visibly fails and terminal machinery earns its keep. Two cases of the same model. Case 1 runs five sampling steps with the infinite-horizon terminal segment (drto.infinite_horizon, Dinh et al. 2025) carrying the tail, which deactivates the declared terminal cost since the tail owns the cost-to-go. Case 2 runs fifty steps of plain finite horizon with the terminal cost live. The pole is fast relative to the one-second sampling, so both cases discretize with five collocation points per element.

Case 1: five samples plus the infinite-horizon tail#

import pyomo.environ as pyo
import pyomo_pounce  # registers pounce with the native factory
from pyomo.contrib.solver.common.factory import SolverFactory
import drto
from models.cart_pole import cart_pole
from drto import plot_states, plot_controls, plot_stage_cost

m1 = cart_pole(N=5)
pyo.TransformationFactory("dae.collocation").apply_to(m1, wrt=m1.t, nfe=5, ncp=5, scheme="LAGRANGE-RADAU")
pyo.TransformationFactory("drto.infinite_horizon").apply_to(
    m1,
    nfe=3,                    # finite elements on the terminal segment
    ncp=5,                    # Gauss-Legendre points per element
    beta=1.2,                 # tail overestimation factor, > 1
    gamma="rule",             # the mesh rule, tanh(gamma*h) = tau_11; pass a number to override
    profile="collocation",    # segment control profile (pyomo-cvp)
)
pyo.TransformationFactory("drto.parameterize").apply_to(m1)  # the finite-horizon profiles
drto.build_objective(m1)
SolverFactory("pounce").solve(m1, tee=True)
********************************************************************************

                    ####    ###   /   # /#   #/  ####  #####
                    #   #  #   # /#   #/ ##  /  #      #
                    ####   #   #/ #   /  # #/#  #      ####
                    #      #   /  #  /#  # /##  #      #
                    #       ##/    #/#   #/  #   ####  #####

********************************************************************************
This program contains POUNCE, a pure-Rust interior-point optimization solver
for nonlinear, conic, and global problems (its NLP core is ported from Ipopt).
Released under the Eclipse Public License (EPL) — drop-in compatible with Ipopt.
         For more information visit https://github.com/jkitchin/pounce
********************************************************************************

This is POUNCE version 0.10.0, running with linear solver FERAL.

Reading C:\Users\Devin\AppData\Local\Temp\tmplgs4iuwv\unknown9kmwopo0.nl...
Parsed 287 vars, 267 cons, jac_nnz=1606, h_nnz=266 in 0.02s
Problem class: NLP. Selected solver: NLP filter line-search interior-point (pounce-nlp) [solver_selection=auto].

Number of nonzeros in equality constraint Jacobian...:     1586
Number of nonzeros in inequality constraint Jacobian.:       20
Number of nonzeros in Lagrangian Hessian.............:      266

Total number of variables............................:      287
                     variables with only lower bounds:        8
                variables with lower and upper bounds:      149
                     variables with only upper bounds:        0
Total number of equality constraints.................:      263
Total number of inequality constraints...............:        4
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        4
        inequality constraints with only upper bounds:        0

iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
   0  3.9040898e+05 1.60e+03 7.73e-01   -1.0 0.00e+00      - 0.00e+00 0.00e+00   0
   1  2.1641846e+04 2.34e+03 1.27e+02   -1.0 4.49e+03      - 9.38e-02 5.21e-01f  1
   2  1.8445620e+04 2.17e+03 1.18e+02   -1.0 2.31e+03      - 4.43e-01 7.19e-02h  1
   3  2.9852072e+03 5.86e+02 1.04e+01   -1.0 2.16e+03      - 6.39e-01 8.72e-01h  1
   4  5.6215611e+03 1.08e+01 2.06e+00   -1.0 5.78e+02      - 9.04e-01 1.00e+00h  1
   5  5.8060678e+03 3.15e+00 9.75e-02   -1.0 4.70e+01      - 9.75e-01 1.00e+00h  1
   6  5.4919846e+03 9.59e+01 2.17e-02   -1.0 2.14e+02      - 9.98e-01 1.00e+00f  1
   7  5.6103968e+03 1.16e+01 3.33e-03   -1.0 9.89e+01      - 1.00e+00 1.00e+00h  1
   8  3.7358229e+03 8.45e-01 9.84e-04   -1.7 1.30e+01      - 1.00e+00 1.00e+00h  1
   9  2.8649417e+03 3.01e+00 7.57e-04   -2.5 2.05e+01      - 1.00e+00 1.00e+00h  1
iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
  10  2.7200894e+03 9.27e-01 2.09e-05   -2.5 1.27e+01      - 1.00e+00 1.00e+00h  1
  11  2.6170314e+03 1.82e-01 3.86e-05   -3.8 5.19e+00      - 1.00e+00 1.00e+00h  1
  12  2.6137740e+03 3.48e-03 6.69e-08   -3.8 7.88e-01      - 1.00e+00 1.00e+00h  1
  13  2.6078132e+03 9.50e-04 1.53e-07   -5.7 3.89e-01      - 1.00e+00 1.00e+00h  1
  14  2.6078026e+03 6.31e-08 1.85e-11   -5.7 3.12e-03      - 1.00e+00 1.00e+00h  1
  15  2.6077284e+03 1.50e-07 2.39e-11   -8.6 4.92e-03      - 1.00e+00 1.00e+00h  1
pounce: wrote C:\Users\Devin\AppData\Local\Temp\tmplgs4iuwv\unknown9kmwopo0.sol


Number of Iterations....: 15

                                   (scaled)                 (unscaled)
Objective...............:   5.8259174334481034e-01    2.6077284235805473e+03
Dual infeasibility......:   2.3896653507485739e-11    1.0696338094692884e-07
Constraint violation....:   3.2771265228348642e-09    1.5021241495105642e-07
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   2.6974195762806260e-09    1.2073871248165611e-05
Overall NLP error.......:   3.2771265228348642e-09    1.2073871248165611e-05


Number of objective function evaluations             = 16
Number of objective gradient evaluations             = 16
Number of equality constraint evaluations            = 16
Number of inequality constraint evaluations          = 16
Number of equality constraint Jacobian evaluations   = 16
Number of inequality constraint Jacobian evaluations = 16
Number of Lagrangian Hessian evaluations             = 16
Total seconds in POUNCE                              = 0.022

EXIT: Optimal Solution Found.

POUNCE 0.10.0: Optimal Solution Found.
Status: Solve_Succeeded
<pyomo.contrib.solver.common.results.Results at 0x1b24ce67390>
drto.info(m1)
drto registry
states: 4, controls: 1
horizont (ContinuousSet, 26 points)
statesx (free), x_dot (free), theta (free), theta_dot (free)
dynamicsdx[t] == x_dot[t] for t in t
dynamicsdx_dot[t] == (m_p*g*sin(theta[t])*cos(theta[t]) - (1 + kJ)*(F[t] + m_p*l*theta_dot[t]**2*sin(theta[t]) - b*x_dot[t]))/(m_p*cos(theta[t])**2 - (1 + kJ)*m_c) for t in t
dynamicsdtheta[t] == theta_dot[t] for t in t
dynamicsdtheta_dot[t] == (m_c*g*sin(theta[t]) - cos(theta[t])*(F[t] + m_p*l*theta_dot[t]**2*sin(theta[t])))/((1 + kJ)*m_c*l - m_p*l*cos(theta[t])**2) for t in t
controlsF (piecewise_constant, free)
tracking stage costcost[t] == (theta[t] - theta_ss)**2/theta_scale**2 + 0.01*(x[t] - x_ss)**2 + 0.01*(x_dot[t] - x_dot_ss)**2 + 0.01*(theta_dot[t] - theta_dot_ss)**2/theta_scale**2 + 0.1*(F[t] - F_ss)**2 for t in sorted(t)[:-1]
terminal costterm == (theta[5] - theta_ss)**2/theta_scale**2 + 0.01*(x[5] - x_ss)**2 + 0.01*(x_dot[5] - x_dot_ss)**2 + 0.01*(theta_dot[5] - theta_dot_ss)**2/theta_scale**2
initial conditionsx[0] == x_hat
initial conditionsx_dot[0] == x_dot_hat
initial conditionstheta[0] == theta_hat
initial conditionstheta_dot[0] == theta_dot_hat
steady-state targetsx_ss (of x), x_dot_ss (of x_dot), theta_ss (of theta), theta_dot_ss (of theta_dot)
steady-state control targetsF_ss (of F)
transformations
  1. drto.infinite_horizon: segment=3 elements x 5 Legendre points, beta=1.2, gamma=0.01563827, profile=collocation, horizon=kept, infinite tail appended, terminal_cost=terminal deactivated (the tail owns it), terminal=soft pin z(tau=1)=z_s on 4 states, mu=1000.0
  2. drto.parameterize: profiles=F (piecewise_constant)
  3. drto.build_objective: objective=sum of 36 weighted cost terms
plot_states(m1);
../_images/f38247329779442a3307f3cafb5d14595d2a2871c1ac00a6a06486594d21be57.png
plot_controls(m1);
../_images/30b7ceee2f921bb262109ae77220f606c13e08066ca54aa27a1b2ed7aaeb519c.png
plot_stage_cost(m1);
../_images/443f2e631788a7794d9aee30725ef5b43640f52f31a51227006a5a7f237d6fb9.png

Case 2: fifty samples, finite horizon with the terminal cost#

m2 = cart_pole(N=50)
pyo.TransformationFactory("dae.collocation").apply_to(m2, wrt=m2.t, nfe=50, ncp=5, scheme="LAGRANGE-RADAU")
pyo.TransformationFactory("drto.parameterize").apply_to(m2)
drto.build_objective(m2)
SolverFactory("pounce").solve(m2, tee=True)
********************************************************************************

                    ####    ###   /   # /#   #/  ####  #####
                    #   #  #   # /#   #/ ##  /  #      #
                    ####   #   #/ #   /  # #/#  #      ####
                    #      #   /  #  /#  # /##  #      #
                    #       ##/    #/#   #/  #   ####  #####

********************************************************************************
This program contains POUNCE, a pure-Rust interior-point optimization solver
for nonlinear, conic, and global problems (its NLP core is ported from Ipopt).
Released under the Eclipse Public License (EPL) — drop-in compatible with Ipopt.
         For more information visit https://github.com/jkitchin/pounce
********************************************************************************

This is POUNCE version 0.10.0, running with linear solver FERAL.

Reading C:\Users\Devin\AppData\Local\Temp\tmpcnu0y99m\unknownqnjk4oo9.nl...
Parsed 1603 vars, 1553 cons, jac_nnz=9535, h_nnz=1400 in 0.03s
Problem class: NLP. Selected solver: NLP filter line-search interior-point (pounce-nlp) [solver_selection=auto].

Number of nonzeros in equality constraint Jacobian...:     9535
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:     1400

Total number of variables............................:     1603
                     variables with only lower bounds:        0
                variables with lower and upper bounds:      800
                     variables with only upper bounds:        0
Total number of equality constraints.................:     1553
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
   0  0.0000000e+00 1.60e+03 1.00e+00   -1.0 0.00e+00      - 0.00e+00 0.00e+00   0
   1 -4.7831606e+03 1.62e+03 1.65e+03   -1.0 1.60e+03   -2.0 1.66e-01 1.97e-01f  1
   2 -4.9386073e+03 1.62e+03 1.64e+03   -1.0 1.29e+03   -2.5 3.00e-01 5.26e-03f  1
   3 -8.1285305e+03 1.56e+03 1.57e+03   -1.0 3.15e+03   -3.0 2.46e-01 3.76e-02f  1
   4 -1.0147716e+04 1.45e+03 1.45e+03   -1.0 1.23e+03   -2.5 5.17e-01 7.34e-02f  1
   5 -1.1328805e+04 1.44e+03 1.54e+03   -1.0 5.98e+04   -3.0 7.33e-03 3.58e-03f  1
   6 -1.1918021e+04 1.41e+03 1.50e+03   -1.0 1.50e+03   -2.6 4.80e-01 2.14e-02f  1
   7 -1.4883800e+04 1.40e+03 1.48e+03   -1.0 1.48e+04   -3.1 9.28e-03 1.17e-02f  1
   8 -1.5327527e+04 1.38e+03 1.45e+03   -1.0 1.38e+03   -2.6 5.19e-02 1.77e-02f  1
   9 -1.5878497e+04 1.29e+03 1.37e+03   -1.0 1.38e+03   -2.2 4.79e-04 6.35e-02f  1
iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
  10 -2.3601865e+04 1.09e+03 1.36e+03   -1.0 1.85e+03   -2.7 3.81e-02 1.94e-01f  1
  11 -2.3464372e+04 1.06e+03 1.42e+03   -1.0 3.56e+03   -2.3 7.84e-03 2.66e-02h  1
  12 -2.2478682e+04 9.79e+02 1.08e+03   -1.0 1.00e+03   -1.8 8.92e-02 8.15e-02h  1
  13 -2.2250073e+04 9.61e+02 1.06e+03   -1.0 9.47e+02   -1.4 2.62e-02 1.86e-02h  1
  14 -1.8890541e+04 7.58e+02 2.12e+03   -1.0 9.32e+02   -1.9 7.86e-04 3.27e-01h  1
  15 -1.9692594e+04 8.34e+02 2.12e+03   -1.0 2.21e+05      - 2.04e-03 2.21e-03f  1
  16 -2.0589133e+04 1.84e+03 2.11e+03   -1.0 1.98e+05      - 7.22e-04 3.06e-03f  1
  17 -2.2951726e+04 2.94e+03 2.07e+03   -1.0 8.08e+04      - 1.48e-04 1.80e-02f  1
  18 -2.4464508e+04 3.49e+03 1.98e+03   -1.0 8.91e+03      - 1.56e-02 4.42e-02f  1
  19 -2.4259463e+04 3.48e+03 1.88e+03   -1.0 4.28e+03      - 2.56e-03 5.61e-02h  1
iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
  20 -2.3674020e+04 3.38e+03 1.82e+03   -1.0 3.38e+03      - 8.84e-02 2.79e-02h  1
  21 -2.2546039e+04 3.20e+03 1.73e+03   -1.0 3.31e+03      - 5.40e-02 5.32e-02h  1
  22 -1.6865403e+04 2.39e+03 1.31e+03   -1.0 3.14e+03      - 8.02e-02 2.56e-01h  1
pounce: wrote C:\Users\Devin\AppData\Local\Temp\tmpcnu0y99m\unknownqnjk4oo9.sol
  23 -4.5444712e+03 9.48e+02 4.67e+02   -1.0 2.31e+03      - 1.49e-01 6.09e-01h  1
  24 -5.6057708e+02 5.35e+02 3.90e+02   -1.0 9.41e+02      - 4.00e-02 4.37e-01h  1
  25  1.9198174e+03 2.57e+02 3.13e+02   -1.0 5.33e+02      - 2.45e-01 5.20e-01h  1
  26  3.5975233e+03 3.55e+01 4.58e+02   -1.0 2.57e+02      - 6.23e-02 1.00e+00h  1
  27  3.1856407e+03 2.55e+01 4.07e+02   -1.0 6.92e+01      - 1.18e-01 1.00e+00f  1
  28  2.4749926e+03 4.28e+01 3.22e+02   -1.0 9.43e+01      - 2.05e-01 1.00e+00f  1
  29  2.4245947e+03 3.87e+01 2.11e+02   -1.0 7.68e+01      - 3.44e-01 1.00e+00f  1
iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
  30  2.5874731e+03 5.00e-01 1.47e+01   -1.0 3.84e+01      - 9.30e-01 1.00e+00h  1
  31  2.5894820e+03 5.03e-04 1.26e-05   -1.0 4.89e-01      - 1.00e+00 1.00e+00h  1
  32  2.5893878e+03 2.43e-05 1.60e-05   -2.5 5.88e-02      - 1.00e+00 1.00e+00f  1
  33  2.5893851e+03 2.00e-08 1.26e-08   -3.8 1.70e-03      - 1.00e+00 1.00e+00f  1
  34  2.5893850e+03 5.72e-11 3.74e-11   -5.7 9.01e-05      - 1.00e+00 1.00e+00f  1
  35  2.5893850e+03 1.14e-13 6.57e-13   -8.6 1.12e-06      - 1.00e+00 1.00e+00h  1


Number of Iterations....: 35

                                   (scaled)                 (unscaled)
Objective...............:   2.5893849586465190e+03    2.5893849586465190e+03
Dual infeasibility......:   6.5728632969498408e-13    6.5728632969498408e-13
Constraint violation....:   1.1368683772161603e-13    1.1368683772161603e-13
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   2.5059503395178417e-09    2.5059503395178417e-09
Overall NLP error.......:   2.5059503395178417e-09    2.5059503395178417e-09


Number of objective function evaluations             = 36
Number of objective gradient evaluations             = 36
Number of equality constraint evaluations            = 36
Number of inequality constraint evaluations          = 36
Number of equality constraint Jacobian evaluations   = 36
Number of inequality constraint Jacobian evaluations = 36
Number of Lagrangian Hessian evaluations             = 36
Total seconds in POUNCE                              = 0.231

EXIT: Optimal Solution Found.

POUNCE 0.10.0: Optimal Solution Found.
Status: Solve_Succeeded
<pyomo.contrib.solver.common.results.Results at 0x1b24fe35270>
drto.info(m2)
drto registry
states: 4, controls: 1
horizont (ContinuousSet, 251 points)
statesx (free), x_dot (free), theta (free), theta_dot (free)
dynamicsdx[t] == x_dot[t] for t in t
dynamicsdx_dot[t] == (m_p*g*sin(theta[t])*cos(theta[t]) - (1 + kJ)*(F[t] + m_p*l*theta_dot[t]**2*sin(theta[t]) - b*x_dot[t]))/(m_p*cos(theta[t])**2 - (1 + kJ)*m_c) for t in t
dynamicsdtheta[t] == theta_dot[t] for t in t
dynamicsdtheta_dot[t] == (m_c*g*sin(theta[t]) - cos(theta[t])*(F[t] + m_p*l*theta_dot[t]**2*sin(theta[t])))/((1 + kJ)*m_c*l - m_p*l*cos(theta[t])**2) for t in t
controlsF (piecewise_constant, free)
tracking stage costcost[t] == (theta[t] - theta_ss)**2/theta_scale**2 + 0.01*(x[t] - x_ss)**2 + 0.01*(x_dot[t] - x_dot_ss)**2 + 0.01*(theta_dot[t] - theta_dot_ss)**2/theta_scale**2 + 0.1*(F[t] - F_ss)**2 for t in sorted(t)[:-1]
terminal costterm == (theta[50] - theta_ss)**2/theta_scale**2 + 0.01*(x[50] - x_ss)**2 + 0.01*(x_dot[50] - x_dot_ss)**2 + 0.01*(theta_dot[50] - theta_dot_ss)**2/theta_scale**2
initial conditionsx[0] == x_hat
initial conditionsx_dot[0] == x_dot_hat
initial conditionstheta[0] == theta_hat
initial conditionstheta_dot[0] == theta_dot_hat
steady-state targetsx_ss (of x), x_dot_ss (of x_dot), theta_ss (of theta), theta_dot_ss (of theta_dot)
steady-state control targetsF_ss (of F)
transformations
  1. drto.parameterize: profiles=F (piecewise_constant)
  2. drto.build_objective: objective=sum of 51 weighted cost terms
plot_states(m2);
../_images/43a877a1aceee45f88366c0f29962796a9923978f8894581aef6f90259970974.png
plot_controls(m2);
../_images/ce329bb3b380ebf22a474d7ec948676b5a1c700a9319fe640dbbc358ef541778.png
plot_stage_cost(m2);
../_images/bd35f88d6db064a8e5c9fa13ab75b73a7cdc4c550981db1593e36ec79906ac68.png