pylinkage.optimization package

Subpackages

Submodules

pylinkage.optimization.async_optimization module

Async wrappers for optimization functions.

This module provides async versions of the optimization functions that allow: - Progress callbacks without blocking - Cancellation support via asyncio.CancelledError - Integration with async frameworks

Created on 2025.

@author: HugoFara

class pylinkage.optimization.async_optimization.OptimizationProgress(current_iteration: int, total_iterations: int, best_score: float | None = None, is_complete: bool = False)

Bases: object

Progress information for async optimization.

Variables:
  • current_iteration (int) – Current iteration number.

  • total_iterations (int) – Total number of iterations.

  • best_score (float | None) – Best score found so far (may be None if not yet available).

  • is_complete (bool) – Whether the optimization has completed.

best_score: float | None = None
current_iteration: int
is_complete: bool = False
property progress_fraction: float

Return progress as a fraction between 0.0 and 1.0.

total_iterations: int
pylinkage.optimization.async_optimization.ProgressCallback

Type alias for progress callback functions.

alias of Callable[[OptimizationProgress], None]

async pylinkage.optimization.async_optimization.differential_evolution_optimization_async(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, bounds: tuple[~collections.abc.Sequence[float], ~collections.abc.Sequence[float]] | None = None, order_relation: ~collections.abc.Callable[[float, float], float] = <built-in function max>, strategy: str = 'best1bin', maxiter: int = 1000, popsize: int = 15, tol: float = 0.01, mutation: tuple[float, float] | float = (0.5, 1.0), recombination: float = 0.7, seed: int | None = None, on_progress: ~collections.abc.Callable[[~pylinkage.optimization.async_optimization.OptimizationProgress], None] | None = None, executor: ~concurrent.futures.thread.ThreadPoolExecutor | None = None, **kwargs: ~typing.Any) → Ensemble

Async version of differential_evolution_optimization.

This function runs the differential evolution optimization in a thread pool executor to avoid blocking the event loop, while providing progress callbacks and cancellation support.

Parameters:
  • eval_func – The evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float).

  • linkage – Linkage to be optimized.

  • bounds – Bounds to the space, in format (lower_bound, upper_bound).

  • order_relation – How to compare scores (max or min). Default is max.

  • strategy – Differential evolution strategy. Default is “best1bin”.

  • maxiter – Maximum number of generations. Default is 1000.

  • popsize – Population size multiplier. Default is 15.

  • tol – Relative tolerance for convergence. Default is 0.01.

  • mutation – Mutation constant. Default is (0.5, 1.0).

  • recombination – Recombination constant. Default is 0.7.

  • seed – Random seed for reproducibility.

  • on_progress – Optional callback function called with progress updates.

  • executor – Optional ThreadPoolExecutor to use.

  • kwargs – Additional keyword arguments passed to differential_evolution.

Returns:

List containing single Agent with best score, dimensions, and positions.

Raises:
  • asyncio.CancelledError – If the optimization is cancelled.

  • OptimizationError – If parameters are invalid or optimization fails.

async pylinkage.optimization.async_optimization.minimize_linkage_async(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, x0: ~collections.abc.Sequence[float] | None = None, bounds: tuple[~collections.abc.Sequence[float], ~collections.abc.Sequence[float]] | None = None, order_relation: ~collections.abc.Callable[[float, float], float] = <built-in function max>, method: str = 'Nelder-Mead', maxiter: int | None = None, tol: float | None = None, on_progress: ~collections.abc.Callable[[~pylinkage.optimization.async_optimization.OptimizationProgress], None] | None = None, executor: ~concurrent.futures.thread.ThreadPoolExecutor | None = None, **kwargs: ~typing.Any) → Ensemble

Async version of minimize_linkage.

This function runs the local optimization in a thread pool executor to avoid blocking the event loop, while providing progress callbacks and cancellation support.

Parameters:
  • eval_func – The evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float).

  • linkage – Linkage to be optimized.

  • x0 – Initial guess for the parameters.

  • bounds – Bounds to the space, in format (lower_bound, upper_bound).

  • order_relation – How to compare scores (max or min). Default is max.

  • method – Optimization method. Default is “Nelder-Mead”.

  • maxiter – Maximum number of iterations.

  • tol – Tolerance for termination.

  • on_progress – Optional callback function called with progress updates.

  • executor – Optional ThreadPoolExecutor to use.

  • kwargs – Additional keyword arguments passed to minimize.

Returns:

List containing single Agent with best score, dimensions, and positions.

Raises:
  • asyncio.CancelledError – If the optimization is cancelled.

  • OptimizationError – If parameters are invalid or optimization fails.

async pylinkage.optimization.async_optimization.particle_swarm_optimization_async(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, center: ~collections.abc.Sequence[float] | float | None = None, dimensions: int | None = None, n_particles: int = 100, leader: float = 3.0, follower: float = 0.1, inertia: float = 0.6, neighbors: int = 17, iterations: int = 200, bounds: tuple[~collections.abc.Sequence[float], ~collections.abc.Sequence[float]] | None = None, order_relation: ~collections.abc.Callable[[float, float], float] = <built-in function max>, on_progress: ~collections.abc.Callable[[~pylinkage.optimization.async_optimization.OptimizationProgress], None] | None = None, executor: ~concurrent.futures.thread.ThreadPoolExecutor | None = None, **kwargs: ~typing.Any) → Ensemble

Async version of particle_swarm_optimization.

