Code Monkey home page Code Monkey logo

consim's Introduction

CONSIM - CONtact SIMulation

CONSIM is C++ library for simulation of rigid multi-body dynamics with contacts, based on Pinocchio.

Dependencies

For the cpp code:

For the python code:

Installation

First you need to install all dependencies. To install pinocchio and the other dependencies you can follow the instruction on the associated website, or you can install them via apt: sudo apt install robotpkg-py27-example-robot-data robotpkg-collada-dom robotpkg-gepetto-viewer robotpkg-gepetto-viewer-corba robotpkg-hpp-fcl+doc robotpkg-osg-dae robotpkg-py27-pinocchio

To build this library

cd $DEVEL/openrobots/src/
git clone --recursive [email protected]:andreadelprete/consim.git
cd consim
mkdir _build-RELEASE
cd _build-RELEASE
cmake .. -DCMAKE_BUILD_TYPE=RELEASE -DCMAKE_INSTALL_PREFIX=$DEVEL/openrobots
make install

Python Bindings

To use this library in python, we offer python bindings based on Boost.Python and EigenPy.

To install EigenPy you can compile the source code:

git clone https://github.com/stack-of-tasks/eigenpy

or, on Ubuntu, you can use apt-get:

sudo apt-get install robotpkg-py27-eigenpy

For testing the python bindings, you can run the unit test scripts in the script folder, for instance:

ipython script/test_solo_cpp_sim.py

To test the python version of the simulator (with matrix exponential) you can run:

python scripts/test_exp_integrator_with_quadruped.py

Citing CONSIM

To cite CONSIM in your academic research, please use the following bibtex line:

