pylinkage.simulation package

Submodules

pylinkage.simulation.linkage module

Linkage container for simulating planar mechanisms.

The Linkage class orchestrates a collection of components (Ground, actuators, dyads) to simulate a planar mechanism.

class pylinkage.simulation.linkage.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.

Parameters:

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.

Parameters:
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.

Parameters:

values – Flat list of constraint values.

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

Set positions for all components.

Parameters:

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.

Parameters:
  • 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.

Parameters:
  • 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.

Parameters:
  • 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).

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

  • dt – Time step multiplier (default 1.0).

Returns:

(positions, velocities, accelerations) — each a numpy array of shape (iterations, n_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().

Parameters:
  • 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().

Module contents

Simulation - containers for running mechanism simulations.

This module provides container classes for orchestrating mechanism simulations:

Classes:

Linkage: Container that manages components and runs step-by-step simulation

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)
class pylinkage.simulation.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.

Parameters:

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.

Parameters:
  • 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.

Parameters:

values – Flat list of constraint values.

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

Set positions for all components.

Parameters:

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.

Parameters:
  • 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.

Parameters:
  • 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.

Parameters:
  • 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).

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

  • dt – Time step multiplier (default 1.0).

Returns:

(positions, velocities, accelerations) — each a numpy array of shape (iterations, n_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().

Parameters:
  • 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().