-
Notifications
You must be signed in to change notification settings - Fork 0
/
controller.py
1369 lines (1171 loc) · 48.8 KB
/
controller.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import time
import numpy as np
import mujoco
from mujoco import viewer
import os
# import cvxpy as cp
import trimesh
import tools.rotations as rot
import kinematics.hand_sym as hand_sym
import copy
import matplotlib
# matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
class Robot(object):
def __init__(self, xml_path, auto_sync=True,
q0=None, finger_num=5, dof=4, obj_names=[], path_suffix='', v='v1'):
self.m = mujoco.MjModel.from_xml_path(path_suffix + xml_path)
self.d = mujoco.MjData(self.m)
self.view = viewer.launch_passive(self.m, self.d)
self.auto_sync = auto_sync
self.obj_names = obj_names
self.finger_num = finger_num
self.dof = dof
self.n = finger_num * dof
self.q_ref = np.zeros(self.n)
if q0 is None:
self.q0 = np.zeros(self.n)
else:
self.q0 = q0
if len(q0)==7+ self.n:
self.q0 = q0[7:]
# hand fk
self.hand_kine = hand_sym.Robot(path_suffix=path_suffix, finger_num=finger_num,
version=v) # all frames are wrt the `hand_base`
q_limit = [[-1, 1]] + [[-np.pi / 2, np.pi / 2]] * (dof - 1)
self.q_limit = np.array(q_limit)
self.q_limit = self.q_limit.T
self.path = 'data_records/finger_' + str(finger_num) + '_dof_' + str(dof)
file_name = self.path + '_standup_pos.npy'
if os.path.isfile(file_name):
self.standup_pos = np.array(np.load(file_name))
# self.standup_pos[8+7:12+7] = np.zeros(4)
self.standup_pos[8 + 7:12 + 7] = np.array([0, -np.pi / 2 + 0.1, -0.8, - 0.8])
self.standup_pos[4 * 4 + 7:4 * 4 + 1 + 7] = -1
# self.standup_pos[3:7] = np.array([0, 1, 0, 0])
else:
print('Need to generate the standup position')
self.standup_pos = np.zeros(self.n + 7)
self.standup_pos[3:7] = np.array([0, 1, 0, 0])
if q0 is not None:
self.standup_pos = q0
# for contact
self.finger_name = ['finger_' + str(i + 1) for i in range(self.finger_num)]
self.finger_tips_name = ['distal_' + str(i + 1) for i in range(self.finger_num)]
self.finger_tips_site_name = ['finger_' + str(i + 1) + '_tip' for i in range(self.finger_num)]
self.floor_name = 'floor'
self._tip_force = {}
for i in self.finger_tips_name:
self._tip_force[i] = np.zeros(3)
self.modify_joint(self.standup_pos) # set the initial joint positions
self.step()
self.sync()
self.dt = 0.002
# self.viewer_setup()
def warmup(self, t=None, q=None):
t0 = time.time()
if t is None:
t = 2
if q is None:
q = self.standup_pos[7:]
while 1:
self.joint_computed_torque_control(q)
time.sleep(0.002)
if time.time() - t0 > t:
break
def step(self):
mujoco.mj_step(self.m, self.d) # run one-step dynamics simulation
def modify_joint(self, joints: np.ndarray) -> None:
"""
:param joints: (7,) or (20,) or (27,), modify joints for iiwa or/and allegro hand
:return:
"""
if len(joints) == 7:
self.d.qpos[:7] = joints
if len(joints) == self.n:
self.d.qpos[7: 7 + self.n] = joints
elif len(joints) == self.n + 7:
self.d.qpos[: 7 + self.n] = joints
else:
pass
self.step()
self.sync()
def joint_limits(self, q):
q_tmp = q.reshape(self.finger_num, self.dof)
q_tmp = np.clip(q_tmp, self.q_limit[0, :], self.q_limit[1, :])
return q_tmp.flatten()
def sync(self):
if self.view is not None:
self.view.sync()
self._update_contact_force()
def modify_obj_pose(self, obj_name: str, pose: np.ndarray) -> None:
"""
:param obj_name: the name of the object, from self.obj_names
:param pose: (7,) or (3,), the pose/position command
:return: None
"""
assert obj_name in self.obj_names
start_index = self.obj_names.index(obj_name) * 7 + 7 + self.n
len_pose = len(pose)
assert len_pose in [3, 7]
self.d.qpos[start_index: start_index + len_pose] = pose
self.step()
self.sync()
def reset(self, xq=None, t=None):
"""
reset all states of the robot
# """
if xq is None:
xq = self.standup_pos
self.modify_joint(xq)
self.warmup(t=t, q=xq[7:])
def send_torque(self, torque, torque_limit=1.):
"""
control joints by torque and send to mujoco, then run a step.
input the joint control torque
Here I imply that the joints with id from 0 to n are the actuators.
so, please put robot xml before the object xml.
todo, use joint_name2id to avoid this problem
:param torque: (n, ) numpy array
:return:
"""
torque = np.clip(torque, -torque_limit, torque_limit)
self.d.ctrl[:len(torque)] = torque
mujoco.mj_step(self.m, self.d)
if self.auto_sync:
self.sync()
def finger_ik(self):
pass
def Cartesian_space_impedance_control(self, positions: list, d_positions=None):
"""
############ the precision is not good
Given the reference traj in cartesian space, control the finger/leg to track it.
The orientation is ignored
1st task, track the position
2nd task, close the standup joints
:param d_positions:
:param positions, (15, ) position of fingertips
:return:
"""
if d_positions is None:
d_positions = [np.zeros(3)] * self.finger_num
kp = 20
kd = np.sqrt(kp) * 0.1
nominal_qpos = self.standup_pos[7:]
null_space_damping = 0.1 * 1
null_space_damping = 0.1 * 0
null_space_stiffness = 1
null_space_stiffness = 1 * 0
tau = []
for i in range(self.finger_num): # for each finger
xh = self.xh_local[i][:3] # positions of fingertips, in the hand base frame
dq = self.dq[i * self.dof: i * self.dof + self.dof]
q = self.q[i * self.dof: i * self.dof + self.dof]
dxh = self.dxh[i][:3]
J = self.jac_local
J = J[i][:3, :] # only translational jacobian (3, 4)
Fx = kp * (positions[i] - xh) + kd * (d_positions[i] - dxh) # (3,)
impedance_acc_des1 = J.T @ Fx # (4, )
# Add stiffness and damping in the null space of the Jacobian, to make the joints close to initial one
projection_matrix = J.T.dot(np.linalg.solve(J.dot(J.T) + 1e-10 * np.eye(3), J)) # (4, 4)
projection_matrix = np.eye(projection_matrix.shape[0]) - projection_matrix
null_space_control = -null_space_damping * dq - null_space_stiffness * (
q - nominal_qpos[i * self.dof: i * self.dof + self.dof])
tau_null = projection_matrix.dot(null_space_control) # (4, )
tau.append(impedance_acc_des1 + tau_null)
tau = np.hstack(tau) + self.C
# tau = np.hstack(tau)
self.send_torque(tau)
def Cartesian_space_cmd_each(self, i: int, position: np.ndarray, d_positions=None):
"""
:param i: i-th leg
:param position:
:param d_positions:
:return:tau, (4, ) control torque for the i-th leg
"""
if d_positions is None:
dq = np.zeros(4) # (4, )
else:
dq = np.linalg.pinv(self.jac_local[i]) @ d_positions
positions = [[] for i in range(5)]
positions[i] = position
q_desired = self.hand_kine.ik(positions, q_init=self.q, k=i) # (4,)
if q_desired is None:
print('IK failed', i)
q_desired = np.copy(self.q[i * 4:i * 4 + 4])
kp = 20000 * 2
kd = 200
# computed torque control: M(kp e + kd \dot{e}) + C + G
tau = (self.M[i * 4:i * 4 + 4, i * 4:i * 4 + 4] @ (
kp * (q_desired - self.q[i * 4:i * 4 + 4]) + kd * (dq - self.dq[i * 4:i * 4 + 4]))
+ self.C[i * 4:i * 4 + 4])
return tau
def Cartesian_space_cmd(self, positions: list, d_positions=None, return_q=False, return_tau=False):
"""
:param d_positions: vel at fingertips
:param positions: a list of positions
:param return_tau:
:return:
"""
if d_positions is None:
dq = None
else:
dq = [np.linalg.pinv(self.jac_local[i]) @ d_positions[i] for i in range(self.finger_num)]
dq = np.hstack(dq)
q_desired = self.hand_kine.ik(positions, q_init=self.q) # (20,)
if q_desired is None:
print('IK failed')
q_desired = np.copy(self.q)
tau = self.joint_computed_torque_control(q_desired, dq=dq, return_tau=return_tau)
if return_q and return_tau:
return q_desired, tau
if return_q:
return q_desired
if return_tau:
return tau
def sin_test_cartesian_space(self, i=0):
"""
:param i: i-th finger
:return:
"""
x_record = []
t0 = time.time()
f = 0.2
A = 0.005 * 1
j = 2
x0 = self.xh_local.copy()
while 1:
t = time.time() - t0
x1 = copy.deepcopy(x0)
dx1 = [np.zeros(3) for i in range(self.finger_num)]
x1[i][j] += A * np.sin(2 * np.pi * f * t)
dx1[i][j] = A * 2 * np.pi * f * np.cos(2 * np.pi * f * t)
self.Cartesian_space_cmd(x1, d_positions=dx1)
time.sleep(0.002)
error = x1[i][j] - self.xh_local[i][j]
# print(t, error)
x_record.append(np.array([t, x1[i][j], self.xh_local[i][j], error]))
if t > 10:
break
x_record = np.vstack(x_record)
error = np.abs(x_record[:, 3])
print('Error:', np.mean(error), np.std(error))
plt.plot(x_record[:, 0], x_record[:, 1])
plt.plot(x_record[:, 0], x_record[:, 2])
plt.xlabel('Time (s)')
plt.ylabel('Ref (rad)')
plt.title(str(j) + '-axis tracking for finger ' + str(i))
plt.xlim([0, np.max(x_record[:, 0])])
# plt.ylim([None, np.max(q_record[:, 1])])
plt.show()
def sin_test_joint_space(self, i=0):
q_record = []
t0 = time.time()
f = 0.2
A = np.pi / 2 * 0.5
q0 = np.copy(self.q)
while 1:
t = time.time() - t0
q1 = np.copy(q0)
dq1 = np.zeros(self.n)
q1[i] += A * np.sin(2 * np.pi * f * t)
dq1[i] = A * 2 * np.pi * f * np.cos(2 * np.pi * f * t)
self.joint_computed_torque_control(q1, dq=dq1)
time.sleep(0.002)
error = q1[i] - self.q[i]
# print(t, error)
q_record.append(np.array([t, q1[i], self.q[i], error]))
if t > 10:
break
q_record = np.vstack(q_record)
error = np.abs(q_record[:, 3])
print('Error:', np.mean(error), np.std(error))
plt.plot(q_record[:, 0], q_record[:, 1])
plt.plot(q_record[:, 0], q_record[:, 2])
plt.xlabel('Time (s)')
plt.ylabel('Ref (rad)')
plt.title('Joint tracking ' + str(i))
plt.xlim([0, np.max(q_record[:, 0])])
# plt.ylim([None, np.max(q_record[:, 1])])
plt.show()
def joint_motion_generator(self, q, vel=0.5) -> None:
"""
:param q:
:param vel:
:return:
"""
error = q - self.q
t = np.max(np.abs(error)) / vel
NTIME = int(t / self.dt)
q_list = np.linspace(self.q, q, NTIME)
for i in range(NTIME):
self.joint_computed_torque_control(q_list[i, :])
time.sleep(self.dt)
def move_to_q0(self):
"""
:return:
"""
self.joint_motion_generator(self.q0)
def cartesian_motion_generator(self, x, vel=0.1) -> None:
"""
:param x: list
:param vel:
:return:
"""
x_array = np.hstack(x)
x_a = np.hstack([self.xh_local[i][:3] for i in range(self.finger_num)])
error = x_array - x_a
t = np.max(np.abs(error)) / vel
NTIME = int(t / self.dt)
x_all = np.linspace(x_a, x_array, NTIME)
for i in range(NTIME):
x_ref = [x_all[i, 4 * j: 4 + 4 * j] for j in range(self.finger_num)]
q_desired = self.hand_kine.ik(x_ref, q_init=self.q)
self.joint_computed_torque_control(q_desired)
def joint_computed_torque_control(self, q: np.ndarray, dq=None, ddq=None, return_tau=False):
"""
\tau = M(ddq + kp * q_error + kd * dq_error) + C + g
:param return_tau:
:param q: (20, ) desired joint positions
:param dq: (20, ) desired joint velocities
:param ddq:
:return:
"""
# kp = 20000 *2
kp = 10000
kd = 200 / 2
if dq is None:
dq = np.zeros(self.n) # (20, )
if ddq is None:
ddq = np.zeros(self.n) # (20, )
tau = self.M @ (ddq + kp * (q - self.q) + kd * (dq - self.dq)) + self.C
if np.any(tau > 2):
print("Torque out of limit")
pass
else:
pass
# print(np.max(np.abs(tau)))
if return_tau:
return tau
else:
self.send_torque(tau)
def joint_impedance_control(self, q, dq=None, k=0.5):
q = self.joint_limits(q)
kp = np.ones(self.n) * 0.4 * k
kd = 2 * np.sqrt(kp) * 0.005 * k
if dq is None:
dq = np.zeros(self.n)
error_q = q - self.q
error_dq = dq - self.dq
qacc_des = kp * error_q + kd * error_dq + self.C
# qacc_des = self.M @ (kp * error_q + kd * error_dq) + self.C
self.send_torque(qacc_des)
self.q_ref = np.copy(q)
def _update_contact_force(self):
# reset
self._tip_force = {}
for i in self.finger_tips_name:
self._tip_force[i] = np.zeros(3)
if self.d.ncon: # number of contacts
for i in range(self.d.ncon):
c_array = np.zeros(6)
c = self.d.contact[i]
mujoco.mj_contactForce(self.m, self.d, i, c_array)
# type 5 means geom,
# https://mujoco.readthedocs.io/en/latest/APIreference/APItypes.html?highlight=mjtObj%20type#mjtobj
geom_name = [mujoco.mj_id2name(self.m, 5, c.geom1), mujoco.mj_id2name(self.m, 5, c.geom2)]
F = trans_force2world(c.pos, c.frame.reshape(3, 3).T, c_array) # transfer it to world frame
# F = trans_force2world(c.pos, c.frame.reshape(3, 3), c_array) # transfer it to world frame
# F is the force that geom1 applies on geom2
if geom_name[0] == self.floor_name and geom_name[1] in self.finger_tips_name:
self._tip_force[geom_name[1]] = F[:3]
elif geom_name[1] == self.floor_name and geom_name[0] in self.finger_tips_name:
self._tip_force[geom_name[0]] = -F[:3]
else:
# print('Unexpected contact', geom_name, F)
pass
@property
def q(self):
"""
hand joint angles
:return: (10, ), numpy array
"""
return self.d.qpos[7: 7 + self.n] # noting that the order of joints is based on the order in *.xml file
@property
def dq(self):
"""
hand joint velocities
:return: (7, )
"""
return self.d.qvel[6:6 + self.n]
@property
def C(self):
"""
for iiwa, C(qpos,qvel), Coriolis, computed by mj_fwdVelocity/mj_rne (without acceleration)
:return: (20, )
"""
return self.d.qfrc_bias[6:6 + self.n]
@property
def M(self):
"""
mass matrix for joints
:return: (self.n, self.n)
"""
M = np.zeros([self.m.nv, self.m.nv])
mujoco.mj_fullM(self.m, M, self.d.qM)
return M[6:6 + self.n, 6:6 + self.n]
@property
def x(self):
"""
pose of the palm
:return: (7,)
"""
return self.d.qpos[:7]
@property
def dx(self):
"""
velocity of the palm
:return: (6,)
"""
return self.d.qvel[:6]
@property
def ddx(self):
"""
acceleration of the palm
:return: (6,)
"""
# mujoco.mj_rnePostConstraint(self.m, self.d)
return self.d.qacc[:6]
@property
def x_obj(self) -> list:
"""
:return: [(7,),...] objects poses by list,
// computed by mj_fwdPosition/mj_kinematics
https://mujoco.readthedocs.io/en/stable/APIreference/APItypes.html?highlight=xipos#mjdata
""" # print(joint_id)
poses = []
for i in self.obj_names:
poses.append(np.concatenate([self.d.body(i).xpos, self.d.body(i).xquat]))
return poses
@property
def x_obj_dict(self) -> dict:
"""
:return: [(7,),...] objects poses by list,
// computed by mj_fwdPosition/mj_kinematics
https://mujoco.readthedocs.io/en/stable/APIreference/APItypes.html?highlight=xipos#mjdata
""" # print(joint_id)
poses = {}
for i in self.obj_names:
poses[i] = (np.concatenate([self.d.body(i).xpos, self.d.body(i).xquat]))
return poses
@property
def xh(self) -> list:
"""
get the fingertip positions, wrt the world
:return: [(7,), (7,),...] a list of poses(position and quaternion)
"""
x_list = []
for i in self.finger_tips_site_name:
x = self.d.site(i).xpos # get position of the site
R = self.d.site(i).xmat.reshape(3, 3)
q = rot.mat2quat(R)
x_list.append(np.concatenate([x, q]))
return x_list
@property
def xh_local(self) -> list:
"""
get the fingertip positions, wrt the hand_base
:return: [(7,), (7,),...] a list of poses(position and quaternion)
"""
return self.hand_kine.forward_kine(self.q)
def validate_fk(self):
# to validate the correctness of fk
for i in range(5):
error = rot.pose_mul(self.x, self.xh_local[i]) - self.xh[i]
print(error) # should be all zeros
@property
def jac_local(self) -> list:
"""
get the jacobian of fingertips, wrt the hand_base
:return: jacobian, [(3,4), (3,4)...]
"""
return self.hand_kine.get_jac(self.q)
@property
def dxh(self):
"""
get fingertip velocities
:return:
"""
vel_list = []
for i in self.finger_tips_site_name:
vel = np.zeros(6)
# get the velocity of site
mujoco.mj_objectVelocity(self.m, self.d, mujoco.mjtObj.mjOBJ_SITE,
self.d.site(i).id, vel, 0) # 1 for local, 0 for world, rot:linear
vel_list.append(np.concatenate([vel[3:], vel[:3]])) # linear, rot
# J_tips = self.J_tips(full=True)
# dq = [np.concatenate(self.dx, self.dq[i*4:i*4+4]) for i in range(self.finger_num)]
# v_list = [J_tips[i]@ dq[i] for i in range(self.finger_num)]
return vel_list
@property
def tip_force_dict(self):
"""
contact forces between the floor and fingertips
:return: a dict {'distal_1': np.zeros(3), 'distal_2': np.zeros(3),...}
"""
return self._tip_force
@property
def tip_force(self):
"""
contact forces between the floor and fingertips
:return: a list of contact forces {np.zeros(3), np.zeros(3),...}
"""
return list(self._tip_force.values())
# @property
# def J_tips(self, full=False):
# """
# fingertip jacobian wrt world
# \dot{x} = J(q) @ \dot{q}
# :param: full, if full jacobian or just to the end of finger
# :return: [(6, 4), (6, 4)...], a list of jacobian
# """
# J_list = []
# for i, site_name in enumerate(self.finger_tips_site_name):
# jacp = np.zeros([3, self.m.nv]) # translational jacobian wrt world
# jacr = np.zeros([3, self.m.nv]) # rotational jacobian wrt world
# mujoco.mj_jacSite(self.m, self.d, jacp, jacr, self.m.site(site_name).id)
# if full:
# pass
# jacp = np.concatenate([jacp[:, :6], jacp[:, 6 + i * 4: 6 + (i + 1) * 4]], axis=1) # (3, 10)
# jacr = np.concatenate([jacr[:, :6], jacr[:, 6 + i * 4: 6 + (i + 1) * 4]], axis=1) # (3, 10)
# else:
# jacp = jacp[:, 6 + i * 4: 6 + (i + 1) * 4] # (3,4)
# jacr = jacr[:, 6 + i * 4: 6 + (i + 1) * 4] # (3,4)
# J = np.concatenate([jacp, jacr], axis=0)
# J_list.append(J)
# return J_list
def trans_force2world(pos, R, F):
"""
Transformation of force and torque from a frame to world frame
:param pos: position of the frame wrt world
:param R: rotation matrix
:param F: force and torque (6, )
:return: force and torque in world. (6, )
"""
S = np.array([[0, -pos[2], pos[1]],
[pos[2], 0, -pos[0]],
[-pos[1], pos[0], 0]])
T1 = np.concatenate([R, np.zeros((3, 3))], axis=1)
T2 = np.concatenate([S, R], axis=1)
T = np.concatenate([T1, T2])
return T @ F
# for locomotion,
# the goal is that, given a command in Cartesian space, control the robot to move
class locomotion(Robot):
def __init__(self, xml_path,
q0=None, finger_num=5, dof=4, obj_names=[], var_num=12, path_suffix='', v='v1'):
super().__init__(xml_path, q0=q0, finger_num=finger_num, dof=dof, obj_names=obj_names, path_suffix=path_suffix,
v=v)
# self.q0 = np.copy(self.q)
# self.x0 = self.xh.copy()
self.leg_states = [1] * self.finger_num # 1 means supporting leg, 0 for swing
self.f = 1. # frequency
self.mass = 3.11 * 0.2
# self.mass = 3.11
mesh = trimesh.load(path_suffix + 'descriptions/single_finger/shortest/meshes/palm_01.stl')
mass_scale = self.mass / mesh.mass
inertia = mesh.mass_properties['inertia'] * mass_scale # the mesh has different xyz axis wrt mujoco
self.I = np.array([[inertia[2, 2], 0, 0],
[0, inertia[1, 1], 0],
[0, 0, inertia[0, 0]]]) # [xx, yy, zz] inertia
# self.g = np.array([])9.81
self.var_num = var_num
self.used_fingers = [0, 1, 3, 4]
self.used_fingers_num = len(self.used_fingers)
self.contact_states = np.ones(self.used_fingers_num)
self.vel_cmd = np.zeros(6)
# for swing leg
self.swing_t = 0
self.swing_t_total = 0.5
self.stance_t_total = 0.5
self.four_legs_t_total = 0.2
self.lifting_height = 0.01
# COM control
self.kp_COM = np.diag([100, 100, 200, 100, 100, 200])
self.kd_COM = np.diag([5, 5, 25, 5, 5, 5])
self.var_num = 12
# DS for locomotion
self.linear_DS = linear_system(np.zeros(2))
self.xh_local_init = copy.deepcopy(
self.xh_local) # the middle position of fingertips wrt the center of the robot.
self.dis_error = 0
self.ori_error = 0
def update_leg_switch_state(self):
"""
based on the contact states of feet, update contact type
Based on the contact model coming out from the contact detection,
the normal forces of swing legs are constrained to be zeros
:return:
"""
AA = np.zeros([self.var_num, self.var_num])
F_contact_norm = np.linalg.norm(np.vstack(self.tip_force)[self.used_fingers, :], axis=1)
F_contact_norm_b = F_contact_norm < 1e-10
self.contact_states = ~ F_contact_norm_b
for i in range(len(F_contact_norm)):
if F_contact_norm_b[i]: # for each swing leg, add equality constraint A @ F = 0, so that F_i = 0
AA[i * 3:i * 3 + 3, i * 3:i * 3 + 3] = np.eye(3)
# self.contact_states[]
return AA
def swing_controller(self, i):
"""
tracking controller for swing legs.
using a parabolic traj
:param i: the index of leg
:return: torque command
"""
def stance_controller(self, i):
"""
:param i: the index of leg
:return:
"""
def standby(self, f=0.5):
"""
the crawling robot is standby mode
:param f: frequency of swing legs
:return:
"""
pass
def sin_test(self):
"""
tuning the parameters of Cartesian impedance control
:return:
"""
pass
def standby_test(self, v=None, lifting_height=0.01, return_q_list=False, T=10):
"""
:param lifting_height: height of lifting legs
:param v: (3,) moving velocity in world frame
:return:
"""
swing_leg = 0
if v is None:
v = np.zeros(3)
t0 = time.time()
x0 = self.xh_local.copy()
self.leg_states = [0, 1, 0, 1, 1]
self.f = 1
q_list = []
first = 0
while 1:
v_inhand = np.linalg.inv(rot.pose2T(self.x)[:3, :3]) @ v # vel in hand_base frame
t = time.time() - t0
state_switch = int(t * self.f) % 2
if state_switch == 0:
self.leg_states = [0, 1, 0, 1, 1]
elif state_switch == 1:
self.leg_states = [1, 0, 1, 0, 1]
first = 1
else:
# self.leg_states = [1, 1, 0, 1, 0]
pass
t1 = t - int(t * self.f) / self.f
if int(t * self.f) > T:
break
x1 = copy.deepcopy(x0)
x_cmd = []
dx_cmd = [np.zeros(3)] * self.finger_num
for i, state in enumerate(self.leg_states):
if not state: # if swing leg
x1[i][2] += lifting_height * (1 - np.cos(2 * np.pi * self.f * t + 0))
x1[i][:3] += v_inhand * t1
else: # for supporting leg
x1[i][:3] = x0[i][:3] + (v_inhand * (1 / self.f - t1)) * first
if i == 4:
x1[i] = x0[i]
x_cmd.append(x1[i][:3])
if return_q_list:
q_desired = self.Cartesian_space_cmd(x_cmd, return_q=True)
q_desired = q_desired[[0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 16, 17, 18]]
q_list.append(q_desired)
else:
self.Cartesian_space_cmd(x_cmd)
error = [np.linalg.norm(x_cmd[i] - self.xh_local[i][:3]) for i in range(5)]
# print(error)
time.sleep(self.dt)
print(self.ddx)
if return_q_list:
return q_list
def Grf_ref_pre(self, base_acc, base_r, base_r_acc):
"""
pre-computed reference of ground reaction force
:return:
"""
# base_acc[2, 0] += self.g
# F_total = self.mass * base_acc
#
# Body_Rmatrix = self.RotMatrixfromEuler(base_r)
# Global_I = np.dot(np.dot(Body_Rmatrix, self.body_I), Body_Rmatrix.T)
# Torque_total = np.dot(Global_I, base_r_acc)
#
# self.F_Tor_tot[0:3, 0] = F_total[0:3, 0]
# self.F_Tor_tot[3:6, 0] = Torque_total[0:3, 0]
#
# return self.F_Tor_tot
pass
def Grf_ref_opt(self):
"""
optimal force by QP solver
:return:
"""
base_acc = self.kp_COM @ self.vel_cmd * self.dt + self.kd_COM @ (self.vel_cmd - self.dx) # (6, )
## qp formulation
var_num = self.var_num
con_num = 24
leg_force_opt_old = np.zeros([var_num, 1])
F_balance = np.zeros([var_num, 1])
F_balance[[2, 5, 8, 11], 0] = self.mass * np.abs(self.m.opt.gravity)[2] / 4 # the gravity to 4 legs
A = np.zeros([6, 12])
A[0:3, 0:3] = np.eye(3)
A[0:3, 3:6] = np.eye(3)
A[0:3, 6:9] = np.eye(3)
A[0:3, 9:12] = np.eye(3)
# qp parameter
qp_S = np.diag([100000, 100000, 10000000, 100000, 100000, 10000000])
qp_alpha = 10
qp_beta = 1
qp_gama = 1
for i, leg in enumerate([0, 1, 3, 4]):
com_fr = -self.x[:3] + self.xh[leg][:3]
w_hat = self.skew_hat(com_fr)
A[3:6, i * 3: i * 3 + 3] = w_hat
H = 2 * A.T @ qp_S @ A + (qp_alpha + qp_beta) * np.eye(var_num)
H = 0.5 * (H.T + H)
# Eq.(9) in "Optimized Jumping on the MIT Cheetah 3 Robot"
b = np.zeros([6, 1])
b[:3, 0] = self.mass * (base_acc[:3] - self.m.opt.gravity) # + 9.81
I_global = rot.quat2mat(self.x[3:]) @ self.I @ rot.quat2mat(self.x[3:]).T
b[3:6, 0] = I_global @ base_acc[3:]
C = -2 * A.T @ qp_S @ b + qp_beta * F_balance + qp_gama * leg_force_opt_old # (12, 1)
################ build constraints
# AA @ x = 0
AA = self.update_leg_switch_state() # (12, 12)
# inequality constraints for friction cone
qp_H = np.zeros([con_num, var_num])
qp_L = np.zeros([con_num, 1])
#### 0<=fz<=fz_max
mu = 0.5
fz_max = - self.mass * self.m.opt.gravity[2] * 1.5
for i in range(0, 4):
qp_H[2 * i, 3 * i + 2] = -1
qp_H[2 * i + 1, 3 * i + 2] = 1
qp_L[2 * i + 1, 0] = fz_max
# # pring(qp_L)
#### -u f_z =< f_x <= u f_z
for i in range(0, 4):
qp_H[8 + 2 * i, 3 * i] = -1
qp_H[8 + 2 * i, 3 * i + 2] = -mu
qp_H[8 + 2 * i + 1, 3 * i] = 1
qp_H[8 + 2 * i + 1, 3 * i + 2] = -mu
for i in range(0, 4):
qp_H[16 + 2 * i, 3 * i + 1] = -1
qp_H[16 + 2 * i, 3 * i + 2] = -mu
qp_H[16 + 2 * i + 1, 3 * i + 1] = 1
qp_H[16 + 2 * i + 1, 3 * i + 2] = -mu
# use cvxpy to solve the qp problem
t_opt = time.time()
x = cp.Variable([var_num, 1])
prob = cp.Problem(cp.Minimize((1 / 2) * cp.quad_form(x, H) + C.T @ x), [qp_H @ x <= qp_L,
AA @ x == np.zeros([var_num, 1])])
prob.solve()
leg_force_opt = x.value # (12, )
return leg_force_opt
def skew_hat(self, vec_w):
"""
skew matrix
:param vec_w:
:return:
"""
w_hat = np.array([[0, -vec_w[2], vec_w[1]],
[vec_w[2], 0, -vec_w[0]],
[-vec_w[1], vec_w[0], 0]])
return w_hat
def bezier(self, t, x0, x1):
"""
bezier curve for gait planning
:param t: [0, 1]
:param x0: start point
:param x1: end point
:return:
"""
assert 0 <= t <= 1
return x0 + (t ** 3 + 3 * (t ** 2 * (1 - t))) * (x1 - x0)
def collision_free_DS(self, attractor, x=None, x_objs=None, dx=None, alpha=30, mu=1, rho=1,
radius=[0.2, 0.2, 0.3, 0.3]):
"""
:param attractor: (7,), position and orientation reference for the center of the robot
:return: (2,), linear vel along xy
"""
dim = 2
self.linear_DS.attractor = attractor[:dim] # update the attrator into the linear DS
if x is None:
x = self.x # get the current position
if x_objs is None:
x_objs = self.x_obj[:len(radius)]
if x_objs == []:
vel = self.linear_DS.eval(x[:2], )
# print(vel)
else:
len_obstacles = len(x_objs)
phi = [alpha * (np.linalg.norm(x[:2] - x_objs[i][:2]) / mu - radius[i]) + 1 for i in
range(len_obstacles)] # distance function
vector = [alpha * (x[:2] - x_obj[:2]) / np.linalg.norm(x[:2] - x_obj[:2]) for x_obj in
x_objs] # gradient wrt x
M_all = np.eye(dim) # calculate the modulation matrix
for i in range(len_obstacles):
w = 1
for j in range(len_obstacles):
if j != i:
w = w * (phi[j] - 1) / (phi[i] - 1 + phi[j] - 1)
# w = np.abs(w) # w in [0, 1]
w = np.clip(w, 0, 1)
E = np.zeros((dim, dim))
D = np.zeros((dim, dim))
a0 = vector[i][np.nonzero(vector[i])[0][0]] # the first nonzero component in vector (gradient)
E[:, 0] = vector[i]
tmp = np.power(np.abs(phi[i]), 1 / rho)
# solve the tail-effect
if dx is None:
lambda_0 = 1 - w / tmp
else:
if np.dot(vector[i], dx) >= 0: # remove the tailor effect. After passing the obstacle, recover
lambda_0 = 1
else:
lambda_0 = 1 - w / tmp
lambda_1 = 1 + w / tmp
# if phi[i] < 1: # if in collision
# # lambda_0 = lambda_0 * 40
# lambda_0 = 20
# lambda_1 = 0 # remove the vel towards the attractor
# lambda_0 = np.abs(lambda_0)
D[0, 0] = lambda_0
for j in range(1, dim):
if dim in [2, 4]:
E[0, j] = - vector[i][j]
E[j, j] = a0
else:
E[0, j] = - vector[i][0, j]
E[j, j] = a0[j]
D[j, j] = lambda_1
M = E @ D @ np.linalg.inv(E)
# if np.any(np.array(phi) <1):
# M = np.zeros([dim, dim]) # if goes into the obstacle, then return zero velocity
M_all = M_all @ M
vel = M_all @ self.linear_DS.eval(x[:2], ) # desired vel in world frame