Custom Joint Creation
This tutorial shows how to create custom joint types by extending the base
Joint class. Custom joints let you model specialized mechanical constraints
not covered by the built-in joint types.
Understanding the Joint Interface
All joints in pylinkage inherit from the abstract Joint class and must
implement three methods:
get_constraints(): Returns the geometric constraints (distances, angles)set_constraints(): Sets the geometric constraintsreload(dt): Computes the joint’s position based on its parents and constraints
The base Joint class provides:
x,y: Current position coordinatesjoint0,joint1: Parent joints (can beNone)name: Human-readable identifiercoord(): Returns(x, y)tupleset_coord(x, y): Sets position
Example: Slider Joint
Let’s create a slider joint that moves along a line defined by two parent points.
Unlike Linear which constrains to an infinite line, our slider will be
constrained to move only between its parent points.
Step 1: Define the Class
from pylinkage.joints.joint import Joint
class Slider(Joint):
"""A joint that slides between two parent points.
The joint's position is determined by a parameter ``t`` where:
- t=0 means the joint is at joint0's position
- t=1 means the joint is at joint1's position
- Values between 0 and 1 interpolate linearly
"""
__slots__ = ("t",)
def __init__(
self,
x=0,
y=0,
joint0=None,
joint1=None,
t=0.5,
name=None,
):
"""Create a Slider joint.
:param x: Initial x position.
:param y: Initial y position.
:param joint0: First parent joint (start of slide).
:param joint1: Second parent joint (end of slide).
:param t: Position parameter (0 to 1).
:param name: Joint name.
"""
super().__init__(x, y, joint0, joint1, name)
self.t = t
Step 2: Implement get_constraints
Return the geometric parameters that define this joint’s motion:
def get_constraints(self):
"""Return the slide parameter as constraint."""
return (self.t,)
Step 3: Implement set_constraints
Accept new constraint values:
def set_constraints(self, t=None):
"""Set the slide parameter.
:param t: New position parameter (0 to 1).
"""
if t is not None:
self.t = t
Step 4: Implement reload
Compute the joint’s position based on parent positions and constraints:
def reload(self, dt=1):
"""Recompute position by interpolating between parents.
:param dt: Time step (unused for this joint type).
"""
if self.joint0 is None or self.joint1 is None:
return
# Get parent positions
x0, y0 = self.joint0.coord()
x1, y1 = self.joint1.coord()
# Linear interpolation
self.x = x0 + self.t * (x1 - x0)
self.y = y0 + self.t * (y1 - y0)
Complete Slider Implementation
Here’s the complete custom joint:
from pylinkage.joints.joint import Joint
class Slider(Joint):
"""A joint that slides between two parent points."""
__slots__ = ("t",)
def __init__(
self,
x=0,
y=0,
joint0=None,
joint1=None,
t=0.5,
name=None,
):
super().__init__(x, y, joint0, joint1, name)
self.t = t
def get_constraints(self):
return (self.t,)
def set_constraints(self, t=None):
if t is not None:
self.t = t
def reload(self, dt=1):
if self.joint0 is None or self.joint1 is None:
return
x0, y0 = self.joint0.coord()
x1, y1 = self.joint1.coord()
self.x = x0 + self.t * (x1 - x0)
self.y = y0 + self.t * (y1 - y0)
Using the Custom Joint
Here’s how to use the slider in a linkage:
import pylinkage as pl
# Define anchor points
p1 = pl.Static(0, 0, name="P1")
p2 = pl.Static(4, 0, name="P2")
# Create slider between points
slider = Slider(
joint0=p1,
joint1=p2,
t=0.5, # Start in the middle
name="Slider"
)
# Create a crank to drive motion
crank = pl.Crank(
joint0=(2, 2),
angle=0,
distance=1,
name="Crank"
)
# Connect crank to slider with a revolute joint
connector = pl.Revolute(
joint0=crank,
joint1=slider,
distance0=2,
distance1=0.5,
name="Connector"
)
# Note: For this to work, slider.t would need to be updated
# based on the mechanism's geometry, which requires more
# sophisticated constraint solving.
Example: Oscillating Joint
Here’s another example: a joint that oscillates sinusoidally over time.
import math
from pylinkage.joints.joint import Joint
class Oscillator(Joint):
"""A joint that moves sinusoidally around a center point."""
__slots__ = ("amplitude", "frequency", "phase", "_time")
def __init__(
self,
x=0,
y=0,
joint0=None,
amplitude=1.0,
frequency=1.0,
phase=0.0,
name=None,
):
"""Create an oscillating joint.
:param joint0: Center point of oscillation.
:param amplitude: Maximum displacement from center.
:param frequency: Oscillation frequency.
:param phase: Phase offset in radians.
"""
super().__init__(x, y, joint0, None, name)
self.amplitude = amplitude
self.frequency = frequency
self.phase = phase
self._time = 0.0
def get_constraints(self):
return (self.amplitude, self.frequency, self.phase)
def set_constraints(self, amplitude=None, frequency=None, phase=None):
if amplitude is not None:
self.amplitude = amplitude
if frequency is not None:
self.frequency = frequency
if phase is not None:
self.phase = phase
def reload(self, dt=1):
self._time += dt
if self.joint0 is None:
center_x, center_y = 0, 0
else:
center_x, center_y = self.joint0.coord()
offset = self.amplitude * math.sin(
self.frequency * self._time + self.phase
)
self.x = center_x + offset
self.y = center_y
Best Practices
When creating custom joints:
Use __slots__: Define
__slots__with your additional attributes to save memory and prevent accidental attribute creation.Handle None parents: Check if parent joints are
Noneinreload().Return tuples from get_constraints: Always return a tuple, even if empty.
Document constraints: Clearly document what each constraint value means.
Consider optimization: If your joint will be used in optimization, ensure constraints are continuous values that can be interpolated.
Validate inputs: Consider raising
NotCompletelyDefinedErrorif required parameters are missing.
Next Steps
Advanced Optimization Techniques - Optimize linkages with custom joints
See
pylinkage.jointsfor built-in joint implementations