This function runs the PSO optimization in a thread pool executor to avoid blocking the event loop, while providing progress callbacks and cancellation support.

Parameters:
  • eval_func – The evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float). The swarm will look for the HIGHEST score.

  • linkage – Linkage to be optimized.

  • center – A list of initial dimensions. If None, dimensions will be generated randomly between bounds. The default is None.

  • dimensions – Number of dimensions of the swarm space. If None, it takes len(tuple(linkage.get_constraints())).

  • n_particles – Number of particles in the swarm. The default is 100.

  • inertia – Inertia of each particle. The default is 0.6.

  • leader – Learning coefficient of each particle. The default is 3.0.

  • follower – Social coefficient. The default is 0.1.

  • neighbors – Number of neighbors to consider. The default is 17.

  • iterations – Number of iterations. The default is 200.

  • bounds – Bounds to the space, in format (lower_bound, upper_bound).

  • order_relation – How to compare scores (max or min). Default is max.

  • on_progress – Optional callback function called with progress updates. The callback receives an OptimizationProgress object.

  • executor – Optional ThreadPoolExecutor to use. If None, a default executor will be created.

  • kwargs – Additional keyword arguments passed to LocalBestPSO.

Returns:

best score, best dimensions and initial positions.

Return type:

List of Agents

Raises:
  • asyncio.CancelledError – If the optimization is cancelled.

  • OptimizationError – If parameters are invalid or optimization fails.

Example:

async def my_optimization():
    def progress_handler(progress):
        print(f"Progress: {progress.progress_fraction:.1%}")

    results = await particle_swarm_optimization_async(
        eval_func=my_fitness,
        linkage=my_linkage,
        iterations=100,
        on_progress=progress_handler,
    )
    return results
async pylinkage.optimization.async_optimization.trials_and_errors_optimization_async(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, parameters: Sequence[float] | None = None, n_results: int = 10, divisions: int = 5, on_progress: Callable[[OptimizationProgress], None] | None = None, executor: ThreadPoolExecutor | None = None, **kwargs: Any) → Ensemble

Async version of trials_and_errors_optimization.

This function runs the grid search optimization in a thread pool executor to avoid blocking the event loop, while providing progress callbacks and cancellation support.

Parameters:
  • eval_func – Evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float).

  • linkage – Linkage to evaluate.

  • parameters – Parameters that will be modified. If None, uses tuple(linkage.get_constraints()).

  • n_results – Number of best candidates to return. The default is 10.

  • divisions – Number of subdivisions between bounds. The default is 5.

  • on_progress – Optional callback function called with progress updates.

  • executor – Optional ThreadPoolExecutor to use. If None, a default executor will be created.

  • kwargs – Additional arguments for the optimization: - bounds: A 2-tuple containing minimal and maximal bounds. - order_relation: Function to compare scores (max, min, abs). - sequential: If True, consecutive linkages have small variation.

Returns:

List of (score, dimensions, initial_position) tuples.

Raises:
  • asyncio.CancelledError – If the optimization is cancelled.

  • OptimizationError – If parameters are invalid or no valid solution found.

Example:

async def my_optimization():
    def progress_handler(progress):
        print(f"Progress: {progress.progress_fraction:.1%}")

    results = await trials_and_errors_optimization_async(
        eval_func=my_fitness,
        linkage=my_linkage,
        divisions=10,
        on_progress=progress_handler,
    )
    return results

pylinkage.optimization.co_optimization_types module

Type definitions for topology + dimension co-optimization.

Provides the chromosome encoding, configuration, and result types used by the mixed-variable evolutionary optimizer.

class pylinkage.optimization.co_optimization_types.CoOptSolution(chromosome: MixedChromosome, scores: tuple[float, ...], linkage: Linkage | None = None, topology_entry: CatalogEntry | None = None)

Bases: object

A single solution from co-optimization.

Variables:
  • chromosome (MixedChromosome) – The mixed chromosome that produced this solution.

  • scores (tuple[float, ...]) – Objective values (one per objective, all minimized).

  • linkage (Linkage | None) – The built Linkage (if construction succeeded).

  • topology_entry (CatalogEntry | None) – The catalog entry for this topology.

chromosome: MixedChromosome
linkage: Linkage | None = None
scores: tuple[float, ...]
topology_entry: CatalogEntry | None = None
class pylinkage.optimization.co_optimization_types.CoOptimizationConfig(max_links: int = 8, algorithm: Literal['nsga2', 'nsga3'] = 'nsga2', n_generations: int = 200, pop_size: int = 100, dimension_bounds_factor: float = 5.0, topology_mutation_rate: float = 0.1, dimension_mutation_sigma: float = 0.2, crossover_prob: float = 0.9, seed: int | None = None, verbose: bool = True)

Bases: object

Configuration for a co-optimization run.

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

  • algorithm (Literal['nsga2', 'nsga3']) – NSGA variant (“nsga2” or “nsga3”).

  • n_generations (int) – Number of evolutionary generations.

  • pop_size (int) – Population size.

  • dimension_bounds_factor (float) – Link lengths are bounded to [original / factor, original * factor].

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

  • dimension_mutation_sigma (float) – Std dev for Gaussian dimension mutation (as fraction of the variable range).

  • crossover_prob (float) – Crossover probability.

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

  • verbose (bool) – Print progress during optimization.

