API¶
Public names are eco_opt.__all__. Import them from the package
(import eco_opt or from eco_opt import Namespace). Internal
modules such as eco_opt.system are not the public contract.
Constraint tuples (same forms everywhere; t_0/t_f for a boundary):
(lb, "<=", expr, "<=", ub) # two-sided
(lb, "<=", expr) # lower: lb <= expr
(ub, ">=", expr) # upper: ub >= expr (expr <= ub)
(eq, "==", expr) # equality
(1.0/9.0, ">=", position) is \(x\le 1/9\). Full table:
Constraints and time.
Roles of states, controls, parameters, forcings, and costs:
Posing a problem. Collocation and piecewise-constant
\(u\): Transcription. Boxes, path constraints,
t_0/t_f, horizon: Constraints and time.
initial_guess, build / optimize, scaling by hand:
Guesses, mesh, scaling. If solve fails:
Troubleshooting.
System¶
System(states, controls, model, algebraics=0, parameters=0, name="sys")
Builds symbolic time sys.t, state sys.x, control sys.u, optional
algebraics sys.z and parameters sys.p, then calls model once to
get \(\dot{\mathbf{x}}\) and a Namespace of named signals sys.y.
model signatures:
| Dimensions | Callable | Return |
|---|---|---|
| ODE | (t, x, u) |
(dx, y) or dx |
| ODE + parameters | (t, x, u, p) |
(dx, y) or dx |
| DAE | (t, x, u, z) or (t, x, u, z, p) |
(dx, dz, y) or (dx, dz) |
dx is a list or tuple of states expressions (System stacks it).
dz is the residual \(0=g\)
(algebraics rows). y is optional; sys.y is then an empty
Namespace. Namespace does not import CasADi for you: use
import casadi as cs when \(f\) needs exp, sin, sign, or
interpolants. Function list: CasADi expressions.
| Attribute | Meaning |
|---|---|
sys.t |
time |
sys.x, sys.u |
state and control vectors |
sys.z, sys.p |
algebraics and parameters (length 0 if unused) |
sys.y |
named signals |
sys.nx, sys.nu, sys.nz, sys.np |
dimensions |
sys.name |
label |
Constraints and objectives may use sys.x[i] or a named field of
sys.y.
Namespace¶
Namespace(height=h, velocity=v, mass=m)
Attribute bag. Nesting is allowed (y.rocket.height).
t_0, t_f¶
t_0(expr) # value at the initial time of the (phase) horizon
t_f(expr) # value at the terminal time
Each returns a TimepointExpression. Use these in constraints and in
Mayer objectives. TimepointExpression is in __all__ because it is
the return type; it is not constructed by user code.
Ocp¶
ocp = Ocp()
ocp.max(expr) # or ocp.min(expr)
ocp.st(system=sys, constraints=[...])
sol = ocp.solve()
One of max or min. A later call replaces the earlier one. A list
of expressions is a running (Lagrange) cost, integrated in time.
A t_f(expr) term is a terminal (Mayer) cost. A list may mix
both.
st(system=sys, constraints=[...]) is the single-phase form.
st(phases=[...], continuity=True) is the multi-phase form (below).
solve and build¶
sol = ocp.solve(
control_intervals=100,
polynomial_degree=5,
collocation_points="legendre",
initial_guess=None,
ipopt_options=None,
tf_guess=1.0,
nlpsol_options=None,
)
solve compiles the NLP and runs IPOPT once (warm_start=False).
collocation_points is "legendre" or "radau". initial_guess
maps a symbolic component (sys.x[i], sys.u[i], …) to a
constant. Extra IPOPT keys go in ipopt_options
(Troubleshooting). Top-level CasADi
nlpsol options (for example {"expand": True}) go in nlpsol_options;
see Build and optimize.
Bound and parameter sweeps on a frozen mesh: Build and optimize.
ocp.build(
control_intervals=100,
polynomial_degree=5,
collocation_points="legendre",
initial_guess=None,
tf_guess=1.0,
)
build freezes the mesh. Numeric bounds and pinned parameters may
then change without re-discretizing (single-phase):
ocp.set_constraints([(new_p, "==", sys.p[0])])
sol_b = ocp.optimize(
initial_guess=None,
ipopt_options=None,
tf_guess=None,
warm_start=True,
nlpsol_options=None,
)
set_constraints uses the same tuple syntax as st and updates
ocp.constraints. What may change after build (boxes, existing
path bounds, pins, IPOPT options, guess) and what requires a new
build is tabulated in
Build and optimize. A new path
expression requires st and build again. A later build() keeps
the retuned bounds.
optimize runs IPOPT on the compiled NLP. After the first successful
call, warm_start=True (the default) uses the previous primal as
\(x_0\). solve always passes warm_start=False.
Multi-phase¶
p1 = Phase(sys_a, constraints=[...], control_intervals=20, name="ascent")
p2 = Phase(sys_b, constraints=[...], control_intervals=20, name="landing")
ocp.st(phases=[p1, p2], continuity=True)
sol = ocp.solve(tf_guess=5.0)
Each phase has its own System (symbols are not shared).
control_intervals=None inherits the value passed to solve /
build. name defaults to system.name.
continuity=True (default) links time and the state vector at each
junction. Algebraics and closed_loop are single-phase only.
Replay¶
sim = ocp.simulate(sol, t_grid=None, x0=None, plant=None, p=None, z0=None)
Open-loop integration of plant with sol.u. Defaults: plant is
the OCP System, t_grid is sol.time, x0 is sol.x[0], p and
z0 are taken from sol when present. Same result type as
eco_opt.simulate.
The keyword plant= is the simulated process model
(control-theory name). It is not vegetation.
Phase¶
Phase(system, constraints=None, control_intervals=None, name=None)
One stage of a sequential multi-phase problem. Constraint tuples are interpreted on that stage’s horizon \([t_0, t_f]\).
OcpResults¶
Returned by solve / optimize.
| Attribute | Meaning |
|---|---|
sol.success |
IPOPT stats["success"] |
sol.return_status |
IPOPT string |
sol.stats |
full IPOPT stats dict |
sol.objective |
scalar \(J\) in the declared sense (max or min) |
sol.t0, sol.tf |
horizon |
sol.time |
interval boundaries, length \(N+1\) |
sol.x |
states on sol.time, shape (N+1, nx) |
sol.u |
piecewise-constant controls, shape (N, nu) |
sol.z |
algebraics on sol.time, shape (N+1, nz) |
sol.p |
numerical parameters |
sol.time_intervals |
same knots as sol.time (control edges) |
sol.time_nodes, sol.x_nodes |
knots and collocation interiors |
sol.N, sol.d |
interval count and polynomial degree |
sol.sys |
System used in the transcription |
sol.phases |
list of PhaseTrajectory; single-phase is [self] |
sol.switch_times |
junction times (empty if one phase) |
sol.costate |
shadow prices \(\lambda = \partial J/\partial x\) at the collocation points, shape (N*d, nx); None for multi-phase |
sol.costate_time |
collocation-point times, length \(N d\) |
sol.hamiltonian |
\(H = L + \lambda^\top f\) at the same points |
Methods:
sol.signal(expr) # expression in sys.t, x, u, z; length N+1
sol.state(i) # sol.x[:, i]
sol.control(i) # sol.u[:, i]
sol.signal holds the last control piece at \(T\). Plot sol.u
against sol.time_intervals as steps
(Results and plots;
Transcription).
When every phase has the same nx, sol.time / sol.x / sol.u
are concatenated across phases (duplicate junction times dropped).
Otherwise those arrays are the last phase; use sol.phases[i].
PhaseTrajectory¶
One entry of sol.phases. Fields: sys, t0, tf, time, x,
u, z, time_intervals, N, d, tau, and signal(expr).
simulate¶
simulate(sys, x0, t_grid, u=None, p=None, z0=None) -> SimulationResults
Integrates \(\dot{\mathbf{x}}=f(t,\mathbf{x},\mathbf{u})\) with CVODES
(or IDAS if there are algebraics). t_grid must have at least two
points. u may be a constant, a callable u(t), or an OcpResults
(piecewise-constant hold of sol.u). Time in \(f\) is the integrator
clock.
closed_loop¶
closed_loop(
ocp, x0, dt, t_final=None, *,
horizon="shrinking",
prediction_horizon=None,
plant=None,
p=None,
z0=None,
control_intervals=None,
polynomial_degree=3,
collocation_points="radau",
ipopt_options=None,
initial_guess=None,
) -> ClosedLoopResults
ocp.closed_loop(x0, dt, t_final=..., **kwargs) is the same call.
sol = ocp.solve(control_intervals=14, polynomial_degree=3, tf_guess=T)
cl = ocp.closed_loop(
x0=sol.x[0],
dt=T / 14,
t_final=T,
horizon="shrinking",
)
T must be pinned in st. Receding: horizon="receding" and
prediction_horizon=.... A different process: plant= with the same
(nx, nu). Posing: Open loop vs closed loop.
At each sampling instant, the OCP is solved from the current process
state; the first control piece is applied for dt; the process is
integrated; the loop repeats. horizon="shrinking" keeps the original
terminal time. horizon="receding" looks prediction_horizon ahead
(default: the original horizon length).
t_final defaults to a pinned t_f(sys.t) in st. plant defaults
to the OCP System and must have the same (nx, nu). Multi-phase
problems raise NotImplementedError.
Mesh defaults differ from solve:
solve / build |
closed_loop |
|
|---|---|---|
control_intervals |
100 | from remaining (or prediction) horizon / dt, unless passed |
polynomial_degree |
5 | 3 |
collocation_points |
"legendre" |
"radau" |
IPOPT print_level |
0 unless ipopt_options overrides |
0 |
IPOPT max_iter |
solver default | 2000 |
Passing control_intervals fixes the interval count at every step.
initial_guess supplies extra constants; the current process state is
filled in for sys.x[i] when missing.
control_from_solution¶
u_fn = control_from_solution(sol) # u_fn(t) -> 1-d ndarray, length nu
Piecewise-constant hold of sol.u on sol.time_intervals.
SimulationResults¶
| Attribute | Meaning |
|---|---|
time |
integrator grid |
x |
states, (len(time), nx) |
u |
controls, (len(time)-1, nu) |
z, p |
algebraics on time; parameters |
t0, tf |
first and last times |
sys |
integrated System |
ClosedLoopResults¶
Same time series as SimulationResults, plus:
| Attribute | Meaning |
|---|---|
horizon |
"shrinking" or "receding" |
statuses |
IPOPT string per step |
objectives |
IPOPT \(J\) of each shrinking/receding NLP (remaining horizon), not the realized integral along the applied \(u\) |
success |
every status is Solve_Succeeded or Solved_To_Acceptable_Level |