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/directions.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loads whose direction is not known: a force of known size that may point
|
|
3
|
+
anywhere ("direction": "any"), or anywhere within a cone round its nominal
|
|
4
|
+
direction ("cone": degrees).
|
|
5
|
+
|
|
6
|
+
Compliance is quadratic in the force, so for f(d) = f0 + sum_i d_i b_i (f0
|
|
7
|
+
the load case's fixed loads, b_i the uncertain force pointing along axis i,
|
|
8
|
+
d a unit vector) it is
|
|
9
|
+
|
|
10
|
+
c(d) = c00 + 2 g.d + d' A d, c00 = f0.u0, g_i = b_i.u0, A_ij = b_i.u_j
|
|
11
|
+
|
|
12
|
+
with u0, u_i the displacements under f0 and b_i. Four solves (three when
|
|
13
|
+
nothing else loads the case) give the compliance in every direction, and
|
|
14
|
+
the worst one is a 3x3 problem: with no fixed load it is the largest
|
|
15
|
+
eigenvalue of A, exactly. By the envelope theorem the worst case's
|
|
16
|
+
sensitivity is the ordinary compliance sensitivity of the displacement
|
|
17
|
+
under the worst force, u0 + sum_i d_i u_i, so the optimizer's update is
|
|
18
|
+
unchanged (robust compliance, Ben-Tal and Nemirovski's worst case over an
|
|
19
|
+
ellipsoid of loads, here the sphere or a cap of it).
|
|
20
|
+
|
|
21
|
+
A von Mises stress at a point is the same shape (a quadratic in d through
|
|
22
|
+
the six stress components), which is how verification finds the worst
|
|
23
|
+
direction for stress.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
__all__ = ["Spread", "Worst", "worst_direction", "expand", "collapse", "fibonacci_sphere", "worst_stress_direction"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Spread:
|
|
36
|
+
"""One load case's uncertain force, on a mesh's dofs."""
|
|
37
|
+
base: np.ndarray # the case's fixed loads (the rest of the case)
|
|
38
|
+
basis: list # 3 force vectors: the uncertain force along x, y, z
|
|
39
|
+
nominal: np.ndarray # unit vector: the force as written
|
|
40
|
+
cos_half: float = -1.0 # cone: cos of the half-angle round `nominal`; -1 is any direction
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def kind(self) -> str:
|
|
44
|
+
return "any" if self.cos_half <= -1.0 else "cone"
|
|
45
|
+
|
|
46
|
+
def load(self, d) -> np.ndarray:
|
|
47
|
+
d = np.asarray(d, float)
|
|
48
|
+
return self.base + d[0] * self.basis[0] + d[1] * self.basis[1] + d[2] * self.basis[2]
|
|
49
|
+
|
|
50
|
+
def with_base(self, extra) -> "Spread":
|
|
51
|
+
return Spread(self.base + extra, self.basis, self.nominal, self.cos_half)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fibonacci_sphere(n: int) -> np.ndarray:
|
|
55
|
+
i = np.arange(n) + 0.5
|
|
56
|
+
z = 1.0 - 2.0 * i / n
|
|
57
|
+
r = np.sqrt(np.maximum(0.0, 1.0 - z * z))
|
|
58
|
+
t = np.pi * (1.0 + 5 ** 0.5) * i
|
|
59
|
+
return np.column_stack([r * np.cos(t), r * np.sin(t), z])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_SAMPLES = fibonacci_sphere(600)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _to_cap(d, axis, cos_half):
|
|
66
|
+
"""Nearest point of the cap {|d| = 1, d.axis >= cos_half} to unit d."""
|
|
67
|
+
if axis is None or cos_half <= -1.0:
|
|
68
|
+
return d
|
|
69
|
+
c = d @ axis
|
|
70
|
+
if c >= cos_half:
|
|
71
|
+
return d
|
|
72
|
+
perp = d - c * axis
|
|
73
|
+
n = np.linalg.norm(perp)
|
|
74
|
+
if n < 1e-12:
|
|
75
|
+
perp = np.cross(axis, [1.0, 0, 0] if abs(axis[0]) < 0.9 else [0, 1.0, 0])
|
|
76
|
+
n = np.linalg.norm(perp)
|
|
77
|
+
return cos_half * axis + np.sqrt(max(0.0, 1.0 - cos_half ** 2)) * perp / n
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def worst_direction(c00: float, g, a, axis=None, cos_half: float = -1.0):
|
|
81
|
+
"""Maximize c00 + 2 g.d + d'Ad over unit d (within the cap round `axis`
|
|
82
|
+
when cos_half > -1). Returns (d, value)."""
|
|
83
|
+
g = np.asarray(g, float)
|
|
84
|
+
a = 0.5 * (np.asarray(a, float) + np.asarray(a, float).T)
|
|
85
|
+
|
|
86
|
+
def q(d):
|
|
87
|
+
return float(c00 + 2.0 * g @ d + d @ a @ d)
|
|
88
|
+
|
|
89
|
+
capped = axis is not None and cos_half > -1.0
|
|
90
|
+
lam, vec = np.linalg.eigh(a)
|
|
91
|
+
gn = float(np.linalg.norm(g))
|
|
92
|
+
if not capped and gn <= 1e-12 * max(abs(lam[-1]), 1e-300):
|
|
93
|
+
d = vec[:, -1]
|
|
94
|
+
return d, q(d)
|
|
95
|
+
# Candidates: the eigenvectors, a dense spread of directions, and the
|
|
96
|
+
# nominal axis; then a monotone ascent from the best of them. The
|
|
97
|
+
# quadratic on a sphere has at most a couple of local maxima, which 600
|
|
98
|
+
# samples (7 degrees apart) separate.
|
|
99
|
+
cands = np.vstack([vec.T, -vec.T, _SAMPLES] + ([axis[None]] if capped else []))
|
|
100
|
+
if capped:
|
|
101
|
+
cands = np.array([_to_cap(c, axis, cos_half) for c in cands])
|
|
102
|
+
vals = c00 + 2.0 * cands @ g + np.einsum("ij,jk,ik->i", cands, a, cands)
|
|
103
|
+
d = cands[int(np.argmax(vals))]
|
|
104
|
+
# minorize-maximize: d <- normalize((A + sI) d + g) never lowers q on the
|
|
105
|
+
# sphere for s >= -lambda_min (a shifted power step)
|
|
106
|
+
s = max(0.0, -lam[0]) + 1e-12 * max(abs(lam[-1]), 1.0)
|
|
107
|
+
best, bq = d, q(d)
|
|
108
|
+
for _ in range(500):
|
|
109
|
+
n = (a + s * np.eye(3)) @ d + g
|
|
110
|
+
nn = np.linalg.norm(n)
|
|
111
|
+
if nn < 1e-300:
|
|
112
|
+
break
|
|
113
|
+
dn = _to_cap(n / nn, axis, cos_half) if capped else n / nn
|
|
114
|
+
if np.linalg.norm(dn - d) < 1e-12:
|
|
115
|
+
d = dn
|
|
116
|
+
break
|
|
117
|
+
d = dn
|
|
118
|
+
qd = q(d)
|
|
119
|
+
if qd > bq:
|
|
120
|
+
best, bq = d, qd
|
|
121
|
+
return best, bq
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def expand(loads, spreads, extra=None):
|
|
125
|
+
"""The right-hand sides to solve for every case, and per case the
|
|
126
|
+
indices of its fixed part and basis among them (None for a zero fixed
|
|
127
|
+
part, which needs no solve). `extra` (self-weight) adds to every case's
|
|
128
|
+
fixed part."""
|
|
129
|
+
rhs, layout = [], []
|
|
130
|
+
for ci, f in enumerate(loads):
|
|
131
|
+
s = spreads[ci] if spreads else None
|
|
132
|
+
if s is None:
|
|
133
|
+
layout.append((len(rhs),))
|
|
134
|
+
rhs.append(f + extra if extra is not None else f)
|
|
135
|
+
continue
|
|
136
|
+
base = s.base + extra if extra is not None else s.base
|
|
137
|
+
if np.any(base):
|
|
138
|
+
i0 = len(rhs)
|
|
139
|
+
rhs.append(base)
|
|
140
|
+
else:
|
|
141
|
+
i0 = None
|
|
142
|
+
layout.append((i0, len(rhs), len(rhs) + 1, len(rhs) + 2))
|
|
143
|
+
rhs.extend(s.basis)
|
|
144
|
+
return rhs, layout
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class Worst:
|
|
149
|
+
"""One load case judged in its worst direction."""
|
|
150
|
+
u: np.ndarray # displacement under the worst force
|
|
151
|
+
compliance: float # f.u there, the exact worst case
|
|
152
|
+
direction: np.ndarray | None
|
|
153
|
+
force: np.ndarray
|
|
154
|
+
# the optimizer's sensitivity: sum of w * strain energy of v over
|
|
155
|
+
# (w, v), and the mean displacement under the same weights (self-weight)
|
|
156
|
+
energy: list
|
|
157
|
+
u_mean: np.ndarray
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Smoothing of the worst case for the optimizer. The exact worst direction
|
|
161
|
+
# jumps when two directions tie (a square section is as weak sideways as
|
|
162
|
+
# down), and following it alone the design swings between them. The
|
|
163
|
+
# optimizer instead
|
|
164
|
+
# follows a soft maximum over directions, each weighted by
|
|
165
|
+
# exp(SMOOTH * (c - c_max) / c_max): directions within a few percent of the
|
|
166
|
+
# worst share the gradient, as for a repeated eigenvalue. (With the
|
|
167
|
+
# optimizer's sensitivity damping, simp.optimize, 10 and 40 land within 3%.)
|
|
168
|
+
SMOOTH = 40.0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def collapse(rhs, layout, us, spreads, smooth: float = SMOOTH):
|
|
172
|
+
"""Per case, a Worst: the exact worst direction and its compliance, and
|
|
173
|
+
the smoothed sensitivity terms."""
|
|
174
|
+
out = []
|
|
175
|
+
for ci, idx in enumerate(layout):
|
|
176
|
+
if len(idx) == 1:
|
|
177
|
+
u, f = us[idx[0]], rhs[idx[0]]
|
|
178
|
+
out.append(Worst(u, float(f @ u), None, f, [(1.0, u)], u))
|
|
179
|
+
continue
|
|
180
|
+
s = spreads[ci]
|
|
181
|
+
i0, ib = idx[0], idx[1:]
|
|
182
|
+
ub = [us[i] for i in ib]
|
|
183
|
+
bb = [rhs[i] for i in ib]
|
|
184
|
+
a = np.array([[bb[i] @ ub[j] for j in range(3)] for i in range(3)])
|
|
185
|
+
a = 0.5 * (a + a.T)
|
|
186
|
+
if i0 is None:
|
|
187
|
+
c00, g, u0, f0 = 0.0, np.zeros(3), np.zeros_like(ub[0]), 0.0
|
|
188
|
+
else:
|
|
189
|
+
u0, f0 = us[i0], rhs[i0]
|
|
190
|
+
c00 = float(f0 @ u0)
|
|
191
|
+
g = np.array([f0 @ ub[j] for j in range(3)])
|
|
192
|
+
d, c = worst_direction(c00, g, a, s.nominal, s.cos_half)
|
|
193
|
+
u = u0 + d[0] * ub[0] + d[1] * ub[1] + d[2] * ub[2]
|
|
194
|
+
f = f0 + d[0] * bb[0] + d[1] * bb[1] + d[2] * bb[2]
|
|
195
|
+
# soft maximum over a spread of directions (and the worst itself):
|
|
196
|
+
# sum_k w_k (1, d_k)(1, d_k)' = W, then strain energy of U q for W's
|
|
197
|
+
# eigenpairs (mu, q), U = [u0, u1, u2, u3]
|
|
198
|
+
dirs = np.vstack([_cap_samples(s), d[None]])
|
|
199
|
+
vals = c00 + 2.0 * dirs @ g + np.einsum("ij,jk,ik->i", dirs, a, dirs)
|
|
200
|
+
w = np.exp(smooth * (vals - c) / max(abs(c), 1e-300))
|
|
201
|
+
w /= w.sum()
|
|
202
|
+
dd = np.column_stack([np.ones(len(dirs)), dirs])
|
|
203
|
+
wm = (dd * w[:, None]).T @ dd
|
|
204
|
+
mu, q = np.linalg.eigh(wm)
|
|
205
|
+
uu = [u0] + ub
|
|
206
|
+
energy = [(float(m), sum(q[k, j] * uu[k] for k in range(4)))
|
|
207
|
+
for j, m in enumerate(mu) if m > 1e-9 * mu[-1]]
|
|
208
|
+
mean = dd.T @ w
|
|
209
|
+
u_mean = sum(mean[k] * uu[k] for k in range(4))
|
|
210
|
+
out.append(Worst(u, c, d, f, energy, u_mean))
|
|
211
|
+
return out
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# von Mises squared as a quadratic form on (sx, sy, sz, txy, tyz, tzx)
|
|
215
|
+
_VM = np.array([[1, -.5, -.5, 0, 0, 0], [-.5, 1, -.5, 0, 0, 0], [-.5, -.5, 1, 0, 0, 0],
|
|
216
|
+
[0, 0, 0, 3, 0, 0], [0, 0, 0, 0, 3, 0], [0, 0, 0, 0, 0, 3]], float)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _cap_samples(spread: Spread, n: int = 600) -> np.ndarray:
|
|
220
|
+
if spread.cos_half <= -1.0:
|
|
221
|
+
return fibonacci_sphere(n)
|
|
222
|
+
# a Fibonacci spread over the cap only: z uniform on [cos_half, 1]
|
|
223
|
+
i = np.arange(n) + 0.5
|
|
224
|
+
z = 1.0 - (1.0 - spread.cos_half) * i / n
|
|
225
|
+
r = np.sqrt(np.maximum(0.0, 1.0 - z * z))
|
|
226
|
+
t = np.pi * (1.0 + 5 ** 0.5) * i
|
|
227
|
+
local = np.column_stack([r * np.cos(t), r * np.sin(t), z])
|
|
228
|
+
a = spread.nominal
|
|
229
|
+
e1 = np.cross(a, [1.0, 0, 0] if abs(a[0]) < 0.9 else [0, 1.0, 0])
|
|
230
|
+
e1 /= np.linalg.norm(e1)
|
|
231
|
+
e2 = np.cross(a, e1)
|
|
232
|
+
return np.vstack([local @ np.array([e1, e2, a]), a[None]])
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def worst_stress_direction(spread: Spread, s0, sb, mask, top: int = 40):
|
|
236
|
+
"""The direction of the uncertain force that raises the peak von Mises
|
|
237
|
+
over the nodes in `mask` most. s0: nodal stress (n, 6) under the case's
|
|
238
|
+
fixed loads (or None); sb: the three (n, 6) stresses under the basis.
|
|
239
|
+
Samples directions for the peak, then solves the exact 3x3 problem at
|
|
240
|
+
the nodes that came out on top. Returns (d, peak von Mises)."""
|
|
241
|
+
idx = np.nonzero(mask)[0]
|
|
242
|
+
if idx.size == 0:
|
|
243
|
+
idx = np.arange(sb[0].shape[0])
|
|
244
|
+
s = np.stack([b[idx] for b in sb], axis=1) # (m, 3, 6)
|
|
245
|
+
z = s0[idx] if s0 is not None else np.zeros((idx.size, 6))
|
|
246
|
+
dirs = _cap_samples(spread)
|
|
247
|
+
best = np.zeros(idx.size)
|
|
248
|
+
for k in range(0, len(dirs), 32):
|
|
249
|
+
sig = z[:, None, :] + np.einsum("kd,mdj->mkj", dirs[k:k + 32], s)
|
|
250
|
+
vm2 = np.einsum("mkj,jl,mkl->mk", sig, _VM, sig)
|
|
251
|
+
best = np.maximum(best, vm2.max(axis=1))
|
|
252
|
+
order = np.argsort(best)[::-1][:top]
|
|
253
|
+
bd, bv = spread.nominal, -1.0
|
|
254
|
+
for m in order:
|
|
255
|
+
sq = s[m] @ _VM # (3, 6)
|
|
256
|
+
a = sq @ s[m].T
|
|
257
|
+
g = sq @ z[m]
|
|
258
|
+
c00 = float(z[m] @ _VM @ z[m])
|
|
259
|
+
d, v = worst_direction(c00, g, a, spread.nominal, spread.cos_half)
|
|
260
|
+
if v > bv:
|
|
261
|
+
bd, bv = d, v
|
|
262
|
+
return bd, float(np.sqrt(max(bv, 0.0)))
|