Source code for myogen.simulator.neuron.joint_dynamics
"""
Joint dynamics module for closed-loop neuromechanical control.
This module provides the JointDynamics class that integrates muscle torques
into joint motion using second-order biomechanical equations. This enables
closed-loop control where motor commands drive joint movement through
realistic joint biomechanics.
"""
import numpy as np
from typing import Tuple
from myogen.utils.decorators import beartowertype
[docs]
@beartowertype
class JointDynamics:
"""
Joint dynamics integrator for closed-loop neuromechanical control.
This class implements second-order joint dynamics using the equation:
I⋅α = τ - B⋅ω - K⋅θ
where α = angular acceleration, τ = torque, ω = angular velocity,
θ = joint angle, I = inertia, B = damping, K = stiffness.
Parameters
----------
inertia__kg_m2 : float
Joint rotational inertia in kg⋅m².
damping__Nm_s_per_rad : float
Joint viscous damping coefficient in N⋅m⋅s/rad.
Controls velocity-dependent resistance.
stiffness__Nm_per_rad : float, default=0.0
Joint elastic stiffness in N⋅m/rad.
Set to 0 for passive joints, >0 for spring-loaded joints.
initial_angle__deg : float, default=0.0
Initial joint angle in degrees.
initial_velocity__deg_per_s : float, default=0.0
Initial angular velocity in degrees per second.
Attributes
----------
angle__rad : float
Current joint angle in radians.
velocity__rad_per_s : float
Current angular velocity in rad/s.
angle__deg : float
Current joint angle in degrees (computed property).
"""
[docs]
def __init__(
self,
inertia__kg_m2: float,
damping__Nm_s_per_rad: float,
stiffness__Nm_per_rad: float = 0.0,
initial_angle__deg: float = 0.0,
initial_velocity__deg_per_s: float = 0.0,
) -> None:
# Input validation
if inertia__kg_m2 <= 0:
raise ValueError(
f"inertia__kg_m2 must be positive, got {inertia__kg_m2}. "
"Typical values: finger=0.001-0.01, elbow=0.1-0.5, knee=1.0-5.0 kg⋅m²"
)
if damping__Nm_s_per_rad < 0:
raise ValueError(
f"damping__Nm_s_per_rad must be non-negative, got {damping__Nm_s_per_rad}. "
"Typical values: 0.001-0.1 N⋅m⋅s/rad"
)
if stiffness__Nm_per_rad < 0:
raise ValueError(
f"stiffness__Nm_per_rad must be non-negative, got {stiffness__Nm_per_rad}. "
"Use 0 for passive joints, >0 for spring-loaded joints"
)
# Immutable public parameters
self.inertia__kg_m2 = inertia__kg_m2
self.damping__Nm_s_per_rad = damping__Nm_s_per_rad
self.stiffness__Nm_per_rad = stiffness__Nm_per_rad
self.initial_angle__deg = initial_angle__deg
self.initial_velocity__deg_per_s = initial_velocity__deg_per_s
# Private working copies
self._inertia = inertia__kg_m2
self._damping = damping__Nm_s_per_rad
self._stiffness = stiffness__Nm_per_rad
# Joint state variables
self.angle__rad = np.radians(initial_angle__deg)
self.velocity__rad_per_s = np.radians(initial_velocity__deg_per_s)
@property
def angle__deg(self) -> float:
"""Current joint angle in degrees."""
return np.degrees(self.angle__rad)
[docs]
def integrate(self, torque__Nm: float, dt__s: float) -> Tuple[float, float]:
"""
Integrate joint dynamics for one time step.
Parameters
----------
torque__Nm : float
Applied muscle torque in N⋅m.
dt__s : float
Integration time step in seconds.
Returns
-------
tuple[float, float]
Updated (angle__deg, velocity__deg_per_s).
"""
# Second-order dynamics: I⋅α = τ - B⋅ω - K⋅θ
spring_torque = -self._stiffness * self.angle__rad
damping_torque = -self._damping * self.velocity__rad_per_s
angular_acceleration = (
torque__Nm + spring_torque + damping_torque
) / self._inertia
# Euler integration (could use RK4 for higher accuracy)
self.velocity__rad_per_s += angular_acceleration * dt__s
self.angle__rad += self.velocity__rad_per_s * dt__s
return self.angle__deg, np.degrees(self.velocity__rad_per_s)
[docs]
def reset(self) -> None:
"""Reset joint to initial conditions."""
self.angle__rad = np.radians(self.initial_angle__deg)
self.velocity__rad_per_s = np.radians(self.initial_velocity__deg_per_s)
[docs]
def get_state(self) -> dict:
"""
Get current joint state.
Returns
-------
dict
Dictionary containing current angle, velocity, and parameters.
"""
return {
"angle__deg": self.angle__deg,
"angle__rad": self.angle__rad,
"velocity__deg_per_s": np.degrees(self.velocity__rad_per_s),
"velocity__rad_per_s": self.velocity__rad_per_s,
"inertia__kg_m2": self._inertia,
"damping__Nm_s_per_rad": self._damping,
"stiffness__Nm_per_rad": self._stiffness,
}