68 lines
2.0 KiB
Python
68 lines
2.0 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from math import pi, sin
|
|
|
|
import numpy as np
|
|
|
|
|
|
@dataclass
|
|
class PDGaitConfig:
|
|
frequency_hz: float = 1.4
|
|
kp: float = 24.0
|
|
kd: float = 2.5
|
|
near_bias: float = 0.0
|
|
near_amp: float = 1.0
|
|
far_phase_offset: float = pi
|
|
|
|
|
|
def angle_wrap(rad: float) -> float:
|
|
return (rad + pi) % (2.0 * pi) - pi
|
|
|
|
|
|
class PDGaitController:
|
|
"""Phase-based crank PD controller for the tandem 4-leg linkage."""
|
|
|
|
JOINTS = (
|
|
"near_crank_joint",
|
|
"far_crank_joint",
|
|
)
|
|
|
|
def __init__(self, model, data, config: PDGaitConfig | None = None):
|
|
self.model = model
|
|
self.data = data
|
|
self.cfg = config or PDGaitConfig()
|
|
|
|
self._joint_info = []
|
|
for name in self.JOINTS:
|
|
jid = model.joint(name).id
|
|
actuator_name = "near_crank_motor" if name.startswith("near") else "far_crank_motor"
|
|
aid = model.actuator(actuator_name).id
|
|
qpos_adr = model.jnt_qposadr[jid]
|
|
qvel_adr = model.jnt_dofadr[jid]
|
|
ctrl_min, ctrl_max = model.actuator_ctrlrange[aid]
|
|
self._joint_info.append((aid, qpos_adr, qvel_adr, ctrl_min, ctrl_max))
|
|
|
|
def desired_positions(self, t: float) -> np.ndarray:
|
|
w = 2.0 * pi * self.cfg.frequency_hz
|
|
phase_n = w * t
|
|
phase_f = phase_n + self.cfg.far_phase_offset
|
|
|
|
q_des = np.array(
|
|
[
|
|
self.cfg.near_bias + self.cfg.near_amp * sin(phase_n),
|
|
self.cfg.near_bias + self.cfg.near_amp * sin(phase_f),
|
|
],
|
|
dtype=np.float64,
|
|
)
|
|
return q_des
|
|
|
|
def step(self, t: float) -> None:
|
|
q_des = self.desired_positions(t)
|
|
for i, (aid, qpos_adr, qvel_adr, ctrl_min, ctrl_max) in enumerate(self._joint_info):
|
|
q = self.data.qpos[qpos_adr]
|
|
qd = self.data.qvel[qvel_adr]
|
|
err = angle_wrap(float(q_des[i] - q))
|
|
tau = self.cfg.kp * err - self.cfg.kd * qd
|
|
self.data.ctrl[aid] = np.clip(tau, ctrl_min, ctrl_max)
|