algorithm: Literal['nsga2', 'nsga3'] = 'nsga2'
crossover_prob: float = 0.9
dimension_bounds_factor: float = 5.0
dimension_mutation_sigma: float = 0.2
n_generations: int = 200
pop_size: int = 100
seed: int | None = None
topology_mutation_rate: float = 0.1
verbose: bool = True
class pylinkage.optimization.co_optimization_types.CoOptimizationResult(pareto_front: ParetoFront, solutions: list[CoOptSolution] = <factory>, config: CoOptimizationConfig = <factory>, n_evaluations: int = 0, convergence_history: list[float] = <factory>)

Bases: object

Result of topology + dimension co-optimization.

Variables:
  • pareto_front (ParetoFront) – The Pareto front of non-dominated solutions.

  • solutions (list[CoOptSolution]) – All solutions with topology metadata.

  • config (CoOptimizationConfig) – The configuration used.

  • n_evaluations (int) – Total number of fitness evaluations.

  • convergence_history (list[float]) – Best score per generation (if tracked).

config: CoOptimizationConfig
convergence_history: list[float]
n_evaluations: int = 0
pareto_front: ParetoFront
solutions: list[CoOptSolution]
class pylinkage.optimization.co_optimization_types.MixedChromosome(topology_idx: int, dimensions: ndarray[tuple[Any, ...], dtype[floating[Any]]])

Bases: object

Chromosome encoding for co-optimization.

Encodes both discrete topology choices and continuous dimensional parameters in a single structure that genetic operators can manipulate.

Variables:
  • topology_idx (int) – Integer index into the catalog’s topology list.

  • dimensions (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.floating[Any]]]) – Continuous variables — link lengths in the order defined by the topology’s edge list.

copy() → MixedChromosome

Return a deep copy.

dimensions: ndarray[tuple[Any, ...], dtype[floating[Any]]]
topology_idx: int

pylinkage.optimization.mixed_variable module

Mixed-variable co-optimization of topology and dimensions.

Jointly searches discrete topology space and continuous dimensional space using NSGA-II/III with custom genetic operators. Each chromosome encodes a topology index (discrete) and link lengths (continuous).

This is the Phase 4 entry point for co-optimization. For warm-starting from Phase 3 synthesis results, see warm_start.

Example:

from pylinkage.optimization.mixed_variable import co_optimize
from pylinkage.synthesis.ranking import compute_path_accuracy, compute_compactness

points = [(0, 0), (1, 2), (2, 3), (3, 2), (4, 0)]

def accuracy_obj(linkage):
    return compute_path_accuracy(linkage, points)

def compactness_obj(linkage):
    return compute_compactness(linkage)

result = co_optimize(
    objectives=[accuracy_obj, compactness_obj],
    precision_points=points,
    objective_names=["Path Error", "Compactness"],
)
print(f"Found {len(result.pareto_front)} Pareto solutions")
pylinkage.optimization.mixed_variable.co_optimize(objectives: Sequence[Callable[..., float]], precision_points: list[PrecisionPoint] | None = None, catalog: TopologyCatalog | None = None, config: CoOptimizationConfig | None = None, initial_population: list[MixedChromosome] | None = None, objective_names: Sequence[str] | None = None) → CoOptimizationResult

Co-optimize topology and dimensions across all catalog topologies.

Each candidate is a (topology, link_lengths) pair. The optimizer uses NSGA-II/III with custom operators that handle the mixed discrete/continuous encoding.

Objectives receive a Linkage object and should return a float (minimized). Use float('inf') for infeasible solutions.

Parameters:
  • objectives – Callables (linkage) -> float, all minimized.

  • precision_points – Target points (passed to objectives that need them).

  • catalog – Topology catalog. If None, loads the built-in catalog.

  • config – Optimizer configuration. If None, uses defaults.

  • initial_population – Seed chromosomes (e.g., from warm_start).

  • objective_names – Names for each objective (for plotting).

Returns:

CoOptimizationResult with Pareto front and solution metadata.

Raises:

ImportError – If pymoo is not installed.

pylinkage.optimization.multi_objective module

Multi-objective optimization using NSGA-II/NSGA-III.

This module provides Pareto-optimal solutions for linkage optimization with multiple competing objectives.

Created on 2025.

@author: HugoFara

class pylinkage.optimization.multi_objective.LinkageProblem(linkage: Linkage, objectives: Sequence[Callable[..., float]], bounds: tuple[Sequence[float], Sequence[float]], joint_pos: JointPositions, n_workers: int = 1, linkage_factory: Callable[[], Linkage] | None = None)

Bases: object

Pymoo Problem wrapper for linkage optimization.

This class adapts the linkage constraint optimization problem to pymoo’s Problem interface.

close() → None

Shut down the shared process pool, if one was created.

Safe to call repeatedly. multi_objective_optimization() invokes this in a finally block so workers don’t outlive the optimization call.

property problem: Any

Return the pymoo Problem instance.

pylinkage.optimization.multi_objective.multi_objective_optimization(objectives: Sequence[Callable[..., float]], linkage: Linkage, bounds: tuple[Sequence[float], Sequence[float]] | None = None, objective_names: Sequence[str] | None = None, algorithm: Literal['nsga2', 'nsga3'] = 'nsga2', n_generations: int = 100, pop_size: int = 100, seed: int | None = None, verbose: bool = True, n_workers: int = 1, linkage_factory: Callable[[], Linkage] | None = None, **kwargs: Any) → Ensemble

Multi-objective optimization using NSGA-II or NSGA-III.

