pylinkage.dyads package

Submodules

pylinkage.dyads.factory module

Factory function for creating dyads from isomer signatures.

This module provides a unified interface for creating any of the 12 dyadic isomers using a signature string.

Notation:

R = Revolute joint (pin) T = Translating element (slider block) _ = Guide (rail/slot that slider moves along)

The 12 isomers map to 3 geometry types:

  • RRR: circle-circle intersection

  • Circle-line isomers (RR_T, RRT_, RT_R, R_T_T, RT_T_, R_TT_, RT__T): RRPDyad

  • Line-line isomers (T_R_T, T_RT_, _TRT_): PPDyad

pylinkage.dyads.factory.create_dyad(signature: str, anchors: dict[str, Component | _AnchorProxy], constraints: dict[str, float] | None = None, x: float | None = None, y: float | None = None, name: str | None = None) RRRDyad | RRPDyad | PPDyad

Create a dyad from an isomer signature.

This factory function provides a unified interface for creating any of the 12 dyadic isomers. It determines the appropriate geometry type from the signature and creates the corresponding dyad class.

Parameters:
  • signature – Isomer signature (e.g., “RRR”, “RT_R”, “T_R_T”).

  • anchors – Dict mapping anchor roles to components. Required keys depend on geometry type: - circle_circle (RRR): “anchor1”, “anchor2” - circle_line (RRP variants): “revolute”, “line1”, “line2” - line_line (PP variants): “line1_p1”, “line1_p2”, “line2_p1”, “line2_p2”

  • constraints – Dict of constraints. Required keys depend on geometry type: - circle_circle (RRR): “distance1”, “distance2” - circle_line (RRP variants): “distance” - line_line (PP variants): none required

  • x – Initial x position hint (optional).

  • y – Initial y position hint (optional).

  • name – Human-readable identifier.

Returns:

The appropriate dyad instance (RRRDyad, RRPDyad, or PPDyad).

Raises:

ValueError – If signature is unknown or required anchors/constraints are missing.

Examples

>>> # Circle-circle (RRR)
>>> dyad = create_dyad(
...     signature="RRR",
...     anchors={"anchor1": crank.output, "anchor2": ground2},
...     constraints={"distance1": 2.0, "distance2": 1.5},
...     name="rocker",
... )
>>> # Circle-line (RT_R - slider crank)
>>> dyad = create_dyad(
...     signature="RT_R",
...     anchors={"revolute": crank.output, "line1": L1, "line2": L2},
...     constraints={"distance": 1.5},
...     name="slider",
... )
>>> # Line-line (T_R_T - double slider)
>>> dyad = create_dyad(
...     signature="T_R_T",
...     anchors={
...         "line1_p1": A, "line1_p2": B,
...         "line2_p1": C, "line2_p2": D,
...     },
...     name="double_slider",
... )
pylinkage.dyads.factory.get_isomer_geometry(signature: str) str

Get the geometry type for an isomer signature.

Parameters:

signature – The isomer signature.

Returns:

“circle_circle”, “circle_line”, or “line_line”.

Return type:

Geometry type

Raises:

ValueError – If signature is unknown.

pylinkage.dyads.factory.get_required_anchors(signature: str) list[str]

Get the required anchor names for an isomer signature.

Parameters:

signature – The isomer signature.

Returns:

List of required anchor keys.

Raises:

ValueError – If signature is unknown.

pylinkage.dyads.factory.get_required_constraints(signature: str) list[str]

Get the required constraint names for an isomer signature.

Parameters:

signature – The isomer signature.

Returns:

List of required constraint keys.

Raises:

ValueError – If signature is unknown.

pylinkage.dyads.fixed module

FixedDyad - deterministic polar constraint.

A fixed dyad positions a joint at a fixed distance and angle relative to two anchor joints. Unlike RRR dyad, the position is deterministic (no ambiguity).

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

Bases: BinaryDyad

