Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 40 additions & 17 deletions opensourceleg/actuators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@


@dataclass
class MOTOR_CONSTANTS:
class MotorConstants:
"""
Class to define the motor constants.

Examples:
>>> constants = MOTOR_CONSTANTS(
>>> constants = MotorConstants(
... MOTOR_COUNT_PER_REV=2048,
... NM_PER_AMP=0.02,
... NM_PER_RAD_TO_K=0.001,
Expand Down Expand Up @@ -59,21 +59,23 @@ class MOTOR_CONSTANTS:
WINDING_SOFT_LIMIT: float = 70.0 # soft winding limit (°C)
CASE_SOFT_LIMIT: float = 60.0 # soft case limit (°C)

on_change: Optional[Callable[[], None]] = None # Callback when any attribute changes

def __post_init__(self) -> None:
"""
Function to validate the motor constants and thermal parameters.

Examples:
>>> # This will raise a ValueError because a negative value is invalid.
>>> MOTOR_CONSTANTS(
>>> MotorConstants(
... MOTOR_COUNT_PER_REV=-2048,
... NM_PER_AMP=0.02,
... MAX_CASE_TEMPERATURE=80.0,
... MAX_WINDING_TEMPERATURE=120.0
... )
"""
if any(x <= 0 for x in self.__dict__.values()):
raise ValueError("All values in MOTOR_CONSTANTS must be non-zero and positive.")
if any(x <= 0 for name, x in self.__dict__.items() if isinstance(x, (int, float)) and name != "on_change"):
raise ValueError("All numeric values in MotorConstants must be non-zero and positive.")

# Validate thermal safety limits
if self.MAX_WINDING_TEMPERATURE <= self.MAX_CASE_TEMPERATURE:
Expand All @@ -83,6 +85,15 @@ def __post_init__(self) -> None:
if self.CASE_SOFT_LIMIT >= self.MAX_CASE_TEMPERATURE:
raise ValueError("CASE_SOFT_LIMIT must be less than MAX_CASE_TEMPERATURE")

def __setattr__(self, name: str, value: Any) -> None:
"""
Override setattr to call on_change callback when attributes are modified.
"""
super().__setattr__(name, value)
# Call callback if it exists and the attribute is not the callback itself
if name != "on_change" and self.on_change is not None:
self.on_change()

@property
def RAD_PER_COUNT(self) -> float:
"""
Expand All @@ -92,7 +103,7 @@ def RAD_PER_COUNT(self) -> float:
float: Radians per count.

Examples:
>>> constants = MOTOR_CONSTANTS(2048, 0.02, 0.001, 0.0001, 80.0, 120.0)
>>> constants = MotorConstants(2048, 0.02, 0.001, 0.0001, 80.0, 120.0)
>>> constants.RAD_PER_COUNT
0.0030679615757712823
"""
Expand All @@ -107,7 +118,7 @@ def NM_PER_MILLIAMP(self) -> float:
float: NM per milliamp.

Examples:
>>> constants = MOTOR_CONSTANTS(2048, 0.02, 0.001, 0.0001, 80.0, 120.0)
>>> constants = MotorConstants(2048, 0.02, 0.001, 0.0001, 80.0, 120.0)
>>> constants.NM_PER_MILLIAMP
2e-05
"""
Expand Down Expand Up @@ -427,7 +438,7 @@ def __init__(
self,
tag: str,
gear_ratio: float,
motor_constants: MOTOR_CONSTANTS,
motor_constants: MotorConstants,
frequency: int = 1000,
offline: bool = False,
**kwargs: Any,
Expand All @@ -438,7 +449,7 @@ def __init__(
Args:
tag (str): A unique identifier for the actuator.
gear_ratio (float): The gear ratio of the actuator.
motor_constants (MOTOR_CONSTANTS): Motor constant configuration parameters.
motor_constants (MotorConstants): Motor constant configuration parameters.
frequency (int, optional): Control frequency in Hz. Defaults to 1000.
offline (bool, optional): Flag indicating if the actuator operates in offline mode. Defaults to False.
**kwargs (Any): Additional keyword arguments.
Expand All @@ -447,10 +458,10 @@ def __init__(
>>> actuator = DummyActuator(
... tag="act1",
... gear_ratio=100,
... motor_constants=MOTOR_CONSTANTS(2048, 0.02, 0.001, 0.0001, 80.0, 120.0)
... motor_constants=MotorConstants(2048, 0.02, 0.001, 0.0001, 80.0, 120.0)
... )
"""
self._MOTOR_CONSTANTS: MOTOR_CONSTANTS = motor_constants
self.MOTOR_CONSTANTS: MotorConstants = motor_constants
self._gear_ratio: float = gear_ratio
self._tag: str = tag
self._frequency: int = frequency
Expand Down Expand Up @@ -991,7 +1002,7 @@ def motor_torque(self) -> float:
pass

@property
def MOTOR_CONSTANTS(self) -> MOTOR_CONSTANTS:
def MOTOR_CONSTANTS(self) -> MotorConstants:
"""
Get the motor constants configuration.

Expand All @@ -1006,21 +1017,33 @@ def MOTOR_CONSTANTS(self) -> MOTOR_CONSTANTS:
return self._MOTOR_CONSTANTS

@MOTOR_CONSTANTS.setter
def MOTOR_CONSTANTS(self, value: MOTOR_CONSTANTS) -> None:
def MOTOR_CONSTANTS(self, value: MotorConstants) -> None:
"""
Set the motor constants configuration.

Args:
value (MOTOR_CONSTANTS): The new motor constants to set.
value (MotorConstants): The new motor constants to set.

Examples:
>>> new_constants = MOTOR_CONSTANTS(2048, 0.03, 0.001, 0.0001, 85.0, 125.0)
>>> new_constants = MotorConstants(2048, 0.03, 0.001, 0.0001, 85.0, 125.0)
>>> actuator.MOTOR_CONSTANTS = new_constants
>>> actuator.MOTOR_CONSTANTS.MAX_CASE_TEMPERATURE
85.0
"""
if not isinstance(value, MotorConstants):
raise TypeError(f"Expected MotorConstants, got {type(value)}")

# Copy callback from old MOTOR_CONSTANTS to new one if it exists
old_constants = getattr(self, "_MOTOR_CONSTANTS", None)
if old_constants is not None and hasattr(old_constants, "on_change") and old_constants.on_change is not None:
value.on_change = old_constants.on_change

self._MOTOR_CONSTANTS = value

# Call the callback if it exists and is callable (to update derived constants)
if hasattr(value, "on_change") and callable(value.on_change):
value.on_change()

@property
def mode(self) -> CONTROL_MODES:
"""
Expand Down Expand Up @@ -1103,7 +1126,7 @@ def max_case_temperature(self) -> float:
>>> actuator.max_case_temperature
80.0
"""
return self._MOTOR_CONSTANTS.MAX_CASE_TEMPERATURE
return self.MOTOR_CONSTANTS.MAX_CASE_TEMPERATURE

@property
@abstractmethod
Expand Down Expand Up @@ -1153,7 +1176,7 @@ def max_winding_temperature(self) -> float:
>>> actuator.max_winding_temperature
120.0
"""
return self._MOTOR_CONSTANTS.MAX_WINDING_TEMPERATURE
return self.MOTOR_CONSTANTS.MAX_WINDING_TEMPERATURE

@property
def motor_zero_position(self) -> float:
Expand Down
39 changes: 4 additions & 35 deletions opensourceleg/actuators/dephy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from opensourceleg.actuators.base import (
CONTROL_MODE_CONFIGS,
CONTROL_MODES,
MOTOR_CONSTANTS,
ActuatorBase,
ControlGains,
ControlModeConfig,
MotorConstants,
)
from opensourceleg.actuators.decorators import (
check_actuator_connection,
Expand Down Expand Up @@ -42,7 +42,7 @@
IMPEDANCE_C = 0.0007812


DEPHY_ACTUATOR_CONSTANTS = MOTOR_CONSTANTS(
DEPHY_ACTUATOR_CONSTANTS = MotorConstants(
MOTOR_COUNT_PER_REV=16384,
NM_PER_AMP=0.1133,
MAX_CASE_TEMPERATURE=80,
Expand Down Expand Up @@ -151,8 +151,8 @@ def __init__(
offline=offline,
)

# Override motor constants to ensure setter is called
self.MOTOR_CONSTANTS = DEPHY_ACTUATOR_CONSTANTS
self.MOTOR_CONSTANTS.on_change = self._update_derived_constants
self._update_derived_constants()

self._debug_level: int = debug_level if dephy_log else 6
self._dephy_log: bool = dephy_log
Expand Down Expand Up @@ -695,37 +695,6 @@ def set_motor_impedance(
b=int(b * self.NM_S_PER_RAD_TO_MOTOR_UNITS),
)

@property
def MOTOR_CONSTANTS(self) -> MOTOR_CONSTANTS:
"""
Get the motor constants configuration.
Redefines the property from the ABC so we can set a setter.

Returns:
MOTOR_CONSTANTS: The motor constants.

Examples:
>>> constants = actuator.MOTOR_CONSTANTS
>>> constants.MAX_CASE_TEMPERATURE
80.0
"""
return self._MOTOR_CONSTANTS

@MOTOR_CONSTANTS.setter
def MOTOR_CONSTANTS(self, value: MOTOR_CONSTANTS) -> None:
"""
Setter for MOTOR_CONSTANTS property.
Updates the motor constants and recalculates derived conversion factors.

Args:
value (MOTOR_CONSTANTS): New motor constants to set.
"""

if not isinstance(value, MOTOR_CONSTANTS):
raise TypeError(f"Expected MOTOR_CONSTANTS, got {type(value)}")
self._MOTOR_CONSTANTS = value
self._update_derived_constants()

def _update_derived_constants(self) -> None:
"""
Recalculate conversion factors based on current motor constants.
Expand Down
4 changes: 2 additions & 2 deletions opensourceleg/actuators/moteus.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from opensourceleg.actuators.base import (
CONTROL_MODE_CONFIGS,
CONTROL_MODES,
MOTOR_CONSTANTS,
ActuatorBase,
ControlGains,
ControlModeConfig,
MotorConstants,
)
from opensourceleg.actuators.decorators import (
check_actuator_connection,
Expand Down Expand Up @@ -43,7 +43,7 @@

RAD_PER_DEG = np.pi / 180

MOTEUS_ACTUATOR_CONSTANTS = MOTOR_CONSTANTS(
MOTEUS_ACTUATOR_CONSTANTS = MotorConstants(
MOTOR_COUNT_PER_REV=16384,
NM_PER_AMP=0.1133,
MAX_CASE_TEMPERATURE=80,
Expand Down
8 changes: 4 additions & 4 deletions opensourceleg/math/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import numpy as np

from opensourceleg.actuators.base import MOTOR_CONSTANTS
from opensourceleg.actuators.base import MotorConstants

# Sensor validation constants
MIN_SENSIBLE_CURRENT = -80000.0 # -80A in mA
Expand Down Expand Up @@ -63,20 +63,20 @@ class ThermalModel:
- Soft-limiting thermal safety controller with formal guarantees
- Physically sensible value validation with slope-based projection
- Backward compatible API with enhanced methods
- Motor-specific parameter configuration via MOTOR_CONSTANTS
- Motor-specific parameter configuration via MotorConstants

Authors:
- Gray Thomas, Senthur Ayyappan

Args:
motor_constants: MOTOR_CONSTANTS instance with thermal parameters
motor_constants: MotorConstants instance with thermal parameters
actuator_tag: Actuator identifier for error messages. Defaults to "actuator"
ambient_temperature: Ambient temperature in °C. Defaults to 21.0
"""

def __init__(
self,
motor_constants: MOTOR_CONSTANTS,
motor_constants: MotorConstants,
actuator_tag: str = "actuator",
ambient_temperature: float = 21.0,
) -> None:
Expand Down
Loading