Changelog
All notable changes to the LeggedSnake will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.6.1] - 2026-09-12
Requires pylinkage >= 1.2.1.
Changed
Every pylinkage import goes through a package, not a module. pylinkage 1.2.0 defines its public surface as the
__all__of its packages; modules inside a package (hypergraph.core,topology.catalog,visualizer.plotly_viz,optimization.co_optimization_types,mechanism.link, …) are implementation and may move. leggedsnake now imports frompylinkage.hypergraph,pylinkage.topology,pylinkage.visualizer,pylinkage.optimizationandpylinkage.mechanisminstead.ArcDriverLinkandTrackerJointare exported frompylinkage.mechanismas of 1.2.0, which is why that release is the floor.
Fixed
``Walker.dof`` and ``Walker.mobility`` are right for real walkers. They delegate to
pylinkage.topology.compute_mobility, which until pylinkage 1.2.1 counted every edge as a link and every node as a joint: Jansen reported 17 DOF, Chebyshev 5, Klann 11, TrotBot 34, Strider 41. All are 1 now, and a test says so. The dependency floor moves to 1.2.1 for it.``multi_objective_walking_optimization`` returns the ``ParetoFront`` it documents. pylinkage’s optimizers have returned an
Ensemblesince 1.0.0, so the wrapper’s promise of.best_compromise()/.plot()/.filter()on the result was false and itsalgorithmparameter was typed as a barestr. It now converts throughEnsemble.to_pareto_front()(new in pylinkage 1.2.0) and is tested.``mypy`` is clean again.
Walker.set_coordssets each joint directly instead of passingNone-able coordinates toMechanism.set_joint_positions, the clone-pose loop inWalker.add_legsskips unsolved joints, and two lookups inhypergraph_physics/urdf_exportno longer reuse a loop variable for an optional value. No behavioural change.uv.lockrecords pylinkage 1.2.0 (and itstyping_extensionsmarker for Python 3.10).The Sphinx build is warning-free (was 124). Dataclass
Attributessections render as:ivar:fields (napoleon_use_ivar) so autodoc no longer documents every annotated attribute twice; nested bold/code/link markup inREADME.mdand this file, which reStructuredText cannot express, is unnested; thechain_walking_optimizersexample is a literal block.CONTRIBUTING.mdnames the real docs source directory.
[0.6.0] - 2026-08-12
Requires pylinkage >= 1.1.0. This release drops the compatibility shims that carried leggedsnake across the 0.9.x → 1.0.0 transition, handing the work back to upstream now that it is supported there, and opens the synthesis hand-off as public API.
Added
``Walker.from_synthesis``. Dimensional synthesis answers “which mechanism traces this foot path?”; this factory turns that answer into a physics-ready walker in one call, where the route previously ran through the private
_walker_from_sim_linkage\ ``. It accepts every containerpylinkage.synthesisemits —SynthesisResult(path_generation,function_generation,motion_generation_3_poses,six_bar_path_generation,path_generation_with_timing),TopologySolutionfrommulti_topology_synthesize,NBarSolutionfromgeneralized_synthesis, a rawFourBarSolution, anEnsemble, aLinkage, or a sequence of any of those — selecting among candidates withindex.Ensemble`` members are materialised onto a private copy of the shared linkage, so converting one never disturbs the ensemble. pylinkage’s 1.0.0 changelog referred to this factory before it existed; it exists now.``omega`` / ``alpha`` on ``Walker.step_with_derivatives``. Both take a float (all drivers) or a
dictkeyed by driver node id, mirroringmotor_rates.alphais the only way to express a driver that is spinning up or down; the previous implementation could not represent one at all.
Changed
``Walker.step_with_derivatives`` now solves derivatives analytically. It delegates to pylinkage 1.0.0’s
Mechanism.step_with_derivatives(solver.velocity/solver.acceleration) instead of central-differencing its own position stream. Velocity and acceleration values change: they are now exact rather than O(dt²)-accurate, including at the first and last frame, where the finite-difference stencil had to degrade to one-sided. A unit crank atomeganow reports exactly|omega| * ron every frame. The method also streams instead of materialising the whole run before yielding.skip_unbuildableis preserved. Upstream’s generator dies on the firstUnbuildableError, so dead zones are stepped one frame at a time: blank frames yield(None, None)per joint, drivers keep advancing, and the stream length still equalsiterations. Positions are identical tostep()‘s, frame for frame.Auditing the switch — every joint of every shipped factory against finite differences of the position stream, rather than only checking for
None— turned up two defects in pylinkage 1.0.0, both fixed upstream and released in 1.1.0. A joint collinear with its two anchors (a ternary link carrying its foot on the coupler line, which is a standard walking-linkage arrangement) was mistaken for a dead centre and reported no derivative; and an unresolved anchor was read as a stationary one, so joints downstream of it reported plausible but wrong values rather thanNone. This release therefore requires pylinkage >= 1.1.0. Against 1.0.0,from_chebyshev‘s foot and several offrom_trotbot‘s report no velocity, andfrom_trotbot‘sj5/j7report wrong ones. With the upstream fix every joint of every factory agrees with finite differences to the reference’s own truncation error.A joint may still legitimately yield
(None, None): prismatic joints, and genuine dead centres where the mechanism is at a toggle. Those propagate, so an undefined derivative stays visibly undefined instead of turning into a number downstream.``Walker.set_constraints`` accepts nested input. It flattens a nested sequence (what a
param_expandersuch asparam2dimensionsreturns) in order, and passes flat input through unchanged. This absorbs theflat=Falsekeyword that only existed on the deprecatedset_num_constraints, so callers no longer need a keyword to declare which shape they are holding.Internal constraint plumbing.
utilitygains three private helpers —_flatten_constraints\ ``,_get_constraints,_set_constraints— used byfitness,genetic_optimizer,nsga_optimizerandwalking_objectives. The optimizers accept any object honouring the pylinkage optimizer contract rather than only aWalker, so these prefer the new method names and fall back to the old ones, keeping unmigrated third-party linkages working. As a side effectagents_to_ensemble`` now distinguishes “linkage reports zero constraints” from “linkage has no such method” instead of silently treating both as zero.
Removed
``_walker_from_sim_linkage``’s in-tree component walker. The function had a hand-written converter covering
Ground/Crank/ArcCrank/RRRDyad/FixedDyad, gated behind agetattrprobe so it could run on pylinkage 0.9.x. pylinkage 1.0.0 shipspylinkage.simulation.Linkage.to_hypergraph(), which covers those types plusLinearActuator,RRPDyadandPPDyad. The function is now a thin wrapper over the native bridge, and non-SimLinkage input raisesTypeErrorup front rather than part-way through conversion. TheFixedDyadbranch is the one behaviour change worth noting: it approximated the second anchor distance from current joint positions, where upstream derives it from the dyad itself.``serialization._dimensions_to_dict`` / ``_dimensions_from_dict``. Replaced by pylinkage’s native
Dimensions.to_dict/Dimensions.from_dict. Files written by leggedsnake < 0.6.0 still load: upstream’sfrom_dictaccepts both the current[node_a, node_b, distance]triples and the older stringified-tuple hyperedge keys. Newly written files use triples. The old in-tree parser stripped punctuation and split on commas, which quietly corrupted any node id containing a comma or a quote; the upstream format has no such failure mode.
Deprecated
``Walker.get_num_constraints`` / ``Walker.set_num_constraints``. Use
get_constraints/set_constraints. The old pair now emits aDeprecationWarningand will be removed in 0.7.0. pylinkage made the same rename in 1.0.0 and removed its own wrappers outright; ours survive one further cycle.
[0.5.1] - 2026-07-23
Changed
Widened the pylinkage requirement to ``>=0.9.0,<2.0.0``. 0.5.0 shipped with
<0.10.0, which excludes the now-published pylinkage 1.0.0 and forces a resolver conflict on anyone wanting both packages. The pin was stricter than reality: the 0.5.0 code is compatible with both series and the full test suite passes unchanged against pylinkage 0.9.0 and 1.0.0. This is a packaging-only release — no source changes.The compatibility shims that 1.0.0 makes redundant (
_walker_from_sim_linkage\ ``'s in-tree component walker, the manualDimensionsdict converters inserialization) are still present and will be removed in 0.6.0, which will raise the floor to>=1.0.0``.
[0.5.0] - 2026-04-24
Changed
Module filenames normalised to PEP 8 snake_case. The four flatcase modules
dynamiclinkage,geneticoptimizer,physicsengine,worldvisualizerare nowdynamic_linkage,genetic_optimizer,physics_engine,world_visualizer. The old names remain importable as thin shims that forward every attribute lookup via module-level__getattr__\ ``and emit aDeprecationWarning`` at import time. Update callers to the new names; the shims will be removed in a future release.
Added
Passive (motor-less) drivers in topology co-optimization. Two coordinated changes:
hypergraph_physics._create_motor_constraintsnow treats a resolved rate of|rate| < 1e-9 rad/sas passive: noSimpleMotorconstraint is created and the crank stays a free-pivoting rigid body driven only by gravity, wind, inertia, and ground contact. Previously a zero-rateSimpleMotoracted as a velocity-lock, freezing the crank — the new behaviour is what callers passingmotor_rates=0.0actually want.TopologyCoOptConfig.allow_passive: bool = False. When set (alongsideevolve_motor_rates=Trueand bounds spanning zero), evolved motor rates within1e-6 rad/sof zero snap to exactly0.0, letting NSGA discover wind- or slope-driven mechanisms in the same sweep that searches powered ones. Pair with a non-zerowind_force(or sloped terrain) to make the passive region competitive.
Phase-offset genes folded into the topology co-optimization chromosome (Phase 8.3).
TopologyCoOptConfiggains anevolve_offsets: boolflag and a derivedn_offset_genesproperty. When set, the NSGA-II/III chromosome becomes[topology_idx, [n_legs,] dim_1, ..., dim_max, off_1, ..., off_M]withM = max(leg_bounds) - 1continuous genes bounded[0, tau). Geometry, structure, and gait now co-evolve in a single Pareto sweep instead of runningoptimize_gaitas a separate pass on top of a fixed mechanism. Offset genes use the same fixed-length-padded encoding as the dimension genes — the chromosome length stays constant under a variablen_legsand the trailing offsets are simply ignored for candidates with fewer legs. Each Pareto solution’sTopologySolutionInfonow carries the evolvedphase_offsets(orNonewhenevolve_offsets=False, in which case the classical evenly-spaced rotating-stack gait fromWalker.add_legs(n)is used).Sphinx documentation refresh:
docs_src/concepts.rst— orientation page covering the three pieces that compose into everything else: topology + dimensions = walker, theDynamicFitness/FitnessResultprotocol (and theas_eval_func/as_ga_fitnessadapters), and the optimizer landscape from kinematic-fast (StrideFitness+ PSO) to dynamic-multi-objective (CompositeFitness+ NSGA) plus the recommended pipeline.docs_src/migration_world_config.rst— fullparams→WorldConfigmigration guide with side-by-side code, the legacy key → dataclass-field mapping table, terrain-preset usage, and the list of new fields with no legacy equivalent (payload_offset/wind_force/drag_coefficient).API reference stubs for fifteen previously undocumented modules:
fitness,stability,gait_analysis,gait_optimization,nsga_optimizer,topology_optimization,co_design,walking_objectives,leg_count,hypergraph_physics,serialization,urdf_export,plotting,worldvisualizer,show_evolution.index.rsttoctree regrouped by capability (Mechanism, Physics, Evaluation, Optimization, I/O & Plotting) so the API surface is navigable instead of a single flat list.conf.pyreleasebumped to0.5.0(was stale at0.4.0).
Sparse loci recording in
CompositeFitnessandGaitFitness:New
loci_stride: int = 1parameter — keep every Nth recorded physics step.stride=10cuts loci memory ~20× for long sweeps while preserving gait-event detection (analyze_gaitis automatically rescaled via the new_SimulationResult.loci_dt\ ``field). Default1`` preserves the existing recording fidelity.
Structured failure metric
buildable_fraction:New helper
_compute_buildable_fraction(walker, iterations=None)\ ``runs a kinematic preview withskip_unbuildable=Trueand returns the fraction of crank angles that produce a real assembly.1.0is fully buildable,0.0is never buildable, and intermediate values describe near-misses — replacing the binaryUnbuildableError/valid=False`` cliff with a smooth gradient so optimizers can learn from designs that almost work.Surfaced as
FitnessResult.metrics["buildable_fraction"]for every built-in fitness (DistanceFitness,EfficiencyFitness,StabilityFitness,CompositeFitness,GaitFitness,StrideFitness) — including the early-return failure paths, where the metric is the only signal the optimizer gets back.Replaces the binary
try/except UnbuildableErrorkinematic pre-check inside_run_simulation\ ``: physics now runs on any walker withbuildable_fraction > 0`` and only short-circuits on a fully unbuildable design.
Scale-invariant locomotion metrics:
compute_froude_number(speed, gravity, leg_length)— Alexander’s walking Froude numberFr = v² / (g · L), the canonical dimensionless gait metric. Predicts the walk-to-run transition nearFr ≈ 0.5and lets walkers of very different sizes be compared directly.compute_cost_of_transport(energy, mass, distance)—COT = E / (m · d), the standard locomotion-efficiency metric (lower is better).Both are surfaced as
froude_numberandcost_of_transportinFitnessResult.metricsforDistanceFitness,EfficiencyFitness,StabilityFitness,CompositeFitness, andGaitFitness— derived from the simulation’s mass, mean speed, energy, and the linkage’s vertical extent (proxy leg length).
Multi-gait support via phase-offset optimization:
Walker.add_legsnow accepts either anint(even-spacing, the classic rotating-stack gait) or aSequence[float]of explicit per-leg phase offsets in radians, enabling trot, pace, canter, bound, and other asymmetric gaits. Offsets are taken modulotau.optimize_gait(GaitOptimizationConfig)evolves then_legs - 1phase offsets of a multi-leg walker usingscipy.optimize.differential_evolution, with optionalinitial_offsetswarm-start and parallel workers.GaitOptimizationResultreports best offsets, best score, and evaluation count.
External force-field extensions to
WorldConfig:payload_offset: tuple[float, float]— offsets the chassis centre of gravity in body-local coordinates, simulating an uneven or off-centre payload without moving the reference position.wind_force: tuple[float, float]— constant (fx, fy) force in Newtons applied to the chassis each physics step.drag_coefficient: float— linear drag (F = -c·v) applied to the chassis, modelling air or fluid resistance.
Ground reaction force metrics:
sample_ground_reaction_force(linkage, static_body, dt)— sums and finds the peak of pymunk arbiter impulses between a linkage and the space’s static body, call afterspace.step.StabilitySnapshotgainsground_reaction_forceandpeak_contact_forcefields (Newtons, zero when no foot contact).StabilityTimeSeriesgainspeak_ground_reaction_force,mean_ground_reaction_force, andpeak_contact_forceproperties, surfaced insummary_metrics()soStabilityFitnessandCompositeFitnessexpose them automatically.compute_stability_snapshotnow accepts an optionalstatic_bodyargument that wires in the GRF sampling.
Gait, energy, and speed fitness metrics:
GaitAnalysisResult.gait_asymmetry— population standard deviation of per-foot duty factors, zero for perfectly symmetric gaits.GaitAnalysisResult.energy_per_cycle(total_energy)— joules spent per walker stride, normalising across feet.GaitAnalysisResult.total_cycles— total stride count across feet.StabilityTimeSeries.mean_speedand.speed_variance— CoM forward-velocity statistics, surfaced insummary_metrics().GaitFitness— newDynamicFitnessscoring onmean_stride_lengthwith the full gait metric panel inFitnessResult.metrics.CompositeFitnessnow accepts"gait"inobjectivesso the metrics populate from the single shared simulation run.
Procedural terrain slope profiles:
SlopeProfile.SINUSOIDAL— smooth sinusoidal undulation in physical x-space, period set byTerrainConfig.wave_period.SlopeProfile.FREQUENCY_SWEEP— linear chirp where wave frequency grows with distance, controlled byTerrainConfig.wave_sweep_rate; useful for probing a walker’s speed response across terrain frequencies in one run.TerrainPreset.SLOPE_UP/SLOPE_DOWN/SINUSOIDAL— preassembled terrain-benchmark configs.
Classical walking-linkage factories on
Walker: one-call constructors for six canonical mechanisms, each with unit-scaled geometries and published bar lengths.Walker.from_jansen()— Theo Jansen’s 8-bar, Holy-Number lengths.Walker.from_klann()— Klann’s 6-bar (US Patent 6,260,862).Walker.from_chebyshev()— Chebyshev’s 4-bar lambda.Walker.from_strider()— Vagle’s symmetric Strider (DIY Walkers).Walker.from_trotbot()— Vagle’s TrotBot (DIY Walkers, 2024).Walker.from_ghassaei()— Amanda Ghassaei’s 5-dyad leg (Figure 5.4.4 of her 2011 Pomona thesis / boim.com Walkin8r). Uses the thesis’s classical dimensions exactly (crank=26, ground=53, 56/77 inner/outer, 75 closing bars); the H-to-E arm is not given on the figure and defaults to 130 to reproduce the published foot-locus aspect (x:y ≈ 1:0.24) from the Wikibooks reference.
Leg count as a first-class design variable:
sweep_leg_counts(walker, objective, n_legs_range, ...): evaluate a finished walker design across a range of leg counts and return an ordered mapping ofFitnessResultper count, for post-hoc “how many legs?” analysis.TopologyCoOptConfig.n_legs_min/n_legs_max: when set and different, leg count joins the NSGA chromosome as an integer gene so it co-evolves with topology and dimensions. Chromosome grows from[topology, dims…]to[topology, n_legs, dims…].TopologySolutionInfo.n_legsrecords the chosen leg count on every Pareto solution for post-optimisation analysis.
Selective foot–ground collision: only edges touching foot nodes collide with the ground surface, preventing non-foot linkage parts (frame, crank, coupler) from scraping the road and distorting the gait.
Walker.get_foot_edges()auto-detects which edges should touch the ground based onget_feet()topology analysis.Walker.foot_edge_idsproperty allows explicit override.Uses pymunk collision categories internally: foot edges (
0x1), non-foot edges (0x2), ground segments (0x4).Fully backward-compatible: when no feet are detected or
foot_edge_idsis empty, all edges collide as before.
Improved foot detection in
Walker.get_feet(): now detects “outermost driven nodes” in addition to terminal (degree-1) nodes, correctly identifying coupler points (P) in synthesised four-bars.Multi-DOF mechanism support: mechanisms can now have multiple independent drivers, each with its own angular velocity.
Walker.motor_ratesaccepts adict[str, float]mapping driver node IDs to individual rates, or a singlefloatfor all drivers.PhysicsMapping.motor_node_idstracks which driver each motor corresponds to.
Hypergraph-native Walker:
Walkernow stores aHypergraphLinkage(topology) andDimensions(geometry) directly. Mechanisms are constructed withNode,Edge,Hyperedge, andDimensionsfrom pylinkage’s hypergraph API.Re-exports of pylinkage hypergraph API:
HypergraphLinkage,Node,Edge,Hyperedge,NodeRole,Dimensions,DriverAngle.Walker.to_mechanism()converts to pylinkage’sMechanismfor kinematic simulation with proper multi-driver support.Walker.get_feet()returns terminal node IDs (replacesget_foots()).DynamicLinkageacceptsHypergraphLinkage+Dimensionsdirectly (no intermediatefrom_linkage()conversion).NodeProxylightweight class replaces theDynamicJointhierarchy for reading physics body positions.World.add_linkage()now acceptsWalkerdirectly.Re-exports of pylinkage optimization pipeline:
Agent,MutableAgent,differential_evolution_optimization,dual_annealing_optimization,minimize_linkage,chain_optimizers,multi_objective_optimization,ParetoFront,ParetoSolution,OptimizationProgress, and async variants.genetic_algorithm_optimization: standard-signature wrapper for the built-in GA, compatible withchain_optimizers.walking_objectivesmodule with factory functions:stride_length_objective,energy_efficiency_objective,total_distance_objective,multi_objective_walking_optimization.add_opposite_leg()mirrors a leg across a vertical axis.``WorldConfig`` dataclass: structured replacement for the global
paramsdict. Passconfig=WorldConfig(...)toWorld()to parameterize gravity, physics period, torque, terrain, and friction.TerrainConfigdataclass for terrain generation parameters.DEFAULT_CONFIGmodule-level instance with the previous defaults.
Physics-aware fitness protocol (
fitnessmodule):FitnessResultdataclass withscore,metrics,valid, andlocifields for rich evaluation results.DynamicFitnessruntime-checkable Protocol for standardized fitness function signatures:(topology, dimensions, config) → FitnessResult.Built-in implementations:
DistanceFitness(total walking distance),EfficiencyFitness(energy efficiency ratio),StrideFitness(kinematic stride length, no physics).as_eval_func()adapter: wrapsDynamicFitnessinto pylinkage’s(linkage, dims, pos) → floatoptimizer contract.as_ga_fitness()adapter: wrapsDynamicFitnessinto the GA optimizer’s(dna) → (score, positions)contract.Walking objective factories (
total_distance_objective,energy_efficiency_objective) accept an optionalconfigparameter.
Dynamic Co-Design (
co_designmodule): connect pylinkage’s topology co-optimization with leggedsnake’s physics simulation.Walker.from_catalog(entry, dimensions): build Walker from a topologyCatalogEntry.Walker.from_hierarchy(hierarchy, dimensions): build Walker from aHierarchicalLinkage(flattened automatically).Walker.from_synthesis(solution): build Walker fromTopologySolutionorCoOptSolution.co_optimize_objective(fitness): adaptDynamicFitnessto pylinkage’sco_optimize()contract (Linkage → float, minimized). Supports two-stage kinematic pre-filter + dynamic evaluation.optimize_walking_mechanism(spec): end-to-end pipeline fromWalkingDesignSpecto rankedWalkersolutions with metrics.WalkingDesignSpec,WalkingDesignResultdataclasses.
URDF export (
urdf_exportmodule):to_urdf(walker)generates a URDF XML string for a Walker.to_urdf_file(walker, path)writes to file.URDFConfigdataclass for export options.
Stability metrics (
stabilitymodule):StabilitySnapshotdataclass: single-timestep CoM, ZMP, support polygon, tip-over margin, and body angle.StabilityTimeSeriesdataclass with properties:mean_tip_over_margin,min_tip_over_margin,zmp_excursion,angular_stability,com_trajectory,summary_metrics().compute_com(linkage)andcompute_com_velocity(linkage): mass-weighted center of mass from pymunk rigid bodies.approximate_zmp(): zero-moment-point x-coordinate via linear inverted pendulum model.get_support_polygon(): convex hull of foot positions near ground.compute_tip_over_margin(): signed distance from CoM projection to support polygon boundary.compute_stability_snapshot(): one-call assembly of all metrics.
Gait analysis (
gait_analysismodule):FootEventdataclass for touchdown/liftoff events.GaitCycledataclass with duty factor, stride period, stance/swing duration.GaitAnalysisResultdataclass withmean_duty_factor,mean_stride_frequency,mean_stride_length,phase_offsets,summary_metrics().detect_foot_events(): y-threshold crossing detection.extract_gait_cycles(): groups events into stride cycles.compute_phase_offsets(): normalized [0,1) phase between foot pairs.compute_foot_trajectory_metrics(): max height, horizontal range, path length, smoothness.analyze_gait(): one-call entry point from simulation loci.
NSGA-II/III multi-objective optimizer (
nsga_optimizermodule):NsgaWalkingConfigdataclass: generations, population, algorithm, seed, crossover/mutation parameters, andn_workersfor parallel evaluation.NsgaWalkingResultdataclass: Pareto front with optional per-solution gait analysis and stability time series.WalkingNsgaProblem: pymoo Problem wrapper that evaluatesDynamicFitnessobjectives on Walker candidates.nsga_walking_optimization(): high-level entry point returningParetoFrontof non-dominated solutions.StabilityFitness: scores by mean tip-over margin.CompositeFitness: evaluates distance + efficiency + stability in a single physics simulation (avoids redundant runs).
Visualization and reporting (
plottingmodule):plot_pareto_front(): 2D and 3D Pareto front scatter plots with best-compromise highlighting.plot_gait_diagram(): gait timing diagram (stance/swing bars per foot).plot_stability_timeseries(): four-panel plot of tip-over margin, ZMP, body angle, and CoM height.plot_com_trajectory(): 2D CoM path colored by tip-over margin with support polygon snapshots.plot_foot_trajectories(): per-foot trajectory shape plots.plot_optimization_dashboard(): combined four-panel dashboard for a single Pareto solution.
Topology co-optimization (
topology_optimizationmodule):TopologyCoOptConfigdataclass: max links, bounds, mutation rate,n_workersfor parallel evaluation.TopologyWalkingResultdataclass: extends NSGA result with per-solutionTopologySolutionInfo(topology name, ID, link count).topology_walking_optimization(): jointly optimizes mechanism topology (from pylinkage’s 19-entry catalog) and link dimensions using NSGA-II. Mixed chromosome:[topology_idx, dim_1, ..., dim_N].solutions_by_topology(): groups Pareto solutions by mechanism type.
Parallel fitness evaluation:
n_workersparameter onNsgaWalkingConfigandTopologyCoOptConfig. When > 1, candidate evaluation usesconcurrent.futures.ProcessPoolExecutor.
Walker serialization (
serializationmodule):walker_to_dict()/walker_from_dict(): serialize Walker (topology + dimensions + motor rates) to/from plain dicts.save_walker()/load_walker(): JSON file I/O for Walkers.result_to_dict()/result_from_dict(): serializeNsgaWalkingResult(Pareto front scores, dimensions, config, topology metadata) to/from plain dicts.save_result()/load_result(): JSON file I/O for optimization results.
examples/optimization_pipeline.py: end-to-end example demonstrating Walker definition, NSGA-II optimization, gait/stability analysis, and all visualization plots.Expanded terrain generation in
TerrainConfig/World:seedfield for reproducible terrain via a seedednp.random.Generator(replaces barenp.randomcalls).friction_rangefield: per-segment friction randomized uniformly within(lo, hi), overriding the globalfrictionvalue.gap_freq/gap_width: configurable chasms (empty space) in the road that the walker must step over.obstacle_freq/obstacle_height/obstacle_width: rectangular bumps placed on the road surface.slope_profilefield: deterministic slope generators for repeatable benchmarking. Accepts aSlopeProfileenum (RANDOM,FLAT,CONSTANT,VALLEY,SAWTOOTH), a string key, or a custom callable with signature(terrain, rng, step) → angle.SLOPE_PROFILESregistry mapping string keys to generator callables.Re-enabled discrete step generation (was disabled with
and False).TerrainPresetenum withTerrainConfig.from_preset()factory:FLAT,HILLY,ROUGH,STAIRS,MIXEDready-made terrain configurations.SlopeProfile,SLOPE_PROFILES, andTerrainPresetare re-exported from the package.
Plotly / SVG renderings:
plot_walker_plotly(walker)returns an interactive plotlyFigureof a Walker’s one-revolution trajectory;save_walker_svg(walker, path)writes a drawsvg export to disk. Both delegate to pylinkage’s visualizers (plot_linkage_plotly/save_linkage_svg) with a pre-computed locus fromWalker.stepso they work against hypergraph-backed Walkers.plotlyanddrawsvgare imported lazily — callers only pay for them when they use them.Six-bar walker factories:
Walker.from_watt(...)andWalker.from_stephenson(...)wrap pylinkage 0.9’swatt_from_lengths/stephenson_from_lengthsand feed the resulting SimLinkage through_walker_from_sim_linkage\ ``. Both topologies yield a 6-node Walker (2 grounds + driver + 3 driven joints) usable with the physics stepper, optimizers, andadd_legs()``. Watt and Stephenson six-bars open richer foot-path geometries than the four-bar baseline for leg design.pylinkage 0.9 adoption:
Walker.step(skip_unbuildable=True): mirrors pylinkage 0.9’sLinkage.stepflag — dead-zone frames yield(None, None)tuples instead of aborting. Adopted inStrideFitnessandstride_length_objectiveso non-Grashof / double-rocker candidates contribute a partial locus instead of being zeroed out.extract_trajectory/extract_trajectorieshelpers (re-exported from pylinkage) replace the manual[(p[i][0], p[i][1]) for p in loci if p[i][0] is not None]boilerplate infitness.py,walking_objectives.py, andexamples/verify_mechanisms.py.Walker.dof/Walker.mobilityproperties delegating topylinkage.topology.compute_dof/compute_mobility. Fast pre-flight check for GA / NSGA inner loops: reject candidates with DOF ≠ 1 before building the mechanism.compute_dof,compute_mobility,MobilityInfore-exported from the package root.
chain_walking_optimizers(fitness, linkage, stages, ...): walking-specific wrapper aroundpylinkage.optimization.chain_optimizers. Adapts aDynamicFitnessviaas_eval_funcand forwards stages verbatim. Each stage receives the previous stage’s best as its starting point — global → local pipelines (DE → dual annealing → Nelder-Mead) just work.``Walker.joints`` property deferring to
to_mechanism().jointsso pylinkage’s_compat.get_parts\ ``sniffing sees Walker as a valid linkage. Unblocks direct use ofchain_optimizers/minimize_linkage/particle_swarm_optimizationagainst aWalker``.NSGA pipeline aligned with pylinkage:
nsga_walking_optimizationnow delegates multi-objective sequential runs topylinkage.optimization.multi_objective_optimizationrather than maintaining a bespoke pymoo wrapper. The customWalkingNsgaProblempath is retained only for parallel evaluation (n_workers > 1) and single-objective runs (pylinkage 0.9’s multi-objective wrapper assumes 2-Dres.F).as_eval_funcgainedwalker_factoryandnegateparameters: usewalker_factoryfor thread-safe fresh walkers per evaluation,negate=Truefor pylinkage’s minimization-based optimizers. A single adapter now coversmulti_objective_optimization,chain_optimizers, and standalone optimizers._ensemble_to_pareto_front\ ``bridges pylinkage'sEnsemblereturn into leggedsnake'sParetoFront-basedNsgaWalkingResult``, preserving the public API.
Temporary compat shims (to be deprecated once pylinkage 1.0 ships hypergraph-native equivalents):
Walker.step_with_derivatives(iterations, dt, skip_unbuildable): three-point central finite differences over the position stream, yielding(positions, velocities, accelerations)triples. Lets callers build smoothness-based fitness today against a stable API. Will delegate to pylinkage’sMechanism.step_with_derivativesonce that lands in a release.leggedsnake.walker._walker_from_sim_linkage: SimLinkage → Walker bridge covering the component types pylinkage’s N-bar synthesis and catalog-basedco_optimizeemit (Ground,Crank/ArcCrank,RRRDyad,FixedDyad). Unknown types raiseNotImplementedErrorso upstream additions fail loudly.
Changed
WalkingNsgaProblemreuses a singleProcessPoolExecutoracross NSGA generations instead of forking a fresh pool per_evaluate_batch\ ``call.nsga_walking_optimizationshuts the pool down infinallyso worker processes don't outlive the call. Saves one pool startup (N forks each) per generation — ~15% wall-time reduction atn_workers=4`` on short bench runs; bigger absolute savings on production-scale optimizations._walker_from_sim_linkage\ ``now delegates toSimLinkage.to_hypergraphwhen pylinkage exposes it (post-0.9.0 releases), and falls back to the in-tree component-walking shim otherwise. The native bridge handles a wider component set (LinearActuator,RRPDyad,PPDyad); the fallback stays in place until thepylinkage`` floor is bumped.Breaking: default
WorldConfig.torquelowered from1e3to1e2N·m. The old default over-drove typical Strandbeest / Klann walkers into pitch chaos before the stance phase could react — atscale=0.1Jansen,1e3N·m per motor implied a ~6.5 g chassis acceleration ceiling, yielding unstable tumbling. Users who relied on the old value should passWorldConfig(torque=1e3)explicitly.World.add_linkage(walker)now passescfg.load_massto theDynamicLinkageconstructor by default (previously theload=0default silently ignored the configured chassis mass; users had to passadd_linkage(walker, load=cfg.load_mass)to get the mass they asked for). Passload=explicitly to override.Breaking:
Walkerno longer inherits frompylinkage.Linkage. It is now a standalone class withtopologyanddimensionsattributes.Breaking:
DynamicLinkageno longer inherits fromLinkage. It takes(topology, dimensions, space)instead of(joints, space).Breaking:
motor_rateparameter renamed tomotor_ratesincreate_bodies_from_hypergraph()andDynamicLinkage. Acceptsdict[str, float]for per-driver rates orfloatfor uniform rate.Breaking: Rigid triangle detection in
hypergraph_physicsnow usesHyperedgeobjects instead ofisinstance(joint, Fixed)checks. AddHyperedgeto your topology for Fixed/ternary joints._find_effective_ground_nodes()\ ``no longer requires ajoints`` parameter; ground detection is purely topology-based.World.update()power calculation usessum(power)across all motors instead ofpower[0]only. Fixes incorrect energy accounting for multi-motor mechanisms.World.__update_linkage__()enables/checks all motors viaphysics_mapping.motorsinstead ofisinstancechecks.Joint color assignment in
VisualWorldnow usesNodeRole(GROUND/DRIVER/DRIVEN) instead ofisinstancechecks on legacy joint classes.GeneticOptimization.run()returnslist[Agent](was raw lists).Breaking:
genetic_algorithm_optimization()returns a pylinkageEnsembleinstead oflist[Agent], matching the pylinkage 0.9 optimizer contract. Useensemble[i].score/.dimensions/.initial_positions(numpy array), orensemble[i].to_agent()for the legacy tuple shape.ensemble.top(),.rank(),.filter_by_score()are now available.New helper
agents_to_ensemble(agents, linkage)converts legacylist[Agent]results (e.g. fromGeneticOptimization.run()) to anEnsembleon demand.leggedsnake.co_design.optimize_walking_mechanismnow routes topology + dimensions co-optimization through pylinkage’sco_optimizeand converts each returnedCoOptSolution.linkageto a Walker via the SimLinkage shim.leggedsnake.topology_optimization.topology_walking_optimizationcarries a docstring pointer marking it as a deprecation candidate. Preferoptimize_walking_mechanism(pylinkage-backed) unless the legacy multi-process evaluation / post-hoc gait+stability analysis matters.All examples rewritten to use hypergraph construction pattern.
examples/is now in the main folder (was indocs/).Minimum Python version is now 3.10 (was 3.7).
Support for Python 3.12, 3.13, and 3.14.
Requires
pylinkage>=0.9.0. The removedHypostaticErroralias is replaced byUnderconstrainedErrorin the package re-exports.Requires
pymoo>=0.6.1.6(for NSGA-II/III and topology co-optimization).Requires
scipy>=1.15.3.Version bumped to 0.5.0.
Fixed
Walker.add_legs(n)now produces genuinely desynchronized legs. The cloned cranks share the template’sDriverAngle.initial_angle + offsetand a pre-stepped kinematic pose, becauseto_mechanismderives the crank’s phase fromatan2(crank_pos - motor_pos)and previously every clone shared the template’s position. Cloned legs now start their cycle at their intended phase, which the kinematic solver and pymunk physics both observe.Gear-coupled drivers in
create_bodies_from_hypergraph: crank bodies that share the same motor rate are now locked together withpymunk.GearJoint(ratio=1, phase=0) so they rotate in lockstep. Without the constraint, independent torque-limitedSimpleMotors slipped against asymmetric ground load and the 8 Jansen cranks drifted through the full 360° of relative phase within ~0.5 s, collapsing the gait and letting the body fall. This emulates a real Strandbeest bolting every crank to a shared shaft.PhysicsMappingexposes the new joints viagear_joints.Multi-motor energy accounting:
World.update()now sums power from all motors (was using only the first motor’s power).add_legs()/add_opposite_leg()no longer crash with semantic edge IDs (e.g.,"frame_crank"). The edge counter no longer assumes numeric suffixes.add_legs()no longer requiresto_mechanism()to succeed on the current topology. Cloned drivers useDriverAngle.initial_anglephase offsets instead of kinematic simulation.add_opposite_leg()now creates independent DRIVER nodes with pi phase offset (was creating DRIVEN nodes linked to original driver).Project links fixed in pyproject.toml.
Road step/gap/obstacle direction logic: new road features now extend in the correct direction (forward or backward) matching slope segments.
NSGA single-objective shape bug:
nsga_walking_optimizationandtopology_walking_optimizationno longer crash withTypeError: 'numpy.float64' is not iterablewhen pymoo collapsesres.Fto a 1-D array (single solution, or single-objective run with multiple solutions). Results are now reshaped against the known objective count to disambiguate the two collapse directions.
Removed
DynamicJointabstract base class and subclasses:Nail,Motor,PinUp,DynamicPivot. Replaced byNodeProxy._joint_adapters.pymodule (adapter classes for legacy pylinkage API).convert_to_dynamic_joints()method onDynamicLinkage.Re-exports of legacy pylinkage joint classes:
Static,Crank,Fixed,Pivot,Revolute,Linkage,Ground,FixedDyad,RRRDyad,bounding_box,show_linkage. Import these frompylinkagedirectly if needed.Walker.get_foots()(replaced byget_feet()).walker_from_legacy(linkage)factory. Followed pylinkage’s removal ofpylinkage.hypergraph.from_linkagealongside the legacy joints module — there is no supported way to round-trip a pre-0.8 joint-basedLinkageinto a Walker today. Rebuild withHypergraphLinkage/Dimensions/Walker(...)directly.Walker.from_synthesis(solution)factory. Wrappedwalker_from_legacyand followed it out. Will return once pylinkage 1.0 ships a stable SimLinkage → HypergraphLinkage bridge.convert_to_dynamic_linkage‘s legacy-Linkagefallback path. Only the Walker-in code path remains.setup.cfg,setup.py,requirements.txt,requirements-dev.txt,environment.yml.
[0.4.0] - 2023-06-21
Added in 0.4.0
View all walkers!
show_all_walkersindocs/examples/strider.pylet you see all walkers in one simulation!You can set the color of walkers during display.
Genetic optimization:
GeneticOptimizationclass ingeneticoptimizer.pythat will replace the previous functional paradigm.The average score is now displayed.
VisualWorldhas a new method calledreload_visuals.show_evolution.pyis a new script plotting various data about the Walkers population’s evolution during genetic optimization.In
docs/examples/strider.pywe recommend to usetotal_distanceas the fitness function.
Changed in 0.4.0
Genetic optimization:
During genetic optimization, population is now stable at max_pop (it used to fluctuate a lot).
Genetic optimization do no longer display all dimensions in the progress bar.
startnstopargument may now be the name of the file to use (a string).max_genetic_distancewas changed from 0.7 to 10. Results are much better now!
Visuals:
updatemethod ofVisualWorldreplaced byvisual_update. It clearly separates physics and display time.Frame rate and physics speed are now independent parameters.
Visuals go to a new file
worldvisualizer.py.Camera parameters should now be accessed from
CAMERAinstead ofparams["camera"].The camera feels more cinematic.
You can define a custom load when using
World.add_linkageorVisualWorld.add_linkage. The default is 0.pyproject.tomlupdated with the data ofsetup.cfg. This is now the recommended metadata for the project.In
docs/example/strider.py, simulation time was increased from 30 seconds to 40. It was just not enough.
Fixed in 0.4.0
Documentation of
evolutionary_optimization_builtinwas wrong: returned data were in order (fitness, dimensions, position), but (fitness, position, dimensions) was indicated.After a genetic optimization, the example script was assigning wrong data to the demo walker.
kwargs_switcherfromgeneticoptimizer.pydo no longer pop (destroy) argument from the input dictionary.
Deprecated in 0.4.0
setup.cfgshould no longer be used, as it is replaced bypyproject.toml.
Removed in 0.4.0
evolutionary_optimizationfunction is removed. UseGeneticOptimizationclass instead.You can no longer use the argument “init_pop” to change the size of the initial population. It now always set to max_pop.
time_coef,calc_rateandmax_subparameters ofparams["simul"]replaced by a uniquephysics_periodset to 0.02 (s).leggedsnake/Population evolution.jsonremoved. It contained data about an evolution run and is not relevant for users.
[0.3.1] - 2023-06-14
Starting from 0.3.1, we won’t include “-alpha” or “-beta” in the naming scheme, as it is considered irrelevant.
Added in 0.3.1
requirements-dev.txtthat contain dev requirements. It makes contribution easier.PyCharm configuration files.
Changed in 0.3.1
Animations are now all stored in local variables, and no longer in an “ani” global list of animations.
Fixed in 0.3.1
The main example file
strider.pywas launching animations for each subprocess. This file is now considered an executable.evolutionary_optimization_builtinwas during the last evaluation of linkages.data_descriptorswere not save for the first line of data only ingeneticoptimizer.Multiple grammar corrections.
The
videofunction ofphysicsengine.pynow effectively launches the video (no call to plt.show required).The
videofunction ofphysicsengine.pyusingdebug=Truewas crashing.
[0.3.0-beta] - 2021-07-21
Added in 0.3.0
Multiprocessing is here! The genetic optimization can now be run in parallel! Performances got improved by 65 % using 4 processes only.
Changed in 0.3.0
We now save data using JSON! Slow computer users, you can relax and stop computing when you want.
The sidebar in the documentation is a bit more useful.
Not having tqdm will cause an exception.
Fixed in 0.3.0
Corrected the example, the genetic optimization is now properly fixed but slower.
Removed in 0.3.0
Native support for PyGAD is no longer present.
evolutionnary_optimization(replaced byevolutionary_optimization).Data saved in the old txt format are no longer readable (were they readable?)
[0.2.0-alpha] - 2021-07-14
Added in 0.2.0
Dependency to tqdm and matplotlib.
The
evolutionary_optimizationreplacesevolutionnary_optimization.The
iteparameter renameditersfor consistency with pylinkage.The new parameter
verboselet you display a nice progress bar, more information on optimization state, or nothing.
The best solution can be displayed with PyGAD as well.
Changed in 0.2.0
Typos and cleans-up in
docs/examples/strider.py.evolutionnary_optimization_legacyrenamed toevolutionary_optimization_builtin.
Deprecated in 0.2.0
evolutionnary_optimizationis now deprecated. Please useevolutionary_optimization.
Removed in 0.2.0
Explicit dependency to PyGAD. There is no longer an annoying message when PyGAD is not installed.
[0.1.4-alpha] - 2021-07-12
Added in 0.1.4
It is now possible and advised to import class and functions using quick paths, for instance
from leggedsnake import Walkerinstead offrom leggedsnake.walker import Walker.You do no longer have to manually import pylinkage, we silently import the useful stuff for you.
We now use bump2version for version maintenance.
This is fixed by the
road_yparameter inWorldlet you define a custom height for the base ground.
Changed in 0.1.4
docs/examples/strider.pyhas been updated to the latest version of leggedsnake 0.1.4.
Fixed in 0.1.4
The full swarm representation in polar graph has been repaired in
docs/examples/strider.py.During a dynamic simulation, linkages with long legs could appear through the road.
The documentation was not properly rendered because Napoleon (NumPy coding style) was not integrated.
[0.1.3-alpha] - 2021-07-10
This package was lacking real documentation, it is fixed in this version.
Added in 0.1.3
Sphinx documentation!
Website hosted on GitHub pages, check hugofara.github.io/leggedsnake!
Expanded README with the quick links section.
Changed in 0.1.3
Tests moved from
leggedsnake/teststotests/.Examples moved from
leggedsnake/examples/todocs/examples/.I was testing my code on
leggedsnake/examples/strider.py(the old path) and that’s why it was a big mess. I cleaned up that all. Sorry for the inconvenience!
Fixed in 0.1.3
A lot of outdated code in the
leggedsnake/examples/strider.pyChangelog URL was broken in
setup.cfg.
[0.1.2-alpha] - 2021-07-07
Added in 0.1.2
Security: tests with
tox.ininow include Python 3.9 and Flake 8.
Changed in 0.1.2
The
stepfunction execution speed has been increased by 25% whenreturn_resisTrue! Small performance improvement whenreturn_resisFalse.The
sizeargument ofstepfunction is now known aswitdh.We now require pylinkage>=0.4.0.
Fixed in 0.1.2
Files in
leggedsnake/examples/were not included in the PyPi package.The example was incompatible with pylinkage 0.4.0.
Test suite was unusable by tox.
Tests fixed.
Incompatible argument between PyGAD init_pop and built-in GA.
[0.1.1-alpha] - 2021-06-26
Added in 0.1.1
The example file
examples/strider.pyis now shipped with the Python package.leggedsnake/geneticoptimizer.pycan now automatically switch to the built-in GA algorithm if PyGAD is not installed.
Changed in 0.1.1
setup.cfgmetadata
[0.1.0-alpha] - 2021-06-25
Added in 0.1.0
Code vulnerabilities automatic checks
Example videos in
examples/images/
Changed in 0.1.0
Many reforms in code style in order to make the dynamic part of naming conventions consistent with Pymunk.
Images in the
README.md!
Fixed in 0.1.0
You can now define linkages with an enormous number of legs. Systems with many should no longer break physics but your CPU instead :)
[0.0.3-alpha] - 2021-06-23
Added in 0.0.3
Started walkthrough demo in
README.mdAutomatic release to PyPi
Fixed in 0.0.3
Pymunk version should be at least 6.0.0 in requirement files.
Some URLs typos in
README.mdVersioning tests not executing (GitHub action)
[0.0.2-alpha] - 2021-06-22
Added in 0.0.2
requirement.txtwas absent due to.gitignoremisconfiguration.
Changed in 0.0.2
.gitignorenow ignores .txt files only in the leggedsnake folder.environment.ymlmore flexible (versions can be superior to the selected). pymunk>5.0.0 and pylinkage added.leggedsnake/utility.pynot having zipfile or xml modules error encapsulation.
Fixed in 0.0.2
setup.cfgwas not PyPi compatible. Removed mail (use GitHub!), we now explicitly say thatREADME.mdis markdown (PyPi is conservative)
[0.0.1-alpha] - 2021-06-22
Basic version, supporting Genetic Algorithm optimization, but with various problems.
Added in 0.0.1
CODE_OF_CONDUCT.mdto help community.LICENSEMIT License.MANIFEST.into include more files.README.mdas a very minimal version.environment.ymlwith matplotlib, numpy, and pygad requirement.examples/strider.pya complete demo with Strider linkage.leggedsnake/__init__.py.leggedsnake/dynamiclinkage.py.leggedsnake/geneticoptimizer.py.leggedsnake/physicsengine.py.leggedsnake/show_evolution.pyjust a legacy package, no utility.leggedsnake/tests/test_utility.pyuntested test caseleggedsnake/utility.pycontain some useful evaluation function (stepandstride) and a broken GeoGebra interface.walker.pydefines theWalkerobject.pyproject.toml.setup.cfg.setup.pyempty, for compatibility purposes only.tox.initox with Python 3.7 and 3.8.