topology_optimization

Joint topology + dimensions co-optimization over the catalog.

Topology co-optimization for walking mechanisms.

Jointly optimizes the discrete mechanism topology (four-bar, six-bar, eight-bar variants) alongside continuous link dimensions — and, when evolve_offsets=True, the per-leg phase offsets that determine gait, and when evolve_motor_rates=True, the crank angular velocity per DRIVER node — using NSGA-II/III. Each candidate is a mixed chromosome: [topology_index, [n_legs,] dim_1, ..., dim_N, [off_1, ..., off_M], [motor_1, ..., motor_K]].

Leverages pylinkage’s topology catalog and co-optimization infrastructure while evaluating candidates through leggedsnake’s physics-based fitness functions.

Example:

from leggedsnake import (
    DistanceFitness,
    StabilityFitness,
    NsgaWalkingConfig,
)
from leggedsnake.topology_optimization import (
    TopologyCoOptConfig,
    topology_walking_optimization,
)

result = topology_walking_optimization(
    objectives=[DistanceFitness(duration=10), StabilityFitness(duration=10)],
    objective_names=["distance", "stability"],
    config=TopologyCoOptConfig(
        max_links=6,
        n_generations=50,
        pop_size=40,
    ),
)

for sol in result.pareto_front.solutions:
    print(sol.scores, sol.metadata.get("topology_name"))
class leggedsnake.topology_optimization.TopologyCoOptConfig(max_links: int = 8, n_generations: int = 100, pop_size: int = 100, algorithm: Literal['nsga2', 'nsga3'] = 'nsga2', seed: int | None = None, verbose: bool = True, n_legs: int = 2, n_legs_min: int | None = None, n_legs_max: int | None = None, motor_rates: float | dict[str, float] = -4.0, dimension_lower: float = 0.3, dimension_upper: float = 5.0, topology_mutation_rate: float = 0.1, evolve_offsets: bool = False, evolve_motor_rates: bool = False, motor_rate_lower: float = -8.0, motor_rate_upper: float = 8.0, allow_passive: bool = False, wind_force: tuple[float, float] = (0.0, 0.0), n_workers: int = 1)

Bases: object

Configuration for topology co-optimization.

Variables:
  • max_links (int) – Maximum number of links to consider from catalog.

  • n_generations (int) – Number of evolutionary generations.

  • pop_size (int) – Population size per generation.

  • algorithm ({"nsga2", "nsga3"}) – Multi-objective algorithm.

  • seed (int | None) – Random seed for reproducibility.

  • verbose (bool) – Print progress.

  • n_legs (int) – Fixed number of legs per walker candidate. Applied when n_legs_min and n_legs_max are both unset (the default).

  • n_legs_max (n_legs_min,) – If both are set and differ, leg count joins the chromosome as an integer gene sampled from [n_legs_min, n_legs_max]. When equal (or either is None), the fixed n_legs is used.

  • motor_rates (float | dict[str, float]) – Motor angular velocities.

  • dimension_lower (float) – Lower bound for link lengths.

  • dimension_upper (float) – Upper bound for link lengths.

  • topology_mutation_rate (float) – Probability of mutating the topology gene.

  • evolve_offsets (bool) – If True, append per-leg phase-offset genes (radians, bounded [0, tau)) to the chromosome so the optimizer co-evolves gait pattern alongside topology + dimensions. The offset region is sized for max(leg_bounds) - 1 so the chromosome length stays fixed under a variable n_legs; surplus offset genes are ignored when a candidate uses fewer legs (mirroring the existing dimension-padding scheme). Requires max(leg_bounds) >= 2. Default False keeps the classical evenly-spaced rotating-stack gait of Walker.add_legs(n).

  • evolve_motor_rates (bool) – If True, append per-DRIVER motor-rate genes (rad/s, bounded [motor_rate_lower, motor_rate_upper]) to the chromosome so the optimizer co-evolves crank angular velocity alongside structure and geometry. The motor region is sized for the maximum DRIVER count across all candidate topologies; topologies with fewer drivers consume the leading slots and the rest are ignored (mirroring the dimension-padding scheme). Default False keeps the fixed motor_rates value. The evolved rates override the motor_rates attribute on each fitness objective (which defaults to -4.0 and would otherwise always win against Dimensions.driver_angles); fitness objects without a settable motor_rates attribute are not affected.

  • motor_rate_upper (motor_rate_lower,) – Bounds (rad/s) for the motor-rate genes when evolve_motor_rates=True. Default [-8.0, +8.0] covers the practical walking-speed range; the sign sets crank direction. Using 0 as a bound endpoint allows the optimizer to converge on a stationary driver — useful for prototyping wind- or slope-driven passive walkers.

  • allow_passive (bool) – If True, drivers whose evolved rate is within 1e-6 rad/s of zero are snapped to exactly 0.0, which the physics layer treats as passive: no SimpleMotor constraint is created and the crank stays a free-pivoting rigid body driven only by gravity, wind, inertia, and ground contact. Requires evolve_motor_rates=True and bounds spanning zero (motor_rate_lower <= 0 <= motor_rate_upper); the optimizer otherwise has no way to reach the passive region. Default False keeps every driver powered. Pair with a non-zero wind_force (or a sloped world_config terrain) when looking for purely passive walkers.

  • wind_force (tuple[float, float]) – Constant external force (Newtons, (fx, fy)) applied to each candidate’s chassis every physics step during evaluation. Convenience knob: when world_config is left at None in topology_walking_optimization(), a default WorldConfig is built with this wind. Set both an explicit world_config and a non-zero wind_force only if they agree; topology_walking_optimization() raises ValueError otherwise. Default (0.0, 0.0) keeps the existing windless behaviour.

