Sampling-Based Planning and Model Predictive Control › Constraints, Warm Starts, and Real-Time Budgets

Michael BrenndoerferJuly 7, 202612 min read
World Models HandbookAll chapters
Part 7 of 12Planning and Agency
Chapter 37Sampling-Based Planning and Model Predictive Control › Constraints, Warm Starts, and Real-Time Budgets
Reading section 5 of 8 · Constraints, Warm Starts, and Real-Time Budgets · 12 minSection 5 of 8 · 12 min

Book contents

Section 5 of 8 · 12 min read

Sampling-Based Planning and Model Predictive Control › Constraints, Warm Starts, and Real-Time BudgetsOne section · Sections in this chapter5 / 8 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

We have so far treated the MPC problem as unconstrained except for the box on controls. Real systems often impose joint, velocity, geofence, or clearance constraints. Sampling-based planners have several options: reject infeasible samples, project or transform samples into a feasible parameterization, use a constrained optimizer, or add violation penalties to the objective. We use a finite penalty here. It converts a hard requirement into a soft preference; it neither diverges at the boundary nor guarantees feasibility.

Rejection preserves feasibility among retained samples, but its acceptance rate equals the proposal probability of the feasible set and can become very small in a narrow corridor. It still offers no guarantee that the best retained finite sample is globally optimal. A finite penalty always assigns a numerical cost, but the selected plan may remain infeasible. If violation is unacceptable, a soft-penalty planner needs a separate verified feasibility or safety mechanism rather than an assumption that a small violation is harmless.

Consider an obstacle at position cc with radius rr for a point-mass robot. A soft constraint for the obstacle is

penalty(τ)=w∑k=1Hmax⁡(0,r−∥pk−c∥)2,\text{penalty}(\tau) = w \sum_{k=1}^{H} \max\bigl(0, r - \|p_k - c\|\bigr)^2,

where ww is a positive weight. Any evaluated point inside the disc contributes a quadratic penalty; points on or outside the boundary contribute zero. The squared hinge is continuously differentiable at the boundary, although its second derivative changes there. Because the terms are summed, several shallow violations can outweigh one deeper violation. The obstacle term itself does not reward extra clearance once outside, so the task and effort terms determine whether a plan grazes the boundary or takes a wider route.

Let us add an obstacle directly on the straight-line path from start to goal. The finite penalty makes routing around it compete with goal error and effort; feasibility is checked after optimization.

In[22]:
Code
obstacles = [(np.array([1.5, 1.2]), 0.55)]
PENALTY_WEIGHT = 5000.0


def make_constrained_cost(goal, obstacles, weight=PENALTY_WEIGHT):
    def _cost(states, actions):
        base, traj = batched_cost(states, actions, goal)
        pos = traj[:, 1:, :2]
        viol = np.zeros(states.shape[0])
        for c, r in obstacles:
            d = np.linalg.norm(pos - c[None, None, :], axis=2)
            viol += np.sum(np.maximum(0.0, r - d) ** 2, axis=1)
        return base + weight * viol, traj

    return _cost


constrained_cost = make_constrained_cost(goal, obstacles)

constrained_actions, _, _ = cem(
    state0,
    goal,
    PLAN_HORIZON,
    n_samples=1000,
    n_elite=100,
    n_iters=20,
    rng=np.random.default_rng(7),
    cost_fn=constrained_cost,
)
constrained_traj = batched_rollout(
    state0[None, :], constrained_actions[None, :, :]
)[0]
obstacle_distances = np.linalg.norm(
    constrained_traj[:, :2] - obstacles[0][0][None, :], axis=1
)
min_clearance = float(obstacle_distances.min() - obstacles[0][1])

Notice the structure of make_constrained_cost. It wraps the base cost and adds one soft-penalty component; a truly hard constraint would require different handling. The obstacle penalty is also evaluated only at discrete rollout states. A segment can cross the disc between time steps even when both endpoints are outside, so tighter temporal discretization or segment-intersection checks are needed to rule that out. The numerical weight 5000 is large relative to this example's task term, but its physical units depend on how position and control costs are normalized. It is a tuning coefficient, not a dimensionless universal ratio of importance.

Out[23]:
Console
Obstacle radius: 0.55 m
Minimum clearance along the plan: 0.003 m
Negative clearance means the plan touches or passes through the obstacle.

For this discretized example, the squared-hinge term makes sampled states inside the obstacle expensive. Increasing ww tends to prioritize reducing penetration, while decreasing it allows more tradeoff against goal and effort costs. No finite weight guarantees that an infeasible trajectory is never preferred, especially when no feasible sample is drawn. A practical sweep should therefore report both task performance and violation, followed by an explicit feasibility check on the selected trajectory.

