pylinkage.mechanism package

Submodules

pylinkage.mechanism.builder module

MechanismBuilder - Links-first approach to mechanism definition.

This module provides a builder pattern for creating Mechanism objects where users define links with their intrinsic properties (lengths, port geometry) first, then connect them with joints. Joint positions are computed automatically during assembly.

Example

>>> from pylinkage.mechanism import MechanismBuilder
>>> mechanism = (
...     MechanismBuilder("four-bar")
...     .add_ground_link("ground", ports={"O1": (0, 0), "O2": (3, 0)})
...     .add_driver_link("crank", length=1.0, motor_port="O1", omega=0.1)
...     .add_link("coupler", length=2.5)
...     .add_link("rocker", length=1.5)
...     .connect("crank.tip", "coupler.0")
...     .connect("coupler.1", "rocker.0")
...     .connect("rocker.1", "ground.O2")
...     .build()
... )
class pylinkage.mechanism.builder.Connection(port1: str, port2: str, joint_type: str = 'revolute')

Bases: object

A connection between two ports on different links.

Variables:
  • port1 (str) – Full port identifier “link_id.port_id”.

  • port2 (str) – Full port identifier “link_id.port_id”.

  • joint_type (str) – Type of joint (“revolute” or “prismatic”).

joint_type: str = 'revolute'
port1: str
port2: str
class pylinkage.mechanism.builder.MechanismBuilder(name: str = '', _ground_link_id: str | None = None, _ground_ports: dict[str, tuple[float, float]]=<factory>, _pending_links: dict[str, ~pylinkage.mechanism.builder.PendingLink]=<factory>, _connections: list[Connection] = <factory>, _prismatic_connections: list[PrismaticConnection] = <factory>, _slide_axes: dict[str, ~pylinkage.mechanism.builder.SlideAxis]=<factory>, _configuration: dict[str, int]=<factory>, _pending_trackers: list[PendingTracker] = <factory>)

Bases: object

Builder for creating Mechanism objects using a links-first approach.

This builder allows defining mechanisms by specifying link properties (lengths, port geometry) rather than joint positions. Joint positions are computed automatically during the build() step.

Example

>>> builder = MechanismBuilder("four-bar")
>>> builder.add_ground_link("ground", ports={"O1": (0, 0), "O2": (3, 0)})
>>> builder.add_driver_link("crank", length=1.0, motor_port="O1")
>>> builder.add_link("coupler", length=2.5)
>>> builder.add_link("rocker", length=1.5)
>>> builder.connect("crank.tip", "coupler.0")
>>> builder.connect("coupler.1", "rocker.0")
>>> builder.connect("rocker.1", "ground.O2")
>>> mechanism = builder.build()

Add an oscillating motor-driven link (arc crank).

An arc driver link oscillates around a ground port between angle limits (arc_start and arc_end), reversing direction at boundaries. Unlike add_driver_link which creates a continuously rotating crank, this creates a bounded-rotation driver.

Parameters:
  • id – Unique identifier for the link.

  • length – Distance from motor to output (crank radius).

  • motor_port – Name of the ground port where motor attaches.

  • omega – Angular velocity magnitude in radians per step.

  • arc_start – Minimum angle limit in radians.

  • arc_end – Maximum angle limit in radians.

  • initial_angle – Starting angle (defaults to arc_start).

Returns:

Self for method chaining.

Example

>>> builder.add_arc_driver_link("crank", length=1.0, motor_port="O1",
...                             arc_start=0.5, arc_end=2.5)

Add a motor-driven link (crank).

A driver link rotates around a ground port at a specified angular velocity. It has two ports: the motor port (at the ground) and the output port (“tip”) at distance length from the motor.

Parameters:
  • id – Unique identifier for the link.

  • length – Distance from motor to output (crank radius).

  • motor_port – Name of the ground port where motor attaches.

  • omega – Angular velocity in radians per step.

  • initial_angle – Starting angle in radians (from positive x-axis).

Returns:

Self for method chaining.

Example

>>> builder.add_driver_link("crank", length=1.0, motor_port="O1", omega=0.1)

Add the ground (frame) link with fixed port positions.

The ground link represents the stationary frame of the mechanism. Each port on the ground link has a fixed position in the global coordinate system.

Parameters:
  • id – Unique identifier for the ground link.

  • ports – Dictionary mapping port names to (x, y) positions.

Returns:

Self for method chaining.

Example

>>> builder.add_ground_link("ground", ports={"O1": (0, 0), "O2": (3, 0)})

Add a binary link with given length.

A binary link has two ports (connection points) named “0” and “1”, separated by the specified length.

Parameters:
  • id – Unique identifier for the link.

  • length – Distance between the two ports.

Returns:

Self for method chaining.

Example

>>> builder.add_link("coupler", length=2.5)
add_point_tracker(id: str, ref_port1: str, ref_port2: str, distance: float | None = None, angle: float = 0.0) Self

Add a point tracker (observer) on a link.

A point tracker observes a position at a fixed distance and angle from ref_port1, with the angle measured relative to the line from ref_port1 to ref_port2. This is useful for tracking coupler points.

For tracking the midpoint of a link, set distance to half the link length and angle to 0.

Parameters:
  • id – Unique identifier for the tracker.

  • ref_port1 – Reference port for origin (e.g., “coupler.0”).

  • ref_port2 – Reference port for direction (e.g., “coupler.1”).

  • distance – Distance from ref_port1. If None, uses half the link length.

  • angle – Angle offset from ref_port1->ref_port2 direction (radians).

Returns:

Self for method chaining.

Example

>>> # Track the midpoint of the coupler link
>>> builder.add_point_tracker("coupler_mid", "coupler.0", "coupler.1")

Add a quaternary (4-port) link.

A quaternary link has four ports. The geometry is specified as local coordinates for each port.

Parameters:
  • id – Unique identifier for the link.

  • port_geometry – Dictionary mapping port names to (x, y) positions in the link’s local coordinate frame.

Returns:

Self for method chaining.

Raises:

ValueError – If port_geometry doesn’t have exactly 4 ports.

add_slide_axis(id: str, through: tuple[float, float], direction: tuple[float, float]) Self

Define a slide axis for prismatic joints.

A slide axis is a line along which a prismatic joint can translate.

Parameters:
  • id – Unique identifier for the axis.

  • through – A point (x, y) that the line passes through.

  • direction – Direction vector (dx, dy) of the line.

Returns:

Self for method chaining.

Example

>>> builder.add_slide_axis("rail", through=(0, 0), direction=(1, 0))

Add a ternary (3-port) link with triangle geometry.

A ternary link has three ports arranged in a triangle. The geometry is specified as local coordinates for each port, from which all pairwise distances are derived.

Parameters:
  • id – Unique identifier for the link.

  • port_geometry – Dictionary mapping port names to (x, y) positions in the link’s local coordinate frame.

Returns:

Self for method chaining.

Raises:

ValueError – If port_geometry doesn’t have exactly 3 ports.

Example

>>> builder.add_ternary_link(
...     "coupler",
...     port_geometry={"A": (0, 0), "B": (3, 0), "P": (1.5, 1)}
... )
build() Mechanism

Assemble and return the Mechanism.

Validates the link definitions, computes all joint positions from constraints, and creates a Mechanism object.

Returns:

Assembled Mechanism ready for simulation.

Raises:
connect(port1: str, port2: str) Self

Connect two ports with a revolute joint.

Creates a pin joint between two ports on different links. The port identifiers use the format “link_id.port_id”.

