nhr_lstm_mpc: When a Robot’s Memory Becomes Part of the Controller

A learned-dynamics control stack for nonholonomic robots that combines angle-safe physics, residual sequence models, and MPC or MPPI inside one research framework.

8 to 10 min read · pozihamebaystar/nhr_lstm_mpc

A wide editorial scene shows a compact robot chassis centered between two systems. On one side, a classical optimization engine appears as interlocking gears and constraint braces. On the other side, a neural memory spine feeds hidden state into the solver through a narrow bridge. The image explains that the controller does not sit outside the model, it carries the model’s memory inside its own state.
The project’s core move is not to replace physics with a neural net. It is to wire learned memory into the control loop so the solver can reason with it.
Key Takeaways

The best way to read nhr_lstm_mpc is not as a neural-network project. It is a control project that refuses to pretend memory does not exist. The repo’s central idea is simple and sharp: if the learned dynamics need history, the controller has to inherit that history too.

The bridge layer is the repo’s signature move. It expands the control state so a learned model with memory can be optimized like part of the plant.

The controller is not blind to memory

Most learned-control systems make the same quiet assumption: the next step depends only on the current state. That works until the robot’s behavior depends on unobserved momentum, delayed effects, or any other history-sensitive detail. This repo breaks that assumption on purpose.

In the LSTM path, the solver does not receive a plain plant state. It receives an augmented state that includes the neural network’s hidden and cell state, so the optimization loop can carry memory forward instead of dropping it between timesteps. That is the real bridge in this project.

The implication is bigger than one architecture choice. The controller is no longer asking a black box for a prediction and hoping the internals stay stable. It is treating memory as a first-class control variable.

Why the physics layer uses cos and sin instead of angles

The repo’s state design is doing more work than it first appears. For heading, it uses cos θ and sin θ instead of a raw angle, which avoids the wraparound seam at 2π and keeps the learned model from seeing a fake discontinuity.

A close-up diagram shows a circular heading representation. One side has a jagged crack where a raw angle wraps from 2π back to 0. The other side traces the same rotation smoothly using two orthogonal vectors labeled visually by their geometry rather than text. The image explains why cos and sin make learned dynamics smoother and more stable.
Angle wraparound is a tiny modeling choice with oversized consequences. Encoding heading as a pair of continuous coordinates avoids a discontinuity that neural networks tend to mishandle.
def ss_disc_sincos_nhr(x, u, dt):
    # x contains position, velocity, cos(theta), sin(theta)
    # propagate the robot without ever exposing a wrapped angle seam
    x_next = dynamics_step(x, u, dt)
    x_next[2:4] = normalize_sincos(x_next[2:4])
    return x_next

Two ways to learn motion

The repository keeps two learned-dynamics strategies side by side. One is an LSTM that carries latent state forward. The other is a history-window MLP that sees a fixed slice of past inputs and observations. Both predict residuals instead of absolute positions, which makes the model less brittle and more position-invariant.

ModelMemory shapeWhat it is good atTrade-off
LSTMDynamicsModelHidden state and cell stateLearning sequence effects that do not fit in a fixed windowNeeds warmup and careful state management
HistoryMLPModelConcatenated history windowSimple, explicit feature engineering and easier reasoningCan miss long-range temporal effects
Residual predictionDelta state outputGeneralization across map positionDepends on good state normalization

That comparison matters because the repo is not worshipping one model family. It is testing two different answers to the same control problem. If the robot’s behavior is genuinely sequential, the LSTM earns its keep. If a smaller history slice is enough, the MLP path is easier to inspect.

What warmup really fixes

Recurrent models are not ready to control on the first tick. They need context to reconstruct their internal state, which is why the warmup routine matters. Before the controller starts making decisions, the model digests recent history so its hidden state stops being random noise.

state = lstm_model.warmup(history)
action = controller.solve(state)
next_state = lstm_model.step(state, action)

That is a small piece of plumbing with big consequences. Without it, the controller would optimize against a model that has not yet remembered enough to be useful.

How the learned model enters MPC

The bridge into control happens in the wrapper layer and the Acados integration. The learned network has to be translated into a form the solver can consume, differentiate through, and stabilize. That is where tools like l4casadi matter, because they turn a PyTorch model into something a symbolic optimizer can actually use.

This is also where the repo gets practical. Learned Jacobians can be ugly, so the code adds regularization to keep the optimizer from getting lost in its own math. The result is not just a neural controller. It is a controller that behaves like it expects numerical friction.

Sampling against solving

The repo offers two control philosophies on top of the same learned dynamics. MPC searches for a locally optimal solution under constraints. MPPI samples many candidate trajectories and ranks them by cost. Both are useful, but they fail in different ways.

ControllerHow it thinksStrengthWeakness
MPCOptimizes a constrained horizonDisciplined and structuredSensitive to model smoothness and solver conditioning
MPPISamples and reweights trajectoriesMore forgiving with messy learned modelsCan be compute-heavy
Shared learned modelSame dynamics interfaceOne model, two controller stylesRequires careful wrapper design

That side-by-side setup is the article’s real comparison story. The repo is not choosing a winner. It is showing that the same memory-aware dynamics can support both a solver-driven controller and a sampling-driven one.

Why this repo feels like a research template

The codebase reads like a reusable stack, not a one-off demo. Data generation, model training, warmup, wrappers, MPC, MPPI, and simulation are separated into different files, which makes the research logic legible. That matters because learned control only becomes useful when the pipeline is inspectable.

The deeper value here is architectural discipline. The repo shows how to combine system identification, residual dynamics, state encoding, and solver-based control without collapsing everything into one opaque script.