The weight is not a parameter that can be set to "large" and forgotten, because it controls a tradeoff between clearance and goal progress. The cell below re-solves the same configured problem at nine penalty weights, changing ww while holding the start state, horizon, sample budget, and pseudorandom seed fixed. Ties are ordered by cost and then sample index in cem.

In[24]:
Code
penalty_weights = [0.01, 0.5, 1.0, 5.0, 10.0, 50.0, 250.0, 1000.0, 5000.0]
penalty_min_clearance = []
penalty_goal_error = []
for w in penalty_weights:
    cost_w = make_constrained_cost(goal, obstacles, weight=w)
    acts_w, _, _ = cem(
        state0,
        goal,
        PLAN_HORIZON,
        n_samples=500,
        n_elite=50,
        n_iters=10,
        rng=np.random.default_rng(7),
        cost_fn=cost_w,
    )
    traj_w = batched_rollout(state0[None, :], acts_w[None, :, :])[0]
    d_w = np.linalg.norm(traj_w[:, :2] - obstacles[0][0][None, :], axis=1)
    penalty_min_clearance.append(float(d_w.min() - obstacles[0][1]))
    penalty_goal_error.append(float(np.linalg.norm(traj_w[-1, :2] - goal)))

penalty_weights = np.array(penalty_weights)
penalty_min_clearance = np.array(penalty_min_clearance)
penalty_goal_error = np.array(penalty_goal_error)
Out[25]:
Visualization
Line chart of sampled-state obstacle clearance versus penalty weight, with a dashed zero-clearance line.
Minimum sampled-state clearance from the obstacle in one fixed-seed CEM sweep as the soft-constraint weight varies. The dashed line marks zero clearance; checking only rollout states does not detect collisions between steps.
Line chart of final distance to the goal versus penalty weight.
Final goal error for the same plans and weight sweep. Reading this panel with clearance exposes the task-feasibility tradeoff rather than treating a large penalty as a guarantee.

Read together, clearance and final goal error identify candidate weights that improve sampled-state feasibility without unacceptable task degradation in this run. They do not certify the continuous path. Weight selection should be repeated across seeds and starting conditions, followed by the safety checks appropriate to the deployment.

Out[26]:
Visualization
Path from origin to a goal star that arcs around a shaded obstacle disc.
CEM planning with a soft obstacle penalty evaluated at discrete rollout states. The selected trajectory's sampled positions curve around the disc; continuous-path feasibility between states is not certified.

The figure shows the selected plan's sampled positions curving around the disc, while the plotted straight line intersects it. The detour is smooth under the acceleration dynamics and effort cost. Any visible margin is an outcome of this finite-sample run, not a robustness guarantee: because the obstacle penalty is zero outside the disc, it supplies no direct reward for additional clearance.

The second half of this section is warm starting. When the state, goal, and constraints change slowly between ticks, the shifted previous plan can be a useful initial proposal for the current problem:

a~0:H−1=(a1⋆,a2⋆,…,aH−1⋆,0)\tilde{a}_{0:H-1} = (a_1^\star, a_2^\star, \dots, a_{H-1}^\star, 0)

We can pass this sequence as mu_init to CEM. When the prior remains relevant, it can improve finite-iteration plan quality or reduce the iterations needed to meet a target quality. It may also reduce between-tick variation by anchoring consecutive proposals. Abrupt goal, constraint, or dynamics changes can make the same prior harmful, so implementations should retain enough proposal variance or support a cold restart.

Let us demonstrate. We first solve a CEM problem at a previous state and apply its first action to obtain the next state. At that next state, we compare a one-iteration cold start against a one-iteration warm start initialized from the shifted prior plan.

In[27]:
Code
previous_state = np.array([0.4, 0.3, 0.5, 0.2])
prior_plan, _, _ = cem(
    previous_state,
    goal,
    PLAN_HORIZON,
    n_samples=800,
    n_elite=80,
    n_iters=12,
    rng=np.random.default_rng(21),
)
nearby_state = batched_rollout(
    previous_state[None, :], prior_plan[None, :1, :]
)[0, 1]
shifted_prior = np.vstack([prior_plan[1:], np.zeros((1, 2))])

ONE_ITER = 1
cold_plan, _, _ = cem(
    nearby_state,
    goal,
    PLAN_HORIZON,
    n_samples=800,
    n_elite=80,
    n_iters=ONE_ITER,
    rng=np.random.default_rng(99),
)
warm_plan, _, _ = cem(
    nearby_state,
    goal,
    PLAN_HORIZON,
    n_samples=800,
    n_elite=80,
    n_iters=ONE_ITER,
    rng=np.random.default_rng(99),
    mu_init=shifted_prior,
)
cold_cost = float(
    batched_cost(nearby_state[None, :], cold_plan[None, :, :], goal)[0][0]
)
warm_cost = float(
    batched_cost(nearby_state[None, :], warm_plan[None, :, :], goal)[0][0]
)