Parameters:
  • port1 – First port identifier (e.g., “crank.tip”).

  • port2 – Second port identifier (e.g., “coupler.0”).

Returns:

Self for method chaining.

Example

>>> builder.connect("crank.tip", "coupler.0")
connect_prismatic(port: str, axis: str) Self

Connect a port to a slide axis with a prismatic joint.

Creates a slider joint that constrains the port to move along the specified axis.

Parameters:
  • port – Port identifier (e.g., “rod.1”).

  • axis – Slide axis identifier.

Returns:

Self for method chaining.

Example

>>> builder.connect_prismatic("rod.1", "rail")
name: str = ''
set_branch(joint: str, branch: int) Self

Set the assembly branch for a joint with two solutions.

When computing joint positions via circle-circle intersection, there are typically two solutions. This method allows selecting which solution to use.

Parameters:
  • joint – Joint identifier (typically “link_id.port_id”).

  • branch – 0 for first solution, 1 for second solution.

Returns:

Self for method chaining.

Example

>>> builder.set_branch("coupler.1", 1)  # Use "lower" configuration

Bases: object

A link awaiting assembly.

Stores link definition before joint positions are computed.

Variables:
  • id (str) – Unique identifier for the link.

  • ports (dict[str, pylinkage.mechanism.builder.Port]) – Dictionary mapping port ID to Port object.

  • length (float | None) – For binary links, the distance between ports.

  • port_geometry (dict[str, tuple[float, float]] | None) – For ternary+ links, local coordinates of each port.

  • is_driver (bool) – True if this link is motor-driven.

  • is_arc_driver (bool) – True if this is an arc (oscillating) driver.

  • motor_port (str | None) – For driver links, the ground port where motor attaches.

  • angular_velocity (float) – For driver links, rotation rate in rad/step.

  • initial_angle (float) – For driver links, starting angle in radians.

  • arc_start (float) – For arc drivers, minimum angle limit.

  • arc_end (float) – For arc drivers, maximum angle limit.

angular_velocity: float = 0.0
arc_end: float = 3.141592653589793
arc_start: float = 0.0
get_port_distance(port1_id: str, port2_id: str) float | None

Get distance between two ports on this link.

For binary links, returns the length. For ternary+ links, computes from port_geometry.

id: str
initial_angle: float = 0.0
is_arc_driver: bool = False
is_driver: bool = False
length: float | None = None
motor_port: str | None = None
port_geometry: dict[str, tuple[float, float]] | None = None
ports: dict[str, Port]
class pylinkage.mechanism.builder.PendingTracker(id: str, ref_port1: str, ref_port2: str, distance: float, angle: float = 0.0)

Bases: object

A point tracker awaiting assembly.

Stores tracker definition before reference joint positions are computed.

Variables:
  • id (str) – Unique identifier for the tracker.

  • ref_port1 (str) – Full port identifier for first reference joint.

  • ref_port2 (str) – Full port identifier for second reference joint.

  • distance (float) – Distance from ref_port1 to tracker.

  • angle (float) – Angle offset from ref_port1->ref_port2 direction (radians).

angle: float = 0.0
distance: float
id: str
ref_port1: str
ref_port2: str
class pylinkage.mechanism.builder.Port(id: str, local_position: tuple[float, float] | None = None)

Bases: object

A connection point on a link.

Ports define where joints can be placed on a link. For binary links: 2 ports (endpoints, named “0” and “1” or custom) For ternary links: 3 ports (triangle vertices)

Variables:
  • id (str) – Unique identifier within the link.

  • local_position (tuple[float, float] | None) – Position relative to link’s local frame. For binary links this is None (determined by length). For ternary+ links this is (x, y) in local coordinates.

id: str
local_position: tuple[float, float] | None = None
class pylinkage.mechanism.builder.PrismaticConnection(port: str, axis_id: str)

Bases: object

A connection of a port to a slide axis.

Variables:
  • port (str) – Full port identifier “link_id.port_id”.

  • axis_id (str) – ID of the SlideAxis.

axis_id: str
port: str
class pylinkage.mechanism.builder.SlideAxis(id: str, point: tuple[float, float], direction: tuple[float, float])

Bases: object

Definition of a prismatic joint axis.

Variables:
  • id (str) – Unique identifier for the axis.

  • point (tuple[float, float]) – A point on the slide line.

  • direction (tuple[float, float]) – Direction vector (will be normalized).

direction: tuple[float, float]
get_line_points() tuple[tuple[float, float], tuple[float, float]]

Return two points on the line for intersection computation.

get_normalized_direction() tuple[float, float]

Return normalized direction vector.

id: str
point: tuple[float, float]

pylinkage.mechanism.factories module

Factory functions for common planar mechanisms.

These helpers build standard parametric mechanisms with a single call, so users don’t need to repeat the MechanismBuilder() .add_ground_link() .add_driver_link() .connect() ... boilerplate (or hand-roll a circle-circle intersection) every time they want a four-bar or slider-crank.

pylinkage.mechanism.factories.fourbar(crank: float, coupler: float, rocker: float, ground: float, omega: float = 0.06283185307179587, initial_angle: float = 0.0, branch: int = 1, name: str = 'fourbar') Mechanism

Build a four-bar Mechanism from link lengths.

Ground pivots are placed at A = (0, 0) and D = (ground, 0). The crank rotates about A and the rocker oscillates about D.

Parameters:
  • crank – Crank length (link a, A-B).

  • coupler – Coupler length (link b, B-C).

  • rocker – Rocker length (link c, C-D).

  • ground – Ground link length (link d, A-D).

  • omega – Driver angular velocity, in rad/step.

  • initial_angle – Starting crank angle, in radians.

  • branch0 or 1 — which of the two circle-circle intersections to pick for the coupler-rocker joint. Branch 1 (default) is the upper (positive y) configuration, matching synthesis.fourbar_from_lengths.

  • name – Name of the resulting mechanism.

Returns:

An assembled Mechanism.

Raises:

pylinkage.exceptions.UnbuildableError – if the link lengths cannot form a closed loop at initial_angle.

Example

>>> from pylinkage.mechanism import fourbar
>>> mech = fourbar(crank=1.0, coupler=3.0, rocker=3.0, ground=4.0)
>>> loci = list(mech.step())
pylinkage.mechanism.factories.slider_crank(crank: float, rod: float, omega: float = 0.06283185307179587, initial_angle: float = 0.0, slide_through: tuple[float, float] = (0.0, 0.0), slide_direction: tuple[float, float] = (1.0, 0.0), name: str = 'slider-crank') Mechanism

Build a slider-crank Mechanism.

A crank of length crank rotates about the origin and drives a rod of length rod whose far end slides along the line through slide_through in direction slide_direction.

Parameters:
  • crank – Crank length.

  • rod – Connecting rod length.

  • omega – Driver angular velocity, in rad/step.

  • initial_angle – Starting crank angle, in radians.

  • slide_through – A point the slide axis passes through.

  • slide_direction – Direction vector of the slide axis.

  • name – Name of the resulting mechanism.

Returns:

An assembled Mechanism.

Example

>>> from pylinkage.mechanism import slider_crank
>>> mech = slider_crank(crank=1.0, rod=3.0)
>>> loci = list(mech.step())

pylinkage.mechanism.joint module

Joint classes for the mechanism module.

This module defines the fundamental joint types used in planar mechanisms: - RevoluteJoint: Pin joint allowing rotation (1 DOF) - PrismaticJoint: Slider joint allowing translation (1 DOF) - GroundJoint: Revolute joint fixed to the frame

