step2torque 0.1.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- step2torque/__init__.py +28 -0
- step2torque/_assembly.cp313-win_amd64.pyd +0 -0
- step2torque/_assembly.cpp +161 -0
- step2torque/_assembly_version.py +1 -0
- step2torque/_contact.py +289 -0
- step2torque/_cuda.py +110 -0
- step2torque/_currents.py +98 -0
- step2torque/_field.py +263 -0
- step2torque/_kernel.py +117 -0
- step2torque/_mesh.py +435 -0
- step2torque/_motion.py +76 -0
- step2torque/_parallel.hpp +59 -0
- step2torque/_results.py +68 -0
- step2torque/_source.py +175 -0
- step2torque/_torque.py +402 -0
- step2torque/_worker.py +345 -0
- step2torque/materials/1j22.csv +8 -0
- step2torque/materials/__init__.py +51 -0
- step2torque/materials/dt4.csv +14 -0
- step2torque/materials/kede_1j21.csv +11 -0
- step2torque/materials/rotor.csv +14 -0
- step2torque/materials/sources.json +15 -0
- step2torque/model.py +271 -0
- step2torque/solution.py +307 -0
- step2torque/visualization/__init__.py +6 -0
- step2torque/visualization/__main__.py +64 -0
- step2torque/visualization/appearance.py +52 -0
- step2torque/visualization/archive.py +177 -0
- step2torque/visualization/html.py +43 -0
- step2torque/visualization/lines.py +140 -0
- step2torque/visualization/native.py +325 -0
- step2torque/visualization/packed.py +122 -0
- step2torque/visualization/plane.py +74 -0
- step2torque/visualization/setup.py +290 -0
- step2torque/visualization/viewer.html +42 -0
- step2torque/visualization/viewer.js +3915 -0
- step2torque/visualization/viewer.py +355 -0
- step2torque-0.1.0.dist-info/DELVEWHEEL +2 -0
- step2torque-0.1.0.dist-info/METADATA +87 -0
- step2torque-0.1.0.dist-info/RECORD +45 -0
- step2torque-0.1.0.dist-info/WHEEL +5 -0
- step2torque-0.1.0.dist-info/entry_points.txt +2 -0
- step2torque-0.1.0.dist-info/licenses/LICENSE +22 -0
- step2torque-0.1.0.dist-info/top_level.txt +1 -0
- step2torque.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
step2torque/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Load a STEP, configure its components, and solve without blocking.
|
|
2
|
+
|
|
3
|
+
motor = Motor("motor.step")
|
|
4
|
+
motor.components["coil"].make_coil(phase=1, turns=72, direction="+z")
|
|
5
|
+
motor.components["magnet"].make_magnet(direction="radial")
|
|
6
|
+
solution = motor.solve(current="optimized", power=5.5)
|
|
7
|
+
solution.wait()
|
|
8
|
+
print(solution.torque) # N m
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# start delvewheel patch
|
|
13
|
+
def _delvewheel_patch_1_13_2():
|
|
14
|
+
import os
|
|
15
|
+
if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'step2torque.libs'))):
|
|
16
|
+
os.add_dll_directory(libs_dir)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_delvewheel_patch_1_13_2()
|
|
20
|
+
del _delvewheel_patch_1_13_2
|
|
21
|
+
# end delvewheel patch
|
|
22
|
+
|
|
23
|
+
from .model import Motor
|
|
24
|
+
from .solution import Solution, solve_position
|
|
25
|
+
from .visualization import Plane, Viewer
|
|
26
|
+
|
|
27
|
+
__all__ = ["Motor", "Solution", "solve_position", "Plane", "Viewer"]
|
|
28
|
+
__version__ = "0.1.0"
|
|
Binary file
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Same affine P2, degree-6 iron quadrature and B(H) energy as _field.py.
|
|
2
|
+
#include <pybind11/pybind11.h>
|
|
3
|
+
#include <pybind11/numpy.h>
|
|
4
|
+
#include "_parallel.hpp"
|
|
5
|
+
#include <array>
|
|
6
|
+
#include <cmath>
|
|
7
|
+
#include <cstdint>
|
|
8
|
+
#include <stdexcept>
|
|
9
|
+
namespace py = pybind11;
|
|
10
|
+
using Real = py::array_t<double, py::array::c_style>;
|
|
11
|
+
using Int = py::array_t<int, py::array::c_style | py::array::forcecast>;
|
|
12
|
+
using Index = py::array_t<int64_t, py::array::c_style | py::array::forcecast>;
|
|
13
|
+
struct Curve {
|
|
14
|
+
std::vector<double> h,b,slope,area;
|
|
15
|
+
std::array<double,3> evaluate(double value) const {
|
|
16
|
+
int i=std::upper_bound(h.begin(),h.end(),value)-h.begin()-1;
|
|
17
|
+
i=std::max(0,std::min(i,int(h.size())-2));
|
|
18
|
+
double t=value-h[i];
|
|
19
|
+
return {b[i]+t*slope[i],slope[i],area[i]+t*(b[i]+.5*t*slope[i])};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
class Assembly {
|
|
23
|
+
int n,ne,nq;
|
|
24
|
+
int64_t nnz;
|
|
25
|
+
std::vector<int64_t> ptr,entries,cell_ptr,cell_entries;
|
|
26
|
+
std::vector<int> col,dofs,curve;
|
|
27
|
+
std::vector<double> inverse,volume,reference,weights,points,source;
|
|
28
|
+
std::vector<double> local_matrix,local_load,local_energy;
|
|
29
|
+
std::vector<Curve> curves;
|
|
30
|
+
Parallel pool;
|
|
31
|
+
const double *potential=nullptr;
|
|
32
|
+
double *matrix=nullptr,*residual=nullptr;
|
|
33
|
+
double factor=1,floor=.1;
|
|
34
|
+
public:
|
|
35
|
+
Assembly(Index pointers,Int columns,Int indices,Int labels,Real jac,
|
|
36
|
+
Real volumes,Real shapes,Real quadrature,Real locations,
|
|
37
|
+
py::list laws,int threads)
|
|
38
|
+
: n(pointers.size()-1),ne(labels.size()),nq(quadrature.size()),nnz(columns.size()),
|
|
39
|
+
ptr(pointers.data(),pointers.data()+pointers.size()),
|
|
40
|
+
col(columns.data(),columns.data()+columns.size()),
|
|
41
|
+
dofs(indices.data(),indices.data()+indices.size()),
|
|
42
|
+
curve(labels.data(),labels.data()+labels.size()),
|
|
43
|
+
inverse(jac.data(),jac.data()+jac.size()),
|
|
44
|
+
volume(volumes.data(),volumes.data()+volumes.size()),
|
|
45
|
+
reference(shapes.data(),shapes.data()+shapes.size()),
|
|
46
|
+
weights(quadrature.data(),quadrature.data()+quadrature.size()),
|
|
47
|
+
points(locations.data(),locations.data()+locations.size()),
|
|
48
|
+
source(size_t(ne)*12),local_matrix(size_t(ne)*55),local_load(size_t(ne)*10),
|
|
49
|
+
local_energy(ne),pool(threads) {
|
|
50
|
+
if(indices.size()!=int64_t(ne)*10 || shapes.size()!=nq*30 || jac.size()!=int64_t(ne)*9)
|
|
51
|
+
throw std::runtime_error("Compiled assembly requires the affine P2 tetrahedron map");
|
|
52
|
+
for(auto record:laws) {
|
|
53
|
+
py::tuple pair=py::cast<py::tuple>(record);
|
|
54
|
+
Real h=py::cast<Real>(pair[0]),b=py::cast<Real>(pair[1]);
|
|
55
|
+
Curve c;
|
|
56
|
+
c.h.assign(h.data(),h.data()+h.size());c.b.assign(b.data(),b.data()+b.size());
|
|
57
|
+
c.slope.resize(h.size()-1);c.area.assign(h.size(),0);
|
|
58
|
+
for(int i=0;i<h.size()-1;++i) {
|
|
59
|
+
c.slope[i]=(c.b[i+1]-c.b[i])/(c.h[i+1]-c.h[i]);
|
|
60
|
+
c.area[i+1]=c.area[i]+.5*(c.b[i]+c.b[i+1])*(c.h[i+1]-c.h[i]);
|
|
61
|
+
}
|
|
62
|
+
curves.push_back(std::move(c));
|
|
63
|
+
}
|
|
64
|
+
entries.resize(size_t(ne)*100);cell_ptr.assign(n+1,0);cell_entries.resize(size_t(ne)*10);
|
|
65
|
+
for(int row:dofs) {
|
|
66
|
+
if(row<0 || row>=n)throw std::runtime_error("Invalid P2 degree of freedom");
|
|
67
|
+
++cell_ptr[row+1];
|
|
68
|
+
}
|
|
69
|
+
for(int i=1;i<=n;++i)cell_ptr[i]+=cell_ptr[i-1];
|
|
70
|
+
auto cursor=cell_ptr;
|
|
71
|
+
for(int64_t i=0;i<int64_t(ne)*10;++i)cell_entries[cursor[dofs[i]]++]=i;
|
|
72
|
+
for(int e=0;e<ne;++e)for(int i=0;i<10;++i)for(int j=0;j<10;++j) {
|
|
73
|
+
int row=dofs[e*10+i],column=dofs[e*10+j];
|
|
74
|
+
auto it=std::lower_bound(col.begin()+ptr[row],col.begin()+ptr[row+1],column);
|
|
75
|
+
if(it==col.begin()+ptr[row+1] || *it!=column)
|
|
76
|
+
throw std::runtime_error("Tangent graph differs from finite element map");
|
|
77
|
+
entries[int64_t(e)*100+i*10+j]=it-col.begin();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
void set_source(Real values) {
|
|
81
|
+
if(values.size()!=int64_t(ne)*12)throw std::runtime_error("Winding source differs from mesh");
|
|
82
|
+
std::copy(values.data(),values.data()+values.size(),source.begin());
|
|
83
|
+
}
|
|
84
|
+
void assemble(Real phi,double scale,double lower,Real values,Real load) {
|
|
85
|
+
potential=phi.data();factor=scale;floor=lower;
|
|
86
|
+
if(phi.size()!=n || values.size()!=nnz || load.size()!=n)
|
|
87
|
+
throw std::runtime_error("Tangent arrays differ from finite element map");
|
|
88
|
+
matrix=values.mutable_data();residual=load.mutable_data();
|
|
89
|
+
py::gil_scoped_release release;
|
|
90
|
+
integrate(true);
|
|
91
|
+
}
|
|
92
|
+
double energy(Real phi,double scale) {
|
|
93
|
+
potential=phi.data();factor=scale;
|
|
94
|
+
py::gil_scoped_release release;
|
|
95
|
+
return integrate(false);
|
|
96
|
+
}
|
|
97
|
+
private:
|
|
98
|
+
double integrate(bool assemble) {
|
|
99
|
+
if(assemble) {std::fill(matrix,matrix+nnz,0);std::fill(residual,residual+n,0);}
|
|
100
|
+
pool.run(ne,[&](int begin,int end,int){for(int e=begin;e<end;++e) {
|
|
101
|
+
const double *jac=inverse.data()+9*e,*hs=source.data()+12*e;
|
|
102
|
+
const int *ids=dofs.data()+10*e;
|
|
103
|
+
const Curve &law=curves[curve[e]];
|
|
104
|
+
double local[55]={},load[10]={},cell_energy=0;
|
|
105
|
+
double offset=law.evaluate(.0001)[2];
|
|
106
|
+
for(int q=0;q<nq;++q) {
|
|
107
|
+
const double *ref=reference.data()+30*q,*point=points.data()+3*q;
|
|
108
|
+
double gradient[10][3],h[3],s[3];
|
|
109
|
+
for(int c=0;c<3;++c) {
|
|
110
|
+
s[c]=factor*(hs[c]+point[0]*(hs[3+c]-hs[c])+point[1]*(hs[6+c]-hs[c])+point[2]*(hs[9+c]-hs[c]));
|
|
111
|
+
h[c]=s[c];
|
|
112
|
+
}
|
|
113
|
+
for(int i=0;i<10;++i) for(int c=0;c<3;++c) {
|
|
114
|
+
gradient[i][c]=ref[3*i]*jac[c]+ref[3*i+1]*jac[3+c]+ref[3*i+2]*jac[6+c];
|
|
115
|
+
h[c]-=potential[ids[i]]*gradient[i][c];
|
|
116
|
+
}
|
|
117
|
+
double norm2=h[0]*h[0]+h[1]*h[1]+h[2]*h[2]+1e-8;
|
|
118
|
+
double hn=std::sqrt(norm2),w=volume[e]*weights[q];
|
|
119
|
+
auto bh=law.evaluate(hn);
|
|
120
|
+
constexpr double mu0=1.2566370614359173e-6;
|
|
121
|
+
cell_energy+=w*(bh[2]-offset-.5*mu0*(s[0]*s[0]+s[1]*s[1]+s[2]*s[2]));
|
|
122
|
+
if(!assemble) continue;
|
|
123
|
+
double secant=bh[0]/hn;
|
|
124
|
+
double anisotropic=(std::max(bh[1],floor*secant)-secant)/norm2;
|
|
125
|
+
double hg[10];
|
|
126
|
+
for(int i=0;i<10;++i) {
|
|
127
|
+
hg[i]=h[0]*gradient[i][0]+h[1]*gradient[i][1]+h[2]*gradient[i][2];
|
|
128
|
+
load[i]+=w*secant*hg[i];
|
|
129
|
+
}
|
|
130
|
+
for(int i=0;i<10;++i) for(int j=0;j<=i;++j) {
|
|
131
|
+
double gram=gradient[i][0]*gradient[j][0]+gradient[i][1]*gradient[j][1]+gradient[i][2]*gradient[j][2];
|
|
132
|
+
local[i*(i+1)/2+j]+=w*(secant*gram+anisotropic*hg[i]*hg[j]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
local_energy[e]=cell_energy;
|
|
136
|
+
if(assemble) {
|
|
137
|
+
std::copy(local,local+55,local_matrix.begin()+e*55);
|
|
138
|
+
std::copy(load,load+10,local_load.begin()+e*10);
|
|
139
|
+
}
|
|
140
|
+
}});
|
|
141
|
+
if(assemble)pool.run(n,[&](int begin,int end,int){for(int row=begin;row<end;++row) {
|
|
142
|
+
for(int64_t k=cell_ptr[row];k<cell_ptr[row+1];++k) {
|
|
143
|
+
int64_t entry=cell_entries[k];int e=entry/10,i=entry%10;
|
|
144
|
+
residual[row]+=local_load[entry];
|
|
145
|
+
for(int j=0;j<10;++j) {
|
|
146
|
+
int a=std::max(i,j),b=std::min(i,j);
|
|
147
|
+
matrix[entries[int64_t(e)*100+i*10+j]]+=local_matrix[e*55+a*(a+1)/2+b];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}});
|
|
151
|
+
double energy=0;for(double value:local_energy)energy+=value;
|
|
152
|
+
return energy;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
PYBIND11_MODULE(_assembly,m) {
|
|
156
|
+
py::class_<Assembly>(m,"Assembly")
|
|
157
|
+
.def(py::init<Index,Int,Int,Int,Real,Real,Real,Real,Real,py::list,int>())
|
|
158
|
+
.def("set_source",&Assembly::set_source)
|
|
159
|
+
.def("assemble",&Assembly::assemble)
|
|
160
|
+
.def("energy",&Assembly::energy);
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SHA256 = 'e14b65543e07120fb0dabd8dddab806d8618979dda1f412211678ea15706bd02'
|
step2torque/_contact.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Zero-thickness air-gap contact: B_n = (mu0 / gap) * [phi]."""
|
|
2
|
+
|
|
3
|
+
import ngsolve as ng
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pyngcore as core
|
|
6
|
+
import netgen.meshing as nm
|
|
7
|
+
from scipy.sparse import coo_matrix
|
|
8
|
+
from scipy.sparse.csgraph import connected_components
|
|
9
|
+
|
|
10
|
+
from .materials import MU0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_contact(mesh, space, matrix, contact):
|
|
14
|
+
"""Add the exact P2 surface mass matrix on paired, coincident traces.
|
|
15
|
+
|
|
16
|
+
The mesh is continuous away from the contact. Its physical contact rim
|
|
17
|
+
shares vertex/edge DOFs, so the jump vanishes at the rim. Rotational
|
|
18
|
+
sector cuts retain independent, periodically identified contact traces.
|
|
19
|
+
This is a physical Robin term, with no penalty or stabilization factor.
|
|
20
|
+
"""
|
|
21
|
+
gap = float(contact["gap"])
|
|
22
|
+
if gap <= 0:
|
|
23
|
+
raise ValueError("Contact gap must be positive")
|
|
24
|
+
points = np.asarray(mesh.ngmesh.Coordinates())
|
|
25
|
+
cells = mesh.ngmesh.Elements3D().NumPy()["nodes"] - 1
|
|
26
|
+
rule = ng.IntegrationRule(ng.ET.TRIG, 4)
|
|
27
|
+
q = np.array([p.point[:2] for p in rule])
|
|
28
|
+
weights = np.array([p.weight for p in rule])
|
|
29
|
+
rows, columns, entries = [], [], []
|
|
30
|
+
for face in contact["faces"]:
|
|
31
|
+
triangle = points[face["vertices"]]
|
|
32
|
+
locations = (
|
|
33
|
+
triangle[0]
|
|
34
|
+
+ q[:, :1] * (triangle[1] - triangle[0])
|
|
35
|
+
+ q[:, 1:] * (triangle[2] - triangle[0])
|
|
36
|
+
)
|
|
37
|
+
jacobian = np.linalg.norm(
|
|
38
|
+
np.cross(triangle[1] - triangle[0], triangle[2] - triangle[0])
|
|
39
|
+
)
|
|
40
|
+
traces, dofs = [], []
|
|
41
|
+
for side, sign in (("minus_cell", 1), ("plus_cell", -1)):
|
|
42
|
+
cell = int(face[side])
|
|
43
|
+
element = ng.ElementId(ng.VOL, cell)
|
|
44
|
+
xyz = points[cells[cell]]
|
|
45
|
+
reference = np.linalg.solve((xyz[:3] - xyz[3]).T, (locations - xyz[3]).T).T
|
|
46
|
+
fe = space.GetFE(element)
|
|
47
|
+
shape = np.array([fe.CalcShape(*p) for p in reference])
|
|
48
|
+
shape[abs(shape) < 1e-12] = 0
|
|
49
|
+
traces.append(sign * shape)
|
|
50
|
+
dofs.extend(space.GetDofNrs(element))
|
|
51
|
+
unique, inverse = np.unique(dofs, return_inverse=True)
|
|
52
|
+
jump = np.zeros((len(rule), len(unique)))
|
|
53
|
+
for j, values in enumerate(np.concatenate(traces, axis=1).T):
|
|
54
|
+
jump[:, inverse[j]] += values
|
|
55
|
+
local = MU0 / gap * jacobian * (jump.T * weights) @ jump
|
|
56
|
+
i, j = np.nonzero(local)
|
|
57
|
+
rows.extend(unique[i])
|
|
58
|
+
columns.extend(unique[j])
|
|
59
|
+
entries.extend(local[i, j])
|
|
60
|
+
values, cols, ptr = (np.asarray(x) for x in matrix.CSR())
|
|
61
|
+
volume_rows = np.repeat(np.arange(matrix.height), np.diff(ptr).astype(np.int64))
|
|
62
|
+
# Fill native arrays directly; Python conversion of the full volume graph
|
|
63
|
+
# would cost more than assembling the contact itself.
|
|
64
|
+
count = len(values)
|
|
65
|
+
i, j = core.Array_I_S(count + len(rows)), core.Array_I_S(count + len(rows))
|
|
66
|
+
data = core.Array_D_S(count + len(rows))
|
|
67
|
+
i.NumPy()[:count], i.NumPy()[count:] = volume_rows, rows
|
|
68
|
+
j.NumPy()[:count], j.NumPy()[count:] = cols, columns
|
|
69
|
+
data.NumPy()[:count], data.NumPy()[count:] = values, entries
|
|
70
|
+
return ng.la.SparseMatrixd.CreateFromCOO(i, j, data, matrix.height, matrix.width)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def split(mesh, gap, materials, moving=()):
|
|
74
|
+
"""Separate potential traces on actual, conforming iron mating faces."""
|
|
75
|
+
if not gap:
|
|
76
|
+
return mesh, None
|
|
77
|
+
names = mesh.GetMaterials()
|
|
78
|
+
iron = {
|
|
79
|
+
name
|
|
80
|
+
for name, law in materials.items()
|
|
81
|
+
if "h" in law or ("br" not in law and law.get("mur", 1) > 1)
|
|
82
|
+
}
|
|
83
|
+
descriptors = [nm.FaceDescriptor(fd) for fd in mesh.ngmesh.FaceDescriptors()]
|
|
84
|
+
selected = {}
|
|
85
|
+
for i, fd in enumerate(descriptors, 1):
|
|
86
|
+
if not fd.domin or not fd.domout:
|
|
87
|
+
continue
|
|
88
|
+
a, b = names[fd.domin - 1], names[fd.domout - 1]
|
|
89
|
+
if a != b and a in iron and b in iron and (a in moving) == (b in moving):
|
|
90
|
+
selected[i] = tuple(sorted((a, b)))
|
|
91
|
+
if not selected:
|
|
92
|
+
return mesh, None
|
|
93
|
+
tets = mesh.ngmesh.Elements3D().NumPy()["nodes"] - 1
|
|
94
|
+
boundary = mesh.ngmesh.Elements2D().NumPy().copy()
|
|
95
|
+
triangles, indices = boundary["nodes"] - 1, boundary["index"]
|
|
96
|
+
contact = np.isin(indices, list(selected))
|
|
97
|
+
local = np.array([[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]])
|
|
98
|
+
faces = tets[:, local].reshape(-1, 3)
|
|
99
|
+
corners = np.argsort(faces, axis=1)
|
|
100
|
+
occurrences = (4 * np.arange(mesh.ne)[:, None, None] + local).reshape(-1, 3)
|
|
101
|
+
occurrences = np.take_along_axis(occurrences, corners, axis=1)
|
|
102
|
+
keys = face_keys(faces)
|
|
103
|
+
order = np.argsort(keys)
|
|
104
|
+
keys, occurrences = keys[order], occurrences[order]
|
|
105
|
+
positions = np.searchsorted(keys, face_keys(triangles))
|
|
106
|
+
if not np.all(keys[positions] == face_keys(triangles)):
|
|
107
|
+
raise ValueError("Contact mesh has an unmatched surface face")
|
|
108
|
+
same = keys[1:] == keys[:-1]
|
|
109
|
+
if np.any(same[:-1] & same[1:]):
|
|
110
|
+
raise ValueError("Contact mesh has a nonmanifold face")
|
|
111
|
+
paired = np.flatnonzero(same)
|
|
112
|
+
cut = positions[contact]
|
|
113
|
+
if not np.all(same[cut]):
|
|
114
|
+
raise ValueError("Contact must have iron on both sides")
|
|
115
|
+
joined = paired[~np.isin(paired, cut)]
|
|
116
|
+
a, b = occurrences[joined].ravel(), occurrences[joined + 1].ravel()
|
|
117
|
+
graph = coo_matrix(
|
|
118
|
+
(np.ones(len(a), dtype=np.int8), (a, b)), shape=(4 * mesh.ne, 4 * mesh.ne)
|
|
119
|
+
).tocsr()
|
|
120
|
+
_, labels = connected_components(graph, directed=False)
|
|
121
|
+
_, first = np.unique(labels, return_index=True)
|
|
122
|
+
nodes = tets.ravel()[first]
|
|
123
|
+
_, original = np.unique(nodes, return_index=True)
|
|
124
|
+
copies = np.isin(nodes, triangles[contact])
|
|
125
|
+
copies[original] = False
|
|
126
|
+
points = np.asarray(mesh.ngmesh.Coordinates())
|
|
127
|
+
new_points = np.concatenate([points, points[nodes[copies]]])
|
|
128
|
+
numbers = nodes.copy()
|
|
129
|
+
numbers[copies] = mesh.nv + np.arange(copies.sum())
|
|
130
|
+
new_tets = numbers[labels].reshape(-1, 4)
|
|
131
|
+
|
|
132
|
+
owners = order[positions] // 4
|
|
133
|
+
|
|
134
|
+
def trace(cell, vertices):
|
|
135
|
+
return new_tets[cell, np.argmax(tets[cell, :, None] == vertices, axis=0)]
|
|
136
|
+
|
|
137
|
+
new_triangles = np.array([trace(ci, tri) for ci, tri in zip(owners, triangles)])
|
|
138
|
+
# Interior contact edges need independent P2 edge DOFs even when both
|
|
139
|
+
# endpoints lie on the physical rim. One conforming bisection supplies them.
|
|
140
|
+
edges, tied = {}, set()
|
|
141
|
+
for si in np.flatnonzero(contact):
|
|
142
|
+
pos = positions[si]
|
|
143
|
+
lo, hi = order[pos] // 4, order[pos + 1] // 4
|
|
144
|
+
tri = triangles[si]
|
|
145
|
+
equal = trace(lo, tri) == trace(hi, tri)
|
|
146
|
+
for i, j in ((0, 1), (0, 2), (1, 2)):
|
|
147
|
+
edge = tuple(sorted((int(tri[i]), int(tri[j]))))
|
|
148
|
+
key = selected[int(indices[si])], edge
|
|
149
|
+
edges[key] = edges.get(key, 0) + 1
|
|
150
|
+
if equal[i] and equal[j]:
|
|
151
|
+
tied.add(key)
|
|
152
|
+
refine = sorted({edge for pair, edge in tied if edges[pair, edge] == 2})
|
|
153
|
+
if refine:
|
|
154
|
+
return split(bisect(mesh, refine), gap, materials, moving)
|
|
155
|
+
|
|
156
|
+
interfaces, pairs, records = [], {}, []
|
|
157
|
+
extra_triangles, extra_indices = [], []
|
|
158
|
+
domains = mesh.ngmesh.Elements3D().NumPy()["index"]
|
|
159
|
+
for index, pair in selected.items():
|
|
160
|
+
if pair not in pairs:
|
|
161
|
+
pairs[pair] = len(interfaces)
|
|
162
|
+
interfaces.append(dict(parts=list(pair), area_m2=0.0))
|
|
163
|
+
fd = descriptors[index - 1]
|
|
164
|
+
other = nm.FaceDescriptor(fd)
|
|
165
|
+
other.domin, other.domout = fd.domout, 0
|
|
166
|
+
fd.domout = 0
|
|
167
|
+
k = pairs[pair]
|
|
168
|
+
fd.bcname = f"contact_{k}_minus"
|
|
169
|
+
other.bc = len(descriptors) + 1
|
|
170
|
+
other.bcname = f"contact_{k}_plus"
|
|
171
|
+
descriptors.append(other)
|
|
172
|
+
for si in np.flatnonzero(indices == index):
|
|
173
|
+
pos = positions[si]
|
|
174
|
+
lo, hi = int(order[pos] // 4), int(order[pos + 1] // 4)
|
|
175
|
+
if domains[lo] != fd.domin:
|
|
176
|
+
lo, hi = hi, lo
|
|
177
|
+
tri = triangles[si]
|
|
178
|
+
lower, upper = trace(lo, tri), trace(hi, tri)
|
|
179
|
+
new_triangles[si] = lower
|
|
180
|
+
extra_triangles.append(upper)
|
|
181
|
+
extra_indices.append(len(descriptors))
|
|
182
|
+
xyz = points[tri]
|
|
183
|
+
interfaces[k]["area_m2"] += float(
|
|
184
|
+
np.linalg.norm(np.cross(xyz[1] - xyz[0], xyz[2] - xyz[0])) / 2
|
|
185
|
+
)
|
|
186
|
+
records.append(
|
|
187
|
+
dict(
|
|
188
|
+
interface=k,
|
|
189
|
+
minus_cell=lo,
|
|
190
|
+
plus_cell=hi,
|
|
191
|
+
vertices=lower.tolist(),
|
|
192
|
+
plus_vertices=upper.tolist(),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
# Match each periodic boundary triangle's two traces independently. This
|
|
196
|
+
# works for rotational or translated sectors without assuming a face normal.
|
|
197
|
+
surface_lookup = {tuple(sorted(tri)): i for i, tri in enumerate(triangles)}
|
|
198
|
+
identifications = set()
|
|
199
|
+
groups = {}
|
|
200
|
+
for a, b, ident in mesh.ngmesh.GetIdentifications():
|
|
201
|
+
groups.setdefault(ident, {})[a.nr - 1] = b.nr - 1
|
|
202
|
+
for ident, mapping in groups.items():
|
|
203
|
+
for si, tri in enumerate(triangles):
|
|
204
|
+
target = [mapping.get(int(i), -1) for i in tri]
|
|
205
|
+
other = surface_lookup.get(tuple(sorted(target)))
|
|
206
|
+
if other is None:
|
|
207
|
+
continue
|
|
208
|
+
for j, node in enumerate(target):
|
|
209
|
+
k = list(triangles[other]).index(node)
|
|
210
|
+
identifications.add(
|
|
211
|
+
(int(new_triangles[si, j]), int(new_triangles[other, k]), ident)
|
|
212
|
+
)
|
|
213
|
+
native = rebuild(
|
|
214
|
+
mesh,
|
|
215
|
+
new_points,
|
|
216
|
+
new_tets,
|
|
217
|
+
domains,
|
|
218
|
+
np.concatenate([new_triangles, extra_triangles]),
|
|
219
|
+
np.r_[indices, extra_indices],
|
|
220
|
+
descriptors,
|
|
221
|
+
identifications,
|
|
222
|
+
)
|
|
223
|
+
return native, dict(gap=gap, faces=records, interfaces=interfaces)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def face_keys(vertices):
|
|
227
|
+
dtype = np.dtype([(name, np.int32) for name in ("a", "b", "c")])
|
|
228
|
+
return (
|
|
229
|
+
np.ascontiguousarray(np.sort(vertices, axis=1), dtype=np.int32)
|
|
230
|
+
.view(dtype)
|
|
231
|
+
.ravel()
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def rebuild(
|
|
236
|
+
mesh, points, tets, domains, triangles, indices, descriptors, identifications
|
|
237
|
+
):
|
|
238
|
+
native = nm.Mesh(dim=3)
|
|
239
|
+
for fd in descriptors:
|
|
240
|
+
native.Add(fd)
|
|
241
|
+
native.SetBCName(fd.bc - 1, fd.bcname)
|
|
242
|
+
for p in points:
|
|
243
|
+
native.Add(nm.MeshPoint(nm.Pnt(*p)))
|
|
244
|
+
for tet, domain in zip(tets, domains):
|
|
245
|
+
native.Add(nm.Element3D(index=int(domain), vertices=(tet + 1).tolist()))
|
|
246
|
+
for tri, index in zip(triangles, indices):
|
|
247
|
+
native.Add(nm.Element2D(index=int(index), vertices=(tri + 1).tolist()))
|
|
248
|
+
for i, name in enumerate(mesh.GetMaterials(), 1):
|
|
249
|
+
native.SetMaterial(i, name)
|
|
250
|
+
for a, b, ident in sorted(identifications):
|
|
251
|
+
native.AddPointIdentification(
|
|
252
|
+
a + 1, b + 1, ident, mesh.ngmesh.GetIdentificationType(ident)
|
|
253
|
+
)
|
|
254
|
+
return ng.Mesh(native)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def bisect(mesh, edges):
|
|
258
|
+
points = np.asarray(mesh.ngmesh.Coordinates()).tolist()
|
|
259
|
+
cells = mesh.ngmesh.Elements3D().NumPy()
|
|
260
|
+
tets, domains = cells["nodes"] - 1, cells["index"].copy()
|
|
261
|
+
surfaces = mesh.ngmesh.Elements2D().NumPy()
|
|
262
|
+
triangles, indices = surfaces["nodes"] - 1, surfaces["index"].copy()
|
|
263
|
+
for a, b in edges:
|
|
264
|
+
c = len(points)
|
|
265
|
+
points.append(((np.array(points[a]) + points[b]) / 2).tolist())
|
|
266
|
+
for volume in (True, False):
|
|
267
|
+
nodes, labels = (tets, domains) if volume else (triangles, indices)
|
|
268
|
+
hit = np.any(nodes == a, axis=1) & np.any(nodes == b, axis=1)
|
|
269
|
+
first, second = nodes[hit].copy(), nodes[hit].copy()
|
|
270
|
+
first[first == a], second[second == b] = c, c
|
|
271
|
+
nodes = np.concatenate([nodes[~hit], first, second])
|
|
272
|
+
labels = np.r_[labels[~hit], labels[hit], labels[hit]]
|
|
273
|
+
if volume:
|
|
274
|
+
tets, domains = nodes, labels
|
|
275
|
+
else:
|
|
276
|
+
triangles, indices = nodes, labels
|
|
277
|
+
identifications = {
|
|
278
|
+
(a.nr - 1, b.nr - 1, ident) for a, b, ident in mesh.ngmesh.GetIdentifications()
|
|
279
|
+
}
|
|
280
|
+
return rebuild(
|
|
281
|
+
mesh,
|
|
282
|
+
points,
|
|
283
|
+
tets,
|
|
284
|
+
domains,
|
|
285
|
+
triangles,
|
|
286
|
+
indices,
|
|
287
|
+
[nm.FaceDescriptor(fd) for fd in mesh.ngmesh.FaceDescriptors()],
|
|
288
|
+
identifications,
|
|
289
|
+
)
|
step2torque/_cuda.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Stateful cuDSS Cholesky for real NGSolve matrices with fixed sparsity."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata as metadata
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import weakref
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import scipy.sparse as sp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CuDSS:
|
|
13
|
+
def __init__(self, matrix, free_dofs):
|
|
14
|
+
import nvmath.sparse.advanced as sparse
|
|
15
|
+
|
|
16
|
+
self.matrix = matrix
|
|
17
|
+
self.free = np.flatnonzero(np.asarray(free_dofs, dtype=bool))
|
|
18
|
+
self.rhs = np.zeros(len(self.free))
|
|
19
|
+
self.solver = None
|
|
20
|
+
self.sparse = sparse
|
|
21
|
+
self.stats = dict(
|
|
22
|
+
export_s=0.0,
|
|
23
|
+
setup_s=0.0,
|
|
24
|
+
factor_s=0.0,
|
|
25
|
+
solve_s=0.0,
|
|
26
|
+
factorizations=0,
|
|
27
|
+
solves=0,
|
|
28
|
+
)
|
|
29
|
+
self.Update()
|
|
30
|
+
|
|
31
|
+
def Update(self):
|
|
32
|
+
started = time.perf_counter()
|
|
33
|
+
values, columns, pointers = (np.asarray(x) for x in self.matrix.CSR())
|
|
34
|
+
if self.solver is None:
|
|
35
|
+
rows = np.repeat(
|
|
36
|
+
np.arange(self.matrix.height), np.diff(pointers).astype(np.int64)
|
|
37
|
+
)
|
|
38
|
+
mapping = np.full(self.matrix.height, -1, dtype=np.int32)
|
|
39
|
+
mapping[self.free] = np.arange(len(self.free), dtype=np.int32)
|
|
40
|
+
active = (mapping[rows] >= 0) & (mapping[columns] >= 0)
|
|
41
|
+
if np.any(active & (rows > columns)) and not np.any(
|
|
42
|
+
active & (rows < columns)
|
|
43
|
+
):
|
|
44
|
+
raise ValueError("NGSolve CSR must expose the full symmetric matrix.")
|
|
45
|
+
self.entries = np.flatnonzero(active & (rows <= columns))
|
|
46
|
+
r, c = mapping[rows[self.entries]], mapping[columns[self.entries]]
|
|
47
|
+
assert np.count_nonzero(r == c) == len(self.free)
|
|
48
|
+
order = np.lexsort((c, r))
|
|
49
|
+
self.entries = self.entries[order]
|
|
50
|
+
indptr = np.r_[
|
|
51
|
+
0, np.cumsum(np.bincount(r, minlength=len(self.free)), dtype=np.int64)
|
|
52
|
+
]
|
|
53
|
+
assert indptr[-1] < 2**31
|
|
54
|
+
self.upper = sp.csr_matrix(
|
|
55
|
+
(values[self.entries].copy(), c[order], indptr.astype(np.int32)),
|
|
56
|
+
shape=(len(self.free), len(self.free)),
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
# CSR values share NGSolve storage; the sparsity map stays fixed.
|
|
60
|
+
self.upper.data[:] = values[self.entries]
|
|
61
|
+
self.stats["last_export_s"] = time.perf_counter() - started
|
|
62
|
+
self.stats["export_s"] += self.stats["last_export_s"]
|
|
63
|
+
if self.solver is None:
|
|
64
|
+
library = metadata.distribution("nvidia-cudss-cu12")
|
|
65
|
+
threading = [
|
|
66
|
+
library.locate_file(path)
|
|
67
|
+
for path in library.files
|
|
68
|
+
if str(path).endswith("libcudss_mtlayer_gomp.so.0")
|
|
69
|
+
]
|
|
70
|
+
logger = logging.Logger("cudss", level=logging.ERROR)
|
|
71
|
+
options = dict(
|
|
72
|
+
sparse_system_type=self.sparse.DirectSolverMatrixType.SPD,
|
|
73
|
+
sparse_system_view=self.sparse.DirectSolverMatrixViewType.UPPER,
|
|
74
|
+
blocking=True,
|
|
75
|
+
logger=logger,
|
|
76
|
+
)
|
|
77
|
+
if threading:
|
|
78
|
+
options["multithreading_lib"] = str(threading[0])
|
|
79
|
+
started = time.perf_counter()
|
|
80
|
+
self.solver = self.sparse.DirectSolver(
|
|
81
|
+
self.upper, self.rhs, options=options, execution="cuda"
|
|
82
|
+
)
|
|
83
|
+
# The native object requires explicit cleanup, including after the last pose.
|
|
84
|
+
self.cleanup = weakref.finalize(self, self.solver.free)
|
|
85
|
+
self.solver.plan()
|
|
86
|
+
self.stats["setup_s"] += time.perf_counter() - started
|
|
87
|
+
else:
|
|
88
|
+
self.solver.reset_operands(a=self.upper)
|
|
89
|
+
started = time.perf_counter()
|
|
90
|
+
self.solver.factorize()
|
|
91
|
+
self.stats["factor_s"] += time.perf_counter() - started
|
|
92
|
+
self.stats["factorizations"] += 1
|
|
93
|
+
|
|
94
|
+
def __mul__(self, rhs):
|
|
95
|
+
started = time.perf_counter()
|
|
96
|
+
self.rhs[:] = rhs.FV().NumPy()[self.free]
|
|
97
|
+
self.solver.reset_operands(b=self.rhs)
|
|
98
|
+
values = self.solver.solve()
|
|
99
|
+
if not np.isfinite(values).all():
|
|
100
|
+
raise RuntimeError("CUDA linear solve returned a non-finite field")
|
|
101
|
+
answer = self.matrix.CreateColVector()
|
|
102
|
+
array = answer.FV().NumPy()
|
|
103
|
+
array[:] = 0
|
|
104
|
+
array[self.free] = values
|
|
105
|
+
self.stats["solve_s"] += time.perf_counter() - started
|
|
106
|
+
self.stats["solves"] += 1
|
|
107
|
+
return answer
|
|
108
|
+
|
|
109
|
+
def close(self):
|
|
110
|
+
self.cleanup()
|