Chen Allgoewer¶
Chen–Allgöwer unstable OCP (OpenSCvx port).
Port of the acados Chen–Allgöwer example
(export_chen_allgoewer_model + feasibility / unconstrained OCP setup).
The system is a 2-state unstable plant with scalar control u and parameter
mu = 0.7:
x1_dot = x2 + u * (mu + (1 - mu) * x2)
x2_dot = x1 + u * (mu - 4 * (1 - mu) * x2)
Two modes mirror acados SOLVE_FEASIBILITY_PROBLEM:
- Feasibility (default): drive from a parametric initial state to the
terminal point
[0, 0.03]overT_f = 5s with|u| <= 1.5. No running cost (lam_cost = 0). - Unconstrained OCP: quadratic tracking cost on
xanduwith soft control limits at|u| <= 1(barrier-style penalty in the integrand).
Horizon: N = 21 nodes (acados N_horizon = 20 intervals).
If chen_allgoewer_initial_guess.npy sits next to this script (acados format:
first array X_init shape (2, N+1), second U_init shape (1, N)),
it is loaded as the warm start; otherwise a linear state interpolation from
x0 to x_f with zero control is used.
File: examples/abstract/chen_allgoewer.py
from __future__ import annotations
import os
import sys
import numpy as np
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(current_dir))
if project_root not in sys.path:
sys.path.append(project_root)
import openscvx as ox
from openscvx import Problem
from openscvx.plotting import plot_controls, plot_states
# ── Problem parameters (match acados defaults) ───────────────────────────────
N = 21 # nodes (acados N_horizon = 20 intervals)
TF = 5.0
MU = 0.7
ONE_MINUS_MU = 1.0 - MU
# Feasibility mode
U_MAX_FEAS = 1.5
X_TERMINAL = np.array([0.0, 0.03])
# Unconstrained OCP mode
Q_X = np.diag([0.5, 0.5])
R_U = np.array([[0.8]])
P_X = np.diag([10.0, 10.0])
U_MAX_SOFT = 1.0
TAU_SOFT = 100.0
DEFAULT_INITIAL_CONDITIONS = (
np.array([0.42, 0.45]),
np.array([0.42, 0.5]),
)
INITIAL_GUESS_PATH = os.path.join(current_dir, "chen_allgoewer_initial_guess.npy")
def _load_initial_guess(n: int) -> tuple[np.ndarray, np.ndarray] | None:
if not os.path.isfile(INITIAL_GUESS_PATH):
return None
with open(INITIAL_GUESS_PATH, "rb") as f:
x_init = np.load(f)
u_init = np.load(f)
if x_init.shape != (2, n):
raise ValueError(f"Expected X_init shape (2, {n}); got {x_init.shape}.")
if u_init.shape == (1, n - 1):
# acados stores one control per shooting interval; pad for OpenSCvx nodes.
u_init = np.pad(u_init, ((0, 0), (0, 1)), mode="edge")
elif u_init.shape != (1, n):
raise ValueError(f"Expected U_init shape (1, {n}) or (1, {n - 1}); got {u_init.shape}.")
return x_init, u_init
def _default_initial_guess(x0: np.ndarray, xf: np.ndarray, n: int) -> tuple[np.ndarray, np.ndarray]:
tau = np.linspace(0.0, 1.0, n)
x_init = (1.0 - tau) * x0[:, None] + tau * xf[:, None]
u_init = np.zeros((1, n))
return x_init, u_init
def _apply_initial_guess(
x1: ox.State,
x2: ox.State,
control: ox.Control,
x0: np.ndarray,
xf: np.ndarray,
n: int,
) -> None:
loaded = _load_initial_guess(n)
if loaded is None:
x_init, u_init = _default_initial_guess(x0, xf, n)
else:
x_init, u_init = loaded
x1.guess = x_init[0, :].reshape(-1, 1)
x2.guess = x_init[1, :].reshape(-1, 1)
control.guess = u_init[0, :].reshape(-1, 1)
def build_problem(
*, solve_feasibility: bool = True
) -> tuple[Problem, ox.State, ox.State, ox.Control]:
u_max = U_MAX_FEAS if solve_feasibility else U_MAX_SOFT
x1 = ox.State("x1", shape=(1,))
x1.min = np.array([-5.0])
x1.max = np.array([5.0])
x1.initial = np.array([DEFAULT_INITIAL_CONDITIONS[0][0]])
x1.final = [X_TERMINAL[0]] if solve_feasibility else [ox.Free(0.0)]
x2 = ox.State("x2", shape=(1,))
x2.min = np.array([-5.0])
x2.max = np.array([5.0])
x2.initial = np.array([DEFAULT_INITIAL_CONDITIONS[0][1]])
x2.final = [X_TERMINAL[1]] if solve_feasibility else [ox.Free(0.0)]
control = ox.Control("u", shape=(1,), parameterization="ZOH")
control.min = np.array([-u_max])
control.max = np.array([u_max])
states: list[ox.State] = [x1, x2]
controls = [control]
dynamics: dict = {
"x1": x2[0] + control[0] * (ox.Constant(MU) + ox.Constant(ONE_MINUS_MU) * x2[0]),
"x2": x1[0] + control[0] * (ox.Constant(MU) - ox.Constant(4.0 * ONE_MINUS_MU) * x2[0]),
}
if not solve_feasibility:
soft_upper = ox.Constant(TAU_SOFT) * ox.Max(ox.Constant(0.0), control[0] - U_MAX_SOFT) ** 2
soft_lower = ox.Constant(TAU_SOFT) * ox.Min(ox.Constant(0.0), control[0] + U_MAX_SOFT) ** 2
stage_cost = ox.State("stage_cost", shape=(1,))
stage_cost.min = np.array([0.0])
stage_cost.max = np.array([1e4])
stage_cost.initial = np.array([0.0])
stage_cost.final = [ox.Minimize(0.0)]
dynamics["stage_cost"] = (
ox.Constant(0.5) * (Q_X[0, 0] * x1[0] ** 2 + Q_X[1, 1] * x2[0] ** 2)
+ ox.Constant(0.5) * R_U[0, 0] * control[0] ** 2
+ soft_upper
+ soft_lower
)
states.append(stage_cost)
constraints: list = []
for state in states:
constraints.extend([ox.ctcs(state <= state.max), ox.ctcs(state.min <= state)])
constraints.extend([ox.ctcs(control <= control.max), ox.ctcs(control.min <= control)])
if solve_feasibility:
_apply_initial_guess(
x1,
x2,
control,
DEFAULT_INITIAL_CONDITIONS[0],
X_TERMINAL,
N,
)
else:
_apply_initial_guess(
x1,
x2,
control,
DEFAULT_INITIAL_CONDITIONS[0],
np.zeros(2),
N,
)
time = ox.Time(
initial=0.0,
final=TF,
min=0.0,
max=TF,
uniform_time_grid=True,
)
algorithm: dict = {
"lam_prox": 1e0,
"lam_vc": 1e1,
"autotuner": ox.RampProximalWeight(ramp_factor=1.04, lam_prox_max=1e3),
}
if solve_feasibility:
algorithm["lam_cost"] = 0e0
else:
algorithm["lam_cost"] = 0e0
problem = Problem(
dynamics=dynamics,
states=states,
controls=controls,
time=time,
constraints=constraints,
N=N,
# float_dtype="float64",
algorithm=algorithm,
discretizer=ox.LinearizeDiscretizeSparse(dis_type="ZOH"),
# solver = {
# "cvx_solver": "PIQP",
# "solver_args": {"canon_backend": "COO", "enforce_dpp": True},
# }
solver={
"cvx_solver": "qocogen",
"solver_args": {},
"cvxpygen": True,
},
)
problem.settings.dev.printing = False
return problem, x1, x2, control
def _show_plot(fig):
try:
fig.show()
except PermissionError as exc:
print(f"Skipping plot display: {exc}")
def solve_for_initial_condition(
problem: Problem,
x1: ox.State,
x2: ox.State,
control: ox.Control,
x0: np.ndarray,
*,
plot_solution: bool = False,
) -> ox.OptimizationResults:
x1.initial = np.array([x0[0]])
x2.initial = np.array([x0[1]])
_apply_initial_guess(x1, x2, control, x0, X_TERMINAL, N)
problem.initialize()
results = problem.solve()
results = problem.post_process()
nodes = results.nodes
print(f"x0 = {x0}")
print(f" x1(T) = {nodes['x1'][-1, 0]:.6f} (target {X_TERMINAL[0]:.6f})")
print(f" x2(T) = {nodes['x2'][-1, 0]:.6f} (target {X_TERMINAL[1]:.6f})")
print(f" converged: {results.converged}")
if plot_solution:
_show_plot(plot_states(results))
_show_plot(plot_controls(results))
return results
if __name__ == "__main__":
SOLVE_FEASIBILITY_PROBLEM = True
PLOT_SOLUTION = os.environ.get("OPENSCVX_NO_PLOT") is None
problem, x1, x2, control = build_problem(solve_feasibility=SOLVE_FEASIBILITY_PROBLEM)
for x0 in DEFAULT_INITIAL_CONDITIONS:
solve_for_initial_condition(problem, x1, x2, control, x0, plot_solution=PLOT_SOLUTION)