pylinkage package

Subpackages

Submodules

pylinkage.dimensions module

Dimensional data for linkage mechanisms.

This module provides the Dimensions class which holds all geometric data (positions, distances, angles) separate from topology. This separation allows pure topological analysis without geometric constraints.

The Dimensions class is shared by both the hypergraph and assur modules.

class pylinkage.dimensions.Dimensions(node_positions: dict[str, tuple[float, float]] = <factory>, driver_angles: dict[str, ~pylinkage.dimensions.DriverAngle] = <factory>, edge_distances: dict[str, float] = <factory>, hyperedge_constraints: dict[str, dict[tuple[str, str], float]] = <factory>, name: str = '')

Bases: object

Geometric data for a linkage topology.

Holds all dimensional information separate from the topological structure. This allows the same topology to be instantiated with different dimensions.

Variables:
  • node_positions (dict[str, tuple[float, float]]) – Mapping from node ID to (x, y) coordinates.

  • driver_angles (dict[str, pylinkage.dimensions.DriverAngle]) – Mapping from driver node ID to angular parameters.

  • edge_distances (dict[str, float]) – Mapping from edge ID to link length.

  • hyperedge_constraints (dict[str, dict[tuple[str, str], float]]) – Mapping from hyperedge ID to pairwise distance constraints. Each value is a dict mapping (node1, node2) pairs to distances.

  • name (str) – Optional name for this dimension set.

Example

>>> dims = Dimensions(
...     node_positions={"A": (0.0, 0.0), "B": (1.0, 0.0)},
...     driver_angles={"B": DriverAngle(0.1, 0.0)},
...     edge_distances={"AB": 1.0},
... )
copy() Dimensions

Create a deep copy of this Dimensions object.

Returns:

A new Dimensions object with copied data.

driver_angles: dict[str, DriverAngle]
edge_distances: dict[str, float]
classmethod from_dict(data: dict[str, Any]) Dimensions

Build a Dimensions from a dict produced by to_dict().

Accepts both the canonical [node1, node2, distance] triples emitted by to_dict() and the legacy {"('a', 'b')": distance} stringified-tuple form.

get_driver_angle(node_id: str) DriverAngle | None

Get the driver angle parameters for a node.

Parameters:

node_id – The node identifier.

Returns:

The DriverAngle, or None if not defined.

get_edge_distance(edge_id: str) float | None

Get the distance constraint for an edge.

Parameters:

edge_id – The edge identifier.

Returns:

The distance, or None if not defined.

get_hyperedge_distance(hyperedge_id: str, node1: str, node2: str) float | None

Get a pairwise distance constraint from a hyperedge.

Parameters:
  • hyperedge_id – The hyperedge identifier.

  • node1 – First node in the pair.

  • node2 – Second node in the pair.

Returns:

The distance, or None if not defined.

get_node_position(node_id: str) tuple[float, float] | None

Get the position of a node.

Parameters:

node_id – The node identifier.

Returns:

The (x, y) position, or None if not defined.

hyperedge_constraints: dict[str, dict[tuple[str, str], float]]
name: str = ''
node_positions: dict[str, tuple[float, float]]
to_dict() dict[str, Any]

Return a JSON-safe dict representation.

Hyperedge constraint keys are tuples of node IDs; to stay within JSON’s string-only key model they are serialised as [node1, node2] lists rather than stringified tuples. The companion from_dict() accepts both that canonical shape and the legacy "('a', 'b')" stringified form for back-compat.

validate_against(node_ids: Iterable[str], edge_ids: Iterable[str], hyperedge_ids: Iterable[str] | None = None) list[str]

Validate that dimensions are compatible with a topology.

Checks that all referenced node/edge IDs exist in the provided topology ID sets.

Parameters:
  • node_ids – Valid node IDs from the topology.

  • edge_ids – Valid edge IDs from the topology.

  • hyperedge_ids – Valid hyperedge IDs from the topology (optional).

Returns:

List of error messages. Empty list means validation passed.

Example

>>> dims = Dimensions(node_positions={"A": (0, 0), "X": (1, 1)})
>>> errors = dims.validate_against(["A", "B"], ["AB"])
>>> "X" in errors[0]  # "X" is not a valid node
True
class pylinkage.dimensions.DriverAngle(angular_velocity: float, initial_angle: float = 0.0)

Bases: object

Angular parameters for a driver joint.

Variables:
  • angular_velocity (float) – Rotation angle per simulation step (radians).

  • initial_angle (float) – Starting angle of the driver (radians).

angular_velocity: float
classmethod from_dict(data: dict[str, Any]) DriverAngle

Build a DriverAngle from a dict produced by to_dict().

initial_angle: float = 0.0
to_dict() dict[str, float]

Return a JSON-safe dict representation.

pylinkage.exceptions module

The exceptions module is a simple quick way to access the built-in exceptions.

