a b/arm_model/util.py
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
import os
4
import time
5
import sympy as sp
6
import numpy as np
7
import pandas as pd
8
from tqdm import tqdm
9
import matplotlib.pyplot as plt
10
from mpl_toolkits.mplot3d import Axes3D
11
from matplotlib.collections import EllipseCollection
12
from matplotlib import patches
13
from fractions import Fraction
14
15
# ------------------------------------------------------------------------------
16
# logger
17
# ------------------------------------------------------------------------------
18
19
# logging.basicConfig(format='%(levelname)s %(asctime)s @%(name)s # %(message)s',
20
#                     datefmt='%m/%d/%Y %I:%M:%S %p',
21
#                     # filename='example.log',
22
#                     level=logging.DEBUG)
23
24
# ------------------------------------------------------------------------------
25
# utilities
26
# ------------------------------------------------------------------------------
27
28
29
def mat_show(mat):
30
    """Graphical visualization of a 2D matrix.
31
    """
32
    fig = plt.figure()
33
    ax = fig.add_subplot(111)
34
    cax = ax.matshow(to_np_mat(mat), interpolation='nearest')
35
    fig.colorbar(cax)
36
37
38
def is_symmetric(a, tol=1e-8):
39
    """Check if matrix is symmetric.
40
41
    """
42
    return np.allclose(a, a.T, atol=tol)
43
44
45
def mat(array):
46
    """For a given 2D array return a numpy matrix.
47
    """
48
    return np.matrix(array)
49
50
51
def vec(vector):
52
    """Construct a column vector of type numpy matrix.
53
    """
54
    return np.matrix(vector).reshape(-1, 1)
55
56
57
def to_np_mat(sympy_mat):
58
    """Cast sympy Matrix to numpy matrix of float type.
59
60
    Parameters
61
    ----------
62
    m: sympy 2D matrix
63
64
    Returns
65
    -------
66
    a numpy asmatrix
67
68
    """
69
    return np.asmatrix(sympy_mat.tolist(), dtype=np.float)
70
71
72
def to_np_array(sympy_mat):
73
    """Cast sympy Matrix to numpy matrix of float type. Works for N-D matrices as
74
    compared to to_np_mat().
75
76
    Parameters
77
    ----------
78
    m: sympy 2D matrix
79
80
    Returns
81
    -------
82
    a numpy asmatrix
83
84
    """
85
    return np.asarray(sympy_mat.tolist(), dtype=np.float)
86
87
88
def to_np_vec(sympy_vec):
89
    """Transforms a 1D sympy vector (e.g. 5 x 1) to numpy array (e.g. (5,)).
90
91
    Parameters
92
    ----------
93
    v: 1D sympy vector
94
95
    Returns
96
    -------
97
    a 1D numpy array
98
99
    """
100
    return np.asarray(sp.flatten(sympy_vec), dtype=np.float)
101
102
103
def is_pd(A):
104
    """Check if matrix is positive definite
105
    """
106
    return np.all(np.linalg.eigvals(A) > 0)
107
108
109
def nullspace(A, atol=1e-13, rtol=0):
110
    """Compute an approximate basis for the nullspace of A.
111
112
    The algorithm used by this function is based on the singular value
113
    decomposition of `A`.
114
115
    Parameters
116
    ----------
117
    A : ndarray
118
        A should be at most 2-D.  A 1-D array with length k will be treated
119
        as a 2-D with shape (1, k)
120
    atol : float
121
        The absolute tolerance for a zero singular value.  Singular values
122
        smaller than `atol` are considered to be zero.
123
    rtol : float
124
        The relative tolerance.  Singular values less than rtol*smax are
125
        considered to be zero, where smax is the largest singular value.
126
127
    If both `atol` and `rtol` are positive, the combined tolerance is the
128
    maximum of the two; that is::
129
        tol = max(atol, rtol * smax)
130
    Singular values smaller than `tol` are considered to be zero.
131
132
    Return value
133
    ------------
134
    ns : ndarray
135
        If `A` is an array with shape (m, k), then `ns` will be an array
136
        with shape (k, n), where n is the estimated dimension of the
137
        nullspace of `A`.  The columns of `ns` are a basis for the
138
        nullspace; each element in numpy.dot(A, ns) will be approximately
139
        zero.
140
    """
