LevSim is the simulation environment I built so levitation controllers can be designed and tuned against the rig's real magnetic behaviour — not a hand-derived model that falls apart on contact with hardware.
- Role
- CAD · All Software Architecture + Implementation · Ansys work guided by Aditya Matam & Neha Ramachandran
- Timeline
- Nov – Dec 2025 · realism & MuJoCo overhaul Aug 2026
- Stack
- Ansys Electronics Desktop · SciKit-Learn · MuJoCo · Gymnasium
- Status
- Actively developed
The problem
Hybrid electromagnetic suspension (HEMS) is a proven low-power way to levitate, but it's brutally nonlinear — and for a student team there's no clean way to simulate one without re-deriving the magnetic flux through the whole structure (and the air around it) for every design change. At best that's tedious; at worst it's a wall that keeps teams without graduate-level magnetostatics from ever touching the control problem.
After two years on the lev sub-team I'd watched the same gap open again and again: controllers that held rock-steady in our MATLAB sim under any disturbance fell apart on the real rig under near-perfect conditions. We were missing a fundamental piece of the analytical model, and MATLAB wasn't scaling past PID anyway. The fix was to stop hand-calculating forces and root the simulation in empirical data instead — Ansys FEA (verified against load-cell measurements) → a polynomial force model → a PyBullet rigid-body sim.
System overview
Lev_pod_env.py ⇄
MuJoCo, 240 Hz control over 1.2 kHz physics) queries every step while a controller
drives the pod. Redrawn from the project's dataflow diagram.The pipeline splits in two. Offline, an Ansys magnetostatic model of a single yoke is
swept across the coil currents, gap heights and roll angles the rig actually sees; the resulting
force/torque table is fit to a polynomial (Maglev_Model.pkl) so it can be evaluated
in microseconds. At run time, the Gymnasium pod environment is the orchestrator: each step
it pulls the pod's state from MuJoCo, converts the controller's PWM output into real coil
currents with maglev_coil.py, asks the MagLev Predictor for the per-yoke force and
torque, and feeds those point loads back into MuJoCo — all wrapped in a Gymnasium interface so
any controller can drive it. It began life on PyBullet; the Aug 2026 overhaul below moved it to
MuJoCo and made the plant a great deal more honest.
Decisions & deep dives
Why FEA, not an analytical model?
I first proved FEA was trustworthy: a load-cell setup measured the force on a single small yoke, and Ansys magnetostatic results at the same gaps agreed within ~10%. That made an FEA-derived lookup a reliable replacement for the broken analytical chain.
Why a polynomial on top of FEA?
Running Ansys inside the sim loop is infeasible. A parametric sweep across the rig's working
range of heights, roll angles and coil currents gives a discrete table; fitting it in
SciKit-Learn (lev_sim/Function Fitting.ipynb) turns each step into a cheap
polynomial evaluation — and makes the pipeline plug-and-play for new yoke designs.
Why MuJoCo + Gymnasium?
The physics engine handles the rigid-body dynamics; a custom maglev_coil component
models coil current so the force model gets real currents, and Gymnasium standardizes the
interface, so the same environment runs under PID (lev_PID) or RL
(lev_PPO). It started on PyBullet; in Aug 2026 I moved it to MuJoCo — a maintained
engine with named sites for sensors and force application (replacing a collision-shape-parsing
heuristic), world-frame force injection, and an MJX path to thousands of parallel envs when RL
training scales up. The visual meshes come from the same CAD GLB as the 3D viewer on this page.
How do you reuse it across controllers?
lev_pod_env.py exposes exactly the I/O a controller sees on the real pod —
distance-sensor readings in, motor-controller PWM out — so anything written against the sim
can move straight onto the rig with no interface changes.
Making the sim honest (Aug 2026)
Months of "stable" PID and PPO results turned out to rest on three quiet defects. The coil integrator ran forward-Euler right at its stability limit, ringing in a way that made the actuator look ~40% faster than the real coil. The sensor-noise model was computed and then silently discarded. And because a noise-free sim is perfectly symmetric, two unstable axes were simply never excited — every controller that "worked" had never actually been asked to stabilize them. The overhaul rebuilt the plant around honesty: exact coil integration, real noise, decoupled control/physics rates (240 Hz control over 1.2 kHz physics), actuation latency, filtered PID derivatives, the CAD's measured mass properties, and the engine swap to MuJoCo.
The bug that hid an unstable axis
With sensor noise accidentally disabled, the sim was perfectly fore-aft symmetric — so the pitch axis was never disturbed, and nobody could see that its magnetic stiffness (~250 N·m/rad, destabilizing) outgunned the tuned controller by ~12×. Restore noise, even 10 µm of it, and pitch ran away, doubling every ~50 ms. The fix was structural: attitude gains in the same class as the height loop, and a tuner search space that stopped capping them an order of magnitude too low.
Two sign errors, hiding each other
Roll was worse: the env fed a left-handed roll angle into the right-handed FEA torque model, which flipped the magnetic roll stiffness from destabilizing to restoring — an artificial 4.3 Hz rocking mode. And the PID's roll error was inverted. Each bug masked the other: on the self-stabilizing fake plant, the tuner quietly learned to keep the wrong-signed roll gain near zero. Found by tilting the pod ±1° with no controller and checking the sign of the angular acceleration against physics — attraction across a gap must grow a tilt.
The flight envelope, from data
With the CAD's real mass properties (10.52 kg — a kilogram heavier than the sim had assumed) the force model dictates a hard envelope: beyond ~14.7 mm gap the ±10.2 A coils can't out-pull gravity, below ~7.5 mm the permanent-magnet bias captures the pod irrecoverably, so the usable release window is ~9–14 mm and hover costs ~10 A total. Numbers a controller can't tune its way around — and exactly what the hardware team needs to know before removing the guide slides.
A live cockpit for the sim
A standalone interactive demo (live_demo.py) runs the tuned closed loop in
MuJoCo with a control panel: poke the pod with forces at its corners and midpoints, kick it
with torques, start random-disturbance generators, retune the PID gains and target gap live,
and watch gap, attitude and all four coil currents on scrolling charts. The retuned controller
holds 11.85 ± 0.04 mm and returns to zero-mean roll after kicks up to
12.5 N·m.
Results & honest limits
- A repeatable simulation process that works for complex systems like maglev, grounded in FEA data and tested with both a hand-tuned PID loop and a trained PPO policy — and, since the overhaul, a plant honest enough to expose controllers that only worked on paper.
- The tuned loop releases from anywhere in the feasible window, hovers at 11.85 ± 0.04 mm, and recovers from impulse and continuous random disturbances; the residual 0.2° attitude jitter is exactly the sensor-noise floor.
- The force model is only as good as the swept range — inputs are now clamped to the FEA's validity hull (the 1/gap polynomial predicted 41 kN where the data says 400 N).
- Still open: gap-dependent coil inductance + motional back-EMF from a new Ansys flux-linkage sweep, sensor-noise calibration against the real Baumer sensors, and system-ID against rig telemetry to close the sim-to-real loop for good.
The team