Source code for linkforge.core.models.transmission

"""Transmission models for ros2_control integration.

This module defines the mechanical coupling between actuators and joints,
handling gear ratios, offsets, and hardware interface mappings.

Transmission Categories:
- **Simple**: 1-to-1 mapping between an actuator and a joint.
- **Differential**: 2-to-2 mapping (e.g., wrist or differential drive).
- **Linkage**: Complex mappings like four-bar linkages.
- **Custom**: Extensible plugin-based transmissions.
"""

from __future__ import annotations

from collections import Counter
from collections.abc import Sequence
from dataclasses import dataclass, field, replace
from enum import StrEnum

from .._utils.string_utils import is_valid_name
from ..constants import (
    EPSILON,
    HW_IF_POSITION,
)
from ..exceptions import RobotValidationError, ValidationErrorCode


class TransmissionType(StrEnum):
    """Standard transmission types in ros2_control."""

    SIMPLE = "transmission_interface/SimpleTransmission"
    DIFFERENTIAL = "transmission_interface/DifferentialTransmission"
    FOUR_BAR_LINKAGE = "transmission_interface/FourBarLinkageTransmission"
    CUSTOM = "custom"


[docs] @dataclass(frozen=True) class TransmissionJoint: """Joint specification in a transmission.""" name: str hardware_interfaces: Sequence[str] = field(default_factory=lambda: (HW_IF_POSITION,)) mechanical_reduction: float | None = 1.0 offset: float = 0.0
[docs] def __post_init__(self) -> None: """Validate transmission joint.""" if not self.name: raise RobotValidationError( ValidationErrorCode.NAME_EMPTY, "Transmission joint name cannot be empty", target="JointName", value=self.name, ) if not self.hardware_interfaces: raise RobotValidationError( ValidationErrorCode.VALUE_EMPTY, f"Transmission joint '{self.name}' must have at least one interface", target="HardwareInterfaces", value=self.name, ) if self.mechanical_reduction is not None and abs(self.mechanical_reduction) < EPSILON: raise RobotValidationError( ValidationErrorCode.INVALID_VALUE, f"Mechanical reduction for transmission joint '{self.name}' cannot be zero", target="MechanicalReduction", value=self.mechanical_reduction, ) object.__setattr__(self, "hardware_interfaces", tuple(self.hardware_interfaces))
[docs] def with_prefix(self, prefix: str) -> TransmissionJoint: """Create a new transmission joint with a prefixed name.""" return replace(self, name=f"{prefix}{self.name}")
[docs] def normalized(self) -> TransmissionJoint: """Return a new transmission joint with sorted interfaces.""" return replace(self, hardware_interfaces=tuple(sorted(self.hardware_interfaces)))
[docs] @dataclass(frozen=True) class TransmissionActuator: """Actuator specification in a transmission.""" name: str hardware_interfaces: Sequence[str] = field(default_factory=lambda: (HW_IF_POSITION,)) mechanical_reduction: float | None = 1.0 offset: float = 0.0
[docs] def __post_init__(self) -> None: """Validate transmission actuator.""" if not self.name: raise RobotValidationError( ValidationErrorCode.NAME_EMPTY, "Transmission actuator name cannot be empty", target="ActuatorName", value=self.name, ) if not self.hardware_interfaces: raise RobotValidationError( ValidationErrorCode.VALUE_EMPTY, f"Transmission actuator '{self.name}' must have at least one interface", target="HardwareInterfaces", value=self.name, ) if self.mechanical_reduction is not None and abs(self.mechanical_reduction) < EPSILON: raise RobotValidationError( ValidationErrorCode.INVALID_VALUE, f"Mechanical reduction for transmission actuator '{self.name}' cannot be zero", target="MechanicalReduction", value=self.mechanical_reduction, ) object.__setattr__(self, "hardware_interfaces", tuple(self.hardware_interfaces))
[docs] def with_prefix(self, prefix: str) -> TransmissionActuator: """Create a new transmission actuator with a prefixed name.""" return replace(self, name=f"{prefix}{self.name}")
[docs] def normalized(self) -> TransmissionActuator: """Return a new transmission actuator with sorted interfaces.""" return replace(self, hardware_interfaces=tuple(sorted(self.hardware_interfaces)))
[docs] @dataclass(frozen=True) class Transmission: """Mechanical transmission mapping between joints and actuators. Transmissions describe how mathematical joint states relate to physical actuator commands, including hardware interface specifications for ros2_control. """ name: str type: str # Plugin name (e.g., TransmissionType enum or custom) joints: Sequence[TransmissionJoint] = field(default_factory=tuple) actuators: Sequence[TransmissionActuator] = field(default_factory=tuple) # Additional parameters for complex transmissions parameters: dict[str, str] = field(default_factory=dict)
[docs] def __post_init__(self) -> None: """Validate transmission configuration.""" if not self.name: raise RobotValidationError( ValidationErrorCode.NAME_EMPTY, "Transmission name cannot be empty", target="TransmissionName", value=self.name, ) if not self.type: raise RobotValidationError( ValidationErrorCode.VALUE_EMPTY, "Transmission type cannot be empty", target="TransmissionType", value=self.type, ) # Validate naming convention if not is_valid_name(self.name): raise RobotValidationError( ValidationErrorCode.INVALID_NAME, "Invalid characters in transmission name", target="TransmissionName", value=self.name, ) # Must have at least one joint and one actuator if not self.joints: raise RobotValidationError( ValidationErrorCode.VALUE_EMPTY, "Transmission must have at least one joint", target="TransmissionJoints", value=self.name, ) if not self.actuators: raise RobotValidationError( ValidationErrorCode.VALUE_EMPTY, "Transmission must have at least one actuator", target="TransmissionActuators", value=self.name, ) # Specific constraints for standard transmission types if self.type == TransmissionType.SIMPLE.value: if len(self.joints) != 1 or len(self.actuators) != 1: raise RobotValidationError( ValidationErrorCode.INVALID_VALUE, "Simple transmission must have exactly 1 joint and 1 actuator", target="TransmissionComponents", ) elif self.type in ( TransmissionType.DIFFERENTIAL.value, TransmissionType.FOUR_BAR_LINKAGE.value, ) and (len(self.joints) != 2 or len(self.actuators) != 2): raise RobotValidationError( ValidationErrorCode.INVALID_VALUE, f"{self.type} must have exactly 2 joints and 2 actuators", target="TransmissionComponents", ) # Check for duplicate joint names joint_names = [j.name for j in self.joints] duplicates = {name for name, count in Counter(joint_names).items() if count > 1} if duplicates: raise RobotValidationError( ValidationErrorCode.DUPLICATE_NAME, f"Duplicate joints in transmission: {duplicates}", target="DuplicateJoints", value=self.name, ) # Check for duplicate actuator names actuator_names = [a.name for a in self.actuators] duplicates = {name for name, count in Counter(actuator_names).items() if count > 1} if duplicates: raise RobotValidationError( ValidationErrorCode.DUPLICATE_NAME, f"Duplicate actuators in transmission: {duplicates}", target="DuplicateActuators", value=self.name, ) object.__setattr__(self, "joints", tuple(self.joints)) object.__setattr__(self, "actuators", tuple(self.actuators))
[docs] def with_prefix(self, prefix: str) -> Transmission: """Create a new transmission with prefixed name, joints, and actuators.""" return replace( self, name=f"{prefix}{self.name}", joints=tuple(j.with_prefix(prefix) for j in self.joints), actuators=tuple(a.with_prefix(prefix) for a in self.actuators), )
[docs] def normalized(self) -> Transmission: """Return a new transmission with sorted joints and actuators.""" return replace( self, joints=tuple(sorted([j.normalized() for j in self.joints], key=lambda x: x.name)), actuators=tuple(sorted([a.normalized() for a in self.actuators], key=lambda x: x.name)), )
[docs] @classmethod def create_simple( cls, name: str, joint_name: str, actuator_name: str | None = None, mechanical_reduction: float = 1.0, hardware_interfaces: list[str] | None = None, ) -> Transmission: """Create a simple 1-to-1 transmission. Args: name: Transmission name joint_name: Name of the joint actuator_name: Name of the actuator (defaults to joint_name + "_motor") mechanical_reduction: Gear ratio (default 1.0) hardware_interfaces: Interface types (default [HW_IF_POSITION]) Returns: Configured simple transmission """ if actuator_name is None: actuator_name = f"{joint_name}_motor" # Handle interfaces actual_interfaces = hardware_interfaces or [] if not actual_interfaces: actual_interfaces = [HW_IF_POSITION] return cls( name=name, type=TransmissionType.SIMPLE.value, joints=( TransmissionJoint( name=joint_name, hardware_interfaces=actual_interfaces, mechanical_reduction=mechanical_reduction, ), ), actuators=( TransmissionActuator( name=actuator_name, hardware_interfaces=actual_interfaces, mechanical_reduction=1.0, ), ), )
[docs] @classmethod def create_differential( cls, name: str, joint1_name: str, joint2_name: str, actuator1_name: str | None = None, actuator2_name: str | None = None, mechanical_reduction: float = 1.0, hardware_interfaces: list[str] | None = None, ) -> Transmission: """Create a differential transmission (2 actuators, 2 joints). Args: name: Transmission name joint1_name: First joint name joint2_name: Second joint name actuator1_name: First actuator name (defaults to joint1_name + "_motor") actuator2_name: Second actuator name (defaults to joint2_name + "_motor") mechanical_reduction: Gear ratio (default 1.0) hardware_interfaces: Interface types (default [HW_IF_POSITION]) Returns: Configured differential transmission """ if actuator1_name is None: actuator1_name = f"{joint1_name}_motor" if actuator2_name is None: actuator2_name = f"{joint2_name}_motor" # Handle interfaces actual_interfaces = hardware_interfaces or [] if not actual_interfaces: actual_interfaces = [HW_IF_POSITION] return cls( name=name, type=TransmissionType.DIFFERENTIAL.value, joints=( TransmissionJoint( name=joint1_name, hardware_interfaces=actual_interfaces, mechanical_reduction=mechanical_reduction, ), TransmissionJoint( name=joint2_name, hardware_interfaces=actual_interfaces, mechanical_reduction=mechanical_reduction, ), ), actuators=( TransmissionActuator( name=actuator1_name, hardware_interfaces=actual_interfaces, mechanical_reduction=1.0, ), TransmissionActuator( name=actuator2_name, hardware_interfaces=actual_interfaces, mechanical_reduction=1.0, ), ), )