Created on Wed Jun 16, 15:20:06 2021.

@author: HugoFara

exception pylinkage.exceptions.NotCompletelyDefinedError(joint: Any, message: str = 'The joint is not completely defined!')

Bases: Exception

The linkage definition is incomplete.

exception pylinkage.exceptions.OptimizationError(message: str = 'Optimization failed')

Bases: Exception

Should be raised when the optimization process fails.

exception pylinkage.exceptions.UnbuildableError(joint: Any, message: str = 'Unable to solve constraints')

Bases: Exception

Should be raised when the constraints cannot be solved.

exception pylinkage.exceptions.UnderconstrainedError(linkage: Linkage | str, message: str = 'The linkage is under-constrained!')

Bases: Exception

The linkage is under-constrained and multiple solutions may exist.

Module contents

PyLinkage is a module to create, optimize and visualize linkages.

Please see the documentation at https://hugofara.github.io/pylinkage/. A copy of the documentation should have been distributed on your system in the docs/ folder.

Created on Thu Jun 10 21:30:52 2021

@author: HugoFara

class pylinkage.ArcCrank(anchor: Ground, radius: float, angular_velocity: float = 0.017453292519943295, arc_start: float = 0.0, arc_end: float = 3.141592653589793, initial_angle: float | None = None, name: str | None = None)

Bases: ConnectedComponent

A motor-driven oscillating rotary input (arc crank).

An arc crank oscillates around a ground anchor between two angle limits at constant angular velocity, producing an output point that traces an arc. Direction reverses (“bounces”) when reaching angle limits, similar to LinearActuator behavior at stroke limits.

Variables:
  • anchor (Ground) – The ground point this arc crank rotates around.

  • radius (float) – Distance from anchor to output.

  • angular_velocity (float) – Rotation rate magnitude in radians per step.

  • arc_start (float) – Minimum angle limit in radians.

  • arc_end (float) – Maximum angle limit in radians.

  • initial_angle (float) – Starting angle in radians (must be between arc_start and arc_end).

Example

>>> O1 = Ground(0.0, 0.0, name="O1")
>>> arc_crank = ArcCrank(
...     anchor=O1,
...     radius=1.0,
...     angular_velocity=0.1,
...     arc_start=0.0,
...     arc_end=math.pi/2,
... )
>>> arc_crank.position
(1.0, 0.0)
anchor: Ground
property anchors: tuple[Ground]

Return the parent dyads (just the ground anchor).

property angle: float

Return current angle.

angular_velocity: float
arc_end: float
arc_start: float
get_constraints() tuple[float, float, float]

Return optimizable constraints.

Returns:

Tuple containing (radius, arc_start, arc_end).

initial_angle: float
property output: _AnchorProxy

Return the output joint (end of the arc crank).

This proxy can be used as an anchor for other dyads.

Returns:

An anchor proxy representing the arc crank output.

radius: float
reload(dt: float = 1) None

Advance the arc crank by one step.

Rotates the arc crank position by angular_velocity * dt radians, reversing direction when hitting angle limits.

Parameters:

dt – Time step multiplier.

Raises:

ValueError – If anchor position is undefined.

set_constraints(radius: float | None = None, arc_start: float | None = None, arc_end: float | None = None, *args: float | None) None

Set the constraints.

Parameters:
  • radius – New radius value.

  • arc_start – New arc_start value.

  • arc_end – New arc_end value.

  • *args – Ignored (for interface compatibility).

pylinkage.ComponentId

alias of str

class pylinkage.Crank(anchor: Ground, radius: float, angular_velocity: float = 0.017453292519943295, initial_angle: float = 0.0, name: str | None = None)

Bases: ConnectedComponent

A motor-driven rotary input (crank).

A crank rotates around a ground anchor at constant angular velocity, producing an output point that traces a circle. This is the primary input driver for most linkage mechanisms.

Variables:
  • anchor (Ground) – The ground point this crank rotates around.

  • radius (float) – Distance from anchor to output.

  • angular_velocity (float) – Rotation rate in radians per step.

  • initial_angle (float) – Starting angle in radians.

Example

>>> O1 = Ground(0.0, 0.0, name="O1")
>>> crank = Crank(anchor=O1, radius=1.0)
>>> crank.position
(1.0, 0.0)
>>> crank.output.position  # Same as crank.position
(1.0, 0.0)
anchor: Ground
property anchors: tuple[Ground]

Return the parent dyads (just the ground anchor).

angular_velocity: float
get_constraints() tuple[float]

Return the radius (optimizable constraint).

Returns:

Tuple containing the crank radius.

initial_angle: float
property output: _AnchorProxy

Return the output joint (end of the crank).

This proxy can be used as an anchor for other dyads.

Returns:

An anchor proxy representing the crank output.

radius: float
reload(dt: float = 1) None

Advance the crank by one step.