Fixed Dyad - deterministic polar projection.

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

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

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

  • anchor2 – Second anchor (defines reference direction).

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

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

Example

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

Return the distance and angle constraints.

Returns:

Tuple of (distance, angle).

reload(dt: float = 1) None

Recompute position using polar projection.

The position is always deterministic - no ambiguity.

Parameters:

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

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

Set the distance and angle constraints.

Parameters:
  • distance – New distance from anchor1.

  • angle – New angle offset (radians).

  • *args – Ignored (for interface compatibility).

pylinkage.dyads.oscillating_cam module

Oscillating cam follower - rocker arm driven by cam profile.

An oscillating cam follower pivots about a fixed point based on the cam’s rotation angle. The cam profile determines the follower’s angular displacement as a function of the cam angle.

class pylinkage.dyads.oscillating_cam.OscillatingCamFollower(cam_driver: Crank, profile: CamProfile, pivot_anchor: Ground, arm_length: float, initial_angle: float = 0.0, roller_radius: float = 0.0, name: str | None = None)

Bases: ConnectedComponent

Oscillating cam follower - rocker arm driven by cam profile.

The follower pivots about a fixed point (pivot_anchor). The cam profile maps cam angle to follower arm angle offset. The output is at a fixed distance from the pivot along the arm direction.

For oscillating followers, the profile’s “displacement” is interpreted as an angular displacement (in radians) added to the initial_angle.

Knife-edge vs roller is controlled by the roller_radius parameter.

Variables:
  • cam_driver (Crank) – Crank actuator driving the cam rotation.

  • profile (CamProfile) – Cam profile defining output angle offset vs input angle.

  • pivot_anchor (Ground) – Fixed pivot point for the rocker arm.

  • arm_length (float) – Distance from pivot to follower output.

  • initial_angle (float) – Starting angle of the arm (radians from +x).

  • roller_radius (float) – Radius of roller (0 for knife-edge).

Example

>>> from pylinkage.components import Ground
>>> from pylinkage.actuators import Crank
>>> from pylinkage.cam import FunctionProfile, CycloidalMotionLaw
>>> import math
>>>
>>> O_cam = Ground(0.0, 0.0, name="cam_center")
>>> O_pivot = Ground(2.0, 0.0, name="pivot")
>>> cam = Crank(anchor=O_cam, radius=0.1, angular_velocity=0.1)
>>> # Profile: maps cam angle to arm angle offset (radians)
>>> profile = FunctionProfile(
...     motion_law=CycloidalMotionLaw(),
...     base_radius=0.0,  # No offset at base
...     total_lift=math.pi/4,  # 45 degree swing
... )
>>> follower = OscillatingCamFollower(
...     cam_driver=cam,
...     profile=profile,
...     pivot_anchor=O_pivot,
...     arm_length=1.5,
...     initial_angle=math.pi/2,  # Arm starts vertical
...     roller_radius=0.1,
... )
property anchors: tuple[Component, ...]

Return parent components (cam driver and pivot).

property angular_displacement: float

Return the current angular displacement from initial angle.

property arm_angle: float

Return the current arm angle in radians.

arm_length: float
property cam_angle: float

Return the current cam angle in radians.

cam_driver: Crank
get_constraints() tuple[float, ...]

Return optimizable constraints.

Returns arm_length, initial_angle, roller_radius, and profile constraints.

initial_angle: float
property output: _AnchorProxy

Return the output proxy for connecting other components.

Returns:

An anchor proxy representing the follower output.

pivot_anchor: Ground
profile: CamProfile
reload(dt: float = 1) None

Recompute follower position from cam angle.

Parameters:

dt – Time step (unused, cam angle from driver position).

Raises:

ValueError – If cam driver or pivot has undefined position.

roller_radius: float
set_constraints(arm_length: float | None = None, initial_angle: float | None = None, roller_radius: float | None = None, *profile_constraints: float | None) None

Set constraints from optimization.

