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/narrowband.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Narrow-band sampling of an implicit field for meshing.
|
|
2
|
+
|
|
3
|
+
A part defined by a field f(P) < 0 only needs f near its surface: deep
|
|
4
|
+
inside or far outside, marching cubes finds nothing. `narrowband_mc` cuts
|
|
5
|
+
the meshing grid into blocks of `block` cells, samples f once per block
|
|
6
|
+
(at its centre) and skips every block whose centre is further from the
|
|
7
|
+
zero level than the field can change across the block (Lipschitz bound
|
|
8
|
+
`L` times the half-diagonal). Only the remaining blocks are sampled at
|
|
9
|
+
full resolution and marched, one block at a time.
|
|
10
|
+
|
|
11
|
+
The surviving blocks share their boundary samples with their neighbours,
|
|
12
|
+
and marching cubes puts a vertex on an edge from that edge's two samples
|
|
13
|
+
alone, so the blocks' meshes join exactly: after merging the shared
|
|
14
|
+
vertices the result is the same mesh dense marching cubes gives on the
|
|
15
|
+
whole grid (tests/test_narrowband.py checks this), at a fraction of the
|
|
16
|
+
field evaluations and with memory set by one batch of blocks, not by the
|
|
17
|
+
grid. That is what lets a field go to a finer pitch than a dense grid
|
|
18
|
+
fits in memory.
|
|
19
|
+
|
|
20
|
+
The skip test is only as safe as `L`. For a distance field (shapes,
|
|
21
|
+
booleans, shells, offsets) L = 1; smooth unions, remaps of a distance and
|
|
22
|
+
plain TPMS level sets in fields.py stay close to it, but a warp-graded
|
|
23
|
+
lattice reaches 30 or more. `lipschitz=None` estimates L as the steepest
|
|
24
|
+
gradient at 20k random points or between neighbouring block centres,
|
|
25
|
+
times `margin`, and never below 1. A bound that is still too low leaves
|
|
26
|
+
a crossing in a skipped leaf, which shows as a hole where it meets a
|
|
27
|
+
sampled one: fields.field_mesh checks the mesh is watertight and, if it
|
|
28
|
+
is not, marches again with every block sampled.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import math
|
|
33
|
+
import os
|
|
34
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
__all__ = ["narrowband_mc", "march_blocks", "peval", "watertight", "signed_volume", "shell_volumes", "face_cross"]
|
|
39
|
+
|
|
40
|
+
_BATCH = 2_000_000 # field samples per call
|
|
41
|
+
_CHUNK = 250_000 # field samples per thread task
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _threads():
|
|
45
|
+
env = os.environ.get("LOADPATH_THREADS")
|
|
46
|
+
if env:
|
|
47
|
+
return max(1, int(env))
|
|
48
|
+
try:
|
|
49
|
+
return max(1, len(os.sched_getaffinity(0)))
|
|
50
|
+
except AttributeError:
|
|
51
|
+
return max(1, os.cpu_count() or 1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_POOL = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def peval(field, P, chunk=_CHUNK):
|
|
58
|
+
"""field(P) as float64, in chunks spread over threads. The fields are
|
|
59
|
+
numpy expressions, which release the GIL, so 4 cores evaluate about
|
|
60
|
+
3.7x faster, and every chunk is the same computation, so the result is
|
|
61
|
+
bit for bit the serial one. The first chunk runs alone, so a field that
|
|
62
|
+
builds a cached resource on first use (a warp, a mesh's distance grid)
|
|
63
|
+
builds it once. LOADPATH_THREADS=1 turns this off. A field with a
|
|
64
|
+
`batch` attribute (the torch evaluator, which threads itself or runs
|
|
65
|
+
on a GPU) is called serially in chunks of that many points instead."""
|
|
66
|
+
global _POOL
|
|
67
|
+
n = len(P)
|
|
68
|
+
out = np.empty(n)
|
|
69
|
+
nt = _threads()
|
|
70
|
+
chunk = getattr(field, "chunk", None) or chunk
|
|
71
|
+
batch = getattr(field, "batch", None)
|
|
72
|
+
if batch:
|
|
73
|
+
nt, chunk = 1, batch
|
|
74
|
+
if nt == 1 or n <= 2 * chunk:
|
|
75
|
+
for i in range(0, n, chunk):
|
|
76
|
+
out[i:i + chunk] = np.asarray(field(P[i:i + chunk]), float)
|
|
77
|
+
return out
|
|
78
|
+
out[:chunk] = np.asarray(field(P[:chunk]), float)
|
|
79
|
+
if _POOL is None or _POOL._max_workers != nt:
|
|
80
|
+
_POOL = ThreadPoolExecutor(nt, thread_name_prefix="loadpath-field")
|
|
81
|
+
|
|
82
|
+
def one(i):
|
|
83
|
+
out[i:i + chunk] = np.asarray(field(P[i:i + chunk]), float)
|
|
84
|
+
|
|
85
|
+
for _ in _POOL.map(one, range(chunk, n, chunk)):
|
|
86
|
+
pass
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pblock(fn, P, rows, chunk=_CHUNK):
|
|
91
|
+
"""fn(P) for a function giving (rows, len(P)) blocks (a crease tree's
|
|
92
|
+
leaf values), in chunks spread over threads as peval does: the first
|
|
93
|
+
chunk alone, then the rest on the pool."""
|
|
94
|
+
global _POOL
|
|
95
|
+
n = len(P)
|
|
96
|
+
out = np.empty((rows, n))
|
|
97
|
+
nt = _threads()
|
|
98
|
+
if nt == 1 or n <= 2 * chunk:
|
|
99
|
+
for i in range(0, n, chunk):
|
|
100
|
+
out[:, i:i + chunk] = fn(P[i:i + chunk])
|
|
101
|
+
return out
|
|
102
|
+
out[:, :chunk] = fn(P[:chunk])
|
|
103
|
+
if _POOL is None or _POOL._max_workers != nt:
|
|
104
|
+
_POOL = ThreadPoolExecutor(nt, thread_name_prefix="loadpath-field")
|
|
105
|
+
|
|
106
|
+
def one(i):
|
|
107
|
+
out[:, i:i + chunk] = fn(P[i:i + chunk])
|
|
108
|
+
|
|
109
|
+
for _ in _POOL.map(one, range(chunk, n, chunk)):
|
|
110
|
+
pass
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _grid_points(lo, pitch, i0, n):
|
|
115
|
+
"""Points of the sub-grid starting at index i0 (3,) with n (3,) samples."""
|
|
116
|
+
ax = [lo[k] + (i0[k] + np.arange(n[k])) * pitch for k in range(3)]
|
|
117
|
+
X, Y, Z = np.meshgrid(*ax, indexing="ij")
|
|
118
|
+
return np.stack([X.ravel(), Y.ravel(), Z.ravel()], axis=1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _eval(field, P):
|
|
122
|
+
# grid samples (kept as float32 for marching cubes): lattice trig in
|
|
123
|
+
# single precision
|
|
124
|
+
from .lattice import single_precision
|
|
125
|
+
|
|
126
|
+
with single_precision():
|
|
127
|
+
return peval(field, P)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _lipschitz(field, lo, hi, pitch, n=20_000):
|
|
131
|
+
"""The steepest gradient seen at n random points (central differences)."""
|
|
132
|
+
P = np.random.default_rng(0).uniform(lo, hi, size=(n, 3))
|
|
133
|
+
h = 0.05 * pitch
|
|
134
|
+
E = np.eye(3) * h
|
|
135
|
+
f = _eval(field, np.concatenate([P + e for e in E] + [P - e for e in E])).reshape(6, n)
|
|
136
|
+
g = (f[:3] - f[3:]) / (2 * h)
|
|
137
|
+
return float(np.sqrt((g * g).sum(axis=0)).max())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_LOCAL = True
|
|
141
|
+
_LOCAL_MARGIN = 3.0 # local slopes come from differences a leaf apart, which smooth over a steep spot
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _local_lipschitz(fl, nl, step, margin, floor):
|
|
145
|
+
"""A Lipschitz bound per leaf (blocks, nl**3): the steepest difference
|
|
146
|
+
between its leaf centre and the next one along each axis (inside its
|
|
147
|
+
block), over the spacing, times `margin`, never below `floor`. The global
|
|
148
|
+
bound is the steepest slope anywhere, and a TPMS sheet's distance
|
|
149
|
+
(value over gradient) climbs steeply round the gradient's zeros, deep
|
|
150
|
+
in the channels, far from any wall: there it is 20-30, which keeps
|
|
151
|
+
every leaf of a lattice. Next to a wall the slope is ~1-3, and the
|
|
152
|
+
field varies over a cell (millimetres), not a leaf, so the local slope
|
|
153
|
+
is the one that matters for whether the leaf reaches the surface."""
|
|
154
|
+
F = fl.reshape(-1, nl, nl, nl)
|
|
155
|
+
G = np.zeros_like(F)
|
|
156
|
+
with np.errstate(invalid="ignore"):
|
|
157
|
+
for ax in range(1, 4):
|
|
158
|
+
d = np.abs(np.diff(F, axis=ax))
|
|
159
|
+
d = np.where(np.isnan(d), 0.0, d) # two empty leaves (inf - inf)
|
|
160
|
+
lo = [slice(None)] * 4
|
|
161
|
+
hi = [slice(None)] * 4
|
|
162
|
+
lo[ax] = slice(0, -1)
|
|
163
|
+
hi[ax] = slice(1, None)
|
|
164
|
+
G[tuple(lo)] = np.maximum(G[tuple(lo)], d)
|
|
165
|
+
G[tuple(hi)] = np.maximum(G[tuple(hi)], d)
|
|
166
|
+
return np.maximum(floor, margin * G / step).reshape(fl.shape)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def narrowband_mc(field, lo, dims, pitch, block=32, leaf=4, lipschitz=None, margin=1.5, budget=None,
|
|
170
|
+
progress=None):
|
|
171
|
+
"""Marching cubes of field < 0 on the grid lo + i * pitch, i < dims,
|
|
172
|
+
sampling only near the zero level. The grid's outer layer is forced
|
|
173
|
+
outside (+pitch), as fields.field_mesh does, so the mesh is closed.
|
|
174
|
+
|
|
175
|
+
Two levels: blocks of `block` cells are tested at their centres; in
|
|
176
|
+
the blocks that pass, leaves of `leaf` cells are tested the same way,
|
|
177
|
+
and only the samples of the leaves that pass are evaluated. A sample
|
|
178
|
+
of a block that no passing leaf touches gets the sign of its leaf at
|
|
179
|
+
a large magnitude: with the Lipschitz bound holding, every edge the
|
|
180
|
+
surface crosses has both its samples evaluated, so the block's mesh is
|
|
181
|
+
the dense one. Each block is marched on its own and the shared
|
|
182
|
+
vertices merged. Raises ValueError, before the fine pass, when the
|
|
183
|
+
passing leaves need more than `budget` samples. Returns (verts (mm),
|
|
184
|
+
faces, stats)."""
|
|
185
|
+
stats = {}
|
|
186
|
+
verts, faces, nv = [], [], 0
|
|
187
|
+
for i0, n, v, fa in march_blocks(field, lo, dims, pitch, block, leaf, lipschitz, margin, budget, progress,
|
|
188
|
+
stats):
|
|
189
|
+
verts.append(v)
|
|
190
|
+
faces.append(fa + nv)
|
|
191
|
+
nv += len(v)
|
|
192
|
+
if not verts:
|
|
193
|
+
return np.zeros((0, 3)), np.zeros((0, 3), int), stats
|
|
194
|
+
V, Fc = merge_blocks(np.concatenate(verts), np.concatenate(faces), block)
|
|
195
|
+
return np.asarray(lo, float) + V * pitch, Fc, stats
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def merge_blocks(V, F, block):
|
|
199
|
+
"""Merge the vertices the blocks share. A vertex on a block's face comes
|
|
200
|
+
out of both blocks bit for bit (same edge, same two samples) and every
|
|
201
|
+
other vertex is unique already, so only the ones on a block plane (a
|
|
202
|
+
coordinate that is a multiple of `block`) go through the (slow) row
|
|
203
|
+
unique. Drops the faces the merge collapses."""
|
|
204
|
+
on = (np.mod(V, block) == 0).any(axis=1)
|
|
205
|
+
idx = np.flatnonzero(on)
|
|
206
|
+
remap = np.arange(len(V))
|
|
207
|
+
if len(idx):
|
|
208
|
+
_, first, inv = np.unique(V[idx], axis=0, return_index=True, return_inverse=True)
|
|
209
|
+
remap[idx] = idx[first][inv.reshape(-1)]
|
|
210
|
+
keep_v = remap == np.arange(len(V))
|
|
211
|
+
new = np.cumsum(keep_v) - 1
|
|
212
|
+
F = new[remap[F]]
|
|
213
|
+
V = V[keep_v]
|
|
214
|
+
ok = (F[:, 0] != F[:, 1]) & (F[:, 1] != F[:, 2]) & (F[:, 0] != F[:, 2])
|
|
215
|
+
return V, F[ok]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def march_blocks(field, lo, dims, pitch, block=32, leaf=4, lipschitz=None, margin=1.5, budget=None,
|
|
219
|
+
progress=None, stats=None):
|
|
220
|
+
"""The narrow-band blocks of narrowband_mc, marched one at a time, in
|
|
221
|
+
x-major order (all the blocks of one x slab before the next): yields
|
|
222
|
+
(i0, n, verts, faces) per block that the surface crosses, verts in grid
|
|
223
|
+
index units (add lo, times pitch, for mm). Fills `stats` once the
|
|
224
|
+
blocks are planned. The field is evaluated in batches of blocks; memory
|
|
225
|
+
is one batch, whatever the grid."""
|
|
226
|
+
from skimage.measure import marching_cubes
|
|
227
|
+
|
|
228
|
+
lo = np.asarray(lo, float)
|
|
229
|
+
dims = np.asarray(dims, int)
|
|
230
|
+
B, lf = int(block), int(leaf)
|
|
231
|
+
if B % lf:
|
|
232
|
+
raise ValueError("block must be a multiple of leaf")
|
|
233
|
+
ncells = dims - 1
|
|
234
|
+
nb = np.maximum(1, np.ceil(ncells / B).astype(int))
|
|
235
|
+
|
|
236
|
+
def centres(i0, i1):
|
|
237
|
+
"""Centres (mm) and half-diagonals of the boxes [i0, i1) in cells."""
|
|
238
|
+
return lo + 0.5 * (i0 + i1) * pitch, 0.5 * np.linalg.norm((i1 - i0) * pitch, axis=-1)
|
|
239
|
+
|
|
240
|
+
bi = np.stack(np.meshgrid(*[np.arange(n) for n in nb], indexing="ij"), -1).reshape(-1, 3)
|
|
241
|
+
b0 = bi * B
|
|
242
|
+
b1 = np.minimum(b0 + B, ncells)
|
|
243
|
+
C, half = centres(b0, b1)
|
|
244
|
+
fc = _eval(field, C)
|
|
245
|
+
if lipschitz is None:
|
|
246
|
+
L = _lipschitz(field, lo, lo + ncells * pitch, pitch)
|
|
247
|
+
F3 = fc.reshape(tuple(nb))
|
|
248
|
+
for k in range(3):
|
|
249
|
+
if nb[k] > 1:
|
|
250
|
+
L = max(L, float(np.max(np.abs(np.diff(F3, axis=k)))) / (B * pitch))
|
|
251
|
+
L = max(1.0, L * margin)
|
|
252
|
+
else:
|
|
253
|
+
L = float(lipschitz)
|
|
254
|
+
on_border = (b0 == 0).any(axis=1) | (b1 == ncells).any(axis=1)
|
|
255
|
+
act = (np.abs(fc) <= L * half + pitch) | (on_border & (fc < 0))
|
|
256
|
+
bi, b0, b1 = bi[act], b0[act], b1[act]
|
|
257
|
+
|
|
258
|
+
# leaves of the passing blocks
|
|
259
|
+
nl = B // lf
|
|
260
|
+
li = np.stack(np.meshgrid(*[np.arange(nl)] * 3, indexing="ij"), -1).reshape(-1, 3)
|
|
261
|
+
l0 = (b0[:, None, :] + li[None] * lf)
|
|
262
|
+
l1 = np.minimum(l0 + lf, b1[:, None, :])
|
|
263
|
+
real = (l0 < b1[:, None, :]).all(axis=2) # leaves past a short block's end are empty
|
|
264
|
+
Cl, hl = centres(l0.reshape(-1, 3), l1.reshape(-1, 3))
|
|
265
|
+
fl = np.full(len(Cl), np.inf)
|
|
266
|
+
rf = real.ravel()
|
|
267
|
+
fl[rf] = _eval(field, Cl[rf])
|
|
268
|
+
fl = fl.reshape(len(b0), -1)
|
|
269
|
+
Ll = L
|
|
270
|
+
if lipschitz is None and _LOCAL:
|
|
271
|
+
Ll = np.minimum(L, _local_lipschitz(fl, nl, lf * pitch, _LOCAL_MARGIN, max(1.0, margin)))
|
|
272
|
+
lon = ((l0 == 0) | (l1 == ncells)).any(axis=2)
|
|
273
|
+
lact = real & ((np.abs(fl) <= Ll * hl.reshape(len(b0), -1) + pitch) | (lon & (fl < 0)))
|
|
274
|
+
keep = lact.any(axis=1)
|
|
275
|
+
b0, b1, fl, lact = b0[keep], b1[keep], fl[keep], lact[keep]
|
|
276
|
+
|
|
277
|
+
def mask(k):
|
|
278
|
+
"""The samples block k needs: every sample of a passing leaf."""
|
|
279
|
+
n = b1[k] - b0[k] + 1
|
|
280
|
+
# a leaf's samples run from its first corner to one past its last
|
|
281
|
+
# cell: its cells, grown by one sample on the high side of each axis
|
|
282
|
+
c = np.zeros((nl * lf + 1,) * 3, bool)
|
|
283
|
+
c[:-1, :-1, :-1] = np.repeat(np.repeat(np.repeat(lact[k].reshape(nl, nl, nl), lf, 0), lf, 1), lf, 2)
|
|
284
|
+
m = c.copy()
|
|
285
|
+
m[1:] |= c[:-1]
|
|
286
|
+
m[:, 1:] |= m[:, :-1].copy()
|
|
287
|
+
m[:, :, 1:] |= m[:, :, :-1].copy()
|
|
288
|
+
return m[:n[0], :n[1], :n[2]]
|
|
289
|
+
|
|
290
|
+
fine = None
|
|
291
|
+
if budget is not None:
|
|
292
|
+
fine = sum(int(mask(k).sum()) for k in range(len(b0)))
|
|
293
|
+
if fine > budget:
|
|
294
|
+
raise ValueError(f"the surface needs {fine / 1e6:.1f}M samples at pitch {pitch:g}, over the "
|
|
295
|
+
f"{budget / 1e6:.0f}M budget: use a coarser pitch")
|
|
296
|
+
if stats is None:
|
|
297
|
+
stats = {}
|
|
298
|
+
stats.update({"blocks": int(np.prod(nb)), "activeBlocks": int(len(b0)), "leaves": int(lact.sum()),
|
|
299
|
+
"lipschitz": round(float(L), 3) if np.isfinite(L) else None,
|
|
300
|
+
"samples": int(len(C) + rf.sum()), "denseSamples": int(np.prod(dims))})
|
|
301
|
+
|
|
302
|
+
big = np.float32(1e3 * pitch)
|
|
303
|
+
k = 0
|
|
304
|
+
while k < len(b0):
|
|
305
|
+
# a batch of blocks, one field call
|
|
306
|
+
group, P, cnt = [], [], 0
|
|
307
|
+
while k < len(b0) and (not group or cnt < _BATCH):
|
|
308
|
+
n = b1[k] - b0[k] + 1
|
|
309
|
+
ii = np.argwhere(mask(k))
|
|
310
|
+
P.append(lo + (b0[k] + ii) * pitch)
|
|
311
|
+
group.append((k, n, ii))
|
|
312
|
+
cnt += len(ii)
|
|
313
|
+
k += 1
|
|
314
|
+
F = _eval(field, np.concatenate(P)).astype(np.float32)
|
|
315
|
+
del P
|
|
316
|
+
stats["samples"] += len(F)
|
|
317
|
+
o = 0
|
|
318
|
+
for kk, n, ii in group:
|
|
319
|
+
# unevaluated samples: their leaf's sign, far from zero
|
|
320
|
+
lix = np.minimum(np.indices(tuple(n)).reshape(3, -1).T // lf, nl - 1)
|
|
321
|
+
sgn = np.sign(fl[kk][(lix[:, 0] * nl + lix[:, 1]) * nl + lix[:, 2]])
|
|
322
|
+
f = (sgn * big).astype(np.float32).reshape(tuple(n))
|
|
323
|
+
f[tuple(ii.T)] = F[o:o + len(ii)]
|
|
324
|
+
o += len(ii)
|
|
325
|
+
i0 = b0[kk]
|
|
326
|
+
for ax in range(3):
|
|
327
|
+
if i0[ax] == 0:
|
|
328
|
+
f[(slice(None),) * ax + (0,)] = pitch
|
|
329
|
+
if i0[ax] + n[ax] == dims[ax]:
|
|
330
|
+
f[(slice(None),) * ax + (-1,)] = pitch
|
|
331
|
+
f[f == 0.0] = -1e-4 * pitch
|
|
332
|
+
if f.min() >= 0 or f.max() <= 0:
|
|
333
|
+
continue
|
|
334
|
+
v, fa, _, _ = marching_cubes(f, 0.0)
|
|
335
|
+
yield i0, n, v.astype(np.float64) + i0, fa
|
|
336
|
+
if progress:
|
|
337
|
+
progress({"stage": "field", "done": int(k), "of": int(len(b0))})
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---- fast bookkeeping on big triangle meshes (trimesh's takes tens of seconds) ----
|
|
341
|
+
|
|
342
|
+
def watertight(F, nv=None):
|
|
343
|
+
"""Every edge shared by exactly two faces (trimesh's is_watertight)."""
|
|
344
|
+
F = np.asarray(F, np.int64)
|
|
345
|
+
if not len(F):
|
|
346
|
+
return False
|
|
347
|
+
nv = int(F.max()) + 1 if nv is None else int(nv)
|
|
348
|
+
a = F.ravel()
|
|
349
|
+
b = np.roll(F, -1, axis=1).ravel()
|
|
350
|
+
e = np.sort(np.minimum(a, b) * nv + np.maximum(a, b))
|
|
351
|
+
if len(e) % 2:
|
|
352
|
+
return False
|
|
353
|
+
if not np.array_equal(e[0::2], e[1::2]):
|
|
354
|
+
return False
|
|
355
|
+
return bool(len(e) < 4 or not np.any(e[1:-1:2] == e[2::2]))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def face_cross(V, F):
|
|
359
|
+
"""(b - a) x (c - a) for every face (a, b, c): twice its area along its
|
|
360
|
+
normal (compiled, meshops.py; else column by column in numpy)."""
|
|
361
|
+
from .meshops import face_cross as fc
|
|
362
|
+
|
|
363
|
+
return fc(V, F)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _face_cross_np(V, F):
|
|
367
|
+
# column by column, ~3x numpy's cross on (n, 3) rows
|
|
368
|
+
VT = np.ascontiguousarray(np.asarray(V, float).T)
|
|
369
|
+
a = VT[:, F[:, 0]]
|
|
370
|
+
b = VT[:, F[:, 1]]
|
|
371
|
+
c = VT[:, F[:, 2]]
|
|
372
|
+
b -= a
|
|
373
|
+
c -= a
|
|
374
|
+
out = np.empty((len(F), 3))
|
|
375
|
+
out[:, 0] = b[1] * c[2] - b[2] * c[1]
|
|
376
|
+
out[:, 1] = b[2] * c[0] - b[0] * c[2]
|
|
377
|
+
out[:, 2] = b[0] * c[1] - b[1] * c[0]
|
|
378
|
+
return out
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _triple(V, F):
|
|
382
|
+
"""a . (b x c) per face (six times its signed volume from the origin)."""
|
|
383
|
+
from .meshops import triple
|
|
384
|
+
|
|
385
|
+
return triple(V, F)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _triple_np(V, F):
|
|
389
|
+
VT = np.ascontiguousarray(np.asarray(V, float).T)
|
|
390
|
+
a = VT[:, F[:, 0]]
|
|
391
|
+
b = VT[:, F[:, 1]]
|
|
392
|
+
c = VT[:, F[:, 2]]
|
|
393
|
+
return a[0] * (b[1] * c[2] - b[2] * c[1]) + a[1] * (b[2] * c[0] - b[0] * c[2]) + a[2] * (b[0] * c[1] - b[1] * c[0])
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def signed_volume(V, F):
|
|
397
|
+
"""Enclosed volume (divergence theorem), positive with outward normals."""
|
|
398
|
+
return float(_triple(V, F).sum() / 6.0)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def shell_volumes(V, F):
|
|
402
|
+
"""(label per face, signed volume per connected shell): shells joined
|
|
403
|
+
through shared vertices. A closed body's outside is positive, the wall
|
|
404
|
+
of a void sealed inside it negative."""
|
|
405
|
+
import scipy.sparse as sp
|
|
406
|
+
from scipy.sparse.csgraph import connected_components
|
|
407
|
+
|
|
408
|
+
n = len(V)
|
|
409
|
+
A = sp.coo_matrix((np.ones(2 * len(F), np.int8), (np.r_[F[:, 0], F[:, 1]], np.r_[F[:, 1], F[:, 2]])),
|
|
410
|
+
shape=(n, n))
|
|
411
|
+
_, lab = connected_components(A, directed=False)
|
|
412
|
+
fl = lab[F[:, 0]]
|
|
413
|
+
vol = np.bincount(fl, weights=_triple(V, F) / 6.0)
|
|
414
|
+
# renumber to the shells that have faces
|
|
415
|
+
used = np.unique(fl)
|
|
416
|
+
ren = np.full(lab.max() + 1, -1)
|
|
417
|
+
ren[used] = np.arange(len(used))
|
|
418
|
+
return ren[fl], vol[used]
|
loadpath/nlkernel.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compiled element kernel for the large-deflection solver (nonlinear.py).
|
|
3
|
+
|
|
4
|
+
nonlinear._element builds each hexahedron's internal force and tangent from
|
|
5
|
+
stacks of small numpy operations (3x3 inverses, determinants, 4th-order
|
|
6
|
+
einsums, transposed copies), whose overhead, not their arithmetic, was most
|
|
7
|
+
of a large-deflection solve: about 54 us per element per Newton iteration on
|
|
8
|
+
the 1056-cell tendon finger. Here the same element (total Lagrangian hex8,
|
|
9
|
+
2x2x2 Gauss, neo-Hookean or St. Venant-Kirchhoff, optionally with the nine
|
|
10
|
+
incompatible modes condensed out) is one compiled loop over elements, run on
|
|
11
|
+
all cores, and the scatter into the CSR tangent is a second compiled loop.
|
|
12
|
+
|
|
13
|
+
Same arithmetic as the numpy element, in a different summation order, so
|
|
14
|
+
forces and tangents agree to ~1e-14 relative, not bit for bit.
|
|
15
|
+
LOADPATH_NLKERNEL=0 runs the numpy element instead.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import numba as _nb
|
|
25
|
+
except ImportError: # pragma: no cover - numba is a dependency, kept optional
|
|
26
|
+
_nb = None
|
|
27
|
+
|
|
28
|
+
__all__ = ["available", "elements", "scatter"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def available() -> bool:
|
|
32
|
+
return _nb is not None and os.environ.get("LOADPATH_NLKERNEL", "1") != "0"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if _nb is not None:
|
|
36
|
+
@_nb.njit(cache=True, nogil=True)
|
|
37
|
+
def _gauss(ux, G, h, lam, mu, svk, fx, kx):
|
|
38
|
+
"""Accumulate internal force fx (3n) and tangent kx (3n, 3n) of one
|
|
39
|
+
element with generalized displacements ux (n, 3); returns min det F."""
|
|
40
|
+
nn = ux.shape[0]
|
|
41
|
+
w = h ** 3 / 8.0
|
|
42
|
+
wk = w / (h * h)
|
|
43
|
+
jmin = np.inf
|
|
44
|
+
F = np.empty((3, 3))
|
|
45
|
+
S = np.empty((3, 3))
|
|
46
|
+
P = np.empty((3, 3))
|
|
47
|
+
Ci = np.empty((3, 3))
|
|
48
|
+
A = np.empty((3, 3, 3, 3))
|
|
49
|
+
FA = np.empty((3, 3, 3, 3))
|
|
50
|
+
Q = np.empty((3, 3, 3, 3))
|
|
51
|
+
T = np.empty((nn, 3, 3, 3))
|
|
52
|
+
for g in range(G.shape[0]):
|
|
53
|
+
for i in range(3):
|
|
54
|
+
for j in range(3):
|
|
55
|
+
s = 1.0 if i == j else 0.0
|
|
56
|
+
for a in range(nn):
|
|
57
|
+
s += ux[a, i] * G[g, a, j] / h
|
|
58
|
+
F[i, j] = s
|
|
59
|
+
J = (F[0, 0] * (F[1, 1] * F[2, 2] - F[1, 2] * F[2, 1])
|
|
60
|
+
- F[0, 1] * (F[1, 0] * F[2, 2] - F[1, 2] * F[2, 0])
|
|
61
|
+
+ F[0, 2] * (F[1, 0] * F[2, 1] - F[1, 1] * F[2, 0]))
|
|
62
|
+
if J < jmin:
|
|
63
|
+
jmin = J
|
|
64
|
+
# C = F^T F
|
|
65
|
+
c00 = F[0, 0] * F[0, 0] + F[1, 0] * F[1, 0] + F[2, 0] * F[2, 0]
|
|
66
|
+
c11 = F[0, 1] * F[0, 1] + F[1, 1] * F[1, 1] + F[2, 1] * F[2, 1]
|
|
67
|
+
c22 = F[0, 2] * F[0, 2] + F[1, 2] * F[1, 2] + F[2, 2] * F[2, 2]
|
|
68
|
+
c01 = F[0, 0] * F[0, 1] + F[1, 0] * F[1, 1] + F[2, 0] * F[2, 1]
|
|
69
|
+
c12 = F[0, 1] * F[0, 2] + F[1, 1] * F[1, 2] + F[2, 1] * F[2, 2]
|
|
70
|
+
c02 = F[0, 0] * F[0, 2] + F[1, 0] * F[1, 2] + F[2, 0] * F[2, 2]
|
|
71
|
+
if svk:
|
|
72
|
+
e00, e11, e22 = 0.5 * (c00 - 1.0), 0.5 * (c11 - 1.0), 0.5 * (c22 - 1.0)
|
|
73
|
+
tr = e00 + e11 + e22
|
|
74
|
+
S[0, 0] = lam * tr + 2 * mu * e00
|
|
75
|
+
S[1, 1] = lam * tr + 2 * mu * e11
|
|
76
|
+
S[2, 2] = lam * tr + 2 * mu * e22
|
|
77
|
+
S[0, 1] = S[1, 0] = mu * c01
|
|
78
|
+
S[1, 2] = S[2, 1] = mu * c12
|
|
79
|
+
S[0, 2] = S[2, 0] = mu * c02
|
|
80
|
+
for i in range(3):
|
|
81
|
+
for j in range(3):
|
|
82
|
+
for k in range(3):
|
|
83
|
+
for l in range(3):
|
|
84
|
+
A[i, j, k, l] = (lam * (i == j) * (k == l)
|
|
85
|
+
+ mu * ((i == k) * (j == l) + (i == l) * (j == k)))
|
|
86
|
+
else:
|
|
87
|
+
# C^-1 by cofactors (C symmetric); det C = J^2
|
|
88
|
+
d = c00 * (c11 * c22 - c12 * c12) - c01 * (c01 * c22 - c12 * c02) + c02 * (c01 * c12 - c11 * c02)
|
|
89
|
+
Ci[0, 0] = (c11 * c22 - c12 * c12) / d
|
|
90
|
+
Ci[1, 1] = (c00 * c22 - c02 * c02) / d
|
|
91
|
+
Ci[2, 2] = (c00 * c11 - c01 * c01) / d
|
|
92
|
+
Ci[0, 1] = Ci[1, 0] = (c02 * c12 - c01 * c22) / d
|
|
93
|
+
Ci[1, 2] = Ci[2, 1] = (c01 * c02 - c00 * c12) / d
|
|
94
|
+
Ci[0, 2] = Ci[2, 0] = (c01 * c12 - c02 * c11) / d
|
|
95
|
+
lnJ = np.log(max(J, 1e-12))
|
|
96
|
+
for i in range(3):
|
|
97
|
+
for j in range(3):
|
|
98
|
+
S[i, j] = mu * ((1.0 if i == j else 0.0) - Ci[i, j]) + lam * lnJ * Ci[i, j]
|
|
99
|
+
cc = mu - lam * lnJ
|
|
100
|
+
for i in range(3):
|
|
101
|
+
for j in range(3):
|
|
102
|
+
for k in range(3):
|
|
103
|
+
for l in range(3):
|
|
104
|
+
A[i, j, k, l] = (lam * Ci[i, j] * Ci[k, l]
|
|
105
|
+
+ cc * (Ci[i, k] * Ci[j, l] + Ci[i, l] * Ci[j, k]))
|
|
106
|
+
for i in range(3):
|
|
107
|
+
for l in range(3):
|
|
108
|
+
P[i, l] = F[i, 0] * S[0, l] + F[i, 1] * S[1, l] + F[i, 2] * S[2, l]
|
|
109
|
+
for a in range(nn):
|
|
110
|
+
for i in range(3):
|
|
111
|
+
fx[3 * a + i] += w * (P[i, 0] * G[g, a, 0] + P[i, 1] * G[g, a, 1] + P[i, 2] * G[g, a, 2]) / h
|
|
112
|
+
# Q_iljn = F_ik A_klpn F_jp + S_ln delta_ij
|
|
113
|
+
for i in range(3):
|
|
114
|
+
for l in range(3):
|
|
115
|
+
for p in range(3):
|
|
116
|
+
for n in range(3):
|
|
117
|
+
FA[i, l, p, n] = F[i, 0] * A[0, l, p, n] + F[i, 1] * A[1, l, p, n] + F[i, 2] * A[2, l, p, n]
|
|
118
|
+
for i in range(3):
|
|
119
|
+
for l in range(3):
|
|
120
|
+
for j in range(3):
|
|
121
|
+
for n in range(3):
|
|
122
|
+
v = FA[i, l, 0, n] * F[j, 0] + FA[i, l, 1, n] * F[j, 1] + FA[i, l, 2, n] * F[j, 2]
|
|
123
|
+
if i == j:
|
|
124
|
+
v += S[l, n]
|
|
125
|
+
Q[i, l, j, n] = v
|
|
126
|
+
# K_aibj += wk G_al Q_iljn G_bn, upper triangle in (a, b)
|
|
127
|
+
for a in range(nn):
|
|
128
|
+
for i in range(3):
|
|
129
|
+
for j in range(3):
|
|
130
|
+
for n in range(3):
|
|
131
|
+
T[a, i, j, n] = G[g, a, 0] * Q[i, 0, j, n] + G[g, a, 1] * Q[i, 1, j, n] + G[g, a, 2] * Q[i, 2, j, n]
|
|
132
|
+
for a in range(nn):
|
|
133
|
+
for b in range(a, nn):
|
|
134
|
+
gb0, gb1, gb2 = G[g, b, 0], G[g, b, 1], G[g, b, 2]
|
|
135
|
+
for i in range(3):
|
|
136
|
+
for j in range(3):
|
|
137
|
+
kx[3 * a + i, 3 * b + j] += wk * (T[a, i, j, 0] * gb0 + T[a, i, j, 1] * gb1 + T[a, i, j, 2] * gb2)
|
|
138
|
+
n3 = 3 * nn
|
|
139
|
+
for r in range(n3):
|
|
140
|
+
for c in range(r):
|
|
141
|
+
if c // 3 < r // 3:
|
|
142
|
+
kx[r, c] = kx[c, r]
|
|
143
|
+
return jmin
|
|
144
|
+
|
|
145
|
+
@_nb.njit(cache=True, parallel=True)
|
|
146
|
+
def _elements(u, edof, alpha, mod, h, lam, mu, svk, tangent, G, im):
|
|
147
|
+
m = edof.shape[0]
|
|
148
|
+
nn = G.shape[1]
|
|
149
|
+
fe = np.zeros((m, 24))
|
|
150
|
+
ke = np.zeros((m, 24, 24)) if tangent else np.zeros((1, 24, 24))
|
|
151
|
+
jm = np.empty(m)
|
|
152
|
+
anew = np.zeros((m, 3, 3)) if im else np.zeros((1, 3, 3))
|
|
153
|
+
ra = np.zeros((m, 9)) if im else np.zeros((1, 9))
|
|
154
|
+
cpl = np.zeros((m, 9, 24)) if im else np.zeros((1, 9, 24))
|
|
155
|
+
for e in _nb.prange(m):
|
|
156
|
+
ux = np.zeros((nn, 3))
|
|
157
|
+
for a in range(8):
|
|
158
|
+
for i in range(3):
|
|
159
|
+
ux[a, i] = u[edof[e, 3 * a + i]]
|
|
160
|
+
if im:
|
|
161
|
+
for a in range(3):
|
|
162
|
+
for i in range(3):
|
|
163
|
+
ux[8 + a, i] = alpha[e, a, i]
|
|
164
|
+
fx = np.zeros(3 * nn)
|
|
165
|
+
kx = np.zeros((3 * nn, 3 * nn))
|
|
166
|
+
jm[e] = _gauss(ux, G, h, lam, mu, svk, fx, kx)
|
|
167
|
+
me = mod[e]
|
|
168
|
+
if not im:
|
|
169
|
+
for r in range(24):
|
|
170
|
+
fe[e, r] = fx[r] * me
|
|
171
|
+
if tangent:
|
|
172
|
+
for r in range(24):
|
|
173
|
+
for c in range(24):
|
|
174
|
+
ke[e, r, c] = kx[r, c] * me
|
|
175
|
+
continue
|
|
176
|
+
# condense the nine incompatible modes: one local Newton step
|
|
177
|
+
kaa = kx[24:, 24:].copy()
|
|
178
|
+
rhs = np.empty((9, 25))
|
|
179
|
+
for r in range(9):
|
|
180
|
+
rhs[r, 0] = fx[24 + r]
|
|
181
|
+
for c in range(24):
|
|
182
|
+
rhs[r, 1 + c] = kx[c, 24 + r] # K_au = K_ua^T
|
|
183
|
+
sol = np.linalg.solve(kaa, rhs)
|
|
184
|
+
for r in range(24):
|
|
185
|
+
s = fx[r]
|
|
186
|
+
for q in range(9):
|
|
187
|
+
s -= kx[r, 24 + q] * sol[q, 0]
|
|
188
|
+
fe[e, r] = s * me
|
|
189
|
+
if tangent:
|
|
190
|
+
for r in range(24):
|
|
191
|
+
for c in range(24):
|
|
192
|
+
s = kx[r, c]
|
|
193
|
+
for q in range(9):
|
|
194
|
+
s -= kx[r, 24 + q] * sol[q, 1 + c]
|
|
195
|
+
ke[e, r, c] = s * me
|
|
196
|
+
for a in range(3):
|
|
197
|
+
for i in range(3):
|
|
198
|
+
anew[e, a, i] = alpha[e, a, i] - sol[3 * a + i, 0]
|
|
199
|
+
for q in range(9):
|
|
200
|
+
ra[e, q] = fx[24 + q]
|
|
201
|
+
for c in range(24):
|
|
202
|
+
cpl[e, q, c] = sol[q, 1 + c]
|
|
203
|
+
return fe, ke, jm, anew, ra, cpl
|
|
204
|
+
|
|
205
|
+
@_nb.njit(cache=True, nogil=True)
|
|
206
|
+
def _scatter(data, slots, ke, fint, fe, edof, tangent):
|
|
207
|
+
m = edof.shape[0]
|
|
208
|
+
for e in range(m):
|
|
209
|
+
for r in range(24):
|
|
210
|
+
fint[edof[e, r]] += fe[e, r]
|
|
211
|
+
if tangent:
|
|
212
|
+
for a in range(8):
|
|
213
|
+
for b in range(8):
|
|
214
|
+
p = 8 * a + b
|
|
215
|
+
for i in range(3):
|
|
216
|
+
for j in range(3):
|
|
217
|
+
data[slots[p, e, 3 * i + j]] += ke[e, 3 * a + i, 3 * b + j]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def elements(u, edof, alpha, mod, h, nu, kind, tangent, grads):
|
|
221
|
+
"""All elements' (fint (m, 24), ke (m, 24, 24), det F per element,
|
|
222
|
+
alpha_new, R_alpha (m, 9), K_aa^-1 K_au (m, 9, 24)), scaled by the
|
|
223
|
+
moduli (fint, ke) as nonlinear._assemble scatters them. grads: (8, n, 3)
|
|
224
|
+
unit-cube gradients, n = 8 (plain) or 11 (incompatible modes)."""
|
|
225
|
+
lam, mu = nu / ((1 + nu) * (1 - 2 * nu)), 1.0 / (2 * (1 + nu))
|
|
226
|
+
im = alpha is not None
|
|
227
|
+
a = np.ascontiguousarray(alpha, float) if im else np.zeros((1, 3, 3))
|
|
228
|
+
return _elements(np.ascontiguousarray(u, float), np.ascontiguousarray(edof), a,
|
|
229
|
+
np.ascontiguousarray(mod, float), float(h), lam, mu, kind == "svk", bool(tangent),
|
|
230
|
+
np.ascontiguousarray(grads, float), im)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def scatter(n_dof, nnz, slots, edof, fe, ke, tangent):
|
|
234
|
+
"""Global internal force and the CSR data array of the tangent."""
|
|
235
|
+
fint = np.zeros(n_dof)
|
|
236
|
+
data = np.zeros(nnz) if tangent else np.zeros(1)
|
|
237
|
+
if ke is None or not tangent:
|
|
238
|
+
ke = np.zeros((1, 24, 24))
|
|
239
|
+
_scatter(data, np.ascontiguousarray(slots), np.ascontiguousarray(ke), fint, np.ascontiguousarray(fe),
|
|
240
|
+
np.ascontiguousarray(edof), bool(tangent))
|
|
241
|
+
return fint, (data if tangent else None)
|