Rotates the crank position by angular_velocity * dt radians.

Parameters:

dt – Time step multiplier.

Raises:

ValueError – If anchor position is undefined.

set_constraints(distance: float | None = None, *args: float | None) None

Set the radius constraint.

Parameters:
  • distance – New radius value.

  • *args – Ignored (for interface compatibility).

pylinkage.EdgeId

alias of str

class pylinkage.FixedDyad(anchor1: Component | _AnchorProxy, anchor2: Component | _AnchorProxy, distance: float, angle: float, name: str | None = None)

Bases: BinaryDyad

Fixed Dyad - deterministic polar projection.

Positions a joint at a fixed distance and angle from anchor1, with the angle measured relative to the line from anchor1 to anchor2.

Unlike RRRDyad which has two possible solutions, FixedDyad always has exactly one deterministic solution.

Variables:
  • anchor1 – First anchor (origin for polar coordinates).

  • anchor2 – Second anchor (defines reference direction).

  • distance (float) – Distance from anchor1 to this joint.

  • angle (float) – Angle offset from anchor1->anchor2 direction (radians).

Example

>>> O1 = Ground(0.0, 0.0, name="O1")
>>> O2 = Ground(2.0, 0.0, name="O2")
>>> crank = Crank(anchor=O1, radius=1.0)
>>> # Create a point at 90 degrees from crank->O2 line
>>> fixed = FixedDyad(
...     anchor1=crank.output,
...     anchor2=O2,
...     distance=1.0,
...     angle=math.pi/2,
...     name="coupler_point"
... )
angle: float
distance: float
get_constraints() tuple[float, float]

Return the distance and angle constraints.

Returns:

Tuple of (distance, angle).

reload(dt: float = 1) None

Recompute position using polar projection.

The position is always deterministic - no ambiguity.

Parameters:

dt – Time step (unused for Fixed, but required for interface).

set_constraints(distance: float | None = None, angle: float | None = None, *args: float | None) None

Set the distance and angle constraints.

Parameters:
  • distance – New distance from anchor1.

  • angle – New angle offset (radians).

  • *args – Ignored (for interface compatibility).

class pylinkage.Ground(x: float, y: float, name: str | None = None)

Bases: Component

A fixed point on the frame (ground link).

Ground components define the stationary reference points of a mechanism. They don’t move during simulation and serve as anchors for other kinematic elements like actuators and dyads.

Example

>>> O1 = Ground(0.0, 0.0, name="O1")
>>> O2 = Ground(2.0, 0.0, name="O2")
>>> O1.position
(0.0, 0.0)
get_constraints() tuple[()]

Return empty tuple - ground has no constraints to optimize.

reload(dt: float = 1) None

No-op - ground doesn’t move.

set_constraints(*args: float | None) None

No-op - ground has no constraints.

pylinkage.HyperedgeId

alias of str

class pylinkage.JointType(value)

Bases: IntEnum

Kinematic joint type.

This enum classifies joints by their allowed degrees of freedom. Values are explicit integers for serialization stability.

Variables:
  • REVOLUTE – Pin joint allowing rotation (R). 1 DOF rotation.

  • PRISMATIC – Slider joint allowing translation (P). 1 DOF translation.

  • GROUND – Fixed revolute joint on frame. Special case for mechanism module.

  • TRACKER – Observer joint (T). 0 DOF, just tracks a position.

GROUND = 3
PRISMATIC = 2
REVOLUTE = 1
TRACKER = 4
class pylinkage.LinearActuator(anchor: Ground, angle: float, stroke: float, speed: float = 0.1, initial_extension: float = 0.0, name: str | None = None)

Bases: ConnectedComponent

A motor-driven linear input (linear actuator).

A linear actuator moves along a line from its anchor at constant speed, producing an output point that oscillates between 0 and the stroke limit. This provides linear reciprocating motion.

Variables:
  • anchor (Ground) – The ground point this actuator extends from.

  • angle (float) – Direction angle in radians (from +x axis).

  • stroke (float) – Maximum extension distance.

  • speed (float) – Linear speed magnitude (units per step).

  • initial_extension (float) – Starting extension from anchor.

Example

>>> O1 = Ground(0.0, 0.0, name="O1")
>>> actuator = LinearActuator(anchor=O1, angle=0.0, stroke=2.0, speed=0.1)
>>> actuator.position
(0.0, 0.0)
>>> actuator.reload()
>>> actuator.position  # Moved 0.1 units along x-axis
(0.1, 0.0)
anchor: Ground
property anchors: tuple[Ground]

Return the parent dyads (just the ground anchor).

angle: float
property extension: float

Return current extension from anchor.

get_constraints() tuple[float, float]

Return the stroke and speed (optimizable constraints).

Returns:

Tuple containing (stroke, speed).

initial_extension: float
property output: _AnchorProxy

Return the output joint (end of the actuator).