These are actual mechanical joints, unlike the ‘joints’ module which defines Assur groups (combinations of joints and links).

class pylinkage.mechanism.joint.GroundJoint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None, is_ground: bool = True)

Bases: RevoluteJoint

Revolute joint fixed to the frame (ground link).

A ground joint is a revolute joint whose position is fixed in the global coordinate frame. It connects a moving link to the stationary frame.

Ground joints are typically: - The base of a crank (motor attachment point) - The pivot point of a rocker - Any fixed pivot in the mechanism

Example

>>> ground = GroundJoint("O", position=(0.0, 0.0))
>>> ground.is_ground
True
is_ground: bool = True
property joint_type: JointType

Return GROUND type.

class pylinkage.mechanism.joint.Joint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None)

Bases: ABC

Base class for mechanical joints.

A joint is a connection point between rigid links that allows relative motion. Each joint type permits specific degrees of freedom.

Variables:
  • id (str) – Unique identifier for this joint.

  • position (MaybeCoord) – Current (x, y) coordinates in the global frame.

  • name (str | None) – Human-readable name for display (defaults to id).

Note

The links attribute is populated when the joint is added to a Mechanism, establishing the connectivity graph.

acceleration: Coord | None = None
coord() tuple[float | None, float | None]

Return the current coordinates.

id: str
is_defined() bool

Return True if position is fully defined (no None values).

abstract property joint_type: JointType

Return the type of this joint.

Return the links connected at this joint.

name: str | None = None
position: MaybeCoord = (None, None)
set_coord(x: float | None, y: float | None) None

Set the coordinates.

velocity: Coord | None = None
property x: float | None

Return the x coordinate.

property y: float | None

Return the y coordinate.

class pylinkage.mechanism.joint.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.mechanism.joint.PrismaticJoint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None, axis: Coord = (1.0, 0.0), line_point: Coord = (0.0, 0.0), slide_distance: float = 0.0)

Bases: Joint

Slider joint allowing translation along an axis.

A prismatic joint permits one degree of freedom: translation along the joint axis.

Variables:
  • axis (Coord) – Direction of allowed translation as (dx, dy). Should be normalized for consistency.

  • line_point (Coord) – A fixed point (x, y) on the slide line. The joint is constrained to move along the line passing through this point in the axis direction.

  • slide_distance (float) – Current displacement along the axis from origin.

Example

>>> joint = PrismaticJoint("S", position=(0.0, 0.0), axis=(1.0, 0.0))
>>> joint.joint_type
<JointType.PRISMATIC: 2>
axis: Coord = (1.0, 0.0)
get_axis_normalized() tuple[float, float]

Return the normalized axis direction.

property joint_type: JointType

Return PRISMATIC type.

line_point: Coord = (0.0, 0.0)
slide_distance: float = 0.0
class pylinkage.mechanism.joint.RevoluteJoint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None)

Bases: Joint

Pin joint allowing rotation between two links.

A revolute joint permits one degree of freedom: rotation about the joint axis (perpendicular to the plane for planar mechanisms).

This is the most common joint type in planar linkages.

Example

>>> joint = RevoluteJoint("A", position=(1.0, 2.0))
>>> joint.joint_type
<JointType.REVOLUTE: 1>
property joint_type: JointType

Return REVOLUTE type.

class pylinkage.mechanism.joint.TrackerJoint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None, ref_joint1_id: str = '', ref_joint2_id: str = '', distance: float = 0.0, angle: float = 0.0)

Bases: Joint

Observer joint that tracks a position relative to two reference joints.

A tracker joint is a sensor/observer that computes its position as a point at a fixed distance and angle from a reference joint, with the angle measured relative to the line connecting the two reference joints.

This is useful for: - Tracking coupler points on a link (e.g., Chebyshev straight-line mechanism) - Observing positions without affecting the kinematic chain - Adding tracer points for visualization

Variables:
  • ref_joint1_id (str) – ID of the first reference joint (origin for polar coords).

  • ref_joint2_id (str) – ID of the second reference joint (defines reference direction).

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

  • angle (float) – Angle offset from ref_joint1->ref_joint2 direction (radians).

Example

>>> # Track the midpoint of a link between joints A and B
>>> tracker = TrackerJoint("midpoint", ref_joint1_id="A", ref_joint2_id="B",
...                        distance=0.5, angle=0.0)
angle: float = 0.0
distance: float = 0.0
property joint_type: JointType

Return TRACKER type.

ref_joint1_id: str = ''
ref_joint2_id: str = ''
update_position(ref1_pos: tuple[float, float], ref2_pos: tuple[float, float]) None

Compute position from reference joint positions.

Parameters:
  • ref1_pos – Position of first reference joint (x, y).

  • ref2_pos – Position of second reference joint (x, y).

pylinkage.mechanism.mechanism module

Mechanism class - the main orchestrator for planar linkages.

This module provides the Mechanism class, which manages the collection of links and joints that form a planar mechanism. It handles: - Solving order computation - Constraint management - Simulation stepping - Position computation

The Mechanism class uses proper mechanical engineering terminology: - Joints are actual connection points (revolute, prismatic) - Links are rigid bodies connecting joints - Dyads are solved as constraint satisfaction problems

class pylinkage.mechanism.mechanism.Mechanism(name: str = '', joints: list[Joint] = <factory>, links: list[Link] = <factory>, ground: GroundLink | None = None, _solve_order: list[Joint] = <factory>, _driver_links: list[DriverLink | ArcDriverLink] = <factory>, _joint_map: dict[str, Joint]=<factory>, _link_map: dict[str, Link]=<factory>, _decomposition: DecompositionResult | None = None, _assur_graph: LinkageGraph | None = None, _assur_dimensions: Dimensions | None = None, _use_group_solver: bool = False, _solver_data: SolverData | None = None)

Bases: object

A planar linkage mechanism.

A mechanism is a collection of rigid links connected by joints that transmits and transforms motion. This class manages the topology and provides simulation capabilities.

Variables:
  • name (str) – Human-readable name for the mechanism.

  • joints (list[Joint]) – All joints in the mechanism.

  • links (list[Link]) – All links in the mechanism.

  • ground (GroundLink | None) – The ground (frame) link.

Example

>>> from pylinkage.mechanism import Mechanism, GroundJoint, create_crank, create_rrr_dyad
>>> # Create a four-bar linkage
>>> O1 = GroundJoint("O1", position=(0.0, 0.0))
>>> O2 = GroundJoint("O2", position=(2.0, 0.0))
>>> ground = GroundLink("ground", joints=[O1, O2])
>>> crank, A = create_crank(O1, radius=1.0, angular_velocity=0.1)
>>> link1, link2, B = create_rrr_dyad(A, O2, distance1=2.0, distance2=1.5)
>>> mechanism = Mechanism("Four-Bar", joints=[O1, O2, A, B],
...                       links=[ground, crank, link1, link2])
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.

get_accelerations() list[Coord | None]

Return per-joint linear accelerations, in joint order.

get_constraints() list[float]

Get all distance constraints as a flat list.

Used for optimization. Returns link lengths in a consistent order.

get_coords() list[Coord]

Alias of get_joint_positions() for cross-API compatibility.

get_joint(joint_id: str) Joint | None

Get a joint by ID.

get_joint_positions() list[Coord]

Get current positions of all joints.

Get a link by ID.

get_rotation_period() int

Get the number of steps for one full cycle.