Parameters:
  • arm_length – New arm length.

  • initial_angle – New initial angle.

  • roller_radius – New roller radius.

  • *profile_constraints – Constraints passed to profile.

pylinkage.dyads.pp module

PPDyad - line-line intersection.

A PP dyad consists of two prismatic constraints, positioning a joint at the intersection of two lines. This covers isomers like T_R_T, T_RT_ and _TRT_.

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

Bases: ConnectedComponent

PP Dyad - line-line intersection.

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

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

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

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

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

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

Example

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

Return the parent dyads (four line anchors).

get_constraints() tuple[()]

Return the constraints (none for PP dyad).

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

Returns:

Empty tuple (no constraints).

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

Recompute position using line-line intersection.

Parameters:

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

Raises:

UnbuildableError – If lines are parallel (no intersection).

set_constraints(*args: float | None) None

Set constraints (no-op for PP dyad).

PP dyads have no constraints to set.

Parameters:

*args – Ignored (for interface compatibility).

pylinkage.dyads.rrp module

RRPDyad - circle-line intersection (slider mechanism).

An RRP dyad consists of a revolute connection to an anchor plus a prismatic (sliding) connection along a line.

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

Bases: ConnectedComponent

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

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

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

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

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

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

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

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

Example

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

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

distance: float
get_constraints() tuple[float]

Return the distance constraint.

Returns:

Tuple containing the distance to revolute anchor.

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

Recompute position using circle-line intersection.

Parameters:

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

Raises:

UnbuildableError – If circle doesn’t intersect line.

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

Set the distance constraint.

Parameters:
  • distance – New distance to revolute anchor.

  • *args – Ignored (for interface compatibility).

pylinkage.dyads.rrr module

RRRDyad - circle-circle intersection (two revolute joints meeting at one).

The most common Assur group, consisting of two links connected by revolute joints, meeting at a computed internal revolute joint.

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

Bases: BinaryDyad

RRR Dyad - circle-circle intersection.

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

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

Variables:
  • anchor1 – First connection point.

  • anchor2 – Second connection point.

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

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

Example

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

Return the two distance constraints.

Returns:

Tuple of (distance1, distance2).

reload(dt: float = 1) None

Recompute position using circle-circle intersection.

Parameters:

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

Raises:

UnbuildableError – If the circles don’t intersect.

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

Set the distance constraints.

Parameters:
  • distance1 – New distance to anchor1.

  • distance2 – New distance to anchor2.

  • *args – Ignored (for interface compatibility).

pylinkage.dyads.translating_cam module

Translating cam follower - linear motion driven by cam profile.

A translating cam follower moves along a fixed axis (guide) based on the cam’s rotation angle. The cam profile determines the follower’s displacement as a function of the cam angle.

class pylinkage.dyads.translating_cam.TranslatingCamFollower(cam_driver: Crank, profile: CamProfile, guide: Ground, guide_angle: float = 0.0, roller_radius: float = 0.0, name: str | None = None)

Bases: ConnectedComponent

Translating cam follower - linear motion driven by cam profile.

The follower moves along a fixed axis (defined by guide_angle) based on the cam’s rotation angle. The output position is determined by the cam profile evaluation.

Knife-edge vs roller is controlled by the roller_radius parameter:
  • roller_radius=0: knife-edge follower (point contact)

  • roller_radius>0: roller follower (uses pitch curve)

Variables:
  • cam_driver (Crank) – Crank actuator driving the cam rotation.

  • profile (CamProfile) – Cam profile defining displacement vs angle.

  • guide (Ground) – Ground point defining the guide axis origin.

  • guide_angle (float) – Angle of the guide axis (radians from +x).

  • roller_radius (float) – Radius of roller (0 for knife-edge).

Example

