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.
Files changed (220) hide show
  1. loadpath/README.md +294 -0
  2. loadpath/SKILL.md +584 -0
  3. loadpath/__init__.py +17 -0
  4. loadpath/__main__.py +1202 -0
  5. loadpath/am.py +316 -0
  6. loadpath/amgfast.py +326 -0
  7. loadpath/assembly.py +985 -0
  8. loadpath/benchmarks/__init__.py +0 -0
  9. loadpath/benchmarks/beams.py +266 -0
  10. loadpath/benchmarks/cases/ge-bracket.json +380 -0
  11. loadpath/benchmarks/evaluate.py +172 -0
  12. loadpath/benchmarks/mechanisms.py +416 -0
  13. loadpath/benchmarks/problems.py +140 -0
  14. loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
  15. loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
  16. loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
  17. loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
  18. loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
  19. loadpath/benchmarks/reference.py +138 -0
  20. loadpath/benchmarks/run.py +301 -0
  21. loadpath/brep.py +1470 -0
  22. loadpath/bucklingopt.py +300 -0
  23. loadpath/buildsim.py +1490 -0
  24. loadpath/cadimport.py +904 -0
  25. loadpath/calibration.py +885 -0
  26. loadpath/cavities.py +211 -0
  27. loadpath/checks/__init__.py +0 -0
  28. loadpath/checks/dfm.py +354 -0
  29. loadpath/checks/exchanger.py +7 -0
  30. loadpath/checks/fea.py +212 -0
  31. loadpath/checks/fit.py +475 -0
  32. loadpath/checks/networks.py +260 -0
  33. loadpath/checks/opening.py +67 -0
  34. loadpath/checks/sdf_kit.py +7 -0
  35. loadpath/conjugate.py +267 -0
  36. loadpath/constrained.py +329 -0
  37. loadpath/contact.py +691 -0
  38. loadpath/coopt.py +342 -0
  39. loadpath/cores.py +163 -0
  40. loadpath/crease.py +1138 -0
  41. loadpath/critic.py +212 -0
  42. loadpath/crosscheck.py +1107 -0
  43. loadpath/cutcell.py +510 -0
  44. loadpath/data/lattice_buckling.json +439 -0
  45. loadpath/data/lattice_tensors.json +27449 -0
  46. loadpath/data/published_experiments.json +71 -0
  47. loadpath/decimate.py +177 -0
  48. loadpath/delivery.py +190 -0
  49. loadpath/directions.py +262 -0
  50. loadpath/domain.py +630 -0
  51. loadpath/drains.py +615 -0
  52. loadpath/durability.py +413 -0
  53. loadpath/editor.html +3135 -0
  54. loadpath/editor.py +610 -0
  55. loadpath/escape.py +354 -0
  56. loadpath/evals/__init__.py +0 -0
  57. loadpath/evals/baselines/coarse.json +1543 -0
  58. loadpath/evals/baselines/full.json +1497 -0
  59. loadpath/evals/brief_kinds/__init__.py +1 -0
  60. loadpath/evals/brief_kinds/heatx.py +387 -0
  61. loadpath/evals/brief_kinds/lattice.py +571 -0
  62. loadpath/evals/brief_kinds/multimat.py +593 -0
  63. loadpath/evals/briefs/assets/bracket.step +1201 -0
  64. loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
  65. loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
  66. loadpath/evals/briefs/assets/motor-body.stl +0 -0
  67. loadpath/evals/briefs/assets/sketch.png +0 -0
  68. loadpath/evals/briefs/base-adapter-fdm.json +492 -0
  69. loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
  70. loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
  71. loadpath/evals/briefs/camera-boom-mjf.json +339 -0
  72. loadpath/evals/briefs/camera-mount-pla.json +203 -0
  73. loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
  74. loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
  75. loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
  76. loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
  77. loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
  78. loadpath/evals/briefs/forearm-fdm.json +146 -0
  79. loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
  80. loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
  81. loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
  82. loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
  83. loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
  84. loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
  85. loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
  86. loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
  87. loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
  88. loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
  89. loadpath/evals/briefs/lighten-step-bracket.json +220 -0
  90. loadpath/evals/briefs/nema17-mount-stl.json +429 -0
  91. loadpath/evals/briefs/pillow-block-sls.json +220 -0
  92. loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
  93. loadpath/evals/briefs/rod-hook-pla.json +154 -0
  94. loadpath/evals/briefs/sensor-arm-resin.json +150 -0
  95. loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
  96. loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
  97. loadpath/evals/briefs/sketch-clevis.json +229 -0
  98. loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
  99. loadpath/evals/briefs/tool-flange-ti64.json +280 -0
  100. loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
  101. loadpath/evals/briefs/turret-fdm.json +281 -0
  102. loadpath/evals/briefs/upper-arm-sls.json +179 -0
  103. loadpath/evals/briefs/urdf-forearm.json +233 -0
  104. loadpath/evals/briefs/wrist-link-mjf.json +278 -0
  105. loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
  106. loadpath/evals/briefs.py +393 -0
  107. loadpath/evals/cases/arm-link-cast.json +157 -0
  108. loadpath/evals/cases/arm-link-driven.json +171 -0
  109. loadpath/evals/cases/arm-link-sandwich.json +315 -0
  110. loadpath/evals/cases/arm-link.json +111 -0
  111. loadpath/evals/cases/bell-crank.json +116 -0
  112. loadpath/evals/cases/grip-turret.json +479 -0
  113. loadpath/evals/cases/grip-upper-arm.json +253 -0
  114. loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
  115. loadpath/evals/cases/grip-wrist-yoke.json +831 -0
  116. loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
  117. loadpath/evals/cases/mbb-beam.json +61 -0
  118. loadpath/evals/cases/motor-body.stl +0 -0
  119. loadpath/evals/cases/motor-mount.json +265 -0
  120. loadpath/evals/cases/printed-hook.json +77 -0
  121. loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
  122. loadpath/evals/cases/wall-bracket.json +126 -0
  123. loadpath/evals/corpus.py +179 -0
  124. loadpath/evals/pareto.py +90 -0
  125. loadpath/evals/run.py +860 -0
  126. loadpath/examples/arm-link.json +25 -0
  127. loadpath/examples/bumpered-bracket-compression.json +225 -0
  128. loadpath/examples/bumpered-bracket-lattice.json +204 -0
  129. loadpath/examples/bumpered-bracket.json +34 -0
  130. loadpath/examples/gripper-finger-mm.json +185 -0
  131. loadpath/examples/heat-exchanger-counterflow.json +21 -0
  132. loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
  133. loadpath/examples/heat-exchanger-crossflow.json +20 -0
  134. loadpath/examples/pedal-lattice.json +23 -0
  135. loadpath/examples/pressure-plenum.json +12 -0
  136. loadpath/examples/shelf-bracket.json +27 -0
  137. loadpath/exchanger.py +831 -0
  138. loadpath/fasteners.py +199 -0
  139. loadpath/fem.py +668 -0
  140. loadpath/femops.py +70 -0
  141. loadpath/fieldjit.py +981 -0
  142. loadpath/fieldops.py +119 -0
  143. loadpath/fields.py +3697 -0
  144. loadpath/fieldtorch.py +464 -0
  145. loadpath/flow.py +984 -0
  146. loadpath/flowtopo.py +300 -0
  147. loadpath/gpu.py +521 -0
  148. loadpath/heatx.py +1067 -0
  149. loadpath/heatx_thermal.py +432 -0
  150. loadpath/homogenized.py +886 -0
  151. loadpath/ibrep.py +552 -0
  152. loadpath/impact.py +693 -0
  153. loadpath/infill.py +376 -0
  154. loadpath/interface_library.py +340 -0
  155. loadpath/interlock.py +186 -0
  156. loadpath/isolator.py +149 -0
  157. loadpath/knn.py +204 -0
  158. loadpath/lattice.py +1065 -0
  159. loadpath/lattice3mf.py +760 -0
  160. loadpath/lattice_buckling.py +245 -0
  161. loadpath/lattice_grade.py +325 -0
  162. loadpath/lattice_orient.py +595 -0
  163. loadpath/lattice_print.py +159 -0
  164. loadpath/lattice_tensors.py +558 -0
  165. loadpath/latticestep.py +1210 -0
  166. loadpath/manufacture.py +446 -0
  167. loadpath/material_library.py +489 -0
  168. loadpath/materials.py +144 -0
  169. loadpath/mcp_server.py +786 -0
  170. loadpath/mechanism.py +218 -0
  171. loadpath/mechanism_nl.py +811 -0
  172. loadpath/meshops.py +452 -0
  173. loadpath/mg.py +853 -0
  174. loadpath/mma.py +214 -0
  175. loadpath/modal.py +210 -0
  176. loadpath/multimat.py +1788 -0
  177. loadpath/narrowband.py +418 -0
  178. loadpath/nlkernel.py +241 -0
  179. loadpath/nonlinear.py +1095 -0
  180. loadpath/orient.py +334 -0
  181. loadpath/porous.py +994 -0
  182. loadpath/pressure.py +171 -0
  183. loadpath/preview.py +335 -0
  184. loadpath/printcheck.py +263 -0
  185. loadpath/printsetup.py +207 -0
  186. loadpath/processcheck.py +376 -0
  187. loadpath/quick.py +113 -0
  188. loadpath/recall.py +406 -0
  189. loadpath/reconstruct.py +797 -0
  190. loadpath/render.py +436 -0
  191. loadpath/resolved.py +189 -0
  192. loadpath/schema.py +1701 -0
  193. loadpath/sdf_kit.py +843 -0
  194. loadpath/selfx.py +292 -0
  195. loadpath/shapes.py +584 -0
  196. loadpath/sharp.py +717 -0
  197. loadpath/simp.py +1170 -0
  198. loadpath/sizing.py +312 -0
  199. loadpath/slicer.py +321 -0
  200. loadpath/spec.py +1072 -0
  201. loadpath/step.py +806 -0
  202. loadpath/struts.py +639 -0
  203. loadpath/study.py +1937 -0
  204. loadpath/tasks.py +822 -0
  205. loadpath/thermal.py +1691 -0
  206. loadpath/threemf.py +487 -0
  207. loadpath/tune.py +437 -0
  208. loadpath/validation.py +145 -0
  209. loadpath/verify.py +1215 -0
  210. loadpath/viewer.html +694 -0
  211. loadpath/viewer.py +446 -0
  212. loadpath/viscoelastic.py +106 -0
  213. loadpath/voxelprint.py +582 -0
  214. loadpath/voxels.py +402 -0
  215. loadpath-0.1.1.dist-info/METADATA +319 -0
  216. loadpath-0.1.1.dist-info/RECORD +220 -0
  217. loadpath-0.1.1.dist-info/WHEEL +5 -0
  218. loadpath-0.1.1.dist-info/entry_points.txt +2 -0
  219. loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
  220. loadpath-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,300 @@
