ECO stands for Economic Combustion Optimization and solves for the optimal injector inputs of a direct-injection compression-ignition engine. Using the software package acados, a continuous-time optimal control problem (OCP) is formulated and solved by direct method (multiple shooting). The resulting nonlinear program (NLP) is solved by sequential quadratic programming (SQP) using the solver HPIPM. The resulting Hessian is regularized using Levenberg-Marquardt regularization.
ECO was implemented on a real engine test bench using a rapid prototyping system and embedded controllers. A demonstration is available here: https://vimeo.com/933704668
The OCP uses an economic cost function over one high pressure cycle of a cylinder with states
Minimizing injected fuel at requested IMEP is equivalent to maximizing indicated efficiency, so the solution is the most fuel-efficient injection strategy that still respects the mechanical (peak pressure, pressure rise rate) and emission (NOx, exhaust temperature, equivalence ratio) limits.
The NLP as built in eco/formulation/create_acados_ocp.py and eco/formulation/init_acados_ocp.py.
For
in en_nox = True.
Since
so the problem has no controls: the only free decision is the injection part
of
| Constraint | Bound | Default | Type | Slacked |
|---|---|---|---|---|
| Peak pressure | 150 bar | path constraint | — | |
| Pressure rise rate | 4 bar/degCA | path constraint | yes | |
| Center of combustion |
|
20 degCA aTDC | path constraint | — |
| Indicated mean effective pressure | 6 bar | terminal constraint | — | |
| Exhaust gas temperature | swept (0…540 °C) | terminal constraint | — | |
| NOx concentration | $0 \le \mathrm{NO}\mathrm{ppm} \le c{\mathrm{NO}_x}$ | swept (10⁴…900 ppm) | terminal constraint | yes |
| Equivalence ratio | terminal constraint | — | ||
| Injection spacing |
|
terminal constraint | — |
The pressure rise rate and the NOx concentration are the two constraints that can
render the QP infeasible from a poor initial guess. They are therefore relaxed by
non-negative slack variables
with idxsh = [1]) and idxsh_e). The large
linear weights
The OCP is transcribed by multiple shooting with an explicit fourth-order
Runge-Kutta integrator on a non-uniform crank-angle grid: fine steps
(
and the NLP that acados hands to the SQP method is
The economic cost
For the default two-injection, NOx-enabled setup this gives
The NLP is not solved by an interior-point method such as IPOPT. It is solved
by sequential quadratic programming (nlp_solver_type = 'SQP'; 'SQP_RTI'
performs a single real-time iteration when n_sqp_max == 1). Collecting the
equality constraints in
and each SQP iteration
followed by the full step
Levenberg-Marquardt regularization. Setting exact_hess_dyn = 0 and
exact_hess_constr = 0 drops the indefinite second-order terms of the dynamics
and constraints from
This keeps regularize_method = 'NO_REGULARIZE').
Iterations stop once the KKT residual falls below the tolerances, or after
n_sqp_max iterations.
| Option | Value |
|---|---|
nlp_solver_type |
SQP (SQP_RTI if n_sqp_max == 1) |
qp_solver |
PARTIAL_CONDENSING_HPIPM |
qp_solver_cond_N |
5 (condensed horizon) |
nlp_solver_max_iter |
par_opt.sqp['n_sqp_max'] |
nlp_solver_step_length |
par_opt.sqp['step_size'] (1.0) |
levenberg_marquardt |
1e-2 |
hessian_approx |
EXACT, with exact_hess_dyn = 0, exact_hess_constr = 0 |
regularize_method |
NO_REGULARIZE |
integrator_type |
ERK, sim_method_num_stages = 4 |
| tolerances | stationarity 1e-4; equality / inequality / complementarity 1e-6 |
ECO requires a built acados installation with its Python interface — follow the official installation guide.
Its location is the only path you need to configure, in python_code/.env:
ACADOS_SOURCE_DIR=/path/to/acadosThis is the directory containing acados' lib/ and include/. Everything else
— the shared libraries to preload, LD_LIBRARY_PATH for the generated C code —
is derived from it by eco/acados_env.py, which every entry
point calls via load_acados(). Exporting ACADOS_SOURCE_DIR as a shell
variable works too and takes precedence; if neither is set, ../acados and
~/acados are tried before an error is raised.
Then create the environment and install ECO with its dependencies, which are declared in pyproject.toml:
cd python_code
python3 -m venv env
source env/bin/activate
pip install -e ".[dev]"Finally install the acados Python interface from the tree you configured above — it generates and compiles C code against the acados library, so it has to come from the same installation rather than from PyPI:
pip install -e "$ACADOS_SOURCE_DIR/interfaces/acados_template"load_acados() checks this on every run: it raises if acados_template is
missing, and warns if it was imported from somewhere other than
ACADOS_SOURCE_DIR, since a mismatch between the Python interface and the
compiled library produces confusing build errors.
Tested with Python 3.12.3, acados v0.5.3 (C library) and acados_template
0.5.1, CasADi 3.7.2, NumPy 2.4.0, SciPy 1.16.3, matplotlib 3.10.8.
The scripts can be run from any working directory. Each one generates and
compiles C code into a c_generated_code* folder on first execution, which takes
a while; later runs reuse it.
| Order | Script | What it does |
|---|---|---|
| 1 | examples/simulate_acados_and_plot.py | Forward simulation only (IVC → EVO) with fixed injection inputs. Use this first to check the model and the acados build. Writes cylinder_pressure.png. |
| 2 | examples/main_python.py |
Main entry point. Pre-integrates IVC → OCP start, builds the OCP, solves it unconstrained, then runs Pareto sweeps over examples/pareto_results.png. |
| 3 | examples/optimize_two_injections.py | Single operating point, two injections, with a homotopy that ramps the constraints from loose to target values and reports the tightest feasible solution. Use this when a direct solve at the target constraints fails. |
| 4 | examples/example_injection_optimization.py | Minimal, self-contained demonstration of the formulation API. |
python examples/simulate_acados_and_plot.py
python examples/main_python.py
python examples/optimize_two_injections.pyTo obtain optimal injector inputs for your own operating point, edit the
OptimizationParameters class inside the example script (references, bounds,
OperatingPoint
(eco/simulation/par_op_def.py: engine speed,
intake pressure/temperature, burnt gas fraction, rail pressure), then rerun.
Initialize the solver with an initial guess close to a previously obtained solution. Otherwise robust convergence to a feasible solution is not guaranteed.
Any custom driver script follows the same three steps:
from eco.formulation.create_acados_ocp import create_acados_functions_inj_opt
from eco.formulation.run_sqp_acados import run_sqp_acados_inj_opt
# 1. build + compile the OCP (once)
ocp_solver, fcn = create_acados_functions_inj_opt(par_opt, par_model, par_op)
# 2. + 3. initialize bounds/guess and solve (repeat per reference value)
u_opt, status, result = run_sqp_acados_inj_opt(
ocp_solver, fcn, par_model, par_sim, par_opt)run_sqp_acados_inj_opt calls create_init_acados_ocp_inj_opt internally.
It returns the optimal $u = [\mathrm{SOE}1,\dots,\mathrm{DOE}{n_\mathrm{inj}}]$
in physical units, the acados status (0 = success), and the full unscaled
trajectories in result['x'], result['u'], result['ca'].
The simulation-based initial trajectory is set only on the very first solve. Subsequent solves keep the previous solution as their guess and only refresh the constraint bounds, so a tightening reference is approached gradually.
python -m pytest tests/test_model_casadi.py -veco/
├── model_casadi/ # Physical models, CasADi SX throughout
│ ├── model_parameters.py # Engine geometry, thermodynamics, calibration
│ ├── complete_model.py # Full RHS: combustion + optional NOx
│ ├── in_cylinder_model.py # In-cylinder thermodynamics
│ ├── combustion_model.py # Heat release
│ ├── algebraic_injector_model.py
│ ├── ign_del_model.py # Total ignition delay (chem. + phys.)
│ ├── ignition_delay_joerg.py # Chemical ignition delay correlation
│ ├── utils.py # Smooth validity/saturation helpers
│ └── subfunctions/ # cyl. volume, kappa, Woschni, two-zone, Zeldovich
│
├── simulation/ # Forward simulation
│ ├── par_op_def.py # Operating point (IVC conditions, valve timings)
│ ├── export_complete_model.py # AcadosModel export for the integrator
│ ├── acados_simulation.py # AcadosSimSolver-based integration (RK4 / Euler)
│ └── complete_simulation.py # Simulation over a crank-angle grid
│
└── formulation/ # Optimal control problem
├── casadi_model.py # Bridge to the symbolic model
├── scale_unscale.py # Affine variable scaling
├── create_acados_ocp.py # OCP: cost, constraints, solver options
├── init_acados_ocp.py # Bounds, references, warm start
└── run_sqp_acados.py # Solve + unscale results