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;
}
|