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

Switch to side-by-side view

--- a
+++ b/arm_model/model.py
@@ -0,0 +1,651 @@
+import sympy as sp
+# import scipy as scp
+import numpy as np
+import pylab as plt
+from sympy import cos, sin
+from sympy.physics.mechanics import dynamicsymbols
+from pydy.codegen.ode_function_generators import generate_ode_function
+from util import coordinate_limiting_force, coriolis_matrix, \
+    apply_generalized_force, substitute
+from logger import Logger
+from functools import reduce
+
+# ------------------------------------------------------------------------
+# ArmModel
+# ------------------------------------------------------------------------
+
+
+class ArmModel:
+    """A planar toy model composed of 3 degrees of freedom and 9 muscles.
+
+    The forces that act on the model are: gravity (tau_g), Coriolis and
+    centrifugal (tau_c), coordinate limiting forces (tau_l) and viscous joints
+    (tau_b). We assume the following:
+
+    M qddot + tau_c + tau_g = tau + tau_l + tau_b
+
+    M qddot = forcing
+
+    forcing = tau - f, f = tau_c + tau_g - tau_l - tau_b
+
+    or
+
+    M qddot + f = tau
+
+    Furthermore, we use 9 linear muscles. The musculotendon lengths and their
+    derivatives are given by lm, lmd, lmdd. The muscle's moment arm matrix (R)
+    is given by:
+
+    R =  d lm(q) / qdot
+
+    such as that
+
+    lmd = R qdot, lmdd = R qddot + Rdot qdot
+
+    tau = -R^T f_m
+
+    Notes
+    -----
+
+    (a) The model can represent a 2-link (used for validation) or 3-link system
+    by changing nd = 2/3. Unfortunately, the muscle geometry is defined for the
+    3-body case.
+
+    (b) The Newton's 3rd law is applied for each force and torque. For example,
+    for the first body: tau_1 - tau_2 is applied.
+
+    (c) For the derivation of the equations of motion we used the Euler-Lagrange
+    Method assuming:
+
+    M qddot + C qdot + d V / dq = tau, V: potential energy, C: Coriolis Matrix
+
+    """
+
+    def __init__(self, use_gravity=0, use_coordinate_limits=1, use_viscosity=1):
+        """
+        """
+        self.logger = Logger('ArmModel')
+        # n: DoFs, md: muscles
+        self.nd = 3
+        self.md = 9
+        # used for selecting since we use 1-based indexing
+        self.s = self.nd + 1
+        # used for iterating
+        self.dim = list(range(1, self.s))
+        # enable/disable gravity in EoM [0/1] (simulation too slow)
+        self.use_gravity = use_gravity
+        # enable/disable coordinate limits in EoM [0/1]
+        self.use_coordinate_limits = use_coordinate_limits
+        # enable/disable viscous joints in EoM [0/1]
+        self.use_viscosity = use_viscosity
+        # true if constants are not substituted
+        self.sub_constants = True
+        # default model state
+        self.state0 = np.array([
+            np.deg2rad(45.0),  # q1
+            np.deg2rad(45.0),  # q2
+            np.deg2rad(45.0),  # q3
+            np.deg2rad(0.00),  # u1
+            np.deg2rad(0.00),  # u2
+            np.deg2rad(0.00)   # u3
+        ])
+        # reference pose used for calculating the optimal fiber length
+        self.reference_pose = np.array([
+            np.deg2rad(60.0),  # q1
+            np.deg2rad(70.0),  # q2
+            np.deg2rad(50.0),  # q3
+        ])
+        self.logger.debug('Constructing model...')
+        # construct model
+        self.__construct_symbols()
+        self.__construct_kinematics()
+        self.__construct_coordinate_limiting_forces()
+        self.__construct_kinetics()
+        self.__construct_drawables()
+        self.__construct_muscle_geometry()
+        self.__define_muscle_parameters()
+        self.__construct_rhs()
+
+    def __construct_symbols(self):
+        """Define the symbols used by the analytical model.
+
+        """
+        self.logger.debug('__construct_symbols')
+        # define model constants 10 a's and b's are used but indexed from 1-9
+        # thus 0 is not used (for correspondence with the paper)
+        self.a = sp.Matrix(sp.symbols('a0:10'))
+        self.b = sp.Matrix(sp.symbols('b0:10'))
+        # segment lengths
+        self.L = sp.Matrix(sp.symbols('L0:4'))
+        # distance to segment's CoM
+        self.Lc = sp.Matrix(sp.symbols('Lc0:4'))
+        # body parameters
+        # inertia
+        self.Iz = sp.Matrix(sp.symbols('Iz0:4'))
+        # mass
+        self.m = sp.Matrix(sp.symbols('m0:4'))
+        # time
+        self.t = sp.symbols('t')
+        # gravity
+        self.g = sp.symbols('g')
+        # q's are used instead of $\theta$
+        self.q = sp.Matrix(dynamicsymbols('theta0:4'))
+        self.u = sp.Matrix(dynamicsymbols('u:4'))
+        self.dq = sp.Matrix([sp.diff(x, self.t) for x in self.q])
+        self.ddq = sp.Matrix([sp.diff(x, self.t) for x in self.dq])
+        # tau acting forces
+        self.tau = sp.Matrix(dynamicsymbols('tau0:4'))
+        # define a dictionary that maps symbols to values
+        # parameters are derived from [1]
+        self.constants = dict({self.a[1]: 0.055, self.a[2]: 0.055, self.a[3]:
+                               0.220, self.a[4]: 0.24, self.a[5]: 0.040,
+                               self.a[6]: 0.040, self.a[7]: 0.220, self.a[8]:
+                               0.06, self.a[9]: 0.26, self.b[1]: 0.080,
+                               self.b[2]: 0.11, self.b[3]: 0.030, self.b[4]:
+                               0.03, self.b[5]: 0.045, self.b[6]: 0.045,
+                               self.b[7]: 0.048, self.b[8]: 0.050, self.b[9]:
+                               0.03, self.L[1]: 0.310, self.L[2]: 0.270,
+                               self.L[3]: 0.150, self.Lc[1]: 0.165, self.Lc[2]:
+                               0.135, self.Lc[3]: 0.075, self.m[1]: 1.93,
+                               self.m[2]: 1.32, self.m[3]: 0.35, self.Iz[1]:
+                               0.0141, self.Iz[2]: 0.0120, self.Iz[3]: 0.001,
+                               self.g: 9.81})
+
+        # pickle workaround
+        for q in self.q:
+            q.__class__.__module__ = '__main__'
+
+        for dq in self.dq:
+            dq.__class__.__module__ = '__main__'
+
+        for ddq in self.ddq:
+            ddq.__class__.__module__ = '__main__'
+
+        for u in self.u:
+            u.__class__.__module__ = '__main__'
+
+        for tau in self.tau:
+            tau.__class__.__module__ = '__main__'
+
+        # max isometrix force
+        # TODO all muscles are equally strong
+        fmax = 50
+        self.Fmax = sp.diag(fmax, fmax, fmax,
+                            fmax, 0.5 * fmax, 0.5 * fmax,
+                            fmax, fmax, 0.5 * fmax)
+
+    def __construct_kinematics(self):
+        """Define points of interest for the derivation of the EoM. These are used for
+        constructing the EoM.
+
+        """
+        self.logger.debug('__construct_kinematics')
+        L = self.L
+        Lc = self.Lc
+        q = self.q
+        # define the spatial coordinates for the Lc in terms of Lc s' and q's
+        # arm
+        xc1 = sp.Matrix([Lc[1] * cos(q[1]),
+                         Lc[1] * sin(q[1]),
+                         0,
+                         0,
+                         0,
+                         q[1]])
+        # forearm
+        xc2 = sp.Matrix([L[1] * cos(q[1]) + Lc[2] * cos(q[1] + q[2]),
+                         L[1] * sin(q[1]) + Lc[2] * sin(q[1] + q[2]),
+                         0,
+                         0,
+                         0,
+                         q[1] + q[2]])
+
+        # hand
+        xc3 = sp.Matrix([L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) +
+                         Lc[3] * cos(q[1] + q[2] + q[3]),
+                         L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) +
+                         Lc[3] * sin(q[1] + q[2] + q[3]),
+                         0,
+                         0,
+                         0,
+                         q[1] + q[2] + q[3]])
+        self.xc = [sp.Matrix([0]), xc1, xc2, xc3]
+        # CoM velocities
+        self.vc = [sp.diff(x, self.t) for x in self.xc]
+        # calculate CoM Jacobian
+        self.Jc = [x.jacobian(self.QDot()) for x in self.vc]
+
+    def __construct_kinetics(self):
+        """Construct model's dynamics (M, tau_c, tau_g).
+
+        """
+        self.logger.debug('__construct_kinetics')
+        # generate the mass matrix [6 x 6] for each body
+        self.M = [sp.diag(self.m[i], self.m[i], self.m[i], 0, 0,
+                          self.Iz[i]) for i in self.dim]
+        # dummy 0 for 1-based indexing
+        self.M.insert(0, 0)
+        # map spatial to generalized inertia
+        self.M = [self.Jc[i].T * self.M[i] * self.Jc[i]
+                  for i in self.dim]
+        # sum the mass product of each body
+        self.M = reduce(lambda x, y: x + y, self.M)
+        self.M = sp.trigsimp(self.M)
+        # Coriolis matrix
+        self.C = sp.trigsimp(coriolis_matrix(self.M, self.Q(), self.QDot()))
+        # Coriolis forces
+        self.tau_c = sp.trigsimp(self.C * sp.Matrix(self.QDot()))
+        # potential energy due to gravity force
+        self.V = 0
+        for i in self.dim:
+            self.V = self.V + self.m[i] * self.g * self.xc[i][1]
+
+        self.tau_g = sp.Matrix([sp.diff(self.V, x) for x in self.Q()])
+
+    def __construct_coordinate_limiting_forces(self):
+        """Construct coordinate limiting forces.
+
+        """
+        self.logger.debug('__construct_coordinate_limiting_forces')
+
+        a = 5
+        b = 50
+        q_low = [None, np.deg2rad(5), np.deg2rad(5), np.deg2rad(5)]
+        q_up = [None, np.deg2rad(175), np.pi, np.deg2rad(100)]
+
+        self.__tau_l = [coordinate_limiting_force(self.q[i], q_low[i], q_up[i], a,
+                                                  b) for i in range(1, self.s)]
+
+    def __construct_drawables(self):
+        """Construct points of interest (e.g. muscle insertion, CoM, joint centers).
+
+        """
+        self.logger.debug('__construct_drawables')
+        a = self.a
+        b = self.b
+        q = self.q
+        L = self.L
+        # define muscle a
+        self.ap = [[-a[1], sp.Rational(0)],  # a1
+                   [a[2], sp.Rational(0)],  # a2
+                   [a[3] * cos(q[1]), a[3] * sin(q[1])],  # a3
+                   [a[4] * cos(q[1]), a[4] * sin(q[1])],  # a4
+                   [-a[5], sp.Rational(0)],  # a5
+                   [a[6], sp.Rational(0)],  # a6
+                   [L[1] * cos(q[1]) + a[7] * cos(q[1] + q[2]),
+                    L[1] * sin(q[1]) + a[7] * sin(q[1] + q[2])],  # a7
+                   [L[1] * cos(q[1]) + a[8] * cos(q[1] + q[2]),
+                    L[1] * sin(q[1]) + a[8] * sin(q[1] + q[2])],  # a8
+                   [a[9] * cos(q[1]), a[9] * sin(q[1])]  # a9
+                   ]
+        # define muscle b
+        self.bp = [[b[1] * cos(q[1]), b[1] * sin(q[1])],  # b1
+                   [b[2] * cos(q[1]), b[2] * sin(q[1])],  # b2
+                   [L[1] * cos(q[1]) + b[3] * cos(q[1] + q[2]),
+                    L[1] * sin(q[1]) + b[3] * sin(q[1] + q[2])],  # b3
+                   [L[1] * cos(q[1]) - b[4] * cos(q[1] + q[2]),
+                    L[1] * sin(q[1]) - b[4] * sin(q[1] + q[2])],  # b4
+                   [L[1] * cos(q[1]) + b[5] * cos(q[1] + q[2]),
+                    L[1] * sin(q[1]) + b[5] * sin(q[1] + q[2])],  # b5
+                   [L[1] * cos(q[1]) - b[6] * cos(q[1] + q[2]),
+                    L[1] * sin(q[1]) - b[6] * sin(q[1] + q[2])],  # b6
+                   [L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) + b[7] * cos(q[1] + q[2] + q[3]),
+                    L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) + b[7] * sin(q[1] + q[2] + q[3])],  # b7
+                   [L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) - b[8] * cos(q[1] + q[2] + q[3]),
+                    L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) - b[8] * sin(q[1] + q[2] + q[3])],  # b8
+                   [L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) + b[9] * cos(q[1] + q[2] + q[3]),
+                    L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2]) + b[9] * sin(q[1] + q[2] + q[3])]  # b9
+                   ]
+        # define CoM
+        self.bc = [[self.xc[1][0], self.xc[1][1]],
+                   [self.xc[2][0], self.xc[2][1]],
+                   [self.xc[3][0], self.xc[3][1]]
+                   ]
+        # joint center
+        self.jc = [[sp.Rational(0), sp.Rational(0)],
+                   [L[1] * cos(q[1]), L[1] * sin(q[1])],
+                   [L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]),
+                    L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2])]
+                   ]
+        # end effector
+        if self.nd == 3:
+            self.ee = sp.Matrix([L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]) +
+                                 L[3] * cos(q[1] + q[2] + q[3]), L[1] *
+                                 sin(q[1]) + L[2] * sin(q[1] + q[2]) + L[3] *
+                                 sin(q[1] + q[2] + q[3])])
+        elif self.nd == 2:
+            self.ee = sp.Matrix([L[1] * cos(q[1]) + L[2] * cos(q[1] + q[2]),
+                                 L[1] * sin(q[1]) + L[2] * sin(q[1] + q[2])
+                                 ])
+
+    def __construct_muscle_geometry(self):
+        """Construct muscle length function and moment arm.
+
+        """
+        self.logger.debug('__construct_geometry')
+        a = self.a
+        b = self.b
+        L = self.L
+        q = self.q
+        # muscle length ($l(q)$)
+        self.lm = sp.Matrix([
+            (a[1] ** 2 + b[1] ** 2 + 2 * a[1] *
+             b[1] * cos(q[1])) ** sp.Rational(1, 2),
+            (a[2] ** 2 + b[2] ** 2 - 2 * a[2] *
+             b[2] * cos(q[1])) ** sp.Rational(1, 2),
+            ((L[1] - a[3]) ** 2 + b[3] ** 2 + 2 * (L[1] - a[3])
+             * b[3] * cos(q[2])) ** sp.Rational(1, 2),
+            ((L[1] - a[4]) ** 2 + b[4] ** 2 - 2 * (L[1] - a[4])
+             * b[4] * cos(q[2])) ** sp.Rational(1, 2),
+            (a[5] ** 2 + b[5] ** 2 + L[1] ** 2 + 2 * a[5] * L[1] * cos(q[1]) +
+             2 * b[5] * L[1] * cos(q[2]) + 2 * a[5] * b[5] * cos(q[1] + q[2])) ** sp.Rational(1, 2),
+            (a[6] ** 2 + b[6] ** 2 + L[1] ** 2 - 2 * a[6] * L[1] * cos(q[1]) -
+             2 * b[6] * L[1] * cos(q[2]) + 2 * a[6] * b[6] * cos(q[1] + q[2])) ** sp.Rational(1, 2),
+            ((L[2] - a[7]) ** 2 + b[7] ** 2 + 2 * (L[2] - a[7])
+             * b[7] * cos(q[3])) ** sp.Rational(1, 2),
+            ((L[2] - a[8]) ** 2 + b[8] ** 2 - 2 * (L[2] - a[8])
+             * b[8] * cos(q[3])) ** sp.Rational(1, 2),
+            ((L[1] - a[9]) ** 2 + b[9] ** 2 + L[2] ** 2 + 2 * (L[1] - a[9]) * L[2] * cos(q[2]) +
+             2 * b[9] * L[2] * cos(q[3]) +
+             2 * (L[1] - a[9]) * b[9] * cos(q[2] + q[3])) ** sp.Rational(1, 2)
+        ])
+
+        self.lmd = sp.diff(self.lm, self.t)
+        self.lmdd = sp.diff(self.lmd, self.t)
+
+        # calculate the moment arm matrix and its derivatives
+        self.R = sp.trigsimp(self.lm.jacobian(self.Q()))
+        self.RDot = sp.diff(self.R, self.t)
+        self.RDotQDot = self.RDot * sp.Matrix(self.U())
+
+    def __define_muscle_parameters(self):
+        """Define muscle parameters, such as optimal fiber length (reference pose) and
+        tendon stiffness.
+
+        """
+        self.logger.debug('__define_muscle_parameters')
+        parameters = self.model_parameters(q=self.reference_pose,
+                                           in_deg=False)
+        self.lm0 = self.lm.subs(parameters)
+        # the derivative of a matrix with a vector is a rank 3 tensor (3D
+        # array), [dM/dq1, dM/dq2, ...]
+        self.RTDq = sp.derive_by_array(self.R.transpose(), self.Q())
+
+    def __construct_rhs(self):
+        """Construct a callable function that can be used to integrate the mode.
+
+        rhs = rhs(x, t, controller specifieds, parameters values)
+
+        """
+        self.logger.debug('__construct_rhs')
+        self.logger.debug('Use Gravity: ' + str(self.use_gravity))
+        self.logger.debug('Use Coordinate Limits: ' +
+                          str(self.use_coordinate_limits))
+        self.logger.debug('Use Viscous Joints: ' + str(self.use_viscosity))
+
+        # forces
+        # Newton's 3rd law
+        b = 0.05
+        tau = sp.Matrix(apply_generalized_force(self.Tau()))
+        self.tau_l = sp.Matrix(apply_generalized_force(self.__tau_l))
+        self.tau_b = -b * sp.Matrix(apply_generalized_force(self.U()))
+        # tau = sp.Matrix(self.Tau())
+        # self.tau_l = sp.Matrix(self.__tau_l)
+        # self.tau_b = -b *sp.Matrix(self.U())
+
+        # M qdd + tau_c + tau_g = tau + tau_l + tau_b-> M qdd = forcing
+        # f = tau_c + tau_g - tau_l - tau_b
+        self.f = self.tau_c \
+            + self.use_gravity * self.tau_g \
+            - self.use_coordinate_limits * self.tau_l \
+            - self.use_viscosity * self.tau_b
+        self.forcing = tau - self.f
+
+        # substitute dq with u (required for code-gen)
+        for i in range(0, self.forcing.shape[0]):
+            self.forcing = self.forcing.subs(self.dq[i + 1], self.u[i + 1])
+
+        # rhs
+        self.coordinates = sp.Matrix(self.Q())
+        self.speeds = sp.Matrix(self.U())
+        self.coordinates_derivatives = self.speeds
+        self.specifieds = sp.Matrix(self.Tau())
+        self.rhs = generate_ode_function(
+            self.forcing,
+            self.coordinates,
+            self.speeds,
+            list(self.constants.keys()),
+            mass_matrix=self.M,
+            coordinate_derivatives=self.coordinates_derivatives,
+            specifieds=self.specifieds)
+
+# ------------------------------------------------------------------------
+# ArmModel public interface
+# ------------------------------------------------------------------------
+
+    def Q(self):
+        'Get active coordinates (q) [1, s].'
+        return self.q[1:self.s]
+
+    def QDot(self):
+        'Get active speeds (qdot) [1, s].'
+        return self.dq[1:self.s]
+
+    def U(self):
+        'Get active speeds (qdot = u) [1, s].'
+        return self.u[1:self.s]
+
+    def Tau(self):
+        'Get active speeds (tau) [1, s].'
+        return self.tau[1:self.s]
+
+    def model_parameters(self, **kwargs):
+        """Get the model parameters dictionary given q, u, in_deg=[True/False].
+
+        Parameters
+        ----------
+
+        kwargs: q=q, u=u, in_deg=[True/False]
+
+        """
+
+        expected_args = ["q", "u", "in_deg"]
+        kwargsdict = {}
+        for key in list(kwargs.keys()):
+            if key in expected_args:
+                kwargsdict[key] = kwargs[key]
+            else:
+                raise Exception("Unexpected Argument")
+
+        return self.__model_parameters(kwargsdict)
+
+    def __model_parameters(self, dic):
+        """A private implementation of model_parameters.
+
+        Parameters
+        ----------
+
+        dic: dictionary containing {q, u, in_deg}
+
+        """
+
+        constants = {}
+        if self.sub_constants:
+            constants = self.constants.copy()
+
+        q = self.Q()
+        dq = self.QDot()
+        u = self.U()
+        in_deg = False
+        if "in_deg" in list(dic.keys()):
+            in_deg = dic["in_deg"]
+
+        if "q" in list(dic.keys()):
+            qs = dic["q"]
+            if in_deg:
+                qs = np.deg2rad(dic["q"])
+
+            constants.update({q[i]: qs[i] for i in range(0, self.nd)})
+
+        if "u" in list(dic.keys()):
+            us = dic["u"]
+
+            constants.update({dq[i]: us[i] for i in range(0, self.nd)})
+
+            constants.update({u[i]: us[i] for i in range(0, self.nd)})
+
+        return constants
+
+    def pre_substitute_parameters(self):
+        """Substitute model parameters into the variables of the model to improve speed.
+
+        """
+
+        self.logger.debug('pre_substitute_parameters')
+        self.sub_constants = False
+        constants = self.constants
+
+        self.M = self.M.subs(constants)
+        self.tau_c = self.tau_c.subs(constants)
+        self.tau_g = self.tau_g.subs(constants)
+        self.tau_l = self.tau_l.subs(constants)
+        self.tau_b = self.tau_b.subs(constants)
+        self.f = self.f.subs(constants)
+
+        self.lm = self.lm.subs(constants)
+        self.lmd = self.lmd.subs(constants)
+        self.lmdd = self.lmdd.subs(constants)
+        self.R = self.R.subs(constants)
+        self.RDot = self.RDot.subs(constants)
+        self.RDotQDot = self.RDotQDot.subs(constants)
+        self.RTDq = self.RTDq.subs(constants)
+
+        self.ap = substitute(self.ap, constants)
+        self.bp = substitute(self.bp, constants)
+        self.bc = substitute(self.bc, constants)
+        self.jc = substitute(self.jc, constants)
+        self.ee = substitute(self.ee, constants)
+
+        # self.L = self.L.subs(constants)
+        # self.Lc = self.Lc.subs(constants)
+        # self.Iz = self.Iz.subs(constants)
+
+        self.xc = substitute(self.xc, constants)
+        self.vc = substitute(self.vc, constants)
+        self.Jc = substitute(self.Jc, constants)
+
+        # self.m = self.m.subs(constants)
+        # self.g = self.g.subs(constants)
+
+    def draw_model(self, q, in_deg, ax=None, scale=0.8, use_axis_limits=True, alpha=1.0,
+                   text=True):
+        """Draws the 3D toy model.
+
+        Parameters
+        ----------
+
+        q: coordinate values in degrees or rad
+        in_deg: True/False
+        ax: axis 1D
+        scale: if figure is small this helps in visualizing details
+        use_axis_limits: use axis limits from max length
+
+        """
+        if self.nd == 2:
+            self.logger.debug('draw_model supports 3DoFs case')
+            return
+
+        constants = self.model_parameters(q=q, in_deg=in_deg)
+
+        joints = substitute(self.jc, constants)
+        muscle_a = substitute(self.ap, constants)
+        muscle_b = substitute(self.bp, constants)
+        end_effector = substitute(self.ee, constants)
+        CoM = substitute(self.bc, constants)
+
+        linewidth = 4 * scale
+        gd_markersize = 14 * scale
+        jc_markersize = 12 * scale
+        mo_markersize = 7 * scale
+        ef_markersize = 15 * scale
+        fontsize = 12 * scale
+
+        if ax == None:
+            fig, ax = plt.subplots(1, 1, figsize=(5, 5))
+
+        # arm
+        ax.plot([joints[0, 0], joints[1, 0]], [joints[0, 1], joints[1, 1]],
+                'r', linewidth=linewidth, alpha=alpha)
+        # forearm
+        ax.plot([muscle_b[5, 0], joints[2, 0]], [muscle_b[5, 1], joints[2, 1]],
+                'r', linewidth=linewidth, alpha=alpha)
+        # hand
+        ax.plot([muscle_b[7, 0], end_effector[0]], [muscle_b[7, 1], end_effector[1]],
+                'r', linewidth=linewidth, alpha=alpha)
+        # muscles
+        for i in range(0, muscle_a.shape[0]):
+            ax.plot([muscle_a[i, 0], muscle_b[i, 0]], [
+                muscle_a[i, 1], muscle_b[i, 1]], 'b', alpha=alpha)
+            if text:
+                ax.text(muscle_a[i, 0], muscle_a[i, 1],
+                        r'$a_' + str(i + 1) + '$', fontsize=fontsize, alpha=alpha)
+                ax.text(muscle_b[i, 0], muscle_b[i, 1],
+                        r'$b_' + str(i + 1) + '$', fontsize=fontsize, alpha=alpha)
+                ax.text((muscle_b[i, 0] + muscle_a[i, 0]) / 2.0,
+                        (muscle_b[i, 1] + muscle_a[i, 1]) / 2.0,
+                        r'$l_' + str(i + 1) + '$', fontsize=fontsize, alpha=alpha)
+
+        # joint centers
+        ax.plot(joints[:, 0], joints[:, 1], 'or',
+                markersize=gd_markersize, alpha=alpha)
+        if text:
+            for i in range(0, joints.shape[0]):
+                ax.text(joints[i, 0], joints[i, 1], r'$J_' +
+                        str(i + 1) + '$', fontsize=fontsize, alpha=alpha)
+
+        # CoM
+        ax.plot(CoM[:, 0], CoM[:, 1], 'oy',
+                markersize=jc_markersize, alpha=alpha)
+        if text:
+            for i in range(0, CoM.shape[0]):
+                ax.text(CoM[i, 0], CoM[i, 1], r'$Lc_' +
+                        str(i + 1) + '$', fontsize=fontsize, alpha=alpha)
+
+        # end effector
+        ax.plot(end_effector[0], end_effector[1],
+                '<b', markersize=ef_markersize, alpha=alpha)
+        # muscle origin
+        ax.plot(muscle_a[:, 0], muscle_a[:, 1], 'dy',
+                markersize=mo_markersize, alpha=alpha)
+        # muscle insertion
+        ax.plot(muscle_b[:, 0], muscle_b[:, 1], 'db',
+                markersize=mo_markersize, alpha=alpha)
+
+        ax.axis('equal')
+        ax.set_title('Model Pose')
+        ax.set_xlabel('$x \; (m)$')
+        ax.set_ylabel('$y \; (m)$')
+
+        # axis limits
+        if use_axis_limits:
+            L_max = self.constants[self.L[1]] + \
+                self.constants[self.L[2]] + self.constants[self.L[3]]
+            ax.set_xlim([-L_max, L_max])
+            ax.set_ylim([-L_max / 2, 1.5 * L_max])
+
+
+# ------------------------------------------------------------------------
+# ArmModel tests
+# ------------------------------------------------------------------------
+
+    def test_muscle_geometry(self):
+        """Evaluate the muscle geometry for a random pose against the ground truth.
+
+        """
+        ma = self.ap
+        mb = self.bp
+        lmt = sp.Matrix([sp.trigsimp(
+            sp.sqrt(pow(ma[i][0] - mb[i][0], 2) +
+                    pow(ma[i][1] - mb[i][1], 2))) for i in range(0, len(ma))])
+
+        pose = self.model_parameters(q=np.random.random(3), in_deg=False)
+        assert_if_same(self.lm.subs(pose), lmt.subs(pose))