pylinkage package

Subpackages

Submodules

pylinkage.exceptions module

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

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

@author: HugoFara

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

Bases: Exception

The linkage definition is incomplete.

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

Bases: Exception

Should be raised when the optimization process fails.

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

Bases: Exception

Should be raised when the constraints cannot be solved.

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

Bases: Exception

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

Module contents

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

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

Created on Thu Jun 10 21:30:52 2021

@author: HugoFara

pylinkage.ComponentId

alias of str

pylinkage.EdgeId

alias of str

pylinkage.HyperedgeId

alias of str

class pylinkage.JointType(*values)

Bases: IntEnum

Kinematic joint type.

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

Attributes:

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.Linkage(components: Iterable[Component], order: Iterable[Component] | None = None, name: str | None = None)

Bases: object

A planar linkage mechanism built from components.

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

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

Compute sensitivity of an output path to constraint perturbations.

See pylinkage.linkage.analyze_sensitivity().

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

Analyze stroke/slide position over a full motion cycle.

See pylinkage.linkage.analyze_stroke().

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

Monte-Carlo tolerance analysis over the output path.

See pylinkage.linkage.analyze_tolerance().

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

Analyze transmission angle over a full motion cycle.

See pylinkage.linkage.analyze_transmission() for details.

compile() None

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

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

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

Return components (backwards compatibility alias).

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

Return accelerations for all components.

Returns:

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

get_constraints() list[float]

Return all geometric constraints as a flat list.

Returns:

Flat list of all constraint values, used by optimizers.

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

Return positions of all components.

Returns:

List of (x, y) positions.

get_rotation_period() int

Return number of steps for one full cycle.

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

Returns:

Number of iterations with dt=1.

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

Return velocities for all components.

Returns:

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

indeterminacy() int

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

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

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

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

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

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

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

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

Rebuild the linkage, optionally setting initial positions.

Args:
positions: Initial positions for each component. If None, uses

current positions.

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

Apply both constraints and initial positions in one call.

Args:

constraints: Flat list (as accepted by set_constraints()). positions: Per-component (x, y) positions

(as accepted by set_coords()).

set_constraints(values: list[float]) None

Set constraints from a flat list.

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

Args:

values: Flat list of constraint values.

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

Set positions for all components.

Args:

coords: List of (x, y) positions.

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

Set angular velocity and acceleration for a crank actuator.

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

Args:

actuator: The crank actuator to set velocity for. omega: Angular velocity in rad/s (physical units for analysis). alpha: Angular acceleration in rad/s² (default 0).

Raises:

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

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

Return a context manager that simulates this linkage.

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

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

Simulate the linkage.

Yields positions for all components at each step.

Args:

iterations: Number of steps. If None, uses get_rotation_period(). dt: Time step multiplier for actuators (cranks and linear actuators).

Yields:

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

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

Run the simulation through the numba-compiled solver.

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

Args:

iterations: Number of steps. Defaults to get_rotation_period(). dt: Time step multiplier (default 1.0).

Returns:

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

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

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

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

Args:

iterations: Number of steps. Defaults to get_rotation_period(). dt: Time step multiplier (default 1.0).

Returns:

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

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

Simulate the linkage with velocity and acceleration computation.

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

Args:

iterations: Number of steps. If None, uses get_rotation_period(). dt: Time step multiplier for actuators (cranks and linear actuators).

Yields:

Tuple of (positions, velocities, accelerations) where: - positions: Tuple of (x, y) for each component - velocities: Tuple of (vx, vy) or None for each component - accelerations: Tuple of (ax, ay) or None for each component

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

Slide position of a prismatic joint at the current pose.

See pylinkage.linkage.stroke_at_position().

to_hypergraph() tuple[HypergraphLinkage, Dimensions]

Return a hypergraph view of this linkage.

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

transmission_angle() float

Transmission angle at the current pose, in degrees.

See pylinkage.linkage.transmission_angle_at_position().

pylinkage.NodeId

alias of str

class pylinkage.NodeRole(*values)

Bases: IntEnum

Role of a node/joint in the mechanism.

Classifies joints by their kinematic role in the linkage structure.

Attributes:

GROUND: Fixed frame point that does not move. DRIVER: Input/motor joint that provides motion (actuated). DRIVEN: Position computed from constraints (passive, part of Assur groups).

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

Bases: Exception

The linkage definition is incomplete.

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

Bases: Exception

Should be raised when the optimization process fails.

pylinkage.PortId

alias of str

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

Bases: object

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

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

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

Number of iterations for this simulation.

property linkage: Any

The linkage being simulated.

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

Bases: Exception

Should be raised when the constraints cannot be solved.

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

Bases: Exception

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

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

Compute the bounding box of a locus.

Parameters:

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

Returns:

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

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

Get the intersections of two circles.

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

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

  • y1 – Y coordinate of first circle center.

  • r1 – Radius of first circle.

  • x2 – X coordinate of second circle center.

  • y2 – Y coordinate of second circle center.

  • r2 – Radius of second circle.

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

Returns:

Tuple of (n_intersections, x1, y1, x2, y2) where: - n=0: No intersection (other values undefined) - n=1: One intersection at (x1, y1) - n=2: Two intersections at (x1, y1) and (x2, y2) - n=3: Same circle (x1, y1, x2 are center and radius)

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

Convert polar coordinates into cartesian.

Parameters:
  • radius – Distance from origin.

  • theta – Angle starting from abscissa axis.

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

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

Returns:

Cartesian coordinates (x, y).

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

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

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

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

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

Returns:

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

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

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

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

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

  • joint

    Which joint’s trajectory to extract. Can be:

    • an integer index into each frame (default -1 = last joint),

    • a joint/component name (requires linkage),

    • a joint/component instance (requires linkage).

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

Returns:

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

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

Intersection of two arbitrary objects.

The input objects should be points or circles.

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

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

  • tol – Absolute tolerance to use if provided.

Returns:

The intersection found, if any.

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

Standard run for any linkage before a complete fitness evaluation.

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

Parameters:
  • func – Fitness function to be decorated.

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

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

Return the norm of a 2-dimensional vector.

Parameters:
  • x – X component.

  • y – Y component.

Returns:

Vector magnitude.

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

Square of the distance between two points.

Faster than dist when only comparing distances.

Parameters:
  • x1 – X coordinate of first point.

  • y1 – Y coordinate of first point.

  • x2 – X coordinate of second point.

  • y2 – Y coordinate of second point.

Returns:

Squared distance.