141
142
    A = np.atleast_2d(A)
143
    u, s, vh = np.linalg.svd(A)
144
    tol = max(atol, rtol * s[0])
145
    nnz = (s >= tol).sum()
146
    ns = vh[nnz:].conj().T
147
    return ns
148
149
150
def lrs_inequality_vertex_enumeration(A, b):
151
    """Find the vertices given an inequality system A * x <= b. This function
152
    depends on lrs library.
153
154
    Parameters
155
    ----------
156
157
    A: numpy array [m x n]
158
159
    b: numpy array [m]
160
161
    Returns
162
    -------
163
164
    v: numpy array [k x n]
165
        the vertices of the polytope
166
167
    """
168
    # export H-representation
169
    with open('temp.ine', 'w') as file_handle:
170
        file_handle.write('Feasible_Set\n')
171
        file_handle.write('H-representation\n')
172
        file_handle.write('begin\n')
173
        file_handle.write(str(A.shape[0]) + ' ' +
174
                          str(A.shape[1] + 1) + ' rational\n')
175
        for i in range(0, A.shape[0]):
176
            file_handle.write(str(Fraction(b[i])))
177
            for j in range(0, A.shape[1]):
178
                file_handle.write(' ' + str(Fraction(-A[i, j])))
179
180
            file_handle.write('\n')
181
182
        file_handle.write('end\n')
183
184
    # call lrs
185
    try:
186
        os.system('lrs temp.ine > temp.ext')
187
    except OSError as e:
188
        raise RuntimeError(e)
189
190
    # read the V-representation
191
    vertices = []
192
    with open('temp.ext', 'r') as file_handle:
193
        begin = False
194
        for line in file_handle:
195
            if begin:
196
                if 'end' in line:
197
                    break
198
199
                comp = line.split()
200
                try:
201
                    v_type = comp.pop(0)
202
                except:
203
                    print('No feasible solution')
204
205
                if v_type is '1':
206
                    v = [float(Fraction(i)) for i in comp]
207
                    vertices.append(v)
208
209
            else:
210
                if 'begin' in line:
211
                    begin = True
212
213
    # delete temporary files
214
    try:
215
        os.system('rm temp.ine temp.ext')
216
    except OSError as e:
217
        pass
218
219
    return vertices
220
221
222
def ccd_inequality_vertex_enumeration(A, b):
223
    """Find the vertices given an inequality system A * x <= b. This function
224
    depends on pycddlib (cdd).
225
226
    Parameters
227
    ----------
228
229
    A: numpy array [m x n]
230
231
    b: numpy array [m]
232
233
    Returns
234
    -------
235
236
    v: numpy array [k x n]
237
        the vertices of the polytope
238
239
    """
240
    import cdd
241
    # try floating point, if problem fails try exact arithmetics (slow)
242
    try:
243
        M = cdd.Matrix(np.hstack((b.reshape(-1, 1), -A)),
244
                       number_type='float')
245
        M.rep_type = cdd.RepType.INEQUALITY
246
        p = cdd.Polyhedron(M)
247
    except:
248
        print('Warning: switch to exact arithmetics')
249
        M = cdd.Matrix(np.hstack((b.reshape(-1, 1), -A)),
250
                       number_type='fraction')
251
        M.rep_type = cdd.RepType.INEQUALITY
252
        p = cdd.Polyhedron(M)
253
254
    G = np.array(p.get_generators())
255
256
    if not G.shape[0] == 0:
257
        return G[np.where(G[:, 0] == 1.0)[0], 1:].tolist()
258
    else:
259
        raise ValueError('Infeasible Inequality')
260
261
262
def optimization_based_sampling(A, b, optimziation_samples,
263
                                closiness_tolerance, max_opt_iterations):
