|
a |
|
b/myosuite/envs/myo/fatigue.py |
|
|
1 |
from myosuite.utils import gym |
|
|
2 |
import mujoco |
|
|
3 |
import numpy as np |
|
|
4 |
|
|
|
5 |
class CumulativeFatigue(): |
|
|
6 |
# 3CC-r model, adapted from https://dl.acm.org/doi/pdf/10.1145/3313831.3376701 for muscles |
|
|
7 |
# based on the implementation from Aleksi Ikkala and Florian Fischer https://github.com/aikkala/user-in-the-box/blob/main/uitb/bm_models/effort_models.py |
|
|
8 |
def __init__(self, mj_model, frame_skip=1, seed=None): |
|
|
9 |
self._r = 10 * 15 # Recovery time multiplier i.e. how many times more than during rest intervals https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6092960/ (factor 10 to compensate for 0.1 below) |
|
|
10 |
self._F = 0.00912 # Fatigue coefficients (default parameter was identified for elbow torque https://pubmed.ncbi.nlm.nih.gov/22579269/) |
|
|
11 |
self._R = 0.1 * 0.00094 # Recivery coefficients (default parameter was identified for elbow torque https://pubmed.ncbi.nlm.nih.gov/22579269/; factor 0.1 to get an approx. 1% R/F ratio) |
|
|
12 |
# self.na = mj_model.na |
|
|
13 |
self._dt = mj_model.opt.timestep * frame_skip # dt might be different from model dt because it might include a frame skip |
|
|
14 |
muscle_act_ind = mj_model.actuator_dyntype == mujoco.mjtDyn.mjDYN_MUSCLE |
|
|
15 |
self.na = sum(muscle_act_ind) #WARNING: here, self.na denotes the number of muscle actuators only! |
|
|
16 |
self._tauact = np.array([mj_model.actuator_dynprm[i][0] for i in range(len(muscle_act_ind)) if muscle_act_ind[i]]) |
|
|
17 |
self._taudeact = np.array([mj_model.actuator_dynprm[i][1] for i in range(len(muscle_act_ind)) if muscle_act_ind[i]]) |
|
|
18 |
self._MA = np.zeros((self.na,)) # Muscle Active |
|
|
19 |
self._MR = np.ones((self.na,)) # Muscle Resting |
|
|
20 |
self._MF = np.zeros((self.na,)) # Muscle Fatigue |
|
|
21 |
self.TL = np.zeros((self.na,)) # Target Load |
|
|
22 |
|
|
|
23 |
self.seed(seed) # Create own Random Number Generator (RNG) used when reset is called with fatigue_reset_random=True |
|
|
24 |
### NOTE: the seed from CumulativeFatigue is not synchronised with the seed used for the rest of MujocoEnv! |
|
|
25 |
|
|
|
26 |
def set_FatigueCoefficient(self, F): |
|
|
27 |
# Set Fatigue coefficients |
|
|
28 |
self._F = F |
|
|
29 |
|
|
|
30 |
def set_RecoveryCoefficient(self, R): |
|
|
31 |
# Set Recovery coefficients |
|
|
32 |
self._R = R |
|
|
33 |
|
|
|
34 |
def set_RecoveryMultiplier(self, r): |
|
|
35 |
# Set Recovery time multiplier |
|
|
36 |
self._r = r |
|
|
37 |
|
|
|
38 |
def compute_act(self, act): |
|
|
39 |
# Get target load (actual activation, which might be reached only with some "effort", |
|
|
40 |
# depending on how many muscles can be activated (fast enough) and how many are in fatigue state) |
|
|
41 |
self.TL = act.copy() |
|
|
42 |
|
|
|
43 |
# Calculate effective time constant tau (see https://mujoco.readthedocs.io/en/stable/modeling.html#muscles) |
|
|
44 |
self._LD = 1/self._tauact*(0.5 + 1.5*self._MA) |
|
|
45 |
self._LR = (0.5 + 1.5*self._MA)/self._taudeact |
|
|
46 |
## TODO: account for smooth transition phase of length tausmooth (tausmooth = mj_model.actuator_dynprm[i][2])? |
|
|
47 |
|
|
|
48 |
# Calculate C(t) -- transfer rate between MR and MA |
|
|
49 |
C = np.zeros_like(self._MA) |
|
|
50 |
idxs = (self._MA < self.TL) & (self._MR > (self.TL - self._MA)) |
|
|
51 |
C[idxs] = self._LD[idxs] * (self.TL[idxs] - self._MA[idxs]) |
|
|
52 |
idxs = (self._MA < self.TL) & (self._MR <= (self.TL - self._MA)) |
|
|
53 |
C[idxs] = self._LD[idxs] * self._MR[idxs] |
|
|
54 |
idxs = self._MA >= self.TL |
|
|
55 |
C[idxs] = self._LR[idxs] * (self.TL[idxs] - self._MA[idxs]) |
|
|
56 |
|
|
|
57 |
# Calculate rR |
|
|
58 |
rR = np.zeros_like(self._MA) |
|
|
59 |
idxs = self._MA >= self.TL |
|
|
60 |
rR[idxs] = self._r*self._R |
|
|
61 |
idxs = self._MA < self.TL |
|
|
62 |
rR[idxs] = self._R |
|
|
63 |
|
|
|
64 |
# Clip C(t) if needed, to ensure that MA, MR, and MF remain between 0 and 1 |
|
|
65 |
C = np.clip(C, np.maximum(-self._MA/self._dt + self._F*self._MA, (self._MR - 1)/self._dt + rR*self._MF), |
|
|
66 |
np.minimum((1 - self._MA)/self._dt + self._F*self._MA, self._MR/self._dt + rR*self._MF)) |
|
|
67 |
|
|
|
68 |
# Update MA, MR, MF |
|
|
69 |
dMA = (C - self._F*self._MA)*self._dt |
|
|
70 |
dMR = (-C + rR*self._MF)*self._dt |
|
|
71 |
dMF = (self._F*self._MA - rR*self._MF)*self._dt |
|
|
72 |
self._MA += dMA |
|
|
73 |
self._MR += dMR |
|
|
74 |
self._MF += dMF |
|
|
75 |
|
|
|
76 |
return self._MA, self._MR, self._MF |
|
|
77 |
|
|
|
78 |
def get_effort(self): |
|
|
79 |
# Calculate effort |
|
|
80 |
return np.linalg.norm(self._MA - self.TL) |
|
|
81 |
|
|
|
82 |
def reset(self, fatigue_reset_vec=None, fatigue_reset_random=False): |
|
|
83 |
if fatigue_reset_random: |
|
|
84 |
assert fatigue_reset_vec is None, "Cannot use 'fatigue_reset_vec' if fatigue_reset_random=False." |
|
|
85 |
non_fatigued_muscles = self.np_random.random(size=(self.na,)) |
|
|
86 |
active_percentage = self.np_random.random(size=(self.na,)) |
|
|
87 |
self._MA = non_fatigued_muscles * active_percentage # Muscle Active |
|
|
88 |
self._MR = non_fatigued_muscles * (1 - active_percentage) # Muscle Resting |
|
|
89 |
self._MF = 1 - non_fatigued_muscles # Muscle Fatigue |
|
|
90 |
else: |
|
|
91 |
if fatigue_reset_vec is not None: |
|
|
92 |
assert len(fatigue_reset_vec) == self.na, f"Invalid length of initial/reset fatigue vector (expected {self.na}, but obtained {len(fatigue_reset_vec)})." |
|
|
93 |
self._MF = fatigue_reset_vec # Muscle Fatigue |
|
|
94 |
self._MR = 1 - fatigue_reset_vec # Muscle Resting |
|
|
95 |
self._MA = np.zeros((self.na,)) # Muscle Active |
|
|
96 |
else: |
|
|
97 |
self._MA = np.zeros((self.na,)) # Muscle Active |
|
|
98 |
self._MR = np.ones((self.na,)) # Muscle Resting |
|
|
99 |
self._MF = np.zeros((self.na,)) # Muscle Fatigue |
|
|
100 |
|
|
|
101 |
def seed(self, seed=None): |
|
|
102 |
""" |
|
|
103 |
Set random number seed |
|
|
104 |
""" |
|
|
105 |
self.input_seed = seed |
|
|
106 |
self.np_random, seed = gym.utils.seeding.np_random(seed) |
|
|
107 |
return [seed] |
|
|
108 |
|
|
|
109 |
@property |
|
|
110 |
def MF(self): |
|
|
111 |
return self._MF |
|
|
112 |
|
|
|
113 |
@property |
|
|
114 |
def MR(self): |
|
|
115 |
return self._MR |
|
|
116 |
|
|
|
117 |
@property |
|
|
118 |
def MA(self): |
|
|
119 |
return self._MA |
|
|
120 |
|
|
|
121 |
@property |
|
|
122 |
def F(self): |
|
|
123 |
return self._F |
|
|
124 |
|
|
|
125 |
@property |
|
|
126 |
def R(self): |
|
|
127 |
return self._R |
|
|
128 |
|
|
|
129 |
@property |
|
|
130 |
def r(self): |
|
|
131 |
return self._r |
|
|
132 |
|
|
|
133 |
|