Parameter Management in Acados OCPs¶
This tutorial walks through AcadosParameter and AcadosParameterManager - the two
classes that control how numerical parameters flow into an acados Optimal Control Problem
(OCP) and, optionally, into a learning loop via AcadosPlanner.
Parameter types at a glance¶
Every parameter has an interface that determines its role:
Interface |
Symbolic in OCP? |
Changeable after solver creation? |
Exposed to learning? |
|---|---|---|---|
|
No (constant) |
No |
No |
|
Yes ( |
Yes |
No |
|
Yes ( |
Yes |
Yes (gradients available) |
Minimal example¶
The scenario below is a 10-step temperature-control problem with one parameter of each type, plus a stage-varying learnable parameter to illustrate the indicator mechanism.
Step 1 - Define parameters¶
Canonical location:
tutorial/parameter_manager/utils.py::make_params()
import gymnasium as gym
import numpy as np
from leap_c.ocp.acados.parameters import AcadosParameter, AcadosParameterManager
N_horizon = 10 # stages 0 .. 10 (inclusive)
params = [
# Fixed: known constant, baked into the solver at compile time.
AcadosParameter(
name="dt",
default=np.array([0.25]), # 15-minute time step [h]
interface="fix",
),
# Non-learnable: changes every call (e.g. a weather forecast),
# but is NOT differentiated through.
AcadosParameter(
name="outdoor_temp",
default=np.array([20.0]), # ambient temperature [degC]
interface="non-learnable",
),
# Learnable, constant across stages: a single scalar the learning
# algorithm can adjust.
AcadosParameter(
name="comfort_setpoint",
default=np.array([21.0]),
space=gym.spaces.Box(low=np.array([15.0]), high=np.array([28.0]), dtype=np.float64),
interface="learnable",
),
# Learnable, stage-varying: two price blocks - one for stages 0-4,
# another for stages 5-10. The manager handles the indicator mechanism
# automatically; you still call get("price") once in the OCP formulation.
AcadosParameter(
name="price",
default=np.array([0.15]), # electricity price [EUR/kWh]
space=gym.spaces.Box(low=np.array([0.0]), high=np.array([1.0]), dtype=np.float64),
interface="learnable",
end_stages=[4, N_horizon], # block 0: stages 0-4, block 1: stages 5-10
),
]
manager = AcadosParameterManager(params, N_horizon=N_horizon)
Step 2 - Use symbolic variables in the OCP formulation¶
Inside the function that builds your AcadosOcp, retrieve each parameter’s CasADi
expression (or constant numpy value for "fix") via manager.get().
Canonical location:
tutorial/parameter_manager/utils.py::build_ocp()
import numpy as np
from acados_template import AcadosOcp, AcadosModel
import casadi as ca
def build_ocp(manager: AcadosParameterManager, N_horizon: int) -> AcadosOcp:
ocp = AcadosOcp()
model = AcadosModel()
model.name = "temp_ctrl"
# State: room temperature [degC]
T = ca.SX.sym("T")
model.x = T
# Control: heating power [kW]
q = ca.SX.sym("q")
model.u = q
# Retrieve parameters - "fix" returns a numpy array, others return CasADi expressions.
dt = manager.get("dt")[0] # numpy scalar (unwrap from array)
outdoor_temp = manager.get("outdoor_temp") # CasADi SX, from p (per-stage)
comfort_ref = manager.get("comfort_setpoint") # CasADi SX, from p_global
price = manager.get("price") # CasADi SX expression, stage-aware
# (weighted sum over price_0_4 / price_5_10)
# Discrete-time dynamics: simple RC model
R, C = 2.0, 1.5 # thermal resistance, capacitance
model.disc_dyn_expr = T + dt * ((outdoor_temp - T) / (R * C) + q / C)
# Stage cost: comfort deviation + price-weighted energy
model.cost_expr_ext_cost = (T - comfort_ref) ** 2 + price * q
# Terminal cost: comfort deviation only (no control at terminal stage)
model.cost_expr_ext_cost_e = (T - comfort_ref) ** 2
ocp.cost.cost_type = "EXTERNAL"
ocp.cost.cost_type_e = "EXTERNAL"
ocp.model = model
manager.assign_to_ocp(ocp) # wires p_global and p into the ocp object
# Provide a nominal x0 so acados allocates lbx/ubx at stage 0.
# The actual value is overwritten at each solve call by AcadosDiffMpcTorch.
ocp.constraints.x0 = np.array([20.0])
ocp.solver_options.tf = N_horizon * dt
ocp.solver_options.N_horizon = N_horizon
ocp.solver_options.qp_solver = "PARTIAL_CONDENSING_HPIPM"
ocp.solver_options.hessian_approx = "EXACT"
ocp.solver_options.integrator_type = "DISCRETE"
return ocp
manager.assign_to_ocp(ocp) flattens both parameter structs and writes them into:
ocp.model.p_global- learnable parameters (shared across all stages)ocp.model.p- non-learnable parameters (a vector applied per stage)
Step 3 - Set values at runtime¶
Non-learnable parameters: combine_non_learnable_parameter_values¶
Call this before each solver invocation to pack a batch of per-stage parameter arrays.
Pass stage-wise forecasts as (batch_size, N_horizon + 1, param_dim) arrays in overwrite.
See also:
tutorial/parameter_manager/pm_tutorial.py(lines that buildp_stagewise)
rng = np.random.default_rng(seed=0)
batch_size = 4
N_stages = N_horizon + 1 # 11 stages (0 .. 10)
# Outdoor temperature forecast: shape (batch_size, N_stages, 1)
temp_forecast = rng.uniform(5.0, 25.0, size=(batch_size, N_stages, 1))
# Returns shape (batch_size, N_stages, n_nonlearnable)
p_stagewise = manager.combine_non_learnable_parameter_values(
batch_size=batch_size,
outdoor_temp=temp_forecast,
)
# p_stagewise[:,k,:] is the non-learnable parameter vector at stage k.
Without overwrite, all stages use the default value declared in AcadosParameter.
Learnable parameters: combine_default_learnable_parameter_values¶
Use this to create an initial parameter batch, optionally overwriting specific entries.
For stage-varying parameters the overwrite array must have shape
(batch_size, N_horizon + 1, param_dim) - the manager picks the value at the start of
each block.
See also:
tutorial/parameter_manager/pm_tutorial_forecast.py(non-default learnable params block)
# Stage-wise price forecast: shape (batch_size, N_stages, 1)
price_forecast = rng.uniform(0.05, 0.40, size=(batch_size, N_stages, 1))
# Per-batch comfort setpoint: shape (batch_size, 1)
comfort_values = np.array([[19.0], [21.0], [23.0], [22.5]])
# Returns shape (batch_size, N_learnable).
# Starts from defaults and replaces the named entries.
param = manager.combine_default_learnable_parameter_values(
comfort_setpoint=comfort_values,
price=price_forecast, # block 0 value taken from stage 0, block 1 from stage 5
)
The indicator mechanism¶
price has two stage blocks (end_stages=[4, 10]). Internally the manager stores
price_0_4 and price_5_10 in p_global, and appends an indicator vector of length
N_horizon + 1 to p. At stage k, indicator[k] = 1 and all other entries are zero.
manager.get("price") returns:
sum(indicator[0:5]) * price_0_4 + sum(indicator[5:11]) * price_5_10
This single CasADi expression evaluates to the correct block value at every stage without any conditional logic in the OCP.
combine_non_learnable_parameter_values sets the indicator automatically - you never
have to touch it manually. If you bypass this method and leave the indicator all-zero,
every stage silently evaluates to zero for all stage-varying learnable parameters.
Using with AcadosPlanner¶
Basic usage¶
AcadosPlanner wraps the above pattern. Its forward method calls
combine_non_learnable_parameter_values internally using the default values for all
non-learnable parameters; the caller only needs to supply the initial state and the
learnable parameter tensor param.
Complete example:
tutorial/parameter_manager/pm_tutorial.py
from leap_c.ocp.acados.planner import AcadosPlanner
from leap_c.ocp.acados.torch import AcadosDiffMpcTorch
ocp = build_ocp(manager, N_horizon)
diff_mpc = AcadosDiffMpcTorch(ocp)
planner = AcadosPlanner(param_manager=manager, diff_mpc=diff_mpc)
# p_global batch: shape (batch_size, N_learnable)
param = manager.combine_default_learnable_parameter_values(batch_size=batch_size)
# planner.forward calls combine_non_learnable_parameter_values internally
# (outdoor_temp stays at its default 20 degC for every stage)
ctx, u0, x, u, value = planner.forward(obs=x0_batch, param=param)
Forecast-aware planner (subclassing AcadosPlanner)¶
When non-learnable parameters must be set from per-call data (e.g. a live weather
forecast), subclass AcadosPlanner and override forward. The override should:
Extract the forecast from the observation dict.
Call
combine_non_learnable_parameter_valueswith the overwrite to buildp_stagewise.Pass
p_stagewisedirectly toself.diff_mpc.
Complete example:
tutorial/parameter_manager/pm_tutorial_forecast.py
from typing import Any
import numpy as np
import torch
from leap_c.ocp.acados.diff_mpc import AcadosDiffMpcCtx
from leap_c.ocp.acados.planner import AcadosPlanner
class TempCtrlPlanner(AcadosPlanner):
"""Planner that injects an outdoor temperature forecast at every solve call."""
def forward(
self,
obs: dict[str, Any],
action: torch.Tensor | None = None,
param: torch.Tensor | None = None,
ctx: AcadosDiffMpcCtx | None = None,
):
state = obs["state"] # (batch_size, 1)
temp_forecast = obs["outdoor_temp_forecast"] # (batch_size, N_stages, 1)
batch_size = state.shape[0]
# Build per-stage non-learnable params with the forecast injected.
p_stagewise = self.param_manager.combine_non_learnable_parameter_values(
batch_size=batch_size,
outdoor_temp=temp_forecast,
)
return self.diff_mpc(state, action, param, p_stagewise, ctx=ctx)
The caller assembles the observation dict and non-default learnable params, then calls
forward as usual:
planner = TempCtrlPlanner(param_manager=manager, diff_mpc=diff_mpc)
obs = {
"state": x0_batch, # torch.Tensor (batch_size, 1)
"outdoor_temp_forecast": temp_forecast, # np.ndarray (batch_size, N_stages, 1)
}
# Non-default learnable params: per-batch comfort setpoint + stage-varying price
param = torch.tensor(
manager.combine_default_learnable_parameter_values(
comfort_setpoint=comfort_values, # (batch_size, 1)
price=price_forecast, # (batch_size, N_stages, 1)
)
)
ctx, u0, x, u, value = planner.forward(obs=obs, param=param)
Complete implementations¶
The runnable scripts for this tutorial live in tutorial/parameter_manager/:
File |
What it shows |
|---|---|
|
Shared constants ( |
|
Basic |
|
|