264
    """Efficient method for sampling the feasible set for a large system of
265
    inequalities (A x <= b). When the dimension of the set (x) is large,
266
    deterministic and pure randomized techniques fail to solve this problem
267
    efficiently.
268
269
    This method uses constrained optimization in order to find n solutions that
270
    satisfy the inequality. Each iteration new randomized objective function is
271
    assigned so that the optimization will find a different solution.
272
273
    Parameters
274
    ----------
275
    A: numpy array [m x n]
276
277
    b: numpy array [m]
278
279
    optimziation_samples: integer
280
        number of samples to generate
281
282
    closiness_tolerance: float
283
        accept solution if distance is larger than the provided tolerance
284
285
    max_opt_iterations: integer
286
        maximum iteration of the optimization algorithm
287
288
    Returns
289
    -------
290
291
    solutions: list
292
293
    """
294
    from scipy.optimize import minimize
295
296
    nullity = A.shape[1]
297
    solutions = []
298
    j = 0
299
    while j < optimziation_samples:
300
        # change objective function randomly (Dirichlet distribution ensures
301
        # that w sums to 1)
302
        w = np.random.dirichlet(np.ones(nullity), size=1)
303
304
        def objective(x):
305
            return np.sum((w * x) ** 2)
306
307
        # solve the optimization_based_sampling
308
        def inequality_constraint(x):
309
            return A.dot(x) - b
310
311
        x0 = np.random.uniform(-1, 1, nullity)
312
        bounds = tuple([(None, None) for i in range(0, nullity)])
313
        constraints = ({'type': 'ineq', 'fun': inequality_constraint})
314
        options = {'maxiter': max_opt_iterations}
315
        sol = minimize(objective, x0, method='SLSQP',
316
                       bounds=bounds,
317
                       constraints=constraints,
318
                       options=options)
319
320
        x = sol.x
321
        # check if solution satisfies the system
322
        if np.all(A.dot(x) <= b):
323
            # if solution is not close to the rest then accept
324
            close = False
325
            for xs in solutions:
326
                if np.linalg.norm(xs - x, 2) < closiness_tolerance:
327
                    close = True
328
                    break
329
330
            if not close:
331
                solutions.append(x)
332
                j = j + 1
333
334
    return solutions
335
336
337
def convex_bounded_vertex_enumeration(A,
338
                                      b,
339
                                      convex_combination_passes=1,
340
                                      method='lrs'):
341
    """Sample a convex, bounded inequality system A * x <= b. The vertices of the
342
    convex polytope are first determined. Then the convexity property is used to
343
    generate additional solutions within the polytope.
344
345
    Parameters
346
    ----------
347
348
    A: numpy array [m x n]
349
350
    b: numpy array [m]
351
352
    convex_combination_passes: int (default 1)
353
        recombine vertices to generate additional solutions using the convex
354
        property
355
356
    method: str (lrs or cdd or rnd)
357
358
    Returns
359
    -------
360
361
    v: numpy array [k x n]
362
        solutions within the convex polytope
363
364
    """
365
    # find polytope vertices
366
    if method == 'lrs':
367
        solutions = lrs_inequality_vertex_enumeration(A, b)
368
    elif method == 'cdd':
369
        solutions = ccd_inequality_vertex_enumeration(A, b)
370
    elif method == 'rnd':
371
        solutions = optimization_based_sampling(A, b,
372
                                                A.shape[0] ** 2,
373
                                                1e-3, 1000)
374
    else:
375
        raise RuntimeError('Unsupported method: choose "lrs" or "cdd" or "rnd"')
376
377
    # since the feasible space is a convex set we can find additional solution
378
    # in the form z = a * x_i + (1-a) x_j
379
    for g in range(0, convex_combination_passes):
380
        n = len(solutions)
381
        for i in range(0, n):
382
            for j in range(0, n):
383
                if i == j:
384
                    continue
385
386
                a = 0.5
387
                x1 = np.array(solutions[i])
388
                x2 = np.array(solutions[j])
389
                z = a * x1 + (1 - a) * x2
390
                solutions.append(z.tolist())
391
392
    # remove duplicates from 2D list
393
    solutions = [list(t) for t in set(tuple(element) for element in solutions)]
394
395
    return np.array(solutions, np.float)
396
397
398
def test_inequality_sampling(d):
399
    A = np.array([[0, 0, -1],
400
                  [0, -1, 0],
401
                  [1, 0, 0],
402
                  [-1, 0, 0],
403
                  [0, 1, 0],
404
                  [0, 0, 1]])
405
    b = 0.5 * np.ones(6)
