Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Practical NLP Implementation of LGR Optimal Control

The previous sessions developed the mathematical foundations of Legendre pseudospectral transcription and costate recovery. This section explains how to implement an LGR transcription inside a general nonlinear programming solver.

The central practical issue is that an optimal-control transcription is naturally expressed using matrices of state and control values, whereas an NLP solver expects a single decision vector. Therefore, the implementation must provide a systematic mapping between:

  1. the continuous optimal-control problem;

  2. the LGR-discretized state and control arrays;

  3. the NLP decision vector;

  4. the scalar objective function;

  5. the stacked equality and inequality constraints; and

  6. the initial guess supplied to the solver.

Standard Nonlinear Programming Form

A general nonlinear program may be written as

minzRnzf(z)subject tog(z)=0,hminh(z)hmax,zminzzmax.\boxed{ \begin{aligned} \min_{\boldsymbol{z}\in\mathbb{R}^{n_z}} \quad & f(\boldsymbol{z}) \\ \text{subject to}\quad & \boldsymbol{g}(\boldsymbol{z})=\boldsymbol{0}, \\ & \boldsymbol{h}_{\min} \leq \boldsymbol{h}(\boldsymbol{z}) \leq \boldsymbol{h}_{\max}, \\ & \boldsymbol{z}_{\min} \leq \boldsymbol{z} \leq \boldsymbol{z}_{\max}. \end{aligned} }

Here:

For implementation, the equality and inequality constraints may be stacked into one vector,

k(z)=[g(z)h(z)],\boldsymbol{k}(\boldsymbol{z}) = \begin{bmatrix} \boldsymbol{g}(\boldsymbol{z})\\ \boldsymbol{h}(\boldsymbol{z}) \end{bmatrix},

with corresponding lower and upper bounds.

LGR-Discretized Bolza Problem

Consider a Bolza problem on [t0,tf][t_0,t_f]:

J=Φ(x(t0),t0,x(tf),tf)+t0tfL(x(t),u(t),t)dt.\begin{aligned} J ={}& \Phi\left( \boldsymbol{x}(t_0),t_0, \boldsymbol{x}(t_f),t_f \right) \nonumber\\ &+ \int_{t_0}^{t_f} L\left( \boldsymbol{x}(t),\boldsymbol{u}(t),t \right)\,\mathrm{d} t. \end{aligned}

After mapping the interval to [1,1][-1,1] and applying LGR collocation, the objective is approximated by

JN=Φ(X1,t0,XN+1,tf)+tft02i=1NwiL(Xi,Ui,ti).\begin{aligned} J_N ={}& \Phi\left( \boldsymbol{X}_1,t_0, \boldsymbol{X}_{N+1},t_f \right) \nonumber\\ &+ \frac{t_f-t_0}{2} \sum_{i=1}^{N} w_i L\left( \boldsymbol{X}_i,\boldsymbol{U}_i,t_i \right). \end{aligned}

The defect equations are

DX1:N+1tft02F1:N=0.\boxed{ \boldsymbol{D}\boldsymbol{X}_{1:N+1} - \frac{t_f-t_0}{2}\boldsymbol{F}_{1:N} = \boldsymbol{0}. }

Endpoint constraints are

ϕ(X1,t0,XN+1,tf)=0.\boldsymbol{\phi}\left( \boldsymbol{X}_1,t_0, \boldsymbol{X}_{N+1},t_f \right) = \boldsymbol{0}.

Path constraints are imposed at the NN LGR points:

cminc(Xi,Ui,ti)cmax,i=1,,N.\boldsymbol{c}_{\min} \leq \boldsymbol{c}\left( \boldsymbol{X}_i,\boldsymbol{U}_i,t_i \right) \leq \boldsymbol{c}_{\max}, \qquad i=1,\ldots,N.

Dimensions of the Discrete Variables

Let:

Then

XR(N+1)×nx,\boldsymbol{X}\in\mathbb{R}^{(N+1)\times n_x},

because the state is stored at the NN LGR points plus the noncollocated terminal point.

Similarly,

URN×nu,\boldsymbol{U}\in\mathbb{R}^{N\times n_u},

because the control is represented only at the collocation points.

If both t0t_0 and tft_f are optimization variables, the total number of NLP variables is

nz=(N+1)nx+Nnu+2.\boxed{ n_z = (N+1)n_x + Nn_u + 2. }

