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:
objectProgress 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:
objectA 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
- 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:
objectConfiguration 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
- max_links: int = 8
- 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:
objectResult 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:
objectChromosome 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.grid_search module
Implementation of a grid search optimization.
It should be used for reference only as the search space will almost certainly be too big.
- pylinkage.optimization.grid_search.fast_variator(divisions: int, bounds: tuple[Sequence[float], Sequence[float]]) Generator[list[float], None, None]
Return an iterable of elements’ all possibles variations.
Number of variations: ((max_dim - 1 / min_dim) / delta_dim) ** len(ite).
Here the order in the variations is not important.
- Parameters:
divisions – Number of subdivisions between bounds.
bounds – 2-uple of minimal then maximal bounds.
- Returns:
An iterable of all the dimension combinations.
- pylinkage.optimization.grid_search.sequential_variator(center: Sequence[float] | ndarray[tuple[Any, ...], dtype[floating]], divisions: int, bounds: tuple[Sequence[float], Sequence[float]]) Generator[ndarray[tuple[Any, ...], dtype[floating]], None, None]
Return an iterable of each possible variation for the elements.
Number of variations: ((max_dim - 1 / min_dim) / delta_dim) ** len(ite).
Because linkage is not tolerant to violent changes, the order of output for the coefficients is very important.
The coefficient is in order: middle → min (step 2), min → middle (step 2), middle → max (step 1), so that there is no huge variation.
- Parameters:
center – Elements that should vary.
divisions – Number of subdivisions between bounds.
bounds – 2-uple of minimal then maximal bounds.
- Returns:
An iterable of all the dimension combinations.
- pylinkage.optimization.grid_search.trials_and_errors_optimization(eval_func: Callable[[Linkage, Sequence[float], JointPositions], float], linkage: Linkage, parameters: Sequence[float] | None = None, n_results: int = 10, divisions: int = 5, **kwargs: Any) Ensemble
Return the best dimensions optimizing eval_func as an Ensemble.
Each dimension set has a score. The returned Ensemble contains up to n_results members with the best scores (maximization by default).
- Parameters:
eval_func – Evaluation function. Input: (linkage, num_constraints, initial_coordinates). Output: score (float).
linkage – Linkage to evaluate.
parameters – Parameters that will be modified. Geometric constraints. If not, it will be assigned tuple(linkage.get_constraints()). The default is None.
n_results – Number of the best candidates to return. The default is 10.
divisions – Number of subdivisions between bounds. The default is 5.
kwargs –
Extra arguments for the optimization:
bounds: A 2-uple containing the minimal and maximal bounds. If None, we will use parameters as a center. (Default value = None).order_relation: A function of two arguments, should return the best score of two scores. Common examples aremin,max,abs. (Default value =max()).verbose: The number of combinations will be printed in console ifTrue. (Default value = True).sequential: If True, two consecutive linkages will have a small variation.
- Returns:
Ensemble with up to n_results members.
- Raises:
OptimizationError – If parameters are invalid or no valid solution is found.
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
Linkageobject and should return a float (minimized). Usefloat('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:
objectPymoo 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 afinallyblock 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_minimizationor 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 aconcurrent.futures.ProcessPoolExecutorand evaluates one candidate per worker. Thelinkageand the objective functions must be picklable whenn_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 numbaSolverData).**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:
objectCrossover 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:
objectCombined mutation for mixed chromosomes.
Topology gene: with
topology_rate, mutate to a neighbor in the topology neighborhood graph.Dimension genes: Gaussian perturbation with
sigmafraction 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_sizeby: 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
centerfor population-based methods, orx0for local methods).- Parameters:
eval_func – The evaluation function, shared across all stages.
linkage – Linkage to be optimized.
stages – Sequence of
(optimizer_func, kwargs)tuples. Eachoptimizer_funcmust accepteval_funcandlinkageas its first two positional arguments.kwargsare passed through. Do not includeeval_funcorlinkageinkwargs.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:
objectA 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.
Run multi-topology synthesis to get candidate solutions.
Convert candidates to MixedChromosome seeds.
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:
objectCollection of non-dominated solutions from multi-objective optimization.
- Variables:
solutions (list[pylinkage.optimization.collections.pareto.ParetoSolution]) – List of Pareto-optimal solutions.
objective_names (tuple[str, ...]) – Names for each objective (for plotting).
- 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:
objectA 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.