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:
objectA 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:
objectBuilder 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:
UnbuildableError – If geometry is impossible (links can’t reach).
UnderconstrainedError – If system is underconstrained.
ValueError – If link definitions are invalid.
- 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.builder.PendingLink(id: str, ports: dict[str, ~pylinkage.mechanism.builder.Port]=<factory>, length: float | None = None, port_geometry: dict[str, tuple[float, float]] | None=None, is_driver: bool = False, is_arc_driver: bool = False, motor_port: str | None = None, angular_velocity: float = 0.0, initial_angle: float = 0.0, arc_start: float = 0.0, arc_end: float = 3.141592653589793)
Bases:
objectA 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
- class pylinkage.mechanism.builder.PendingTracker(id: str, ref_port1: str, ref_port2: str, distance: float, angle: float = 0.0)
Bases:
objectA 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:
objectA 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:
objectA 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:
objectDefinition 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)andD = (ground, 0). The crank rotates aboutAand the rocker oscillates aboutD.- 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.
branch –
0or1— which of the two circle-circle intersections to pick for the coupler-rocker joint. Branch1(default) is the upper (positive y) configuration, matchingsynthesis.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
crankrotates about the origin and drives a rod of lengthrodwhose far end slides along the line throughslide_throughin directionslide_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:
RevoluteJointRevolute 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
- 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:
ABCBase 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).
- 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:
IntEnumKinematic 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:
JointSlider 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.
- 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:
JointPin 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>
- 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:
JointObserver 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
- 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.link module
Link classes for the mechanism module.
This module defines rigid body types used in planar mechanisms: - Link: A rigid body connecting two or more joints - GroundLink: The stationary frame (ground) of the mechanism - DriverLink: An input link driven by a motor
Links are the structural members of a mechanism that transmit forces and motion between joints.
- class pylinkage.mechanism.link.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:
LinkAn 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
- motor_joint: GroundJoint | None = None
- 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.link.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:
LinkAn 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
- motor_joint: GroundJoint | None = None
- 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.link.GroundLink(id: str, joints: list[Joint] = <factory>, name: str | None = None, _cached_distances: dict[tuple[str, str], float] = <factory>, is_ground: bool = True)
Bases:
LinkThe 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
- class pylinkage.mechanism.link.Link(id: str, joints: list[Joint] = <factory>, name: str | None = None, _cached_distances: dict[tuple[str, str], float] = <factory>)
Bases:
objectA 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
- 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.
- 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
distanceis not positive.
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:
objectA 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_dataand 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_positions() list[Coord]
Get current positions of all joints.
- 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)orNoneif the joint’s velocity has not been computed (i.e. beforestep_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 − Pwherenis the total number of links (including the ground frame),Rcounts revolute pairs (RevoluteJointandGroundJoint) andPcounts prismatic pairs.Positive ⇒ unconstrained DOF (a 1-DOF four-bar returns
1); zero ⇒ statically determinate; negative ⇒ over-constrained.
- 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 nextstep_fast()recompiles.- Parameters:
initial_positions – Optional
(x, y)positions per joint (order matchesself.joints). WhenNonethe 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 nextstep()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 nextstep_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 ofDriverLink.angular_velocity(which is in radians per simulation step) —omegais 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
driveris 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.ndarrayof 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/alphainputs must be set viaset_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). Theomega(and optionallyalpha) of every driver link used as input must have been set viaset_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.
- pylinkage.mechanism.serialization.link_from_dict(data: dict[str, Any], joint_map: dict[str, Joint]) Link
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.
- pylinkage.mechanism.serialization.link_to_dict(link: Link) dict[str, Any]
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)
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:
LinkAn 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:
LinkAn 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:
RevoluteJointRevolute 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:
LinkThe 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:
ABCBase 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:
IntEnumKinematic 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:
objectA 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
distanceis not positive.
- class pylinkage.mechanism.LinkType(value)
Bases:
IntEnumEnumeration 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:
objectA 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_dataand 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)orNoneif the joint’s velocity has not been computed (i.e. beforestep_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 − Pwherenis the total number of links (including the ground frame),Rcounts revolute pairs (RevoluteJointandGroundJoint) andPcounts 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 nextstep_fast()recompiles.- Parameters:
initial_positions – Optional
(x, y)positions per joint (order matchesself.joints). WhenNonethe 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 nextstep()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 nextstep_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 ofDriverLink.angular_velocity(which is in radians per simulation step) —omegais 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
driveris 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.ndarrayof 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/alphainputs must be set viaset_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). Theomega(and optionallyalpha) of every driver link used as input must have been set viaset_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:
objectBuilder 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:
UnbuildableError – If geometry is impossible (links can’t reach).
UnderconstrainedError – If system is underconstrained.
ValueError – If link definitions are invalid.
- 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:
JointSlider 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:
JointPin 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:
JointObserver 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)andD = (ground, 0). The crank rotates aboutAand the rocker oscillates aboutD.- 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.
branch –
0or1— which of the two circle-circle intersections to pick for the coupler-rocker joint. Branch1(default) is the upper (positive y) configuration, matchingsynthesis.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
crankrotates about the origin and drives a rod of lengthrodwhose far end slides along the line throughslide_throughin directionslide_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())