pylinkage.assur package

Submodules

pylinkage.assur.analysis module

Analysis results and utilities for mechanism structural analysis.

This module provides data classes for representing the results of various structural analyses on mechanisms.

class pylinkage.assur.analysis.MobilityResult(degree_of_freedom: int, num_links: int, num_joints: int, num_1dof_joints: int = 0, num_2dof_joints: int = 0, num_ground_joints: int = 0, num_driver_joints: int = 0, is_determinate: bool = False, is_overconstrained: bool = False, is_underconstrained: bool = False, warnings: list[str] = <factory>)

Bases: object

Result of mobility analysis for a mechanism.

Mobility analysis uses Gruebler’s equation (or Chebyshev-Gruebler-Kutzbach) to determine the degree of freedom of a planar mechanism:

DOF = 3(n - 1) - 2*j1 - j2

where: - n = number of links (including ground) - j1 = number of 1-DOF joints (revolute, prismatic) - j2 = number of 2-DOF joints (roll-slide, cam-follower)

Variables:
  • degree_of_freedom (int) – Computed DOF of the mechanism.

  • num_links (int) – Number of links in the mechanism.

  • num_joints (int) – Total number of joints.

  • num_1dof_joints (int) – Number of 1-DOF joints (revolute, prismatic).

  • num_2dof_joints (int) – Number of 2-DOF joints.

  • num_ground_joints (int) – Number of joints fixed to ground.

  • num_driver_joints (int) – Number of driver/motor joints.

  • is_determinate (bool) – True if DOF equals number of drivers.

  • is_overconstrained (bool) – True if DOF < 0 (statically indeterminate).

  • is_underconstrained (bool) – True if DOF > number of drivers.

  • warnings (list[str]) – List of warning messages from analysis.

degree_of_freedom: int
is_determinate: bool = False
is_overconstrained: bool = False
is_underconstrained: bool = False
property is_valid_mechanism: bool

Check if this represents a valid, solvable mechanism.

Returns:

True if mechanism is determinate and not overconstrained.

property mobility_status: str

Get a human-readable status string.

Returns:

String describing the mobility status.

num_1dof_joints: int = 0
num_2dof_joints: int = 0
num_driver_joints: int = 0
num_ground_joints: int = 0
num_joints: int
warnings: list[str]
class pylinkage.assur.analysis.StructuralAnalysis(mobility: MobilityResult, num_assur_groups: int = 0, group_signatures: list[str] = <factory>, validation_messages: list[str] = <factory>)

Bases: object

Complete structural analysis of a mechanism.

Combines decomposition and mobility analysis into a single result.

Variables:
  • mobility (pylinkage.assur.analysis.MobilityResult) – Mobility analysis result.

  • num_assur_groups (int) – Number of Assur groups in decomposition.

  • group_signatures (list[str]) – Joint signatures of each group (e.g., [“RRR”, “RRR”]).

  • validation_messages (list[str]) – Messages from structural validation.

  • is_valid – True if no validation issues.

group_signatures: list[str]
property is_valid: bool

Check if the structure is valid.

Returns:

True if no validation messages and mobility is valid.

mobility: MobilityResult
num_assur_groups: int = 0
validation_messages: list[str]

pylinkage.assur.assur_mechanism module

AssurMechanism: Wrapper providing Assur analysis for mechanisms.

This module provides the AssurMechanism class, which wraps a Mechanism and adds formal Assur group analysis capabilities.

The AssurMechanism class represents the key design principle of the assur module: Assur groups define logical/structural properties, not behaviors. Solving and simulation are delegated to the Mechanism and solver modules.

Example

>>> from pylinkage.mechanism import Mechanism
>>> from pylinkage.assur import AssurMechanism
>>>
>>> mechanism = Mechanism(joints=[...], links=[...])
>>> assur = AssurMechanism(mechanism)
>>>
>>> # Access formal properties
>>> print(f"DOF: {assur.degree_of_freedom}")
>>> print(f"Groups: {[g.joint_signature for g in assur.assur_groups]}")
>>>
>>> # Simulation still works via delegation
>>> for positions in assur.step():
...     print(positions)
class pylinkage.assur.assur_mechanism.AssurMechanism(mechanism: Mechanism, _decomposition: DecompositionResult | None = None, _graph: LinkageGraph | None = None, _dimensions: Dimensions | None = None)

Bases: object

Wrapper that adds Assur group analysis to a Mechanism.

This class provides formal kinematic analysis capabilities without modifying the underlying Mechanism’s simulation behavior. It embodies the design principle that Assur groups define logical properties (structure, classification, constraints) rather than behaviors.

The AssurMechanism: - Wraps an existing Mechanism instance - Computes and caches Assur group decomposition - Provides structural analysis properties (DOF, groups, validation) - Delegates simulation to the underlying Mechanism

Variables:

mechanism (Mechanism) – The wrapped Mechanism instance.

Properties:

decomposition: Cached DecompositionResult. assur_groups: List of AssurGroup objects in solving order. degree_of_freedom: Computed DOF of the mechanism.

analyze() DecompositionResult

Force recomputation of the decomposition.

This clears the cached decomposition and graph, then recomputes the decomposition from the current mechanism state.

Returns:

The newly computed DecompositionResult.

property assur_groups: list[AssurGroup]

Return the Assur groups in solving order.

Returns:

List of AssurGroup instances representing the structural decomposition of the mechanism.

property decomposition: DecompositionResult

Get or compute the Assur group decomposition.

The decomposition is computed once and cached. Use analyze() to force recomputation.

Returns:

DecompositionResult with groups in solving order.

property degree_of_freedom: int

Compute the degree of freedom using Gruebler’s equation.

For planar mechanisms: DOF = 3(n - 1) - 2*j1 - j2

where: - n = number of links (including ground) - j1 = number of 1-DOF joints (revolute, prismatic) - j2 = number of 2-DOF joints

Returns:

The computed degree of freedom.

classmethod from_graph(graph: LinkageGraph, dimensions: Dimensions) AssurMechanism

Create AssurMechanism from a LinkageGraph.

Converts the graph to a Mechanism and wraps it.

Parameters:
  • graph – The LinkageGraph to convert and wrap.

  • dimensions – The dimensions (positions, distances, angles) for the graph.

Returns:

AssurMechanism instance with the converted mechanism.

classmethod from_mechanism(mechanism: Mechanism) AssurMechanism

Create AssurMechanism wrapper from an existing Mechanism.

Parameters:

mechanism – The Mechanism to wrap.

Returns:

AssurMechanism instance wrapping the mechanism.

get_joint_positions() list[Coord]

Get current positions of all joints.

Delegates to the underlying mechanism.

Returns:

List of (x, y) positions for each joint.

property graph: LinkageGraph

Get or compute the LinkageGraph representation.

If created from a graph, returns that graph. Otherwise, converts the mechanism to a graph.

is_valid() bool

Check if the mechanism structure is valid.

Returns:

True if validate() returns no messages.

mechanism: Mechanism
property num_assur_groups: int

Return the number of Assur groups.

property num_driver_nodes: int

Return the number of driver nodes.

property num_ground_nodes: int

Return the number of ground nodes.

reset() None

Reset the mechanism to initial state.

Delegates to the underlying mechanism.

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

Simulate one full rotation cycle.

Delegates to the underlying mechanism’s step() method.

Parameters:

dt – Time step (default 1.0).

Yields:

Tuple of (x, y) positions for each joint at each step.

validate() list[str]

Validate the mechanism structure.

Checks for common structural issues: - No ground nodes - No driver nodes - Nodes not accounted for in groups

Returns:

List of warning/error messages. Empty if valid.

pylinkage.assur.decomposition module

Assur group decomposition algorithm (topology only).

This module provides algorithms to decompose a linkage graph into Assur groups, which enables systematic kinematic analysis.

The decomposition algorithm: 1. Identifies ground (frame) nodes 2. Identifies driver (input) nodes 3. Iteratively finds solvable Assur groups where all anchors are known 4. Returns groups in solving order