406
    print(A, b)
407
    t1 = time.time()
408
    # solutions = optimization_based_sampling(A, b, 20, 0.3, 1000)
409
    solutions = convex_bounded_vertex_enumeration(A, b, d)
410
    t2 = time.time()
411
    print('Execution time: ' + str(t2 - t1) + 's')
412
413
    fig = plt.figure()
414
    ax = fig.add_subplot(111, projection='3d')
415
    ax.scatter(solutions[:, 0], solutions[:, 1], solutions[:, 2])
416
    ax.set_xlabel('x')
417
    ax.set_ylabel('y')
418
    ax.set_zlabel('z')
419
    plt.show()
420
    fig.tight_layout()
421
    fig.savefig('results/inequality_sampling/inequality_sampling_d' + str(d) + '.png',
422
                format='png', dpi=300)
423
    fig.savefig('results/inequality_sampling/inequality_sampling_d' + str(d) + '.pdf',
424
                format='pdf', dpi=300)
425
    fig.savefig('results/inequality_sampling/inequality_sampling_d' + str(d) + '.eps',
426
                format='eps', dpi=300)
427
428
429
# test_inequality_sampling(0)
430
# test_inequality_sampling(1)
431
# test_inequality_sampling(2)
432
433
def tensor3_vector_product(T, v):
434
    """Implements a product of a rank-3 tensor (3D array) with a vector using
435
    tensor product and tensor contraction.
436
437
    Parameters
438
    ----------
439
440
    T: sp.Array of dimensions n x m x k
441
442
    v: sp.Array of dimensions k x 1
443
444
    Returns
445
    -------
446
447
    A: sp.Array of dimensions n x m
448
449
    Example
450
    -------
451
452
    >>>T = sp.Array([[[1, 4, 7, 10], [2, 5, 8, 11], [3, 6, 9, 12]],
453
                     [[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]]])
454
    ⎡⎡1  4  7  10⎤  ⎡13  16  19  22⎤⎤
455
    ⎢⎢           ⎥  ⎢              ⎥⎥
456
    ⎢⎢2  5  8  11⎥  ⎢14  17  20  23⎥⎥
457
    ⎢⎢           ⎥  ⎢              ⎥⎥
458
    ⎣⎣3  6  9  12⎦  ⎣15  18  21  24⎦⎦
459
    >>>v = sp.Array([1, 2, 3, 4]).reshape(4, 1)
460
    ⎡1⎤
461
    ⎢ ⎥
462
    ⎢2⎥
463
    ⎢ ⎥
464
    ⎢3⎥
465
    ⎢ ⎥
466
    ⎣4⎦
467
    >>>tensor3_vector_product(T, v)
468
    ⎡⎡70⎤  ⎡190⎤⎤
469
    ⎢⎢  ⎥  ⎢   ⎥⎥
470
    ⎢⎢80⎥  ⎢200⎥⎥
471
    ⎢⎢  ⎥  ⎢   ⎥⎥
472
    ⎣⎣90⎦  ⎣210⎦⎦
473
474
    """
475
    import sympy as sp
476
    assert(T.rank() == 3)
477
    # reshape v to ensure 1D vector so that contraction do not contain x 1
478
    # dimension
479
    v.reshape(v.shape[0], )
480
    p = sp.tensorproduct(T, v)
481
    return sp.tensorcontraction(p, (2, 3))
482
483
484
def test_tensor_product():
485
    T = sp.Array([[[1, 4, 7, 10], [2, 5, 8, 11], [3, 6, 9, 12]],
486
                  [[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]]])
487
    v = sp.Array([1, 2, 3, 4]).reshape(4, 1)
488
    display(T, v)
489
    display(tensor3_vector_product(T, v))
490
491
492
# test_tensor_product()
493
494
495
def draw_ellipse(ax, xc, A, scale=1.0, show_axis=False):
496
    """Construct an ellipse representation of a 2x2 matrix.
497
498
    Parameters
499
    ----------
500
    ax: plot axis
501
502
    xc: np.array 2 x 1
503
        center of the ellipse
504
    mat: np.array 2 x 2
505
506
    scale: float (default=1.0)
507
        scale factor of the principle axes
508
509
    """
