pylinkage package
Subpackages
- pylinkage.collections package
- pylinkage.geometry package
- pylinkage.joints package
- pylinkage.linkage package
- Submodules
- pylinkage.linkage.analysis module
- pylinkage.linkage.linkage module
LinkageLinkage.analyze_stroke()Linkage.analyze_transmission()Linkage.compile()Linkage.from_dict()Linkage.from_json()Linkage.get_accelerations()Linkage.get_coords()Linkage.get_num_constraints()Linkage.get_rotation_period()Linkage.get_velocities()Linkage.indeterminacy()Linkage.jointsLinkage.nameLinkage.rebuild()Linkage.sensitivity_analysis()Linkage.set_completely()Linkage.set_coords()Linkage.set_input_velocity()Linkage.set_num_constraints()Linkage.simulation()Linkage.step()Linkage.step_fast()Linkage.step_fast_with_kinematics()Linkage.stroke_position()Linkage.to_dict()Linkage.to_json()Linkage.tolerance_analysis()Linkage.transmission_angle()
Simulation
- pylinkage.linkage.sensitivity module
SensitivityAnalysisSensitivityAnalysis.baseline_path_metricSensitivityAnalysis.baseline_transmissionSensitivityAnalysis.constraint_namesSensitivityAnalysis.most_sensitiveSensitivityAnalysis.perturbation_deltaSensitivityAnalysis.perturbed_path_metricsSensitivityAnalysis.perturbed_transmissionSensitivityAnalysis.sensitivitiesSensitivityAnalysis.sensitivity_rankingSensitivityAnalysis.to_dataframe()
ToleranceAnalysisanalyze_sensitivity()analyze_tolerance()
- pylinkage.linkage.transmission module
StrokeAnalysisTransmissionAngleAnalysisTransmissionAngleAnalysis.acceptable_rangeTransmissionAngleAnalysis.anglesTransmissionAngleAnalysis.is_acceptableTransmissionAngleAnalysis.max_angleTransmissionAngleAnalysis.max_angle_stepTransmissionAngleAnalysis.max_deviationTransmissionAngleAnalysis.mean_angleTransmissionAngleAnalysis.min_angleTransmissionAngleAnalysis.min_angle_stepTransmissionAngleAnalysis.min_deviationTransmissionAngleAnalysis.worst_angle()
analyze_stroke()analyze_transmission()compute_slide_position()compute_transmission_angle()stroke_at_position()transmission_angle_at_position()
- Module contents
LinkageLinkage.analyze_stroke()Linkage.analyze_transmission()Linkage.compile()Linkage.from_dict()Linkage.from_json()Linkage.get_accelerations()Linkage.get_coords()Linkage.get_num_constraints()Linkage.get_rotation_period()Linkage.get_velocities()Linkage.indeterminacy()Linkage.jointsLinkage.nameLinkage.rebuild()Linkage.sensitivity_analysis()Linkage.set_completely()Linkage.set_coords()Linkage.set_input_velocity()Linkage.set_num_constraints()Linkage.simulation()Linkage.step()Linkage.step_fast()Linkage.step_fast_with_kinematics()Linkage.stroke_position()Linkage.to_dict()Linkage.to_json()Linkage.tolerance_analysis()Linkage.transmission_angle()
SensitivityAnalysisSensitivityAnalysis.baseline_path_metricSensitivityAnalysis.baseline_transmissionSensitivityAnalysis.constraint_namesSensitivityAnalysis.most_sensitiveSensitivityAnalysis.perturbation_deltaSensitivityAnalysis.perturbed_path_metricsSensitivityAnalysis.perturbed_transmissionSensitivityAnalysis.sensitivitiesSensitivityAnalysis.sensitivity_rankingSensitivityAnalysis.to_dataframe()
SimulationStrokeAnalysisToleranceAnalysisTransmissionAngleAnalysisTransmissionAngleAnalysis.acceptable_rangeTransmissionAngleAnalysis.anglesTransmissionAngleAnalysis.is_acceptableTransmissionAngleAnalysis.max_angleTransmissionAngleAnalysis.max_angle_stepTransmissionAngleAnalysis.max_deviationTransmissionAngleAnalysis.mean_angleTransmissionAngleAnalysis.min_angleTransmissionAngleAnalysis.min_angle_stepTransmissionAngleAnalysis.min_deviationTransmissionAngleAnalysis.worst_angle()
analyze_sensitivity()analyze_stroke()analyze_tolerance()analyze_transmission()bounding_box()compute_slide_position()compute_transmission_angle()kinematic_default_test()
- pylinkage.optimization package
- pylinkage.visualizer package
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
- pylinkage.exceptions.HypostaticError
alias of
UnderconstrainedError
- exception pylinkage.exceptions.NotCompletelyDefinedError(joint: Any, message: str = 'The joint is not completely defined!')
Bases:
ExceptionThe linkage definition is incomplete.
- exception pylinkage.exceptions.OptimizationError(message: str = 'Optimization failed')
Bases:
ExceptionShould be raised when the optimization process fails.
- exception pylinkage.exceptions.UnbuildableError(joint: Any, message: str = 'Unable to solve constraints')
Bases:
ExceptionShould be raised when the constraints cannot be solved.
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
- pylinkage.HypostaticError
alias of
UnderconstrainedError
- class pylinkage.JointType(value)
Bases:
IntEnumKinematic 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(joints: Iterable[Component], order: Iterable[Component] | None = None, name: str | None = None)
Bases:
objectA linkage is a set of Joint objects.
It is defined as a kinematic linkage. Coordinates are given relative to its own base.
- analyze_stroke(iterations: int | None = None) StrokeAnalysis
Analyze stroke/slide position over a full motion cycle.
For a linkage with a Prismatic joint, this tracks the slide position throughout the motion cycle.
- Args:
iterations: Number of simulation steps. Defaults to one full rotation.
- Returns:
StrokeAnalysis with min/max/range/mean statistics.
- Raises:
ValueError: If no Prismatic joint found or no valid positions.
- Example:
>>> analysis = linkage.analyze_stroke() >>> print(f"Stroke range: {analysis.stroke_range:.2f}") >>> print(f"Min: {analysis.min_position:.2f}") >>> print(f"Max: {analysis.max_position:.2f}")
- analyze_transmission(iterations: int | None = None, acceptable_range: tuple[float, float] = (40.0, 140.0)) TransmissionAngleAnalysis
Analyze transmission angle over a full motion cycle.
For a standard four-bar, joints are auto-detected.
- Args:
iterations: Number of simulation steps. Defaults to one full rotation. acceptable_range: (min, max) acceptable angles in degrees.
- Returns:
TransmissionAngleAnalysis with min/max/mean/is_acceptable.
- Raises:
ValueError: If joints cannot be detected or no valid positions found.
- Example:
>>> analysis = linkage.analyze_transmission() >>> print(f"Min: {analysis.min_angle:.1f}°") >>> print(f"Max: {analysis.max_angle:.1f}°") >>> print(f"Acceptable: {analysis.is_acceptable}")
- compile() None
Prepare numba-optimized solver for fast simulation.
This converts the linkage structure into numeric arrays for use by the numba-compiled simulation loop. The compilation is done automatically on first call to step_fast(), but can be called explicitly to control when the overhead occurs.
The compiled solver is cached and reused until invalidated by changes to constraints (set_num_constraints) or structure.
- classmethod from_dict(data: dict[str, Any]) Linkage
Create a linkage from a dictionary representation.
- Args:
data: Dictionary containing linkage data (as produced by to_dict()).
- Returns:
A new Linkage instance.
- Example:
>>> data = linkage.to_dict() >>> new_linkage = Linkage.from_dict(data)
- classmethod from_json(path: str | Path) Linkage
Load a linkage from a JSON file.
- Args:
path: Path to the JSON file.
- Returns:
A new Linkage instance.
- Example:
>>> linkage.to_json("my_linkage.json") >>> loaded = Linkage.from_json("my_linkage.json")
- get_accelerations() list[tuple[float, float] | None]
Return accelerations for all joints.
- Returns:
List of (ax, ay) tuples, one per joint. Returns None for joints whose acceleration has not been computed.
- get_coords() list[tuple[float | None, float | None]]
Return the positions of each element in the system.
- get_num_constraints(flat: bool = True) list[float | None] | list[tuple[float | None, ...]]
Numeric constraints of this linkage.
- Parameters:
flat – Whether to force one-dimensional representation of constraints. The default is True.
- Returns:
List of geometric constraints.
- get_rotation_period() int
The number of iterations to finish in the previous state.
Formally, it is the common denominator of all crank periods.
- Returns:
Number of iterations with dt=1.
- get_velocities() list[tuple[float, float] | None]
Return velocities for all joints.
- Returns:
List of (vx, vy) tuples, one per joint. Returns None for joints whose velocity has not been computed.
- indeterminacy() int
Return the static indeterminacy degree of the linkage in 2D.
- Uses a variant of the Gruebler-Kutzbach criterion for 2D planar mechanisms:
DOF = 3 * (n - 1) - kinematic_pairs + mobilities
- Where:
n = number of bodies (including the ground/frame)
kinematic_pairs = sum of constraint DOFs removed by joints
mobilities = input degrees of freedom (e.g., from motors/cranks)
A positive return value indicates the mechanism is under-constrained, zero means it’s exactly constrained, and negative means it’s over-constrained.
- Returns:
The indeterminacy degree (negative DOF when over-constrained).
- Note:
This implementation is experimental and results should be verified for complex mechanisms. The algorithm counts bodies and constraints based on joint types (Static, Crank, Revolute, Fixed).
- joints: tuple[Component, ...]
- name: str
- rebuild(pos: Sequence[tuple[float | None, float | None]] | None = None) None
Redefine linkage joints and given initial positions to joints.
- Parameters:
pos – Initial positions for each joint in self.joints. Coordinates do not need to be precise, they will allow us the best fitting position between all possible positions satisfying constraints. (Default value = None).
- sensitivity_analysis(output_joint: object | int | None = None, delta: float = 0.01, include_transmission: bool = True, iterations: int | None = None) SensitivityAnalysis
Analyze sensitivity of output path to each constraint dimension.
For each constraint, this function perturbs the value by delta and measures how much the output path changes.
- Args:
output_joint: Joint to measure, index, or None (auto-detect last). delta: Relative perturbation magnitude (e.g., 0.01 for 1%). include_transmission: Whether to also measure transmission angle. iterations: Number of simulation steps. Defaults to one full rotation.
- Returns:
SensitivityAnalysis with sensitivity coefficients and rankings.
- Example:
>>> analysis = linkage.sensitivity_analysis(delta=0.01) >>> print(f"Most sensitive: {analysis.most_sensitive}") >>> for name, sens in analysis.sensitivity_ranking: ... print(f"{name}: {sens:.4f}")
- set_completely(dimensions: Iterable[float] | Iterable[tuple[float, ...]], positions: Sequence[tuple[float | None, float | None]], flat: bool = True) None
Set both dimension and initial positions.
- Parameters:
dimensions – List of dimensions.
positions – Initial positions.
flat – If the dimensions are in “flat mode”. The default is True.
- set_coords(coords: Sequence[tuple[float | None, float | None]]) None
Set coordinates for all joints of the linkage.
- Parameters:
coords – Joint coordinates.
- set_input_velocity(crank: Component, omega: float, alpha: float = 0.0) None
Set angular velocity and acceleration for a crank joint.
This is used for kinematics computation (velocity/acceleration analysis).
- Args:
crank: The crank joint to set velocity for. omega: Angular velocity in rad/s. alpha: Angular acceleration in rad/s² (default 0).
- Raises:
ValueError: If the crank is not part of this linkage.
- Example:
>>> linkage.set_input_velocity(crank, omega=10.0) # 10 rad/s >>> positions, velocities = linkage.step_fast_with_kinematics(100)
- set_num_constraints(constraints: Iterable[float] | Iterable[tuple[float, ...]], flat: bool = True) None
Set numeric constraints for this linkage.
Numeric constraints are distances or angles between joints.
- Note:
This invalidates any cached solver data. The next call to step_fast() will recompile the solver automatically.
- Parameters:
constraints – Sequence of constraints to pass to the joints.
flat – If True, constraints should be a one-dimensional sequence of floats. If False, constraints should be a sequence of tuples of digits. Each element will be passed to the set_constraints method of each corresponding Joint. (Default value = True).
- simulation(iterations: int | None = None, dt: float = 1) Simulation
Create a simulation context manager for the linkage.
This provides a convenient way to run and iterate over linkage simulations with automatic resource management.
- Example:
>>> with linkage.simulation(iterations=100) as sim: ... for step, coords in sim: ... process(coords)
- Parameters:
iterations – Number of iterations to run. If None, uses get_rotation_period().
dt – Time step for crank rotation. Default is 1.
- Returns:
A Simulation context manager.
- step(iterations: int | None = None, dt: float = 1) Generator[tuple[tuple[float | None, float | None], ...], None, None]
Make a step of the linkage.
- Parameters:
iterations – Number of iterations to run across. If None, the default is self.get_rotation_period(). (Default value = None).
dt – Amount of rotation to turn the cranks by. All cranks rotate by their self.angle * dt. The default is 1. (Default value = 1).
- Returns:
Iterable of the joints’ coordinates.
- step_fast(iterations: int | None = None, dt: float = 1) ndarray[tuple[Any, ...], dtype[float64]]
Run simulation using numba-optimized solver.
This is significantly faster than step() for large iteration counts, as it avoids Python method call overhead in the hot loop.
The first call may be slower due to numba compilation (JIT warm-up). Subsequent calls will be fast.
- Args:
- iterations: Number of iterations to run. If None, uses
get_rotation_period(). (Default: None)
- dt: Amount of rotation per step. Cranks rotate by their
angle * dt. (Default: 1)
- Returns:
Array of shape (iterations, n_joints, 2) containing all joint positions at each step. Access individual step positions via trajectory[step_idx, joint_idx] which gives (x, y).
- Note:
If any configuration becomes unbuildable during simulation, the corresponding positions will be NaN. Check for this with np.isnan(trajectory).any() or use solver.has_nan_positions().
- Example:
>>> trajectory = linkage.step_fast(iterations=1000) >>> print(trajectory.shape) # (1000, n_joints, 2) >>> # Get position of joint 2 at step 100: >>> pos = trajectory[100, 2] # (x, y)
- step_fast_with_kinematics(iterations: int | None = None, dt: float = 1) tuple[ndarray[tuple[Any, ...], dtype[float64]], ndarray[tuple[Any, ...], dtype[float64]], ndarray[tuple[Any, ...], dtype[float64]]]
Run simulation with velocity and acceleration using numba-optimized solver.
This extends step_fast() to also compute velocities and accelerations at each step. Requires that omega (and optionally alpha) is set on crank joints to specify angular velocities and accelerations.
- Args:
- iterations: Number of iterations to run. If None, uses
get_rotation_period(). (Default: None)
- dt: Amount of rotation per step. Cranks rotate by their
angle * dt. (Default: 1)
- Returns:
Tuple of (positions, velocities, accelerations) arrays, each with shape (iterations, n_joints, 2).
- Example:
>>> linkage.set_input_velocity(crank, omega=10.0, alpha=0.0) >>> pos, vel, acc = linkage.step_fast_with_kinematics(1000) >>> print(vel[100, 2]) # velocity of joint 2 at step 100 >>> print(acc[100, 2]) # acceleration of joint 2 at step 100
- stroke_position() float
Get the slide position of a prismatic joint at current position.
For a linkage with a Prismatic joint, this returns the slide position along the prismatic axis.
- Returns:
Slide position along the axis.
- Raises:
ValueError: If no Prismatic joint found.
- Example:
>>> pos = linkage.stroke_position() >>> print(f"Current slide position: {pos:.2f}")
- to_dict() dict[str, Any]
Convert this linkage to a dictionary representation.
The dictionary can be used for serialization or reconstruction.
- Returns:
Dictionary containing the linkage’s data including joints and solve order.
- Example:
>>> data = linkage.to_dict() >>> new_linkage = Linkage.from_dict(data)
- to_json(path: str | Path) None
Save this linkage to a JSON file.
- Args:
path: Path to the output JSON file.
- Example:
>>> linkage.to_json("my_linkage.json") >>> loaded = Linkage.from_json("my_linkage.json")
- tolerance_analysis(tolerances: dict[str, float], output_joint: object | int | None = None, iterations: int | None = None, n_samples: int = 1000, seed: int | None = None) ToleranceAnalysis
Analyze manufacturing tolerance effects via Monte Carlo simulation.
Each sample randomly perturbs constraints within tolerance bounds and simulates the linkage to measure output variation.
- Args:
tolerances: Constraint name -> tolerance value (e.g., {“crank_radius”: 0.1}). output_joint: Joint to measure, index, or None (auto-detect last). iterations: Number of simulation steps. Defaults to one full rotation. n_samples: Number of Monte Carlo samples. seed: Random seed for reproducibility.
- Returns:
ToleranceAnalysis with statistics and output cloud for visualization.
- Example:
>>> tolerances = {"Crank_radius": 0.1, "Revolute_dist1": 0.2} >>> analysis = linkage.tolerance_analysis(tolerances, n_samples=500) >>> print(f"Max deviation: {analysis.max_deviation:.4f}") >>> analysis.plot_cloud()
- transmission_angle() float
Get the transmission angle at the current linkage position.
For a four-bar linkage, this is the angle between the coupler and rocker links at their common joint.
- Returns:
Transmission angle in degrees (0-180).
- Raises:
ValueError: If joints cannot be auto-detected.
- Example:
>>> angle = linkage.transmission_angle() >>> print(f"Current transmission angle: {angle:.1f}°")
- pylinkage.NodeId
alias of
str
- class pylinkage.NodeRole(value)
Bases:
IntEnumRole 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:
ExceptionThe linkage definition is incomplete.
- exception pylinkage.OptimizationError(message: str = 'Optimization failed')
Bases:
ExceptionShould be raised when the optimization process fails.
- pylinkage.PortId
alias of
str
- class pylinkage.Simulation(linkage: Linkage, iterations: int | None = None, dt: float = 1)
Bases:
objectContext manager for linkage simulation.
Provides a clean interface for iterating over linkage positions with automatic setup and teardown.
- Example:
>>> with linkage.simulation(iterations=100) as sim: ... for step, coords in sim: ... # coords is a tuple of (x, y) for each joint ... print(f"Step {step}: {coords}")
- property iterations: int
Number of iterations for this simulation.
- exception pylinkage.UnbuildableError(joint: Any, message: str = 'Unable to solve constraints')
Bases:
ExceptionShould be raised when the constraints cannot be solved.
- exception pylinkage.UnderconstrainedError(linkage: Linkage | str, message: str = 'The linkage is under-constrained!')
Bases:
ExceptionThe 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.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.