For continuous rotation drivers: steps for 2*pi rotation. For arc drivers: steps for a full back-and-forth oscillation. Based on the slowest driver link’s angular velocity.

get_velocities() list[Coord | None]

Return per-joint linear velocities, in joint order.

Each entry is (vx, vy) or None if the joint’s velocity has not been computed (i.e. before step_with_derivatives() has been run).

ground: GroundLink | None = None
indeterminacy() int

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

Returns 3·(n 1) 2·R P where n is the total number of links (including the ground frame), R counts revolute pairs (RevoluteJoint and GroundJoint) and P counts prismatic pairs.

Positive ⇒ unconstrained DOF (a 1-DOF four-bar returns 1); zero ⇒ statically determinate; negative ⇒ over-constrained.

joints: list[Joint]
name: str = ''
rebuild(initial_positions: list[Coord] | None = None) None

Reset joint positions to an initial configuration.

Convenience wrapper that calls set_joint_positions() when a position list is supplied, and invalidates any cached SolverData so the next step_fast() recompiles.

Parameters:

initial_positions – Optional (x, y) positions per joint (order matches self.joints). When None the joint positions are left untouched.

reset() None

Reset all driver links to initial state.

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

Apply both constraints and joint positions in one call.

set_constraints(values: list[float]) None

Set distance constraints from a flat list.

Used for optimization. Applies constraints in the same order as get_constraints(): the radius of each driver and the length of each binary link. The values become the rigid dimensions the solver maintains, so the next step() re-solves every joint against them (a driver’s output joint is moved right away, keeping its current angle). Invalidates any cached SolverData so the next step_fast() recompiles.

Parameters:

values – List of constraint values to apply.

set_coords(positions: list[Coord]) None

Alias of set_joint_positions() for cross-API compatibility.

set_input_velocity(driver: DriverLink | ArcDriverLink, omega: float, alpha: float = 0.0) None

Set the angular velocity (and optional acceleration) of a driver.

These values are used by step_with_derivatives() to compute joint linear velocities and accelerations. They are independent of DriverLink.angular_velocity (which is in radians per simulation step) — omega is interpreted in physical units, typically rad/s.

Parameters:
  • driver – Driver link to set the input on.

  • omega – Angular velocity (rad/s).

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

Raises:

ValueError – If driver is not part of this mechanism.

set_joint_positions(positions: list[Coord]) None

Set positions of all joints.

simulation(iterations: int | None = None, dt: float = 1.0) _SimulationContext

Return a context manager that simulates this mechanism.

The context restores the initial joint positions on exit. See pylinkage._simulation_context.Simulation.

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

Simulate the mechanism.

Yields joint positions at each step of the simulation.

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

  • dt – Time step multiplier (default 1.0).

Yields:

Tuple of (x, y) coordinates for all joints.

step_fast(iterations: int | None = None, dt: float = 1.0) NDArray[np.float64]

Run the simulation through the numba-compiled solver.

Significantly faster than step() for large iteration counts.

Parameters:
Returns:

numpy.ndarray of shape (iterations, n_joints, 2). Unbuildable configurations appear as NaN.

step_fast_with_kinematics(iterations: int | None = None, dt: float = 1.0) tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]

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

Per-driver omega/alpha inputs must be set via set_input_velocity() (drivers 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_joints, 2).

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

Simulate the mechanism while computing velocities and accelerations.

On each step yields (positions, velocities, accelerations). The omega (and optionally alpha) of every driver link used as input must have been set via set_input_velocity(); otherwise the driver is treated as having zero input velocity.

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

  • dt – Time step multiplier (default 1.0).

Yields:

Three tuples of length len(self.joints) containing the joint positions, velocities, and accelerations for the step.

stroke_position() float

Slide position of a prismatic joint at the current pose.

See pylinkage.linkage.stroke_at_position().

transmission_angle() float

Transmission angle at the current pose, in degrees.

See pylinkage.linkage.transmission_angle_at_position().

pylinkage.mechanism.serialization module

Serialization for the mechanism module.

This module provides functions to serialize and deserialize Mechanism objects to/from JSON-compatible dictionaries.

The serialization format separates joints and links clearly: - Joints have types, positions, and properties - Links reference joints by ID and include constraints

pylinkage.mechanism.serialization.is_legacy_format(data: dict[str, Any]) bool

Check if data is in the legacy (joints/) format.

The legacy format has a ‘joints’ key where each joint has a ‘type’ like ‘Static’, ‘Crank’, ‘Revolute’, etc.

Parameters:

data – Dictionary to check.

Returns:

True if this appears to be the legacy format.

pylinkage.mechanism.serialization.joint_from_dict(data: dict[str, Any]) Joint

Deserialize a joint from a dictionary.

Parameters:

data – Dictionary representation of a joint.

Returns:

The deserialized Joint object.

pylinkage.mechanism.serialization.joint_to_dict(joint: Joint) dict[str, Any]

Serialize a joint to a dictionary.

Parameters:

joint – The joint to serialize.

Returns:

Dictionary representation of the joint.

Deserialize a link from a dictionary.

Parameters:
  • data – Dictionary representation of a link.

  • joint_map – Map of joint IDs to Joint objects.

Returns:

The deserialized Link object.

Serialize a link to a dictionary.

Parameters:

link – The link to serialize.

Returns:

Dictionary representation of the link.

pylinkage.mechanism.serialization.mechanism_from_dict(data: dict[str, Any]) Mechanism

Deserialize a mechanism from a dictionary.

Parameters:

data – Dictionary representation of a mechanism.

Returns:

The deserialized Mechanism object.

Example

>>> mechanism = mechanism_from_dict(data)
pylinkage.mechanism.serialization.mechanism_from_json(path: str | Path) Mechanism

Load a mechanism from a JSON file.

Parameters:

path – Path to the JSON file.

Returns:

The loaded Mechanism object.

pylinkage.mechanism.serialization.mechanism_to_dict(mechanism: Mechanism) dict[str, Any]

Serialize a mechanism to a dictionary.

Parameters:

mechanism – The mechanism to serialize.

Returns:

Dictionary representation of the mechanism.

Example

>>> data = mechanism_to_dict(mechanism)
>>> json.dumps(data, indent=2)
pylinkage.mechanism.serialization.mechanism_to_json(mechanism: Mechanism, path: str | Path) None

Save a mechanism to a JSON file.

Parameters:
  • mechanism – The mechanism to save.

  • path – Path to the output JSON file.

Module contents

Mechanism module - proper Links + Joints model for planar linkages.

This module provides the low-level API for defining planar mechanisms using standard mechanical engineering terminology:

  • Joints are actual connection points (revolute pins, prismatic sliders)

  • Links are rigid bodies connecting joints

  • Mechanism orchestrates joints and links for simulation

For a higher-level API using Assur group building blocks, see pylinkage.dyads.

Basic Usage:
>>> from pylinkage.mechanism import (
...     Mechanism, GroundJoint, GroundLink,
...     RevoluteJoint, Link, DriverLink,
... )
>>>
>>> # Create ground joints
>>> O1 = GroundJoint("O1", position=(0.0, 0.0))
>>> O2 = GroundJoint("O2", position=(2.0, 0.0))
>>> ground = GroundLink("ground", joints=[O1, O2])
Classes:

Joint: Base class for all joints RevoluteJoint: Pin joint (1 DOF rotation) PrismaticJoint: Slider joint (1 DOF translation) GroundJoint: Fixed revolute joint on the frame

Link: Rigid body connecting joints GroundLink: The stationary frame DriverLink: Motor-driven input link

