--- a +++ b/arm_model/util.py @@ -0,0 +1,839 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import time +import sympy as sp +import numpy as np +import pandas as pd +from tqdm import tqdm +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +from matplotlib.collections import EllipseCollection +from matplotlib import patches +from fractions import Fraction + +# ------------------------------------------------------------------------------ +# logger +# ------------------------------------------------------------------------------ + +# logging.basicConfig(format='%(levelname)s %(asctime)s @%(name)s # %(message)s', +# datefmt='%m/%d/%Y %I:%M:%S %p', +# # filename='example.log', +# level=logging.DEBUG) + +# ------------------------------------------------------------------------------ +# utilities +# ------------------------------------------------------------------------------ + + +def mat_show(mat): + """Graphical visualization of a 2D matrix. + """ + fig = plt.figure() + ax = fig.add_subplot(111) + cax = ax.matshow(to_np_mat(mat), interpolation='nearest') + fig.colorbar(cax) + + +def is_symmetric(a, tol=1e-8): + """Check if matrix is symmetric. + + """ + return np.allclose(a, a.T, atol=tol) + + +def mat(array): + """For a given 2D array return a numpy matrix. + """ + return np.matrix(array) + + +def vec(vector): + """Construct a column vector of type numpy matrix. + """ + return np.matrix(vector).reshape(-1, 1) + + +def to_np_mat(sympy_mat): + """Cast sympy Matrix to numpy matrix of float type. + + Parameters + ---------- + m: sympy 2D matrix + + Returns + ------- + a numpy asmatrix + + """ + return np.asmatrix(sympy_mat.tolist(), dtype=np.float) + + +def to_np_array(sympy_mat): + """Cast sympy Matrix to numpy matrix of float type. Works for N-D matrices as + compared to to_np_mat(). + + Parameters + ---------- + m: sympy 2D matrix + + Returns + ------- + a numpy asmatrix + + """ + return np.asarray(sympy_mat.tolist(), dtype=np.float) + + +def to_np_vec(sympy_vec): + """Transforms a 1D sympy vector (e.g. 5 x 1) to numpy array (e.g. (5,)). + + Parameters + ---------- + v: 1D sympy vector + + Returns + ------- + a 1D numpy array + + """ + return np.asarray(sp.flatten(sympy_vec), dtype=np.float) + + +def is_pd(A): + """Check if matrix is positive definite + """ + return np.all(np.linalg.eigvals(A) > 0) + + +def nullspace(A, atol=1e-13, rtol=0): + """Compute an approximate basis for the nullspace of A. + + The algorithm used by this function is based on the singular value + decomposition of `A`. + + Parameters + ---------- + A : ndarray + A should be at most 2-D. A 1-D array with length k will be treated + as a 2-D with shape (1, k) + atol : float + The absolute tolerance for a zero singular value. Singular values + smaller than `atol` are considered to be zero. + rtol : float + The relative tolerance. Singular values less than rtol*smax are + considered to be zero, where smax is the largest singular value. + + If both `atol` and `rtol` are positive, the combined tolerance is the + maximum of the two; that is:: + tol = max(atol, rtol * smax) + Singular values smaller than `tol` are considered to be zero. + + Return value + ------------ + ns : ndarray + If `A` is an array with shape (m, k), then `ns` will be an array + with shape (k, n), where n is the estimated dimension of the + nullspace of `A`. The columns of `ns` are a basis for the + nullspace; each element in numpy.dot(A, ns) will be approximately + zero. + """ + + A = np.atleast_2d(A) + u, s, vh = np.linalg.svd(A) + tol = max(atol, rtol * s[0]) + nnz = (s >= tol).sum() + ns = vh[nnz:].conj().T + return ns + + +def lrs_inequality_vertex_enumeration(A, b): + """Find the vertices given an inequality system A * x <= b. This function + depends on lrs library. + + Parameters + ---------- + + A: numpy array [m x n] + + b: numpy array [m] + + Returns + ------- + + v: numpy array [k x n] + the vertices of the polytope + + """ + # export H-representation + with open('temp.ine', 'w') as file_handle: + file_handle.write('Feasible_Set\n') + file_handle.write('H-representation\n') + file_handle.write('begin\n') + file_handle.write(str(A.shape[0]) + ' ' + + str(A.shape[1] + 1) + ' rational\n') + for i in range(0, A.shape[0]): + file_handle.write(str(Fraction(b[i]))) + for j in range(0, A.shape[1]): + file_handle.write(' ' + str(Fraction(-A[i, j]))) + + file_handle.write('\n') + + file_handle.write('end\n') + + # call lrs + try: + os.system('lrs temp.ine > temp.ext') + except OSError as e: + raise RuntimeError(e) + + # read the V-representation + vertices = [] + with open('temp.ext', 'r') as file_handle: + begin = False + for line in file_handle: + if begin: + if 'end' in line: + break + + comp = line.split() + try: + v_type = comp.pop(0) + except: + print('No feasible solution') + + if v_type is '1': + v = [float(Fraction(i)) for i in comp] + vertices.append(v) + + else: + if 'begin' in line: + begin = True + + # delete temporary files + try: + os.system('rm temp.ine temp.ext') + except OSError as e: + pass + + return vertices + + +def ccd_inequality_vertex_enumeration(A, b): + """Find the vertices given an inequality system A * x <= b. This function + depends on pycddlib (cdd). + + Parameters + ---------- + + A: numpy array [m x n] + + b: numpy array [m] + + Returns + ------- + + v: numpy array [k x n] + the vertices of the polytope + + """ + import cdd + # try floating point, if problem fails try exact arithmetics (slow) + try: + M = cdd.Matrix(np.hstack((b.reshape(-1, 1), -A)), + number_type='float') + M.rep_type = cdd.RepType.INEQUALITY + p = cdd.Polyhedron(M) + except: + print('Warning: switch to exact arithmetics') + M = cdd.Matrix(np.hstack((b.reshape(-1, 1), -A)), + number_type='fraction') + M.rep_type = cdd.RepType.INEQUALITY + p = cdd.Polyhedron(M) + + G = np.array(p.get_generators()) + + if not G.shape[0] == 0: + return G[np.where(G[:, 0] == 1.0)[0], 1:].tolist() + else: + raise ValueError('Infeasible Inequality') + + +def optimization_based_sampling(A, b, optimziation_samples, + closiness_tolerance, max_opt_iterations): + """Efficient method for sampling the feasible set for a large system of + inequalities (A x <= b). When the dimension of the set (x) is large, + deterministic and pure randomized techniques fail to solve this problem + efficiently. + + This method uses constrained optimization in order to find n solutions that + satisfy the inequality. Each iteration new randomized objective function is + assigned so that the optimization will find a different solution. + + Parameters + ---------- + A: numpy array [m x n] + + b: numpy array [m] + + optimziation_samples: integer + number of samples to generate + + closiness_tolerance: float + accept solution if distance is larger than the provided tolerance + + max_opt_iterations: integer + maximum iteration of the optimization algorithm + + Returns + ------- + + solutions: list + + """ + from scipy.optimize import minimize + + nullity = A.shape[1] + solutions = [] + j = 0 + while j < optimziation_samples: + # change objective function randomly (Dirichlet distribution ensures + # that w sums to 1) + w = np.random.dirichlet(np.ones(nullity), size=1) + + def objective(x): + return np.sum((w * x) ** 2) + + # solve the optimization_based_sampling + def inequality_constraint(x): + return A.dot(x) - b + + x0 = np.random.uniform(-1, 1, nullity) + bounds = tuple([(None, None) for i in range(0, nullity)]) + constraints = ({'type': 'ineq', 'fun': inequality_constraint}) + options = {'maxiter': max_opt_iterations} + sol = minimize(objective, x0, method='SLSQP', + bounds=bounds, + constraints=constraints, + options=options) + + x = sol.x + # check if solution satisfies the system + if np.all(A.dot(x) <= b): + # if solution is not close to the rest then accept + close = False + for xs in solutions: + if np.linalg.norm(xs - x, 2) < closiness_tolerance: + close = True + break + + if not close: + solutions.append(x) + j = j + 1 + + return solutions + + +def convex_bounded_vertex_enumeration(A, + b, + convex_combination_passes=1, + method='lrs'): + """Sample a convex, bounded inequality system A * x <= b. The vertices of the + convex polytope are first determined. Then the convexity property is used to + generate additional solutions within the polytope. + + Parameters + ---------- + + A: numpy array [m x n] + + b: numpy array [m] + + convex_combination_passes: int (default 1) + recombine vertices to generate additional solutions using the convex + property + + method: str (lrs or cdd or rnd) + + Returns + ------- + + v: numpy array [k x n] + solutions within the convex polytope + + """ + # find polytope vertices + if method == 'lrs': + solutions = lrs_inequality_vertex_enumeration(A, b) + elif method == 'cdd': + solutions = ccd_inequality_vertex_enumeration(A, b) + elif method == 'rnd': + solutions = optimization_based_sampling(A, b, + A.shape[0] ** 2, + 1e-3, 1000) + else: + raise RuntimeError('Unsupported method: choose "lrs" or "cdd" or "rnd"') + + # since the feasible space is a convex set we can find additional solution + # in the form z = a * x_i + (1-a) x_j + for g in range(0, convex_combination_passes): + n = len(solutions) + for i in range(0, n): + for j in range(0, n): + if i == j: + continue + + a = 0.5 + x1 = np.array(solutions[i]) + x2 = np.array(solutions[j]) + z = a * x1 + (1 - a) * x2 + solutions.append(z.tolist()) + + # remove duplicates from 2D list + solutions = [list(t) for t in set(tuple(element) for element in solutions)] + + return np.array(solutions, np.float) + + +def test_inequality_sampling(d): + A = np.array([[0, 0, -1], + [0, -1, 0], + [1, 0, 0], + [-1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + b = 0.5 * np.ones(6) + print(A, b) + t1 = time.time() + # solutions = optimization_based_sampling(A, b, 20, 0.3, 1000) + solutions = convex_bounded_vertex_enumeration(A, b, d) + t2 = time.time() + print('Execution time: ' + str(t2 - t1) + 's') + + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + ax.scatter(solutions[:, 0], solutions[:, 1], solutions[:, 2]) + ax.set_xlabel('x') + ax.set_ylabel('y') + ax.set_zlabel('z') + plt.show() + fig.tight_layout() + fig.savefig('results/inequality_sampling/inequality_sampling_d' + str(d) + '.png', + format='png', dpi=300) + fig.savefig('results/inequality_sampling/inequality_sampling_d' + str(d) + '.pdf', + format='pdf', dpi=300) + fig.savefig('results/inequality_sampling/inequality_sampling_d' + str(d) + '.eps', + format='eps', dpi=300) + + +# test_inequality_sampling(0) +# test_inequality_sampling(1) +# test_inequality_sampling(2) + +def tensor3_vector_product(T, v): + """Implements a product of a rank-3 tensor (3D array) with a vector using + tensor product and tensor contraction. + + Parameters + ---------- + + T: sp.Array of dimensions n x m x k + + v: sp.Array of dimensions k x 1 + + Returns + ------- + + A: sp.Array of dimensions n x m + + Example + ------- + + >>>T = sp.Array([[[1, 4, 7, 10], [2, 5, 8, 11], [3, 6, 9, 12]], + [[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]]]) + ⎡⎡1 4 7 10⎤ ⎡13 16 19 22⎤⎤ + ⎢⎢ ⎥ ⎢ ⎥⎥ + ⎢⎢2 5 8 11⎥ ⎢14 17 20 23⎥⎥ + ⎢⎢ ⎥ ⎢ ⎥⎥ + ⎣⎣3 6 9 12⎦ ⎣15 18 21 24⎦⎦ + >>>v = sp.Array([1, 2, 3, 4]).reshape(4, 1) + ⎡1⎤ + ⎢ ⎥ + ⎢2⎥ + ⎢ ⎥ + ⎢3⎥ + ⎢ ⎥ + ⎣4⎦ + >>>tensor3_vector_product(T, v) + ⎡⎡70⎤ ⎡190⎤⎤ + ⎢⎢ ⎥ ⎢ ⎥⎥ + ⎢⎢80⎥ ⎢200⎥⎥ + ⎢⎢ ⎥ ⎢ ⎥⎥ + ⎣⎣90⎦ ⎣210⎦⎦ + + """ + import sympy as sp + assert(T.rank() == 3) + # reshape v to ensure 1D vector so that contraction do not contain x 1 + # dimension + v.reshape(v.shape[0], ) + p = sp.tensorproduct(T, v) + return sp.tensorcontraction(p, (2, 3)) + + +def test_tensor_product(): + T = sp.Array([[[1, 4, 7, 10], [2, 5, 8, 11], [3, 6, 9, 12]], + [[13, 16, 19, 22], [14, 17, 20, 23], [15, 18, 21, 24]]]) + v = sp.Array([1, 2, 3, 4]).reshape(4, 1) + display(T, v) + display(tensor3_vector_product(T, v)) + + +# test_tensor_product() + + +def draw_ellipse(ax, xc, A, scale=1.0, show_axis=False): + """Construct an ellipse representation of a 2x2 matrix. + + Parameters + ---------- + ax: plot axis + + xc: np.array 2 x 1 + center of the ellipse + mat: np.array 2 x 2 + + scale: float (default=1.0) + scale factor of the principle axes + + """ + eigen_values, eigen_vectors = np.linalg.eig(A) + idx = np.abs(eigen_values).argsort()[::-1] + eigen_values = eigen_values[idx] + eigen_vectors = eigen_vectors[:, idx] + phi = np.rad2deg(np.arctan2(eigen_vectors[1, 0], eigen_vectors[0, 0])) + + ellipse = patches.Ellipse(xy=(xc[0, 0], xc[1, 0]), + width=2 * scale * eigen_values[0], + height=2 * scale * eigen_values[1], + angle=phi, + linewidth=2, fill=False) + ax.add_patch(ellipse) + + # axis + if show_axis: + x_axis = np.array([[xc[0, 0], xc[1, 0]], + [xc[0, 0] + scale * np.abs(eigen_values[0]) * eigen_vectors[0, 0], + xc[1, 0] + scale * np.abs(eigen_values[0]) * eigen_vectors[1, 0]]]) + y_axis = np.array([[xc[0, 0], xc[1, 0]], + [xc[0, 0] + scale * eigen_values[1] * eigen_vectors[0, 1], + xc[1, 0] + scale * eigen_values[1] * eigen_vectors[1, 1]]]) + ax.plot(x_axis[:, 0], x_axis[:, 1], '-r', label='x-axis') + ax.plot(y_axis[:, 0], y_axis[:, 1], '-g', label='y-axis') + + return phi, eigen_values, eigen_vectors + + +def test_ellipse(): + fig, ax = plt.subplots() + xc = vec([0, 0]) + M = mat([[2, 1], [1, 2]]) + # M = mat([[-2.75032375, -11.82938331], [-11.82938331, -53.5627191]]) + print np.linalg.matrix_rank(M) + phi, l, v = draw_ellipse(ax, xc, M, 1, True) + print(phi, l, v) + ax.set_xlabel('x') + ax.set_ylabel('y') + ax.axis('equal') + ax.legend() + fig.show() + + +# test_ellipse() + + +def calculate_feasible_muscle_set(feasible_muscle_set_analysis, base_name, + t_start, t_end, dt, speed): + """ Calculates the feasible muscle space of a simulation. + + Parameters + ---------- + + feasible_muscle_set_analysis: FeasibleMuscleSetAnalysis + + base_name: base name of simulation files + + t_start: t start + + t_end: t end + + dt: time interval for reporting + + speed: speed of animation + + """ + print('Calculating feasible muscle set ...') + time = np.linspace(t_start, t_end, t_end / dt + 1, endpoint=True) + for i, t in enumerate(tqdm(time)): + visualize_feasible_muscle_set(feasible_muscle_set_analysis, t, + base_name + str(i).zfill(6), 'png') + + command = 'convert -delay ' + \ + str(speed * dt) + ' -loop 0 ' + base_name + \ + '*.png ' + base_name + 'anim.gif' + + print(command) + try: + os.system(command) + except: + print('unable to execute command') + + +def visualize_feasible_muscle_set(feasible_muscle_set_analysis, t, + fig_name='fig/feasible_muscle_set', format='png'): + """ Visualization of the feasible muscle space. + + Parameters + ---------- + + feasible_muscle_set_analysis: FeasibleMuscleSetAnalysis + t: time instance to evaluate the feasible + fig_name: figure name for saving + format: format (e.g. .png, .pdf, .eps) + + """ + fig, ax = plt.subplots(1, 3, figsize=(15, 5)) + feasible_muscle_set_analysis.visualize_simple_muscle(t, ax) + fig.suptitle('t = ' + str(np.around(t, decimals=2)), + y=1.00, fontsize=12, fontweight='bold') + fig.tight_layout() + fig.savefig(fig_name + '.' + format, format=format, dpi=300) + fig.savefig(fig_name + '.pdf', format='pdf', dpi=300) + fig.savefig(fig_name + '.eps', format='eps', dpi=300) + + +def apply_generalized_force(f): + """Applies a generalized force (f) in a manner that is consistent with Newton's + 3rd law. + + Parameters + ---------- + f: generalized force + + """ + n = len(f) + tau = [] + for i in range(0, n): + if i == n - 1: + tau.append(f[i]) + else: + tau.append(f[i] - f[i + 1]) + + return tau + + +def custom_exponent(q, A, k, q_lim): + """ Sympy representation of custom exponent function. + + f(q) = A e^(k (q - q_lim)) / (150) ** k + + """ + return A * sp.exp(k * (q - q_lim)) / (148.42) ** k + + +def coordinate_limiting_force(q, q_low, q_up, a, b): + """A continuous coordinate limiting force for a rotational joint. + + It applies an exponential force when approximating a limit. The convention + is that positive force is generated when approaching the lower limit and + negative when approaching the upper. For a = 1, F ~= 1N at the limits. + + Parameters + ---------- + q: generalized coordinate + q_low: lower limit + q_up: upper limit + a: force at limits + b: rate of the slop + + Note: q, q_low, q_up must have the same units (e.g. rad) + + """ + return custom_exponent(q_low + 5, a, b, q) - custom_exponent(q, a, b, q_up - 5) + + +def test_limiting_force(): + """ + """ + q = np.linspace(0, np.pi / 4, 100, endpoint=True) + f = [coordinate_limiting_force(qq, 0, np.pi / 4, 1, 50) for qq in q] + + plt.plot(q, np.array(f)) + plt.show() + + +def gaussian(x, a, m, s): + """Gaussian function. + + f(x) = a e^(-(x - m)^2 / (2 s ^2)) + + Parameters + ---------- + x: x + a: peak + m: mean + s: standard deviation + + For a good approximation of an impulse at t = 0.3 [x, 1, 0.3, 0.01]. + + """ + return a * np.exp(- (x - m) ** 2 / (2 * s ** 2)) + + +def test_gaussian(): + """ + """ + t = np.linspace(0, 2, 200) + y = [gaussian(tt, 0.4, 0.4, 0.01) for tt in t] + plt.plot(t, y) + plt.show() + + +def rotate(origin, point, angle): + """Rotate a point counterclockwise by a given angle around a given origin. + + The angle should be given in radians. + + """ + R = np.asmatrix([[np.cos(angle), - np.sin(angle)], + [np.sin(angle), np.cos(angle)]]) + + q = origin + R * (point - origin) + + return q + + +def sigmoid(t, t0, A, B): + """Implementation of smooth sigmoid function. + + Parameters + ---------- + t: time to be evalutaed + t0: delay + A: magnitude + B: slope + + Returns + ------- + (y, y', y'') + + """ + return (A * (np.tanh(B * (t - t0)) + 1) / 2, + A * B * (- np.tanh(B * (t - t0)) ** 2 + 1) / 2, + - A * B ** 2 * (- np.tanh(B * (t - t0)) ** 2 + 1) * np.tanh(B * (t - t0))) + + +def test_sigmoid(): + """ + """ + t, A, B, t0 = sp.symbols('t A B t0') + y = A / 2 * (sp.tanh(B * (t - t0 - 1)) + 1) + yd = sp.diff(y, t) + ydd = sp.diff(yd, t) + print('\n', y, '\n', yd, '\n', ydd) + + tt = np.linspace(-2, 2, 100) + yy = np.array([sigmoid(x, 0.5, 2, 5) for x in tt]) + plt.plot(tt, yy) + plt.show() + + +def plot_corr_ellipses(data, ax=None, **kwargs): + """For a given correlation matrix "data", plot the correlation matrix in terms + of ellipses. + + parameters + ---------- + data: Pandas dataframe containing the correlation of the data (df.corr()) + ax: axis (e.g. fig, ax = plt.subplots(1, 1)) + kwards: keywords arguments (cmap="Greens") + + https://stackoverflow.com/questions/34556180/ + how-can-i-plot-a-correlation-matrix-as-a-set-of-ellipses-similar-to-the-r-open + + """ + M = np.array(data) + if not M.ndim == 2: + raise ValueError('data must be a 2D array') + if ax is None: + fig, ax = plt.subplots(1, 1, subplot_kw={'aspect': 'equal'}) + ax.set_xlim(-0.5, M.shape[1] - 0.5) + ax.set_ylim(-0.5, M.shape[0] - 0.5) + + # xy locations of each ellipse center + xy = np.indices(M.shape)[::-1].reshape(2, -1).T + + # set the relative sizes of the major/minor axes according to the strength of + # the positive/negative correlation + w = np.ones_like(M).ravel() + h = 1 - np.abs(M).ravel() + a = 45 * np.sign(M).ravel() + + ec = EllipseCollection(widths=w, heights=h, angles=a, units='x', offsets=xy, + transOffset=ax.transData, array=M.ravel(), **kwargs) + ax.add_collection(ec) + + # if data is a DataFrame, use the row/column names as tick labels + if isinstance(data, pd.DataFrame): + ax.set_xticks(np.arange(M.shape[1])) + ax.set_xticklabels(data.columns, rotation=90) + ax.set_yticks(np.arange(M.shape[0])) + ax.set_yticklabels(data.index) + + return ec + + +def get_cmap(n, name='hsv'): + """Returns a function that maps each index in 0, 1, ..., n-1 to a distinct RGB + color; the keyword argument name must be a standard mpl colormap name. + + """ + return plt.cm.get_cmap(name, n) + + +def assert_if_same(A, B): + """Assert whether two quantities (value, vector, matrix) are the same.""" + assert np.isclose( + np.array(A).astype(np.float64), + np.array(B).astype(np.float64)).all() == True, 'quantities not equal' + + +def christoffel_symbols(M, q, i, j, k): + """ + M [n x n]: inertia mass matrix + q [n x 1]: generalized coordinates + i, j, k : the indexies to be computed + """ + return sp.Rational(1, 2) * (sp.diff(M[i, j], q[k]) + sp.diff(M[i, k], q[j]) - + sp.diff(M[k, j], q[i])) + + +def coriolis_matrix(M, q, dq): + """ + Coriolis matrix C(q, qdot) [n x n] + Coriolis forces are computed as C(q, qdot) * qdot [n x 1] + """ + n = M.shape[0] + C = sp.zeros(n, n) + for i in range(0, n): + for j in range(0, n): + for k in range(0, n): + C[i, j] = C[i, j] + christoffel_symbols(M, q, i, j, k) * dq[k] + return C + + +def substitute(symbols, constants): + """For a given symbolic sequence substitute symbols.""" + return np.array([substitute(exp, constants) + if hasattr(exp, '__iter__') + else exp.subs(constants) for exp in symbols])