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.