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,595 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lattice orientation as a design variable.
|
|
3
|
+
|
|
4
|
+
A strut or sheet cell is stiff and strong along some directions and soft
|
|
5
|
+
along others (a simple cubic cell carries a pull along its rods at about
|
|
6
|
+
ten times the stiffness it has along the cube diagonal; BCCz carries its
|
|
7
|
+
vertical struts' axis). homogenized_check reads a lattice from its full
|
|
8
|
+
tensors (lattice_tensors), so it can tell how the cell lies relative to the
|
|
9
|
+
load. `optimize_orientation` turns the cell (the lattice node's `orient`,
|
|
10
|
+
which rotates the cells and leaves the fields grading them where they are)
|
|
11
|
+
for the best worst-case safety factor, or the least compliance:
|
|
12
|
+
|
|
13
|
+
1. One homogenized analysis with the cell as it is gives every lattice
|
|
14
|
+
cell's macro stress per load case.
|
|
15
|
+
2. Screen: holding those macro stresses (the assumption fully stressed
|
|
16
|
+
sizing makes), each candidate rotation's peak wall stress (the stress
|
|
17
|
+
surface, as a p-norm over the cells the peak is read over) and
|
|
18
|
+
complementary energy are a rotation of tensors away, so thousands of
|
|
19
|
+
rotations cost a few seconds: rotations that put each of the cell's
|
|
20
|
+
stiffest directions on the dominant stress direction, spun about it,
|
|
21
|
+
and a random spread over all rotations.
|
|
22
|
+
3. The best few go through a full homogenized analysis each (the load
|
|
23
|
+
moves onto what got stiffer), then Nelder-Mead on the rotation vector
|
|
24
|
+
refines the best of those, again on full analyses.
|
|
25
|
+
|
|
26
|
+
The orientation is uniform over the lattice. It wins most where one stress
|
|
27
|
+
direction dominates (a beam's skinless core, a strut, a bracket arm): see
|
|
28
|
+
docs/fields.md. A gyroid is close to isotropic, so it gains little.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import copy
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
__all__ = ["confirm_turn", "dominant_directions", "optimize_orientation", "optimize_zones", "screen_rotations"]
|
|
38
|
+
|
|
39
|
+
# directions the cell's stiff axes are looked for along (cube axes, face
|
|
40
|
+
# and body diagonals; with a stretched cell these are in its own axes)
|
|
41
|
+
_LATTICE_DIRS = np.array([(1, 0, 0), (0, 1, 0), (0, 0, 1),
|
|
42
|
+
(1, 1, 0), (1, -1, 0), (1, 0, 1), (1, 0, -1), (0, 1, 1), (0, 1, -1),
|
|
43
|
+
(1, 1, 1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)], float)
|
|
44
|
+
_LATTICE_DIRS /= np.linalg.norm(_LATTICE_DIRS, axis=1, keepdims=True)
|
|
45
|
+
|
|
46
|
+
P_NORM = 8.0 # the screen's soft maximum of the wall stress
|
|
47
|
+
|
|
48
|
+
# a lattice with more than this share of its volume cut open at a bare face
|
|
49
|
+
# (homogenized_check's latticeExposed) is not turned on the homogenized
|
|
50
|
+
# reading alone, unless its cell is in homogenized.FACE_CUT_VALIDATED (one
|
|
51
|
+
# direction only; zones and grading keep the guard). Cut square, its cells keep struts running along the face
|
|
52
|
+
# and the free-surface knockdown reads them 1.5-2.8x conservative; turned,
|
|
53
|
+
# they are cut on the bias into stubs and read about right or too stiff. So
|
|
54
|
+
# the optimizer saw gains that a full-resolution check reverses: skinless
|
|
55
|
+
# bars claimed -23/-15/-7% mass and came out 3.2x/2.2x/1.3x weaker than as
|
|
56
|
+
# laid at the same wall (bench/results/lattice-orient-confirm.json).
|
|
57
|
+
EXPOSED_LIMIT = 0.1
|
|
58
|
+
TURN_MIN_DEG = 5.0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _turn_deg(rv):
|
|
62
|
+
return float(np.linalg.norm(np.asarray(rv, float)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def confirm_turn(laid_doc, turned_doc, study, objective="safetyFactor", node=None, base_dir=None,
|
|
66
|
+
pitch=None, sources=None, tolerance=0.02) -> dict:
|
|
67
|
+
"""Both designs at full resolution (resolved.resolved_check): is the
|
|
68
|
+
turned one at least as strong (objective "safetyFactor": worst-case
|
|
69
|
+
peak) or as stiff ("stiffness": total compliance), within `tolerance`?
|
|
70
|
+
Returns {"confirmed", "asLaid", "turned" (worst peak / SF / compliance,
|
|
71
|
+
seconds), "ratio" (turned over as laid, of the peak or compliance)}."""
|
|
72
|
+
from .resolved import resolved_check
|
|
73
|
+
|
|
74
|
+
rows = {}
|
|
75
|
+
for tag, d in (("asLaid", laid_doc), ("turned", turned_doc)):
|
|
76
|
+
r = resolved_check(d, study, pitch=pitch, node=node, base_dir=base_dir, sources=sources)
|
|
77
|
+
rows[tag] = {"safetyFactor": r["safetyFactor"], "peakStress": max(c["peakStress"] for c in r["cases"]),
|
|
78
|
+
"compliance": float(sum(c["compliance"] for c in r["cases"])), "mass": r["mass"],
|
|
79
|
+
"cells": r["cells"], "pitch": r["pitch"], "seconds": r["seconds"]}
|
|
80
|
+
key = "peakStress" if objective == "safetyFactor" else "compliance"
|
|
81
|
+
ratio = rows["turned"][key] / max(rows["asLaid"][key], 1e-30)
|
|
82
|
+
return {"confirmed": bool(ratio <= 1.0 + tolerance), "ratio": float(ratio), "measure": key, **rows}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _guard(confirm, exposed, turned, laid_doc, turned_doc, study, objective, node, base_dir, pitch=None,
|
|
86
|
+
validated=False):
|
|
87
|
+
"""None when the turn stands on the homogenized reading; else a dict
|
|
88
|
+
with "confirmed" (False/None: keep the cells as laid) and why.
|
|
89
|
+
`validated`: the cell's bare-face reading holds turned against as laid
|
|
90
|
+
(homogenized.FACE_CUT_VALIDATED), so "auto" trusts it."""
|
|
91
|
+
if confirm not in ("auto", "resolved", "none"):
|
|
92
|
+
raise ValueError("confirm is 'auto', 'resolved' or 'none'")
|
|
93
|
+
if confirm == "none" or not turned or exposed <= EXPOSED_LIMIT or (confirm == "auto" and validated):
|
|
94
|
+
return None
|
|
95
|
+
if confirm == "resolved":
|
|
96
|
+
out = confirm_turn(laid_doc, turned_doc, study, objective, node=node, base_dir=base_dir, pitch=pitch)
|
|
97
|
+
out["latticeExposed"] = exposed
|
|
98
|
+
out["reason"] = (f"{100 * exposed:.0f}% of the lattice is cut open at bare faces; full-resolution check: "
|
|
99
|
+
f"turned/as laid {out['measure']} {out['ratio']:.2f}"
|
|
100
|
+
+ ("" if out["confirmed"] else ", so the cells stay as laid"))
|
|
101
|
+
return out
|
|
102
|
+
return {"confirmed": None, "latticeExposed": exposed,
|
|
103
|
+
"reason": f"{100 * exposed:.0f}% of the lattice is cut open at bare faces (no skin), where the "
|
|
104
|
+
"homogenized check misreads turned cells of this kind against square-cut ones (skinless "
|
|
105
|
+
"bars and an L-bracket it read 7-41% lighter turned came out 1.3-3.4x weaker at full "
|
|
106
|
+
"resolution; only octet's turn has held), so the cells stay as "
|
|
107
|
+
"laid. Add a skin, or pass confirm='resolved' (--confirm resolved) to check the turn at "
|
|
108
|
+
"full resolution"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _voigt_tensor(s):
|
|
112
|
+
"""(n, 6) Voigt stresses in loadpath's order (xx, yy, zz, xy, yz, zx,
|
|
113
|
+
lattice_tensors._VOIGT) -> (n, 3, 3)."""
|
|
114
|
+
t = np.empty((len(s), 3, 3))
|
|
115
|
+
t[:, 0, 0], t[:, 1, 1], t[:, 2, 2] = s[:, 0], s[:, 1], s[:, 2]
|
|
116
|
+
t[:, 0, 1] = t[:, 1, 0] = s[:, 3]
|
|
117
|
+
t[:, 1, 2] = t[:, 2, 1] = s[:, 4]
|
|
118
|
+
t[:, 2, 0] = t[:, 0, 2] = s[:, 5]
|
|
119
|
+
return t
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _principal(stresses):
|
|
123
|
+
"""Per cell and case, the principal direction of the largest |stress|
|
|
124
|
+
and that stress: (n, cases, 3), (n, cases)."""
|
|
125
|
+
dirs, mags = [], []
|
|
126
|
+
for st in stresses:
|
|
127
|
+
lam, vec = np.linalg.eigh(_voigt_tensor(st))
|
|
128
|
+
i = np.argmax(np.abs(lam), axis=1)
|
|
129
|
+
dirs.append(vec[np.arange(len(st)), :, i])
|
|
130
|
+
mags.append(np.abs(lam[np.arange(len(st)), i]))
|
|
131
|
+
return np.stack(dirs, 1), np.stack(mags, 1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def dominant_directions(stresses, weights):
|
|
135
|
+
"""Principal axes of sum_e w_e sum_cases s1^2 n1 n1^T over cells and
|
|
136
|
+
cases, n1 the direction of each cell's largest principal stress s1,
|
|
137
|
+
strongest first (columns): the direction the lattice's stress mostly
|
|
138
|
+
runs along, whatever its sign. (sigma . sigma would do for an axial
|
|
139
|
+
stress but has no preferred axis in pure shear.)"""
|
|
140
|
+
n, s = _principal(stresses)
|
|
141
|
+
m = np.einsum("e,ec,eci,ecj->ij", weights, s ** 2, n, n)
|
|
142
|
+
w, v = np.linalg.eigh(m)
|
|
143
|
+
return v[:, ::-1], w[::-1]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _align(a, b):
|
|
147
|
+
"""A rotation taking unit vector a onto unit vector b."""
|
|
148
|
+
from scipy.spatial.transform import Rotation
|
|
149
|
+
|
|
150
|
+
c = np.cross(a, b)
|
|
151
|
+
s, d = np.linalg.norm(c), float(np.dot(a, b))
|
|
152
|
+
if s < 1e-12:
|
|
153
|
+
if d > 0:
|
|
154
|
+
return np.eye(3)
|
|
155
|
+
p = np.cross(a, [1, 0, 0] if abs(a[0]) < 0.9 else [0, 1, 0])
|
|
156
|
+
return Rotation.from_rotvec(np.pi * p / np.linalg.norm(p)).as_matrix()
|
|
157
|
+
return Rotation.from_rotvec(c / s * np.arctan2(s, d)).as_matrix()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def candidate_rotations(C, dirs, n_spin=12, n_random=1500, seed=0):
|
|
161
|
+
"""Rotations (k, 3, 3), columns the cell's axes in global components:
|
|
162
|
+
the identity; each of the cell's stiffest lattice directions (within
|
|
163
|
+
15% of the stiffest) onto the dominant stress direction, spun about it
|
|
164
|
+
in n_spin steps; random ones."""
|
|
165
|
+
from scipy.spatial.transform import Rotation
|
|
166
|
+
|
|
167
|
+
from .lattice_tensors import directional_modulus
|
|
168
|
+
|
|
169
|
+
e = np.array([directional_modulus(C, d) for d in _LATTICE_DIRS])
|
|
170
|
+
stiff = _LATTICE_DIRS[e >= 0.85 * e.max()]
|
|
171
|
+
out = [np.eye(3)]
|
|
172
|
+
for d in stiff:
|
|
173
|
+
base = _align(d, dirs[:, 0])
|
|
174
|
+
for k in range(n_spin):
|
|
175
|
+
spin = Rotation.from_rotvec(dirs[:, 0] * np.pi * k / n_spin).as_matrix()
|
|
176
|
+
out.append(spin @ base)
|
|
177
|
+
out += list(Rotation.random(n_random, random_state=seed).as_matrix())
|
|
178
|
+
return np.array(out)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def screen_rotations(R, fields, aspect=None, mask=None, absolute=False):
|
|
182
|
+
"""Score rotations holding each lattice cell's macro stress. Returns
|
|
183
|
+
(peak, energy): per rotation the p-norm of the wall stress over the
|
|
184
|
+
cells the peak is read over (worst case) and the complementary energy
|
|
185
|
+
summed over cases, both relative to the cells' tensors in their current
|
|
186
|
+
frames turned by R (the cell axes are frames @ R)."""
|
|
187
|
+
from .lattice_tensors import stress_rotation, stress_surface, tensors_at
|
|
188
|
+
|
|
189
|
+
rho, fc, fill = fields["rho"], fields["fc"], fields["fill"]
|
|
190
|
+
lat = fc > 0.05
|
|
191
|
+
if mask is not None:
|
|
192
|
+
lat = lat & mask
|
|
193
|
+
C, P, Q, F = tensors_at(fields["kind"], rho[lat], aspect)
|
|
194
|
+
S = np.linalg.inv(C)
|
|
195
|
+
w = (fc * fill)[lat]
|
|
196
|
+
frames = fields["frames"]
|
|
197
|
+
# absolute: R is the cells' frame itself (not a turn of the current one)
|
|
198
|
+
f0 = np.broadcast_to(np.eye(3), (lat.sum(), 3, 3)) if frames is None or absolute else frames[lat]
|
|
199
|
+
peak = np.zeros(len(R))
|
|
200
|
+
energy = np.zeros(len(R))
|
|
201
|
+
for ci, sig in enumerate(fields["latticeStress"]):
|
|
202
|
+
sig = sig[lat]
|
|
203
|
+
body = fields["body"][ci][lat]
|
|
204
|
+
for k, r in enumerate(R):
|
|
205
|
+
t = stress_rotation(np.einsum("eij,jk->eik", f0, r))
|
|
206
|
+
sc = np.einsum("eab,eb->ea", t, sig)
|
|
207
|
+
s = stress_surface(sc[body], P[body], Q[body], F[body])
|
|
208
|
+
m = s.max() if len(s) else 0.0
|
|
209
|
+
pn = m * (np.sum(w[body] * (s / max(m, 1e-30)) ** P_NORM) / max(w[body].sum(), 1e-30)) ** (1 / P_NORM)
|
|
210
|
+
peak[k] = max(peak[k], pn)
|
|
211
|
+
energy[k] += float(np.einsum("e,ea,eab,eb->", w, sc, S, sc))
|
|
212
|
+
return peak, energy
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _cell_group(kind, aspect=None):
|
|
216
|
+
from .lattice_tensors import _group, symmetry_of
|
|
217
|
+
|
|
218
|
+
sym = "orthorhombic" if aspect is not None and not np.allclose(aspect, aspect[0]) else symmetry_of(kind)
|
|
219
|
+
return _group(sym) if sym != "none" else np.eye(3)[None]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _angle_mod(a, b, group):
|
|
223
|
+
"""Least rotation angle between cell frames a and b, the cell's
|
|
224
|
+
symmetry rotations (in its own axes) taken as no turn."""
|
|
225
|
+
tr = np.einsum("ji,jk,gki->g", a, b, group)
|
|
226
|
+
return float(np.arccos(np.clip((tr.max() - 1.0) / 2.0, -1.0, 1.0)))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _smallest(r, group):
|
|
230
|
+
"""The frame r @ g (g a symmetry rotation) nearest the identity."""
|
|
231
|
+
tr = np.einsum("ij,gji->g", r, group)
|
|
232
|
+
return r @ group[int(np.argmax(tr))]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _with_orient(doc, lat, rv):
|
|
236
|
+
d = copy.deepcopy(doc)
|
|
237
|
+
nodes = d["nodes"]
|
|
238
|
+
spec = nodes[lat]
|
|
239
|
+
if np.allclose(rv, 0.0):
|
|
240
|
+
spec.pop("orient", None)
|
|
241
|
+
else:
|
|
242
|
+
spec["orient"] = [round(float(x), 3) for x in rv]
|
|
243
|
+
return d
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _rotvec_deg(R):
|
|
247
|
+
from scipy.spatial.transform import Rotation
|
|
248
|
+
|
|
249
|
+
return np.degrees(Rotation.from_matrix(R).as_rotvec())
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def optimize_orientation(doc, study, node=None, base_dir=None, cells: int = 15_000, objective: str = "safetyFactor",
|
|
253
|
+
full_checks: int = 4, refine: int = 24, progress=None, confirm: str = "auto",
|
|
254
|
+
resolved_pitch: float | None = None) -> dict:
|
|
255
|
+
"""Turn the graph's lattice (its `orient`) for the best worst-case
|
|
256
|
+
safety factor (objective "safetyFactor") or the least total compliance
|
|
257
|
+
("stiffness"), on homogenized analyses of `cells` cells (module
|
|
258
|
+
docstring). Returns {"doc" (the graph with the best orient written),
|
|
259
|
+
"orient" (degrees, rotation vector), "start", "best" (homogenized
|
|
260
|
+
reports, trimmed), "screened", "evaluations", "history", "seconds"}.
|
|
261
|
+
The lattice must be cartesian with a constant cell (orient's limits);
|
|
262
|
+
an orient it already has is the starting point.
|
|
263
|
+
|
|
264
|
+
`confirm`: a lattice cut open at bare faces (latticeExposed over
|
|
265
|
+
EXPOSED_LIMIT) is misread turned against as laid, so with "auto" (the
|
|
266
|
+
default) its cells stay as laid and "guard" says why, unless the cell
|
|
267
|
+
is one whose bare-face reading was confirmed turned
|
|
268
|
+
(homogenized.FACE_CUT_VALIDATED: octet); "resolved" checks
|
|
269
|
+
the turn against as laid at full resolution (confirm_turn, minutes;
|
|
270
|
+
`resolved_pitch`, default resolved.default_pitch) and keeps it only if
|
|
271
|
+
it holds; "none" trusts the homogenized reading."""
|
|
272
|
+
from scipy.optimize import minimize
|
|
273
|
+
from scipy.spatial.transform import Rotation
|
|
274
|
+
|
|
275
|
+
from .fields import FieldGraph, expand_groups
|
|
276
|
+
from .homogenized import homogenized_check, lattice_nodes
|
|
277
|
+
from .lattice_tensors import has_tensors, tensors_at
|
|
278
|
+
|
|
279
|
+
if objective not in ("safetyFactor", "stiffness"):
|
|
280
|
+
raise ValueError("objective is 'safetyFactor' or 'stiffness'")
|
|
281
|
+
t0 = time.time()
|
|
282
|
+
full = expand_groups(doc)
|
|
283
|
+
name = node or full.get("output") or list(full["nodes"])[-1]
|
|
284
|
+
g = FieldGraph({**full, "output": name}, base_dir=base_dir)
|
|
285
|
+
lats = lattice_nodes(g, name)
|
|
286
|
+
if len(lats) != 1:
|
|
287
|
+
raise ValueError(f"orientation needs one lattice node in {name!r} (found {len(lats)})")
|
|
288
|
+
lat = lats[0]
|
|
289
|
+
spec = full["nodes"][lat]
|
|
290
|
+
kind = spec.get("type", "gyroid")
|
|
291
|
+
aspect = tuple(spec["aspect"]) if "aspect" in spec else None
|
|
292
|
+
if not has_tensors(kind, aspect):
|
|
293
|
+
raise ValueError(f"lattice {lat!r} ({kind}) has no cell tensors to orient by")
|
|
294
|
+
say = progress or (lambda *_: None)
|
|
295
|
+
r_start = (Rotation.from_rotvec(np.radians(np.asarray(spec["orient"], float))).as_matrix()
|
|
296
|
+
if isinstance(spec.get("orient"), list) else np.eye(3))
|
|
297
|
+
if isinstance(spec.get("orient"), dict):
|
|
298
|
+
from .fields import lattice_orientation
|
|
299
|
+
|
|
300
|
+
r_start = lattice_orientation(g.nodes[lat])
|
|
301
|
+
r_start = np.eye(3) if r_start is None else r_start
|
|
302
|
+
base = _with_orient(full, lat, np.zeros(3))
|
|
303
|
+
history = []
|
|
304
|
+
|
|
305
|
+
def score(rep):
|
|
306
|
+
if objective == "safetyFactor":
|
|
307
|
+
return -float(rep["safetyFactor"])
|
|
308
|
+
return float(sum(c["compliance"] for c in rep["cases"]))
|
|
309
|
+
|
|
310
|
+
def trim(rep, rv):
|
|
311
|
+
return {"orient": [float(x) for x in rv], "mass": rep["mass"], "safetyFactor": rep["safetyFactor"],
|
|
312
|
+
"compliance": float(sum(c["compliance"] for c in rep["cases"])),
|
|
313
|
+
"cases": [{k: c[k] for k in ("name", "safetyFactor", "deflection", "compliance")}
|
|
314
|
+
for c in rep["cases"]],
|
|
315
|
+
"latticeTurnedExposed": rep.get("latticeTurnedExposed"), "warnings": rep.get("warnings", [])}
|
|
316
|
+
|
|
317
|
+
cache = {}
|
|
318
|
+
|
|
319
|
+
def check(rv):
|
|
320
|
+
key = tuple(np.round(rv, 2))
|
|
321
|
+
if key not in cache:
|
|
322
|
+
rep = homogenized_check(_with_orient(base, lat, rv), study, node=name, base_dir=base_dir, cells=cells)
|
|
323
|
+
cache[key] = (score(rep), trim(rep, rv))
|
|
324
|
+
history.append(cache[key][1])
|
|
325
|
+
say({"stage": "lattice-orient", "orient": np.round(rv, 1).tolist(),
|
|
326
|
+
"safetyFactor": rep["safetyFactor"], "compliance": history[-1]["compliance"]})
|
|
327
|
+
return cache[key]
|
|
328
|
+
|
|
329
|
+
# the cell as it lies (no orient) for the macro stresses and frames
|
|
330
|
+
ref = homogenized_check(base, study, node=name, base_dir=base_dir, cells=cells, fields=True)
|
|
331
|
+
fl = ref["fields"]
|
|
332
|
+
if fl["latticeStress"] is None:
|
|
333
|
+
raise ValueError("the anisotropic reading is off (LOADPATH_LATTICE_ANISO=0): orientation needs it")
|
|
334
|
+
lat_cells = fl["fc"] > 0.05
|
|
335
|
+
dirs, strength = dominant_directions([s[lat_cells] for s in fl["latticeStress"]],
|
|
336
|
+
(fl["fc"] * fl["fill"])[lat_cells])
|
|
337
|
+
rho_mid = float(np.median(fl["rho"][lat_cells]))
|
|
338
|
+
C = tensors_at(kind, [rho_mid], aspect)[0][0]
|
|
339
|
+
# candidates are the cell's rotation from its unrotated lie; the stiff
|
|
340
|
+
# directions are in the cell's axes, the stress directions in global
|
|
341
|
+
f0 = fl["frames"]
|
|
342
|
+
f_mean = np.eye(3) if f0 is None else f0[lat_cells].mean(axis=0)
|
|
343
|
+
u, _, vt = np.linalg.svd(f_mean)
|
|
344
|
+
f_mean = u @ vt
|
|
345
|
+
R = candidate_rotations(C, f_mean.T @ dirs)
|
|
346
|
+
R = np.concatenate([[r_start], R])
|
|
347
|
+
peak, energy = screen_rotations(R, fl, aspect)
|
|
348
|
+
rank = np.argsort(peak if objective == "safetyFactor" else energy)
|
|
349
|
+
group = _cell_group(kind, aspect)
|
|
350
|
+
picks = [np.eye(3)] # the unrotated cell is checked anyway
|
|
351
|
+
for k in rank:
|
|
352
|
+
# one of each set the cell's symmetry makes the same, written as
|
|
353
|
+
# its smallest turn
|
|
354
|
+
if min(_angle_mod(R[k], p, group) for p in picks) < np.radians(8.0):
|
|
355
|
+
continue
|
|
356
|
+
picks.append(_smallest(R[k], group))
|
|
357
|
+
if len(picks) > full_checks:
|
|
358
|
+
break
|
|
359
|
+
picks = [_rotvec_deg(p) for p in picks[1:]]
|
|
360
|
+
start_rv = _rotvec_deg(r_start)
|
|
361
|
+
start = check(start_rv)
|
|
362
|
+
check(np.zeros(3))
|
|
363
|
+
for rv in picks:
|
|
364
|
+
check(rv)
|
|
365
|
+
best_key = min(cache, key=lambda k: cache[k][0])
|
|
366
|
+
x0 = np.array(best_key, float)
|
|
367
|
+
if refine > 0:
|
|
368
|
+
simplex = [x0] + [x0 + 12.0 * e for e in np.eye(3)]
|
|
369
|
+
minimize(lambda x: check(x)[0], x0, method="Nelder-Mead",
|
|
370
|
+
options={"initial_simplex": simplex, "maxfev": refine, "xatol": 1.0, "fatol": 1e-3})
|
|
371
|
+
best_key = min(cache, key=lambda k: cache[k][0])
|
|
372
|
+
best = cache[best_key][1]
|
|
373
|
+
src = copy.deepcopy(doc) if "nodes" in doc and lat in doc["nodes"] else full
|
|
374
|
+
out_doc = _with_orient(src, lat, np.array(best_key, float))
|
|
375
|
+
zero = tuple(np.round(np.zeros(3), 2))
|
|
376
|
+
from .homogenized import FACE_CUT_VALIDATED
|
|
377
|
+
|
|
378
|
+
guard = _guard(confirm, float(ref.get("latticeExposed") or 0.0), _turn_deg(best_key) > TURN_MIN_DEG,
|
|
379
|
+
_with_orient(src, lat, np.zeros(3)), out_doc, study, objective, name, base_dir, resolved_pitch,
|
|
380
|
+
validated=kind in FACE_CUT_VALIDATED and aspect is None)
|
|
381
|
+
candidate = None
|
|
382
|
+
if guard is not None and not guard["confirmed"]:
|
|
383
|
+
candidate, best_key = best, zero
|
|
384
|
+
best = cache[zero][1]
|
|
385
|
+
out_doc = _with_orient(src, lat, np.zeros(3))
|
|
386
|
+
say({"stage": "lattice-orient-guard", "reason": guard["reason"]})
|
|
387
|
+
return {"doc": out_doc, "orient": [float(x) for x in best_key], "lattice": lat, "objective": objective,
|
|
388
|
+
"guard": guard, "candidate": candidate,
|
|
389
|
+
"start": start[1], "unrotated": cache[zero][1], "best": best,
|
|
390
|
+
"screened": int(len(R)), "dominantStress": dirs[:, 0].tolist(),
|
|
391
|
+
"stressAnisotropy": float(strength[0] / max(strength.sum(), 1e-30)),
|
|
392
|
+
"evaluations": len(history), "history": history, "seconds": time.time() - t0}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _cell_directions(stresses, weights):
|
|
396
|
+
"""Per cell, the axis its lattice stress mostly runs along (the top
|
|
397
|
+
eigenvector of sum over cases of s1^2 n1 n1^T) and its strength."""
|
|
398
|
+
n, s = _principal(stresses)
|
|
399
|
+
m = np.einsum("ec,eci,ecj->eij", s ** 2, n, n)
|
|
400
|
+
w, v = np.linalg.eigh(m)
|
|
401
|
+
return v[:, :, -1], w[:, -1]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def stress_zones(centers, stresses, weights, k, spatial=1.0, seed=0):
|
|
405
|
+
"""Split the lattice cells into k zones whose stress runs one way:
|
|
406
|
+
k-means on the cells' stress axis (as n n^T, sign free) and position
|
|
407
|
+
(scaled by the lattice's extent, times `spatial`), weighted by stress.
|
|
408
|
+
Returns (labels, zone points: the stress-weighted centroids)."""
|
|
409
|
+
from scipy.cluster.vq import kmeans2
|
|
410
|
+
|
|
411
|
+
n, mag = _cell_directions(stresses, weights)
|
|
412
|
+
nn = np.einsum("ei,ej->eij", n, n).reshape(-1, 9)[:, [0, 1, 2, 4, 5, 8]]
|
|
413
|
+
nn[:, [1, 2, 4]] *= np.sqrt(2.0)
|
|
414
|
+
span = np.ptp(centers, axis=0).max()
|
|
415
|
+
x = np.hstack([nn, spatial * (centers - centers.mean(axis=0)) / max(span, 1e-9)])
|
|
416
|
+
# weight by stress: repeat the stressed cells (kmeans2 has no weights)
|
|
417
|
+
sw = np.sqrt(np.maximum(mag, 0.0)) * weights
|
|
418
|
+
keep = sw > 0.05 * sw.max()
|
|
419
|
+
rng = np.random.default_rng(seed)
|
|
420
|
+
best = None
|
|
421
|
+
for _ in range(6):
|
|
422
|
+
cen, lab = kmeans2(x[keep], k, minit="++", seed=int(rng.integers(1 << 31)))
|
|
423
|
+
cost = float(((x[keep] - cen[lab]) ** 2).sum())
|
|
424
|
+
if best is None or cost < best[0]:
|
|
425
|
+
best = (cost, cen)
|
|
426
|
+
cen = best[1]
|
|
427
|
+
labels = np.argmin(((x[:, None, :] - cen[None]) ** 2).sum(-1), axis=1)
|
|
428
|
+
at = np.array([np.average(centers[labels == i], axis=0, weights=sw[labels == i] + 1e-12)
|
|
429
|
+
if (labels == i).any() else centers.mean(axis=0) for i in range(k)])
|
|
430
|
+
return labels, at
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def optimize_zones(doc, study, zones: int = 3, node=None, base_dir=None, cells: int = 15_000,
|
|
434
|
+
objective: str = "safetyFactor", rounds: int = 2, alternatives: int = 2, blend=None,
|
|
435
|
+
progress=None, spatial: float = 1.0, uniform_refine: int = 8, zone_refine: int = 6,
|
|
436
|
+
start=None, seam=1.0, confirm: str = "auto", resolved_pitch: float | None = None) -> dict:
|
|
437
|
+
"""Cell direction that follows the stress: the lattice split into
|
|
438
|
+
`zones` zones (stress_zones), each turned on its own (orient.zones,
|
|
439
|
+
cross-faded over `blend` mm, default one cell, where they meet). Each
|
|
440
|
+
round analyzes the current design for the macro stresses, screens every
|
|
441
|
+
zone's rotations on its own cells (absolute frames, the stresses held)
|
|
442
|
+
and takes the best per zone; then for each zone in turn its next
|
|
443
|
+
`alternatives` screened rotations are tried with full analyses, keeping
|
|
444
|
+
any that improve. Returns like optimize_orientation, with "zones" (the
|
|
445
|
+
orient.zones written) and "uniform" (the best single orient, its own
|
|
446
|
+
run, for comparison) left to the caller. `seam` (mm, default 1): a
|
|
447
|
+
solid plate between zones (orient.seam), which the zones' cells end on
|
|
448
|
+
as on a skin; 0 for none (cross-faded zones, which read too stiff where
|
|
449
|
+
thin cells of two directions meet). `confirm`: as optimize_orientation;
|
|
450
|
+
a guarded result comes back with the cells as laid ("zones" None)."""
|
|
451
|
+
from .fields import FieldGraph, expand_groups, zone_weights
|
|
452
|
+
from .homogenized import homogenized_check, lattice_nodes
|
|
453
|
+
from .lattice_tensors import has_tensors, tensors_at
|
|
454
|
+
|
|
455
|
+
t0 = time.time()
|
|
456
|
+
full = expand_groups(doc)
|
|
457
|
+
name = node or full.get("output") or list(full["nodes"])[-1]
|
|
458
|
+
g = FieldGraph({**full, "output": name}, base_dir=base_dir)
|
|
459
|
+
lats = lattice_nodes(g, name)
|
|
460
|
+
if len(lats) != 1:
|
|
461
|
+
raise ValueError(f"orientation needs one lattice node in {name!r} (found {len(lats)})")
|
|
462
|
+
lat = lats[0]
|
|
463
|
+
spec = full["nodes"][lat]
|
|
464
|
+
kind = spec.get("type", "gyroid")
|
|
465
|
+
aspect = tuple(spec["aspect"]) if "aspect" in spec else None
|
|
466
|
+
if not has_tensors(kind, aspect):
|
|
467
|
+
raise ValueError(f"lattice {lat!r} ({kind}) has no cell tensors to orient by")
|
|
468
|
+
say = progress or (lambda *_: None)
|
|
469
|
+
blend = float(blend if blend is not None else (spec.get("cell", 8.0) if isinstance(spec.get("cell", 8.0),
|
|
470
|
+
(int, float)) else 8.0))
|
|
471
|
+
history = []
|
|
472
|
+
|
|
473
|
+
def doc_for(at, rots):
|
|
474
|
+
d = copy.deepcopy(full)
|
|
475
|
+
d["nodes"][lat]["orient"] = {"zones": [{"at": [round(float(v), 3) for v in a],
|
|
476
|
+
"rotate": [round(float(v), 3) for v in _rotvec_deg(r)]}
|
|
477
|
+
for a, r in zip(at, rots)], "blend": blend}
|
|
478
|
+
if seam:
|
|
479
|
+
d["nodes"][lat]["orient"]["seam"] = float(seam)
|
|
480
|
+
return d
|
|
481
|
+
|
|
482
|
+
def score(rep):
|
|
483
|
+
if objective == "safetyFactor":
|
|
484
|
+
return -float(rep["safetyFactor"])
|
|
485
|
+
return float(sum(c["compliance"] for c in rep["cases"]))
|
|
486
|
+
|
|
487
|
+
def check(at, rots, fields=False):
|
|
488
|
+
rep = homogenized_check(doc_for(at, rots), study, node=name, base_dir=base_dir, cells=cells, fields=fields)
|
|
489
|
+
row = {"zones": doc_for(at, rots)["nodes"][lat]["orient"]["zones"], "mass": rep["mass"],
|
|
490
|
+
"safetyFactor": rep["safetyFactor"], "compliance": float(sum(c["compliance"] for c in rep["cases"])),
|
|
491
|
+
"latticeTurnedExposed": rep.get("latticeTurnedExposed"), "warnings": rep.get("warnings", [])}
|
|
492
|
+
history.append(row)
|
|
493
|
+
say({"stage": "lattice-zones", "safetyFactor": rep["safetyFactor"], "compliance": row["compliance"]})
|
|
494
|
+
return score(rep), row, rep
|
|
495
|
+
|
|
496
|
+
base = copy.deepcopy(full)
|
|
497
|
+
base["nodes"][lat].pop("orient", None)
|
|
498
|
+
ref = homogenized_check(base, study, node=name, base_dir=base_dir, cells=cells, fields=True)
|
|
499
|
+
fl = ref["fields"]
|
|
500
|
+
if fl["latticeStress"] is None:
|
|
501
|
+
raise ValueError("the anisotropic reading is off (LOADPATH_LATTICE_ANISO=0): orientation needs it")
|
|
502
|
+
unrot = {"mass": ref["mass"], "safetyFactor": ref["safetyFactor"],
|
|
503
|
+
"compliance": float(sum(c["compliance"] for c in ref["cases"]))}
|
|
504
|
+
lat_cells = fl["fc"] > 0.05
|
|
505
|
+
w = (fl["fc"] * fl["fill"])
|
|
506
|
+
labels, at = stress_zones(fl["centers"][lat_cells], [s[lat_cells] for s in fl["latticeStress"]],
|
|
507
|
+
w[lat_cells], zones, spatial=spatial)
|
|
508
|
+
group = _cell_group(kind, aspect)
|
|
509
|
+
rho_mid = float(np.median(fl["rho"][lat_cells]))
|
|
510
|
+
C = tensors_at(kind, [rho_mid], aspect)[0][0]
|
|
511
|
+
# start from the best single direction (so zones can only gain on it)
|
|
512
|
+
if start is None:
|
|
513
|
+
uni = optimize_orientation(doc, study, node=node, base_dir=base_dir, cells=cells, objective=objective,
|
|
514
|
+
full_checks=2, refine=uniform_refine, progress=progress, confirm="none")
|
|
515
|
+
start = uni["orient"]
|
|
516
|
+
uniform = uni["best"]
|
|
517
|
+
else:
|
|
518
|
+
uniform = None
|
|
519
|
+
r0 = _rotation_from_deg(start)
|
|
520
|
+
rots = [r0 for _ in range(zones)]
|
|
521
|
+
sc0, row0, rep0 = check(at, rots, fields=True)
|
|
522
|
+
best = (sc0, row0, list(rots))
|
|
523
|
+
fl = rep0["fields"]
|
|
524
|
+
for rnd in range(rounds):
|
|
525
|
+
# the zones' weights at the cells: a cell screens with the zone it mostly belongs to
|
|
526
|
+
wz = zone_weights(at, blend, fl["centers"])
|
|
527
|
+
own = np.argmax(wz, axis=1)
|
|
528
|
+
ranked = []
|
|
529
|
+
for i in range(zones):
|
|
530
|
+
m = own == i
|
|
531
|
+
dirs, _ = dominant_directions([s[m & lat_cells] for s in fl["latticeStress"]], w[m & lat_cells])
|
|
532
|
+
R = np.concatenate([[rots[i]], candidate_rotations(C, dirs, n_random=600, seed=rnd * 31 + i)])
|
|
533
|
+
peak, energy = screen_rotations(R, fl, aspect, mask=m, absolute=True)
|
|
534
|
+
order = np.argsort(peak if objective == "safetyFactor" else energy)
|
|
535
|
+
picks = []
|
|
536
|
+
for k in order:
|
|
537
|
+
if picks and min(_angle_mod(R[k], p, group) for p in picks) < np.radians(8.0):
|
|
538
|
+
continue
|
|
539
|
+
picks.append(_smallest(R[k], group))
|
|
540
|
+
if len(picks) > alternatives:
|
|
541
|
+
break
|
|
542
|
+
ranked.append(picks)
|
|
543
|
+
trial = [p[0] for p in ranked]
|
|
544
|
+
sc, row, rep = check(at, trial)
|
|
545
|
+
if sc < best[0]:
|
|
546
|
+
best = (sc, row, list(trial))
|
|
547
|
+
for i in range(zones):
|
|
548
|
+
for alt in ranked[i][1:]:
|
|
549
|
+
cand = list(best[2])
|
|
550
|
+
cand[i] = alt
|
|
551
|
+
sc2, row2, _ = check(at, cand)
|
|
552
|
+
if sc2 < best[0]:
|
|
553
|
+
best = (sc2, row2, cand)
|
|
554
|
+
rots = best[2]
|
|
555
|
+
# the next round's stresses: the design as it now stands
|
|
556
|
+
fl = homogenized_check(doc_for(at, rots), study, node=name, base_dir=base_dir, cells=cells,
|
|
557
|
+
fields=True)["fields"] if rnd + 1 < rounds else fl
|
|
558
|
+
# then each zone's rotation vector on its own, the others held
|
|
559
|
+
if zone_refine > 0:
|
|
560
|
+
from scipy.optimize import minimize
|
|
561
|
+
|
|
562
|
+
for i in range(zones):
|
|
563
|
+
x0 = _rotvec_deg(best[2][i])
|
|
564
|
+
|
|
565
|
+
def f(x, i=i):
|
|
566
|
+
nonlocal best
|
|
567
|
+
cand = list(best[2])
|
|
568
|
+
cand[i] = _rotation_from_deg(x)
|
|
569
|
+
sc2, row2, _ = check(at, cand)
|
|
570
|
+
if sc2 < best[0]:
|
|
571
|
+
best = (sc2, row2, cand)
|
|
572
|
+
return sc2
|
|
573
|
+
|
|
574
|
+
minimize(f, x0, method="Nelder-Mead", options={
|
|
575
|
+
"initial_simplex": [x0] + [x0 + 10.0 * e for e in np.eye(3)], "maxfev": zone_refine})
|
|
576
|
+
out_doc = doc_for(at, best[2])
|
|
577
|
+
zones_out = out_doc["nodes"][lat]["orient"]["zones"]
|
|
578
|
+
turned = max(_turn_deg(z["rotate"]) for z in zones_out) > TURN_MIN_DEG
|
|
579
|
+
guard = _guard(confirm, float(ref.get("latticeExposed") or 0.0), turned, base, out_doc, study, objective,
|
|
580
|
+
name, base_dir, resolved_pitch)
|
|
581
|
+
candidate = None
|
|
582
|
+
if guard is not None and not guard["confirmed"]:
|
|
583
|
+
candidate, out_doc, zones_out = best[1], base, None
|
|
584
|
+
best = (None, {**unrot, "zones": None}, None)
|
|
585
|
+
say({"stage": "lattice-zones-guard", "reason": guard["reason"]})
|
|
586
|
+
return {"doc": out_doc, "lattice": lat, "objective": objective, "zones": zones_out,
|
|
587
|
+
"guard": guard, "candidate": candidate,
|
|
588
|
+
"blend": blend, "unrotated": unrot, "uniform": uniform, "uniformOrient": list(map(float, start)),
|
|
589
|
+
"best": best[1], "evaluations": len(history), "history": history, "seconds": time.time() - t0}
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _rotation_from_deg(rv):
|
|
593
|
+
from scipy.spatial.transform import Rotation
|
|
594
|
+
|
|
595
|
+
return Rotation.from_rotvec(np.radians(np.asarray(rv, float))).as_matrix()
|