Sensitivity & Tolerance Analysis

This tutorial covers how to analyze how manufacturing variations affect linkage behavior. Understanding sensitivity helps identify critical dimensions and assess whether a design is robust to manufacturing tolerances.

Overview

Pylinkage provides two complementary analysis tools:

  • Sensitivity Analysis: Measures how much each constraint dimension affects the output path. Identifies which dimensions are most critical.

  • Tolerance Analysis: Monte Carlo simulation that shows the statistical variation in output path given manufacturing tolerances.

These tools help answer questions like:

  • Which link length is most critical to the output path accuracy?

  • How much output variation should I expect with ±0.1mm tolerances?

  • Is my design robust enough for the manufacturing process?

Basic Setup

First, create a linkage to analyze:

import pylinkage as pl

# Create a four-bar linkage
crank = pl.Crank(
    x=1, y=0,
    joint0=(0, 0),
    angle=0.1,
    distance=1.0,
    name="crank"
)
coupler = pl.Revolute(
    x=3, y=1,
    joint0=crank,
    joint1=(4, 0),
    distance0=3.0,
    distance1=2.0,
    name="coupler"
)
linkage = pl.Linkage(
    joints=(crank, coupler),
    order=(crank, coupler),
)

Sensitivity Analysis

Basic Usage

Sensitivity analysis measures how each constraint dimension affects the output:

# Run sensitivity analysis with 1% perturbation
analysis = linkage.sensitivity_analysis(delta=0.01)

# View the most sensitive constraint
print(f"Most sensitive: {analysis.most_sensitive}")

# View all constraints ranked by sensitivity
for name, sensitivity in analysis.sensitivity_ranking:
    print(f"  {name}: {sensitivity:.4f}")

Output:

Most sensitive: coupler_dist1
  coupler_dist1: 0.0312
  coupler_dist2: 0.0287
  crank_radius: 0.0156

Understanding Constraint Names

Constraint names are auto-generated based on joint type and name:

  • crank_radius: Distance from crank pivot to crank end

  • coupler_dist1: Distance from first anchor to coupler joint

  • coupler_dist2: Distance from second anchor to coupler joint

The naming follows the pattern {joint_name}_{constraint_type}.

Analyzing Specific Output Joints

By default, the last joint is analyzed. You can specify a different output:

# Analyze sensitivity for the crank output
analysis = linkage.sensitivity_analysis(output_joint=0)

# Or by joint object
analysis = linkage.sensitivity_analysis(output_joint=crank)

Including Transmission Angle

For four-bar linkages, you can also track transmission angle sensitivity:

analysis = linkage.sensitivity_analysis(
    delta=0.01,
    include_transmission=True
)

print(f"Baseline transmission: {analysis.baseline_transmission:.1f}°")

# Transmission angles for each perturbation
if analysis.perturbed_transmission is not None:
    for name, trans in zip(analysis.constraint_names, analysis.perturbed_transmission):
        print(f"  {name}: {trans:.1f}°")

Exporting to DataFrame

For detailed analysis, export to a pandas DataFrame:

# Requires: pip install pylinkage[analysis]
df = analysis.to_dataframe()
print(df)

Output:

   constraint  sensitivity  perturbed_metric  perturbed_transmission
0  crank_radius     0.0156           0.00156                   89.5
1  coupler_dist1    0.0312           0.00312                   90.2
2  coupler_dist2    0.0287           0.00287                   89.8

Tolerance Analysis

Basic Usage

Tolerance analysis uses Monte Carlo simulation to assess manufacturing variability:

# Define tolerances for each constraint
tolerances = {
    "crank_radius": 0.1,     # +/- 0.1 mm
    "coupler_dist1": 0.2,    # +/- 0.2 mm
    "coupler_dist2": 0.2,    # +/- 0.2 mm
}

# Run Monte Carlo analysis
result = linkage.tolerance_analysis(
    tolerances=tolerances,
    n_samples=1000,
    seed=42  # For reproducibility
)

# View statistics
print(f"Mean deviation: {result.mean_deviation:.4f}")
print(f"Max deviation:  {result.max_deviation:.4f}")
print(f"Std deviation:  {result.std_deviation:.4f}")

Understanding the Results

The ToleranceAnalysis result contains:

  • nominal_path: Output path at nominal dimensions (n_steps, 2)

  • output_cloud: All Monte Carlo samples (n_samples, n_steps, 2)

  • mean_deviation: Average distance from nominal path

  • max_deviation: Worst-case deviation

  • std_deviation: Standard deviation of deviations

  • position_std: Per-position standard deviation (n_steps,)

Visualizing the Tolerance Cloud

Use plot_cloud() to visualize the output variation:

import matplotlib.pyplot as plt