Mechanism: The main orchestrator class MechanismBuilder: Links-first builder for creating mechanisms

Serialization:

mechanism_to_dict: Serialize to dictionary mechanism_from_dict: Deserialize from dictionary mechanism_to_json: Save to JSON file mechanism_from_json: Load from JSON file

See also

pylinkage.dyads: High-level API using Assur group building blocks

class pylinkage.mechanism.ArcDriverLink(id: str, joints: list[Joint] = <factory>, name: str | None = None, _cached_distances: dict[tuple[str, str], float] = <factory>, motor_joint: GroundJoint | None = None, angular_velocity: float = 0.017453292519943295, arc_start: float = 0.0, arc_end: float = 3.141592653589793, initial_angle: float | None = None, current_angle: float = 0.0, _direction: float = 1.0)

Bases: Link

An input link that oscillates within an arc (bounded rotation).

Unlike DriverLink which rotates continuously, ArcDriverLink oscillates between angle limits (arc_start and arc_end), reversing direction when reaching boundaries. This models mechanisms like rockers that don’t complete full rotations.

Variables:
  • motor_joint (GroundJoint | None) – The ground joint where the motor is attached.

  • 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 | None) – Starting angle in radians (must be between limits).

  • current_angle (float) – Current angle during simulation.

  • _direction (float) – Current rotation direction (+1 or -1).

Example

>>> from pylinkage.mechanism.joint import GroundJoint, RevoluteJoint
>>> O = GroundJoint("O", position=(0.0, 0.0))
>>> A = RevoluteJoint("A", position=(1.0, 0.0))
>>> arc_crank = ArcDriverLink("crank", joints=[O, A], motor_joint=O,
...                           angular_velocity=0.1,
...                           arc_start=0.5, arc_end=2.5)
angular_velocity: float = 0.017453292519943295
arc_end: float = 3.141592653589793
arc_start: float = 0.0
current_angle: float = 0.0
initial_angle: float | None = None
property link_type: LinkType

Return DRIVER type.

motor_joint: GroundJoint | None = None
property output_joint: Joint | None

Return the non-motor joint (output of the crank).

property radius: float | None

Return the crank radius (distance from motor to output joint).

reset() None

Reset the arc crank to its initial angle.

step(dt: float = 1.0) None

Advance the arc crank by one time step.

Updates current_angle and repositions the output joint. Reverses direction when hitting angle limits.

Parameters:

dt – Time step multiplier (default 1.0 = full step).

class pylinkage.mechanism.DriverLink(id: str, joints: list[Joint] = <factory>, name: str | None = None, _cached_distances: dict[tuple[str, str], float] = <factory>, motor_joint: GroundJoint | None = None, angular_velocity: float = 0.017453292519943295, initial_angle: float = 0.0, current_angle: float = 0.0)

Bases: Link

An input link driven by a motor.

A driver link is connected to the ground at one joint (the motor joint) and rotates at a specified angular velocity. It is the source of motion in the mechanism.

Variables:
  • motor_joint (GroundJoint | None) – The ground joint where the motor is attached.

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

  • initial_angle (float) – Starting angle in radians (from positive x-axis).

  • current_angle (float) – Current angle during simulation.

Example

>>> from pylinkage.mechanism.joint import GroundJoint, RevoluteJoint
>>> O = GroundJoint("O", position=(0.0, 0.0))
>>> A = RevoluteJoint("A", position=(1.0, 0.0))
>>> crank = DriverLink("crank", joints=[O, A], motor_joint=O,
...                    angular_velocity=0.1)
angular_velocity: float = 0.017453292519943295
current_angle: float = 0.0
initial_angle: float = 0.0
property link_type: LinkType

Return DRIVER type.

motor_joint: GroundJoint | None = None
property output_joint: Joint | None

Return the non-motor joint (output of the crank).

property radius: float | None

Return the crank radius (distance from motor to output joint).

Only valid for binary driver links. Returns the distance from the motor joint to the other joint.

reset() None

Reset the crank to its initial angle.

step(dt: float = 1.0) None

Advance the crank by one time step.

Updates current_angle and repositions the output joint based on the angular velocity.

Parameters:

dt – Time step multiplier (default 1.0 = full step).

class pylinkage.mechanism.GroundJoint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None, is_ground: bool = True)

Bases: RevoluteJoint

Revolute joint fixed to the frame (ground link).

A ground joint is a revolute joint whose position is fixed in the global coordinate frame. It connects a moving link to the stationary frame.

Ground joints are typically: - The base of a crank (motor attachment point) - The pivot point of a rocker - Any fixed pivot in the mechanism

Example

>>> ground = GroundJoint("O", position=(0.0, 0.0))
>>> ground.is_ground
True
is_ground: bool = True
property joint_type: JointType

Return GROUND type.

class pylinkage.mechanism.GroundLink(id: str, joints: list[Joint] = <factory>, name: str | None = None, _cached_distances: dict[tuple[str, str], float] = <factory>, is_ground: bool = True)

Bases: Link

The stationary frame (ground) of a mechanism.

Every mechanism has exactly one ground link, which represents the fixed reference frame. All ground joints are attached to this link.

The ground link is special because: - Its joints never move during simulation - It establishes the global coordinate system - All motion is measured relative to it

Example

>>> from pylinkage.mechanism.joint import GroundJoint
>>> O1 = GroundJoint("O1", position=(0.0, 0.0))
>>> O2 = GroundJoint("O2", position=(2.0, 0.0))
>>> ground = GroundLink("ground", joints=[O1, O2])
is_ground: bool = True
property link_type: LinkType

Return GROUND type.

class pylinkage.mechanism.Joint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None)

Bases: ABC

Base class for mechanical joints.

A joint is a connection point between rigid links that allows relative motion. Each joint type permits specific degrees of freedom.

Variables:
  • id (str) – Unique identifier for this joint.

  • position (MaybeCoord) – Current (x, y) coordinates in the global frame.

  • name (str | None) – Human-readable name for display (defaults to id).

Note

The links attribute is populated when the joint is added to a Mechanism, establishing the connectivity graph.

acceleration: Coord | None = None
coord() tuple[float | None, float | None]

Return the current coordinates.

id: str
is_defined() bool

Return True if position is fully defined (no None values).

abstract property joint_type: JointType

Return the type of this joint.

property links: list[Link]

Return the links connected at this joint.

name: str | None = None
position: MaybeCoord = (None, None)
set_coord(x: float | None, y: float | None) None

Set the coordinates.

velocity: Coord | None = None
property x: float | None

Return the x coordinate.

property y: float | None

Return the y coordinate.

class pylinkage.mechanism.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.mechanism.Link(id: str, joints: list[Joint] = <factory>, name: str | None = None, _cached_distances: dict[tuple[str, str], float] = <factory>)

Bases: object

A rigid body connecting two or more joints.

A link is a rigid member that maintains fixed distances between all joints attached to it. Most common are binary links (2 joints), but ternary (3 joints) and higher-order links also exist.

Variables:
  • id (str) – Unique identifier for this link.

  • joints (list[Joint]) – List of joints connected by this link.

  • name (str | None) – Human-readable name for display (defaults to id).

Example

>>> from pylinkage.mechanism.joint import RevoluteJoint
>>> j1 = RevoluteJoint("A", position=(0.0, 0.0))
>>> j2 = RevoluteJoint("B", position=(1.0, 0.0))
>>> link = Link("AB", joints=[j1, j2])
>>> link.length
1.0
cache_distances() None

