physics_engine

Dynamic Simulator

Physics engine for dynamic walking simulation.

Uses the 2D physics engine pymunk (chipmunk) for planar mechanism simulation. Manages the simulation space, road generation, and energy tracking.

leggedsnake.physics_engine.DEFAULT_CONFIG = WorldConfig(gravity=(0, -9.80665), physics_period=0.02, torque=100.0, load_mass=10.0, ground_friction=0.7071067811865476, terrain=TerrainConfig(slope=0.17453292519943295, max_step=0.5, step_freq=0.1, noise=0.9, section_len=1.0, friction=0.7071067811865476, friction_range=None, seed=None, gap_freq=0.0, gap_width=1.0, obstacle_freq=0.0, obstacle_height=0.3, obstacle_width=0.4, slope_profile=<SlopeProfile.RANDOM: 'random'>, wave_period=10.0, wave_sweep_rate=0.0), payload_offset=(0.0, 0.0), wind_force=(0.0, 0.0), drag_coefficient=0.0)

Module-level default configuration, used when no config is passed.

class leggedsnake.physics_engine.GroundParams

Bases: TypedDict

friction: float
max_step: float
noise: float
section_len: float
slope: float
step_freq: float
class leggedsnake.physics_engine.LinkageParams

Bases: TypedDict

crank_len: float
load: float
masses: float
torque: float
class leggedsnake.physics_engine.Params

Bases: TypedDict

ground: GroundParams
linkage: LinkageParams
physics: PhysicsParams
simul: SimulParams
class leggedsnake.physics_engine.PhysicsParams

Bases: TypedDict

gravity: tuple[float, float]
max_force: float
class leggedsnake.physics_engine.SimulParams

Bases: TypedDict

physics_period: float
leggedsnake.physics_engine.SlopeGenerator

Registry mapping profile keys to generator callables. Signature: (terrain, rng, step_counter) -> angle_in_radians

alias of Callable[[TerrainConfig, Generator, int], float]

class leggedsnake.physics_engine.SlopeProfile(*values)

Bases: Enum

Named slope profiles for repeatable terrain generation.

Each value is a string key; the actual generator is looked up in SLOPE_PROFILES.

CONSTANT = 'constant'

Constant uphill at the configured slope angle.

FLAT = 'flat'

Perfectly flat ground (slope = 0).

FREQUENCY_SWEEP = 'frequency_sweep'

Linear chirp — wave frequency grows with distance, controlled by TerrainConfig.wave_sweep_rate. Useful for probing the speed response of a walker across a range of terrain frequencies in one run.

RANDOM = 'random'

Default Gaussian-distributed random slopes.

SAWTOOTH = 'sawtooth'

Repeating climb-then-drop pattern.

SINUSOIDAL = 'sinusoidal'

Sinusoidal undulation with period TerrainConfig.wave_period.

VALLEY = 'valley'

V-shaped descent then ascent.

class leggedsnake.physics_engine.TerrainConfig(slope: float = 0.17453292519943295, max_step: float = 0.5, step_freq: float = 0.1, noise: float = 0.9, section_len: float = 1.0, friction: float = 0.7071067811865476, friction_range: tuple[float, float] | None = None, seed: int | None = None, gap_freq: float = 0.0, gap_width: float = 1.0, obstacle_freq: float = 0.0, obstacle_height: float = 0.3, obstacle_width: float = 0.4, slope_profile: SlopeProfile | Callable[[TerrainConfig, Generator, int], float] | str = SlopeProfile.RANDOM, wave_period: float = 10.0, wave_sweep_rate: float = 0.0)

Bases: object

Terrain generation parameters.

__init__(slope: float = 0.17453292519943295, max_step: float = 0.5, step_freq: float = 0.1, noise: float = 0.9, section_len: float = 1.0, friction: float = 0.7071067811865476, friction_range: tuple[float, float] | None = None, seed: int | None = None, gap_freq: float = 0.0, gap_width: float = 1.0, obstacle_freq: float = 0.0, obstacle_height: float = 0.3, obstacle_width: float = 0.4, slope_profile: SlopeProfile | Callable[[TerrainConfig, Generator, int], float] | str = SlopeProfile.RANDOM, wave_period: float = 10.0, wave_sweep_rate: float = 0.0) None
friction: float = 0.7071067811865476

Ground friction coefficient (square root of mu).

friction_range: tuple[float, float] | None = None

If set, friction is randomized uniformly within this (min, max) range for each new road segment, overriding friction.

static from_preset(preset: TerrainPreset | str) TerrainConfig

Create a TerrainConfig from a named preset.