Finds Pareto-optimal solutions that trade off between multiple objectives. All objectives are MINIMIZED. For maximization, negate the objective (e.g., use lambda loci, **kw: -original_func(loci, **kw)).

Parameters:
  • objectives – List of objective functions. Each should be decorated with @kinematic_minimization or take the signature (linkage, constraints, joint_positions) -> float.

  • linkage – The linkage to optimize.

  • bounds – Tuple of (lower_bounds, upper_bounds) for constraints. If None, bounds are auto-generated from current constraints.

  • objective_names – Names for each objective (used in plotting). If None, names are auto-generated as “Objective 0”, etc.

  • algorithm – Optimization algorithm. Options: - “nsga2”: NSGA-II (default), good for 2-3 objectives - “nsga3”: NSGA-III, better for many objectives (>3)

  • n_generations – Number of generations to run. Default is 100.

  • pop_size – Population size. Default is 100.

  • seed – Random seed for reproducibility.

  • verbose – Print progress if True. Default is True.

  • n_workers – Parallel evaluation worker count. 1 (default) evaluates candidates serially in the calling process; anything higher spawns a concurrent.futures.ProcessPoolExecutor and evaluates one candidate per worker. The linkage and the objective functions must be picklable when n_workers > 1.

  • linkage_factory – Optional zero-arg callable that returns a fresh linkage. When supplied and n_workers > 1, each worker builds its own linkage via this callable instead of receiving a pickled copy. Use this escape hatch when the linkage itself is not picklable (e.g. holds cached numba SolverData).

  • **kwargs – Additional arguments passed to the algorithm.

Returns:

Ensemble containing all non-dominated solutions, with one score column per objective.

Raises:
  • ImportError – If pymoo is not installed.

  • OptimizationError – If parameters are invalid.

Example:

from pylinkage.optimization import (
    multi_objective_optimization,
    kinematic_minimization,
)

@kinematic_minimization
def path_error(loci, **kwargs):
    # Compute error from target path
    return compute_path_error(loci)

@kinematic_minimization
def transmission_penalty(loci, linkage, **kwargs):
    # Penalize poor transmission angles
    analysis = linkage.analyze_transmission()
    return abs(90 - analysis.mean_angle)

pareto = multi_objective_optimization(
    objectives=[path_error, transmission_penalty],
    linkage=my_linkage,
    objective_names=["Path Error", "Transmission Penalty"],
    n_generations=100,
)

# Visualize trade-offs
pareto.plot()

# Get best compromise solution
best = pareto.best_compromise()
my_linkage.set_constraints(best.dimensions)

pylinkage.optimization.operators module

Custom pymoo operators for mixed topology + dimension optimization.

Provides crossover, mutation, and sampling operators that handle the mixed discrete/continuous chromosome encoding used by the co-optimizer.

class pylinkage.optimization.operators.MixedCrossover(crossover_prob: float = 0.9, alpha: float = 0.5)

Bases: object

Crossover operator for mixed chromosomes.

For topology: randomly inherit from either parent. For dimensions: BLX-alpha blend crossover on continuous variables. When parents have different topologies, the child gets one parent’s topology and the other parent’s dimensions are re-sampled within bounds (since dimensions are topology-specific).

class pylinkage.optimization.operators.MixedMutation(topology_rate: float = 0.1, dimension_sigma: float = 0.2, dimension_rate: float = 0.2)

Bases: object

Combined mutation for mixed chromosomes.

  • Topology gene: with topology_rate, mutate to a neighbor in the topology neighborhood graph.

  • Dimension genes: Gaussian perturbation with sigma fraction of the variable range.

pylinkage.optimization.operators.warm_start_sampling(chromosomes: list[MixedChromosome], pop_size: int, n_dim: int, xl: NDArray[np.floating[Any]], xu: NDArray[np.floating[Any]], n_topologies: int, rng: np.random.Generator) → NDArray[np.floating[Any]]

Create initial population seeded with synthesis results.

Fills up to pop_size by: 1. Including all provided chromosomes. 2. Filling remaining slots with random chromosomes.

Parameters:
  • chromosomes – Seed chromosomes from Phase 3 synthesis.

  • pop_size – Target population size.

  • n_dim – Total chromosome length (1 + n_dimensions).

  • xl – Lower bounds for all variables.

  • xu – Upper bounds for all variables.

  • n_topologies – Number of topologies in catalog.

  • rng – Random number generator.

Returns:

Population array of shape (pop_size, n_dim).

pylinkage.optimization.particle_swarm module

Implementation of a particle swarm optimization.

Pure NumPy local-best PSO — no external dependencies beyond numpy.

Created on Fri Mar 8, 13:51:45 2019.

@author: HugoFara

pylinkage.optimization.particle_swarm.particle_swarm_optimization(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, center: ~collections.abc.Sequence[float] | float | None = None, dimensions: int | None = None, n_particles: int = 100, leader: float = 3.0, follower: float = 0.1, inertia: float = 0.6, neighbors: int = 17, iterations: int = 200, bounds: tuple[~collections.abc.Sequence[float], ~collections.abc.Sequence[float]] | None = None, order_relation: ~collections.abc.Callable[[float, float], float] = <built-in function max>, verbose: bool = True, **kwargs: int) → Ensemble

Particle Swarm Optimization for linkage parameters.

Uses a local-best ring-topology PSO implemented in pure NumPy.

