Diff of /arm_model/model.py [000000] .. [17a672]

Switch to unified view

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))