Advanced Optimization Techniques
This tutorial covers advanced optimization techniques for linkage mechanisms using pylinkage’s Particle Swarm Optimization (PSO) and grid search capabilities.
Overview of Optimization
Linkage optimization finds the best geometric parameters (distances, angles) to achieve a desired motion. Pylinkage provides:
Particle Swarm Optimization (PSO): Efficient global optimization using swarm intelligence
Trials and Errors (Grid Search): Exhaustive search over a parameter grid
Defining a Fitness Function
The fitness function evaluates how well a linkage configuration meets your goals.
Use the @kinematic_minimization or @kinematic_maximization decorators.
Basic Fitness Function
import pylinkage as pl
@pl.kinematic_minimization
def fitness(loci, **kwargs):
"""Evaluate linkage fitness.
:param loci: Joint positions for each simulation step.
Structure: tuple[tuple[tuple[float, float], ...], ...]
- Outer tuple: simulation steps
- Middle tuple: joints at each step
- Inner tuple: (x, y) coordinates
:param kwargs: Additional arguments (linkage, constraints, etc.)
:return: Fitness score (lower is better for minimization)
"""
# Get the locus (path) of the last joint
output_locus = [step[-1] for step in loci]
# Calculate your fitness metric
score = calculate_score(output_locus)
return score
The decorator handles:
Setting up the linkage with candidate constraints
Running the simulation
Catching
UnbuildableErrorand returning infinity
Working with Loci Data
Understanding the loci structure is key to writing good fitness functions:
@pl.kinematic_minimization
def analyze_loci(loci, **kwargs):
"""Example showing loci structure."""
# loci[step][joint] = (x, y)
# Get all positions of joint 0 (usually the crank)
crank_path = [step[0] for step in loci]
# Get all positions of the last joint (output)
output_path = [step[-1] for step in loci]
# Get positions at a specific step
positions_at_step_5 = loci[5] # All joint positions at step 5
# Calculate bounding box of output path
bbox = pl.bounding_box(output_path)
# bbox = (min_y, max_x, max_y, min_x)
return some_score
Example: Optimizing for Path Shape
Let’s optimize a four-bar linkage so its output traces a specific rectangular path:
import pylinkage as pl
def create_linkage():
"""Create the base linkage to optimize."""
crank = pl.Crank(
x=0, y=1,
joint0=(0, 0),
angle=0.31,
distance=1,
name="Crank"
)
output = pl.Revolute(
x=3, y=2,
joint0=crank,
joint1=(3, 0),
distance0=3,
distance1=1,
name="Output"
)
return pl.Linkage(
joints=(crank, output),
order=(crank, output),
)
@pl.kinematic_minimization
def rectangle_fitness(loci, **kwargs):
"""Minimize distance from a target rectangle."""
output_path = [step[-1] for step in loci]
bbox = pl.bounding_box(output_path)
# Target rectangle: min_y=0, max_x=5, max_y=2, min_x=3
target = (0, 5, 2, 3)
# Sum of squared differences
return sum((actual - target_val) ** 2
for actual, target_val in zip(bbox, target))
# Run optimization
linkage = create_linkage()
# Generate search bounds around current constraints
bounds = pl.generate_bounds(linkage.get_num_constraints())
results = pl.particle_swarm_optimization(
eval_func=rectangle_fitness,
linkage=linkage,
bounds=bounds,
)
# Best result
best_score, best_constraints, best_coords = results[0]
print(f"Best score: {best_score}")
# Apply best constraints and visualize
linkage.set_num_constraints(best_constraints)
pl.show_linkage(linkage)
Particle Swarm Optimization Parameters
Fine-tune PSO behavior for better results:
results = pl.particle_swarm_optimization(
eval_func=fitness_function,
linkage=linkage,
bounds=bounds,
# Number of particles in the swarm
n_particles=100, # More particles = better exploration, slower
# Number of iterations
iters=200, # More iterations = better convergence, slower
# Starting position (optional)
center=None, # Use current linkage constraints as center
# Number of dimensions (usually auto-detected)
dimensions=None,
# Order relation for optimization
order_relation=min, # min for minimization, max for maximization
)
Generating Bounds
The generate_bounds function creates search ranges around current values:
constraints = linkage.get_num_constraints()
# Example: [0.31, 1.0, 3.0, 1.0]
bounds = pl.generate_bounds(constraints)
# Returns: (lower_bounds, upper_bounds)
# Default: values * 0.5 to values * 2.0
# Custom bounds
bounds = pl.generate_bounds(
constraints,
min_ratio=0.8, # Lower bound = value * 0.8
max_ratio=1.2, # Upper bound = value * 1.2
)
# Or define bounds manually for precise control
bounds = (
[0.0, 0.5, 2.0, 0.5], # Lower bounds
[6.28, 2.0, 5.0, 2.0], # Upper bounds
)
Grid Search Optimization
For simpler problems or exhaustive search:
results = pl.trials_and_errors_optimization(
eval_func=fitness_function,
linkage=linkage,
divisions=20, # Points per dimension
order_relation=min, # min or max
)
# Note: Grid search is O(divisions^n) where n = number of constraints
# Use sparingly for high-dimensional problems
Multi-Objective Optimization
Combine multiple objectives in your fitness function:
@pl.kinematic_minimization
def multi_objective_fitness(loci, **kwargs):
"""Optimize for both path shape and mechanism size."""
output_path = [step[-1] for step in loci]
crank_path = [step[0] for step in loci]
# Objective 1: Match target bounding box
bbox = pl.bounding_box(output_path)
target = (0, 5, 2, 3)
shape_error = sum((a - t) ** 2 for a, t in zip(bbox, target))
# Objective 2: Minimize total mechanism size
all_points = [p for step in loci for p in step]
mech_bbox = pl.bounding_box(all_points)
mechanism_size = (mech_bbox[1] - mech_bbox[3]) * (mech_bbox[2] - mech_bbox[0])
# Weighted combination
return shape_error + 0.1 * mechanism_size
Constraint Preservation
Sometimes you want to optimize only certain constraints while keeping others fixed:
@pl.kinematic_minimization
def constrained_fitness(loci, linkage=None, constraints=None, **kwargs):
"""Fitness function that enforces additional constraints."""
# Penalize if crank arm (constraint 1) is too short
if constraints[1] < 0.5:
return float('inf')
# Normal fitness calculation
output_path = [step[-1] for step in loci]
return calculate_path_score(output_path)
Optimizing Initial Positions
Sometimes the issue isn’t the constraints but the initial joint positions:
# Save and restore initial positions
init_coords = linkage.get_coords()
# Optimize
results = pl.particle_swarm_optimization(
eval_func=fitness,
linkage=linkage,
bounds=bounds,
)
# Apply results
linkage.set_num_constraints(results[0][1])
linkage.set_coords(init_coords) # Restore initial positions
Visualizing Optimization Progress
Track optimization progress with the strider example pattern:
history = []
def tracking_fitness(linkage, constraints, initial_positions):
"""Wrapper that records optimization history."""
# Your actual fitness calculation
score = my_fitness(linkage, constraints, initial_positions)
history.append((score, list(constraints), initial_positions))
return score
# Run optimization with tracking
results = pl.particle_swarm_optimization(
lambda *args: tracking_fitness(*args),
linkage,
bounds=bounds,
n_particles=50,
iters=100,
)
# Analyze history
scores = [h[0] for h in history]
print(f"Best score: {min(scores)}")
print(f"Score improvement: {scores[0]} -> {scores[-1]}")
Async Optimization
For long-running optimizations, use the async version with progress callbacks:
import asyncio
async def optimize_with_progress():
def on_progress(iteration, best_score):
print(f"Iteration {iteration}: best = {best_score}")
results = await pl.particle_swarm_optimization_async(
eval_func=fitness,
linkage=linkage,
bounds=bounds,
progress_callback=on_progress,
)
return results
# Run async optimization
results = asyncio.run(optimize_with_progress())
Troubleshooting
Optimization converges to poor solutions:
Increase
n_particlesfor better explorationWiden the search bounds
Check if your fitness function correctly penalizes bad configurations
Many configurations are unbuildable:
Your bounds may include geometrically impossible regions
Narrow the bounds around known-good configurations
The
@kinematic_minimizationdecorator returnsinffor unbuildable configs
Optimization is too slow:
Reduce
n_particlesoritersUse coarser simulation (fewer steps in
linkage.step())Consider grid search for low-dimensional problems
Next Steps
See Example Scripts for complete optimization examples
Check
pylinkage.optimizationfor API detailsThe strider example demonstrates advanced PSO visualization techniques