1
+ """
2
+ Linear buckling as an optimization constraint: the smallest buckling load
3
+ factors of a density design and their sensitivities, for the constrained
4
+ method (constrained.py).
5
+
6
+ The check (verify._buckling) reads the finished part; the optimizer needs
7
+ the same number on the grey design, every iteration, with its gradient.
8
+ For a design x with stiffness K(x) and geometric stiffness Kg(x, u(x)) of
9
+ the linear stresses under the load case,
10
+
11
+ K phi = lambda (-Kg) phi <=> (-Kg) phi = mu K phi, mu = 1 / lambda,
12
+
13
+ and the largest mu are the smallest positive buckling loads. With phi
14
+ normalized to phi^T K phi = 1, the sensitivity of mu to cell e is
15
+
16
+ dmu/dx_e = -E_G'(x_e) phi_e^T kg_e phi_e - mu E'(x_e) phi_e^T ke phi_e
17
+ + E'(x_e) w_e^T ke u_e, K w = a, a = d(phi^T Kg phi)/du,
18
+
19
+ the last term because Kg depends on the displacements, which depend on the
20
+ design (one more solve per mode, on the same factorization). The modes are
21
+ aggregated with a Kreisselmeier-Steinhauser max, which keeps the constraint
22
+ smooth where two modes cross.
23
+
24
+ Pseudo-modes. Grey cells are soft in K (E ~ x^3) but carry stress in Kg,
25
+ and a soft cell far from the load path can "buckle" at a tiny load (Neves
26
+ et al. 1995; Ferrari and Sigmund 2019). The model is built on the live
27
+ cells only (density above simp.ALIVE and joined to a support, as the
28
+ stiffness solve), and Kg's stresses take a harsher interpolation than K
29
+ below a density of `cutoff`, so a nearly empty cell neither stiffens nor
30
+ loads the mode.
31
+
32
+ Element: incompatible-mode hex8 (fem.hex8_element), the verify check's own,
33
+ so a member 2-3 cells thick reads its Euler load within a few percent.
34
+ The factorization is PARDISO (pypardiso, if installed) or SuperLU.
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import time
39
+
40
+ import numpy as np
41
+ import scipy.sparse as sp
42
+ from scipy import ndimage
43
+ from scipy.sparse.linalg import LinearOperator, eigsh, lobpcg
44
+
45
+ from .fem import HexMesh, Stiffness, _hex8_parts, hex8_element
46
+
47
+ __all__ = ["BucklingModel", "geometric_parts"]
48
+
49
+ _CONN26 = np.ones((3, 3, 3), bool)
50
+
51
+
52
+ def _factor(k: sp.csr_matrix):
53
+ """A solve with K for one right-hand side or a block of them: PARDISO
54
+ when available (symmetric positive definite, upper triangle; 10-30x
55
+ SuperLU on 3D hex meshes, and a block of eight costs about two single
56
+ solves), else SuperLU."""
57
+ try:
58
+ import pypardiso
59
+ except ImportError:
60
+ from .nonlinear import _splu
61
+
62
+ lu = _splu(k)
63
+ return lu.solve, None
64
+ a = sp.triu(k, format="csr")
65
+ a.sort_indices()
66
+ solver = pypardiso.PyPardisoSolver()
67
+ solver.set_matrix_type(2)
68
+ try:
69
+ solver.factorize(a)
70
+ except Exception: # noqa: BLE001 - a zero pivot (a hinge in the live cells): pivoting LU
71
+ solver.free_memory(everything=True)
72
+ a = k.tocsr()
73
+ a.sort_indices()
74
+ solver = pypardiso.PyPardisoSolver()
75
+ solver.set_matrix_type(11)
76
+ solver.factorize(a)
77
+ solver.set_phase(33)
78
+
79
+ def solve(b):
80
+ b = np.asarray(b, float)
81
+ # straight to the solve phase: pypardiso.solve() hashes the matrix
82
+ # every call to see whether it is factored already
83
+ return solver._call_pardiso(a, np.asfortranarray(b) if b.ndim == 2 else np.ascontiguousarray(b))
84
+
85
+ return solve, lambda: solver.free_memory(everything=True)
86
+
87
+
88
+ def _gauss_im():
89
+ from .nonlinear import _GAUSS, _GAUSS_IM
90
+
91
+ return _GAUSS_IM if hex8_element() == "im" else _GAUSS
92
+
93
+
94
+ def geometric_parts(ue, phis, h, nu):
95
+ """Per element, for displacements ue (m, 24) and modes phis [(m, 24)]:
96
+ g (m,) = phi_e^T kg_e(u_e) phi_e and a (m, 24) = d g / d u_e, at unit
97
+ stress modulus. kg_e is nonlinear._geometric's element matrix (the
98
+ bubbles' linear amplitudes in both the stresses and the modes)."""
99
+ from .nonlinear import _lame
100
+
101
+ lam, mu = _lame(nu)
102
+ grads = _gauss_im()
103
+ r = _hex8_parts(nu, hex8_element())[1]
104
+ im = r is not None and len(grads[0]) == 11
105
+ t = np.vstack([np.eye(24), r]) if im else np.eye(24)
106
+ n = t.shape[0] // 3
107
+ m = len(ue)
108
+ w = h ** 3 / 8.0
109
+ ux = (ue @ t.T).reshape(m, n, 3)
110
+ out = []
111
+ for ph in phis:
112
+ px = (ph @ t.T).reshape(m, n, 3)
113
+ g = np.zeros(m)
114
+ da = np.zeros((m, n, 3))
115
+ for dn0 in grads:
116
+ dn = dn0 / h
117
+ gp = np.einsum("mak,ai->mki", px, dn) # d phi_k / d x_i
118
+ hh = np.einsum("mki,mkj->mij", gp, gp)
119
+ s = 2 * mu * hh
120
+ s[:, [0, 1, 2], [0, 1, 2]] += lam * np.trace(hh, axis1=1, axis2=2)[:, None]
121
+ gu = np.einsum("mai,aj->mij", ux, dn) # d u_i / d x_j
122
+ g += w * np.einsum("mij,mij->m", gu, s)
123
+ da += w * np.einsum("aj,mij->mai", dn, s)
124
+ out.append((g, da.reshape(m, 3 * n) @ t))
125
+ return out
126
+
127
+
128
+ class BucklingModel:
129
+ """Buckling load factors and their gradients for density designs on
130
+ `domain`'s grid. `n_modes` modes per load case are computed and
131
+ aggregated (KS, `rho`)."""
132
+
133
+ def __init__(self, domain, nu: float, n_modes: int = 4, rho: float = 40.0, cutoff: float = 0.3,
134
+ alive: float = 1e-3):
135
+ self.domain = domain
136
+ self.nu = nu
137
+ self.n_modes = n_modes
138
+ self.rho = rho
139
+ self.cutoff = cutoff
140
+ self.alive = alive
141
+ mesh = domain.mesh
142
+ self.support_touch = domain.support_nodes[mesh.enode].any(axis=1)
143
+ self.anchor = self.support_touch | domain.load_nodes[mesh.enode].any(axis=1) | domain.preserve
144
+ self._v0 = {}
145
+ self._mu0 = {}
146
+ self.last = {}
147
+
148
+ def interp(self, xe, p):
149
+ """(E, dE/dx, E_G, dE_G/dx) per cell at unit modulus: E is SIMP's
150
+ (E_MIN floor), E_G the stress interpolation of Kg, x^p above the
151
+ cutoff and faded to zero below it as (x / cutoff)^3 more."""
152
+ from .simp import E_MIN
153
+
154
+ e = E_MIN + xe ** p * (1 - E_MIN)
155
+ de = p * xe ** (p - 1) * (1 - E_MIN)
156
+ c = self.cutoff
157
+ if c > 0:
158
+ r = np.clip(xe / c, 0.0, 1.0)
159
+ f = np.where(xe < c, r ** 3, 1.0)
160
+ df = np.where(xe < c, 3 * r ** 2 / c, 0.0)
161
+ else:
162
+ f, df = np.ones_like(xe), np.zeros_like(xe)
163
+ eg = xe ** p * f
164
+ deg = p * xe ** (p - 1) * f + xe ** p * df
165
+ return e, de, eg, deg
166
+
167
+ def _modes(self, c, a, k, minv, nm, fd, n_full):
168
+ """The nm largest mu of a phi = mu k phi, largest first. Warm
169
+ (LOBPCG from the last iteration's modes, preconditioned with the
170
+ factorization: a few block solves) when there are last modes,
171
+ else ARPACK in shift-invert-like mode 2 (tens of solves)."""
172
+ prev = self._v0.get(c)
173
+ if prev is not None:
174
+ p = nm + 2
175
+ x0 = prev[fd]
176
+ if x0.shape[1] < p:
177
+ x0 = np.hstack([x0, np.random.default_rng(c).standard_normal((len(fd), p - x0.shape[1]))])
178
+ try:
179
+ with np.errstate(all="ignore"):
180
+ import warnings
181
+
182
+ with warnings.catch_warnings():
183
+ warnings.simplefilter("ignore")
184
+ # residuals to 0.3% of mu: the eigenvalues come out
185
+ # good to about the square of that
186
+ tol = 3e-3 * self._mu0.get(c, 1.0)
187
+ mu, vec, res = lobpcg(a, x0, B=k, M=minv, largest=True, tol=tol, maxiter=25,
188
+ retResidualNormsHistory=True)
189
+ order = np.argsort(-mu)
190
+ mu, vec = mu[order], vec[:, order]
191
+ # converged on the wanted modes (relative to mu_max)?
192
+ r = np.asarray(res[-1])[order][:nm] if len(res) else np.full(nm, np.inf)
193
+ if mu[0] > 0 and np.all(np.isfinite(mu)) and np.all(r < 1e-2 * abs(mu[0])):
194
+ self._mu0[c] = float(mu[0])
195
+ self._store(c, vec, fd, n_full)
196
+ self.last_method = "lobpcg"
197
+ return mu[:nm], vec[:, :nm]
198
+ except Exception: # noqa: BLE001 - fall back to ARPACK below
199
+ pass
200
+ try:
201
+ mu, vec = eigsh(a, k=nm, M=k, Minv=minv, which="LA", tol=1e-4, maxiter=2000)
202
+ except Exception: # noqa: BLE001 - ARPACK did not converge: once more, looser
203
+ mu, vec = eigsh(a, k=nm, M=k, Minv=minv, which="LA", tol=1e-3, maxiter=5000)
204
+ order = np.argsort(-mu)
205
+ mu, vec = mu[order], vec[:, order]
206
+ self._store(c, vec, fd, n_full)
207
+ self._mu0[c] = float(mu[0])
208
+ self.last_method = "arpack"
209
+ return mu, vec
210
+
211
+ def _store(self, c, vec, fd, n_full):
212
+ full = np.zeros((n_full, vec.shape[1]))
213
+ full[fd] = vec
214
+ self._v0[c] = full
215
+
216
+ def _live(self, xe):
217
+ mesh = self.domain.mesh
218
+ alive = (xe > self.alive) | self.anchor
219
+ g = np.zeros(self.domain.dims, bool)
220
+ ijk = tuple(mesh.elem_ijk.T)
221
+ g[ijk] = alive
222
+ lbl, _ = ndimage.label(g, structure=_CONN26)
223
+ el = lbl[ijk]
224
+ keep_l = np.unique(el[alive & self.support_touch])
225
+ return alive & np.isin(el, keep_l[keep_l > 0])
226
+
227
+ def evaluate(self, xe: np.ndarray, p: float, cases=None):
228
+ """For each load case (default all): (mu_ks, dmu_ks/dxe (n,), mus),
229
+ where 1 / mu_ks is the aggregated smallest buckling load factor
230
+ (inf when the case does not compress anything)."""
231
+ t0 = time.time()
232
+ d = self.domain
233
+ mesh = d.mesh
234
+ cases = list(range(len(d.loads))) if cases is None else list(cases)
235
+ keep = self._live(xe)
236
+ el = np.flatnonzero(keep)
237
+ sub = HexMesh(mesh.elem_ijk[el], mesh.pitch, mesh.origin)
238
+ fn = mesh.node_id(sub.node_ijk)
239
+ fd = (3 * fn[:, None] + np.arange(3)).ravel()
240
+ fixed = d.fixed[fd]
241
+ stiff = Stiffness(sub, self.nu, fixed)
242
+ e, de, eg, deg = self.interp(xe[el], p)
243
+ k = stiff.assemble(e)
244
+ if d.springs is not None:
245
+ free = sp.diags((~fixed).astype(float))
246
+ k = (k + free @ d.springs[fd][:, fd] @ free).tocsr()
247
+ solve, free_memory = _factor(k)
248
+ try:
249
+ ndof = sub.n_dof
250
+ minv = LinearOperator((ndof, ndof), matvec=solve, matmat=solve, dtype=float)
251
+ ke = stiff.ke
252
+ out = []
253
+ for c in cases:
254
+ f = d.loads[c][fd].copy()
255
+ f[fixed] = 0.0
256
+ if not np.any(f):
257
+ out.append((0.0, np.zeros(mesh.n_elem), np.zeros(0)))
258
+ continue
259
+ u = solve(f)
260
+ from .nonlinear import _geometric
261
+
262
+ kg = _geometric(stiff, u, eg, self.nu)
263
+ nm = min(self.n_modes, ndof - 2)
264
+ mu, vec = self._modes(c, -kg, k, minv, nm, fd, mesh.n_dof)
265
+ if mu[0] <= 0:
266
+ out.append((0.0, np.zeros(mesh.n_elem), mu))
267
+ continue
268
+ # KS over the modes, on mu / mu_max
269
+ mu = np.asarray(mu[:nm], float)
270
+ mmax = float(mu[0])
271
+ wts = np.exp(self.rho * (mu / mmax - 1.0))
272
+ ks = mmax * (1.0 + np.log(wts.sum()) / self.rho)
273
+ wts = wts / wts.sum()
274
+ use = [i for i in range(nm) if wts[i] >= 1e-6]
275
+ # modes normalized phi^T K phi = 1
276
+ phis = []
277
+ for i in use:
278
+ ph = vec[:, i]
279
+ phis.append(ph / np.sqrt(max(float(ph @ (k @ ph)), 1e-300)))
280
+ ue = u[sub.edof]
281
+ parts = geometric_parts(ue, [ph[sub.edof] for ph in phis], mesh.pitch, self.nu)
282
+ # the adjoints of every counted mode in one block solve
283
+ arhs = np.zeros((ndof, len(use)))
284
+ for j, (_, a_e) in enumerate(parts):
285
+ arhs[:, j] = np.bincount(sub.edof.ravel(), weights=(eg[:, None] * a_e).ravel(), minlength=ndof)
286
+ arhs[fixed] = 0.0
287
+ wsol = np.asarray(solve(arhs)).reshape(ndof, len(use))
288
+ grad = np.zeros(mesh.n_elem)
289
+ for j, i in enumerate(use):
290
+ pe = phis[j][sub.edof]
291
+ g_e = parts[j][0]
292
+ kphi = np.einsum("mi,ij,mj->m", pe, ke, pe)
293
+ term = np.einsum("mi,ij,mj->m", wsol[:, j][sub.edof], ke, ue)
294
+ grad[el] += wts[i] * (-deg * g_e - mu[i] * de * kphi + de * term)
295
+ out.append((ks, grad, mu))
296
+ finally:
297
+ if free_memory is not None:
298
+ free_memory()
299
+ self.last = {"cells": int(len(el)), "seconds": time.time() - t0}
300
+ return out