loadpath 0.1.1__py3-none-any.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.
- loadpath/README.md +294 -0
- loadpath/SKILL.md +584 -0
- loadpath/__init__.py +17 -0
- loadpath/__main__.py +1202 -0
- loadpath/am.py +316 -0
- loadpath/amgfast.py +326 -0
- loadpath/assembly.py +985 -0
- loadpath/benchmarks/__init__.py +0 -0
- loadpath/benchmarks/beams.py +266 -0
- loadpath/benchmarks/cases/ge-bracket.json +380 -0
- loadpath/benchmarks/evaluate.py +172 -0
- loadpath/benchmarks/mechanisms.py +416 -0
- loadpath/benchmarks/problems.py +140 -0
- loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference.py +138 -0
- loadpath/benchmarks/run.py +301 -0
- loadpath/brep.py +1470 -0
- loadpath/bucklingopt.py +300 -0
- loadpath/buildsim.py +1490 -0
- loadpath/cadimport.py +904 -0
- loadpath/calibration.py +885 -0
- loadpath/cavities.py +211 -0
- loadpath/checks/__init__.py +0 -0
- loadpath/checks/dfm.py +354 -0
- loadpath/checks/exchanger.py +7 -0
- loadpath/checks/fea.py +212 -0
- loadpath/checks/fit.py +475 -0
- loadpath/checks/networks.py +260 -0
- loadpath/checks/opening.py +67 -0
- loadpath/checks/sdf_kit.py +7 -0
- loadpath/conjugate.py +267 -0
- loadpath/constrained.py +329 -0
- loadpath/contact.py +691 -0
- loadpath/coopt.py +342 -0
- loadpath/cores.py +163 -0
- loadpath/crease.py +1138 -0
- loadpath/critic.py +212 -0
- loadpath/crosscheck.py +1107 -0
- loadpath/cutcell.py +510 -0
- loadpath/data/lattice_buckling.json +439 -0
- loadpath/data/lattice_tensors.json +27449 -0
- loadpath/data/published_experiments.json +71 -0
- loadpath/decimate.py +177 -0
- loadpath/delivery.py +190 -0
- loadpath/directions.py +262 -0
- loadpath/domain.py +630 -0
- loadpath/drains.py +615 -0
- loadpath/durability.py +413 -0
- loadpath/editor.html +3135 -0
- loadpath/editor.py +610 -0
- loadpath/escape.py +354 -0
- loadpath/evals/__init__.py +0 -0
- loadpath/evals/baselines/coarse.json +1543 -0
- loadpath/evals/baselines/full.json +1497 -0
- loadpath/evals/brief_kinds/__init__.py +1 -0
- loadpath/evals/brief_kinds/heatx.py +387 -0
- loadpath/evals/brief_kinds/lattice.py +571 -0
- loadpath/evals/brief_kinds/multimat.py +593 -0
- loadpath/evals/briefs/assets/bracket.step +1201 -0
- loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
- loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
- loadpath/evals/briefs/assets/motor-body.stl +0 -0
- loadpath/evals/briefs/assets/sketch.png +0 -0
- loadpath/evals/briefs/base-adapter-fdm.json +492 -0
- loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
- loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
- loadpath/evals/briefs/camera-boom-mjf.json +339 -0
- loadpath/evals/briefs/camera-mount-pla.json +203 -0
- loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
- loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
- loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
- loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
- loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
- loadpath/evals/briefs/forearm-fdm.json +146 -0
- loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
- loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
- loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
- loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
- loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
- loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
- loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
- loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
- loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
- loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
- loadpath/evals/briefs/lighten-step-bracket.json +220 -0
- loadpath/evals/briefs/nema17-mount-stl.json +429 -0
- loadpath/evals/briefs/pillow-block-sls.json +220 -0
- loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
- loadpath/evals/briefs/rod-hook-pla.json +154 -0
- loadpath/evals/briefs/sensor-arm-resin.json +150 -0
- loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
- loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
- loadpath/evals/briefs/sketch-clevis.json +229 -0
- loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
- loadpath/evals/briefs/tool-flange-ti64.json +280 -0
- loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
- loadpath/evals/briefs/turret-fdm.json +281 -0
- loadpath/evals/briefs/upper-arm-sls.json +179 -0
- loadpath/evals/briefs/urdf-forearm.json +233 -0
- loadpath/evals/briefs/wrist-link-mjf.json +278 -0
- loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
- loadpath/evals/briefs.py +393 -0
- loadpath/evals/cases/arm-link-cast.json +157 -0
- loadpath/evals/cases/arm-link-driven.json +171 -0
- loadpath/evals/cases/arm-link-sandwich.json +315 -0
- loadpath/evals/cases/arm-link.json +111 -0
- loadpath/evals/cases/bell-crank.json +116 -0
- loadpath/evals/cases/grip-turret.json +479 -0
- loadpath/evals/cases/grip-upper-arm.json +253 -0
- loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
- loadpath/evals/cases/grip-wrist-yoke.json +831 -0
- loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
- loadpath/evals/cases/mbb-beam.json +61 -0
- loadpath/evals/cases/motor-body.stl +0 -0
- loadpath/evals/cases/motor-mount.json +265 -0
- loadpath/evals/cases/printed-hook.json +77 -0
- loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
- loadpath/evals/cases/wall-bracket.json +126 -0
- loadpath/evals/corpus.py +179 -0
- loadpath/evals/pareto.py +90 -0
- loadpath/evals/run.py +860 -0
- loadpath/examples/arm-link.json +25 -0
- loadpath/examples/bumpered-bracket-compression.json +225 -0
- loadpath/examples/bumpered-bracket-lattice.json +204 -0
- loadpath/examples/bumpered-bracket.json +34 -0
- loadpath/examples/gripper-finger-mm.json +185 -0
- loadpath/examples/heat-exchanger-counterflow.json +21 -0
- loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
- loadpath/examples/heat-exchanger-crossflow.json +20 -0
- loadpath/examples/pedal-lattice.json +23 -0
- loadpath/examples/pressure-plenum.json +12 -0
- loadpath/examples/shelf-bracket.json +27 -0
- loadpath/exchanger.py +831 -0
- loadpath/fasteners.py +199 -0
- loadpath/fem.py +668 -0
- loadpath/femops.py +70 -0
- loadpath/fieldjit.py +981 -0
- loadpath/fieldops.py +119 -0
- loadpath/fields.py +3697 -0
- loadpath/fieldtorch.py +464 -0
- loadpath/flow.py +984 -0
- loadpath/flowtopo.py +300 -0
- loadpath/gpu.py +521 -0
- loadpath/heatx.py +1067 -0
- loadpath/heatx_thermal.py +432 -0
- loadpath/homogenized.py +886 -0
- loadpath/ibrep.py +552 -0
- loadpath/impact.py +693 -0
- loadpath/infill.py +376 -0
- loadpath/interface_library.py +340 -0
- loadpath/interlock.py +186 -0
- loadpath/isolator.py +149 -0
- loadpath/knn.py +204 -0
- loadpath/lattice.py +1065 -0
- loadpath/lattice3mf.py +760 -0
- loadpath/lattice_buckling.py +245 -0
- loadpath/lattice_grade.py +325 -0
- loadpath/lattice_orient.py +595 -0
- loadpath/lattice_print.py +159 -0
- loadpath/lattice_tensors.py +558 -0
- loadpath/latticestep.py +1210 -0
- loadpath/manufacture.py +446 -0
- loadpath/material_library.py +489 -0
- loadpath/materials.py +144 -0
- loadpath/mcp_server.py +786 -0
- loadpath/mechanism.py +218 -0
- loadpath/mechanism_nl.py +811 -0
- loadpath/meshops.py +452 -0
- loadpath/mg.py +853 -0
- loadpath/mma.py +214 -0
- loadpath/modal.py +210 -0
- loadpath/multimat.py +1788 -0
- loadpath/narrowband.py +418 -0
- loadpath/nlkernel.py +241 -0
- loadpath/nonlinear.py +1095 -0
- loadpath/orient.py +334 -0
- loadpath/porous.py +994 -0
- loadpath/pressure.py +171 -0
- loadpath/preview.py +335 -0
- loadpath/printcheck.py +263 -0
- loadpath/printsetup.py +207 -0
- loadpath/processcheck.py +376 -0
- loadpath/quick.py +113 -0
- loadpath/recall.py +406 -0
- loadpath/reconstruct.py +797 -0
- loadpath/render.py +436 -0
- loadpath/resolved.py +189 -0
- loadpath/schema.py +1701 -0
- loadpath/sdf_kit.py +843 -0
- loadpath/selfx.py +292 -0
- loadpath/shapes.py +584 -0
- loadpath/sharp.py +717 -0
- loadpath/simp.py +1170 -0
- loadpath/sizing.py +312 -0
- loadpath/slicer.py +321 -0
- loadpath/spec.py +1072 -0
- loadpath/step.py +806 -0
- loadpath/struts.py +639 -0
- loadpath/study.py +1937 -0
- loadpath/tasks.py +822 -0
- loadpath/thermal.py +1691 -0
- loadpath/threemf.py +487 -0
- loadpath/tune.py +437 -0
- loadpath/validation.py +145 -0
- loadpath/verify.py +1215 -0
- loadpath/viewer.html +694 -0
- loadpath/viewer.py +446 -0
- loadpath/viscoelastic.py +106 -0
- loadpath/voxelprint.py +582 -0
- loadpath/voxels.py +402 -0
- loadpath-0.1.1.dist-info/METADATA +319 -0
- loadpath-0.1.1.dist-info/RECORD +220 -0
- loadpath-0.1.1.dist-info/WHEEL +5 -0
- loadpath-0.1.1.dist-info/entry_points.txt +2 -0
- loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
- loadpath-0.1.1.dist-info/top_level.txt +1 -0
loadpath/gpu.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GPU backend for the matrix-free multigrid solve (mg.GridElasticity).
|
|
3
|
+
|
|
4
|
+
Everything the large-grid solver spends its time on is dense array work on
|
|
5
|
+
the voxel node grid: eight slice gathers, one GEMM with a shared 24x24
|
|
6
|
+
matrix, eight slice scatters, Chebyshev smoothing, trilinear transfers.
|
|
7
|
+
That maps one to one onto torch, which runs it on Apple GPUs (MPS) and
|
|
8
|
+
NVIDIA GPUs (CUDA) from the same code. Hierarchy setup stays in numpy (about
|
|
9
|
+
1% of the time) and the tensors are uploaded once per `set_moduli`.
|
|
10
|
+
|
|
11
|
+
Precision follows the CPU solver: the V-cycle in float32, the outer
|
|
12
|
+
conjugate gradient and its operator in float64, so the answer is the same
|
|
13
|
+
float64 one (same CG counts, compliance equal to ~1e-9).
|
|
14
|
+
|
|
15
|
+
Apple GPUs have no float64, and consumer NVIDIA cards run it at 1/32 to
|
|
16
|
+
1/64 of float32 speed. A plain float32 CG diverged on the arm link (~40
|
|
17
|
+
iterations where float64 needs 12): a slender part's displacement is
|
|
18
|
+
hundreds of times a cell's stretch, mostly rigid translation and rotation
|
|
19
|
+
of each cell, and K u cancels that away together with float32's digits. So
|
|
20
|
+
those devices run a float32-only solve (`fp32`, below): iterative
|
|
21
|
+
refinement whose outer residual keeps the displacement as a float32 pair
|
|
22
|
+
(hi + lo, ~48 bits) and applies each cell's matrix only to the cell's
|
|
23
|
+
deformation, with its rigid translation and mean rotation taken out first
|
|
24
|
+
in pair arithmetic (K annihilates rigid motion, so the product is the
|
|
25
|
+
same, but now it is computed from small numbers). Inner corrections are
|
|
26
|
+
plain float32 CG. On the arm link (114k cells) this lands within 4e-6 of
|
|
27
|
+
the float64 compliance and 1e-5 of the peak strain energy in 33 V-cycles
|
|
28
|
+
against float64's 26, with no float64 anywhere. Selection: on by default
|
|
29
|
+
on MPS; LOADPATH_FP32=1 forces it on any device, 0 keeps the old split
|
|
30
|
+
(CG in float64 on the CPU, V-cycle on the GPU).
|
|
31
|
+
|
|
32
|
+
The coarsest level stays where it is (a sparse LU or pyamg on the CPU):
|
|
33
|
+
it is a few thousand dofs, and copying its right-hand side over is cheaper
|
|
34
|
+
than a device factorization.
|
|
35
|
+
|
|
36
|
+
Selection: LOADPATH_DEVICE=auto (default: CUDA, then MPS, then torch on the
|
|
37
|
+
CPU, which on its own beats the numpy kernels several times over), cuda,
|
|
38
|
+
mps, torch (torch on the CPU), or cpu (the numpy solver). torch is an
|
|
39
|
+
optional dependency (extra "gpu"); without it everything runs as before.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
import numpy as np
|
|
46
|
+
|
|
47
|
+
from .mg import _CORNER_OFF, LMAX_SAFETY
|
|
48
|
+
|
|
49
|
+
__all__ = ["resolve_device", "TorchMG"]
|
|
50
|
+
|
|
51
|
+
# Bytes of gather buffer per chunk of x-planes (like mg._slab, larger: the
|
|
52
|
+
# device has the bandwidth and launch overhead dominates small chunks).
|
|
53
|
+
_CHUNK_BYTES = 256e6
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resolve_device(name: str | None = None):
|
|
57
|
+
"""The torch device to run the multigrid on, or None for the numpy path."""
|
|
58
|
+
name = (name if name is not None else os.environ.get("LOADPATH_DEVICE", "auto")).strip().lower()
|
|
59
|
+
if name in ("", "cpu", "none", "off", "numpy"):
|
|
60
|
+
return None
|
|
61
|
+
try:
|
|
62
|
+
import torch
|
|
63
|
+
except ImportError:
|
|
64
|
+
if name in ("auto",):
|
|
65
|
+
return None
|
|
66
|
+
raise RuntimeError(f"LOADPATH_DEVICE={name} needs torch (pip install 'loadpath[gpu]')")
|
|
67
|
+
if name == "auto":
|
|
68
|
+
if torch.cuda.is_available():
|
|
69
|
+
return torch.device("cuda")
|
|
70
|
+
if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available():
|
|
71
|
+
return torch.device("mps")
|
|
72
|
+
return torch.device("cpu")
|
|
73
|
+
if name == "torch":
|
|
74
|
+
return torch.device("cpu")
|
|
75
|
+
if name == "cuda" and not torch.cuda.is_available():
|
|
76
|
+
raise RuntimeError("LOADPATH_DEVICE=cuda but torch sees no CUDA device")
|
|
77
|
+
if name == "mps" and not torch.backends.mps.is_available():
|
|
78
|
+
raise RuntimeError("LOADPATH_DEVICE=mps but torch sees no Metal device")
|
|
79
|
+
return torch.device(name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _prolong(c):
|
|
83
|
+
"""mg.prolong on a tensor (..., n0, n1, n2) -> (..., 2n0-1, 2n1-1, 2n2-1)."""
|
|
84
|
+
import torch
|
|
85
|
+
|
|
86
|
+
out = c
|
|
87
|
+
for ax in (-3, -2, -1):
|
|
88
|
+
n = out.shape[ax]
|
|
89
|
+
shape = list(out.shape)
|
|
90
|
+
shape[ax] = 2 * n - 1
|
|
91
|
+
f = torch.empty(shape, dtype=out.dtype, device=out.device)
|
|
92
|
+
sl_even = [slice(None)] * f.ndim
|
|
93
|
+
sl_even[ax] = slice(0, None, 2)
|
|
94
|
+
sl_odd = [slice(None)] * f.ndim
|
|
95
|
+
sl_odd[ax] = slice(1, None, 2)
|
|
96
|
+
f[tuple(sl_even)] = out
|
|
97
|
+
f[tuple(sl_odd)] = 0.5 * (out.narrow(ax, 0, n - 1) + out.narrow(ax, 1, n - 1))
|
|
98
|
+
out = f
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _restrict(f):
|
|
103
|
+
"""Transpose of _prolong."""
|
|
104
|
+
out = f
|
|
105
|
+
for ax in (-3, -2, -1):
|
|
106
|
+
n = (out.shape[ax] + 1) // 2
|
|
107
|
+
sl_even = [slice(None)] * out.ndim
|
|
108
|
+
sl_even[ax] = slice(0, None, 2)
|
|
109
|
+
sl_odd = [slice(None)] * out.ndim
|
|
110
|
+
sl_odd[ax] = slice(1, None, 2)
|
|
111
|
+
c = out[tuple(sl_even)].clone()
|
|
112
|
+
half = 0.5 * out[tuple(sl_odd)]
|
|
113
|
+
c.narrow(ax, 0, n - 1).add_(half)
|
|
114
|
+
c.narrow(ax, 1, n - 1).add_(half)
|
|
115
|
+
out = c
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _two_sum(a, b):
|
|
120
|
+
"""Error-free a + b = s + e (Knuth), elementwise in the tensors' dtype."""
|
|
121
|
+
s = a + b
|
|
122
|
+
bb = s - a
|
|
123
|
+
return s, (a - (s - bb)) + (b - bb)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _rigid_free(h, lo, corners):
|
|
127
|
+
"""(24, n) gathers of a cell's corner displacements as a float32 pair
|
|
128
|
+
(hi h, lo lo or None) -> (24, n) float32 deformation: the corner-0
|
|
129
|
+
translation and the mean (infinitesimal) rotation removed, in pair
|
|
130
|
+
arithmetic so nothing large is rounded. K times it equals K times the
|
|
131
|
+
input exactly in exact arithmetic (K annihilates rigid motion)."""
|
|
132
|
+
n = h.shape[-1]
|
|
133
|
+
h = h.reshape(8, 3, n)
|
|
134
|
+
s, e = _two_sum(h, -h[0:1])
|
|
135
|
+
if lo is not None:
|
|
136
|
+
lo = lo.reshape(8, 3, n)
|
|
137
|
+
e = e + (lo - lo[0:1])
|
|
138
|
+
d = s + e
|
|
139
|
+
# mean displacement gradient over the cell (cell units), its skew part
|
|
140
|
+
import torch
|
|
141
|
+
|
|
142
|
+
g = 0.5 * torch.einsum("ka,kin->ian", corners["c"], d)
|
|
143
|
+
w = 0.5 * (g - g.transpose(0, 1))
|
|
144
|
+
for a in range(3):
|
|
145
|
+
sel = corners["sel"][a]
|
|
146
|
+
s2, e2 = _two_sum(s[sel], -w[:, a].unsqueeze(0))
|
|
147
|
+
s[sel] = s2
|
|
148
|
+
e[sel] += e2
|
|
149
|
+
return (s + e).reshape(24, n)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _DevLevel:
|
|
153
|
+
"""A level's operator on a torch device in `dtype`, built from an mg._Level."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, lv, dev, dtype):
|
|
156
|
+
import torch
|
|
157
|
+
|
|
158
|
+
t = lambda a: torch.as_tensor(np.ascontiguousarray(a), dtype=dtype, device=dev)
|
|
159
|
+
self.kind = lv.kind
|
|
160
|
+
self.dims = lv.dims
|
|
161
|
+
self.node_shape = lv.node_shape
|
|
162
|
+
self.mask = t(lv.mask)
|
|
163
|
+
self.dinv = t(lv.dinv)
|
|
164
|
+
self.lmax = None
|
|
165
|
+
act = lv.active()
|
|
166
|
+
self.box = None
|
|
167
|
+
if act.any():
|
|
168
|
+
lo = [int(np.flatnonzero(act.any(axis=tuple(j for j in range(3) if j != d)))[0]) for d in range(3)]
|
|
169
|
+
hi = [int(np.flatnonzero(act.any(axis=tuple(j for j in range(3) if j != d)))[-1]) + 1 for d in range(3)]
|
|
170
|
+
self.box = (lo[0], hi[0], lo[1], hi[1], lo[2], hi[2])
|
|
171
|
+
if lv.kind == "scaled":
|
|
172
|
+
self.ke = t(lv.ke)
|
|
173
|
+
self.e = t(lv.e)
|
|
174
|
+
off = torch.as_tensor(np.array(_CORNER_OFF), device=dev)
|
|
175
|
+
self.corners = {"c": off.to(dtype) - 0.5, "sel": [off[:, a] > 0 for a in range(3)]}
|
|
176
|
+
elif lv.kind == "weights":
|
|
177
|
+
self.mcat = t(lv.mcat)
|
|
178
|
+
self.w = t(lv.w)
|
|
179
|
+
else:
|
|
180
|
+
# (x, y, z, 24, 24): one batched matvec per cell
|
|
181
|
+
self.k = t(np.moveaxis(lv.k, (0, 1), (-2, -1)))
|
|
182
|
+
self.pidx = self.pk = None
|
|
183
|
+
if lv.patch_ijk is not None and len(lv.patch_ijk):
|
|
184
|
+
self.pidx = torch.as_tensor(lv._patch_index(), device=dev)
|
|
185
|
+
self.pk = t(lv.patch_k)
|
|
186
|
+
self.extra = None
|
|
187
|
+
if lv.extra is not None and lv.extra.nnz:
|
|
188
|
+
s = lv.extra.tocoo()
|
|
189
|
+
self.extra = (torch.as_tensor(s.row.astype(np.int64), device=dev),
|
|
190
|
+
torch.as_tensor(s.col.astype(np.int64), device=dev), t(s.data))
|
|
191
|
+
cyz = (self.box[3] - self.box[2]) * (self.box[5] - self.box[4]) if self.box else 1
|
|
192
|
+
per_plane = 24 * cyz * 4 * (8 if lv.kind == "weights" else 2)
|
|
193
|
+
self.chunk = max(1, int(_CHUNK_BYTES // per_plane))
|
|
194
|
+
|
|
195
|
+
def _kernel(self, flat, r):
|
|
196
|
+
x0, x1, y0, y1, z0, z1 = r
|
|
197
|
+
shp = (x1 - x0, y1 - y0, z1 - z0)
|
|
198
|
+
if self.kind == "scaled":
|
|
199
|
+
f = (self.ke @ flat).reshape((24,) + shp)
|
|
200
|
+
return f.mul_(self.e[x0:x1, y0:y1, z0:z1])
|
|
201
|
+
if self.kind == "weights":
|
|
202
|
+
g = (self.mcat @ flat).reshape((8, 24) + shp)
|
|
203
|
+
w = self.w[:, x0:x1, y0:y1, z0:z1]
|
|
204
|
+
return (g * w[:, None]).sum(0)
|
|
205
|
+
kk = self.k[x0:x1, y0:y1, z0:z1].reshape(-1, 24, 24)
|
|
206
|
+
f = (kk @ flat.T.unsqueeze(-1)).squeeze(-1) # (n, 24)
|
|
207
|
+
return f.T.reshape((24,) + shp)
|
|
208
|
+
|
|
209
|
+
def apply_k(self, u):
|
|
210
|
+
import torch
|
|
211
|
+
|
|
212
|
+
out = torch.zeros(self.node_shape, dtype=u.dtype, device=u.device)
|
|
213
|
+
if self.box is not None:
|
|
214
|
+
bx0, bx1, y0, y1, z0, z1 = self.box
|
|
215
|
+
for x0 in range(bx0, bx1, self.chunk):
|
|
216
|
+
x1 = min(x0 + self.chunk, bx1)
|
|
217
|
+
r = (x0, x1, y0, y1, z0, z1)
|
|
218
|
+
buf = torch.cat([u[:, x0 + dx:x1 + dx, y0 + dy:y1 + dy, z0 + dz:z1 + dz]
|
|
219
|
+
for (dx, dy, dz) in _CORNER_OFF], 0)
|
|
220
|
+
f = self._kernel(buf.reshape(24, -1), r)
|
|
221
|
+
for c, (dx, dy, dz) in enumerate(_CORNER_OFF):
|
|
222
|
+
out[:, x0 + dx:x1 + dx, y0 + dy:y1 + dy, z0 + dz:z1 + dz] += f[3 * c:3 * c + 3]
|
|
223
|
+
flat_out = out.view(-1)
|
|
224
|
+
if self.pidx is not None:
|
|
225
|
+
ue = u.reshape(-1)[self.pidx] # (n, 24)
|
|
226
|
+
fe = (self.pk @ ue.unsqueeze(-1)).squeeze(-1)
|
|
227
|
+
flat_out.index_add_(0, self.pidx.reshape(-1), fe.reshape(-1))
|
|
228
|
+
if self.extra is not None:
|
|
229
|
+
rows, cols, vals = self.extra
|
|
230
|
+
flat_out.index_add_(0, rows, vals * u.reshape(-1)[cols])
|
|
231
|
+
return out
|
|
232
|
+
|
|
233
|
+
def apply_rf(self, u, lo=None):
|
|
234
|
+
"""apply() on the pair u + lo for the fine ("scaled") level, each
|
|
235
|
+
cell's matrix applied to its rigid-motion-free deformation (see
|
|
236
|
+
_rigid_free): float32 only, accurate on slender parts."""
|
|
237
|
+
import torch
|
|
238
|
+
|
|
239
|
+
m = self.mask
|
|
240
|
+
uh = u * m
|
|
241
|
+
ul = lo * m if lo is not None else None
|
|
242
|
+
out = torch.zeros(self.node_shape, dtype=u.dtype, device=u.device)
|
|
243
|
+
if self.box is not None:
|
|
244
|
+
bx0, bx1, y0, y1, z0, z1 = self.box
|
|
245
|
+
for x0 in range(bx0, bx1, self.chunk):
|
|
246
|
+
x1 = min(x0 + self.chunk, bx1)
|
|
247
|
+
gat = lambda v: torch.cat([v[:, x0 + dx:x1 + dx, y0 + dy:y1 + dy, z0 + dz:z1 + dz]
|
|
248
|
+
for (dx, dy, dz) in _CORNER_OFF], 0).reshape(24, -1)
|
|
249
|
+
d = _rigid_free(gat(uh), gat(ul) if ul is not None else None, self.corners)
|
|
250
|
+
f = self._kernel(d, (x0, x1, y0, y1, z0, z1))
|
|
251
|
+
for c, (dx, dy, dz) in enumerate(_CORNER_OFF):
|
|
252
|
+
out[:, x0 + dx:x1 + dx, y0 + dy:y1 + dy, z0 + dz:z1 + dz] += f[3 * c:3 * c + 3]
|
|
253
|
+
if self.extra is not None:
|
|
254
|
+
rows, cols, vals = self.extra
|
|
255
|
+
out.view(-1).index_add_(0, rows, vals * uh.reshape(-1)[cols])
|
|
256
|
+
out.mul_(m)
|
|
257
|
+
out.add_((1 - m) * u)
|
|
258
|
+
return out
|
|
259
|
+
|
|
260
|
+
def apply(self, u):
|
|
261
|
+
m = self.mask
|
|
262
|
+
out = self.apply_k(u * m)
|
|
263
|
+
out.mul_(m)
|
|
264
|
+
out.add_((1 - m) * u)
|
|
265
|
+
return out
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _pair_arithmetic_ok(dev) -> bool:
|
|
269
|
+
"""True if the device rounds float32 adds to nearest without
|
|
270
|
+
reassociating them (fast-math compilers can), which _two_sum needs."""
|
|
271
|
+
import torch
|
|
272
|
+
|
|
273
|
+
a = torch.tensor([1.0, 3.0e7, -2.0], dtype=torch.float32, device=dev)
|
|
274
|
+
b = torch.tensor([1.0e-8, 1.0, 1.0e-7], dtype=torch.float32, device=dev)
|
|
275
|
+
s, e = _two_sum(a, b)
|
|
276
|
+
exact = (s.double().cpu() + e.double().cpu()).numpy()
|
|
277
|
+
return bool(np.allclose(exact, a.double().cpu().numpy() + b.double().cpu().numpy(), rtol=0, atol=1e-15))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class TorchMG:
|
|
281
|
+
"""Device copy of a GridElasticity hierarchy, and the mixed-precision
|
|
282
|
+
solve on it. `load()` after every set_moduli; `solve(b, x0)` on
|
|
283
|
+
float64 numpy node grids returns (x, device iterations, converged)."""
|
|
284
|
+
|
|
285
|
+
def __init__(self, grid, device):
|
|
286
|
+
import torch
|
|
287
|
+
|
|
288
|
+
self.torch = torch
|
|
289
|
+
self.g = grid
|
|
290
|
+
self.dev = device
|
|
291
|
+
self.dtype = torch.float32
|
|
292
|
+
self.levels = []
|
|
293
|
+
self._lmax_vec = {}
|
|
294
|
+
# The outer CG and its operator need float64: in float32 a slender
|
|
295
|
+
# part's K u cancels away its digits (displacements are hundreds of
|
|
296
|
+
# times a cell's stretch) and CG diverged after ~40 iterations on
|
|
297
|
+
# the arm link where float64 takes 12. Apple GPUs have no float64,
|
|
298
|
+
# so there the CG runs on the CPU (torch, unified memory: the copies
|
|
299
|
+
# are cheap) and only the V-cycle, ~85% of the work, on the GPU.
|
|
300
|
+
fp32 = os.environ.get("LOADPATH_FP32", "").strip().lower()
|
|
301
|
+
self.fp32 = device.type == "mps" if fp32 in ("", "auto") else fp32 in ("1", "true", "on", "yes")
|
|
302
|
+
if self.fp32 and not _pair_arithmetic_ok(device):
|
|
303
|
+
self.fp32 = False
|
|
304
|
+
self.cg_dev = torch.device("cpu") if device.type == "mps" else device
|
|
305
|
+
self.a64 = None
|
|
306
|
+
|
|
307
|
+
def load(self):
|
|
308
|
+
g = self.g
|
|
309
|
+
self.levels = [_DevLevel(g._l0s, self.dev, self.dtype)] + \
|
|
310
|
+
[_DevLevel(lv, self.dev, self.dtype) for lv in g.levels[1:]]
|
|
311
|
+
self.a64 = None if self.fp32 else _DevLevel(g.levels[0], self.cg_dev, self.torch.float64)
|
|
312
|
+
for i, lv in enumerate(self.levels):
|
|
313
|
+
lv.lmax = self._estimate_lmax(i)
|
|
314
|
+
# keep the CPU levels' bound in step (diagnostics, CPU fallback)
|
|
315
|
+
g._op(i).lmax = lv.lmax
|
|
316
|
+
|
|
317
|
+
def _estimate_lmax(self, i, its=12):
|
|
318
|
+
torch = self.torch
|
|
319
|
+
lv = self.levels[i]
|
|
320
|
+
prev = self._lmax_vec.get(i)
|
|
321
|
+
if prev is not None and tuple(prev.shape) == tuple(lv.node_shape):
|
|
322
|
+
v = prev * lv.mask
|
|
323
|
+
else:
|
|
324
|
+
rng = np.random.default_rng(i)
|
|
325
|
+
v = torch.as_tensor(rng.random(lv.node_shape) - 0.5, dtype=self.dtype, device=self.dev) * lv.mask
|
|
326
|
+
lam = 1.0
|
|
327
|
+
for _ in range(its):
|
|
328
|
+
nv = float(torch.linalg.vector_norm(v))
|
|
329
|
+
if nv == 0:
|
|
330
|
+
return 2.0
|
|
331
|
+
v = v / nv
|
|
332
|
+
av = lv.dinv * lv.apply(v) * lv.mask
|
|
333
|
+
lam = float(torch.dot(v.reshape(-1), av.reshape(-1)))
|
|
334
|
+
v = av
|
|
335
|
+
self._lmax_vec[i] = v
|
|
336
|
+
return LMAX_SAFETY * max(lam, 1e-12)
|
|
337
|
+
|
|
338
|
+
def _smooth(self, lv, b, x):
|
|
339
|
+
hi = lv.lmax
|
|
340
|
+
lo = hi / 30.0
|
|
341
|
+
theta = 0.5 * (hi + lo)
|
|
342
|
+
delta = 0.5 * (hi - lo)
|
|
343
|
+
sigma = theta / delta
|
|
344
|
+
rho = 1.0 / sigma
|
|
345
|
+
if x is None:
|
|
346
|
+
r = b.clone()
|
|
347
|
+
x = self.torch.zeros_like(b)
|
|
348
|
+
else:
|
|
349
|
+
r = b - lv.apply(x)
|
|
350
|
+
d = lv.dinv * r / theta
|
|
351
|
+
for it in range(self.g.degree):
|
|
352
|
+
x += d
|
|
353
|
+
if it + 1 == self.g.degree:
|
|
354
|
+
break
|
|
355
|
+
r -= lv.apply(d)
|
|
356
|
+
rho_new = 1.0 / (2.0 * sigma - rho)
|
|
357
|
+
d = rho_new * rho * d + (2.0 * rho_new / delta) * (lv.dinv * r)
|
|
358
|
+
rho = rho_new
|
|
359
|
+
return x
|
|
360
|
+
|
|
361
|
+
def _vcycle(self, i, b):
|
|
362
|
+
torch = self.torch
|
|
363
|
+
g = self.g
|
|
364
|
+
lv = self.levels[i]
|
|
365
|
+
if i == len(self.levels) - 1:
|
|
366
|
+
if g._coarse is not None:
|
|
367
|
+
xc = g._coarse_solve(b.cpu().numpy())
|
|
368
|
+
return torch.as_tensor(xc, dtype=self.dtype, device=self.dev)
|
|
369
|
+
x = self._smooth(lv, b, None)
|
|
370
|
+
return self._smooth(lv, b, x)
|
|
371
|
+
x = self._smooth(lv, b, None)
|
|
372
|
+
r = b - lv.apply(x)
|
|
373
|
+
cs = self.levels[i + 1].node_shape
|
|
374
|
+
rf = torch.zeros((3,) + tuple(2 * n - 1 for n in cs[1:]), dtype=r.dtype, device=self.dev)
|
|
375
|
+
sx, sy, sz = lv.node_shape[1:]
|
|
376
|
+
rf[:, :sx, :sy, :sz] = r * lv.mask
|
|
377
|
+
cm = self.levels[i + 1].mask
|
|
378
|
+
xc = self._vcycle(i + 1, _restrict(rf) * cm)
|
|
379
|
+
x += _prolong(xc * cm)[:, :sx, :sy, :sz] * lv.mask
|
|
380
|
+
return self._smooth(lv, b, x)
|
|
381
|
+
|
|
382
|
+
def _warm(self, A, b, x, norm, dot):
|
|
383
|
+
"""GridElasticity._pcg's warm start: the energy-optimal multiple of
|
|
384
|
+
x0, or zero if even that is worse. Returns (x, r)."""
|
|
385
|
+
ax = A.apply(x)
|
|
386
|
+
xax = dot(x, ax)
|
|
387
|
+
s = dot(b, x) / xax if xax > 0 else 0.0
|
|
388
|
+
if s > 0:
|
|
389
|
+
rs = b - s * ax
|
|
390
|
+
if norm(rs) < norm(b):
|
|
391
|
+
return x * s, rs
|
|
392
|
+
return x.zero_(), b.clone()
|
|
393
|
+
|
|
394
|
+
def solve(self, b, x0):
|
|
395
|
+
if self.fp32:
|
|
396
|
+
return self.solve_fp32(b, x0)
|
|
397
|
+
return self.solve_mixed(b, x0)
|
|
398
|
+
|
|
399
|
+
def _pcg32(self, r, tol, maxiter):
|
|
400
|
+
"""Plain float32 PCG on the fine level (rigid-free operator) for a
|
|
401
|
+
correction: K d = r to tol relative. Returns (d, iterations)."""
|
|
402
|
+
torch = self.torch
|
|
403
|
+
a = self.levels[0]
|
|
404
|
+
dot = lambda p, q: float(torch.dot(p.reshape(-1), q.reshape(-1)))
|
|
405
|
+
rnorm = float(torch.linalg.vector_norm(r))
|
|
406
|
+
d = torch.zeros_like(r)
|
|
407
|
+
if rnorm == 0:
|
|
408
|
+
return d, 0
|
|
409
|
+
r = r.clone()
|
|
410
|
+
z = self._vcycle(0, r)
|
|
411
|
+
p = z.clone()
|
|
412
|
+
rz = dot(r, z)
|
|
413
|
+
for it in range(1, maxiter + 1):
|
|
414
|
+
ap = a.apply_rf(p)
|
|
415
|
+
pap = dot(p, ap)
|
|
416
|
+
if pap <= 0:
|
|
417
|
+
return d, it
|
|
418
|
+
alpha = rz / pap
|
|
419
|
+
d.add_(p, alpha=alpha)
|
|
420
|
+
r.sub_(ap, alpha=alpha)
|
|
421
|
+
if float(torch.linalg.vector_norm(r)) <= tol * rnorm:
|
|
422
|
+
return d, it
|
|
423
|
+
z = self._vcycle(0, r)
|
|
424
|
+
rz_new = dot(r, z)
|
|
425
|
+
p.mul_(rz_new / rz).add_(z)
|
|
426
|
+
rz = rz_new
|
|
427
|
+
return d, maxiter
|
|
428
|
+
|
|
429
|
+
def solve_fp32(self, b, x0, inner_rtol=3e-3, inner_max=50, outer_max=40):
|
|
430
|
+
"""float32-only solve for devices without (fast) float64: iterative
|
|
431
|
+
refinement on the pair x = hi + lo. Each outer step forms the
|
|
432
|
+
residual with the rigid-free operator from the pair, solves the
|
|
433
|
+
correction with float32 PCG to 3e-3, and adds it in pair
|
|
434
|
+
arithmetic. Converged when the correction's energy d.r is under
|
|
435
|
+
rtol^2 of the solution's x.b: the residual's 2-norm floors at
|
|
436
|
+
float32 assembly noise on slender parts (internal forces dwarf the
|
|
437
|
+
load), noise that does not move the solution. Returns (x float64
|
|
438
|
+
numpy, V-cycles, converged)."""
|
|
439
|
+
torch = self.torch
|
|
440
|
+
g = self.g
|
|
441
|
+
a = self.levels[0]
|
|
442
|
+
b = np.asarray(b, np.float64)
|
|
443
|
+
if not np.any(b):
|
|
444
|
+
return np.zeros_like(b), 0, True
|
|
445
|
+
bh = torch.as_tensor(b.astype(np.float32), device=self.dev)
|
|
446
|
+
bl = torch.as_tensor((b - b.astype(np.float32)).astype(np.float32), device=self.dev)
|
|
447
|
+
dot = lambda p, q: float(torch.dot(p.reshape(-1), q.reshape(-1)))
|
|
448
|
+
xh = torch.zeros_like(bh)
|
|
449
|
+
xl = torch.zeros_like(bh)
|
|
450
|
+
if x0 is not None and np.any(x0):
|
|
451
|
+
# GridElasticity._pcg's warm start: the energy-optimal multiple
|
|
452
|
+
# of x0, or zero if even that is worse
|
|
453
|
+
hi = x0.astype(np.float32)
|
|
454
|
+
th = torch.as_tensor(hi, device=self.dev)
|
|
455
|
+
tl = torch.as_tensor((x0 - hi).astype(np.float32), device=self.dev)
|
|
456
|
+
ax = a.apply_rf(th, tl)
|
|
457
|
+
xax = dot(th, ax) + dot(tl, ax)
|
|
458
|
+
s = (dot(bh, th) + dot(bh, tl)) / xax if xax > 0 else 0.0
|
|
459
|
+
if s > 0 and float(torch.linalg.vector_norm(bh - s * ax)) < float(torch.linalg.vector_norm(bh)):
|
|
460
|
+
x = x0 * s
|
|
461
|
+
hi = x.astype(np.float32)
|
|
462
|
+
xh = torch.as_tensor(hi, device=self.dev)
|
|
463
|
+
xl = torch.as_tensor((x - hi).astype(np.float32), device=self.dev)
|
|
464
|
+
tol2 = g.rtol ** 2
|
|
465
|
+
total = 0
|
|
466
|
+
for _ in range(outer_max):
|
|
467
|
+
r = (bh - a.apply_rf(xh, xl)) + bl
|
|
468
|
+
d, it = self._pcg32(r, inner_rtol, inner_max)
|
|
469
|
+
total += it
|
|
470
|
+
xh, e = _two_sum(xh, d)
|
|
471
|
+
xl = xl + e
|
|
472
|
+
xh, xl = _two_sum(xh, xl)
|
|
473
|
+
de = abs(dot(d, r))
|
|
474
|
+
xe = dot(xh, bh)
|
|
475
|
+
if xe <= 0 or not np.isfinite(de):
|
|
476
|
+
break
|
|
477
|
+
if de <= tol2 * xe:
|
|
478
|
+
x = xh.cpu().numpy().astype(np.float64) + xl.cpu().numpy()
|
|
479
|
+
return x, total, True
|
|
480
|
+
x = xh.cpu().numpy().astype(np.float64) + xl.cpu().numpy()
|
|
481
|
+
return x, total, False
|
|
482
|
+
|
|
483
|
+
def solve_mixed(self, b, x0):
|
|
484
|
+
"""GridElasticity._pcg with torch: float64 CG on `cg_dev`, float32
|
|
485
|
+
V-cycle on `dev`. float64 b, x0 (node grids, masked) -> (x,
|
|
486
|
+
iterations, converged)."""
|
|
487
|
+
torch = self.torch
|
|
488
|
+
g = self.g
|
|
489
|
+
f64 = torch.float64
|
|
490
|
+
dot = lambda p, q: float(torch.dot(p.reshape(-1), q.reshape(-1)))
|
|
491
|
+
norm = lambda v: float(torch.linalg.vector_norm(v))
|
|
492
|
+
A = self.a64
|
|
493
|
+
cdev = self.cg_dev
|
|
494
|
+
bd = torch.as_tensor(b, dtype=f64, device=cdev).clone()
|
|
495
|
+
bnorm = norm(bd)
|
|
496
|
+
if bnorm == 0:
|
|
497
|
+
return np.zeros_like(b), 0, True
|
|
498
|
+
if x0 is not None:
|
|
499
|
+
x, r = self._warm(A, bd, torch.as_tensor(x0, dtype=f64, device=cdev).clone(), norm, dot)
|
|
500
|
+
else:
|
|
501
|
+
x, r = torch.zeros_like(bd), bd.clone()
|
|
502
|
+
pre = lambda r: self._vcycle(0, r.to(self.dev, self.dtype)).to(cdev, f64)
|
|
503
|
+
z = pre(r)
|
|
504
|
+
p = z.clone()
|
|
505
|
+
rz = dot(r, z)
|
|
506
|
+
tol = g.rtol * bnorm
|
|
507
|
+
for it in range(1, g.maxiter + 1):
|
|
508
|
+
ap = A.apply(p)
|
|
509
|
+
pap = dot(p, ap)
|
|
510
|
+
if pap <= 0:
|
|
511
|
+
return x.cpu().numpy(), it, False
|
|
512
|
+
alpha = rz / pap
|
|
513
|
+
x.add_(p, alpha=alpha)
|
|
514
|
+
r.sub_(ap, alpha=alpha)
|
|
515
|
+
if norm(r) <= tol:
|
|
516
|
+
return x.cpu().numpy(), it, True
|
|
517
|
+
z = pre(r)
|
|
518
|
+
rz_new = dot(r, z)
|
|
519
|
+
p.mul_(rz_new / rz).add_(z)
|
|
520
|
+
rz = rz_new
|
|
521
|
+
return x.cpu().numpy(), g.maxiter, False
|