>>> from pylinkage.components import Ground
>>> from pylinkage.actuators import Crank
>>> from pylinkage.cam import FunctionProfile, HarmonicMotionLaw
>>> import math
>>>
>>> O = Ground(0.0, 0.0, name="cam_center")
>>> cam = Crank(anchor=O, radius=0.1, angular_velocity=0.1)
>>> profile = FunctionProfile(
...     motion_law=HarmonicMotionLaw(),
...     base_radius=1.0,
...     total_lift=0.5,
... )
>>> guide = Ground(0.0, 0.0, name="guide")
>>> follower = TranslatingCamFollower(
...     cam_driver=cam,
...     profile=profile,
...     guide=guide,
...     guide_angle=math.pi/2,  # Vertical motion
...     roller_radius=0.1,
... )
property anchors: tuple[Component, ...]

Return parent components (cam driver and guide).

property cam_angle: float

Return the current cam angle in radians.

cam_driver: Crank
property displacement: float

Return the current follower displacement from base position.

get_constraints() tuple[float, ...]

Return optimizable constraints.

Returns roller_radius and profile constraints.

guide: Ground
guide_angle: float
property output: _AnchorProxy

Return the output proxy for connecting other components.

Returns:

An anchor proxy representing the follower output.

profile: CamProfile
reload(dt: float = 1) None

Recompute follower position from cam angle.

Parameters:

dt – Time step (used to get cam angle from driver).

Raises:

ValueError – If cam driver or guide has undefined position.

roller_radius: float
set_constraints(roller_radius: float | None = None, *profile_constraints: float | None) None

Set constraints from optimization.

Parameters:
  • roller_radius – New roller radius.

  • *profile_constraints – Constraints passed to profile.

Module contents

Dyads - Assur group building blocks for planar linkages.

This module provides true Assur group dyads (0 DOF structural units):

Classes:

RRRDyad: Circle-circle intersection (two links meeting at one joint) RRPDyad: Circle-line intersection (slider mechanism) PPDyad: Line-line intersection (double slider) FixedDyad: Deterministic polar projection BinaryDyad: Base class for binary Assur groups TranslatingCamFollower: Translating follower driven by cam profile OscillatingCamFollower: Oscillating (rocker) follower driven by cam profile

Functions:

create_dyad: Factory function to create dyads from isomer signatures

For other kinematic elements, use the appropriate modules:
  • pylinkage.components: Ground, base classes (Component, ConnectedComponent)

  • pylinkage.actuators: Crank, LinearActuator

  • pylinkage.cam: CamProfile, FunctionProfile, motion laws

  • pylinkage.simulation: Linkage

Example

Build and simulate a four-bar linkage:

from pylinkage.components import Ground
from pylinkage.actuators import Crank
from pylinkage.dyads import RRRDyad
from pylinkage.simulation import Linkage

# Ground points
O1 = Ground(0.0, 0.0, name="O1")
O2 = Ground(2.0, 0.0, name="O2")

# Crank (driver)
crank = Crank(anchor=O1, radius=1.0, angular_velocity=0.1)

# Rocker (RRR dyad)
rocker = RRRDyad(
    anchor1=crank.output,
    anchor2=O2,
    distance1=2.0,
    distance2=1.5,
)

# Build and simulate
linkage = Linkage([O1, O2, crank, rocker], name="Four-Bar")
for positions in linkage.step():
    print(positions)
Deprecated re-exports:

This module used to re-export Ground, PointTracker, Crank, ArcCrank, LinearActuator, Linkage, Component and ConnectedComponent from their home modules. Those names still resolve here but warn; import them from pylinkage.components, pylinkage.actuators and pylinkage.simulation. See the Deprecations page.

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

Bases: ConnectedComponent

Base class for dyads connecting exactly two parents.

Binary dyads have two anchor points and typically compute their position as an intersection (circle-circle, circle-line, etc.). These are the true Assur groups that add 0 degrees of freedom.

anchor1: Component | _AnchorProxy
anchor2: Component | _AnchorProxy
property anchors: tuple[Component, Component]