Parameters:

preset (TerrainPreset or str) – One of the built-in terrain presets.

gap_freq: float = 0.0

Probability of a gap (chasm) instead of a solid segment.

gap_width: float = 1.0

Width of gaps in meters.

max_step: float = 0.5

Maximum step height.

noise: float = 0.9

Terrain variation factor (should be ≤ 1).

obstacle_freq: float = 0.0

Probability of placing a rectangular obstacle on a segment.

obstacle_height: float = 0.3

Maximum obstacle height in meters.

obstacle_width: float = 0.4

Obstacle width in meters.

section_len: float = 1.0

Length of each road section.

seed: int | None = None

Random seed for reproducible terrain generation. None means non-deterministic.

slope: float = 0.17453292519943295

Nominal slope in radians.

slope_profile: SlopeProfile | Callable[[TerrainConfig, Generator, int], float] | str = 'random'

Slope generation strategy. Accepts a SlopeProfile enum, a string key from SLOPE_PROFILES, or a custom callable with signature (terrain, rng, step_counter) -> angle.

step_freq: float = 0.1

Probability of a step vs. a slope segment.

wave_period: float = 10.0

Spatial wavelength in metres for SINUSOIDAL and FREQUENCY_SWEEP slope profiles. Ignored by other profiles.

wave_sweep_rate: float = 0.0

Frequency sweep rate (cycles per metre²) for FREQUENCY_SWEEP. With 0 the chirp degenerates to a pure sinusoid.

class leggedsnake.physics_engine.TerrainPreset(*values)

Bases: Enum

Ready-made terrain configurations.

FLAT = 'flat'
HILLY = 'hilly'
MIXED = 'mixed'
ROUGH = 'rough'
SINUSOIDAL = 'sinusoidal'

Smooth undulating ground for measuring speed-variance response.

SLOPE_DOWN = 'slope_down'

Constant downhill grade — pairs with the Phase 9 terrain panel.

SLOPE_UP = 'slope_up'

Constant uphill grade — pairs with the Phase 9 terrain panel.

STAIRS = 'stairs'
class leggedsnake.physics_engine.World(space: Space | None = None, road_y: float = -5, config: WorldConfig | None = None)

Bases: object

Simulation world containing a pymunk space, linkages, and a road.

Not intended to be rendered visually per se, see VisualWorld for that.

Parameters:
  • space (pm.Space | None) – Pymunk space. Created automatically if None.

  • road_y (float) – Initial road height.

  • config (WorldConfig | None) – Simulation parameters. Uses DEFAULT_CONFIG when None.

__init__(space: Space | None = None, road_y: float = -5, config: WorldConfig | None = None) None
_apply_ground_filter() None

Tag all static-body shapes with the ground collision filter.

Called once when a linkage with foot-edge filtering is added. Also patches newly built road segments in the road-building methods via _tag_ground_segment.

_rng: Generator
_segment_counter: int
_segment_friction(terrain: TerrainConfig) float

Return friction for a new road segment.

_slope_fn: Callable[[TerrainConfig, Generator, int], float]
_tag_ground_segment(seg: Segment) None

Apply ground collision filter to seg when foot filtering is on.

add_linkage(source: Any, load: float | None = None) None

Add a linkage to the simulation.

Parameters:
  • source (Walker, DynamicLinkage, or legacy Linkage) – The mechanism to simulate.

  • load (float, optional) – Chassis mass override (only used when converting from Walker/Linkage). When None (the default) the value from WorldConfig.load_mass is used.

build_road(positive: bool = False) None

Build a road part.

Parameters:

positive (bool) – If False (default) the road is extended to the left.

config: WorldConfig
linkages: list[DynamicLinkage]
road: list[tuple[float, float]]
space: Space
tune_solver() None

Auto-tune solver parameters for stability.

update(dt: float | None = None) tuple[float, float] | None

Update simulation by one time step.

Parameters:

dt (float | None) – Time step. Uses config.physics_period if None.

class leggedsnake.physics_engine.WorldConfig(gravity: tuple[float, float] = (0, -9.80665), physics_period: float = 0.02, torque: float = 100.0, load_mass: float = 10.0, ground_friction: float = 0.7071067811865476, terrain: ~leggedsnake.physics_engine.TerrainConfig = <factory>, payload_offset: tuple[float, float] = (0.0, 0.0), wind_force: tuple[float, float] = (0.0, 0.0), drag_coefficient: float = 0.0)

Bases: object

Complete simulation configuration.

Replaces the global params dict with a structured, immutable-by-default configuration object. Pass to World(config=...) to parameterize a simulation; or omit to use DEFAULT_CONFIG.

