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/mma.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Method of Moving Asymptotes (Svanberg 1987; the 2007 formulation in
|
|
3
|
+
"MMA and GCMMA — two methods for nonlinear optimization"), written for
|
|
4
|
+
loadpath from the published method so it stays MIT.
|
|
5
|
+
|
|
6
|
+
MMA is what lets an optimization carry real constraints (a stress limit, a
|
|
7
|
+
deflection limit, several at once) instead of the single volume constraint
|
|
8
|
+
optimality criteria can handle. Each iteration builds a convex separable
|
|
9
|
+
approximation of the problem around the current design, using moving
|
|
10
|
+
asymptotes that tighten when the design oscillates and relax when it moves
|
|
11
|
+
steadily, and solves it exactly with a primal-dual interior-point method.
|
|
12
|
+
The problem solved is
|
|
13
|
+
|
|
14
|
+
minimize f0(x) + a0 z + sum_i (c_i y_i + 0.5 d_i y_i^2)
|
|
15
|
+
subject to f_i(x) - a_i z - y_i <= 0, xmin <= x <= xmax,
|
|
16
|
+
y_i >= 0, z >= 0
|
|
17
|
+
|
|
18
|
+
with the usual choice a0 = 1, a_i = 0, d_i = 1 and large c_i, which makes
|
|
19
|
+
the artificial y_i zero whenever the real constraints can be met.
|
|
20
|
+
|
|
21
|
+
Sized for topology optimization: n (design cells) is large and m
|
|
22
|
+
(constraints) is small, so the Newton steps solve an m x m system.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
__all__ = ["MMA"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MMA:
|
|
32
|
+
def __init__(self, n, m, xmin=0.0, xmax=1.0, move=0.2, c=1000.0, asyinit=0.5,
|
|
33
|
+
asyincr=1.2, asydecr=0.7):
|
|
34
|
+
self.n, self.m = n, m
|
|
35
|
+
self.xmin = np.broadcast_to(np.asarray(xmin, float), (n,)).copy()
|
|
36
|
+
self.xmax = np.broadcast_to(np.asarray(xmax, float), (n,)).copy()
|
|
37
|
+
self.move = move
|
|
38
|
+
self.a0 = 1.0
|
|
39
|
+
self.a = np.zeros(m)
|
|
40
|
+
self.c = np.full(m, float(c))
|
|
41
|
+
self.d = np.ones(m)
|
|
42
|
+
self.asyinit, self.asyincr, self.asydecr = asyinit, asyincr, asydecr
|
|
43
|
+
self.xold1 = None
|
|
44
|
+
self.xold2 = None
|
|
45
|
+
self.low = None
|
|
46
|
+
self.upp = None
|
|
47
|
+
self.iter = 0
|
|
48
|
+
|
|
49
|
+
def tighten(self, factor):
|
|
50
|
+
"""Pull the asymptotes toward the last design by `factor`: a more
|
|
51
|
+
curved, more conservative model for the next step (what GCMMA's
|
|
52
|
+
inner loop does, without its extra function evaluations)."""
|
|
53
|
+
if self.xold1 is not None and self.low is not None:
|
|
54
|
+
self.low = self.xold1 - factor * (self.xold1 - self.low)
|
|
55
|
+
self.upp = self.xold1 + factor * (self.upp - self.xold1)
|
|
56
|
+
|
|
57
|
+
def update(self, x, f0, df0, f, df):
|
|
58
|
+
"""One MMA step from design x with objective value/gradient (f0, df0)
|
|
59
|
+
and constraint values/gradients (f: (m,), df: (m, n)). Returns the new
|
|
60
|
+
design."""
|
|
61
|
+
xmin, xmax = self.xmin, self.xmax
|
|
62
|
+
# Non-finite values (a solve that broke down) would turn the whole
|
|
63
|
+
# subproblem NaN; read them as a large violation and stay in bounds.
|
|
64
|
+
x = np.clip(np.nan_to_num(np.asarray(x, float), nan=0.5), xmin, xmax)
|
|
65
|
+
f = np.nan_to_num(np.asarray(f, float).reshape(self.m), nan=1e6, posinf=1e6, neginf=-1e6)
|
|
66
|
+
df = np.nan_to_num(np.asarray(df, float).reshape(self.m, self.n), nan=0.0, posinf=1e6, neginf=-1e6)
|
|
67
|
+
df0 = np.nan_to_num(np.asarray(df0, float), nan=0.0, posinf=1e6, neginf=-1e6)
|
|
68
|
+
self.iter += 1
|
|
69
|
+
span = xmax - xmin
|
|
70
|
+
if self.iter <= 2 or self.low is None:
|
|
71
|
+
self.low = x - self.asyinit * span
|
|
72
|
+
self.upp = x + self.asyinit * span
|
|
73
|
+
else:
|
|
74
|
+
zzz = (x - self.xold1) * (self.xold1 - self.xold2)
|
|
75
|
+
factor = np.ones(self.n)
|
|
76
|
+
factor[zzz > 0] = self.asyincr
|
|
77
|
+
factor[zzz < 0] = self.asydecr
|
|
78
|
+
self.low = x - factor * (self.xold1 - self.low)
|
|
79
|
+
self.upp = x + factor * (self.upp - self.xold1)
|
|
80
|
+
self.low = np.clip(self.low, x - 10 * span, x - 0.01 * span)
|
|
81
|
+
self.upp = np.clip(self.upp, x + 0.01 * span, x + 10 * span)
|
|
82
|
+
low, upp = self.low, self.upp
|
|
83
|
+
albefa = 0.1
|
|
84
|
+
alfa = np.maximum.reduce([low + albefa * (x - low), x - self.move * span, xmin])
|
|
85
|
+
beta = np.minimum.reduce([upp - albefa * (upp - x), x + self.move * span, xmax])
|
|
86
|
+
|
|
87
|
+
raa0 = 1e-5
|
|
88
|
+
xmami = np.maximum(span, 1e-5)
|
|
89
|
+
ux1 = upp - x
|
|
90
|
+
xl1 = x - low
|
|
91
|
+
ux2, xl2 = ux1 * ux1, xl1 * xl1
|
|
92
|
+
p0 = np.maximum(df0, 0.0)
|
|
93
|
+
q0 = np.maximum(-df0, 0.0)
|
|
94
|
+
pq0 = 0.001 * (p0 + q0) + raa0 / xmami
|
|
95
|
+
p0 = (p0 + pq0) * ux2
|
|
96
|
+
q0 = (q0 + pq0) * xl2
|
|
97
|
+
P = np.maximum(df, 0.0)
|
|
98
|
+
Q = np.maximum(-df, 0.0)
|
|
99
|
+
PQ = 0.001 * (P + Q) + raa0 / xmami
|
|
100
|
+
P = (P + PQ) * ux2
|
|
101
|
+
Q = (Q + PQ) * xl2
|
|
102
|
+
b = P @ (1.0 / ux1) + Q @ (1.0 / xl1) - f
|
|
103
|
+
|
|
104
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
105
|
+
xnew = self._subsolv(low, upp, alfa, beta, p0, q0, P, Q, b)
|
|
106
|
+
if not np.all(np.isfinite(xnew)):
|
|
107
|
+
# the interior point broke down: keep those variables in place
|
|
108
|
+
xnew = np.where(np.isfinite(xnew), xnew, x)
|
|
109
|
+
xnew = np.clip(xnew, alfa, beta)
|
|
110
|
+
self.xold2 = self.xold1 if self.xold1 is not None else x.copy()
|
|
111
|
+
self.xold1 = x.copy()
|
|
112
|
+
return xnew
|
|
113
|
+
|
|
114
|
+
def _subsolv(self, low, upp, alfa, beta, p0, q0, P, Q, b, epsimin=1e-7):
|
|
115
|
+
m = self.m
|
|
116
|
+
a0, a, c, d = self.a0, self.a, self.c, self.d
|
|
117
|
+
x = 0.5 * (alfa + beta)
|
|
118
|
+
y = np.ones(m)
|
|
119
|
+
z = 1.0
|
|
120
|
+
lam = np.ones(m)
|
|
121
|
+
xsi = np.maximum(1.0 / (x - alfa), 1.0)
|
|
122
|
+
eta = np.maximum(1.0 / (beta - x), 1.0)
|
|
123
|
+
mu = np.maximum(1.0, 0.5 * c)
|
|
124
|
+
zet = 1.0
|
|
125
|
+
s = np.ones(m)
|
|
126
|
+
epsi = 1.0
|
|
127
|
+
|
|
128
|
+
def residual(x, y, z, lam, xsi, eta, mu, zet, s, epsi):
|
|
129
|
+
ux1 = upp - x
|
|
130
|
+
xl1 = x - low
|
|
131
|
+
plam = p0 + P.T @ lam
|
|
132
|
+
qlam = q0 + Q.T @ lam
|
|
133
|
+
gvec = P @ (1.0 / ux1) + Q @ (1.0 / xl1)
|
|
134
|
+
dpsidx = plam / (ux1 * ux1) - qlam / (xl1 * xl1)
|
|
135
|
+
r = np.concatenate([
|
|
136
|
+
dpsidx - xsi + eta,
|
|
137
|
+
c + d * y - mu - lam,
|
|
138
|
+
[a0 - zet - a @ lam],
|
|
139
|
+
gvec - a * z - y + s - b,
|
|
140
|
+
xsi * (x - alfa) - epsi,
|
|
141
|
+
eta * (beta - x) - epsi,
|
|
142
|
+
mu * y - epsi,
|
|
143
|
+
[zet * z - epsi],
|
|
144
|
+
lam * s - epsi,
|
|
145
|
+
])
|
|
146
|
+
return r
|
|
147
|
+
|
|
148
|
+
while epsi > epsimin:
|
|
149
|
+
r = residual(x, y, z, lam, xsi, eta, mu, zet, s, epsi)
|
|
150
|
+
norm = np.linalg.norm(r)
|
|
151
|
+
rmax = np.abs(r).max()
|
|
152
|
+
it = 0
|
|
153
|
+
while rmax > 0.9 * epsi and it < 200:
|
|
154
|
+
it += 1
|
|
155
|
+
ux1 = upp - x
|
|
156
|
+
xl1 = x - low
|
|
157
|
+
ux2, xl2 = ux1 * ux1, xl1 * xl1
|
|
158
|
+
ux3, xl3 = ux1 * ux2, xl1 * xl2
|
|
159
|
+
plam = p0 + P.T @ lam
|
|
160
|
+
qlam = q0 + Q.T @ lam
|
|
161
|
+
gvec = P @ (1.0 / ux1) + Q @ (1.0 / xl1)
|
|
162
|
+
GG = P / ux2 - Q / xl2
|
|
163
|
+
dpsidx = plam / ux2 - qlam / xl2
|
|
164
|
+
delx = dpsidx - epsi / (x - alfa) + epsi / (beta - x)
|
|
165
|
+
dely = c + d * y - lam - epsi / y
|
|
166
|
+
delz = a0 - a @ lam - epsi / z
|
|
167
|
+
dellam = gvec - a * z - y - b + epsi / lam
|
|
168
|
+
diagx = 2.0 * (plam / ux3 + qlam / xl3) + xsi / (x - alfa) + eta / (beta - x)
|
|
169
|
+
diagy = d + mu / y
|
|
170
|
+
diaglamyi = s / lam + 1.0 / diagy
|
|
171
|
+
blam = dellam + dely / diagy - GG @ (delx / diagx)
|
|
172
|
+
alam = np.diag(diaglamyi) + (GG / diagx) @ GG.T
|
|
173
|
+
aa = np.zeros((m + 1, m + 1))
|
|
174
|
+
aa[:m, :m] = alam
|
|
175
|
+
aa[:m, m] = a
|
|
176
|
+
aa[m, :m] = a
|
|
177
|
+
aa[m, m] = -zet / z
|
|
178
|
+
sol = np.linalg.solve(aa, np.concatenate([blam, [delz]]))
|
|
179
|
+
dlam, dz = sol[:m], sol[m]
|
|
180
|
+
dx = -delx / diagx - (GG.T @ dlam) / diagx
|
|
181
|
+
dy = -dely / diagy + dlam / diagy
|
|
182
|
+
dxsi = -xsi + epsi / (x - alfa) - xsi * dx / (x - alfa)
|
|
183
|
+
deta = -eta + epsi / (beta - x) + eta * dx / (beta - x)
|
|
184
|
+
dmu = -mu + epsi / y - mu * dy / y
|
|
185
|
+
dzet = -zet + epsi / z - zet * dz / z
|
|
186
|
+
ds = -s + epsi / lam - s * dlam / lam
|
|
187
|
+
|
|
188
|
+
xx = np.concatenate([y, [z], lam, xsi, eta, mu, [zet], s])
|
|
189
|
+
dxx = np.concatenate([dy, [dz], dlam, dxsi, deta, dmu, [dzet], ds])
|
|
190
|
+
stmxx = np.max(-1.01 * dxx / xx)
|
|
191
|
+
stmalfa = np.max(-1.01 * dx / (x - alfa))
|
|
192
|
+
stmbeta = np.max(1.01 * dx / (beta - x))
|
|
193
|
+
steg = 1.0 / max(stmalfa, stmbeta, stmxx, 1.0)
|
|
194
|
+
old = (x, y, z, lam, xsi, eta, mu, zet, s)
|
|
195
|
+
for _ in range(50):
|
|
196
|
+
x = old[0] + steg * dx
|
|
197
|
+
y = old[1] + steg * dy
|
|
198
|
+
z = old[2] + steg * dz
|
|
199
|
+
lam = old[3] + steg * dlam
|
|
200
|
+
xsi = old[4] + steg * dxsi
|
|
201
|
+
eta = old[5] + steg * deta
|
|
202
|
+
mu = old[6] + steg * dmu
|
|
203
|
+
zet = old[7] + steg * dzet
|
|
204
|
+
s = old[8] + steg * ds
|
|
205
|
+
r = residual(x, y, z, lam, xsi, eta, mu, zet, s, epsi)
|
|
206
|
+
newnorm = np.linalg.norm(r)
|
|
207
|
+
if newnorm < 2.0 * norm:
|
|
208
|
+
break
|
|
209
|
+
steg /= 2.0
|
|
210
|
+
norm = newnorm
|
|
211
|
+
rmax = np.abs(r).max()
|
|
212
|
+
epsi *= 0.1
|
|
213
|
+
self.lam = lam
|
|
214
|
+
return x
|
loadpath/modal.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Natural frequencies, mode shapes and damping, on the same voxel hex mesh.
|
|
3
|
+
|
|
4
|
+
A part that shakes (a motor mount, an arm link carrying a gripper, a sensor
|
|
5
|
+
bracket on a printer gantry) fails by resonance long before its static
|
|
6
|
+
safety factor says it should: if a mode sits near the excitation the
|
|
7
|
+
response is the static one times 1/(2 zeta). This module finds the modes:
|
|
8
|
+
|
|
9
|
+
- `modal_analysis`: the generalized eigenproblem K phi = omega^2 M phi with
|
|
10
|
+
the consistent-free lumped mass (each cell's mass split over its 8 nodes),
|
|
11
|
+
per-cell modulus and density, so a homogenized graded lattice (modulus
|
|
12
|
+
E C rho^n, density rho_s rho) is analysed exactly like a solid. Point
|
|
13
|
+
masses (a motor, a gripper) attach to nodes. Returns frequencies (Hz),
|
|
14
|
+
modes, and each mode's effective mass fraction per axis (how much of
|
|
15
|
+
the part a base shake in that direction moves).
|
|
16
|
+
- `transmissibility`: base-to-part amplification of a single mode at a
|
|
17
|
+
forcing frequency, with modal damping zeta = eta / 2 from the material's
|
|
18
|
+
loss factor (LOSS_FACTOR).
|
|
19
|
+
|
|
20
|
+
On damping: a part made of one linear viscoelastic material has the modal
|
|
21
|
+
loss factor of that material whatever its shape (every mode stores its
|
|
22
|
+
energy in the same material), so a lattice of PA12 damps like solid PA12.
|
|
23
|
+
Lattices raise damping only through something else: a second, lossy
|
|
24
|
+
material (a TPU lattice, a TPU-filled core), friction, or unfused powder
|
|
25
|
+
left in the cells (particle damping, large but unmodelled here). What a
|
|
26
|
+
lattice does change is where the modes are: E/rho drops as rho^(n-1), so a
|
|
27
|
+
lattice core lowers frequencies, which is what an isolator wants and a
|
|
28
|
+
stiff bracket does not.
|
|
29
|
+
|
|
30
|
+
Units: mm, N, MPa, g/cm^3 for density (converted to tonne/mm^3, which with
|
|
31
|
+
N and mm gives seconds), kg for point masses, Hz out.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
import scipy.sparse as sp
|
|
39
|
+
|
|
40
|
+
from .fem import HexMesh, Stiffness
|
|
41
|
+
|
|
42
|
+
__all__ = ["ModalResult", "modal_analysis", "transmissibility", "base_transmissibility", "LOSS_FACTOR", "loss_factor", "vibration_report"]
|
|
43
|
+
|
|
44
|
+
# Room-temperature loss factor (tan delta) near 10-100 Hz, typical values
|
|
45
|
+
# from DMA data sheets and literature; rough (+-50%), and strongly
|
|
46
|
+
# temperature dependent near Tg.
|
|
47
|
+
LOSS_FACTOR = {
|
|
48
|
+
"pla": 0.015, "petg": 0.02, "abs": 0.02, "asa": 0.02, "pc": 0.012, "pa12": 0.04, "pa11": 0.05,
|
|
49
|
+
"pa12-gb": 0.03, "pa12-cf": 0.03, "tpu": 0.12, "resin-rigid": 0.03, "resin-tough": 0.05,
|
|
50
|
+
"alsi10mg": 0.002, "ti64": 0.002, "316l": 0.002, "aluminum": 0.001, "steel": 0.001,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def loss_factor(material) -> float:
|
|
55
|
+
name = getattr(material, "name", str(material)).lower()
|
|
56
|
+
return LOSS_FACTOR.get(name, 0.02)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ModalResult:
|
|
61
|
+
frequencies: np.ndarray # Hz, ascending
|
|
62
|
+
modes: np.ndarray # (n_dof, k), mass-normalized
|
|
63
|
+
mass: float # kg, the part plus point masses
|
|
64
|
+
participation: np.ndarray # (k, 3) effective mass fraction per axis
|
|
65
|
+
mass_vector: np.ndarray | None = None # (n_dof,) lumped mass per dof, tonnes
|
|
66
|
+
|
|
67
|
+
def to_dict(self, eta: float | None = None) -> dict:
|
|
68
|
+
out = {"frequencies": [round(float(f), 2) for f in self.frequencies], "mass": round(self.mass, 5),
|
|
69
|
+
"massFraction": [[round(float(x), 3) for x in row] for row in self.participation]}
|
|
70
|
+
if eta is not None:
|
|
71
|
+
out["lossFactor"] = eta
|
|
72
|
+
out["dampingRatio"] = round(eta / 2.0, 4)
|
|
73
|
+
out["resonantAmplification"] = round(1.0 / eta, 1)
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _lumped_mass(mesh: HexMesh, density):
|
|
78
|
+
rho = np.broadcast_to(np.asarray(density, float), (mesh.n_elem,)) * 1e-9 # g/cm^3 -> t/mm^3
|
|
79
|
+
m_node = np.zeros(mesh.n_nodes)
|
|
80
|
+
np.add.at(m_node, mesh.enode, (rho * mesh.pitch ** 3 / 8.0)[:, None])
|
|
81
|
+
return m_node
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def modal_analysis(mesh: HexMesh, modulus, density, nu: float, fixed: np.ndarray, n_modes: int = 6,
|
|
85
|
+
point_masses: list | None = None) -> ModalResult:
|
|
86
|
+
"""Lowest `n_modes` natural frequencies (Hz) of a voxel model.
|
|
87
|
+
|
|
88
|
+
modulus: E per element (MPa) or scalar; density: g/cm^3 per element or
|
|
89
|
+
scalar; fixed: bool per dof (clamped). point_masses: [(node_ids, kg)],
|
|
90
|
+
each spread evenly over its nodes (all three directions)."""
|
|
91
|
+
from scipy.sparse.linalg import eigsh
|
|
92
|
+
|
|
93
|
+
fixed = np.asarray(fixed, bool)
|
|
94
|
+
st = Stiffness(mesh, nu, fixed)
|
|
95
|
+
k = st.assemble(np.broadcast_to(np.asarray(modulus, float), (mesh.n_elem,)))
|
|
96
|
+
m_node = _lumped_mass(mesh, density)
|
|
97
|
+
for nodes, kg in point_masses or []:
|
|
98
|
+
nodes = np.asarray(nodes, np.int64)
|
|
99
|
+
m_node[nodes] += kg * 1e-3 / len(nodes)
|
|
100
|
+
m = np.repeat(m_node, 3)
|
|
101
|
+
free = np.flatnonzero(~fixed)
|
|
102
|
+
kf = k[free][:, free].tocsc()
|
|
103
|
+
mf = m[free]
|
|
104
|
+
nm = min(n_modes, len(free) - 2)
|
|
105
|
+
if len(free) <= 120_000:
|
|
106
|
+
w, v = eigsh(kf, k=nm, M=sp.diags(mf).tocsc(), sigma=0.0, which="LM")
|
|
107
|
+
else:
|
|
108
|
+
# big models: LOBPCG on the mass-scaled operator with an AMG preconditioner
|
|
109
|
+
from scipy.sparse.linalg import lobpcg
|
|
110
|
+
|
|
111
|
+
from .fem import amg_solver
|
|
112
|
+
|
|
113
|
+
ml = amg_solver(kf.tocsr())
|
|
114
|
+
x0 = np.random.default_rng(0).standard_normal((len(free), nm + 4))
|
|
115
|
+
w, v = lobpcg(kf, x0, B=sp.diags(mf), M=ml.aspreconditioner(), largest=False, tol=1e-6, maxiter=400)
|
|
116
|
+
w, v = w[:nm], v[:, :nm]
|
|
117
|
+
order = np.argsort(w)
|
|
118
|
+
w, v = np.maximum(w[order], 0.0), v[:, order]
|
|
119
|
+
# mass-normalize (phi^T M phi = 1)
|
|
120
|
+
v = v / np.sqrt(np.einsum("ik,i,ik->k", v, mf, v))[None, :]
|
|
121
|
+
modes = np.zeros((mesh.n_dof, nm))
|
|
122
|
+
modes[free] = v
|
|
123
|
+
total = m_node.sum()
|
|
124
|
+
part = np.zeros((nm, 3))
|
|
125
|
+
for a in range(3):
|
|
126
|
+
r = np.zeros(mesh.n_dof)
|
|
127
|
+
r[a::3] = 1.0
|
|
128
|
+
g = (v * (mf * r[free])[:, None]).sum(axis=0) # participation factor phi^T M r
|
|
129
|
+
part[:, a] = g ** 2 / total
|
|
130
|
+
return ModalResult(np.sqrt(w) / (2 * np.pi), modes, float(total * 1e3), part, m)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def transmissibility(f_forcing, f_natural, eta: float):
|
|
134
|
+
"""Base-excitation transmissibility |X / Y| of one mode with structural
|
|
135
|
+
damping (loss factor eta): 1 / eta at resonance, below 1 above
|
|
136
|
+
sqrt(2) f_n (isolation)."""
|
|
137
|
+
r2 = (np.asarray(f_forcing, float) / f_natural) ** 2
|
|
138
|
+
return np.sqrt((1 + eta ** 2) / ((1 - r2) ** 2 + eta ** 2))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def base_transmissibility(res: ModalResult, dofs, axis: int, freqs, eta: float, fixed=None,
|
|
142
|
+
stiffness_scale=None) -> np.ndarray:
|
|
143
|
+
"""|absolute motion / base motion| at the given dofs (averaged, as complex
|
|
144
|
+
numbers) for a base shaken along `axis` (0, 1, 2) at each frequency, by
|
|
145
|
+
modal superposition with structural damping eta. The base is the
|
|
146
|
+
clamped dofs; `fixed` (bool per dof) leaves their mass out of the
|
|
147
|
+
participation factors. Truncated to the computed modes, so it holds up
|
|
148
|
+
to well below the highest of them. stiffness_scale (one per frequency)
|
|
149
|
+
multiplies every mode's stiffness at that frequency: a viscoelastic
|
|
150
|
+
mount that stiffens with frequency (viscoelastic.dynamic_factor over its
|
|
151
|
+
value where the modes were computed)."""
|
|
152
|
+
m = res.mass_vector.copy()
|
|
153
|
+
if fixed is not None:
|
|
154
|
+
m[np.asarray(fixed, bool)] = 0.0
|
|
155
|
+
r = np.zeros(len(m))
|
|
156
|
+
r[axis::3] = 1.0
|
|
157
|
+
gam = res.modes.T @ (m * r) # participation factors
|
|
158
|
+
phi = res.modes[np.asarray(dofs, np.int64)].mean(axis=0) # (k,)
|
|
159
|
+
wr2 = (2 * np.pi * res.frequencies) ** 2
|
|
160
|
+
w2 = (2 * np.pi * np.asarray(freqs, float))[:, None] ** 2
|
|
161
|
+
if stiffness_scale is not None:
|
|
162
|
+
wr2 = wr2[None, :] * np.asarray(stiffness_scale, float)[:, None]
|
|
163
|
+
drive = 1.0 if np.all(np.asarray(dofs) % 3 == axis) else 0.0
|
|
164
|
+
wr2 = np.broadcast_to(wr2, (w2.shape[0], len(gam)))
|
|
165
|
+
rel = (phi * gam)[None, :] * w2 / (wr2 - w2 + 1j * eta * wr2)
|
|
166
|
+
return np.abs(drive + rel.sum(axis=1))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def vibration_report(graph, spec: dict, material) -> dict:
|
|
170
|
+
"""The field graph's `vibration` block: modes of the `envelope` filled
|
|
171
|
+
with the `lattice` at `density` (homogenized: E C rho^n, rho_s rho),
|
|
172
|
+
clamped inside the `fixed` solid, with point masses."""
|
|
173
|
+
from .impact import sample_core
|
|
174
|
+
from .lattice import sheet_modulus
|
|
175
|
+
|
|
176
|
+
kind = spec.get("lattice", "gyroid")
|
|
177
|
+
inside, rho, lo, h = sample_core(graph, spec["envelope"], spec.get("density"), float(spec.get("pitch", 1.0)))
|
|
178
|
+
ijk = np.argwhere(inside)
|
|
179
|
+
mesh = HexMesh(ijk, h, lo)
|
|
180
|
+
r = rho[inside]
|
|
181
|
+
mod = float(material.E) * sheet_modulus(kind, r)
|
|
182
|
+
dens = float(material.density) * r
|
|
183
|
+
|
|
184
|
+
def nodes_in(node):
|
|
185
|
+
return np.flatnonzero(graph.evaluate(mesh.node_xyz, node) <= 0.5 * h)
|
|
186
|
+
|
|
187
|
+
fixed = np.zeros(mesh.n_dof, bool)
|
|
188
|
+
fn = nodes_in(spec["fixed"])
|
|
189
|
+
if len(fn) == 0:
|
|
190
|
+
raise ValueError(f"vibration: no nodes inside the fixed node {spec['fixed']!r}")
|
|
191
|
+
for a in range(3):
|
|
192
|
+
fixed[3 * fn + a] = True
|
|
193
|
+
pm = []
|
|
194
|
+
for p in spec.get("pointMasses", []):
|
|
195
|
+
nn = nodes_in(p["region"])
|
|
196
|
+
if len(nn) == 0:
|
|
197
|
+
raise ValueError(f"vibration: point mass region {p['region']!r} has no nodes")
|
|
198
|
+
pm.append((nn, float(p["mass"])))
|
|
199
|
+
res = modal_analysis(mesh, mod, dens, float(material.nu), fixed, int(spec.get("modes", 6)), pm)
|
|
200
|
+
eta = float(spec.get("lossFactor", loss_factor(material)))
|
|
201
|
+
out = {"lattice": kind, "cells": int(mesh.n_elem), **res.to_dict(eta)}
|
|
202
|
+
f1 = float(res.frequencies[0])
|
|
203
|
+
if spec.get("minFrequency") is not None:
|
|
204
|
+
out["minFrequency"] = float(spec["minFrequency"])
|
|
205
|
+
out["ok"] = bool(f1 >= float(spec["minFrequency"]))
|
|
206
|
+
if spec.get("excitation") is not None:
|
|
207
|
+
fx = float(spec["excitation"])
|
|
208
|
+
out["excitation"] = fx
|
|
209
|
+
out["transmissibility"] = round(float(max(transmissibility(fx, f, eta) for f in res.frequencies)), 2)
|
|
210
|
+
return out
|