Return the two parent dyads.

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

Bases: BinaryDyad

Fixed Dyad - deterministic polar projection.

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

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

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

  • anchor2 – Second anchor (defines reference direction).

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

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

Example

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

Return the distance and angle constraints.

Returns:

Tuple of (distance, angle).

reload(dt: float = 1) None

Recompute position using polar projection.

The position is always deterministic - no ambiguity.

Parameters:

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

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

Set the distance and angle constraints.

Parameters:
  • distance – New distance from anchor1.

  • angle – New angle offset (radians).

  • *args – Ignored (for interface compatibility).

class pylinkage.dyads.OscillatingCamFollower(cam_driver: Crank, profile: CamProfile, pivot_anchor: Ground, arm_length: float, initial_angle: float = 0.0, roller_radius: float = 0.0, name: str | None = None)

Bases: ConnectedComponent

Oscillating cam follower - rocker arm driven by cam profile.

The follower pivots about a fixed point (pivot_anchor). The cam profile maps cam angle to follower arm angle offset. The output is at a fixed distance from the pivot along the arm direction.

For oscillating followers, the profile’s “displacement” is interpreted as an angular displacement (in radians) added to the initial_angle.

Knife-edge vs roller is controlled by the roller_radius parameter.

Variables:
  • cam_driver (Crank) – Crank actuator driving the cam rotation.

  • profile (CamProfile) – Cam profile defining output angle offset vs input angle.

  • pivot_anchor (Ground) – Fixed pivot point for the rocker arm.

  • arm_length (float) – Distance from pivot to follower output.

  • initial_angle (float) – Starting angle of the arm (radians from +x).

  • roller_radius (float) – Radius of roller (0 for knife-edge).

Example

>>> from pylinkage.components import Ground
>>> from pylinkage.actuators import Crank
>>> from pylinkage.cam import FunctionProfile, CycloidalMotionLaw
>>> import math
>>>
>>> O_cam = Ground(0.0, 0.0, name="cam_center")
>>> O_pivot = Ground(2.0, 0.0, name="pivot")
>>> cam = Crank(anchor=O_cam, radius=0.1, angular_velocity=0.1)
>>> # Profile: maps cam angle to arm angle offset (radians)
>>> profile = FunctionProfile(
...     motion_law=CycloidalMotionLaw(),
...     base_radius=0.0,  # No offset at base
...     total_lift=math.pi/4,  # 45 degree swing
... )
>>> follower = OscillatingCamFollower(
...     cam_driver=cam,
...     profile=profile,
...     pivot_anchor=O_pivot,
...     arm_length=1.5,
...     initial_angle=math.pi/2,  # Arm starts vertical
...     roller_radius=0.1,
... )
property anchors: tuple[Component, ...]

Return parent components (cam driver and pivot).

property angular_displacement: float

Return the current angular displacement from initial angle.

property arm_angle: float

Return the current arm angle in radians.

arm_length: float
property cam_angle: float

Return the current cam angle in radians.

cam_driver: Crank
get_constraints() tuple[float, ...]

Return optimizable constraints.

Returns arm_length, initial_angle, roller_radius, and profile constraints.

initial_angle: float
property output: _AnchorProxy

Return the output proxy for connecting other components.

Returns:

An anchor proxy representing the follower output.

pivot_anchor: Ground
profile: CamProfile
reload(dt: float = 1) None

Recompute follower position from cam angle.

Parameters:

dt – Time step (unused, cam angle from driver position).

Raises:

ValueError – If cam driver or pivot has undefined position.

roller_radius: float
set_constraints(arm_length: float | None = None, initial_angle: float | None = None, roller_radius: float | None = None, *profile_constraints: float | None) None

Set constraints from optimization.

Parameters:
  • arm_length – New arm length.

  • initial_angle – New initial angle.

  • roller_radius – New roller radius.

  • *profile_constraints – Constraints passed to profile.

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

Bases: ConnectedComponent

