Skip to content

Ivan/feat/motion - #3572

Draft
leshy wants to merge 15 commits into
mainfrom
ivan/feat/motion
Draft

Ivan/feat/motion#3572
leshy wants to merge 15 commits into
mainfrom
ivan/feat/motion

Conversation

@leshy

@leshy leshy commented Aug 20, 2026

Copy link
Copy Markdown
Member

No description provided.

leshy added 8 commits August 20, 2026 15:47
The conformance test reads a wrapper's ports off `In`/`Out` type hints only, so
a port that both subscribes and publishes reads as absent and the manifest that
declares it fails: `motion_planner.tf is not a port of ...MotionPlannerNative`.

tf is that port. It rides in the manifest's `inputs` table because the bake
graph has no io kind, and the wrapper types it `IO[TFMessage]` because the rust
`#[tf]` does both. Counting `IO` is what makes the two agree.
Ported from ivan/feat/trajectory_ctrl onto current main. What comes over is the
shipped code and the proof it works: the SE(2) local planner (the evolved rust
crate) with its referee -- worlds, gold oracle, judge, invariance tests, all
pure numpy -- the follower's per-track laws in python and bit-exact rust, the
shared domain the two agree on, and the adapter that makes both dimos modules.

What stays behind is everything that trains, searches or simulates: the matched
Go2 MuJoCo env, the closed-loop executor referee that drives it, and both evo
labs. README's "What is not here" says where to go and what to re-measure
there. That cut leaves three loose ends, tied off rather than papered over:

- the plant's command slew was read out of simulation/walk.py, which is a sim
  constant the shipped law is held to. It moves to embodiment.py, next to the
  body table, and test_hinted.py holds python and rust to it there.
- adapter/planner.py borrowed the referee's path_clearance; the adapter already
  has the same function, in array form. One copy now.
- the "ml" registry entry named a candidate that does not exist here.

Against main rather than against the branch: dimos.memory2 is dimos.memory,
GO2ZenohConfig takes mid360_mount presets instead of a raw rpy triple, and this
Plot has no axis labels. mypy and the fast suite are green.
The motion host is four modules, not two: the follower's twist has to reach the
robot past teleop, and both baked modules wait on a tf leg that used to come
over wifi from the laptop.

- CmdVelMux (movement_manager/): teleop preempts nav on cmd_vel and a watchdog
  zeros it when nav dies, python twin and rust native. Pairs with
  MovementManager, which keeps the click relay; both see tele_cmd_vel.
- Go2Tf (go2/tf/): GO2Zenoh's mount tree and odom edge in rust, so base_link <-
  mid360_link no longer depends on the laptop being up. tf_parity.json is
  derived from the python and asserted by both sides.

go2_tf came over reading a raw rpy triple, because that is what GO2ZenohConfig
took on the branch it was written on. Main has mount PRESETS instead, so the
parity test pins the default to ATHENS rather than to GO2ZenohConfig's default
(SF): the mount is per-rig, and what has to agree is the two halves of one rig.

Both Cargo.locks are regenerated against main's dimos-module, whose dependency
set has moved on from the branch these crates were written against.
Three blueprints, added beside the existing ones rather than replacing them --
nav, nav-baked and htc are untouched.

- go2-zenoh-motion: MLS keeps the global graph but its path moves to
  planner_path and becomes a carrot source; the local planner replans ~5 m
  along it over the raycaster's local map and the follower tracks that plan on
  the hinted track, with the clearance array recomputed from the same map.
- go2-zenoh-motion-blind: the same graph, hint withheld. The law recovers the
  required precision from the path's own stamps, so an A/B isolates the law.
- go2-zenoh-motion-local: planner, follower and mux lifted onto the robot as
  one baked host; what stays is expensive (raycaster), global (MLS) or attached
  to the operator (rerun, clicks, teleop).

SPEEDS ARE SIM-CALIBRATED, and the comment above the stack says so: both
envelopes were measured against the freewalk blob in the matched MuJoCo env,
not against the gait the robot runs. Dial the ceiling down here before trusting
either at speed.

One thing the branch had that this does not: region_radius_m=5.0 on the
raycaster. Its emitted window is sized to a percentile of the last ten sweeps,
so it breathes -- 68 % of the voxel churn the local planner saw was the window
moving, not the world -- and the plan flips with it. The fix threads a fixed
radius through the raycaster's rust, which is not planner-or-follower, so it
stays on ivan/feat/trajectory_ctrl until someone wants it on its own terms. The
comment in _motion_raycaster carries the whole story.
… in CI

