Rename multi-segment functions to two-concept names; polish docstrings
Naming pass: rename functions whose third+ segment is redundant or implementation-detail, sticking to the codebase's preferred ``noun_verb`` / ``verb_noun`` two-concept idiom. Renames are atomic across definitions, callers, and tests. is_penned_position → is_penned modulate_speed_near_sheep → modulate_speed mecanum_kinematics_step → mecanum_step policy_forward_mean → forward_mean Two-concept patterns like ``velocity_to_wheels`` / ``detections_from_scan`` / ``make_strombom_predictor`` are left alone — they're idiomatic converters / factories that read as a single concept, and the longer form aids grep-ability. Docstring polish: * ``herding/config.py`` header drops the "previously lived as a module-level literal" historical framing — we ship as a single thing, so the refactor anecdote no longer earns its keep. The usage examples now mention both ``HERDING_WEBOTS`` and ``HERDING_MEC_WEBOTS`` presets. 126 pytest cases still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,7 @@ from herding.world.diffdrive import heading_speed_to_wheels
|
|||||||
from herding.world.flocking_sim import MAX_SPEED, compute_heading_speed
|
from herding.world.flocking_sim import MAX_SPEED, compute_heading_speed
|
||||||
from herding.world.geometry import (
|
from herding.world.geometry import (
|
||||||
SHEEP_MAX_WHEEL_OMEGA,
|
SHEEP_MAX_WHEEL_OMEGA,
|
||||||
is_penned_position,
|
is_penned,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ while robot.step(timestep) != -1:
|
|||||||
pos = gps.getValues()
|
pos = gps.getValues()
|
||||||
x, y = pos[0], pos[1]
|
x, y = pos[0], pos[1]
|
||||||
|
|
||||||
if not penned and is_penned_position(x, y):
|
if not penned and is_penned(x, y):
|
||||||
penned = True
|
penned = True
|
||||||
paint_pink()
|
paint_pink()
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ import numpy as np
|
|||||||
from controller import Robot
|
from controller import Robot
|
||||||
|
|
||||||
from herding.control.active_scan import ActiveScanTeacher
|
from herding.control.active_scan import ActiveScanTeacher
|
||||||
from herding.control.modulation import modulate_speed_near_sheep
|
from herding.control.modulation import modulate_speed
|
||||||
from herding.control.sequential import compute_action as sequential_action
|
from herding.control.sequential import compute_action as sequential_action
|
||||||
from herding.control.strombom import compute_action as strombom_action
|
from herding.control.strombom import compute_action as strombom_action
|
||||||
from herding.control.universal import compute_action as universal_action
|
from herding.control.universal import compute_action as universal_action
|
||||||
@@ -95,7 +95,7 @@ from herding.perception.sheep_tracker import SheepTracker
|
|||||||
from herding.world.diffdrive import velocity_to_mecanum_wheels, velocity_to_wheels
|
from herding.world.diffdrive import velocity_to_mecanum_wheels, velocity_to_wheels
|
||||||
from herding.world.geometry import (
|
from herding.world.geometry import (
|
||||||
DOG_SOUTH_LIMIT,
|
DOG_SOUTH_LIMIT,
|
||||||
PEN_ENTRY, is_penned_position,
|
PEN_ENTRY, is_penned,
|
||||||
)
|
)
|
||||||
from herding.config import HERDING_WEBOTS, RobotConfig
|
from herding.config import HERDING_WEBOTS, RobotConfig
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ if MODE == "calibrate":
|
|||||||
_pos0 = gps.getValues(); _x0, _y0 = _pos0[0], _pos0[1]
|
_pos0 = gps.getValues(); _x0, _y0 = _pos0[0], _pos0[1]
|
||||||
_n_calib = compass.getValues(); _h0 = math.atan2(_n_calib[0], _n_calib[1])
|
_n_calib = compass.getValues(); _h0 = math.atan2(_n_calib[0], _n_calib[1])
|
||||||
# Gym-predicted displacement using shared kinematics.
|
# Gym-predicted displacement using shared kinematics.
|
||||||
from herding.world.diffdrive import velocity_to_mecanum_wheels, mecanum_kinematics_step
|
from herding.world.diffdrive import velocity_to_mecanum_wheels, mecanum_step
|
||||||
from herding.world.geometry import WEBOTS_DT as _DT
|
from herding.world.geometry import WEBOTS_DT as _DT
|
||||||
_xg, _yg, _hg = _x0, _y0, _h0
|
_xg, _yg, _hg = _x0, _y0, _h0
|
||||||
for _ in range(_calib_n):
|
for _ in range(_calib_n):
|
||||||
@@ -343,7 +343,7 @@ if MODE == "calibrate":
|
|||||||
max_wheel_omega=DOG_MAX_WHEEL_OMEGA, k_turn=4.0,
|
max_wheel_omega=DOG_MAX_WHEEL_OMEGA, k_turn=4.0,
|
||||||
wheel_base=DOG_WHEEL_BASE,
|
wheel_base=DOG_WHEEL_BASE,
|
||||||
)
|
)
|
||||||
_xg, _yg, _hg = mecanum_kinematics_step(
|
_xg, _yg, _hg = mecanum_step(
|
||||||
_xg, _yg, _hg, _wfl, _wfr, _wrl, _wrr,
|
_xg, _yg, _hg, _wfl, _wfr, _wrl, _wrr,
|
||||||
DOG_WHEEL_RADIUS, DOG_WHEEL_BASE_X / 2, DOG_WHEEL_BASE_Y / 2, _DT,
|
DOG_WHEEL_RADIUS, DOG_WHEEL_BASE_X / 2, DOG_WHEEL_BASE_Y / 2, _DT,
|
||||||
)
|
)
|
||||||
@@ -414,7 +414,7 @@ while robot.step(timestep) != -1:
|
|||||||
if USE_GT_PERCEPTION and _gt_sheep:
|
if USE_GT_PERCEPTION and _gt_sheep:
|
||||||
# Bypass tracker: feed GT emitter positions directly to policy/teacher.
|
# Bypass tracker: feed GT emitter positions directly to policy/teacher.
|
||||||
sheep_positions = {k: v for k, v in _gt_sheep.items()
|
sheep_positions = {k: v for k, v in _gt_sheep.items()
|
||||||
if not is_penned_position(v[0], v[1])}
|
if not is_penned(v[0], v[1])}
|
||||||
tracker.update(detections) # still advance tracker for diagnostics
|
tracker.update(detections) # still advance tracker for diagnostics
|
||||||
else:
|
else:
|
||||||
sheep_positions = tracker.update(detections)
|
sheep_positions = tracker.update(detections)
|
||||||
@@ -453,7 +453,7 @@ while robot.step(timestep) != -1:
|
|||||||
vx, vy, _mode_str = result
|
vx, vy, _mode_str = result
|
||||||
|
|
||||||
# Near-sheep speed modulation (shared by every mode).
|
# Near-sheep speed modulation (shared by every mode).
|
||||||
vx, vy = modulate_speed_near_sheep(vx, vy, dog_xy, sheep_positions)
|
vx, vy = modulate_speed(vx, vy, dog_xy, sheep_positions)
|
||||||
|
|
||||||
# EMA smoothing — kills frame-to-frame action jitter.
|
# EMA smoothing — kills frame-to-frame action jitter.
|
||||||
if DRIVE_MODE == "mecanum":
|
if DRIVE_MODE == "mecanum":
|
||||||
@@ -489,7 +489,7 @@ while robot.step(timestep) != -1:
|
|||||||
# fire during the first few steps while the receiver fills.
|
# fire during the first few steps while the receiver fills.
|
||||||
if _gt_sheep and not _run_done:
|
if _gt_sheep and not _run_done:
|
||||||
gt_active = sum(1 for x, y in _gt_sheep.values()
|
gt_active = sum(1 for x, y in _gt_sheep.values()
|
||||||
if not is_penned_position(x, y))
|
if not is_penned(x, y))
|
||||||
if gt_active == 0:
|
if gt_active == 0:
|
||||||
os.makedirs(os.path.dirname(RUN_DONE_FILE), exist_ok=True)
|
os.makedirs(os.path.dirname(RUN_DONE_FILE), exist_ok=True)
|
||||||
open(RUN_DONE_FILE, "w").close()
|
open(RUN_DONE_FILE, "w").close()
|
||||||
@@ -499,7 +499,7 @@ while robot.step(timestep) != -1:
|
|||||||
|
|
||||||
if step_count % 200 == 0:
|
if step_count % 200 == 0:
|
||||||
gt_penned = sum(1 for x, y in _gt_sheep.values()
|
gt_penned = sum(1 for x, y in _gt_sheep.values()
|
||||||
if is_penned_position(x, y))
|
if is_penned(x, y))
|
||||||
gt_total = len(_gt_sheep)
|
gt_total = len(_gt_sheep)
|
||||||
common = (f"[dog mode={MODE} drive={DRIVE_MODE}] step={step_count} "
|
common = (f"[dog mode={MODE} drive={DRIVE_MODE}] step={step_count} "
|
||||||
f"GT_penned={gt_penned}/{gt_total} "
|
f"GT_penned={gt_penned}/{gt_total} "
|
||||||
|
|||||||
+16
-18
@@ -1,32 +1,30 @@
|
|||||||
"""Central configuration dataclasses for the herding simulation.
|
"""Central configuration dataclasses for the herding simulation.
|
||||||
|
|
||||||
Every tunable constant that previously lived as a module-level literal in
|
Every tunable parameter lives here as a frozen dataclass field — LiDAR
|
||||||
perception/lidar_sim.py, perception/lidar_perception.py,
|
spec, cluster detection thresholds, tracker gates, robot kinematics,
|
||||||
perception/sheep_tracker.py, world/geometry.py, or training/herding_env.py
|
and domain-randomisation knobs — composed into :class:`HerdingConfig`.
|
||||||
is now represented here as a field with its original default value.
|
|
||||||
|
|
||||||
Usage — use the module defaults unchanged::
|
Usage — accept the defaults::
|
||||||
|
|
||||||
env = HerdingEnv() # same behaviour as before
|
env = HerdingEnv()
|
||||||
|
|
||||||
Override a subset of parameters::
|
Override a subset::
|
||||||
|
|
||||||
from herding.config import HerdingConfig, TrackerConfig
|
|
||||||
cfg = HerdingConfig(tracker=TrackerConfig(forget_steps=60))
|
cfg = HerdingConfig(tracker=TrackerConfig(forget_steps=60))
|
||||||
env = HerdingEnv(herding_cfg=cfg)
|
env = HerdingEnv(herding_cfg=cfg)
|
||||||
|
|
||||||
Use a named preset for Webots-matched training::
|
Use a named preset::
|
||||||
|
|
||||||
from herding.config import HERDING_WEBOTS
|
env = HerdingEnv(herding_cfg=HERDING_WEBOTS) # 140° FOV
|
||||||
env = HerdingEnv(herding_cfg=HERDING_WEBOTS)
|
env = HerdingEnv(herding_cfg=HERDING_MEC_WEBOTS) # + mecanum slip
|
||||||
|
|
||||||
Design notes
|
Design notes
|
||||||
------------
|
------------
|
||||||
* All dataclasses are frozen — instances are immutable after construction.
|
* All dataclasses are frozen so instances are immutable after construction.
|
||||||
* This module must not import from other ``herding.*`` packages to avoid
|
* This module must not import from other ``herding.*`` packages —
|
||||||
import cycles. Field-geometry constants (pen coordinates, field size)
|
field-geometry constants live in ``herding.world.geometry`` because
|
||||||
stay in ``herding.world.geometry`` because they depend on the world
|
they depend on the world variant selected at runtime via
|
||||||
variant selected at runtime via ``HERDING_WORLD``.
|
``HERDING_WORLD``, which would create an import cycle here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -257,7 +255,7 @@ class RobotConfig:
|
|||||||
|
|
||||||
``1.0`` (default) = perfect mecanum kinematics. ``0.4`` matches the
|
``1.0`` (default) = perfect mecanum kinematics. ``0.4`` matches the
|
||||||
Webots roller-hinge mecanum proto calibration (62% slip on strafe,
|
Webots roller-hinge mecanum proto calibration (62% slip on strafe,
|
||||||
11% on forward). Used by ``mecanum_kinematics_step`` only — has no
|
11% on forward). Used by ``mecanum_step`` only — has no
|
||||||
effect on differential drive.
|
effect on differential drive.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -266,7 +264,7 @@ class RobotConfig:
|
|||||||
|
|
||||||
``0.0`` (default) = no bleed. ``-0.28`` matches the Webots proto's
|
``0.0`` (default) = no bleed. ``-0.28`` matches the Webots proto's
|
||||||
consistent backward push under strafe commands. Used by
|
consistent backward push under strafe commands. Used by
|
||||||
``mecanum_kinematics_step`` only.
|
``mecanum_step`` only.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ exploration behaviours:
|
|||||||
beyond the 12 m LiDAR range).
|
beyond the 12 m LiDAR range).
|
||||||
|
|
||||||
When the tracker has detections the base teacher's action is used,
|
When the tracker has detections the base teacher's action is used,
|
||||||
post-processed by ``modulate_speed_near_sheep`` so the dog doesn't
|
post-processed by ``modulate_speed`` so the dog doesn't
|
||||||
charge the flock.
|
charge the flock.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from herding.control.modulation import modulate_speed_near_sheep
|
from herding.control.modulation import modulate_speed
|
||||||
|
|
||||||
|
|
||||||
INITIAL_SCAN_STEPS = 80 # ≈1.3 s — covers one full rotation
|
INITIAL_SCAN_STEPS = 80 # ≈1.3 s — covers one full rotation
|
||||||
@@ -117,6 +117,6 @@ class ActiveScanTeacher:
|
|||||||
else:
|
else:
|
||||||
vx, vy, mode = result
|
vx, vy, mode = result
|
||||||
omega = 0.0
|
omega = 0.0
|
||||||
vx, vy = modulate_speed_near_sheep(vx, vy, dog_xy, sheep_positions)
|
vx, vy = modulate_speed(vx, vy, dog_xy, sheep_positions)
|
||||||
self.last_action = (vx, vy)
|
self.last_action = (vx, vy)
|
||||||
return vx, vy, omega, mode
|
return vx, vy, omega, mode
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Shared action post-processing.
|
"""Shared action post-processing.
|
||||||
|
|
||||||
Every dog mode routes its action through ``modulate_speed_near_sheep``
|
Every dog mode routes its action through ``modulate_speed``
|
||||||
so the magnitude is reduced near sheep — direction (intent) is
|
so the magnitude is reduced near sheep — direction (intent) is
|
||||||
preserved.
|
preserved.
|
||||||
"""
|
"""
|
||||||
@@ -14,7 +14,7 @@ SLOW_NEAR_SHEEP = 2.5 # m — distance below which action norm is scaled down
|
|||||||
MIN_SPEED = 0.30 # action norm at zero distance
|
MIN_SPEED = 0.30 # action norm at zero distance
|
||||||
|
|
||||||
|
|
||||||
def modulate_speed_near_sheep(
|
def modulate_speed(
|
||||||
vx: float, vy: float,
|
vx: float, vy: float,
|
||||||
dog_xy: tuple[float, float],
|
dog_xy: tuple[float, float],
|
||||||
sheep_positions,
|
sheep_positions,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Three-stage greedy nearest-neighbour data association:
|
|||||||
extrapolated for up to ``PREDICT_STEPS`` frames, then falls back to
|
extrapolated for up to ``PREDICT_STEPS`` frames, then falls back to
|
||||||
last-seen static memory until ``FORGET_STEPS`` deletes it.
|
last-seen static memory until ``FORGET_STEPS`` deletes it.
|
||||||
3. **Pen latching**. A track whose estimated position crosses the gate
|
3. **Pen latching**. A track whose estimated position crosses the gate
|
||||||
plane south of ``is_penned_position`` is marked penned, excluded
|
plane south of ``is_penned`` is marked penned, excluded
|
||||||
from ``get_positions``, and kept indefinitely.
|
from ``get_positions``, and kept indefinitely.
|
||||||
|
|
||||||
Output of :meth:`SheepTracker.get_positions` is ``{name: (x, y)}`` —
|
Output of :meth:`SheepTracker.get_positions` is ``{name: (x, y)}`` —
|
||||||
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from herding.config import TrackerConfig
|
from herding.config import TrackerConfig
|
||||||
|
|
||||||
from herding.world.geometry import MAX_SHEEP, in_pen, is_penned_position
|
from herding.world.geometry import MAX_SHEEP, in_pen, is_penned
|
||||||
|
|
||||||
|
|
||||||
GATE_M = 2.5 # m — primary NN gate (recently observed tracks)
|
GATE_M = 2.5 # m — primary NN gate (recently observed tracks)
|
||||||
@@ -356,10 +356,10 @@ class SheepTracker:
|
|||||||
at y ≈ -15) from being permanently latched as penned tracks.
|
at y ≈ -15) from being permanently latched as penned tracks.
|
||||||
"""
|
"""
|
||||||
from herding.world.geometry import GATE_Y
|
from herding.world.geometry import GATE_Y
|
||||||
# Apply depth threshold to both in_pen and is_penned_position so
|
# Apply depth threshold to both in_pen and is_penned so
|
||||||
# that any position in the gate column must clear GATE_Y - depth.
|
# that any position in the gate column must clear GATE_Y - depth.
|
||||||
threshold = GATE_Y - self._pen_latch_depth
|
threshold = GATE_Y - self._pen_latch_depth
|
||||||
return (in_pen(x, y) or is_penned_position(x, y)) and y <= threshold
|
return (in_pen(x, y) or is_penned(x, y)) and y <= threshold
|
||||||
|
|
||||||
def get_positions(self, min_freshness: int | None = None) -> dict[str, tuple[float, float]]:
|
def get_positions(self, min_freshness: int | None = None) -> dict[str, tuple[float, float]]:
|
||||||
"""Promoted (non-candidate, non-penned) tracks as ``{name: (x, y)}``.
|
"""Promoted (non-candidate, non-penned) tracks as ``{name: (x, y)}``.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ controllers.
|
|||||||
|
|
||||||
First-order rigid-body model — no slip, wheel-accel limits, or contact
|
First-order rigid-body model — no slip, wheel-accel limits, or contact
|
||||||
forces by default. Pass ``slip_std`` and an ``rng`` to
|
forces by default. Pass ``slip_std`` and an ``rng`` to
|
||||||
:func:`kinematics_step` / :func:`mecanum_kinematics_step` to add
|
:func:`kinematics_step` / :func:`mecanum_step` to add
|
||||||
per-wheel Gaussian speed noise for domain randomisation.
|
per-wheel Gaussian speed noise for domain randomisation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ def heading_speed_to_wheels(heading, speed_motor, h, max_wheel_omega,
|
|||||||
# Mecanum (4-wheel omnidirectional) kinematics
|
# Mecanum (4-wheel omnidirectional) kinematics
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def mecanum_kinematics_step(x, y, h, w_fl, w_fr, w_rl, w_rr,
|
def mecanum_step(x, y, h, w_fl, w_fr, w_rl, w_rr,
|
||||||
wheel_radius, lx, ly, dt,
|
wheel_radius, lx, ly, dt,
|
||||||
slip_std: float = 0.0,
|
slip_std: float = 0.0,
|
||||||
rng: Optional[np.random.Generator] = None,
|
rng: Optional[np.random.Generator] = None,
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ def in_gate_corridor(x: float, y: float, margin: float = 0.0) -> bool:
|
|||||||
and PEN_Y[0] - margin <= y <= GATE_Y + margin)
|
and PEN_Y[0] - margin <= y <= GATE_Y + margin)
|
||||||
|
|
||||||
|
|
||||||
def is_penned_position(x: float, y: float, latch_margin: float = 0.2) -> bool:
|
def is_penned(x: float, y: float, latch_margin: float = 0.2) -> bool:
|
||||||
"""True iff (x, y) is in the gate column and south of the gate line."""
|
"""True iff (x, y) is in the gate column and south of the gate line."""
|
||||||
return (GATE_X[0] - latch_margin <= x <= GATE_X[1] + latch_margin
|
return (GATE_X[0] - latch_margin <= x <= GATE_X[1] + latch_margin
|
||||||
and y <= GATE_Y)
|
and y <= GATE_Y)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from herding.control.active_scan import (
|
|||||||
EMPTY_DEBOUNCE_STEPS, INITIAL_SCAN_STEPS, ActiveScanTeacher,
|
EMPTY_DEBOUNCE_STEPS, INITIAL_SCAN_STEPS, ActiveScanTeacher,
|
||||||
)
|
)
|
||||||
from herding.control.modulation import (
|
from herding.control.modulation import (
|
||||||
MIN_SPEED, SLOW_NEAR_SHEEP, modulate_speed_near_sheep,
|
MIN_SPEED, SLOW_NEAR_SHEEP, modulate_speed,
|
||||||
)
|
)
|
||||||
from herding.control.sequential import compute_action as sequential_action
|
from herding.control.sequential import compute_action as sequential_action
|
||||||
from herding.control.strombom import (
|
from herding.control.strombom import (
|
||||||
@@ -23,23 +23,23 @@ from herding.world.geometry import PEN_ENTRY
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_modulation_empty_input_passthrough():
|
def test_modulation_empty_input_passthrough():
|
||||||
assert modulate_speed_near_sheep(1.0, 0.0, (0.0, 0.0), []) == (1.0, 0.0)
|
assert modulate_speed(1.0, 0.0, (0.0, 0.0), []) == (1.0, 0.0)
|
||||||
assert modulate_speed_near_sheep(1.0, 0.0, (0.0, 0.0), {}) == (1.0, 0.0)
|
assert modulate_speed(1.0, 0.0, (0.0, 0.0), {}) == (1.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
def test_modulation_far_sheep_passthrough():
|
def test_modulation_far_sheep_passthrough():
|
||||||
vx, vy = modulate_speed_near_sheep(1.0, 0.0, (0.0, 0.0), [(100.0, 0.0)])
|
vx, vy = modulate_speed(1.0, 0.0, (0.0, 0.0), [(100.0, 0.0)])
|
||||||
assert (vx, vy) == (1.0, 0.0)
|
assert (vx, vy) == (1.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
def test_modulation_close_sheep_min_speed():
|
def test_modulation_close_sheep_min_speed():
|
||||||
vx, vy = modulate_speed_near_sheep(1.0, 0.0, (0.0, 0.0), [(0.0, 0.0)])
|
vx, vy = modulate_speed(1.0, 0.0, (0.0, 0.0), [(0.0, 0.0)])
|
||||||
assert math.isclose(vx, MIN_SPEED)
|
assert math.isclose(vx, MIN_SPEED)
|
||||||
assert vy == 0.0
|
assert vy == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_modulation_preserves_direction():
|
def test_modulation_preserves_direction():
|
||||||
vx, vy = modulate_speed_near_sheep(0.6, 0.8, (0.0, 0.0), [(1.0, 0.0)])
|
vx, vy = modulate_speed(0.6, 0.8, (0.0, 0.0), [(1.0, 0.0)])
|
||||||
ratio = math.hypot(vx, vy)
|
ratio = math.hypot(vx, vy)
|
||||||
# Direction preserved.
|
# Direction preserved.
|
||||||
assert math.isclose(vx / ratio, 0.6, abs_tol=1e-6)
|
assert math.isclose(vx / ratio, 0.6, abs_tol=1e-6)
|
||||||
@@ -47,16 +47,16 @@ def test_modulation_preserves_direction():
|
|||||||
|
|
||||||
|
|
||||||
def test_modulation_linear_ramp_midpoint():
|
def test_modulation_linear_ramp_midpoint():
|
||||||
vx, _ = modulate_speed_near_sheep(1.0, 0.0, (0.0, 0.0),
|
vx, _ = modulate_speed(1.0, 0.0, (0.0, 0.0),
|
||||||
[(SLOW_NEAR_SHEEP / 2, 0.0)])
|
[(SLOW_NEAR_SHEEP / 2, 0.0)])
|
||||||
expected = MIN_SPEED + (1.0 - MIN_SPEED) * 0.5
|
expected = MIN_SPEED + (1.0 - MIN_SPEED) * 0.5
|
||||||
assert math.isclose(vx, expected, abs_tol=1e-6)
|
assert math.isclose(vx, expected, abs_tol=1e-6)
|
||||||
|
|
||||||
|
|
||||||
def test_modulation_accepts_dict_input():
|
def test_modulation_accepts_dict_input():
|
||||||
vx_list, _ = modulate_speed_near_sheep(1.0, 0.0, (0.0, 0.0),
|
vx_list, _ = modulate_speed(1.0, 0.0, (0.0, 0.0),
|
||||||
[(1.0, 0.0)])
|
[(1.0, 0.0)])
|
||||||
vx_dict, _ = modulate_speed_near_sheep(1.0, 0.0, (0.0, 0.0),
|
vx_dict, _ = modulate_speed(1.0, 0.0, (0.0, 0.0),
|
||||||
{"t0": (1.0, 0.0)})
|
{"t0": (1.0, 0.0)})
|
||||||
assert math.isclose(vx_list, vx_dict)
|
assert math.isclose(vx_list, vx_dict)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from herding.world.diffdrive import (
|
from herding.world.diffdrive import (
|
||||||
heading_speed_to_wheels, kinematics_step,
|
heading_speed_to_wheels, kinematics_step,
|
||||||
mecanum_inverse, mecanum_kinematics_step,
|
mecanum_inverse, mecanum_step,
|
||||||
velocity_to_mecanum_wheels, velocity_to_wheels,
|
velocity_to_mecanum_wheels, velocity_to_wheels,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ LY = 0.14 # half wheel_base_y
|
|||||||
|
|
||||||
|
|
||||||
def test_mecanum_kinematics_zero_is_identity():
|
def test_mecanum_kinematics_zero_is_identity():
|
||||||
x, y, h = mecanum_kinematics_step(
|
x, y, h = mecanum_step(
|
||||||
1.0, 2.0, 0.5, 0.0, 0.0, 0.0, 0.0, WHEEL_R, LX, LY, DT,
|
1.0, 2.0, 0.5, 0.0, 0.0, 0.0, 0.0, WHEEL_R, LX, LY, DT,
|
||||||
)
|
)
|
||||||
assert (x, y, h) == (1.0, 2.0, 0.5)
|
assert (x, y, h) == (1.0, 2.0, 0.5)
|
||||||
@@ -104,7 +104,7 @@ def test_mecanum_kinematics_zero_is_identity():
|
|||||||
def test_mecanum_kinematics_pure_forward():
|
def test_mecanum_kinematics_pure_forward():
|
||||||
# All 4 wheels equal → pure forward (vx_body > 0, vy_body = 0).
|
# All 4 wheels equal → pure forward (vx_body > 0, vy_body = 0).
|
||||||
w = 10.0
|
w = 10.0
|
||||||
x, y, h = mecanum_kinematics_step(
|
x, y, h = mecanum_step(
|
||||||
0.0, 0.0, 0.0, w, w, w, w, WHEEL_R, LX, LY, DT,
|
0.0, 0.0, 0.0, w, w, w, w, WHEEL_R, LX, LY, DT,
|
||||||
)
|
)
|
||||||
assert h == pytest.approx(0.0, abs=1e-9)
|
assert h == pytest.approx(0.0, abs=1e-9)
|
||||||
@@ -118,7 +118,7 @@ def test_mecanum_kinematics_pure_strafe():
|
|||||||
# vy_body = (-w_fl+w_fr+w_rl-w_rr)*r/4 > 0
|
# vy_body = (-w_fl+w_fr+w_rl-w_rr)*r/4 > 0
|
||||||
# Use w_fl=-10, w_fr=10, w_rl=10, w_rr=-10.
|
# Use w_fl=-10, w_fr=10, w_rl=10, w_rr=-10.
|
||||||
w_fl, w_fr, w_rl, w_rr = -10.0, 10.0, 10.0, -10.0
|
w_fl, w_fr, w_rl, w_rr = -10.0, 10.0, 10.0, -10.0
|
||||||
x, y, h = mecanum_kinematics_step(
|
x, y, h = mecanum_step(
|
||||||
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
||||||
)
|
)
|
||||||
assert h == pytest.approx(0.0, abs=1e-9)
|
assert h == pytest.approx(0.0, abs=1e-9)
|
||||||
@@ -130,7 +130,7 @@ def test_mecanum_kinematics_pure_strafe():
|
|||||||
def test_mecanum_kinematics_strafe_efficiency_scales_y():
|
def test_mecanum_kinematics_strafe_efficiency_scales_y():
|
||||||
# With strafe_efficiency=0.4, realised strafe should be 40% of ideal.
|
# With strafe_efficiency=0.4, realised strafe should be 40% of ideal.
|
||||||
w_fl, w_fr, w_rl, w_rr = -10.0, 10.0, 10.0, -10.0
|
w_fl, w_fr, w_rl, w_rr = -10.0, 10.0, 10.0, -10.0
|
||||||
x, y, _ = mecanum_kinematics_step(
|
x, y, _ = mecanum_step(
|
||||||
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
||||||
strafe_efficiency=0.4,
|
strafe_efficiency=0.4,
|
||||||
)
|
)
|
||||||
@@ -142,7 +142,7 @@ def test_mecanum_kinematics_strafe_efficiency_scales_y():
|
|||||||
def test_mecanum_kinematics_strafe_bleed_pushes_backward():
|
def test_mecanum_kinematics_strafe_bleed_pushes_backward():
|
||||||
# Negative bleed means strafe commands also push the body backward.
|
# Negative bleed means strafe commands also push the body backward.
|
||||||
w_fl, w_fr, w_rl, w_rr = -10.0, 10.0, 10.0, -10.0
|
w_fl, w_fr, w_rl, w_rr = -10.0, 10.0, 10.0, -10.0
|
||||||
x, y, _ = mecanum_kinematics_step(
|
x, y, _ = mecanum_step(
|
||||||
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
||||||
strafe_efficiency=1.0,
|
strafe_efficiency=1.0,
|
||||||
strafe_to_forward_bleed=-0.28,
|
strafe_to_forward_bleed=-0.28,
|
||||||
@@ -156,7 +156,7 @@ def test_mecanum_kinematics_strafe_bleed_pushes_backward():
|
|||||||
def test_mecanum_kinematics_forward_unaffected_by_strafe_params():
|
def test_mecanum_kinematics_forward_unaffected_by_strafe_params():
|
||||||
# Forward command should be untouched by strafe_efficiency / bleed.
|
# Forward command should be untouched by strafe_efficiency / bleed.
|
||||||
w_fl = w_fr = w_rl = w_rr = 10.0
|
w_fl = w_fr = w_rl = w_rr = 10.0
|
||||||
x, y, _ = mecanum_kinematics_step(
|
x, y, _ = mecanum_step(
|
||||||
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
||||||
strafe_efficiency=0.4,
|
strafe_efficiency=0.4,
|
||||||
strafe_to_forward_bleed=-0.28,
|
strafe_to_forward_bleed=-0.28,
|
||||||
@@ -170,7 +170,7 @@ def test_mecanum_kinematics_pure_rotation():
|
|||||||
# Pure rotation: vx_body=0, vy_body=0, omega>0.
|
# Pure rotation: vx_body=0, vy_body=0, omega>0.
|
||||||
# w_fl=-10, w_fr=10, w_rl=-10, w_rr=10 → all sums cancel except omega.
|
# w_fl=-10, w_fr=10, w_rl=-10, w_rr=10 → all sums cancel except omega.
|
||||||
w_fl, w_fr, w_rl, w_rr = -10.0, 10.0, -10.0, 10.0
|
w_fl, w_fr, w_rl, w_rr = -10.0, 10.0, -10.0, 10.0
|
||||||
x, y, h = mecanum_kinematics_step(
|
x, y, h = mecanum_step(
|
||||||
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
0.0, 0.0, 0.0, w_fl, w_fr, w_rl, w_rr, WHEEL_R, LX, LY, DT,
|
||||||
)
|
)
|
||||||
assert x == pytest.approx(0.0, abs=1e-9)
|
assert x == pytest.approx(0.0, abs=1e-9)
|
||||||
|
|||||||
+12
-12
@@ -5,7 +5,7 @@ import math
|
|||||||
from herding.world.geometry import (
|
from herding.world.geometry import (
|
||||||
FIELD_X, FIELD_Y, GATE_X, GATE_Y, MAX_SHEEP, PEN_ENTRY, PEN_X, PEN_Y,
|
FIELD_X, FIELD_Y, GATE_X, GATE_Y, MAX_SHEEP, PEN_ENTRY, PEN_X, PEN_Y,
|
||||||
distance_to_pen_entry, in_field, in_gate_corridor, in_pen,
|
distance_to_pen_entry, in_field, in_gate_corridor, in_pen,
|
||||||
is_penned_position,
|
is_penned,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -44,23 +44,23 @@ def test_in_gate_corridor():
|
|||||||
assert not in_gate_corridor(5.0, -18.0)
|
assert not in_gate_corridor(5.0, -18.0)
|
||||||
|
|
||||||
|
|
||||||
def test_is_penned_position_latches_below_gate():
|
def test_is_penned_latches_below_gate():
|
||||||
# In the gate column and south of the gate plane → penned.
|
# In the gate column and south of the gate plane → penned.
|
||||||
assert is_penned_position(11.5, -15.0)
|
assert is_penned(11.5, -15.0)
|
||||||
assert is_penned_position(10.5, -18.0)
|
assert is_penned(10.5, -18.0)
|
||||||
assert is_penned_position(12.5, -22.0)
|
assert is_penned(12.5, -22.0)
|
||||||
# Above the gate plane → not yet.
|
# Above the gate plane → not yet.
|
||||||
assert not is_penned_position(11.5, -14.9)
|
assert not is_penned(11.5, -14.9)
|
||||||
# Outside the gate column → not penned even if south.
|
# Outside the gate column → not penned even if south.
|
||||||
assert not is_penned_position(0.0, -16.0)
|
assert not is_penned(0.0, -16.0)
|
||||||
assert not is_penned_position(14.0, -16.0)
|
assert not is_penned(14.0, -16.0)
|
||||||
|
|
||||||
|
|
||||||
def test_is_penned_position_latch_margin():
|
def test_is_penned_latch_margin():
|
||||||
# Slight tolerance on the gate column.
|
# Slight tolerance on the gate column.
|
||||||
assert is_penned_position(9.9, -15.5)
|
assert is_penned(9.9, -15.5)
|
||||||
assert is_penned_position(13.1, -15.5)
|
assert is_penned(13.1, -15.5)
|
||||||
assert not is_penned_position(9.7, -15.5)
|
assert not is_penned(9.7, -15.5)
|
||||||
|
|
||||||
|
|
||||||
def test_distance_to_pen_entry():
|
def test_distance_to_pen_entry():
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ def test_tracker_forgets_stale_tracks():
|
|||||||
def test_tracker_penned_position_promotes_track():
|
def test_tracker_penned_position_promotes_track():
|
||||||
t = SheepTracker()
|
t = SheepTracker()
|
||||||
t.update([(11.5, -16.0)]) # spawn inside the pen column
|
t.update([(11.5, -16.0)]) # spawn inside the pen column
|
||||||
# is_penned_position is True for this point.
|
# is_penned is True for this point.
|
||||||
assert t.n_penned() == 1
|
assert t.n_penned() == 1
|
||||||
assert t.n_active() == 0
|
assert t.n_active() == 0
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ def build_model(net_arch_pi, net_arch_vf, log_std_init: float,
|
|||||||
return model, env
|
return model, env
|
||||||
|
|
||||||
|
|
||||||
def policy_forward_mean(policy, obs_batch):
|
def forward_mean(policy, obs_batch):
|
||||||
"""Return the deterministic mean action for an obs batch.
|
"""Return the deterministic mean action for an obs batch.
|
||||||
|
|
||||||
SB3's ActorCriticPolicy routes ``forward`` through a Distribution
|
SB3's ActorCriticPolicy routes ``forward`` through a Distribution
|
||||||
@@ -177,7 +177,7 @@ def main():
|
|||||||
ob_batch = ob_batch.to(args.device)
|
ob_batch = ob_batch.to(args.device)
|
||||||
act_batch = act_batch.to(args.device)
|
act_batch = act_batch.to(args.device)
|
||||||
optimizer.zero_grad()
|
optimizer.zero_grad()
|
||||||
mean_action = policy_forward_mean(policy, ob_batch)
|
mean_action = forward_mean(policy, ob_batch)
|
||||||
loss, mse_val, cos_val = combined_loss(mean_action, act_batch)
|
loss, mse_val, cos_val = combined_loss(mean_action, act_batch)
|
||||||
loss.backward()
|
loss.backward()
|
||||||
optimizer.step()
|
optimizer.step()
|
||||||
@@ -196,7 +196,7 @@ def main():
|
|||||||
for ob_batch, act_batch in val_loader:
|
for ob_batch, act_batch in val_loader:
|
||||||
ob_batch = ob_batch.to(args.device)
|
ob_batch = ob_batch.to(args.device)
|
||||||
act_batch = act_batch.to(args.device)
|
act_batch = act_batch.to(args.device)
|
||||||
mean_action = policy_forward_mean(policy, ob_batch)
|
mean_action = forward_mean(policy, ob_batch)
|
||||||
bs = ob_batch.size(0)
|
bs = ob_batch.size(0)
|
||||||
val_total += nn.functional.mse_loss(
|
val_total += nn.functional.mse_loss(
|
||||||
mean_action, act_batch, reduction="sum",
|
mean_action, act_batch, reduction="sum",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from gymnasium import spaces
|
|||||||
|
|
||||||
from herding.world.diffdrive import (
|
from herding.world.diffdrive import (
|
||||||
heading_speed_to_wheels, kinematics_step,
|
heading_speed_to_wheels, kinematics_step,
|
||||||
mecanum_kinematics_step, velocity_to_mecanum_wheels, velocity_to_wheels,
|
mecanum_step, velocity_to_mecanum_wheels, velocity_to_wheels,
|
||||||
)
|
)
|
||||||
from herding.world.flocking_sim import (
|
from herding.world.flocking_sim import (
|
||||||
FLEE_SPEED, MAX_SPEED, WANDER_SPEED, compute_heading_speed,
|
FLEE_SPEED, MAX_SPEED, WANDER_SPEED, compute_heading_speed,
|
||||||
@@ -40,7 +40,7 @@ from herding.world.geometry import (
|
|||||||
GATE_X, GATE_Y, MAX_SHEEP,
|
GATE_X, GATE_Y, MAX_SHEEP,
|
||||||
PEN_ENTRY, PEN_X, PEN_Y,
|
PEN_ENTRY, PEN_X, PEN_Y,
|
||||||
SHEEP_MAX_WHEEL_OMEGA, SHEEP_WHEEL_BASE, SHEEP_WHEEL_RADIUS,
|
SHEEP_MAX_WHEEL_OMEGA, SHEEP_WHEEL_BASE, SHEEP_WHEEL_RADIUS,
|
||||||
WEBOTS_DT, clip_to_field, is_penned_position,
|
WEBOTS_DT, clip_to_field, is_penned,
|
||||||
)
|
)
|
||||||
from herding.perception.lidar_perception import detections_from_scan
|
from herding.perception.lidar_perception import detections_from_scan
|
||||||
from herding.perception.lidar_sim import simulate_scan
|
from herding.perception.lidar_sim import simulate_scan
|
||||||
@@ -302,7 +302,7 @@ class HerdingEnv(gym.Env):
|
|||||||
if robot_cfg is not None else 1.0)
|
if robot_cfg is not None else 1.0)
|
||||||
strafe_bleed = (robot_cfg.strafe_to_forward_bleed
|
strafe_bleed = (robot_cfg.strafe_to_forward_bleed
|
||||||
if robot_cfg is not None else 0.0)
|
if robot_cfg is not None else 0.0)
|
||||||
self.dog_x, self.dog_y, self.dog_heading = mecanum_kinematics_step(
|
self.dog_x, self.dog_y, self.dog_heading = mecanum_step(
|
||||||
self.dog_x, self.dog_y, self.dog_heading,
|
self.dog_x, self.dog_y, self.dog_heading,
|
||||||
w_fl, w_fr, w_rl, w_rr,
|
w_fl, w_fr, w_rl, w_rr,
|
||||||
DOG_WHEEL_RADIUS,
|
DOG_WHEEL_RADIUS,
|
||||||
@@ -337,7 +337,7 @@ class HerdingEnv(gym.Env):
|
|||||||
self._step_one_sheep(i)
|
self._step_one_sheep(i)
|
||||||
for i in range(self.n_sheep):
|
for i in range(self.n_sheep):
|
||||||
if (not self.sheep_penned[i]
|
if (not self.sheep_penned[i]
|
||||||
and is_penned_position(self.sheep_x[i], self.sheep_y[i])):
|
and is_penned(self.sheep_x[i], self.sheep_y[i])):
|
||||||
self.sheep_penned[i] = True
|
self.sheep_penned[i] = True
|
||||||
|
|
||||||
# LiDAR perception runs after sheep move; feeds the obs and the
|
# LiDAR perception runs after sheep move; feeds the obs and the
|
||||||
|
|||||||
Reference in New Issue
Block a user