Cache distances between all pairs of joints.

Call this after all joints have their initial positions set to store the fixed link constraints. These cached distances are used during simulation to maintain link rigidity.

get_distance(joint1: Joint, joint2: Joint) float | None

Get the distance constraint between two joints on this link.

Returns the cached distance if available (for use during simulation), otherwise computes from current positions (for initialization).

Parameters:
  • joint1 – First joint (must be in this link).

  • joint2 – Second joint (must be in this link).

Returns:

Distance between joints, or None if undefined.

Raises:

ValueError – If either joint is not part of this link.

id: str
joints: list[Joint]
property length: float | None

Return the length of a binary link.

Only meaningful for binary links (2 joints). Returns the rigid distance the solver maintains between the two joints (see get_distance()): the cached constraint once the mechanism is built, the distance between the joint positions before that.

Returns:

Distance between joints, or None if not a binary link or if positions are undefined.

property link_type: LinkType

Return the type based on number of joints.

name: str | None = None
property order: int

Return the order (number of joints) of this link.

other_joint(joint: Joint) Joint | None

Get the other joint in a binary link.

Parameters:

joint – One of the joints in this binary link.

Returns:

The other joint, or None if not a binary link or if joint is not in this link.

set_distance(joint1: Joint, joint2: Joint, distance: float) None

Set the distance constraint between two joints on this link.

Changes the rigid dimension the solver maintains between the two joints; their positions are not moved, the next simulation step re-solves them against the new value.

Parameters:
  • joint1 – First joint (must be in this link).

  • joint2 – Second joint (must be in this link).

  • distance – New distance, must be positive.

Raises:

ValueError – If either joint is not part of this link, or if distance is not positive.

class pylinkage.mechanism.LinkType(value)

Bases: IntEnum

Enumeration of link types.

BINARY = 2
DRIVER = 1
GROUND = 0
QUATERNARY = 4
TERNARY = 3
class pylinkage.mechanism.Mechanism(name: str = '', joints: list[Joint] = <factory>, links: list[Link] = <factory>, ground: GroundLink | None = None, _solve_order: list[Joint] = <factory>, _driver_links: list[DriverLink | ArcDriverLink] = <factory>, _joint_map: dict[str, Joint]=<factory>, _link_map: dict[str, Link]=<factory>, _decomposition: DecompositionResult | None = None, _assur_graph: LinkageGraph | None = None, _assur_dimensions: Dimensions | None = None, _use_group_solver: bool = False, _solver_data: SolverData | None = None)

Bases: object

A planar linkage mechanism.

A mechanism is a collection of rigid links connected by joints that transmits and transforms motion. This class manages the topology and provides simulation capabilities.

Variables:
  • name (str) – Human-readable name for the mechanism.

  • joints (list[Joint]) – All joints in the mechanism.

  • links (list[Link]) – All links in the mechanism.

  • ground (GroundLink | None) – The ground (frame) link.

Example

>>> from pylinkage.mechanism import Mechanism, GroundJoint, create_crank, create_rrr_dyad
>>> # Create a four-bar linkage
>>> O1 = GroundJoint("O1", position=(0.0, 0.0))
>>> O2 = GroundJoint("O2", position=(2.0, 0.0))
>>> ground = GroundLink("ground", joints=[O1, O2])
>>> crank, A = create_crank(O1, radius=1.0, angular_velocity=0.1)
>>> link1, link2, B = create_rrr_dyad(A, O2, distance1=2.0, distance2=1.5)
>>> mechanism = Mechanism("Four-Bar", joints=[O1, O2, A, B],
...                       links=[ground, crank, link1, link2])
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.

get_accelerations() list[Coord | None]

Return per-joint linear accelerations, in joint order.

get_constraints() list[float]

Get all distance constraints as a flat list.

Used for optimization. Returns link lengths in a consistent order.

get_coords() list[Coord]

Alias of get_joint_positions() for cross-API compatibility.

get_joint(joint_id: str) Joint | None

Get a joint by ID.

get_joint_positions() list[Coord]

Get current positions of all joints.

get_link(link_id: str) Link | None

Get a link by ID.

get_rotation_period() int

Get the number of steps for one full cycle.

For continuous rotation drivers: steps for 2*pi rotation. For arc drivers: steps for a full back-and-forth oscillation. Based on the slowest driver link’s angular velocity.

get_velocities() list[Coord | None]

Return per-joint linear velocities, in joint order.

Each entry is (vx, vy) or None if the joint’s velocity has not been computed (i.e. before step_with_derivatives() has been run).

ground: GroundLink | None = None
indeterminacy() int

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

Returns 3·(n 1) 2·R P where n is the total number of links (including the ground frame), R counts revolute pairs (RevoluteJoint and GroundJoint) and P counts prismatic pairs.

Positive ⇒ unconstrained DOF (a 1-DOF four-bar returns 1); zero ⇒ statically determinate; negative ⇒ over-constrained.

joints: list[Joint]
links: list[Link]
name: str = ''
rebuild(initial_positions: list[Coord] | None = None) None

Reset joint positions to an initial configuration.

Convenience wrapper that calls set_joint_positions() when a position list is supplied, and invalidates any cached SolverData so the next step_fast() recompiles.

Parameters:

initial_positions – Optional (x, y) positions per joint (order matches self.joints). When None the joint positions are left untouched.

reset() None

Reset all driver links to initial state.

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

Apply both constraints and joint positions in one call.

set_constraints(values: list[float]) None

Set distance constraints from a flat list.

Used for optimization. Applies constraints in the same order as get_constraints(): the radius of each driver and the length of each binary link. The values become the rigid dimensions the solver maintains, so the next step() re-solves every joint against them (a driver’s output joint is moved right away, keeping its current angle). Invalidates any cached SolverData so the next step_fast() recompiles.

Parameters:

values – List of constraint values to apply.

set_coords(positions: list[Coord]) None

Alias of set_joint_positions() for cross-API compatibility.

set_input_velocity(driver: DriverLink | ArcDriverLink, omega: float, alpha: float = 0.0) None

Set the angular velocity (and optional acceleration) of a driver.

These values are used by step_with_derivatives() to compute joint linear velocities and accelerations. They are independent of DriverLink.angular_velocity (which is in radians per simulation step) — omega is interpreted in physical units, typically rad/s.

Parameters:
  • driver – Driver link to set the input on.

  • omega – Angular velocity (rad/s).

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

Raises:

ValueError – If driver is not part of this mechanism.

set_joint_positions(positions: list[Coord]) None

Set positions of all joints.

simulation(iterations: int | None = None, dt: float = 1.0) _SimulationContext

Return a context manager that simulates this mechanism.

The context restores the initial joint positions on exit. See pylinkage._simulation_context.Simulation.

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

Simulate the mechanism.

Yields joint positions at each step of the simulation.

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

  • dt – Time step multiplier (default 1.0).

Yields:

Tuple of (x, y) coordinates for all joints.

step_fast(iterations: int | None = None, dt: float = 1.0) NDArray[np.float64]

Run the simulation through the numba-compiled solver.

Significantly faster than step() for large iteration counts.

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

  • dt – Time step multiplier.

Returns:

numpy.ndarray of shape (iterations, n_joints, 2). Unbuildable configurations appear as NaN.

step_fast_with_kinematics(iterations: int | None = None, dt: float = 1.0) tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]

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

Per-driver omega/alpha inputs must be set via set_input_velocity() (drivers 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_joints, 2).

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

Simulate the mechanism while computing velocities and accelerations.

