Posing a problem¶
Turn a process ODE into an optimal-control problem by naming each symbol’s role, then writing \(J\) and the bounds. The package does not choose the hypothesis.
Roles¶
| Role | Symbol | In eco_opt |
Meaning |
|---|---|---|---|
| Time | \(t\) | sys.t |
Independent variable |
| State | \(\mathbf{x}(t)\) | sys.x |
A quantity that is integrated; requires an initial condition |
| Control | \(\mathbf{u}(t)\) | sys.u |
A quantity the optimizer may vary in time |
| Parameter | \(\mathbf{p}\) | sys.p |
Constant during one solve; may be retuned in a sweep |
| Algebraic | \(\mathbf{z}(t)\) | sys.z |
Unknown on \(g=0\), not an ODE (quasi-steady closure) |
| Forcing | \(w(t)\) | inside \(f\) | Known function of time (weather, a prescribed input) |
| Diagnostic | — | sys.y |
Named signal for \(J\) or a constraint, not extra dynamics |
Namespace holds diagnostics (sys.y.rocket.height). Nesting is
allowed. Import it from eco_opt. It does not replace CasADi
(CasADi expressions):
+, -, *, / on states work without import casadi; exp,
sin, sign, and interpolants do not. A vector \(\dot{\mathbf{x}}\)
may be a list of expressions; System stacks it. Import casadi as cs
only when the right-hand side needs those functions (Goddard:
cs.exp, cs.sign). A one-state model typically needs only
eco_opt.
The keyword plant= on closed_loop is the simulated process
model (control-theory name). It is not vegetation.
What is a control¶
A quantity that is a fixed number in the process model remains a parameter or a forcing. It is \(u(t)\) only if the hypothesis allows that quantity to vary in time in order to improve \(J\).
Examples in this package:
- irrigation;
- greenhouse heat;
- lake loading;
- harvest;
- microbial uptake;
- plant allocation;
- rocket mass-flow.
Rain in the irrigation forecast is forcing (\(P\equiv 0\) in the OCP). Realised rain on the simulated water store is a process disturbance, not a second control.
How the continuous problem becomes an NLP, and what may appear in \(f\): Transcription. Boxes, path constraints, and whether \(T\) is fixed: Constraints and time.
Running cost vs terminal cost¶
Classify per term. An entry that is not wrapped in t_f(...) is
a running (Lagrange) cost, integrated from \(t_0\) to \(T\). A
t_f(...) term is a terminal (Mayer) cost, the value at \(T\). A
list may mix both (greenhouse: harvest at \(T\) minus heating).
max / min replace the whole criterion, so mixed \(J\) is one list.
ocp.max([growth]) # maximize ∫ G dt
ocp.min([irrigation]) # minimize ∫ u dt
ocp.max(eco_opt.t_f(rocket.height)) # maximize h(T)
ocp.max(eco_opt.t_f(seed)) # maximize x₂(T)
ocp.min([eco_opt.t_f(-P5 * crop), P4 * heat]) # Mayer + Lagrange
Free horizon: bound t_f(sys.t) from below (and optionally above)
and pass tf_guess of the right order of magnitude. Time bounds are
explicit; omitting them does not default \(t_0=0\) or free \(T\).
See Constraints and time.
Open loop vs closed loop in a methods section¶
ocp.solve is the statement of the hypothesis: given this \(f\),
this \(J\), these bounds, and this initial state, what is
\(u^*(\cdot)\)? The open-loop trajectory is that statement on the
declared model. ocp.simulate(sol) integrates the same \(u^*\) and
checks that the collocation tape and the ODE agree.
Use closed loop (ocp.closed_loop) when the claim is about
applying the same hypothesis after the process has left the
forecast — rain on the water store, a wind gust, an unforecast load.
Re-planning updates the initial condition of the remaining OCP; it
does not change \(J\) or \(f\). If the simulated process is identical
to the OCP model, shrinking-horizon closed loop reprints the
open-loop tape and is a code check, not a new process result.
horizon="shrinking" shortens the remaining window (the usual
finite-horizon plan). horizon="receding" keeps a rolling prediction
window. \(T\) must be pinned ((T, "==", t_f(sys.t))). Signature:
API.
T, dt = 14.0, 1.0
sol = ocp.solve(control_intervals=14, polynomial_degree=3, tf_guess=T)
cl = ocp.closed_loop(
x0=sol.x[0],
dt=dt,
t_final=T,
horizon="shrinking",
)
# receding (rolling look-ahead):
# cl = ocp.closed_loop(
# x0=sol.x[0], dt=dt, t_final=T,
# horizon="receding", prediction_horizon=4.0,
# )
# unforecast process: plant=other_sys (same nx, nu)
Worked scripts: irrigation, microbial growth, and lignin.
Failed solves: Guesses, mesh, scaling, then Troubleshooting. Bound and parameter sweeps: Build and optimize.
Further reading¶
Optimal control (statement and necessary conditions):
- Pontryagin, Boltyanskii, Gamkrelidze and Mishchenko, The Mathematical Theory of Optimal Processes.
- Bryson and Ho, Applied Optimal Control.
- Kirk, Optimal Control Theory: An Introduction.
- Liberzon, Calculus of Variations and Optimal Control Theory (Princeton, 2012).
- Lenhart and Workman, Optimal Control Applied to Biological Models (Chapman & Hall/CRC, 2007).
Direct collocation and the numerical stack:
- Betts, Practical Methods for Optimal Control and Estimation Using Nonlinear Programming (SIAM).
- Biegler, Nonlinear Programming (SIAM, 2010) — collocation of ODEs and DAEs.
- Rao (2009), A survey of numerical methods for optimal control.
- Andersson, Gillis, Horn, Rawlings and Diehl (2019), CasADi.
- Wächter and Biegler (2006), IPOPT.
- Leek (2016), Yop — modeling
workflow that
eco_optfollows in Python.
An ecological transcription of the same kind:
- Manzoni, Chakrawal and Ledder (2023), finite-substrate microbial growth.