------------------------------------------------------------ revno: 1935 committer: Václav Šmilauer <[email protected]> branch nick: trunk timestamp: Fri 2010-01-01 16:21:30 +0100 message: 1. Preliminary attempt at https://blueprints.launchpad.net/yade/+spec/c++-numpy-arrays (for postprocessing and similar), example in eudoxos.testNumpy 2. SpherePack::makeCloud can be asked for negative number of spheres (= as much as possible) added: lib/pyutil/ lib/pyutil/README lib/pyutil/gil.hpp lib/pyutil/numpy.hpp lib/pyutil/numpy_boost.hpp modified: pkg/common/Engine/Dispatcher/InteractionDispatchers.cpp pkg/common/Engine/Dispatcher/InteractionGeometryDispatcher.cpp pkg/common/Engine/GlobalEngine/InsertionSortCollider.cpp pkg/common/Engine/GlobalEngine/PeriodicPythonRunner.hpp pkg/dem/DataClass/SpherePack.cpp pkg/dem/DataClass/SpherePack.hpp py/_eudoxos.cpp py/yadeWrapper/customConverters.cpp scripts/test/periodic-triax-velgrad.py
-- lp:yade https://code.launchpad.net/~yade-dev/yade/trunk Your team Yade developers is subscribed to branch lp:yade. To unsubscribe from this branch go to https://code.launchpad.net/~yade-dev/yade/trunk/+edit-subscription.
=== added directory 'lib/pyutil' === added file 'lib/pyutil/README' --- lib/pyutil/README 1970-01-01 00:00:00 +0000 +++ lib/pyutil/README 2010-01-01 15:21:30 +0000 @@ -0,0 +1,2 @@ +numpy_boost.cpp: + http://code.google.com/p/numpy-boost/source/checkout (svn rev 2) === added file 'lib/pyutil/gil.hpp' --- lib/pyutil/gil.hpp 1970-01-01 00:00:00 +0000 +++ lib/pyutil/gil.hpp 2010-01-01 15:21:30 +0000 @@ -0,0 +1,15 @@ +// 2009 © Václav Å milauer <[email protected]> + +#include<Python.h> +//! class (scoped lock) managing python's Global Interpreter Lock (gil) +class gilLock{ + PyGILState_STATE state; + public: + gilLock(){ state=PyGILState_Ensure(); } + ~gilLock(){ PyGILState_Release(state); } +}; + +//! run string as python command; locks & unlocks GIL automatically +void pyRunString(const std::string& cmd){ + gilLock lock; PyRun_SimpleString(cmd.c_str()); +}; === added file 'lib/pyutil/numpy.hpp' --- lib/pyutil/numpy.hpp 1970-01-01 00:00:00 +0000 +++ lib/pyutil/numpy.hpp 2010-01-01 15:21:30 +0000 @@ -0,0 +1,6 @@ +// 2009 © Václav Å milauer <[email protected] + +#include"numpy_boost.hpp" + +// helper macro do assign Vector3r values to a subarray +#define VECTOR3R_TO_NUMPY(vec,arr) arr[0]=vec[0]; arr[1]=vec[1]; arr[2]=vec[2] === added file 'lib/pyutil/numpy_boost.hpp' --- lib/pyutil/numpy_boost.hpp 1970-01-01 00:00:00 +0000 +++ lib/pyutil/numpy_boost.hpp 2010-01-01 15:21:30 +0000 @@ -0,0 +1,199 @@ +/* +Copyright (c) 2008, Michael Droettboom +All rights reserved. + +Licensed under the BSD license. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of its contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* + Documentation to come +*/ + +#ifndef __NUMPY_BOOST_HPP__ +#define __NUMPY_BOOST_HPP__ + +#include <Python.h> +#include <numpy/arrayobject.h> +#include <boost/multi_array.hpp> +#include <boost/cstdint.hpp> +#include <complex> +#include <algorithm> + +namespace detail { + template<class T> + class numpy_type_map { + public: + static const int typenum; + }; + + template<> + const int numpy_type_map<float>::typenum = NPY_FLOAT; + + template<> + const int numpy_type_map<std::complex<float> >::typenum = NPY_CFLOAT; + + template<> + const int numpy_type_map<double>::typenum = NPY_DOUBLE; + + template<> + const int numpy_type_map<std::complex<double> >::typenum = NPY_CDOUBLE; + + template<> + const int numpy_type_map<long double>::typenum = NPY_LONGDOUBLE; + + template<> + const int numpy_type_map<std::complex<long double> >::typenum = NPY_CLONGDOUBLE; + + template<> + const int numpy_type_map<boost::int8_t>::typenum = NPY_INT8; + + template<> + const int numpy_type_map<boost::uint8_t>::typenum = NPY_UINT8; + + template<> + const int numpy_type_map<boost::int16_t>::typenum = NPY_INT16; + + template<> + const int numpy_type_map<boost::uint16_t>::typenum = NPY_UINT16; + + template<> + const int numpy_type_map<boost::int32_t>::typenum = NPY_INT32; + + template<> + const int numpy_type_map<boost::uint32_t>::typenum = NPY_UINT32; + + template<> + const int numpy_type_map<boost::int64_t>::typenum = NPY_INT64; + + template<> + const int numpy_type_map<boost::uint64_t>::typenum = NPY_INT64; +} + +class numpy_boost_exception : public std::exception { + +}; + +template<class T, int NDims> +class numpy_boost : public boost::multi_array_ref<T, NDims> +{ +public: + typedef numpy_boost<T, NDims> self_type; + typedef boost::multi_array_ref<T, NDims> super; + typedef typename super::size_type size_type; + typedef T* TPtr; + +private: + PyArrayObject* array; + + void init_from_array(PyArrayObject* a) { + array = a; + super::base_ = (TPtr)PyArray_DATA(a); + + // It would seem like we would want to choose C or Fortran + // ordering here based on the flags in the Numpy array. However, + // those flags are purely informational, the actually information + // about storage order is recorded in the strides. + super::storage_ = boost::c_storage_order(); + + boost::detail::multi_array::copy_n(PyArray_DIMS(a), NDims, super::extent_list_.begin()); + for (size_t i = 0; i < NDims; ++i) { + super::stride_list_[i] = PyArray_STRIDE(a, i) / sizeof(T); + } + std::fill_n(super::index_base_list_.begin(), NDims, 0); + super::origin_offset_ = 0; + super::directional_offset_ = 0; + super::num_elements_ = std::accumulate(super::extent_list_.begin(), + super::extent_list_.end(), + size_type(1), + std::multiplies<size_type>()); + } + +public: + numpy_boost(PyObject* obj) : + super(NULL, std::vector<boost::uint32_t>(NDims, 0)), + array(NULL) + { + PyArrayObject* a; + + a = (PyArrayObject*)PyArray_FromObject(obj, detail::numpy_type_map<T>::typenum, NDims, NDims); + if (a == NULL) { + // TODO: Extract Python exception + throw numpy_boost_exception(); + } + + init_from_array(a); + } + + numpy_boost(const self_type &other) : + super(NULL, std::vector<boost::uint32_t>(NDims, 0)), + array(NULL) + { + Py_INCREF(other.array); + init_from_array(other.array); + } + + template<class ExtentsList> + explicit numpy_boost(const ExtentsList& extents) : + super(NULL, std::vector<boost::uint32_t>(NDims, 0)), + array(NULL) + { + npy_intp shape[NDims]; + PyArrayObject* a; + + boost::detail::multi_array::copy_n(extents, NDims, shape); + + a = (PyArrayObject*)PyArray_SimpleNew(NDims, shape, detail::numpy_type_map<T>::typenum); + if (a == NULL) { + // TODO: Extract Python exception + throw numpy_boost_exception(); + } + + init_from_array(a); + } + + ~numpy_boost() { + Py_DECREF(array); + } + + void operator=(const self_type &other) { + Py_DECREF(array); + Py_INCREF(other.array); + init_from_array(other.array); + } + + PyObject* + py_ptr() { + Py_INCREF(array); + return (PyObject*)array; + } +}; + +#endif === modified file 'pkg/common/Engine/Dispatcher/InteractionDispatchers.cpp' --- pkg/common/Engine/Dispatcher/InteractionDispatchers.cpp 2009-12-25 14:46:48 +0000 +++ pkg/common/Engine/Dispatcher/InteractionDispatchers.cpp 2010-01-01 15:21:30 +0000 @@ -36,7 +36,8 @@ geomDispatcher->updateScenePtr(); physDispatcher->updateScenePtr(); lawDispatcher->updateScenePtr(); - Vector3r cellSize; if(scene->isPeriodic) cellSize=scene->cell->getSize(); + // transformed cell size + Vector3r cellSize; if(scene->isPeriodic) cellSize=scene->cell->trsf*scene->cell->refSize; bool removeUnseenIntrs=(scene->interactions->iterColliderLastRun>=0 && scene->interactions->iterColliderLastRun==scene->currentIteration); #ifdef YADE_OPENMP const long size=scene->interactions->size(); @@ -89,8 +90,6 @@ if(!scene->isPeriodic) geomCreated=I->functorCache.geom->go(b1->shape,b2->shape, *b1->state, *b2->state, Vector3r::ZERO, /*force*/false, I); else{ // handle periodicity Vector3r shift2(I->cellDist[0]*cellSize[0],I->cellDist[1]*cellSize[1],I->cellDist[2]*cellSize[2]); - // in sheared cell, apply shear on the mutual position as well - shift2=scene->cell->shearPt(shift2); geomCreated=I->functorCache.geom->go(b1->shape,b2->shape,*b1->state,*b2->state,shift2,/*force*/false,I); } if(!geomCreated){ === modified file 'pkg/common/Engine/Dispatcher/InteractionGeometryDispatcher.cpp' --- pkg/common/Engine/Dispatcher/InteractionGeometryDispatcher.cpp 2009-12-20 22:03:17 +0000 +++ pkg/common/Engine/Dispatcher/InteractionGeometryDispatcher.cpp 2010-01-01 15:21:30 +0000 @@ -48,7 +48,7 @@ updateScenePtr(); shared_ptr<BodyContainer>& bodies = scene->bodies; - Vector3r cellSize; if(scene->isPeriodic) cellSize=scene->cell->getSize(); + Vector3r cellSize; if(scene->isPeriodic) cellSize=scene->cell->trsf*scene->cell->refSize; bool removeUnseenIntrs=(scene->interactions->iterColliderLastRun>=0 && scene->interactions->iterColliderLastRun==scene->currentIteration); #ifdef YADE_OPENMP const long size=scene->interactions->size(); @@ -74,7 +74,6 @@ geomCreated=operator()(b1->shape, b2->shape, *b1->state, *b2->state, Vector3r::ZERO, /*force*/ false, I); } else{ Vector3r shift2(I->cellDist[0]*cellSize[0],I->cellDist[1]*cellSize[1],I->cellDist[2]*cellSize[2]); // add periodicity to the position of the 2nd body - shift2=scene->cell->shearPt(shift2); geomCreated=operator()(b1->shape, b2->shape, *b1->state, *b2->state, shift2, /*force*/ false, I); } // reset && erase interaction that existed but now has no geometry anymore === modified file 'pkg/common/Engine/GlobalEngine/InsertionSortCollider.cpp' --- pkg/common/Engine/GlobalEngine/InsertionSortCollider.cpp 2009-12-30 20:10:59 +0000 +++ pkg/common/Engine/GlobalEngine/InsertionSortCollider.cpp 2010-01-01 15:21:30 +0000 @@ -426,9 +426,6 @@ TRVAR4(pmn1,pmx1,pmn2,pmx2); } #endif - /* FIXME: condition temporary disabled - if wMn==m1 and minima[id2]<m1, then it might give spurious error; why should it be error, though? - */ if(((pmn1!=pmx1) || (pmn2!=pmx2))){ Real span=(pmn1!=pmx1?mx1-mn1:mx2-mn2); if(span<0) span=dim-span; LOG_FATAL("Body #"<<(pmn1!=pmx1?id1:id2)<<" spans over half of the cell size "<<dim<<" (axis="<<axis<<", min="<<(pmn1!=pmx1?mn1:mn2)<<", max="<<(pmn1!=pmx1?mx1:mx2)<<", span="<<span<<")"); === modified file 'pkg/common/Engine/GlobalEngine/PeriodicPythonRunner.hpp' --- pkg/common/Engine/GlobalEngine/PeriodicPythonRunner.hpp 2009-12-09 17:11:51 +0000 +++ pkg/common/Engine/GlobalEngine/PeriodicPythonRunner.hpp 2010-01-01 15:21:30 +0000 @@ -3,6 +3,7 @@ #include<yade/core/GlobalEngine.hpp> #include<yade/core/Scene.hpp> #include<yade/pkg-common/PeriodicEngines.hpp> +#include<yade/lib-pyutil/gil.hpp> /* Execute a python command (in addPlotDataCall) periodically, with defined (and adjustable) periodicity. * * Period constraints are iterInterval and timeInterval. When either of them is exceeded, the addPlotDataCall is run. @@ -15,12 +16,7 @@ public : PeriodicPythonRunner(): command("pass"){}; /* virtual bool isActivated: not overridden, StretchPeriodicEngine handles that */ - virtual void action(Scene* b){ - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - PyRun_SimpleString(command.c_str()); // this is suboptimal, since it has to be parsed at every execution; critical? - PyGILState_Release(gstate); - } + virtual void action(Scene* b){ pyRunString(command); } REGISTER_ATTRIBUTES(StretchPeriodicEngine,(command)); REGISTER_CLASS_NAME(PeriodicPythonRunner); REGISTER_BASE_CLASS_NAME(StretchPeriodicEngine); === modified file 'pkg/dem/DataClass/SpherePack.cpp' --- pkg/dem/DataClass/SpherePack.cpp 2009-12-20 22:03:17 +0000 +++ pkg/dem/DataClass/SpherePack.cpp 2010-01-01 15:21:30 +0000 @@ -78,14 +78,14 @@ if(scene->isPeriodic) { cellSize=scene->cell->getSize(); } } -long SpherePack::makeCloud(Vector3r mn, Vector3r mx, Real rMean, Real rRelFuzz, size_t num, bool periodic){ +long SpherePack::makeCloud(Vector3r mn, Vector3r mx, Real rMean, Real rRelFuzz, int num, bool periodic){ static boost::minstd_rand randGen(TimingInfo::getNow(/* get the number even if timing is disabled globally */ true)); static boost::variate_generator<boost::minstd_rand&, boost::uniform_real<> > rnd(randGen, boost::uniform_real<>(0,1)); - const size_t maxTry=1000; + const int maxTry=1000; Vector3r size=mx-mn; if(periodic)(cellSize=size); - for(size_t i=0; i<num; i++) { - size_t t; + for(int i=0; (i<num) || (num<0); i++) { + int t; Real r=(rnd()-.5)*rRelFuzz*rMean+rMean; for(t=0; t<maxTry; ++t){ Vector3r c; @@ -104,7 +104,7 @@ if(!overlap) { pack.push_back(Sph(c,r)); break; } } if (t==maxTry) { - LOG_WARN("Exceeded "<<maxTry<<" tries to insert non-overlapping sphere to packing. Only "<<i<<" spheres was added, although you requested "<<num); + if(num>0) LOG_WARN("Exceeded "<<maxTry<<" tries to insert non-overlapping sphere to packing. Only "<<i<<" spheres was added, although you requested "<<num); return i; } } === modified file 'pkg/dem/DataClass/SpherePack.hpp' --- pkg/dem/DataClass/SpherePack.hpp 2009-11-29 11:03:25 +0000 +++ pkg/dem/DataClass/SpherePack.hpp 2010-01-01 15:21:30 +0000 @@ -50,8 +50,8 @@ void toFile(const string file) const; void fromSimulation(); - // random generation - long makeCloud(Vector3r min, Vector3r max, Real rMean, Real rFuzz, size_t num, bool periodic=false); + // random generation; if num<0, insert as many spheres as possible + long makeCloud(Vector3r min, Vector3r max, Real rMean, Real rFuzz, int num, bool periodic=false); // periodic repetition void cellRepeat(Vector3<int> count); === modified file 'py/_eudoxos.cpp' --- py/_eudoxos.cpp 2009-12-18 09:48:16 +0000 +++ py/_eudoxos.cpp 2010-01-01 15:21:30 +0000 @@ -1,7 +1,10 @@ +#include<yade/lib-pyutil/numpy.hpp> + #include<yade/pkg-dem/ConcretePM.hpp> #include<boost/python.hpp> #include<yade/extra/boost_python_len.hpp> #include<yade/pkg-dem/Shop.hpp> + using namespace boost::python; using namespace std; #ifdef YADE_LOG4CXX @@ -126,15 +129,36 @@ return ret; } #endif + void particleConfinement(){ CpmStateUpdater::update(); } +python::dict testNumpy(){ + Scene* scene=Omega::instance().getScene().get(); + int dim1[]={scene->bodies->size()}; + int dim2[]={scene->bodies->size(),3}; + numpy_boost<Real,1> mass(dim1); + numpy_boost<Real,2> vel(dim2); + FOREACH(const shared_ptr<Body>& b, *scene->bodies){ + if(!b) continue; + mass[b->getId()]=b->state->mass; + VECTOR3R_TO_NUMPY(vel[b->getId()],b->state->vel); + } + python::dict ret; + ret["mass"]=mass; + ret["vel"]=vel; + return ret; +} + + BOOST_PYTHON_MODULE(_eudoxos){ + import_array(); def("velocityTowardsAxis",velocityTowardsAxis,velocityTowardsAxis_overloads(args("axisPoint","axisDirection","timeToAxis","subtractDist","perturbation"))); def("yieldSigmaTMagnitude",yieldSigmaTMagnitude); // def("spiralSphereStresses2d",spiralSphereStresses2d,(python::arg("dH_dTheta"),python::arg("axis")=2)); def("particleConfinement",particleConfinement); + def("testNumpy",testNumpy); } === modified file 'py/yadeWrapper/customConverters.cpp' --- py/yadeWrapper/customConverters.cpp 2009-12-18 15:04:57 +0000 +++ py/yadeWrapper/customConverters.cpp 2010-01-01 15:21:30 +0000 @@ -9,6 +9,8 @@ #include<indexing_suite/vector.hpp> #endif +#include<yade/lib-pyutil/numpy.hpp> + #include<boost/python.hpp> #include<boost/python/class.hpp> #include<boost/python/module.hpp> @@ -21,6 +23,8 @@ #include<string> #include<stdexcept> #include<iostream> +#include<map> + #include<yade/lib-base/yadeWm3.hpp> #include<yade/lib-base/yadeWm3Extra.hpp> @@ -126,6 +130,13 @@ +template<typename numT, int dim> +struct custom_numpyBoost_to_py{ + static PyObject* convert(numpy_boost<numT, dim> nb){ + return nb.py_ptr(); // handles incref internally + } +}; + @@ -137,6 +148,8 @@ custom_Se3r_from_seq(); to_python_converter<Se3r,custom_se3_to_tuple>(); // Vector3<int> to python (not implemented the other way around yet) custom_vector3i_to_seq(); to_python_converter<Vector3<int>,custom_vector3i_to_seq>(); + // StrArrayMap (typedef for std::map<std::string,numpy_boost>) â python dictionary + //custom_StrArrayMap_to_dict(); // register from-python converter and to-python converter custom_vector_from_seq<int>(); to_python_converter<std::vector<int>, custom_vector_to_list<int> >(); custom_vector_from_seq<Real>(); to_python_converter<std::vector<Real>, custom_vector_to_list<Real> >(); @@ -148,6 +161,13 @@ custom_vector_from_seq<shared_ptr<Material> >(); to_python_converter<std::vector<shared_ptr<Material> >, custom_vector_to_list<shared_ptr<Material> > >(); custom_vector_from_seq<shared_ptr<Serializable> >(); to_python_converter<std::vector<shared_ptr<Serializable> >, custom_vector_to_list<shared_ptr<Serializable> > >(); + import_array(); + to_python_converter<numpy_boost<Real,1>, custom_numpyBoost_to_py<Real,1> >(); + to_python_converter<numpy_boost<Real,2>, custom_numpyBoost_to_py<Real,2> >(); + to_python_converter<numpy_boost<int,1>, custom_numpyBoost_to_py<int,1> >(); + to_python_converter<numpy_boost<int,2>, custom_numpyBoost_to_py<int,2> >(); + + #define VECTOR_ENGINE_UNIT(ENGINE_UNIT) custom_vector_from_seq<shared_ptr<ENGINE_UNIT> >(); to_python_converter<std::vector<shared_ptr<ENGINE_UNIT> >,custom_vector_to_list<shared_ptr<ENGINE_UNIT> > >(); VECTOR_ENGINE_UNIT(BoundFunctor) #ifdef YADE_GEOMETRICALMODEL === modified file 'scripts/test/periodic-triax-velgrad.py' --- scripts/test/periodic-triax-velgrad.py 2009-12-30 15:45:32 +0000 +++ scripts/test/periodic-triax-velgrad.py 2010-01-01 15:21:30 +0000 @@ -7,12 +7,11 @@ #log.setLevel('Shop',log.TRACE) O.periodic=True O.cell.refSize=Vector3(.1,.1,.1) -#O.cell.Hsize=Matrix3(0.1,0,0, 0,0.1,0, 0,0,0.1) O.cell.trsf=Matrix3().IDENTITY; sp=pack.SpherePack() radius=5e-3 -num=sp.makeCloud(Vector3().ZERO,O.cell.refSize,radius,.2,500,periodic=True) # min,max,radius,rRelFuzz,spheresInCell,periodic +num=sp.makeCloud(Vector3().ZERO,O.cell.refSize,radius,.6,-1,periodic=True) # min,max,radius,rRelFuzz,spheresInCell,periodic O.bodies.append([utils.sphere(s[0],s[1]) for s in sp]) @@ -25,14 +24,15 @@ [SimpleElasticRelationships()], [Law2_Dem3Dof_Elastic_Elastic()] ), - #PeriTriaxController(goal=[-1e5,-1e5,0],stressMask=3,globUpdate=5,maxStrainRate=[1.,1.,1.],doneHook='triaxDone()',label='triax'), - NewtonIntegrator(damping=.6, homotheticCellResize=1), - PeriodicPythonRunner(command='utils.flipCell()',iterPeriod=1000), # broken for larger strains? + #PeriTriaxController(goal=[-1e3,-1e3,-1e3],stressMask=7,globUpdate=5,maxStrainRate=[5.,5.,5.],label='triax'), + NewtonIntegrator(damping=.6, homotheticCellResize=0), + PeriodicPythonRunner(command='utils.flipCell()',iterPeriod=1000), ] O.dt=0.5*utils.PWaveTimeStep() O.run(1) qt.View() -O.cell.velGrad=Matrix3(0,5,0,0,0,0,0,0,0) +O.cell.velGrad=Matrix3(0,10,0,0,0,0,0,0,0) +#O.cell.velGrad=Matrix3(0,5,2,-5,0,0,-2,0,0) O.saveTmp() O.run(); rrr=qt.Renderer(); rrr['intrAllWire'],rrr['Body_interacting_geom']=True,False
_______________________________________________ Mailing list: https://launchpad.net/~yade-dev Post to : [email protected] Unsubscribe : https://launchpad.net/~yade-dev More help : https://help.launchpad.net/ListHelp