510
    eigen_values, eigen_vectors = np.linalg.eig(A)
511
    idx = np.abs(eigen_values).argsort()[::-1]
512
    eigen_values = eigen_values[idx]
513
    eigen_vectors = eigen_vectors[:, idx]
514
    phi = np.rad2deg(np.arctan2(eigen_vectors[1, 0], eigen_vectors[0, 0]))
515
516
    ellipse = patches.Ellipse(xy=(xc[0, 0], xc[1, 0]),
517
                              width=2 * scale * eigen_values[0],
518
                              height=2 * scale * eigen_values[1],
519
                              angle=phi,
520
                              linewidth=2, fill=False)
521
    ax.add_patch(ellipse)
522
523
    # axis
524
    if show_axis:
525
        x_axis = np.array([[xc[0, 0], xc[1, 0]],
526
                           [xc[0, 0] + scale * np.abs(eigen_values[0]) * eigen_vectors[0, 0],
527
                            xc[1, 0] + scale * np.abs(eigen_values[0]) * eigen_vectors[1, 0]]])
528
        y_axis = np.array([[xc[0, 0], xc[1, 0]],
529
                           [xc[0, 0] + scale * eigen_values[1] * eigen_vectors[0, 1],
530
                            xc[1, 0] + scale * eigen_values[1] * eigen_vectors[1, 1]]])
531
        ax.plot(x_axis[:, 0], x_axis[:, 1], '-r', label='x-axis')
532
        ax.plot(y_axis[:, 0], y_axis[:, 1], '-g', label='y-axis')
533
534
    return phi, eigen_values, eigen_vectors
535
536
537
def test_ellipse():
538
    fig, ax = plt.subplots()
539
    xc = vec([0, 0])
540
    M = mat([[2, 1], [1, 2]])
541
    # M = mat([[-2.75032375, -11.82938331], [-11.82938331, -53.5627191]])
542
    print np.linalg.matrix_rank(M)
543
    phi, l, v = draw_ellipse(ax, xc, M, 1, True)
544
    print(phi, l, v)
545
    ax.set_xlabel('x')
546
    ax.set_ylabel('y')
547
    ax.axis('equal')
548
    ax.legend()
549
    fig.show()
550
551
552
# test_ellipse()
553
554
555
def calculate_feasible_muscle_set(feasible_muscle_set_analysis, base_name,
556
                                  t_start, t_end, dt, speed):
557
    """ Calculates the feasible muscle space of a simulation.
558
559
    Parameters
560
    ----------
561
562
    feasible_muscle_set_analysis: FeasibleMuscleSetAnalysis
563
564
    base_name: base name of simulation files
565
566
    t_start: t start
567
568
    t_end: t end
569
570
    dt: time interval for reporting
571
572
    speed: speed of animation
573
574
    """
575
    print('Calculating feasible muscle set ...')
576
    time = np.linspace(t_start, t_end, t_end / dt + 1, endpoint=True)
577
    for i, t in enumerate(tqdm(time)):
578
        visualize_feasible_muscle_set(feasible_muscle_set_analysis, t,
579
                                      base_name + str(i).zfill(6), 'png')
580
581
    command = 'convert -delay ' + \
582
              str(speed * dt) + ' -loop 0 ' + base_name + \
583
              '*.png ' + base_name + 'anim.gif'
584
585
    print(command)
586
    try:
587
        os.system(command)
588
    except:
589
        print('unable to execute command')
590
591
592
def visualize_feasible_muscle_set(feasible_muscle_set_analysis, t,
593
                                  fig_name='fig/feasible_muscle_set', format='png'):
594
    """ Visualization of the feasible muscle space.
595
596
    Parameters
597
    ----------
598
599
    feasible_muscle_set_analysis: FeasibleMuscleSetAnalysis
600
    t: time instance to evaluate the feasible
601
    fig_name: figure name for saving
602
    format: format (e.g. .png, .pdf, .eps)
603
604
    """
605
    fig, ax = plt.subplots(1, 3, figsize=(15, 5))
606
    feasible_muscle_set_analysis.visualize_simple_muscle(t, ax)
607
    fig.suptitle('t = ' + str(np.around(t, decimals=2)),
608
                 y=1.00, fontsize=12, fontweight='bold')
