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/cutcell.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cut cells: the voxel analysis with the part's true boundary inside each cell.
|
|
3
|
+
|
|
4
|
+
verify.py keeps a cell when its centre is inside the part and gives it the
|
|
5
|
+
whole cube's stiffness. Member thickness then rounds to whole cells, and a
|
|
6
|
+
member two or three cells thick reads its bending stress tens of percent
|
|
7
|
+
off, either way (benchmarks/beams.py: 0.75-1.28x CalculiX at 2 cells,
|
|
8
|
+
30-45 degrees to the grid). This module keeps the same grid, nodes and
|
|
9
|
+
solvers and changes two things:
|
|
10
|
+
|
|
11
|
+
- Stiffness: every cell the part touches (fill >= MIN_FILL) is kept, and a
|
|
12
|
+
cell the boundary cuts gets its stiffness integrated over the part of it
|
|
13
|
+
that is solid: an n x n x n split of the cell, each solid sub-cell
|
|
14
|
+
integrated exactly (2x2x2 Gauss), the finite cell method. The element is
|
|
15
|
+
fem.py's incompatible-mode hex8; on a cut cell the bubbles' strain is made
|
|
16
|
+
mean-free over the solid part (Taylor's correction on the material
|
|
17
|
+
domain), so the constant-strain patch test still holds. Full cells keep
|
|
18
|
+
the shared unit-cube matrix, so the assembled path adds the cut cells'
|
|
19
|
+
matrices as explicit elements and the matrix-free multigrid takes them as
|
|
20
|
+
an extra sparse operator (mg.GridElasticity's `springs`, which its
|
|
21
|
+
Galerkin coarsening carries down exactly).
|
|
22
|
+
- Stress: a cut cell's corners can sit a cell outside the part, where its
|
|
23
|
+
field means nothing. Nodes near the boundary read the stress ON the true
|
|
24
|
+
surface instead: a linear least-squares fit (superconvergent patch
|
|
25
|
+
recovery) over the element fields at the solid sample points around the
|
|
26
|
+
node's closest surface point. Bending stress is linear through a member,
|
|
27
|
+
so the fit takes it to the surface whole.
|
|
28
|
+
|
|
29
|
+
The boundary comes from a voxel grid n times finer (`fine`, bool), which
|
|
30
|
+
gives the sub-cells, the fill, and (its boundary faces) the surface points.
|
|
31
|
+
|
|
32
|
+
Accuracy (benchmarks/beams.py, peak / CalculiX): 0.95-1.09 in bending
|
|
33
|
+
and 0.95-1.17 in tension from 2 cells thick up, at any angle, except two
|
|
34
|
+
cells at 45 degrees (0.86): two cells across a diagonal strip are too few
|
|
35
|
+
for a trilinear cell to bend in, which verify's thin-member submodels
|
|
36
|
+
resolve (0.97-1.06 at 2.5x finer). The whole-cell staircase reads
|
|
37
|
+
0.75-1.95 over the same cases.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import numpy as np
|
|
42
|
+
import scipy.sparse as sp
|
|
43
|
+
from scipy import ndimage
|
|
44
|
+
|
|
45
|
+
from .fem import CORNERS, HexMesh, Stiffness, _b_matrix, _hex8_parts, elastic_c
|
|
46
|
+
|
|
47
|
+
__all__ = ["CutGrid", "SubcellRule", "MIN_FILL", "SUB"]
|
|
48
|
+
|
|
49
|
+
SUB = 4 # sub-cells per cell edge
|
|
50
|
+
MIN_FILL = 0.02 # a cell with less of the part than this is left out (conditioning)
|
|
51
|
+
_GAUSS = 1.0 / np.sqrt(3.0)
|
|
52
|
+
# how far from the surface (cells) a node reads the surface fit instead of
|
|
53
|
+
# its corner average, and the fit's neighbourhood
|
|
54
|
+
NEAR_SURFACE = 1.0
|
|
55
|
+
FIT_POINTS = 48
|
|
56
|
+
FIT_TAPER = 1.5
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _strain_rows(dn):
|
|
60
|
+
"""(n, k, 3) gradients of k scalar fields -> (n, 6, 3k) strain rows."""
|
|
61
|
+
n, k, _ = dn.shape
|
|
62
|
+
b = np.zeros((n, 6, 3 * k))
|
|
63
|
+
dx, dy, dz = dn[..., 0], dn[..., 1], dn[..., 2]
|
|
64
|
+
b[:, 0, 0::3], b[:, 1, 1::3], b[:, 2, 2::3] = dx, dy, dz
|
|
65
|
+
b[:, 3, 0::3], b[:, 3, 1::3] = dy, dx
|
|
66
|
+
b[:, 4, 1::3], b[:, 4, 2::3] = dz, dy
|
|
67
|
+
b[:, 5, 0::3], b[:, 5, 2::3] = dz, dx
|
|
68
|
+
return b
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def b_nodes(xi):
|
|
72
|
+
"""Trilinear strain rows at natural points xi (n, 3): (n, 6, 24), unit cube."""
|
|
73
|
+
xi = np.asarray(xi, float)
|
|
74
|
+
sg = 2.0 * CORNERS - 1.0
|
|
75
|
+
f = 1.0 + xi[:, None, :] * sg[None]
|
|
76
|
+
dn = np.empty((len(xi), 8, 3))
|
|
77
|
+
dn[..., 0] = 0.25 * sg[None, :, 0] * f[..., 1] * f[..., 2]
|
|
78
|
+
dn[..., 1] = 0.25 * sg[None, :, 1] * f[..., 0] * f[..., 2]
|
|
79
|
+
dn[..., 2] = 0.25 * sg[None, :, 2] * f[..., 0] * f[..., 1]
|
|
80
|
+
return _strain_rows(dn)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def b_bubbles(xi):
|
|
84
|
+
"""Strain rows of the nine bubbles (1 - xi_m^2) at natural points: (n, 6, 9)."""
|
|
85
|
+
xi = np.asarray(xi, float)
|
|
86
|
+
dn = np.zeros((len(xi), 3, 3))
|
|
87
|
+
for m in range(3):
|
|
88
|
+
dn[:, m, m] = -4.0 * xi[:, m]
|
|
89
|
+
return _strain_rows(dn)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SubcellRule:
|
|
93
|
+
"""Integrals over each sub-cell of an n^3 split of the unit cube of the
|
|
94
|
+
pieces of the element matrices, so a cut cell's matrices are its
|
|
95
|
+
sub-cell indicator times these."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, nu, n=SUB):
|
|
98
|
+
self.n = n
|
|
99
|
+
c = elastic_c(1.0, nu)
|
|
100
|
+
self.c = c
|
|
101
|
+
ns = n ** 3
|
|
102
|
+
ii = (2 * np.arange(n) + 1) / n - 1.0
|
|
103
|
+
self.centers = np.stack(np.meshgrid(ii, ii, ii, indexing="ij"), -1).reshape(-1, 3)
|
|
104
|
+
g = np.array([[a, b, cc] for a in (-_GAUSS, _GAUSS) for b in (-_GAUSS, _GAUSS) for cc in (-_GAUSS, _GAUSS)])
|
|
105
|
+
pts = (self.centers[:, None, :] + g[None] / n).reshape(-1, 3) # (ns*8, 3)
|
|
106
|
+
bu = b_nodes(pts).reshape(ns, 8, 6, 24)
|
|
107
|
+
ba = b_bubbles(pts).reshape(ns, 8, 6, 9)
|
|
108
|
+
w = 0.125 / ns # Gauss weight (det J = 1/8 on the unit cube)
|
|
109
|
+
cb = np.einsum("ij,sgjk->sgik", c, bu)
|
|
110
|
+
ca = np.einsum("ij,sgjk->sgik", c, ba)
|
|
111
|
+
self.S_uu = w * np.einsum("sgia,sgib->sab", bu, cb)
|
|
112
|
+
self.S_ua = w * np.einsum("sgia,sgib->sab", bu, ca)
|
|
113
|
+
self.S_aa = w * np.einsum("sgia,sgib->sab", ba, ca)
|
|
114
|
+
self.P_u = w * bu.sum(1)
|
|
115
|
+
self.P_a = w * ba.sum(1)
|
|
116
|
+
self.vol = np.full(ns, 1.0 / ns)
|
|
117
|
+
|
|
118
|
+
def matrices(self, ind):
|
|
119
|
+
"""(ke (m, 24, 24), r (m, 9, 24), mbar (m, 6, 9)) at unit modulus and
|
|
120
|
+
unit side for cells with sub-cell indicators ind (m, n^3): r maps the
|
|
121
|
+
nodal displacements to the condensed bubble amplitudes, mbar is the
|
|
122
|
+
mean bubble strain taken off on that cell."""
|
|
123
|
+
ind = np.asarray(ind, float)
|
|
124
|
+
c = self.c
|
|
125
|
+
kuu = ind @ self.S_uu.reshape(len(self.vol), -1)
|
|
126
|
+
kuu = kuu.reshape(-1, 24, 24)
|
|
127
|
+
v = ind @ self.vol
|
|
128
|
+
pa = (ind @ self.P_a.reshape(len(self.vol), -1)).reshape(-1, 6, 9)
|
|
129
|
+
pu = (ind @ self.P_u.reshape(len(self.vol), -1)).reshape(-1, 6, 24)
|
|
130
|
+
mbar = pa / v[:, None, None]
|
|
131
|
+
kua = (ind @ self.S_ua.reshape(len(self.vol), -1)).reshape(-1, 24, 9) \
|
|
132
|
+
- np.einsum("eki,kl,elj->eij", pu, c, mbar)
|
|
133
|
+
kaa = ((ind @ self.S_aa.reshape(len(self.vol), -1)).reshape(-1, 9, 9)
|
|
134
|
+
- np.einsum("eki,kl,elj->eij", pa, c, mbar)
|
|
135
|
+
- np.einsum("eki,kl,elj->eij", mbar, c, pa)
|
|
136
|
+
+ v[:, None, None] * np.einsum("eki,kl,elj->eij", mbar, c, mbar))
|
|
137
|
+
kaa = 0.5 * (kaa + kaa.transpose(0, 2, 1))
|
|
138
|
+
r = -np.linalg.solve(kaa, kua.transpose(0, 2, 1)) # (m, 9, 24)
|
|
139
|
+
ke = kuu + kua @ r
|
|
140
|
+
return 0.5 * (ke + ke.transpose(0, 2, 1)), r, mbar
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class CutGrid:
|
|
144
|
+
"""The cut-cell model of a part given as a bool grid `fine` at pitch / n
|
|
145
|
+
whose (0, 0, 0) corner is `origin` (each cell of the analysis grid is an
|
|
146
|
+
n^3 block of it). `cells` picks the analysis cells (fill >= MIN_FILL by
|
|
147
|
+
default). Attributes: mesh (fem.HexMesh), fill (per cell), cut (bool per
|
|
148
|
+
cell), ke/r/mbar for the cut cells."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, fine, n, origin, pitch, nu, min_fill=MIN_FILL):
|
|
151
|
+
fine = np.asarray(fine, bool)
|
|
152
|
+
dims = np.array(fine.shape) // n
|
|
153
|
+
fine = fine[:dims[0] * n, :dims[1] * n, :dims[2] * n]
|
|
154
|
+
self.n, self.pitch, self.nu = n, float(pitch), nu
|
|
155
|
+
self.origin = np.asarray(origin, float)
|
|
156
|
+
blocks = fine.reshape(dims[0], n, dims[1], n, dims[2], n).transpose(0, 2, 4, 1, 3, 5)
|
|
157
|
+
blocks = blocks.reshape(dims[0], dims[1], dims[2], n ** 3)
|
|
158
|
+
fill = blocks.mean(axis=3)
|
|
159
|
+
keep = fill >= min_fill
|
|
160
|
+
ijk = np.argwhere(keep)
|
|
161
|
+
self.mesh = HexMesh(ijk, self.pitch, self.origin)
|
|
162
|
+
self.fill = fill[keep]
|
|
163
|
+
self.ind = blocks[keep]
|
|
164
|
+
self.cut = self.fill < 1.0 - 1e-12
|
|
165
|
+
self.rule = SubcellRule(nu, n)
|
|
166
|
+
cutidx = np.nonzero(self.cut)[0]
|
|
167
|
+
self.cut_index = cutidx
|
|
168
|
+
ke = np.empty((len(cutidx), 24, 24))
|
|
169
|
+
r = np.empty((len(cutidx), 9, 24))
|
|
170
|
+
mb = np.empty((len(cutidx), 6, 9))
|
|
171
|
+
for s in range(0, len(cutidx), 4000):
|
|
172
|
+
sl = slice(s, s + 4000)
|
|
173
|
+
ke[sl], r[sl], mb[sl] = self.rule.matrices(self.ind[cutidx[sl]])
|
|
174
|
+
self.ke_cut, self.r_cut, self.mbar_cut = ke * self.pitch, r, mb
|
|
175
|
+
self.ke_full, self.r_full = _hex8_parts(nu, "im")
|
|
176
|
+
self.ke_full = self.ke_full * self.pitch
|
|
177
|
+
# the true surface, as the fine grid's boundary faces (to within
|
|
178
|
+
# half a fine cell, pitch / 2n), for the surface stress points
|
|
179
|
+
hf = self.pitch / n
|
|
180
|
+
pad = np.pad(fine, 1)
|
|
181
|
+
pts = []
|
|
182
|
+
for ax in range(3):
|
|
183
|
+
d = np.diff(pad.astype(np.int8), axis=ax)
|
|
184
|
+
idx = np.argwhere(d != 0).astype(float)
|
|
185
|
+
# diff index i along ax is the face between fine cells i-1 and i,
|
|
186
|
+
# at i; across it the padded index j is fine cell j-1, centre j-0.5
|
|
187
|
+
idx[:, [a for a in range(3) if a != ax]] -= 0.5
|
|
188
|
+
pts.append(idx)
|
|
189
|
+
self._faces = self.origin + np.concatenate(pts) * hf
|
|
190
|
+
self._fine = fine
|
|
191
|
+
self._fine_h = hf
|
|
192
|
+
self._tree = None
|
|
193
|
+
|
|
194
|
+
def restrict(self, keep):
|
|
195
|
+
"""The same model on the elements `keep` (bool per element), e.g.
|
|
196
|
+
without pieces no support holds."""
|
|
197
|
+
keep = np.asarray(keep, bool)
|
|
198
|
+
if keep.all():
|
|
199
|
+
return self
|
|
200
|
+
new = object.__new__(CutGrid)
|
|
201
|
+
new.__dict__.update(self.__dict__)
|
|
202
|
+
new.mesh = HexMesh(self.mesh.elem_ijk[keep], self.pitch, self.origin)
|
|
203
|
+
new.fill, new.ind, new.cut = self.fill[keep], self.ind[keep], self.cut[keep]
|
|
204
|
+
pos = np.full(self.mesh.n_elem, -1, np.int64)
|
|
205
|
+
pos[self.cut_index] = np.arange(len(self.cut_index))
|
|
206
|
+
old = np.nonzero(keep)[0]
|
|
207
|
+
k = pos[old[new.cut]]
|
|
208
|
+
new.cut_index = np.nonzero(new.cut)[0]
|
|
209
|
+
new.ke_cut, new.r_cut, new.mbar_cut = self.ke_cut[k], self.r_cut[k], self.mbar_cut[k]
|
|
210
|
+
return new
|
|
211
|
+
|
|
212
|
+
def solid_grid(self):
|
|
213
|
+
"""(bool grid of the kept cells, origin) as verify.voxelize returns."""
|
|
214
|
+
dims = np.array(self._fine.shape) // self.n
|
|
215
|
+
g = np.zeros(dims, bool)
|
|
216
|
+
g[tuple(self.mesh.elem_ijk.T)] = True
|
|
217
|
+
return g, self.origin
|
|
218
|
+
|
|
219
|
+
# --- stiffness ------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def full_moduli(self, modulus):
|
|
222
|
+
"""Per-element moduli for the shared-matrix path: cut cells 0 (their
|
|
223
|
+
matrices go in separately)."""
|
|
224
|
+
e = np.broadcast_to(np.asarray(modulus, float), (self.mesh.n_elem,)).copy()
|
|
225
|
+
e[self.cut] = 0.0
|
|
226
|
+
return e
|
|
227
|
+
|
|
228
|
+
def cut_matrix(self, stiff, modulus):
|
|
229
|
+
"""The cut cells' stiffness as a CSR matrix in stiff's pattern
|
|
230
|
+
(fixed rows and columns cleared, zero on their diagonal)."""
|
|
231
|
+
mod = np.broadcast_to(np.asarray(modulus, float), (self.mesh.n_elem,))[self.cut_index]
|
|
232
|
+
data = np.zeros(stiff.nnz)
|
|
233
|
+
for s in range(0, len(self.cut_index), 20000):
|
|
234
|
+
sl = slice(s, s + 20000)
|
|
235
|
+
stiff.add_elements(data, self.ke_cut[sl] * mod[sl, None, None], self.cut_index[sl])
|
|
236
|
+
return stiff.finish(data, diag=0.0)
|
|
237
|
+
|
|
238
|
+
def assemble(self, stiff: Stiffness, modulus):
|
|
239
|
+
"""The assembled K of the cut model (stiff: fem.Stiffness on self.mesh)."""
|
|
240
|
+
return (stiff.assemble(self.full_moduli(modulus)) + self.cut_matrix(stiff, modulus)).tocsr()
|
|
241
|
+
|
|
242
|
+
def cut_operator(self, modulus):
|
|
243
|
+
"""The cut cells' stiffness as an unconstrained sparse matrix in mesh
|
|
244
|
+
dof numbering (mg.GridElasticity's `springs`)."""
|
|
245
|
+
m = self.mesh
|
|
246
|
+
mod = np.broadcast_to(np.asarray(modulus, float), (m.n_elem,))[self.cut_index]
|
|
247
|
+
edof = m.edof[self.cut_index]
|
|
248
|
+
rows = np.repeat(edof, 24, axis=1).ravel()
|
|
249
|
+
cols = np.tile(edof, (1, 24)).ravel()
|
|
250
|
+
vals = (self.ke_cut * mod[:, None, None]).ravel()
|
|
251
|
+
return sp.csr_matrix((vals, (rows, cols)), shape=(m.n_dof, m.n_dof))
|
|
252
|
+
|
|
253
|
+
# --- stress ---------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def _element_stress(self, elems, xi, u, modulus, thermal=None):
|
|
256
|
+
"""Stress (p, 6) of elements elems at natural points xi (p, 3)."""
|
|
257
|
+
m = self.mesh
|
|
258
|
+
c = elastic_c(1.0, self.nu)
|
|
259
|
+
out = np.empty((len(elems), 6))
|
|
260
|
+
pos = np.full(m.n_elem, -1, np.int64)
|
|
261
|
+
pos[self.cut_index] = np.arange(len(self.cut_index))
|
|
262
|
+
mod = np.broadcast_to(np.asarray(modulus, float), (m.n_elem,))
|
|
263
|
+
for s in range(0, len(elems), 50000):
|
|
264
|
+
sl = slice(s, s + 50000)
|
|
265
|
+
e = elems[sl]
|
|
266
|
+
ue = u[m.edof[e]]
|
|
267
|
+
eps = np.einsum("pij,pj->pi", b_nodes(xi[sl]), ue)
|
|
268
|
+
ba = b_bubbles(xi[sl])
|
|
269
|
+
k = pos[e]
|
|
270
|
+
isc = k >= 0
|
|
271
|
+
r = np.broadcast_to(self.r_full, (len(e), 9, 24)).copy()
|
|
272
|
+
r[isc] = self.r_cut[k[isc]]
|
|
273
|
+
if isc.any():
|
|
274
|
+
ba[isc] = ba[isc] - self.mbar_cut[k[isc]]
|
|
275
|
+
alpha = np.einsum("pij,pj->pi", r, ue)
|
|
276
|
+
eps = (eps + np.einsum("pij,pj->pi", ba, alpha)) / self.pitch
|
|
277
|
+
if thermal is not None:
|
|
278
|
+
eps[:, :3] -= np.asarray(thermal, float)[e][:, None]
|
|
279
|
+
out[sl] = (eps @ c.T) * mod[e][:, None]
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
def _alpha(self, ue):
|
|
283
|
+
"""Condensed bubble amplitudes (e, 9) for element displacements (e, 24)."""
|
|
284
|
+
alpha = ue @ self.r_full.T
|
|
285
|
+
if len(self.cut_index):
|
|
286
|
+
alpha[self.cut_index] = np.einsum("pij,pj->pi", self.r_cut, ue[self.cut_index])
|
|
287
|
+
return alpha
|
|
288
|
+
|
|
289
|
+
def _samples(self, near, u, modulus, thermal=None):
|
|
290
|
+
"""Stress at the solid sample points of the elements in `near` (bool
|
|
291
|
+
per element): a full cell's eight Gauss points, a cut cell's solid
|
|
292
|
+
sub-cell centres. Returns (xyz, weight, stress)."""
|
|
293
|
+
m = self.mesh
|
|
294
|
+
c = elastic_c(1.0, self.nu)
|
|
295
|
+
mod = np.broadcast_to(np.asarray(modulus, float), (m.n_elem,))
|
|
296
|
+
th = None if thermal is None else np.asarray(thermal, float)
|
|
297
|
+
pos = np.full(m.n_elem, -1, np.int64)
|
|
298
|
+
pos[self.cut_index] = np.arange(len(self.cut_index))
|
|
299
|
+
g = np.array([[a, b, cc] for a in (-_GAUSS, _GAUSS) for b in (-_GAUSS, _GAUSS) for cc in (-_GAUSS, _GAUSS)])
|
|
300
|
+
xyz, w, sig = [], [], []
|
|
301
|
+
|
|
302
|
+
def put(el, xi_pt, weight):
|
|
303
|
+
ue = u[m.edof[el]]
|
|
304
|
+
alpha = ue @ self.r_full.T
|
|
305
|
+
k = pos[el]
|
|
306
|
+
isc = k >= 0
|
|
307
|
+
if isc.any():
|
|
308
|
+
alpha[isc] = np.einsum("pij,pj->pi", self.r_cut[k[isc]], ue[isc])
|
|
309
|
+
bu, ba = b_nodes(xi_pt[None])[0], b_bubbles(xi_pt[None])[0]
|
|
310
|
+
eps = ue @ bu.T + alpha @ ba.T
|
|
311
|
+
if isc.any():
|
|
312
|
+
eps[isc] -= np.einsum("pij,pj->pi", self.mbar_cut[k[isc]], alpha[isc])
|
|
313
|
+
eps /= self.pitch
|
|
314
|
+
if th is not None:
|
|
315
|
+
eps[:, :3] -= th[el][:, None]
|
|
316
|
+
sig.append((eps @ c.T) * mod[el][:, None])
|
|
317
|
+
xyz.append(m.origin + (m.elem_ijk[el] + 0.5 * (xi_pt + 1.0)) * self.pitch)
|
|
318
|
+
w.append(np.full(len(el), weight))
|
|
319
|
+
|
|
320
|
+
full = np.nonzero(near & ~self.cut)[0]
|
|
321
|
+
if len(full):
|
|
322
|
+
for q in g:
|
|
323
|
+
put(full, q, 1.0 / 8)
|
|
324
|
+
cut = np.nonzero(near & self.cut)[0]
|
|
325
|
+
if len(cut):
|
|
326
|
+
ind = self.ind[cut]
|
|
327
|
+
for sidx, q in enumerate(self.rule.centers):
|
|
328
|
+
el = cut[ind[:, sidx]]
|
|
329
|
+
if len(el):
|
|
330
|
+
put(el, q, 1.0 / self.n ** 3)
|
|
331
|
+
if not xyz:
|
|
332
|
+
return np.zeros((0, 3)), np.zeros(0), np.zeros((0, 6))
|
|
333
|
+
return np.concatenate(xyz), np.concatenate(w), np.concatenate(sig)
|
|
334
|
+
|
|
335
|
+
def surface_points(self, xyz, k=8):
|
|
336
|
+
"""Points on the true surface nearest each of xyz (the mean of the k
|
|
337
|
+
nearest boundary faces of the fine grid, which smooths its
|
|
338
|
+
staircase) and the signed distance to them (mm, negative inside)."""
|
|
339
|
+
from scipy.spatial import cKDTree
|
|
340
|
+
|
|
341
|
+
if self._tree is None:
|
|
342
|
+
self._tree = cKDTree(self._faces)
|
|
343
|
+
xyz = np.asarray(xyz, float)
|
|
344
|
+
dist, idx = self._tree.query(xyz, k=k)
|
|
345
|
+
p = self._faces[idx].mean(axis=1)
|
|
346
|
+
ijk = np.floor((xyz - self.origin) / self._fine_h).astype(np.int64)
|
|
347
|
+
ok = np.all((ijk >= 0) & (ijk < np.array(self._fine.shape)), axis=1)
|
|
348
|
+
inside = np.zeros(len(xyz), bool)
|
|
349
|
+
inside[ok] = self._fine[tuple(ijk[ok].T)]
|
|
350
|
+
d = np.linalg.norm(xyz - p, axis=1)
|
|
351
|
+
return p, np.where(inside, -d, d)
|
|
352
|
+
|
|
353
|
+
def nodal_stress(self, u, modulus, thermal=None):
|
|
354
|
+
"""Stress per node (n_nodes, 6). Nodes within NEAR_SURFACE cells of
|
|
355
|
+
the true surface read the surface fit at their closest surface
|
|
356
|
+
point; the rest the fill-weighted average of their elements' corner
|
|
357
|
+
stresses (verify.nodal_stress's recovery)."""
|
|
358
|
+
from scipy.spatial import cKDTree
|
|
359
|
+
|
|
360
|
+
m = self.mesh
|
|
361
|
+
c = elastic_c(1.0, self.nu)
|
|
362
|
+
mod = np.broadcast_to(np.asarray(modulus, float), (m.n_elem,))
|
|
363
|
+
ue = u[m.edof] # (e, 24)
|
|
364
|
+
alpha = ue @ self.r_full.T # (e, 9) bubble amplitudes
|
|
365
|
+
if len(self.cut_index):
|
|
366
|
+
alpha[self.cut_index] = np.einsum("pij,pj->pi", self.r_cut, ue[self.cut_index])
|
|
367
|
+
acc = np.zeros((m.n_nodes, 6))
|
|
368
|
+
wsum = np.bincount(m.enode.ravel(), weights=np.repeat(self.fill, 8), minlength=m.n_nodes)
|
|
369
|
+
corners = 2.0 * CORNERS - 1.0
|
|
370
|
+
bu, ba = b_nodes(corners), b_bubbles(corners)
|
|
371
|
+
for k in range(8):
|
|
372
|
+
eps = ue @ bu[k].T + alpha @ ba[k].T
|
|
373
|
+
if len(self.cut_index):
|
|
374
|
+
eps[self.cut_index] -= np.einsum("pij,pj->pi", self.mbar_cut, alpha[self.cut_index])
|
|
375
|
+
eps /= self.pitch
|
|
376
|
+
if thermal is not None:
|
|
377
|
+
eps[:, :3] -= np.asarray(thermal, float)[:, None]
|
|
378
|
+
sk = (eps @ c.T) * (mod * self.fill)[:, None]
|
|
379
|
+
for j in range(6):
|
|
380
|
+
acc[:, j] += np.bincount(m.enode[:, k], weights=sk[:, j], minlength=m.n_nodes)
|
|
381
|
+
out = acc / np.maximum(wsum, 1e-30)[:, None]
|
|
382
|
+
# nodes on cut cells, or touching the grid's empty neighbours
|
|
383
|
+
on_cut = np.zeros(m.n_nodes, bool)
|
|
384
|
+
on_cut[m.enode[self.cut].ravel()] = True
|
|
385
|
+
cnt = np.bincount(m.enode.ravel(), minlength=m.n_nodes)
|
|
386
|
+
on_cut |= cnt < 8
|
|
387
|
+
cand = np.nonzero(on_cut)[0]
|
|
388
|
+
if not len(cand):
|
|
389
|
+
return out
|
|
390
|
+
p, d = self.surface_points(m.node_xyz[cand])
|
|
391
|
+
sel = d >= -NEAR_SURFACE * self.pitch # every node outside, and those just inside
|
|
392
|
+
cand, p = cand[sel], p[sel]
|
|
393
|
+
if not len(cand):
|
|
394
|
+
return out
|
|
395
|
+
# samples from elements within two cells of a surface node
|
|
396
|
+
near_node = np.zeros(m.n_nodes, bool)
|
|
397
|
+
near_node[cand] = True
|
|
398
|
+
near_el = near_node[m.enode].any(axis=1)
|
|
399
|
+
for _ in range(2):
|
|
400
|
+
nn = np.zeros(m.n_nodes, bool)
|
|
401
|
+
nn[m.enode[near_el].ravel()] = True
|
|
402
|
+
near_el = nn[m.enode].any(axis=1)
|
|
403
|
+
xyz, w, sig = self._samples(near_el, u, modulus, thermal)
|
|
404
|
+
tree = cKDTree(xyz)
|
|
405
|
+
k = min(FIT_POINTS, len(xyz))
|
|
406
|
+
dist, idx = tree.query(p, k=k)
|
|
407
|
+
out[cand] = _fit_linear(p, xyz, sig, w, dist, idx, self.pitch)
|
|
408
|
+
return out
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _fit_linear(p, xyz, sig, w, dist, idx, h):
|
|
412
|
+
"""Weighted least-squares linear field over each point's neighbours,
|
|
413
|
+
evaluated at the point: (p, 6)."""
|
|
414
|
+
out = np.empty((len(p), 6))
|
|
415
|
+
for s in range(0, len(p), 20000):
|
|
416
|
+
sl = slice(s, s + 20000)
|
|
417
|
+
d = (xyz[idx[sl]] - p[sl, None, :]) / h # (q, k, 3)
|
|
418
|
+
# Gaussian-ish taper so the fit is local, and the sample's volume
|
|
419
|
+
ww = w[idx[sl]] * np.exp(-(dist[sl] / (FIT_TAPER * h)) ** 2)
|
|
420
|
+
a = np.concatenate([np.ones(d.shape[:2] + (1,)), d], axis=2) # (q, k, 4)
|
|
421
|
+
aw = a * ww[..., None]
|
|
422
|
+
ata = np.einsum("qki,qkj->qij", aw, a) + 1e-9 * np.eye(4)
|
|
423
|
+
atb = np.einsum("qki,qkc->qic", aw, sig[idx[sl]])
|
|
424
|
+
coef = np.linalg.solve(ata, atb)
|
|
425
|
+
out[sl] = coef[:, 0, :]
|
|
426
|
+
return out
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def scan_voxelize(mesh, pitch, origin, dims, chunk=200_000):
|
|
430
|
+
"""Solid cells (bool, dims) of a closed mesh on the grid at `origin`,
|
|
431
|
+
cell centres inside by crossing parity along +z, as verify.voxelize
|
|
432
|
+
decides them, but found by rasterizing each triangle onto the cell
|
|
433
|
+
columns it covers instead of casting a ray per column: the sub-cell grid
|
|
434
|
+
has 16x the columns, and per-ray queries took 20 s of a 100 s verify."""
|
|
435
|
+
tri = np.asarray(mesh.triangles, float)
|
|
436
|
+
dims = np.asarray(dims, int)
|
|
437
|
+
jit = pitch * 1e-4
|
|
438
|
+
# column centres, with verify.voxelize's jitter off exact edges
|
|
439
|
+
ox = origin[0] + 0.5 * pitch + 0.37 * jit
|
|
440
|
+
oy = origin[1] + 0.5 * pitch + 0.61 * jit
|
|
441
|
+
i0 = np.clip(np.ceil((tri[:, :, 0].min(1) - ox) / pitch), 0, dims[0]).astype(np.int64)
|
|
442
|
+
i1 = np.clip(np.floor((tri[:, :, 0].max(1) - ox) / pitch) + 1, 0, dims[0]).astype(np.int64)
|
|
443
|
+
j0 = np.clip(np.ceil((tri[:, :, 1].min(1) - oy) / pitch), 0, dims[1]).astype(np.int64)
|
|
444
|
+
j1 = np.clip(np.floor((tri[:, :, 1].max(1) - oy) / pitch) + 1, 0, dims[1]).astype(np.int64)
|
|
445
|
+
ni, nj = np.maximum(i1 - i0, 0), np.maximum(j1 - j0, 0)
|
|
446
|
+
cnt = ni * nj
|
|
447
|
+
cols, zs = [], []
|
|
448
|
+
t_all = np.nonzero(cnt)[0]
|
|
449
|
+
start = 0
|
|
450
|
+
while start < len(t_all):
|
|
451
|
+
# triangles whose candidate columns fit in one chunk
|
|
452
|
+
cs = np.cumsum(cnt[t_all[start:]])
|
|
453
|
+
stop = start + max(1, int(np.searchsorted(cs, chunk, side="right")))
|
|
454
|
+
t = t_all[start:stop]
|
|
455
|
+
start = stop
|
|
456
|
+
c = cnt[t]
|
|
457
|
+
tt = np.repeat(t, c)
|
|
458
|
+
k = np.arange(c.sum()) - np.repeat(np.cumsum(c) - c, c)
|
|
459
|
+
ii = i0[tt] + k // nj[tt]
|
|
460
|
+
jj = j0[tt] + k % nj[tt]
|
|
461
|
+
px, py = ox + ii * pitch, oy + jj * pitch
|
|
462
|
+
a, b, cc = tri[tt, 0], tri[tt, 1], tri[tt, 2]
|
|
463
|
+
# 2D barycentrics of the column in the triangle's xy projection
|
|
464
|
+
d = (b[:, 0] - a[:, 0]) * (cc[:, 1] - a[:, 1]) - (cc[:, 0] - a[:, 0]) * (b[:, 1] - a[:, 1])
|
|
465
|
+
ok = np.abs(d) > 1e-18
|
|
466
|
+
dd = np.where(ok, d, 1.0)
|
|
467
|
+
l1 = ((px - a[:, 0]) * (cc[:, 1] - a[:, 1]) - (cc[:, 0] - a[:, 0]) * (py - a[:, 1])) / dd
|
|
468
|
+
l2 = ((b[:, 0] - a[:, 0]) * (py - a[:, 1]) - (px - a[:, 0]) * (b[:, 1] - a[:, 1])) / dd
|
|
469
|
+
l0 = 1.0 - l1 - l2
|
|
470
|
+
hit = ok & (l0 >= 0) & (l1 >= 0) & (l2 >= 0)
|
|
471
|
+
z = l0 * a[:, 2] + l1 * b[:, 2] + l2 * cc[:, 2]
|
|
472
|
+
cols.append((ii * dims[1] + jj)[hit])
|
|
473
|
+
zs.append(z[hit])
|
|
474
|
+
solid = np.zeros(dims, bool)
|
|
475
|
+
if not cols:
|
|
476
|
+
return solid
|
|
477
|
+
col = np.concatenate(cols)
|
|
478
|
+
z = np.concatenate(zs)
|
|
479
|
+
order = np.lexsort((z, col))
|
|
480
|
+
col, z = col[order], z[order]
|
|
481
|
+
# merge near-duplicate hits in a column (a shared edge counted twice)
|
|
482
|
+
dup = np.zeros(len(z), bool)
|
|
483
|
+
dup[1:] = (col[1:] == col[:-1]) & (np.diff(z) <= 1e-7)
|
|
484
|
+
col, z = col[~dup], z[~dup]
|
|
485
|
+
first = np.r_[True, col[1:] != col[:-1]]
|
|
486
|
+
run = np.cumsum(first) - 1
|
|
487
|
+
starts = np.nonzero(first)[0]
|
|
488
|
+
rank = np.arange(len(col)) - starts[run]
|
|
489
|
+
n_in = np.bincount(run)
|
|
490
|
+
# an odd count drops its last hit (verify.voxelize does the same)
|
|
491
|
+
keep = rank < (n_in[run] // 2) * 2
|
|
492
|
+
col, z, rank = col[keep], z[keep], rank[keep]
|
|
493
|
+
za, zb, cl = z[rank % 2 == 0], z[rank % 2 == 1], col[rank % 2 == 0]
|
|
494
|
+
zc0 = origin[2] + 0.5 * pitch
|
|
495
|
+
ka = np.clip(np.floor((za - zc0) / pitch) + 1, 0, dims[2]).astype(np.int64) # first centre > za
|
|
496
|
+
kb = np.clip(np.ceil((zb - zc0) / pitch), 0, dims[2]).astype(np.int64) # first centre >= zb
|
|
497
|
+
diff = np.zeros((dims[0] * dims[1], dims[2] + 1), np.int32)
|
|
498
|
+
np.add.at(diff, (cl, ka), 1)
|
|
499
|
+
np.add.at(diff, (cl, kb), -1)
|
|
500
|
+
return (np.cumsum(diff, axis=1)[:, :-1] > 0).reshape(dims)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def cut_from_mesh(mesh, pitch, nu, box=None, n=SUB, min_fill=MIN_FILL):
|
|
504
|
+
"""CutGrid of a closed trimesh on verify.voxelize's grid at `pitch` (the
|
|
505
|
+
same origin and cells), its sub-cells from voxelizing at pitch / n."""
|
|
506
|
+
lo, hi = (np.asarray(b, float) for b in box) if box is not None else np.asarray(mesh.bounds, float)
|
|
507
|
+
dims = np.maximum(np.ceil((hi - lo) / pitch - 1e-6).astype(int), 1)
|
|
508
|
+
origin = lo - 0.5 * (dims * pitch - (hi - lo))
|
|
509
|
+
fine = scan_voxelize(mesh, pitch / n, origin, dims * n)
|
|
510
|
+
return CutGrid(fine, n, origin, pitch, nu, min_fill=min_fill)
|