|
a |
|
b/arm_model/analysis.py |
|
|
1 |
import itertools |
|
|
2 |
import numpy as np |
|
|
3 |
import pylab as plt |
|
|
4 |
import matplotlib as mpl |
|
|
5 |
# from mpl_toolkits.mplot3d import Axes3D |
|
|
6 |
import seaborn as sns |
|
|
7 |
import pandas as pd |
|
|
8 |
from util import to_np_mat, to_np_array, plot_corr_ellipses, draw_ellipse, \ |
|
|
9 |
convex_bounded_vertex_enumeration, nullspace |
|
|
10 |
from logger import Logger |
|
|
11 |
|
|
|
12 |
|
|
|
13 |
# ------------------------------------------------------------------------ |
|
|
14 |
# FeasibleMuscleSetAnalysis |
|
|
15 |
# ------------------------------------------------------------------------ |
|
|
16 |
|
|
|
17 |
def construct_muscle_space_inequality(NR, fm_par, Fmax): |
|
|
18 |
"""Construct the feasible muscle space Z f_m0 <= B . |
|
|
19 |
|
|
|
20 |
Parameters |
|
|
21 |
---------- |
|
|
22 |
|
|
|
23 |
NR: moment arm null space matrix |
|
|
24 |
|
|
|
25 |
fm_par: particular muscle forces |
|
|
26 |
|
|
|
27 |
Fmax: maximum muscle force |
|
|
28 |
|
|
|
29 |
""" |
|
|
30 |
Z0 = -NR |
|
|
31 |
Z1 = NR |
|
|
32 |
B0 = fm_par |
|
|
33 |
B1 = np.asmatrix(np.diag(Fmax)).reshape(Fmax.shape[0], 1) - fm_par |
|
|
34 |
Z = np.concatenate((Z0, Z1), axis=0) |
|
|
35 |
B = np.concatenate((B0, B1), axis=0) |
|
|
36 |
return Z, B |
|
|
37 |
|
|
|
38 |
|
|
|
39 |
class FeasibleMuscleSetAnalysis: |
|
|
40 |
"""Feasible muscle set analysis. |
|
|
41 |
|
|
|
42 |
The required command along with the state of the system are recorded. Then |
|
|
43 |
this information is used to compute the feasible muscle null space and |
|
|
44 |
visualize it. |
|
|
45 |
|
|
|
46 |
""" |
|
|
47 |
|
|
|
48 |
def __init__(self, model, simulation_reporter): |
|
|
49 |
""" |
|
|
50 |
""" |
|
|
51 |
self.logger = Logger('FeasibleMuscleSetAnalsysis') |
|
|
52 |
self.model = model |
|
|
53 |
self.simulation_reporter = simulation_reporter |
|
|
54 |
|
|
|
55 |
def visualize_simple_muscle(self, t, ax=None): |
|
|
56 |
"""Visualize the feasible force set at a particular time instance for a linear |
|
|
57 |
muscle. |
|
|
58 |
|
|
|
59 |
Parameters |
|
|
60 |
---------- |
|
|
61 |
|
|
|
62 |
t: time |
|
|
63 |
|
|
|
64 |
ax: 1 x 3 axis |
|
|
65 |
|
|
|
66 |
""" |
|
|
67 |
m = self.model.md |
|
|
68 |
q, Z, B, NR, fm_par = self.calculate_simple_muscles(t) |
|
|
69 |
x_max = np.max(to_np_mat(self.model.Fmax)) |
|
|
70 |
fm_set = self.generate_solutions(Z, B, NR, fm_par) |
|
|
71 |
dataframe = pd.DataFrame(fm_set, columns=['$m_' + str(i) + '$' for i in |
|
|
72 |
range(1, m + 1)]) |
|
|
73 |
|
|
|
74 |
# box plot |
|
|
75 |
if ax is None or ax.shape[0] < 3: |
|
|
76 |
fig, ax = plt.subplots(1, 3, figsize=(15, 5)) |
|
|
77 |
|
|
|
78 |
# box plot |
|
|
79 |
dataframe.plot.box(ax=ax[0]) |
|
|
80 |
ax[0].set_xlabel('muscle id') |
|
|
81 |
ax[0].set_ylabel('force $(N)$') |
|
|
82 |
ax[0].set_title('Muscle-Force Box Plot') |
|
|
83 |
ax[0].set_ylim([0, 1.1 * x_max]) |
|
|
84 |
|
|
|
85 |
# correlation matrix |
|
|
86 |
cmap = mpl.cm.jet |
|
|
87 |
norm = mpl.colors.Normalize(vmin=-1, vmax=1) |
|
|
88 |
corr = dataframe.corr() |
|
|
89 |
m = plot_corr_ellipses(corr, ax=ax[1], norm=norm, cmap=cmap) |
|
|
90 |
cb = plt.colorbar(m, ax=ax[1], orientation='vertical', norm=norm, |
|
|
91 |
cmap=cmap) |
|
|
92 |
cb.set_label('Correlation Coefficient') |
|
|
93 |
ax[1].margins(0.1) |
|
|
94 |
ax[1].set_xlabel('muscle id') |
|
|
95 |
ax[1].set_ylabel('muscle id') |
|
|
96 |
ax[1].set_title('Correlation Matrix') |
|
|
97 |
ax[1].axis('equal') |
|
|
98 |
|
|
|
99 |
# draw model |
|
|
100 |
self.model.draw_model(q, False, ax[2], scale=0.7, text=False) |
|
|
101 |
|
|
|
102 |
def calculate_simple_muscles(self, t): |
|
|
103 |
"""Construct Z f_m0 <= B for the case of a linear muscle model for a particular |
|
|
104 |
time instance. |
|
|
105 |
|
|
|
106 |
Parameters |
|
|
107 |
---------- |
|
|
108 |
|
|
|
109 |
t: time |
|
|
110 |
|
|
|
111 |
""" |
|
|
112 |
# find nearesrt index corresponding to t |
|
|
113 |
idx = np.abs(np.array(self.simulation_reporter.t) - t).argmin() |
|
|
114 |
t = self.simulation_reporter.t[idx] |
|
|
115 |
q = self.simulation_reporter.q[idx] |
|
|
116 |
u = self.simulation_reporter.u[idx] |
|
|
117 |
tau = self.simulation_reporter.tau[idx] |
|
|
118 |
pose = self.model.model_parameters(q=q, u=u) |
|
|
119 |
n = self.model.nd |
|
|
120 |
|
|
|
121 |
# calculate required variables |
|
|
122 |
R = to_np_mat(self.model.R.subs(pose)) |
|
|
123 |
RBarT = np.asmatrix(np.linalg.pinv(R.T)) |
|
|
124 |
# reduce to independent columns to avoid singularities (proposition 3) |
|
|
125 |
NR = nullspace(R.transpose()) |
|
|
126 |
fm_par = np.asmatrix(-RBarT * tau.reshape((n, 1))) |
|
|
127 |
Fmax = to_np_mat(self.model.Fmax) |
|
|
128 |
|
|
|
129 |
Z, B = construct_muscle_space_inequality(NR, fm_par, Fmax) |
|
|
130 |
|
|
|
131 |
return q, Z, B, NR, fm_par |
|
|
132 |
|
|
|
133 |
def generate_solutions(self, A, b, NR, fm_par): |
|
|
134 |
"""Sample the solution space that satisfy A x <= b. |
|
|
135 |
|
|
|
136 |
Parameters |
|
|
137 |
---------- |
|
|
138 |
|
|
|
139 |
A: matrix A |
|
|
140 |
|
|
|
141 |
b: column vector |
|
|
142 |
|
|
|
143 |
NR: moment arm nullspace |
|
|
144 |
|
|
|
145 |
fm_par: particular solution |
|
|
146 |
|
|
|
147 |
Returns |
|
|
148 |
------- |
|
|
149 |
|
|
|
150 |
muscle forces: a set of solutions that satisfy the problem |
|
|
151 |
|
|
|
152 |
""" |
|
|
153 |
feasible_set = [] |
|
|
154 |
fm0_set = convex_bounded_vertex_enumeration(np.array(A), |
|
|
155 |
np.array(b).flatten(), 0) |
|
|
156 |
n = fm0_set.shape[0] |
|
|
157 |
for i in range(0, n): |
|
|
158 |
fm = fm_par + NR * np.matrix(fm0_set[i, :]).reshape(-1, 1) |
|
|
159 |
feasible_set.append(fm) |
|
|
160 |
|
|
|
161 |
return np.array(feasible_set).reshape(n, -1) |
|
|
162 |
|
|
|
163 |
|
|
|
164 |
def test_feasible_set(model): |
|
|
165 |
feasible_set = FeasibleMuscleSetAnalysis(model) |
|
|
166 |
n = model.nd |
|
|
167 |
m = model.md |
|
|
168 |
feasible_set.record(1, |
|
|
169 |
np.random.random((m, 1)), |
|
|
170 |
np.random.random((m, m)), |
|
|
171 |
np.random.random((n, 1))) |
|
|
172 |
fig, ax = plt.subplots(2, 3, figsize=(10, 10)) |
|
|
173 |
feasible_set.visualize_simple_muscle(1, ax[0]) |
|
|
174 |
feasible_set.visualize_simple_muscle(1, ax[1]) |
|
|
175 |
plt.show() |
|
|
176 |
|
|
|
177 |
# ------------------------------------------------------------------------ |
|
|
178 |
# StiffnessAnalysis |
|
|
179 |
# ------------------------------------------------------------------------ |
|
|
180 |
|
|
|
181 |
|
|
|
182 |
class StiffnessAnalysis: |
|
|
183 |
"""Stiffness analysis. |
|
|
184 |
|
|
|
185 |
""" |
|
|
186 |
|
|
|
187 |
def __init__(self, model, task, simulation_reporter, |
|
|
188 |
feasible_muscle_set_analysis): |
|
|
189 |
"""Constructor. |
|
|
190 |
|
|
|
191 |
Parameters |
|
|
192 |
---------- |
|
|
193 |
|
|
|
194 |
model: ArmModel |
|
|
195 |
|
|
|
196 |
task: TaskSpace |
|
|
197 |
|
|
|
198 |
simulation_reporter: SimulationReporter |
|
|
199 |
|
|
|
200 |
feasible_muscle_set_analysis: FeasibleMuscleSetAnalysis |
|
|
201 |
|
|
|
202 |
""" |
|
|
203 |
self.logger = Logger('StiffnessAnalysis') |
|
|
204 |
self.model = model |
|
|
205 |
self.task = task |
|
|
206 |
self.simulation_reporter = simulation_reporter |
|
|
207 |
self.feasible_muscle_set_analysis = feasible_muscle_set_analysis |
|
|
208 |
self.dataframe = pd.DataFrame() |
|
|
209 |
self.color = itertools.cycle(('r', 'g', 'b')) |
|
|
210 |
self.marker = itertools.cycle(('o', '+', '*')) |
|
|
211 |
self.linestyle = itertools.cycle(('-', '--', ':')) |
|
|
212 |
self.hatch = itertools.cycle(('//', '\\', 'x')) |
|
|
213 |
|
|
|
214 |
def visualize_stiffness_properties(self, t, calc_feasible_stiffness, |
|
|
215 |
scale_factor, alpha, ax, |
|
|
216 |
axis_limits=None): |
|
|
217 |
"""Visualize task stiffness ellipse. |
|
|
218 |
|
|
|
219 |
Parameters |
|
|
220 |
---------- |
|
|
221 |
|
|
|
222 |
t: float |
|
|
223 |
time |
|
|
224 |
|
|
|
225 |
calc_feasible_stiffness: bool |
|
|
226 |
whether to calculate the feasible stiffness |
|
|
227 |
|
|
|
228 |
scale_factor: float |
|
|
229 |
ellipse scale factor |
|
|
230 |
|
|
|
231 |
alpha: float |
|
|
232 |
alpha value for drawing the model |
|
|
233 |
|
|
|
234 |
ax: matplotlib |
|
|
235 |
|
|
|
236 |
axis_limits: list of pairs |
|
|
237 |
axis limits for the 3D plot |
|
|
238 |
|
|
|
239 |
""" |
|
|
240 |
at, q, xc, Km, Kj, Kt = self.calculate_stiffness_properties(t, |
|
|
241 |
calc_feasible_stiffness) |
|
|
242 |
|
|
|
243 |
# ellipse |
|
|
244 |
self.model.draw_model(q, False, ax[0], 1, True, alpha, False) |
|
|
245 |
phi = [] |
|
|
246 |
eigen_values = [] |
|
|
247 |
area = [] |
|
|
248 |
eccentricity = [] |
|
|
249 |
for K in Kt: |
|
|
250 |
phi_temp, eigen_values_temp, v = draw_ellipse(ax[0], xc, K, |
|
|
251 |
scale_factor, |
|
|
252 |
True) |
|
|
253 |
axes_length = np.abs(eigen_values_temp).flatten() |
|
|
254 |
idx = axes_length.argsort()[::-1] |
|
|
255 |
eccentricity_temp = np.sqrt(1 - axes_length[idx[1]]**2 |
|
|
256 |
/ axes_length[idx[0]]**2) |
|
|
257 |
area_temp = scale_factor ** 2 * np.pi * axes_length[0] * axes_length[1] |
|
|
258 |
|
|
|
259 |
if phi_temp < 0: |
|
|
260 |
phi_temp = phi_temp + 180 |
|
|
261 |
|
|
|
262 |
phi.append(phi_temp) |
|
|
263 |
eigen_values.append(eigen_values_temp) |
|
|
264 |
area.append(area_temp) |
|
|
265 |
eccentricity.append(eccentricity_temp) |
|
|
266 |
|
|
|
267 |
ax[0].set_xlabel('x $(m)$') |
|
|
268 |
ax[0].set_ylabel('y $(m)$') |
|
|
269 |
ax[0].set_title('Task Stiffness Ellipses') |
|
|
270 |
|
|
|
271 |
# ellipse properties |
|
|
272 |
if axis_limits is not None: |
|
|
273 |
ax[1].set_xlim(axis_limits[0][0], axis_limits[0][1]) |
|
|
274 |
# ax[1].set_ylim(axis_limits[1][0], axis_limits[1][1]) |
|
|
275 |
ax[1].set_ylim(axis_limits[2][0], axis_limits[2][1]) |
|
|
276 |
for i in range(0, len(area)): # remove outliers |
|
|
277 |
if area[i] < axis_limits[0][0] or area[i] > axis_limits[0][1] or \ |
|
|
278 |
eccentricity[i] < axis_limits[1][0] or eccentricity[i] > axis_limits[1][1] or \ |
|
|
279 |
phi[i] < axis_limits[2][0] or phi[i] > axis_limits[2][1]: |
|
|
280 |
area[i] = np.NaN |
|
|
281 |
eccentricity[i] = np.NaN |
|
|
282 |
phi[i] = np.NaN |
|
|
283 |
|
|
|
284 |
|
|
|
285 |
# color_cycle = ax[1]._get_lines.prop_cycler |
|
|
286 |
# color = next(color_cycle)['color'] |
|
|
287 |
color = self.color.next() |
|
|
288 |
marker = self.marker.next() |
|
|
289 |
linestyle = self.linestyle.next() |
|
|
290 |
data = pd.DataFrame() |
|
|
291 |
data['area'] = area |
|
|
292 |
data['phi'] = phi |
|
|
293 |
sns.distplot(data['area'].dropna(), kde=True, rug=False, hist=False, vertical=False, |
|
|
294 |
color=color, kde_kws={'linestyle': linestyle}, ax=ax[1]) |
|
|
295 |
sns.distplot(data['phi'].dropna(), kde=True, rug=False, hist=False, vertical=True, |
|
|
296 |
color=color, kde_kws={'linestyle': linestyle}, ax=ax[1]) |
|
|
297 |
ax[1].scatter(area, phi, label=str(t) + 's', color=color, marker=marker) |
|
|
298 |
ax[1].set_xlabel('area $(m^2)$') |
|
|
299 |
# ax[1].set_ylabel('$\epsilon$') |
|
|
300 |
ax[1].set_ylabel('$\phi (deg)$') |
|
|
301 |
ax[1].set_title('Task Stiffness Properties') |
|
|
302 |
ax[1].legend() |
|
|
303 |
|
|
|
304 |
# joint stiffness |
|
|
305 |
Kj_temp = [np.abs(kj.diagonal()).reshape(-1, 1) for kj in Kj] |
|
|
306 |
Kj_temp = np.array(Kj_temp).reshape(len(Kj_temp), -1) |
|
|
307 |
n = self.model.nd |
|
|
308 |
current_df = pd.DataFrame(Kj_temp, columns=['$J_' + str(i) + '$' |
|
|
309 |
for i in range(1, n + 1)]) |
|
|
310 |
current_df['Time'] = t |
|
|
311 |
if not self.dataframe.empty: |
|
|
312 |
self.dataframe = self.dataframe.append(current_df) |
|
|
313 |
else: |
|
|
314 |
self.dataframe = current_df |
|
|
315 |
|
|
|
316 |
ax[2].clear() |
|
|
317 |
boxplot = sns.boxplot(x='Time', y='Stiffness', hue='Joint', |
|
|
318 |
data=self.dataframe.set_index('Time', append=True) |
|
|
319 |
.stack() |
|
|
320 |
.to_frame() |
|
|
321 |
.reset_index() |
|
|
322 |
.rename(columns={'level_2': 'Joint', 0: 'Stiffness'}) |
|
|
323 |
.drop('level_0', axis='columns'), ax=ax[2]) |
|
|
324 |
for b in boxplot.artists: |
|
|
325 |
b.set_hatch(self.hatch.next()) |
|
|
326 |
|
|
|
327 |
boxplot.legend() |
|
|
328 |
ax[2].set_xlabel('time $(s)$') |
|
|
329 |
ax[2].set_ylabel('joint stiffness $(Nm / rad)$') |
|
|
330 |
ax[2].set_title('Joint Stiffness') |
|
|
331 |
ax[2].set_ylim([0, 50]) |
|
|
332 |
|
|
|
333 |
|
|
|
334 |
def calculate_stiffness_properties(self, t, calc_feasible_stiffness=False): |
|
|
335 |
"""Calculates the stiffness properties of the model at a particular time |
|
|
336 |
instance. |
|
|
337 |
|
|
|
338 |
Parameters |
|
|
339 |
---------- |
|
|
340 |
|
|
|
341 |
t: float |
|
|
342 |
time of interest |
|
|
343 |
|
|
|
344 |
calc_feasible_stiffness: bool |
|
|
345 |
whether to calculate the feasible stiffness |
|
|
346 |
|
|
|
347 |
Returns |
|
|
348 |
------- |
|
|
349 |
|
|
|
350 |
t: float |
|
|
351 |
actual time (closest to recorded values, not interpolated) |
|
|
352 |
|
|
|
353 |
q: mat n x 1 (mat = numpy.matrix) |
|
|
354 |
generalized coordinates |
|
|
355 |
|
|
|
356 |
xc: mat d x 1 |
|
|
357 |
position of the task |
|
|
358 |
|
|
|
359 |
Km: mat m x m |
|
|
360 |
muscle space stiffness |
|
|
361 |
|
|
|
362 |
Kj: mat n x n |
|
|
363 |
joint space stiffness |
|
|
364 |
|
|
|
365 |
Kt: mat d x d |
|
|
366 |
task space stiffness |
|
|
367 |
|
|
|
368 |
""" |
|
|
369 |
# find nearesrt index corresponding to t |
|
|
370 |
idx = np.abs(np.array(self.simulation_reporter.t) - t).argmin() |
|
|
371 |
t = self.simulation_reporter.t[idx] |
|
|
372 |
q = self.simulation_reporter.q[idx] |
|
|
373 |
u = self.simulation_reporter.u[idx] |
|
|
374 |
fm = self.simulation_reporter.fm[idx] |
|
|
375 |
ft = self.simulation_reporter.ft[idx] |
|
|
376 |
pose = self.model.model_parameters(q=q, u=u) |
|
|
377 |
|
|
|
378 |
# calculate required variables |
|
|
379 |
R = to_np_mat(self.model.R.subs(pose)) |
|
|
380 |
RT = R.transpose() |
|
|
381 |
RTDq = to_np_array(self.model.RTDq.subs(pose)) |
|
|
382 |
Jt = to_np_mat(self.task.Jt.subs(pose)) |
|
|
383 |
JtPInv = np.linalg.pinv(Jt) |
|
|
384 |
JtTPInv = JtPInv.transpose() |
|
|
385 |
JtTDq = to_np_array(self.task.JtTDq.subs(pose)) |
|
|
386 |
xc = to_np_mat(self.task.x(pose)) |
|
|
387 |
|
|
|
388 |
# calculate feasible muscle forces |
|
|
389 |
if calc_feasible_stiffness: |
|
|
390 |
q, Z, B, NR, fm_par = self.feasible_muscle_set_analysis\ |
|
|
391 |
.calculate_simple_muscles(t) |
|
|
392 |
fm_set = self.feasible_muscle_set_analysis.generate_solutions(Z, B, |
|
|
393 |
NR, |
|
|
394 |
fm_par) |
|
|
395 |
|
|
|
396 |
# calculate stiffness properties |
|
|
397 |
Km = [] |
|
|
398 |
Kj = [] |
|
|
399 |
Kt = [] |
|
|
400 |
for i in range(0, fm_set.shape[0]): # , fm_set.shape[0] / 500): |
|
|
401 |
if calc_feasible_stiffness: |
|
|
402 |
fm = fm_set[i, :] |
|
|
403 |
|
|
|
404 |
# calculate muscle stiffness from sort range stiffness (ignores |
|
|
405 |
# tendon stiffness) |
|
|
406 |
gamma = 23.5 |
|
|
407 |
Km.append(np.asmatrix(np.diag([gamma * fm[i] / self.model.lm0[i] |
|
|
408 |
for i in |
|
|
409 |
range(0, self.model.lm0.shape[0])]), |
|
|
410 |
np.float)) |
|
|
411 |
|
|
|
412 |
# switches for taking into account (=1) the tensor products |
|
|
413 |
dynamic = 1.0 |
|
|
414 |
static = 1.0 |
|
|
415 |
|
|
|
416 |
# transpose is required in the tensor product because n(dq) x n(q) x |
|
|
417 |
# d(t) and we need n(q) x n(dq) |
|
|
418 |
|
|
|
419 |
# calculate joint stiffness |
|
|
420 |
RTDqfm = np.matmul(RTDq, fm) |
|
|
421 |
Kj.append(-dynamic * RTDqfm.T - static * RT * Km[-1] * R) |
|
|
422 |
|
|
|
423 |
# calculate task stiffness |
|
|
424 |
JtTDqft = np.matmul(JtTDq, ft) |
|
|
425 |
Kt.append(JtTPInv * (Kj[-1] - dynamic * JtTDqft.T) * JtPInv) |
|
|
426 |
|
|
|
427 |
return t, q, xc, Km, Kj, Kt |