Merge lp:~bruno-chareyre/yade/collide into lp:~yade-dev/yade/trunk

Proposed by Bruno Chareyre
Status: Merged
Merged at revision: 2922
Proposed branch: lp:~bruno-chareyre/yade/collide
Merge into: lp:~yade-dev/yade/trunk
Diff against target: 552 lines (+194/-71)
9 files modified
core/Bound.hpp (+7/-0)
core/InteractionContainer.hpp (+3/-0)
pkg/common/Dispatching.cpp (+32/-2)
pkg/common/Dispatching.hpp (+6/-0)
pkg/common/InsertionSortCollider.cpp (+88/-43)
pkg/common/InsertionSortCollider.hpp (+22/-3)
pkg/common/InteractionLoop.cpp (+7/-3)
pkg/dem/NewtonIntegrator.cpp (+22/-20)
pkg/dem/NewtonIntegrator.hpp (+7/-0)
To merge this branch: bzr merge lp:~bruno-chareyre/yade/collide
Reviewer Review Type Date Requested Status
Yade developers Pending
Review via email: mp+59029@code.launchpad.net

Description of the change

Improved colliding (only implemented for non-periodic BCs at the moment).
- Some useless operations are removed (see diff).
- The behaviour with nBins=0 tracks displacements (while nBins>0 tracks max velocity) to update bounds
- Experimental features: targetInterval (adjust bound sizes so that they will be updated each N iterations), updatingDispFactor (only update bounds where the body's motion is at least boundSize/factor, hence saving sorting time).

Overall CPU time reduced by a factor 0.6 (average).

To post a comment you must log in.
lp:~bruno-chareyre/yade/collide updated
2823. By Bruno Chareyre

- default params + a smarter inversion handling for not-overlapping bounds

2824. By Bruno Chareyre

-no change

2825. By Bruno Chareyre

-merge

2826. By Bruno Chareyre

- fix commit mistake + documentation

2827. By Bruno Chareyre

- missing bracket

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'core/Bound.hpp'
--- core/Bound.hpp 2010-11-30 13:51:41 +0000
+++ core/Bound.hpp 2011-04-26 10:44:50 +0000
@@ -22,6 +22,13 @@
22class Bound: public Serializable, public Indexable{22class Bound: public Serializable, public Indexable{
23 public:23 public:
24 YADE_CLASS_BASE_DOC_ATTRS_DEPREC_INIT_CTOR_PY(Bound,Serializable,"Object bounding part of space taken by associated body; might be larger, used to optimalize collision detection",24 YADE_CLASS_BASE_DOC_ATTRS_DEPREC_INIT_CTOR_PY(Bound,Serializable,"Object bounding part of space taken by associated body; might be larger, used to optimalize collision detection",
25 #define ORI_VERLET
26 #ifdef ORI_VERLET
27 ((int,lastUpdateIter,0,Attr::readonly,"record iteration of last reference position update |yupdate|"))
28 ((Vector3r,refPos,Vector3r(NaN,NaN,NaN),Attr::readonly,"Reference position, updated at current body position each time the bound dispatcher update bounds - only used if oriVerlet striding is active |yupdate|"))
29 ((bool,isBounding,false,Attr::readonly,"A flag used to tell when the body moves out of bounds - only used if oriVerlet striding is active :yref:`BoundDispatcher::updatingDispFactor`>0 |yupdate|"))
30 ((Real,sweepLength,0, Attr::readonly,"The length used to increase the bounding boxe size, can be adjusted on the basis of previous displacement if :yref:`BoundDispatcher::targetInterv`>0. Only used if oriVerlet striding is active (:yref:`InsertionSortCollider::oriVerlet`=true) |yupdate|"))
31 #endif
25 ((Vector3r,color,Vector3r(1,1,1),,"Color for rendering this object"))32 ((Vector3r,color,Vector3r(1,1,1),,"Color for rendering this object"))
26 ((Vector3r,min,Vector3r(NaN,NaN,NaN),(Attr::noSave | Attr::readonly),"Lower corner of box containing this bound (and the :yref:`Body` as well)"))33 ((Vector3r,min,Vector3r(NaN,NaN,NaN),(Attr::noSave | Attr::readonly),"Lower corner of box containing this bound (and the :yref:`Body` as well)"))
27 ((Vector3r,max,Vector3r(NaN,NaN,NaN),(Attr::noSave | Attr::readonly),"Lower corner of box containing this bound (and the :yref:`Body` as well)"))34 ((Vector3r,max,Vector3r(NaN,NaN,NaN),(Attr::noSave | Attr::readonly),"Lower corner of box containing this bound (and the :yref:`Body` as well)"))
2835
=== modified file 'core/InteractionContainer.hpp'
--- core/InteractionContainer.hpp 2011-02-27 13:54:43 +0000
+++ core/InteractionContainer.hpp 2011-04-26 10:44:50 +0000
@@ -82,6 +82,9 @@
82 bool insert(const shared_ptr<Interaction>& i);82 bool insert(const shared_ptr<Interaction>& i);
83 bool erase(Body::id_t id1,Body::id_t id2);83 bool erase(Body::id_t id1,Body::id_t id2);
84 const shared_ptr<Interaction>& find(Body::id_t id1,Body::id_t id2);84 const shared_ptr<Interaction>& find(Body::id_t id1,Body::id_t id2);
85// bool found(Body::id_t id1,Body::id_t id2);
86 inline bool found(const Body::id_t& id1,const Body::id_t& id2){
87 assert(bodies); return (id1>id2)?(*bodies)[id2]->intrs.count(id1):(*bodies)[id1]->intrs.count(id2);}
85 // index access88 // index access
86 shared_ptr<Interaction>& operator[](size_t id){return linIntrs[id];}89 shared_ptr<Interaction>& operator[](size_t id){return linIntrs[id];}
87 const shared_ptr<Interaction>& operator[](size_t id) const { return linIntrs[id];}90 const shared_ptr<Interaction>& operator[](size_t id) const { return linIntrs[id];}
8891
=== modified file 'pkg/common/Dispatching.cpp'
--- pkg/common/Dispatching.cpp 2011-02-20 10:28:40 +0000
+++ pkg/common/Dispatching.cpp 2011-04-26 10:44:50 +0000
@@ -21,12 +21,33 @@
21 const long numBodies=(long)bodies->size();21 const long numBodies=(long)bodies->size();
22 bool haveBins=(bool)velocityBins;22 bool haveBins=(bool)velocityBins;
23 if(sweepDist!=0 && haveBins){ LOG_FATAL("Only one of sweepDist or velocityBins can used!"); abort(); }23 if(sweepDist!=0 && haveBins){ LOG_FATAL("Only one of sweepDist or velocityBins can used!"); abort(); }
24 //#pragma omp parallel for24// #pragma omp parallel for
25 for(int id=0; id<numBodies; id++){25 for(int id=0; id<numBodies; id++){
26 if(!bodies->exists(id)) continue; // don't delete this check - Janek26 if(!bodies->exists(id)) continue; // don't delete this check - Janek
27 const shared_ptr<Body>& b=(*bodies)[id];27 const shared_ptr<Body>& b=(*bodies)[id];
28 shared_ptr<Shape>& shape=b->shape;28 shared_ptr<Shape>& shape=b->shape;
29 if(!shape || !b->isBounded()) continue;29 if(!shape || !b->isBounded()) continue;
30 #ifdef ORI_VERLET
31 if(oriVerlet && b->bound) {
32 //Do we need to update this bound (determined in Newton)?
33 if(!b->bound->isBounding){
34 Real& sweepLength = b->bound->sweepLength;
35 if (targetInterv>=0) {
36 Vector3r disp = b->state->pos-b->bound->refPos;
37 Real dist = max(abs(disp[0]),max(abs(disp[1]),abs(disp[2])));
38 assert(b->bound->lastUpdateIter<scene->iter);
39// Real sweepMult = dist*targetInterv/((scene->iter-b->bound->lastUpdateIter)*sweepLength);
40 Real newLength = dist*targetInterv/(scene->iter-b->bound->lastUpdateIter);
41 newLength = max(0.9*sweepLength,newLength);//don't decrease size too fast to prevent time consuming oscillations
42 sweepLength=max(minSweepDistFactor*sweepDist,min(newLength,sweepDist));
43 } else {
44 sweepLength=sweepDist;
45 }
46 }
47 else continue;
48 }
49 #endif
50
30 #ifdef BV_FUNCTOR_CACHE51 #ifdef BV_FUNCTOR_CACHE
31 if(!shape->boundFunctor){ shape->boundFunctor=this->getFunctor1D(shape); if(!shape->boundFunctor) continue; }52 if(!shape->boundFunctor){ shape->boundFunctor=this->getFunctor1D(shape); if(!shape->boundFunctor) continue; }
32 // LOG_DEBUG("shape->boundFunctor.get()=="<<shape->boundFunctor.get()<<" for "<<b->shape->getClassName()<<", #"<<id);53 // LOG_DEBUG("shape->boundFunctor.get()=="<<shape->boundFunctor.get()<<" for "<<b->shape->getClassName()<<", #"<<id);
@@ -37,9 +58,18 @@
37 #endif58 #endif
38 if(!b->bound) continue; // the functor did not create new bound59 if(!b->bound) continue; // the functor did not create new bound
39 if(sweepDist>0){60 if(sweepDist>0){
61 #ifdef ORI_VERLET
62 b->bound->refPos=b->state->pos;
63 b->bound->lastUpdateIter=scene->iter;
64 const Real& sweepLength = b->bound->sweepLength;
65 Aabb* aabb=YADE_CAST<Aabb*>(b->bound.get());
66 aabb->min-=Vector3r(sweepLength,sweepLength,sweepLength);
67 aabb->max+=Vector3r(sweepLength,sweepLength,sweepLength);
68 #else
40 Aabb* aabb=YADE_CAST<Aabb*>(b->bound.get());69 Aabb* aabb=YADE_CAST<Aabb*>(b->bound.get());
41 aabb->min-=Vector3r(sweepDist,sweepDist,sweepDist);70 aabb->min-=Vector3r(sweepDist,sweepDist,sweepDist);
42 aabb->max+=Vector3r(sweepDist,sweepDist,sweepDist);71 aabb->max+=Vector3r(sweepDist,sweepDist,sweepDist);
72 #endif
43 }73 }
44 if(haveBins){74 if(haveBins){
45 Aabb* aabb=YADE_CAST<Aabb*>(b->bound.get());75 Aabb* aabb=YADE_CAST<Aabb*>(b->bound.get());
4676
=== modified file 'pkg/common/Dispatching.hpp'
--- pkg/common/Dispatching.hpp 2011-01-29 22:47:18 +0000
+++ pkg/common/Dispatching.hpp 2011-04-26 10:44:50 +0000
@@ -89,6 +89,12 @@
89 /*additional attrs*/89 /*additional attrs*/
90 ((bool,activated,true,,"Whether the engine is activated (only should be changed by the collider)"))90 ((bool,activated,true,,"Whether the engine is activated (only should be changed by the collider)"))
91 ((Real,sweepDist,0,,"Distance by which enlarge all bounding boxes, to prevent collider from being run at every step (only should be changed by the collider)."))91 ((Real,sweepDist,0,,"Distance by which enlarge all bounding boxes, to prevent collider from being run at every step (only should be changed by the collider)."))
92 #ifdef ORI_VERLET
93 ((Real,minSweepDistFactor,0.05,,"Minimal distance by which enlarge all bounding boxes; superseeds computed value of sweepDist when lower that minSweepDist."))
94 ((Real,updatingDispFactor,-1,,"see :yref:`InsertionSortCollider::updatingDispFactor` |yupdate|"))
95 ((Real,targetInterv,-1,,"see :yref:`InsertionSortCollider::targetInterv` |yupdate|"))
96 ((bool,oriVerlet,false,,"Compare Verlet distance with displacement instead of velocity (only should be changed by the collider)"))
97 #endif
92 ,/*ctor*/,/*py*/98 ,/*ctor*/,/*py*/
93 );99 );
94};100};
95101
=== modified file 'pkg/common/InsertionSortCollider.cpp'
--- pkg/common/InsertionSortCollider.cpp 2011-02-27 13:54:43 +0000
+++ pkg/common/InsertionSortCollider.cpp 2011-04-26 10:44:50 +0000
@@ -18,44 +18,39 @@
18YADE_PLUGIN((InsertionSortCollider))18YADE_PLUGIN((InsertionSortCollider))
19CREATE_LOGGER(InsertionSortCollider);19CREATE_LOGGER(InsertionSortCollider);
2020
21// return true if bodies bb overlap in all 3 dimensions
22bool InsertionSortCollider::spatialOverlap(Body::id_t id1, Body::id_t id2) const {
23 assert(!periodic);
24 return
25 (minima[3*id1+0]<=maxima[3*id2+0]) && (maxima[3*id1+0]>=minima[3*id2+0]) &&
26 (minima[3*id1+1]<=maxima[3*id2+1]) && (maxima[3*id1+1]>=minima[3*id2+1]) &&
27 (minima[3*id1+2]<=maxima[3*id2+2]) && (maxima[3*id1+2]>=minima[3*id2+2]);
28}
2921
30// called by the insertion sort if 2 bodies swapped their bounds22// called by the insertion sort if 2 bodies swapped their bounds
31void InsertionSortCollider::handleBoundInversion(Body::id_t id1, Body::id_t id2, InteractionContainer* interactions, Scene*){23void InsertionSortCollider::handleBoundInversion(Body::id_t id1, Body::id_t id2, InteractionContainer* interactions, Scene*){
32 assert(!periodic);24 assert(!periodic);
33 assert(id1!=id2);25 assert(id1!=id2);
34 // do bboxes overlap in all 3 dimensions?26 //existing interaction?
35 bool overlap=spatialOverlap(id1,id2);27 if (interactions->found(id1,id2)) {
36 // existing interaction?28 //if existing interaction is virtual and bounds don't overlap, remove it
29 if (!interactions->find(id1,id2)->isReal() && !spatialOverlap(id1,id2)){ interactions->erase(id1,id2); return;}}
30 //if it doesn't exist and bounds overlap, create a virtual interaction
31 else if (spatialOverlap(id1,id2) && Collider::mayCollide(Body::byId(id1,scene).get(),Body::byId(id2,scene).get()))
32 interactions->insert(shared_ptr<Interaction>(new Interaction(id1,id2)));
33}
34
35// called by the insertion sort if 2 bodies swapped their bounds
36void InsertionSortCollider::handleBoundSplit(Body::id_t id1, Body::id_t id2, InteractionContainer* interactions, Scene*){
37 assert(!periodic);
38 assert(id1!=id2);
37 const shared_ptr<Interaction>& I=interactions->find(id1,id2);39 const shared_ptr<Interaction>& I=interactions->find(id1,id2);
38 bool hasInter=(bool)I;40 if ((bool)I && !I->isReal()) interactions->erase(id1,id2);
39 // interaction doesn't exist and shouldn't, or it exists and should
40 if(likely(!overlap && !hasInter)) return;
41 if(overlap && hasInter){ return; }
42 // create interaction if not yet existing
43 if(overlap && !hasInter){ // second condition only for readability
44 if(!Collider::mayCollide(Body::byId(id1,scene).get(),Body::byId(id2,scene).get())) return;
45 // LOG_TRACE("Creating new interaction #"<<id1<<"+#"<<id2);
46 shared_ptr<Interaction> newI=shared_ptr<Interaction>(new Interaction(id1,id2));
47 interactions->insert(newI);
48 return;
49 }
50 if(!overlap && hasInter){ if(!I->isReal()) interactions->erase(id1,id2); return; }
51 assert(false); // unreachable
52}41}
5342
54void InsertionSortCollider::insertionSort(VecBounds& v, InteractionContainer* interactions, Scene*, bool doCollide){43void InsertionSortCollider::insertionSort(VecBounds& v, InteractionContainer* interactions, Scene*, bool doCollide){
55 assert(!periodic);44 assert(!periodic);
56 assert(v.size==(long)v.vec.size());45 assert(v.size==(long)v.vec.size());
57 for(long i=0; i<v.size; i++){46 for(long i=1; i<v.size; i++){
47 #ifdef ORI_VERLET
48 assert (v[i].flags.hasBB && v[i-1].flags.hasBB);
49 if (updatingDispFactor>0 && v[i].isBounding && v[i-1].isBounding) continue;
50 #endif
58 const Bounds viInit=v[i]; long j=i-1; /* cache hasBB; otherwise 1% overall performance hit */ const bool viInitBB=viInit.flags.hasBB;51 const Bounds viInit=v[i]; long j=i-1; /* cache hasBB; otherwise 1% overall performance hit */ const bool viInitBB=viInit.flags.hasBB;
52 const bool isMin=viInit.flags.isMin;
53
59 while(j>=0 && v[j]>viInit){54 while(j>=0 && v[j]>viInit){
60 v[j+1]=v[j];55 v[j+1]=v[j];
61 #ifdef PISC_DEBUG56 #ifdef PISC_DEBUG
@@ -65,7 +60,11 @@
65 // no collisions without bounding boxes60 // no collisions without bounding boxes
66 // also, do not collide body with itself; it sometimes happens for facets aligned perpendicular to an axis, for reasons that are not very clear61 // also, do not collide body with itself; it sometimes happens for facets aligned perpendicular to an axis, for reasons that are not very clear
67 // see https://bugs.launchpad.net/yade/+bug/66909562 // see https://bugs.launchpad.net/yade/+bug/669095
68 if(likely(doCollide && viInitBB && v[j].flags.hasBB && (viInit.id!=v[j].id))) handleBoundInversion(viInit.id,v[j].id,interactions,scene);63 // skip bounds with same isMin flags, since inversion doesn't imply anything in that case
64 if(isMin!=v[j].flags.isMin && likely(doCollide && viInitBB && v[j].flags.hasBB && (viInit.id!=v[j].id))) {
65 if (isMin) handleBoundInversion(viInit.id,v[j].id,interactions,scene);
66 else handleBoundSplit(viInit.id,v[j].id,interactions,scene);
67 }
69 j--;68 j--;
70 }69 }
71 v[j+1]=viInit;70 v[j+1]=viInit;
@@ -94,6 +93,7 @@
94}93}
9594
96// STRIDE95// STRIDE
96
97 bool InsertionSortCollider::isActivated(){97 bool InsertionSortCollider::isActivated(){
98 // activated if number of bodies changes (hence need to refresh collision information)98 // activated if number of bodies changes (hence need to refresh collision information)
99 // or the time of scheduled run already came, or we were never scheduled yet99 // or the time of scheduled run already came, or we were never scheduled yet
@@ -102,8 +102,18 @@
102 if(nBins>=1 && newton->velocityBins->checkSize_incrementDists_shouldCollide(scene)) return true;102 if(nBins>=1 && newton->velocityBins->checkSize_incrementDists_shouldCollide(scene)) return true;
103 if(nBins<=0){103 if(nBins<=0){
104 if(fastestBodyMaxDist<0){fastestBodyMaxDist=0; return true;}104 if(fastestBodyMaxDist<0){fastestBodyMaxDist=0; return true;}
105 #ifdef ORI_VERLET
106 if (!oriVerlet){
107 fastestBodyMaxDist+=sqrt(newton->maxVelocitySq)*scene->dt;
108 if(fastestBodyMaxDist>=verletDist) return true;
109 } else {
110 fastestBodyMaxDist=newton->maxVelocitySq;
111 if(fastestBodyMaxDist>=1 || fastestBodyMaxDist==0) return true;}
112 #else
105 fastestBodyMaxDist+=sqrt(newton->maxVelocitySq)*scene->dt;113 fastestBodyMaxDist+=sqrt(newton->maxVelocitySq)*scene->dt;
106 if(fastestBodyMaxDist>=verletDist) return true;114 if(fastestBodyMaxDist>=verletDist) return true;
115 #endif
116
107 }117 }
108 if((size_t)BB[0].size!=2*scene->bodies->size()) return true;118 if((size_t)BB[0].size!=2*scene->bodies->size()) return true;
109 if(scene->interactions->dirty) return true;119 if(scene->interactions->dirty) return true;
@@ -159,16 +169,6 @@
159 // compatibility block, can be removed later169 // compatibility block, can be removed later
160 findBoundDispatcherInEnginesIfNoFunctorsAndWarn();170 findBoundDispatcherInEnginesIfNoFunctorsAndWarn();
161171
162 // update bounds via boundDispatcher
163 boundDispatcher->scene=scene;
164 boundDispatcher->action();
165
166 // if interactions are dirty, force reinitialization
167 if(scene->interactions->dirty){
168 doInitSort=true;
169 scene->interactions->dirty=false;
170 }
171
172 if(verletDist<0){172 if(verletDist<0){
173 Real minR=std::numeric_limits<Real>::infinity();173 Real minR=std::numeric_limits<Real>::infinity();
174 FOREACH(const shared_ptr<Body>& b, *scene->bodies){174 FOREACH(const shared_ptr<Body>& b, *scene->bodies){
@@ -181,15 +181,33 @@
181 verletDist=isinf(minR) ? 0 : abs(verletDist)*minR;181 verletDist=isinf(minR) ? 0 : abs(verletDist)*minR;
182 }182 }
183 183
184 // update bounds via boundDispatcher
185 boundDispatcher->scene=scene;
186 #ifdef ORI_VERLET
187 if (oriVerlet) {
188 nBins=0;
189 if(boundDispatcher->velocityBins) boundDispatcher->velocityBins=shared_ptr<VelocityBins>();//avoid FATAL in boundDispatcher->action() when switching from nBin>0 to oriVerlet=true
190 boundDispatcher->oriVerlet=true;
191 boundDispatcher->sweepDist=verletDist;
192 boundDispatcher->targetInterv=targetInterv;
193 boundDispatcher->updatingDispFactor=updatingDispFactor;}
194 #endif
195 boundDispatcher->action();
184196
197 // if interactions are dirty, force reinitialization
198 if(scene->interactions->dirty){
199 doInitSort=true;
200 scene->interactions->dirty=false;
201 }
202
185 // STRIDE203 // STRIDE
186 if(verletDist>0){204 if(verletDist>0){
187 // get NewtonIntegrator, to ask for the maximum velocity value205 // get NewtonIntegrator, to ask for the maximum velocity value
188 if(!newton){206 if(!newton){
189 FOREACH(shared_ptr<Engine>& e, scene->engines){ newton=dynamic_pointer_cast<NewtonIntegrator>(e); if(newton) break; }207 FOREACH(shared_ptr<Engine>& e, scene->engines){ newton=dynamic_pointer_cast<NewtonIntegrator>(e); if(newton) break; }
190 if(!newton){ throw runtime_error("InsertionSortCollider.verletDist>0, but unable to locate NewtonIntegrator within O.engines."); }208 if(!newton){ throw runtime_error("InsertionSortCollider.verletDist>0, but unable to locate NewtonIntegrator within O.engines."); }
191 }
192 }209 }
210 }
193 ISC_CHECKPOINT("init");211 ISC_CHECKPOINT("init");
194212
195 // STRIDE213 // STRIDE
@@ -203,6 +221,20 @@
203 // reset bins, in case they were active but are not anymore221 // reset bins, in case they were active but are not anymore
204 if(newton->velocityBins) newton->velocityBins=shared_ptr<VelocityBins>(); if(boundDispatcher->velocityBins) boundDispatcher->velocityBins=shared_ptr<VelocityBins>();222 if(newton->velocityBins) newton->velocityBins=shared_ptr<VelocityBins>(); if(boundDispatcher->velocityBins) boundDispatcher->velocityBins=shared_ptr<VelocityBins>();
205 assert(strideActive); assert(newton->maxVelocitySq>=0); assert(sweepFactor>1.);223 assert(strideActive); assert(newton->maxVelocitySq>=0); assert(sweepFactor>1.);
224 #ifdef ORI_VERLET
225 newton->oriVerlet=oriVerlet;
226 newton->updatingDispFactor=updatingDispFactor;
227
228 if (!oriVerlet){
229 Real sweepVelocity=sqrt(newton->maxVelocitySq)*sweepFactor; int stride=-1;
230 if(sweepVelocity>0) {
231 stride=max(1,int((verletDist/sweepVelocity)/scene->dt));
232 boundDispatcher->sweepDist=scene->dt*(stride-1)*sweepVelocity;
233 } else { // no motion
234 boundDispatcher->sweepDist=0; // nothing moves, no need to make bboxes larger
235 }
236 }
237 #else
206 Real sweepVelocity=sqrt(newton->maxVelocitySq)*sweepFactor; int stride=-1;238 Real sweepVelocity=sqrt(newton->maxVelocitySq)*sweepFactor; int stride=-1;
207 if(sweepVelocity>0) {239 if(sweepVelocity>0) {
208 stride=max(1,int((verletDist/sweepVelocity)/scene->dt));240 stride=max(1,int((verletDist/sweepVelocity)/scene->dt));
@@ -212,6 +244,9 @@
212 }244 }
213 LOG_DEBUG(scene->time<<"s: stride ≈"<<stride<<"; maxVelocity="<<sqrt(newton->maxVelocitySq)<<", sweepDist="<<boundDispatcher->sweepDist);245 LOG_DEBUG(scene->time<<"s: stride ≈"<<stride<<"; maxVelocity="<<sqrt(newton->maxVelocitySq)<<", sweepDist="<<boundDispatcher->sweepDist);
214 fastestBodyMaxDist=0; // reset246 fastestBodyMaxDist=0; // reset
247 #endif
248
249
215 } else { // nBins>=1250 } else { // nBins>=1
216 if(!newton->velocityBins){ newton->velocityBins=shared_ptr<VelocityBins>(new VelocityBins(nBins,newton->maxVelocitySq,binCoeff,binOverlap)); }251 if(!newton->velocityBins){ newton->velocityBins=shared_ptr<VelocityBins>(new VelocityBins(nBins,newton->maxVelocitySq,binCoeff,binOverlap)); }
217 if(!boundDispatcher->velocityBins) boundDispatcher->velocityBins=newton->velocityBins;252 if(!boundDispatcher->velocityBins) boundDispatcher->velocityBins=newton->velocityBins;
@@ -235,9 +270,16 @@
235 const shared_ptr<Body>& b=Body::byId(id,scene);270 const shared_ptr<Body>& b=Body::byId(id,scene);
236 if(likely(b)){271 if(likely(b)){
237 const shared_ptr<Bound>& bv=b->bound;272 const shared_ptr<Bound>& bv=b->bound;
273 #ifdef ORI_VERLET
274 if (oriVerlet){
275 if (bv && bv->isBounding) {BBj[i].isBounding=true; continue;}
276 else BBj[i].isBounding=false;}
277 #endif
238 // coordinate is min/max if has bounding volume, otherwise both are the position. Add periodic shift so that we are inside the cell278 // coordinate is min/max if has bounding volume, otherwise both are the position. Add periodic shift so that we are inside the cell
239 // watch out for the parentheses around ?: within ?: (there was unwanted conversion of the Reals to bools!)279 // watch out for the parentheses around ?: within ?: (there was unwanted conversion of the Reals to bools!)
280
240 BBj[i].coord=((BBj[i].flags.hasBB=((bool)bv)) ? (BBj[i].flags.isMin ? bv->min[j] : bv->max[j]) : (b->state->pos[j])) - (periodic ? BBj.cellDim*BBj[i].period : 0.);281 BBj[i].coord=((BBj[i].flags.hasBB=((bool)bv)) ? (BBj[i].flags.isMin ? bv->min[j] : bv->max[j]) : (b->state->pos[j])) - (periodic ? BBj.cellDim*BBj[i].period : 0.);
282
241 } else { BBj[i].flags.hasBB=false; /* for vanished body, keep the coordinate as-is, to minimize inversions. */ }283 } else { BBj[i].flags.hasBB=false; /* for vanished body, keep the coordinate as-is, to minimize inversions. */ }
242 // if initializing periodic, shift coords & record the period into BBj[i].period284 // if initializing periodic, shift coords & record the period into BBj[i].period
243 if(doInitSort && periodic) {285 if(doInitSort && periodic) {
@@ -251,6 +293,9 @@
251 const shared_ptr<Body>& b=Body::byId(id,scene);293 const shared_ptr<Body>& b=Body::byId(id,scene);
252 if(likely(b)){294 if(likely(b)){
253 const shared_ptr<Bound>& bv=b->bound;295 const shared_ptr<Bound>& bv=b->bound;
296 #ifdef ORI_VERLET
297 if (oriVerlet && bv && bv->isBounding) continue;
298 #endif
254 if(likely(bv)) { memcpy(&minima[3*id],&bv->min,3*sizeof(Real)); memcpy(&maxima[3*id],&bv->max,3*sizeof(Real)); } // ⇐ faster than 6 assignments 299 if(likely(bv)) { memcpy(&minima[3*id],&bv->min,3*sizeof(Real)); memcpy(&maxima[3*id],&bv->max,3*sizeof(Real)); } // ⇐ faster than 6 assignments
255 else{ const Vector3r& pos=b->state->pos; memcpy(&minima[3*id],&pos,3*sizeof(Real)); memcpy(&maxima[3*id],&pos,3*sizeof(Real)); }300 else{ const Vector3r& pos=b->state->pos; memcpy(&minima[3*id],&pos,3*sizeof(Real)); memcpy(&maxima[3*id],&pos,3*sizeof(Real)); }
256 } else { memset(&minima[3*id],0,3*sizeof(Real)); memset(&maxima[3*id],0,3*sizeof(Real)); }301 } else { memset(&minima[3*id],0,3*sizeof(Real)); memset(&maxima[3*id],0,3*sizeof(Real)); }
257302
=== modified file 'pkg/common/InsertionSortCollider.hpp'
--- pkg/common/InsertionSortCollider.hpp 2011-01-09 16:34:50 +0000
+++ pkg/common/InsertionSortCollider.hpp 2011-04-26 10:44:50 +0000
@@ -68,7 +68,7 @@
6868
6969
70// #define this macro to enable timing within this engine70// #define this macro to enable timing within this engine
71//#define ISC_TIMING71// #define ISC_TIMING
7272
73// #define to turn on some tracing information for the periodic part73// #define to turn on some tracing information for the periodic part
74// all code under this can be probably removed at some point, when the collider will have been tested thoroughly74// all code under this can be probably removed at some point, when the collider will have been tested thoroughly
@@ -90,6 +90,9 @@
90 Real coord;90 Real coord;
91 //! id of the body this bound belongs to91 //! id of the body this bound belongs to
92 Body::id_t id;92 Body::id_t id;
93 #ifdef ORI_VERLET
94 bool isBounding;
95 #endif
93 //! periodic cell coordinate96 //! periodic cell coordinate
94 int period;97 int period;
95 //! is it the minimum (true) or maximum (false) bound?98 //! is it the minimum (true) or maximum (false) bound?
@@ -152,12 +155,21 @@
152 */155 */
153 void insertionSort(VecBounds& v,InteractionContainer*,Scene*,bool doCollide=true);156 void insertionSort(VecBounds& v,InteractionContainer*,Scene*,bool doCollide=true);
154 void handleBoundInversion(Body::id_t,Body::id_t,InteractionContainer*,Scene*);157 void handleBoundInversion(Body::id_t,Body::id_t,InteractionContainer*,Scene*);
155 bool spatialOverlap(Body::id_t,Body::id_t) const;158// bool spatialOverlap(Body::id_t,Body::id_t) const;
156159
157 // periodic variants160 // periodic variants
158 void insertionSortPeri(VecBounds& v,InteractionContainer*,Scene*,bool doCollide=true);161 void insertionSortPeri(VecBounds& v,InteractionContainer*,Scene*,bool doCollide=true);
159 void handleBoundInversionPeri(Body::id_t,Body::id_t,InteractionContainer*,Scene*);162 void handleBoundInversionPeri(Body::id_t,Body::id_t,InteractionContainer*,Scene*);
163 void handleBoundSplit(Body::id_t,Body::id_t,InteractionContainer*,Scene*);
164
160 bool spatialOverlapPeri(Body::id_t,Body::id_t,Scene*,Vector3i&) const;165 bool spatialOverlapPeri(Body::id_t,Body::id_t,Scene*,Vector3i&) const;
166 inline bool spatialOverlap(const Body::id_t& id1, const Body::id_t& id2) const {
167 assert(!periodic);
168 return (minima[3*id1+0]<=maxima[3*id2+0]) && (maxima[3*id1+0]>=minima[3*id2+0]) &&
169 (minima[3*id1+1]<=maxima[3*id2+1]) && (maxima[3*id1+1]>=minima[3*id2+1]) &&
170 (minima[3*id1+2]<=maxima[3*id2+2]) && (maxima[3*id1+2]>=minima[3*id2+2]);
171 }
172
161 static Real cellWrap(const Real, const Real, const Real, int&);173 static Real cellWrap(const Real, const Real, const Real, int&);
162 static Real cellWrapRel(const Real, const Real, const Real);174 static Real cellWrapRel(const Real, const Real, const Real);
163175
@@ -199,7 +211,14 @@
199 ",211 ",
200 ((int,sortAxis,0,,"Axis for the initial contact detection."))212 ((int,sortAxis,0,,"Axis for the initial contact detection."))
201 ((bool,sortThenCollide,false,,"Separate sorting and colliding phase; it is MUCH slower, but all interactions are processed at every step; this effectively makes the collider non-persistent, not remembering last state. (The default behavior relies on the fact that inversions during insertion sort are overlaps of bounding boxes that just started/ceased to exist, and only processes those; this makes the collider much more efficient.)"))213 ((bool,sortThenCollide,false,,"Separate sorting and colliding phase; it is MUCH slower, but all interactions are processed at every step; this effectively makes the collider non-persistent, not remembering last state. (The default behavior relies on the fact that inversions during insertion sort are overlaps of bounding boxes that just started/ceased to exist, and only processes those; this makes the collider much more efficient.)"))
202 ((Real,verletDist,((void)"Automatically initialized",-.05),,"Length by which to enlarge particle bounds, to avoid running collider at every step. Stride disabled if zero. Negative value will trigger automatic computation, so that the real value will be |verletDist| × minimum spherical particle radius; if there are no spherical particles, it will be disabled."))214 #ifdef ORI_VERLET
215 ((bool,oriVerlet,true,,"Compare Verlet distance with displacement instead of velocity (only used if nBins<=0)"))
216 ((int,targetInterv,30,,"(experimental) Target number of iterations between bound update, used to define a smaller sweep distance for slower grains if >0, else always use 1*verletDist. Usefull in simulations with strong velocity contrasts between slow bodies and fast bodies. Only used if oriVerlet=true."))
217 ((Real,updatingDispFactor,-1,,"(experimental) Displacement factor used to trigger bound update when oriVerlet=true: the bound is updated only if updatingDispFactor*disp>sweepDist when >0, else all bounds are updated."))
218 ((Real,verletDist,((void)"Automatically initialized",-.9),,"Length by which to enlarge particle bounds, to avoid running collider at every step. Stride disabled if zero. Negative value will trigger automatic computation, so that the real value will be |verletDist| × minimum spherical particle radius; if there are no spherical particles, it will be disabled."))
219 #else
220 ((Real,verletDist,((void)"Automatically initialized",-.15),,"Length by which to enlarge particle bounds, to avoid running collider at every step. Stride disabled if zero. Negative value will trigger automatic computation, so that the real value will be |verletDist| × minimum spherical particle radius; if there are no spherical particles, it will be disabled."))
221 #endif
203 ((Real,sweepFactor,1.05,,"Overestimation factor for the sweep velocity; must be >=1.0. Has no influence on verletDist, only on the computed stride. [DEPRECATED, is used only when bins are not used]."))222 ((Real,sweepFactor,1.05,,"Overestimation factor for the sweep velocity; must be >=1.0. Has no influence on verletDist, only on the computed stride. [DEPRECATED, is used only when bins are not used]."))
204 ((Real,fastestBodyMaxDist,-1,,"Maximum displacement of the fastest body since last run; if >= verletDist, we could get out of bboxes and will trigger full run. DEPRECATED, was only used without bins. |yupdate|"))223 ((Real,fastestBodyMaxDist,-1,,"Maximum displacement of the fastest body since last run; if >= verletDist, we could get out of bboxes and will trigger full run. DEPRECATED, was only used without bins. |yupdate|"))
205 ((int,nBins,5,,"Number of velocity bins for striding. If <=0, bin-less strigin is used (this is however DEPRECATED)."))224 ((int,nBins,5,,"Number of velocity bins for striding. If <=0, bin-less strigin is used (this is however DEPRECATED)."))
206225
=== modified file 'pkg/common/InteractionLoop.cpp'
--- pkg/common/InteractionLoop.cpp 2011-02-27 14:26:15 +0000
+++ pkg/common/InteractionLoop.cpp 2011-04-26 10:44:50 +0000
@@ -103,7 +103,11 @@
103103
104 bool swap=false;104 bool swap=false;
105 // IGeomDispatcher105 // IGeomDispatcher
106 #ifdef ORI_VERLET
107 if(unlikely(!I->functorCache.geom)){
108 #else
106 if(unlikely(!I->functorCache.geom || !I->functorCache.phys)){109 if(unlikely(!I->functorCache.geom || !I->functorCache.phys)){
110 #endif
107 I->functorCache.geom=geomDispatcher->getFunctor2D(b1_->shape,b2_->shape,swap);111 I->functorCache.geom=geomDispatcher->getFunctor2D(b1_->shape,b2_->shape,swap);
108 // returns NULL ptr if no functor exists; remember that and shortcut112 // returns NULL ptr if no functor exists; remember that and shortcut
109 if(!I->functorCache.geom) {I->functorCache.geomExists=false; continue; }113 if(!I->functorCache.geom) {I->functorCache.geomExists=false; continue; }
@@ -112,10 +116,10 @@
112 // arguments for the geom functor are in the reverse order (dispatcher would normally call goReverse).116 // arguments for the geom functor are in the reverse order (dispatcher would normally call goReverse).
113 // we don't remember the fact that is reverse, so we swap bodies within the interaction117 // we don't remember the fact that is reverse, so we swap bodies within the interaction
114 // and can call go in all cases118 // and can call go in all cases
115 if(unlikely(swap)){I->swapOrder(); }119 if(unlikely(swap)){I->swapOrder();}
116 // body pointers must be updated, in case we swapped120 // body pointers must be updated, in case we swapped
117 const shared_ptr<Body>& b1=Body::byId(I->getId1(),scene);121 const shared_ptr<Body>& b1=swap?b2_:b1_;
118 const shared_ptr<Body>& b2=Body::byId(I->getId2(),scene);122 const shared_ptr<Body>& b2=swap?b1_:b2_;
119123
120 assert(I->functorCache.geom);124 assert(I->functorCache.geom);
121 bool wasReal=I->isReal();125 bool wasReal=I->isReal();
122126
=== modified file 'pkg/dem/NewtonIntegrator.cpp'
--- pkg/dem/NewtonIntegrator.cpp 2011-04-22 09:01:59 +0000
+++ pkg/dem/NewtonIntegrator.cpp 2011-04-26 10:44:50 +0000
@@ -9,7 +9,6 @@
9#include<yade/pkg/dem/NewtonIntegrator.hpp>9#include<yade/pkg/dem/NewtonIntegrator.hpp>
10#include<yade/core/Scene.hpp>10#include<yade/core/Scene.hpp>
11#include<yade/pkg/dem/Clump.hpp>11#include<yade/pkg/dem/Clump.hpp>
12#include<yade/pkg/common/VelocityBins.hpp>
13#include<yade/lib/base/Math.hpp>12#include<yade/lib/base/Math.hpp>
1413
15YADE_PLUGIN((NewtonIntegrator));14YADE_PLUGIN((NewtonIntegrator));
@@ -66,7 +65,21 @@
66 maxVelocitySq=max(maxVelocitySq,state->vel.squaredNorm());65 maxVelocitySq=max(maxVelocitySq,state->vel.squaredNorm());
67 #endif66 #endif
68}67}
6968#ifdef ORI_VERLET
69void NewtonIntegrator::saveMaximaDisplacement(const shared_ptr<Body>& b){
70 Vector3r disp=b->state->pos-b->bound->refPos;
71 Real maxDisp=max(abs(disp[0]),max(abs(disp[1]),abs(disp[2])));
72 if (!maxDisp || maxDisp<=b->bound->sweepLength) {b->bound->isBounding = (updatingDispFactor>0 && (updatingDispFactor*maxDisp)<b->bound->sweepLength);
73 maxDisp=0.5;//not 0, else it will be seen as "not updated" by the collider
74 }
75 else {b->bound->isBounding = false; maxDisp=2;}
76 #ifdef YADE_OPENMP
77 Real& thrMaxVSq=threadMaxVelocitySq[omp_get_thread_num()]; thrMaxVSq=max(thrMaxVSq,maxDisp);
78 #else
79 maxVelocitySq=max(maxVelocitySq,maxDisp);
80 #endif
81}
82#endif
70void NewtonIntegrator::action()83void NewtonIntegrator::action()
71{84{
72 scene->forces.sync();85 scene->forces.sync();
@@ -108,21 +121,7 @@
108 if(unlikely(!b || b->isClumpMember())) continue;121 if(unlikely(!b || b->isClumpMember())) continue;
109122
110 State* state=b->state.get(); const Body::id_t& id=b->getId();123 State* state=b->state.get(); const Body::id_t& id=b->getId();
111 Vector3r f=Vector3r::Zero(), m=Vector3r::Zero();124 Vector3r f=scene->forces.getForce(id), m=scene->forces.getTorque(id);
112 // clumps forces
113 if(b->isClump()) {
114 b->shape->cast<Clump>().addForceTorqueFromMembers(state,scene,f,m);
115 #ifdef YADE_OPENMP
116 //it is safe here, since onky one thread will read/write
117 scene->forces.getTorqueUnsynced(id)+=m;
118 scene->forces.getForceUnsynced(id)+=f;
119 #else
120 scene->forces.addTorque(id,m);
121 scene->forces.addForce(id, f);
122 #endif
123 }
124 //in most cases, the initial force on clumps will zero and next line is not changing f and m, but make sure we don't miss something (e.g. user defined forces on clumps)
125 f=scene->forces.getForce(id); m=scene->forces.getTorque(id);
126 #ifdef YADE_DEBUG125 #ifdef YADE_DEBUG
127 if(isnan(f[0])||isnan(f[1])||isnan(f[2])) throw runtime_error(("NewtonIntegrator: NaN force acting on #"+lexical_cast<string>(id)+".").c_str());126 if(isnan(f[0])||isnan(f[1])||isnan(f[2])) throw runtime_error(("NewtonIntegrator: NaN force acting on #"+lexical_cast<string>(id)+".").c_str());
128 if(isnan(m[0])||isnan(m[1])||isnan(m[2])) throw runtime_error(("NewtonIntegrator: NaN torque acting on #"+lexical_cast<string>(id)+".").c_str());127 if(isnan(m[0])||isnan(m[1])||isnan(m[2])) throw runtime_error(("NewtonIntegrator: NaN torque acting on #"+lexical_cast<string>(id)+".").c_str());
@@ -134,8 +133,6 @@
134 // in aperiodic boundaries, it is equal to absolute velocity133 // in aperiodic boundaries, it is equal to absolute velocity
135 Vector3r fluctVel=isPeriodic?scene->cell->bodyFluctuationVel(b->state->pos,b->state->vel):state->vel;134 Vector3r fluctVel=isPeriodic?scene->cell->bodyFluctuationVel(b->state->pos,b->state->vel):state->vel;
136135
137
138
139 // numerical damping & kinetic energy136 // numerical damping & kinetic energy
140 if(unlikely(trackEnergy)) updateEnergy(b,state,fluctVel,f,m);137 if(unlikely(trackEnergy)) updateEnergy(b,state,fluctVel,f,m);
141138
@@ -144,6 +141,8 @@
144141
145 // for particles not totally blocked, compute accelerations; otherwise, the computations would be useless142 // for particles not totally blocked, compute accelerations; otherwise, the computations would be useless
146 if (state->blockedDOFs!=State::DOF_ALL) {143 if (state->blockedDOFs!=State::DOF_ALL) {
144 // forces
145 if(b->isClump()) b->shape->cast<Clump>().addForceTorqueFromMembers(state,scene,f,m);
147 // linear acceleration146 // linear acceleration
148 Vector3r linAccel=computeAccel(f,state->mass,state->blockedDOFs);147 Vector3r linAccel=computeAccel(f,state->mass,state->blockedDOFs);
149 cundallDamp2nd(dt,f,fluctVel,linAccel);148 cundallDamp2nd(dt,f,fluctVel,linAccel);
@@ -163,7 +162,10 @@
163 leapfrogTranslate(state,id,dt);162 leapfrogTranslate(state,id,dt);
164 if(!useAspherical) leapfrogSphericalRotate(state,id,dt);163 if(!useAspherical) leapfrogSphericalRotate(state,id,dt);
165 else leapfrogAsphericalRotate(state,id,dt,m);164 else leapfrogAsphericalRotate(state,id,dt,m);
166165 #ifdef ORI_VERLET
166 if (oriVerlet) saveMaximaDisplacement(b);
167 else
168 #endif
167 saveMaximaVelocity(id,state);169 saveMaximaVelocity(id,state);
168 // move individual members of the clump, save maxima velocity (for collider stride)170 // move individual members of the clump, save maxima velocity (for collider stride)
169 if(b->isClump()) Clump::moveMembers(b,scene,this);171 if(b->isClump()) Clump::moveMembers(b,scene,this);
170172
=== modified file 'pkg/dem/NewtonIntegrator.hpp'
--- pkg/dem/NewtonIntegrator.hpp 2011-04-22 09:01:59 +0000
+++ pkg/dem/NewtonIntegrator.hpp 2011-04-26 10:44:50 +0000
@@ -11,6 +11,7 @@
11#include<yade/core/Interaction.hpp>11#include<yade/core/Interaction.hpp>
12#include<yade/lib/base/Math.hpp>12#include<yade/lib/base/Math.hpp>
13#include<yade/pkg/common/Callbacks.hpp>13#include<yade/pkg/common/Callbacks.hpp>
14#include<yade/pkg/common/VelocityBins.hpp>
14#ifdef YADE_OPENMP15#ifdef YADE_OPENMP
15 #include<omp.h>16 #include<omp.h>
16#endif17#endif
@@ -38,14 +39,19 @@
3839
39 // whether the cell has changed from the previous step40 // whether the cell has changed from the previous step
40 bool cellChanged;41 bool cellChanged;
42
41 // wether a body has been selected in Qt view43 // wether a body has been selected in Qt view
42 bool bodySelected;44 bool bodySelected;
43 int homoDeform; // updated from scene at every call; -1 for aperiodic simulations, otherwise equal to scene->cell->homoDeform45 int homoDeform; // updated from scene at every call; -1 for aperiodic simulations, otherwise equal to scene->cell->homoDeform
44 Matrix3r dVelGrad; // dtto46 Matrix3r dVelGrad; // dtto
4547
46 public:48 public:
49 Real updatingDispFactor;//(experimental) Displacement factor used to trigger bound update when oriVerlet=true: the bound is updated only if updatingDispFactor*disp>sweepDist when >0, else all bounds are updated.
47 // function to save maximum velocity, for the verlet-distance optimization50 // function to save maximum velocity, for the verlet-distance optimization
48 void saveMaximaVelocity(const Body::id_t& id, State* state);51 void saveMaximaVelocity(const Body::id_t& id, State* state);
52 #ifdef ORI_VERLET
53 void saveMaximaDisplacement(const shared_ptr<Body>& b);
54 #endif
49 #ifdef YADE_OPENMP55 #ifdef YADE_OPENMP
50 vector<Real> threadMaxVelocitySq;56 vector<Real> threadMaxVelocitySq;
51 #endif57 #endif
@@ -56,6 +62,7 @@
56 ((Real,damping,0.2,,"damping coefficient for Cundall's non viscous damping (see [Chareyre2005]_) [-]"))62 ((Real,damping,0.2,,"damping coefficient for Cundall's non viscous damping (see [Chareyre2005]_) [-]"))
57 ((Real,maxVelocitySq,NaN,,"store square of max. velocity, for informative purposes; computed again at every step. |yupdate|"))63 ((Real,maxVelocitySq,NaN,,"store square of max. velocity, for informative purposes; computed again at every step. |yupdate|"))
58 ((bool,exactAsphericalRot,true,,"Enable more exact body rotation integrator for :yref:`aspherical bodies<Body.aspherical>` *only*, using formulation from [Allen1989]_, pg. 89."))64 ((bool,exactAsphericalRot,true,,"Enable more exact body rotation integrator for :yref:`aspherical bodies<Body.aspherical>` *only*, using formulation from [Allen1989]_, pg. 89."))
65 ((bool,oriVerlet,false,,"Compare Verlet distance with displacement instead of velocity (updated by the collider)"))
59 ((Matrix3r,prevVelGrad,Matrix3r::Zero(),,"Store previous velocity gradient (:yref:`Cell::velGrad`) to track acceleration. |yupdate|"))66 ((Matrix3r,prevVelGrad,Matrix3r::Zero(),,"Store previous velocity gradient (:yref:`Cell::velGrad`) to track acceleration. |yupdate|"))
60 #ifdef YADE_BODY_CALLBACK67 #ifdef YADE_BODY_CALLBACK
61 ((vector<shared_ptr<BodyCallback> >,callbacks,,,"List (std::vector in c++) of :yref:`BodyCallbacks<BodyCallback>` which will be called for each body as it is being processed."))68 ((vector<shared_ptr<BodyCallback> >,callbacks,,,"List (std::vector in c++) of :yref:`BodyCallbacks<BodyCallback>` which will be called for each body as it is being processed."))

Subscribers

People subscribed via source and target branches