Two halves of the same rule, both already the repo's, neither of which came
over with the port: a test that needs a maturin extension says so with
`importorskip`, and CI builds the extension so the test actually runs.

Without the first, a checkout that has not run `maturin develop` collects a
`ModuleNotFoundError` out of test_grid_invariance and test_rust_parity instead
of a skip -- which is how the whole motion package reads as broken every time
`uv sync` prunes the two editable installs. `test_commitment` splits finer: the
rust candidate is skipped, the python port spec runs either way.

Without the second, the skip is the last word and nothing upstream ever
compares python to rust. The PyO3 job now builds both motion crates next to
ray_tracing and mls_planner, and runs the two tests whose whole point is the
extension.
`dimos bake --target aarch64-unknown-linux-gnu.2.31` needs three things a plain
nixpkgs shell does not have: a rustc carrying the aarch64 std (nixpkgs' does
not, and without it every cross build dies with "can't find crate for core"),
zig, and cargo-zigbuild -- which is what makes the `.2.31` glibc pin possible
at all. deployment_plan.md already promised all three; now the flake delivers
them, via rust-overlay pinned to 1.97.1 so the toolchain does not drift under
contributors when the overlay moves.

Two deployment docs were describing a topology that is no longer the one in the
blueprint, and both would have cost a robot session:

- the quick start told the laptop to run `go2-zenoh-motion` against a live
  baked host, which is two planners, two followers and two muxes on the same
  topics. It is `-local` that leaves those three to the robot.
- the unit's comment still explained a listen port on 7448 with the laptop
  dialing both. The unit's own environment has no DIMOS_ZENOH_LISTEN: go2web is
  the router, the host is a client of it, and it listens on nothing.
Two bugs the first robot run found, both mine, both in the same three edges.

`go2-zenoh-motion-local` left GO2Zenoh publishing tf from the laptop while
go2_tf publishes it from the robot. Two publishers of one static tree is not
redundancy: base_link jumps between them at their combined rate. The robot is
the one that should win -- that is the whole reason go2_tf is baked into the
host -- so the laptop's tf is remapped onto a dead channel.

And they disagreed about where the lidar is. go2_tf defaults to the ATHENS
mount (-60, 0, -90); main's blueprint constant is "SF" (0, 60, 0). Whichever
tick arrived last moved the body. The motion stacks now name their rig once, in
MOTION_MID360_MOUNT, with the constraint written down: it has to equal go2_tf's
default on any robot that runs the baked host. nav/htc keep MID360_MOUNT.
The port carried a field-debugging kit that nothing on the runtime path
imports: `adapter/diagnose.py` and its tests, `precision.py`, `replay.py`, and
`planner/referee/measure_margin.py` (which bakes a constant that is already
baked, and reaches into diagnose to do it). Good tools, and a reviewer of a
planner has no reason to read 2.8k lines of them. They stay on
`ivan/feat/trajectory_ctrl` and can come back as their own change.

Same for four writeups nothing cites: a dated diagnosis, a replay-provenance
proposal, the 3D revision sketch, and the envelope sweep's result dump.
`native_module_recipe.md` goes too -- how to write a native module is a dimos
topic, not a motion one, and `dimos/cli/bake/` is the code it describes.

`planner/revision.md` STAYS. Seventeen comments in shipped code cite it for why
a number is what it is -- the body box, the lattice pitch, the commitment
margin -- so deleting it would leave those dangling, which is the opposite of
trimming. `diagnostics.py` and `viz.py` stay too: the follower, the planner and
the blueprint import them.