On each step yields (positions, velocities, accelerations). The omega (and optionally alpha) of every driver link used as input must have been set via set_input_velocity(); otherwise the driver is treated as having zero input velocity.

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

  • dt – Time step multiplier (default 1.0).

Yields:

Three tuples of length len(self.joints) containing the joint positions, velocities, and accelerations for the step.

stroke_position() float

Slide position of a prismatic joint at the current pose.

See pylinkage.linkage.stroke_at_position().

transmission_angle() float

Transmission angle at the current pose, in degrees.

See pylinkage.linkage.transmission_angle_at_position().

class pylinkage.mechanism.MechanismBuilder(name: str = '', _ground_link_id: str | None = None, _ground_ports: dict[str, tuple[float, float]]=<factory>, _pending_links: dict[str, ~pylinkage.mechanism.builder.PendingLink]=<factory>, _connections: list[Connection] = <factory>, _prismatic_connections: list[PrismaticConnection] = <factory>, _slide_axes: dict[str, ~pylinkage.mechanism.builder.SlideAxis]=<factory>, _configuration: dict[str, int]=<factory>, _pending_trackers: list[PendingTracker] = <factory>)

Bases: object

Builder for creating Mechanism objects using a links-first approach.

This builder allows defining mechanisms by specifying link properties (lengths, port geometry) rather than joint positions. Joint positions are computed automatically during the build() step.

Example

>>> builder = MechanismBuilder("four-bar")
>>> builder.add_ground_link("ground", ports={"O1": (0, 0), "O2": (3, 0)})
>>> builder.add_driver_link("crank", length=1.0, motor_port="O1")
>>> builder.add_link("coupler", length=2.5)
>>> builder.add_link("rocker", length=1.5)
>>> builder.connect("crank.tip", "coupler.0")
>>> builder.connect("coupler.1", "rocker.0")
>>> builder.connect("rocker.1", "ground.O2")
>>> mechanism = builder.build()
add_arc_driver_link(id: str, length: float, motor_port: str, omega: float = 0.1, arc_start: float = 0.0, arc_end: float = 3.141592653589793, initial_angle: float | None = None) Self

Add an oscillating motor-driven link (arc crank).

An arc driver link oscillates around a ground port between angle limits (arc_start and arc_end), reversing direction at boundaries. Unlike add_driver_link which creates a continuously rotating crank, this creates a bounded-rotation driver.

Parameters:
  • id – Unique identifier for the link.

  • length – Distance from motor to output (crank radius).

  • motor_port – Name of the ground port where motor attaches.

  • omega – Angular velocity magnitude in radians per step.

  • arc_start – Minimum angle limit in radians.

  • arc_end – Maximum angle limit in radians.

  • initial_angle – Starting angle (defaults to arc_start).

Returns:

Self for method chaining.

Example

>>> builder.add_arc_driver_link("crank", length=1.0, motor_port="O1",
...                             arc_start=0.5, arc_end=2.5)
add_driver_link(id: str, length: float, motor_port: str, omega: float = 0.1, initial_angle: float = 0.0) Self

Add a motor-driven link (crank).

A driver link rotates around a ground port at a specified angular velocity. It has two ports: the motor port (at the ground) and the output port (“tip”) at distance length from the motor.

Parameters:
  • id – Unique identifier for the link.

  • length – Distance from motor to output (crank radius).

  • motor_port – Name of the ground port where motor attaches.

  • omega – Angular velocity in radians per step.

  • initial_angle – Starting angle in radians (from positive x-axis).

Returns:

Self for method chaining.

Example

>>> builder.add_driver_link("crank", length=1.0, motor_port="O1", omega=0.1)
add_ground_link(id: str, ports: dict[str, tuple[float, float]]) Self

Add the ground (frame) link with fixed port positions.

The ground link represents the stationary frame of the mechanism. Each port on the ground link has a fixed position in the global coordinate system.

Parameters:
  • id – Unique identifier for the ground link.

  • ports – Dictionary mapping port names to (x, y) positions.

Returns:

Self for method chaining.

Example

>>> builder.add_ground_link("ground", ports={"O1": (0, 0), "O2": (3, 0)})
add_link(id: str, length: float) Self

Add a binary link with given length.

A binary link has two ports (connection points) named “0” and “1”, separated by the specified length.

Parameters:
  • id – Unique identifier for the link.

  • length – Distance between the two ports.

Returns:

Self for method chaining.

Example

>>> builder.add_link("coupler", length=2.5)
add_point_tracker(id: str, ref_port1: str, ref_port2: str, distance: float | None = None, angle: float = 0.0) Self

Add a point tracker (observer) on a link.

A point tracker observes a position at a fixed distance and angle from ref_port1, with the angle measured relative to the line from ref_port1 to ref_port2. This is useful for tracking coupler points.

For tracking the midpoint of a link, set distance to half the link length and angle to 0.

Parameters:
  • id – Unique identifier for the tracker.

  • ref_port1 – Reference port for origin (e.g., “coupler.0”).

  • ref_port2 – Reference port for direction (e.g., “coupler.1”).

  • distance – Distance from ref_port1. If None, uses half the link length.

  • angle – Angle offset from ref_port1->ref_port2 direction (radians).

Returns:

Self for method chaining.

Example

>>> # Track the midpoint of the coupler link
>>> builder.add_point_tracker("coupler_mid", "coupler.0", "coupler.1")
add_quaternary_link(id: str, port_geometry: dict[str, tuple[float, float]]) Self

Add a quaternary (4-port) link.

A quaternary link has four ports. The geometry is specified as local coordinates for each port.

Parameters:
  • id – Unique identifier for the link.

  • port_geometry – Dictionary mapping port names to (x, y) positions in the link’s local coordinate frame.

Returns:

Self for method chaining.

Raises:

ValueError – If port_geometry doesn’t have exactly 4 ports.

add_slide_axis(id: str, through: tuple[float, float], direction: tuple[float, float]) Self

Define a slide axis for prismatic joints.

A slide axis is a line along which a prismatic joint can translate.

Parameters:
  • id – Unique identifier for the axis.

  • through – A point (x, y) that the line passes through.

  • direction – Direction vector (dx, dy) of the line.

Returns:

Self for method chaining.

Example

>>> builder.add_slide_axis("rail", through=(0, 0), direction=(1, 0))
add_ternary_link(id: str, port_geometry: dict[str, tuple[float, float]]) Self

Add a ternary (3-port) link with triangle geometry.

A ternary link has three ports arranged in a triangle. The geometry is specified as local coordinates for each port, from which all pairwise distances are derived.

Parameters:
  • id – Unique identifier for the link.

  • port_geometry – Dictionary mapping port names to (x, y) positions in the link’s local coordinate frame.

Returns:

Self for method chaining.

Raises:

ValueError – If port_geometry doesn’t have exactly 3 ports.

Example

>>> builder.add_ternary_link(
...     "coupler",
...     port_geometry={"A": (0, 0), "B": (3, 0), "P": (1.5, 1)}
... )
build() Mechanism

Assemble and return the Mechanism.

Validates the link definitions, computes all joint positions from constraints, and creates a Mechanism object.

Returns:

Assembled Mechanism ready for simulation.

Raises:
connect(port1: str, port2: str) Self

Connect two ports with a revolute joint.

Creates a pin joint between two ports on different links. The port identifiers use the format “link_id.port_id”.

Parameters:
  • port1 – First port identifier (e.g., “crank.tip”).

  • port2 – Second port identifier (e.g., “coupler.0”).

