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:
Johnny Fernandes
2026-05-17 01:58:15 +00:00
parent 10c01a938e
commit 7ab69ab0f3
14 changed files with 75 additions and 77 deletions
+16 -18
View File
@@ -1,32 +1,30 @@
"""Central configuration dataclasses for the herding simulation.
Every tunable constant that previously lived as a module-level literal in
perception/lidar_sim.py, perception/lidar_perception.py,
perception/sheep_tracker.py, world/geometry.py, or training/herding_env.py
is now represented here as a field with its original default value.
Every tunable parameter lives here as a frozen dataclass field — LiDAR
spec, cluster detection thresholds, tracker gates, robot kinematics,
and domain-randomisation knobs — composed into :class:`HerdingConfig`.
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))
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)
env = HerdingEnv(herding_cfg=HERDING_WEBOTS) # 140° FOV
env = HerdingEnv(herding_cfg=HERDING_MEC_WEBOTS) # + mecanum slip
Design notes
------------
* All dataclasses are frozen instances are immutable after construction.
* This module must not import from other ``herding.*`` packages to avoid
import cycles. Field-geometry constants (pen coordinates, field size)
stay in ``herding.world.geometry`` because they depend on the world
variant selected at runtime via ``HERDING_WORLD``.
* All dataclasses are frozen so instances are immutable after construction.
* This module must not import from other ``herding.*`` packages
field-geometry constants live in ``herding.world.geometry`` because
they depend on the world variant selected at runtime via
``HERDING_WORLD``, which would create an import cycle here.
"""
from __future__ import annotations
@@ -257,7 +255,7 @@ class RobotConfig:
``1.0`` (default) = perfect mecanum kinematics. ``0.4`` matches the
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.
"""
@@ -266,7 +264,7 @@ class RobotConfig:
``0.0`` (default) = no bleed. ``-0.28`` matches the Webots proto's
consistent backward push under strafe commands. Used by
``mecanum_kinematics_step`` only.
``mecanum_step`` only.
"""
def __post_init__(self) -> None:
+3 -3
View File
@@ -12,7 +12,7 @@ exploration behaviours:
beyond the 12 m LiDAR range).
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.
"""
@@ -20,7 +20,7 @@ from __future__ import annotations
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
@@ -117,6 +117,6 @@ class ActiveScanTeacher:
else:
vx, vy, mode = result
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)
return vx, vy, omega, mode
+2 -2
View File
@@ -1,6 +1,6 @@
"""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
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
def modulate_speed_near_sheep(
def modulate_speed(
vx: float, vy: float,
dog_xy: tuple[float, float],
sheep_positions,
+4 -4
View File
@@ -15,7 +15,7 @@ Three-stage greedy nearest-neighbour data association:
extrapolated for up to ``PREDICT_STEPS`` frames, then falls back to
last-seen static memory until ``FORGET_STEPS`` deletes it.
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.
Output of :meth:`SheepTracker.get_positions` is ``{name: (x, y)}`` —
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
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)
@@ -356,10 +356,10 @@ class SheepTracker:
at y ≈ -15) from being permanently latched as penned tracks.
"""
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.
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]]:
"""Promoted (non-candidate, non-penned) tracks as ``{name: (x, y)}``.
+2 -2
View File
@@ -3,7 +3,7 @@ controllers.
First-order rigid-body model — no slip, wheel-accel limits, or contact
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.
"""
@@ -80,7 +80,7 @@ def heading_speed_to_wheels(heading, speed_motor, h, max_wheel_omega,
# 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,
slip_std: float = 0.0,
rng: Optional[np.random.Generator] = None,
+1 -1
View File
@@ -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)
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."""
return (GATE_X[0] - latch_margin <= x <= GATE_X[1] + latch_margin
and y <= GATE_Y)