And .gitattributes now marks what is machine-generated. The five Cargo.locks
and the evolved planner crate are 15.1k of the diff that nobody reads line by
line; labelled, a review renders the ~19k that was actually written.
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.57980% with 1026 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/navigation/motion/scenarios.py 44.68% 272 Missing and 4 partials ⚠️
dimos/navigation/motion/geometry.py 33.56% 194 Missing ⚠️
...imos/navigation/motion/control/test_rust_parity.py 6.03% 187 Missing ⚠️
dimos/navigation/motion/control/laws/blind.py 22.85% 81 Missing ⚠️
dimos/navigation/motion/adapter/planner.py 63.68% 70 Missing and 3 partials ⚠️
dimos/navigation/motion/adapter/follower.py 64.64% 60 Missing and 4 partials ⚠️
dimos/navigation/motion/control/laws/hinted.py 67.44% 46 Missing and 10 partials ⚠️
dimos/navigation/movement_manager/cmd_vel_mux.py 70.73% 24 Missing ⚠️
dimos/navigation/motion/planner/planners/target.py 67.74% 19 Missing and 1 partial ⚠️
dimos/navigation/motion/control/controller.py 77.96% 13 Missing ⚠️
... and 10 more
@@            Coverage Diff             @@
##             main    #3572      +/-   ##
==========================================
- Coverage   75.81%   75.51%   -0.31%     
==========================================
  Files        1263     1296      +33     
  Lines      121714   125098    +3384     
  Branches    10834    11203     +369     