Returns:

Self for method chaining.

Example

>>> builder.connect("crank.tip", "coupler.0")
connect_prismatic(port: str, axis: str) Self

Connect a port to a slide axis with a prismatic joint.

Creates a slider joint that constrains the port to move along the specified axis.

Parameters:
  • port – Port identifier (e.g., “rod.1”).

  • axis – Slide axis identifier.

Returns:

Self for method chaining.

Example

>>> builder.connect_prismatic("rod.1", "rail")
name: str = ''
set_branch(joint: str, branch: int) Self

Set the assembly branch for a joint with two solutions.

When computing joint positions via circle-circle intersection, there are typically two solutions. This method allows selecting which solution to use.

Parameters:
  • joint – Joint identifier (typically “link_id.port_id”).

  • branch – 0 for first solution, 1 for second solution.

Returns:

Self for method chaining.

Example

>>> builder.set_branch("coupler.1", 1)  # Use "lower" configuration
class pylinkage.mechanism.PrismaticJoint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None, axis: Coord = (1.0, 0.0), line_point: Coord = (0.0, 0.0), slide_distance: float = 0.0)

Bases: Joint

Slider joint allowing translation along an axis.

A prismatic joint permits one degree of freedom: translation along the joint axis.

Variables:
  • axis (Coord) – Direction of allowed translation as (dx, dy). Should be normalized for consistency.

  • line_point (Coord) – A fixed point (x, y) on the slide line. The joint is constrained to move along the line passing through this point in the axis direction.

  • slide_distance (float) – Current displacement along the axis from origin.

Example

>>> joint = PrismaticJoint("S", position=(0.0, 0.0), axis=(1.0, 0.0))
>>> joint.joint_type
<JointType.PRISMATIC: 2>
axis: Coord = (1.0, 0.0)
get_axis_normalized() tuple[float, float]

Return the normalized axis direction.

property joint_type: JointType

Return PRISMATIC type.

line_point: Coord = (0.0, 0.0)
slide_distance: float = 0.0
class pylinkage.mechanism.RevoluteJoint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None)

Bases: Joint

Pin joint allowing rotation between two links.

A revolute joint permits one degree of freedom: rotation about the joint axis (perpendicular to the plane for planar mechanisms).

This is the most common joint type in planar linkages.

Example

>>> joint = RevoluteJoint("A", position=(1.0, 2.0))
>>> joint.joint_type
<JointType.REVOLUTE: 1>
property joint_type: JointType

Return REVOLUTE type.

class pylinkage.mechanism.TrackerJoint(id: str, position: MaybeCoord = (None, None), name: str | None = None, _links: list[Link] = <factory>, velocity: Coord | None = None, acceleration: Coord | None = None, ref_joint1_id: str = '', ref_joint2_id: str = '', distance: float = 0.0, angle: float = 0.0)

Bases: Joint

Observer joint that tracks a position relative to two reference joints.

A tracker joint is a sensor/observer that computes its position as a point at a fixed distance and angle from a reference joint, with the angle measured relative to the line connecting the two reference joints.

This is useful for: - Tracking coupler points on a link (e.g., Chebyshev straight-line mechanism) - Observing positions without affecting the kinematic chain - Adding tracer points for visualization

Variables:
  • ref_joint1_id (str) – ID of the first reference joint (origin for polar coords).

  • ref_joint2_id (str) – ID of the second reference joint (defines reference direction).

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

  • angle (float) – Angle offset from ref_joint1->ref_joint2 direction (radians).

Example

>>> # Track the midpoint of a link between joints A and B
>>> tracker = TrackerJoint("midpoint", ref_joint1_id="A", ref_joint2_id="B",
...                        distance=0.5, angle=0.0)
angle: float = 0.0
distance: float = 0.0
property joint_type: JointType

Return TRACKER type.

ref_joint1_id: str = ''
ref_joint2_id: str = ''
update_position(ref1_pos: tuple[float, float], ref2_pos: tuple[float, float]) None

Compute position from reference joint positions.

Parameters:
  • ref1_pos – Position of first reference joint (x, y).

  • ref2_pos – Position of second reference joint (x, y).

pylinkage.mechanism.fourbar(crank: float, coupler: float, rocker: float, ground: float, omega: float = 0.06283185307179587, initial_angle: float = 0.0, branch: int = 1, name: str = 'fourbar') Mechanism

Build a four-bar Mechanism from link lengths.

Ground pivots are placed at A = (0, 0) and D = (ground, 0). The crank rotates about A and the rocker oscillates about D.

Parameters:
  • crank – Crank length (link a, A-B).

  • coupler – Coupler length (link b, B-C).

  • rocker – Rocker length (link c, C-D).

  • ground – Ground link length (link d, A-D).

  • omega – Driver angular velocity, in rad/step.

  • initial_angle – Starting crank angle, in radians.

  • branch0 or 1 — which of the two circle-circle intersections to pick for the coupler-rocker joint. Branch 1 (default) is the upper (positive y) configuration, matching synthesis.fourbar_from_lengths.

  • name – Name of the resulting mechanism.

Returns:

An assembled Mechanism.

Raises:

pylinkage.exceptions.UnbuildableError – if the link lengths cannot form a closed loop at initial_angle.

Example

>>> from pylinkage.mechanism import fourbar
>>> mech = fourbar(crank=1.0, coupler=3.0, rocker=3.0, ground=4.0)
>>> loci = list(mech.step())
pylinkage.mechanism.mechanism_from_dict(data: dict[str, Any]) Mechanism

Deserialize a mechanism from a dictionary.

Parameters:

data – Dictionary representation of a mechanism.

Returns:

The deserialized Mechanism object.

Example

>>> mechanism = mechanism_from_dict(data)
pylinkage.mechanism.mechanism_from_json(path: str | Path) Mechanism

Load a mechanism from a JSON file.

Parameters:

path – Path to the JSON file.

Returns:

The loaded Mechanism object.

pylinkage.mechanism.mechanism_to_dict(mechanism: Mechanism) dict[str, Any]

Serialize a mechanism to a dictionary.

Parameters:

mechanism – The mechanism to serialize.

Returns:

Dictionary representation of the mechanism.

Example

>>> data = mechanism_to_dict(mechanism)
>>> json.dumps(data, indent=2)
pylinkage.mechanism.mechanism_to_json(mechanism: Mechanism, path: str | Path) None

Save a mechanism to a JSON file.

Parameters:
  • mechanism – The mechanism to save.

  • path – Path to the output JSON file.

pylinkage.mechanism.slider_crank(crank: float, rod: float, omega: float = 0.06283185307179587, initial_angle: float = 0.0, slide_through: tuple[float, float] = (0.0, 0.0), slide_direction: tuple[float, float] = (1.0, 0.0), name: str = 'slider-crank') Mechanism

Build a slider-crank Mechanism.

A crank of length crank rotates about the origin and drives a rod of length rod whose far end slides along the line through slide_through in direction slide_direction.

Parameters:
  • crank – Crank length.

  • rod – Connecting rod length.

  • omega – Driver angular velocity, in rad/step.

  • initial_angle – Starting crank angle, in radians.

  • slide_through – A point the slide axis passes through.

  • slide_direction – Direction vector of the slide axis.

  • name – Name of the resulting mechanism.

Returns:

An assembled Mechanism.

Example

>>> from pylinkage.mechanism import slider_crank
>>> mech = slider_crank(crank=1.0, rod=3.0)
>>> loci = list(mech.step())