@article{Hammoud2022,
author = {Hammoud, Bilal and Olivieri, Luca and Righetti, Ludovic and Carpentier, Justin and {Del Prete}, Andrea},
doi = {10.1007/s11044-022-09818-z},
issn = {1573-272X},
journal = {Multibody System Dynamics},
number = {4},
pages = {443--460},
title = {{Exponential integration for efficient and accurate multibody simulation with stiff viscoelastic contacts}},
url = {https://doi.org/10.1007/s11044-022-09818-z},
volume = {54},
year = {2022}
}

consim's People

Contributors

andreadelprete avatar hammoudbilal avatar jcarpent avatar olimexsmart avatar skleff1994 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

skleff1994

consim's Issues

stop-watch.hpp already included in expokit headers

Hi @andreadelprete
I managed to compile everything perfectly, however, the profiler you use (e.g. stopwatch) are already included in the expokit headers. This could result in multiple definition conflict while compiling.
For now I just removed the line (#include "consim/utils/stop-watch.hpp") from simulator.cpp
we might want to think of separating this in a better way

gcc internal error segmentation fault

This issue is just to remember how I fixed the segmentation fault of gcc that I was getting when compiling consim. It was a memory issue: basically the compiler did not have enough memory to compile and so it crashed. To solve the problem I've created a swap memory (4 GB) following this tutorial:
https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04
That's it. This could be useful to somebody else to know, or even just to my future self.

Check for pulling force

Hi @andreadelprete,
for the euler simulator, whenever we compute a contact force that turns out to exert a pull, we set it manually to zero to avoid a pulling force, this is implemented in the link below
https://github.com/andreadelprete/consim/blob/master/src/contact.cpp#L48

however, computing the force the way we do currently in con_sim, it seems to me that we have no way of setting this pull force to zero, most likely this explains the different bounce back during the ball drop between both simulators.
The easiest solution I can think of is to set f_projection during checking the friction cone to zero in case this occurs. and use
dv_projection = Minv*(tau - h + J.T * f_projection)
however, I cannot think of a solution for the case when there is no cone violation.

we could use this method to integrate all the time, but we lose the advantage of the closed form second order solution for the positions.

how do you think I should proceed with this ?

Quadruped Trot Results

Now that the C++ and the Python versions of the simulators are giving the same results, I've repeated the tests with Solo12 trotting:
figure_1
Exponential is always better than Euler, except for the smallest time step tested, which is 1/16 ms. Looking at the error over time we can see that actually Exponential is always better than Euler until the very end of the motion, when a very large error occurs that make the overall performance of Exponential worse of Euler:
figure_3
The superiority of Exponential is even clearer if we reset the state of the system to the ground truth after every step, so that we measure the local integration error rather than the global one.
figure_3
There are only two time instants when Exponential has a larger error than Euler, but since one of these errors is very large (at 2.6 s), this makes the total error of Exponential be larger than the one of Euler. We need to investigate to understand what happens at that specific moment in time.

Quadruped Jump Results

I've ran some tests on the Solo jumping motion generated by @hammoudbilal .
It seems to work well, and contrary to the trotting motion here we get even use a control time step of 10 ms (for trotting the max control time step to achieve stability was 2 ms).
Here you can see the local integration errors:
jump_dt_10ms_mean_inf_norm
For large time steps the gain of the exponential integrator is huge because Euler gets unstable. If we are not interested in high accuracy (as in MPC), exponential is an interesting choice, because it gives reasonable results for large time steps (10 ms => ndt=1). To achieve the same accuracy (i.e. 0.2) with Euler we should use a time step 24 times smaller!
Exponential keeps beating Euler even for smaller time steps, but the gain decreases.

few issues with HalfPlane

I setup a small demo with a point mass falling onto the inclined plane then sliding down,
there are few issues that remain

  1. sliding of point mass doesn't stop no matter how much I increase coefficient of friction
  2. the plane I setup intersects the floor at z = 0, ideally the contact object should switch there from half plane to floor, it doesn't

looking at the friction force, it is in the opposite direction (with the inclination of the plane),
it is probably something wrong with how I am picturing the tangent directions in my head and transforming them

Avoid for loops for vector operations

I just noticed that often vector operations are performed through for loops. See for instance here. This is in general inefficient because vector operations can exploit custom CPU instructions, so we should try to fix that.

Bug in exponential integrator without double integral

Hi @hammoudbilal

I think I've found a bug in the exponential integrator, in the branch of the code dealing with the case in which contact forces are slipping. In this case, you don't compute the double integral of the forces, but you simply saturate the average contact forces computed through the first integral, and then assume that those forces have been constant through the time step. Here it is in the code of your pull request.

I think the mistake is this line:

vMean_ = v_ + sub_dt* dvMean_; 

which should be:

vMean_ = v_ + 0.5*sub_dt* dvMean_; 

This is also coherent with the equations I had written in the paper (eq. 24 in particular).

I've actually discovered this bug by mistake, simply because I was running some tests to see how not using the second integral could affect accuracy, and here it is what I've found:
exp_int_without_double_integral_after_bug_fix
According to this result, it seems that the double integral is basically useless for small time steps (<=0.3ms) but rather important for larger time steps. In conclusion, I think we should try to use the double integral also when contacts are slipping! for this reason, I've just modified the theory in the paper to do so (at least in the first method to handle friction cones). It's actually pretty simple. Let me know if you have questions though.

Sanity check: Bilateral contacts

To verify that the exponential integrator is working fine we can test it on a linear system with bilateral contacts. In this case the result of the integration should be independent of the integration time step.

I added an input parameter in the method addContactPoint to specify whether the contact should be unilateral. If that flag is set to false, then the contact is treated as bilateral. This means that, once that point collides with an object (whichever), the contact will never break. Moreover, all checks related to friction cones are disabled for bilateral contacts.

Using this new feature I then modified the point_mass_simulation.py script to compare exponential and Euler integrators in the case of a point mass system. To avoid errors due to the differences in collision detection, I started the simulation with the point mass already colliding with the ground (just an epsilon below the floor). The "outer" time step was set to 10 ms, but then I specified a different number of "inner" time steps, either 1, or 10, or 100 (which are the numbers in the figure legend). Here you can see the resulting trajectories (z axis).

ball_height

As expected, the exponential integrators always give the same result, regardless of the number of inner time steps (1, 10, 100). "Euler 100" (i.e. with an inner time step of 0.1 ms) gave results quite similar to the exponential integrators. "Euler 10" instead gave a very different result. I didn't include "Euler 1" because it went unstable.

In conclusion, this test seems to validate the exponential integrators, and it confirms that they give exact results when applied to linear dynamical systems.

bug in c++ exponential simulator

hi @andreadelprete

I finally managed to pin down the source of part of the code with the bug but i still can't figure out what is causing it, so maybe you could take a look ?

the bug appears at any change in the number of active contacts, so I turned to printing things at contact switches, I start by tracking change in number of active contacts here.

I print each active contact position, velocity and force here, and up till this point things still match the python code,

then I try to populate the jacobian matrix here , at this part of the code, there seems to be a miss match of which contacts are active, this prints out the following results

0.251 Number of active contacts changed from 4 to 3
this is the print from computeContactForces()
HL_FOOT p [ -0.195408 0.145106 -6.2304e-05] v [ 0.0227138 -0.00558171 0.0454345] f [0 0 0]
HR_FOOT p [ -0.19545 -0.145132 -0.000144996] v [-0.0180531 -0.0047408 -0.0212274] f [6.69764 3.07576 15.6711]
FL_FOOT p [ 0.195432 0.145123 -0.000199007] v [ 0.0124034 0.00010649 -0.017879] f [-4.37274 -1.8537 15.3091]

this is the print from computeIntegrationTerms(), only two active contacts, even though Jc_ is resized with the correct dimensions, i.e the last three rows remain zero
jacobian of contact HR_FOOT
1 8.2631e-05 -0.000400766 7.73157e-05 -0.205176 0.150616 0 0 0 0 0 0 0 0 0 3.53956e-05 -0.20523 -0.0915469
-8.21861e-05 0.999999 0.00110999 0.204925 0.000248528 -0.208728 0 0 0 0 0 0 0 0 0 0.205041 9.08537e-05 0.000526424
0.000400858 -0.00110996 0.999999 -0.150861 0.208633 0.000292048 0 0 0 0 0 0 0 0 0 -0.0462609 0.0186329 0.131221
jacobian of contact FL_FOOT
1 8.2631e-05 -0.000400766 -3.90305e-05 -0.204908 -0.139638 2.88968e-06 -0.202932 -0.106201 0 0 0 0 0 0 0 0 0
-8.21861e-05 0.999999 0.00110999 0.205136 -0.000185331 0.182155 0.20502 0.000425632 -0.00621826 0 0 0 0 0 0 0 0 0
0.000400858 -0.00110996 0.999999 0.139425 -0.182225 -0.000258153 0.0348255 0.00776463 -0.11951 0 0 0 0 0 0 0 0 0

you can see that computeContactForce() can see all three active contacts
where as the function right after it, computeIntegrationTerms() seems to only see two active contacts, I still can't pinpoint what is changing the status of one of the active contacts, could you please take a look ?

Contacts implementation

Hi @andreadelprete
This assumes all contacts are active all the time

If I have switching contacts I will have to keep changing the size of the vectors/matrices used,
eg: x, intx, int2x ... etc
to implement this I will have to resize the Eigen::MatrixXd & Eigen::VectorXd at every integration step, are you aware of more efficient ways for implementing this?

i thought about keeping the maximum size and filling the variables relative to the inactive contacts with zero value, but it seems a bit dangerous to me to do that. Also it makes computing the exponential more expensive as the matrix dimension increases by 6 for every inactive contact.

Romeo walking Results

I've added to the benchmarks a two-step walk with Romeo, just to have a biped. In short, results are good.
Here the local integration errors, time step 2 ms:
romeo_walk_err
For dt=2 ms Euler is unstable so it gives a huge error. For smaller time steps it remains stable, but the integration error is always a couple of orders of magnitude larger than for Exponential.

This plots shows the integration error over time:
romeo_walk_err_traj
For most of the time the integration error of Exponential is well below Euler. In a few time steps we can see spikes in the Exponential error. These spikes are due to errors in contact detection, which are independent of the integration scheme, so during these time steps the error of Exponential is comparable to the one of Euler.

Since now we have good results for Solo squatting, Solo jumping, Solo trotting and Romeo walking, we can start working on the computation time.

Results with seldom updates of matrix exponential

I've carried out some preliminary tests to check this idea: what if we did not update the matrix exponential at every iteration? This would save a lot of computation time, and if the force prediction remains accurate over a long horizon, maybe we don't lose much accuracy.
Here are the integration errors I've got by updating the matrix exponential every 1, 2 and 4 iterations:
integrator_error_with_seldom_expm_updates
Updating every 2 iterations we don't lose any accuracy (for the time steps tested, but we would for larger time steps). Updating every 4 iterations we have a large loss of accuracy for ndt = 8 and 16, but everything remains stable. For ndt>=32 instead we have no loss of accuracy.

Overall, this test shows that the force predictions obtained with the matrix exponential are more accurate than the other terms of the dynamics (M, h and J), so we don't need to update them very often to achieve high accuracy. This affects much more the "right part" of our plots, that depicts the high accuracy cases. For the cases with large time steps (>1 ms), not updating the matrix exponential at every iteration would probably degrade performance. I'm gonna run more tests and update the issue soon.

Anchor point update during slipping

Currently, when a contact is slipping, the anchor point is updated to be equal to the contact point, see here. I believe this is not correct, because this makes the elastic force null during sliding. I think instead the anchor point should move as little as possible while maintaining the contact force inside the friction cone (i.e. on its boundaries).

Accuracy of the Euler integration

These lines of code perform a classic first-order Euler integration step

consim/src/simulator.cpp

Lines 237 to 240 in c1d6b07

vMean_ = v_ + .5 * sub_dt*dv_;
pinocchio::integrate(*model_, q_, vMean_ * sub_dt, qnext_);
q_ = qnext_;
v_ += dv_ * sub_dt;

I was wondering why to not use a more stable approach based on Euler semi-implicit, namely:

 v_ += dv_ * sub_dt; 
 pinocchio::integrate(*model_, q_, v_ * sub_dt, qnext_); 
 q_ = qnext_; 

This would lead to a second-order accuracy.

Potential influcence of the cone projection

The following lines are performing a projection on the friction cone:

consim/src/contact.cpp

Lines 109 to 122 in c1d6b07

/*!< force along normal to contact object */
normalNorm_ = f.dot(cp.contactNormal_);
/*!< unilateral force, no pulling into contact object */
if (cp.unilateral && normalNorm_<0.0){
f.setZero();
return;
}
tangentF_ = f - normalNorm_*cp.contactNormal_;
tangentNorm_ = tangentF_.norm();
if (cp.unilateral && (tangentNorm_ > friction_coeff_*normalNorm_)){
tangentDir_ = tangentF_/tangentNorm_;
f = normalNorm_*cp.contactNormal_ + (friction_coeff_*normalNorm_)*tangentDir_;
}

but this does not correspond to an orthogonal projection. This might lead to a bias in the solution.
I don't know how much this may influence the final solution, but for sure, if you want to use this step inside a gradient descent framework, it might lead to a feasible but not optimal solution.

Sanity check: Sliding point mass

I've implemented the force projection method in the exponential simulator in Python and I've run some tests with a simple point mass sliding on a flat floor. The outer time step was 5 ms, stiffness 1e5, damping 2e2, and I apply a sinusoidal lateral force to the mass so that it slips.
Here you can see the integration errors as a function of the number of inner time steps, in case the Euler simulator is used to compute the ground truth:
slipping_point_mass_euler_ground_truth

And here in case the Exponential Simulator is used for the ground truth:
slipping_point_mass_exp_ground_truth

As you can see, results are almost the same in the two case, which means that for small time steps the two approaches converge to similar solutions. Moreover, we can see that the exponential simulator works better in general. The large error decrease happening at ndt=4 (which corresponds to an inner time step of 5/4 ms) is due to the improved accuracy in detecting when slippage ends, as we can see in this plot showing the integration error vs time:
slipping_point_mass_exp_int_error_vs_time
We can also notice that the integration error stays constant during slippage (i.e. [0.03, 0.3]).
Next I'm gonna check this works even with the quadruped.

computing frame acceleration dJv

hi @andreadelprete
returning frame acceleration from pinocchio as I understand requires you to call second order forward kinematics. i.e. FK(q, dq, ddq)

dJv_local = se3.getFrameAcceleration(self.model, self.data, self.frame_id)

is it called anywhere else, the only forward kinematics call I can find is
https://github.com/andreadelprete/consim/blob/master/script/robot_simulator_exponential_integrator.py#L274

ExponentialSimulator accuracy with nonzero impact velocity

Carrying on with the benchmarks, I was unable to get good results with the quadruped trotting motion. Exponential shows good stability with large time steps, but for smaller time steps, when also Euler is stable, the integration error is lower for Euler than for Exponential, which doesn't make sense to me.

So I went back to the squatting test, where results were good, and I was able to figure out the difference between squatting and trotting: during squatting we have just one impact (at the beginning of the motion) and contact points have zero velocity. Indeed, by increasing the initial velocity of the base in the vertical downward direction, I was able to deteriorate the accuracy of Exponential:

INITIAL VEL 0
euler  32 Chol Total error: -3.58
exp  32 Total error: -4.59

INITIAL VEL  2e-2
euler  32 Chol Total error: -3.63
exp  32 Total error: -4.28

INITIAL VEL  5e-2
euler  32 Chol Total error: -3.71
exp  32 Total error: -3.98

INITIAL VEL 10e-2
euler  32 Chol Total error: -3.73
exp  32 Total error: -3.70

Also the accuracy of Euler deteriorates with increasing velocity, but much more slowly. I think this points to a bug in the code or a flaw in the theory. I'm investigating. I think this has top priority at the moment over all other aspects.

Integration accuracy without contacts

In theory I think that Euler and Exponential should be equivalent as long as the robot is in the air. However, according to a test I just did (with the c++ simulators) they are not. Exponential is much more accurate. I just initialize Solo12, 1 meter above the ground and let it fall with zero joint torques. These are the integration errors over time:
solo_in_the_air
For some reason, Euler gets a very large error in the first integration step, whereas Exponential does not. Unless there's something I am missing, I'd say this is a bug.

@hammoudbilal could you look into this? I am using the latest version of the c++ simulator in master. The python script you can use to reproduce these results is this. This script is supposed to be used for a trotting motion, but since results looked bad for trotting I've started simplifying things (in particular here and here) until I've found this issue.

Exponential VS Rigid-contacts

Today I've run some tests on a classic rigid-contact simulator, without much hope to find anything interesting, but I was wrong.

I've implemented a simple simulator that uses the pinocchio::forwardDynamics function to compute the accelerations of a robot subject to rigid contact constraints J*dv = -dJ*v + PD, where PD is a proportional-derivative contact stabilization term. Then these accelerations are integrated using explicit Euler. I've assumed contacts cannot break (i.e. bilateral contacts), and I've used the "solo squat" test to compare the accuracy-speed trade-off of this RigidEuler simulator against our good-old Exponential simulator. I was expecting RigidEuler to be better because the rigid contact assumption doesn't lead to stiff differential equations as the viscoelastic model.

First of all, I've seen that it's very important to properly tune the PD contact stabilization gains to get good results, otherwise RigidEuler can perform terribly for large time steps. The best tuning rule I've found is to set kd=1/dt (which tries to bring the contact point velocity to zero in one time step) and kp=0.5*kd^2 (which makes the PD critically damped). Using this tuning I got the following results plotting integration error VS dt:
expo_vs_rigid_dt
The two simulators performed very similarly for dt<=2.5, but for larger dt (5 and 10 ms) RigidEuler deteriorated. Looking at the results in the viewer I could see the robot's feet penetrating into the ground with some very fast movements before going back in place.

Plotting the integration error VS real-time factor (as in the paper):
expo_vs_rigid_rtf
RigidEuler benefits from being faster for 1 iteration (about 2x), but we can see there is a small range of RT-factors where Exponential seems the best choice. Most importantly, this happens in the high-speed low-accuracy range, which is what we are interested in for MPC.

This is of course a small win, but considering I started these tests expecting not to find anything worth reporting, it's kind-of a big result.

First benchmark Exponential vs Euler

I've carried out the first rigorous test to compare Exponential and Euler. I describe the settings and show the results in this issue.

Description

  • robot: quadruped Solo
  • motion: standing on four feet, following sinusoidal trajectory on CoM vertical direction
  • controller: TSID with small gains (kp=2)
  • controller time step: 10 ms
  • total simulation time: 0.1 s

I computed the ground truth using either Exponential or Euler with a very small time step (i.e. about 10 us), and then I measured the error between the ground-truth state trajectories and the ones computed by Exponential and Euler using larger time steps. Time steps have been chosen starting from the controller time step (10 ms) and dividing it by powers of 2, starting from 1 up to 1024 (which gives the time step used to compute the ground truth). The power of two by which we divide the controller time step to get the simulation time step is called ndt, and it appears in the X axis of the plots below.

Since choosing one of the two methods to compute the ground truth does not seem fair, I repeated the test twice, one using Euler for the ground truth, one using Exponential for the ground truth. The results however do not change much, which makes sense because we've already seen that for small time steps the two approaches converge.

Results

These are the results using Exponential for the ground truth.
consim_integration_error_using_exp_as_ground_truth

These are the results using Euler for the ground truth.
consim_integration_error_using_euler_as_ground_truth

Comments

  • The plots show two errors: the final error is the error in the final state, whereas the total error (more interesting) is the error throughout the whole trajectory
  • The results in the two plots are very similar, except for the final point on the right, where of course the method that has been used for the ground truth has an advantage, but that's obvious and uninteresting.
  • There are no results of Euler for ndt smaller than 128 because it always went unstable, whereas Exponential remained stable up to ndt=1
  • For any given time step, Exponential has a smaller error than Euler

Force prediction accuracy

I open this issue to discuss the accuracy of the exponential simulator, which in my latest tests with the Python simulator does not seem to exceed the accuracy of the Euler simulator.

As a first test I've started investigating the accuracy of the force prediction based on the linear dynamical system, which is at the core of the exponential simulator. I've run some tests with the quadruped performing a sinusoidal squatting motion with the CoM. Looking at this plot, the accuracy seems rather good (at least for time steps of 5 ms):
force_prediction_exp_integration
In this test I was integrating with the Exponential simulator (using dt=5ms and ndt=50), and using the matrix exponential computed at the first inner step to predict the contact forces at the next 49 inner steps. The blue circles are the forces at the beginning of each time step, used as a starting point for the prediction. The red triangles are the predicted forces, and the blue crosses are the real forces. We can see that the prediction matches rather well the real forces, especially in the first part.

Reset of contact state

For benchmarking I think it's gonna be useful to be able to reset the state of both robot and contacts. At the moment we can only reset the state of the robot with the reset_state method. However, each contact has an internal state, which is the anchor point x_start. When comparing different simulators in case of slippage we cannot compare long trajectories because if one simulator makes a mistake in the computation of the slipping velocity at the beginning, that would create an offset in the resulting trajectory. The solution suggested in [1] is to compare short trajectory pieces, re-initializing every time the simulator to the state of the ground-truth. I think we should be able to do the same, having a python method to set the anchor point for each contact.

[1] Erez, T., Tassa, Y., & Todorov, E. (2015). Simulation Tools for Model-Based Robotics: Comparison of Bullet, Havok, MuJoCo, ODE and PhysX. In International Conference on Robotics and Automation. https://doi.org/10.3171/PED/2008/1/2/138

Quadruped Squat Results

In the last week I've optimized a bit the c++ code, and here I report the results I got for the quadruped squatting task. I've decided to start with this task because here the number of contacts is constant throughout the simulation, so we don't have to worry about memory allocation that is needed when resizing.

Besides testing the standard Exponential and Euler integrators, now I am also testing different versions of Exponential in which I bound the number of matrix multiplications to be used for computing the matrix exponential. Moreover, now I am plotting the integration error VS the computation time (per time step, which is 10 ms in this test) rather than VS ndt.

local_err_vs_comp_time

The best method for a given accuracy (y axis) is the one corresponding to the smallest computation time (x axis). We can see that Euler is never the best, it just gets almost as good as Exponential for integration error = 2e-4. Comparing the different versions of Exponential, the one performing the default number of matrix multiplications ("mmm-1" in the legend) is constantly outperformed by the other versions. The best versions are the ones using 0, 2, or 3 matrix multiplications, depending on the time step used. In general, for smaller time steps (hence smaller integration errors) it is preferable to use less matrix multiplications. Automatically finding the optimal number of matrix multiplications does not seem easy, but even if we simply fix it to 3, we would still outperform the default method for any time step.

Code refactoring

Hi @hammoudbilal

as we already started discussing, the code could probably use some refactoring because at the moment it's a bit messy. I haven't had the time to think about it properly, but I have recently developed a simple simulator in Python for a class I am teaching. I've tried to make it as simple as possible so that student could understand it easily. I'm sharing it with you here. Maybe this code could be a starting point to think of the new code

import pinocchio as se3
import numpy as np
from numpy.linalg import norm
import os
import math
import time

class ContactPoint:
    ''' A point on the robot surface that can make contact with surfaces.
    '''
    def __init__(self, model, data, frame_name):
        self.model = model      # robot model
        self.data = data        # robot data
        self.frame_name = frame_name    # name of reference frame associated to this contact point
        self.frame_id = model.getFrameId(frame_name)    # id of the reference frame
        self.active = False         # True if this contact point is in contact
        
    def get_position(self):
        ''' Get the current position of this contact point 
        '''
        M = self.data.oMf[self.frame_id]
        return M.translation
        
    def get_velocity(self):
        M = self.data.oMf[self.frame_id]
        R = se3.SE3(M.rotation, 0*M.translation)    # same as M but with translation set to zero
        v_local = se3.getFrameVelocity(self.model, self.data, self.frame_id)
        v_world = (R.act(v_local)).linear   # convert velocity from local frame to world frame
        return v_world
        
    def get_jacobian(self):
        J6 = se3.getFrameJacobian(self.model, self.data, self.frame_id, se3.ReferenceFrame.LOCAL_WORLD_ALIGNED)
        return J6[:3,:]
        
        
class ContactSurface:
    ''' A visco-elastic planar surface
    '''
    def __init__(self, name, pos, normal, K, B, mu):
        self.name = name        # name of this contact surface
        self.x0 = pos           # position of a point of the surface
        self.normal = normal    # direction of the normal to the surface
        self.K = K              # stiffness of the surface material
        self.B = B              # damping of the surface material
        self.mu = mu            # friction coefficient of the surface
        self.bias = self.x0.dot(self.normal)
        
    def check_collision(self, p):
        ''' Check the collision of the given point
            with this contact surface. If the point is not
            inside this surface, then return False.
        '''
        normal_penetration = self.bias - p.dot(self.normal)
        if(normal_penetration < 0.0):
            return False # no penetration
        return True
        
    def compute_force(self, contact_point, anchor_point):
        cp = contact_point
        p0 = anchor_point
        p = cp.get_position()
        v = cp.get_velocity()

        # compute contact force using spring-damper law
        f = self.K.dot(p0 - p) - self.B.dot(v)
        
        # check whether contact force is outside friction cone
        f_N = f.dot(self.normal)   # norm of normal force
        f_T = f - f_N*self.normal  # tangential force (3d)
        f_T_norm = norm(f_T)            # norm of tangential force
        if(f_T_norm > self.mu*f_N):
            # contact is slipping 
            t_dir = f_T / f_T_norm  # direction of tangential force
            # saturate force at the friction cone boundary
            f = f_N*self.normal + self.mu*f_N*t_dir
            
            # update anchor point so that f is inside friction cone
            delta_p0 = (f_T_norm - self.mu*f_N) / self.K[0,0]
            p0 -= t_dir*delta_p0
            
        return f, p0
        


class Contact:
    ''' A contact between a contact-point and a contact-surface
    '''
    def __init__(self, contact_point, contact_surface):
        self.cp = contact_point
        self.cs = contact_surface
        self.reset_contact_position()

    def reset_contact_position(self):
        # Initialize anchor point p0, that is the initial (0-load) position of the spring
        self.p0 = self.cp.get_position()
        self.in_contact = True

    def compute_force(self):
        self.f, self.p0 = self.cs.compute_force(self.cp, self.p0)
        return self.f
        
    def get_jacobian(self):
        return self.cp.get_jacobian()


class RobotSimulator:

    # Class constructor
    def __init__(self, conf, robot):
        self.conf = conf
        self.robot = robot
        self.model = self.robot.model
        self.data = self.model.createData()
        self.t = 0.0                    # time
        self.nv = nv = self.model.nv    # Dimension of joint velocities vector
        self.na = na = robot.na         # number of actuated joints
        # Matrix S used as filter of vetor of inputs U
        self.S = np.hstack((np.zeros((na, nv-na)), np.eye(na, na)))

        self.contacts = []
        self.candidate_contact_points = [] # candidate contact points
        self.contact_surfaces = []

        self.init(conf.q0, None, True)
        
        self.tau_c = np.zeros(na)   # Coulomb friction torque
        self.simulate_coulomb_friction = conf.simulate_coulomb_friction
        self.simulation_type = conf.simulation_type
        if(self.simulate_coulomb_friction):
            self.tau_coulomb_max = 1e-2*conf.tau_coulomb_max*self.model.effortLimit
        else:
            self.tau_coulomb_max = np.zeros(na)
            

    # Re-initialize the simulator
    def init(self, q0=None, v0=None, reset_contact_positions=False):
        self.first_iter = True

        if q0 is not None:
            self.q = q0.copy()
            
        if(v0 is None):
            self.v = np.zeros(self.robot.nv)
        else:
            self.v = v0.copy()
        self.dv = np.zeros(self.robot.nv)
        self.resize_contact_data(reset_contact_positions)
        
        
    def resize_contact_data(self, reset_contact_positions=False):
        self.nc = len(self.contacts)                    # number of contacts
        self.nk = 3*self.nc                             # size of contact force vector
        self.f = np.zeros(self.nk)                      # contact forces
        self.Jc = np.zeros((self.nk, self.model.nv))    # contact Jacobian

        # reset contact position
        if(reset_contact_positions):
            se3.forwardKinematics(self.model, self.data, self.q)
            se3.updateFramePlacements(self.model, self.data)
            for c in self.contacts:
                c.reset_contact_position()

        self.compute_forces(compute_data=True)

    def add_candidate_contact_point(self, frame_name):
        self.candidate_contact_points += [ContactPoint(self.model, self.data, frame_name)]
        
    def add_contact_surface(self, name, pos, normal, K, B, mu):
        ''' Add a contact surface (i.e., a wall) located at "pos", with normal 
            outgoing direction "normal", 3d stiffness K, 3d damping B.
        '''
        self.contact_surfaces += [ContactSurface(name, pos, normal, K, B, mu)]
         
    def collision_detection(self):
        for s in self.contact_surfaces:     # for each contact surface
            for cp in self.candidate_contact_points: # for each candidate contact point
                p = cp.get_position()
                if(s.check_collision(p)):   # check whether the point is colliding with the surface
                    if(not cp.active): # if the contact was not already active
                        print("Collision detected between point", cp.frame_name, " at ", p)
                        cp.active = True
                        cp.contact = Contact(cp, s)
                        self.contacts += [cp.contact]
                        self.resize_contact_data()
                else:
                    if(cp.active):
                        print("Contact lost between point", cp.frame_name, " at ", p)
                        cp.active = False
                        self.contacts.remove(cp.contact)
                        self.resize_contact_data()


    def compute_forces(self, compute_data=True):
        '''Compute the contact forces from q, v and elastic model'''
        if compute_data:
            se3.forwardKinematics(self.model, self.data, self.q, self.v)
            # Computes the placements of all the operational frames according to the current joint placement stored in data
            se3.updateFramePlacements(self.model, self.data)
            self.collision_detection()
            
        i = 0
        for c in self.contacts:
            self.f[i:i+3] = c.compute_force()
            self.Jc[i:i+3, :] = c.get_jacobian()
            i += 3
        return self.f


    def step(self, u, dt):
        # (Forces are directly in the world frame, and aba wants them in the end effector frame)
        se3.computeAllTerms(self.model, self.data, self.q, self.v)
        se3.updateFramePlacements(self.model, self.data)
        M = self.data.M
        h = self.data.nle
        self.collision_detection()
        self.compute_forces(False)

        self.tau_c = self.tau_coulomb_max*np.sign(self.v[-self.na:])
        self.dv = np.linalg.solve(M, self.S.T.dot(u-self.tau_c) - h + self.Jc.T.dot(self.f))
        v_mean = self.v + 0.5*dt*self.dv
        self.v += self.dv*dt
        self.q = se3.integrate(self.model, self.q, v_mean*dt)

        self.t += dt
        return self.q, self.v

    def reset(self):
        self.first_iter = True

    def simulate(self, u, dt=0.001, ndt=1):
        ''' Perform ndt steps, each lasting dt/ndt seconds '''
        tau_c_avg = 0*self.tau_c
        for i in range(ndt):
            self.q, self.v = self.step(u, dt/ndt)
            tau_c_avg += self.tau_c
        self.tau_c = tau_c_avg / ndt

        return self.q, self.v, self.f

If you prefer I can push it in the repo, together with a simple test and the additional code for visualization with gepetto-viewer. I don't know if all of this is necessary though if we wanna use it just as inspiration for the design.

New method for friction cone constraints

I was not completely satisfied with the QP-based method for handling friction cones. The reason was already explained in the paper in footnote 2:

This expression is based on the assumption of constant u, but if we assume that p_0 moves at constant velocity then u is not a constant, but a linear function of time. I should investigate how this affect the equations.

In the last week I've investigated how the computation changes if we consider a linear function of time in the contact dynamics, which then takes this form:
\begin{align}
\dot{x} = A x + b_0 + t b_1
\end{align}
Another extra piece of complexity is that, besides computing the first two integrals of x, we also need their expressions as linear functions of b_0 and b_1, which will then appear in the constraints of the QP.

It turns out that, if computation time is not critical, computing these quantities is rather easy because we can simply define an augmented state including b_0, b_1, t*b_1 and the first two integrals of x, and this augmented state has a purely linear dynamics. Therefore, by computing the expm of a large and sparse matrix we can get everything we need. However, since this matrix is 6 times larger than A and the computational complexity of expm is cubic, we can expect this to be about 216 times more expensive than expm(A). This is unacceptable for our application.

The good news is that this large matrix of which we should compute expm is very sparse and structured, so I wrote down a few pages of math and I've figured out a way to compute everything by exploiting the sparsity. In the beginning I thought I could get to the same complexity of expm(A), but it turns out that I cannot: the new approach should be about 3 times more expensive than expm(A); this is a big improvement wrt what I described above, but not as good as I hoped. I've implemented this in Python, it seems to work fine, and now @olimexsmart could take care of switching it to C++.

However, I don't know whether following this path is a good idea.

  • The implementation of this computation is not trivial, and would probably take @olimexsmart several days to do it in C++.
  • We would be losing speed because it's gonna take about 50% more time than what we are currently doing (which is basically computing 2 expm of matrices slightly larger than A)
  • This new approach seems theoretically more sound: we account for a linear motion of the anchor point during the time step, instead of assuming the anchor point is fixed during the time step and the QP can choose where to place it; however, I haven't implemented it yet in the python simulator, so I have no idea whether it really works better than the QP-based approach currently described in the paper

What do you think @hammoudbilal @olimexsmart ?
Maybe before investing time in the c++ implementation I should do some tests in the Python simulator?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.