PP Dyad - line-line intersection.

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

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

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

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

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

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

Example

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

Return the parent dyads (four line anchors).

get_constraints() tuple[()]

Return the constraints (none for PP dyad).

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

Returns:

Empty tuple (no constraints).

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

Recompute position using line-line intersection.

Parameters:

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

Raises:

UnbuildableError – If lines are parallel (no intersection).

set_constraints(*args: float | None) None

Set constraints (no-op for PP dyad).

PP dyads have no constraints to set.

Parameters:

*args – Ignored (for interface compatibility).

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

Bases: ConnectedComponent

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

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

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

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

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

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

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

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

Example

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

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

distance: float
get_constraints() tuple[float]

Return the distance constraint.

Returns:

Tuple containing the distance to revolute anchor.

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

Recompute position using circle-line intersection.

Parameters:

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

Raises:

UnbuildableError – If circle doesn’t intersect line.

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

Set the distance constraint.

Parameters:
  • distance – New distance to revolute anchor.

  • *args – Ignored (for interface compatibility).

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

Bases: BinaryDyad

RRR Dyad - circle-circle intersection.

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

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

Variables:
  • anchor1 – First connection point.

  • anchor2 – Second connection point.

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

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

Example

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

Return the two distance constraints.

Returns:

Tuple of (distance1, distance2).

reload(dt: float = 1) None

Recompute position using circle-circle intersection.

Parameters:

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

Raises:

UnbuildableError – If the circles don’t intersect.

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

Set the distance constraints.

Parameters:
  • distance1 – New distance to anchor1.

  • distance2 – New distance to anchor2.

  • *args – Ignored (for interface compatibility).

class pylinkage.dyads.TranslatingCamFollower(cam_driver: Crank, profile: CamProfile, guide: Ground, guide_angle: float = 0.0, roller_radius: float = 0.0, name: str | None = None)

Bases: ConnectedComponent

Translating cam follower - linear motion driven by cam profile.

The follower moves along a fixed axis (defined by guide_angle) based on the cam’s rotation angle. The output position is determined by the cam profile evaluation.

Knife-edge vs roller is controlled by the roller_radius parameter:
  • roller_radius=0: knife-edge follower (point contact)

  • roller_radius>0: roller follower (uses pitch curve)

Variables:
  • cam_driver (Crank) – Crank actuator driving the cam rotation.

  • profile (CamProfile) – Cam profile defining displacement vs angle.

  • guide (Ground) – Ground point defining the guide axis origin.

  • guide_angle (float) – Angle of the guide axis (radians from +x).

  • roller_radius (float) – Radius of roller (0 for knife-edge).

Example

>>> from pylinkage.components import Ground
>>> from pylinkage.actuators import Crank
>>> from pylinkage.cam import FunctionProfile, HarmonicMotionLaw
>>> import math
>>>
>>> O = Ground(0.0, 0.0, name="cam_center")
>>> cam = Crank(anchor=O, radius=0.1, angular_velocity=0.1)
>>> profile = FunctionProfile(
...     motion_law=HarmonicMotionLaw(),
...     base_radius=1.0,
...     total_lift=0.5,
... )
>>> guide = Ground(0.0, 0.0, name="guide")
>>> follower = TranslatingCamFollower(
...     cam_driver=cam,
...     profile=profile,
...     guide=guide,
...     guide_angle=math.pi/2,  # Vertical motion
...     roller_radius=0.1,
... )
property anchors: tuple[Component, ...]

Return parent components (cam driver and guide).

property cam_angle: float

Return the current cam angle in radians.

cam_driver: Crank
property displacement: float

Return the current follower displacement from base position.

get_constraints() tuple[float, ...]

Return optimizable constraints.

Returns roller_radius and profile constraints.

guide: Ground
guide_angle: float
property output: _AnchorProxy

Return the output proxy for connecting other components.

Returns:

An anchor proxy representing the follower output.

