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: .. code-block:: python 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: .. code-block:: python # 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: .. code-block:: text 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: .. code-block:: python # 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: .. code-block:: python 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: .. code-block:: python # Requires: pip install pylinkage[analysis] df = analysis.to_dataframe() print(df) Output: .. code-block:: text 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: .. code-block:: python # 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: .. code-block:: python 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: .. code-block:: python # 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: .. code-block:: python @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: .. code-block:: python 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 ------------------------- .. code-block:: python 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 ------------- - :py:class:`pylinkage.linkage.SensitivityAnalysis` - Sensitivity analysis results - :py:class:`pylinkage.linkage.ToleranceAnalysis` - Tolerance analysis results - :py:func:`pylinkage.linkage.analyze_sensitivity` - Sensitivity analysis function - :py:func:`pylinkage.linkage.analyze_tolerance` - Tolerance analysis function - :py:meth:`pylinkage.linkage.Linkage.sensitivity_analysis` - Convenience method - :py:meth:`pylinkage.linkage.Linkage.tolerance_analysis` - Convenience method Next Steps ---------- - See :doc:`advanced_optimization` for optimization techniques - See :doc:`kinematics_optimization` for velocity/acceleration analysis - Explore :py:mod:`pylinkage.linkage` for the complete linkage API