diff --git a/python/quadruped_reactive_walking/WB_MPC/CasadiOCP.py b/python/quadruped_reactive_walking/WB_MPC/CasadiOCP.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0d5470aadfa2a6b731a941c1101fa32aa2f84ddd 100644 --- a/python/quadruped_reactive_walking/WB_MPC/CasadiOCP.py +++ b/python/quadruped_reactive_walking/WB_MPC/CasadiOCP.py @@ -0,0 +1,247 @@ +import os +from time import time + +import casadi +import numpy as np +import pinocchio as pin +from .ShootingNode import ShootingNode +from .ProblemData import ProblemData +from .Target import Target +from .OcpResult import OcpResult + + +class NodeData: + def __init__(self, isTerminal=False): + self.x = None + self.cost = None + self.isTerminal = isTerminal + if not isTerminal: + self.a = None + self.u = None + self.f = None + self.xnext = None + + + +class CasadiOCP: + def __init__(self, pd: ProblemData, target:Target): + """Define an optimal ocntrol problem. + :param robot: Pinocchio RobotWrapper instance + :param gait: list of list containing contact pattern i.e. for two steps[ [1,1,1,1], [1,0,0,1] ]. \ + Its length determines the horizon of the ocp + :param x0: starting configuration + :param x_ref: reference configuration + :param target: array of target positions + :param dt: timestep integration + """ + self.pd = pd + self.target = target + self.results = OcpResult() + + # build the different shooting nodes + self.shooting_nodes = {contacts: ShootingNode(pd=pd, contactIds=contacts) + for contacts in set(target.contactSequence)} + + def solve(self, x0, guess={}): + """ Solve the NLP + :param guess: ditctionary in the form: {'xs':array([T+1, n_states]), 'acs':array([T, nv]),\ + 'us':array([T, nu]), 'fs': [array([T, nv]),]} + """ + self.runningModels = [self.shooting_nodes[self.target.contactSequence[t]] + for t in range(self.pd.T)] + self.terminalModel = self.shooting_nodes[self.target.contactSequence[self.pd.T]] + self.datas = [] + for i in range(self.pd.T + 1): + if i == self.pd.T: + self.datas += [NodeData(True)] + else: + self.datas += [NodeData()] + + self.make_opti_variables(x0) + self.cost, eq_constraints, ineq_constraints = self.make_ocp() + + self.opti.minimize(self.cost) + self.opti.subject_to(eq_constraints == 0) + self.opti.subject_to(ineq_constraints <= 0) + + p_opts = {"expand": False} + s_opts = {"tol": 1e-4, + "acceptable_tol": 1e-4, + # "max_iter": 21, + # "compl_inf_tol": 1e-2, + # "constr_viol_tol": 1e-2 + # "resto_failure_feasibility_threshold": 1 + # "linear_solver": "ma57" + } + + self.opti.solver("ipopt", p_opts, s_opts) + + self.warmstart(x0, guess) + + # SOLVE + self.opti.solve_limited() + + for data in self.datas: + data.x = self.opti.value(data.x) + data.cost = self.opti.value(data.cost) + if not data.isTerminal: + data.u = self.opti.value(data.u) + data.xnext = self.opti.value(data.xnext) + + def make_opti_variables(self, x0): + opti = casadi.Opti() + self.opti = opti + # Optimization variables + self.dxs = [opti.variable(self.pd.ndx)for _ in self.runningModels+[self.terminalModel]] + self.acs = [opti.variable(self.pd.nv) for _ in self.runningModels] + self.us = [opti.variable(self.pd.nu) for _ in self.runningModels] + self.xs = [m.integrate(x0, dx) for m, dx in zip(self.runningModels+[self.terminalModel], self.dxs)] + self.fs = [] + for m in self.runningModels: + f_tmp = [opti.variable(3) for _ in range(len(m.contactIds))] + self.fs.append(f_tmp) + self.fs = self.fs + + def make_ocp(self): + totalcost = 0 + eq = [] + ineq = [] + + eq.append(self.dxs[0]) + for t in range(self.pd.T): + # These change every time! Do not use runningModels[0].x to get the state!!!! use xs[0] + self.runningModels[t].init(self.datas[t], self.xs[t], self.acs[t], self.us[t], self.fs[t], False) + + # If it is landing + if (self.target.contactSequence[t] != self.target.contactSequence[t-1] and t >= 1): + print('Contact on ', str(self.runningModels[t].contactIds)) + eq.append(self.runningModels[t].constraint_landing_feet_eq(self.pd.xref)) + + xnext, rcost = self.runningModels[t].calc(x_ref=self.pd.xref, + u_ref=self.pd.uref, + target=self.target.target) + + # Constraints + eq.append(self.runningModels[t].constraint_standing_feet_eq()) + eq.append(self.runningModels[t].constraint_dynamics_eq()) + eq.append(self.runningModels[t].difference(self.xs[t + 1], xnext) - np.zeros(self.pd.ndx)) + + ineq.append(self.runningModels[t].constraint_swing_feet_ineq(self.pd.xref, k = self.pd.k)) + ineq.append(self.runningModels[t].constraint_standing_feet_ineq()) + + totalcost += rcost + + eq.append(self.xs[self.pd.T][self.pd.nq :]) + self.terminalModel.init(data=self.datas[self.pd.T], x=self.xs[self.pd.T], isTerminal=True) + _, rcost = self.terminalModel.calc(x_ref=self.pd.xref) + #totalcost += self.terminalModel.calc(x_ref=self.pd.xref)[1] + totalcost += rcost + + eq_constraints = casadi.vertcat(*eq) + ineq_constraints = casadi.vertcat(*ineq) + + + return totalcost, eq_constraints, ineq_constraints + + def warmstart(self, x0, guess=None): + for g in guess: + if guess[g] == []: + print("No warmstart provided") + return 0 + + try: + xs_g = np.array(guess['xs']) + us_g = np.array(guess['us']) + acs_g = np.array(guess['acs']) + fs_g = guess['fs'] + + def xdiff(x1, x2): + return np.concatenate([ + pin.difference(self.pd.model, x1[:self.pd.nq], x2[:self.pd.nq]), + x2[self.pd.nq:]-x1[self.pd.nq:]]) + for x, xg in zip(self.dxs, xs_g): + self.opti.set_initial(x, xdiff(x0, xg)) + for a, ag in zip(self.acs, acs_g): + self.opti.set_initial(a, ag) + for u, ug in zip(self.us, us_g): + self.opti.set_initial(u, ug) + for f, fg in zip(self.fs, fs_g): + fgsplit = np.split(fg, len(f)) + fgc = [] + [fgc.append(f) for f in fgsplit] + [self.opti.set_initial(f[i], fgc[i]) for i in range(len(f))] + print("Got warm start") + except: + print("Can't load warm start") + + def get_results(self): + xs_sol = [self.opti.value(x) for x in self.xs] + us_sol = [self.opti.value(u) for u in self.us] + acs_sol = [self.opti.value(a) for a in self.acs] + fsol = {name: [] for name in self.pd.allContactIds} + fs_world = {name: [] for name in self.pd.allContactIds} + fsol_to_ws = [] + for t in range(self.pd.T): + for i, st_foot in enumerate(self.runningModels[t].contactIds): + fsol[st_foot].append(self.opti.value(self.fs[t][i])) + for i, sw_foot in enumerate(self.runningModels[t].freeIds): + fsol[sw_foot].append(np.zeros(3)) + + fsol_to_ws.append(np.concatenate([self.opti.value( + self.fs[t][j]) for j in range(len(self.runningModels[t].contactIds))])) + + pin.framesForwardKinematics(self.pd.model, self.pd.rdata, np.array(xs_sol)[t, : self.pd.nq]) + [fs_world[foot].append(self.pd.rdata.oMf[foot].rotation @ fsol[foot][t]) for foot in fs_world] + + for foot in fs_world: + fs_world[foot] = np.array(fs_world[foot]) + + self.results.x = xs_sol + self.results.a = acs_sol + self.results.u = us_sol + self.results.fs = fsol_to_ws + return self.results + + + def get_feet_position(self, xs_sol): + """ Get the feet positions computed durnig one ocp + + :param xs_sol: Array of dimension [n_steps, n_states] + + """ + feet_log = {i: [] for i in self.pd.allContactIds} + for foot in feet_log: + tmp = [] + for i in range(len(xs_sol)): + tmp += [self.terminalModel.feet[foot](xs_sol[i]).full()[:, 0]] + feet_log[foot] = np.array(tmp) + + return feet_log + + def get_feet_velocity(self, xs_sol): + """ Get the feet velocities in LOCAL_WORLD_ALIGNED frame computed durnig one ocp + + :param xs_sol: Array of dimension [n_steps, n_states] + + """ + feet_log = {i: [] for i in self.pd.allContactIds} + for foot in feet_log: + tmp = [] + for i in range(len(xs_sol)): + tmp += [self.terminalModel.vfeet[foot](xs_sol[i]).full()[:, 0]] + feet_log[foot] = np.array(tmp) + + return feet_log + + def get_base_log(self, xs_sol): + """ Get the base positions computed durnig one ocp + + :param xs_sol: Array of dimension [n_steps, n_states] + + """ + base_pos_log = [] + [base_pos_log.append(self.terminalModel.baseTranslation( + xs_sol[i]).full()[:, 0]) for i in range(len(xs_sol))] + base_pos_log = np.array(base_pos_log) + return base_pos_log + diff --git a/python/quadruped_reactive_walking/WB_MPC/CrocoddylOCP.py b/python/quadruped_reactive_walking/WB_MPC/CrocoddylOCP.py index 69494179fcd35ad94e42903c52e318100f473827..3f8b9ce9bc3b8e8a559c7b7e37e8b7cd4466dd8b 100644 --- a/python/quadruped_reactive_walking/WB_MPC/CrocoddylOCP.py +++ b/python/quadruped_reactive_walking/WB_MPC/CrocoddylOCP.py @@ -159,6 +159,10 @@ class OCP: self.make_running_model(model, support_feet, tasks) return model + if isTerminal: + self.make_terminal_model() + else: + self.make_running_model() def create_standard_model(self, support_feet, switching): """ @@ -294,6 +298,10 @@ class OCP: self.pd.ground_collision_weight, ) + def remove_contacts(self): + allContacts = self.dmodel.contacts.contacts.todict() + for c in allContacts: + self.dmodel.contacts.removeContact(c) name = "base_velocity_tracking" ref = pin.Motion(tasks[0], tasks[1]) residual_base_velocity = crocoddyl.ResidualModelFrameVelocity( diff --git a/python/quadruped_reactive_walking/WB_MPC/ShootingNode.py b/python/quadruped_reactive_walking/WB_MPC/ShootingNode.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..be1b27200607c8a4040909f96d820f10d82a5d27 100644 --- a/python/quadruped_reactive_walking/WB_MPC/ShootingNode.py +++ b/python/quadruped_reactive_walking/WB_MPC/ShootingNode.py @@ -0,0 +1,255 @@ +import os +from time import time + +import casadi +import numpy as np +import pinocchio as pin +from pinocchio import casadi as cpin +from .ProblemData import ProblemData + +class ShootingNode(): + def __init__(self, pd: ProblemData, contactIds): + + self.pd = pd + self.contactIds = contactIds + self.freeIds = [] + + cmodel = cpin.Model(pd.model) + cdata = cmodel.createData() + + self.baseID = pd.model.getFrameId('base_link') + pin.framesForwardKinematics(pd.model, pd.rdata, pd.q0) + self.robotweight = - \ + sum([Y.mass for Y in pd.model.inertias]) * \ + pd.model.gravity.linear[2] + [self.freeIds.append(idf) + for idf in pd.allContactIds if idf not in contactIds] + + cx = casadi.SX.sym("x", pd.nx, 1) + cq = casadi.SX.sym("q", pd.nq, 1) + cv = casadi.SX.sym("v", pd.nv, 1) + cx2 = casadi.SX.sym("x2", pd.nx, 1) + cu = casadi.SX.sym("u", pd.nu, 1) + ca = casadi.SX.sym("a", pd.nv, 1) + ctau = casadi.SX.sym("tau", pd.ntau, 1) + cdx = casadi.SX.sym("dx", pd.ndx, 1) + cfs = [casadi.SX.sym("f"+cmodel.frames[idf].name, 3, 1) for idf in self.contactIds] + R = casadi.SX.sym('R', 3, 3) + R_ref = casadi.SX.sym('R_ref', 3, 3) + + # Build force list for ABA + forces = [cpin.Force.Zero() for _ in cmodel.joints] + # I am supposing here that all contact frames are on separate joints. This is asserted below: + assert(len(set([cmodel.frames[idf].parentJoint for idf in contactIds])) == len(contactIds)) + for f, idf in zip(cfs, self.contactIds): + # Contact forces introduced in ABA as spatial forces at joint frame. F (opti variable) is defined at contact frame + forces[cmodel.frames[idf].parentJoint] = cmodel.frames[idf].placement * cpin.Force(f, 0*f) + self.forces = cpin.StdVec_Force() + for f in forces: + self.forces.append(f) + + acc = cpin.aba(cmodel, cdata, cx[:pd.nq], cx[pd.nq:], ctau, self.forces) + + # Casadi Functions to wrap pinocchio methods (needed cause pinocchio.casadi methods can't handle MX variables used in potimization) + # acceleration(x,u,f) = ABA(q,v,tau,f) with x=q,v, tau=u, and f built using StdVec_Force syntaxt + self.acc = casadi.Function('acc', [cx, ctau]+cfs, [acc]) + + # integrate(x,dx) = [q+dq,v+dv], with the q+dq function implemented with pin.integrate. + self.integrate = casadi.Function('xplus', [cx, cdx], [casadi.vertcat(cpin.integrate(cmodel, cx[:pd.nq], cdx[:pd.nv]), + cx[-pd.nv:]+cdx[-pd.nv:])]) + + # integrate_q(q,dq) = pin.integrate(q,dq) + self.integrate_q = casadi.Function('qplus', [cq, cv], [cpin.integrate(cmodel, cq, cv)]) + + # Lie difference(x1,x2) = [ pin.difference(q1,q2),v2-v1 ] + self.difference = casadi.Function('xminus', [cx, cx2], [casadi.vertcat(cpin.difference(cmodel, cx2[:pd.nq], cx[:pd.nq]), + cx2[pd.nq:]-cx[pd.nq:])]) + + vel_reference_frame = pin.LOCAL #Reference frame in which velocities of base will be expressed (for locomotion) + + cpin.forwardKinematics(cmodel, cdata, cx[:pd.nq], cx[pd.nq:], ca) + cpin.updateFramePlacements(cmodel, cdata) + + # com(x) = centerOfMass(x[:nq]) + self.com = casadi.Function('com', [cx], [cpin.centerOfMass(cmodel, cdata, cx[:pd.nq])]) + + # Base link position and orientation + self.baseTranslation = casadi.Function('base_translation', [cx], [cdata.oMf[self.baseID].translation]) + self.baseRotation = casadi.Function('base_rotation', [cx], [cdata.oMf[self.baseID].rotation]) + + # Base velocity + self.baseVelocityLin = casadi.Function('base_velocity_linear', [cx], + [cpin.getFrameVelocity(cmodel, cdata, self.baseID, vel_reference_frame).linear]) + self.baseVelocityAng = casadi.Function('base_velocity_angular', [cx], + [cpin.getFrameVelocity(cmodel, cdata, self.baseID, vel_reference_frame).angular]) + + # feet[c](x) = position of the foot <c> at configuration q=x[:nq] + self.feet = {idf: casadi.Function('foot'+cmodel.frames[idf].name, [cx], [cdata.oMf[idf].translation]) for idf in pd.allContactIds} + + # Rfeet[c](x) = orientation of the foot <c> at configuration q=x[:nq] + self.Rfeet = {idf: casadi.Function('Rfoot'+cmodel.frames[idf].name, [cx], [cdata.oMf[idf].rotation]) for idf in pd.allContactIds} + + # vfeet[c](x) = linear velocity of the foot <c> at configuration q=x[:nq] with vel v=x[nq:] + self.vfeet = {idf: casadi.Function('vfoot'+cmodel.frames[idf].name, + [cx], [cpin.getFrameVelocity(cmodel, cdata, idf, pin.LOCAL_WORLD_ALIGNED).linear]) + for idf in pd.allContactIds} + + # vfeet[c](x,a) = linear acceleration of the foot <c> at configuration q=x[:nq] with vel v=x[nq:] and acceleration a + self.afeet = {idf: casadi.Function('afoot'+cmodel.frames[idf].name, + [cx, ca], [cpin.getFrameClassicalAcceleration(cmodel, cdata, idf, + pin.LOCAL_WORLD_ALIGNED).linear]) + for idf in pd.allContactIds} + + # wrapper for the inverse of the exponential map + self.log3 = casadi.Function('log', [R, R_ref], [cpin.log3(R.T @ R_ref)]) + + def init(self, data, x, a=None, u=None, fs=None, isTerminal=False): + self.data = data + self.data.x = x + self.data.a = a + self.data.u = u + self.data.f = fs + + self.x = x + self.isTerminal = isTerminal + if not isTerminal: + self.a = a + self.u = u + self.tau = casadi.vertcat(np.zeros(6), u) + self.fs = fs + + def calc(self, x_ref, u_ref=None, target={}): + ''' + This function return xnext,cost + ''' + + dt = self.pd.dt + + # Euler integration, using directly the acceleration <a> introduced as a slack variable. + use_rk2 = False + if not use_rk2: + vnext = self.x[self.pd.nq:] + self.a*dt + qnext = self.integrate_q(self.x[:self.pd.nq], vnext*dt) + else: + # half-dt step over x=(q,v) + vm = self.x[self.pd.nq:] + self.a*.5*dt + qm = self.integrate_q(self.x[:self.pd.nq], .5 * self.x[self.pd.nq:]*dt) + xm = casadi.vertcat(qm, vm) + amid = self.acc(xm, self.tau, *self.fs) + + # then simple Euler step over (qm, vm) + qnext = self.integrate_q(qm, vm*dt) + vnext = vm + amid*dt + + xnext = casadi.vertcat(qnext, vnext) + self.data.xnext = xnext + + # Cost functions: + self.compute_cost(x_ref, u_ref, target) + self.data.cost = self.cost + + return xnext, self.cost + + def constraint_landing_feet_eq(self, x_ref): + eq = [] + for stFoot in self.contactIds: + eq.append(self.feet[stFoot](self.x)[2] - self.feet[stFoot](x_ref)[2]) + eq.append(self.vfeet[stFoot](self.x)) + return(casadi.vertcat(*eq)) + + def constraint_standing_feet_eq(self): + eq = [] + for stFoot in self.contactIds: + eq.append(self.afeet[stFoot](self.x, self.a)) # stiff contact + + return(casadi.vertcat(*eq)) + + def constraint_standing_feet_ineq(self): + # Friction cone + ineq = [] + for i, stFoot in enumerate(self.contactIds): + R = self.Rfeet[stFoot](self.x) + f_ = self.fs[i] + fw = R @ f_ + ineq.append(-fw[2]) + """ ineq.append( fw[0] - self.pd.mu*fw[2] ) + ineq.append( -fw[0] - self.pd.mu*fw[2] ) + ineq.append( fw[1] - self.pd.mu*fw[2] ) + ineq.append( -fw[1] - self.pd.mu*fw[2] ) """ + + return (casadi.vertcat(*ineq)) + + def constraint_standing_feet_cost(self): + # Friction cone + for i, stFoot in enumerate(self.contactIds): + R = self.Rfeet[stFoot](self.x) + f_ = self.fs[i] + fw = f_ # PREMULTIPLY BY R !!!!! + + A = np.matrix([[1, 0, -self.pd.mu], [-1, 0, -self.pd.mu], [6.1234234 *1e-17, 1, -self.pd.mu],\ + [-6.1234234*1e-17, -1, -self.pd.mu], [0, 0, 1]]) + lb = np.array([-casadi.inf, -casadi.inf, -casadi.inf, -casadi.inf, 0]) + ub = np.array([0, 0, 0, 0, casadi.inf]) + r = A @ fw + self.cost += self.pd.friction_cone_w * casadi.sumsqr(casadi.if_else(r <= lb, r, 0))/2 * self.pd.dt + self.cost += self.pd.friction_cone_w * casadi.sumsqr(casadi.if_else(r >= ub, r, 0))/2 * self.pd.dt + + def constraint_control_cost(self): + self.cost += self.pd.control_bound_w * casadi.sumsqr(casadi.if_else(self.u <= -self.pd.effort_limit, self.u, 0))/2 * self.pd.dt + self.cost += self.pd.control_bound_w * casadi.sumsqr(casadi.if_else(self.u >= self.pd.effort_limit, self.u, 0))/2 * self.pd.dt + + def constraint_dynamics_eq(self): + eq = [] + eq.append(self.acc(self.x, self.tau, *self.fs) - self.a) + return(casadi.vertcat(*eq)) + + def constraint_swing_feet_ineq(self, x_ref, k): + ineq = [] + for sw_foot in self.freeIds: + ineq.append(-self.feet[sw_foot](self.x)[2] + self.feet[sw_foot](x_ref)[2]) + ineq.append(self.vfeet[sw_foot](self.x)[0:2] - k* self.feet[sw_foot](self.x)[2]) + #ineq.append(self.vfeet[sw_foot](self.x)[1] - k[1]* self.feet[sw_foot](self.x)[2]) + + return(casadi.vertcat(*ineq)) + + def force_reg_cost(self): + for i, stFoot in enumerate(self.contactIds): + R = self.Rfeet[stFoot](self.x) + f_ = self.fs[i] + fw = R @ f_ + self.cost += self.pd.force_reg_w * casadi.norm_2(fw[2] - + self.robotweight/len(self.contactIds)) * self.pd.dt + + def control_cost(self, u_ref): + self.cost += 1/2*self.pd.control_reg_w * \ + casadi.sumsqr(self.u - u_ref) * self.pd.dt + + def body_reg_cost(self, x_ref): + if self.isTerminal: + self.cost += 1/2 * \ + casadi.sumsqr(self.pd.state_reg_w * + self.difference(self.x, x_ref)) + #self.cost += 1/2 * \ + # casadi.sumsqr(self.pd.terminal_velocity_w * + # self.difference(self.x, x_ref)) + else: + self.cost += 1/2 * \ + casadi.sumsqr(self.pd.state_reg_w * + self.difference(self.x, x_ref)) * self.pd.dt + + def target_cost(self, target): + # I am Assuming just FR FOOt to be free + for obj in target: + self.cost += casadi.sumsqr(self.pd.linear_velocity_w*(self.baseVelocityLin(self.x) - target[obj]["linear_velocity"])) * self.pd.dt + self.cost += casadi.sumsqr(self.pd.angular_velocity_w*(self.baseVelocityAng(self.x) - target[obj]["angular_velocity"])) * self.pd.dt + + def compute_cost(self, x_ref, u_ref, target): + self.cost = 0 + if not self.isTerminal: + self.control_cost(u_ref) + #self.constraint_control_cost() + self.target_cost(target) + + self.body_reg_cost(x_ref=x_ref) + + return self.cost \ No newline at end of file diff --git a/python/quadruped_reactive_walking/WB_MPC_Wrapper.py b/python/quadruped_reactive_walking/WB_MPC_Wrapper.py index cbcb8aa74b7bdc08145265b462d4dfd0589912ad..87c8cc58ebc8784102982f7c93fcf5e5cd0c5f13 100644 --- a/python/quadruped_reactive_walking/WB_MPC_Wrapper.py +++ b/python/quadruped_reactive_walking/WB_MPC_Wrapper.py @@ -6,7 +6,7 @@ from time import time, sleep import numpy as np -from .WB_MPC.CrocoddylOCP import OCP +from .WB_MPC.CasadiOCP import CasadiOCP import quadruped_reactive_walking as qrw diff --git a/python/quadruped_reactive_walking/tools/PyBulletSimulator.py b/python/quadruped_reactive_walking/tools/PyBulletSimulator.py index 22e140b9860d75287fb39f91c3f98c6dc6f79436..3bffd88dfb8dafab1c0f9cbcf8849a332c554b26 100644 --- a/python/quadruped_reactive_walking/tools/PyBulletSimulator.py +++ b/python/quadruped_reactive_walking/tools/PyBulletSimulator.py @@ -323,6 +323,42 @@ class pybullet_simulator: pin.Quaternion(pin.rpy.rpyToMatrix(p_roll, p_pitch, 0.0)).coeffs(), ) + # Progressively raise the base to achieve proper contact (take into account radius of the foot) + while ( + pyb.getClosestPoints( + self.robotId, + self.planeId, + distance=0.005, + linkIndexA=feetLinksID[i_min], + ) + )[0][8] < -0.001: + z_min -= 0.001 + pyb.resetBasePositionAndOrientation( + self.robotId, + [0.0, 0.0, -z_min], + pin.Quaternion(pin.rpy.rpyToMatrix(p_roll, p_pitch, 0.0)).coeffs(), + ) + # Get position of feet in world frame with base at (0, 0, 0) + feetLinksID = [3, 7, 11, 15] + linkStates = pyb.getLinkStates(self.robotId, feetLinksID) + + # Get minimum height of feet (they are in the ground since base is at 0, 0, 0) + z_min = linkStates[0][4][2] + i_min = 0 + i = 1 + for link in linkStates[1:]: + if link[4][2] < z_min: + z_min = link[4][2] + i_min = i + i += 1 + + # Set base at (0, 0, -z_min) so that the lowest foot is at z = 0 + pyb.resetBasePositionAndOrientation( + self.robotId, + [0.0, 0.0, -z_min], + pin.Quaternion(pin.rpy.rpyToMatrix(p_roll, p_pitch, 0.0)).coeffs(), + ) + # Progressively raise the base to achieve proper contact (take into account radius of the foot) while ( pyb.getClosestPoints(