profile: CamProfile
reload(dt: float = 1) None

Recompute follower position from cam angle.

Parameters:

dt – Time step (used to get cam angle from driver).

Raises:

ValueError – If cam driver or guide has undefined position.

roller_radius: float
set_constraints(roller_radius: float | None = None, *profile_constraints: float | None) None

Set constraints from optimization.

Parameters:
  • roller_radius – New roller radius.

  • *profile_constraints – Constraints passed to profile.

pylinkage.dyads.create_dyad(signature: str, anchors: dict[str, Component | _AnchorProxy], constraints: dict[str, float] | None = None, x: float | None = None, y: float | None = None, name: str | None = None) RRRDyad | RRPDyad | PPDyad

Create a dyad from an isomer signature.

This factory function provides a unified interface for creating any of the 12 dyadic isomers. It determines the appropriate geometry type from the signature and creates the corresponding dyad class.

Parameters:
  • signature – Isomer signature (e.g., “RRR”, “RT_R”, “T_R_T”).

  • anchors – Dict mapping anchor roles to components. Required keys depend on geometry type: - circle_circle (RRR): “anchor1”, “anchor2” - circle_line (RRP variants): “revolute”, “line1”, “line2” - line_line (PP variants): “line1_p1”, “line1_p2”, “line2_p1”, “line2_p2”

  • constraints – Dict of constraints. Required keys depend on geometry type: - circle_circle (RRR): “distance1”, “distance2” - circle_line (RRP variants): “distance” - line_line (PP variants): none required

  • x – Initial x position hint (optional).

  • y – Initial y position hint (optional).

  • name – Human-readable identifier.

Returns:

The appropriate dyad instance (RRRDyad, RRPDyad, or PPDyad).

Raises:

ValueError – If signature is unknown or required anchors/constraints are missing.

Examples

>>> # Circle-circle (RRR)
>>> dyad = create_dyad(
...     signature="RRR",
...     anchors={"anchor1": crank.output, "anchor2": ground2},
...     constraints={"distance1": 2.0, "distance2": 1.5},
...     name="rocker",
... )
>>> # Circle-line (RT_R - slider crank)
>>> dyad = create_dyad(
...     signature="RT_R",
...     anchors={"revolute": crank.output, "line1": L1, "line2": L2},
...     constraints={"distance": 1.5},
...     name="slider",
... )
>>> # Line-line (T_R_T - double slider)
>>> dyad = create_dyad(
...     signature="T_R_T",
...     anchors={
...         "line1_p1": A, "line1_p2": B,
...         "line2_p1": C, "line2_p2": D,
...     },
...     name="double_slider",
... )
pylinkage.dyads.get_isomer_geometry(signature: str) str

Get the geometry type for an isomer signature.

Parameters:

signature – The isomer signature.

Returns:

“circle_circle”, “circle_line”, or “line_line”.

Return type:

Geometry type

Raises:

ValueError – If signature is unknown.

pylinkage.dyads.get_required_anchors(signature: str) list[str]

Get the required anchor names for an isomer signature.

Parameters:

signature – The isomer signature.

Returns:

List of required anchor keys.

Raises:

ValueError – If signature is unknown.

pylinkage.dyads.get_required_constraints(signature: str) list[str]

Get the required constraint names for an isomer signature.

Parameters:

signature – The isomer signature.

Returns:

List of required constraint keys.

Raises:

ValueError – If signature is unknown.

pylinkage.dyads.to_mechanism(linkage: Linkage) Mechanism

Convert a dyads Linkage to a mechanism Mechanism.

This creates a low-level Mechanism object from a dyads Linkage, allowing access to the full mechanism API (Joint/Link classes, step_fast(), etc.).

Parameters:

linkage – A dyads Linkage to convert.

Returns:

A mechanism.Mechanism object.

Note

This is a one-way conversion. Changes to the returned Mechanism will not be reflected in the original Linkage.