IMPORTANT: This module provides structural/topological decomposition only. Assur groups are pure topology. For solving kinematics, use pylinkage.solver.solve.solve_decomposition() with a Dimensions object.

class pylinkage.assur.decomposition.DecompositionResult(ground: list[str] = <factory>, drivers: list[str] = <factory>, groups: list[AssurGroup] = <factory>, graph: LinkageGraph | None = None)

Bases: object

Result of Assur group decomposition.

Contains the ordered sequence of structural elements needed to solve the linkage kinematics.

Variables:

Example

>>> result = decompose_assur_groups(graph)
>>> print(f"Ground: {result.ground}")
>>> print(f"Drivers: {result.drivers}")
>>> for group in result.groups:
...     print(f"  {group.joint_signature}: {group.internal_nodes}")
all_nodes_in_order() list[str]

Return all node IDs in solving order.

Returns:

Ground nodes, then driver nodes, then internal nodes from each group in order.

drivers: list[str]
graph: LinkageGraph | None = None
ground: list[str]
groups: list[AssurGroup]
solve_order() list[str | AssurGroup]

Return the complete solving order (drivers + groups).

Returns:

List of driver node IDs followed by AssurGroup objects, in the order they should be solved.

pylinkage.assur.decomposition.decompose_assur_groups(graph: LinkageGraph) DecompositionResult

Decompose a linkage graph into Assur groups.

This algorithm identifies: 1. Ground nodes (fixed frame points) 2. Driver nodes (inputs/motors) 3. Assur groups in solvable order

The algorithm works iteratively: - Start with ground and drivers as “known” - Find groups that can be solved (all anchors known) - Add solved group nodes to known set - Repeat until all nodes are assigned

Parameters:

graph – The linkage graph to decompose.

Returns:

DecompositionResult with groups in solving order.

Raises:

ValueError – If decomposition fails (e.g., underconstrained mechanism).

Example

>>> graph = LinkageGraph(name="Four-bar")
>>> # ... add nodes and edges ...
>>> result = decompose_assur_groups(graph)
>>> print(f"Found {len(result.groups)} Assur groups")
pylinkage.assur.decomposition.validate_decomposition(result: DecompositionResult) list[str]

Validate a decomposition result.

Checks for common issues that might indicate an invalid decomposition.

Parameters:

result – The DecompositionResult to validate.

Returns:

List of warning/error messages (empty if valid).

pylinkage.assur.graph module

Graph-based representation of planar linkages (topology only).

This module provides data structures for representing linkages as graphs where nodes are joints and edges are rigid links. This representation enables Assur group decomposition and provides an alternative syntax for defining linkages.

This is a pure topological representation - dimensional data (positions, distances, angles) is stored separately in a Dimensions object.

class pylinkage.assur.graph.Edge(id: str, source: str, target: str, body_id: str | None = None)

Bases: object

A rigid link between two joints (topology only).

Edges represent rigid bodies connecting joints. Distance constraints are stored separately in a Dimensions object.

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

  • source (str) – ID of the source node.

  • target (str) – ID of the target node.

  • body_id (str | None) – Optional tag for grouping edges that belong to the same rigid body.

Example

>>> edge = Edge("AB", source="A", target="B")
body_id: str | None = None
connects(node_id: str) bool

Check if this edge connects to the given node.

id: str
other_node(node_id: str) str

Get the node on the other end of this edge.

Parameters:

node_id – One endpoint of this edge.

Returns:

The ID of the other endpoint.

Raises:

ValueError – If node_id is not an endpoint of this edge.

source: str
target: str
class pylinkage.assur.graph.LinkageGraph(nodes: dict[str, ~pylinkage.assur.graph.Node]=<factory>, edges: dict[str, ~pylinkage.assur.graph.Edge]=<factory>, name: str = '', _adjacency: dict[str, list[str]]=<factory>)

Bases: object

Graph representation of a planar linkage (topology only).

A linkage is represented as a graph where: - Nodes are joints (revolute R or prismatic P) - Edges are rigid links

This is a pure topological representation - dimensional data (positions, distances, angles) is stored separately in a Dimensions object.

This representation enables: - Assur group decomposition - Alternative linkage definition syntax - Graph algorithms for analysis

Variables:

Example

>>> graph = LinkageGraph(name="Four-bar")
>>> graph.add_node(Node("A", role=NodeRole.GROUND))
>>> graph.add_node(Node("B", role=NodeRole.DRIVER))
>>> graph.add_edge(Edge("AB", source="A", target="B"))
>>> graph.neighbors("A")
['B']
add_edge(edge: Edge) None

Add an edge to the graph.

Parameters:

edge – The Edge to add.

Raises:

ValueError – If an edge with the same ID already exists, or if source/target nodes don’t exist.

add_node(node: Node) None

Add a node to the graph.

Parameters:

node – The Node to add.

Raises:

ValueError – If a node with the same ID already exists.

copy() LinkageGraph

Create a deep copy of the graph.

Returns:

A new LinkageGraph with copied nodes and edges.

degree(node_id: str) int

Get the degree (number of connections) of a node.

Parameters:

node_id – The node ID.

Returns:

Number of edges connected to the node.

driven_nodes() list[Node]

Get all driven nodes (Assur group members).

driver_nodes() list[Node]

Get all driver/input nodes.

edges: dict[str, Edge]
get_edge_between(node1: str, node2: str) Edge | None

Get the edge connecting two nodes, if any.

Parameters:
  • node1 – First node ID.

  • node2 – Second node ID.

Returns:

The Edge connecting the nodes, or None if not connected.

get_edges_for_node(node_id: str) list[Edge]

Get all edges connected to a node.

Parameters:

node_id – The node ID.

Returns:

List of Edge objects connected to the node.

ground_nodes() list[Node]

Get all ground/frame nodes.

name: str = ''
neighbors(node_id: str) list[str]

Get all nodes connected to the given node.

Parameters:

node_id – The node to find neighbors of.

Returns:

List of node IDs connected to the given node.

nodes: dict[str, Node]
remove_edge(edge_id: str) Edge

Remove an edge from the graph.

Parameters:

edge_id – ID of the edge to remove.

Returns:

The removed Edge.

Raises:

KeyError – If edge not found.

remove_node(node_id: str) Node

Remove a node and all its edges from the graph.

Parameters:

node_id – ID of the node to remove.

Returns:

The removed Node.

Raises:

KeyError – If node not found.

class pylinkage.assur.graph.Node(id: str, joint_type: JointType = JointType.REVOLUTE, role: NodeRole = NodeRole.DRIVEN, name: str | None = None)

Bases: object

A joint in the linkage graph (topology only).

Represents the topological aspects of a joint. Dimensional data (position, angles) is stored separately in a Dimensions object.

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

  • joint_type (pylinkage._types.JointType) – The kinematic joint type (REVOLUTE or PRISMATIC).

  • role (pylinkage._types.NodeRole) – The role in the mechanism (GROUND, DRIVER, or DRIVEN).

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

Example

>>> node = Node("A", role=NodeRole.GROUND)
>>> node.joint_type
<JointType.REVOLUTE: 1>
id: str
joint_type: JointType = 1
name: str | None = None
role: NodeRole = 2

pylinkage.assur.groups module

Assur group classes for kinematic analysis (topology only).

This module provides the base class and implementations for Assur groups, which are the fundamental structural units in planar mechanism decomposition.

An Assur group is a kinematic substructure with DOF = 0 when attached to the frame (or previously solved groups). It represents the minimal structural unit that can be identified and analyzed independently.

Groups are parameterized by signature rather than having one class per joint-type combination. This scales to triads (64 combinations) and tetrads (512 combinations) without an explosion of classes.

IMPORTANT: These classes are pure topological data structures. They do NOT contain solving behavior or dimensional data. Use pylinkage.solver.solve.solve_group() with a Dimensions object to compute positions.

class pylinkage.assur.groups.AssurGroup(internal_nodes: tuple[str, ...]=<factory>, anchor_nodes: tuple[str, ...]=<factory>, internal_edges: tuple[str, ...]=<factory>)

Bases: ABC

Base class for Assur groups (topology only).