With identical sample budgets and random-number seeds, the warm-started plan is lower-cost in this run after one iteration. The prior was computed at the previous state, shifted by the applied action, and evaluated at the resulting next state. The only configured difference between the two one-iteration calls is the initial proposal mean. This isolates the warm-start mechanism for the stated seed, but repeated disturbances and planner seeds are needed to estimate its typical benefit.

Warm starting can also reduce the number of refinement iterations needed for a target plan quality. A real-time controller must account for worst-case planner latency, because a missed deadline changes when an action is applied. Random shooting evaluates NN independent rollouts. CEM evaluates approximately KiterNK_{\text{iter}}N rollouts plus elite selection and distribution updates. The product is therefore a first-order compute indicator, not a complete latency model. For a fixed product, the best split between iterations and samples depends on proposal dimension, elite fraction, hardware, and objective; more iterations do not win universally.

Let us measure the two planners and see the picture.

In[28]:
Code
def timed_planner(fn, repeats=15):
    t0 = time.perf_counter()
    for _ in range(repeats):
        fn()
    return (time.perf_counter() - t0) / repeats


RS_SAMPLES_TIMING = 4000
CEM_SAMPLES_TIMING = 800
CEM_ELITE_TIMING = 80
CEM_ITERS_TIMING = 8

rs_time = timed_planner(
    lambda: random_shooting(
        nearby_state,
        goal,
        PLAN_HORIZON,
        n_samples=RS_SAMPLES_TIMING,
        rng=np.random.default_rng(0),
    )
)
cem_time = timed_planner(
    lambda: cem(
        nearby_state,
        goal,
        PLAN_HORIZON,
        n_samples=CEM_SAMPLES_TIMING,
        n_elite=CEM_ELITE_TIMING,
        n_iters=CEM_ITERS_TIMING,
        rng=np.random.default_rng(0),
    )
)

CONTROL_HZ = 100
TICK_MS = 1000.0 / CONTROL_HZ
Out[29]:
Console
Random shooting (4000 samples, no iterations): 3.87 ms/plan
CEM (8 iterations x 800 samples):              10.11 ms/plan
Real-time control at 100 Hz costs 10.0 ms/tick.

The numbers should be read with the stated budgets: random shooting performs 4,000 rollouts, while CEM performs 6,400 plus eight elite-selection and refit steps. These wall-clock measurements depend on the current machine, Python and NumPy versions, load, and array sizes. Neither the absolute times nor their ratio is generally transferable. A deployment should measure objective value and worst-case latency under equal operational budgets; warm-started CEM or MPPI are candidate designs, not automatic winners.

Out[30]:
Visualization
Bar chart comparing cold and warm start costs; warm start is lower.
Plan cost in one fixed-seed comparison after a single CEM iteration, cold-started from a zero mean versus initialized from a shifted plan computed at the previous state. The warm proposal is lower-cost in this illustrative run.
Bar chart comparing random shooting and CEM planning times.
Wall-clock planning time per call for random shooting and CEM under representative budgets. The two planners differ in how their cost scales with samples and iterations, which determines how much planning fits inside a control tick.

In the fixed-seed left panel, the warm proposal yields a lower plan cost at the same one-iteration budget. The right panel reports local wall-clock measurements for the two stated workloads; it is a hardware-specific timing check, not an algorithmic ranking.

A few practical notes on budgets. At 100 Hz, one control period is 10 ms, and sensing, state estimation, communication, and actuation consume part of it. Possible overrun policies include returning the best candidate found so far, reducing samples or horizon, or running planning at a lower rate than the low-level controller. For example, a planner at 25 Hz may supply four actions or a local policy to a 100 Hz loop; its planning horizon is a separate design choice and need not equal four. Reusing a previous action can itself be unsafe, so the deadline fallback must be designed and verified for the plant rather than treated as a universal rule.

The deadline is part of the controller model rather than a service-level preference. The plant evolves while planning, so an overrun changes the state and time at which the computed action is applied. The effect need not grow monotonically with every delay, but the implemented input schedule is no longer the one assumed by a synchronous discrete-time design. Timing validation must therefore include tail latency and the specified fallback behavior.

Failure Modes to Watch For

Sampling-based MPC has recurring failure patterns: short horizons can cause myopia; long horizons raise compute and expose more model error; small sample budgets can make plans variable across ticks; clipping actions does not enforce state constraints; and an unbounded proposal can spend evaluations on controls the plant cannot apply.