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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Elastic buckling of a lattice's walls and struts, from a periodic unit-cell
|
|
3
|
+
eigenvalue solve.
|
|
4
|
+
|
|
5
|
+
The homogenized check reads a lattice's first yield from its walls' linear
|
|
6
|
+
stress. A thin wall or a slender strut can instead buckle first, under a
|
|
7
|
+
compressive macro stress well below the one that yields it, and the linear
|
|
8
|
+
check does not see that. Here a block of `cells`^3 unit cells on a periodic
|
|
9
|
+
voxel grid (the nodes wrap across the block's faces, as lattice_tensors'
|
|
10
|
+
solves do) is put under a unit macro stress, and the linear eigenvalue
|
|
11
|
+
problem
|
|
12
|
+
|
|
13
|
+
K phi = lambda (-K_sigma) phi
|
|
14
|
+
|
|
15
|
+
gives the smallest load factor lambda at which the walls buckle: the macro
|
|
16
|
+
stress, in units of the solid's modulus, at which a perfect infinite lattice
|
|
17
|
+
buckles. A one-cell block finds the modes that repeat every cell, a two-cell
|
|
18
|
+
block also the ones that alternate between neighbours (often lower); the
|
|
19
|
+
long-wave (whole-part, Euler) modes are not in it, a part-level check
|
|
20
|
+
(nonlinear.linear_buckling) covers those.
|
|
21
|
+
|
|
22
|
+
K uses the element in force (the incompatible-mode hex8, which bends without
|
|
23
|
+
locking at a few voxels through a wall); K_sigma is the trilinear geometric
|
|
24
|
+
stiffness of the linear stress at the Gauss points.
|
|
25
|
+
|
|
26
|
+
Tables over relative density per cell type and load direction live in
|
|
27
|
+
data/lattice_buckling.json (bench/lattice_buckling.py writes them);
|
|
28
|
+
`buckling_stress` reads them. Real walls are not perfect: printed walls are
|
|
29
|
+
wavy, porous and stair-stepped, and shells lose much of their perfect
|
|
30
|
+
buckling load to that. How the check in homogenized.py knocks the perfect
|
|
31
|
+
value down (with the as-printed stiffness, lattice_print.py) is documented
|
|
32
|
+
there and in docs/VALIDATION.md.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
from functools import lru_cache
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
__all__ = ["unit_cell_buckling", "buckling_stress", "has_buckling", "TABLE_PATH", "DIRECTIONS"]
|
|
43
|
+
|
|
44
|
+
TABLE_PATH = Path(__file__).resolve().parent / "data" / "lattice_buckling.json"
|
|
45
|
+
|
|
46
|
+
# macro uniaxial compression directions the tables hold, in the cell's axes
|
|
47
|
+
DIRECTIONS = {"100": (0.0, 0.0, 1.0), "111": (1.0, 1.0, 1.0)}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _grid(kind, wall_over_cell, n, cells):
|
|
51
|
+
from .lattice import tpms
|
|
52
|
+
from .struts import is_strut, unit_cell_distance
|
|
53
|
+
|
|
54
|
+
m = n * cells
|
|
55
|
+
h = 1.0 / n
|
|
56
|
+
axes = (np.arange(m) + 0.5) * h
|
|
57
|
+
g = np.stack(np.meshgrid(axes, axes, axes, indexing="ij"), -1).reshape(-1, 3)
|
|
58
|
+
if is_strut(kind):
|
|
59
|
+
u = g % 1.0
|
|
60
|
+
solid = unit_cell_distance(kind, u, 0.0) < 0.5 * wall_over_cell
|
|
61
|
+
else:
|
|
62
|
+
solid = np.abs(tpms(kind, g, 1.0)) < 0.5 * wall_over_cell
|
|
63
|
+
return np.argwhere(solid.reshape(m, m, m)), m, h
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def unit_cell_buckling(kind: str, wall_over_cell: float, direction=(0.0, 0.0, 1.0), n: int | None = None,
|
|
67
|
+
cells: int = 1, nu: float = 0.35, n_modes: int = 2, direct_max_dof: int = 60_000) -> dict:
|
|
68
|
+
"""Perfect-lattice elastic buckling under uniaxial macro compression
|
|
69
|
+
along `direction` (cell axes): the critical macro stress over the
|
|
70
|
+
solid's modulus (`sigmaOverE`), the relative density of the voxel cell,
|
|
71
|
+
and its modulus along the direction (over E), for a block of
|
|
72
|
+
`cells`^3 periodic unit cells, `n` voxels per cell (default: five
|
|
73
|
+
voxels through the wall or strut, 24-72)."""
|
|
74
|
+
import time
|
|
75
|
+
|
|
76
|
+
import scipy.sparse as sp
|
|
77
|
+
from scipy.sparse.linalg import LinearOperator, eigsh
|
|
78
|
+
|
|
79
|
+
from .fem import CORNERS, elastic_c, hex8_b, hex8_ke
|
|
80
|
+
|
|
81
|
+
t0 = time.time()
|
|
82
|
+
if n is None:
|
|
83
|
+
n = int(np.clip(np.ceil(5.0 / float(wall_over_cell)), 24, 72))
|
|
84
|
+
eijk, m, h = _grid(kind, float(wall_over_cell), n, cells)
|
|
85
|
+
corner = (eijk[:, None, :] + CORNERS[None]) % m
|
|
86
|
+
eijk, corner = _connected(eijk, corner, m)
|
|
87
|
+
ne = len(eijk)
|
|
88
|
+
rho = ne / float(m ** 3)
|
|
89
|
+
used, inv = np.unique((corner[..., 0] * m + corner[..., 1]) * m + corner[..., 2], return_inverse=True)
|
|
90
|
+
enode = inv.reshape(ne, 8)
|
|
91
|
+
nn = len(used)
|
|
92
|
+
edof = (3 * enode[:, :, None] + np.arange(3)).reshape(ne, 24)
|
|
93
|
+
rows = np.repeat(edof, 24, axis=1).ravel()
|
|
94
|
+
cols = np.tile(edof, (1, 24)).ravel()
|
|
95
|
+
ke = hex8_ke(nu) * h
|
|
96
|
+
k = sp.csr_matrix((np.tile(ke.ravel(), ne), (rows, cols)), shape=(3 * nn, 3 * nn))
|
|
97
|
+
# keep the block from floating: hold one node; the rest is periodic
|
|
98
|
+
free = np.ones(3 * nn, bool)
|
|
99
|
+
free[:3] = False
|
|
100
|
+
kf = k[free][:, free].tocsc()
|
|
101
|
+
nf = int(free.sum())
|
|
102
|
+
kinv = _factorized(kf, nf <= direct_max_dof)
|
|
103
|
+
xc = (eijk[:, None, :] + CORNERS[None]) * h
|
|
104
|
+
c0 = elastic_c(1.0, nu)
|
|
105
|
+
vol = float(cells ** 3)
|
|
106
|
+
bc = hex8_b(0.0, 0.0, 0.0, nu) / h
|
|
107
|
+
|
|
108
|
+
def macro_solve(e):
|
|
109
|
+
eps = np.array([[e[0], e[3] / 2, e[5] / 2], [e[3] / 2, e[1], e[4] / 2], [e[5] / 2, e[4] / 2, e[2]]])
|
|
110
|
+
u0 = (xc @ eps.T).reshape(ne, 24)
|
|
111
|
+
f = np.zeros(3 * nn)
|
|
112
|
+
np.add.at(f, edof, -(u0 @ ke.T))
|
|
113
|
+
u = np.zeros(3 * nn)
|
|
114
|
+
u[free] = kinv(f[free])
|
|
115
|
+
ue = u[edof] + u0
|
|
116
|
+
return ue, ((ue @ bc.T) @ c0.T).sum(axis=0) * h ** 3 / vol
|
|
117
|
+
|
|
118
|
+
# effective stiffness, then the macro strain of a unit uniaxial compression
|
|
119
|
+
C = np.zeros((6, 6))
|
|
120
|
+
fields = []
|
|
121
|
+
for comp in range(6):
|
|
122
|
+
e = np.zeros(6)
|
|
123
|
+
e[comp] = 1.0
|
|
124
|
+
ue, s = macro_solve(e)
|
|
125
|
+
C[:, comp] = s
|
|
126
|
+
fields.append(ue)
|
|
127
|
+
d = np.asarray(direction, float)
|
|
128
|
+
d = d / np.linalg.norm(d)
|
|
129
|
+
sig = -np.array([d[0] ** 2, d[1] ** 2, d[2] ** 2, d[0] * d[1], d[1] * d[2], d[2] * d[0]])
|
|
130
|
+
eps = np.linalg.solve(C, sig)
|
|
131
|
+
ue = sum(eps[c] * fields[c] for c in range(6))
|
|
132
|
+
e_dir = 1.0 / float(-sig @ np.linalg.solve(C, -sig)) # modulus along d, over E
|
|
133
|
+
# geometric stiffness of the linear stress at the 2x2x2 Gauss points
|
|
134
|
+
g = 1.0 / np.sqrt(3.0)
|
|
135
|
+
s = 2.0 * CORNERS - 1.0
|
|
136
|
+
w = h ** 3 / 8.0
|
|
137
|
+
kq = np.zeros((ne, 8, 8))
|
|
138
|
+
for xi in (-g, g):
|
|
139
|
+
for eta in (-g, g):
|
|
140
|
+
for zeta in (-g, g):
|
|
141
|
+
dn = np.empty((8, 3))
|
|
142
|
+
dn[:, 0] = 0.125 * s[:, 0] * (1 + eta * s[:, 1]) * (1 + zeta * s[:, 2]) * 2 / h
|
|
143
|
+
dn[:, 1] = 0.125 * s[:, 1] * (1 + xi * s[:, 0]) * (1 + zeta * s[:, 2]) * 2 / h
|
|
144
|
+
dn[:, 2] = 0.125 * s[:, 2] * (1 + xi * s[:, 0]) * (1 + eta * s[:, 1]) * 2 / h
|
|
145
|
+
b = hex8_b(xi, eta, zeta, nu) / h
|
|
146
|
+
sv = (ue @ b.T) @ c0.T # (ne, 6) Voigt, tensor shears
|
|
147
|
+
st = np.empty((ne, 3, 3))
|
|
148
|
+
st[:, 0, 0], st[:, 1, 1], st[:, 2, 2] = sv[:, 0], sv[:, 1], sv[:, 2]
|
|
149
|
+
st[:, 0, 1] = st[:, 1, 0] = sv[:, 3]
|
|
150
|
+
st[:, 1, 2] = st[:, 2, 1] = sv[:, 4]
|
|
151
|
+
st[:, 0, 2] = st[:, 2, 0] = sv[:, 5]
|
|
152
|
+
kq += w * np.einsum("ai,eij,bj->eab", dn, st, dn)
|
|
153
|
+
kg_e = np.einsum("eab,ij->eaibj", kq, np.eye(3)).reshape(ne, 576)
|
|
154
|
+
kg = sp.csr_matrix((kg_e.ravel(), (rows, cols)), shape=(3 * nn, 3 * nn))
|
|
155
|
+
kgf = kg[free][:, free].tocsr()
|
|
156
|
+
del kg_e, kq
|
|
157
|
+
op = LinearOperator((nf, nf), matvec=kinv, dtype=float)
|
|
158
|
+
from scipy.sparse.linalg import ArpackNoConvergence
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
mu, _ = eigsh(-kgf, k=n_modes, M=kf.tocsr(), Minv=op, which="LA", tol=1e-4, ncv=max(20, 4 * n_modes),
|
|
162
|
+
maxiter=600)
|
|
163
|
+
except ArpackNoConvergence as e:
|
|
164
|
+
# clustered modes (a cell whose struts buckle in many equal ways):
|
|
165
|
+
# the converged ones, else the iteration's best estimate
|
|
166
|
+
mu = e.eigenvalues if len(e.eigenvalues) else np.array([0.0])
|
|
167
|
+
mu = np.sort(np.atleast_1d(mu))[::-1]
|
|
168
|
+
lam = float(1.0 / mu[0]) if mu[0] > 1e-14 else float("inf")
|
|
169
|
+
return {"kind": kind, "wallOverCell": float(wall_over_cell), "direction": d.tolist(), "cells": cells, "n": n,
|
|
170
|
+
"rho": rho, "elements": ne, "dof": nf, "sigmaOverE": lam, "modulusOverE": e_dir,
|
|
171
|
+
"strainAtBuckling": lam / e_dir, "seconds": time.time() - t0}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _connected(eijk, corner, m):
|
|
175
|
+
"""The voxels of the largest piece joined through shared nodes (a thin
|
|
176
|
+
strut can voxelize into islands that would float)."""
|
|
177
|
+
import scipy.sparse as sp
|
|
178
|
+
from scipy.sparse.csgraph import connected_components
|
|
179
|
+
|
|
180
|
+
ne = len(eijk)
|
|
181
|
+
key = ((corner[..., 0] * m + corner[..., 1]) * m + corner[..., 2]).ravel()
|
|
182
|
+
_, node = np.unique(key, return_inverse=True)
|
|
183
|
+
inc = sp.csr_matrix((np.ones(ne * 8), (np.repeat(np.arange(ne), 8), node)), shape=(ne, node.max() + 1))
|
|
184
|
+
_, lab = connected_components(inc @ inc.T, directed=False)
|
|
185
|
+
big = lab == np.argmax(np.bincount(lab))
|
|
186
|
+
return eijk[big], corner[big]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _factorized(kf, direct):
|
|
190
|
+
"""x = K^-1 r: PARDISO (pypardiso, nested-dissection ordering, threaded)
|
|
191
|
+
when installed, else SuperLU for small systems and AMG-preconditioned
|
|
192
|
+
CG above `direct_max_dof` (SuperLU's orderings fill in badly on 3D
|
|
193
|
+
grids: a 170k-dof sheet cell took over 10 minutes and 8 GB)."""
|
|
194
|
+
import scipy.sparse as sp
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
import pypardiso
|
|
198
|
+
|
|
199
|
+
a = sp.csr_matrix(kf)
|
|
200
|
+
solver = pypardiso.PyPardisoSolver()
|
|
201
|
+
solver.factorize(a)
|
|
202
|
+
return lambda r: solver.solve(a, np.ascontiguousarray(r, float))
|
|
203
|
+
except ImportError:
|
|
204
|
+
pass
|
|
205
|
+
if direct:
|
|
206
|
+
from scipy.sparse.linalg import splu
|
|
207
|
+
|
|
208
|
+
return splu(kf.tocsc(), permc_spec="MMD_AT_PLUS_A").solve
|
|
209
|
+
import pyamg
|
|
210
|
+
from scipy.sparse.linalg import cg
|
|
211
|
+
|
|
212
|
+
ml = pyamg.smoothed_aggregation_solver(sp.csr_matrix(kf), symmetry="hermitian", max_coarse=400,
|
|
213
|
+
coarse_solver="splu")
|
|
214
|
+
pre = ml.aspreconditioner()
|
|
215
|
+
return lambda r: cg(kf, r, rtol=1e-10, maxiter=4000, M=pre)[0]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@lru_cache(maxsize=1)
|
|
219
|
+
def _table() -> dict:
|
|
220
|
+
if not TABLE_PATH.exists():
|
|
221
|
+
return {}
|
|
222
|
+
return json.loads(TABLE_PATH.read_text())
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def has_buckling(kind: str) -> bool:
|
|
226
|
+
return kind in _table().get("cells", {})
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def buckling_stress(kind: str, rho, direction: str | None = None) -> np.ndarray:
|
|
230
|
+
"""Perfect-lattice buckling stress over the solid's modulus at relative
|
|
231
|
+
densities `rho`: the table's least over its directions (or the one
|
|
232
|
+
named), log-log in rho between rows, the end rows' power law beyond."""
|
|
233
|
+
row = _table()["cells"][kind]
|
|
234
|
+
xs, keep = np.unique(np.asarray(row["rho"], float), return_index=True)
|
|
235
|
+
dirs = [direction] if direction else list(row["sigmaOverE"])
|
|
236
|
+
ys = np.min([np.asarray(row["sigmaOverE"][dd], float)[keep] for dd in dirs], axis=0)
|
|
237
|
+
lr, ly = np.log(xs), np.log(ys)
|
|
238
|
+
x = np.log(np.clip(np.atleast_1d(np.asarray(rho, float)), 1e-4, 1.0))
|
|
239
|
+
out = np.interp(x, lr, ly)
|
|
240
|
+
if len(xs) > 1:
|
|
241
|
+
lo = (ly[1] - ly[0]) / (lr[1] - lr[0])
|
|
242
|
+
hi = (ly[-1] - ly[-2]) / (lr[-1] - lr[-2])
|
|
243
|
+
out = np.where(x < lr[0], ly[0] + lo * (x - lr[0]), out)
|
|
244
|
+
out = np.where(x > lr[-1], ly[-1] + hi * (x - lr[-1]), out)
|
|
245
|
+
return np.exp(out)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lattice walls graded to the stress, for field-graph lattices, together
|
|
3
|
+
with the cells' direction.
|
|
4
|
+
|
|
5
|
+
`grade_lattice` replaces a lattice node's constant `thickness` with a
|
|
6
|
+
`density` field (a grid node) sized by fully stressed iteration on the
|
|
7
|
+
homogenized check (homogenized.py, anisotropic tensors):
|
|
8
|
+
|
|
9
|
+
1. analyze the part with the current density; every lattice cell's macro
|
|
10
|
+
stress per load case comes back in global axes;
|
|
11
|
+
2. holding that stress, each cell takes the least density (of 28
|
|
12
|
+
candidates between the wall limits, log-interpolated) whose wall stress
|
|
13
|
+
surface, in the cell's own frame, stays under the allowable in every
|
|
14
|
+
case; cells where lattice meets a skin carry the skin-junction factor,
|
|
15
|
+
as the check does;
|
|
16
|
+
3. the requirement spreads over a quarter cell (a peak spreads over a cell
|
|
17
|
+
before the lattice can follow it), moves at most halving or doubling a
|
|
18
|
+
pass, and the allowable is tightened or eased by how far the analysis
|
|
19
|
+
lands from the target, as lattice.lattice_infill's sizing does.
|
|
20
|
+
|
|
21
|
+
With `orient` the cells are turned too: "uniform" one direction for the
|
|
22
|
+
whole lattice, an integer k for k zones (stress_zones). The direction is
|
|
23
|
+
chosen for the least material the sized lattice needs, not for the peak:
|
|
24
|
+
holding each cell's macro stress, every candidate rotation's per-cell need
|
|
25
|
+
is a few matrix products away (the quartic features of the turned stress
|
|
26
|
+
dotted with every candidate density's surface), so hundreds of rotations
|
|
27
|
+
per zone are scored by the mass they would leave. Sizing and turning
|
|
28
|
+
alternate for `rounds` rounds.
|
|
29
|
+
|
|
30
|
+
The density is a grid (`op: grid`, `source` while sizing; written to an
|
|
31
|
+
.npz the result's graph points at), so `loadpath field` meshes the graded
|
|
32
|
+
lattice as sized. Stretched (`aspect`) cells and graded (warp) cells are not
|
|
33
|
+
graded here yet.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import copy
|
|
38
|
+
import time
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
__all__ = ["grade_lattice", "need_density"]
|
|
43
|
+
|
|
44
|
+
SOURCE = "lattice-density"
|
|
45
|
+
N_CAND = 28
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _cand_tensors(kind, rho_min, rho_max, aspect=None):
|
|
49
|
+
from .lattice_tensors import tensors_at
|
|
50
|
+
|
|
51
|
+
cand = np.geomspace(rho_min, rho_max, N_CAND)
|
|
52
|
+
C, P, Q, F = tensors_at(kind, cand, aspect)
|
|
53
|
+
return cand, P, Q, F
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _surfaces(sc, P, Q, F):
|
|
57
|
+
"""Wall stress surface of stresses sc (n, 6), in the cells' axes, for
|
|
58
|
+
every candidate density's (P, Q, F): (n, J)."""
|
|
59
|
+
from .lattice_tensors import quartic_features
|
|
60
|
+
|
|
61
|
+
f = quartic_features(sc)
|
|
62
|
+
q4 = f @ Q.T
|
|
63
|
+
quad = np.sqrt(np.maximum(np.einsum("na,jab,nb->nj", sc, P, sc), 0.0))
|
|
64
|
+
return np.maximum(np.maximum(q4, 0.0) ** 0.25, F[None, :] * quad)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def need_density(worst, cand, allow, over):
|
|
68
|
+
"""Least candidate density with worst (n, J) <= allow, log-interpolated
|
|
69
|
+
between candidates; `over` where none is."""
|
|
70
|
+
ok = worst <= allow
|
|
71
|
+
first = np.where(ok.any(axis=1), ok.argmax(axis=1), len(cand))
|
|
72
|
+
need = np.full(len(worst), float(over))
|
|
73
|
+
at = first < len(cand)
|
|
74
|
+
need[at] = cand[first[at]]
|
|
75
|
+
mid = at & (first > 0)
|
|
76
|
+
j = first[mid]
|
|
77
|
+
w0 = np.maximum(worst[mid, j - 1], 1e-30)
|
|
78
|
+
w1 = np.maximum(worst[mid, j], 1e-30)
|
|
79
|
+
t = np.clip((np.log(w0) - np.log(allow)) / np.maximum(np.log(w0) - np.log(w1), 1e-12), 0.0, 1.0)
|
|
80
|
+
need[mid] = np.exp(np.log(cand[j - 1]) + t * (np.log(cand[j]) - np.log(cand[j - 1])))
|
|
81
|
+
return need
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _cell_stress(fl, mask, frames=None):
|
|
85
|
+
"""Per case the lattice macro stress of the masked cells in their own
|
|
86
|
+
axes: frames (n, 3, 3) columns the cell axes, or the check's own."""
|
|
87
|
+
from .lattice_tensors import stress_rotation
|
|
88
|
+
|
|
89
|
+
fr = fl["frames"] if frames is None else frames
|
|
90
|
+
out = []
|
|
91
|
+
for sig in fl["latticeStress"]:
|
|
92
|
+
s = sig[mask]
|
|
93
|
+
if fr is not None:
|
|
94
|
+
s = np.einsum("eab,eb->ea", stress_rotation(fr[mask] if fr.ndim == 3 else fr), s)
|
|
95
|
+
out.append(s)
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _grid_of(centers, values, origin, pitch):
|
|
100
|
+
"""A dense grid (origin at the first cell centre) of per-cell values,
|
|
101
|
+
cells with none filled from their nearest neighbour."""
|
|
102
|
+
from scipy import ndimage
|
|
103
|
+
|
|
104
|
+
ijk = np.round((centers - origin) / pitch).astype(int)
|
|
105
|
+
dims = ijk.max(axis=0) + 1
|
|
106
|
+
grid = np.zeros(tuple(dims))
|
|
107
|
+
have = np.zeros(tuple(dims), bool)
|
|
108
|
+
grid[tuple(ijk.T)] = values
|
|
109
|
+
have[tuple(ijk.T)] = True
|
|
110
|
+
if not have.all():
|
|
111
|
+
idx = ndimage.distance_transform_edt(~have, return_distances=False, return_indices=True)
|
|
112
|
+
grid = grid[tuple(idx)]
|
|
113
|
+
return grid
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def grade_lattice(doc, study, target_sf=None, wall_min=None, wall_max=None, cells: int = 15_000,
|
|
117
|
+
iterations: int = 12, orient=None, rounds: int = 2, base_dir=None, node=None,
|
|
118
|
+
progress=None, n_rot: int = 800, seam: float = 1.0, confirm: str = "auto") -> dict:
|
|
119
|
+
"""Grade the graph's lattice wall to the stress (module docstring).
|
|
120
|
+
Returns {"doc" (density as a grid node reading the in-memory source),
|
|
121
|
+
"density": {"origin", "pitch", "values"}, "best" (the lightest analyzed
|
|
122
|
+
design that met the target), "history", "orient", "seconds"}. `seam`:
|
|
123
|
+
zones only, mm of solid plate between zones (orient.seam; 0 for none).
|
|
124
|
+
`confirm`: with "auto" (default) a lattice cut open at bare faces
|
|
125
|
+
(latticeExposed over lattice_orient.EXPOSED_LIMIT) is graded but not
|
|
126
|
+
turned, and "guard" says why (lattice_orient.optimize_orientation);
|
|
127
|
+
"none" turns it on the homogenized reading. Check a turned graded
|
|
128
|
+
design yourself with resolved.resolved_check."""
|
|
129
|
+
from scipy import ndimage
|
|
130
|
+
|
|
131
|
+
from .fields import FieldGraph, expand_groups
|
|
132
|
+
from .homogenized import SKIN_JUNCTION, homogenized_check, lattice_nodes
|
|
133
|
+
from .lattice import relative_density
|
|
134
|
+
from .lattice_orient import (_cell_group, _rotvec_deg, _smallest, candidate_rotations,
|
|
135
|
+
dominant_directions, stress_zones)
|
|
136
|
+
from .lattice_tensors import has_tensors, stress_rotation, tensors_at
|
|
137
|
+
from .struts import is_strut
|
|
138
|
+
from .verify import STRESS_ALLOWANCE
|
|
139
|
+
|
|
140
|
+
t0 = time.time()
|
|
141
|
+
say = progress or (lambda *_: None)
|
|
142
|
+
full = expand_groups(doc)
|
|
143
|
+
name = node or full.get("output") or list(full["nodes"])[-1]
|
|
144
|
+
g = FieldGraph({**full, "output": name}, base_dir=base_dir)
|
|
145
|
+
lats = lattice_nodes(g, name)
|
|
146
|
+
if len(lats) != 1:
|
|
147
|
+
raise ValueError(f"grading needs one lattice node in {name!r} (found {len(lats)})")
|
|
148
|
+
lat = lats[0]
|
|
149
|
+
spec = full["nodes"][lat]
|
|
150
|
+
kind = spec.get("type", "gyroid")
|
|
151
|
+
if "aspect" in spec:
|
|
152
|
+
raise ValueError(f"lattice {lat!r}: grading a stretched (aspect) cell is not supported yet")
|
|
153
|
+
if not isinstance(spec.get("cell", 8.0), (int, float)):
|
|
154
|
+
raise ValueError(f"lattice {lat!r}: grading needs a constant cell")
|
|
155
|
+
if not has_tensors(kind):
|
|
156
|
+
raise ValueError(f"lattice {lat!r} ({kind}) has no cell tensors to grade by")
|
|
157
|
+
cell = float(spec.get("cell", 8.0))
|
|
158
|
+
target = float(target_sf or (study.goal.safety_factor if getattr(study, "goal", None) else 2.0) or 2.0)
|
|
159
|
+
proc = getattr(study, "process", None)
|
|
160
|
+
wmin = float(wall_min if wall_min is not None else max(getattr(proc, "min_feature", 0.0) or 0.0, 0.6))
|
|
161
|
+
lo_lim = 0.02 if is_strut(kind) else 0.05
|
|
162
|
+
rho_min = float(np.clip(relative_density(kind, wmin, cell), lo_lim, 0.95))
|
|
163
|
+
rho_max = float(np.clip(relative_density(kind, wall_max, cell), rho_min, 0.95)) if wall_max else 0.6
|
|
164
|
+
cand, Pc, Qc, Fc = _cand_tensors(kind, rho_min, rho_max)
|
|
165
|
+
allow = study.material.strength / (target * STRESS_ALLOWANCE)
|
|
166
|
+
group = _cell_group(kind)
|
|
167
|
+
|
|
168
|
+
work = copy.deepcopy(full)
|
|
169
|
+
ls = work["nodes"][lat]
|
|
170
|
+
rho0 = float(np.clip(relative_density(kind, float(ls.get("thickness", 1.0)), cell), rho_min, rho_max)) \
|
|
171
|
+
if not isinstance(ls.get("thickness"), str) else 0.5 * (rho_min + rho_max)
|
|
172
|
+
ls.pop("thickness", None)
|
|
173
|
+
dens = f"{lat}__density"
|
|
174
|
+
ls["density"] = dens
|
|
175
|
+
work["nodes"][dens] = {"op": "grid", "source": SOURCE}
|
|
176
|
+
if isinstance(orient, (int, np.integer)) and orient > 1 or orient == "uniform":
|
|
177
|
+
ls.pop("orient", None)
|
|
178
|
+
|
|
179
|
+
def analyze(values, origin, pitch, fields=True):
|
|
180
|
+
src = {SOURCE: {"origin": origin, "pitch": pitch, "values": values}}
|
|
181
|
+
return homogenized_check(copy.deepcopy(work), study, node=name, base_dir=base_dir, cells=cells,
|
|
182
|
+
fields=fields, sources=src)
|
|
183
|
+
|
|
184
|
+
# the grid: the check's own, from one analysis at the uniform start
|
|
185
|
+
probe = {SOURCE: {"origin": np.zeros(3), "pitch": 1e3, "values": np.full((2, 2, 2), rho0)}}
|
|
186
|
+
rep = homogenized_check(copy.deepcopy(work), study, node=name, base_dir=base_dir, cells=cells, fields=True,
|
|
187
|
+
sources=probe)
|
|
188
|
+
fl = rep["fields"]
|
|
189
|
+
pitch = float(rep["analysisPitch"])
|
|
190
|
+
origin = fl["centers"].min(axis=0)
|
|
191
|
+
dims = np.round((fl["centers"].max(axis=0) - origin) / pitch).astype(int) + 1
|
|
192
|
+
G = np.full(tuple(dims), rho0) # the density grid (cell centres)
|
|
193
|
+
history, best = [], None
|
|
194
|
+
grow = max(1, int(round(0.25 * cell / pitch)))
|
|
195
|
+
|
|
196
|
+
def ijk_of(c):
|
|
197
|
+
return np.clip(np.round((c - origin) / pitch).astype(int), 0, dims - 1)
|
|
198
|
+
|
|
199
|
+
def size(G, fl, allow):
|
|
200
|
+
"""The grid's next density from the analysis `fl` of it."""
|
|
201
|
+
c = fl["centers"]
|
|
202
|
+
ijk = ijk_of(c)
|
|
203
|
+
rho = G[tuple(ijk.T)]
|
|
204
|
+
latc = fl["fc"] > 0.05
|
|
205
|
+
junction = (fl["fs"] > 0.05) & latc
|
|
206
|
+
worst = np.zeros((len(c), len(cand)))
|
|
207
|
+
for sc in _cell_stress(fl, np.ones(len(c), bool)):
|
|
208
|
+
worst = np.maximum(worst, _surfaces(sc, Pc, Qc, Fc))
|
|
209
|
+
worst = np.where(junction[:, None], worst * SKIN_JUNCTION, worst)
|
|
210
|
+
need = np.where(latc, need_density(worst, cand, allow, rho_max), 0.0)
|
|
211
|
+
g3 = np.zeros(tuple(dims))
|
|
212
|
+
g3[tuple(ijk.T)] = need
|
|
213
|
+
g3 = ndimage.maximum_filter(g3, size=2 * grow + 1)
|
|
214
|
+
need = np.where(latc, g3[tuple(ijk.T)], rho)
|
|
215
|
+
new = G.copy()
|
|
216
|
+
new[tuple(ijk.T)] = np.clip(np.clip(need, 0.5 * rho, 2.0 * rho), rho_min, rho_max)
|
|
217
|
+
return new
|
|
218
|
+
|
|
219
|
+
def run_sizing(G, fl, allow, tag):
|
|
220
|
+
nonlocal best
|
|
221
|
+
for it in range(iterations):
|
|
222
|
+
new = size(G, fl, allow)
|
|
223
|
+
rep = analyze(new, origin, pitch)
|
|
224
|
+
fl = rep["fields"]
|
|
225
|
+
sf = float(rep["safetyFactor"])
|
|
226
|
+
latc = fl["fc"] > 0.05
|
|
227
|
+
r_now = new[tuple(ijk_of(fl["centers"][latc]).T)]
|
|
228
|
+
row = {"stage": tag, "iteration": it, "mass": rep["mass"], "safetyFactor": sf,
|
|
229
|
+
"densityRange": [float(r_now.min()), float(r_now.max())] if len(r_now) else None,
|
|
230
|
+
"latticeTurnedExposed": rep.get("latticeTurnedExposed")}
|
|
231
|
+
if rep.get("warnings"):
|
|
232
|
+
row["warnings"] = rep["warnings"]
|
|
233
|
+
history.append(row)
|
|
234
|
+
say({"stage": "lattice-grade", **row})
|
|
235
|
+
if sf >= target * 0.995 and (best is None or rep["mass"] < best["mass"]):
|
|
236
|
+
best = {**row, "values": new.copy(), "orient": copy.deepcopy(work["nodes"][lat].get("orient"))}
|
|
237
|
+
short = sf / target
|
|
238
|
+
if it >= 1:
|
|
239
|
+
allow *= float(np.clip(short, 0.85, 1.15)) ** 0.5
|
|
240
|
+
converged = np.allclose(new, G, rtol=0.02, atol=0.003) and abs(short - 1.0) < 0.03
|
|
241
|
+
G = new
|
|
242
|
+
if converged:
|
|
243
|
+
break
|
|
244
|
+
return G, fl, allow
|
|
245
|
+
|
|
246
|
+
G, fl, allow = run_sizing(G, fl, allow, "size")
|
|
247
|
+
guard = None
|
|
248
|
+
if orient:
|
|
249
|
+
from .lattice_orient import _guard
|
|
250
|
+
|
|
251
|
+
if confirm not in ("auto", "none"):
|
|
252
|
+
raise ValueError("grade_lattice confirm is 'auto' or 'none' (check a turned design with resolved_check)")
|
|
253
|
+
exposed = float(rep.get("latticeExposed") or 0.0)
|
|
254
|
+
guard = _guard(confirm, exposed, True, None, None, study, "safetyFactor", name, base_dir)
|
|
255
|
+
if guard is not None:
|
|
256
|
+
say({"stage": "lattice-grade-guard", "reason": guard["reason"]})
|
|
257
|
+
orient = None
|
|
258
|
+
if orient:
|
|
259
|
+
# zones start from the best single direction: one uniform round first
|
|
260
|
+
k_z = 1 if orient == "uniform" else int(orient)
|
|
261
|
+
plan = [1] * rounds if k_z == 1 else [1] + [k_z] * rounds
|
|
262
|
+
current = [np.eye(3)]
|
|
263
|
+
at = None
|
|
264
|
+
for rnd, k in enumerate(plan):
|
|
265
|
+
centers = fl["centers"]
|
|
266
|
+
latc = fl["fc"] > 0.05
|
|
267
|
+
w = fl["fc"] * fl["fill"] * pitch ** 3
|
|
268
|
+
rho = G[tuple(ijk_of(centers).T)]
|
|
269
|
+
if k > 1:
|
|
270
|
+
from .fields import zone_weights
|
|
271
|
+
|
|
272
|
+
if at is None:
|
|
273
|
+
_, at = stress_zones(centers[latc], [s[latc] for s in fl["latticeStress"]], w[latc], k,
|
|
274
|
+
spatial=2.0)
|
|
275
|
+
current = [current[0]] * k
|
|
276
|
+
own = np.argmax(zone_weights(at, cell, centers), axis=1)
|
|
277
|
+
else:
|
|
278
|
+
own = np.zeros(len(centers), int)
|
|
279
|
+
rho_mid = float(np.median(rho[latc]))
|
|
280
|
+
C = tensors_at(kind, [rho_mid])[0][0]
|
|
281
|
+
rots = []
|
|
282
|
+
for i in range(k):
|
|
283
|
+
m = (own == i) & latc
|
|
284
|
+
dirs, _ = dominant_directions([s[m] for s in fl["latticeStress"]], w[m])
|
|
285
|
+
# the zone's current turn first (cost[0]): it is kept unless beaten
|
|
286
|
+
R = np.concatenate([[current[i]], candidate_rotations(C, dirs, n_random=n_rot, seed=17 * rnd + i)])
|
|
287
|
+
sig = [s[m] for s in fl["latticeStress"]]
|
|
288
|
+
junction = (fl["fs"][m] > 0.05)
|
|
289
|
+
vol = w[m]
|
|
290
|
+
cost = np.empty(len(R))
|
|
291
|
+
for j, r in enumerate(R):
|
|
292
|
+
t = stress_rotation(r)
|
|
293
|
+
worst = np.zeros((m.sum(), len(cand)))
|
|
294
|
+
for s in sig:
|
|
295
|
+
worst = np.maximum(worst, _surfaces(s @ t.T, Pc, Qc, Fc))
|
|
296
|
+
worst = np.where(junction[:, None], worst * SKIN_JUNCTION, worst)
|
|
297
|
+
need = need_density(worst, cand, allow, 2.0 * rho_max)
|
|
298
|
+
cost[j] = float((vol * need).sum())
|
|
299
|
+
rots.append(_smallest(R[int(np.argmin(cost))], group))
|
|
300
|
+
say({"stage": "lattice-grade-orient", "zone": i, "screened": len(R),
|
|
301
|
+
"massNeeded": float(cost.min()), "asLaid": float(cost[0])})
|
|
302
|
+
current = rots
|
|
303
|
+
if k > 1:
|
|
304
|
+
work["nodes"][lat]["orient"] = {"zones": [{"at": [round(float(v), 3) for v in a],
|
|
305
|
+
"rotate": [round(float(v), 3) for v in _rotvec_deg(r)]}
|
|
306
|
+
for a, r in zip(at, rots)], "blend": cell}
|
|
307
|
+
if seam:
|
|
308
|
+
work["nodes"][lat]["orient"]["seam"] = float(seam)
|
|
309
|
+
else:
|
|
310
|
+
work["nodes"][lat]["orient"] = [round(float(v), 3) for v in _rotvec_deg(rots[0])]
|
|
311
|
+
# re-read the stresses in the turned lattice before sizing on
|
|
312
|
+
rep = analyze(G, origin, pitch)
|
|
313
|
+
fl = rep["fields"]
|
|
314
|
+
G, fl, allow = run_sizing(G, fl, allow, f"size-after-orient-{rnd}")
|
|
315
|
+
out = copy.deepcopy(work)
|
|
316
|
+
if best is not None and best.get("orient") is not None:
|
|
317
|
+
out["nodes"][lat]["orient"] = best["orient"]
|
|
318
|
+
elif best is not None:
|
|
319
|
+
out["nodes"][lat].pop("orient", None)
|
|
320
|
+
return {"doc": out, "lattice": lat, "densityNode": dens, "source": SOURCE,
|
|
321
|
+
"density": None if best is None else {"origin": origin.tolist(), "pitch": pitch,
|
|
322
|
+
"values": best["values"]},
|
|
323
|
+
"best": None if best is None else {k: v for k, v in best.items() if k != "values"},
|
|
324
|
+
"target": target, "densityLimits": [rho_min, rho_max], "history": history, "guard": guard,
|
|
325
|
+
"seconds": time.time() - t0}
|