An Assur group is a kinematic substructure with DOF = 0 when attached to the frame (or previously solved groups). It represents the minimal structural unit that can be identified independently.

This is a pure topological data class. Dimensional data (distances, angles) is stored separately in a Dimensions object. Use pylinkage.solver.solve.solve_group() to compute positions.

Variables:
  • internal_nodes (tuple[str, ...]) – Node IDs that are part of this group.

  • anchor_nodes (tuple[str, ...]) – Node IDs that connect this group to the rest.

  • internal_edges (tuple[str, ...]) – Edge IDs within this group.

Subclasses must implement:
  • group_class: The class k of this Assur group

  • joint_signature: String like “RRR”, “RRP”, etc.

  • can_form: Check if given elements can form this group type

anchor_nodes: tuple[str, ...]
abstractmethod classmethod can_form(internal_node_ids: list[str], anchor_node_ids: list[str], graph: LinkageGraph) bool

Check if the given nodes can form this group type.

Parameters:
  • internal_node_ids – Candidate internal nodes.

  • anchor_node_ids – Candidate anchor nodes.

  • graph – The linkage graph to check against.

Returns:

True if these elements can form this Assur group type.

abstract property group_class: int

Return the class (k) of this Assur group.

Class I groups (dyads) have k=1.

internal_edges: tuple[str, ...]
internal_nodes: tuple[str, ...]
abstract property joint_signature: str

Return the joint type signature (e.g., ‘RRR’, ‘RRP’).

The signature describes the joint types in the group: - R = Revolute (pin joint) - P = Prismatic (slider joint)

property solver_category: str

Return the solving geometry category.

The solver dispatches based on this, not on the specific class. Categories for dyads (group_class=1): - “circle_circle”: All-revolute constraints (e.g., RRR) - “circle_line”: Mixed revolute + prismatic (e.g., RRP, RPR, PRR) - “line_line”: All-prismatic constraints (e.g., PP, PPR) Categories for higher-order groups (group_class>=2): - “newton_raphson”: Simultaneous constraint solving

class pylinkage.assur.groups.Dyad(internal_nodes: tuple[str, ...]=<factory>, anchor_nodes: tuple[str, ...]=<factory>, internal_edges: tuple[str, ...]=<factory>, _signature: str = 'RRR', line_node1: str | None = None, line_node2: str | None = None, line2_node1: str | None = None, line2_node2: str | None = None)

Bases: AssurGroup

Class I Assur group — parameterized by joint signature.

A dyad consists of 2 binary links, 3 joints, and 1 internal node. The signature (e.g., “RRR”, “RRP”, “RPR”) determines the joint types and solving geometry.

Structure:

anchor_0 ----link_0---- internal_0 ----link_1---- anchor_1
(joint_0)               (joint_1)                 (joint_2)

For prismatic variants, additional line-defining nodes are stored:

  • RRP/RPR/PRR: one line defined by (line_node1, line_node2)

  • PP: two lines defined by (line_node1, line_node2) and (line2_node1, line2_node2)

Variables:
  • _signature (str) – Canonical joint signature string (e.g., “RRR”).

  • line_node1 (str | None) – First node defining prismatic line constraint.

  • line_node2 (str | None) – Second node defining prismatic line constraint.

  • line2_node1 (str | None) – First node of second line (PP only).

  • line2_node2 (str | None) – Second node of second line (PP only).

classmethod can_form(internal_node_ids: list[str], anchor_node_ids: list[str], graph: LinkageGraph, *, signature: str | None = None) bool

Check if nodes can form a dyad with the given (or any) signature.

If signature is provided, checks against that specific joint pattern. Otherwise, checks if any valid dyad can be formed.

Parameters:
  • internal_node_ids – Candidate internal nodes (must be exactly 1).

  • anchor_node_ids – Candidate anchor nodes.

  • graph – The linkage graph to check against.

  • signature – Optional specific signature to check (e.g., “RRR”).

Returns:

True if these elements can form a dyad.

property group_class: int

Return the class (k) of this Assur group.

Class I groups (dyads) have k=1.

property joint_signature: str

Return the joint type signature (e.g., ‘RRR’, ‘RRP’).

The signature describes the joint types in the group: - R = Revolute (pin joint) - P = Prismatic (slider joint)