609
    fig.tight_layout()
610
    fig.savefig(fig_name + '.' + format, format=format, dpi=300)
611
    fig.savefig(fig_name + '.pdf', format='pdf', dpi=300)
612
    fig.savefig(fig_name + '.eps', format='eps', dpi=300)
613
614
615
def apply_generalized_force(f):
616
    """Applies a generalized force (f) in a manner that is consistent with Newton's
617
    3rd law.
618
619
    Parameters
620
    ----------
621
    f: generalized force
622
623
    """
624
    n = len(f)
625
    tau = []
626
    for i in range(0, n):
627
        if i == n - 1:
628
            tau.append(f[i])
629
        else:
630
            tau.append(f[i] - f[i + 1])
631
632
    return tau
633
634
635
def custom_exponent(q, A, k, q_lim):
636
    """ Sympy representation of custom exponent function.
637
638
    f(q) = A e^(k (q - q_lim)) / (150) ** k
639
640
    """
641
    return A * sp.exp(k * (q - q_lim)) / (148.42) ** k
642
643
644
def coordinate_limiting_force(q, q_low, q_up, a, b):
645
    """A continuous coordinate limiting force for a rotational joint.
646
647
    It applies an exponential force when approximating a limit. The convention
648
    is that positive force is generated when approaching the lower limit and
649
    negative when approaching the upper. For a = 1, F ~= 1N at the limits.
650
651
    Parameters
652
    ----------
653
    q: generalized coordinate
654
    q_low: lower limit
655
    q_up: upper limit
656
    a: force at limits
657
    b: rate of the slop
658
659
    Note: q, q_low, q_up must have the same units (e.g. rad)
660
661
    """
662
    return custom_exponent(q_low + 5, a, b, q) - custom_exponent(q, a, b, q_up - 5)
663
664
665
def test_limiting_force():
666
    """
667
    """
668
    q = np.linspace(0, np.pi / 4, 100, endpoint=True)
669
    f = [coordinate_limiting_force(qq, 0, np.pi / 4, 1, 50) for qq in q]
670
671
    plt.plot(q, np.array(f))
672
    plt.show()
673
674
675
def gaussian(x, a, m, s):
676
    """Gaussian function.
677
678
    f(x) = a e^(-(x - m)^2 / (2 s ^2))
679
680
    Parameters
681
    ----------
682
    x: x
683
    a: peak
684
    m: mean
685
    s: standard deviation
686
687
    For a good approximation of an impulse at t = 0.3 [x, 1, 0.3, 0.01].
688
689
    """
690
    return a * np.exp(- (x - m) ** 2 / (2 * s ** 2))
691
692
693
def test_gaussian():
694
    """
695
    """
696
    t = np.linspace(0, 2, 200)
697
    y = [gaussian(tt, 0.4, 0.4, 0.01) for tt in t]
698
    plt.plot(t, y)
699
    plt.show()
700
701
702
def rotate(origin, point, angle):
703
    """Rotate a point counterclockwise by a given angle around a given origin.
704
705
    The angle should be given in radians.
706
707
    """
708
    R = np.asmatrix([[np.cos(angle), - np.sin(angle)],
709
                     [np.sin(angle), np.cos(angle)]])
710
711
    q = origin + R * (point - origin)
712
713
    return q
714
715
716
def sigmoid(t, t0, A, B):
717
    """Implementation of smooth sigmoid function.
718
719
    Parameters
720
    ----------
721
    t: time to be evalutaed
722
    t0: delay
723
    A: magnitude
724
    B: slope
725
726
    Returns
727
    -------
728
    (y, y', y'')
729
730
    """
731
    return (A * (np.tanh(B * (t - t0)) + 1) / 2,
732
            A * B * (- np.tanh(B * (t - t0)) ** 2 + 1) / 2,
733
            - A * B ** 2 * (- np.tanh(B * (t - t0)) ** 2 + 1) * np.tanh(B * (t - t0)))
734
735
736
def test_sigmoid():
737
    """
738
    """
739
    t, A, B, t0 = sp.symbols('t A B t0')
740
    y = A / 2 * (sp.tanh(B * (t - t0 - 1)) + 1)