Parameters:
  • eval_func – The evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float). The swarm will look for the HIGHEST score.

  • linkage – Linkage to be optimized. Make sure to give an optimized linkage for better results.

  • center – A list of initial dimensions. If None, dimensions will be generated randomly between bounds. The default is None.

  • dimensions – Number of dimensions of the swarm space, number of parameters. If None, it takes the value len(tuple(linkage.get_constraints())). The default is None.

  • n_particles – Number of particles in the swarm. The default is 100.

  • inertia – Inertia of each particle. The default is 0.6.

  • leader – Cognitive acceleration coefficient (c1). The default is 3.0.

  • follower – Social acceleration coefficient (c2). The default is 0.1.

  • neighbors – Number of neighbors in ring topology. The default is 17.

  • iterations – Number of iterations. The default is 200.

  • bounds – Bounds to the space, in format (lower_bound, upper_bound). (Default value = None).

  • order_relation – How to compare scores. There should not be anything else than the built-in max and min functions. The default is max.

  • verbose – The optimization state will be printed in the console if True. (Default value = True).

Returns:

Ensemble with the best result (single member).

Raises:

OptimizationError – If parameters are invalid or optimization fails.

pylinkage.optimization.scipy_optimize module

SciPy-based optimization algorithms for linkage optimization.

This module provides wrappers around SciPy’s optimization functions: - differential_evolution_optimization: Global optimization using differential evolution - dual_annealing_optimization: Global optimization using generalized simulated annealing - minimize_linkage: Local optimization using various gradient-free methods

Created on 2025.

@author: HugoFara

pylinkage.optimization.scipy_optimize.chain_optimizers(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, stages: ~collections.abc.Sequence[tuple[Callable[..., Ensemble], dict[str, ~typing.Any]]], order_relation: ~collections.abc.Callable[[float, float], float] = <built-in function max>, verbose: bool = True) → Ensemble

Run multiple optimizers in sequence, feeding each result to the next.

A common pattern is global search followed by local refinement, e.g. DE or PSO for exploration, then Nelder-Mead to polish the best solution.

Each stage receives the best solution from the previous stage as its starting point (via center for population-based methods, or x0 for local methods).

Parameters:
  • eval_func – The evaluation function, shared across all stages.

  • linkage – Linkage to be optimized.

  • stages – Sequence of (optimizer_func, kwargs) tuples. Each optimizer_func must accept eval_func and linkage as its first two positional arguments. kwargs are passed through. Do not include eval_func or linkage in kwargs.

  • order_relation – How to compare scores (max or min). Default is max.

  • verbose – Print stage headers and progress. Default is True.

Returns:

Ensemble from the final optimization stage.

Raises:

OptimizationError – If no stages are provided.

Example:

from pylinkage.optimization import (
    chain_optimizers,
    differential_evolution_optimization,
    minimize_linkage,
)

result = chain_optimizers(
    eval_func=fitness,
    linkage=my_linkage,
    stages=[
        (differential_evolution_optimization, {"maxiter": 300}),
        (minimize_linkage, {"method": "Nelder-Mead", "maxiter": 500}),
    ],
    order_relation=min,
)
best = result[0]
pylinkage.optimization.scipy_optimize.differential_evolution_optimization(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, bounds: tuple[~collections.abc.Sequence[float], ~collections.abc.Sequence[float]] | None = None, order_relation: ~collections.abc.Callable[[float, float], float] = <built-in function max>, strategy: str = 'best1bin', maxiter: int = 1000, popsize: int = 15, tol: float = 0.01, mutation: tuple[float, float] | float = (0.5, 1.0), recombination: float = 0.7, seed: int | None = None, workers: int = 1, verbose: bool = True, **kwargs: ~typing.Any) → Ensemble

Differential Evolution optimization wrapper for scipy.

This function is a wrapper to optimize a linkage using Differential Evolution, a global optimization algorithm that does not require gradient information. It is generally faster than grid search and can handle multimodal objective functions.

Parameters:
  • eval_func – The evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float). The optimizer will look for the score based on order_relation.

  • linkage – Linkage to be optimized.

  • bounds – Bounds to the space, in format (lower_bound, upper_bound). If None, bounds will be generated from linkage constraints using generate_bounds().

  • order_relation – How to compare scores (max or min). Default is max.

  • strategy – Differential evolution strategy. Options include: ‘best1bin’, ‘best1exp’, ‘rand1exp’, ‘randtobest1exp’, ‘best2exp’, ‘rand2exp’, ‘randtobest1bin’, ‘best2bin’, ‘rand2bin’, ‘rand1bin’. Default is “best1bin”.

  • maxiter – Maximum number of generations. Default is 1000.

  • popsize – Population size multiplier. The total population will be popsize * dimensions. Default is 15.

  • tol – Relative tolerance for convergence. Default is 0.01.

  • mutation – Mutation constant (dithering). Can be a float in [0, 2] or a tuple (min, max). Default is (0.5, 1.0).

  • recombination – Recombination constant in [0, 1]. Default is 0.7.

  • seed – Random seed for reproducibility.

  • workers – Number of parallel workers. Use -1 for all CPUs. Default is 1.

  • verbose – Print progress if True. Default is True.

  • kwargs – Additional keyword arguments passed to differential_evolution.

Returns:

Ensemble with the best result (single member).

Raises:

OptimizationError – If parameters are invalid or optimization fails.

Example:

from pylinkage.optimization import differential_evolution_optimization
from pylinkage.optimization.utils import kinematic_minimization

