pylinkage.synthesis package
Submodules
pylinkage.synthesis.burmester module
Burmester theory for planar mechanism synthesis.
Burmester theory provides a geometric method for synthesizing planar linkages that pass through a set of precision positions. The key concepts:
Circle Points: Points on the moving body whose world-frame positions at all precision poses lie on a single circle.
Center Points: The centers of those circles — these become the fixed pivots (ground joints) of the linkage.
For exact synthesis:
3 positions: Every body-frame point is a valid circle point (∞ solutions)
4 positions: Circle points form a curve (circular cubic); we find points where all 4 world positions are concyclic
5 positions: Typically 0–6 discrete solutions (highly constrained)
Two compatible dyads (circle point + center point pairs) define a four-bar linkage: the circle points are coupler attachment joints, the center points are ground pivots.
References
Sandor & Erdman, “Advanced Mechanism Design: Analysis and Synthesis”
McCarthy, J.M. “Geometric Design of Linkages” (2nd ed.)
Bottema & Roth, “Theoretical Kinematics”
- pylinkage.synthesis.burmester.complex_to_point(z: complex) tuple[float, float]
Convert complex number to Cartesian point.
- pylinkage.synthesis.burmester.compute_all_poles(poses: list[Pose]) ndarray[tuple[Any, ...], dtype[complex128]]
Compute all poles between pose pairs.
For n positions, computes n*(n-1)/2 poles P_ij for all i < j. The poles are ordered as: P12, P13, …, P1n, P23, …, P(n-1)n
- Parameters:
poses – List of Pose objects.
- Returns:
Array of complex pole locations.
- Raises:
ValueError – If fewer than 2 poses provided.
- pylinkage.synthesis.burmester.compute_circle_point_curve(poses: list[Pose], n_samples: int = 72) BurmesterCurves
Compute the circle point and center point curves.
This is the main entry point for Burmester theory. Given a set of precision positions (poses), it computes all valid dyad attachment points.
For 3 positions: Returns continuous parametric curves For 4 positions: Returns up to 6 discrete Ball’s points For 5 positions: Returns 0-2 discrete solutions (over-constrained)
- Parameters:
poses – List of Pose objects (3, 4, or 5 positions).
n_samples – Number of samples for continuous curves (3 positions).
- Returns:
BurmesterCurves containing sampled/discrete circle and center curves.
- Raises:
ValueError – If number of poses is not 3, 4, or 5.
Example
>>> poses = [Pose(0, 0, 0), Pose(1, 1, 0.5), Pose(2, 0, 1.0)] >>> curves = compute_circle_point_curve(poses) >>> dyad = curves.get_dyad(0)
- pylinkage.synthesis.burmester.compute_pole(pos1: Pose, pos2: Pose) complex
Compute the pole (instant center) between two positions.
The pole is the point that remains stationary during the finite displacement from pos1 to pos2. It is found at the intersection of perpendicular bisectors of corresponding point trajectories.
For a pure rotation, the pole is the center of rotation. For a translation, the pole is at infinity.
- Parameters:
pos1 – First position (x, y, angle).
pos2 – Second position (x, y, angle).
- Returns:
Complex number representing the pole location. Returns complex infinity for pure translation.
- Raises:
ValueError – If positions are identical (no displacement).
- pylinkage.synthesis.burmester.compute_relative_pole(p12: complex, p13: complex, theta12: float, theta13: float) complex
Compute relative pole P23 from P12, P13 and rotation angles.
Uses the theorem of three poles: P12, P23, P13 are collinear (on the “pole triangle” for three positions).
- Parameters:
p12 – Pole between positions 1 and 2.
p13 – Pole between positions 1 and 3.
theta12 – Rotation angle from position 1 to 2.
theta13 – Rotation angle from position 1 to 3.
- Returns:
Pole P23 between positions 2 and 3.
- pylinkage.synthesis.burmester.point_to_complex(p: tuple[float, float]) complex
Convert Cartesian point to complex representation.
- pylinkage.synthesis.burmester.select_compatible_dyads(curves: BurmesterCurves, min_link_length: float = 0.01, max_link_length: float = 1000.0, ground_constraint: tuple[tuple[float, float], tuple[float, float]] | None = None, ground_tolerance: float = 0.1, max_pairs: int | None = None) list[tuple[BurmesterDyad, BurmesterDyad]]
Select pairs of dyads that form valid four-bar linkages.
Two dyads form a valid four-bar if: 1. Their center points are distinct (ground pivots) 2. Their circle points are distinct (coupler attachments) 3. Link lengths are within acceptable range 4. The resulting 4-bar can assemble
- Parameters:
curves – Burmester curves from which to select dyads.
min_link_length – Minimum acceptable link length.
max_link_length – Maximum acceptable link length.
ground_constraint – Optional (A, D) positions for ground pivots.
ground_tolerance – Tolerance for matching ground constraint.
max_pairs – Maximum number of pairs to return (early termination).
- Returns:
List of (dyad_left, dyad_right) pairs forming valid 4-bars.
pylinkage.synthesis.conversion module
Conversion between synthesis results and Linkage objects.
This module provides functions to convert raw mathematical solutions from synthesis algorithms into pylinkage Linkage objects that can be simulated and visualized.
- pylinkage.synthesis.conversion.fourbar_from_lengths(crank_length: float, coupler_length: float, rocker_length: float, ground_length: float, ground_pivot_a: Point2D = (0.0, 0.0), initial_crank_angle: float = 0.0, iterations: int = 360, name: str = 'fourbar') SimLinkage
Create a four-bar linkage from link lengths.
Convenience function to create a four-bar linkage given only the link lengths. The linkage is placed with ground pivot A at the specified position.
Kinematic chain:
A ─── crank ─── B ─── coupler ─── C ─── rocker ─── D (input)- Parameters:
crank_length – Length of crank (a).
coupler_length – Length of coupler (b).
rocker_length – Length of rocker (c).
ground_length – Length of ground link (d).
ground_pivot_a – Position of first ground pivot.
initial_crank_angle – Initial crank angle in radians.
iterations – Number of simulation steps per rotation.
name – Name for the linkage.
- Returns:
Linkage built from the component/actuator/dyad API.
- Raises:
ValueError – If the mechanism cannot be assembled with the given link lengths at the initial crank angle.
Example
>>> linkage = fourbar_from_lengths(1.0, 3.0, 3.0, 4.0) >>> len(list(linkage.step(iterations=100))) 100
- pylinkage.synthesis.conversion.linkage_to_synthesis_params(linkage: SimLinkage) FourBarSolution
Extract synthesis parameters from an existing four-bar linkage.
Analyzes a SimLinkage object to extract the four-bar geometry parameters as a FourBarSolution.
- Parameters:
linkage – A four-bar linkage built with the component/actuator/dyad API.
- Returns:
FourBarSolution tuple with geometry parameters.
- Raises:
ValueError – If linkage is not a valid four-bar.
- pylinkage.synthesis.conversion.nbar_solution_to_linkage(solution: NBarSolution, name: str = 'synthesized', iterations: int = 360) SimLinkage
Convert an NBarSolution to a Linkage object.
Loads the topology from the catalog, decomposes it to determine solving order, and maps joint positions from the solution onto Static, Crank, and Revolute joints.
For six-bars, delegates to the specialized six-bar converter in
six_bar.- Parameters:
solution – NBarSolution with topology_id and joint_positions.
name – Name for the linkage.
iterations – Number of simulation steps per rotation.
- Returns:
Linkage object ready for simulation.
- Raises:
ValueError – If topology_id is not found in the catalog.
- pylinkage.synthesis.conversion.solution_to_linkage(solution: FourBarSolution, name: str = 'synthesized', iterations: int = 360) SimLinkage
Convert a single FourBarSolution to a Linkage object.
Creates a four-bar linkage with:
Two Ground components as ground pivots
One Crank as motor input, or an ArcCrank sweeping
solution.arc_limitswhen those are setOne RRRDyad connecting crank to rocker
Optionally, a FixedDyad for the coupler point that traces the target path (only if
solution.coupler_pointis set)
arc_limitsare crank angles relative to the ground line, from pivot A towards pivot D, ascrank_angle_limits()returns them. Whichever side of the ground line the crank starts on, the arc is placed there. Set them for a solution whose crank cannot turn fully (a double-rocker, or a non-Grashof linkage); a plain Crank would otherwise drive the linkage into an unbuildable position.- Parameters:
solution – FourBarSolution containing geometry.
name – Name for the linkage.
iterations – Number of simulation steps per rotation (per sweep of the arc, for an ArcCrank).
- Returns:
SimLinkage object ready for simulation.
- pylinkage.synthesis.conversion.solutions_to_linkages(solutions: list[FourBarSolution], synthesis_type: SynthesisType, iterations: int = 360) list[SimLinkage]
Convert multiple solutions to Linkage objects.
- Parameters:
solutions – List of FourBarSolution objects.
synthesis_type – Type of synthesis (for naming).
iterations – Number of simulation steps per rotation.
- Returns:
List of SimLinkage objects.
- pylinkage.synthesis.conversion.stephenson_from_lengths(crank: float, coupler: float, rocker: float, link4: float, link5: float, link6: float, ground_length: float, ground_pivot_a: Point2D = (0.0, 0.0), initial_crank_angle: float = 0.0, iterations: int = 360, name: str = 'stephenson') SimLinkage
Create a Stephenson six-bar linkage from link lengths.
A Stephenson six-bar has two four-bar loops where the second loop branches from the coupler joint C and the ground pivot D, closing back to the crank output B. This separates the two ternary links (unlike the Watt, where they are adjacent).
Kinematic chain:
A ─── crank ─── B ─── coupler ─── C ─── rocker ─── D (input) │ │ │ │ └── link4 ───┐ │ └── link6 ── F ── link5 ── E ───┘ │ └─────────┘Loop 1: A → B → C → D (four-bar: crank + coupler + rocker)
Loop 2: C,D → E, then E → F → B (second chain branches from coupler C and ground D, closing back to crank output B)
The topological difference from Watt: in the Watt chain, E depends on (B, C) — both joints of the coupler link, making the ternary links adjacent. Here E depends on (C, D) — the coupler endpoint and ground, separating the ternary links.
- Parameters:
crank – Length of input crank (A–B).
coupler – Length of coupler (B–C).
rocker – Length of rocker (C–D).
link4 – Distance from C to E.
link5 – Distance from D to E.
link6 – Distance from E to F.
ground_length – Distance between ground pivots A and D.
ground_pivot_a – Position of first ground pivot (A).
initial_crank_angle – Starting crank angle in radians.
iterations – Simulation steps per full rotation.
name – Name for the linkage.
- Returns:
Linkage built from the component/actuator/dyad API.
- Raises:
ValueError – If the mechanism cannot be assembled with the given link lengths at the initial crank angle.
Example
>>> linkage = stephenson_from_lengths( ... crank=0.8, coupler=3.5, rocker=3.0, ... link4=2.0, link5=2.5, link6=3.0, ... ground_length=4.0, ... ) >>> len(list(linkage.step(iterations=100))) 100
- pylinkage.synthesis.conversion.watt_from_lengths(crank: float, coupler1: float, rocker1: float, link4: float, link5: float, rocker2: float, ground_length: float, ground_pivot_a: Point2D = (0.0, 0.0), initial_crank_angle: float = 0.0, iterations: int = 360, name: str = 'watt') SimLinkage
Create a Watt six-bar linkage from link lengths.
A Watt six-bar has two four-bar loops sharing the crank output. The crank output (B) connects to two separate dyads, both of which close back to the second ground pivot (D).
Kinematic chain:
A ─── crank ─── B ─── coupler1 ─── C ─── rocker1 ─── D (input) │ │ └─── link4 ─── E ─── link5 ──── rocker2 ─── DIn Assur group terms: the crank drives two cascaded RRR dyads.
Loop 1: A → B → C → D (four-bar: crank + coupler1 + rocker1)
Loop 2: B → E → F → D (second chain: link4 + link5 + rocker2, where E is constrained by B and C)
- Parameters:
crank – Length of input crank (A–B).
coupler1 – Length of first coupler link (B–C).
rocker1 – Length of first rocker (C–D).
link4 – Distance from B to E (first arm of second loop).
link5 – Distance from C to E (second arm, constraining E).
rocker2 – Distance from E to D (closing link back to ground).
ground_length – Distance between ground pivots A and D.
ground_pivot_a – Position of first ground pivot (A).
initial_crank_angle – Starting crank angle in radians.
iterations – Simulation steps per full rotation.
name – Name for the linkage.
- Returns:
Linkage built from the component/actuator/dyad API.
- Raises:
ValueError – If the mechanism cannot be assembled with the given link lengths at the initial crank angle.
Example
>>> linkage = watt_from_lengths( ... crank=1.5, coupler1=4.0, rocker1=3.5, ... link4=3.0, link5=2.5, rocker2=3.0, ... ground_length=6.0, ... ) >>> len(list(linkage.step(iterations=100))) 100
pylinkage.synthesis.core module
Core synthesis concepts and data structures.
This module provides the fundamental data structures for mechanism synthesis: - SynthesisProblem: Definition of a synthesis problem - SynthesisResult: Container for synthesis solutions - BurmesterDyad: A kinematic dyad from Burmester theory - BurmesterCurves: Circle point and center point curves
- class pylinkage.synthesis.core.BurmesterCurves(circle_curve: ndarray[tuple[Any, ...], dtype[complex128]], center_curve: ndarray[tuple[Any, ...], dtype[complex128]], parameter: ndarray[tuple[Any, ...], dtype[float64]], is_discrete: bool = False)
Bases:
objectCircle point and center point curves from Burmester theory.
For a set of precision positions, these curves contain all possible dyad attachment points. The circle points lie on the moving body, and the center points are the corresponding fixed pivots.
For 3 precision positions: continuous parametric curves For 4 precision positions: up to 6 discrete points (Ball’s points) For 5 precision positions: typically 0-2 discrete solutions
The curves are parameterized so that circle_curve[i] and center_curve[i] form a valid dyad pair.
- Variables:
circle_curve (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.complex128]]) – Array of circle point positions (complex).
center_curve (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.complex128]]) – Array of corresponding center point positions.
parameter (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – Parameter values along the curves.
is_discrete (bool) – True if solutions are discrete points, not curves.
- center_curve: ndarray[tuple[Any, ...], dtype[complex128]]
- circle_curve: ndarray[tuple[Any, ...], dtype[complex128]]
- filter_finite() BurmesterCurves
Remove points with infinite or NaN coordinates.
- Returns:
New BurmesterCurves with only finite points.
- get_all_dyads() list[BurmesterDyad]
Get all dyads from the curves.
- Returns:
List of BurmesterDyad objects, one per curve point.
- get_dyad(index: int) BurmesterDyad
Get dyad at specific parameter index.
- Parameters:
index – Index into the curve arrays.
- Returns:
BurmesterDyad at the specified index.
- is_discrete: bool = False
- parameter: ndarray[tuple[Any, ...], dtype[float64]]
- sample(n_samples: int) BurmesterCurves
Resample curves to specified number of points.
Only meaningful for continuous curves (is_discrete=False).
- Parameters:
n_samples – Number of samples desired.
- Returns:
New BurmesterCurves with resampled points.
- class pylinkage.synthesis.core.BurmesterDyad(circle_point: complex, center_point: complex)
Bases:
objectA kinematic dyad (two-link chain) from Burmester theory.
In Burmester theory, a dyad connects a moving point on the coupler (circle point) to a fixed pivot on the frame (center point) through a single revolute joint.
Two dyads combined form a four-bar linkage: the circle points define attachment points on the coupler, and the center points define the fixed ground pivots.
- Variables:
circle_point (complex) – Point on the moving body (complex representation).
center_point (complex) – Fixed pivot point (complex representation).
- center_point: complex
- circle_point: complex
- property link_length: float
Length of the dyad link.
- to_cartesian() tuple[tuple[float, float], tuple[float, float]]
Convert to Cartesian coordinate tuples.
- Returns:
Tuple of (circle_point_xy, center_point_xy).
- to_dyad_solution() DyadSolution
Convert to DyadSolution named tuple.
- class pylinkage.synthesis.core.SynthesisProblem(synthesis_type: SynthesisType, precision_points: list[tuple[float, float]]=<factory>, angle_pairs: list[tuple[float, float]]=<factory>, poses: list[Pose] = <factory>, ground_pivot_a: tuple[float, float] | None=None, ground_pivot_d: tuple[float, float] | None=None, ground_length: float | None = None)
Bases:
objectDefinition of a synthesis problem.
A synthesis problem specifies the requirements that a mechanism must satisfy. Depending on the synthesis type, different inputs are required:
FUNCTION: angle_pairs specifying input/output angle relationships
PATH: precision_points the coupler must pass through
MOTION: poses specifying body positions and orientations
- Variables:
synthesis_type (pylinkage.synthesis._types.SynthesisType) – Type of synthesis (FUNCTION, PATH, or MOTION).
precision_points (list[tuple[float, float]]) – Target points for path generation.
angle_pairs (list[tuple[float, float]]) – Input/output angle pairs for function generation.
poses (list[pylinkage.synthesis._types.Pose]) – Target poses for motion generation.
ground_pivot_a (tuple[float, float] | None) – Optional fixed position of first ground pivot.
ground_pivot_d (tuple[float, float] | None) – Optional fixed position of second ground pivot.
ground_length (float | None) – Optional fixed ground link length.
- angle_pairs: list[tuple[float, float]]
- ground_length: float | None = None
- ground_pivot_a: tuple[float, float] | None = None
- ground_pivot_d: tuple[float, float] | None = None
- property num_precision_positions: int
Number of precision positions specified.
- poses: list[Pose]
- precision_points: list[tuple[float, float]]
- synthesis_type: SynthesisType
- class pylinkage.synthesis.core.SynthesisResult(solutions: list, raw_solutions: list[FourBarSolution], problem: SynthesisProblem, warnings: list[str] = <factory>, branch_info: list[dict[str, int]] = <factory>, _ensemble: Ensemble | None = None)
Bases:
objectResult of a synthesis operation.
Contains the synthesized linkages along with metadata about the synthesis process including warnings, raw mathematical solutions, and branch information.
The primary way to access the solutions is via the
ensembleproperty, which returns anEnsemblefor batch simulation, ranking, filtering, and visualization.- Variables:
solutions (list) – List of valid Linkage objects.
raw_solutions (list[FourBarSolution]) – Raw mathematical solutions before filtering.
problem (SynthesisProblem) – The original synthesis problem.
warnings (list[str]) – Any warnings generated during synthesis.
branch_info (list[dict[str, int]]) – Branch selection info for each solution.
- branch_info: list[dict[str, int]]
- property ensemble: Ensemble
Solutions as an
Ensemble.Built lazily on first access and cached. The Ensemble carries link lengths from
raw_solutionsas score columns (crank_length,coupler_length,rocker_length,ground_length) when available.- Raises:
ValueError – If no valid solutions exist.
- problem: SynthesisProblem
- raw_solutions: list[FourBarSolution]
- solutions: list
- warnings: list[str]
pylinkage.synthesis.function_generation module
Function generation synthesis for four-bar linkages.
Function generation synthesizes a linkage where the input crank angle (theta2) produces a desired output rocker angle (theta4). This is based on Freudenstein’s equation, which relates the input/output angles to the link length ratios.
- The Freudenstein equation:
R1*cos(theta4) - R2*cos(theta2) + R3 - cos(theta2 - theta4) = 0
- Where:
R1 = d/a (ground/crank ratio) R2 = d/c (ground/rocker ratio) R3 = (a^2 - b^2 + c^2 + d^2) / (2*a*c)
And a, b, c, d are crank, coupler, rocker, ground lengths respectively.
Common applications: - Mechanical computing devices - Coordinated motion mechanisms - Transfer functions in machinery
References
Freudenstein, F. “Approximate Synthesis of Four-Bar Linkages” (1955)
Sandor & Erdman, Chapter 3: “Analytical Linkage Synthesis”
- pylinkage.synthesis.function_generation.coefficients_to_link_lengths(R1: float, R2: float, R3: float, ground_length: float = 1.0) tuple[float, float, float, float]
Convert Freudenstein coefficients to link lengths.
Given R1, R2, R3 and choosing ground length d, solve for the link lengths a (crank), b (coupler), c (rocker).
The relationships are:
R1 = d/a => a = d/R1 R2 = d/c => c = d/R2 R3 = (a^2 - b^2 + c^2 + d^2) / (2*a*c) => b^2 = a^2 + c^2 + d^2 - 2*a*c*R3
- Parameters:
R1 – d/a ratio.
R2 – d/c ratio.
R3 – Combined ratio.
ground_length – Desired ground link length d.
- Returns:
Tuple (crank_length, coupler_length, rocker_length, ground_length).
- Raises:
ValueError – If coefficients produce invalid link lengths.
- pylinkage.synthesis.function_generation.freudenstein_equation(theta2: float, theta4: float, R1: float, R2: float, R3: float) float
Evaluate Freudenstein’s equation.
The Freudenstein equation relates input angle theta2 (crank) to output angle theta4 (rocker) through the link length ratios R1, R2, R3.
- Parameters:
theta2 – Input crank angle in radians.
theta4 – Output rocker angle in radians.
R1 – d/a ratio (ground/crank).
R2 – d/c ratio (ground/rocker).
R3 – (a^2 - b^2 + c^2 + d^2) / (2*a*c) ratio.
- Returns:
Residual of the Freudenstein equation (0 if satisfied exactly).
- pylinkage.synthesis.function_generation.function_generation(angle_pairs: list[tuple[float, float]], ground_length: float = 1.0, ground_pivot_a: tuple[float, float] = (0.0, 0.0), require_grashof: bool = True, require_crank_rocker: bool = False) SynthesisResult
Synthesize a four-bar for function generation.
Given input/output angle pairs, find four-bar linkages where the crank angle theta2 produces rocker angle theta4 according to the Freudenstein equation.
For 3 angle pairs: Unique exact solution (if physically valid) For 4+ angle pairs: Least-squares approximate solution
- Parameters:
angle_pairs – List of (input_angle, output_angle) in radians. At least 3 pairs required.
ground_length – Desired ground link length (scaling factor).
ground_pivot_a – Position of input (crank) ground pivot.
require_grashof – If True, reject non-Grashof solutions.
require_crank_rocker – If True, only accept crank-rocker type.
- Returns:
SynthesisResult containing valid linkages.
- Raises:
ValueError – If fewer than 3 angle pairs provided.
Example
>>> pairs = [(0, 0), (math.pi/6, math.pi/4), (math.pi/3, math.pi/2)] >>> result = function_generation(pairs) >>> for linkage in result.solutions: ... print(f"Crank: {linkage.joints[2].r:.3f}")
- pylinkage.synthesis.function_generation.solve_freudenstein_3_positions(angle_pairs: list[tuple[float, float]]) tuple[float, float, float]
Solve for Freudenstein coefficients from 3 precision positions.
With exactly 3 angle pairs, we have a linear system of 3 equations in 3 unknowns (R1, R2, R3). This gives an exact solution.
- Parameters:
angle_pairs – Exactly 3 (theta2, theta4) pairs in radians.
- Returns:
Tuple (R1, R2, R3) of Freudenstein coefficients.
- Raises:
ValueError – If not exactly 3 angle pairs provided.
scipy.linalg.LinAlgError – If the system is singular.
Example
>>> pairs = [(0.0, 0.0), (0.5, 0.6), (1.0, 1.1)] >>> R1, R2, R3 = solve_freudenstein_3_positions(pairs)
- pylinkage.synthesis.function_generation.solve_freudenstein_least_squares(angle_pairs: list[tuple[float, float]]) tuple[float, float, float, float]
Solve over-determined Freudenstein system via least squares.
For more than 3 angle pairs, find the coefficients that minimize the sum of squared residuals.
- Parameters:
angle_pairs – List of (theta2, theta4) pairs (4 or more).
- Returns:
Tuple (R1, R2, R3, residual_norm) where residual_norm is the fitting error.
- Raises:
ValueError – If fewer than 3 angle pairs provided.
- pylinkage.synthesis.function_generation.verify_function_generation(linkage: Linkage, angle_pairs: list[AnglePair], tolerance: float = 0.05) tuple[bool, list[float]]
Verify that a linkage satisfies function generation requirements.
Simulates the linkage at each input angle and checks if the output angle matches the specification.
- Parameters:
linkage – The linkage to verify.
angle_pairs – Expected (input_angle, output_angle) pairs.
tolerance – Maximum acceptable angle error in radians.
- Returns:
Tuple of (all_satisfied, list of actual errors for each pair).
pylinkage.synthesis.generalized module
Generalized N-bar synthesis via Assur decomposition.
For any topology from the catalog, decomposes into Assur groups and synthesizes each group sequentially. Each dyad group is a local Burmester sub-problem; triad groups use Newton-Raphson optimization.
This generalizes the approach in six_bar
to arbitrary topologies (eight-bars and beyond).
- pylinkage.synthesis.generalized.generalized_synthesis(topology: CatalogEntry, precision_points: list[PrecisionPoint], partition_strategy: str = 'greedy', max_solutions: int = 10, n_orientation_samples: int = 24) list[NBarSolution]
Synthesize a linkage of arbitrary topology for path generation.
Retrieves the Assur decomposition of the given topology, partitions precision points across groups, and synthesizes each group sequentially using Burmester (for dyads) or optimization (for triads).
- Parameters:
topology – CatalogEntry from the topology catalog.
precision_points – Target (x, y) points.
partition_strategy –
"greedy"or"exhaustive".max_solutions – Maximum solutions to return.
n_orientation_samples – Orientation search density.
- Returns:
List of NBarSolution objects (unranked).
pylinkage.synthesis.motion_generation module
Motion generation synthesis for four-bar linkages.
Motion generation (rigid body guidance) synthesizes a linkage where a body attached to the coupler moves through specified poses (position + orientation). This is the most constrained form of synthesis because both position and orientation are prescribed.
Applications: - Robot end-effector positioning - Pick-and-place with orientation control - Assembly operations - Door/hatch mechanisms
Constraints: - 3 poses: Continuous curve of solutions (circular cubic) - 4 poses: Up to 6 discrete solutions (Ball’s points) - 5 poses: Typically 0-2 solutions (over-constrained) - 6+ poses: Almost always no solution exists
References
McCarthy, Chapter 6: “Spherical and Planar Four-Bar Motion”
Sandor & Erdman, Chapter 5: “Path, Motion, and Function Generation”
Bottema & Roth, “Theoretical Kinematics”
- pylinkage.synthesis.motion_generation.motion_generation(poses: list[Pose], ground_pivot_a: tuple[float, float] | None = None, ground_pivot_d: tuple[float, float] | None = None, max_solutions: int | None = 10, require_grashof: bool = True, require_crank_rocker: bool = False) SynthesisResult
Synthesize a four-bar for motion generation (rigid body guidance).
Find four-bar linkages where a body attached to the coupler passes through the specified poses (position + orientation).
This is the most constrained synthesis problem because both position and orientation are specified at each precision position.
- Parameters:
poses – List of Pose objects specifying body positions and orientations. Typically 3-5 poses (5 is maximum for exact synthesis).
ground_pivot_a – Optional fixed position for left ground pivot.
ground_pivot_d – Optional fixed position for right ground pivot.
max_solutions – Maximum number of solutions to return (None for all).
require_grashof – If True, reject non-Grashof solutions.
require_crank_rocker – If True, only accept crank-rocker type.
- Returns:
SynthesisResult containing valid linkages.
Example
>>> from pylinkage.synthesis import Pose, motion_generation >>> poses = [ ... Pose(0, 0, 0), ... Pose(1, 1, 0.5), ... Pose(2, 0.5, 1.0), ... ] >>> result = motion_generation(poses) >>> print(f"Found {len(result.solutions)} solutions")
- pylinkage.synthesis.motion_generation.motion_generation_3_poses(poses: list[Pose], n_samples: int = 36, max_solutions: int | None = 50) SynthesisResult
Specialized synthesis for exactly 3 poses.
With 3 poses, there is a continuous curve of solutions. This function samples the curve and returns multiple linkages.
- Parameters:
poses – Exactly 3 Pose objects.
n_samples – Number of samples along the solution curve.
max_solutions – Maximum raw solutions to collect (early termination).
- Returns:
SynthesisResult with sampled solutions from the curve.
pylinkage.synthesis.multi_topology module
Multi-topology synthesis: search across all compatible topologies.
Given precision points, tries every topology from the catalog (four-bar, six-bar, eight-bar) and returns solutions ranked by quality metrics. This is the top-level entry point for topology-aware synthesis.
Example:
from pylinkage.synthesis.multi_topology import synthesize
points = [(0, 0), (1, 2), (2, 3), (3, 2), (4, 0)]
solutions = synthesize(points, max_links=6)
for sol in solutions[:3]:
print(f"{sol.topology_entry.name}: score={sol.metrics.overall_score:.3f}")
sol.linkage.show()
- pylinkage.synthesis.multi_topology.rank_solutions(candidates: list[TopologySolution], weights: dict[str, float] | None = None) list[TopologySolution]
Rank solutions by weighted quality metrics.
- Default weights prioritize:
Path accuracy (0.40)
Transmission angle (0.25)
Link ratio (0.10)
Grashof bonus (0.10)
Simplicity (0.10)
Compactness (0.05)
- Parameters:
candidates – Unranked solutions.
weights – Optional custom weight dict.
- Returns:
Solutions sorted by
overall_score(ascending = best first).
- pylinkage.synthesis.multi_topology.synthesize(precision_points: list[tuple[float, float]], max_links: int = 8, synthesis_type: SynthesisType = SynthesisType.PATH, max_solutions_per_topology: int = 5, max_total_solutions: int = 20, n_orientation_samples: int = 24) list[TopologySolution]
Synthesize linkages across all compatible topologies.
Tries every topology from the built-in catalog (up to
max_links) and returns solutions ranked by quality metrics (path accuracy, transmission angle, link ratios, compactness, simplicity).For four-bars, delegates to the existing
path_generation(). For six-bars, delegates tosix_bar_path_generation(). For eight-bars and beyond, delegates togeneralized_synthesis().- Parameters:
precision_points – Target (x, y) points.
max_links – Maximum number of links to consider (4, 6, or 8).
synthesis_type –
PATH,FUNCTION, orMOTION. Currently only PATH is supported for multi-topology.max_solutions_per_topology – Max solutions per topology type.
max_total_solutions – Total solutions to return.
n_orientation_samples – Orientation search density.
- Returns:
List of TopologySolution ranked by
overall_score(best first).- Raises:
NotImplementedError – If synthesis_type is not PATH.
Example
>>> solutions = synthesize([(0,1), (1,2), (2,1.5), (3,0), (4,1)]) >>> print(f"Best: {solutions[0].topology_entry.name}")
pylinkage.synthesis.path_generation module
Path generation synthesis for four-bar linkages.
Path generation synthesizes a linkage where a point on the coupler (the coupler point) traces a curve passing through specified precision points. This is used for:
Walking mechanisms (leg tip trajectories)
Pick-and-place motions
General path-following applications
The coupler curve of a four-bar is a tricircular sextic (degree 6 algebraic curve), which can produce a wide variety of shapes including loops, cusps, and approximate straight lines.
Path generation is more complex than function generation because the coupler orientation at each precision point is not specified (free variables). We must search over possible orientations.
For exact synthesis: - 4 precision points with prescribed timing: Exact solution possible - 4-5 points without timing: Search over orientation space - 6+ points: Over-constrained, typically no exact solution
References
Wampler et al., “Complete Solution of the Nine-Point Path Synthesis”
McCarthy, Chapter 7: “Path Synthesis”
Sandor & Erdman, Chapter 5: “Path, Motion, and Function Generation”
- pylinkage.synthesis.path_generation.DEFAULT_ORIENTATION_RESOLUTION = 6
Angles sampled per free orientation. The grid holds this many raised to the power of (precision points - 1), so raising it is expensive quickly.
- pylinkage.synthesis.path_generation.MAX_ORIENTATION_CANDIDATES = 1296
Hard ceiling on grid candidates, so that six or more precision points cannot run unbounded. It does not bind for three to five points at the default resolution, which is the documented working range.
- pylinkage.synthesis.path_generation.path_generation(precision_points: list[tuple[float, float]], coupler_point_offset: tuple[float, float] = (0.0, 0.0), ground_pivot_a: tuple[float, float] | None = None, ground_pivot_d: tuple[float, float] | None = None, n_orientation_samples: int | None = None, max_solutions: int | None = 10, require_grashof: bool = True, require_crank_rocker: bool = False, *, orientation_resolution: int = 6) SynthesisResult
Synthesize a four-bar for path generation.
Find four-bar linkages where a coupler point passes through the specified precision points.
For path generation without prescribed timing, the coupler orientation at each point is a free variable. This function searches over orientation candidates using Burmester theory, then verifies each surviving candidate by simulating it.
A call typically costs on the order of a hundred milliseconds and can reach several seconds, so prefer not to place it inside a loop or behind an interactive control without lowering
max_solutionsfirst. Cost grows exponentially in the number of precision points, since the search carries one free orientation per point after the first.- Parameters:
precision_points – List of (x, y) points the coupler should pass through. Best results with 3-5 points.
coupler_point_offset – Offset of traced point from coupler reference.
ground_pivot_a – Optional fixed position for left ground pivot.
ground_pivot_d – Optional fixed position for right ground pivot.
n_orientation_samples – Deprecated, removed in 2.0.0. It never denoted a number of samples; use
orientation_resolutioninstead. A value given here is translated to the search it used to produce.max_solutions – Maximum number of solutions to return (None for all). This is what governs the running time, since the search stops as soon as it has this many verified solutions. Lowering it is the effective way to make this function faster.
require_grashof – If True, reject non-Grashof solutions.
require_crank_rocker – If True, only accept crank-rocker type.
orientation_resolution – Angles sampled per free orientation. The grid holds
orientation_resolution ** (len(precision_points) - 1)candidates, so raising this is exponentially expensive. Lowering it is rarely a good trade: a coarser grid tends to return no solutions at all rather than fewer. Usemax_solutionsto control cost.
- Returns:
SynthesisResult containing valid linkages.
Example
>>> points = [(0, 1), (1, 2), (2, 1.5), (3, 0)] >>> result = path_generation(points) >>> print(f"Found {len(result.solutions)} solutions") >>> for linkage in result.solutions: ... linkage.show()
- pylinkage.synthesis.path_generation.path_generation_with_timing(precision_points: list[tuple[float, float]], crank_angles: list[float], ground_pivot_a: tuple[float, float] = (0.0, 0.0), ground_length: float | None = None, require_grashof: bool = True) SynthesisResult
Synthesize a four-bar with prescribed timing.
Path generation with prescribed timing specifies both the coupler points AND the crank angles at which they should be reached. This is more constrained than general path generation.
- Parameters:
precision_points – List of (x, y) coupler points.
crank_angles – Crank angles (radians) for each point.
ground_pivot_a – Position of crank ground pivot.
ground_length – Fixed ground link length (optional).
require_grashof – If True, reject non-Grashof solutions.
- Returns:
SynthesisResult containing valid linkages.
Note
This is a more specialized synthesis that combines elements of path and function generation. With 4 points and 4 angles, exact synthesis is possible.
- pylinkage.synthesis.path_generation.verify_path_generation(linkage: Linkage, precision_points: list[PrecisionPoint], tolerance: float | None = None) tuple[bool, list[float]]
Verify that a synthesized linkage’s coupler point passes near target points.
Simulates the linkage for one full crank rotation and finds, for each precision point, the minimum distance from the coupler point trajectory to that target point.
The linkage must have a joint named “P” (the coupler point tracker added by
solution_to_linkage). If no such joint exists, the last joint in the solve order is used as the coupler point.- Parameters:
linkage – The synthesized Linkage to verify.
precision_points – Target (x, y) points the coupler should pass through.
tolerance – Maximum acceptable distance for each point. If None, defaults to 5% of the bounding box diagonal of the precision points.
- Returns:
Tuple of (all_satisfied, distances) where distances[i] is the minimum distance from the coupler trajectory to precision_points[i].
pylinkage.synthesis.ranking module
Solution quality metrics and ranking utilities.
Provides functions to evaluate and rank synthesis solutions by simulating the resulting linkage and computing kinematic quality metrics (path accuracy, transmission angle, link ratios, etc.).
- pylinkage.synthesis.ranking.compute_compactness(linkage: Linkage) float
Compute bounding box area of the mechanism trajectory.
Simulates the linkage and computes the bounding box of all joint positions over the full cycle.
- Parameters:
linkage – Linkage to evaluate.
- Returns:
Bounding box area (width * height).
- pylinkage.synthesis.ranking.compute_link_ratio(linkage: Linkage) float
Compute max/min link length ratio.
Examines all constrained distances in the linkage (crank radius, dyad distances, etc.) and returns the ratio of longest to shortest.
- Parameters:
linkage – Linkage to evaluate.
- Returns:
Ratio >= 1.0 (lower is better). Returns inf if any length is 0.
- pylinkage.synthesis.ranking.compute_metrics(linkage: Linkage, precision_points: list[PrecisionPoint], num_links: int = 4) QualityMetrics
Compute quality metrics for a synthesized linkage.
Simulates the linkage and evaluates path accuracy, transmission angle quality, link ratios, and compactness.
- Parameters:
linkage – Synthesized Linkage to evaluate.
precision_points – Target points for accuracy measurement.
num_links – Number of links in the topology (for simplicity score).
- Returns:
QualityMetrics with all fields populated including overall_score.
- pylinkage.synthesis.ranking.compute_path_accuracy(linkage: Linkage, precision_points: list[PrecisionPoint]) float
Compute RMS path accuracy of coupler trajectory vs precision points.
Simulates one full cycle and finds the minimum distance from the coupler point trajectory to each precision point. Returns the RMS of these minimum distances.
- Parameters:
linkage – Linkage to evaluate.
precision_points – Target points.
- Returns:
RMS distance (0.0 = perfect).
- pylinkage.synthesis.ranking.compute_transmission_angle(linkage: Linkage) float
Compute minimum transmission angle over the motion cycle.
The transmission angle is the angle between the coupler and the output link (rocker). A value near 90 degrees is ideal; values near 0 or 180 indicate poor force transmission.
For non-four-bar linkages, estimates from the first Revolute joint.
- Parameters:
linkage – Linkage to evaluate.
- Returns:
Minimum transmission angle in degrees (0-90 range).
- pylinkage.synthesis.ranking.score_solution(metrics: QualityMetrics, weights: dict[str, float] | None = None) float
Compute weighted overall score from quality metrics.
Default weights prioritize path accuracy, then transmission angle, then link ratio, then simplicity, then compactness.
- Parameters:
metrics – Quality metrics to score.
weights – Optional custom weight dict. Keys: “accuracy”, “transmission”, “link_ratio”, “compactness”, “simplicity”, “grashof”.
- Returns:
Overall score (lower is better).
pylinkage.synthesis.six_bar module
Six-bar path generation synthesis.
Decomposes six-bar topologies (Watt, Stephenson) into Assur groups and synthesizes each group sequentially using Burmester theory.
A Watt six-bar decomposes into two RRR dyads stacked in series. The algorithm:
Synthesize the driving four-bar (first dyad pair) via Burmester on a subset of precision points.
Simulate the driving four-bar to compute intermediate coupler positions at the remaining precision points.
Synthesize the second dyad using those intermediate positions.
Combine and validate the complete six-bar assembly.
References
Plecnik & McCarthy, “Kinematic synthesis of Stephenson III six-bar function generators” (2016)
McCarthy & Soh, “Geometric Design of Linkages” (2nd ed., 2011)
- pylinkage.synthesis.six_bar.six_bar_path_generation(precision_points: list[tuple[float, float]], topology: str = 'watt', max_solutions: int = 10, require_grashof_driver: bool = True, n_orientation_samples: int = 24) SynthesisResult
Synthesize a six-bar linkage for path generation.
Uses decomposition-based synthesis: the six-bar is split into Assur groups, and each group is synthesized sequentially using Burmester theory.
- For Watt type (2 stacked RRR dyads):
First dyad pair: driving four-bar synthesized on a subset of precision points.
Second dyad pair: refines path using remaining points, with the first dyad’s coupler providing anchor positions.
- Parameters:
precision_points – Target (x, y) points. 4-7 recommended for six-bars (more points than four-bar can handle).
topology –
"watt"or"stephenson".max_solutions – Maximum solutions to return.
require_grashof_driver – Require Grashof criterion on the driving four-bar loop.
n_orientation_samples – Orientation search density per stage.
- Returns:
SynthesisResult containing valid six-bar Linkage objects.
Example
>>> points = [(0, 0), (1, 2), (2, 3), (3, 2), (4, 0)] >>> result = six_bar_path_generation(points, topology="watt") >>> print(f"Found {len(result.solutions)} solutions")
pylinkage.synthesis.topology_types module
Type definitions for topology-aware synthesis.
Extends the four-bar-specific types in _types.py with general
N-bar solution structures used by six-bar, generalized, and
multi-topology synthesis.
- class pylinkage.synthesis.topology_types.GroupSynthesisResult(group_index: int, group_signature: str, dyads: tuple[BurmesterDyad, BurmesterDyad] | None=None, joint_positions: dict[str, Point2D]=<factory>, precision_indices: tuple[int, ...]=(), residual: float = 0.0)
Bases:
objectResult of synthesizing a single Assur group in the decomposition chain.
- Variables:
group_index (int) – Index in the decomposition order.
group_signature (str) – Joint signature (e.g., “RRR”).
dyads (tuple[BurmesterDyad, BurmesterDyad] | None) – Burmester dyad pair if group is a dyad, None for triads.
joint_positions (dict[str, Point2D]) – Computed positions for this group’s internal nodes.
precision_indices (tuple[int, ...]) – Which precision points were assigned to this group.
residual (float) – Synthesis error (0.0 for exact Burmester solutions).
- dyads: tuple[BurmesterDyad, BurmesterDyad] | None = None
- group_index: int
- group_signature: str
- joint_positions: dict[str, Point2D]
- precision_indices: tuple[int, ...] = ()
- residual: float = 0.0
- class pylinkage.synthesis.topology_types.NBarSolution(topology_id: str, joint_positions: dict[str, tuple[float, float]], link_lengths: dict[str, float], group_results: list[~pylinkage.synthesis.topology_types.GroupSynthesisResult] = <factory>, coupler_node: str | None = None, coupler_point: tuple[float, float] | None = None)
Bases:
objectA general N-bar linkage solution from topology-aware synthesis.
Generalizes FourBarSolution to arbitrary topologies. Instead of named pivots (A, B, C, D), stores a dict of joint positions keyed by node ID, plus the topology graph connectivity.
- Variables:
topology_id (str) – ID from the topology catalog (e.g., “watt”).
joint_positions (dict[str, tuple[float, float]]) – Map from node ID to (x, y) position.
link_lengths (dict[str, float]) – Map from edge ID to link length.
group_results (list[pylinkage.synthesis.topology_types.GroupSynthesisResult]) – Per-group synthesis results in decomposition order.
coupler_node (str | None) – Node ID of the traced coupler point (if any).
coupler_point (tuple[float, float] | None) – World-frame position of the traced point.
- coupler_node: str | None = None
- coupler_point: tuple[float, float] | None = None
- group_results: list[GroupSynthesisResult]
- joint_positions: dict[str, tuple[float, float]]
- link_lengths: dict[str, float]
- topology_id: str
- class pylinkage.synthesis.topology_types.QualityMetrics(path_accuracy: float = inf, min_transmission_angle: float = 0.0, link_ratio: float = inf, compactness: float = inf, num_links: int = 4, is_grashof: bool = False, overall_score: float = inf)
Bases:
objectQuality metrics for ranking synthesis solutions.
All metrics are computed by simulating the synthesized linkage and evaluating its kinematic properties.
- Variables:
path_accuracy (float) – RMS error of coupler path vs. precision points (lower is better).
min_transmission_angle (float) – Worst transmission angle in degrees over the motion cycle (higher is better, ideal is 90).
link_ratio (float) – Max/min link length ratio (lower is better, ideal is 1.0).
compactness (float) – Bounding box area of the mechanism trajectory (lower is better).
num_links (int) – Number of links (fewer is simpler).
is_grashof (bool) – Whether the driving loop is Grashof (full crank rotation possible).
overall_score (float) – Weighted composite score (lower is better).
- compactness: float = inf
- is_grashof: bool = False
- link_ratio: float = inf
- min_transmission_angle: float = 0.0
- num_links: int = 4
- overall_score: float = inf
- path_accuracy: float = inf
- class pylinkage.synthesis.topology_types.TopologySolution(solution: NBarSolution, linkage: Linkage, topology_entry: CatalogEntry, metrics: QualityMetrics)
Bases:
objectA ranked solution from multi-topology synthesis.
Wraps an NBarSolution with its quality metrics, the converted Linkage object for simulation, and the catalog entry identifying which topology was used.
- Variables:
solution (NBarSolution) – The raw N-bar synthesis solution.
linkage (Linkage) – Converted Linkage object ready for simulation.
topology_entry (CatalogEntry) – Catalog entry for the topology used.
metrics (QualityMetrics) – Computed quality metrics.
- metrics: QualityMetrics
- solution: NBarSolution
- topology_entry: CatalogEntry
pylinkage.synthesis.utils module
Utility functions for mechanism synthesis.
This module provides helper functions for: - Coordinate conversions between Cartesian and complex representations - Grashof criterion checking for four-bar mobility - Solution validation and filtering - Numerical stability utilities
- class pylinkage.synthesis.utils.GrashofType(value)
Bases:
EnumClassification of four-bar linkage by Grashof criterion.
- CHANGE_POINT = 6
s + l = p + q exactly.
- Type:
Change point (special Grashof)
- GRASHOF_CRANK_ROCKER = 1
full rotation of crank, oscillation of rocker.
- Type:
Shortest link is crank
- GRASHOF_DOUBLE_CRANK = 2
both crank and rocker can fully rotate.
- Type:
Shortest link is frame
- GRASHOF_DOUBLE_ROCKER = 4
both crank and rocker oscillate.
- Type:
Shortest link is coupler
- GRASHOF_ROCKER_CRANK = 3
crank oscillates, rocker fully rotates.
- Type:
Shortest link is rocker
- NON_GRASHOF = 5
s + l > p + q, no link can fully rotate.
- Type:
Non-Grashof
- pylinkage.synthesis.utils.angle_between_points(origin: tuple[float, float], target: tuple[float, float]) float
Compute angle from origin to target point.
- Parameters:
origin – Reference point.
target – Target point.
- Returns:
Angle in radians from positive x-axis.
- pylinkage.synthesis.utils.check_condition_number(matrix: ndarray[tuple[Any, ...], dtype[floating]], threshold: float = 10000000000.0) bool
Check if matrix is well-conditioned.
- Parameters:
matrix – Matrix to check.
threshold – Maximum acceptable condition number.
- Returns:
True if condition number is below threshold.
- pylinkage.synthesis.utils.complex_to_point(z: complex) tuple[float, float]
Convert complex number to Cartesian point.
- Parameters:
z – Complex number x + iy.
- Returns:
Point as (x, y) tuple.
- pylinkage.synthesis.utils.crank_angle_limits(crank: float, coupler: float, rocker: float, ground: float) tuple[float, float] | None
Angular range of the crank when it cannot make a full rotation.
In a non-Grashof four-bar the crank oscillates. It is stopped where the coupler and rocker become collinear, once extended and once folded. The crank angle at each stop follows from the law of cosines in the triangle formed by the crank, the ground and the coupler-rocker line.
Angles are measured from the ground line, crank pivot towards rocker pivot. The range returned is the one with positive angles; the mirror range below the ground line is reached by negating both bounds. A margin of 0.02 rad (about one degree) is taken off each end so that a crank driven through the range never lands exactly on the singular position.
- Parameters:
crank – Length of crank (input link).
coupler – Length of coupler (connecting link).
rocker – Length of rocker (output link).
ground – Length of ground (frame link).
- Returns:
(min_angle, max_angle)in radians, orNonewhen the crank can rotate fully, or when the range is too narrow to survive the margin.
Example
>>> crank_angle_limits(1.0, 3.0, 3.0, 4.0) is None True >>> lo, hi = crank_angle_limits(3.0, 1.5, 1.5, 2.0) >>> 0 < lo < hi < math.pi True
- pylinkage.synthesis.utils.distance(p1: tuple[float, float], p2: tuple[float, float]) float
Euclidean distance between two points.
- Parameters:
p1 – First point (x, y).
p2 – Second point (x, y).
- Returns:
Distance between points.
- pylinkage.synthesis.utils.grashof_check(crank: float, coupler: float, rocker: float, ground: float) GrashofType
Determine Grashof classification of a four-bar linkage.
The Grashof criterion states that for a four-bar linkage to have at least one link capable of full rotation, the sum of the shortest and longest links must be less than or equal to the sum of the remaining two links: s + l <= p + q
- Parameters:
crank – Length of crank (input link).
coupler – Length of coupler (connecting link).
rocker – Length of rocker (output link).
ground – Length of ground (frame link).
- Returns:
GrashofType classification of the linkage.
Example
>>> grashof_check(1.0, 3.0, 3.0, 4.0) GrashofType.GRASHOF_CRANK_ROCKER
- pylinkage.synthesis.utils.is_crank_rocker(crank: float, coupler: float, rocker: float, ground: float) bool
Check if linkage is a crank-rocker mechanism.
A crank-rocker has the crank as the shortest link and satisfies Grashof. The crank can fully rotate while the rocker oscillates.
- Parameters:
crank – Length of crank (input link).
coupler – Length of coupler (connecting link).
rocker – Length of rocker (output link).
ground – Length of ground (frame link).
- Returns:
True if linkage is crank-rocker type.
- pylinkage.synthesis.utils.is_grashof(crank: float, coupler: float, rocker: float, ground: float) bool
Check if a four-bar linkage satisfies Grashof criterion.
A Grashof linkage has at least one link that can fully rotate.
- Parameters:
crank – Length of crank (input link).
coupler – Length of coupler (connecting link).
rocker – Length of rocker (output link).
ground – Length of ground (frame link).
- Returns:
True if Grashof criterion is satisfied.
- pylinkage.synthesis.utils.normalize_angle(angle: float) float
Normalize angle to [-pi, pi] range.
- Parameters:
angle – Angle in radians.
- Returns:
Equivalent angle in [-pi, pi].
- pylinkage.synthesis.utils.point_to_complex(p: tuple[float, float]) complex
Convert Cartesian point to complex representation.
- Parameters:
p – Point as (x, y) tuple.
- Returns:
Complex number x + iy.
- pylinkage.synthesis.utils.rotate_point(point: tuple[float, float], angle: float, center: tuple[float, float] = (0.0, 0.0)) tuple[float, float]
Rotate a point about a center.
- Parameters:
point – Point to rotate.
angle – Rotation angle in radians (counterclockwise positive).
center – Center of rotation.
- Returns:
Rotated point.
- pylinkage.synthesis.utils.rotation_matrix_2d(angle: float) ndarray[tuple[Any, ...], dtype[float64]]
Create 2D rotation matrix.
- Parameters:
angle – Rotation angle in radians.
- Returns:
2x2 rotation matrix.
- pylinkage.synthesis.utils.validate_fourbar(solution: FourBarSolution) tuple[bool, list[str]]
Validate a four-bar solution for geometric consistency.
Checks for: - Non-zero link lengths - Positive link lengths - Assembly feasibility (triangle inequality)
- Parameters:
solution – FourBarSolution to validate.
- Returns:
Tuple of (is_valid, list of error messages).
Module contents
Classical mechanism synthesis methods for planar linkages.
This module implements classical analytical synthesis methods for designing four-bar linkages to achieve specific motion requirements:
Function Generation: Match input/output angle relationships using Freudenstein’s equation.
Path Generation: Synthesize linkages where a coupler point traces a curve through specified precision points.
Motion Generation: Guide a rigid body through specified poses (position + orientation).
Burmester Theory: The foundational geometric method underlying all synthesis types.
Quick Start
Path Generation (most common use case):
from pylinkage.synthesis import path_generation
# Find 4-bar where coupler passes through these points
precision_points = [(0, 1), (1, 2), (2, 1.5), (3, 0)]
result = path_generation(precision_points)
print(f"Found {len(result.solutions)} solutions")
for linkage in result.solutions:
linkage.show()
Function Generation:
import math
from pylinkage.synthesis import function_generation
# Match input/output angle relationship
angle_pairs = [
(0, 0),
(math.pi/6, math.pi/4),
(math.pi/3, math.pi/2),
]
result = function_generation(angle_pairs)
if result.solutions:
linkage = result.solutions[0]
print(f"Crank length: {linkage.joints[2].r:.3f}")
Motion Generation:
from pylinkage.synthesis import Pose, motion_generation
# Guide body through these poses (x, y, angle)
poses = [
Pose(0, 0, 0),
Pose(1, 1, 0.5),
Pose(2, 0.5, 1.0),
]
result = motion_generation(poses)
Working with Results
All synthesis functions return a SynthesisResult object:
result = path_generation(points)
# Check if solutions were found
if result.solutions:
print(f"Found {len(result.solutions)} solutions")
# Iterate over solutions
for linkage in result.solutions:
linkage.show()
# Access warnings
for warning in result.warnings:
print(f"Warning: {warning}")
# Access raw mathematical solutions
for sol in result.raw_solutions:
print(f"Crank: {sol.crank_length:.3f}")
Creating Linkages from Link Lengths
Use fourbar_from_lengths to create a four-bar directly:
from pylinkage.synthesis import fourbar_from_lengths
linkage = fourbar_from_lengths(
crank_length=1.0,
coupler_length=3.0,
rocker_length=3.0,
ground_length=4.0,
)
linkage.show()
Synthesis Theory Overview
Burmester Theory: For a set of precision positions (poses), Burmester theory identifies all possible attachment points (circle points) on a moving body and their corresponding fixed pivots (center points) such that the attachment traces a circular arc during motion through the precision positions.
Function Generation: Uses Freudenstein’s equation to relate input crank angle to output rocker angle. Given 3 angle pairs, there is a unique solution. With more pairs, least-squares fitting is used.
Path Generation: More complex because the coupler orientation at each point is not specified. The algorithm searches over possible orientations using Burmester theory.
Motion Generation: Most constrained because both position AND orientation are specified. Uses Burmester theory directly on the poses.
References
Freudenstein, F. “Approximate Synthesis of Four-Bar Linkages” (1955)
McCarthy, J.M. “Geometric Design of Linkages” (2nd ed., 2011)
Sandor & Erdman, “Advanced Mechanism Design” (1984)
Bottema & Roth, “Theoretical Kinematics” (1979)
- class pylinkage.synthesis.BurmesterCurves(circle_curve: ndarray[tuple[Any, ...], dtype[complex128]], center_curve: ndarray[tuple[Any, ...], dtype[complex128]], parameter: ndarray[tuple[Any, ...], dtype[float64]], is_discrete: bool = False)
Bases:
objectCircle point and center point curves from Burmester theory.
For a set of precision positions, these curves contain all possible dyad attachment points. The circle points lie on the moving body, and the center points are the corresponding fixed pivots.
For 3 precision positions: continuous parametric curves For 4 precision positions: up to 6 discrete points (Ball’s points) For 5 precision positions: typically 0-2 discrete solutions
The curves are parameterized so that circle_curve[i] and center_curve[i] form a valid dyad pair.
- Variables:
circle_curve (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.complex128]]) – Array of circle point positions (complex).
center_curve (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.complex128]]) – Array of corresponding center point positions.
parameter (numpy.ndarray[tuple[Any, ...], numpy.dtype[numpy.float64]]) – Parameter values along the curves.
is_discrete (bool) – True if solutions are discrete points, not curves.
- center_curve: ndarray[tuple[Any, ...], dtype[complex128]]
- circle_curve: ndarray[tuple[Any, ...], dtype[complex128]]
- filter_finite() BurmesterCurves
Remove points with infinite or NaN coordinates.
- Returns:
New BurmesterCurves with only finite points.
- get_all_dyads() list[BurmesterDyad]
Get all dyads from the curves.
- Returns:
List of BurmesterDyad objects, one per curve point.
- get_dyad(index: int) BurmesterDyad
Get dyad at specific parameter index.
- Parameters:
index – Index into the curve arrays.
- Returns:
BurmesterDyad at the specified index.
- is_discrete: bool = False
- parameter: ndarray[tuple[Any, ...], dtype[float64]]
- sample(n_samples: int) BurmesterCurves
Resample curves to specified number of points.
Only meaningful for continuous curves (is_discrete=False).
- Parameters:
n_samples – Number of samples desired.
- Returns:
New BurmesterCurves with resampled points.
- class pylinkage.synthesis.BurmesterDyad(circle_point: complex, center_point: complex)
Bases:
objectA kinematic dyad (two-link chain) from Burmester theory.
In Burmester theory, a dyad connects a moving point on the coupler (circle point) to a fixed pivot on the frame (center point) through a single revolute joint.
Two dyads combined form a four-bar linkage: the circle points define attachment points on the coupler, and the center points define the fixed ground pivots.
- Variables:
circle_point (complex) – Point on the moving body (complex representation).
center_point (complex) – Fixed pivot point (complex representation).
- center_point: complex
- circle_point: complex
- property link_length: float
Length of the dyad link.
- to_cartesian() tuple[tuple[float, float], tuple[float, float]]
Convert to Cartesian coordinate tuples.
- Returns:
Tuple of (circle_point_xy, center_point_xy).
- to_dyad_solution() DyadSolution
Convert to DyadSolution named tuple.
- class pylinkage.synthesis.DyadSolution(circle_point: complex, center_point: complex)
Bases:
NamedTupleA dyad (two-link chain) from Burmester synthesis.
A dyad connects a moving point (circle point on coupler) to a fixed pivot (center point on frame).
- Variables:
circle_point (complex) – Point on the moving body (complex representation).
center_point (complex) – Fixed pivot point (complex representation).
- center_point: complex
Alias for field number 1
- circle_point: complex
Alias for field number 0
- property link_length: float
Length of the dyad link.
- to_cartesian() tuple[tuple[float, float], tuple[float, float]]
Convert to Cartesian coordinate tuples.
- class pylinkage.synthesis.FourBarSolution(ground_pivot_a: tuple[float, float], ground_pivot_d: tuple[float, float], crank_pivot_b: tuple[float, float], coupler_pivot_c: tuple[float, float], crank_length: float, coupler_length: float, rocker_length: float, ground_length: float, coupler_point: tuple[float, float] | None = None, arc_limits: tuple[float, float] | None = None)
Bases:
NamedTupleA four-bar linkage solution from synthesis.
The four-bar consists of: - Ground link: from A to D (fixed frame) - Crank (input link): from A to B - Coupler: from B to C - Rocker (output link): from D to C
Optionally, a coupler point P (on the coupler link B-C) that traces the desired path. This is the point that should pass through the precision points in path/motion generation.
- Variables:
ground_pivot_a (tuple[float, float]) – Position of fixed pivot A (crank base).
ground_pivot_d (tuple[float, float]) – Position of fixed pivot D (rocker base).
crank_pivot_b (tuple[float, float]) – Initial position of crank pin B.
coupler_pivot_c (tuple[float, float]) – Initial position of coupler-rocker pin C.
crank_length (float) – Length of crank link (A to B).
coupler_length (float) – Length of coupler link (B to C).
rocker_length (float) – Length of rocker link (D to C).
ground_length (float) – Length of ground link (A to D).
coupler_point (tuple[float, float] | None) – Optional traced point on the coupler (world frame at initial position). None if not applicable (e.g. function gen).
- arc_limits: tuple[float, float] | None
Alias for field number 9
- coupler_length: float
Alias for field number 5
- coupler_pivot_c: tuple[float, float]
Alias for field number 3
- coupler_point: tuple[float, float] | None
Alias for field number 8
- crank_length: float
Alias for field number 4
- crank_pivot_b: tuple[float, float]
Alias for field number 2
- ground_length: float
Alias for field number 7
- ground_pivot_a: tuple[float, float]
Alias for field number 0
- ground_pivot_d: tuple[float, float]
Alias for field number 1
- rocker_length: float
Alias for field number 6
- class pylinkage.synthesis.GrashofType(value)
Bases:
EnumClassification of four-bar linkage by Grashof criterion.
- CHANGE_POINT = 6
s + l = p + q exactly.
- Type:
Change point (special Grashof)
- GRASHOF_CRANK_ROCKER = 1
full rotation of crank, oscillation of rocker.
- Type:
Shortest link is crank
- GRASHOF_DOUBLE_CRANK = 2
both crank and rocker can fully rotate.
- Type:
Shortest link is frame
- GRASHOF_DOUBLE_ROCKER = 4
both crank and rocker oscillate.
- Type:
Shortest link is coupler
- GRASHOF_ROCKER_CRANK = 3
crank oscillates, rocker fully rotates.
- Type:
Shortest link is rocker
- NON_GRASHOF = 5
s + l > p + q, no link can fully rotate.
- Type:
Non-Grashof
- class pylinkage.synthesis.GroupSynthesisResult(group_index: int, group_signature: str, dyads: tuple[BurmesterDyad, BurmesterDyad] | None=None, joint_positions: dict[str, Point2D]=<factory>, precision_indices: tuple[int, ...]=(), residual: float = 0.0)
Bases:
objectResult of synthesizing a single Assur group in the decomposition chain.
- Variables:
group_index (int) – Index in the decomposition order.
group_signature (str) – Joint signature (e.g., “RRR”).
dyads (tuple[BurmesterDyad, BurmesterDyad] | None) – Burmester dyad pair if group is a dyad, None for triads.
joint_positions (dict[str, Point2D]) – Computed positions for this group’s internal nodes.
precision_indices (tuple[int, ...]) – Which precision points were assigned to this group.
residual (float) – Synthesis error (0.0 for exact Burmester solutions).
- dyads: tuple[BurmesterDyad, BurmesterDyad] | None = None
- group_index: int
- group_signature: str
- joint_positions: dict[str, Point2D]
- precision_indices: tuple[int, ...] = ()
- residual: float = 0.0
- class pylinkage.synthesis.NBarSolution(topology_id: str, joint_positions: dict[str, tuple[float, float]], link_lengths: dict[str, float], group_results: list[~pylinkage.synthesis.topology_types.GroupSynthesisResult] = <factory>, coupler_node: str | None = None, coupler_point: tuple[float, float] | None = None)
Bases:
objectA general N-bar linkage solution from topology-aware synthesis.
Generalizes FourBarSolution to arbitrary topologies. Instead of named pivots (A, B, C, D), stores a dict of joint positions keyed by node ID, plus the topology graph connectivity.
- Variables:
topology_id (str) – ID from the topology catalog (e.g., “watt”).
joint_positions (dict[str, tuple[float, float]]) – Map from node ID to (x, y) position.
link_lengths (dict[str, float]) – Map from edge ID to link length.
group_results (list[pylinkage.synthesis.topology_types.GroupSynthesisResult]) – Per-group synthesis results in decomposition order.
coupler_node (str | None) – Node ID of the traced coupler point (if any).
coupler_point (tuple[float, float] | None) – World-frame position of the traced point.
- coupler_node: str | None = None
- coupler_point: tuple[float, float] | None = None
- group_results: list[GroupSynthesisResult]
- joint_positions: dict[str, tuple[float, float]]
- link_lengths: dict[str, float]
- topology_id: str
- class pylinkage.synthesis.Pose(x: float, y: float, angle: float)
Bases:
objectA rigid body pose: position + orientation.
- Variables:
x (float) – X-coordinate of the pose origin.
y (float) – Y-coordinate of the pose origin.
angle (float) – Orientation angle in radians.
- angle: float
- classmethod from_point_angle(point: tuple[float, float], angle: float) Pose
Create a Pose from a point and angle.
- to_complex() complex
Convert position to complex representation.
- x: float
- y: float
- class pylinkage.synthesis.QualityMetrics(path_accuracy: float = inf, min_transmission_angle: float = 0.0, link_ratio: float = inf, compactness: float = inf, num_links: int = 4, is_grashof: bool = False, overall_score: float = inf)
Bases:
objectQuality metrics for ranking synthesis solutions.
All metrics are computed by simulating the synthesized linkage and evaluating its kinematic properties.
- Variables:
path_accuracy (float) – RMS error of coupler path vs. precision points (lower is better).
min_transmission_angle (float) – Worst transmission angle in degrees over the motion cycle (higher is better, ideal is 90).
link_ratio (float) – Max/min link length ratio (lower is better, ideal is 1.0).
compactness (float) – Bounding box area of the mechanism trajectory (lower is better).
num_links (int) – Number of links (fewer is simpler).
is_grashof (bool) – Whether the driving loop is Grashof (full crank rotation possible).
overall_score (float) – Weighted composite score (lower is better).
- compactness: float = inf
- is_grashof: bool = False
- link_ratio: float = inf
- min_transmission_angle: float = 0.0
- num_links: int = 4
- overall_score: float = inf
- path_accuracy: float = inf
- class pylinkage.synthesis.SynthesisProblem(synthesis_type: SynthesisType, precision_points: list[tuple[float, float]]=<factory>, angle_pairs: list[tuple[float, float]]=<factory>, poses: list[Pose] = <factory>, ground_pivot_a: tuple[float, float] | None=None, ground_pivot_d: tuple[float, float] | None=None, ground_length: float | None = None)
Bases:
objectDefinition of a synthesis problem.
A synthesis problem specifies the requirements that a mechanism must satisfy. Depending on the synthesis type, different inputs are required:
FUNCTION: angle_pairs specifying input/output angle relationships
PATH: precision_points the coupler must pass through
MOTION: poses specifying body positions and orientations
- Variables:
synthesis_type (pylinkage.synthesis._types.SynthesisType) – Type of synthesis (FUNCTION, PATH, or MOTION).
precision_points (list[tuple[float, float]]) – Target points for path generation.
angle_pairs (list[tuple[float, float]]) – Input/output angle pairs for function generation.
poses (list[pylinkage.synthesis._types.Pose]) – Target poses for motion generation.
ground_pivot_a (tuple[float, float] | None) – Optional fixed position of first ground pivot.
ground_pivot_d (tuple[float, float] | None) – Optional fixed position of second ground pivot.
ground_length (float | None) – Optional fixed ground link length.
- angle_pairs: list[tuple[float, float]]
- ground_length: float | None = None
- ground_pivot_a: tuple[float, float] | None = None
- ground_pivot_d: tuple[float, float] | None = None
- property num_precision_positions: int
Number of precision positions specified.
- poses: list[Pose]
- precision_points: list[tuple[float, float]]
- synthesis_type: SynthesisType
- class pylinkage.synthesis.SynthesisResult(solutions: list, raw_solutions: list[FourBarSolution], problem: SynthesisProblem, warnings: list[str] = <factory>, branch_info: list[dict[str, int]] = <factory>, _ensemble: Ensemble | None = None)
Bases:
objectResult of a synthesis operation.
Contains the synthesized linkages along with metadata about the synthesis process including warnings, raw mathematical solutions, and branch information.
The primary way to access the solutions is via the
ensembleproperty, which returns anEnsemblefor batch simulation, ranking, filtering, and visualization.- Variables:
solutions (list) – List of valid Linkage objects.
raw_solutions (list[FourBarSolution]) – Raw mathematical solutions before filtering.
problem (SynthesisProblem) – The original synthesis problem.
warnings (list[str]) – Any warnings generated during synthesis.
branch_info (list[dict[str, int]]) – Branch selection info for each solution.
- branch_info: list[dict[str, int]]
- property ensemble: Ensemble
Solutions as an
Ensemble.Built lazily on first access and cached. The Ensemble carries link lengths from
raw_solutionsas score columns (crank_length,coupler_length,rocker_length,ground_length) when available.- Raises:
ValueError – If no valid solutions exist.
- problem: SynthesisProblem
- raw_solutions: list[FourBarSolution]
- solutions: list
- warnings: list[str]
- class pylinkage.synthesis.SynthesisType(value)
Bases:
EnumType of synthesis problem.
- FUNCTION = 1
match input/output angle relationships.
- Type:
Function generation
- MOTION = 3
rigid body guidance through poses.
- Type:
Motion generation
- PATH = 2
coupler curve through precision points.
- Type:
Path generation
- class pylinkage.synthesis.TopologySolution(solution: NBarSolution, linkage: Linkage, topology_entry: CatalogEntry, metrics: QualityMetrics)
Bases:
objectA ranked solution from multi-topology synthesis.
Wraps an NBarSolution with its quality metrics, the converted Linkage object for simulation, and the catalog entry identifying which topology was used.
- Variables:
solution (NBarSolution) – The raw N-bar synthesis solution.
linkage (Linkage) – Converted Linkage object ready for simulation.
topology_entry (CatalogEntry) – Catalog entry for the topology used.
metrics (QualityMetrics) – Computed quality metrics.
- linkage: Linkage
- metrics: QualityMetrics
- solution: NBarSolution
- topology_entry: CatalogEntry
- pylinkage.synthesis.compute_all_poles(poses: list[Pose]) ndarray[tuple[Any, ...], dtype[complex128]]
Compute all poles between pose pairs.
For n positions, computes n*(n-1)/2 poles P_ij for all i < j. The poles are ordered as: P12, P13, …, P1n, P23, …, P(n-1)n
- Parameters:
poses – List of Pose objects.
- Returns:
Array of complex pole locations.
- Raises:
ValueError – If fewer than 2 poses provided.
- pylinkage.synthesis.compute_circle_point_curve(poses: list[Pose], n_samples: int = 72) BurmesterCurves
Compute the circle point and center point curves.
This is the main entry point for Burmester theory. Given a set of precision positions (poses), it computes all valid dyad attachment points.
For 3 positions: Returns continuous parametric curves For 4 positions: Returns up to 6 discrete Ball’s points For 5 positions: Returns 0-2 discrete solutions (over-constrained)
- Parameters:
poses – List of Pose objects (3, 4, or 5 positions).
n_samples – Number of samples for continuous curves (3 positions).
- Returns:
BurmesterCurves containing sampled/discrete circle and center curves.
- Raises:
ValueError – If number of poses is not 3, 4, or 5.
Example
>>> poses = [Pose(0, 0, 0), Pose(1, 1, 0.5), Pose(2, 0, 1.0)] >>> curves = compute_circle_point_curve(poses) >>> dyad = curves.get_dyad(0)
- pylinkage.synthesis.compute_pole(pos1: Pose, pos2: Pose) complex
Compute the pole (instant center) between two positions.
The pole is the point that remains stationary during the finite displacement from pos1 to pos2. It is found at the intersection of perpendicular bisectors of corresponding point trajectories.
For a pure rotation, the pole is the center of rotation. For a translation, the pole is at infinity.
- Parameters:
pos1 – First position (x, y, angle).
pos2 – Second position (x, y, angle).
- Returns:
Complex number representing the pole location. Returns complex infinity for pure translation.
- Raises:
ValueError – If positions are identical (no displacement).
- pylinkage.synthesis.crank_angle_limits(crank: float, coupler: float, rocker: float, ground: float) tuple[float, float] | None
Angular range of the crank when it cannot make a full rotation.
In a non-Grashof four-bar the crank oscillates. It is stopped where the coupler and rocker become collinear, once extended and once folded. The crank angle at each stop follows from the law of cosines in the triangle formed by the crank, the ground and the coupler-rocker line.
Angles are measured from the ground line, crank pivot towards rocker pivot. The range returned is the one with positive angles; the mirror range below the ground line is reached by negating both bounds. A margin of 0.02 rad (about one degree) is taken off each end so that a crank driven through the range never lands exactly on the singular position.
- Parameters:
crank – Length of crank (input link).
coupler – Length of coupler (connecting link).
rocker – Length of rocker (output link).
ground – Length of ground (frame link).
- Returns:
(min_angle, max_angle)in radians, orNonewhen the crank can rotate fully, or when the range is too narrow to survive the margin.
Example
>>> crank_angle_limits(1.0, 3.0, 3.0, 4.0) is None True >>> lo, hi = crank_angle_limits(3.0, 1.5, 1.5, 2.0) >>> 0 < lo < hi < math.pi True
- pylinkage.synthesis.fourbar_from_lengths(crank_length: float, coupler_length: float, rocker_length: float, ground_length: float, ground_pivot_a: Point2D = (0.0, 0.0), initial_crank_angle: float = 0.0, iterations: int = 360, name: str = 'fourbar') SimLinkage
Create a four-bar linkage from link lengths.
Convenience function to create a four-bar linkage given only the link lengths. The linkage is placed with ground pivot A at the specified position.
Kinematic chain:
A ─── crank ─── B ─── coupler ─── C ─── rocker ─── D (input)- Parameters:
crank_length – Length of crank (a).
coupler_length – Length of coupler (b).
rocker_length – Length of rocker (c).
ground_length – Length of ground link (d).
ground_pivot_a – Position of first ground pivot.
initial_crank_angle – Initial crank angle in radians.
iterations – Number of simulation steps per rotation.
name – Name for the linkage.
- Returns:
Linkage built from the component/actuator/dyad API.
- Raises:
ValueError – If the mechanism cannot be assembled with the given link lengths at the initial crank angle.
Example
>>> linkage = fourbar_from_lengths(1.0, 3.0, 3.0, 4.0) >>> len(list(linkage.step(iterations=100))) 100
- pylinkage.synthesis.function_generation(angle_pairs: list[tuple[float, float]], ground_length: float = 1.0, ground_pivot_a: tuple[float, float] = (0.0, 0.0), require_grashof: bool = True, require_crank_rocker: bool = False) SynthesisResult
Synthesize a four-bar for function generation.
Given input/output angle pairs, find four-bar linkages where the crank angle theta2 produces rocker angle theta4 according to the Freudenstein equation.
For 3 angle pairs: Unique exact solution (if physically valid) For 4+ angle pairs: Least-squares approximate solution
- Parameters:
angle_pairs – List of (input_angle, output_angle) in radians. At least 3 pairs required.
ground_length – Desired ground link length (scaling factor).
ground_pivot_a – Position of input (crank) ground pivot.
require_grashof – If True, reject non-Grashof solutions.
require_crank_rocker – If True, only accept crank-rocker type.
- Returns:
SynthesisResult containing valid linkages.
- Raises:
ValueError – If fewer than 3 angle pairs provided.
Example
>>> pairs = [(0, 0), (math.pi/6, math.pi/4), (math.pi/3, math.pi/2)] >>> result = function_generation(pairs) >>> for linkage in result.solutions: ... print(f"Crank: {linkage.joints[2].r:.3f}")
- pylinkage.synthesis.generalized_synthesis(topology: CatalogEntry, precision_points: list[PrecisionPoint], partition_strategy: str = 'greedy', max_solutions: int = 10, n_orientation_samples: int = 24) list[NBarSolution]
Synthesize a linkage of arbitrary topology for path generation.
Retrieves the Assur decomposition of the given topology, partitions precision points across groups, and synthesizes each group sequentially using Burmester (for dyads) or optimization (for triads).
- Parameters:
topology – CatalogEntry from the topology catalog.
precision_points – Target (x, y) points.
partition_strategy –
"greedy"or"exhaustive".max_solutions – Maximum solutions to return.
n_orientation_samples – Orientation search density.
- Returns:
List of NBarSolution objects (unranked).
- pylinkage.synthesis.grashof_check(crank: float, coupler: float, rocker: float, ground: float) GrashofType
Determine Grashof classification of a four-bar linkage.
The Grashof criterion states that for a four-bar linkage to have at least one link capable of full rotation, the sum of the shortest and longest links must be less than or equal to the sum of the remaining two links: s + l <= p + q
- Parameters:
crank – Length of crank (input link).
coupler – Length of coupler (connecting link).
rocker – Length of rocker (output link).
ground – Length of ground (frame link).
- Returns:
GrashofType classification of the linkage.
Example
>>> grashof_check(1.0, 3.0, 3.0, 4.0) GrashofType.GRASHOF_CRANK_ROCKER
- pylinkage.synthesis.is_crank_rocker(crank: float, coupler: float, rocker: float, ground: float) bool
Check if linkage is a crank-rocker mechanism.
A crank-rocker has the crank as the shortest link and satisfies Grashof. The crank can fully rotate while the rocker oscillates.
- Parameters:
crank – Length of crank (input link).
coupler – Length of coupler (connecting link).
rocker – Length of rocker (output link).
ground – Length of ground (frame link).
- Returns:
True if linkage is crank-rocker type.
- pylinkage.synthesis.is_grashof(crank: float, coupler: float, rocker: float, ground: float) bool
Check if a four-bar linkage satisfies Grashof criterion.
A Grashof linkage has at least one link that can fully rotate.
- Parameters:
crank – Length of crank (input link).
coupler – Length of coupler (connecting link).
rocker – Length of rocker (output link).
ground – Length of ground (frame link).
- Returns:
True if Grashof criterion is satisfied.
- pylinkage.synthesis.linkage_to_synthesis_params(linkage: SimLinkage) FourBarSolution
Extract synthesis parameters from an existing four-bar linkage.
Analyzes a SimLinkage object to extract the four-bar geometry parameters as a FourBarSolution.
- Parameters:
linkage – A four-bar linkage built with the component/actuator/dyad API.
- Returns:
FourBarSolution tuple with geometry parameters.
- Raises:
ValueError – If linkage is not a valid four-bar.
- pylinkage.synthesis.motion_generation(poses: list[Pose], ground_pivot_a: tuple[float, float] | None = None, ground_pivot_d: tuple[float, float] | None = None, max_solutions: int | None = 10, require_grashof: bool = True, require_crank_rocker: bool = False) SynthesisResult
Synthesize a four-bar for motion generation (rigid body guidance).
Find four-bar linkages where a body attached to the coupler passes through the specified poses (position + orientation).
This is the most constrained synthesis problem because both position and orientation are specified at each precision position.
- Parameters:
poses – List of Pose objects specifying body positions and orientations. Typically 3-5 poses (5 is maximum for exact synthesis).
ground_pivot_a – Optional fixed position for left ground pivot.
ground_pivot_d – Optional fixed position for right ground pivot.
max_solutions – Maximum number of solutions to return (None for all).
require_grashof – If True, reject non-Grashof solutions.
require_crank_rocker – If True, only accept crank-rocker type.
- Returns:
SynthesisResult containing valid linkages.
Example
>>> from pylinkage.synthesis import Pose, motion_generation >>> poses = [ ... Pose(0, 0, 0), ... Pose(1, 1, 0.5), ... Pose(2, 0.5, 1.0), ... ] >>> result = motion_generation(poses) >>> print(f"Found {len(result.solutions)} solutions")
- pylinkage.synthesis.motion_generation_3_poses(poses: list[Pose], n_samples: int = 36, max_solutions: int | None = 50) SynthesisResult
Specialized synthesis for exactly 3 poses.
With 3 poses, there is a continuous curve of solutions. This function samples the curve and returns multiple linkages.
- Parameters:
poses – Exactly 3 Pose objects.
n_samples – Number of samples along the solution curve.
max_solutions – Maximum raw solutions to collect (early termination).
- Returns:
SynthesisResult with sampled solutions from the curve.
- pylinkage.synthesis.multi_topology_synthesize(precision_points: list[tuple[float, float]], max_links: int = 8, synthesis_type: SynthesisType = SynthesisType.PATH, max_solutions_per_topology: int = 5, max_total_solutions: int = 20, n_orientation_samples: int = 24) list[TopologySolution]
Synthesize linkages across all compatible topologies.
Tries every topology from the built-in catalog (up to
max_links) and returns solutions ranked by quality metrics (path accuracy, transmission angle, link ratios, compactness, simplicity).For four-bars, delegates to the existing
path_generation(). For six-bars, delegates tosix_bar_path_generation(). For eight-bars and beyond, delegates togeneralized_synthesis().- Parameters:
precision_points – Target (x, y) points.
max_links – Maximum number of links to consider (4, 6, or 8).
synthesis_type –
PATH,FUNCTION, orMOTION. Currently only PATH is supported for multi-topology.max_solutions_per_topology – Max solutions per topology type.
max_total_solutions – Total solutions to return.
n_orientation_samples – Orientation search density.
- Returns:
List of TopologySolution ranked by
overall_score(best first).- Raises:
NotImplementedError – If synthesis_type is not PATH.
Example
>>> solutions = synthesize([(0,1), (1,2), (2,1.5), (3,0), (4,1)]) >>> print(f"Best: {solutions[0].topology_entry.name}")
- pylinkage.synthesis.nbar_solution_to_linkage(solution: NBarSolution, name: str = 'synthesized', iterations: int = 360) SimLinkage
Convert an NBarSolution to a Linkage object.
Loads the topology from the catalog, decomposes it to determine solving order, and maps joint positions from the solution onto Static, Crank, and Revolute joints.
For six-bars, delegates to the specialized six-bar converter in
six_bar.- Parameters:
solution – NBarSolution with topology_id and joint_positions.
name – Name for the linkage.
iterations – Number of simulation steps per rotation.
- Returns:
Linkage object ready for simulation.
- Raises:
ValueError – If topology_id is not found in the catalog.
- pylinkage.synthesis.path_generation(precision_points: list[tuple[float, float]], coupler_point_offset: tuple[float, float] = (0.0, 0.0), ground_pivot_a: tuple[float, float] | None = None, ground_pivot_d: tuple[float, float] | None = None, n_orientation_samples: int | None = None, max_solutions: int | None = 10, require_grashof: bool = True, require_crank_rocker: bool = False, *, orientation_resolution: int = 6) SynthesisResult
Synthesize a four-bar for path generation.
Find four-bar linkages where a coupler point passes through the specified precision points.
For path generation without prescribed timing, the coupler orientation at each point is a free variable. This function searches over orientation candidates using Burmester theory, then verifies each surviving candidate by simulating it.
A call typically costs on the order of a hundred milliseconds and can reach several seconds, so prefer not to place it inside a loop or behind an interactive control without lowering
max_solutionsfirst. Cost grows exponentially in the number of precision points, since the search carries one free orientation per point after the first.- Parameters:
precision_points – List of (x, y) points the coupler should pass through. Best results with 3-5 points.
coupler_point_offset – Offset of traced point from coupler reference.
ground_pivot_a – Optional fixed position for left ground pivot.
ground_pivot_d – Optional fixed position for right ground pivot.
n_orientation_samples – Deprecated, removed in 2.0.0. It never denoted a number of samples; use
orientation_resolutioninstead. A value given here is translated to the search it used to produce.max_solutions – Maximum number of solutions to return (None for all). This is what governs the running time, since the search stops as soon as it has this many verified solutions. Lowering it is the effective way to make this function faster.
require_grashof – If True, reject non-Grashof solutions.
require_crank_rocker – If True, only accept crank-rocker type.
orientation_resolution – Angles sampled per free orientation. The grid holds
orientation_resolution ** (len(precision_points) - 1)candidates, so raising this is exponentially expensive. Lowering it is rarely a good trade: a coarser grid tends to return no solutions at all rather than fewer. Usemax_solutionsto control cost.
- Returns:
SynthesisResult containing valid linkages.
Example
>>> points = [(0, 1), (1, 2), (2, 1.5), (3, 0)] >>> result = path_generation(points) >>> print(f"Found {len(result.solutions)} solutions") >>> for linkage in result.solutions: ... linkage.show()
- pylinkage.synthesis.path_generation_with_timing(precision_points: list[tuple[float, float]], crank_angles: list[float], ground_pivot_a: tuple[float, float] = (0.0, 0.0), ground_length: float | None = None, require_grashof: bool = True) SynthesisResult
Synthesize a four-bar with prescribed timing.
Path generation with prescribed timing specifies both the coupler points AND the crank angles at which they should be reached. This is more constrained than general path generation.
- Parameters:
precision_points – List of (x, y) coupler points.
crank_angles – Crank angles (radians) for each point.
ground_pivot_a – Position of crank ground pivot.
ground_length – Fixed ground link length (optional).
require_grashof – If True, reject non-Grashof solutions.
- Returns:
SynthesisResult containing valid linkages.
Note
This is a more specialized synthesis that combines elements of path and function generation. With 4 points and 4 angles, exact synthesis is possible.
- pylinkage.synthesis.select_compatible_dyads(curves: BurmesterCurves, min_link_length: float = 0.01, max_link_length: float = 1000.0, ground_constraint: tuple[tuple[float, float], tuple[float, float]] | None = None, ground_tolerance: float = 0.1, max_pairs: int | None = None) list[tuple[BurmesterDyad, BurmesterDyad]]
Select pairs of dyads that form valid four-bar linkages.
Two dyads form a valid four-bar if: 1. Their center points are distinct (ground pivots) 2. Their circle points are distinct (coupler attachments) 3. Link lengths are within acceptable range 4. The resulting 4-bar can assemble
- Parameters:
curves – Burmester curves from which to select dyads.
min_link_length – Minimum acceptable link length.
max_link_length – Maximum acceptable link length.
ground_constraint – Optional (A, D) positions for ground pivots.
ground_tolerance – Tolerance for matching ground constraint.
max_pairs – Maximum number of pairs to return (early termination).
- Returns:
List of (dyad_left, dyad_right) pairs forming valid 4-bars.
- pylinkage.synthesis.six_bar_path_generation(precision_points: list[tuple[float, float]], topology: str = 'watt', max_solutions: int = 10, require_grashof_driver: bool = True, n_orientation_samples: int = 24) SynthesisResult
Synthesize a six-bar linkage for path generation.
Uses decomposition-based synthesis: the six-bar is split into Assur groups, and each group is synthesized sequentially using Burmester theory.
- For Watt type (2 stacked RRR dyads):
First dyad pair: driving four-bar synthesized on a subset of precision points.
Second dyad pair: refines path using remaining points, with the first dyad’s coupler providing anchor positions.
- Parameters:
precision_points – Target (x, y) points. 4-7 recommended for six-bars (more points than four-bar can handle).
topology –
"watt"or"stephenson".max_solutions – Maximum solutions to return.
require_grashof_driver – Require Grashof criterion on the driving four-bar loop.
n_orientation_samples – Orientation search density per stage.
- Returns:
SynthesisResult containing valid six-bar Linkage objects.
Example
>>> points = [(0, 0), (1, 2), (2, 3), (3, 2), (4, 0)] >>> result = six_bar_path_generation(points, topology="watt") >>> print(f"Found {len(result.solutions)} solutions")
- pylinkage.synthesis.solution_to_linkage(solution: FourBarSolution, name: str = 'synthesized', iterations: int = 360) SimLinkage
Convert a single FourBarSolution to a Linkage object.
Creates a four-bar linkage with:
Two Ground components as ground pivots
One Crank as motor input, or an ArcCrank sweeping
solution.arc_limitswhen those are setOne RRRDyad connecting crank to rocker
Optionally, a FixedDyad for the coupler point that traces the target path (only if
solution.coupler_pointis set)
arc_limitsare crank angles relative to the ground line, from pivot A towards pivot D, ascrank_angle_limits()returns them. Whichever side of the ground line the crank starts on, the arc is placed there. Set them for a solution whose crank cannot turn fully (a double-rocker, or a non-Grashof linkage); a plain Crank would otherwise drive the linkage into an unbuildable position.- Parameters:
solution – FourBarSolution containing geometry.
name – Name for the linkage.
iterations – Number of simulation steps per rotation (per sweep of the arc, for an ArcCrank).
- Returns:
SimLinkage object ready for simulation.
- pylinkage.synthesis.solutions_to_linkages(solutions: list[FourBarSolution], synthesis_type: SynthesisType, iterations: int = 360) list[SimLinkage]
Convert multiple solutions to Linkage objects.
- Parameters:
solutions – List of FourBarSolution objects.
synthesis_type – Type of synthesis (for naming).
iterations – Number of simulation steps per rotation.
- Returns:
List of SimLinkage objects.
- pylinkage.synthesis.stephenson_from_lengths(crank: float, coupler: float, rocker: float, link4: float, link5: float, link6: float, ground_length: float, ground_pivot_a: Point2D = (0.0, 0.0), initial_crank_angle: float = 0.0, iterations: int = 360, name: str = 'stephenson') SimLinkage
Create a Stephenson six-bar linkage from link lengths.
A Stephenson six-bar has two four-bar loops where the second loop branches from the coupler joint C and the ground pivot D, closing back to the crank output B. This separates the two ternary links (unlike the Watt, where they are adjacent).
Kinematic chain:
A ─── crank ─── B ─── coupler ─── C ─── rocker ─── D (input) │ │ │ │ └── link4 ───┐ │ └── link6 ── F ── link5 ── E ───┘ │ └─────────┘Loop 1: A → B → C → D (four-bar: crank + coupler + rocker)
Loop 2: C,D → E, then E → F → B (second chain branches from coupler C and ground D, closing back to crank output B)
The topological difference from Watt: in the Watt chain, E depends on (B, C) — both joints of the coupler link, making the ternary links adjacent. Here E depends on (C, D) — the coupler endpoint and ground, separating the ternary links.
- Parameters:
crank – Length of input crank (A–B).
coupler – Length of coupler (B–C).
rocker – Length of rocker (C–D).
link4 – Distance from C to E.
link5 – Distance from D to E.
link6 – Distance from E to F.
ground_length – Distance between ground pivots A and D.
ground_pivot_a – Position of first ground pivot (A).
initial_crank_angle – Starting crank angle in radians.
iterations – Simulation steps per full rotation.
name – Name for the linkage.
- Returns:
Linkage built from the component/actuator/dyad API.
- Raises:
ValueError – If the mechanism cannot be assembled with the given link lengths at the initial crank angle.
Example
>>> linkage = stephenson_from_lengths( ... crank=0.8, coupler=3.5, rocker=3.0, ... link4=2.0, link5=2.5, link6=3.0, ... ground_length=4.0, ... ) >>> len(list(linkage.step(iterations=100))) 100
- pylinkage.synthesis.validate_fourbar(solution: FourBarSolution) tuple[bool, list[str]]
Validate a four-bar solution for geometric consistency.
Checks for: - Non-zero link lengths - Positive link lengths - Assembly feasibility (triangle inequality)
- Parameters:
solution – FourBarSolution to validate.
- Returns:
Tuple of (is_valid, list of error messages).
- pylinkage.synthesis.verify_function_generation(linkage: Linkage, angle_pairs: list[AnglePair], tolerance: float = 0.05) tuple[bool, list[float]]
Verify that a linkage satisfies function generation requirements.
Simulates the linkage at each input angle and checks if the output angle matches the specification.
- Parameters:
linkage – The linkage to verify.
angle_pairs – Expected (input_angle, output_angle) pairs.
tolerance – Maximum acceptable angle error in radians.
- Returns:
Tuple of (all_satisfied, list of actual errors for each pair).
- pylinkage.synthesis.verify_path_generation(linkage: Linkage, precision_points: list[PrecisionPoint], tolerance: float | None = None) tuple[bool, list[float]]
Verify that a synthesized linkage’s coupler point passes near target points.
Simulates the linkage for one full crank rotation and finds, for each precision point, the minimum distance from the coupler point trajectory to that target point.
The linkage must have a joint named “P” (the coupler point tracker added by
solution_to_linkage). If no such joint exists, the last joint in the solve order is used as the coupler point.- Parameters:
linkage – The synthesized Linkage to verify.
precision_points – Target (x, y) points the coupler should pass through.
tolerance – Maximum acceptable distance for each point. If None, defaults to 5% of the bounding box diagonal of the precision points.
- Returns:
Tuple of (all_satisfied, distances) where distances[i] is the minimum distance from the coupler trajectory to precision_points[i].
- pylinkage.synthesis.watt_from_lengths(crank: float, coupler1: float, rocker1: float, link4: float, link5: float, rocker2: float, ground_length: float, ground_pivot_a: Point2D = (0.0, 0.0), initial_crank_angle: float = 0.0, iterations: int = 360, name: str = 'watt') SimLinkage
Create a Watt six-bar linkage from link lengths.
A Watt six-bar has two four-bar loops sharing the crank output. The crank output (B) connects to two separate dyads, both of which close back to the second ground pivot (D).
Kinematic chain:
A ─── crank ─── B ─── coupler1 ─── C ─── rocker1 ─── D (input) │ │ └─── link4 ─── E ─── link5 ──── rocker2 ─── DIn Assur group terms: the crank drives two cascaded RRR dyads.
Loop 1: A → B → C → D (four-bar: crank + coupler1 + rocker1)
Loop 2: B → E → F → D (second chain: link4 + link5 + rocker2, where E is constrained by B and C)
- Parameters:
crank – Length of input crank (A–B).
coupler1 – Length of first coupler link (B–C).
rocker1 – Length of first rocker (C–D).
link4 – Distance from B to E (first arm of second loop).
link5 – Distance from C to E (second arm, constraining E).
rocker2 – Distance from E to D (closing link back to ground).
ground_length – Distance between ground pivots A and D.
ground_pivot_a – Position of first ground pivot (A).
initial_crank_angle – Starting crank angle in radians.
iterations – Simulation steps per full rotation.
name – Name for the linkage.
- Returns:
Linkage built from the component/actuator/dyad API.
- Raises:
ValueError – If the mechanism cannot be assembled with the given link lengths at the initial crank angle.
Example
>>> linkage = watt_from_lengths( ... crank=1.5, coupler1=4.0, rocker1=3.5, ... link4=3.0, link5=2.5, rocker2=3.0, ... ground_length=6.0, ... ) >>> len(list(linkage.step(iterations=100))) 100