Skip to content

Double Integrator Static

1D double integrator with box bounds and terminal goal (OpenSCvx port).

Port of the RobotZoo / TrajectoryOptimization.jl DoubleIntegrator setup::

x0 = [0, 0], xf = [1, 0], tf = 2 s, N = 21
LQR stage cost  (x - xf)' Q (x - xf) + u' R u  with Q = I, R = 0.1 I
|u| <= 3, |v| <= 0.6, position unconstrained
terminal goal at the final node

Julia SolverOptions: penalty_scaling=1000, penalty_initial=1.

File: examples/double_integrator/double_integrator_static.py

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 Julia DoubleIntegrator) ─────────────────────────
N = 21
TF = 2.0
DT = TF / (N - 1)

X0 = np.array([0.0, 0.0])
XF = np.array([1.0, 0.0])

Q_POS = 1.0
Q_VEL = 1.0
R_U = 0.1

U_BND = 3.0
V_BND = 0.6

# ── States ───────────────────────────────────────────────────────────────────
position = ox.State("position", shape=(1,))
# Julia uses Inf (unbounded); OpenSCvx needs finite box limits for CTCS.
position.min = np.array([-100.0])
position.max = np.array([100.0])
position.initial = np.array([X0[0]])
position.final = np.array([XF[0]])

velocity = ox.State("velocity", shape=(1,))
velocity.min = np.array([-V_BND])
velocity.max = np.array([V_BND])
velocity.initial = np.array([X0[1]])
velocity.final = np.array([XF[1]])

stage_cost = ox.State("stage_cost", shape=(1,))
stage_cost.min = np.array([0.0])
stage_cost.max = np.array([1e3])
stage_cost.initial = np.array([0.0])
stage_cost.final = [ox.Minimize(0.0)]

# ── Control ──────────────────────────────────────────────────────────────────
force = ox.Control("force", shape=(1,), parameterization="ZOH")
force.min = np.array([-U_BND])
force.max = np.array([U_BND])
force.guess = np.zeros((N, 1))

states = [position, velocity, stage_cost]
controls = [force]

# ── Dynamics ─────────────────────────────────────────────────────────────────
dynamics = {
    "position": velocity[0],
    "velocity": force[0],
    "stage_cost": (
        Q_POS * (position[0] - ox.Constant(XF[0])) ** 2
        + Q_VEL * (velocity[0] - ox.Constant(XF[1])) ** 2
        + R_U * force[0] ** 2
    ),
}

# ── Constraints ──────────────────────────────────────────────────────────────
constraints: list = []
for state in states:
    constraints.extend([ox.ctcs(state <= state.max), ox.ctcs(state.min <= state)])
constraints.extend([ox.ctcs(force <= force.max), ox.ctcs(force.min <= force)])

# ── Initial guess (straight-line rollout analogue) ───────────────────────────
tau = np.linspace(0.0, 1.0, N)
position.guess = ((1.0 - tau) * X0[0] + tau * XF[0]).reshape(-1, 1)
velocity.guess = np.zeros((N, 1))
t_guess = np.linspace(0.0, TF, N)
stage_cost.guess = np.cumsum(
    (
        Q_POS * (position.guess[:, 0] - XF[0]) ** 2
        + Q_VEL * velocity.guess[:, 0] ** 2
        + R_U * force.guess[:, 0] ** 2
    )
    * np.gradient(t_guess)
).reshape(-1, 1)

time = ox.Time(
    initial=0.0,
    final=TF,
    min=0.0,
    max=TF,
    uniform_time_grid=True,
)

problem = Problem(
    dynamics=dynamics,
    states=states,
    controls=controls,
    time=time,
    constraints=constraints,
    N=N,
    float_dtype="float64",
    algorithm={
        # Julia SolverOptions: penalty_scaling=1000, penalty_initial=1
        "lam_prox": 1.0,
        "lam_vc": 1e3,
        "lam_cost": 1.0,
    },
)

if __name__ == "__main__":
    problem.initialize()
    results = problem.solve()
    results = problem.post_process()

    nodes = results.nodes
    print(f"dt = {DT:.4f} s")
    print(f"Final position: {nodes['position'][-1, 0]:.6f} (target {XF[0]})")
    print(f"Final velocity: {nodes['velocity'][-1, 0]:.6f} (target {XF[1]})")
    print(f"Integrated stage cost: {nodes['stage_cost'][-1, 0]:.6f}")

    plot_states(results).show()
    plot_controls(results).show()