Transcription Methods
laOPT currently provides two direct transcription methods. Models with discrete-time dynamics (DiscreteDynamics option) are supported by multiple shooting only.
Multiple Shooting
MultipleShooting<ControlProblem, Segments> introduces state variables at the shooting nodes and enforces continuity through integrated dynamics. The default integrator is explicit fourth-order Runge-Kutta (ERK4).
using Transcription = laopt_tools::MultipleShooting<MyOcp, 40>;
For Segments = N, the discrete solution contains N + 1 states and N inputs. States are reconstructed between shooting nodes with linear interpolation. Inputs use a zero-order hold and are constant over each shooting segment.
Discrete-Time Dynamics
Multiple shooting is the only transcription that supports models with the DiscreteDynamics option (see Defining an Optimal Control Problem). In that case, no integrator is used. The continuity constraint of each segment is \(x_{k+1} = f_d(x_k, u_k, p, t_0, t_f, \tau_k)\) with \(f_d\) given by the user’s discrete_dynamics_impl, so the number of Segments equals the number of discrete steps.
The Lagrange cost is also treated differently. For continuous dynamics, it is approximated by the left Riemann sum \(\sum_k h\,L_k\) with segment length \(h\). For discrete dynamics, it is the plain sum \(\sum_k L_k\) over the samples, because the step size belongs to the user’s discrete model. DiscreteDynamics is not compatible with FreeEndTime.
Radau Collocation
RadauCollocation<ControlProblem, Segments, Degree> uses a multi-segment pseudospectral scheme with Legendre-Gauss-Radau points.
using Transcription = laopt_tools::RadauCollocation<MyOcp, /*S*/ 10, /*D*/ 3>;
For Segments = S and Degree = D, the discrete solution contains S * D + 1 state and input nodes. Adjacent segments share their boundary node. Radau collocation requires continuous-time dynamics_impl and does not support the DiscreteDynamics option. The nodes inside each segment are non-uniformly spaced, and the state and input are represented by degree-D Lagrange polynomials through the D + 1 nodes of that segment.
Constructing the Nonlinear Program
The transcription classes wrap the same workflow described in Modeling Nonlinear Programs. A transcription derives from laopt::Differentiable, owns the generated laopt::Variable blocks, and implements define_problem to add the discretized cost, dynamics, bounds, and path constraints. laopt::Problem<Transcription> then assembles that model into the solver-facing NLP in exactly the same way as a hand-written nonlinear program.
Both transcriptions therefore expose the same construction pattern:
auto ocp = std::make_shared<MyOcp>();
auto transcription = std::make_shared<Transcription>(ocp);
using Problem = laopt::Problem<Transcription>;
auto problem = std::make_shared<Problem>(transcription);
The complete handoff is ControlProblemBase → transcription → Problem<Transcription> → solver. The solver and Problem retain the same transcription instance. After solver.solve(), the optimized decision variables are therefore available through transcription.
Retrieve the Discrete Solution
Both transcription classes provide the following solution API:
double get_tf_opt() const;
TimeTrajectory get_T_opt() const;
StateTrajectory get_X_opt() const;
InputTrajectory get_U_opt() const;
Param get_p_opt() const;
void print_diagnostics() const;
The getters return copies owned by the caller. Time is returned in physical units over \([t_0,t_f]\), not as normalized transcription time.
solver.solve();
const double tf = transcription->get_tf_opt();
const auto T = transcription->get_T_opt();
const auto X = transcription->get_X_opt();
const auto U = transcription->get_U_opt();
const auto p = transcription->get_p_opt();
The trajectory layouts depend on the transcription:
| Method | Number of Nodes | T | X | U |
|---|---|---|---|---|
| Multiple shooting | N + 1 state nodes | (N + 1) × 1 | NX × (N + 1) | NU × N |
| Radau collocation | S * D + 1 shared collocation nodes | (S * D + 1) × 1 | NX × (S * D + 1) | NU × (S * D + 1) |
Each column of X corresponds to the same column of T. Radau input columns also correspond directly to T. For multiple shooting, U.col(k) is the input held over the interval from T(k) to T(k + 1).
Sample at One Time
Both methods expose point-sampling functions in physical time:
Eigen::Vector<Numeric, ControlProblem::NX> get_x_at(const Numeric& t) const;
Eigen::Vector<Numeric, ControlProblem::NU> get_u_at(const Numeric& t) const;
const double t_query = 0.35;
const auto x = transcription->get_x_at(t_query);
const auto u = transcription->get_u_at(t_query);
| Method | get_x_at(t) | get_u_at(t) |
|---|---|---|
| Multiple shooting | Linear interpolation between adjacent state nodes. | Zero-order hold of the segment input. |
| Radau collocation | Evaluates the segment’s degree-D Lagrange polynomial. | Evaluates the segment’s degree-D Lagrange polynomial. |
For Radau collocation, use point sampling for \(t_0 \leq t < t_f\). Retrieve the terminal node with X.rightCols(1) or U.rightCols(1). The trajectory resampling functions below include the terminal point automatically.
Resample the Complete Trajectory
Use the batch resampling API when a uniformly ordered, denser trajectory is needed for plotting, simulation interfaces, or a controller:
Eigen::MatrixX<Numeric> get_TX_resampled(const Numeric& Ts_max) const;
Eigen::MatrixX<Numeric> get_TU_resampled(const Numeric& Ts_max) const;
Ts_max is the maximum spacing between returned samples in physical time. Each transcription segment is divided into an integer number of equal intervals, so the actual sample period may be smaller than Ts_max.
const double Ts_max = 0.01;
const Eigen::MatrixXd TX = transcription->get_TX_resampled(Ts_max);
const Eigen::MatrixXd TU = transcription->get_TU_resampled(Ts_max);
// Time occupies the first row; trajectory values occupy the remaining rows.
const auto T_sampled = TX.row(0);
const auto X_sampled = TX.bottomRows(MyOcp::NX);
const auto U_sampled = TU.bottomRows(MyOcp::NU);
The packed outputs have the layouts
\[TX = \begin{bmatrix}T\\X\end{bmatrix} \in \mathbb{R}^{(N_X+1)\times N_s}, \qquad TU = \begin{bmatrix}T\\U\end{bmatrix} \in \mathbb{R}^{(N_U+1)\times N_s}.\]Multiple shooting uses the same linear-state and held-input reconstruction as its point-sampling methods. Radau resampling locates each sample in its segment and evaluates the collocation polynomial there. It does not linearly interpolate between the nonuniform Radau nodes and does not reintegrate the dynamics.
get_T_opt(),get_X_opt(), andget_U_opt()expose the transcription nodes. For Radau collocation, useget_x_at,get_u_at, or the resampling helpers whenever values between those nodes are needed.