@kinematic_minimization
def fitness(loci, **kwargs):
    return some_metric(loci)

result = differential_evolution_optimization(
    eval_func=fitness,
    linkage=my_linkage,
    maxiter=500,
    order_relation=min,
)
best = result[0]  # Member with .score, .dimensions, .initial_positions
pylinkage.optimization.scipy_optimize.dual_annealing_optimization(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, bounds: tuple[~collections.abc.Sequence[float], ~collections.abc.Sequence[float]] | None = None, order_relation: ~collections.abc.Callable[[float, float], float] = <built-in function max>, maxiter: int = 1000, initial_temp: float = 5230.0, restart_temp_ratio: float = 2e-05, visit: float = 2.62, accept: float = -5.0, seed: int | None = None, verbose: bool = True, **kwargs: ~typing.Any) → Ensemble

Dual Annealing optimization wrapper for scipy.

This function wraps scipy’s generalized simulated annealing optimizer. Unlike population-based methods (PSO, DE), it follows a single trajectory with controlled random jumps, making it effective for problems with many local minima and expensive evaluations.

Parameters:
  • eval_func – The evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float).

  • linkage – Linkage to be optimized.

  • bounds – Bounds to the space, in format (lower_bound, upper_bound). If None, bounds will be generated from linkage constraints.

  • order_relation – How to compare scores (max or min). Default is max.

  • maxiter – Maximum number of global iterations. Default is 1000.

  • initial_temp – Initial temperature for the annealing schedule. Higher values allow more exploration. Default is 5230.0.

  • restart_temp_ratio – Fraction of initial_temp at which the temperature is restarted. Default is 2e-5.

  • visit – Parameter for the visiting distribution. Higher values give heavier tails (more long-range jumps). Default is 2.62.

  • accept – Parameter for the acceptance distribution. Lower values make acceptance more restrictive. Default is -5.0.

  • seed – Random seed for reproducibility.

  • verbose – Print progress if True. Default is True.

  • kwargs – Additional keyword arguments passed to dual_annealing.

Returns:

Ensemble with the best result (single member).

Raises:

OptimizationError – If parameters are invalid or optimization fails.

Example:

from pylinkage.optimization import dual_annealing_optimization
from pylinkage.optimization.utils import kinematic_minimization

@kinematic_minimization
def fitness(loci, **kwargs):
    return some_metric(loci)

result = dual_annealing_optimization(
    eval_func=fitness,
    linkage=my_linkage,
    maxiter=500,
    order_relation=min,
)
best = result[0]  # Member with .score, .dimensions, .initial_positions
pylinkage.optimization.scipy_optimize.minimize_linkage(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, x0: ~collections.abc.Sequence[float] | None = None, bounds: tuple[~collections.abc.Sequence[float], ~collections.abc.Sequence[float]] | None = None, order_relation: ~collections.abc.Callable[[float, float], float] = <built-in function max>, method: ~typing.Literal['Nelder-Mead', 'Powell', 'COBYLA', 'L-BFGS-B', 'SLSQP', 'TNC'] = 'Nelder-Mead', maxiter: int | None = None, tol: float | None = None, verbose: bool = True, **kwargs: ~typing.Any) → Ensemble

Local optimization using scipy.optimize.minimize.

This function is a wrapper for local optimization of a linkage using scipy’s minimize function. It supports various gradient-free methods suitable for linkage optimization.

Parameters:
  • eval_func – The evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float). The optimizer will look for the score based on order_relation.

  • linkage – Linkage to be optimized.

  • x0 – Initial guess for the parameters. If None, uses current linkage constraints.

  • bounds – Bounds to the space, in format (lower_bound, upper_bound). If None and method supports bounds, bounds will be generated from linkage constraints.

  • order_relation – How to compare scores (max or min). Default is max.

  • method – Optimization method. Options: - “Nelder-Mead”: Simplex method, no bounds (default) - “Powell”: Powell’s method, no bounds - “COBYLA”: Constrained optimization by linear approximation - “L-BFGS-B”: Limited-memory BFGS with bounds - “SLSQP”: Sequential Least Squares Programming - “TNC”: Truncated Newton with bounds

  • maxiter – Maximum number of iterations. If None, uses scipy default.

  • tol – Tolerance for termination. If None, uses scipy default.

  • verbose – Print progress if True. Default is True.

  • kwargs – Additional keyword arguments passed to minimize.

Returns:

Ensemble with the best result (single member).

Raises:

OptimizationError – If parameters are invalid or optimization fails.

Example:

from pylinkage.optimization import minimize_linkage
from pylinkage.optimization.utils import kinematic_minimization

@kinematic_minimization
def fitness(loci, **kwargs):
    return some_metric(loci)

# Refine a solution from global optimization
result = minimize_linkage(
    eval_func=fitness,
    linkage=my_linkage,
    x0=initial_guess,
    method="Nelder-Mead",
    order_relation=min,
)
best = result[0]  # Member with .score, .dimensions, .initial_positions

pylinkage.optimization.topology_neighborhood module

Topology neighborhood graph for evolutionary search.

Defines adjacency between catalog topologies so that topology mutations can navigate smoothly between related topologies (e.g., four-bar to six-bar via dyad addition).

Used by the mixed-variable co-optimizer (Phase 4) to implement topology-aware mutation operators.

class pylinkage.optimization.topology_neighborhood.TopologyNeighbor(target_id: str, operation: str, description: str)