__init__(max_links: int = 8, n_generations: int = 100, pop_size: int = 100, algorithm: Literal['nsga2', 'nsga3'] = 'nsga2', seed: int | None = None, verbose: bool = True, n_legs: int = 2, n_legs_min: int | None = None, n_legs_max: int | None = None, motor_rates: float | dict[str, float] = -4.0, dimension_lower: float = 0.3, dimension_upper: float = 5.0, topology_mutation_rate: float = 0.1, evolve_offsets: bool = False, evolve_motor_rates: bool = False, motor_rate_lower: float = -8.0, motor_rate_upper: float = 8.0, allow_passive: bool = False, wind_force: tuple[float, float] = (0.0, 0.0), n_workers: int = 1) None
algorithm: Literal['nsga2', 'nsga3'] = 'nsga2'
allow_passive: bool = False
dimension_lower: float = 0.3
dimension_upper: float = 5.0
evolve_motor_rates: bool = False
evolve_offsets: bool = False
property leg_bounds: tuple[int, int]

Effective (low, high) leg-count bounds. Inclusive.

property leg_gene_active: bool

True when leg count joins the chromosome as a variable gene.

motor_rate_lower: float = -8.0
motor_rate_upper: float = 8.0
motor_rates: float | dict[str, float] = -4.0
n_generations: int = 100
n_legs: int = 2
n_legs_max: int | None = None
n_legs_min: int | None = None
property n_offset_genes: int

Number of phase-offset genes appended to the chromosome.

Zero when evolve_offsets is False. Otherwise sized for the upper bound on leg count (max(leg_bounds) - 1) so the chromosome remains fixed-length under a variable n_legs; only the first current_n_legs - 1 offsets are read per evaluation and the rest are ignored.

n_workers: int = 1

Number of parallel workers. 1 = sequential. >1 uses process pool.

pop_size: int = 100
seed: int | None = None
topology_mutation_rate: float = 0.1
verbose: bool = True
wind_force: tuple[float, float] = (0.0, 0.0)
class leggedsnake.topology_optimization.TopologySolutionInfo(topology_name: str, topology_id: str, topology_idx: int, num_links: int, n_legs: int = 1, phase_offsets: list[float] | None = None, motor_rates: dict[str, float] | None = None)

Bases: object

Topology metadata for a single Pareto solution.

Variables:
  • topology_name (str) – Human-readable name from catalog (e.g., “Four-bar linkage”).

  • topology_id (str) – Catalog ID (e.g., “four-bar”).

  • topology_idx (int) – Index in the catalog used during optimization.

  • num_links (int) – Number of links in the topology.

  • n_legs (int) – Number of legs in the evaluated walker. Equal to config.n_legs when the leg-count range is fixed, or the value chosen by the evolutionary search when n_legs_min != n_legs_max.

  • phase_offsets (list[float] | None) – Evolved per-leg phase offsets (radians) when TopologyCoOptConfig.evolve_offsets is set. None when offsets are not part of the chromosome — the walker was built with the classic evenly-spaced rotating-stack gait. Length equals n_legs - 1.

  • motor_rates (dict[str, float] | None) – Evolved motor angular velocities (rad/s) keyed by DRIVER node ID, when TopologyCoOptConfig.evolve_motor_rates is set. None when the chromosome did not carry motor genes — the walker used the fixed TopologyCoOptConfig.motor_rates.

