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/constrained.py
ADDED
|
@@ -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
|
+
|