Bases: object

A neighboring topology reachable via a single structural mutation.

Variables:
  • target_id (str) – Catalog topology ID of the neighbor.

  • operation (str) – Type of structural change.

  • description (str) – Human-readable explanation.

description: str
operation: str
target_id: str
pylinkage.optimization.topology_neighborhood.build_neighborhood_graph(catalog: TopologyCatalog) → dict[str, list[TopologyNeighbor]]

Precompute adjacency between all catalog topologies.

Adjacency rules:

  • add_dyad: A topology with N links connects to topologies with N+2 links whose Assur decomposition contains the smaller topology’s decomposition as a prefix.

  • remove_dyad: Inverse of add_dyad.

  • swap_variant: Same family, different topology (e.g., Watt to Stephenson — both are six-bars).

  • restructure: Same link count, different Assur decomposition (connects across eight-bar variants).

The graph is small (19 topologies in the default catalog) so exhaustive pair-checking is fine.

Parameters:

catalog – Topology catalog to build adjacency for.

Returns:

Dict mapping topology_id to list of neighbors.

pylinkage.optimization.topology_neighborhood.topology_distance(id_a: str, id_b: str, catalog: TopologyCatalog, neighborhood: dict[str, list[TopologyNeighbor]] | None = None) → int

Shortest path distance between two topologies.

Uses BFS on the neighborhood graph. Returns -1 if unreachable.

Parameters:
  • id_a – Source topology ID.

  • id_b – Target topology ID.

  • catalog – The topology catalog.

  • neighborhood – Pre-built neighborhood graph (optional).

Returns:

Number of steps, or -1 if no path exists.

pylinkage.optimization.topology_neighborhood.topology_neighbors(topology_id: str, catalog: TopologyCatalog, neighborhood: dict[str, list[TopologyNeighbor]] | None = None) → list[TopologyNeighbor]

Return neighbors of a given topology.

Parameters:
  • topology_id – ID to look up.

  • catalog – The topology catalog.

  • neighborhood – Pre-built neighborhood graph (optional). If None, builds it on the fly.

Returns:

List of TopologyNeighbor reachable in one step.

pylinkage.optimization.utils module

This utility module provides various useful functions for optimization.

Created on Mon Jul 12 00:00:01 2021.

@author: HugoFara

pylinkage.optimization.utils.generate_bounds(center: Iterable[float], min_ratio: float = 5, max_factor: float = 5) → tuple[ndarray[tuple[Any, ...], dtype[floating]], ndarray[tuple[Any, ...], dtype[floating]]]

Simple function to generate bounds from a linkage.

Parameters:
  • center – 1-D sequence, often in the form of linkage.get_constraints().

  • min_ratio – Minimal compression ratio for the bounds. Minimal bounds will be of the shape center[x] / min_ratio. (Default value = 5).

  • max_factor – Dilation factor for the upper bounds. Maximal bounds will be of the shape center[x] * max_factor. (Default value = 5).

Raises:

OptimizationError – If min_ratio or max_factor are not positive.

pylinkage.optimization.utils.kinematic_maximization(func: Callable[[...], float]) → Callable[[Linkage, Iterable[float], JointPositions | None], float]

Standard run for any linkage before a complete fitness evaluation.

This decorator makes a kinematic simulation, before passing the loci to the decorated function. In case of error, the penalty value is -float(‘inf’).

Parameters:

func – Fitness function to be decorated.

pylinkage.optimization.utils.kinematic_minimization(func: Callable[[...], float]) → Callable[[Linkage, Iterable[float], JointPositions | None], float]

Standard run for any linkage before a complete fitness evaluation.

This decorator makes a kinematic simulation, before passing the loci to the decorated function. In case of error, the penalty value is float(‘inf’).

Parameters:

func – Fitness function to be decorated.

pylinkage.optimization.warm_start module

Warm-start co-optimization from Phase 3 synthesis results.

Converts multi-topology synthesis solutions into an initial population for the evolutionary co-optimizer, then runs the optimizer with those seeds. This avoids cold-start and focuses search on promising regions.

Example:

from pylinkage.optimization.warm_start import warm_start_co_optimization
from pylinkage.synthesis.ranking import compute_path_accuracy

points = [(0, 0), (1, 2), (2, 3), (3, 2), (4, 0)]

def accuracy(linkage):
    return compute_path_accuracy(linkage, points)

result = warm_start_co_optimization(
    precision_points=points,
    objectives=[accuracy],
    objective_names=["Path Error"],
)
pylinkage.optimization.warm_start.synthesis_to_chromosomes(solutions: list[TopologySolution], catalog: TopologyCatalog) → list[MixedChromosome]

Convert Phase 3 TopologySolution list to MixedChromosome seeds.

For each TopologySolution: 1. Look up the topology index from the catalog. 2. Extract link lengths from the NBarSolution. 3. Pack into a MixedChromosome.

Parameters:
  • solutions – Ranked solutions from multi-topology synthesis.

  • catalog – The topology catalog (needed for index mapping).

Returns:

List of MixedChromosome, one per valid solution.

pylinkage.optimization.warm_start.warm_start_co_optimization(precision_points: list[PrecisionPoint], objectives: Sequence[Callable[..., float]], catalog: TopologyCatalog | None = None, config: CoOptimizationConfig | None = None, objective_names: Sequence[str] | None = None, max_synthesis_solutions: int = 20, n_orientation_samples: int = 12) → CoOptimizationResult