__init__(topology_name: str, topology_id: str, topology_idx: int, num_links: int, n_legs: int = 1, phase_offsets: list[float] | None = None, motor_rates: dict[str, float] | None = None) None
motor_rates: dict[str, float] | None = None
n_legs: int = 1
phase_offsets: list[float] | None = None
topology_id: str
topology_idx: int
topology_name: str
class leggedsnake.topology_optimization.TopologyWalkingResult(pareto_front: ~pylinkage.optimization.collections.pareto.ParetoFront, topology_info: dict[int, ~leggedsnake.topology_optimization.TopologySolutionInfo] = <factory>, gait_analyses: dict[int, ~leggedsnake.gait_analysis.GaitAnalysisResult] | None = None, stability_series: dict[int, ~leggedsnake.stability.StabilityTimeSeries] | None = None, config: ~leggedsnake.nsga_optimizer.NsgaWalkingConfig = <factory>, co_opt_config: ~leggedsnake.topology_optimization.TopologyCoOptConfig = <factory>)

Bases: object

Result of topology co-optimization.

Extends NsgaWalkingResult with per-solution topology metadata.

Variables:
  • pareto_front (ParetoFront) – Non-dominated solutions.

  • topology_info (dict[int, TopologySolutionInfo]) – Topology metadata for each Pareto solution (index -> info).

  • gait_analyses (dict[int, GaitAnalysisResult] | None) – Gait analysis per solution.

  • stability_series (dict[int, StabilityTimeSeries] | None) – Stability per solution.

  • config (NsgaWalkingConfig) – The NSGA configuration used.

  • co_opt_config (TopologyCoOptConfig) – The topology co-opt configuration used.

__init__(pareto_front: ~pylinkage.optimization.collections.pareto.ParetoFront, topology_info: dict[int, ~leggedsnake.topology_optimization.TopologySolutionInfo] = <factory>, gait_analyses: dict[int, ~leggedsnake.gait_analysis.GaitAnalysisResult] | None = None, stability_series: dict[int, ~leggedsnake.stability.StabilityTimeSeries] | None = None, config: ~leggedsnake.nsga_optimizer.NsgaWalkingConfig = <factory>, co_opt_config: ~leggedsnake.topology_optimization.TopologyCoOptConfig = <factory>) None
best_compromise(weights: Sequence[float] | None = None) ParetoSolution

Return the best compromise solution.

best_for_objective(objective_index: int) ParetoSolution

Return best Pareto solution for a single objective.

co_opt_config: TopologyCoOptConfig
config: NsgaWalkingConfig
gait_analyses: dict[int, GaitAnalysisResult] | None = None
pareto_front: ParetoFront
solutions_by_topology() dict[str, list[int]]

Group solution indices by topology ID.

stability_series: dict[int, StabilityTimeSeries] | None = None
topology_info: dict[int, TopologySolutionInfo]
leggedsnake.topology_optimization.topology_walking_optimization(objectives: Sequence[DynamicFitness], objective_names: Sequence[str] | None = None, config: TopologyCoOptConfig | None = None, world_config: Any | None = None, catalog: Any | None = None, include_gait: bool = False, include_stability: bool = False) TopologyWalkingResult

Topology co-optimization for walking mechanisms.

Jointly optimizes the mechanism topology (from pylinkage’s catalog) and link dimensions using NSGA-II/III with physics-based fitness evaluation.

Note

This function reimplements the NSGA-II + mixed-chromosome plumbing that now ships in pylinkage.optimization.co_optimize. The pylinkage-backed path — leggedsnake.optimize_walking_mechanism() — delegates the optimizer loop to pylinkage and wraps walking fitness through leggedsnake.fitness.co_optimize_objective(). Prefer it for new code. This standalone implementation is kept for backwards compatibility and will be deprecated once the pylinkage-backed path reaches feature parity (multi-process evaluation, gait / stability post-analysis).

Parameters:
  • objectives (sequence of DynamicFitness) – Fitness evaluators (e.g., DistanceFitness, StabilityFitness).

  • objective_names (sequence of str, optional) – Human-readable names for each objective.

  • config (TopologyCoOptConfig, optional) – Optimization configuration. Uses defaults if None.

  • world_config (WorldConfig, optional) – Simulation config passed to fitness evaluators.

  • catalog (TopologyCatalog, optional) – Topology catalog. Uses built-in catalog if None.

  • include_gait (bool) – If True, run gait analysis on Pareto-front solutions.

  • include_stability (bool) – If True, collect stability time series for Pareto-front solutions.

Returns:

Pareto front with optional gait/stability analysis. Each ParetoSolution.metadata contains topology_name and topology_id identifying the mechanism type.

Return type:

NsgaWalkingResult