|
a |
|
b/arm_model/model.py |
|
|
1 |
import sympy as sp |
|
|
2 |
# import scipy as scp |
|
|
3 |
import numpy as np |
|
|
4 |
import pylab as plt |
|
|
5 |
from sympy import cos, sin |
|
|
6 |
from sympy.physics.mechanics import dynamicsymbols |
|
|
7 |
from pydy.codegen.ode_function_generators import generate_ode_function |
|
|
8 |
from util import coordinate_limiting_force, coriolis_matrix, \ |
|
|
9 |
apply_generalized_force, substitute |
|
|
10 |
from logger import Logger |
|
|
11 |
from functools import reduce |
|
|
12 |
|
|
|
13 |
# ------------------------------------------------------------------------ |
|
|
14 |
# ArmModel |
|
|
15 |
# ------------------------------------------------------------------------ |
|
|
16 |
|
|
|
17 |
|
|
|
18 |
class ArmModel: |
|
|
19 |
"""A planar toy model composed of 3 degrees of freedom and 9 muscles. |
|
|
20 |
|
|
|
21 |
The forces that act on the model are: gravity (tau_g), Coriolis and |
|
|
22 |
centrifugal (tau_c), coordinate limiting forces (tau_l) and viscous joints |
|
|
23 |
(tau_b). We assume the following: |
|
|
24 |
|
|
|
25 |
M qddot + tau_c + tau_g = tau + tau_l + tau_b |
|
|
26 |
|
|
|
27 |
M qddot = forcing |
|
|
28 |
|
|
|
29 |
forcing = tau - f, f = tau_c + tau_g - tau_l - tau_b |
|
|
30 |
|
|
|
31 |
or |
|
|
32 |
|
|
|
33 |
M qddot + f = tau |
|
|
34 |
|
|
|
35 |
Furthermore, we use 9 linear muscles. The musculotendon lengths and their |
|
|
36 |
derivatives are given by lm, lmd, lmdd. The muscle's moment arm matrix (R) |
|
|
37 |
is given by: |
|
|
38 |
|
|
|
39 |
R = d lm(q) / qdot |
|
|
40 |
|
|
|
41 |
such as that |
|
|
42 |
|
|
|
43 |
lmd = R qdot, lmdd = R qddot + Rdot qdot |
|
|
44 |
|
|
|
45 |
tau = -R^T f_m |
|
|
46 |
|
|
|
47 |
Notes |
|
|
48 |
----- |
|
|
49 |
|
|
|
50 |
(a) The model can represent a 2-link (used for validation) or 3-link system |
|
|
51 |
by changing nd = 2/3. Unfortunately, the muscle geometry is defined for the |
|
|
52 |
3-body case. |
|
|
53 |
|
|
|
54 |
(b) The Newton's 3rd law is applied for each force and torque. For example, |
|
|
55 |
for the first body: tau_1 - tau_2 is applied. |
|
|
56 |
|
|
|
57 |
(c) For the derivation of the equations of motion we used the Euler-Lagrange |
|
|
58 |
Method assuming: |
|
|
59 |
|
|
|
60 |
M qddot + C qdot + d V / dq = tau, V: potential energy, C: Coriolis Matrix |
|
|
61 |
|
|
|
62 |
""" |
|
|
63 |
|
|
|
64 |
def __init__(self, use_gravity=0, use_coordinate_limits=1, use_viscosity=1): |
|
|
65 |
""" |
|
|
66 |
""" |
|
|
67 |
self.logger = Logger('ArmModel') |
|
|
68 |
# n: DoFs, md: muscles |
|
|
69 |
self.nd = 3 |
|
|
70 |
self.md = 9 |
|
|
71 |
# used for selecting since we use 1-based indexing |
|
|
72 |
self.s = self.nd + 1 |
|
|
73 |
# used for iterating |
|
|
74 |
self.dim = list(range(1, self.s)) |
|
|
75 |
# enable/disable gravity in EoM [0/1] (simulation too slow) |
|
|
76 |
self.use_gravity = use_gravity |
|
|
77 |
# enable/disable coordinate limits in EoM [0/1] |
|
|
78 |
self.use_coordinate_limits = use_coordinate_limits |
|
|
79 |
# enable/disable viscous joints in EoM [0/1] |
|
|
80 |
self.use_viscosity = use_viscosity |
|
|
81 |
# true if constants are not substituted |
|
|
82 |
self.sub_constants = True |
|
|
83 |
# default model state |
|
|
84 |
self.state0 = np.array([ |
|
|
85 |
np.deg2rad(45.0), # q1 |
|
|
86 |
np.deg2rad(45.0), # q2 |
|
|
87 |
np.deg2rad(45.0), # q3 |
|
|
88 |
np.deg2rad(0.00), # u1 |
|
|
89 |
np.deg2rad(0.00), # u2 |
|
|
90 |
np.deg2rad(0.00) # u3 |
|
|
91 |
]) |
|
|
92 |
# reference pose used for calculating the optimal fiber length |
|
|
93 |
self.reference_pose = np.array([ |
|
|
94 |
np.deg2rad(60.0), # q1 |
|
|
95 |
np.deg2rad(70.0), # q2 |
|
|
96 |
np.deg2rad(50.0), # q3 |
|
|
97 |
]) |
|
|
98 |
self.logger.debug('Constructing model...') |
|
|
99 |
# construct model |
|
|
100 |
self.__construct_symbols() |
|
|
101 |
self.__construct_kinematics() |
|
|
102 |
self.__construct_coordinate_limiting_forces() |
|
|
103 |
self.__construct_kinetics() |
|
|
104 |
self.__construct_drawables() |
|
|
105 |
self.__construct_muscle_geometry() |
|
|
106 |
self.__define_muscle_parameters() |
|
|
107 |
self.__construct_rhs() |
|
|
108 |
|
|
|
109 |
def __construct_symbols(self): |
|
|
110 |
"""Define the symbols used by the analytical model. |
|
|
111 |
|
|
|
112 |
""" |
|
|
113 |
self.logger.debug('__construct_symbols') |
|
|
114 |
# define model constants 10 a's and b's are used but indexed from 1-9 |
|
|
115 |
# thus 0 is not used (for correspondence with the paper) |
|
|
116 |
self.a = sp.Matrix(sp.symbols('a0:10')) |
|
|
117 |
self.b = sp.Matrix(sp.symbols('b0:10')) |
|
|
118 |
# segment lengths |
|
|
119 |
self.L = sp.Matrix(sp.symbols('L0:4')) |
|
|
120 |
# distance to segment's CoM |
|
|
121 |
self.Lc = sp.Matrix(sp.symbols('Lc0:4')) |
|
|
122 |
# body parameters |
|
|
123 |
# inertia |
|
|
124 |
self.Iz = sp.Matrix(sp.symbols('Iz0:4')) |
|
|
125 |
# mass |
|
|
126 |
self.m = sp.Matrix(sp.symbols('m0:4')) |
|
|
127 |
# time |
|
|
128 |
self.t = sp.symbols('t') |
|
|
129 |
# gravity |
|
|
130 |
self.g = sp.symbols('g') |
|
|
131 |
# q's are used instead of $\theta$ |
|
|
132 |
self.q = sp.Matrix(dynamicsymbols('theta0:4')) |
|
|
133 |
self.u = sp.Matrix(dynamicsymbols('u:4')) |
|
|
134 |
self.dq = sp.Matrix([sp.diff(x, self.t) for x in self.q]) |
|
|
135 |
self.ddq = sp.Matrix([sp.diff(x, self.t) for x in self.dq]) |
|
|
136 |
# tau acting forces |
|
|
137 |
self.tau = sp.Matrix(dynamicsymbols('tau0:4')) |
|
|
138 |
# define a dictionary that maps symbols to values |
|
|
139 |
# parameters are derived from [1] |
|
|
140 |
self.constants = dict({self.a[1]: 0.055, self.a[2]: 0.055, self.a[3]: |
|
|
141 |
0.220, self.a[4]: 0.24, self.a[5]: 0.040, |
|
|
142 |
self.a[6]: 0.040, self.a[7]: 0.220, self.a[8]: |
|
|
143 |
0.06, self.a[9]: 0.26, self.b[1]: 0.080, |
|
|
144 |
self.b[2]: 0.11, self.b[3]: 0.030, self.b[4]: |
|
|
145 |
0.03, self.b[5]: 0.045, self.b[6]: 0.045, |
|
|
146 |
self.b[7]: 0.048, self.b[8]: 0.050, self.b[9]: |
|
|
147 |
0.03, self.L[1]: 0.310, self.L[2]: 0.270, |
|
|
148 |
self.L[3]: 0.150, self.Lc[1]: 0.165, self.Lc[2]: |
|
|
149 |
0.135, self.Lc[3]: 0.075, self.m[1]: 1.93, |
|
|
150 |
self.m[2]: 1.32, self.m[3]: 0.35, self.Iz[1]: |
|
|
151 |
0.0141, self.Iz[2]: 0.0120, self.Iz[3]: 0.001, |
|
|
152 |
self.g: 9.81}) |
|
|
153 |
|
|
|
154 |
# pickle workaround |
|
|
155 |
for q in self.q: |
|
|
156 |
q.__class__.__module__ = '__main__' |
|
|
157 |
|
|
|
158 |
for dq in self.dq: |
|
|
159 |
dq.__class__.__module__ = '__main__' |
|
|
160 |
|
|
|
161 |
for ddq in self.ddq: |
|
|
162 |
ddq.__class__.__module__ = '__main__' |
|
|
163 |
|
|
|
164 |
for u in self.u: |
|
|
165 |
u.__class__.__module__ = '__main__' |
|
|
166 |
|
|
|
167 |
for tau in self.tau: |
|
|
168 |
tau.__class__.__module__ = '__main__' |
|
|
169 |
|
|
|
170 |
# max isometrix force |
|
|
171 |
# TODO all muscles are equally strong |
|
|
172 |
fmax = 50 |
|
|
173 |
self.Fmax = sp.diag(fmax, fmax, fmax, |
|
|
174 |
fmax, 0.5 * fmax, 0.5 * fmax, |
|
|
175 |
fmax, fmax, 0.5 * fmax) |
|
|
176 |
|
|
|
177 |
def __construct_kinematics(self): |
|
|
178 |
"""Define points of interest for the derivation of the EoM. These are used for |
|
|
179 |
constructing the EoM. |
|
|
180 |
|
|
|
181 |
""" |
|
|
182 |
self.logger.debug('__construct_kinematics') |
|
|
183 |
L = self.L |
|
|
184 |
Lc = self.Lc |
|
|
185 |
q = self.q |
|
|
186 |
# define the spatial coordinates for the Lc in terms of Lc s' and q's |
|
|
187 |
# arm |
|
|
188 |
xc1 = sp.Matrix([Lc[1] * cos(q[1]), |
|
|
189 |
Lc[1] * sin(q[1]), |
|
|
190 |
0, |
|
|
191 |
0, |
|
|
192 |
0, |
|
|
193 |
q[1]]) |
|
|
194 |
# forearm |
|
|
195 |
xc2 = sp.Matrix([L[1] * cos(q[1]) + Lc[2] * cos(q[1] + q[2]), |
|
|
196 |
L[1] * sin(q[1]) + Lc[2] * sin(q[1] + q[2]), |
|
|
197 |
0, |
|
|
198 |
0, |
|
|
199 |
0, |
|
|
200 |
q[1] + q[2]]) |
|
|
201 |
|
|
|
202 |
# hand |
|
|
203 |
xc3 = sp.Matrix([L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) + |
|
|
204 |
Lc[3] * cos(q[1] + q[2] + q[3]), |
|
|
205 |
L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) + |
|
|
206 |
Lc[3] * sin(q[1] + q[2] + q[3]), |
|
|
207 |
0, |
|
|
208 |
0, |
|
|
209 |
0, |
|
|
210 |
q[1] + q[2] + q[3]]) |
|
|
211 |
self.xc = [sp.Matrix([0]), xc1, xc2, xc3] |
|
|
212 |
# CoM velocities |
|
|
213 |
self.vc = [sp.diff(x, self.t) for x in self.xc] |
|
|
214 |
# calculate CoM Jacobian |
|
|
215 |
self.Jc = [x.jacobian(self.QDot()) for x in self.vc] |
|
|
216 |
|
|
|
217 |
def __construct_kinetics(self): |
|
|
218 |
"""Construct model's dynamics (M, tau_c, tau_g). |
|
|
219 |
|
|
|
220 |
""" |
|
|
221 |
self.logger.debug('__construct_kinetics') |
|
|
222 |
# generate the mass matrix [6 x 6] for each body |
|
|
223 |
self.M = [sp.diag(self.m[i], self.m[i], self.m[i], 0, 0, |
|
|
224 |
self.Iz[i]) for i in self.dim] |
|
|
225 |
# dummy 0 for 1-based indexing |
|
|
226 |
self.M.insert(0, 0) |
|
|
227 |
# map spatial to generalized inertia |
|
|
228 |
self.M = [self.Jc[i].T * self.M[i] * self.Jc[i] |
|
|
229 |
for i in self.dim] |
|
|
230 |
# sum the mass product of each body |
|
|
231 |
self.M = reduce(lambda x, y: x + y, self.M) |
|
|
232 |
self.M = sp.trigsimp(self.M) |
|
|
233 |
# Coriolis matrix |
|
|
234 |
self.C = sp.trigsimp(coriolis_matrix(self.M, self.Q(), self.QDot())) |
|
|
235 |
# Coriolis forces |
|
|
236 |
self.tau_c = sp.trigsimp(self.C * sp.Matrix(self.QDot())) |
|
|
237 |
# potential energy due to gravity force |
|
|
238 |
self.V = 0 |
|
|
239 |
for i in self.dim: |
|
|
240 |
self.V = self.V + self.m[i] * self.g * self.xc[i][1] |
|
|
241 |
|
|
|
242 |
self.tau_g = sp.Matrix([sp.diff(self.V, x) for x in self.Q()]) |
|
|
243 |
|
|
|
244 |
def __construct_coordinate_limiting_forces(self): |
|
|
245 |
"""Construct coordinate limiting forces. |
|
|
246 |
|
|
|
247 |
""" |
|
|
248 |
self.logger.debug('__construct_coordinate_limiting_forces') |
|
|
249 |
|
|
|
250 |
a = 5 |
|
|
251 |
b = 50 |
|
|
252 |
q_low = [None, np.deg2rad(5), np.deg2rad(5), np.deg2rad(5)] |
|
|
253 |
q_up = [None, np.deg2rad(175), np.pi, np.deg2rad(100)] |
|
|
254 |
|
|
|
255 |
self.__tau_l = [coordinate_limiting_force(self.q[i], q_low[i], q_up[i], a, |
|
|
256 |
b) for i in range(1, self.s)] |
|
|
257 |
|
|
|
258 |
def __construct_drawables(self): |
|
|
259 |
"""Construct points of interest (e.g. muscle insertion, CoM, joint centers). |
|
|
260 |
|
|
|
261 |
""" |
|
|
262 |
self.logger.debug('__construct_drawables') |
|
|
263 |
a = self.a |
|
|
264 |
b = self.b |
|
|
265 |
q = self.q |
|
|
266 |
L = self.L |
|
|
267 |
# define muscle a |
|
|
268 |
self.ap = [[-a[1], sp.Rational(0)], # a1 |
|
|
269 |
[a[2], sp.Rational(0)], # a2 |
|
|
270 |
[a[3] * cos(q[1]), a[3] * sin(q[1])], # a3 |
|
|
271 |
[a[4] * cos(q[1]), a[4] * sin(q[1])], # a4 |
|
|
272 |
[-a[5], sp.Rational(0)], # a5 |
|
|
273 |
[a[6], sp.Rational(0)], # a6 |
|
|
274 |
[L[1] * cos(q[1]) + a[7] * cos(q[1] + q[2]), |
|
|
275 |
L[1] * sin(q[1]) + a[7] * sin(q[1] + q[2])], # a7 |
|
|
276 |
[L[1] * cos(q[1]) + a[8] * cos(q[1] + q[2]), |
|
|
277 |
L[1] * sin(q[1]) + a[8] * sin(q[1] + q[2])], # a8 |
|
|
278 |
[a[9] * cos(q[1]), a[9] * sin(q[1])] # a9 |
|
|
279 |
] |
|
|
280 |
# define muscle b |
|
|
281 |
self.bp = [[b[1] * cos(q[1]), b[1] * sin(q[1])], # b1 |
|
|
282 |
[b[2] * cos(q[1]), b[2] * sin(q[1])], # b2 |
|
|
283 |
[L[1] * cos(q[1]) + b[3] * cos(q[1] + q[2]), |
|
|
284 |
L[1] * sin(q[1]) + b[3] * sin(q[1] + q[2])], # b3 |
|
|
285 |
[L[1] * cos(q[1]) - b[4] * cos(q[1] + q[2]), |
|
|
286 |
L[1] * sin(q[1]) - b[4] * sin(q[1] + q[2])], # b4 |
|
|
287 |
[L[1] * cos(q[1]) + b[5] * cos(q[1] + q[2]), |
|
|
288 |
L[1] * sin(q[1]) + b[5] * sin(q[1] + q[2])], # b5 |
|
|
289 |
[L[1] * cos(q[1]) - b[6] * cos(q[1] + q[2]), |
|
|
290 |
L[1] * sin(q[1]) - b[6] * sin(q[1] + q[2])], # b6 |
|
|
291 |
[L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) + b[7] * cos(q[1] + q[2] + q[3]), |
|
|
292 |
L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) + b[7] * sin(q[1] + q[2] + q[3])], # b7 |
|
|
293 |
[L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) - b[8] * cos(q[1] + q[2] + q[3]), |
|
|
294 |
L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) - b[8] * sin(q[1] + q[2] + q[3])], # b8 |
|
|
295 |
[L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) + b[9] * cos(q[1] + q[2] + q[3]), |
|
|
296 |
L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) + b[9] * sin(q[1] + q[2] + q[3])] # b9 |
|
|
297 |
] |
|
|
298 |
# define CoM |
|
|
299 |
self.bc = [[self.xc[1][0], self.xc[1][1]], |
|
|
300 |
[self.xc[2][0], self.xc[2][1]], |
|
|
301 |
[self.xc[3][0], self.xc[3][1]] |
|
|
302 |
] |
|
|
303 |
# joint center |
|
|
304 |
self.jc = [[sp.Rational(0), sp.Rational(0)], |
|
|
305 |
[L[1] * cos(q[1]), L[1] * sin(q[1])], |
|
|
306 |
[L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]), |
|
|
307 |
L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2])] |
|
|
308 |
] |
|
|
309 |
# end effector |
|
|
310 |
if self.nd == 3: |
|
|
311 |
self.ee = sp.Matrix([L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) + |
|
|
312 |
L[3] * cos(q[1] + q[2] + q[3]), L[1] * |
|
|
313 |
sin(q[1]) + L[2] * sin(q[1] + q[2]) + L[3] * |
|
|
314 |
sin(q[1] + q[2] + q[3])]) |
|
|
315 |
elif self.nd == 2: |
|
|
316 |
self.ee = sp.Matrix([L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]), |
|
|
317 |
L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) |
|
|
318 |
]) |
|
|
319 |
|
|
|
320 |
def __construct_muscle_geometry(self): |
|
|
321 |
"""Construct muscle length function and moment arm. |
|
|
322 |
|
|
|
323 |
""" |
|
|
324 |
self.logger.debug('__construct_geometry') |
|
|
325 |
a = self.a |
|
|
326 |
b = self.b |
|
|
327 |
L = self.L |
|
|
328 |
q = self.q |
|
|
329 |
# muscle length ($l(q)$) |
|
|
330 |
self.lm = sp.Matrix([ |
|
|
331 |
(a[1] ** 2 + b[1] ** 2 + 2 * a[1] * |
|
|
332 |
b[1] * cos(q[1])) ** sp.Rational(1, 2), |
|
|
333 |
(a[2] ** 2 + b[2] ** 2 - 2 * a[2] * |
|
|
334 |
b[2] * cos(q[1])) ** sp.Rational(1, 2), |
|
|
335 |
((L[1] - a[3]) ** 2 + b[3] ** 2 + 2 * (L[1] - a[3]) |
|
|
336 |
* b[3] * cos(q[2])) ** sp.Rational(1, 2), |
|
|
337 |
((L[1] - a[4]) ** 2 + b[4] ** 2 - 2 * (L[1] - a[4]) |
|
|
338 |
* b[4] * cos(q[2])) ** sp.Rational(1, 2), |
|
|
339 |
(a[5] ** 2 + b[5] ** 2 + L[1] ** 2 + 2 * a[5] * L[1] * cos(q[1]) + |
|
|
340 |
2 * b[5] * L[1] * cos(q[2]) + 2 * a[5] * b[5] * cos(q[1] + q[2])) ** sp.Rational(1, 2), |
|
|
341 |
(a[6] ** 2 + b[6] ** 2 + L[1] ** 2 - 2 * a[6] * L[1] * cos(q[1]) - |
|
|
342 |
2 * b[6] * L[1] * cos(q[2]) + 2 * a[6] * b[6] * cos(q[1] + q[2])) ** sp.Rational(1, 2), |
|
|
343 |
((L[2] - a[7]) ** 2 + b[7] ** 2 + 2 * (L[2] - a[7]) |
|
|
344 |
* b[7] * cos(q[3])) ** sp.Rational(1, 2), |
|
|
345 |
((L[2] - a[8]) ** 2 + b[8] ** 2 - 2 * (L[2] - a[8]) |
|
|
346 |
* b[8] * cos(q[3])) ** sp.Rational(1, 2), |
|
|
347 |
((L[1] - a[9]) ** 2 + b[9] ** 2 + L[2] ** 2 + 2 * (L[1] - a[9]) * L[2] * cos(q[2]) + |
|
|
348 |
2 * b[9] * L[2] * cos(q[3]) + |
|
|
349 |
2 * (L[1] - a[9]) * b[9] * cos(q[2] + q[3])) ** sp.Rational(1, 2) |
|
|
350 |
]) |
|
|
351 |
|
|
|
352 |
self.lmd = sp.diff(self.lm, self.t) |
|
|
353 |
self.lmdd = sp.diff(self.lmd, self.t) |
|
|
354 |
|
|
|
355 |
# calculate the moment arm matrix and its derivatives |
|
|
356 |
self.R = sp.trigsimp(self.lm.jacobian(self.Q())) |
|
|
357 |
self.RDot = sp.diff(self.R, self.t) |
|
|
358 |
self.RDotQDot = self.RDot * sp.Matrix(self.U()) |
|
|
359 |
|
|
|
360 |
def __define_muscle_parameters(self): |
|
|
361 |
"""Define muscle parameters, such as optimal fiber length (reference pose) and |
|
|
362 |
tendon stiffness. |
|
|
363 |
|
|
|
364 |
""" |
|
|
365 |
self.logger.debug('__define_muscle_parameters') |
|
|
366 |
parameters = self.model_parameters(q=self.reference_pose, |
|
|
367 |
in_deg=False) |
|
|
368 |
self.lm0 = self.lm.subs(parameters) |
|
|
369 |
# the derivative of a matrix with a vector is a rank 3 tensor (3D |
|
|
370 |
# array), [dM/dq1, dM/dq2, ...] |
|
|
371 |
self.RTDq = sp.derive_by_array(self.R.transpose(), self.Q()) |
|
|
372 |
|
|
|
373 |
def __construct_rhs(self): |
|
|
374 |
"""Construct a callable function that can be used to integrate the mode. |
|
|
375 |
|
|
|
376 |
rhs = rhs(x, t, controller specifieds, parameters values) |
|
|
377 |
|
|
|
378 |
""" |
|
|
379 |
self.logger.debug('__construct_rhs') |
|
|
380 |
self.logger.debug('Use Gravity: ' + str(self.use_gravity)) |
|
|
381 |
self.logger.debug('Use Coordinate Limits: ' + |
|
|
382 |
str(self.use_coordinate_limits)) |
|
|
383 |
self.logger.debug('Use Viscous Joints: ' + str(self.use_viscosity)) |
|
|
384 |
|
|
|
385 |
# forces |
|
|
386 |
# Newton's 3rd law |
|
|
387 |
b = 0.05 |
|
|
388 |
tau = sp.Matrix(apply_generalized_force(self.Tau())) |
|
|
389 |
self.tau_l = sp.Matrix(apply_generalized_force(self.__tau_l)) |
|
|
390 |
self.tau_b = -b * sp.Matrix(apply_generalized_force(self.U())) |
|
|
391 |
# tau = sp.Matrix(self.Tau()) |
|
|
392 |
# self.tau_l = sp.Matrix(self.__tau_l) |
|
|
393 |
# self.tau_b = -b *sp.Matrix(self.U()) |
|
|
394 |
|
|
|
395 |
# M qdd + tau_c + tau_g = tau + tau_l + tau_b-> M qdd = forcing |
|
|
396 |
# f = tau_c + tau_g - tau_l - tau_b |
|
|
397 |
self.f = self.tau_c \ |
|
|
398 |
+ self.use_gravity * self.tau_g \ |
|
|
399 |
- self.use_coordinate_limits * self.tau_l \ |
|
|
400 |
- self.use_viscosity * self.tau_b |
|
|
401 |
self.forcing = tau - self.f |
|
|
402 |
|
|
|
403 |
# substitute dq with u (required for code-gen) |
|
|
404 |
for i in range(0, self.forcing.shape[0]): |
|
|
405 |
self.forcing = self.forcing.subs(self.dq[i + 1], self.u[i + 1]) |
|
|
406 |
|
|
|
407 |
# rhs |
|
|
408 |
self.coordinates = sp.Matrix(self.Q()) |
|
|
409 |
self.speeds = sp.Matrix(self.U()) |
|
|
410 |
self.coordinates_derivatives = self.speeds |
|
|
411 |
self.specifieds = sp.Matrix(self.Tau()) |
|
|
412 |
self.rhs = generate_ode_function( |
|
|
413 |
self.forcing, |
|
|
414 |
self.coordinates, |
|
|
415 |
self.speeds, |
|
|
416 |
list(self.constants.keys()), |
|
|
417 |
mass_matrix=self.M, |
|
|
418 |
coordinate_derivatives=self.coordinates_derivatives, |
|
|
419 |
specifieds=self.specifieds) |
|
|
420 |
|
|
|
421 |
# ------------------------------------------------------------------------ |
|
|
422 |
# ArmModel public interface |
|
|
423 |
# ------------------------------------------------------------------------ |
|
|
424 |
|
|
|
425 |
def Q(self): |
|
|
426 |
'Get active coordinates (q) [1, s].' |
|
|
427 |
return self.q[1:self.s] |
|
|
428 |
|
|
|
429 |
def QDot(self): |
|
|
430 |
'Get active speeds (qdot) [1, s].' |
|
|
431 |
return self.dq[1:self.s] |
|
|
432 |
|
|
|
433 |
def U(self): |
|
|
434 |
'Get active speeds (qdot = u) [1, s].' |
|
|
435 |
return self.u[1:self.s] |
|
|
436 |
|
|
|
437 |
def Tau(self): |
|
|
438 |
'Get active speeds (tau) [1, s].' |
|
|
439 |
return self.tau[1:self.s] |
|
|
440 |
|
|
|
441 |
def model_parameters(self, **kwargs): |
|
|
442 |
"""Get the model parameters dictionary given q, u, in_deg=[True/False]. |
|
|
443 |
|
|
|
444 |
Parameters |
|
|
445 |
---------- |
|
|
446 |
|
|
|
447 |
kwargs: q=q, u=u, in_deg=[True/False] |
|
|
448 |
|
|
|
449 |
""" |
|
|
450 |
|
|
|
451 |
expected_args = ["q", "u", "in_deg"] |
|
|
452 |
kwargsdict = {} |
|
|
453 |
for key in list(kwargs.keys()): |
|
|
454 |
if key in expected_args: |
|
|
455 |
kwargsdict[key] = kwargs[key] |
|
|
456 |
else: |
|
|
457 |
raise Exception("Unexpected Argument") |
|
|
458 |
|
|
|
459 |
return self.__model_parameters(kwargsdict) |
|
|
460 |
|
|
|
461 |
def __model_parameters(self, dic): |
|
|
462 |
"""A private implementation of model_parameters. |
|
|
463 |
|
|
|
464 |
Parameters |
|
|
465 |
---------- |
|
|
466 |
|
|
|
467 |
dic: dictionary containing {q, u, in_deg} |
|
|
468 |
|
|
|
469 |
""" |
|
|
470 |
|
|
|
471 |
constants = {} |
|
|
472 |
if self.sub_constants: |
|
|
473 |
constants = self.constants.copy() |
|
|
474 |
|
|
|
475 |
q = self.Q() |
|
|
476 |
dq = self.QDot() |
|
|
477 |
u = self.U() |
|
|
478 |
in_deg = False |
|
|
479 |
if "in_deg" in list(dic.keys()): |
|
|
480 |
in_deg = dic["in_deg"] |
|
|
481 |
|
|
|
482 |
if "q" in list(dic.keys()): |
|
|
483 |
qs = dic["q"] |
|
|
484 |
if in_deg: |
|
|
485 |
qs = np.deg2rad(dic["q"]) |
|
|
486 |
|
|
|
487 |
constants.update({q[i]: qs[i] for i in range(0, self.nd)}) |
|
|
488 |
|
|
|
489 |
if "u" in list(dic.keys()): |
|
|
490 |
us = dic["u"] |
|
|
491 |
|
|
|
492 |
constants.update({dq[i]: us[i] for i in range(0, self.nd)}) |
|
|
493 |
|
|
|
494 |
constants.update({u[i]: us[i] for i in range(0, self.nd)}) |
|
|
495 |
|
|
|
496 |
return constants |
|
|
497 |
|
|
|
498 |
def pre_substitute_parameters(self): |
|
|
499 |
"""Substitute model parameters into the variables of the model to improve speed. |
|
|
500 |
|
|
|
501 |
""" |
|
|
502 |
|
|
|
503 |
self.logger.debug('pre_substitute_parameters') |
|
|
504 |
self.sub_constants = False |
|
|
505 |
constants = self.constants |
|
|
506 |
|
|
|
507 |
self.M = self.M.subs(constants) |
|
|
508 |
self.tau_c = self.tau_c.subs(constants) |
|
|
509 |
self.tau_g = self.tau_g.subs(constants) |
|
|
510 |
self.tau_l = self.tau_l.subs(constants) |
|
|
511 |
self.tau_b = self.tau_b.subs(constants) |
|
|
512 |
self.f = self.f.subs(constants) |
|
|
513 |
|
|
|
514 |
self.lm = self.lm.subs(constants) |
|
|
515 |
self.lmd = self.lmd.subs(constants) |
|
|
516 |
self.lmdd = self.lmdd.subs(constants) |
|
|
517 |
self.R = self.R.subs(constants) |
|
|
518 |
self.RDot = self.RDot.subs(constants) |
|
|
519 |
self.RDotQDot = self.RDotQDot.subs(constants) |
|
|
520 |
self.RTDq = self.RTDq.subs(constants) |
|
|
521 |
|
|
|
522 |
self.ap = substitute(self.ap, constants) |
|
|
523 |
self.bp = substitute(self.bp, constants) |
|
|
524 |
self.bc = substitute(self.bc, constants) |
|
|
525 |
self.jc = substitute(self.jc, constants) |
|
|
526 |
self.ee = substitute(self.ee, constants) |
|
|
527 |
|
|
|
528 |
# self.L = self.L.subs(constants) |
|
|
529 |
# self.Lc = self.Lc.subs(constants) |
|
|
530 |
# self.Iz = self.Iz.subs(constants) |
|
|
531 |
|
|
|
532 |
self.xc = substitute(self.xc, constants) |
|
|
533 |
self.vc = substitute(self.vc, constants) |
|
|
534 |
self.Jc = substitute(self.Jc, constants) |
|
|
535 |
|
|
|
536 |
# self.m = self.m.subs(constants) |
|
|
537 |
# self.g = self.g.subs(constants) |
|
|
538 |
|
|
|
539 |
def draw_model(self, q, in_deg, ax=None, scale=0.8, use_axis_limits=True, alpha=1.0, |
|
|
540 |
text=True): |
|
|
541 |
"""Draws the 3D toy model. |
|
|
542 |
|
|
|
543 |
Parameters |
|
|
544 |
---------- |
|
|
545 |
|
|
|
546 |
q: coordinate values in degrees or rad |
|
|
547 |
in_deg: True/False |
|
|
548 |
ax: axis 1D |
|
|
549 |
scale: if figure is small this helps in visualizing details |
|
|
550 |
use_axis_limits: use axis limits from max length |
|
|
551 |
|
|
|
552 |
""" |
|
|
553 |
if self.nd == 2: |
|
|
554 |
self.logger.debug('draw_model supports 3DoFs case') |
|
|
555 |
return |
|
|
556 |
|
|
|
557 |
constants = self.model_parameters(q=q, in_deg=in_deg) |
|
|
558 |
|
|
|
559 |
joints = substitute(self.jc, constants) |
|
|
560 |
muscle_a = substitute(self.ap, constants) |
|
|
561 |
muscle_b = substitute(self.bp, constants) |
|
|
562 |
end_effector = substitute(self.ee, constants) |
|
|
563 |
CoM = substitute(self.bc, constants) |
|
|
564 |
|
|
|
565 |
linewidth = 4 * scale |
|
|
566 |
gd_markersize = 14 * scale |
|
|
567 |
jc_markersize = 12 * scale |
|
|
568 |
mo_markersize = 7 * scale |
|
|
569 |
ef_markersize = 15 * scale |
|
|
570 |
fontsize = 12 * scale |
|
|
571 |
|
|
|
572 |
if ax == None: |
|
|
573 |
fig, ax = plt.subplots(1, 1, figsize=(5, 5)) |
|
|
574 |
|
|
|
575 |
# arm |
|
|
576 |
ax.plot([joints[0, 0], joints[1, 0]], [joints[0, 1], joints[1, 1]], |
|
|
577 |
'r', linewidth=linewidth, alpha=alpha) |
|
|
578 |
# forearm |
|
|
579 |
ax.plot([muscle_b[5, 0], joints[2, 0]], [muscle_b[5, 1], joints[2, 1]], |
|
|
580 |
'r', linewidth=linewidth, alpha=alpha) |
|
|
581 |
# hand |
|
|
582 |
ax.plot([muscle_b[7, 0], end_effector[0]], [muscle_b[7, 1], end_effector[1]], |
|
|
583 |
'r', linewidth=linewidth, alpha=alpha) |
|
|
584 |
# muscles |
|
|
585 |
for i in range(0, muscle_a.shape[0]): |
|
|
586 |
ax.plot([muscle_a[i, 0], muscle_b[i, 0]], [ |
|
|
587 |
muscle_a[i, 1], muscle_b[i, 1]], 'b', alpha=alpha) |
|
|
588 |
if text: |
|
|
589 |
ax.text(muscle_a[i, 0], muscle_a[i, 1], |
|
|
590 |
r'$a_' + str(i + 1) + '$', fontsize=fontsize, alpha=alpha) |
|
|
591 |
ax.text(muscle_b[i, 0], muscle_b[i, 1], |
|
|
592 |
r'$b_' + str(i + 1) + '$', fontsize=fontsize, alpha=alpha) |
|
|
593 |
ax.text((muscle_b[i, 0] + muscle_a[i, 0]) / 2.0, |
|
|
594 |
(muscle_b[i, 1] + muscle_a[i, 1]) / 2.0, |
|
|
595 |
r'$l_' + str(i + 1) + '$', fontsize=fontsize, alpha=alpha) |
|
|
596 |
|
|
|
597 |
# joint centers |
|
|
598 |
ax.plot(joints[:, 0], joints[:, 1], 'or', |
|
|
599 |
markersize=gd_markersize, alpha=alpha) |
|
|
600 |
if text: |
|
|
601 |
for i in range(0, joints.shape[0]): |
|
|
602 |
ax.text(joints[i, 0], joints[i, 1], r'$J_' + |
|
|
603 |
str(i + 1) + '$', fontsize=fontsize, alpha=alpha) |
|
|
604 |
|
|
|
605 |
# CoM |
|
|
606 |
ax.plot(CoM[:, 0], CoM[:, 1], 'oy', |
|
|
607 |
markersize=jc_markersize, alpha=alpha) |
|
|
608 |
if text: |
|
|
609 |
for i in range(0, CoM.shape[0]): |
|
|
610 |
ax.text(CoM[i, 0], CoM[i, 1], r'$Lc_' + |
|
|
611 |
str(i + 1) + '$', fontsize=fontsize, alpha=alpha) |
|
|
612 |
|
|
|
613 |
# end effector |
|
|
614 |
ax.plot(end_effector[0], end_effector[1], |
|
|
615 |
'<b', markersize=ef_markersize, alpha=alpha) |
|
|
616 |
# muscle origin |
|
|
617 |
ax.plot(muscle_a[:, 0], muscle_a[:, 1], 'dy', |
|
|
618 |
markersize=mo_markersize, alpha=alpha) |
|
|
619 |
# muscle insertion |
|
|
620 |
ax.plot(muscle_b[:, 0], muscle_b[:, 1], 'db', |
|
|
621 |
markersize=mo_markersize, alpha=alpha) |
|
|
622 |
|
|
|
623 |
ax.axis('equal') |
|
|
624 |
ax.set_title('Model Pose') |
|
|
625 |
ax.set_xlabel('$x \; (m)$') |
|
|
626 |
ax.set_ylabel('$y \; (m)$') |
|
|
627 |
|
|
|
628 |
# axis limits |
|
|
629 |
if use_axis_limits: |
|
|
630 |
L_max = self.constants[self.L[1]] + \ |
|
|
631 |
self.constants[self.L[2]] + self.constants[self.L[3]] |
|
|
632 |
ax.set_xlim([-L_max, L_max]) |
|
|
633 |
ax.set_ylim([-L_max / 2, 1.5 * L_max]) |
|
|
634 |
|
|
|
635 |
|
|
|
636 |
# ------------------------------------------------------------------------ |
|
|
637 |
# ArmModel tests |
|
|
638 |
# ------------------------------------------------------------------------ |
|
|
639 |
|
|
|
640 |
def test_muscle_geometry(self): |
|
|
641 |
"""Evaluate the muscle geometry for a random pose against the ground truth. |
|
|
642 |
|
|
|
643 |
""" |
|
|
644 |
ma = self.ap |
|
|
645 |
mb = self.bp |
|
|
646 |
lmt = sp.Matrix([sp.trigsimp( |
|
|
647 |
sp.sqrt(pow(ma[i][0] - mb[i][0], 2) + |
|
|
648 |
pow(ma[i][1] - mb[i][1], 2))) for i in range(0, len(ma))]) |
|
|
649 |
|
|
|
650 |
pose = self.model_parameters(q=np.random.random(3), in_deg=False) |
|
|
651 |
assert_if_same(self.lm.subs(pose), lmt.subs(pose)) |