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,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The benchmark problems: the textbook topology-optimization cases, each
|
|
3
|
+
defined once on a unit voxel grid so the same boundary conditions drive the
|
|
4
|
+
published reference code (top88, top3d, run in Octave), loadpath, and the
|
|
5
|
+
evaluator that scores every design.
|
|
6
|
+
|
|
7
|
+
Grid convention is loadpath's: cell (i, j, k) along x, y, z with z up, unit
|
|
8
|
+
cells, unit Young's modulus, nu = 0.3. A plane (2D) problem has one cell in y
|
|
9
|
+
and is solved in plane stress by the reference and the evaluator; loadpath
|
|
10
|
+
optimizes it as a one-cell slab extruded along y.
|
|
11
|
+
|
|
12
|
+
Boundary conditions are node sets in grid coordinates:
|
|
13
|
+
fixed: [(nodes (N, 3) int, "xz")] components held at zero
|
|
14
|
+
loads: [(nodes (N, 3) int, "z", value)] force per node
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
__all__ = ["Problem", "PROBLEMS", "get"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _nodes(i, j, k):
|
|
26
|
+
"""Every node (i, j, k) of the given index ranges (ints or iterables)."""
|
|
27
|
+
ii, jj, kk = np.meshgrid(np.atleast_1d(i), np.atleast_1d(j), np.atleast_1d(k), indexing="ij")
|
|
28
|
+
return np.stack([ii.ravel(), jj.ravel(), kk.ravel()], axis=1).astype(np.int64)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Problem:
|
|
33
|
+
name: str
|
|
34
|
+
title: str
|
|
35
|
+
source: str
|
|
36
|
+
dims: tuple # cells (nx, ny, nz)
|
|
37
|
+
volfrac: float # of the whole box, as top88/top3d count it
|
|
38
|
+
rmin: float # filter radius, cells
|
|
39
|
+
fixed: list
|
|
40
|
+
loads: list
|
|
41
|
+
references: list # reference solver ids (reference.SOLVERS)
|
|
42
|
+
plane: bool = False
|
|
43
|
+
void: np.ndarray | None = None # bool (nx, ny, nz): passive void cells
|
|
44
|
+
# The reference model is the half of a symmetric part cut at x = 0 (the
|
|
45
|
+
# half MBB beam). loadpath models the whole part with mirror symmetry, as
|
|
46
|
+
# a study would; `full` is that problem and the delivered design's
|
|
47
|
+
# x >= nx half is the one scored.
|
|
48
|
+
full: "Problem | None" = None
|
|
49
|
+
symmetry: list = field(default_factory=list)
|
|
50
|
+
# Also score peak von Mises and run loadpath's stress-constrained
|
|
51
|
+
# minimum-mass formulation against the reference's peak.
|
|
52
|
+
stress: bool = False
|
|
53
|
+
notes: str = ""
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def active(self) -> np.ndarray:
|
|
57
|
+
a = np.ones(self.dims, bool)
|
|
58
|
+
if self.void is not None:
|
|
59
|
+
a &= ~self.void
|
|
60
|
+
return a
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def active_volfrac(self) -> float:
|
|
64
|
+
"""The same amount of material as a share of the cells that exist."""
|
|
65
|
+
return self.volfrac * float(np.prod(self.dims)) / float(self.active.sum())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def mbb_2d(nx=150, nz=50, volfrac=0.5, rmin=2.0):
|
|
69
|
+
"""Half MBB beam exactly as top88 ships it: symmetry line on the left,
|
|
70
|
+
roller at the bottom right, unit load down at the top left."""
|
|
71
|
+
half = Problem(
|
|
72
|
+
name="mbb-2d", title=f"MBB beam, 2D half model ({nx}x{nz})",
|
|
73
|
+
source="Andreassen et al. 2011, 'Efficient topology optimization in MATLAB using 88 lines of code' "
|
|
74
|
+
"(top88.m, default load case)",
|
|
75
|
+
dims=(nx, 1, nz), volfrac=volfrac, rmin=rmin, plane=True,
|
|
76
|
+
fixed=[(_nodes(0, 0, range(nz + 1)), "x"), (_nodes(nx, 0, 0), "z")],
|
|
77
|
+
loads=[(_nodes(0, 0, nz), "z", -1.0)],
|
|
78
|
+
references=["top88-ft1", "top88-ft2"])
|
|
79
|
+
# whole beam: x in [0, 2nx], the half above sits at x >= nx
|
|
80
|
+
half.full = Problem(
|
|
81
|
+
name="mbb-2d-full", title="MBB beam, whole", source=half.source,
|
|
82
|
+
dims=(2 * nx, 1, nz), volfrac=volfrac, rmin=rmin, plane=True,
|
|
83
|
+
fixed=[(_nodes(0, 0, 0), "xz"), (_nodes(2 * nx, 0, 0), "z")],
|
|
84
|
+
loads=[(_nodes(nx, 0, nz), "z", -2.0)],
|
|
85
|
+
references=[], symmetry=["x"])
|
|
86
|
+
return half
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def lbracket_2d(n=100, cut=0.6, volfrac_active=0.4, rmin=2.0):
|
|
90
|
+
"""L-bracket: the 2D stress benchmark (Le et al. 2010 and most stress
|
|
91
|
+
papers since). Clamped along the top edge, loaded down on the top of the
|
|
92
|
+
free arm's tip, the re-entrant corner is where compliance designs
|
|
93
|
+
concentrate stress."""
|
|
94
|
+
c = int(round(n * (1 - cut))) # arm width, cells
|
|
95
|
+
void = np.zeros((n, 1, n), bool)
|
|
96
|
+
void[c:, :, c:] = True
|
|
97
|
+
tip = 6
|
|
98
|
+
active = n * n - (n - c) ** 2
|
|
99
|
+
return Problem(
|
|
100
|
+
name="lbracket-2d", title=f"L-bracket, 2D ({n}x{n}, {n - c}x{n - c} cut out)",
|
|
101
|
+
source="Le, Norato, Bruns, Ha & Tortorelli 2010, 'Stress-based topology optimization for continua' "
|
|
102
|
+
"(geometry); reference design from top88.m with passive voids (Andreassen et al. 2011, sec. 5.2)",
|
|
103
|
+
dims=(n, 1, n), volfrac=volfrac_active * active / (n * n), rmin=rmin, plane=True, void=void,
|
|
104
|
+
fixed=[(_nodes(range(c + 1), 0, n), "xz")],
|
|
105
|
+
loads=[(_nodes(n, 0, range(c - tip + 1, c + 1)), "z", -1.0 / tip)],
|
|
106
|
+
references=["top88-ft2"], stress=True)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cantilever_3d(nx=60, ny=4, nz=20, volfrac=0.3, rmin=1.5):
|
|
110
|
+
"""top3d's default problem: clamped at x = 0, a unit load per node down
|
|
111
|
+
along the bottom edge of the free end."""
|
|
112
|
+
return Problem(
|
|
113
|
+
name="cantilever-3d", title=f"Cantilever beam, 3D ({nx}x{nz}x{ny})",
|
|
114
|
+
source="Liu & Tovar 2014, 'An efficient 3D topology optimization code written in Matlab' "
|
|
115
|
+
"(top3d.m, default load case, top3d(60,20,4,0.3,3,1.5))",
|
|
116
|
+
dims=(nx, ny, nz), volfrac=volfrac, rmin=rmin,
|
|
117
|
+
fixed=[(_nodes(0, range(ny + 1), range(nz + 1)), "xyz")],
|
|
118
|
+
loads=[(_nodes(nx, range(ny + 1), 0), "z", -1.0)],
|
|
119
|
+
references=["top3d"])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def bridge_3d(nx=48, ny=12, nz=16, volfrac=0.15, rmin=1.5):
|
|
123
|
+
"""A deck bridge: pinned along the bottom edges at both ends, a uniform
|
|
124
|
+
load over the whole top face."""
|
|
125
|
+
top = _nodes(range(nx + 1), range(ny + 1), nz)
|
|
126
|
+
return Problem(
|
|
127
|
+
name="bridge-3d", title=f"Bridge, 3D ({nx}x{ny}x{nz}, deck load)",
|
|
128
|
+
source="Classic 3D bridge benchmark (e.g. Aage et al. 2017 giga-voxel bridge, coarse); "
|
|
129
|
+
"reference design from top3d.m with these supports and loads",
|
|
130
|
+
dims=(nx, ny, nz), volfrac=volfrac, rmin=rmin,
|
|
131
|
+
fixed=[(_nodes([0, nx], range(ny + 1), 0), "xyz")],
|
|
132
|
+
loads=[(top, "z", -100.0 / len(top))],
|
|
133
|
+
references=["top3d"], symmetry=["x", "y"])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
PROBLEMS = {p.name: p for p in (mbb_2d(), lbracket_2d(), cantilever_3d(), bridge_3d())}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get(name: str) -> Problem:
|
|
140
|
+
return PROBLEMS[name]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The reference optimizers: line-for-line NumPy ports of the published
|
|
3
|
+
educational codes, run on each benchmark's own supports and loads.
|
|
4
|
+
|
|
5
|
+
- top88: Andreassen, Clausen, Schevenels, Lazarov & Sigmund 2011, "Efficient
|
|
6
|
+
topology optimization in MATLAB using 88 lines of code". 2D, SIMP p = 3,
|
|
7
|
+
Emin = 1e-9, OC with move 0.2, sensitivity filter (ft=1) or density filter
|
|
8
|
+
(ft=2), stops when no density moves more than 0.01. Passive void cells as in
|
|
9
|
+
the paper's section 5.2.
|
|
10
|
+
- top3d: Liu & Tovar 2014, "An efficient 3D topology optimization code
|
|
11
|
+
written in Matlab". The same scheme in 3D with the density filter and at
|
|
12
|
+
most 200 iterations.
|
|
13
|
+
|
|
14
|
+
What is ported: the filter (truncated cone, normalized by its row sums),
|
|
15
|
+
the sensitivities, the OC bisection (l2 = 1e9, tolerance 1e-3 relative), the
|
|
16
|
+
volume measured on physical densities and the stopping rule. The finite
|
|
17
|
+
elements are the evaluator's, which are the same elements (top88's 4-node
|
|
18
|
+
plane-stress square, top3d's 8-node brick).
|
|
19
|
+
|
|
20
|
+
The designs are stored under reference/ as grids in loadpath's layout, so the
|
|
21
|
+
suite only re-runs a reference when asked to.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import scipy.sparse as sp
|
|
30
|
+
|
|
31
|
+
from .evaluate import E_VOID, Evaluator
|
|
32
|
+
from .problems import Problem
|
|
33
|
+
|
|
34
|
+
__all__ = ["SOLVERS", "run_reference", "load_reference", "reference_path"]
|
|
35
|
+
|
|
36
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
37
|
+
SOLVERS = {
|
|
38
|
+
"top88-ft1": "top88, sensitivity filter (Andreassen et al. 2011)",
|
|
39
|
+
"top88-ft2": "top88, density filter (Andreassen et al. 2011)",
|
|
40
|
+
"top3d": "top3d, density filter (Liu & Tovar 2014)",
|
|
41
|
+
}
|
|
42
|
+
# top88 itself has no iteration cap; this one only keeps a chattering run
|
|
43
|
+
# bounded. top3d ships with 200.
|
|
44
|
+
TOP88_MAXIT = 1000
|
|
45
|
+
TOP3D_MAXIT = 200
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _filter(ev: Evaluator, rmin: float):
|
|
49
|
+
"""top88's H and Hs: weights rmin - distance to every cell within
|
|
50
|
+
ceil(rmin) - 1 cells per axis, normalized by the row sum."""
|
|
51
|
+
ijk = np.stack(ev.cell, axis=1)
|
|
52
|
+
dims = np.asarray(ev.p.dims)
|
|
53
|
+
lut = np.full(ev.p.dims, -1, np.int64)
|
|
54
|
+
lut[tuple(ijk.T)] = np.arange(len(ijk))
|
|
55
|
+
reach = int(np.ceil(rmin)) - 1
|
|
56
|
+
span = [range(-reach, reach + 1) if d > 1 else range(1) for d in dims]
|
|
57
|
+
rows, cols, vals = [], [], []
|
|
58
|
+
for dx in span[0]:
|
|
59
|
+
for dy in span[1]:
|
|
60
|
+
for dz in span[2]:
|
|
61
|
+
w = rmin - np.sqrt(dx * dx + dy * dy + dz * dz)
|
|
62
|
+
if w <= 0:
|
|
63
|
+
continue
|
|
64
|
+
nb = ijk + (dx, dy, dz)
|
|
65
|
+
ok = np.all((nb >= 0) & (nb < dims), axis=1)
|
|
66
|
+
rows.append(np.flatnonzero(ok))
|
|
67
|
+
cols.append(lut[tuple(nb[ok].T)])
|
|
68
|
+
vals.append(np.full(int(ok.sum()), w))
|
|
69
|
+
n = len(ijk)
|
|
70
|
+
h = sp.csr_matrix((np.concatenate(vals), (np.concatenate(rows), np.concatenate(cols))), shape=(n, n))
|
|
71
|
+
return h, np.asarray(h.sum(axis=1)).ravel()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def optimize_reference(p: Problem, solver: str, on_iter=None) -> dict:
|
|
75
|
+
ev = Evaluator(p)
|
|
76
|
+
ft = 1 if solver == "top88-ft1" else 2
|
|
77
|
+
maxit = TOP3D_MAXIT if solver == "top3d" else TOP88_MAXIT
|
|
78
|
+
penal, move = 3.0, 0.2
|
|
79
|
+
h, hs = _filter(ev, p.rmin)
|
|
80
|
+
passive = ~ev.active
|
|
81
|
+
n = len(hs)
|
|
82
|
+
x = np.full(n, p.volfrac)
|
|
83
|
+
x[passive] = 0.0
|
|
84
|
+
xphys = x.copy()
|
|
85
|
+
t0 = time.time()
|
|
86
|
+
change, loop, c = 1.0, 0, 0.0
|
|
87
|
+
while change > 0.01 and loop < maxit:
|
|
88
|
+
loop += 1
|
|
89
|
+
u = ev.solve(E_VOID + xphys ** penal * (1 - E_VOID))
|
|
90
|
+
ue = u[ev.edof]
|
|
91
|
+
ce = np.einsum("ij,jk,ik->i", ue, ev.ke, ue)
|
|
92
|
+
c = float(np.sum((E_VOID + xphys ** penal * (1 - E_VOID)) * ce))
|
|
93
|
+
dc = -penal * (1 - E_VOID) * xphys ** (penal - 1) * ce
|
|
94
|
+
dv = np.ones(n)
|
|
95
|
+
if ft == 1:
|
|
96
|
+
dc = (h @ (x * dc)) / hs / np.maximum(1e-3, x)
|
|
97
|
+
else:
|
|
98
|
+
dc = h @ (dc / hs)
|
|
99
|
+
dv = h @ (dv / hs)
|
|
100
|
+
l1, l2 = 0.0, 1e9
|
|
101
|
+
while (l2 - l1) / (l1 + l2) > 1e-3:
|
|
102
|
+
lmid = 0.5 * (l2 + l1)
|
|
103
|
+
xnew = np.maximum(0, np.maximum(x - move, np.minimum(1, np.minimum(x + move, x * np.sqrt(-dc / dv / lmid)))))
|
|
104
|
+
xnew[passive] = 0.0
|
|
105
|
+
xphys = xnew if ft == 1 else (h @ xnew) / hs
|
|
106
|
+
xphys = np.where(passive, 0.0, xphys)
|
|
107
|
+
if xphys.sum() > p.volfrac * n:
|
|
108
|
+
l1 = lmid
|
|
109
|
+
else:
|
|
110
|
+
l2 = lmid
|
|
111
|
+
change = float(np.abs(xnew - x).max())
|
|
112
|
+
x = xnew
|
|
113
|
+
if on_iter is not None:
|
|
114
|
+
on_iter(loop, c, change)
|
|
115
|
+
grid = np.zeros(p.dims)
|
|
116
|
+
grid[ev.cell] = xphys
|
|
117
|
+
return dict(x=grid, iterations=loop, compliance_reported=c, seconds=time.time() - t0, solver=solver)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def reference_path(p: Problem, solver: str) -> str:
|
|
121
|
+
return os.path.join(HERE, "reference", f"{p.name}.{solver}.npz")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run_reference(p: Problem, solver: str, on_iter=None) -> dict:
|
|
125
|
+
rec = optimize_reference(p, solver, on_iter)
|
|
126
|
+
os.makedirs(os.path.dirname(reference_path(p, solver)), exist_ok=True)
|
|
127
|
+
np.savez_compressed(reference_path(p, solver), x=rec["x"].astype(np.float32), iterations=rec["iterations"],
|
|
128
|
+
compliance_reported=rec["compliance_reported"], seconds=rec["seconds"])
|
|
129
|
+
return rec
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_reference(p: Problem, solver: str) -> dict | None:
|
|
133
|
+
path = reference_path(p, solver)
|
|
134
|
+
if not os.path.exists(path):
|
|
135
|
+
return None
|
|
136
|
+
z = np.load(path)
|
|
137
|
+
return dict(x=z["x"].astype(float), iterations=int(z["iterations"]),
|
|
138
|
+
compliance_reported=float(z["compliance_reported"]), seconds=float(z["seconds"]), solver=solver)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Benchmark suite: loadpath against the published topology-optimization codes
|
|
3
|
+
on the textbook problems (problems.py), scored by one evaluator
|
|
4
|
+
(evaluate.py). Separate from the GRIP evals: those ask "is this part good
|
|
5
|
+
enough to print", this asks "how close to the optimum does the optimizer get".
|
|
6
|
+
|
|
7
|
+
For each problem:
|
|
8
|
+
- the reference designs (top88 / top3d ports, reference.py), stored, rerun
|
|
9
|
+
with --refresh;
|
|
10
|
+
- loadpath's optimizer as a study would run it (robust erosion, Heaviside
|
|
11
|
+
projection, the padded filter for 3D, pruning, pinned load cells), and
|
|
12
|
+
"plain" with erosion, padding and pruning off: the same formulation as the
|
|
13
|
+
references, which separates what the solver does from what loadpath's
|
|
14
|
+
printability choices cost;
|
|
15
|
+
- stress problems also run loadpath's minimum-mass stress-constrained
|
|
16
|
+
formulation (constrained.py) with the reference design's peak as the limit.
|
|
17
|
+
|
|
18
|
+
Scores (evaluate.py): compliance as delivered (penalized, p = 3) and
|
|
19
|
+
black-and-white at the target volume; the headline is the lower of the two
|
|
20
|
+
(lower is stiffer). loadpath's volume fraction is lowered by its pinned load
|
|
21
|
+
and support cells so every design uses the same total material. Stress
|
|
22
|
+
problems add the peak von Mises of the black-and-white design. The ratio
|
|
23
|
+
column is each design over the best reference.
|
|
24
|
+
|
|
25
|
+
Run: python -m loadpath.benchmarks.run [problem ...] --out bench-out/
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from ..constrained import Limits, optimize_constrained
|
|
38
|
+
from ..domain import build_domain
|
|
39
|
+
from ..simp import SimpOptions, optimize
|
|
40
|
+
from ..spec import study_from_dict
|
|
41
|
+
from .evaluate import Evaluator, binarize
|
|
42
|
+
from .problems import PROBLEMS, Problem
|
|
43
|
+
from .reference import SOLVERS, load_reference, run_reference
|
|
44
|
+
|
|
45
|
+
NU = 0.3
|
|
46
|
+
VARIANTS = {
|
|
47
|
+
"loadpath": {},
|
|
48
|
+
"loadpath-plain": {"erode": 0.0, "prune": False, "pad_filter": False},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _box(nodes, plane):
|
|
53
|
+
lo = nodes.min(axis=0) - 0.25
|
|
54
|
+
hi = nodes.max(axis=0) + 0.25
|
|
55
|
+
if plane:
|
|
56
|
+
lo[1], hi[1] = -0.25, 1.25
|
|
57
|
+
return {"type": "box", "min": lo.tolist(), "max": hi.tolist()}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _slab(nodes, plane):
|
|
61
|
+
"""A plane problem's node set on both faces of the one-cell slab."""
|
|
62
|
+
if not plane:
|
|
63
|
+
return nodes
|
|
64
|
+
other = nodes.copy()
|
|
65
|
+
other[:, 1] = 1
|
|
66
|
+
return np.concatenate([nodes, other])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def domain_for(p: Problem):
|
|
70
|
+
"""loadpath's Domain for a benchmark, built the way a study builds it (so
|
|
71
|
+
load and support cells are pinned and symmetry is mirrored as in real
|
|
72
|
+
use), with the supports and loads then set exactly to the problem's."""
|
|
73
|
+
nx, ny, nz = p.dims
|
|
74
|
+
d = {
|
|
75
|
+
"designSpace": [{"type": "box", "min": [0, 0, 0], "max": [nx, ny, nz]}],
|
|
76
|
+
"supports": [{"shape": _box(n, p.plane), "fix": "".join(sorted(c))} for n, c in p.fixed],
|
|
77
|
+
"loadCases": [{"name": "load", "loads": [
|
|
78
|
+
{"shape": _box(n, p.plane), "force": [v * len(n) if a == c else 0.0 for a in "xyz"]}
|
|
79
|
+
for n, c, v in p.loads]}],
|
|
80
|
+
"material": {"name": "unit", "E": 1.0, "nu": NU, "strength": 1.0, "density": 1.0},
|
|
81
|
+
"goal": {"volumeFraction": p.active_volfrac},
|
|
82
|
+
"resolution": {"pitch": 1.0},
|
|
83
|
+
}
|
|
84
|
+
if p.symmetry:
|
|
85
|
+
d["symmetry"] = list(p.symmetry)
|
|
86
|
+
if p.plane:
|
|
87
|
+
d["extrusion"] = "y"
|
|
88
|
+
if p.void is not None:
|
|
89
|
+
# the cut-out as keep-out boxes, one per void column run
|
|
90
|
+
ii, _, kk = np.nonzero(p.void)
|
|
91
|
+
d["keepOut"] = [{"id": "cut", "shape": {"type": "box", "min": [float(ii.min()), -1, float(kk.min())],
|
|
92
|
+
"max": [float(ii.max() + 1) + 1, ny + 1, float(kk.max() + 1) + 1]},
|
|
93
|
+
"bore": False}]
|
|
94
|
+
study = study_from_dict(d)
|
|
95
|
+
dom = build_domain(study, pitch=1.0)
|
|
96
|
+
if tuple(dom.dims) != tuple(p.dims) or np.abs(dom.origin).max() > 1e-9:
|
|
97
|
+
raise RuntimeError(f"{p.name}: loadpath grid {dom.dims} at {dom.origin} does not match {p.dims}")
|
|
98
|
+
mesh = dom.mesh
|
|
99
|
+
fixed = np.zeros(mesh.n_dof, bool)
|
|
100
|
+
f = np.zeros(mesh.n_dof)
|
|
101
|
+
for nodes, comps in p.fixed:
|
|
102
|
+
ids = mesh.node_id(_slab(nodes, p.plane))
|
|
103
|
+
for c in comps:
|
|
104
|
+
fixed[3 * ids + "xyz".index(c)] = True
|
|
105
|
+
for nodes, c, v in p.loads:
|
|
106
|
+
all_n = _slab(nodes, p.plane)
|
|
107
|
+
share = v * len(nodes) / len(all_n)
|
|
108
|
+
np.add.at(f, 3 * mesh.node_id(all_n) + "xyz".index(c), share)
|
|
109
|
+
if p.plane:
|
|
110
|
+
# the slab is half of a plate symmetric about y = 0: holds y there
|
|
111
|
+
fixed[3 * np.flatnonzero(mesh.node_ijk[:, 1] == 0) + 1] = True
|
|
112
|
+
dom.fixed, dom.loads, dom.springs = fixed, [f], None
|
|
113
|
+
return dom, study
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _grid(dom, values):
|
|
117
|
+
g = np.zeros(dom.dims)
|
|
118
|
+
g[tuple(dom.mesh.elem_ijk.T)] = values
|
|
119
|
+
return g
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _scored_grid(p: Problem, g):
|
|
123
|
+
"""The part of a loadpath design the problem scores (the half of a
|
|
124
|
+
mirrored whole)."""
|
|
125
|
+
if p.full is not None:
|
|
126
|
+
return g[p.dims[0]:]
|
|
127
|
+
return g
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def design_volfrac(q: Problem, dom) -> float:
|
|
131
|
+
"""loadpath's volume fraction counts design cells only; pinned (load,
|
|
132
|
+
support) cells come on top. This is the fraction of the rest that gives
|
|
133
|
+
the same total material as the references."""
|
|
134
|
+
n, pres = dom.mesh.n_elem, int(dom.preserve.sum())
|
|
135
|
+
return max(0.01, (q.active_volfrac * n - pres) / (n - pres))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_loadpath(p: Problem, variant: str, log=print):
|
|
139
|
+
q = p.full or p
|
|
140
|
+
dom, study = domain_for(q)
|
|
141
|
+
opts = SimpOptions(volume_fraction=design_volfrac(q, dom), filter_radius=q.rmin,
|
|
142
|
+
extrusion=1 if q.plane else None, **VARIANTS[variant])
|
|
143
|
+
t0 = time.time()
|
|
144
|
+
res = optimize(dom, NU, opts)
|
|
145
|
+
log(f" {variant}: {res.iterations} iterations, {time.time() - t0:.0f}s, c {res.compliance:.4g}")
|
|
146
|
+
return dict(x=_scored_grid(p, _grid(dom, res.xphys)), iterations=res.iterations, seconds=time.time() - t0,
|
|
147
|
+
compliance_reported=res.compliance)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def run_constrained(p: Problem, limit: float, log=print):
|
|
151
|
+
q = p.full or p
|
|
152
|
+
dom, _ = domain_for(q)
|
|
153
|
+
# Started from (nearly) solid, a feasible design, as Le et al. 2010 do.
|
|
154
|
+
# Since the fixes in constrained.py (q = 1, a guarded MMA step, the
|
|
155
|
+
# blueprint delivered) it also converges from the reference's volume.
|
|
156
|
+
opts = SimpOptions(volume_fraction=0.95, filter_radius=q.rmin,
|
|
157
|
+
extrusion=1 if q.plane else None, max_iterations=200)
|
|
158
|
+
t0 = time.time()
|
|
159
|
+
res = optimize_constrained(dom, NU, opts, Limits(stress=limit, deflection=None, modulus=1.0))
|
|
160
|
+
log(f" stress-constrained: {res.iterations} iterations, {time.time() - t0:.0f}s, vol {res.volume_fraction:.3f}")
|
|
161
|
+
return dict(x=_scored_grid(p, _grid(dom, res.xphys)), iterations=res.iterations, seconds=time.time() - t0,
|
|
162
|
+
compliance_reported=res.compliance)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def bench_problem(p: Problem, refresh=False, log=print) -> dict:
|
|
166
|
+
ev = Evaluator(p)
|
|
167
|
+
target = p.active_volfrac
|
|
168
|
+
designs, rows = {}, []
|
|
169
|
+
for solver in p.references:
|
|
170
|
+
rec = None if refresh else load_reference(p, solver)
|
|
171
|
+
if rec is None:
|
|
172
|
+
log(f" running {solver} ...")
|
|
173
|
+
rec = run_reference(p, solver)
|
|
174
|
+
designs[solver] = rec
|
|
175
|
+
for v in VARIANTS:
|
|
176
|
+
designs[v] = run_loadpath(p, v, log)
|
|
177
|
+
for name, rec in designs.items():
|
|
178
|
+
s = ev.score(rec["x"], target)
|
|
179
|
+
s.update(design=name, iterations=rec["iterations"], seconds=round(rec["seconds"], 1),
|
|
180
|
+
compliance_reported=rec["compliance_reported"])
|
|
181
|
+
rows.append(s)
|
|
182
|
+
refs = [r for r in rows if r["design"] in p.references]
|
|
183
|
+
best = min(refs, key=lambda r: r["compliance"])
|
|
184
|
+
for r in rows:
|
|
185
|
+
r["ratio"] = r["compliance"] / best["compliance"]
|
|
186
|
+
out = {"problem": p.name, "title": p.title, "source": p.source, "dims": list(p.dims),
|
|
187
|
+
"volfrac": target, "rmin": p.rmin, "rows": rows, "best_reference": best["design"]}
|
|
188
|
+
if p.stress:
|
|
189
|
+
limit = best["peak_von_mises"]
|
|
190
|
+
try:
|
|
191
|
+
rec = run_constrained(p, limit, log)
|
|
192
|
+
except Exception as exc: # noqa: BLE001 - a failing formulation is a result
|
|
193
|
+
log(f" stress-constrained failed: {type(exc).__name__}: {exc}")
|
|
194
|
+
out["stress"] = {"limit": limit, "reference_volume": target, "error": f"{type(exc).__name__}: {exc}"}
|
|
195
|
+
out["_designs"] = designs
|
|
196
|
+
return out
|
|
197
|
+
designs["loadpath-stress"] = rec
|
|
198
|
+
x = ev.cells(rec["x"])
|
|
199
|
+
vol = float(x[ev.active].mean())
|
|
200
|
+
b = np.zeros(p.dims)
|
|
201
|
+
b[ev.cell] = binarize(x, ev.active, vol)
|
|
202
|
+
vm = ev.von_mises(b)
|
|
203
|
+
vm[ev.load_cells()] = 0.0
|
|
204
|
+
out["stress"] = {"limit": limit, "reference_volume": target, "volume": vol,
|
|
205
|
+
"peak_von_mises": float(vm.max()), "compliance": ev.compliance(b),
|
|
206
|
+
"iterations": rec["iterations"], "seconds": round(rec["seconds"], 1)}
|
|
207
|
+
out["_designs"] = designs
|
|
208
|
+
return out
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _render(res: dict, path: str):
|
|
212
|
+
import matplotlib
|
|
213
|
+
matplotlib.use("Agg")
|
|
214
|
+
import matplotlib.pyplot as plt
|
|
215
|
+
|
|
216
|
+
designs = res["_designs"]
|
|
217
|
+
names = list(designs)
|
|
218
|
+
plane = res["dims"][1] == 1
|
|
219
|
+
fig = plt.figure(figsize=(3.6 * len(names), 2.6 if plane else 3.2))
|
|
220
|
+
for k, name in enumerate(names):
|
|
221
|
+
g = designs[name]["x"]
|
|
222
|
+
row = next((r for r in res["rows"] if r["design"] == name), None)
|
|
223
|
+
label = name if row is None else f"{name}\nc = {row['compliance']:.4g}"
|
|
224
|
+
if name == "loadpath-stress":
|
|
225
|
+
label = f"{name}\nvol {res['stress']['volume']:.3f}, peak {res['stress']['peak_von_mises']:.3g}"
|
|
226
|
+
if plane:
|
|
227
|
+
ax = fig.add_subplot(1, len(names), k + 1)
|
|
228
|
+
ax.imshow(1 - g[:, 0, :].T, cmap="gray", origin="lower", vmin=0, vmax=1, interpolation="nearest")
|
|
229
|
+
ax.set_axis_off()
|
|
230
|
+
else:
|
|
231
|
+
from scipy import ndimage
|
|
232
|
+
from skimage.measure import marching_cubes
|
|
233
|
+
ax = fig.add_subplot(1, len(names), k + 1, projection="3d")
|
|
234
|
+
# light smoothing so the voxel steps do not read as the design
|
|
235
|
+
pad = ndimage.gaussian_filter(np.pad(g, 1), 0.6)
|
|
236
|
+
v, f, _, _ = marching_cubes(pad, 0.5)
|
|
237
|
+
ax.plot_trisurf(v[:, 0], v[:, 1], f, v[:, 2], color="#c9c4bc", edgecolor="none", shade=True)
|
|
238
|
+
ax.set_box_aspect(np.array(g.shape) + 2)
|
|
239
|
+
ax.view_init(24, -58)
|
|
240
|
+
ax.set_axis_off()
|
|
241
|
+
ax.set_title(label, fontsize=9)
|
|
242
|
+
fig.tight_layout()
|
|
243
|
+
fig.savefig(path, dpi=110)
|
|
244
|
+
plt.close(fig)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def markdown(results: list) -> str:
|
|
248
|
+
lines = ["| problem | design | compliance | vs best reference | as delivered (p=3) | black/white at target "
|
|
249
|
+
"volume | volume | greyness | iterations | seconds |",
|
|
250
|
+
"|---|---|---|---|---|---|---|---|---|---|"]
|
|
251
|
+
for res in results:
|
|
252
|
+
for r in res["rows"]:
|
|
253
|
+
lines.append(f"| {res['problem']} | {r['design']} | {r['compliance']:.4g} | {r['ratio']:.3f} | "
|
|
254
|
+
f"{r['compliance_grey']:.4g} | {r['compliance_matched']:.4g} | "
|
|
255
|
+
f"{r['volume']:.3f} | {r['greyness']:.3f} | {r['iterations']} | {r['seconds']:.0f} |")
|
|
256
|
+
stress = [r for r in results if "stress" in r]
|
|
257
|
+
if stress:
|
|
258
|
+
lines += ["", "| problem | reference peak von Mises (limit) | reference volume | "
|
|
259
|
+
"loadpath stress-constrained volume | its peak von Mises |", "|---|---|---|---|---|"]
|
|
260
|
+
for res in stress:
|
|
261
|
+
s = res["stress"]
|
|
262
|
+
if "error" in s:
|
|
263
|
+
lines.append(f"| {res['problem']} | {s['limit']:.4g} | {s['reference_volume']:.3f} | "
|
|
264
|
+
f"failed: {s['error']} | |")
|
|
265
|
+
continue
|
|
266
|
+
lines.append(f"| {res['problem']} | {s['limit']:.4g} | {s['reference_volume']:.3f} | {s['volume']:.3f} | "
|
|
267
|
+
f"{s['peak_von_mises']:.4g} |")
|
|
268
|
+
return "\n".join(lines) + "\n"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main(argv=None):
|
|
272
|
+
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
273
|
+
ap.add_argument("problems", nargs="*", help=f"subset of {sorted(PROBLEMS)}")
|
|
274
|
+
ap.add_argument("--out", default="bench-out")
|
|
275
|
+
ap.add_argument("--refresh", action="store_true", help="rerun the reference codes")
|
|
276
|
+
ap.add_argument("--no-render", action="store_true")
|
|
277
|
+
a = ap.parse_args(argv)
|
|
278
|
+
names = a.problems or list(PROBLEMS)
|
|
279
|
+
os.makedirs(a.out, exist_ok=True)
|
|
280
|
+
results = []
|
|
281
|
+
for name in names:
|
|
282
|
+
p = PROBLEMS[name]
|
|
283
|
+
print(f"{name}: {p.title}", flush=True)
|
|
284
|
+
res = bench_problem(p, refresh=a.refresh, log=lambda s: print(s, flush=True))
|
|
285
|
+
np.savez_compressed(os.path.join(a.out, f"{name}.designs.npz"),
|
|
286
|
+
**{k: v["x"].astype(np.float32) for k, v in res["_designs"].items()})
|
|
287
|
+
if not a.no_render:
|
|
288
|
+
_render(res, os.path.join(a.out, f"{name}.png"))
|
|
289
|
+
results.append(res)
|
|
290
|
+
clean = [{k: v for k, v in r.items() if not k.startswith("_")} for r in results]
|
|
291
|
+
with open(os.path.join(a.out, "benchmarks.json"), "w") as fh:
|
|
292
|
+
json.dump(clean, fh, indent=1)
|
|
293
|
+
md = markdown(clean)
|
|
294
|
+
with open(os.path.join(a.out, "benchmarks.md"), "w") as fh:
|
|
295
|
+
fh.write(md)
|
|
296
|
+
print(md)
|
|
297
|
+
return 0
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
sys.exit(main())
|