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_limits when those are set

  • One RRRDyad connecting crank to rocker

  • Optionally, a FixedDyad for the coupler point that traces the target path (only if solution.coupler_point is set)

arc_limits are crank angles relative to the ground line, from pivot A towards pivot D, as crank_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 ─── D

In 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: object

Circle 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: object

A 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

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: object

Definition 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: object

Result 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 ensemble property, which returns an Ensemble for 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_solutions as 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”

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 to six_bar_path_generation(). For eight-bars and beyond, delegates to generalized_synthesis().

Parameters:
  • precision_points – Target (x, y) points.

  • max_links – Maximum number of links to consider (4, 6, or 8).

  • synthesis_typePATH, FUNCTION, or MOTION. 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_solutions first. 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_resolution instead. 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. Use max_solutions to 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).

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:

  1. Synthesize the driving four-bar (first dyad pair) via Burmester on a subset of precision points.

  2. Simulate the driving four-bar to compute intermediate coupler positions at the remaining precision points.

  3. Synthesize the second dyad using those intermediate positions.

  4. 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: object

Result 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: object

A 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]]
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: object

Quality 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
min_transmission_angle: float = 0.0
overall_score: float = inf
path_accuracy: float = inf
class pylinkage.synthesis.topology_types.TopologySolution(solution: NBarSolution, linkage: Linkage, topology_entry: CatalogEntry, metrics: QualityMetrics)

Bases: object

A 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.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: Enum

Classification 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, or None when 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}")

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: object

Circle 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: object

A 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: NamedTuple

A 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: NamedTuple

A 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: Enum

Classification 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: object

Result 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: object

A 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: object

A 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: object

Quality 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: object

Definition 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: object

Result 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 ensemble property, which returns an Ensemble for 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_solutions as 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: Enum

Type 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: object

A 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, or None when 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 to six_bar_path_generation(). For eight-bars and beyond, delegates to generalized_synthesis().

Parameters:
  • precision_points – Target (x, y) points.

  • max_links – Maximum number of links to consider (4, 6, or 8).

  • synthesis_typePATH, FUNCTION, or MOTION. 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_solutions first. 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_resolution instead. 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. Use max_solutions to 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_limits when those are set

  • One RRRDyad connecting crank to rocker

  • Optionally, a FixedDyad for the coupler point that traces the target path (only if solution.coupler_point is set)

arc_limits are crank angles relative to the ground line, from pivot A towards pivot D, as crank_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 ─── D

In 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