0. System identification
Pinning the dynamics parameters once. Outputs sysid_params_<rig>.json,
which pendulum_env.py reads to build the sim. Re-run any time the rig
changes mechanically (new bearings, rebuilt arm, changed microstepping,
swapped motor).
The full protocol
Section titled “The full protocol”End-to-end protocol for measuring the physical parameters of your
pendulum and writing a sysid_params_<rig>.json that the RL pipeline consumes.
Sysid runs against the same LowLevelServer.ino firmware the policy
deploys against — no firmware swap, no risk of measurement-vs-deployment
parameter drift.
The wizard is split into two phases:
- collect — operator-driven recording on the rig. Writes raw
.npzlogs + ametadata.jsonto a timestamped directory. - fit — pure post-processing. Reads a directory of recordings, derives
parameters, writes
sysid_params_<rig>.json, and renders sim-vs-real validation plots. No device required; re-runnable as often as you like.
Re-fitting from saved logs is the fast iteration loop when improving the math. Re-collecting on the rig is only needed when changing sensor read rates, firmware logic, or adding new recording steps.
Prerequisites
Section titled “Prerequisites”- The
policy/environment —uvcreates it on first use, socd policyand prefix commands withuv run. Nothing to install by hand. LowLevelServer.inoflashed onto the Arduino Nano. Auto-magnet-detect at boot; the firmware halts insetup()until it sees the pendulum magnet.- A small rigid block (cardboard, foam, wood) to slide under the pendulum for the free-swing release.
- Pendulum geometry (mass, COM, inertia) comes from
model/model.urdf— no operator-measured inputs are needed. If you’ve rebuilt or modified the pendulum, update Onshape → export → URDF first.
Recommended path: full pipeline
Section titled “Recommended path: full pipeline”cd policypython sysid_wizard.py --out-json sysid_params_<rig>.jsonWalks you through, in order:
- Tare hanging position. Pendulum hangs motionless; we sample for 3 s and use the median as “physical hanging = 0” in the saved coordinate frame. Fixes the firmware-encoder-accumulator drift.
- Free-swing × 3. Hold the motor arm firmly, slide the block under the pendulum to lift it (any reasonable angle — the sensor records the actual release angle), then slide the block out perpendicular to the swing plane. Recording is 10 s by default. Repeat three times.
- Motor ±90° sanity sweep. Engages the motor and drives it through
0 → +90° → 0 → −90° → 0using accel commands. Watch the rig: the arm should reach roughly perpendicular at the peaks. If it visibly under-shoots, the stepper is skipping steps and the firmware position-counter no longer matches the actual mechanical position — a deployment-blocker (lowerMOTOR_ACCELERATIONor bump the DRV8825 Vref). - Fit + plots. Aggregates the three free-swing recordings, derives
friction parameters using URDF-defined geometry, writes
sysid_params_<rig>.json, generatesfreeswing_compare.png(sim trace overlaid on the real recording using the just-derived params) andmotor_sweep.png(motor target vs. actual during the sanity sweep).
Pendulum mass, COM distance, and inertia about the COM are not part
of this pipeline anymore — they’re geometric properties of the part and
live in model/model.urdf, parsed by pendulum_geometry.py. The free-
swing fit cross-checks the URDF inertia against the measured period
(a >10% mismatch flags a likely-stale URDF).
The whole thing takes ~10–15 minutes. Output ends up in
sysid_runs/<timestamp>/ next to the wizard.
Re-fit from existing recordings (no rig needed)
Section titled “Re-fit from existing recordings (no rig needed)”python sysid_wizard.py fit --in-dir sysid_runs/2026-05-20_090000 \ --out-json sysid_params_<rig>.jsonUse this when iterating on sysid_core.derive_pendulum_friction or
investigating the fits — the rig stays unused, and the operator doesn’t
need to be present.
Standalone motor sanity sweep
Section titled “Standalone motor sanity sweep”python sysid_wizard.py validate-motorJust the motor ±90° sweep + plot. Useful after any change to the firmware’s motor configuration (current limit, microstepping, acceleration ceiling) to confirm step counting still tracks reality.
What the wizard writes
Section titled “What the wizard writes”sysid_runs/2026-05-20_HHMMSS/├── metadata.json # port, firmware, timestamp, ...├── tare.npz # pendulum-at-rest recording → hanging zero├── free_run_1.npz # raw free-swing recordings├── free_run_2.npz├── free_run_3.npz├── motor_sweep.npz # motor target/actual during sanity sweep├── freeswing_compare.png # sim-vs-real overlay for validation└── motor_sweep.png # motor target vs. firmware-reported positionThe fitted parameters go wherever --out-json says. Name the rig: the flag is
required precisely so one rig’s friction cannot silently end up describing
another. pendulum_env.py falls back to sysid_params_rig1.json when no
--params-path is given.
What the math does
Section titled “What the math does”fit_free_swing(t, θ) (in sysid_core.py) finds peaks, computes the
period and damped-oscillator decay constants. It also computes the
small-amplitude period T₀ via the elliptic-integral correction
T(θ_max) = T₀ · (2/π) · K(sin²(θ_max/2)) — necessary because the
recordings are at finite amplitude (40–90°) but the inertia formula
I = m·g·d·T²/(4π²) is the small-amplitude one.
derive_pendulum_friction(fit) reads the pendulum geometry (mass, COM,
I_com) from pendulum_geometry (which parses model/model.urdf) and
combines it with the fit to produce viscous and Coulomb friction. It
also reports both inertia_predicted_kg_m2 (CAD: m·d² + I_com) and
inertia_measured_kg_m2 (from T₀: m·g·d/ω²), and validate_free_swing
warns if they disagree by more than 10% — a sign the URDF is stale or
the recording is contaminated.
Viscous friction uses the CAD-predicted pivot inertia: b = 2·α·I_pred, where α = b/(2I) is the directly-measured envelope decay. Coulomb friction comes from the Coulomb-step-per-half-cycle measured in the envelope fit.
Recovery and debugging
Section titled “Recovery and debugging”- Free-swing fit fails (too few extrema, no decay, etc.): the recording is bad — pendulum didn’t move enough, or you released into a vibration source. Just retry that run when prompted.
- Hanging zero drifted between recordings: the firmware’s accumulator
is monotonic, so this happens if the operator manually wound the
pendulum past ±180° between runs. Reset the firmware (USB disconnect
- reconnect) and re-collect.
- Re-fitting gives different numbers: expected — small changes in
the fitter compound. The
freeswing_compare.pngplot is the ground-truth: if real and sim envelopes overlap closely, the fit is correct regardless of the absolute values.