Full pipeline: Phase 3 synthesis -> seed population -> co-optimize.

  1. Run multi-topology synthesis to get candidate solutions.

  2. Convert candidates to MixedChromosome seeds.

  3. Run co-optimization with those seeds as initial population.

Parameters:
  • precision_points – Target (x, y) points.

  • objectives – Callables (linkage) -> float, all minimized.

  • catalog – Topology catalog. If None, loads built-in.

  • config – Co-optimization config. If None, uses defaults.

  • objective_names – Names for each objective.

  • max_synthesis_solutions – Max solutions from Phase 3 synthesis.

  • n_orientation_samples – Orientation search density for synthesis.

Returns:

CoOptimizationResult with Pareto front seeded from synthesis.

Module contents

Optimization package.

class pylinkage.optimization.ParetoFront(solutions: list[~pylinkage.optimization.collections.pareto.ParetoSolution], objective_names: tuple[str, ...] = <factory>)

Bases: object

Collection of non-dominated solutions from multi-objective optimization.

Variables:
best_compromise(weights: Sequence[float] | None = None) → ParetoSolution

Select the best compromise solution.

Uses weighted sum of normalized objectives to find a balanced solution.

Parameters:

weights – Weight for each objective. If None, uses equal weights.

Returns:

The solution with the lowest weighted sum.

Raises:

ValueError – If the front is empty.

filter(max_solutions: int) → ParetoFront

Filter to a subset of well-distributed solutions.

Uses crowding distance to select diverse solutions.

Parameters:

max_solutions – Maximum number of solutions to keep.

Returns:

New ParetoFront with at most max_solutions solutions.

hypervolume(reference_point: Sequence[float]) → float

Compute the hypervolume indicator.

The hypervolume is the volume of the objective space dominated by the Pareto front and bounded by a reference point. Higher is better.

Parameters:

reference_point – Upper bound for each objective. Should be worse than any solution in the front.

Returns:

The hypervolume indicator value.

Raises:

ImportError – If pymoo is not installed (only when the front is non-empty; an empty front returns 0.0 without pymoo).

property n_objectives: int

Return the number of objectives.

objective_names: tuple[str, ...]
plot(ax: Axes | None = None, objective_indices: tuple[int, int] | tuple[int, int, int] = (0, 1), **kwargs: Any) → Figure

Plot the Pareto front.

For 2 objectives: Creates a 2D scatter plot. For 3 objectives: Creates a 3D scatter plot.

Parameters:
  • ax – Matplotlib axes to plot on. If None, creates new figure.

  • objective_indices – Which objectives to plot (indices).

  • **kwargs – Additional arguments passed to scatter().

Returns:

The matplotlib Figure containing the plot.

Raises:

ValueError – If the front is empty or indices are invalid.

scores_array() → ndarray[tuple[Any, ...], dtype[floating[Any]]]

Return all scores as a 2D numpy array.

Returns:

Array of shape (n_solutions, n_objectives).

solutions: list[ParetoSolution]
class pylinkage.optimization.ParetoSolution(scores: tuple[float, ...], dimensions: NDArray[np.floating[Any]], initial_positions: JointPositions = (), *, init_positions: JointPositions | None = None)

Bases: object

A single solution on the Pareto front.

Variables:
  • scores (tuple[float, ...]) – Objective values, one per objective (all minimized).

  • dimensions (NDArray[np.floating[Any]]) – Constraint values that produced this solution.

  • initial_positions (JointPositions) – Initial joint positions used during optimization.

dimensions: NDArray[np.floating[Any]]
dominates(other: ParetoSolution) → bool

Check if this solution dominates another.

A solution dominates another if it is at least as good in all objectives and strictly better in at least one.

Parameters:

other – Another Pareto solution to compare against.

Returns:

True if this solution dominates the other.

property init_positions: JointPositions

Backwards-compatible alias for initial_positions.

initial_positions: JointPositions = ()
scores: tuple[float, ...]
pylinkage.optimization.generate_bounds(center: Iterable[float], min_ratio: float = 5, max_factor: float = 5) → tuple[ndarray[tuple[Any, ...], dtype[floating]], ndarray[tuple[Any, ...], dtype[floating]]]

Simple function to generate bounds from a linkage.

Parameters:
  • center – 1-D sequence, often in the form of linkage.get_constraints().

  • min_ratio – Minimal compression ratio for the bounds. Minimal bounds will be of the shape center[x] / min_ratio. (Default value = 5).

  • max_factor – Dilation factor for the upper bounds. Maximal bounds will be of the shape center[x] * max_factor. (Default value = 5).

Raises:

OptimizationError – If min_ratio or max_factor are not positive.

pylinkage.optimization.kinematic_maximization(func: Callable[[...], float]) → Callable[[Linkage, Iterable[float], JointPositions | None], float]

Standard run for any linkage before a complete fitness evaluation.

This decorator makes a kinematic simulation, before passing the loci to the decorated function. In case of error, the penalty value is -float(‘inf’).

Parameters:

func – Fitness function to be decorated.

pylinkage.optimization.kinematic_minimization(func: Callable[[...], float]) → Callable[[Linkage, Iterable[float], JointPositions | None], float]

Standard run for any linkage before a complete fitness evaluation.

This decorator makes a kinematic simulation, before passing the loci to the decorated function. In case of error, the penalty value is float(‘inf’).

Parameters:

func – Fitness function to be decorated.