741
    yd = sp.diff(y, t)
742
    ydd = sp.diff(yd, t)
743
    print('\n', y, '\n', yd, '\n', ydd)
744
745
    tt = np.linspace(-2, 2, 100)
746
    yy = np.array([sigmoid(x, 0.5, 2, 5) for x in tt])
747
    plt.plot(tt, yy)
748
    plt.show()
749
750
751
def plot_corr_ellipses(data, ax=None, **kwargs):
752
    """For a given correlation matrix "data", plot the correlation matrix in terms
753
    of ellipses.
754
755
    parameters
756
    ----------
757
    data: Pandas dataframe containing the correlation of the data (df.corr())
758
    ax: axis (e.g. fig, ax = plt.subplots(1, 1))
759
    kwards: keywords arguments (cmap="Greens")
760
761
    https://stackoverflow.com/questions/34556180/
762
    how-can-i-plot-a-correlation-matrix-as-a-set-of-ellipses-similar-to-the-r-open
763
764
    """
765
    M = np.array(data)
766
    if not M.ndim == 2:
767
        raise ValueError('data must be a 2D array')
768
    if ax is None:
769
        fig, ax = plt.subplots(1, 1, subplot_kw={'aspect': 'equal'})
770
        ax.set_xlim(-0.5, M.shape[1] - 0.5)
771
        ax.set_ylim(-0.5, M.shape[0] - 0.5)
772
773
    # xy locations of each ellipse center
774
    xy = np.indices(M.shape)[::-1].reshape(2, -1).T
775
776
    # set the relative sizes of the major/minor axes according to the strength of
777
    # the positive/negative correlation
778
    w = np.ones_like(M).ravel()
779
    h = 1 - np.abs(M).ravel()
780
    a = 45 * np.sign(M).ravel()
781
782
    ec = EllipseCollection(widths=w, heights=h, angles=a, units='x', offsets=xy,
783
                           transOffset=ax.transData, array=M.ravel(), **kwargs)
784
    ax.add_collection(ec)
785
786
    # if data is a DataFrame, use the row/column names as tick labels
787
    if isinstance(data, pd.DataFrame):
788
        ax.set_xticks(np.arange(M.shape[1]))
789
        ax.set_xticklabels(data.columns, rotation=90)
790
        ax.set_yticks(np.arange(M.shape[0]))
791
        ax.set_yticklabels(data.index)
792
793
    return ec
794
795
796
def get_cmap(n, name='hsv'):
797
    """Returns a function that maps each index in 0, 1, ..., n-1 to a distinct RGB
798
    color; the keyword argument name must be a standard mpl colormap name.
799
800
    """
801
    return plt.cm.get_cmap(name, n)
802
803
804
def assert_if_same(A, B):
805
    """Assert whether two quantities (value, vector, matrix) are the same."""
806
    assert np.isclose(
807
        np.array(A).astype(np.float64),
808
        np.array(B).astype(np.float64)).all() == True, 'quantities not equal'
809
810
811
def christoffel_symbols(M, q, i, j, k):
812
    """
813
    M [n x n]: inertia mass matrix
814
    q [n x 1]: generalized coordinates
815
    i, j, k  : the indexies to be computed
816
    """
817
    return sp.Rational(1, 2) * (sp.diff(M[i, j], q[k]) + sp.diff(M[i, k], q[j]) -
818
                                sp.diff(M[k, j], q[i]))
819
820
821
def coriolis_matrix(M, q, dq):
822
    """
823
    Coriolis matrix C(q, qdot) [n x n]
824
    Coriolis forces are computed as  C(q, qdot) *  qdot [n x 1]
825
    """
826
    n = M.shape[0]
827
    C = sp.zeros(n, n)
828
    for i in range(0, n):
829
        for j in range(0, n):
830
            for k in range(0, n):
831
                C[i, j] = C[i, j] + christoffel_symbols(M, q, i, j, k) * dq[k]
832
    return C
833
834
835
def substitute(symbols, constants):
836
    """For a given symbolic sequence substitute symbols."""
837
    return np.array([substitute(exp, constants)
838
                     if hasattr(exp, '__iter__')
839
                     else exp.subs(constants) for exp in symbols])