File: LevelSetIg2.cpp

package info (click to toggle)
yade 2026.1.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 34,448 kB
  • sloc: cpp: 97,645; python: 52,173; sh: 677; makefile: 162
file content (922 lines) | stat: -rw-r--r-- 49,473 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
/*************************************************************************
*  2021 jerome.duriez@inrae.fr                                           *
*  This program is free software, see file LICENSE for details.          *
*************************************************************************/

#ifdef YADE_LS_DEM
#include <pkg/common/Sphere.hpp>
#include <pkg/levelSet/LevelSetIGeom.hpp>
#include <pkg/levelSet/LevelSetIg2.hpp>
#include <pkg/levelSet/OtherClassesForLSContact.hpp>
#include <pkg/levelSet/ShopLS.hpp>
#include <preprocessing/dem/Shop.hpp>

namespace yade {
YADE_PLUGIN((Ig2_Sphere_LevelSet_ScGeom)(Ig2_Box_LevelSet_ScGeom)(Ig2_Wall_LevelSet_ScGeom)(Ig2_Wall_LevelSet_MultiScGeom)(Ig2_LevelSet_LevelSet_ScGeom)(
        Ig2_LevelSet_LevelSet_LSnodeGeom)(Ig2_LevelSet_LevelSet_MultiScGeom));
CREATE_LOGGER(Ig2_Sphere_LevelSet_ScGeom);
CREATE_LOGGER(Ig2_Box_LevelSet_ScGeom);
CREATE_LOGGER(Ig2_LevelSet_LevelSet_LSnodeGeom);
CREATE_LOGGER(Ig2_LevelSet_LevelSet_ScGeom);
CREATE_LOGGER(Ig2_LevelSet_LevelSet_MultiScGeom);
CREATE_LOGGER(Ig2_Wall_LevelSet_ScGeom);
CREATE_LOGGER(Ig2_Wall_LevelSet_MultiScGeom);

bool Ig2_Sphere_LevelSet_ScGeom::go(
        const shared_ptr<Shape>&       shape1,
        const shared_ptr<Shape>&       shape2,
        const State&                   state1,
        const State&                   state2,
        const Vector3r&                shift2,
        const bool&                    force,
        const shared_ptr<Interaction>& c)
{
	shared_ptr<Sphere>   sphereSh = YADE_PTR_CAST<Sphere>(shape1);
	shared_ptr<LevelSet> lsSh     = YADE_PTR_CAST<LevelSet>(shape2);
	Vector3r             lsPos(state2.pos + shift2), spherePos(state1.pos);

	// Orientation of the level set (sphere orientation doesn't matter).
	// ori = rotation from reference configuration (local axes) to current one (global axes)
	// ori.conjugate() from the current configuration (global axes) to the reference one (local axes)
	Quaternionr rotLS(state2.ori), rotConjLS(state2.ori.conjugate());

	// Position of the sphere relative to the level set, then rotate to local coordinates of the LS grid
	Vector3r spherePosLocal = rotConjLS * (lsPos - spherePos);

	// Evaluate the level set and the normal, sphere centre may be outside so set true.
	Real     lev         = lsSh->distance(spherePosLocal, true);
	Vector3r normalLocal = lsSh->normal(spherePosLocal, true);

	// Compute the maximum overlap
	Real rad(sphereSh->radius); // The sphere radius
	Real maxOverlap = rad - lev;

	// Bring normal back to local coordinates, adjust to point from body 1 to body 2
	Vector3r normal = (rotLS * normalLocal).normalized();

	// Determine contact point. Middle of overlapping volumes, as usual.
	Vector3r contactPoint = spherePos + (rad - maxOverlap / 2.) * normal;

	if (maxOverlap < 0 && !c->isReal() && !force)
		return false; // We won't create the interaction in this case (but it is not our job here to delete it in case it already exists).

	shared_ptr<ScGeom> geomPtr;
	bool               isNew = !c->geom;

	if (isNew) {
		geomPtr = shared_ptr<ScGeom>(new ScGeom());
		c->geom = geomPtr;
	} else
		geomPtr = YADE_PTR_CAST<ScGeom>(c->geom);
	geomPtr->doIg2Work(
	        contactPoint,
	        maxOverlap, // Doesn't work for maxOverlap > R, like how pretty much any contact law doesn't.
	        rad,        // Inconsequential value since the contact point to centre distance is used for the torque anyway.
	        rad,        // Inconsequential value since the contact point to centre distance is used for the torque anyway.
	        state1,
	        state2,
	        scene,
	        c,
	        normal,
	        shift2,
	        isNew,
	        false);
	return true;
}

bool Ig2_Box_LevelSet_ScGeom::go(
        const shared_ptr<Shape>&       shape1,
        const shared_ptr<Shape>&       shape2,
        const State&                   state1,
        const State&                   state2,
        const Vector3r&                shift2,
        const bool&                    force,
        const shared_ptr<Interaction>& c)
{
	// 1.1 Preliminary declarations
	shared_ptr<Box>      boxSh = YADE_PTR_CAST<Box>(shape1);
	shared_ptr<LevelSet> lsSh  = YADE_PTR_CAST<LevelSet>(shape2);
	Vector3r             lsCenter(state2.pos + shift2), boxCenter(state1.pos);
	Vector3r             axisContact(Vector3r::Zero()); // I will need to .dot this vector with Vector3r => Vector3r (and not Vector3i) as well

	// 1.2 Checking the "contact direction", adopting the two following hypothesis:
	// - H1: lsCenter is outside of the box
	// - H2: projecting lsCenter along the contact direction towards the box will make lsCenter fall onto the  box, and not alongside. This is not always fulfilled, but will always be during triaxial box setups, for instance (as long as the box is closed).
	for (int axis = 0; axis < 3; axis++) {
		if (math::abs(lsCenter[axis] - boxCenter[axis]) > boxSh->extents[axis]) // TODO: account for a change in orientation of the box ?
			axisContact[axis] = 1; // TODO: once I'm sure this will happen only once, I could stop the loop here...
	}
	if (axisContact.norm() != 1) {
		LOG_ERROR(
		        "Problem while determining contact direction for "
		        << c->id1 << "-" << c->id2 << " : we got " << axisContact
		        << ". (0 0 0) means the LevelSet'd body has its center inside the box, which is not supported. Indeed, center = " << lsCenter
		        << " while boxCenter = " << boxCenter << " and extents = " << boxSh->extents);
	}
	LOG_DEBUG("axisContact = " << axisContact);
	Real boxC(axisContact.dot(boxCenter)), boxE(boxSh->extents.dot(axisContact)), lsC(lsCenter.dot(axisContact));

	// 2.1. Preliminary declarations for the surface nodes loop
	// clang-format off
	const int nNodes(lsSh->surfNodes.size());
	if (!nNodes) LOG_ERROR("We have one level-set body without boundary nodes for contact detection. Will probably crash");
	vector<Vector3r> lsNodes; // current positions of the boundary nodes (some of of those, at least) for the level set body. See for loop below
	lsNodes.reserve(nNodes); // nNodes will be a maximum size, reserve() is appropriate, not resize() (see also https://github.com/isocpp/CppCoreGuidelines/issues/493)
	vector<Real> distList; // will include all distance values from the node(s)On1 to shape2. It is a std::vector because we do not know beforehand the number of elements in this "list
	distList.reserve(nNodes); // nNodes might be a maximum size, reserve() is appropriate, not resize() (see also https://github.com/isocpp/CppCoreGuidelines/issues/493)
	vector<int> indicesNodes; // distList will include distance values corresponding to nodes e.g. 2, 5, 7 only out of 10 nodes among surfNodes. This indicesNodes vector will store these 2,5,7 indices
	indicesNodes.reserve(nNodes);
	// NB: it might actually be somewhat faster to not use these vectors and just compare new node with previous node, as done in Ig2_LevelSet_LevelSet*
	// I do not think it is critical for present Ig2_Box_LevelSet_ScGeom
	Real distToNode; // one distance value, for one node
	Real xNode;      // current position (on the axisContact of interest, can be something else than x-axis..) of the level set boundary node
	// clang-format on

	// 2.2. Actual loop over surface nodes
	for (int node = 0; node < nNodes; node++) {
		lsNodes[node] = ShopLS::rigidMapping(lsSh->surfNodes[node], Vector3r::Zero(), lsCenter, state2.ori);
		xNode         = lsNodes[node].dot(axisContact);
		if (xNode < boxC - boxE || xNode > boxC + boxE) continue;
		distToNode = (lsC > boxC ? boxC + boxE - xNode : xNode - (boxC - boxE));
		if (distToNode < 0) LOG_ERROR("Unexpected case ! We obtained " << distToNode << " while waiting for a positive quantity");
		distList.push_back(distToNode);
		indicesNodes.push_back(node);
	}

	// 2.3. Finishing the work when there is no contact
	if (!distList.size()) { // all boundary nodes are outside the bounds' overlap,
		if (!c->isReal()) return false;
		else {
			Ig2_LevelSet_LevelSet_ScGeom::geomPtrForLaterRemoval(state1, state2, c, scene);
			return true;
		}
	}
	Real maxOverlap;
	maxOverlap = *std::max_element(distList.begin(), distList.end());
	if (maxOverlap < 0 && !c->isReal() && !force) // inspired by Ig2_Sphere_Sphere_ScGeom:
		return false;

	// 2.4. Finishing the work when there is a contact
	int indexCont = std::min_element(distList.begin(), distList.end())
	        - distList.begin();                                       //this works: it seems min_element is returning here a random access iterator
	Vector3r           normal((lsC > boxC ? 1. : -1.) * axisContact); // normal from the box body to the level set body, ie from 1 to 2, as expected.
	Real               rad((lsNodes[indicesNodes[indexCont]] - lsCenter).norm());
	shared_ptr<ScGeom> geomPtr;
	bool               isNew = !c->geom;
	if (isNew) {
		geomPtr = shared_ptr<ScGeom>(new ScGeom());
		c->geom = geomPtr;
	} else
		geomPtr = YADE_PTR_CAST<ScGeom>(c->geom);
	geomPtr->doIg2Work(
	        lsNodes[indicesNodes[indexCont]] + maxOverlap / 2. * normal, // middle of overlapping volumes, as usual
	        maxOverlap,                                                  // does not work for very big/huge overlap
	        rad,
	        rad,
	        state1,
	        state2,
	        scene,
	        c,
	        normal,
	        shift2,
	        isNew,
	        false);
	return true;
}

bool Ig2_Wall_LevelSet_ScGeom::go(
        const shared_ptr<Shape>&       shape1,
        const shared_ptr<Shape>&       shape2,
        const State&                   state1,
        const State&                   state2,
        const Vector3r&                shift2,
        const bool&                    force,
        const shared_ptr<Interaction>& c)
{
#ifdef USE_TIMING_DELTAS
	timingDeltas->start();
#endif
	shared_ptr<Wall>     wallSh = YADE_PTR_CAST<Wall>(shape1);
	shared_ptr<LevelSet> lsSh   = YADE_PTR_CAST<LevelSet>(shape2);
	Real                 lsPos(state2.pos[wallSh->axis] + shift2[wallSh->axis]), wallPos(state1.pos[wallSh->axis]);

	const int nNodes(lsSh->surfNodes.size());
	if (!nNodes) LOG_ERROR("We have one level-set body without boundary nodes for contact detection. Will probably crash");
	if (wallSh->sense != 0) LOG_ERROR("Ig2_Wall_LevelSet_* only supports Wall.sense = 0");

	Real distToNode, // one wall-level set distance value (< 0 when contact), for one node
	        nodePos, // current position along the Wall->axis of one given boundary node
	        maxOverlap(-std::numeric_limits<Real>::infinity());
	Vector3r currNode,                                    // current position of one given boundary node
	        contactNode(math::NaN, math::NaN, math::NaN); // the boundary node which is the most inside the wall
#ifdef USE_TIMING_DELTAS
	timingDeltas->checkpoint("Until nodes loop");
#endif
	for (int node = 0; node < nNodes; node++) {
		currNode = ShopLS::rigidMapping(lsSh->surfNodes[node], Vector3r::Zero(), state2.pos + shift2, state2.ori);
		nodePos  = currNode[wallSh->axis];
		if (wallPos >= lsPos && wallPos <= nodePos) // first possibility for the wall to intersect the LevelSet body
			distToNode = wallPos - nodePos;
		else if (wallPos >= nodePos && wallPos <= lsPos) // second possibility for intersection
			distToNode = nodePos - wallPos;
		else
			continue; // go directly to next node
		if (distToNode > 0) LOG_ERROR("Unexpected case ! We obtained " << distToNode << " while waiting for a negative quantity");
		if (-distToNode > maxOverlap) {
			maxOverlap  = -distToNode;
			contactNode = currNode;
		}
	}
	// After that loop we may have (-)infinite maxOverlap and other garbage data in contactNode in case no node was contacting, which would be a problem in case of a neverErase
	if (!std::isfinite(maxOverlap)) {
		LOG_DEBUG("Interaction " << c->id1 << "-" << c->id2 << ": passing here since no node was found to be contacting");
		maxOverlap  = -1;                                      // any arbitrary negative value
		contactNode = state2.pos + shift2 + Vector3r(1, 1, 1); // some quite arbitrary value, that still aims to avoid a 0 "rad" below
	}
#ifdef USE_TIMING_DELTAS
	timingDeltas->checkpoint("After completing nodes loop");
#endif
	if (maxOverlap < 0 && !c->isReal() && !force)
		return false; // we won't create the interaction in this case (but it is not our job here to delete it in case it already exists)
	Vector3r wallNormal(Vector3r::Zero()), normal(Vector3r::Zero());
	wallNormal[wallSh->axis] = 1;
	normal                   = (wallPos - lsPos > 0 ? -1 : 1) * wallNormal; // Points from wall to particle center
	Real rad((contactNode - state2.pos - shift2).norm());                   // Distance from surface to center of level-set body

	shared_ptr<ScGeom> geomPtr;
	bool               isNew = !c->geom;
	if (isNew) {
		geomPtr = shared_ptr<ScGeom>(new ScGeom());
		c->geom = geomPtr;
	} else
		geomPtr = YADE_PTR_CAST<ScGeom>(c->geom);
	geomPtr->doIg2Work(
	        contactNode + maxOverlap / 2. * normal, // middle of overlapping volumes, as usual, with garbage but finite data in case of tensile separation
	        maxOverlap,                             // does not work for very big/huge overlap
	        rad, // considering the 2* feature of radius* (see comments in ScGeom::doIg2work), this is what makes most sense ?
	        rad, // we keep in particular radius1/2 slightly greater than (contactPoint-center).norm. And we just use the same radii for the two particles, as in Ig2_Box_Sphere_ScGeom
	        state1,
	        state2,
	        scene,
	        c,
	        normal,
	        shift2,
	        isNew,
	        false);
#ifdef USE_TIMING_DELTAS
	timingDeltas->checkpoint("Terminating ::go");
#endif
	return true;
}

bool Ig2_Wall_LevelSet_MultiScGeom::go(
        const shared_ptr<Shape>&       shape1,
        const shared_ptr<Shape>&       shape2,
        const State&                   state1,
        const State&                   state2,
        const Vector3r&                shift2,
        const bool&                    force,
        const shared_ptr<Interaction>& c)
{
	bool                    newIgeom(!bool(c->geom));
	shared_ptr<MultiScGeom> geomMulti(new MultiScGeom);
	shared_ptr<MultiPhys>   physMulti(new MultiPhys); // As a reminder, Ig2_*MultiScGeom have to touch the physics
	if (!newIgeom) {
		geomMulti = YADE_PTR_CAST<MultiScGeom>(c->geom);
		physMulti = YADE_PTR_CAST<MultiPhys>(c->phys);
	} // nothing to do in else
	shared_ptr<Wall>     wallSh = YADE_PTR_CAST<Wall>(shape1);
	shared_ptr<LevelSet> lsSh   = YADE_PTR_CAST<LevelSet>(shape2);
	Real                 lsPos(state2.pos[wallSh->axis] + shift2[wallSh->axis]), wallPos(state1.pos[wallSh->axis]);

	const int nNodes(lsSh->surfNodes.size());
	if (!nNodes) LOG_ERROR("We have one level-set body without boundary nodes for contact detection. Will probably crash");
	if (wallSh->sense != 0) LOG_ERROR("Ig2_Wall_LevelSet_* only supports Wall.sense = 0");

	// Before even looking at nodes below, we can compute the normal (which would be a waste if no nodes are touching, but let it be this way):
	Vector3r wallNormal(Vector3r::Zero()), normal(Vector3r::Zero());
	wallNormal[wallSh->axis] = 1;
	normal                   = (wallPos - lsPos > 0 ? -1 : 1) * wallNormal; // points from wall to particle center
	// We will now look at nodes:
	Real distToNode { math::NaN },                      // one wall-level set distance value (< 0 when contact), for one node
	        nodePos { math::NaN };                      // current position along the Wall->axis of one given boundary node
	Vector3r currNode(math::NaN, math::NaN, math::NaN); // current position of one given boundary node
	for (int node = 0; node < nNodes; node++) {
		currNode = ShopLS::rigidMapping(lsSh->surfNodes[node], Vector3r::Zero(), state2.pos + shift2, state2.ori);
		nodePos  = currNode[wallSh->axis];
		LOG_DEBUG(
		        "Looking at node " << node << " located (in current configuration) at " << currNode << " while unitialized distToNode is "
		                           << distToNode)
		if (wallPos >= lsPos
		    && wallPos
		            <= nodePos) // first possibility for the wall to intersect the LevelSet body in a LScenter - Wall - LSsurface alignment when walking along axis
			distToNode = wallPos - nodePos;
		else if (
		        wallPos >= nodePos
		        && wallPos <= lsPos) // second possibility for intersection, in a reverse (LSsurface - Wall - LScenter) alignment along axis
			distToNode = nodePos - wallPos;
		else { // that node does not currently involve contact with the wall
			// we still have to wonder whether that was the case before (to take it out of lists if yes)
			ShopLS::handleNonTouchingNodeForMulti(geomMulti, physMulti, node);
			continue; // and then move on to the next node
		}
		if (distToNode > 0) LOG_ERROR("Unexpected case ! We obtained " << distToNode << " while waiting for a negative quantity");
		if (force) LOG_WARN("force is true, let us think about that");
		// from now on, we know this node is contacting
		Real rad((currNode - state2.pos - shift2).norm());
		ShopLS::handleTouchingNodeForMulti(
		        geomMulti,
		        physMulti,
		        node,
		        currNode - distToNode / 2. * normal, // middle of overlapping volumes, as usual
		        -distToNode,
		        rad,
		        rad,
		        state1,
		        state2,
		        scene,
		        c,
		        normal,
		        shift2);
	}
	LOG_DEBUG(
	        "Nodes loop done, we finally got " << geomMulti->contacts.size() << " in geomMulti.contacts and " << physMulti->contacts.size()
	                                           << " in physMulti for that interaction which is " << (c->isReal() ? "real" : "not real"));
	if (geomMulti->contacts.size() == 0
	    && !c->isReal() // this typically happens just after Aabb overlap for the first time but there is no node-surface contact yet
	    && !force)
		return false; // we won't create the interaction in this case
	if (newIgeom) {
		c->geom = geomMulti;
		c->phys = physMulti;
	}
	LOG_DEBUG("Returning true");
	return true;
}

void Ig2_LevelSet_LevelSet_ScGeom::geomPtrForLaterRemoval(const State& rbp1, const State& rbp2, const shared_ptr<Interaction>& c, const Scene* scene)
{
	// to use when we can not really compute anything, e.g. bodies lsGrid do not overlap anymore, but still need to have some geom data (while returning true as per general InteractionLoop workflow because it is an existing interaction. Otherwise we would need to update InteractionLoop itself to avoid LOG_WARN messages). Data mostly include an infinite tensile stretch to insure subsequent interaction removal (by Law2)
	// this function is only applied onto a real interaction c, i.e. with an existing geom
	shared_ptr<ScGeom> geomPtr(YADE_PTR_CAST<ScGeom>(c->geom));
	LOG_DEBUG(
	        "YADE_PTR_DYN_CAST<ScGeom>(c->geom) = "
	        << YADE_PTR_DYN_CAST<ScGeom>(geomPtr)
	        << " (0 would be a problem maybe because of an unexpected MultiScGeom)"); // the dynamic cast will never be executed here unless LOG_DEBUG (level) is actually desired
	geomPtr->doIg2Work(
	        Vector3r::Zero() /* inconsequential bullsh..*/,
	        -std::numeric_limits<Real>::infinity() /* arbitrary big tensile value to trigger interaction removal by Law2*/,
	        1, /* inconsequential bullsh..*/
	        1, /* inconsequential bullsh..*/
	        rbp1,
	        rbp2,
	        scene,
	        c,
	        Vector3r::UnitX() /* inconsequential bullsh..*/,
	        Vector3r::Zero(), /* inconsequential bullsh..*/
	        false,            /* inconsequential bullsh..*/
	        false);
}

bool Ig2_LevelSet_LevelSet_LSnodeGeom::go(
        const shared_ptr<Shape>& shape1,
        const shared_ptr<Shape>& shape2,
        const State&             state1,
        const State&             state2,
        const Vector3r&          shift2,
        const bool&, // "force" (always false for the record), unused here
        const shared_ptr<Interaction>& c)
{
#ifdef USE_TIMING_DELTAS
	timingDeltas->start();
#endif
	// 1. We first use boundOverlap() very much just like Ig2_LevelSet_LevelSet_ScGeom::goSingleOrMulti
	auto returnOfBoundOverlap(boundOverlap(true, state1, state2, c, shift2));
	if (returnOfBoundOverlap.second.first)                                                                    // an empty bound overlap was detected
		return (returnOfBoundOverlap.second.second);                                                      // we shall stop Ig2*::go work
	Vector3r minBoOverlap(returnOfBoundOverlap.first.first), maxBoOverlap(returnOfBoundOverlap.first.second); // current configuration obviously
	// stop of ~ copied-pasted 1.

	// 2. We go now for the master-slave contact algorithm with surface ie boundary nodes:
	// 2.1 Preliminary declarations, starting some copy-paste from Ig2_LevelSet_LevelSet_ScGeom::goSingleOrMulti:

	// ****** following block initially very much copied-pasted ******** !
	shared_ptr<LevelSet> sh1 = YADE_PTR_CAST<LevelSet>(shape1);
	shared_ptr<LevelSet> sh2 = YADE_PTR_CAST<LevelSet>(shape2);
	bool                 id1isBigger(
                sh1->getVolume() > sh2->getVolume()); // identifying the smallest particle (where the master-slave contact algorithm will locate boundary nodes)

	shared_ptr<LevelSet> shS(id1isBigger ? sh2 : sh1);                          // the shape of the small body, useful to have it defined here
	shared_ptr<LevelSet> shB(id1isBigger ? sh1 : sh2);                          // the shape of the big body, useful to have it defined here
	Vector3r             ctrS(id1isBigger ? (state2.pos + shift2) : state1.pos) // center of the small body
	        ,
	        ctrB(id1isBigger ? state1.pos : (state2.pos + shift2)) // center of the big body
	        ;
	Quaternionr rotS(
	        id1isBigger ? state2.ori
	                    : state1.ori) // ori = rotation from reference configuration (local axes) to current one, useful to have it defined here
	        ,
	        rotB(id1isBigger ? state1.ori : state2.ori) // orientation of the big body
	        ;
	// ****** end of copy-paste ? ********

	// 2.2 We now handle surface nodes searching for the closest one with various workflows depending on surfNodeIdx available data
	std::tuple<Real, int, Vector3r> closestNodeData; // its (distance value; idx; position in global configuration)
	shared_ptr<LSnodeGeom>          geom(new LSnodeGeom);
	bool                            newIgeom(!bool(c->geom));
#ifdef USE_TIMING_DELTAS
	timingDeltas->checkpoint("Until the history-dependent stuff with nodes loop");
#endif
	if (newIgeom) {
		LOG_INFO("A brand new LSnodeGeom at " << c->id1 << "-" << c->id2 << " contact");
		geom->usedWorkflow = 0;
		closestNodeData    = smallestDistanceFullLoop(
                        shS,
                        shB // both shapes
                        ,
                        ctrS // center of the small body
                        ,
                        rotS // orientation of the small body
                        ,
                        ctrB // center of the big body
                        ,
                        rotB // orientation of the big body
                        ,
                        minBoOverlap,
                        maxBoOverlap);
	} else {
		geom = YADE_PTR_CAST<LSnodeGeom>(c->geom); // we have an existing geom we can pick
		LOG_INFO("An existing LSnodeGeom..");
		if (geom->surfNodeIdx
		    < 0) { // but we have not yet identified a closest-node, maybe because they re still all outside the big body bound / lsGrid, let us then keep the whole loop
			geom->usedWorkflow = 1;
			LOG_INFO(".. with no surfNodeIdx defined at " << c->id1 << "-" << c->id2 << " contact");
			// exact same as above:
			closestNodeData = smallestDistanceFullLoop(
			        shS,
			        shB // both shapes
			        ,
			        ctrS // center of the small body
			        ,
			        rotS // orientation of the small body
			        ,
			        ctrB // center of the big body
			        ,
			        rotB // orientation of the big body
			        ,
			        minBoOverlap,
			        maxBoOverlap);
		} else { // using surfNodeIdx data
			geom->usedWorkflow = 2;
			LOG_INFO(".. with surfNodeIdx defined at " << c->id1 << "-" << c->id2 << " contact");
			// we will move accross neighbors as long as we find decreasing phi values, which is defined by this boolean:
			bool move(true), lastThoroughSearch(false) // that other boolean is for triggering when necessary a final full search
			        ;
			if (shS->n_neighborsNodes < 2) // all default -1, 0 and 1 would not be very good..
				LOG_ERROR(
				        "Impossible to use an optimized surfNodeIdx-based node loop with just "
				        << shS->n_neighborsNodes << " neighboring nodes (+ the guy) defined in LevelSet.n_neighborsNodes");
			while (move) {
				LOG_DEBUG("Searching for decreasing phi in neighbors of " << geom->surfNodeIdx);
				closestNodeData = smallestDistanceLoop(
				        shS->neighborsNodes[geom->surfNodeIdx],
				        shS,
				        shB // both shapes
				        ,
				        ctrS,
				        rotS // center and orientation of the small body
				        ,
				        ctrB,
				        rotB // center and orientation of the big body
				        ,
				        minBoOverlap,
				        maxBoOverlap);
				if (std::get<1>(closestNodeData) == geom->surfNodeIdx) {
					// we found back the same node: we are at phi-minimum ! Let us stop the search
					move = false;
					LOG_DEBUG("Closest distance still found at the same node, stoping to move");
				} else if (std::get<1>(closestNodeData) < 0) {
					// this is possible when previous contacting node and all its neighbors are now impossible to test in the distance field
					lastThoroughSearch = true;  // in this case we decide to redo (below) a full search to be sure
					move               = false; // we naturally need to stop working with neighbors
				} else {
					LOG_DEBUG("Closest distance now found at " << std::get<1>(closestNodeData));
					geom->surfNodeIdx = std::get<1>(closestNodeData);
				}
			}
			if (lastThoroughSearch) {
				closestNodeData = smallestDistanceFullLoop(
				        shS,
				        shB // both shapes
				        ,
				        ctrS // center of the small body
				        ,
				        rotS // orientation of the small body
				        ,
				        ctrB // center of the big body
				        ,
				        rotB // orientation of the big body
				        ,
				        minBoOverlap,
				        maxBoOverlap);
				geom->usedWorkflow = 3;
			}
		}
	}
#ifdef USE_TIMING_DELTAS
	timingDeltas->checkpoint("After completing the history-dependent stuff");
#endif
	// 2.3 We finish the work

	// we now know about closestNodeData (and may have already filled geom->surfNodeIdx, for the record), with the possibility still of garbage data (e.g., nodeIdxOfInterest < 0) in case no node could actually be tested against the other distance field:
	// NB: after several tries, we will in this case indirectly trigger interaction removal by returning false (be it real or not)
	int nodeIdxOfInterest(std::get<1>(closestNodeData));
	if (nodeIdxOfInterest < 0) return false;
	Real     smallestPhi(std::get<0>(closestNodeData));
	Vector3r contactNode(std::get<2>(closestNodeData)); // in global configuration
	LOG_INFO("Got closest node as " << nodeIdxOfInterest << " with phi = " << smallestPhi);
	if (!std::isfinite(smallestPhi))
		LOG_FATAL(
		        "Unexpected case at interaction " << c->id1 << "-" << c->id2 << " with nodeIdx = " << nodeIdxOfInterest
		                                          << " but corresponding phi = " << smallestPhi);
	// NB: apart the above case and unlike, e.g., Ig2_Sphere_Sphere_ScGeom, we will here always create the geom (i.e., never return false) in order to use surfNodeIdx info even if id1, id2 are not yet in actual geometric contact
	Vector3r normal(
	        nodeIdxOfInterest >= 0 ? // that test for a special case is now no longer justified ?
	                rotS
	                        * shS->normal(
	                                shS->surfNodes
	                                        [nodeIdxOfInterest]) // shS->surfNodes[..] (and normal() to itself) refers to the initial shape, current normal is obtained with rotS *. It is for now the outward normal to the small particle
	                               : Vector3r(1, 0, 0)           // assigning a completely random (but finite) normal for those distant interactions
	);
	if (id1isBigger) normal *= -1;         // if necessary, we make the normal from 1 to 2, as expected
	geom->surfNodeIdx = nodeIdxOfInterest; // might be redundant with a line above for usedWorkflow = 2, let it be

	// Defining geometric quantities with special cases for the possibility of a distant interaction with nothing meaningfull computed (that no longer exists ? and the special cases could be removed)
	Vector3r middle12((state1.pos + state2.pos) / 2);
	Vector3r ctctPt(
	        std::isfinite(smallestPhi)
	                ? (contactNode + (id1isBigger ? -1 : 1) * smallestPhi / 2. * normal) // middle of overlapping volumes (or gap), as usual
	                : middle12 // in the case of no overlap we do not want to deal with smallestPhi in the quantity since it is infinite.. Ideal would be to have the middle point of the gap, but it is not obvious to know it in case of non-spherical particles.. We thus quite arbitrarily take the middle of the centers (even though it is not even sure it is in the gap)
	);
	Real rad1(
	        std::isfinite(smallestPhi)
	                ? (contactNode - state1.pos + smallestPhi * (id1isBigger ? normal : Vector3r::Zero()))
	                          .norm() // radius1 value: taking center to surface distance for id1, expressed from contactNode and possible offset (depending on which particle contactNode came from)
	                : sh1->maxRad // another finite arbitrary value
	        ),
	        rad2(std::isfinite(smallestPhi) ? (contactNode - state2.pos - shift2 + smallestPhi * (id1isBigger ? Vector3r::Zero() : normal))
	                                                  .norm() // radius2: same thing for id2. Reminder: contactNode does belong to id2 if id1 is bigger)
	                                        : sh2->maxRad     // another finite arbitrary value
	        );
	geom->doIg2Work(
	        ctctPt,
	        std::isfinite(smallestPhi) ? -smallestPhi // does not work for very big/huge overlap, eg when one sphere's center gets into another.
	                                   : -1,          // any negative value
	        rad1,
	        rad2,
	        state1,
	        state2,
	        scene,
	        c,
	        normal,
	        shift2,
	        newIgeom,
	        false);
	if (newIgeom) c->geom = geom;
#ifdef USE_TIMING_DELTAS
	timingDeltas->checkpoint("Terminating ::go");
#endif
	return true;
}

bool Ig2_LevelSet_LevelSet_ScGeom::go(
        const shared_ptr<Shape>&       shape1,
        const shared_ptr<Shape>&       shape2,
        const State&                   state1,
        const State&                   state2,
        const Vector3r&                shift2,
        const bool&                    force,
        const shared_ptr<Interaction>& c)
{
#ifdef USE_TIMING_DELTAS
	timingDeltas->start();
#endif
	bool ret(goSingleOrMulti(true, shape1, shape2, state1, state2, force, c, shift2));
#ifdef USE_TIMING_DELTAS
	timingDeltas->checkpoint("Completing ::go");
#endif
	return ret;
}

bool Ig2_LevelSet_LevelSet_MultiScGeom::go(
        const shared_ptr<Shape>&       shape1,
        const shared_ptr<Shape>&       shape2,
        const State&                   state1,
        const State&                   state2,
        const Vector3r&                shift2,
        const bool&                    force,
        const shared_ptr<Interaction>& c)
{
	return goSingleOrMulti(false, shape1, shape2, state1, state2, force, c, shift2);
}

std::pair<std::pair<Vector3r, Vector3r>, std::pair<bool, bool>>
Ig2_LevelSet_LevelSet_ScGeom::boundOverlap(bool single, const State& state1, const State& state2, const shared_ptr<Interaction>& c, const Vector3r& shift2)
{
	// Shall determine the Aabb zone where bodies bounds overlap (in .first) , and also prepare for a premature return of the Ig2::go if there is none, when .second.first will be true, with .second.second the return value to be sent by Ig2::go
	// TODO: possible use of Eigen AlignedBox ?
	std::array<Real, 6>     overlap; // the xA,xB, yA,yB, zA,zB defining the Aabb where bounds overlap: for x in [xA,xB] ; y in [yA,yB] ; ...
	const shared_ptr<Bound> bound1 = Body::byId(c->id1, scene)->bound;
	const shared_ptr<Bound> bound2 = Body::byId(c->id2, scene)->bound;
	bool                    emptyBoOverlap(false) // whether there is no bounds overlap
	        ,
	        returnValue(true) // will be passed to Ig2*::go as the return value to send if(emptyBoOverlap)
	        ;
	for (int axis = 0; axis < 3; axis++) {
		overlap[2 * axis]     = math::max(bound1->min[axis], bound2->min[axis] + shift2[axis]);
		overlap[2 * axis + 1] = math::min(bound1->max[axis], bound2->max[axis] + shift2[axis]);
		if (overlap[2 * axis + 1]
		    < overlap[2 * axis]) {     // an empty bound overlap here (= is possible and OK: it happens when bodies bounds are just tangent)
			emptyBoOverlap = true; // we already know there is no bound overlap if there is no overlap on one axis
			if (!c->isReal()) returnValue = false;
			else {
				if (single) geomPtrForLaterRemoval(state1, state2, c, scene);
				else {
					shared_ptr<MultiScGeom> multiGeom(YADE_PTR_CAST<MultiScGeom>(c->geom));
					multiGeom->contacts
					        .clear(); // and Law2_MultiScGeom_MultiFrictPhys_CundallStrack::go will also return false and eventually trigger interaction removal
				}
				returnValue = true;
			}
			break; // no need to check other axes
		}
	}
	return std::pair<std::pair<Vector3r, Vector3r>, std::pair<bool, bool>>(
	        std::make_pair(
	                Vector3r(overlap[0], overlap[2], overlap[4]),
	                Vector3r(
	                        overlap[1],
	                        overlap[3],
	                        overlap[5])) // (some of) these could be garbage values if emptyBoOverlap but in this case we will never use them anyway
	        ,
	        std::make_pair(emptyBoOverlap, returnValue));
}

Real Ig2_LevelSet_LevelSet_ScGeom::normalMismatch(const shared_ptr<Interaction>& c)
{
	std::pair<Vector3r, Vector3r> normals; // .first, resp. .second, will be computed considering nodes are on 1, resp. on 2.
	const shared_ptr<Body>&       b1 = Body::byId(c->id1, scene), b2 = Body::byId(c->id2, scene);
	const shared_ptr<LevelSet>    sh1 = YADE_PTR_CAST<LevelSet>(b1->shape);                         // let s go for the static cast
	const shared_ptr<LevelSet>    sh2 = YADE_PTR_CAST<LevelSet>(b2->shape);                         // and leave the responsability to the user
	auto returnOfBoundOverlap(boundOverlap(true, *(b1->state), *(b2->state), c, Vector3r::Zero())); // TODO: shift2 here if PBC are to be supported
	if (returnOfBoundOverlap.second.first)                                                          // an empty bound overlap was detected
		LOG_ERROR("Given interaction shows no Bound overlap, how is this possible ?)");
	Vector3r minBoOverlap(returnOfBoundOverlap.first.first), maxBoOverlap(returnOfBoundOverlap.first.second); // current configuration obviously
	auto     closestNodeOn1Data
	        = smallestDistanceFullLoop(sh1, sh2, b1->state->pos, b1->state->ori, b2->state->pos, b2->state->ori, minBoOverlap, maxBoOverlap);
	auto closestNodeOn2Data
	        = smallestDistanceFullLoop(sh2, sh1, b2->state->pos, b2->state->ori, b1->state->pos, b1->state->ori, minBoOverlap, maxBoOverlap);
	normals.first  = b1->state->ori * sh1->normal(sh1->surfNodes[std::get<1>(closestNodeOn1Data)]);
	normals.second = b2->state->ori * sh2->normal(sh2->surfNodes[std::get<1>(closestNodeOn2Data)]);
	LOG_DEBUG("Normal computed on 1 is " << normals.first << " while on 2 it is " << -normals.second);
	const shared_ptr<ScGeom> geom = YADE_PTR_CAST<ScGeom>(c->geom);
	if ((normals.first.cross(geom->normal).norm() > 1.e-7) and (normals.second.cross(geom->normal).norm() > 1.e-7))
		LOG_ERROR("Could not reproduce cont.geom.normal, how is that possible ?");
	return math::asin(normals.first.cross(normals.second).norm()); // a - normals.second would be logical but useless because of .norm()
}

std::tuple<Real, int, Vector3r> Ig2_LevelSet_LevelSet_ScGeom::smallestDistanceFullLoop(
        const shared_ptr<LevelSet>& shS,
        const shared_ptr<LevelSet>& shB,
        Vector3r                    ctrS,
        Quaternionr                 rotS // describing "S" configuration
        ,
        Vector3r    ctrB,
        Quaternionr rotB // describing "B" configuration
        ,
        Vector3r minBoOverlap,
        Vector3r maxBoOverlap // the min and max of the bounds overlap
)
{
	vector<int> allIndices;
	allIndices.resize(shS->surfNodes.size());
	std::iota(allIndices.begin(), allIndices.end(), 0); // negative performance impact of that line ? or not ?
	return smallestDistanceLoop(allIndices, shS, shB, ctrS, rotS, ctrB, rotB, minBoOverlap, maxBoOverlap);
}

std::tuple<Real, int, Vector3r> Ig2_LevelSet_LevelSet_ScGeom::smallestDistanceLoop(
        vector<int>                 idxOfNodesToLoopOn,
        const shared_ptr<LevelSet>& shS,
        const shared_ptr<LevelSet>& shB,
        Vector3r                    ctrS,
        Quaternionr                 rotS // describing "S" configuration
        ,
        Vector3r    ctrB,
        Quaternionr rotB // describing "B" configuration
        ,
        Vector3r minBoOverlap,
        Vector3r maxBoOverlap // the min and max of the bounds overlap
)
{
	Vector3r minGridB(shB->lsGrid->min), maxGridB(shB->lsGrid->max()); // the min and max of the lsGrid of "B"
	Real     smallestPhi(std::numeric_limits<Real>::infinity())        // will be the first return value
	        ,
	        distToNode // current distance value that will often change below
	        ;
	Vector3r nodeOfS // node at hand, in global configuration
	        ,
	        nodeOfSinB // mapped into "B" configuration
	        ,
	        smallestDistanceNode // nodeOfS value showing smallestPhi
	        ;
	int smallestDistanceNodeIdx(-1) // second return item
	        ;
	LOG_DEBUG("Will loop over " << idxOfNodesToLoopOn.size() << " surface nodes");
	for (const int& nodeIdx : idxOfNodesToLoopOn) {
		if (nodeIdx < 0 || nodeIdx >= int(shS->surfNodes.size()))
			LOG_ERROR("Major problem with nodeIdx " << nodeIdx << " for looping over " << shS->surfNodes.size() << " elements.");
		// 1. We first look at bounds overlap and grid considerations:
		nodeOfS = ShopLS::rigidMapping(
		        shS->surfNodes[nodeIdx],
		        Vector3r::Zero(),
		        ctrS,
		        rotS); // current position of this boundary node. Note that with a correct ls body creation and use, does not matter whether we take LevelSet->getCenter() or 0
		if (!Shop::isInBB(nodeOfS, minBoOverlap, maxBoOverlap)) continue;
		nodeOfSinB = ShopLS::rigidMapping(nodeOfS, ctrB, Vector3r::Zero(), rotB.conjugate()); // mapping this node into the big Body local frame
		if (!Shop::isInBB(
		            nodeOfSinB,
		            minGridB,
		            maxGridB)) // possible when bodies (and their shape.corners) rotate, leading their bounds to possibly "inflate" (think of a sphere)
			continue;
		// 2. Actual distance considerations:
		distToNode = shB->distance(nodeOfSinB);
		LOG_DEBUG("For node " << nodeIdx << " just computed distance = " << distToNode);
		if (distToNode < smallestPhi) {
			// storing just-computed stuff, while the rest (like computing normal) will be done once for all outside that method
			smallestDistanceNodeIdx = nodeIdx;
			smallestPhi             = distToNode;
			smallestDistanceNode    = nodeOfS;
		}
	} // at this stage it is possible we return an infinite phi together with a negative node idx (if all were outside of bounding box/grid considerations) and a garbage nodeOfS
	// this will be taken care of in calling functions
	return std::make_tuple(smallestPhi, smallestDistanceNodeIdx, smallestDistanceNode);
}

bool Ig2_LevelSet_LevelSet_ScGeom::goSingleOrMulti(
        bool                           single,
        const shared_ptr<Shape>&       shape1,
        const shared_ptr<Shape>&       shape2,
        const State&                   state1,
        const State&                   state2,
        const bool&                    force,
        const shared_ptr<Interaction>& c,
        const Vector3r&                shift2)
{
	// 1. We first use boundOverlap() that looked at the Aabb zone where bodies bounds overlap
	auto returnOfBoundOverlap(boundOverlap(single, state1, state2, c, shift2));
	if (returnOfBoundOverlap.second.first)                                                                    // an empty bound overlap was detected
		return (returnOfBoundOverlap.second.second);                                                      // we shall stop Ig2*::go work
	Vector3r minBoOverlap(returnOfBoundOverlap.first.first), maxBoOverlap(returnOfBoundOverlap.first.second); // current configuration obviously

	// 2. We go now for the master-slave contact algorithm with surface ie boundary nodes:
	// 2.1 Preliminary declarations:
	bool                    newIgeom(!bool(c->geom));
	shared_ptr<ScGeom>      geomSingle(new ScGeom);     // useful only if single but let s also make it here in full scope
	shared_ptr<MultiScGeom> geomMulti(new MultiScGeom); // useful only if !single but has to be in full scope
	shared_ptr<MultiPhys>   physMulti(
                new MultiPhys); // useful only if !single but has to be in full scope. As a reminder, Ig2_*MultiScGeom have to touch the physics
	if (!newIgeom) {
		if (single) geomSingle = YADE_PTR_CAST<ScGeom>(c->geom);
		else {
			geomMulti = YADE_PTR_CAST<MultiScGeom>(c->geom);
			physMulti = YADE_PTR_CAST<MultiPhys>(c->phys);
		}
	} // nothing to do in else
	shared_ptr<LevelSet> sh1 = YADE_PTR_CAST<LevelSet>(shape1);
	shared_ptr<LevelSet> sh2 = YADE_PTR_CAST<LevelSet>(shape2);
	bool                 id1isBigger(
                sh1->getVolume() > sh2->getVolume()); // identifying the smallest particle (where the master-slave contact algorithm will locate boundary nodes)
	shared_ptr<LevelSet> shB(id1isBigger ? sh1 : sh2); // the "big" shape
	shared_ptr<LevelSet> shS(id1isBigger ? sh2 : sh1); // the "small" shape
	// centr*end, and rot* below define the bodies' changes in configuration since the beginning of the simulation:
	Vector3r    centrSend(id1isBigger ? (state2.pos + shift2) : state1.pos), centrBend(id1isBigger ? state1.pos : (state2.pos + shift2));
	Quaternionr rotS(id1isBigger ? state2.ori : state1.ori), // ori = rotation from reference configuration (local axes) to current one
	        rotB(id1isBigger ? state1.ori : state2.ori)      // will be .conjugate() below to be from the current configuration to the reference one
	        ;
	const int nNodes(shS->surfNodes.size());
	if (!nNodes) LOG_ERROR("We will loop over no boundary nodes for contact detection. Will probably crash");
	Real     distToNode = 0;                                     // one distance value, for one node
	Real     smallestPhi(std::numeric_limits<Real>::infinity()); // useful only if single but has to be in full scope
	Vector3r minLSgrid   = shB->lsGrid->min;
	Vector3r maxLSgrid   = shB->lsGrid->max();
	Vector3r nodeOfS     = Vector3r::Zero(); // some node of smaller Body, in current configuration
	Vector3r nodeOfSinB  = Vector3r::Zero(); // mapped into initial configuration of larger Body
	Vector3r normal      = Vector3r::Zero();
	Vector3r contactNode = Vector3r::Zero();
	int      surfNodeIdx(-1);

	// 2.2 Actual loop over surface nodes:
	LOG_DEBUG("Will loop over " << nNodes << " surface nodes")
	for (int node = 0; node < nNodes; node++) {
		nodeOfS = ShopLS::rigidMapping(
		        shS->surfNodes[node],
		        Vector3r::Zero(),
		        centrSend,
		        rotS); // current position of this boundary node. Note that with a correct ls body creation and use, does not matter whether we take LevelSet->getCenter() or 0
		if (!Shop::isInBB(nodeOfS, minBoOverlap, maxBoOverlap)) {
			if (!single) ShopLS::handleNonTouchingNodeForMulti(geomMulti, physMulti, node);
			continue;
		}
		nodeOfSinB = ShopLS::rigidMapping(nodeOfS, centrBend, Vector3r::Zero(), rotB.conjugate()); // mapping this node into the big Body local frame
		if (!Shop::isInBB(
		            nodeOfSinB,
		            minLSgrid,
		            maxLSgrid)) { // possible when bodies (and their shape.corners) rotate, leading their bounds to possibly "inflate" (think of a sphere)
			if (!single) ShopLS::handleNonTouchingNodeForMulti(geomMulti, physMulti, node);
			continue;
		}
		distToNode = shB->distance(nodeOfSinB);
		if (single) {
			if (distToNode < 0 and distToNode < smallestPhi) { // when single, only greatest penetration depth is relevant
				surfNodeIdx = node; // storing the idx of that node. The rest (like computing normal) will be done once for all outside the loop
				smallestPhi = distToNode; // also storing just corresponding overlap
			}
		} else { // !single
			if (distToNode < 0) {
				normal = rotS * shS->normal(shS->surfNodes[node]); // getting the normal as above (single)
				if (id1isBigger) normal *= -1;                     // as above (single)
				contactNode = nodeOfS;                             // as above (single)
				ShopLS::handleTouchingNodeForMulti(
				        geomMulti,
				        physMulti,
				        node,
				        contactNode
				                + (id1isBigger ? -1 : 1) * distToNode / 2.
				                        * normal // middle of overlapping volumes, as usual TODO: is this correct or do we apply twice the id1isBigger -1
				        ,
				        -distToNode // does not work for very big/huge overlap, eg when one sphere's center gets into another
				        ,
				        (contactNode - state1.pos).norm() //TODO PBC
				        ,
				        (contactNode - state2.pos).norm() //TODO PBC
				        ,
				        state1,
				        state2,
				        scene,
				        c,
				        normal,
				        shift2);
			} else { // distToNode >= 0 and !single: we do not keep this as a contact point (even if distToNode = 0)
				ShopLS::handleNonTouchingNodeForMulti(geomMulti, physMulti, node);
				// and we automatically move on to the next node since we are at the end of the loop commands
			} // nothing to do in else (when the node was also not contacting before)
		}
	}
	if (single && surfNodeIdx >= 0) /* 2nd condition checks whether we did detect contact */ {
		normal = rotS
		        * shS->normal(
		                shS->surfNodes
		                        [surfNodeIdx]); // shS->surfNodes[..] (and normal() to itself) refers to the initial shape, current normal is obtained with rotS *. It is for now the outward normal to the small particle
		if (id1isBigger) normal *= -1;          // if necessary, we make the normal from 1 to 2, as expected
		contactNode = ShopLS::rigidMapping(
		        shS->surfNodes[surfNodeIdx],
		        Vector3r::Zero(),
		        centrSend,
		        rotS); // current position of this boundary node, this was already computed during the loop ?
	} else
		LOG_DEBUG(
		        "Having here " << geomMulti->contacts.size() << " guys in ig->contacts, vs " << physMulti->contacts.size() << " in ip->contacts\n And"
		                       << geomMulti->nodesIds.size() << " guys in ig->nodesIds, vs " << physMulti->nodesIds.size() << " in ip->nodesIds");

	// 2.3 Finishing the work:
	if (!c->isReal() && !force && ((single && smallestPhi > 0) or (!single && geomMulti->contacts.size() == 0)))
		return false; // we won't create the geom in this case, but it is not our job here to delete the interaction. We just return false on non-real ones, unless force (which will never happen since it is hard-coded to false in InteractionLoop). See e.g. Ig2_Sphere_Sphere_ScGeom
	if (single) {
		geomSingle->doIg2Work(
		        contactNode + (id1isBigger ? -1 : 1) * smallestPhi / 2. * normal, // middle of overlapping volumes, as usual
		        -smallestPhi, // does not work for very big/huge overlap, eg when one sphere's center gets into another.
		        (contactNode - state1.pos + smallestPhi * (id1isBigger ? normal : Vector3r::Zero()))
		                .norm(), // radius1 value: taking center to surface distance for id1, expressed from contactNode and possible offset (depending on which particle contactNode came from)
		        (contactNode - state2.pos - shift2 + smallestPhi * (id1isBigger ? Vector3r::Zero() : normal))
		                .norm(), // radius2: same thing for id2. Reminder: contactNode does belong to id2 if id1 is bigger
		        state1,
		        state2,
		        scene,
		        c,
		        normal,
		        shift2,
		        newIgeom,
		        false); // NB: the avoidGranularRatcheting=1 expression of relative velocity at contact does not make sense with radius1 and radius2 in all cases of LevelSet-sthg interaction (because the shapes are non-spherical): our present radius1+radius2 may have nothing in common with branch vector
	}
	if (newIgeom) {
		if (single) c->geom = geomSingle;
		else {
			c->geom = geomMulti; // ternary expression with above "= geomSingle" not possible because geomSingle and geomMulti are of different types
			c->phys = physMulti;
		}
	}
	if (!single) LOG_DEBUG("Will return true");
	return true;
}
} // namespace yade
#endif //YADE_LS_DEM