This proxy can be used as an anchor for other dyads.

Returns:

An anchor proxy representing the actuator output.

reload(dt: float = 1) None

Advance the actuator by one step.

Moves the actuator position by speed * dt, reversing direction when hitting stroke limits.

Parameters:

dt – Time step multiplier.

Raises:

ValueError – If anchor position is undefined.

set_constraints(stroke: float | None = None, speed: float | None = None, *args: float | None) None

Set the constraints.

Parameters:
  • stroke – New stroke value (must be positive).

  • speed – New speed value.

  • *args – Ignored (for interface compatibility).

speed: float
stroke: float
class pylinkage.Linkage(components: Iterable[Component], order: Iterable[Component] | None = None, name: str | None = None)

Bases: object

A planar linkage mechanism built from components.

The Linkage class orchestrates a collection of components (Ground points, actuators, and dyads) to simulate a planar mechanism. It handles solve order computation, stepping, and constraint management.

Example

>>> from pylinkage.components import Ground
>>> from pylinkage.actuators import Crank
>>> from pylinkage.dyads import RRRDyad
>>> from pylinkage.simulation import Linkage
>>>
>>> O1 = Ground(0.0, 0.0, name="O1")
>>> O2 = Ground(2.0, 0.0, name="O2")
>>> crank = Crank(anchor=O1, radius=1.0, angular_velocity=0.1)
>>> rocker = RRRDyad(crank.output, O2, distance1=2.0, distance2=1.5)
>>> linkage = Linkage([O1, O2, crank, rocker], name="Four-Bar")
>>> for positions in linkage.step():
...     print(positions)
analyze_sensitivity(output_joint: object | int | None = None, delta: float = 0.01, include_transmission: bool = True, iterations: int | None = None) SensitivityAnalysis

Compute sensitivity of an output path to constraint perturbations.

See pylinkage.linkage.analyze_sensitivity().

analyze_stroke(prismatic_joint: object | None = None, iterations: int | None = None) StrokeAnalysis

Analyze stroke/slide position over a full motion cycle.

See pylinkage.linkage.analyze_stroke().

analyze_tolerance(tolerances: dict[str, float], output_joint: object | int | None = None, iterations: int | None = None, n_samples: int = 1000, seed: int | None = None) ToleranceAnalysis

Monte-Carlo tolerance analysis over the output path.

See pylinkage.linkage.analyze_tolerance().

analyze_transmission(iterations: int | None = None, acceptable_range: tuple[float, float] = (40.0, 140.0)) TransmissionAngleAnalysis

Analyze transmission angle over a full motion cycle.

See pylinkage.linkage.analyze_transmission() for details.

compile() None

Pre-compile the numba solver state for step_fast().

Cached on self._solver_data and reused until invalidated by a call to compile() again.

components: tuple[Component, ...]
property dyads: tuple[Component, ...]

Return components (backwards compatibility alias).

get_accelerations() list[tuple[float, float] | None]

Return accelerations for all components.

Returns:

List of (ax, ay) tuples, one per component. Returns None for components whose acceleration has not been computed.

get_constraints() list[float]

Return all geometric constraints as a flat list.

Returns:

Flat list of all constraint values, used by optimizers.

get_coords() list[tuple[float | None, float | None]]

Return positions of all components.

Returns:

List of (x, y) positions.

get_rotation_period() int

Return number of steps for one full cycle.

Computes the LCM of all actuator periods (cranks, arc cranks, and linear actuators). For cranks, period is 2*pi / angular_velocity. For arc cranks, period is 2 * (arc_end - arc_start) / angular_velocity. For linear actuators, period is 2 * stroke / velocity.

Returns:

Number of iterations with dt=1.

get_velocities() list[tuple[float, float] | None]

Return velocities for all components.

Returns:

List of (vx, vy) tuples, one per component. Returns None for components whose velocity has not been computed.

indeterminacy() int

Mobility (DOF) of the linkage — planar Gruebler-Kutzbach.

DOF = 3·(n 1) 2·R P where each non-ground component contributes its share of bodies and kinematic pairs:

  • Ground anchors are points on the frame (no new body, no new pair on their own);

  • Crank / LinearActuator add 1 body + 1 R/P pair;

  • binary dyads (RRRDyad, FixedDyad) add 2 bodies + 3 R-pairs;

  • RRPDyad adds 2 bodies + 2 R-pairs + 1 P-pair.

A standard Grashof four-bar (Crank + RRRDyad) returns 1.

name: str
rebuild(positions: list[tuple[float, float]] | None = None) None

Rebuild the linkage, optionally setting initial positions.

Parameters:

positions – Initial positions for each component. If None, uses current positions.

set_completely(constraints: list[float], positions: list[tuple[float, float]]) None

Apply both constraints and initial positions in one call.

Parameters:
  • constraints – Flat list (as accepted by set_constraints()).

  • positions – Per-component (x, y) positions (as accepted by set_coords()).

