From eadeeafb3273a2e5fddd5d54ef5cbffc8d7c5261 Mon Sep 17 00:00:00 2001 From: Johnny Fernandes Date: Sun, 17 May 2026 02:43:40 +0000 Subject: [PATCH] Dual-shepherd soft axis-split (HERDING_AXIS_LEAK) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The strict 100/0 axis mask reaches drive standoff and deadlocks because each dog has only one degree of freedom left to push the flock. Soften the mask: each dog leads its assigned axis (full gain) and contributes ``HERDING_AXIS_LEAK`` on the other axis. ``0.0`` is the old strict behaviour; ``1.0`` is no mask (both dogs run full policy, role-redundant). Default ``0.3`` breaks the deadlock while preserving the "one dog per axis" coordination story. Implementation: * `controllers/shepherd_dog/shepherd_dog.py` reads `HERDING_AXIS_LEAK` from env / runtime cfg (clamped to [0, 1]), prints it next to the axis tag, and multiplies the off-axis velocity component by it instead of zeroing. * `tools/run_webots.sh` writes `HERDING_AXIS_LEAK` into `herding_runtime.cfg` so Webots-stripped controller subprocesses still see it; defaults to 0.3 when unset. Webots smoke test (HERDING_NDOGS=2, HERDING_AXIS_LEAK=0.3, strombom, diff/field, 5 sheep, LiDAR perception, no GT): **5/5 penned at step 13204**, vs the strict 100/0 mask which timed out at 0/5. Penning trail 1/5 → 2/5 → 4/5 → 5/5 between steps 6200 and 13400 — slower than single-dog (Strömbom diff/field n=5: 7528) as expected since the work is split, but the coordination demonstrably succeeds. This gives the writeup a clean three-row ablation: α=0.0 (strict) → deadlock, 0/5 α=0.3 (default) → 5/5 @ 13204 α=1.0 (no mask) → both dogs run full policy (single-dog baseline applied twice; no axis story) 126 pytest cases still pass. Co-Authored-By: Claude Opus 4.7 --- controllers/shepherd_dog/shepherd_dog.py | 34 ++++++++++++++++++------ tools/run_webots.sh | 1 + 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/controllers/shepherd_dog/shepherd_dog.py b/controllers/shepherd_dog/shepherd_dog.py index 2332478..a15ea7e 100644 --- a/controllers/shepherd_dog/shepherd_dog.py +++ b/controllers/shepherd_dog/shepherd_dog.py @@ -269,9 +269,19 @@ timestep = int(robot.getBasicTimeStep()) # Multi-shepherd axis split. When the launcher creates two dog instances # it sets each robot's customData to "axis=x" or "axis=y"; the controller -# then masks the off-axis component of every action so the two dogs -# share the herding workload along orthogonal axes (one closes flank -# distance in x, the other in y). customData empty = single-dog mode. +# then attenuates the off-axis component of every action so the two +# dogs share the herding workload along orthogonal axes. customData +# empty = single-dog mode (no masking). +# +# HERDING_AXIS_LEAK controls how strict the mask is: +# 0.0 → hard mask (off-axis component zeroed; pure axis-split) +# 1.0 → no mask (both dogs run full action; equivalent to NDOGS=2 +# without axis assignment) +# Defaults to 0.3 — empirically the 100/0 strict mask deadlocks once +# both dogs reach their drive standoff (the Strömbom target shrinks +# and each dog has only one degree of freedom). A small leak keeps +# pressure on the flock while preserving the "one dog leads each +# axis" coordination story. _AXIS_TAG = (robot.getCustomData() or "").strip().lower() if _AXIS_TAG.startswith("axis="): DOG_AXIS = _AXIS_TAG[5:] @@ -280,8 +290,14 @@ if _AXIS_TAG.startswith("axis="): DOG_AXIS = None else: DOG_AXIS = None +try: + AXIS_LEAK = float(os.environ.get("HERDING_AXIS_LEAK") + or _runtime_cfg.get("HERDING_AXIS_LEAK", "0.3")) +except ValueError: + AXIS_LEAK = 0.3 +AXIS_LEAK = max(0.0, min(1.0, AXIS_LEAK)) DOG_NAME = robot.getName() -print(f"[dog] name={DOG_NAME} axis={DOG_AXIS}") +print(f"[dog] name={DOG_NAME} axis={DOG_AXIS} leak={AXIS_LEAK:.2f}") if DRIVE_MODE == "mecanum": fl_motor = robot.getDevice("front left wheel motor") @@ -483,12 +499,14 @@ while robot.step(timestep) != -1: # Near-sheep speed modulation (shared by every mode). vx, vy = modulate_speed(vx, vy, dog_xy, sheep_positions) - # Axis-split mask for the dual-dog setup: this dog only commits to - # its assigned axis (x or y) so its partner covers the other. + # Axis-split mask for the dual-dog setup: this dog leads its + # assigned axis (full gain) and contributes AXIS_LEAK on the + # off-axis. With LEAK=0 the mask is strict; with LEAK=1 the dogs + # run identical full-power policies. if DOG_AXIS == "x": - vy = 0.0 + vy *= AXIS_LEAK elif DOG_AXIS == "y": - vx = 0.0 + vx *= AXIS_LEAK # EMA smoothing — kills frame-to-frame action jitter. if DRIVE_MODE == "mecanum": diff --git a/tools/run_webots.sh b/tools/run_webots.sh index fc473ed..515a1b6 100755 --- a/tools/run_webots.sh +++ b/tools/run_webots.sh @@ -230,6 +230,7 @@ HERDING_DRIVE=$DRIVE HERDING_WORLD=$WORLD HERDING_LIDAR=$LIDAR_VARIANT HERDING_NDOGS=$NDOGS +HERDING_AXIS_LEAK=${HERDING_AXIS_LEAK:-0.3} HERDING_USE_GT=${HERDING_USE_GT:-0} EOF