Packing the NLP Decision Vector

The NLP solver expects a single column vector. A standard column-major packing is

z=[vec(X)vec(U)t0tf].\boxed{ \boldsymbol{z} = \begin{bmatrix} \operatorname{vec}(\boldsymbol{X})\\ \operatorname{vec}(\boldsymbol{U})\\ t_0\\ t_f \end{bmatrix}. }

The state block is

zx=vec(X)R(N+1)nx,\boldsymbol{z}_x = \operatorname{vec}(\boldsymbol{X}) \in \mathbb{R}^{(N+1)n_x},

and the control block is

zu=vec(U)RNnu.\boldsymbol{z}_u = \operatorname{vec}(\boldsymbol{U}) \in \mathbb{R}^{Nn_u}.

The time variables occupy the final two entries.

Index Ranges

The state portion is

zx=z(1:(N+1)nx).\boldsymbol{z}_x = \boldsymbol{z}\left( 1:(N+1)n_x \right).

The control portion is

zu=z((N+1)nx+1:(N+1)nx+Nnu).\boldsymbol{z}_u = \boldsymbol{z}\left( (N+1)n_x+1: (N+1)n_x+Nn_u \right).

The time variables are

t0=z(N+1)nx+Nnu+1,t_0 = z_{(N+1)n_x+Nn_u+1},

and

tf=z(N+1)nx+Nnu+2.t_f = z_{(N+1)n_x+Nn_u+2}.

Unpacking and Reshaping

Inside the objective and constraint functions, reshape the vector blocks back into matrices:

X=reshape(zx,N+1,nx),\boxed{ \boldsymbol{X} = \operatorname{reshape} \left( \boldsymbol{z}_x,N+1,n_x \right), }

and

U=reshape(zu,N,nu).\boxed{ \boldsymbol{U} = \operatorname{reshape} \left( \boldsymbol{z}_u,N,n_u \right). }

In MATLAB:

X = reshape(zx, N+1, nx);
U = reshape(zu, N,   nu);

The state values at the collocation points are

XLGR=X(1:N,:).\boldsymbol{X}_{\mathrm{LGR}} = \boldsymbol{X}(1:N,:).

The endpoint state values are

Xinit=X(1,:),Xfinal=X(N+1,:).\boldsymbol{X}_{\mathrm{init}} = \boldsymbol{X}(1,:), \qquad \boldsymbol{X}_{\mathrm{final}} = \boldsymbol{X}(N+1,:).

Physical Time at the LGR Points

The affine time transformation gives

ti=tft02τi+tf+t02.t_i = \frac{t_f-t_0}{2}\tau_i + \frac{t_f+t_0}{2}.

In vector form,

t=tft02τ+tf+t021.\boxed{ \mathbf{t} = \frac{t_f-t_0}{2}\boldsymbol{\tau} + \frac{t_f+t_0}{2}\boldsymbol{1}. }

These time values are used to evaluate the dynamics, Lagrange integrand, and path constraints.

Evaluating the Dynamics

Evaluate the dynamics at the LGR points:

Fi=f(Xi,Ui,ti),i=1,,N.\boldsymbol{F}_i = \boldsymbol{f}(\boldsymbol{X}_i,\boldsymbol{U}_i,t_i), \qquad i=1,\ldots,N.

Stacking these rows produces

F1:NRN×nx.\boldsymbol{F}_{1:N} \in \mathbb{R}^{N\times n_x}.

The dynamics evaluation is problem dependent.

A MATLAB-style implementation is:

Xlgr = X(1:N,:);
F = dynamics(Xlgr, U, t);

Defect Constraints

The LGR defect matrix is

Δ=DXtft02F.\boxed{ \boldsymbol{\Delta} = \boldsymbol{D}\boldsymbol{X} - \frac{t_f-t_0}{2}\boldsymbol{F}. }

Its dimension is

ΔRN×nx.\boldsymbol{\Delta}\in\mathbb{R}^{N\times n_x}.

An NLP solver requires a vector, so vectorize the matrix:

δ=vec(Δ)RNnx.\boxed{ \boldsymbol{\delta} = \operatorname{vec}(\boldsymbol{\Delta}) \in \mathbb{R}^{Nn_x}. }

In MATLAB:

defect = D*X - 0.5*(tf-t0)*F;
defect_col = defect(:);

The expression

A(:)

stacks the columns of a matrix into one column vector.