set_constraints(values: list[float]) None

Set constraints from a flat list.

Used to apply optimization results. Invalidates any cached SolverData so the next step_fast() recompiles.

Parameters:

values – Flat list of constraint values.

set_coords(coords: list[tuple[float, float]]) None

Set positions for all components.

Parameters:

coords – List of (x, y) positions.

set_input_velocity(actuator: Crank, omega: float, alpha: float = 0.0) None

Set angular velocity and acceleration for a crank actuator.

This is used for kinematics computation (velocity/acceleration analysis). The omega value will be used to compute linear velocities at each joint.

Parameters:
  • actuator – The crank actuator to set velocity for.

  • omega – Angular velocity in rad/s (physical units for analysis).

  • alpha – Angular acceleration in rad/s² (default 0).

Raises:

ValueError – If the actuator is not part of this linkage.

Example

>>> linkage.set_input_velocity(crank, omega=10.0)  # 10 rad/s
>>> for pos, vel, acc in linkage.step_with_derivatives():
...     print(f"Position: {pos}, Velocity: {vel}")
simulation(iterations: int | None = None, dt: float = 1.0) _SimulationContext

Return a context manager that simulates this linkage.

The context restores the initial joint positions on exit, so repeated invocations return to the same starting state.

step(iterations: int | None = None, dt: float = 1) Generator[tuple[tuple[float | None, float | None], ...], None, None]

Simulate the linkage.

Yields positions for all components at each step.

Parameters:
  • iterations – Number of steps. If None, uses get_rotation_period().

  • dt – Time step multiplier for actuators (cranks and linear actuators).

Yields:

Tuple of (x, y) positions for each component.

step_fast(iterations: int | None = None, dt: float = 1) ndarray[tuple[Any, ...], dtype[float64]]

Run the simulation through the numba-compiled solver.

Significantly faster than step() for large iteration counts because it avoids per-step Python dispatch.

Parameters:
  • iterations – Number of steps. Defaults to get_rotation_period().

  • dt – Time step multiplier (default 1.0).

Returns:

Trajectory array of shape (iterations, n_components, 2). Unbuildable configurations appear as NaN — check with np.isnan(trajectory).any().

step_fast_with_kinematics(iterations: int | None = None, dt: float = 1.0) tuple[ndarray[tuple[Any, ...], dtype[float64]], ndarray[tuple[Any, ...], dtype[float64]], ndarray[tuple[Any, ...], dtype[float64]]]

Run the numba-compiled simulation, returning velocities and accelerations.

Per-crank omega/alpha inputs must be set via set_input_velocity() (cranks without an explicit input default to zero).

Parameters:
  • iterations – Number of steps. Defaults to get_rotation_period().

  • dt – Time step multiplier (default 1.0).

Returns:

(positions, velocities, accelerations) — each a numpy array of shape (iterations, n_components, 2).

step_with_derivatives(iterations: int | None = None, dt: float = 1) Generator[tuple[tuple[tuple[float | None, float | None], ...], tuple[tuple[float, float] | None, ...], tuple[tuple[float, float] | None, ...]], None, None]

Simulate the linkage with velocity and acceleration computation.

Yields positions, velocities, and accelerations for all components at each step. Requires that omega (and optionally alpha) is set on crank actuators via set_input_velocity().

Parameters:
  • iterations – Number of steps. If None, uses get_rotation_period().

  • dt – Time step multiplier for actuators (cranks and linear actuators).

Yields:

Tuple of (positions, velocities, accelerations) where

  • positions: Tuple of (x, y) for each component

  • velocities: Tuple of (vx, vy) or None for each component

  • accelerations: Tuple of (ax, ay) or None for each component

Example

>>> linkage.set_input_velocity(crank, omega=10.0)
>>> for pos, vel, acc in linkage.step_with_derivatives():
...     print(f"Crank velocity: {vel[2]}")
stroke_position() float

Slide position of a prismatic joint at the current pose.

See pylinkage.linkage.stroke_at_position().

to_hypergraph() tuple[HypergraphLinkage, Dimensions]

Return a hypergraph view of this linkage.

Delegates to pylinkage.hypergraph.from_sim_linkage(). The return is a tuple (HypergraphLinkage, Dimensions).

transmission_angle() float

Transmission angle at the current pose, in degrees.

See pylinkage.linkage.transmission_angle_at_position().

pylinkage.NodeId

alias of str

class pylinkage.NodeRole(value)

Bases: IntEnum

Role of a node/joint in the mechanism.

Classifies joints by their kinematic role in the linkage structure.

Variables:
  • GROUND – Fixed frame point that does not move.

  • DRIVER – Input/motor joint that provides motion (actuated).

  • DRIVEN – Position computed from constraints (passive, part of Assur groups).

