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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
One evaluator for every design, whoever made it.
|
|
3
|
+
|
|
4
|
+
Reference codes report the compliance of a grey, penalized density field;
|
|
5
|
+
loadpath delivers an almost black-and-white one. Every design is scored here
|
|
6
|
+
the same way, on the problem's own boundary conditions with void at 1e-9:
|
|
7
|
+
its penalized (p = 3) compliance as delivered, and its compliance once
|
|
8
|
+
thresholded to solid/void at the target volume (keeping the densest cells).
|
|
9
|
+
The headline `compliance` is the lower of the two, the most generous reading
|
|
10
|
+
of either kind of design.
|
|
11
|
+
|
|
12
|
+
Plane problems use the 4-node plane-stress element of top88 (so a reference
|
|
13
|
+
design scores what top88 itself would compute); 3D ones use loadpath's 8-node
|
|
14
|
+
hexahedron, the same element as top3d.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import scipy.sparse as sp
|
|
20
|
+
import scipy.sparse.linalg as spla
|
|
21
|
+
|
|
22
|
+
from .problems import Problem
|
|
23
|
+
|
|
24
|
+
__all__ = ["Evaluator", "binarize"]
|
|
25
|
+
|
|
26
|
+
E_VOID = 1e-9
|
|
27
|
+
NU = 0.3
|
|
28
|
+
_AX = {"x": 0, "y": 1, "z": 2}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _direct(k, f):
|
|
32
|
+
"""Sparse direct solve: CHOLMOD when scikit-sparse is installed (what
|
|
33
|
+
MATLAB's backslash uses for these matrices; much faster than SuperLU in
|
|
34
|
+
3D), SuperLU otherwise."""
|
|
35
|
+
try:
|
|
36
|
+
from sksparse.cholmod import cho_solve
|
|
37
|
+
except ImportError:
|
|
38
|
+
return spla.spsolve(k.tocsc(), f)
|
|
39
|
+
return cho_solve(k.tocsc(), f)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def binarize(x: np.ndarray, active: np.ndarray, volfrac: float | None) -> np.ndarray:
|
|
43
|
+
"""Solid where x is highest, `volfrac` of the active cells; None
|
|
44
|
+
thresholds at 0.5 instead."""
|
|
45
|
+
out = np.zeros_like(x, dtype=float)
|
|
46
|
+
xa = x[active]
|
|
47
|
+
if volfrac is None:
|
|
48
|
+
out[active] = (xa > 0.5).astype(float)
|
|
49
|
+
return out
|
|
50
|
+
k = int(round(volfrac * xa.size))
|
|
51
|
+
order = np.argsort(-xa, kind="stable")
|
|
52
|
+
b = np.zeros(xa.size)
|
|
53
|
+
b[order[:k]] = 1.0
|
|
54
|
+
out[active] = b
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _q4_ke(nu=NU):
|
|
59
|
+
a11 = np.array([[12, 3, -6, -3], [3, 12, 3, 0], [-6, 3, 12, -3], [-3, 0, -3, 12]], float)
|
|
60
|
+
a12 = np.array([[-6, -3, 0, 3], [-3, -6, -3, -6], [0, -3, -6, 3], [3, -6, 3, -6]], float)
|
|
61
|
+
b11 = np.array([[-4, 3, -2, 9], [3, -4, -9, 4], [-2, -9, -4, -3], [9, 4, -3, -4]], float)
|
|
62
|
+
b12 = np.array([[2, -3, 4, -9], [-3, 2, 9, -2], [4, 9, 2, 3], [-9, -2, 3, 2]], float)
|
|
63
|
+
return 1 / (1 - nu ** 2) / 24 * (np.block([[a11, a12], [a12.T, a11]]) + nu * np.block([[b11, b12], [b12.T, b11]]))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Evaluator:
|
|
67
|
+
def __init__(self, p: Problem):
|
|
68
|
+
self.p = p
|
|
69
|
+
nx, ny, nz = p.dims
|
|
70
|
+
if p.plane:
|
|
71
|
+
# top88 numbering in grid terms: node (i, k) -> i*(nz+1) + (nz-k),
|
|
72
|
+
# dofs 2n (x), 2n+1 (z up); element nodes BL, BR, TR, TL
|
|
73
|
+
def node(i, k):
|
|
74
|
+
return i * (nz + 1) + (nz - k)
|
|
75
|
+
ii, kk = np.meshgrid(np.arange(nx), np.arange(nz), indexing="ij")
|
|
76
|
+
ii, kk = ii.ravel(), kk.ravel() # element order: i major, k minor
|
|
77
|
+
nodes = np.stack([node(ii, kk), node(ii + 1, kk), node(ii + 1, kk + 1), node(ii, kk + 1)], axis=1)
|
|
78
|
+
self.edof = np.stack([2 * nodes, 2 * nodes + 1], axis=2).reshape(-1, 8)
|
|
79
|
+
self.ke = _q4_ke()
|
|
80
|
+
self.n_dof = 2 * (nx + 1) * (nz + 1)
|
|
81
|
+
self.cell = (ii, np.zeros_like(ii), kk)
|
|
82
|
+
comp = {"x": 0, "z": 1}
|
|
83
|
+
dof = lambda n, c: 2 * node(n[:, 0], n[:, 2]) + comp[c]
|
|
84
|
+
else:
|
|
85
|
+
from ..fem import HexMesh, hex8_ke
|
|
86
|
+
ii, jj, kk = np.meshgrid(np.arange(nx), np.arange(ny), np.arange(nz), indexing="ij")
|
|
87
|
+
ijk = np.stack([ii.ravel(), jj.ravel(), kk.ravel()], axis=1)
|
|
88
|
+
mesh = HexMesh(ijk, 1.0, np.zeros(3))
|
|
89
|
+
self.edof = mesh.edof
|
|
90
|
+
self.ke = hex8_ke(NU)
|
|
91
|
+
self.n_dof = mesh.n_dof
|
|
92
|
+
self.cell = tuple(ijk.T)
|
|
93
|
+
dof = lambda n, c: 3 * mesh.node_id(n) + _AX[c]
|
|
94
|
+
fixed = np.zeros(self.n_dof, bool)
|
|
95
|
+
for nodes, comps in p.fixed:
|
|
96
|
+
for c in comps:
|
|
97
|
+
fixed[dof(nodes, c)] = True
|
|
98
|
+
f = np.zeros(self.n_dof)
|
|
99
|
+
for nodes, c, v in p.loads:
|
|
100
|
+
np.add.at(f, dof(nodes, c), v)
|
|
101
|
+
self.fixed, self.f = fixed, f
|
|
102
|
+
self.free = np.flatnonzero(~fixed)
|
|
103
|
+
ne = len(self.edof)
|
|
104
|
+
self._rows = np.repeat(self.edof, self.edof.shape[1], axis=1).ravel()
|
|
105
|
+
self._cols = np.tile(self.edof, (1, self.edof.shape[1])).ravel()
|
|
106
|
+
self.active = p.active[self.cell]
|
|
107
|
+
|
|
108
|
+
def cells(self, grid: np.ndarray) -> np.ndarray:
|
|
109
|
+
"""Grid (nx, ny, nz) -> this evaluator's element order."""
|
|
110
|
+
return np.asarray(grid, float)[self.cell]
|
|
111
|
+
|
|
112
|
+
def solve(self, e: np.ndarray) -> np.ndarray:
|
|
113
|
+
data = (e[:, None, None] * self.ke[None]).ravel()
|
|
114
|
+
k = sp.csr_matrix((data, (self._rows, self._cols)), shape=(self.n_dof, self.n_dof))
|
|
115
|
+
kf = k[self.free][:, self.free]
|
|
116
|
+
u = np.zeros(self.n_dof)
|
|
117
|
+
u[self.free] = _direct(kf, self.f[self.free])
|
|
118
|
+
return u
|
|
119
|
+
|
|
120
|
+
def compliance(self, grid: np.ndarray, penal: float = 3.0) -> float:
|
|
121
|
+
x = self.cells(grid)
|
|
122
|
+
u = self.solve(E_VOID + x ** penal * (1 - E_VOID))
|
|
123
|
+
return float(self.f @ u)
|
|
124
|
+
|
|
125
|
+
def von_mises(self, grid: np.ndarray) -> np.ndarray:
|
|
126
|
+
"""Peak-relevant von Mises per element of a black-and-white design
|
|
127
|
+
(solid cells only; void reads 0). Plane problems only."""
|
|
128
|
+
if not self.p.plane:
|
|
129
|
+
raise NotImplementedError("von Mises scoring is for plane problems")
|
|
130
|
+
x = self.cells(grid)
|
|
131
|
+
u = self.solve(E_VOID + x ** 3 * (1 - E_VOID))
|
|
132
|
+
b = 0.5 * np.array([[-1, 0, 1, 0, 1, 0, -1, 0],
|
|
133
|
+
[0, -1, 0, -1, 0, 1, 0, 1],
|
|
134
|
+
[-1, -1, -1, 1, 1, 1, 1, -1]], float)
|
|
135
|
+
d = 1 / (1 - NU ** 2) * np.array([[1, NU, 0], [NU, 1, 0], [0, 0, (1 - NU) / 2]])
|
|
136
|
+
s = u[self.edof] @ (d @ b).T
|
|
137
|
+
vm = np.sqrt(s[:, 0] ** 2 + s[:, 1] ** 2 - s[:, 0] * s[:, 1] + 3 * s[:, 2] ** 2)
|
|
138
|
+
return np.where(x > 0.5, vm, 0.0)
|
|
139
|
+
|
|
140
|
+
def load_cells(self, layers: int = 2) -> np.ndarray:
|
|
141
|
+
"""Elements within `layers` cells of a loaded node, which every
|
|
142
|
+
stress score leaves out (point loads make a singular stress there)."""
|
|
143
|
+
nodes = np.concatenate([n for n, _, _ in self.p.loads])
|
|
144
|
+
ijk = np.stack(self.cell, axis=1)
|
|
145
|
+
near = np.zeros(len(ijk), bool)
|
|
146
|
+
for n in nodes:
|
|
147
|
+
near |= np.all(np.abs(ijk + 0.5 - n) <= layers, axis=1)
|
|
148
|
+
return near
|
|
149
|
+
|
|
150
|
+
def score(self, grid: np.ndarray, target: float) -> dict:
|
|
151
|
+
"""Everything the report compares, for one design grid."""
|
|
152
|
+
x = self.cells(grid)
|
|
153
|
+
act = self.active
|
|
154
|
+
full = np.zeros(self.p.dims)
|
|
155
|
+
out = {"volume": float(x[act].mean()), "greyness": float((4 * x * (1 - x))[act].mean()),
|
|
156
|
+
"compliance_grey": self.compliance(grid)}
|
|
157
|
+
for key, vf in (("matched", target), ("threshold", None)):
|
|
158
|
+
b = binarize(x, act, vf)
|
|
159
|
+
full[self.cell] = b
|
|
160
|
+
out[f"compliance_{key}"] = self.compliance(full)
|
|
161
|
+
out[f"volume_{key}"] = float(b[act].mean())
|
|
162
|
+
if key == "matched":
|
|
163
|
+
bin_matched = full.copy()
|
|
164
|
+
# the most generous reading of each design: its own penalized
|
|
165
|
+
# compliance, or black and white at the target volume, whichever is
|
|
166
|
+
# lower (thresholding a grey design can cut its thin members off)
|
|
167
|
+
out["compliance"] = min(out["compliance_grey"], out["compliance_matched"])
|
|
168
|
+
if self.p.stress:
|
|
169
|
+
vm = self.von_mises(bin_matched)
|
|
170
|
+
vm[self.load_cells()] = 0.0
|
|
171
|
+
out["peak_von_mises"] = float(vm.max())
|
|
172
|
+
return out
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compliant-mechanism benchmarks: the textbook force inverter and a gripper,
|
|
3
|
+
run through the published reference code and loadpath's mechanism.py, and
|
|
4
|
+
scored by one independent evaluator.
|
|
5
|
+
|
|
6
|
+
python -m loadpath.benchmarks.mechanisms [--out benchmark-out/]
|
|
7
|
+
|
|
8
|
+
The reference is a line-for-line NumPy port of the compliant-mechanism
|
|
9
|
+
version of Sigmund's 99-line code, as printed in Bendsøe & Sigmund,
|
|
10
|
+
"Topology Optimization" (2003), sec. 2.6 / appendix: 4-node plane-stress
|
|
11
|
+
squares, SIMP p = 3 with x >= 0.001, the mesh-independency sensitivity
|
|
12
|
+
filter, and optimality criteria damped with exponent 0.3 and move 0.1. The
|
|
13
|
+
half inverter is its own example: 40 x 20 cells, volume 0.3, rmin 1.2,
|
|
14
|
+
springs of 0.1 at the input and the output, unit input force.
|
|
15
|
+
|
|
16
|
+
Every design is scored on the plane-stress model the reference uses
|
|
17
|
+
(benchmarks/evaluate.py's element) with the same springs: the output
|
|
18
|
+
displacement in the wanted direction, as delivered (penalized grey) and
|
|
19
|
+
thresholded black and white at the target volume. The black-and-white
|
|
20
|
+
reading is what a printed part does. loadpath's design comes from its 3D
|
|
21
|
+
optimizer on a one-cell slab (as the compliance benchmarks run), so the
|
|
22
|
+
comparison is of designs, not solvers.
|
|
23
|
+
|
|
24
|
+
Hinges: a mechanism optimum on a voxel grid likes one-node hinges (two
|
|
25
|
+
cells meeting at a corner), which carry no load and print as nothing.
|
|
26
|
+
`hinges` counts them in each black-and-white design.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import time
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
import scipy.sparse as sp
|
|
38
|
+
|
|
39
|
+
from .evaluate import _direct, _q4_ke, binarize
|
|
40
|
+
from .problems import _nodes
|
|
41
|
+
|
|
42
|
+
__all__ = ["MechProblem", "MECHANISMS", "MechEvaluator", "optimize_reference_mechanism", "run_loadpath_mechanism",
|
|
43
|
+
"hinges", "bench_mechanism"]
|
|
44
|
+
|
|
45
|
+
NU = 0.3
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class MechProblem:
|
|
50
|
+
name: str
|
|
51
|
+
title: str
|
|
52
|
+
source: str
|
|
53
|
+
dims: tuple # (nx, 1, nz) plane problems
|
|
54
|
+
volfrac: float
|
|
55
|
+
rmin: float
|
|
56
|
+
fixed: list # [(nodes, comps)] clamps (pinned solid in loadpath, as a study would)
|
|
57
|
+
sym: list # [(nodes, comps)] symmetry-line conditions (not pinned)
|
|
58
|
+
inp: tuple # (nodes, comp, force per node)
|
|
59
|
+
out: tuple # (nodes, comp, sign): the wanted motion is sign * u_comp
|
|
60
|
+
k_in: float = 0.1
|
|
61
|
+
k_out: float = 0.1
|
|
62
|
+
void: np.ndarray | None = None
|
|
63
|
+
notes: str = ""
|
|
64
|
+
extra: dict = field(default_factory=dict)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def inverter(nx=40, nz=20):
|
|
68
|
+
"""Half force inverter, Bendsøe & Sigmund (2003) sec. 2.6: the top edge is
|
|
69
|
+
the symmetry line; push right at its left end, and its right end should
|
|
70
|
+
move left. Clamped at the bottom left."""
|
|
71
|
+
return MechProblem(
|
|
72
|
+
name="inverter-2d", title=f"Force inverter, half model ({nx}x{nz})",
|
|
73
|
+
source="Bendsøe & Sigmund 2003, 'Topology Optimization', sec. 2.6 (Sigmund 1997); "
|
|
74
|
+
"the mechanism version of the 99-line code: nelx 40, nely 20, volfrac 0.3, penal 3, rmin 1.2, "
|
|
75
|
+
"springs 0.1",
|
|
76
|
+
dims=(nx, 1, nz), volfrac=0.3, rmin=1.2,
|
|
77
|
+
fixed=[(_nodes(0, 0, [0, 1]), "xz")],
|
|
78
|
+
sym=[(_nodes(range(nx + 1), 0, nz), "z")],
|
|
79
|
+
inp=(_nodes(0, 0, nz), "x", 1.0), out=(_nodes(nx, 0, nz), "x", -1.0))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def gripper(nx=60, nz=30, jaw=(10, 6)):
|
|
83
|
+
"""Half gripper after Sigmund 1997: the bottom edge is the symmetry line;
|
|
84
|
+
push right at its left end, and the jaw (the top of a cut-out at the
|
|
85
|
+
bottom right, where the workpiece sits) should close down onto it.
|
|
86
|
+
Clamped at the top left."""
|
|
87
|
+
jx, jz = jaw
|
|
88
|
+
void = np.zeros((nx, 1, nz), bool)
|
|
89
|
+
void[nx - jx:, :, :jz] = True
|
|
90
|
+
return MechProblem(
|
|
91
|
+
name="gripper-2d", title=f"Gripper, half model ({nx}x{nz}, {jx}x{jz} jaw gap)",
|
|
92
|
+
source="Sigmund 1997, 'On the design of compliant mechanisms using topology optimization' (layout); "
|
|
93
|
+
"reference design from the same 99-line mechanism code",
|
|
94
|
+
dims=(nx, 1, nz), volfrac=0.3, rmin=1.2,
|
|
95
|
+
fixed=[(_nodes(0, 0, [nz - 1, nz]), "xz")],
|
|
96
|
+
sym=[(_nodes(range(nx - jx + 1), 0, 0), "z")],
|
|
97
|
+
inp=(_nodes(0, 0, 0), "x", 1.0), out=(_nodes(range(nx - jx, nx + 1), 0, jz), "z", -1.0),
|
|
98
|
+
void=void)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
MECHANISMS = {p.name: p for p in (inverter(), gripper())}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class MechEvaluator:
|
|
105
|
+
"""top88's plane-stress element on the problem's grid, with the springs."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, p: MechProblem):
|
|
108
|
+
self.p = p
|
|
109
|
+
nx, _, nz = p.dims
|
|
110
|
+
|
|
111
|
+
def node(i, k):
|
|
112
|
+
return i * (nz + 1) + (nz - k)
|
|
113
|
+
ii, kk = np.meshgrid(np.arange(nx), np.arange(nz), indexing="ij")
|
|
114
|
+
ii, kk = ii.ravel(), kk.ravel()
|
|
115
|
+
nodes = np.stack([node(ii, kk), node(ii + 1, kk), node(ii + 1, kk + 1), node(ii, kk + 1)], axis=1)
|
|
116
|
+
self.edof = np.stack([2 * nodes, 2 * nodes + 1], axis=2).reshape(-1, 8)
|
|
117
|
+
self.ke = _q4_ke(NU)
|
|
118
|
+
self.n_dof = 2 * (nx + 1) * (nz + 1)
|
|
119
|
+
self.cell = (ii, np.zeros_like(ii), kk)
|
|
120
|
+
comp = {"x": 0, "z": 1}
|
|
121
|
+
self.dof = lambda n, c: 2 * node(n[:, 0], n[:, 2]) + comp[c]
|
|
122
|
+
fixed = np.zeros(self.n_dof, bool)
|
|
123
|
+
for nodes_, comps in p.fixed + p.sym:
|
|
124
|
+
for c in comps:
|
|
125
|
+
fixed[self.dof(nodes_, c)] = True
|
|
126
|
+
self.fixed = fixed
|
|
127
|
+
self.free = np.flatnonzero(~fixed)
|
|
128
|
+
n, c, v = p.inp
|
|
129
|
+
self.f = np.zeros(self.n_dof)
|
|
130
|
+
np.add.at(self.f, self.dof(n, c), v)
|
|
131
|
+
n, c, sgn = p.out
|
|
132
|
+
self.l = np.zeros(self.n_dof)
|
|
133
|
+
np.add.at(self.l, self.dof(n, c), sgn / len(n)) # mean wanted motion of the output nodes
|
|
134
|
+
self.springs = np.zeros(self.n_dof)
|
|
135
|
+
din = self.dof(p.inp[0], p.inp[1])
|
|
136
|
+
dout = self.dof(p.out[0], p.out[1])
|
|
137
|
+
np.add.at(self.springs, din, p.k_in / len(din))
|
|
138
|
+
np.add.at(self.springs, dout, p.k_out / len(dout))
|
|
139
|
+
self._rows = np.repeat(self.edof, 8, axis=1).ravel()
|
|
140
|
+
self._cols = np.tile(self.edof, (1, 8)).ravel()
|
|
141
|
+
self.active = np.ones(len(ii), bool) if p.void is None else ~p.void[self.cell]
|
|
142
|
+
|
|
143
|
+
def cells(self, grid):
|
|
144
|
+
return np.asarray(grid, float)[self.cell]
|
|
145
|
+
|
|
146
|
+
def matrix(self, e):
|
|
147
|
+
data = (e[:, None, None] * self.ke[None]).ravel()
|
|
148
|
+
k = sp.csr_matrix((data, (self._rows, self._cols)), shape=(self.n_dof, self.n_dof))
|
|
149
|
+
return (k + sp.diags(self.springs))[self.free][:, self.free]
|
|
150
|
+
|
|
151
|
+
def solve(self, e, rhs):
|
|
152
|
+
u = np.zeros(self.n_dof)
|
|
153
|
+
u[self.free] = _direct(self.matrix(e), rhs[self.free])
|
|
154
|
+
return u
|
|
155
|
+
|
|
156
|
+
def output(self, grid, penal=3.0, emin=1e-9):
|
|
157
|
+
x = self.cells(grid)
|
|
158
|
+
u = self.solve(emin + x ** penal * (1 - emin), self.f)
|
|
159
|
+
return float(self.l @ u)
|
|
160
|
+
|
|
161
|
+
def peak_stress(self, grid, layers=2) -> float:
|
|
162
|
+
"""Peak von Mises (solid cells of a black-and-white design, under the
|
|
163
|
+
input force with the springs), leaving out cells within `layers` of a
|
|
164
|
+
loaded, sprung or clamped node. Thin hinges show up here: they carry
|
|
165
|
+
the whole mechanism's bending in a cell or two."""
|
|
166
|
+
x = self.cells(grid)
|
|
167
|
+
u = self.solve(1e-9 + x ** 3 * (1 - 1e-9), self.f)
|
|
168
|
+
b = 0.5 * np.array([[-1, 0, 1, 0, 1, 0, -1, 0],
|
|
169
|
+
[0, -1, 0, -1, 0, 1, 0, 1],
|
|
170
|
+
[-1, -1, -1, 1, 1, 1, 1, -1]], float)
|
|
171
|
+
d = 1 / (1 - NU ** 2) * np.array([[1, NU, 0], [NU, 1, 0], [0, 0, (1 - NU) / 2]])
|
|
172
|
+
st = u[self.edof] @ (d @ b).T
|
|
173
|
+
vm = np.sqrt(st[:, 0] ** 2 + st[:, 1] ** 2 - st[:, 0] * st[:, 1] + 3 * st[:, 2] ** 2)
|
|
174
|
+
vm = np.where(x > 0.5, vm, 0.0)
|
|
175
|
+
p = self.p
|
|
176
|
+
nodes = np.concatenate([p.inp[0], p.out[0]] + [n for n, _ in p.fixed])
|
|
177
|
+
ijk = np.stack(self.cell, axis=1)
|
|
178
|
+
for n in nodes:
|
|
179
|
+
vm[np.all(np.abs(ijk + 0.5 - n) <= layers, axis=1)] = 0.0
|
|
180
|
+
return float(vm.max())
|
|
181
|
+
|
|
182
|
+
def score(self, grid) -> dict:
|
|
183
|
+
x = self.cells(grid)
|
|
184
|
+
act = self.active
|
|
185
|
+
out = {"volume": float(x[act].mean()), "greyness": float((4 * x * (1 - x))[act].mean()),
|
|
186
|
+
"output_grey": self.output(grid)}
|
|
187
|
+
b = np.zeros(self.p.dims)
|
|
188
|
+
b[self.cell] = binarize(x, act, self.p.volfrac * x.size / act.sum())
|
|
189
|
+
out["output_bw"] = self.output(b)
|
|
190
|
+
out["hinges"] = hinges(b)
|
|
191
|
+
out["peak_stress_bw"] = self.peak_stress(b)
|
|
192
|
+
out["_bw"] = b
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def hinges(grid) -> int:
|
|
197
|
+
"""One-node hinges in a black-and-white plane design: 2x2 windows with
|
|
198
|
+
exactly two solid cells on a diagonal."""
|
|
199
|
+
g = np.asarray(grid)[:, 0, :] > 0.5
|
|
200
|
+
a, b, c, d = g[:-1, :-1], g[1:, :-1], g[:-1, 1:], g[1:, 1:]
|
|
201
|
+
return int(((a & d & ~b & ~c) | (b & c & ~a & ~d)).sum())
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _check(nx, nz, rmin, x, dc):
|
|
205
|
+
"""The 99-line code's mesh-independency filter, with x and dc on the
|
|
206
|
+
(nx, nz) grid."""
|
|
207
|
+
out = np.zeros_like(dc)
|
|
208
|
+
r = int(np.floor(rmin))
|
|
209
|
+
for i in range(nx):
|
|
210
|
+
for k in range(nz):
|
|
211
|
+
s = 0.0
|
|
212
|
+
for i2 in range(max(i - r, 0), min(i + r + 1, nx)):
|
|
213
|
+
for k2 in range(max(k - r, 0), min(k + r + 1, nz)):
|
|
214
|
+
fac = rmin - np.hypot(i - i2, k - k2)
|
|
215
|
+
if fac > 0:
|
|
216
|
+
s += fac
|
|
217
|
+
out[i, k] += fac * x[i2, k2] * dc[i2, k2]
|
|
218
|
+
out[i, k] /= x[i, k] * s
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def optimize_reference_mechanism(p: MechProblem, maxit=300, on_iter=None) -> dict:
|
|
223
|
+
"""The 99-line code's mechanism version: minimize the output dof's
|
|
224
|
+
displacement (minus the wanted motion), OC
|
|
225
|
+
with sensitivities clipped positive and damping 0.3, move 0.1. (Its
|
|
226
|
+
dummy load F(dout, 2) = -1 points the way an inverter's output should
|
|
227
|
+
move, so with c = u_out the adjoint is that load's solution.)"""
|
|
228
|
+
ev = MechEvaluator(p)
|
|
229
|
+
nx, _, nz = p.dims
|
|
230
|
+
penal, move = 3.0, 0.1
|
|
231
|
+
act = ev.active.reshape(nx, nz)
|
|
232
|
+
x = np.where(act, p.volfrac * nx * nz / act.sum(), 0.001)
|
|
233
|
+
t0 = time.time()
|
|
234
|
+
loop, change = 0, 1.0
|
|
235
|
+
while change > 0.01 and loop < maxit:
|
|
236
|
+
loop += 1
|
|
237
|
+
xe = x.ravel()
|
|
238
|
+
e = xe ** penal
|
|
239
|
+
u1 = ev.solve(e, ev.f)
|
|
240
|
+
u2 = ev.solve(e, ev.l) # the book's dummy load: -1 on the output dof, i.e. along the wanted motion
|
|
241
|
+
c = float(-ev.l @ u1) # minimized: minus the wanted motion
|
|
242
|
+
# d c / d x = p x^(p-1) u2' ke u1 (the book's sign convention)
|
|
243
|
+
dc = penal * xe ** (penal - 1) * np.einsum("ij,jk,ik->i", u2[ev.edof], ev.ke, u1[ev.edof])
|
|
244
|
+
dc = _check(nx, nz, p.rmin, x, dc.reshape(nx, nz))
|
|
245
|
+
l1, l2 = 0.0, 1e5
|
|
246
|
+
# MATLAB's 0/0 is NaN, which ends the bisection when every move
|
|
247
|
+
# shrinks the volume (early on, few cells can help the output)
|
|
248
|
+
while l1 + l2 > 0 and (l2 - l1) / (l1 + l2) > 1e-4:
|
|
249
|
+
lmid = 0.5 * (l2 + l1)
|
|
250
|
+
xnew = np.maximum(0.001, np.maximum(x - move, np.minimum(1.0, np.minimum(
|
|
251
|
+
x + move, x * np.maximum(1e-10, -dc / lmid) ** 0.3))))
|
|
252
|
+
xnew[~act] = 0.001
|
|
253
|
+
if xnew.sum() - p.volfrac * nx * nz > 0:
|
|
254
|
+
l1 = lmid
|
|
255
|
+
else:
|
|
256
|
+
l2 = lmid
|
|
257
|
+
change = float(np.abs(xnew - x).max())
|
|
258
|
+
x = xnew
|
|
259
|
+
if on_iter is not None:
|
|
260
|
+
on_iter(loop, -c, change)
|
|
261
|
+
grid = np.zeros(p.dims)
|
|
262
|
+
grid[:, 0, :] = x
|
|
263
|
+
return dict(x=grid, iterations=loop, output_reported=-c, seconds=time.time() - t0, solver="sigmund-mech")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _box(nodes):
|
|
267
|
+
lo = nodes.min(axis=0) - 0.25
|
|
268
|
+
hi = nodes.max(axis=0) + 0.25
|
|
269
|
+
lo[1], hi[1] = -0.25, 1.25
|
|
270
|
+
return {"type": "box", "min": lo.tolist(), "max": hi.tolist()}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _slab(nodes):
|
|
274
|
+
other = nodes.copy()
|
|
275
|
+
other[:, 1] = 1
|
|
276
|
+
return np.concatenate([nodes, other])
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def mech_domain(p: MechProblem):
|
|
280
|
+
"""loadpath's Domain for a mechanism benchmark, built as a study would
|
|
281
|
+
(clamp, input and output regions pinned solid; the jaw gap a keep-out),
|
|
282
|
+
then with supports and loads set exactly to the problem's. The input
|
|
283
|
+
case carries the force, the output case the unit wanted direction."""
|
|
284
|
+
from ..domain import build_domain
|
|
285
|
+
from ..spec import study_from_dict
|
|
286
|
+
|
|
287
|
+
nx, ny, nz = p.dims
|
|
288
|
+
n_in, c_in, v_in = p.inp
|
|
289
|
+
n_out, c_out, sgn = p.out
|
|
290
|
+
d = {
|
|
291
|
+
"designSpace": [{"type": "box", "min": [0, 0, 0], "max": [nx, ny, nz]}],
|
|
292
|
+
"supports": [{"shape": _box(n), "fix": "".join(sorted(c))} for n, c in p.fixed],
|
|
293
|
+
"loadCases": [
|
|
294
|
+
{"name": "in", "loads": [{"shape": _box(n_in), "force": [v_in * len(n_in) if a == c_in else 0.0
|
|
295
|
+
for a in "xyz"]}]},
|
|
296
|
+
{"name": "out", "loads": [{"shape": _box(n_out), "force": [sgn if a == c_out else 0.0 for a in "xyz"]}]}],
|
|
297
|
+
"material": {"name": "unit", "E": 1.0, "nu": NU, "strength": 1.0, "density": 1.0},
|
|
298
|
+
"goal": {"volumeFraction": p.volfrac},
|
|
299
|
+
"resolution": {"pitch": 1.0},
|
|
300
|
+
"extrusion": "y",
|
|
301
|
+
}
|
|
302
|
+
if p.void is not None:
|
|
303
|
+
ii, _, kk = np.nonzero(p.void)
|
|
304
|
+
d["keepOut"] = [{"id": "gap", "shape": {"type": "box", "min": [float(ii.min()), -1, float(kk.min()) - 1],
|
|
305
|
+
"max": [float(ii.max() + 1) + 1, ny + 1, float(kk.max() + 1)]},
|
|
306
|
+
"bore": False}]
|
|
307
|
+
dom = build_domain(study_from_dict(d), pitch=1.0)
|
|
308
|
+
if tuple(dom.dims) != tuple(p.dims) or np.abs(dom.origin).max() > 1e-9:
|
|
309
|
+
raise RuntimeError(f"{p.name}: loadpath grid {dom.dims} at {dom.origin} does not match {p.dims}")
|
|
310
|
+
mesh = dom.mesh
|
|
311
|
+
fixed = np.zeros(mesh.n_dof, bool)
|
|
312
|
+
for nodes, comps in p.fixed + p.sym:
|
|
313
|
+
ids = mesh.node_id(_slab(nodes))
|
|
314
|
+
for c in comps:
|
|
315
|
+
fixed[3 * ids + "xyz".index(c)] = True
|
|
316
|
+
fixed[3 * np.flatnonzero(mesh.node_ijk[:, 1] == 0) + 1] = True # half of a plate symmetric about y = 0
|
|
317
|
+
f = np.zeros(mesh.n_dof)
|
|
318
|
+
np.add.at(f, 3 * mesh.node_id(_slab(n_in)) + "xyz".index(c_in), v_in * len(n_in) / (2 * len(n_in)))
|
|
319
|
+
lo = np.zeros(mesh.n_dof)
|
|
320
|
+
np.add.at(lo, 3 * mesh.node_id(_slab(n_out)) + "xyz".index(c_out), sgn)
|
|
321
|
+
dom.fixed, dom.loads, dom.springs = fixed, [f, lo], None
|
|
322
|
+
return dom
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def run_loadpath_mechanism(p: MechProblem, filter_radius=None, log=print, **kw) -> dict:
|
|
326
|
+
from ..mechanism import MechanismOptions, optimize_mechanism
|
|
327
|
+
|
|
328
|
+
dom = mech_domain(p)
|
|
329
|
+
n, pres = dom.mesh.n_elem, int(dom.preserve.sum())
|
|
330
|
+
active = n # the jaw gap is a keep-out: not in the mesh
|
|
331
|
+
vf = max(0.01, (p.volfrac * np.prod(p.dims) - pres) / (active - pres))
|
|
332
|
+
opts = MechanismOptions(volume_fraction=vf, filter_radius=filter_radius or max(p.rmin, 1.5), modulus=1.0,
|
|
333
|
+
k_in=p.k_in, k_out=p.k_out, extrusion=1, **kw)
|
|
334
|
+
t0 = time.time()
|
|
335
|
+
r = optimize_mechanism(dom, NU, opts)
|
|
336
|
+
log(f" loadpath: {r.iterations} iterations, {time.time() - t0:.0f}s, output {r.output_displacement:.4g}")
|
|
337
|
+
g = np.zeros(dom.dims)
|
|
338
|
+
g[tuple(dom.mesh.elem_ijk.T)] = r.xphys
|
|
339
|
+
return dict(x=g, iterations=r.iterations, seconds=time.time() - t0, output_reported=r.output_displacement)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
VARIANTS = {
|
|
343
|
+
"loadpath r1.5": {"filter_radius": 1.5},
|
|
344
|
+
"loadpath r2": {"filter_radius": 2.0},
|
|
345
|
+
# eroded / blueprint / dilated: no one-node hinges
|
|
346
|
+
"loadpath robust": {"filter_radius": 2.0, "robust": 0.15, "max_iterations": 200},
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def bench_mechanism(p: MechProblem, log=print, variants=None) -> dict:
|
|
351
|
+
ev = MechEvaluator(p)
|
|
352
|
+
designs = {"sigmund-mech": optimize_reference_mechanism(p)}
|
|
353
|
+
log(f" reference: {designs['sigmund-mech']['iterations']} iterations")
|
|
354
|
+
for name, kw in (variants or VARIANTS).items():
|
|
355
|
+
designs[name] = run_loadpath_mechanism(p, log=log, **kw)
|
|
356
|
+
rows = []
|
|
357
|
+
for name, rec in designs.items():
|
|
358
|
+
s = ev.score(rec["x"])
|
|
359
|
+
rec["bw"] = s.pop("_bw")
|
|
360
|
+
rows.append({"design": name, "iterations": rec["iterations"], "seconds": round(rec["seconds"], 1),
|
|
361
|
+
"output_reported": rec["output_reported"], **s})
|
|
362
|
+
ref = rows[0]
|
|
363
|
+
for r in rows:
|
|
364
|
+
r["bw_over_reference_bw"] = r["output_bw"] / ref["output_bw"] if ref["output_bw"] else None
|
|
365
|
+
return {"problem": p.name, "title": p.title, "source": p.source, "dims": list(p.dims), "volfrac": p.volfrac,
|
|
366
|
+
"rows": rows, "_designs": designs}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def render(res: dict, path: str):
|
|
370
|
+
import matplotlib
|
|
371
|
+
matplotlib.use("Agg")
|
|
372
|
+
import matplotlib.pyplot as plt
|
|
373
|
+
|
|
374
|
+
designs = res["_designs"]
|
|
375
|
+
names = list(designs)
|
|
376
|
+
fig, axes = plt.subplots(2, len(names), figsize=(3.4 * len(names), 4.6), gridspec_kw={"hspace": 0.45})
|
|
377
|
+
for j, name in enumerate(names):
|
|
378
|
+
row = next(r for r in res["rows"] if r["design"] == name)
|
|
379
|
+
for i, (key, title) in enumerate((("x", f"{name}\ngrey: out {row['output_grey']:.3g}"),
|
|
380
|
+
("bw", f"black/white: out {row['output_bw']:.3g}\n"
|
|
381
|
+
f"peak stress {row['peak_stress_bw']:.2g}, "
|
|
382
|
+
f"{row['hinges']} corner hinges"))):
|
|
383
|
+
ax = axes[i, j]
|
|
384
|
+
ax.imshow(1 - designs[name][key][:, 0, :].T, cmap="gray", origin="lower", vmin=0, vmax=1)
|
|
385
|
+
ax.set_title(title, fontsize=8)
|
|
386
|
+
ax.set_xticks([])
|
|
387
|
+
ax.set_yticks([])
|
|
388
|
+
fig.suptitle(res["title"], fontsize=10)
|
|
389
|
+
fig.tight_layout()
|
|
390
|
+
fig.savefig(path, dpi=130)
|
|
391
|
+
plt.close(fig)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def main(argv=None):
|
|
395
|
+
ap = argparse.ArgumentParser()
|
|
396
|
+
ap.add_argument("--out", default="benchmark-out")
|
|
397
|
+
ap.add_argument("--problems", nargs="*", default=list(MECHANISMS))
|
|
398
|
+
a = ap.parse_args(argv)
|
|
399
|
+
os.makedirs(a.out, exist_ok=True)
|
|
400
|
+
summary = []
|
|
401
|
+
for name in a.problems:
|
|
402
|
+
p = MECHANISMS[name]
|
|
403
|
+
print(p.title, flush=True)
|
|
404
|
+
res = bench_mechanism(p)
|
|
405
|
+
render(res, os.path.join(a.out, f"{p.name}.png"))
|
|
406
|
+
for r in res["rows"]:
|
|
407
|
+
print(f" {r['design']:16s} grey {r['output_grey']:.4g} bw {r['output_bw']:.4g} "
|
|
408
|
+
f"hinges {r['hinges']} peak vm {r['peak_stress_bw']:.3g} vol {r['volume']:.3f} "
|
|
409
|
+
f"grey {r['greyness']:.3f}", flush=True)
|
|
410
|
+
summary.append({k: v for k, v in res.items() if not k.startswith("_")})
|
|
411
|
+
with open(os.path.join(a.out, "mechanisms.json"), "w") as fh:
|
|
412
|
+
json.dump(summary, fh, indent=1, default=float)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
if __name__ == "__main__":
|
|
416
|
+
main()
|