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