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,329 @@
1
+ """
2
+ Minimum mass subject to stress and deflection limits, solved directly with
3
+ MMA: the formulation commercial generative design uses ("as light as
4
+ possible, safety factor at least 3, deflection at most 0.5 mm").
5
+
6
+ The volume-constrained compliance problem (simp.optimize) answers a
7
+ different question and needs an outer search over volume fractions to meet
8
+ a safety factor, several full optimizations deep. Here the limits are the
9
+ constraints, so one run lands on (or near) them, and a short correction
10
+ loop in study.py closes the remaining gap against the verified mesh.
11
+
12
+ - Stress: relaxed von Mises per cell, sigma_e = x_e^q * vm(C B u_e) with
13
+ q = 1 (qp-relaxation), aggregated with a P-mean (P = 8) and the
14
+ adaptive normalization of Le et al. 2010, which rescales the P-mean to
15
+ the true maximum from the previous iteration. Cells touching loads and
16
+ supports are left out, as in verification. One adjoint solve per case.
17
+ - Deflection: compliance over the load's magnitude is the displacement at
18
+ the load along its direction; self-adjoint, no extra solve.
19
+ - Everything else (filter, robust erosion, projection continuation,
20
+ symmetry, extrusion, the AM overhang filter for a build direction, the
21
+ reduced solve) is shared with simp.optimize.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import time
26
+
27
+ import numpy as np
28
+
29
+ from .domain import Domain
30
+ from .fem import elastic_c, hex8_b, hex8_b_center
31
+ from .manufacture import tie_groups
32
+ from .mma import MMA
33
+ from .simp import (E_MIN, PENALTY, SimpOptions, SimpResult, _Analysis, _dproject, _extrude, _pad,
34
+ _project, _symmetrize, density_filter)
35
+
36
+ __all__ = ["Limits", "optimize_constrained"]
37
+
38
+ # q = 0.5 (Le et al.'s choice) let near-void cells next to a member, strained
39
+ # by the member's motion across them, read 100-1000 times the limit: the
40
+ # aggregate chased those cells, and MMA either emptied the part or
41
+ # oscillated on the L-bracket benchmark. At q = 1 they fade with their
42
+ # density and the run converges onto the limit.
43
+ _Q = 1.0
44
+ _P = 8.0
45
+ # iterations an inactive buckling constraint keeps its last reading
46
+ BUCKLING_SKIP = 3
47
+ STRESS_POINTS = "corners" # where cell stress is sampled: "corners" or "center"
48
+ _VM = np.array([
49
+ [1.0, -0.5, -0.5, 0, 0, 0],
50
+ [-0.5, 1.0, -0.5, 0, 0, 0],
51
+ [-0.5, -0.5, 1.0, 0, 0, 0],
52
+ [0, 0, 0, 3.0, 0, 0],
53
+ [0, 0, 0, 0, 3.0, 0],
54
+ [0, 0, 0, 0, 0, 3.0],
55
+ ])
56
+
57
+
58
+ class Limits:
59
+ def __init__(self, stress: float | None, deflection: float | None, modulus: float,
60
+ buckling: float | None = None):
61
+ self.stress = stress # MPa, allowable peak von Mises
62
+ self.deflection = deflection # mm at the loads
63
+ self.modulus = modulus # MPa, to turn unit-E displacements into mm
64
+ self.buckling = buckling # smallest load factor at which the design may buckle
65
+
66
+
67
+ def optimize_constrained(domain: Domain, nu: float, opts: SimpOptions, limits: Limits,
68
+ on_iter=None, should_stop=None) -> SimpResult:
69
+ t0 = time.time()
70
+ mesh = domain.mesh
71
+ n = mesh.n_elem
72
+ pres = domain.preserve
73
+ design = ~pres
74
+ fe = _Analysis(domain, nu, opts.solve_rtol)
75
+ if opts.access is not None or opts.max_member:
76
+ raise ValueError("optimize_constrained does not take milling, casting or maxMember: use the search")
77
+ columns = tie_groups(domain, opts.extrusion, opts.pattern)
78
+ h, hs = density_filter(domain, opts.filter_radius, pad=_pad(opts))
79
+ ht = h.T.tocsr()
80
+ eta_e = opts.eta + opts.erode
81
+
82
+ weights = np.asarray(domain.case_weights, float)
83
+ cases = len(domain.loads)
84
+ force_mag = [float(np.linalg.norm(f.reshape(-1, 3).sum(axis=0))) for f in domain.loads]
85
+ defl_cases = [c for c in range(cases) if limits.deflection is not None and force_mag[c] > 0]
86
+ stress_cases = list(range(cases)) if limits.stress is not None else []
87
+ buck_cases = [c for c in range(cases) if limits.buckling is not None and np.any(domain.loads[c])]
88
+ m = len(defl_cases) + len(stress_cases) + len(buck_cases)
89
+ if m == len(buck_cases):
90
+ raise ValueError("optimize_constrained needs a stress or deflection limit")
91
+ buck = None
92
+ if buck_cases:
93
+ from .bucklingopt import BucklingModel
94
+
95
+ buck = BucklingModel(domain, nu)
96
+ buck_last, buck_age = None, 0
97
+
98
+ # Stress is not counted in cells where loads and supports are applied.
99
+ contact = (domain.support_nodes | domain.load_nodes)[mesh.enode].any(axis=1)
100
+ counted = ~contact
101
+ # Stress per unit-E displacement (6x24) at each sample point: the cell's
102
+ # 8 corners (STRESS_POINTS), with the element's own strain there
103
+ # (fem.hex8_b: the incompatible-mode bubbles carry part of a member's
104
+ # bending). Verification reads the surface of the meshed part, where a
105
+ # bent member peaks; the cell centre read it 1.5-3.4x low on the eval
106
+ # cases (MBB, bell crank, hook, motor mount, wrist yoke at 15k cells),
107
+ # the corners 1.2-2x.
108
+ c0 = elastic_c(1.0, nu)
109
+ if STRESS_POINTS == "corners":
110
+ cbs = [c0 @ hex8_b(a, b, cz, nu) / mesh.pitch for a in (-1.0, 1.0) for b in (-1.0, 1.0) for cz in (-1.0, 1.0)]
111
+ else:
112
+ cbs = [c0 @ hex8_b_center() / mesh.pitch]
113
+
114
+ # Printed without supports along build_direction: the AM filter (am.py)
115
+ # after each projection, as simp.optimize applies it.
116
+ if opts.build_direction is not None:
117
+ from .am import AMFilter
118
+
119
+ pinned = domain.full_grid(pres.astype(float)) > 0.5
120
+ am_b = AMFilter(domain.dims, opts.build_direction, pinned) # blueprint
121
+ am_e = AMFilter(domain.dims, opts.build_direction, pinned) # eroded
122
+ ijk = tuple(mesh.elem_ijk.T)
123
+
124
+ def printed(am, v):
125
+ return am.forward(domain.full_grid(v))[ijk]
126
+
127
+ def printed_grad(am, g):
128
+ return am.backward(domain.full_grid(g))[ijk]
129
+ else:
130
+ am_b = am_e = None
131
+
132
+ def printed(am, v):
133
+ return v
134
+
135
+ def printed_grad(am, g):
136
+ return g
137
+
138
+ free = np.flatnonzero(design)
139
+ x = np.full(n, opts.volume_fraction)
140
+ if opts.x0 is not None:
141
+ x = np.clip(np.asarray(opts.x0, float), 0.0, 1.0)
142
+ x[pres] = 1.0
143
+ mma = MMA(len(free), m, xmin=0.0, xmax=1.0, move=min(opts.move, 0.1))
144
+ move_cap = mma.move
145
+ worst_prev = np.inf
146
+ kappa = [1.0] * cases
147
+ us = [None] * cases
148
+ lams = [None] * cases
149
+ beta = float(opts.beta_start)
150
+ since_beta = 0
151
+ history = []
152
+ converged = False
153
+ it = 0
154
+
155
+ for it in range(1, opts.max_iterations + 1):
156
+ if should_stop is not None and should_stop():
157
+ break
158
+ xt = (h @ x) / hs
159
+ xp = _project(xt, beta, opts.eta)
160
+ xp[pres] = 1.0
161
+ xp = printed(am_b, xp)
162
+ xe = _project(xt, beta, eta_e)
163
+ xe[pres] = 1.0
164
+ xe = printed(am_e, xe)
165
+ dpb = _dproject(xt, beta, opts.eta)
166
+ dpb[pres] = 0.0
167
+ dpe = _dproject(xt, beta, eta_e)
168
+ dpe[pres] = 0.0
169
+
170
+ e = E_MIN + xe ** PENALTY * (1 - E_MIN)
171
+ de = PENALTY * xe ** (PENALTY - 1) * (1 - E_MIN)
172
+ us = fe.solve(e, xe, us, loads=domain.loads)
173
+
174
+ def to_x(g_xe):
175
+ return ht @ (printed_grad(am_e, g_xe) * dpe / hs)
176
+
177
+ cons, grads, record = [], [], {}
178
+ for c in defl_cases:
179
+ comp = float(domain.loads[c] @ us[c])
180
+ defl = comp / (force_mag[c] * limits.modulus)
181
+ ce = fe.strain_energy(us[c])
182
+ g_xe = -de * ce / (force_mag[c] * limits.modulus)
183
+ cons.append(defl / limits.deflection - 1.0)
184
+ grads.append(to_x(g_xe) / limits.deflection)
185
+ record[f"deflection.{domain.case_names[c]}"] = defl
186
+
187
+ if stress_cases:
188
+ adj_rhs, partials = [], []
189
+ for c in stress_cases:
190
+ ue = us[c][mesh.edof] # (n, 24)
191
+ nc = max(int(counted.sum()), 1) * len(cbs)
192
+ parts = []
193
+ for cbk in cbs:
194
+ sig = ue @ cbk.T # (n, 6) solid stress, MPa
195
+ vm = np.sqrt(np.maximum(np.einsum("ij,jk,ik->i", sig, _VM, sig), 1e-30))
196
+ parts.append((cbk, sig, vm, np.where(counted, xe ** _Q * vm / limits.stress, 0.0)))
197
+ smax = max(float(sr.max()) for *_, sr in parts)
198
+ # the p-norm scaled by the peak, so a design that lets go
199
+ # (stress 1e40) does not overflow sr ** P to inf and hand
200
+ # MMA a NaN step (the GRIP upper arm)
201
+ sc = max(smax, 1e-30)
202
+ pm = sc * (sum(float(np.sum((sr / sc) ** _P)) for *_, sr in parts) / nc) ** (1.0 / _P)
203
+ kappa[c] = 0.5 * (kappa[c] + smax / max(pm, 1e-30)) if it > 1 else smax / max(pm, 1e-30)
204
+ g = kappa[c] * pm
205
+ g_xe = np.zeros(n)
206
+ rhs = np.zeros(mesh.n_dof)
207
+ for cbk, sig, vm, srel in parts:
208
+ dg_ds = kappa[c] * (srel / max(pm, 1e-30)) ** (_P - 1) / nc
209
+ # explicit: d s / d xe at fixed u
210
+ g_xe += dg_ds * np.where(counted, _Q * np.maximum(xe, 1e-12) ** (_Q - 1) * vm / limits.stress,
211
+ 0.0)
212
+ # adjoint right-hand side: dg/du
213
+ coef = dg_ds * np.where(counted, xe ** _Q / (vm * limits.stress), 0.0) # (n,)
214
+ dvm_du = (sig @ _VM) @ cbk # d(vm^2)/2du
215
+ rhs += np.bincount(mesh.edof.ravel(), weights=(coef[:, None] * dvm_du).ravel(),
216
+ minlength=mesh.n_dof)
217
+ rhs[domain.fixed] = 0.0
218
+ adj_rhs.append(rhs)
219
+ partials.append((c, g, g_xe, smax))
220
+ lams = fe.resolve(adj_rhs, [l for l in lams] if lams[0] is not None and len(lams) == len(adj_rhs) else None)
221
+ for (c, g, g_xe, smax), lam in zip(partials, lams):
222
+ le = lam[mesh.edof]
223
+ ue = us[c][mesh.edof]
224
+ g_xe = g_xe - de * np.einsum("ij,jk,ik->i", le, fe.ke, ue)
225
+ cons.append(g - 1.0)
226
+ grads.append(to_x(g_xe))
227
+ record[f"stress.{domain.case_names[c]}"] = smax * limits.stress
228
+
229
+ if buck is not None:
230
+ # linear buckling of the eroded design, as stress and stiffness
231
+ # read it (bucklingopt.py): lambda_min >= limits.buckling as
232
+ # limits.buckling * mu_ks - 1 <= 0
233
+ # (at unit modulus the load factor is lambda / E)
234
+ # Far from the limit (load factor over twice the one asked)
235
+ # the last reading stands for up to BUCKLING_SKIP iterations:
236
+ # the eigensolve is most of an iteration on a big grid, and an
237
+ # inactive constraint's stale value and gradient do not steer.
238
+ req = limits.buckling / limits.modulus
239
+ if buck_last is None or buck_age >= BUCKLING_SKIP or max(v for v, _ in buck_last) > -0.5:
240
+ try:
241
+ got = buck.evaluate(xe, PENALTY, buck_cases)
242
+ except Exception as exc: # noqa: BLE001 - a failed eigensolve must not end the run
243
+ got = None
244
+ record["bucklingError"] = f"{type(exc).__name__}: {exc}"
245
+ if got is not None:
246
+ buck_last = [(req * mu_ks - 1.0, to_x(req * dmu)) for mu_ks, dmu, _mus in got]
247
+ record["bucklingSeconds"] = buck.last.get("seconds")
248
+ elif buck_last is None:
249
+ # nothing read yet: inactive this iteration (verify still checks the part)
250
+ buck_last = [(-1.0, np.zeros(n)) for _ in buck_cases]
251
+ buck_age = 0
252
+ else:
253
+ buck_age += 1
254
+ for c, (v, g) in zip(buck_cases, buck_last):
255
+ cons.append(v)
256
+ grads.append(g)
257
+ record[f"buckling.{domain.case_names[c]}"] = limits.buckling / (v + 1.0) if v > -1.0 else float("inf")
258
+
259
+ f0 = 10.0 * float(xp[design].mean())
260
+ df0 = ht @ (printed_grad(am_b, 10.0 * design / design.sum()) * dpb / hs)
261
+ if domain.mirror:
262
+ df0 = _symmetrize(df0, domain.mirror)
263
+ grads = [_symmetrize(g, domain.mirror) for g in grads]
264
+ df0 = _extrude(df0, columns, design)
265
+ grads = [_extrude(g, columns, design) for g in grads]
266
+
267
+ worst = max(cons)
268
+ if worst > 0.02 and worst > worst_prev:
269
+ # Getting less feasible: the convex model promised a step that
270
+ # cut volume and stress together, and the stress went up (from a
271
+ # grey start MMA otherwise ran the L-bracket down to nothing).
272
+ # Halve the step and pull the asymptotes in, a more conservative
273
+ # model, until the design is back inside the limits.
274
+ mma.move = max(0.02, 0.5 * mma.move)
275
+ mma.tighten(0.5)
276
+ elif worst <= 0.02:
277
+ mma.move = min(move_cap, 1.2 * mma.move)
278
+ worst_prev = worst
279
+ xnew_free = mma.update(x[free], f0, df0[free], np.array(cons), np.array([g[free] for g in grads]))
280
+ xnew = x.copy()
281
+ xnew[free] = xnew_free
282
+ if domain.mirror:
283
+ xnew = _symmetrize(xnew, domain.mirror)
284
+ xnew = _extrude(xnew, columns, design)
285
+ change = float(np.abs(xnew - x)[design].max())
286
+ x = xnew
287
+ since_beta += 1
288
+
289
+ rec = {"iteration": it, "volume": float(xp[design].mean()), "beta": beta, "change": change,
290
+ "constraints": [float(v) for v in cons], "seconds": time.time() - t0,
291
+ "cgIterations": fe.last_iters, "solvedCells": fe.last_cells, **record}
292
+ history.append(rec)
293
+ if on_iter is not None:
294
+ on_iter(rec, xp)
295
+
296
+ feasible = max(cons) <= 0.02
297
+ # Sharpen only once the design is inside the limits (or after three
298
+ # times the usual wait): each doubling raises the peak, and doubling
299
+ # on the timer while infeasible stacked those jumps until the run
300
+ # ended at twice the volume it needed (L-bracket, 0.59 against 0.31).
301
+ if beta < opts.beta_max and ((feasible and (since_beta >= opts.beta_every or change < 2 * opts.tol))
302
+ or since_beta >= 3 * opts.beta_every):
303
+ beta = min(beta * 2, opts.beta_max)
304
+ since_beta = 0
305
+ # Stress is far more nonlinear in the design than compliance, and
306
+ # more so as the projection sharpens: with the full move limit a
307
+ # single slack step at beta 8 cut the lug's volume in half and
308
+ # disconnected it. Shrink the steps instead of resetting them.
309
+ move_cap = max(0.02, move_cap * 0.6)
310
+ mma.move = min(mma.move, move_cap)
311
+ elif beta >= opts.beta_max and since_beta >= 5 and feasible:
312
+ w = opts.stall_window
313
+ if change < opts.tol or (len(history) > w and
314
+ abs(history[-1]["volume"] - history[-1 - w]["volume"]) < 2e-3):
315
+ converged = True
316
+ break
317
+
318
+ # Deliver the blueprint, the design whose volume was minimized. The
319
+ # compliance optimizer delivers the opening of the eroded design instead
320
+ # (simp.optimize), but here that reshaped thin members after their stress
321
+ # was checked: on the L-bracket the opening read up to 4x the limit where
322
+ # the blueprint reads within about 10% of it, at the same volume.
323
+ xp = _project((h @ x) / hs, beta, opts.eta)
324
+ xp[pres] = 1.0
325
+ xp = printed(am_b, xp)
326
+ return SimpResult(x=x, xphys=xp, compliance=float("nan"), case_compliance=[],
327
+ volume_fraction=float(xp[design].mean()), iterations=it,
328
+ converged=converged, seconds=time.time() - t0, history=history)
329
+