line2_node1: str | None = None
line2_node2: str | None = None
line_node1: str | None = None
line_node2: str | None = None
class pylinkage.assur.groups.DyadPP(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.groups.DyadPRR(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.groups.DyadRPR(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.groups.DyadRRP(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.groups.DyadRRR(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.groups.Triad(internal_nodes: tuple[str, ...]=<factory>, anchor_nodes: tuple[str, ...]=<factory>, internal_edges: tuple[str, ...]=<factory>, _signature: str = 'RRRRRR', edge_map: dict[str, tuple[str, str]]=<factory>)

Bases: AssurGroup

Class II Assur group — parameterized by joint signature.

A triad consists of 4 links, 6 joints, and 2 internal nodes connected to 3 anchor nodes. The signature has 6 characters (e.g., “RRRRRR”).

Triads require solving 3 simultaneous constraints (Newton-Raphson), unlike dyads which only intersect 2 constraints.

Structure (one possible arrangement):

anchor_0 ------- internal_0 ------- anchor_1
                     |
                 internal_1
                     |
                 anchor_2
Variables:
  • _signature (str) – Canonical joint signature string (6 chars).

  • edge_map (dict[str, tuple[str, str]]) – Maps edge ID → (node_a, node_b) for all internal edges. The solver uses this to know which pair of nodes each distance constraint connects.

classmethod can_form(internal_node_ids: list[str], anchor_node_ids: list[str], graph: LinkageGraph) bool

Check if nodes can form a triad.

Requires: - Exactly 2 internal nodes - At least 3 anchor nodes - Edges connecting internals to anchors (4 total)

edge_map: dict[str, tuple[str, str]]
property group_class: int

Return the class (k) of this Assur group.

Class I groups (dyads) have k=1.

property joint_signature: str

Return the joint type signature (e.g., ‘RRR’, ‘RRP’).

The signature describes the joint types in the group: - R = Revolute (pin joint) - P = Prismatic (slider joint)

pylinkage.assur.groups.identify_dyad_type(internal_node_ids: list[str], anchor_node_ids: list[str], graph: LinkageGraph) type[AssurGroup] | None

Identify which dyad type can be formed from the given elements.

Tries each registered dyad type in order and returns the first one that matches.

Parameters:
  • internal_node_ids – Candidate internal nodes.

  • anchor_node_ids – Candidate anchor nodes.

  • graph – The linkage graph.

Returns:

The matching AssurGroup subclass, or None if no match found.

pylinkage.assur.groups.identify_group_type(internal_node_ids: list[str], anchor_node_ids: list[str], graph: LinkageGraph) Dyad | Triad | None

Identify which Assur group can be formed from the given elements.

Tries dyad first (simpler), then triad. Returns an instance with the matching signature, or None.

Parameters:
  • internal_node_ids – Candidate internal nodes.

  • anchor_node_ids – Candidate anchor nodes.

  • graph – The linkage graph.

Returns:

A Dyad or Triad instance, or None if no match found.

pylinkage.assur.hypergraph_conversion module

Conversion between Assur graph and hypergraph representations (topology only).

This module provides functions to convert between the Assur LinkageGraph and the hypergraph HypergraphLinkage representations.

These conversions are pure topology - no dimensional data is transferred. Dimensions are handled separately and passed through to conversion functions that need them (e.g., graph_to_mechanism, to_mechanism).

Since assur is built on top of hypergraph (as a formal kinematic theory on top of abstract graph math), these conversions live in the assur module.

pylinkage.assur.hypergraph_conversion.from_hypergraph(hypergraph: HypergraphLinkage) LinkageGraph

Convert a HypergraphLinkage to an Assur LinkageGraph (topology only).

This converts the hypergraph to the simpler graph representation used by the Assur decomposition system. Hyperedges are expanded to regular edges.

Both representations are pure topology - no dimensional data is transferred. To convert with dimensions, use this function then pass the same Dimensions object to graph_to_mechanism.

Parameters:

hypergraph – The HypergraphLinkage to convert (topology only).

Returns:

An Assur LinkageGraph suitable for decomposition and analysis.

Example

>>> hg = HypergraphLinkage(name="Four-bar")
>>> # ... add nodes and edges ...
>>> assur_graph = from_hypergraph(hg)
pylinkage.assur.hypergraph_conversion.to_hypergraph(assur_graph: LinkageGraph) HypergraphLinkage

Convert an Assur LinkageGraph to a HypergraphLinkage (topology only).

This converts from the Assur graph representation to the more abstract hypergraph representation. Edges with the same body_id are grouped into hyperedges.

Both representations are pure topology - no dimensional data is transferred.

Parameters:

assur_graph – The Assur LinkageGraph to convert (topology only).

Returns:

A HypergraphLinkage representation (topology only).

Example

>>> assur_graph = LinkageGraph(name="Four-bar")
>>> # ... add nodes and edges ...
>>> hg = to_hypergraph(assur_graph)

pylinkage.assur.mechanism_conversion module

Direct conversion between LinkageGraph and Mechanism.

This module provides direct conversion between the Assur graph representation and the mechanism model, bypassing the legacy Linkage class.

This is the preferred conversion path for new code: - LinkageGraph is the formal kinematic graph representation (Assur theory) - Dimensions holds the geometric data (positions, distances, angles) - Mechanism is the concrete simulation model with Links + Joints

For backward compatibility with legacy Linkage, use conversion.py instead.

pylinkage.assur.mechanism_conversion.graph_to_mechanism(graph: LinkageGraph, dimensions: Dimensions) Mechanism

Convert a LinkageGraph and Dimensions directly to a Mechanism.

This converts the Assur graph and dimensions to the mechanism model without going through the legacy Linkage class. This is the preferred conversion path for new code.

The conversion: 1. Decomposes the graph into Assur groups 2. Creates appropriate Joint and Link instances for each element 3. Constructs a Mechanism with correct topology

Parameters:
  • graph – The LinkageGraph defining topology.

  • dimensions – The Dimensions providing positions, distances, angles.

Returns:

A Mechanism instance ready for simulation.

Raises:
  • ValueError – If the graph cannot be decomposed into valid Assur groups.

  • NotImplementedError – If an unsupported Assur group type is encountered.

Example

>>> graph = LinkageGraph(name="Four-bar")
>>> graph.add_node(Node("A", role=NodeRole.GROUND))
>>> dims = Dimensions(node_positions={"A": (0, 0)}, ...)
>>> mechanism = graph_to_mechanism(graph, dims)
>>> for positions in mechanism.step():
...     print(positions)
pylinkage.assur.mechanism_conversion.mechanism_to_graph(mechanism: Mechanism) tuple[LinkageGraph, Dimensions]

Convert a Mechanism to a LinkageGraph and Dimensions.

This converts an existing Mechanism to the Assur graph representation for analysis, decomposition, or visualization.

Parameters:

mechanism – The Mechanism to convert.

Returns:

A tuple of (LinkageGraph, Dimensions).

Example

>>> mechanism = Mechanism(joints=[...], links=[...])
>>> graph, dims = mechanism_to_graph(mechanism)
>>> decomposition = decompose_assur_groups(graph)

pylinkage.assur.serialization module

Serialization support for graph representations (topology only).

This module provides functions to serialize and deserialize LinkageGraph objects to/from dictionaries and JSON files.

Note: This module only handles topological data. For dimensional data, use the serialization functions in pylinkage.dimensions.

pylinkage.assur.serialization.edge_from_dict(data: dict[str, Any]) Edge

Create an Edge from a dictionary.

Parameters:

data – Dictionary containing edge data.

Returns:

A new Edge instance.

pylinkage.assur.serialization.edge_to_dict(edge: Edge) dict[str, Any]

Convert an Edge to a dictionary (topology only).

Parameters:

edge – The Edge to convert.

Returns:

Dictionary representation of the edge.

pylinkage.assur.serialization.graph_from_dict(data: dict[str, Any]) LinkageGraph

Create a LinkageGraph from a dictionary.

Parameters:

data – Dictionary containing graph data (as produced by graph_to_dict).

Returns:

A new LinkageGraph instance.

Example

>>> data = graph_to_dict(original_graph)
>>> restored_graph = graph_from_dict(data)
pylinkage.assur.serialization.graph_from_json(path: str | Path) LinkageGraph

Load a LinkageGraph from a JSON file.

Parameters:

path – Path to the JSON file.

Returns:

A new LinkageGraph instance.

Example

>>> graph_to_json(graph, "my_linkage.json")
>>> restored = graph_from_json("my_linkage.json")
pylinkage.assur.serialization.graph_to_dict(graph: LinkageGraph) dict[str, Any]

Convert a LinkageGraph to a dictionary.

The dictionary format is suitable for JSON serialization and can be used to reconstruct the graph later.

Parameters:

graph – The LinkageGraph to convert.

Returns:

Dictionary representation of the graph.

Example

>>> data = graph_to_dict(graph)
>>> json.dumps(data, indent=2)
pylinkage.assur.serialization.graph_to_json(graph: LinkageGraph, path: str | Path) None

Save a LinkageGraph to a JSON file.

Parameters:
  • graph – The LinkageGraph to save.

  • path – Path to the output JSON file.

Example

>>> graph_to_json(graph, "my_linkage.json")
>>> restored = graph_from_json("my_linkage.json")
pylinkage.assur.serialization.node_from_dict(data: dict[str, Any]) Node

Create a Node from a dictionary.

Parameters:

data – Dictionary containing node data.

Returns:

A new Node instance.

pylinkage.assur.serialization.node_to_dict(node: Node) dict[str, Any]

Convert a Node to a dictionary (topology only).

Parameters:

node – The Node to convert.

Returns:

Dictionary representation of the node.

pylinkage.assur.signature module

Assur group signature parsing and hypergraph generation.

This module provides tools for defining Assur groups using formal kinematic syntax (e.g., “RRR”, “RPR”, “PRR”) and generating pure topological hypergraphs from these signatures.

The generated hypergraphs contain only structure (nodes, edges, joint types) without distance constraints - they are mathematical objects representing topology only.

Example

>>> from pylinkage.assur import parse_signature, signature_to_hypergraph
>>>
>>> # Parse formal syntax
>>> sig = parse_signature("RRR")
>>> print(sig.joint_signature)
'RRR'
>>>
>>> # Generate pure topology
>>> graph = signature_to_hypergraph(sig)
>>> print(list(graph.nodes.keys()))
['anchor_0', 'anchor_1', 'internal_0']
class pylinkage.assur.signature.AssurGroupClass(value)

Bases: IntEnum

Classification of Assur groups by structural complexity.

Assur groups are classified by their “class” (k), which corresponds to the number of internal nodes and structural complexity:

  • Class I (DYAD): 2 links, 3 joints, 1 internal node

  • Class II (TRIAD): 4 links, 6 joints, 2 internal nodes

  • Class III (TETRAD): 6 links, 9 joints, 3 internal nodes

Higher classes follow the pattern: k links = 2*class, joints = 3*class.

DYAD = 1
TETRAD = 3
TRIAD = 2
class pylinkage.assur.signature.AssurSignature(joints: tuple[JointType, ...], group_class: AssurGroupClass, raw_string: str = '')

Bases: object

Parsed representation of an Assur group signature.

This is an immutable, hashable representation of the joint sequence defining an Assur group’s topology.

Variables:

Example

>>> sig = parse_signature("RRR")
>>> sig.joints
(JointType.REVOLUTE, JointType.REVOLUTE, JointType.REVOLUTE)
>>> sig.group_class
<AssurGroupClass.DYAD: 1>
>>> sig.canonical_form
'RRR'
property canonical_form: str

Return canonical string representation (e.g., ‘RRR’, ‘RRP’).

Uses ‘P’ for prismatic joints (preferred over ‘T’).

group_class: AssurGroupClass
joints: tuple[JointType, ...]
property num_anchor_nodes: int

Number of anchor nodes (connections to known positions).

Dyad: 2, Triad: 3, Tetrad: 4, etc.

property num_internal_nodes: int

Number of internal (driven) nodes in this group.

Dyad: 1, Triad: 2, Tetrad: 3, etc.

Number of binary links in this group.

Dyad: 2, Triad: 4, Tetrad: 6, etc.

raw_string: str = ''
pylinkage.assur.signature.isomer_to_canonical(signature: str) str

Convert an isomer signature to canonical form.

Maps extended T/_ notation to standard R/P notation by treating T and _ as parts of a single prismatic joint.

Parameters:

signature – Isomer signature (e.g., “RT_R”, “T_R_T”).

Returns:

Canonical signature (e.g., “RPR”, “PPR”).

Example

>>> isomer_to_canonical("RT_R")
'RPR'
>>> isomer_to_canonical("T_R_T")
'PRP'
>>> isomer_to_canonical("RRR")
'RRR'
pylinkage.assur.signature.parse_isomer_signature(signature: str) tuple[str, tuple[str, ...]]

Parse an extended isomer signature with T/_ notation.

The extended notation distinguishes between: - R = Revolute joint - T = Slider (translating element of prismatic pair) - _ = Guide (rail/slot of prismatic pair)

This allows representing all 12 dyadic isomers explicitly.

Parameters:

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

Returns:

Tuple of (normalized_signature, joint_roles) where joint_roles is a tuple of role strings (“revolute”, “slider”, “guide”).

Raises:

ValueError – If signature contains invalid characters.

Example

>>> sig, roles = parse_isomer_signature("RT_R")
>>> sig
'RT_R'
>>> roles
('revolute', 'slider', 'guide', 'revolute')
>>> sig, roles = parse_isomer_signature("T_R_T")
>>> roles
('slider', 'guide', 'revolute', 'guide', 'slider')
pylinkage.assur.signature.parse_signature(signature: str) AssurSignature

Parse a signature string into an AssurSignature.

The signature describes the joint types in the Assur group: - R = Revolute (pin joint) - P or T = Prismatic (slider joint) - both accepted, P preferred - _ = Optional separator for readability

Parameters:

signature – The signature string (e.g., “RRR”, “R_P_R”, “RPR”).

Returns:

Parsed AssurSignature.

Raises:

ValueError – If signature is empty, contains invalid characters, or has wrong joint count for any known group class.

Example

>>> sig = parse_signature("RRR")
>>> sig.joints
(JointType.REVOLUTE, JointType.REVOLUTE, JointType.REVOLUTE)
>>> sig = parse_signature("R_P_R")  # With separators
>>> sig.canonical_form
'RPR'
>>> sig = parse_signature("RTR")  # T accepted as alias for P
>>> sig.canonical_form
'RPR'
pylinkage.assur.signature.signature_to_group_class(signature: AssurSignature | str) type[AssurGroup] | None

Get the AssurGroup class corresponding to a signature.

This bridges the formal signature syntax to the existing AssurGroup class hierarchy (DyadRRR, DyadRRP, etc.).

Parameters:

signature – The parsed signature or string to parse.

Returns:

The matching AssurGroup subclass, or None if not implemented.

Example

>>> sig = parse_signature("RRR")
>>> cls = signature_to_group_class(sig)
>>> cls.__name__
'DyadRRR'
>>> cls = signature_to_group_class("RPR")
>>> cls.__name__
'DyadRPR'
pylinkage.assur.signature.signature_to_hypergraph(signature: AssurSignature | str, *, prefix: str = '', name: str | None = None) HypergraphLinkage

Generate a topological hypergraph from an Assur signature.

Creates a pure topological hypergraph with NO distance constraints. All edges have distance=None, representing only structural connections.

The generated graph follows standard naming conventions: - Anchor nodes: “anchor_0”, “anchor_1”, … - Internal nodes: “internal_0”, “internal_1”, … - Edges (links): “link_0”, “link_1”, …

Parameters:
  • signature – Either an AssurSignature or a string to parse.

  • prefix – Optional prefix for all node/edge IDs (useful for assembly).

  • name – Optional name for the hypergraph.

Returns:

HypergraphLinkage with pure topology (no constraints).

Raises:

NotImplementedError – For group classes not yet implemented (triad, tetrad).

Example

>>> graph = signature_to_hypergraph("RRR")
>>> len(graph.nodes)
3  # 2 anchors + 1 internal
>>> len(graph.edges)
2  # 2 links
>>> # With prefix for unique IDs
>>> leg = signature_to_hypergraph("RRR", prefix="leg1_")
>>> "leg1_anchor_0" in leg.nodes
True

Module contents

Assur group decomposition for planar linkages.

This module provides a graph-based representation of linkage mechanisms and tools for Assur group decomposition. It offers an alternative way to define and analyze linkages using formal kinematic theory.

IMPORTANT: This module defines logical/structural properties only. Assur groups are pure data classes without solving behavior. Use pylinkage.solver.solve.solve_group() for position computation.

Key components: - AssurMechanism: Wrapper adding Assur analysis to a Mechanism - LinkageGraph: Graph representation with nodes (joints) and edges (links) - AssurGroup: Base class for Assur groups (structural units) - DyadRRR, DyadRRP: Class I Assur groups (dyads) - decompose_assur_groups: Decomposition algorithm - Conversion functions between graph and Mechanism representations

Example usage:

>>> from pylinkage.mechanism import Mechanism
>>> from pylinkage.assur import AssurMechanism
>>>
>>> # Wrap a mechanism for Assur analysis
>>> mechanism = Mechanism(joints=[...], links=[...])
>>> assur = AssurMechanism(mechanism)
>>>
>>> # Access structural properties
>>> print(f"DOF: {assur.degree_of_freedom}")
>>> print(f"Groups: {[g.joint_signature for g in assur.assur_groups]}")
>>>
>>> # Simulation via delegation
>>> for positions in assur.step():
...     print(positions)
class pylinkage.assur.AssurGroup(internal_nodes: tuple[str, ...]=<factory>, anchor_nodes: tuple[str, ...]=<factory>, internal_edges: tuple[str, ...]=<factory>)

Bases: ABC

Base class for Assur groups (topology only).

An Assur group is a kinematic substructure with DOF = 0 when attached to the frame (or previously solved groups). It represents the minimal structural unit that can be identified independently.

This is a pure topological data class. Dimensional data (distances, angles) is stored separately in a Dimensions object. Use pylinkage.solver.solve.solve_group() to compute positions.

Variables:
  • internal_nodes (tuple[str, ...]) – Node IDs that are part of this group.

  • anchor_nodes (tuple[str, ...]) – Node IDs that connect this group to the rest.

  • internal_edges (tuple[str, ...]) – Edge IDs within this group.

Subclasses must implement:
  • group_class: The class k of this Assur group

  • joint_signature: String like “RRR”, “RRP”, etc.

  • can_form: Check if given elements can form this group type

anchor_nodes: tuple[str, ...]
abstractmethod classmethod can_form(internal_node_ids: list[str], anchor_node_ids: list[str], graph: LinkageGraph) bool

Check if the given nodes can form this group type.

Parameters:
  • internal_node_ids – Candidate internal nodes.

  • anchor_node_ids – Candidate anchor nodes.

  • graph – The linkage graph to check against.

Returns:

True if these elements can form this Assur group type.

abstract property group_class: int

Return the class (k) of this Assur group.

Class I groups (dyads) have k=1.

internal_edges: tuple[str, ...]
internal_nodes: tuple[str, ...]
abstract property joint_signature: str

Return the joint type signature (e.g., ‘RRR’, ‘RRP’).

The signature describes the joint types in the group: - R = Revolute (pin joint) - P = Prismatic (slider joint)

property solver_category: str

Return the solving geometry category.

The solver dispatches based on this, not on the specific class. Categories for dyads (group_class=1): - “circle_circle”: All-revolute constraints (e.g., RRR) - “circle_line”: Mixed revolute + prismatic (e.g., RRP, RPR, PRR) - “line_line”: All-prismatic constraints (e.g., PP, PPR) Categories for higher-order groups (group_class>=2): - “newton_raphson”: Simultaneous constraint solving

class pylinkage.assur.AssurGroupClass(value)

Bases: IntEnum

Classification of Assur groups by structural complexity.

Assur groups are classified by their “class” (k), which corresponds to the number of internal nodes and structural complexity:

  • Class I (DYAD): 2 links, 3 joints, 1 internal node

  • Class II (TRIAD): 4 links, 6 joints, 2 internal nodes

  • Class III (TETRAD): 6 links, 9 joints, 3 internal nodes

Higher classes follow the pattern: k links = 2*class, joints = 3*class.

DYAD = 1
TETRAD = 3
TRIAD = 2
class pylinkage.assur.AssurMechanism(mechanism: Mechanism, _decomposition: DecompositionResult | None = None, _graph: LinkageGraph | None = None, _dimensions: Dimensions | None = None)

Bases: object

Wrapper that adds Assur group analysis to a Mechanism.

This class provides formal kinematic analysis capabilities without modifying the underlying Mechanism’s simulation behavior. It embodies the design principle that Assur groups define logical properties (structure, classification, constraints) rather than behaviors.

The AssurMechanism: - Wraps an existing Mechanism instance - Computes and caches Assur group decomposition - Provides structural analysis properties (DOF, groups, validation) - Delegates simulation to the underlying Mechanism

Variables:

mechanism (Mechanism) – The wrapped Mechanism instance.

Properties:

decomposition: Cached DecompositionResult. assur_groups: List of AssurGroup objects in solving order. degree_of_freedom: Computed DOF of the mechanism.

analyze() DecompositionResult

Force recomputation of the decomposition.

This clears the cached decomposition and graph, then recomputes the decomposition from the current mechanism state.

Returns:

The newly computed DecompositionResult.

property assur_groups: list[AssurGroup]

Return the Assur groups in solving order.

Returns:

List of AssurGroup instances representing the structural decomposition of the mechanism.

property decomposition: DecompositionResult

Get or compute the Assur group decomposition.

The decomposition is computed once and cached. Use analyze() to force recomputation.

Returns:

DecompositionResult with groups in solving order.

property degree_of_freedom: int

Compute the degree of freedom using Gruebler’s equation.

For planar mechanisms: DOF = 3(n - 1) - 2*j1 - j2

where: - n = number of links (including ground) - j1 = number of 1-DOF joints (revolute, prismatic) - j2 = number of 2-DOF joints

Returns:

The computed degree of freedom.

classmethod from_graph(graph: LinkageGraph, dimensions: Dimensions) AssurMechanism

Create AssurMechanism from a LinkageGraph.

Converts the graph to a Mechanism and wraps it.

Parameters:
  • graph – The LinkageGraph to convert and wrap.

  • dimensions – The dimensions (positions, distances, angles) for the graph.

Returns:

AssurMechanism instance with the converted mechanism.

classmethod from_mechanism(mechanism: Mechanism) AssurMechanism

Create AssurMechanism wrapper from an existing Mechanism.

Parameters:

mechanism – The Mechanism to wrap.

Returns:

AssurMechanism instance wrapping the mechanism.

get_joint_positions() list[Coord]

Get current positions of all joints.

Delegates to the underlying mechanism.

Returns:

List of (x, y) positions for each joint.

property graph: LinkageGraph

Get or compute the LinkageGraph representation.

If created from a graph, returns that graph. Otherwise, converts the mechanism to a graph.

is_valid() bool

Check if the mechanism structure is valid.

Returns:

True if validate() returns no messages.

mechanism: Mechanism
property num_assur_groups: int

Return the number of Assur groups.

property num_driver_nodes: int

Return the number of driver nodes.

property num_ground_nodes: int

Return the number of ground nodes.

reset() None

Reset the mechanism to initial state.

Delegates to the underlying mechanism.

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

Simulate one full rotation cycle.

Delegates to the underlying mechanism’s step() method.

Parameters:

dt – Time step (default 1.0).

Yields:

Tuple of (x, y) positions for each joint at each step.

validate() list[str]

Validate the mechanism structure.

Checks for common structural issues: - No ground nodes - No driver nodes - Nodes not accounted for in groups

Returns:

List of warning/error messages. Empty if valid.

class pylinkage.assur.AssurSignature(joints: tuple[JointType, ...], group_class: AssurGroupClass, raw_string: str = '')

Bases: object

Parsed representation of an Assur group signature.

This is an immutable, hashable representation of the joint sequence defining an Assur group’s topology.

Variables:

Example

>>> sig = parse_signature("RRR")
>>> sig.joints
(JointType.REVOLUTE, JointType.REVOLUTE, JointType.REVOLUTE)
>>> sig.group_class
<AssurGroupClass.DYAD: 1>
>>> sig.canonical_form
'RRR'
property canonical_form: str

Return canonical string representation (e.g., ‘RRR’, ‘RRP’).

Uses ‘P’ for prismatic joints (preferred over ‘T’).

group_class: AssurGroupClass
joints: tuple[JointType, ...]
property num_anchor_nodes: int

Number of anchor nodes (connections to known positions).

Dyad: 2, Triad: 3, Tetrad: 4, etc.

property num_internal_nodes: int

Number of internal (driven) nodes in this group.

Dyad: 1, Triad: 2, Tetrad: 3, etc.

property num_links: int

Number of binary links in this group.

Dyad: 2, Triad: 4, Tetrad: 6, etc.

raw_string: str = ''
class pylinkage.assur.DecompositionResult(ground: list[str] = <factory>, drivers: list[str] = <factory>, groups: list[AssurGroup] = <factory>, graph: LinkageGraph | None = None)

Bases: object

Result of Assur group decomposition.

Contains the ordered sequence of structural elements needed to solve the linkage kinematics.

Variables:

Example

>>> result = decompose_assur_groups(graph)
>>> print(f"Ground: {result.ground}")
>>> print(f"Drivers: {result.drivers}")
>>> for group in result.groups:
...     print(f"  {group.joint_signature}: {group.internal_nodes}")
all_nodes_in_order() list[str]

Return all node IDs in solving order.

Returns:

Ground nodes, then driver nodes, then internal nodes from each group in order.

drivers: list[str]
graph: LinkageGraph | None = None
ground: list[str]
groups: list[AssurGroup]
solve_order() list[str | AssurGroup]

Return the complete solving order (drivers + groups).

Returns:

List of driver node IDs followed by AssurGroup objects, in the order they should be solved.

class pylinkage.assur.Dyad(internal_nodes: tuple[str, ...]=<factory>, anchor_nodes: tuple[str, ...]=<factory>, internal_edges: tuple[str, ...]=<factory>, _signature: str = 'RRR', line_node1: str | None = None, line_node2: str | None = None, line2_node1: str | None = None, line2_node2: str | None = None)

Bases: AssurGroup

Class I Assur group — parameterized by joint signature.

A dyad consists of 2 binary links, 3 joints, and 1 internal node. The signature (e.g., “RRR”, “RRP”, “RPR”) determines the joint types and solving geometry.

Structure:

anchor_0 ----link_0---- internal_0 ----link_1---- anchor_1
(joint_0)               (joint_1)                 (joint_2)

For prismatic variants, additional line-defining nodes are stored:

  • RRP/RPR/PRR: one line defined by (line_node1, line_node2)

  • PP: two lines defined by (line_node1, line_node2) and (line2_node1, line2_node2)

Variables:
  • _signature (str) – Canonical joint signature string (e.g., “RRR”).

  • line_node1 (str | None) – First node defining prismatic line constraint.

  • line_node2 (str | None) – Second node defining prismatic line constraint.

  • line2_node1 (str | None) – First node of second line (PP only).

  • line2_node2 (str | None) – Second node of second line (PP only).

classmethod can_form(internal_node_ids: list[str], anchor_node_ids: list[str], graph: LinkageGraph, *, signature: str | None = None) bool

Check if nodes can form a dyad with the given (or any) signature.

If signature is provided, checks against that specific joint pattern. Otherwise, checks if any valid dyad can be formed.

Parameters:
  • internal_node_ids – Candidate internal nodes (must be exactly 1).

  • anchor_node_ids – Candidate anchor nodes.

  • graph – The linkage graph to check against.

  • signature – Optional specific signature to check (e.g., “RRR”).

Returns:

True if these elements can form a dyad.

property group_class: int

Return the class (k) of this Assur group.

Class I groups (dyads) have k=1.

property joint_signature: str

Return the joint type signature (e.g., ‘RRR’, ‘RRP’).

The signature describes the joint types in the group: - R = Revolute (pin joint) - P = Prismatic (slider joint)

line2_node1: str | None = None
line2_node2: str | None = None
line_node1: str | None = None
line_node2: str | None = None
class pylinkage.assur.DyadPRR(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.DyadRPR(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.DyadRRP(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.DyadRRR(**kwargs: object)

Bases: Dyad

Backwards-compatible alias. Use Dyad(_signature=…) for new code.

class pylinkage.assur.Edge(id: str, source: str, target: str, body_id: str | None = None)

Bases: object

A rigid link between two joints (topology only).

Edges represent rigid bodies connecting joints. Distance constraints are stored separately in a Dimensions object.

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

  • source (str) – ID of the source node.

  • target (str) – ID of the target node.

  • body_id (str | None) – Optional tag for grouping edges that belong to the same rigid body.

Example

>>> edge = Edge("AB", source="A", target="B")
body_id: str | None = None
connects(node_id: str) bool

Check if this edge connects to the given node.

id: str
other_node(node_id: str) str

Get the node on the other end of this edge.

Parameters:

node_id – One endpoint of this edge.

Returns:

The ID of the other endpoint.

Raises:

ValueError – If node_id is not an endpoint of this edge.

source: str
target: str
pylinkage.assur.EdgeId

alias of str

class pylinkage.assur.JointType(value)

Bases: IntEnum

Kinematic joint type.

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

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

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

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

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

GROUND = 3
PRISMATIC = 2
REVOLUTE = 1
TRACKER = 4
class pylinkage.assur.LinkageGraph(nodes: dict[str, ~pylinkage.assur.graph.Node]=<factory>, edges: dict[str, ~pylinkage.assur.graph.Edge]=<factory>, name: str = '', _adjacency: dict[str, list[str]]=<factory>)

Bases: object

Graph representation of a planar linkage (topology only).

A linkage is represented as a graph where: - Nodes are joints (revolute R or prismatic P) - Edges are rigid links

This is a pure topological representation - dimensional data (positions, distances, angles) is stored separately in a Dimensions object.

This representation enables: - Assur group decomposition - Alternative linkage definition syntax - Graph algorithms for analysis

Variables:

Example

>>> graph = LinkageGraph(name="Four-bar")
>>> graph.add_node(Node("A", role=NodeRole.GROUND))
>>> graph.add_node(Node("B", role=NodeRole.DRIVER))
>>> graph.add_edge(Edge("AB", source="A", target="B"))
>>> graph.neighbors("A")
['B']
add_edge(edge: Edge) None

Add an edge to the graph.

Parameters:

edge – The Edge to add.

Raises:

ValueError – If an edge with the same ID already exists, or if source/target nodes don’t exist.

add_node(node: Node) None

Add a node to the graph.

Parameters:

node – The Node to add.

Raises:

ValueError – If a node with the same ID already exists.

copy() LinkageGraph

Create a deep copy of the graph.

Returns:

A new LinkageGraph with copied nodes and edges.

degree(node_id: str) int

Get the degree (number of connections) of a node.

Parameters:

node_id – The node ID.

Returns:

Number of edges connected to the node.

driven_nodes() list[Node]

Get all driven nodes (Assur group members).

driver_nodes() list[Node]

Get all driver/input nodes.

edges: dict[str, Edge]
get_edge_between(node1: str, node2: str) Edge | None

Get the edge connecting two nodes, if any.

Parameters:
  • node1 – First node ID.

  • node2 – Second node ID.

Returns:

The Edge connecting the nodes, or None if not connected.

get_edges_for_node(node_id: str) list[Edge]

Get all edges connected to a node.

Parameters:

node_id – The node ID.

Returns:

List of Edge objects connected to the node.

ground_nodes() list[Node]

Get all ground/frame nodes.

name: str = ''
neighbors(node_id: str) list[str]

Get all nodes connected to the given node.

Parameters:

node_id – The node to find neighbors of.

Returns:

List of node IDs connected to the given node.

nodes: dict[str, Node]
remove_edge(edge_id: str) Edge

Remove an edge from the graph.

Parameters:

edge_id – ID of the edge to remove.

Returns:

The removed Edge.

Raises:

KeyError – If edge not found.

remove_node(node_id: str) Node

Remove a node and all its edges from the graph.

Parameters:

node_id – ID of the node to remove.

Returns:

The removed Node.

Raises:

KeyError – If node not found.

class pylinkage.assur.Node(id: str, joint_type: JointType = JointType.REVOLUTE, role: NodeRole = NodeRole.DRIVEN, name: str | None = None)

Bases: object

A joint in the linkage graph (topology only).

Represents the topological aspects of a joint. Dimensional data (position, angles) is stored separately in a Dimensions object.

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

  • joint_type (pylinkage._types.JointType) – The kinematic joint type (REVOLUTE or PRISMATIC).

  • role (pylinkage._types.NodeRole) – The role in the mechanism (GROUND, DRIVER, or DRIVEN).

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

Example

>>> node = Node("A", role=NodeRole.GROUND)
>>> node.joint_type
<JointType.REVOLUTE: 1>
id: str
joint_type: JointType = 1
name: str | None = None
role: NodeRole = 2
pylinkage.assur.NodeId

alias of str

class pylinkage.assur.NodeRole(value)

Bases: IntEnum

Role of a node/joint in the mechanism.

Classifies joints by their kinematic role in the linkage structure.

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

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

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

DRIVEN = 2
DRIVER = 1
GROUND = 0
class pylinkage.assur.Triad(internal_nodes: tuple[str, ...]=<factory>, anchor_nodes: tuple[str, ...]=<factory>, internal_edges: tuple[str, ...]=<factory>, _signature: str = 'RRRRRR', edge_map: dict[str, tuple[str, str]]=<factory>)

Bases: AssurGroup

Class II Assur group — parameterized by joint signature.

A triad consists of 4 links, 6 joints, and 2 internal nodes connected to 3 anchor nodes. The signature has 6 characters (e.g., “RRRRRR”).

Triads require solving 3 simultaneous constraints (Newton-Raphson), unlike dyads which only intersect 2 constraints.

Structure (one possible arrangement):

anchor_0 ------- internal_0 ------- anchor_1
                     |
                 internal_1
                     |
                 anchor_2
Variables:
  • _signature (str) – Canonical joint signature string (6 chars).

  • edge_map (dict[str, tuple[str, str]]) – Maps edge ID → (node_a, node_b) for all internal edges. The solver uses this to know which pair of nodes each distance constraint connects.

classmethod can_form(internal_node_ids: list[str], anchor_node_ids: list[str], graph: LinkageGraph) bool

Check if nodes can form a triad.

Requires: - Exactly 2 internal nodes - At least 3 anchor nodes - Edges connecting internals to anchors (4 total)

edge_map: dict[str, tuple[str, str]]
property group_class: int

Return the class (k) of this Assur group.

Class I groups (dyads) have k=1.

property joint_signature: str

Return the joint type signature (e.g., ‘RRR’, ‘RRP’).

The signature describes the joint types in the group: - R = Revolute (pin joint) - P = Prismatic (slider joint)

pylinkage.assur.decompose_assur_groups(graph: LinkageGraph) DecompositionResult

Decompose a linkage graph into Assur groups.

This algorithm identifies: 1. Ground nodes (fixed frame points) 2. Driver nodes (inputs/motors) 3. Assur groups in solvable order

The algorithm works iteratively: - Start with ground and drivers as “known” - Find groups that can be solved (all anchors known) - Add solved group nodes to known set - Repeat until all nodes are assigned

Parameters:

graph – The linkage graph to decompose.

Returns:

DecompositionResult with groups in solving order.

Raises:

ValueError – If decomposition fails (e.g., underconstrained mechanism).

Example

>>> graph = LinkageGraph(name="Four-bar")
>>> # ... add nodes and edges ...
>>> result = decompose_assur_groups(graph)
>>> print(f"Found {len(result.groups)} Assur groups")
pylinkage.assur.from_hypergraph(hypergraph: HypergraphLinkage) LinkageGraph

Convert a HypergraphLinkage to an Assur LinkageGraph (topology only).

This converts the hypergraph to the simpler graph representation used by the Assur decomposition system. Hyperedges are expanded to regular edges.

Both representations are pure topology - no dimensional data is transferred. To convert with dimensions, use this function then pass the same Dimensions object to graph_to_mechanism.

Parameters:

hypergraph – The HypergraphLinkage to convert (topology only).

Returns:

An Assur LinkageGraph suitable for decomposition and analysis.

Example

>>> hg = HypergraphLinkage(name="Four-bar")
>>> # ... add nodes and edges ...
>>> assur_graph = from_hypergraph(hg)
pylinkage.assur.graph_from_dict(data: dict[str, Any]) LinkageGraph

Create a LinkageGraph from a dictionary.

Parameters:

data – Dictionary containing graph data (as produced by graph_to_dict).

Returns:

A new LinkageGraph instance.

Example

>>> data = graph_to_dict(original_graph)
>>> restored_graph = graph_from_dict(data)
pylinkage.assur.graph_from_json(path: str | Path) LinkageGraph

Load a LinkageGraph from a JSON file.

Parameters:

path – Path to the JSON file.

Returns:

A new LinkageGraph instance.

Example

>>> graph_to_json(graph, "my_linkage.json")
>>> restored = graph_from_json("my_linkage.json")
pylinkage.assur.graph_to_dict(graph: LinkageGraph) dict[str, Any]

Convert a LinkageGraph to a dictionary.

The dictionary format is suitable for JSON serialization and can be used to reconstruct the graph later.

Parameters:

graph – The LinkageGraph to convert.

Returns:

Dictionary representation of the graph.

Example

>>> data = graph_to_dict(graph)
>>> json.dumps(data, indent=2)
pylinkage.assur.graph_to_json(graph: LinkageGraph, path: str | Path) None

Save a LinkageGraph to a JSON file.

Parameters:
  • graph – The LinkageGraph to save.

  • path – Path to the output JSON file.

Example

>>> graph_to_json(graph, "my_linkage.json")
>>> restored = graph_from_json("my_linkage.json")
pylinkage.assur.graph_to_mechanism(graph: LinkageGraph, dimensions: Dimensions) Mechanism

Convert a LinkageGraph and Dimensions directly to a Mechanism.

This converts the Assur graph and dimensions to the mechanism model without going through the legacy Linkage class. This is the preferred conversion path for new code.

The conversion: 1. Decomposes the graph into Assur groups 2. Creates appropriate Joint and Link instances for each element 3. Constructs a Mechanism with correct topology

Parameters:
  • graph – The LinkageGraph defining topology.

  • dimensions – The Dimensions providing positions, distances, angles.

Returns:

A Mechanism instance ready for simulation.

Raises:
  • ValueError – If the graph cannot be decomposed into valid Assur groups.

  • NotImplementedError – If an unsupported Assur group type is encountered.

Example

>>> graph = LinkageGraph(name="Four-bar")
>>> graph.add_node(Node("A", role=NodeRole.GROUND))
>>> dims = Dimensions(node_positions={"A": (0, 0)}, ...)
>>> mechanism = graph_to_mechanism(graph, dims)
>>> for positions in mechanism.step():
...     print(positions)
pylinkage.assur.mechanism_to_graph(mechanism: Mechanism) tuple[LinkageGraph, Dimensions]

Convert a Mechanism to a LinkageGraph and Dimensions.

This converts an existing Mechanism to the Assur graph representation for analysis, decomposition, or visualization.

Parameters:

mechanism – The Mechanism to convert.

Returns:

A tuple of (LinkageGraph, Dimensions).

Example

>>> mechanism = Mechanism(joints=[...], links=[...])
>>> graph, dims = mechanism_to_graph(mechanism)
>>> decomposition = decompose_assur_groups(graph)
pylinkage.assur.parse_signature(signature: str) AssurSignature

Parse a signature string into an AssurSignature.

The signature describes the joint types in the Assur group: - R = Revolute (pin joint) - P or T = Prismatic (slider joint) - both accepted, P preferred - _ = Optional separator for readability

Parameters:

signature – The signature string (e.g., “RRR”, “R_P_R”, “RPR”).

Returns:

Parsed AssurSignature.

Raises:

ValueError – If signature is empty, contains invalid characters, or has wrong joint count for any known group class.

Example

>>> sig = parse_signature("RRR")
>>> sig.joints
(JointType.REVOLUTE, JointType.REVOLUTE, JointType.REVOLUTE)
>>> sig = parse_signature("R_P_R")  # With separators
>>> sig.canonical_form
'RPR'
>>> sig = parse_signature("RTR")  # T accepted as alias for P
>>> sig.canonical_form
'RPR'
pylinkage.assur.signature_to_group_class(signature: AssurSignature | str) type[AssurGroup] | None

Get the AssurGroup class corresponding to a signature.

This bridges the formal signature syntax to the existing AssurGroup class hierarchy (DyadRRR, DyadRRP, etc.).

Parameters:

signature – The parsed signature or string to parse.

Returns:

The matching AssurGroup subclass, or None if not implemented.

Example

>>> sig = parse_signature("RRR")
>>> cls = signature_to_group_class(sig)
>>> cls.__name__
'DyadRRR'
>>> cls = signature_to_group_class("RPR")
>>> cls.__name__
'DyadRPR'
pylinkage.assur.signature_to_hypergraph(signature: AssurSignature | str, *, prefix: str = '', name: str | None = None) HypergraphLinkage

Generate a topological hypergraph from an Assur signature.

Creates a pure topological hypergraph with NO distance constraints. All edges have distance=None, representing only structural connections.

The generated graph follows standard naming conventions: - Anchor nodes: “anchor_0”, “anchor_1”, … - Internal nodes: “internal_0”, “internal_1”, … - Edges (links): “link_0”, “link_1”, …

Parameters:
  • signature – Either an AssurSignature or a string to parse.

  • prefix – Optional prefix for all node/edge IDs (useful for assembly).

  • name – Optional name for the hypergraph.

Returns:

HypergraphLinkage with pure topology (no constraints).

Raises:

NotImplementedError – For group classes not yet implemented (triad, tetrad).

Example

>>> graph = signature_to_hypergraph("RRR")
>>> len(graph.nodes)
3  # 2 anchors + 1 internal
>>> len(graph.edges)
2  # 2 links
>>> # With prefix for unique IDs
>>> leg = signature_to_hypergraph("RRR", prefix="leg1_")
>>> "leg1_anchor_0" in leg.nodes
True
pylinkage.assur.to_hypergraph(assur_graph: LinkageGraph) HypergraphLinkage

Convert an Assur LinkageGraph to a HypergraphLinkage (topology only).

This converts from the Assur graph representation to the more abstract hypergraph representation. Edges with the same body_id are grouped into hyperedges.

Both representations are pure topology - no dimensional data is transferred.

Parameters:

assur_graph – The Assur LinkageGraph to convert (topology only).

Returns:

A HypergraphLinkage representation (topology only).

Example

>>> assur_graph = LinkageGraph(name="Four-bar")
>>> # ... add nodes and edges ...
>>> hg = to_hypergraph(assur_graph)
pylinkage.assur.validate_decomposition(result: DecompositionResult) list[str]

Validate a decomposition result.

Checks for common issues that might indicate an invalid decomposition.

Parameters:

result – The DecompositionResult to validate.

Returns:

List of warning/error messages (empty if valid).