Evaluating the Objective

Separate the objective into Mayer and Lagrange terms.

Mayer Term

Evaluate

JM=Φ(Xinit,t0,Xfinal,tf).J_M = \Phi\left( \boldsymbol{X}_{\mathrm{init}},t_0, \boldsymbol{X}_{\mathrm{final}},t_f \right).

Lagrange Term

Evaluate the integrand at all LGR points:

i=L(Xi,Ui,ti).\ell_i = L(\boldsymbol{X}_i,\boldsymbol{U}_i,t_i).

Let

=[1N]T.\boldsymbol{\ell} = \begin{bmatrix} \ell_1&\cdots&\ell_N \end{bmatrix}^{\mathsf{T}}.

Then

JL=tft02wT.J_L = \frac{t_f-t_0}{2} \boldsymbol{w}^{\mathsf{T}}\boldsymbol{\ell}.

The total cost is

JN=JM+JL.\boxed{ J_N = J_M+J_L. }

A MATLAB-style implementation is:

mayer = endpointCost(X(1,:), t0, X(end,:), tf);
ell   = runningCost(X(1:N,:), U, t);
cost  = mayer + 0.5*(tf-t0)*(w.'*ell);

Boundary Constraints

Suppose the endpoint constraints are described by

ϕ(Xinit,t0,Xfinal,tf).\boldsymbol{\phi} \left( \boldsymbol{X}_{\mathrm{init}},t_0, \boldsymbol{X}_{\mathrm{final}},t_f \right).

Evaluate

b=ϕ(X(1,:),t0,X(N+1,:),tf).\boxed{ \mathbf{b} = \boldsymbol{\phi} \left( \boldsymbol{X}(1,:),t_0, \boldsymbol{X}(N+1,:),t_f \right). }

These constraints are usually already returned as a vector and do not require additional reshaping.

Path Constraints

Suppose there are ncn_c path-constraint functions. Evaluating them at all NN LGR points produces

CRN×nc.\boldsymbol{C} \in \mathbb{R}^{N\times n_c}.

The matrix is vectorized as

ccol=vec(C)RNnc.\boxed{ \boldsymbol{c}_{\mathrm{col}} = \operatorname{vec}(\boldsymbol{C}) \in \mathbb{R}^{Nn_c}. }

In MATLAB:

C = pathConstraints(X(1:N,:), U, t);
Ccol = C(:);

Stacking the Complete Constraint Vector

The full NLP constraint vector can be assembled as

k(z)=[δccolb].\boxed{ \boldsymbol{k}(\boldsymbol{z}) = \begin{bmatrix} \boldsymbol{\delta}\\ \boldsymbol{c}_{\mathrm{col}}\\ \mathbf{b} \end{bmatrix}. }

The corresponding lower and upper bounds are stacked in the same order:

kmin=[0cmin,colbmin],\boldsymbol{k}_{\min} = \begin{bmatrix} \boldsymbol{0}\\ \boldsymbol{c}_{\min,\mathrm{col}}\\ \mathbf{b}_{\min} \end{bmatrix},

and

kmax=[0cmax,colbmax].\boldsymbol{k}_{\max} = \begin{bmatrix} \boldsymbol{0}\\ \boldsymbol{c}_{\max,\mathrm{col}}\\ \mathbf{b}_{\max} \end{bmatrix}.

This arrangement is convenient for solvers such as SNOPT that permit general lower and upper bounds on constraint functions.

Objective Function Template

A practical MATLAB-style objective function is:

function J = objective(z, data)

N  = data.N;
nx = data.nx;
nu = data.nu;
w  = data.w;
tau = data.tau;

nX = (N+1)*nx;
nU = N*nu;

zx = z(1:nX);
zu = z(nX+1:nX+nU);
t0 = z(nX+nU+1);
tf = z(nX+nU+2);

X = reshape(zx, N+1, nx);
U = reshape(zu, N, nu);

t = 0.5*(tf-t0)*tau + 0.5*(tf+t0);

JM = endpointCost(X(1,:), t0, X(end,:), tf);
L  = runningCost(X(1:N,:), U, t);

J = JM + 0.5*(tf-t0)*(w.'*L);
end

Constraint Function Template

A practical constraint function is:

function K = constraints(z, data)

N  = data.N;
nx = data.nx;
nu = data.nu;
D  = data.D;
tau = data.tau;

nX = (N+1)*nx;
nU = N*nu;

zx = z(1:nX);
zu = z(nX+1:nX+nU);
t0 = z(nX+nU+1);
tf = z(nX+nU+2);

X = reshape(zx, N+1, nx);
U = reshape(zu, N, nu);

Xlgr = X(1:N,:);
t = 0.5*(tf-t0)*tau + 0.5*(tf+t0);

F = dynamics(Xlgr, U, t);

defect = D*X - 0.5*(tf-t0)*F;
defect_col = defect(:);

BC = endpointConstraints(X(1,:), t0, X(end,:), tf);

C = pathConstraints(Xlgr, U, t);
Ccol = C(:);

K = [defect_col; Ccol; BC];
end

Solver-Specific Treatment of Inequalities

Different NLP solvers accept constraints in different formats.

Bounded Constraint Form

Some solvers allow

kmink(z)kmax.\boldsymbol{k}_{\min} \leq \boldsymbol{k}(\boldsymbol{z}) \leq \boldsymbol{k}_{\max}.

This is natural for path constraints of the form

cminc(z)cmax.\boldsymbol{c}_{\min} \leq \boldsymbol{c}(\boldsymbol{z}) \leq \boldsymbol{c}_{\max}.

MATLAB fmincon Form

The standard fmincon interface uses

c(z)0,ceq(z)=0.\mathbf{c}(\boldsymbol{z})\leq\boldsymbol{0}, \qquad \mathbf{c}_{\mathrm{eq}}(\boldsymbol{z})=\boldsymbol{0}.

A two-sided bound

cminc(z)cmax\boldsymbol{c}_{\min} \leq \boldsymbol{c}(\boldsymbol{z}) \leq \boldsymbol{c}_{\max}

must be converted to

c(z)cmax0,\boldsymbol{c}(\boldsymbol{z})-\boldsymbol{c}_{\max}\leq\boldsymbol{0},

and

cminc(z)0.\boldsymbol{c}_{\min}-\boldsymbol{c}(\boldsymbol{z})\leq\boldsymbol{0}.

Thus:

c = [Ccol - Cmax_col;
     Cmin_col - Ccol];

ceq = [defect_col;
       BC];

Constructing the Initial Guess

An NLP solver requires an initial guess

zguess.\boldsymbol{z}_{\mathrm{guess}}.

A simple and widely used procedure is:

  1. guess the initial and final state;

  2. linearly interpolate the state over the LGR grid plus the terminal point;

  3. guess the control at the first and last collocation points;

  4. linearly interpolate the control over the LGR points;

  5. guess t0t_0 and tft_f;

  6. vectorize and stack all quantities.

State Guess

Given guesses

X1(0)andXN+1(0),\boldsymbol{X}_1^{(0)} \quad\text{and}\quad \boldsymbol{X}_{N+1}^{(0)},

construct

X(0)(τ)\boldsymbol{X}^{(0)}(\tau)

by straight-line interpolation at

τ1,,τN,τN+1=1.\tau_1,\ldots,\tau_N,\tau_{N+1}=1.

Control Guess

Given endpoint or representative control guesses, interpolate over the LGR points.

Packing the Guess

Finally,

zguess=[vec(X(0))vec(U(0))t0(0)tf(0)].\boxed{ \boldsymbol{z}_{\mathrm{guess}} = \begin{bmatrix} \operatorname{vec}(\boldsymbol{X}^{(0)})\\ \operatorname{vec}(\boldsymbol{U}^{(0)})\\ t_0^{(0)}\\ t_f^{(0)} \end{bmatrix}. }

A MATLAB-style implementation is:

Xguess = interp1([-1; 1], [X0guess; Xfguess], ...
                 [tau; 1], 'linear');

Uguess = interp1([-1; 1], [U0guess; Ufguess], ...
                 tau, 'linear');

zguess = [Xguess(:);
          Uguess(:);
          t0guess;
          tfguess];

Variable and Constraint Bounds

Bounds must be packed in exactly the same order as the decision vector:

zmin=[vec(Xmin)vec(Umin)t0,mintf,min],\boldsymbol{z}_{\min} = \begin{bmatrix} \operatorname{vec}(\boldsymbol{X}_{\min})\\ \operatorname{vec}(\boldsymbol{U}_{\min})\\ t_{0,\min}\\ t_{f,\min} \end{bmatrix},

and similarly for zmax\boldsymbol{z}_{\max}.

The defect constraints have zero lower and upper bounds:

δmin=δmax=0.\boldsymbol{\delta}_{\min} = \boldsymbol{\delta}_{\max} = \boldsymbol{0}.

Endpoint equality constraints also receive identical lower and upper bounds.

Recovering the Solution

After the NLP solver returns

z,\boldsymbol{z}^\star,

unpack it using the same indexing convention:

X=reshape(zx,N+1,nx),\boldsymbol{X}^\star = \operatorname{reshape} \left( \boldsymbol{z}_x^\star,N+1,n_x \right),
U=reshape(zu,N,nu).\boldsymbol{U}^\star = \operatorname{reshape} \left( \boldsymbol{z}_u^\star,N,n_u \right).

Then reconstruct the physical time vector and inspect:

Common Implementation Errors

  1. Using inconsistent row-major and column-major packing.

  2. Reshaping the state to N×nxN\times n_x instead of (N+1)×nx(N+1)\times n_x.

  3. Evaluating the control or dynamics at the noncollocated terminal point.

  4. Omitting the time-scaling factor (tft0)/2(t_f-t_0)/2.

  5. Using XN\boldsymbol{X}_N instead of XN+1\boldsymbol{X}_{N+1} as the terminal state.

  6. Stacking constraint values in an order inconsistent with their bounds.

  7. Using the wrong sign convention for the defects.

  8. Providing a differentiation matrix of the wrong size.

  9. Forgetting to remove the row associated with a noncollocated point when a general differentiation routine returns a square matrix.

  10. Starting directly with a complex research problem before validating the implementation on a known problem.

Computing LGR Points, Weights, and Differentiation Matrices

The implementation requires:

A general differentiation routine may return a square matrix on the grid

τ1,,τN,τN+1.\tau_1,\ldots,\tau_N,\tau_{N+1}.

For left-LGR collocation, the final point is noncollocated. Therefore, the row corresponding to τN+1=1\tau_{N+1}=1 must be removed so that

DRN×(N+1).\boldsymbol{D}\in\mathbb{R}^{N\times(N+1)}.

The columns must not be removed because all N+1N+1 state values participate in the interpolation.

A robust development sequence is:

  1. Implement a scalar problem with a known analytical solution.

  2. Verify variable packing and unpacking.

  3. Verify dimensions of every matrix and vector.

  4. Check the objective independently.

  5. Evaluate the constraints at a known feasible trajectory.

  6. Solve the problem at low polynomial order.

  7. Increase NN and verify convergence.

  8. Compare the state and control against the known solution.

  9. Recover and compare the costate when available.

  10. Only then adapt the implementation to the research problem.

Minimal End-to-End Workflow

The complete computational process is:

  1. Choose NN.

  2. Compute LGR points, weights, and D\boldsymbol{D}.

  3. Define state, control, and time bounds.

  4. Construct zguess\boldsymbol{z}_{\mathrm{guess}}.

  5. Call the NLP solver.

  6. Inside the objective function:

    1. unpack z\boldsymbol{z};

    2. reshape X\boldsymbol{X} and U\boldsymbol{U};

    3. compute physical time;

    4. evaluate the Mayer and Lagrange costs.

  7. Inside the constraint function:

    1. unpack and reshape;

    2. evaluate the dynamics;

    3. form the defect matrix;

    4. vectorize defects;

    5. evaluate and vectorize path constraints;

    6. evaluate endpoint constraints;

    7. stack all constraints.

  8. Recover the optimized trajectories.

  9. Check residuals and convergence.

  10. Extract multipliers and estimate costates if needed.

Summary

  1. The NLP solver operates on one decision vector, not state and control matrices.

  2. The discrete state and control arrays must be vectorized and stacked.

  3. Inside each NLP function, the vector is unpacked and reshaped.

  4. The LGR defect matrix is

DXtft02F.\boldsymbol{D}\boldsymbol{X}-\frac{t_f-t_0}{2}\boldsymbol{F}.
  1. Defect and path-constraint matrices must be converted into column vectors.

  2. The objective consists of a Mayer term plus an LGR quadrature approximation.

  3. Endpoint and path constraints are stacked with the defects.

  4. A straight-line state and control profile is often sufficient as an initial guess for simple problems.

  5. Solver interfaces differ mainly in how they accept equality and inequality constraints.

  6. Implementation should first be validated on simple benchmark problems.