# Create scatter plot of output paths
ax = result.plot_cloud(
    show_nominal=True,  # Show nominal path as red line
    alpha=0.1           # Transparency for sample points
)
plt.title("Output Path Tolerance Cloud")
plt.savefig("tolerance_cloud.png", dpi=150)
plt.show()

This creates a scatter plot showing:

  • Blue dots: Individual sample output paths

  • Red line: Nominal (ideal) output path

  • The spread indicates manufacturing variation

Selective Tolerance Analysis

You can analyze tolerance for specific constraints:

# Only analyze crank radius tolerance
result = linkage.tolerance_analysis(
    tolerances={"crank_radius": 0.1},
    n_samples=500
)

print(f"Crank-only max deviation: {result.max_deviation:.4f}")

Use in Optimization

Sensitivity as Fitness Penalty

Design linkages that are insensitive to manufacturing variation:

@pl.kinematic_minimization
def robust_linkage(loci, linkage=None, **kwargs):
    """Optimize for path shape while minimizing sensitivity."""

    # Path shape objective (e.g., bounding box)
    output_path = [step[-1] for step in loci]
    bbox = pl.bounding_box(output_path)
    path_error = compute_path_error(bbox)

    # Sensitivity penalty
    analysis = linkage.sensitivity_analysis(delta=0.01)
    max_sensitivity = max(analysis.sensitivities.values())

    # Combined objective: good path + low sensitivity
    return path_error + 10.0 * max_sensitivity

Tolerance-Based Constraints

Reject designs that exceed tolerance requirements:

from pylinkage.exceptions import UnbuildableError

@pl.kinematic_minimization
def tolerance_constrained(loci, linkage=None, **kwargs):
    """Optimize path, rejecting designs with excessive variation."""

    # Check tolerance
    tolerances = {"crank_radius": 0.1, "coupler_dist1": 0.2, "coupler_dist2": 0.2}
    result = linkage.tolerance_analysis(tolerances, n_samples=100)

    if result.max_deviation > 0.5:  # Reject if max deviation > 0.5mm
        raise UnbuildableError("Excessive tolerance variation")

    # Path objective
    return compute_path_score(loci)

Practical Guidelines

Perturbation Size

The delta parameter controls the relative perturbation size:

  • delta=0.01: 1% perturbation (recommended default)

  • delta=0.001: 0.1% for fine sensitivity analysis

  • delta=0.1: 10% for coarse/fast analysis

Smaller perturbations give more accurate local sensitivity but may be affected by numerical noise.

Sample Count

For tolerance analysis, the number of samples affects accuracy:

  • n_samples=100: Quick estimate (useful in optimization loops)

  • n_samples=1000: Good accuracy for design validation

  • n_samples=10000: High accuracy for final verification

Typical Workflow

  1. Design: Create linkage meeting path requirements

  2. Sensitivity: Run sensitivity_analysis() to identify critical dimensions

  3. Focus: Tighten tolerances on most sensitive constraints

  4. Validate: Run tolerance_analysis() to verify acceptable variation

  5. Iterate: If variation is too high, modify design and repeat

Example Complete Workflow

import pylinkage as pl
import matplotlib.pyplot as plt

# Create linkage
crank = pl.Crank(1, 0, joint0=(0, 0), angle=0.1, distance=1.0, name="crank")
coupler = pl.Revolute(3, 1, joint0=crank, joint1=(4, 0),
                      distance0=3.0, distance1=2.0, name="coupler")
linkage = pl.Linkage(joints=(crank, coupler), order=(crank, coupler))

# Step 1: Sensitivity analysis
print("=== Sensitivity Analysis ===")
sens = linkage.sensitivity_analysis(delta=0.01)
print(f"Most sensitive: {sens.most_sensitive}")
for name, val in sens.sensitivity_ranking:
    print(f"  {name}: {val:.4f}")

# Step 2: Tolerance analysis with realistic tolerances
print("\n=== Tolerance Analysis ===")
tolerances = {
    "crank_radius": 0.05,     # Tight tolerance (sensitive)
    "coupler_dist1": 0.1,     # Normal tolerance
    "coupler_dist2": 0.1,     # Normal tolerance
}
tol = linkage.tolerance_analysis(tolerances, n_samples=500, seed=42)

print(f"Mean deviation: {tol.mean_deviation:.4f}")
print(f"Max deviation:  {tol.max_deviation:.4f}")
print(f"Std deviation:  {tol.std_deviation:.4f}")

# Step 3: Visualize
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))

# Plot tolerance cloud
tol.plot_cloud(ax=ax1)
ax1.set_title("Tolerance Cloud")

# Plot per-position std
ax2.plot(tol.position_std)
ax2.set_xlabel("Simulation Step")
ax2.set_ylabel("Position Std Dev")
ax2.set_title("Per-Position Variation")
ax2.grid(True)

plt.tight_layout()
plt.savefig("tolerance_analysis.png", dpi=150)
plt.show()

API Reference

Next Steps