Dual-shepherd soft axis-split (HERDING_AXIS_LEAK)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user