DRIVEN = 2
DRIVER = 1
GROUND = 0
exception pylinkage.NotCompletelyDefinedError(joint: Any, message: str = 'The joint is not completely defined!')

Bases: Exception

The linkage definition is incomplete.

exception pylinkage.OptimizationError(message: str = 'Optimization failed')

Bases: Exception

Should be raised when the optimization process fails.

class pylinkage.PPDyad(line1_anchor1: Component | _AnchorProxy, line1_anchor2: Component | _AnchorProxy, line2_anchor1: Component | _AnchorProxy, line2_anchor2: Component | _AnchorProxy, x: float | None = None, y: float | None = None, name: str | None = None)

Bases: ConnectedComponent

PP Dyad - line-line intersection.

Positions a joint at the intersection of two lines: - Line 1: defined by line1_anchor1 and line1_anchor2 - Line 2: defined by line2_anchor1 and line2_anchor2

This dyad has no distance constraints - its position is fully determined by the four line-defining anchor points.

Variables:
  • line1_anchor1 (Component | _AnchorProxy) – First point defining line 1.

  • line1_anchor2 (Component | _AnchorProxy) – Second point defining line 1.

  • line2_anchor1 (Component | _AnchorProxy) – First point defining line 2.

  • line2_anchor2 (Component | _AnchorProxy) – Second point defining line 2.

Example

>>> A = Ground(0.0, 0.0, name="A")
>>> B = Ground(2.0, 0.0, name="B")
>>> C = Ground(0.0, 1.0, name="C")
>>> D = Ground(2.0, 2.0, name="D")
>>> joint = PPDyad(
...     line1_anchor1=A,
...     line1_anchor2=B,
...     line2_anchor1=C,
...     line2_anchor2=D,
...     name="intersection"
... )
property anchors: tuple[Component, Component, Component, Component]

Return the parent dyads (four line anchors).

get_constraints() tuple[()]

Return the constraints (none for PP dyad).

PP dyads have no distance constraints - position is fully determined by the four anchor points.

Returns:

Empty tuple (no constraints).

line1_anchor1: Component | _AnchorProxy
line1_anchor2: Component | _AnchorProxy
line2_anchor1: Component | _AnchorProxy
line2_anchor2: Component | _AnchorProxy
reload(dt: float = 1) None

Recompute position using line-line intersection.

Parameters:

dt – Time step (unused for PP, but required for interface).

Raises:

UnbuildableError – If lines are parallel (no intersection).

set_constraints(*args: float | None) None

Set constraints (no-op for PP dyad).

PP dyads have no constraints to set.

Parameters:

*args – Ignored (for interface compatibility).

class pylinkage.PointTracker(anchor1: Component | _AnchorProxy, anchor2: Component | _AnchorProxy, distance: float, angle: float, name: str | None = None)

Bases: ConnectedComponent

A sensor component for tracking positions on a link.

PointTracker computes its position at a fixed distance and angle from anchor1, with the angle measured relative to the line from anchor1 to anchor2. This is functionally identical to FixedDyad but is semantically a “sensor” that observes without contributing constraints to optimization.

Use PointTracker when you want to: - Track a coupler point on a link - Observe a position on a mechanism for analysis - Add tracer points without affecting optimization

Use FixedDyad when: - The distance/angle are parameters to optimize - The point is part of the mechanism structure

Variables:
  • anchor1 (Component | _AnchorProxy) – First anchor (origin for polar coordinates).

  • anchor2 (Component | _AnchorProxy) – Second anchor (defines reference direction).

  • distance (float) – Distance from anchor1 to this tracker.

  • angle (float) – Angle offset from anchor1->anchor2 direction (radians).

Example

>>> O1 = Ground(0.0, 0.0, name="O1")
>>> crank = Crank(anchor=O1, radius=1.0)
>>> O2 = Ground(2.0, 0.0, name="O2")
>>> # Track a point at 45 degrees from crank->O2 line
>>> tracker = PointTracker(
...     anchor1=crank.output,
...     anchor2=O2,
...     distance=0.5,
...     angle=math.pi/4,
...     name="tracer_point"
... )
anchor1: Component | _AnchorProxy
anchor2: Component | _AnchorProxy
property anchors: tuple[Component, Component]

Return the two parent anchors.

angle: float
distance: float
get_constraints() tuple[()]

Return empty tuple - PointTracker has no optimizable constraints.

PointTracker is a sensor/observer; its distance and angle are fixed and should not be included in optimization bounds.

Returns:

Empty tuple.

reload(dt: float = 1) None

Recompute position using polar projection.

The position is always deterministic - no ambiguity.

Parameters:

dt – Time step (unused for PointTracker, required for interface).

set_constraints(*args: float | None) None

No-op - PointTracker has no optimizable constraints.

Parameters:

*args – Ignored.

pylinkage.PortId

alias of str