Examples

>>> cfg = WorldConfig(gravity=(0, -5.0), physics_period=0.01)
>>> world = World(config=cfg)
__init__(gravity: tuple[float, float] = (0, -9.80665), physics_period: float = 0.02, torque: float = 100.0, load_mass: float = 10.0, ground_friction: float = 0.7071067811865476, terrain: ~leggedsnake.physics_engine.TerrainConfig = <factory>, payload_offset: tuple[float, float] = (0.0, 0.0), wind_force: tuple[float, float] = (0.0, 0.0), drag_coefficient: float = 0.0) None
drag_coefficient: float = 0.0

Linear drag coefficient (N·s/m). Each step, a force -drag_coefficient * chassis_velocity is added to the chassis, modelling air / fluid drag. 0 disables drag.

gravity: tuple[float, float] = (0, -9.80665)

Gravity vector (m/s²).

ground_friction: float = 0.7071067811865476

Ground friction coefficient (square root of mu).

load_mass: float = 10.0

Default chassis (frame) mass (kg). Applied automatically when a Walker is passed to World.add_linkage() without an explicit load override (before 0.5.0 the value was silently dropped — users had to pass add_linkage(walker, load=cfg.load_mass) to get it).

payload_offset: tuple[float, float] = (0.0, 0.0)

Offset (metres) of the chassis centre of gravity relative to its reference position. (0, 0) keeps the default behaviour; non-zero values simulate an uneven / off-centre payload.

physics_period: float = 0.02

Time step for each physics computation (s).

terrain: TerrainConfig

Terrain generation parameters.

torque: float = 100.0

Maximum motor torque (N·m). Previously 1e3; lowered in 0.5.0 after confirming the old default overdrove typical Strandbeest/Klann walkers into pitch chaos before the stance phase could react.

wind_force: tuple[float, float] = (0.0, 0.0)

Constant external force (Newtons) applied to each linkage’s chassis every physics step. (+x, 0) models a steady headwind or tailwind. Applied via body.apply_force_at_world_point — does not stack with user-applied forces.

leggedsnake.physics_engine._preset_flat() TerrainConfig
leggedsnake.physics_engine._preset_hilly() TerrainConfig
leggedsnake.physics_engine._preset_mixed() TerrainConfig
leggedsnake.physics_engine._preset_rough() TerrainConfig
leggedsnake.physics_engine._preset_sinusoidal() TerrainConfig
leggedsnake.physics_engine._preset_slope_down() TerrainConfig
leggedsnake.physics_engine._preset_slope_up() TerrainConfig
leggedsnake.physics_engine._preset_stairs() TerrainConfig
leggedsnake.physics_engine._slope_constant(terrain: TerrainConfig, _rng: Generator, _step: int) float
leggedsnake.physics_engine._slope_flat(_terrain: TerrainConfig, _rng: Generator, _step: int) float
leggedsnake.physics_engine._slope_frequency_sweep(terrain: TerrainConfig, _rng: Generator, step: int) float

Linear chirp: frequency grows linearly with distance.

Phase φ(x) = (x / wave_period + sweep_rate · / 2) so the instantaneous wavelength shortens as x increases. sweep_rate has units of cycles per metre². With sweep_rate = 0 the profile reduces to _slope_sinusoidal().

leggedsnake.physics_engine._slope_random(terrain: TerrainConfig, rng: Generator, _step: int) float

Gaussian-distributed slope (original behaviour).

leggedsnake.physics_engine._slope_sawtooth(terrain: TerrainConfig, _rng: Generator, step: int) float

Climb for 10 segments, then a steep single-segment drop.

leggedsnake.physics_engine._slope_sinusoidal(terrain: TerrainConfig, _rng: Generator, step: int) float

Smooth sinusoidal undulation in physical x-space.

Amplitude is terrain.slope; spatial period is terrain.wave_period metres. With wave_period <= 0 the slope collapses to 0.

leggedsnake.physics_engine._slope_valley(terrain: TerrainConfig, _rng: Generator, step: int) float

V-shape: descend for 20 segments, then ascend.

leggedsnake.physics_engine.linkage_bb(linkage: Any) tuple[float, float, float, float]

Return the bounding box (min_y, max_x, max_y, min_x) for a linkage.

leggedsnake.physics_engine.recalc_linkage(linkage: DynamicLinkage) None

Update all joint proxy coordinates from physics bodies.

leggedsnake.physics_engine.set_space_constraints(space: Space) None

Auto-tune solver parameters based on constraint count.