==========================================
+ Hits        92280    94467    +2187     
- Misses      26369    27534    +1165     
- Partials     3065     3097      +32     
Flag Coverage Δ
OS-ubuntu-24.04-arm 70.67% <66.57%> (-0.17%) ⬇️
OS-ubuntu-latest 72.36% <66.57%> (-0.22%) ⬇️
Py-3.10 ?
Py-3.11 72.36% <66.57%> (-0.22%) ⬇️
Py-3.12 72.36% <66.57%> (-0.22%) ⬇️
Py-3.13 ?
Py-3.14 72.36% <66.57%> (-0.22%) ⬇️
Py-3.14t 72.36% <66.57%> (-0.22%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dimos/cli/bake/test_discovery.py 100.00% <100.00%> (ø)
dimos/navigation/motion/adapter/follower_native.py 100.00% <100.00%> (ø)
dimos/navigation/motion/adapter/planner_native.py 100.00% <100.00%> (ø)
dimos/navigation/motion/adapter/test_follower.py 100.00% <100.00%> (ø)
dimos/navigation/motion/adapter/test_viz.py 100.00% <100.00%> (ø)
dimos/navigation/motion/control/test_controller.py 100.00% <100.00%> (ø)
dimos/navigation/motion/control/test_profile.py 100.00% <100.00%> (ø)
dimos/navigation/motion/obstacles.py 100.00% <100.00%> (ø)
dimos/navigation/motion/test_embodiment.py 100.00% <100.00%> (ø)
dimos/navigation/motion/test_obstacles.py 100.00% <100.00%> (ø)
... and 25 more

... and 13 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The port brought `data/.lfs/ml-trajectory-research.tar.gz` along because the
adapter's recording tools read it. Those tools are gone, nothing on this branch
calls `get_data` any more -- and the archive carries `freewalk_mcf.bin` and
`v11_final.bin`, walking policies lifted out of the robot's own binaries, which
are not ours to publish. The pointer is out of every commit on this branch, not
just deleted in a later one, so pushing uploads no LFS object.

The gait calibration constants stay: they are measurements OF that policy, not
the policy, and the comment now says where the blob is not.
@leshy
leshy force-pushed the ivan/feat/motion branch from ab9a40b to 902937b Compare August 20, 2026 18:36
leshy added 2 commits August 20, 2026 21:55
What is left is what someone runs on a robot. The planner's referee -- the
worlds, the SE(2) gold oracle, the judge, its six invariance tests and the
`--score` runner over them -- is a benchmark, not a module, and it belongs with
the sim and the labs on `ivan/feat/trajectory_ctrl`.

`gold.py` was not purely judge-side: the shipped candidate imported three
helpers out of it (`states_of`, `densify_states`, `pose_stamped`). Those move to
`planners/base.py`, where the interface they serve already lives, and the slow
reference oracle leaves with the rest.

What still holds the planner honest here: the crate's own five behavioural
invariants (open world routes, sealed box refuses, thin wall not hopped,
deterministic across calls, no cross-call memoization), the in-src property
tests beside them, and `adapter/test_planner.py` for the module wiring. All of
it runs in CI already -- the rust job discovers every crate with a Cargo.lock
and runs `cargo test --workspace --all-features`. The control side is untouched:
240-case python-vs-rust parity, the profile dialect, the envelope.
Deploying the host on a Go2 found three things wrong with the deployment
artifacts, all fixed on the robot during that session and only now written back
here, which is the wrong order and exactly the config skew the plan warns about.

- The blob must be ONE LINE. `read_launch_config` reads a single line, so the
  repo's `pretty-format-json` hook -- which reformatted this file the moment it
  was committed -- crash-looped the host with "EOF while parsing an object at
  line 1 column 1". The file is now excluded from that hook, with the reason.
- The unit's DIMOS_ZENOH_CONNECT / _MODE / _SCOUTING did nothing. The rust
  module reads only DIMOS_TRANSPORT from the environment; session settings come
  from a `session` block on the launch line, and without one the host opens
  zenoh's defaults -- a peer with multicast scouting, which a router does not
  forward to. The block is in the config now and the dead env is gone.
- The per-module `topics` maps were restating the wiring the bake bakes into the
  binary (stdin topics are overrides only), so they are dropped, and the blob
  gained `--emit-config`'s `graph` stamp -- which makes a config baked for
  another graph a startup error instead of a host wired to keys nobody uses.

2418 bytes over 124 lines becomes 1492 on one, and the only value in it that is
not a class default is still `max_speed` 0.7.
@leshy
leshy force-pushed the ivan/feat/motion branch from a14857d to f310960 Compare August 20, 2026 19:01
leshy added 4 commits August 20, 2026 22:03
The PyO3 job still listed `planner/referee/test_grid_invariance.py`, which left
with the benchmark two commits ago -- pytest would have failed the job on a path
that no longer exists. The planner's rust invariants are covered anyway: the
rust job discovers every crate with a Cargo.lock and runs
`cargo test --workspace --all-features`, which builds and runs
`planner/rust/tests/invariants.rs`.
… history

Two things the first robot run showed, both real.

THE BOXES ARE WIDER THAN THE ROBOT because they are the measured leg swing, not
the 0.31 m trunk: a forward edge is tested against 0.416 m of width, a rotation
against the 0.593 m all-gait union, each plus the 0.05 m clearance floor. So a
straight run wanted a 0.516 m gap and a turn-in-place 0.693 m, which refuses
passages the robot walks through.

`body_dilate_m` grows every planning box by that much PER SIDE, negative to
shrink, and reaches the union and the per-heading rows alike -- python,
`Embodiment.dilated`, and rust, `emb::dilated`, same formula. The follower takes
the same value: its room hint has to price the body the plan was made for, or
the governor creeps through gaps the plan called fine. At the -0.03 the go2
stacks now run, a straight gap of 0.456 m admits a route.

THE PLAN WAS PUBLISHED AT odom z = 0, which on a lidar-odometry stack is
wherever the LIO frame started -- about a body height above the floor. Every
drawn body box floated over the robot. Both modules already track the ground
plane for the obstacle model; the path carries it now. Only a viewer reads the
z, every consumer is planar.

And the viewer drew the all-gait UNION at every waypoint -- the turn-in-place
shape, ~0.18 m wider than what a forward edge has to fit -- which made every
corridor look impassable. It draws the straight-drift box at the dilation in
force, which is 0.759 x 0.356 against the old 0.883 x 0.593.

Riding along, because it is the same files: the comments stop narrating how the
code was arrived at. Experiment ids, lab branch names, score deltas, dated
recordings and ablation tallies are gone from the laws, the domain and both
crates -- what a number IS and why it has to be that stays, 286 lines of how it
was found does not. `planner/revision.md` goes with them; every comment that
cited it now says the thing itself.
They carried their own RayTracingVoxelMap config -- emit_every 10 against the
nav stack's 1, global_emit_every 100 against 50 -- chosen when the local planner
was replanning off every emitted window. It buys the planner nothing here and it
costs the operator the picture: at one frame in ten the emitted cylinder sits
still between updates and steps in visible jumps, and at one global map in a
hundred there is nothing around it, so the local region reads as a hard circle
rather than a moving frontier.

The stacks take `ray_tracing_config` now, same as nav, and the raycaster is
back to being a module this branch does not touch at all.

The window still BREATHES -- it is sized to a percentile of what the last sweeps
saw, so its radius tracks the room rather than the robot. `region_radius_m`,
which pins it, is ours and stays on ivan/feat/trajectory_ctrl with the rest of
the raycaster work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant