File: _utils.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 (1056 lines) | stat: -rw-r--r-- 61,397 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
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
#include <lib/high-precision/Constants.hpp>
#include <core/InteractionLoop.hpp>
#ifdef YADE_LS_DEM
#include <pkg/levelSet/ShopLS.hpp>
#endif // YADE_LS_DEM
#include <py/_utils.hpp>
// https://codeyarns.com/2014/03/11/how-to-selectively-ignore-a-gcc-warning/
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpragmas"
#pragma GCC diagnostic ignored "-Wunused-function"
// Code that generates this warning, Note: we cannot do this trick in yade. If we have a warning in yade, we have to fix it! See also https://gitlab.com/yade-dev/trunk/merge_requests/73
// This method will work once g++ bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431#c34 is fixed.
#include <numpy/arrayobject.h>
#pragma GCC diagnostic pop

CREATE_CPP_LOCAL_LOGGER("_utils.cpp");

namespace yade { // Cannot have #include directive inside.

using math::max;
using math::min; // using inside .cpp file is ok.

py::tuple negPosExtremeIds(int axis, Real distFactor)
{
	const auto extrema  = Shop::aabbExtrema();
	Real       minCoord = extrema.first[axis], maxCoord = extrema.second[axis];
	py::list   minIds, maxIds;
	for (const auto& b : *Omega::instance().getScene()->bodies) {
		shared_ptr<Sphere> s = YADE_PTR_DYN_CAST<Sphere>(b->shape);
		if (!s) continue;
		if (b->state->pos[axis] - s->radius * distFactor <= minCoord) minIds.append(b->getId());
		if (b->state->pos[axis] + s->radius * distFactor >= maxCoord) maxIds.append(b->getId());
	}
	return py::make_tuple(minIds, maxIds);
}

py::tuple coordsAndDisplacements(int axis, py::tuple Aabb)
{
	Vector3r bbMin(Vector3r::Zero()), bbMax(Vector3r::Zero());
	bool     useBB = py::len(Aabb) > 0;
	if (useBB) {
		bbMin = py::extract<Vector3r>(Aabb[0])();
		bbMax = py::extract<Vector3r>(Aabb[1])();
	}
	py::list retCoord, retDispl;
	for (const auto& b : *Omega::instance().getScene()->bodies) {
		if (useBB && !Shop::isInBB(b->state->pos, bbMin, bbMax)) continue;
		retCoord.append(b->state->pos[axis]);
		retDispl.append(b->state->pos[axis] - b->state->refPos[axis]);
	}
	return py::make_tuple(retCoord, retDispl);
}

void setRefSe3()
{
	Scene* scene = Omega::instance().getScene().get();
	for (const auto& b : *scene->bodies) {
		b->state->refPos = b->state->pos;
		b->state->refOri = b->state->ori;
	}
	if (scene->isPeriodic) { scene->cell->refHSize = scene->cell->hSize; }
}

Real PWaveTimeStep() { return Shop::PWaveTimeStep(); };
Real RayleighWaveTimeStep() { return Shop::RayleighWaveTimeStep(); };

py::tuple interactionAnglesHistogram(int axis, int mask, size_t bins, py::tuple aabb, bool sphSph, Real minProjLen)
{
	if (axis < 0 || axis > 2) throw invalid_argument("Axis must be from {0,1,2}=x,y,z.");
	Vector3r bbMin(Vector3r::Zero()), bbMax(Vector3r::Zero());
	bool     useBB = py::len(aabb) > 0;
	if (useBB) {
		bbMin = py::extract<Vector3r>(aabb[0])();
		bbMax = py::extract<Vector3r>(aabb[1])();
	}
	Real              binStep = Mathr::PI / bins;
	int               axis2 = (axis + 1) % 3, axis3 = (axis + 2) % 3;
	vector<Real>      cummProj(bins, 0.);
	shared_ptr<Scene> rb = Omega::instance().getScene();
	for (const auto& i : *rb->interactions) {
		if (!i->isReal()) continue;
		const auto b1 = Body::byId(i->getId1(), rb), b2 = Body::byId(i->getId2(), rb);
		if (!b1->maskOk(mask) || !b2->maskOk(mask)) continue;
		if (useBB && !Shop::isInBB(b1->state->pos, bbMin, bbMax) && !Shop::isInBB(b2->state->pos, bbMin, bbMax)) continue;
		if (sphSph && (!dynamic_cast<Sphere*>(b1->shape.get()) || !dynamic_cast<Sphere*>(b2->shape.get()))) continue;
		GenericSpheresContact* geom = dynamic_cast<GenericSpheresContact*>(i->geom.get());
		if (!geom) continue;
		Vector3r n(geom->normal);
		n[axis]   = 0.;
		Real nLen = n.norm();
		if (nLen < minProjLen) continue; // this interaction is (almost) exactly parallel to our axis; skip that one
		Real theta = acos(n[axis2] / nLen) * (n[axis3] > 0 ? 1 : -1);
		if (theta < 0) theta += Mathr::PI;
		int binNo = int(math::round(theta / binStep));
		cummProj[binNo] += nLen;
	}
	py::list val, binMid;
	for (size_t i = 0; i < (size_t)bins; i++) {
		val.append(cummProj[i]);
		binMid.append(i * binStep);
	}
	return py::make_tuple(binMid, val);
}

py::tuple bodyNumInteractionsHistogram(py::tuple aabb)
{
	Vector3r bbMin(Vector3r::Zero()), bbMax(Vector3r::Zero());
	bool     useBB = py::len(aabb) > 0;
	if (useBB) {
		bbMin = py::extract<Vector3r>(aabb[0])();
		bbMax = py::extract<Vector3r>(aabb[1])();
	}
	const shared_ptr<Scene>& rb = Omega::instance().getScene();
	vector<int>              bodyNumIntr;
	bodyNumIntr.resize(rb->bodies->size(), 0);
	int maxIntr = 0;
	for (const auto& i : *rb->interactions) {
		if (!i->isReal()) continue;
		const Body::id_t id1 = i->getId1(), id2 = i->getId2();
		const auto       b1 = Body::byId(id1, rb), b2 = Body::byId(id2, rb);
		if ((useBB && Shop::isInBB(b1->state->pos, bbMin, bbMax)) || !useBB) {
			if (b1->isClumpMember()) bodyNumIntr[b1->clumpId] += 1; //count bodyNumIntr for the clump, not for the member
			else
				bodyNumIntr[id1] += 1;
		}
		if ((useBB && Shop::isInBB(b2->state->pos, bbMin, bbMax)) || !useBB) {
			if (b2->isClumpMember()) bodyNumIntr[b2->clumpId] += 1; //count bodyNumIntr for the clump, not for the member
			else
				bodyNumIntr[id2] += 1;
		}
		maxIntr = max(max(maxIntr, bodyNumIntr[b1->getId()]), bodyNumIntr[b2->getId()]);
		if (b1->isClumpMember()) maxIntr = max(maxIntr, bodyNumIntr[b1->clumpId]);
		if (b2->isClumpMember()) maxIntr = max(maxIntr, bodyNumIntr[b2->clumpId]);
	}
	vector<int> bins;
	bins.resize(maxIntr + 1, 0);
	for (size_t id = 0; id < bodyNumIntr.size(); id++) {
		const auto b = Body::byId(id, rb);
		if (b) {
			if (bodyNumIntr[id] > 0) bins[bodyNumIntr[id]] += 1;
			// 0 is handled specially: add body to the 0 bin only if it is inside the bb requested (if applicable)
			// otherwise don't do anything, since it is outside the volume of interest
			else if (((useBB && Shop::isInBB(b->state->pos, bbMin, bbMax)) || !useBB) && !(b->isClumpMember()))
				bins[0] += 1;
		}
	}
	py::list count, num;
	for (size_t n = 0; n < bins.size(); n++) {
		if (bins[n] == 0) continue;
		num.append(n);
		count.append(bins[n]);
	}
	return py::make_tuple(num, count);
}

Vector3r inscribedCircleCenter(const Vector3r& v0, const Vector3r& v1, const Vector3r& v2) { return Shop::inscribedCircleCenter(v0, v1, v2); }
py::dict getViscoelasticFromSpheresInteraction(Real tc, Real en, Real es)
{
	shared_ptr<ViscElMat> b = shared_ptr<ViscElMat>(new ViscElMat());
	Shop::getViscoelasticFromSpheresInteraction(tc, en, es, b);
	py::dict d;
	d["kn"] = b->kn;
	d["cn"] = b->cn;
	d["ks"] = b->ks;
	d["cs"] = b->cs;
	return d;
}
/* reset highlight of all bodies */
void highlightNone()
{
	for (const auto& b : *Omega::instance().getScene()->bodies) {
		if (!b->shape) continue;
		b->shape->highlight = false;
	}
}

/*!Sum moments acting on given bodies
 *
 * @param ids is the calculated bodies ids
 * @param axis is the direction of axis with respect to which the moment is calculated.
 * @param axisPt is a point on the axis.
 *
 * The computation is trivial: moment from force is is by definition r×F, where r
 * is position relative to axisPt; moment from moment is m; such moment per body is
 * projected onto axis.
 */
Real sumTorques(py::list ids, const Vector3r& axis, const Vector3r& axisPt)
{
	shared_ptr<Scene> rb = Omega::instance().getScene();
	rb->forces.sync();
	Real   ret = 0;
	size_t len = py::len(ids);
	for (size_t i = 0; i < len; i++) {
		const Body*     b = (*rb->bodies)[py::extract<int>(ids[i])].get();
		const Vector3r& m = rb->forces.getTorque(b->getId());
		const Vector3r& f = rb->forces.getForce(b->getId());
		Vector3r        r = b->state->pos - axisPt;
		ret += axis.dot(m + r.cross(f));
	}
	return ret;
}
/* Sum forces acting on bodies within mask.
 *
 * @param ids list of ids
 * @param direction direction in which forces are summed
 *
 */
Real sumForces(py::list ids, const Vector3r& direction)
{
	shared_ptr<Scene> rb = Omega::instance().getScene();
	rb->forces.sync();
	Real   ret = 0;
	size_t len = py::len(ids);
	for (size_t i = 0; i < len; i++) {
		Body::id_t      id = py::extract<int>(ids[i]);
		const Vector3r& f  = rb->forces.getForce(id);
		ret += direction.dot(f);
	}
	return ret;
}

/* Sum force acting on facets given by their ids in the sense of their respective normals.
   If axis is given, it will sum forces perpendicular to given axis only (not the the facet normals).
*/
Real sumFacetNormalForces(vector<Body::id_t> ids, int axis)
{
	shared_ptr<Scene> rb = Omega::instance().getScene();
	rb->forces.sync();
	Real ret = 0;
	for (const Body::id_t& id : ids) {
		Facet* f = YADE_CAST<Facet*>(Body::byId(id, rb)->shape.get());
		if (axis < 0) ret += rb->forces.getForce(id).dot(f->normal);
		else {
			Vector3r ff = rb->forces.getForce(id);
			ff[axis]    = 0;
			ret += ff.dot(f->normal);
		}
	}
	return ret;
}

/* Set wire display of all/some/none bodies depending on the filter. */
void wireSome(string filter)
{
	enum { none, all, noSpheres, unknown };
	int mode = (filter == "none" ? none : (filter == "all" ? all : (filter == "noSpheres" ? noSpheres : unknown)));
	if (mode == unknown) {
		LOG_WARN("Unknown wire filter `" << filter << "', using noSpheres instead.");
		mode = noSpheres;
	}
	for (const auto& b : *Omega::instance().getScene()->bodies) {
		if (!b->shape) return;
		bool wire;
		switch (mode) {
			case none: wire = false; break;
			case all: wire = true; break;
			case noSpheres: wire = !(bool)(YADE_PTR_DYN_CAST<Sphere>(b->shape)); break;
			default: throw logic_error("No such case possible");
		}
		b->shape->wire = wire;
	}
}
void wireAll() { wireSome("all"); }
void wireNone() { wireSome("none"); }
void wireNoSpheres() { wireSome("noSpheres"); }


/* Tell us whether a point lies in polygon given by array of points.
 *  @param xy is the point that is being tested
 *  @param vertices is Numeric.array (or list or tuple) of vertices of the polygon.
 *         Every row of the array is x and y coordinate, numer of rows is >= 3 (triangle).
 *
 * Copying the algorithm from http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
 * is gratefully acknowledged:
 *
 * License to Use:
 * Copyright (c) 1970-2003, Wm. Randolph Franklin
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *   1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers.
 *   2. Redistributions in binary form must reproduce the above copyright notice in the documentation and/or other materials provided with the distribution.
 *   3. The name of W. Randolph Franklin may not be used to endorse or promote products derived from this Software without specific prior written permission.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * http://numpy.scipy.org/numpydoc/numpy-13.html told me how to use Numeric.array from c
 */
bool pointInsidePolygon(py::tuple xy, py::object vertices)
{
	Real   testx = py::extract<Real>(xy[0])(), testy = py::extract<Real>(xy[1])();
	char** vertData;
	int    rows, cols;
	if (PyArray_API == NULL) import_array();
	if (!PyArray_Check(vertices.ptr())) throw invalid_argument("Vertices must be a NumPy array");
	PyArrayObject* vert = (PyArrayObject*)vertices.ptr();
	if (PyArray_NDIM(vert) != 2) throw invalid_argument("Input array must be 2-dimensional");
	npy_intp dims[2] = { PyArray_DIM(vert, 0), PyArray_DIM(vert, 1) };
	rows = (int)dims[0], cols = (int)dims[1];
	if (cols != 2 || rows < 3) throw invalid_argument("Vertices must have 2 columns (x and y) and at least 3 rows.");
	int result = PyArray_AsCArray((PyObject**)&vert /* is replaced */, (void**)&vertData, dims, 2, PyArray_DescrFromType(NPY_DOUBLE));
	if (result < 0) throw invalid_argument("Unable to cast vertices to 2d array");
	int  i /*current node*/, j /*previous node*/;
	bool inside = false;
	for (i = 0, j = rows - 1; i < rows; j = i++) {
		double vx_i = *(double*)(vert->data + i * vert->strides[0]), vy_i = *(double*)(vert->data + i * vert->strides[0] + vert->strides[1]),
		       vx_j = *(double*)(vert->data + j * vert->strides[0]), vy_j = *(double*)(vert->data + j * vert->strides[0] + vert->strides[1]);
		if (((vy_i > testy) != (vy_j > testy)) && (testx < (vx_j - vx_i) * (testy - vy_i) / (vy_j - vy_i) + vx_i)) inside = !inside;
	}
	PyArray_Free((PyObject*)vert, (void*)vertData);
	return inside;
}

/* Compute area of convex hull when when taking (swept) spheres crossing the plane at coord, perpendicular to axis.

	All spheres that touch the plane are projected as hexagons on their circumference to the plane.
	Convex hull from this cloud is computed.
	The area of the hull is returned.

*/
Real approxSectionArea(Real coord, int axis)
{
	std::list<Vector2r> cloud;
	if (axis < 0 || axis > 2) throw invalid_argument("Axis must be ∈ {0,1,2}");
	const int  ax1 = (axis + 1) % 3, ax2 = (axis + 2) % 3;
	const Real sqrt3 = sqrt(3);
	Vector2r   mm, mx;
	int        i = 0;
	for (const auto& b : *Omega::instance().getScene()->bodies) {
		Sphere* s = dynamic_cast<Sphere*>(b->shape.get());
		if (!s) continue;
		const Vector3r& pos(b->state->pos);
		const Real      r(s->radius);
		if ((pos[axis] > coord && (pos[axis] - r) > coord) || (pos[axis] < coord && (pos[axis] + r) < coord)) continue;
		Vector2r c(pos[ax1], pos[ax2]);
		cloud.push_back(c + Vector2r(r, 0.));
		cloud.push_back(c + Vector2r(-r, 0.));
		cloud.push_back(c + Vector2r(r / 2., sqrt3 * r));
		cloud.push_back(c + Vector2r(r / 2., -sqrt3 * r));
		cloud.push_back(c + Vector2r(-r / 2., sqrt3 * r));
		cloud.push_back(c + Vector2r(-r / 2., -sqrt3 * r));
		if (i++ == 0) { mm = c, mx = c; }
		mm = Vector2r(min(c[0] - r, mm[0]), min(c[1] - r, mm[1]));
		mx = Vector2r(max(c[0] + r, mx[0]), max(c[1] + r, mx[1]));
	}
	if (cloud.size() == 0) return 0;
	ConvexHull2d     ch2d(cloud);
	vector<Vector2r> hull = ch2d();
	return simplePolygonArea2d(hull);
}
/* Find all interactions deriving from NormShearPhys that cross plane given by a point and normal
	(the normal may not be normalized in this case, though) and sum forces (both normal and shear) on them.

	Returns a 3-tuple with the components along global x,y,z axes, which can be viewed as "action from lower part, towards
	upper part" (lower and upper parts with respect to the plane's normal).

	(This could be easily extended to return sum of only normal forces or only of shear forces.)
*/
Vector3r forcesOnPlane(const Vector3r& planePt, const Vector3r& normal)
{
	Vector3r ret(Vector3r::Zero());
	Scene*   scene = Omega::instance().getScene().get();
	for (const auto& I : *scene->interactions) {
		if (!I->isReal()) continue;
		NormShearPhys* nsi = dynamic_cast<NormShearPhys*>(I->phys.get());
		if (!nsi) continue;
		Vector3r pos1, pos2;
		pos1      = Body::byId(I->getId1(), scene)->state->pos;
		pos2      = Body::byId(I->getId2(), scene)->state->pos;
		Real dot1 = (pos1 - planePt).dot(normal), dot2 = (pos2 - planePt).dot(normal);
		if (dot1 * dot2 > 0) continue; // both (centers of) bodies are on the same side of the plane=> this interaction has to be disregarded
		// if pt1 is on the negative plane side, d3dg->normal.Dot(normal)>0, the force is well oriented;
		// otherwise, reverse its contribution. So that we return finally
		// Sum [ ( normal(plane) dot normal(interaction= from 1 to 2) ) "nsi->force" ]
		ret += (dot1 < 0. ? 1. : -1.) * (nsi->normalForce + nsi->shearForce);
	}
	return ret;
}

/* Less general than forcesOnPlane, computes force on plane perpendicular to axis, passing through coordinate coord. */
Vector3r forcesOnCoordPlane(Real coord, int axis)
{
	Vector3r planePt(Vector3r::Zero());
	planePt[axis] = coord;
	Vector3r normal(Vector3r::Zero());
	normal[axis] = 1;
	return forcesOnPlane(planePt, normal);
}


py::tuple spiralProject(const Vector3r& pt, Real dH_dTheta, int axis, Real periodStart, Real theta0)
{
	Real r, h, theta;
	boost::tie(r, h, theta) = Shop::spiralProject(pt, dH_dTheta, axis, periodStart, theta0);
	return py::make_tuple(py::make_tuple(r, h), theta);
}

shared_ptr<Interaction> Shop__createExplicitInteraction(Body::id_t id1, Body::id_t id2, bool virtualI)
{
	return InteractionLoop::createExplicitInteraction(id1, id2, /*force*/ true, virtualI);
}

Real      Shop__unbalancedForce(bool useMaxForce /*false by default*/) { return Shop::unbalancedForce(useMaxForce); }
py::tuple Shop__aabbExtrema(Real cutoff, bool centers)
{
	const auto aabb = Shop::aabbExtrema(cutoff, centers);
	return py::make_tuple(aabb.first, aabb.second);
}

py::tuple Shop__totalForceInVolume()
{
	Real     stiff;
	Vector3r ret = Shop::totalForceInVolume(stiff);
	return py::make_tuple(ret, stiff);
}
Real       Shop__getSpheresVolume(int mask) { return Shop::getSpheresVolume(Omega::instance().getScene(), mask); }
Real       Shop__getSpheresMass(int mask) { return Shop::getSpheresMass(Omega::instance().getScene(), mask); }
py::object Shop__kineticEnergy(bool findMaxId)
{
	if (!findMaxId) return py::object(Shop::kineticEnergy());
	Body::id_t maxId;
	Real       E = Shop::kineticEnergy(NULL, &maxId);
	return py::make_tuple(E, maxId);
}

Real maxOverlapRatio()
{
	Scene* scene = Omega::instance().getScene().get();
	Real   ret   = -1;
	for (const auto& I : *scene->interactions) {
		if (!I->isReal()) continue;
		Sphere *s1(dynamic_cast<Sphere*>(Body::byId(I->getId1(), scene)->shape.get())),
		        *s2(dynamic_cast<Sphere*>(Body::byId(I->getId2(), scene)->shape.get()));
		if ((!s1) || (!s2)) continue;
		ScGeom* geom = dynamic_cast<ScGeom*>(I->geom.get());
		if (!geom) continue;
		Real rEq = 2 * s1->radius * s2->radius / (s1->radius + s2->radius);
		ret      = max(ret, geom->penetrationDepth / rEq);
	}
	return ret;
}

Real Shop__getPorosity(Real volume) { return Shop::getPorosity(Omega::instance().getScene(), volume); }
Real Shop__getVoxelPorosity(int resolution, Vector3r start, Vector3r end)
{
	return Shop::getVoxelPorosity(Omega::instance().getScene(), resolution, start, end);
}

//Matrix3r Shop__stressTensorOfPeriodicCell(bool smallStrains=false){return Shop::stressTensorOfPeriodicCell(smallStrains);}
py::tuple Shop__fabricTensor(Real cutoff, bool splitTensor, Real thresholdForce, vector<Vector3r> extrema)
{
	return Shop::fabricTensor(cutoff, splitTensor, thresholdForce, extrema);
}
py::tuple Shop__normalShearStressTensors(bool compressionPositive, bool splitNormalTensor, Real thresholdForce)
{
	return Shop::normalShearStressTensors(compressionPositive, splitNormalTensor, thresholdForce);
}

py::list Shop__getStressLWForEachBody() { return Shop::getStressLWForEachBody(); }

py::list Shop__getBodyIdsContacts(Body::id_t bodyID) { return Shop::getBodyIdsContacts(bodyID); }

Real shiftBodies(py::list ids, const Vector3r& shift)
{
	shared_ptr<Scene> rb  = Omega::instance().getScene();
	size_t            len = py::len(ids);
	for (size_t i = 0; i < len; i++) {
		const Body* b = (*rb->bodies)[py::extract<int>(ids[i])].get();
		if (!b) continue;
		b->state->pos += shift;
	}
	return 1;
}

void Shop__calm(int mask) { return Shop::calm(Omega::instance().getScene(), mask); }

void setNewVerticesOfFacet(const shared_ptr<Body>& b, const Vector3r& v1, const Vector3r& v2, const Vector3r& v3)
{
	Vector3r center    = inscribedCircleCenter(v1, v2, v3);
	Facet*   facet     = YADE_CAST<Facet*>(b->shape.get());
	facet->vertices[0] = v1 - center;
	facet->vertices[1] = v2 - center;
	facet->vertices[2] = v3 - center;
	b->state->pos      = center;
}

py::list intrsOfEachBody()
{
	py::list          ret, temp;
	shared_ptr<Scene> rb = Omega::instance().getScene();
	size_t            n  = rb->bodies->size();
	// create list of size len(O.bodies) full of zeros
	for (size_t i = 0; i < n; i++) {
		ret.append(py::list());
	}
	// loop over all interactions and fill the list ret
	for (const auto& i : *rb->interactions) {
		if (!i->isReal()) { continue; }
		temp = py::extract<py::list>(ret[i->getId1()]);
		temp.append(i);
		temp = py::extract<py::list>(ret[i->getId2()]);
		temp.append(i);
	}
	return ret;
}

py::list numIntrsOfEachBody()
{
	py::list          ret;
	shared_ptr<Scene> rb = Omega::instance().getScene();
	size_t            n  = rb->bodies->size();
	// create list of size len(O.bodies) full of zeros
	for (size_t i = 0; i < n; i++) {
		ret.append(0);
	}
	// loop over all interactions and fill the list ret
	for (const auto& i : *rb->interactions) {
		if (!i->isReal()) continue;
		ret[i->getId1()] += 1;
		ret[i->getId2()] += 1;
	}
	return ret;
}

Real      Shop__getSpheresVolume2D(int mask = -1) { return Shop::getSpheresVolume2D(Omega::instance().getScene(), mask); }
Real      Shop__getVoidRatio2D(Real zlen = 1) { return Shop::getVoidRatio2D(Omega::instance().getScene(), zlen); }
py::tuple Shop__getStressAndTangent(Real volume = 0, bool symmetry = true) { return Shop::getStressAndTangent(volume, symmetry); }

void setBodyPosition(int id, Vector3r newPos, string axis)
{
	shared_ptr<Scene> rb = Omega::instance().getScene();
	const Body*       b  = (*rb->bodies)[id].get();
	for (char c : axis) {
		if (c == 'x') {
			b->state->pos[0] = newPos[0];
			continue;
		}
		if (c == 'y') {
			b->state->pos[1] = newPos[1];
			continue;
		}
		if (c == 'z') {
			b->state->pos[2] = newPos[2];
			continue;
		}
	}
}

void setBodyVelocity(int id, Vector3r newVel, string axis)
{
	shared_ptr<Scene> rb = Omega::instance().getScene();
	const Body*       b  = (*rb->bodies)[id].get();
	for (char c : axis) {
		if (c == 'x') {
			b->state->vel[0] = newVel[0];
			continue;
		}
		if (c == 'y') {
			b->state->vel[1] = newVel[1];
			continue;
		}
		if (c == 'z') {
			b->state->vel[2] = newVel[2];
			continue;
		}
	}
}

void setBodyOrientation(int id, Quaternionr newOri)
{
	shared_ptr<Scene> rb = Omega::instance().getScene();
	const Body*       b  = (*rb->bodies)[id].get();
	b->state->ori        = newOri;
}

void setBodyAngularVelocity(int id, Vector3r newAngVel)
{
	shared_ptr<Scene> rb = Omega::instance().getScene();
	const Body*       b  = (*rb->bodies)[id].get();
	b->state->angVel     = newAngVel;
}

void setBodyColor(int id, Vector3r newColor)
{
	shared_ptr<Scene> rb = Omega::instance().getScene();
	const Body*       b  = (*rb->bodies)[id].get();
	b->shape->color      = newColor;
}

} // namespace yade

// BOOST_PYTHON_MODULE cannot be inside yade namespace, it has 'extern "C"' keyword, which strips it out of any namespaces.
BOOST_PYTHON_MODULE(_utils)
try {
	using namespace yade; // 'using namespace' inside function keeps namespace pollution under control. Alernatively I could add y:: in front of function names below and put 'namespace y  = ::yade;' here.
	namespace py = ::boost::python;
	YADE_SET_DOCSTRING_OPTS;
	py::def("initMPI", initMPI, "Initialize MPI communicator, for Foam Coupling");
	py::def("PWaveTimeStep",
	        PWaveTimeStep,
	        "Get timestep accoring to the velocity of P-Wave propagation; computed for spheres and/or polyhedra based on their sizes, rigidities and "
	        "masses.");
	py::def("RayleighWaveTimeStep", RayleighWaveTimeStep, "Determination of time step according to Rayleigh wave speed of force propagation.");
	py::def("getSpheresVolume",
	        Shop__getSpheresVolume,
	        (py::arg("mask") = -1),
	        "Compute the total volume of spheres in the simulation, mask parameter is considered");
	py::def("getSpheresMass",
	        Shop__getSpheresMass,
	        (py::arg("mask") = -1),
	        "Compute the total mass of spheres in the simulation, mask parameter is considered");
	py::def("porosity",
	        Shop__getPorosity,
	        (py::arg("volume") = -1),
	        "Compute packing porosity $\\frac{V-V_s}{V}$ where $V$ is overall volume and $V_s$ is volume of spheres.\n\n:param float volume: overall "
	        "volume $V$. For periodic simulations, current volume of the :yref:`Cell` is used. For aperiodic simulations, the value deduced from "
	        "utils.aabbDim() is used. For compatibility reasons, positive values passed by the user are also accepted in this case.\n");
	py::def("voxelPorosity",
	        Shop__getVoxelPorosity,
	        (py::arg("resolution") = 200, py::arg("start") = Vector3r(0, 0, 0), py::arg("end") = Vector3r(0, 0, 0)),
	        // FIXME: use R"""(raw text)""" here, like exaplained in https://yade-dem.org/doc/prog.html#sphinx-documentation and used in py/_libVersions.cpp
	        "Compute packing porosity $\\frac{V-V_v}{V}$ where $V$ is a specified volume (from start to end) and $V_v$ is volume of voxels that fall "
	        "inside any sphere. The calculation method is to divide whole volume into a dense grid of voxels (at given resolution), and count the voxels "
	        "that fall inside any of the spheres. This method allows one to calculate porosity in any given sub-volume of a whole sample. It is properly "
	        "excluding part of a sphere that does not fall inside a specified volume.\n\n:param int resolution: voxel grid resolution, values bigger than "
	        "resolution=1600 require a 64 bit operating system, because more than 4GB of RAM is used, a resolution=800 will use 500MB of RAM.\n:param "
	        "Vector3 start: start corner of the volume.\n:param Vector3 end: end corner of the volume.\n");
	py::def("aabbExtrema",
	        Shop__aabbExtrema,
	        (py::arg("cutoff") = 0.0, py::arg("centers") = false),
	        "Return coordinates of box enclosing all spherical bodies\n\n:param bool centers: do not take sphere radii in account, only their "
	        "centroids\n:param float∈〈0…1〉 cutoff: relative dimension by which the box will be cut away at its boundaries.\n\n\n:return: [lower corner, "
	        "upper corner] as [Vector3,Vector3]\n\n");
	py::def("ptInAABB", Shop::isInBB, "Return True/False whether the point p is within box given by its min and max corners");
	py::def("negPosExtremeIds",
	        negPosExtremeIds,
	        (py::arg("axis"), py::arg("distFactor")),
	        "Return list of ids for spheres (only) that are on extremal ends of the specimen along given axis; distFactor multiplies their radius so that "
	        "sphere that do not touch the boundary coordinate can also be returned.");
	py::def("approxSectionArea",
	        approxSectionArea,
	        "Compute area of convex hull when when taking (swept) spheres crossing the plane at coord, perpendicular to axis.");
	py::def("coordsAndDisplacements",
	        coordsAndDisplacements,
	        (py::arg("axis"), py::arg("Aabb") = py::tuple()),
	        "Return tuple of 2 same-length lists for coordinates and displacements (coordinate minus reference coordinate) along given axis (1st arg); if "
	        "the Aabb=((x_min,y_min,z_min),(x_max,y_max,z_max)) box is given, only bodies within this box will be considered.");
	py::def("setRefSe3",
	        setRefSe3,
	        "Set reference :yref:`positions<State::refPos>` and :yref:`orientations<State::refOri>` of all :yref:`bodies<Body>` equal to their current "
	        ":yref:`positions<State::pos>` and :yref:`orientations<State::ori>`.");
	py::def("interactionAnglesHistogram",
	        interactionAnglesHistogram,
	        (py::arg("axis"),
	         py::arg("mask")       = 0,
	         py::arg("bins")       = 20,
	         py::arg("aabb")       = py::tuple(),
	         py::arg("sphSph")     = 0,
	         py::arg("minProjLen") = 1e-6));
	py::def("bodyNumInteractionsHistogram", bodyNumInteractionsHistogram, (py::arg("aabb")));
	py::def("inscribedCircleCenter",
	        inscribedCircleCenter,
	        (py::arg("v1"), py::arg("v2"), py::arg("v3")),
	        "Return center of inscribed circle for triangle given by its vertices *v1*, *v2*, *v3*.");
	py::def("unbalancedForce",
	        &Shop__unbalancedForce,
	        (py::args("useMaxForce") = false),
	        "Compute the ratio of mean (or maximum, if *useMaxForce*) summary force on bodies and mean force magnitude on interactions. For perfectly "
	        "static equilibrium, summary force on all bodies is zero (since forces from interactions cancel out and induce no acceleration of particles); "
	        "this ratio will tend to zero as simulation stabilizes, though zero is never reached because of finite precision computation. Sufficiently "
	        "small value can be e.g. 1e-2 or smaller, depending on how much equilibrium it should be.");
	py::def("kineticEnergy",
	        Shop__kineticEnergy,
	        (py::args("findMaxId") = false),
	        "Compute overall kinetic energy of the simulation as\n\n.. math:: "
	        "\\sum\\frac{1}{2}\\left(m_i\\vec{v}_i^2+\\vec{\\omega}(\\mat{I}\\vec{\\omega}^T)\\right).\n\nFor :yref:`aspherical<Body.aspherical>` bodies, "
	        "necessary frame transformations are applied to the inertia tensor $\\mat{I}$ as stored in :yref:`state.inertia<State.inertia>`.\n");
	py::def("sumForces",
	        sumForces,
	        (py::arg("ids"), py::arg("direction")),
	        "Return summary force on bodies with given *ids*, projected on the *direction* vector.");
	py::def("sumTorques",
	        sumTorques,
	        (py::arg("ids"), py::arg("axis"), py::arg("axisPt")),
	        "Sum forces and torques on bodies given in *ids* with respect to axis specified by a point *axisPt* and its direction *axis*.");
	py::def("sumFacetNormalForces",
	        sumFacetNormalForces,
	        (py::arg("ids"), py::arg("axis") = -1),
	        "Sum force magnitudes on given bodies (must have :yref:`shape<Body.shape>` of the :yref:`Facet` type), considering only part of forces "
	        "perpendicular to each :yref:`facet's<Facet>` face; if *axis* has positive value, then the specified axis (0=x, 1=y, 2=z) will be used instead "
	        "of facet's normals.");
	py::def("forcesOnPlane",
	        forcesOnPlane,
	        (py::arg("planePt"), py::arg("normal")),
	        "Find all interactions deriving from :yref:`NormShearPhys` that cross given plane and sum forces (both normal and shear) on them.\n\n:param "
	        "Vector3 planePt: a point on the plane\n:param Vector3 normal: plane normal (will be normalized).\n");
	py::def("forcesOnCoordPlane", forcesOnCoordPlane);
	py::def("totalForceInVolume",
	        Shop__totalForceInVolume,
	        "Return summed forces on all interactions and average isotropic stiffness, as tuple (Vector3,float)");
	py::def("createInteraction",
	        Shop__createExplicitInteraction,
	        (py::arg("id1"), py::arg("id2"), py::arg("virtualI") = false),
	        "Create interaction between given bodies by hand.\n\nIf *virtualI=False*, current engines are searched for :yref:`IGeomDispatcher` and "
	        ":yref:`IPhysDispatcher` (might be both hidden in :yref:`InteractionLoop`). Geometry is created using ``force`` parameter of the "
	        ":yref:`geometry dispatcher<IGeomDispatcher>`, wherefore the interaction will exist even if bodies do not spatially overlap and the functor "
	        "would return ``false`` under normal circumstances.\n\n If *virtualI=True* the interaction is left in a virtual state.\n\n.. warning:: This "
	        "function will very likely behave incorrectly for periodic simulations (though it could be extended it to handle it farily easily).");
	py::def("spiralProject",
	        spiralProject,
	        (py::arg("pt"),
	         py::arg("dH_dTheta"),
	         py::arg("axis")        = 2,
	         py::arg("periodStart") = std::numeric_limits<Real>::quiet_NaN(),
	         py::arg("theta0")      = 0));
	py::def("pointInsidePolygon", pointInsidePolygon);
	py::def("scalarOnColorScale",
	        Shop::scalarOnColorScale,
	        (py::arg("x"), py::arg("xmin") = 0, py::arg("xmax") = 1),
	        "Map scalar variable to color scale.\n\n:param float x: scalar value which the function applies to.\n:param float xmin: minimum value for the "
	        "color scale, with a return value of (0,0,1) for *x* $\\leq$ *xmin*, i.e. blue color in RGB.\n:param float xmax: maximum value, with a return "
	        "value of (1,0,0) for *x* $\\geq$ *xmax*, i.e. red color in RGB.\n:return: a Vector3 depending on the relative position of *x* on a "
	        "[*xmin*;*xmax*] scale.");
	py::def("highlightNone", highlightNone, "Reset :yref:`highlight<Shape::highlight>` on all bodies.");
	py::def("wireAll", wireAll, "Set :yref:`Shape::wire` on all bodies to True, rendering them with wireframe only.");
	py::def("wireNone", wireNone, "Set :yref:`Shape::wire` on all bodies to False, rendering them as solids.");
	py::def("wireNoSpheres", wireNoSpheres, "Set :yref:`Shape::wire` to True on non-spherical bodies (:yref:`Facets<Facet>`, :yref:`Walls<Wall>`).");
	py::def("flipCell",
	        &Shop::flipCell,
	        "utils.flipCell is deprecated, use :yref:`O.cell.flipCell<Cell::flipCell>` or :yref:`O.cell.flipFlippable<Cell::flipFlippable>`");
	py::def("getViscoelasticFromSpheresInteraction",
	        getViscoelasticFromSpheresInteraction,
	        (py::arg("tc"), py::arg("en"), py::arg("es")),
	        "Attention! The function is deprecated! Compute viscoelastic interaction parameters from analytical solution of a pair spheres collision "
	        "problem:\n\n.. math:: k_n=\\frac{m}{t_c^2}\\left(\\pi^2+(\\ln e_n)^2\\right) \\\\ c_n=-\\frac{2m}{t_c}\\ln e_n \\\\  "
	        "k_t=\\frac{2}{7}\\frac{m}{t_c^2}\\left(\\pi^2+(\\ln e_t)^2\\right) \\\\ c_t=-\\frac{2}{7}\\frac{m}{t_c}\\ln e_t \n\n\nwhere $k_n$, $c_n$ are "
	        "normal elastic and viscous coefficients and $k_t$, $c_t$ shear elastic and viscous coefficients. For details see [Pournin2001]_.\n\n:param "
	        "float m: sphere mass $m$\n:param float tc: collision time $t_c$\n:param float en: normal restitution coefficient $e_n$\n:param float es: "
	        "tangential restitution coefficient $e_s$\n:return: dictionary with keys ``kn`` (the value of $k_n$), ``cn`` ($c_n$), ``kt`` ($k_t$), ``ct`` "
	        "($c_t$).");
	py::def("stressTensorOfPeriodicCell", Shop::getStress, (py::args("volume") = 0), "Deprecated, use utils.getStress instead |ydeprecated|");
	py::def("normalShearStressTensors",
	        Shop__normalShearStressTensors,
	        (py::args("compressionPositive") = false, py::args("splitNormalTensor") = false, py::args("thresholdForce") = NaN),
	        "Compute overall stress tensor of the periodic cell decomposed in 2 parts, one contributed by normal forces, the other by shear forces. The "
	        "formulation can be found in [Thornton2000]_, eq. (3):\n\n.. math:: \\tens{\\sigma}_{ij}=\\frac{2}{V}\\sum R N \\vec{n}_i "
	        "\\vec{n}_j+\\frac{2}{V}\\sum R T \\vec{n}_i\\vec{t}_j\n\nwhere $V$ is the cell volume, $R$ is \"contact radius\" (in our implementation, "
	        "current distance between particle centroids), $\\vec{n}$ is the normal vector, $\\vec{t}$ is a vector perpendicular to $\\vec{n}$, $N$ and "
	        "$T$ are norms of normal and shear forces.\n\n:param bool splitNormalTensor: if true the function returns normal stress tensor split into two "
	        "parts according to the two subnetworks of strong an weak forces.\n\n:param Real thresholdForce: threshold value according to which the normal "
	        "stress tensor can be split (e.g. a zero value would make distinction between tensile and compressive forces).");
	py::def("fabricTensor",
	        Shop__fabricTensor,
	        (py::args("cutoff") = 0.0, py::args("splitTensor") = false, py::args("thresholdForce") = NaN, py::args("extrema") = py::list()),
	        "Computes the fabric tensor $F_{ij}=\\frac{1}{n_c}\\sum_c n_i n_j$ [Satake1982]_ over all interactions $c$ with $n_c$ the total number of "
	        "interactions and $n_i$ and $n_j$ the $i$ and $j$ components of the contact normal, respectively.\n\n:param Real cutoff: intended to "
	        "disregard boundary effects: to define in [0;1] to focus on the interactions located in the centered inner (1-cutoff)^3*$V$ part of the "
	        "spherical packing $V$.\n\n:param bool splitTensor: split the fabric tensor into two parts related to the strong (greatest compressive normal "
	        "forces) and weak contact forces respectively.\n\n:param Real thresholdForce: if the fabric tensor is split into two parts, a threshold value "
	        "can be specified otherwise the mean contact force is considered by default. Use negative signed values for compressive states. To note that "
	        "this value could be set to zero if one wanted to make distinction between compressive and tensile forces.\n\n:param list extrema: defines "
	        "through a two-Vector3-list (min,max) an axis aligned box that restricts the interactions to consider. A value has to be given for the "
	        "function to be effective with non-spherical particles.");
	py::def("bodyStressTensors",
	        Shop__getStressLWForEachBody,
	        // FIXME: use R"""(raw text)""" here, like exaplained in https://yade-dem.org/doc/prog.html#sphinx-documentation and used in py/_libVersions.cpp
	        "Compute and return a table with per-particle stress tensors. Each tensor represents the average stress in one particle, obtained from the "
	        "contour integral of applied load as detailed below. This definition is considering each sphere as a continuum. It can be considered exact in "
	        "the context of spheres at static equilibrium, interacting at contact points with negligible volume changes of the solid phase (this last "
	        "assumption is not restricting possible deformations and volume changes at the packing scale).\n\nProof: \n\nFirst, we remark the identity:  "
	        "$\\sigma_{ij}=\\delta_{ik}\\sigma_{kj}=x_{i,k}\\sigma_{kj}=(x_{i}\\sigma_{kj})_{,k}-x_{i}\\sigma_{kj,k}$.\n\nAt equilibrium, the divergence "
	        "of stress is null: $\\sigma_{kj,k}=\\vec{0}$. Consequently, after divergence theorem: $\\frac{1}{V}\\int_V \\sigma_{ij}dV = "
	        "\\frac{1}{V}\\int_V (x_{i}\\sigma_{kj})_{,k}dV = \\frac{1}{V}\\int_{\\partial V}x_i\\sigma_{kj}n_kdS = \\frac{1}{V}\\sum_bx_i^bf_j^b$.\n\nThe "
	        "last equality is implicitely based on the representation of external loads as Dirac distributions whose zeros are the so-called *contact "
	        "points*: 0-sized surfaces on which the *contact forces* are applied, located at $x_i$ in the deformed configuration.\n\nA weighted average of "
	        "per-body stresses will give the average stress inside the solid phase. There is a simple relation between the stress inside the solid phase "
	        "and the stress in an equivalent continuum in the absence of fluid pressure. For porosity $n$, the relation reads: "
	        "$\\sigma_{ij}^{equ.}=(1-n)\\sigma_{ij}^{solid}$.\n\nThis last relation may not be very useful if porosity is not homogeneous. If it happens, "
	        "one can define the equivalent bulk stress a the particles scale by assigning a volume to each particle. This volume can be obtained from "
	        ":yref:`TesselationWrapper` (see e.g. [Catalano2014a]_)");
	py::def("getDynamicStress",
	        Shop::getDynamicStress,
	        "Compute the dynamic stress tensor for each body: $\\sigma^p_D= - \\frac{1}{V^p} m^p u'^p \\otimes u'^p$");
	py::def("getTotalDynamicStress",
	        Shop::getTotalDynamicStress,
	        (py::args("volume") = 0),
	        "Compute the total dynamic stress tensor : $\\sigma_D= - \\frac{1}{V}\\sum_p m^p u'^p \\otimes u'^p$. The volume have to be provided for "
	        "non-periodic simulations. It is computed from cell volume for periodic simulations.");
	py::def("getStress",
	        Shop::getStress,
	        (py::args("volume") = 0),
	        "Compute and return Love-Weber stress tensor:\n\n $\\sigma_{ij}=\\frac{1}{V}\\sum_b f_i^b l_j^b$, where the sum is over all interactions, with "
	        "$f$ the contact force and $l$ the branch vector (joining centers of the bodies). Stress is negativ for repulsive contact forces, i.e. "
	        "compression. $V$ can be passed to the function. If it is not, it will be equal to the volume of the cell in periodic cases, or to the one "
	        "deduced from utils.aabbDim() in non-periodic cases.");
	py::def("getStressProfile",
	        Shop::getStressProfile,
	        (py::args("volume"),
	         py::args("nCell"),
	         py::args("dz"),
	         py::args("zRef"),
	         py::args("vPartAverageX"),
	         py::args("vPartAverageY"),
	         py::args("vPartAverageZ")),
	        // FIXME: use R"""(raw text)""" here, like exaplained in https://yade-dem.org/doc/prog.html#sphinx-documentation and used in py/_libVersions.cpp
	        "Compute and return the stress tensor depth profile, including the contribution from Love-Weber stress tensor and the dynamic stress tensor "
	        "taking into account the effect of particles inertia. For each defined cell z, the stress tensor reads: \n\n $\\sigma_{ij}^z= "
	        "\\frac{1}{V}\\sum_c f_i^c l_j^{c,z} - \\frac{1}{V}\\sum_p m^p u'^p_i u'^p_j$,\n\nwhere the first sum is made over the contacts which are "
	        "contained or cross the cell z, f^c is the contact force from particle 1 to particle 2, and l^{c,z} is the part of the branch vector from "
	        "particle 2 to particle 1, contained in the cell. The second sum is made over the particles, and u'^p is the velocity fluctuations of the "
	        "particle p with respect to the spatial averaged particle velocity at this point (given as input parameters). The expression of the stress "
	        "tensor is the same as the one given in getStress plus the inertial contribution. Apart from that, the main difference with getStress stands "
	        "in the fact that it gives a depth profile of stress tensor, i.e. from the reference horizontal plane at elevation zRef (input parameter) "
	        "until the plane of elevation zRef+nCell*dz (input parameters), it is computing the stress tensor for each cell of height dz. For the "
	        "love-Weber stress contribution, the branch vector taken into account in the calculations is only the part of the branch vector contained in "
	        "the cell considered.\nTo validate the formulation, it has been checked that activating only the Love-Weber stress tensor, and suming all the "
	        "contributions at the different altitude, we recover the same stress tensor as when using getStress. For my own use, I have troubles with "
	        "strong overlap between fixed object, so that I made a condition to exclude the contribution to the stress tensor of the fixed objects, this "
	        "can be desactivated easily if needed (and should be desactivated for the comparison with getStress).");
	py::def("getStressProfile_contact",
	        Shop::getStressProfile_contact,
	        (py::args("volume"), py::args("nCell"), py::args("dz"), py::args("zRef")),
	        "same as getStressProfile, only contact contribution.");
	py::def("getDepthProfiles",
	        Shop::getDepthProfiles,
	        (py::args("volume"),
	         py::args("nCell"),
	         py::args("dz"),
	         py::args("zRef"),
	         py::args("activateCond") = false,
	         py::args("radiusPy")     = 0,
	         py::args("direction")    = 2),
	        "Compute and return the particle velocity and solid volume fraction (porosity) depth profile along the direction specified (default is z; "
	        "0=>x,1=>y,2=>z). For each defined cell z, the k component of the average particle velocity reads: \n\n $<v_k>^z= \\sum_p V^p v_k^p/\\sum_p "
	        "V^p$,\n\nwhere the sum is made over the particles contained in the cell, $v_k^p$ is the k component of the velocity associated to particle p, "
	        "and $V^p$ is the part of the volume of the particle p contained inside the cell. This definition allows to smooth the averaging, and is "
	        "equivalent to taking into account the center of the particles only when there is a lot of particles in each cell. As for the solid volume "
	        "fraction, it is evaluated in the same way:  for each defined cell z, it reads: \n\n$<\\phi>^z= \\frac{1}{V_{cell}}\\sum_p V^p$, where "
	        "$V_{cell}$ is the volume of the cell considered, and $V^p$ is the volume of particle p contained in cell z.\n This function gives depth "
	        "profiles of average velocity and solid volume fraction, returning the average quantities in each cell of height dz, from the reference "
	        "horizontal plane at elevation zRef (input parameter) until the plane of elevation zRef+nCell*dz (input parameters). If the argument "
	        "activateCond is set to true, do the average only on particles of radius equal to radiusPy (input parameter)");
	py::def("getDepthProfiles_center",
	        Shop::getDepthProfiles_center,
	        (py::args("volume"), py::args("nCell"), py::args("dz"), py::args("zRef"), py::args("activateCond") = false, py::args("radiusPy") = 0),
	        "Same as getDepthProfiles but taking into account particles as points located at the particle center.");
	py::def("getSlicedProfiles",
	        Shop::getSlicedProfiles,
	        (py::args("vCell"),
	         py::args("nCell"),
	         py::args("dP"),
	         py::args("sliceCenters"),
	         py::args("sliceWidths"),
	         py::args("refP"),
	         py::args("refS"),
	         py::args("dirP")         = 2,
	         py::args("dirS")         = 1,
	         py::args("activateCond") = false,
	         py::args("radiusPy")     = 0,
	         py::args("nSimpson")     = 50),
	        "Compute and return the particle solid volume fraction (porosity) and velocity profiles along a specific direction dirP and for a given "
	        "subdomain. "
	        "In the direction dirP, the subdomain is divided into nCell of size dP. For each cell, the averaged solid volume fraction reads: "
	        "\n\n$<\\phi>= \\frac{1}{V_{cell}}\\sum_p V^p$\n\n and the averaged particle velocity reads: \n\n $<v_k>= \\sum_p V^p v_k^p/\\sum_p V^p$,\n\n "
	        "where the sum is made over the particles p contained in the cell, $v_k^p$ is the k component of the velocity associated to particle p, $V^p$ "
	        "is "
	        "the part of the volume of the particle p contained inside the cell, and $V_{cell}$ is the volume of the cell (all subdomain slices combined). "
	        "The volume of the sliced particle $V^p$ is computed analytically when the particle is not sliced by the subdomain boundaries. Otherwise, "
	        "$V^p$ is computed using a Simpson integration of the sliced area of the sliced sphere.\n"
	        "This function allows to define a discontinuous subdomain made of different slices in direction dirS. This can be useful to exclude specific "
	        "zones "
	        "from the averaging procedure or to target similar zones like symmetric boundaries.\n\n"
	        "Arguments are:\n"
	        "vCell : volume of a cell, all slices combined. (e.g. dP*length*(slicewidth1+slicewidth2))\n"
	        "nCell : number of cells in the profile direction\n"
	        "dP : discretisation interval in the Profile direction,\n"
	        "sliceCenters : array containing the position of the center of each slice from refS in the S direction,\n"
	        "sliceWidths : array containing the width of each slice,\n"
	        "refP : reference position in the Profile direction,\n"
	        "refS : reference position in the slice direction,\n"
	        "dirP : direction of the profile (0:x, 1:y, 2:z), (default:2),\n"
	        "dirS : direction of the slices (0:x, 1:y, 2:z), must be different from dirP, (default:1),\n"
	        "activateCond : if true, will only consider particle of radius equal radiusPy, (default:false),\n"
	        "nSimpson : number of intervals per particle radius for the Simpson integration, (default:50),\n");
	py::def("getCapillaryStress",
	        Shop::getCapillaryStress,
	        (py::args("volume") = 0, py::args("mindlin") = false),
	        "Compute and return Love-Weber capillary stress tensor:\n\n $\\sigma^{cap}_{ij}=\\frac{1}{V}\\sum_b l_i^b f^{cap,b}_j$, where the sum is over "
	        "all interactions, with $l$ the branch vector (joining centers of the bodies) and $f^{cap}$ is the capillary force. $V$ can be passed to the "
	        "function. If it is not, it will be equal to one in non-periodic cases, or equal to the volume of the cell in periodic cases. Only the "
	        "CapillaryPhys interaction type is supported presently. Using this function with physics MindlinCapillaryPhys needs to pass True as second "
	        "argument.");
	py::def("getBodyIdsContacts", Shop__getBodyIdsContacts, (py::args("bodyID") = 0), "Get a list of body-ids, which contacts the given body.");
	py::def("maxOverlapRatio",
	        maxOverlapRatio,
	        "Return maximum overlap ration in interactions (with :yref:`ScGeom`) of two :yref:`spheres<Sphere>`. The ratio is computed as "
	        "$\\frac{u_N}{2(r_1 r_2)/r_1+r_2}$, where $u_N$ is the current overlap distance and $r_1$, $r_2$ are radii of the two spheres in contact.");
	py::def("shiftBodies", shiftBodies, (py::arg("ids"), py::arg("shift")), "Shifts bodies listed in ids without updating their velocities.");
	py::def("calm",
	        Shop__calm,
	        (py::arg("mask") = -1),
	        "Set translational and rotational velocities of bodies to zero. Applied to all :yref:`dynamic<Body.dynamic>` bodies by default. To calm only "
	        "some of them, use mask parameter, it will calm only dynamic bodies with groupMask compatible to given value");
	py::def("setNewVerticesOfFacet",
	        setNewVerticesOfFacet,
	        (py::arg("b"), py::arg("v1"), py::arg("v2"), py::arg("v3")),
	        "Sets new vertices (in global coordinates) to given facet.");
	py::def("setContactFriction",
	        Shop::setContactFriction,
	        py::arg("angleRad"),
	        "Modify the friction angle (in radians) inside the material classes and existing contacts. The friction for non-dynamic bodies is not "
	        "modified.");
	py::def("growParticles",
	        Shop::growParticles,
	        (py::args("multiplier"), py::args("updateMass") = true, py::args("dynamicOnly") = true),
	        "Change the size of spheres and clumps of spheres by the multiplier. If updateMass=True, then the mass and inertia are updated. "
	        "dynamicOnly=True will select dynamic bodies.");
	py::def("growParticle",
	        Shop::growParticle,
	        (py::args("bodyID"), py::args("multiplier"), py::args("updateMass") = true),
	        "Change the size of a single sphere (to be implemented: single clump). If updateMass=True, then the mass is updated.");
	py::def("intrsOfEachBody", intrsOfEachBody, "returns list of lists of interactions of each body");
	py::def("numIntrsOfEachBody", numIntrsOfEachBody, "returns list of number of interactions of each body");
	py::def("TetrahedronSignedVolume", static_cast<Real (*)(const vector<Vector3r>&)>(&TetrahedronSignedVolume), "TODO");
	py::def("TetrahedronVolume", static_cast<Real (*)(const vector<Vector3r>&)>(&TetrahedronVolume), "TODO");
	py::def("TetrahedronInertiaTensor", TetrahedronInertiaTensor, "TODO");
	py::def("TetrahedronCentralInertiaTensor", TetrahedronCentralInertiaTensor, "TODO");
	py::def("TetrahedronWithLocalAxesPrincipal", TetrahedronWithLocalAxesPrincipal, "TODO");
	py::def("momentum", Shop::momentum, "Returns total linear momentum of the simulation");
	py::def("angularMomentum", Shop::angularMomentum, (py::args("origin") = Vector3r(Vector3r::Zero())), "Returns total angularMomentum of the simulation, about input point *origin*");
	py::def("getSpheresVolume2D",
	        Shop__getSpheresVolume2D,
	        (py::arg("mask") = -1),
	        "Compute the total volume of discs in the simulation, mask parameter is considered");
	py::def("voidratio2D",
	        Shop__getVoidRatio2D,
	        (py::arg("zlen") = 1),
	        "Compute 2D packing void ratio $\\frac{V-V_s}{V_s}$ where $V$ is overall volume and $V_s$ is volume of disks.\n\n:param float zlen: length in "
	        "the third direction.\n");
	py::def("getStressAndTangent",
	        Shop__getStressAndTangent,
	        (py::args("volume") = 0, py::args("symmetry") = true),
	        "Compute overall stress of periodic cell using the same equation as function getStress. In addition, the tangent operator is calculated using "
	        "the equation published in [Kruyt and Rothenburg1998]_:\n\n $S_{ijkl}=\\frac{1}{V}\\sum_{c}(k_n n_i l_j n_k l_l + k_t t_i l_j t_k l_l)$"
	        "\n\n Although the above formula gives the elements of the fourth-order stiffness tensor, this tangent operator will be returned in Voigt "
	        "notation, giving a 6 by 6 tensor, where elements of $S_{ijkl}$ mapping to the same Voigt element will be averaged.\n\n:param float volume:"
	        " same as in function getStress\n:param bool symmetry: make the tensors symmetric.\n\n:return: macroscopic stress tensor and tangent operator"
	        " as py::tuple");
	py::def("setBodyPosition",
	        setBodyPosition,
	        (py::args("id"), py::args("pos"), py::args("axis") = "xyz"),
	        "Set a body position from its id and a new vector3r.\n\n:param int id: the body id.\n:param Vector3 pos: the desired updated position.\n:param "
	        "str axis: the axis along which the position has to be updated (ex: if axis==\"xy\" and pos==Vector3r(r0,r1,r2), r2 will be ignored and the "
	        "position along z will not be updated).");
	py::def("setBodyVelocity",
	        setBodyVelocity,
	        (py::args("id"), py::args("vel"), py::args("axis") = "xyz"),
	        "Set a body velocity from its id and a new vector3r.\n\n:param int id: the body id.\n:param Vector3 vel: the desired updated velocity.\n:param "
	        "str axis: the axis along which the velocity has to be updated (ex: if axis==\"xy\" and vel==Vector3r(r0,r1,r2), r2 will be ignored and the "
	        "velocity along z will not be updated).");
	py::def("setBodyOrientation",
	        setBodyOrientation,
	        (py::args("id"), py::args("ori")),
	        "Set a body orientation from its id and a new Quaternionr.\n\n:param int id: the body id.\n:param Quaternion ori: the desired updated "
	        "orientation.");
	py::def("setBodyAngularVelocity",
	        setBodyAngularVelocity,
	        (py::args("id"), py::args("angVel")),
	        "Set a body angular velocity from its id and a new Vector3r.\n\n:param int id: the body id.\n:param Vector3 angVel: the desired updated "
	        "angular velocity.");
	py::def("setBodyColor",
	        setBodyColor,
	        (py::args("id"), py::args("color")),
	        "Set a body color from its id and a new Vector3r.\n\n:param int id: the body id.\n:param Vector3 color: the desired updated color.");
#ifdef YADE_LS_DEM
	// clang-format off
	py::def("cart2spher",
		ShopLS::cart2spher,
		(py::arg("vec")),
		"Converts cartesian coordinates to spherical ones.\n\n:param Vector3 vec: the $(x,y,z)$ cartesian coordinates\n:return: a $(r,\\theta,\\phi)$ Vector3 of spherical coordinates, with $\\theta = (\\vec{e_z},\\vec{e_r}) \\in [0;\\pi]$ and $\\phi \\in [0;2 \\pi]$ measured in $(x,y)$ plane from $x$-axis");
	py::def("spher2cart",
		ShopLS::spher2cart,
		(py::arg("vec")),
		"Converts spherical coordinates to cartesian ones.\n\n:param Vector3 vec: the $(r,\\theta,\\phi)$ spherical coordinates, see cart2spher function for conventions\n:return: a $(x,y,z)$ Vector3 of cartesian coordinates");
	py::def("lsSimpleShape",
		ShopLS::lsSimpleShape,
		(py::arg("shape"),py::arg("aabb"),py::arg("step")=0.1,py::arg("smearCoeff")=1.5,py::arg("epsilons")=Vector2r(Vector2r::Zero()),py::arg("clump")=boost::make_shared<Clump>()), // Vector*r(Vector*r::Zero()) is actually necessary for to-Python conversion to work (and YADE to start)
		R"""(Creates a LevelSet shape among pre-defined ones. Not intended to be used directly, see levelSetBody() instead.

:param int shape: a shape index among supported choices
:param AlignedBox3 aabb: the axis-aligned surrounding box of the body
:param Real step: the LevelSet grid step size
:param Real smearCoeff: passed to LevelSet.smearCoeff
:param Vector2 epsilons: the epsilon exponents in case *shape* = 3 (superellipsoid)
:param Clump clump: the Clump instance to mimick in case *shape* = 4
:return: a LevelSet instance.)""");
 // NB: the rest of the code makes a mixed use of py::arg and py::args, Boost doc does not really help for this https://www.boost.org/doc/libs/1_67_0/libs/python/doc/html/reference/function_invocation_and_creation.html#function_invocation_and_creation.boost_python_args_hpp, see /usr/include/boost/python/args.hpp if you wish...
//	NB for the two following nGP: see overloading comments in ShopLS.hpp.
	py::def("nGP",
		ShopLS::nGPr,
		(py::arg("min"),py::arg("max"),py::arg("step")),
		"Defines how many gridpoints are necessary to go from *min* to (at least) *max*, by *step* increments, eg when constructing a :yref:`RegularGrid`\n\n:param Real min: lowest grid extremity as (min,min,min) in case you just give a number, or as (min[0],min[1],min[2]) in case you give a tuple/list/Vector3\n\n:param Real max: highest gridpoint as (max,max,max) in case you give a number, or as (max[0],max[1],max[2]) in case you give a tuple/list/Vector3. The actual highest point of the grid may be beyond max by something like *step*.\n:param Real step: the distance between two consecutive grid points (the same along each axis).\n\n:return: either an integer, or a Vector3 of, depending on usage"); // html doc output is bound to be less good than elsewhere (for the "Parameters" block), because of the same name below..
	py::def("nGP",
		ShopLS::nGPv,
		(py::arg("min"),py::arg("max"),py::arg("step")),
		"Type-overloaded version of the above, to allow for both types of max/min attributes.");
	py::def("distApproxSE",
		ShopLS::distApproxSE,
		(py::arg("pt"),py::arg("radii"),py::arg("epsilons")),
		R"""(An approximate distance value to a superellipsoid surface defined (in local axes) as $f(x,y,z) = ( |x/r_x|^{2/\epsilon_e} + |y/r_y|^{2/\epsilon_e} )^{\epsilon_e/\epsilon_n} + |z/r_z|^{2/\epsilon_n} = 1$.

:param Vector3 pt: the $(x,y,z)$ of interest
:param Vector3 radii: the $(r_x,r_y,r_z)$
:param Vector2 epsilons: the $(\epsilon_e,\epsilon_n)$ exponents
:return: a 0-approximation distance value.)""");
	py::def("distIniClump",
		ShopLS::distIniClump,
		(py::arg("clump"),py::arg("grid")),
		R"""(An appropriate discrete field to serve as a Fast Marching Method input in :yref:`FastMarchingMethod.phiIni` in order to compute distance to a clump.

:param Clump clump: considered clump instance
:param RegularGrid grid: the :yref:`RegularGrid` instance to consider
:return: an appropriate 3D discrete field for :yref:`FastMarchingMethod.phiIni`.)""");
	py::def("distIniSE",
		ShopLS::distIniSE,
		(py::arg("radii"),py::arg("epsilons"),py::arg("grid")),
		R"""(An appropriate discrete field to serve as a Fast Marching Method input in :yref:`FastMarchingMethod.phiIni` in order to compute distance to a superellipsoid.

:param Vector3 radii: the $(r_x,r_y,r_z)$
:param Vector2 epsilons: the $(\epsilon_e,\epsilon_n)$ exponents
:param RegularGrid grid: the :yref:`RegularGrid` instance to consider
:return: an appropriate 3D discrete field for :yref:`FastMarchingMethod.phiIni`.)""");
	py::def("distApproxRose",
		ShopLS::distApproxRose,
		(py::arg("pt")),
		R"""(An approximate distance value to a rose-like flaky surface defined in spherical coordinates as $r = 3+1.5 \\sin(5 \\theta) \\sin(4\\phi)$ (see :yref:`cart2spher` function for spherical <-> cartesian convention).

:param Vector3 pt: the pt of interest given in $(x,y,z)$ cartesian coordinates.
:return: a 0-approximation distance value.)""");
	py::def("phiIniCppPy",ShopLS::phiIniCppPy,(py::arg("grid")), // jduriez personal note: see fmm8 for comparison with phiIniPy
		R"""(A possibly handy function to construct a :yref:`FastMarchingMethod.phiIni` after applying on *grid* an inside-outside Python function. The latter necessarily names ioFn and takes three floating numbers (cartesian coordinates) as arguments. Code source of the present function is both C++ and Python, and execution should be faster and heavier in memory than the pure Python version utils.phiIniPy, both being under the second and few MB for grids with ~ $10^4$ gridpoints.

:param RegularGrid grid: the yref:`RegularGrid` instance to operate on with a preexisting ioFn Python function
:return: an appropriate 3D discrete field for :yref:`FastMarchingMethod.phiIni`)""");
	py::def("insideClump",ShopLS::insideClump,(py::arg("pt"),py::arg("clump")),
		R"""(Tells whether some point is inside or outside a clump

:param Vector3 pt: the point of interest expressed in local coordinates
:param Clump clump: the clump instance to consider
:return bool: True when strictly inside, False otherwise)""");
	// clang-format on
#endif //YADE_LS_DEM
} catch (...) {
	LOG_FATAL("Importing this module caused an exception and this module is in an inconsistent state now.");
	PyErr_Print();
	PyErr_SetString(PyExc_SystemError, __FILE__);
	boost::python::handle_exception();
	throw;
}