class pylinkage.RRPDyad(revolute_anchor: Component | _AnchorProxy, line_anchor1: Component | _AnchorProxy, line_anchor2: Component | _AnchorProxy, distance: float, x: float | None = None, y: float | None = None, name: str | None = None)

Bases: ConnectedComponent

RRP Dyad - circle-line intersection (slider mechanism).

Positions a joint at the intersection of: - A circle centered at the revolute anchor - A line defined by two line anchor points

The joint slides along the line while maintaining a fixed distance from the revolute anchor.

When two solutions exist, the nearest to current position is chosen (hysteresis for continuity during simulation).

Variables:
  • revolute_anchor (Component | _AnchorProxy) – Joint connected by revolute pair.

  • line_anchor1 (Component | _AnchorProxy) – First joint defining the sliding line.

  • line_anchor2 (Component | _AnchorProxy) – Second joint defining the sliding line.

  • distance (float) – Distance from revolute_anchor to this joint.

Example

>>> O1 = Ground(0.0, 0.0, name="O1")
>>> L1 = Ground(0.0, 1.0, name="L1")
>>> L2 = Ground(2.0, 1.0, name="L2")
>>> crank = Crank(anchor=O1, radius=1.0)
>>> slider = RRPDyad(
...     revolute_anchor=crank.output,
...     line_anchor1=L1,
...     line_anchor2=L2,
...     distance=1.5,
...     name="slider"
... )
property anchors: tuple[Component, Component, Component]

Return the parent dyads (revolute anchor, line anchors).

distance: float
get_constraints() tuple[float]

Return the distance constraint.

Returns:

Tuple containing the distance to revolute anchor.

line_anchor1: Component | _AnchorProxy
line_anchor2: Component | _AnchorProxy
reload(dt: float = 1) None

Recompute position using circle-line intersection.

Parameters:

dt – Time step (unused for RRP, but required for interface).

Raises:

UnbuildableError – If circle doesn’t intersect line.

revolute_anchor: Component | _AnchorProxy
set_constraints(distance: float | None = None, *args: float | None) None

Set the distance constraint.

Parameters:
  • distance – New distance to revolute anchor.

  • *args – Ignored (for interface compatibility).

class pylinkage.RRRDyad(anchor1: Component | _AnchorProxy, anchor2: Component | _AnchorProxy, distance1: float, distance2: float, x: float | None = None, y: float | None = None, name: str | None = None)

Bases: BinaryDyad

RRR Dyad - circle-circle intersection.

Positions a joint at the intersection of two circles centered at the anchor points. This is the most common Assur group.

When two solutions exist, the nearest to current position is chosen (hysteresis for continuity during simulation).

Variables:
  • anchor1 – First connection point.

  • anchor2 – Second connection point.

  • distance1 (float) – Distance from anchor1 to this joint.

  • distance2 (float) – Distance from anchor2 to this joint.

Example

>>> O1 = Ground(0.0, 0.0, name="O1")
>>> O2 = Ground(2.0, 0.0, name="O2")
>>> crank = Crank(anchor=O1, radius=1.0)
>>> rocker = RRRDyad(
...     anchor1=crank.output,
...     anchor2=O2,
...     distance1=2.0,
...     distance2=1.5,
...     name="rocker"
... )
distance1: float
distance2: float
get_constraints() tuple[float, float]

Return the two distance constraints.

Returns:

Tuple of (distance1, distance2).

reload(dt: float = 1) None

Recompute position using circle-circle intersection.

Parameters:

dt – Time step (unused for RRR, but required for interface).

Raises:

UnbuildableError – If the circles don’t intersect.

set_constraints(distance1: float | None = None, distance2: float | None = None, *args: float | None) None

Set the distance constraints.

Parameters:
  • distance1 – New distance to anchor1.

  • distance2 – New distance to anchor2.

  • *args – Ignored (for interface compatibility).

class pylinkage.Simulation(linkage: Any, iterations: int | None = None, dt: float = 1.0)

Bases: object

Context-managed wrapper around a linkage’s step() generator.

Works with any container that exposes get_coords(), set_coords(positions), and step(iterations=..., dt=...).

Example

>>> with linkage.simulation(iterations=100) as sim:
...     for step, coords in sim:
...         print(step, coords)
property iterations: int

Number of iterations for this simulation.

property linkage: Any

The linkage being simulated.

exception pylinkage.UnbuildableError(joint: Any, message: str = 'Unable to solve constraints')

Bases: Exception

Should be raised when the constraints cannot be solved.

exception pylinkage.UnderconstrainedError(linkage: Linkage | str, message: str = 'The linkage is under-constrained!')

Bases: Exception

The linkage is under-constrained and multiple solutions may exist.

pylinkage.bounding_box(locus: Iterable[tuple[float, float]]) tuple[float, float, float, float]

Compute the bounding box of a locus.

Parameters:

locus – A list of points or any iterable with the same structure.

Returns:

Bounding box as (y_min, x_max, y_max, x_min).

pylinkage.circle_intersect(x1: float, y1: float, r1: float, x2: float, y2: float, r2: float, tol: float = 0.0) tuple[int, float, float, float, float]

Get the intersections of two circles.

Transcription of a Matt Woodhead program, method provided by Paul Bourke, 1997. http://paulbourke.net/geometry/circlesphere/.

Parameters:
  • x1 – X coordinate of first circle center.

  • y1 – Y coordinate of first circle center.

  • r1 – Radius of first circle.

  • x2 – X coordinate of second circle center.

  • y2 – Y coordinate of second circle center.

  • r2 – Radius of second circle.

  • tol – Distance under which two points are considered equal.

Returns:

  • n=0: No intersection (other values undefined)

  • n=1: One intersection at (x1, y1)

  • n=2: Two intersections at (x1, y1) and (x2, y2)

  • n=3: Same circle (x1, y1, x2 are center and radius)

Return type:

Tuple of (n_intersections, x1, y1, x2, y2) where

pylinkage.cyl_to_cart(radius: float, theta: float, ori_x: float = 0.0, ori_y: float = 0.0) tuple[float, float]

Convert polar coordinates into cartesian.

Parameters:
  • radius – Distance from origin.

  • theta – Angle starting from abscissa axis.

  • ori_x – Origin X coordinate (Default value = 0.0).

  • ori_y – Origin Y coordinate (Default value = 0.0).

Returns:

Cartesian coordinates (x, y).

pylinkage.extract_trajectories(loci: Sequence[Sequence[tuple[float, float] | tuple[Any, Any]]], linkage: Any | None = None) dict[Any, tuple[ndarray, ndarray]]

Extract the (x, y) path of every joint from simulation loci.

Frames where a given joint’s position is None (unbuildable configuration) are skipped per joint — each joint’s arrays only contain frames where that joint was successfully solved.

Parameters:
  • loci – Sequence of frames as produced by Linkage.step() or Mechanism.step(). Each frame is a sequence of (x, y) tuples, one per joint/component in the linkage’s iteration order.

  • linkage – Optional Linkage or Mechanism. If provided, the returned dict is keyed by joint name; otherwise it is keyed by integer index.

Returns:

Mapping {joint_name_or_index: (xs, ys)}. Each (xs, ys) pair is a tuple of numpy.ndarray with matching length. Empty arrays indicate a joint was never buildable.

pylinkage.extract_trajectory(loci: Sequence[Sequence[tuple[float, float] | tuple[Any, Any]]], joint: int | str | Any = -1, linkage: Any | None = None) tuple[ndarray, ndarray]

Extract the (x, y) path of a single joint from simulation loci.

Frames where the joint position is None (unbuildable configuration) are silently skipped.

Parameters:
  • loci – Sequence of frames as produced by Linkage.step() or Mechanism.step(). Each frame is a sequence of (x, y) tuples, one per joint/component in the linkage’s iteration order.

  • joint – Which joint’s trajectory to extract. Can be:

  • frame (- an integer index into each)

  • name (- a joint/component)

  • instance (- a joint/component)

  • linkage – The Linkage or Mechanism the loci come from. Required when joint is a name or instance.

Returns:

Pair (xs, ys) of numpy.ndarray with the same length. Empty arrays if every frame is unbuildable.

pylinkage.intersection(obj_1: tuple[float, float] | tuple[float, float, float], obj_2: tuple[float, float] | tuple[float, float, float], tol: float = 0.0) tuple[float, float] | tuple[tuple[float, float], ...] | tuple[float, float, float] | None

Intersection of two arbitrary objects.

The input objects should be points or circles.

Parameters:
  • obj_1 – First point or circle (as tuple).

  • obj_2 – Second point or circle (as tuple).

  • tol – Absolute tolerance to use if provided.

Returns:

The intersection found, if any.

pylinkage.kinematic_default_test(func: Callable[[...], float], error_penalty: float) Callable[[Linkage, Iterable[float], JointPositions | None], float]

Standard run for any linkage before a complete fitness evaluation.

This decorator makes a kinematic simulation, before passing the loci to the decorated function.

Parameters:
  • func – Fitness function to be decorated.

  • error_penalty – Penalty value for unbuildable linkage. Common values include float(‘inf’) and 0.

pylinkage.norm(x: float, y: float) float

Return the norm of a 2-dimensional vector.

Parameters:
  • x – X component.

  • y – Y component.

Returns:

Vector magnitude.

pylinkage.sqr_dist(x1: float, y1: float, x2: float, y2: float) float

Square of the distance between two points.

Faster than dist when only comparing distances.

Parameters:
  • x1 – X coordinate of first point.

  • y1 – Y coordinate of first point.

  • x2 – X coordinate of second point.

  • y2 – Y coordinate of second point.

Returns:

Squared distance.