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/mechanism_nl.py
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compliant mechanisms optimized on their large-deflection response.
|
|
3
|
+
|
|
4
|
+
mechanism.py designs on the linear model, which is right for small strokes
|
|
5
|
+
and wrong for a TPU finger that curls tens of degrees: members swing round,
|
|
6
|
+
the leverage changes as they do, and a linkage that looks best at the
|
|
7
|
+
start of the stroke can stall or reverse halfway (Pedersen, Buhl & Sigmund
|
|
8
|
+
2001, "Topology synthesis of large-displacement compliant mechanisms").
|
|
9
|
+
Here every design iteration solves the geometrically nonlinear problem
|
|
10
|
+
(total Lagrangian hex8, neo-Hookean, nonlinear.py's element) with the input
|
|
11
|
+
driven through its full stroke, and the sensitivity comes from one adjoint
|
|
12
|
+
solve on the converged tangent:
|
|
13
|
+
|
|
14
|
+
maximize J = l . u(stroke) (output travel against k_out)
|
|
15
|
+
R(u, x) = f_int(u, x) + k_out u = 0 on the free dofs, input prescribed
|
|
16
|
+
K_T lambda = l, dJ/dx_e = -lambda_e . dR_e/dx_e
|
|
17
|
+
|
|
18
|
+
Void cells are the classic trouble: at 1e-9 of the modulus they turn inside
|
|
19
|
+
out under the large motion of their solid neighbours and Newton fails. The
|
|
20
|
+
fix is Wang, Lazarov & Sigmund 2014 ("Interpolation scheme for fictitious
|
|
21
|
+
domain techniques and topology optimization of finite strain elastic
|
|
22
|
+
problems"): each cell's internal force is
|
|
23
|
+
|
|
24
|
+
f_e = E_e [ gamma_e f_NL(gamma_e u_e) + (1 - gamma_e^2) K0 u_e ]
|
|
25
|
+
|
|
26
|
+
with gamma_e a sharp step of the density at `void_threshold` (1 above, 0
|
|
27
|
+
below), so solid cells are fully nonlinear and void cells linear, which
|
|
28
|
+
cannot invert. The default, 0.1, treats grey material as nonlinear too, so
|
|
29
|
+
the optimizer sees the large-deflection behaviour from the first
|
|
30
|
+
iteration; 0.5 keeps grey cells linear (a continuation from the linear
|
|
31
|
+
design, more forgiving of Newton failures, and on the inverter a worse
|
|
32
|
+
optimum: 2.4 against 3.5). A design step whose Newton solve fails is
|
|
33
|
+
backtracked halfway to the last design that converged.
|
|
34
|
+
|
|
35
|
+
The filter, projection, extrusion, robust (eroded / dilated) option and MMA
|
|
36
|
+
are mechanism.py's. Each iteration costs one Newton solve per realization
|
|
37
|
+
(warm-started from the last design's displacement, with load steps only
|
|
38
|
+
when that fails) plus one linear adjoint solve.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import time
|
|
43
|
+
|
|
44
|
+
import numpy as np
|
|
45
|
+
import scipy.sparse as sp
|
|
46
|
+
|
|
47
|
+
from .fem import Stiffness, hex8_ke
|
|
48
|
+
from .mechanism import MechanismOptions, MechanismResult, _spring_diag
|
|
49
|
+
from .nonlinear import _CENTER, _CHUNK, _GAUSS, _element, _Linear
|
|
50
|
+
from .simp import E_MIN, PENALTY, _dproject, _extrude, _project, density_filter
|
|
51
|
+
|
|
52
|
+
__all__ = ["optimize_mechanism_nonlinear", "nonlinear_response", "stroke_dofs", "RigidCylinder", "RigidObject", "Contacts", "ServoTendon",
|
|
53
|
+
"tendon_grip"]
|
|
54
|
+
|
|
55
|
+
GAMMA_ETA, GAMMA_BETA = 0.1, 100.0 # the solid/void step on the physical density
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _gamma(xp, eta=GAMMA_ETA, beta=GAMMA_BETA):
|
|
59
|
+
t0 = np.tanh(beta * eta)
|
|
60
|
+
den = t0 + np.tanh(beta * (1 - eta))
|
|
61
|
+
g = (t0 + np.tanh(beta * (xp - eta))) / den
|
|
62
|
+
dg = beta * (1 - np.tanh(beta * (xp - eta)) ** 2) / den
|
|
63
|
+
return g, dg
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def stroke_dofs(mesh, f, fixed, stroke):
|
|
67
|
+
"""The input region's dofs moved by `stroke` mm along the input force's
|
|
68
|
+
direction: (dofs, values)."""
|
|
69
|
+
fv = np.asarray(f, float).reshape(-1, 3)
|
|
70
|
+
loaded = np.flatnonzero(np.abs(fv).sum(axis=1) > 0)
|
|
71
|
+
d = fv.sum(axis=0)
|
|
72
|
+
d /= np.linalg.norm(d)
|
|
73
|
+
pd, pv = [], []
|
|
74
|
+
for c in range(3):
|
|
75
|
+
if abs(d[c]) > 1e-9:
|
|
76
|
+
dofs = 3 * loaded + c
|
|
77
|
+
dofs = dofs[~np.asarray(fixed, bool)[dofs]]
|
|
78
|
+
pd.append(dofs)
|
|
79
|
+
pv.append(np.full(len(dofs), stroke * d[c]))
|
|
80
|
+
return np.concatenate(pd), np.concatenate(pv)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ServoTendon:
|
|
84
|
+
"""A tendon wound in by a servo: the actuator pulls `stroke` mm of cable
|
|
85
|
+
in through `path` (as nonlinear.Tendon: fixed points and guides on the
|
|
86
|
+
part), and the cable and servo give like a spring of `stiffness` N/mm,
|
|
87
|
+
so the tension is
|
|
88
|
+
|
|
89
|
+
T = stiffness * (stroke - how far the part has let the cable in)
|
|
90
|
+
|
|
91
|
+
(never negative: a slack cable pushes nothing). It is displacement
|
|
92
|
+
driven, as a position-controlled servo is, so the solve can follow a
|
|
93
|
+
finger past the point where a constant tension would snap it over, and
|
|
94
|
+
the tension can never exceed stiffness * stroke (the stall force, say).
|
|
95
|
+
Load steps scale the stroke."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, path, stroke, stiffness, slack=0.05):
|
|
98
|
+
from .nonlinear import Tendon
|
|
99
|
+
self._t = Tendon(tension=1.0, path=path)
|
|
100
|
+
self.stroke, self.k, self.w = float(stroke), float(stiffness), float(slack)
|
|
101
|
+
self.tension = self.k * self.stroke # the most it can pull
|
|
102
|
+
|
|
103
|
+
def _setup(self, mesh):
|
|
104
|
+
self._t._setup(mesh)
|
|
105
|
+
self._len0 = self._t._len0
|
|
106
|
+
|
|
107
|
+
def pull(self, u, lam):
|
|
108
|
+
"""Tension (N), force on the part, and the load stiffness."""
|
|
109
|
+
f1, k1, length = self._t._load(u, 1.0) # f1 = -dL/du
|
|
110
|
+
z = (length - (self._len0 - lam * self.stroke)) / self.w
|
|
111
|
+
t = self.k * self.w * np.logaddexp(0.0, z)
|
|
112
|
+
dt = self.k * 0.5 * (1 + np.tanh(0.5 * z))
|
|
113
|
+
dl = sp.csr_matrix(-f1)
|
|
114
|
+
return t, t * f1, t * k1 + dt * (dl.T @ dl)
|
|
115
|
+
|
|
116
|
+
def _load(self, u, scale):
|
|
117
|
+
t, f, k = self.pull(u, scale)
|
|
118
|
+
return f, k, self._t._load(u, 1.0)[2]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class RigidCylinder:
|
|
122
|
+
"""A rigid, fixed object for the part to close on: a cylinder along
|
|
123
|
+
`axis` (default y, the thickness of a plate) through `center`, radius
|
|
124
|
+
`radius` (mm). Contact is a smooth penalty on the part's nodes:
|
|
125
|
+
|
|
126
|
+
p(g) = k s ln(1 + exp(-g / s)), g = distance to the axis - radius
|
|
127
|
+
|
|
128
|
+
so a node pressed `d` into the surface pushes back about k d (N), one
|
|
129
|
+
`s` outside it a little, and the response has a gradient everywhere. A
|
|
130
|
+
node's share is weighted by the density around it (cubed, like SIMP),
|
|
131
|
+
so void cells cannot grip. The grip force is the sum of the normal
|
|
132
|
+
forces."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, center, radius, axis="y", stiffness=50.0, smooth=0.05):
|
|
135
|
+
self.center = np.asarray(center, float)
|
|
136
|
+
self.radius, self.axis = float(radius), "xyz".index(axis)
|
|
137
|
+
self.k, self.s = float(stiffness), float(smooth)
|
|
138
|
+
|
|
139
|
+
def _setup(self, mesh):
|
|
140
|
+
self.plane = [i for i in range(3) if i != self.axis]
|
|
141
|
+
adj = sp.csr_matrix((np.ones(mesh.n_elem * 8), (mesh.enode.ravel(), np.repeat(np.arange(mesh.n_elem), 8))),
|
|
142
|
+
shape=(mesh.n_nodes, mesh.n_elem))
|
|
143
|
+
cnt = np.asarray(adj.sum(axis=1)).ravel()
|
|
144
|
+
self.avg = sp.diags(1.0 / np.maximum(cnt, 1)) @ adj # node density = mean of its cells
|
|
145
|
+
self.x0 = mesh.node_xyz[:, self.plane] - self.center[self.plane]
|
|
146
|
+
self.dofs = 3 * np.arange(mesh.n_nodes)[:, None] + np.array(self.plane)[None, :]
|
|
147
|
+
|
|
148
|
+
def weights(self, xp):
|
|
149
|
+
rho = self.avg @ xp
|
|
150
|
+
return 1e-3 + (1 - 1e-3) * rho ** 3, self.avg.multiply((3 * (1 - 1e-3) * rho ** 2)[:, None]).tocsr()
|
|
151
|
+
|
|
152
|
+
def state(self, u):
|
|
153
|
+
r = self.x0 + u[self.dofs]
|
|
154
|
+
dist = np.linalg.norm(r, axis=1)
|
|
155
|
+
nrm = r / np.maximum(dist, 1e-12)[:, None]
|
|
156
|
+
z = -(dist - self.radius) / self.s
|
|
157
|
+
sig = 0.5 * (1 + np.tanh(0.5 * z))
|
|
158
|
+
p = self.k * self.s * np.logaddexp(0.0, z)
|
|
159
|
+
dp = -self.k * sig # dp/dg
|
|
160
|
+
ddp = self.k / self.s * sig * (1 - sig)
|
|
161
|
+
return dist, nrm, p, dp, ddp
|
|
162
|
+
|
|
163
|
+
def load(self, u, w, tangent=True):
|
|
164
|
+
"""Contact as a resisting force (adds to f_int) and its stiffness."""
|
|
165
|
+
dist, nrm, p, dp, ddp = self.state(u)
|
|
166
|
+
act = np.flatnonzero(p * w > 1e-12 * self.k)
|
|
167
|
+
f = np.zeros(u.shape[0])
|
|
168
|
+
np.add.at(f, self.dofs[act].ravel(), (-(w * p)[act, None] * nrm[act]).ravel())
|
|
169
|
+
k = None
|
|
170
|
+
if tangent:
|
|
171
|
+
nn = np.einsum("ni,nj->nij", nrm[act], nrm[act])
|
|
172
|
+
ke = -(w[act] * dp[act])[:, None, None] * nn - (w[act] * p[act] / dist[act])[:, None, None] * \
|
|
173
|
+
(np.eye(2)[None] - nn)
|
|
174
|
+
rows = np.repeat(self.dofs[act], 2, axis=1).ravel()
|
|
175
|
+
cols = np.tile(self.dofs[act], (1, 2)).ravel()
|
|
176
|
+
k = sp.csr_matrix((ke.ravel(), (rows, cols)), shape=(u.shape[0], u.shape[0]))
|
|
177
|
+
return f, k
|
|
178
|
+
|
|
179
|
+
def grip(self, u, w):
|
|
180
|
+
"""Grip force (N), its gradient in u, and per-node pressures."""
|
|
181
|
+
dist, nrm, p, dp, _ = self.state(u)
|
|
182
|
+
dj = np.zeros(u.shape[0])
|
|
183
|
+
np.add.at(dj, self.dofs.ravel(), ((w * dp)[:, None] * nrm).ravel())
|
|
184
|
+
return float(w @ p), dj, p, nrm
|
|
185
|
+
|
|
186
|
+
def weight_terms(self, u, lam, grip):
|
|
187
|
+
"""Per node, what d(objective)/d(contact weight) collects through
|
|
188
|
+
the adjoint lam: the pressure times (1 if it is the grip objective)
|
|
189
|
+
plus the adjoint's travel along the normal (the contact force in
|
|
190
|
+
the residual)."""
|
|
191
|
+
_, nrm, p, _, _ = self.state(u)
|
|
192
|
+
return p * ((1.0 if grip else 0.0) + np.einsum("ni,ni->n", lam[self.dofs], nrm))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class RigidObject(RigidCylinder):
|
|
196
|
+
"""RigidCylinder's grip contact on any contact.RigidObstacle shape (a
|
|
197
|
+
plane, a sphere, a cylinder at any angle, a box; `inside` for a cavity),
|
|
198
|
+
in 3D: the object a finger, a gripper jaw or a clamp closes on. The
|
|
199
|
+
obstacle's own penalty settings are not used: contact here is the
|
|
200
|
+
optimizer's smooth softplus law (`stiffness` N/mm per node, `smooth`
|
|
201
|
+
mm), weighted by the density round each node so void cells cannot
|
|
202
|
+
grip, and the grip force (the objective) is the sum of the normal
|
|
203
|
+
forces."""
|
|
204
|
+
|
|
205
|
+
def __init__(self, obstacle, stiffness=50.0, smooth=0.05):
|
|
206
|
+
self.obstacle = obstacle
|
|
207
|
+
self.k, self.s = float(stiffness), float(smooth)
|
|
208
|
+
|
|
209
|
+
def _setup(self, mesh):
|
|
210
|
+
adj = sp.csr_matrix((np.ones(mesh.n_elem * 8), (mesh.enode.ravel(), np.repeat(np.arange(mesh.n_elem), 8))),
|
|
211
|
+
shape=(mesh.n_nodes, mesh.n_elem))
|
|
212
|
+
cnt = np.asarray(adj.sum(axis=1)).ravel()
|
|
213
|
+
self.avg = sp.diags(1.0 / np.maximum(cnt, 1)) @ adj
|
|
214
|
+
self.x0 = mesh.node_xyz
|
|
215
|
+
self.dofs = 3 * np.arange(mesh.n_nodes)[:, None] + np.arange(3)[None, :]
|
|
216
|
+
|
|
217
|
+
def state(self, u):
|
|
218
|
+
g, nrm, H = self.obstacle.sdf(self.x0 + u[self.dofs])
|
|
219
|
+
self._H = H
|
|
220
|
+
z = -g / self.s
|
|
221
|
+
sig = 0.5 * (1 + np.tanh(0.5 * z))
|
|
222
|
+
p = self.k * self.s * np.logaddexp(0.0, z)
|
|
223
|
+
return g, nrm, p, -self.k * sig, self.k / self.s * sig * (1 - sig)
|
|
224
|
+
|
|
225
|
+
def load(self, u, w, tangent=True):
|
|
226
|
+
g, nrm, p, dp, ddp = self.state(u)
|
|
227
|
+
act = np.flatnonzero(p * w > 1e-12 * self.k)
|
|
228
|
+
f = np.zeros(u.shape[0])
|
|
229
|
+
np.add.at(f, self.dofs[act].ravel(), (-(w * p)[act, None] * nrm[act]).ravel())
|
|
230
|
+
k = None
|
|
231
|
+
if tangent:
|
|
232
|
+
nn = np.einsum("ni,nj->nij", nrm[act], nrm[act])
|
|
233
|
+
ke = -(w[act] * dp[act])[:, None, None] * nn - (w[act] * p[act])[:, None, None] * self._H[act]
|
|
234
|
+
rows = np.repeat(self.dofs[act], 3, axis=1).ravel()
|
|
235
|
+
cols = np.tile(self.dofs[act], (1, 3)).ravel()
|
|
236
|
+
k = sp.csr_matrix((ke.ravel(), (rows, cols)), shape=(u.shape[0], u.shape[0]))
|
|
237
|
+
return f, k
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Contacts:
|
|
241
|
+
"""Several rigid objects at once for the optimizer: the one(s) the part
|
|
242
|
+
grips (summed into the grip objective) and others it may only press on
|
|
243
|
+
(a palm, an end stop, a neighbouring finger's envelope, a wall), which
|
|
244
|
+
resist like the gripped object but count for nothing. Each item is a
|
|
245
|
+
RigidCylinder, a RigidObject or a contact.RigidObstacle (wrapped in a
|
|
246
|
+
RigidObject); `grip` says which of them the grip force sums over
|
|
247
|
+
(default: the first only)."""
|
|
248
|
+
|
|
249
|
+
def __init__(self, items, grip=None, stiffness=50.0, smooth=0.05):
|
|
250
|
+
from .contact import RigidObstacle
|
|
251
|
+
self.items = [RigidObject(c, stiffness, smooth) if isinstance(c, RigidObstacle) else c for c in items]
|
|
252
|
+
if not self.items:
|
|
253
|
+
raise ValueError("Contacts: no objects")
|
|
254
|
+
g = [0] if grip is None else list(grip)
|
|
255
|
+
self.gripped = np.zeros(len(self.items), bool)
|
|
256
|
+
self.gripped[g] = True
|
|
257
|
+
|
|
258
|
+
def _setup(self, mesh):
|
|
259
|
+
for c in self.items:
|
|
260
|
+
c._setup(mesh)
|
|
261
|
+
self.dofs = self.items[0].dofs
|
|
262
|
+
|
|
263
|
+
def weights(self, xp):
|
|
264
|
+
return self.items[0].weights(xp)
|
|
265
|
+
|
|
266
|
+
def load(self, u, w, tangent=True):
|
|
267
|
+
f, k = None, None
|
|
268
|
+
for c in self.items:
|
|
269
|
+
fc, kc = c.load(u, w, tangent)
|
|
270
|
+
f = fc if f is None else f + fc
|
|
271
|
+
if tangent:
|
|
272
|
+
k = kc if k is None else k + kc
|
|
273
|
+
return f, k
|
|
274
|
+
|
|
275
|
+
def grip(self, u, w):
|
|
276
|
+
"""Grip force on the gripped objects (N), its gradient, and the
|
|
277
|
+
pressure per node on them (summed); normals None."""
|
|
278
|
+
j, dj, p = 0.0, np.zeros(u.shape[0]), np.zeros(len(w))
|
|
279
|
+
for c, g in zip(self.items, self.gripped):
|
|
280
|
+
if g:
|
|
281
|
+
jc, djc, pc, _ = c.grip(u, w)
|
|
282
|
+
j, dj, p = j + jc, dj + djc, p + pc
|
|
283
|
+
return j, dj, p, None
|
|
284
|
+
|
|
285
|
+
def forces(self, u, w):
|
|
286
|
+
"""The force on each object (N): what a stop or palm has to hold."""
|
|
287
|
+
out = []
|
|
288
|
+
for c in self.items:
|
|
289
|
+
_, nrm, p, _, _ = c.state(u)
|
|
290
|
+
out.append((-(w * p)[:, None] * nrm).sum(axis=0).tolist())
|
|
291
|
+
return out
|
|
292
|
+
|
|
293
|
+
def weight_terms(self, u, lam, grip):
|
|
294
|
+
return sum(c.weight_terms(u, lam, grip and g) for c, g in zip(self.items, self.gripped))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class _Model:
|
|
298
|
+
"""Mixed linear/nonlinear assembly on one mesh, with the input prescribed."""
|
|
299
|
+
|
|
300
|
+
def __init__(self, mesh, nu, fixed, pd, pv, springs, kind="neo-hookean", tendons=None, contact=None):
|
|
301
|
+
self.mesh, self.kind = mesh, kind
|
|
302
|
+
self.tendons = list(tendons or [])
|
|
303
|
+
for t in self.tendons:
|
|
304
|
+
t._setup(mesh)
|
|
305
|
+
self.contact = contact
|
|
306
|
+
if contact is not None:
|
|
307
|
+
contact._setup(mesh)
|
|
308
|
+
self.w = None # contact weights, set per design
|
|
309
|
+
# Poisson's ratio per element (two materials) or one for all
|
|
310
|
+
nu_e = np.broadcast_to(np.asarray(nu, float), (mesh.n_elem,))
|
|
311
|
+
self.groups = [(float(v), np.flatnonzero(nu_e == v)) for v in np.unique(nu_e)]
|
|
312
|
+
# plain hex8: the linear cells must match the nonlinear element's
|
|
313
|
+
# small-strain limit where the two meet
|
|
314
|
+
self.k0s = {v: hex8_ke(v, "plain") * mesh.pitch for v, _ in self.groups}
|
|
315
|
+
nu = float(nu_e[0])
|
|
316
|
+
cons = np.asarray(fixed, bool).copy()
|
|
317
|
+
cons[pd] = True
|
|
318
|
+
self.cons, self.pd, self.pv = cons, pd, pv
|
|
319
|
+
self.free = ~cons
|
|
320
|
+
self.keep = sp.diags(self.free.astype(float)).tocsr()
|
|
321
|
+
self.stiff = Stiffness(mesh, nu, cons)
|
|
322
|
+
self.springs = np.where(cons, 0.0, springs)
|
|
323
|
+
self.lin = _Linear(mesh)
|
|
324
|
+
|
|
325
|
+
def assemble(self, u, mod, gam, tangent=True, parts=False, lam=1.0):
|
|
326
|
+
mesh = self.mesh
|
|
327
|
+
fint = np.zeros(mesh.n_dof)
|
|
328
|
+
data = np.zeros(self.stiff.nnz) if tangent else None
|
|
329
|
+
jmin = np.inf
|
|
330
|
+
br = np.zeros((mesh.n_elem, 24)) if parts else None
|
|
331
|
+
dbr = np.zeros((mesh.n_elem, 24)) if parts else None
|
|
332
|
+
from . import nlkernel
|
|
333
|
+
fast = nlkernel.available()
|
|
334
|
+
# the compiled kernel takes a whole group at once; the numpy element goes in chunks
|
|
335
|
+
chunks = [(v, idx) for v, idx in self.groups] if fast else \
|
|
336
|
+
[(v, idx[s:s + _CHUNK]) for v, idx in self.groups for s in range(0, len(idx), _CHUNK)]
|
|
337
|
+
for nu, el in chunks:
|
|
338
|
+
k0 = self.k0s[nu]
|
|
339
|
+
edof = mesh.edof[el]
|
|
340
|
+
ue = u[edof]
|
|
341
|
+
g = gam[el]
|
|
342
|
+
if fast:
|
|
343
|
+
m = len(el)
|
|
344
|
+
fh, kh = nlkernel.elements((g[:, None] * ue).ravel(), np.arange(24 * m).reshape(m, 24), None,
|
|
345
|
+
np.ones(m), mesh.pitch, nu, self.kind, True, np.stack(_GAUSS))[:2]
|
|
346
|
+
else:
|
|
347
|
+
fh, kh, _ = _element((g[:, None] * ue).reshape(-1, 8, 3), mesh.pitch, nu, self.kind, True)
|
|
348
|
+
# J of the solid cells only: void ones are linear and may not invert
|
|
349
|
+
solid = g > 0.5
|
|
350
|
+
if solid.any():
|
|
351
|
+
F = np.eye(3)[None] + np.einsum("mai,aj->mij", ue[solid].reshape(-1, 8, 3), _CENTER / mesh.pitch)
|
|
352
|
+
jmin = min(jmin, float(np.linalg.det(F).min()))
|
|
353
|
+
ku = ue @ k0.T
|
|
354
|
+
b = g[:, None] * fh + (1 - g ** 2)[:, None] * ku
|
|
355
|
+
me = mod[el]
|
|
356
|
+
if tangent:
|
|
357
|
+
ke = (g ** 2 * me)[:, None, None] * kh + ((1 - g ** 2) * me)[:, None, None] * k0[None]
|
|
358
|
+
if fast:
|
|
359
|
+
fe_, d_ = nlkernel.scatter(mesh.n_dof, self.stiff.nnz, self.stiff._slots if len(el) == mesh.n_elem else self.stiff._slots[:, el], edof,
|
|
360
|
+
b * me[:, None], ke if tangent else None, tangent)
|
|
361
|
+
fint += fe_
|
|
362
|
+
if tangent:
|
|
363
|
+
data += d_
|
|
364
|
+
else:
|
|
365
|
+
np.add.at(fint, edof.ravel(), (b * me[:, None]).ravel())
|
|
366
|
+
if tangent:
|
|
367
|
+
self.stiff.add_elements(data, ke, el)
|
|
368
|
+
if parts:
|
|
369
|
+
br[el] = b
|
|
370
|
+
dbr[el] = fh + g[:, None] * np.einsum("mij,mj->mi", kh, ue) - 2 * g[:, None] * ku
|
|
371
|
+
fint += self.springs * u
|
|
372
|
+
k = None
|
|
373
|
+
if tangent:
|
|
374
|
+
k = self.stiff.finish(data) + sp.diags(self.springs)
|
|
375
|
+
# the tendons' and contacts' stiffness only on the free dofs: the
|
|
376
|
+
# held ones keep Stiffness's identity rows and columns (left in,
|
|
377
|
+
# they couple a held dof to the rest, and the adjoint, which zeroes
|
|
378
|
+
# the held dofs after the solve, came out wrong wherever a tendon
|
|
379
|
+
# knot or a contact sat on a node with a held direction)
|
|
380
|
+
for t in self.tendons: # the tendon pulls: an outside load, with its own stiffness
|
|
381
|
+
ft, kt, _ = t._load(u, lam) # (a ServoTendon's lam scales its stroke)
|
|
382
|
+
fint -= ft
|
|
383
|
+
if tangent:
|
|
384
|
+
k = k + self.keep @ kt @ self.keep
|
|
385
|
+
if self.contact is not None:
|
|
386
|
+
fc, kc = self.contact.load(u, self.w, tangent)
|
|
387
|
+
fint += fc
|
|
388
|
+
if tangent:
|
|
389
|
+
k = k + self.keep @ kc @ self.keep
|
|
390
|
+
if tangent:
|
|
391
|
+
k = k.tocsr()
|
|
392
|
+
return fint, k, jmin, br, dbr
|
|
393
|
+
|
|
394
|
+
def newton(self, u, mod, gam, lam, rtol=1e-8, max_it=40):
|
|
395
|
+
u = u.copy()
|
|
396
|
+
u[self.pd] = lam * self.pv
|
|
397
|
+
r0 = None
|
|
398
|
+
for _ in range(max_it):
|
|
399
|
+
fint, k, jmin, _, _ = self.assemble(u, mod, gam, lam=lam)
|
|
400
|
+
if not np.isfinite(jmin) or jmin <= 0:
|
|
401
|
+
return None
|
|
402
|
+
r = np.where(self.free, fint, 0.0)
|
|
403
|
+
rn = float(np.linalg.norm(r))
|
|
404
|
+
if r0 is None:
|
|
405
|
+
r0 = max(rn, float(np.linalg.norm(fint[self.pd])), lam * sum(t.tension for t in self.tendons), 1e-30)
|
|
406
|
+
if rn <= rtol * r0:
|
|
407
|
+
return u
|
|
408
|
+
du = self.lin.solve(k, -r)
|
|
409
|
+
du[~self.free] = 0.0
|
|
410
|
+
# damped step: a full Newton step can invert a solid cell next
|
|
411
|
+
# to soft void ones; halve it until no cell inverts and the
|
|
412
|
+
# residual does not blow up
|
|
413
|
+
for _ in range(6):
|
|
414
|
+
un = u + du
|
|
415
|
+
fn, _, jn, _, _ = self.assemble(un, mod, gam, tangent=False, lam=lam)
|
|
416
|
+
if np.all(np.isfinite(un)) and np.isfinite(jn) and jn > 0 and \
|
|
417
|
+
np.linalg.norm(np.where(self.free, fn, 0.0)) < 2.0 * rn:
|
|
418
|
+
break
|
|
419
|
+
du *= 0.5
|
|
420
|
+
else:
|
|
421
|
+
return None
|
|
422
|
+
u = un
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
def solve_path(self, mod, gam, prev=None, steps=4):
|
|
426
|
+
"""Equilibrium at the full stroke followed up the load from rest, so
|
|
427
|
+
the answer is the branch the part actually takes (the one the stroke
|
|
428
|
+
check finds), not whichever equilibrium Newton lands on from the last
|
|
429
|
+
design's answer: near a snap or a buckle the two differ, and which
|
|
430
|
+
one a warm start picks flips on rounding. The levels are fixed
|
|
431
|
+
(1/steps, 2/steps, .. 1); each starts from this design's answer one
|
|
432
|
+
level down plus the last design's increment between the two levels
|
|
433
|
+
(prev: that design's path), and a level that will not converge is
|
|
434
|
+
reached in halved sub-steps. Returns (u at the full stroke, path)."""
|
|
435
|
+
n = self.mesh.n_dof
|
|
436
|
+
u, lam, path = np.zeros(n), 0.0, []
|
|
437
|
+
for i in range(steps):
|
|
438
|
+
t = (i + 1) / steps
|
|
439
|
+
un = None
|
|
440
|
+
if prev is not None and len(prev) == steps:
|
|
441
|
+
un = self.newton(u + prev[i] - (prev[i - 1] if i else 0.0), mod, gam, t)
|
|
442
|
+
if un is None:
|
|
443
|
+
un = self._march(u, lam, t, mod, gam)
|
|
444
|
+
u, lam = un, t
|
|
445
|
+
path.append(u)
|
|
446
|
+
return u, path
|
|
447
|
+
|
|
448
|
+
def _march(self, u, lam, t_end, mod, gam):
|
|
449
|
+
"""From (u, lam) to t_end in sub-steps, halving one that fails."""
|
|
450
|
+
dl, v = t_end - lam, None
|
|
451
|
+
while lam < t_end - 1e-12:
|
|
452
|
+
t = min(t_end, lam + dl)
|
|
453
|
+
pred = u if v is None else u + v * ((t - lam) / dlv)
|
|
454
|
+
un = self.newton(pred, mod, gam, t)
|
|
455
|
+
if un is None:
|
|
456
|
+
dl /= 2
|
|
457
|
+
if dl < 1e-3:
|
|
458
|
+
raise RuntimeError(f"Newton failed at {lam:.3f} of the stroke")
|
|
459
|
+
continue
|
|
460
|
+
v, dlv = un - u, t - lam
|
|
461
|
+
u, lam = un, t
|
|
462
|
+
dl = min(dl * 1.5, t_end - lam) if lam < t_end else dl
|
|
463
|
+
return u
|
|
464
|
+
|
|
465
|
+
def solve(self, mod, gam, u0=None, steps=4):
|
|
466
|
+
"""Equilibrium at the full stroke: straight from u0 (the last design's
|
|
467
|
+
answer), else in load steps from rest, halving a step that fails."""
|
|
468
|
+
if u0 is not None:
|
|
469
|
+
u = self.newton(u0, mod, gam, 1.0)
|
|
470
|
+
if u is not None:
|
|
471
|
+
return u
|
|
472
|
+
u = np.zeros(self.mesh.n_dof)
|
|
473
|
+
lam, dl = 0.0, 1.0 / steps
|
|
474
|
+
while lam < 1.0 - 1e-12:
|
|
475
|
+
t = min(1.0, lam + dl)
|
|
476
|
+
un = self.newton(u * (t / lam) if lam > 0 else u, mod, gam, t)
|
|
477
|
+
if un is None:
|
|
478
|
+
dl /= 2
|
|
479
|
+
if dl < 1e-3:
|
|
480
|
+
raise RuntimeError(f"Newton failed at {lam:.3f} of the stroke")
|
|
481
|
+
continue
|
|
482
|
+
u, lam = un, t
|
|
483
|
+
dl = min(dl * 1.5, 1.0 - lam if lam < 1 else dl)
|
|
484
|
+
return u
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def nonlinear_response(model: _Model, xp, modulus, lvec, u0=None, steps=4, void_threshold=GAMMA_ETA,
|
|
488
|
+
follow=False):
|
|
489
|
+
"""J at the full stroke (or tension) and dJ/dxp per element: J = lvec . u
|
|
490
|
+
(output travel), or with lvec None the grip force on model.contact.
|
|
491
|
+
modulus: scalar or per element (a two-material part: the solid's E in
|
|
492
|
+
each cell). follow: trace the load path from rest (_Model.solve_path);
|
|
493
|
+
u0 is then the last design's path (or None) and the third value
|
|
494
|
+
returned is this design's path, whose last entry is u."""
|
|
495
|
+
modulus = np.asarray(modulus, float)
|
|
496
|
+
mod = modulus * (E_MIN + xp ** PENALTY * (1 - E_MIN))
|
|
497
|
+
dmod = modulus * PENALTY * xp ** (PENALTY - 1) * (1 - E_MIN)
|
|
498
|
+
gam, dgam = _gamma(xp, void_threshold)
|
|
499
|
+
dw = None
|
|
500
|
+
if model.contact is not None:
|
|
501
|
+
model.w, dw = model.contact.weights(xp)
|
|
502
|
+
if follow:
|
|
503
|
+
u, path = model.solve_path(mod, gam, u0, steps)
|
|
504
|
+
else:
|
|
505
|
+
u = path = model.solve(mod, gam, u0, steps)
|
|
506
|
+
_, k, _, br, dbr = model.assemble(u, mod, gam, parts=True)
|
|
507
|
+
if lvec is None:
|
|
508
|
+
j, dju, p, nrm = model.contact.grip(u, model.w)
|
|
509
|
+
else:
|
|
510
|
+
j, dju = float(lvec @ u), lvec
|
|
511
|
+
lam = model.lin.solve(k, np.where(model.free, dju, 0.0))
|
|
512
|
+
lam[~model.free] = 0.0
|
|
513
|
+
le = lam[model.mesh.edof]
|
|
514
|
+
dj = -(np.einsum("mi,mi->m", le, br) * dmod + np.einsum("mi,mi->m", le, dbr) * mod * dgam)
|
|
515
|
+
if dw is not None:
|
|
516
|
+
# the contact weights depend on the density: the grip term itself
|
|
517
|
+
# and the contact force in the residual
|
|
518
|
+
dj += dw.T @ model.contact.weight_terms(u, lam, lvec is None)
|
|
519
|
+
return j, dj, path
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _tension(tendons, u, lam):
|
|
523
|
+
return float(sum(t.pull(u, lam)[0] if isinstance(t, ServoTendon) else lam * t.tension for t in tendons))
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def tendon_grip(domain, xphys, modulus, nu, tendons, contact, threshold=0.5, steps=8,
|
|
527
|
+
material="neo-hookean"):
|
|
528
|
+
"""Check a design the way it will be printed: cells above `threshold`
|
|
529
|
+
solid, the rest empty (linear, at E_MIN, so they carry nothing), the
|
|
530
|
+
tendons pulled to full tension in load steps, the part closing on
|
|
531
|
+
`contact`. Returns the grip force (N), how much cable the actuator
|
|
532
|
+
pulled in (mm), the nodes touching the object, the grip at each step,
|
|
533
|
+
and the deformed positions (for a picture). A design that cannot take
|
|
534
|
+
the full tension reports how far it got and its grip there."""
|
|
535
|
+
mesh = domain.mesh
|
|
536
|
+
xp = (np.asarray(xphys) > threshold).astype(float)
|
|
537
|
+
model = _Model(mesh, nu, domain.fixed, np.zeros(0, np.int64), np.zeros(0), np.zeros(mesh.n_dof), material,
|
|
538
|
+
tendons, contact)
|
|
539
|
+
model.w, _ = contact.weights(xp)
|
|
540
|
+
mod = modulus * (E_MIN + xp ** PENALTY * (1 - E_MIN))
|
|
541
|
+
gam, _ = _gamma(xp, 0.5)
|
|
542
|
+
u, lam, dl, history = np.zeros(mesh.n_dof), 0.0, 1.0 / steps, []
|
|
543
|
+
while lam < 1.0 - 1e-12:
|
|
544
|
+
t = min(1.0, lam + dl)
|
|
545
|
+
un = model.newton(u, mod, gam, t)
|
|
546
|
+
if un is None:
|
|
547
|
+
dl /= 2
|
|
548
|
+
if dl < 1e-3:
|
|
549
|
+
break
|
|
550
|
+
continue
|
|
551
|
+
u, lam = un, t
|
|
552
|
+
history.append({"load": lam, "tension": _tension(model.tendons, u, lam), "grip": contact.grip(u, model.w)[0]})
|
|
553
|
+
dl = min(dl * 1.5, 1.0 - lam if lam < 1 else dl)
|
|
554
|
+
j, _, p, _ = contact.grip(u, model.w)
|
|
555
|
+
rep = {"converged": lam >= 1.0 - 1e-12, "loadReached": lam, "tension": _tension(model.tendons, u, lam),
|
|
556
|
+
"grip": j, "contactNodes": int((model.w * p > 0.01 * max(j, 1e-9)).sum()),
|
|
557
|
+
"tendonTravel": [float(t._len0 - t._load(u, lam)[2]) for t in model.tendons], "history": history,
|
|
558
|
+
"deformed": mesh.node_xyz + u.reshape(-1, 3), "solid": xp > 0.5}
|
|
559
|
+
if isinstance(contact, Contacts):
|
|
560
|
+
rep["forces"] = contact.forces(u, model.w)
|
|
561
|
+
if not rep["converged"]:
|
|
562
|
+
rep["message"] = "the solve stopped short of the full pull (the part snaps or wraps past the object)"
|
|
563
|
+
return rep
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def optimize_mechanism_nonlinear(domain, nu, opts: MechanismOptions, stroke: float | None = None, steps: int = 4,
|
|
567
|
+
material: str = "neo-hookean", void_threshold: float = GAMMA_ETA,
|
|
568
|
+
on_iter=None, tendons: list | None = None,
|
|
569
|
+
contact=None, follow_path: bool = True, settle: bool = True, starts: int = 1,
|
|
570
|
+
start: int = 0, keep_best: bool = True) -> MechanismResult:
|
|
571
|
+
"""mechanism.optimize_mechanism on the large-deflection response.
|
|
572
|
+
|
|
573
|
+
Input: the input case's region driven `stroke` mm along its force, or
|
|
574
|
+
`tendons` (nonlinear.Tendon: a cable at constant tension through guides
|
|
575
|
+
on the part, which follows the part as it curls; the guides should sit
|
|
576
|
+
on preserved cells).
|
|
577
|
+
|
|
578
|
+
Objective: the output case's travel along its direction against k_out
|
|
579
|
+
(the workpiece as a spring), or with `contact` (the object the part
|
|
580
|
+
closes on: a RigidCylinder, a RigidObject, or a contact.RigidObstacle
|
|
581
|
+
of any shape, which is wrapped in a RigidObject) the grip force on it.
|
|
582
|
+
Several objects: a Contacts (or a list, whose first item is gripped and
|
|
583
|
+
the rest are stops the part may press on but that count for nothing).
|
|
584
|
+
|
|
585
|
+
k_in is ignored. nu and opts.modulus may be per element (a two-material
|
|
586
|
+
part). output_displacement is the travel (mm) or the grip force (N).
|
|
587
|
+
|
|
588
|
+
Repeatability: the large-deflection optimum is not unique, and which
|
|
589
|
+
one a run finds can turn on rounding (sharpening refusals and
|
|
590
|
+
backtracks are yes/no decisions). follow_path traces each design's
|
|
591
|
+
equilibrium up the load from rest, so the optimizer sees the branch the
|
|
592
|
+
part really takes, and settle shrinks the move limit once the
|
|
593
|
+
projection is at its sharpest. starts > 1 runs from that many starts
|
|
594
|
+
(start 0 uniform, the others nudged by a seeded 1e-6 per cell) and
|
|
595
|
+
keeps the one whose thresholded design (cells above 0.5 solid, the
|
|
596
|
+
rest void) does best on the large-deflection check;
|
|
597
|
+
history[-1]["starts"] lists every start's thresholded value (None for
|
|
598
|
+
a start that failed; a start whose first design cannot take the stroke
|
|
599
|
+
is skipped). keep_best (default) checks the thresholded design at every
|
|
600
|
+
iteration of the sharpest phase (beta at its cap, full tension, volume
|
|
601
|
+
within 1% of the target) and returns the best one seen, not wherever
|
|
602
|
+
the run stopped (history[-1]["keptIteration"] when that is an earlier
|
|
603
|
+
one): about 15% more time. history[-1]["thresholdedOutput"] is that
|
|
604
|
+
value for the design returned (None if the thresholded design does not
|
|
605
|
+
reach the stroke)."""
|
|
606
|
+
if starts > 1:
|
|
607
|
+
t_all = time.time()
|
|
608
|
+
runs, err = [], None
|
|
609
|
+
for k in range(starts):
|
|
610
|
+
try:
|
|
611
|
+
runs.append(optimize_mechanism_nonlinear(domain, nu, opts, stroke, steps, material, void_threshold,
|
|
612
|
+
on_iter, tendons, contact, follow_path, settle, 1, k,
|
|
613
|
+
keep_best))
|
|
614
|
+
except RuntimeError as ex: # this start's first design could not take the stroke
|
|
615
|
+
runs.append(None)
|
|
616
|
+
err = ex
|
|
617
|
+
if all(r is None for r in runs):
|
|
618
|
+
raise err
|
|
619
|
+
vals = [None if r is None else r.history[-1].get("thresholdedOutput") for r in runs]
|
|
620
|
+
best = max(range(starts), key=lambda k: -np.inf if runs[k] is None else
|
|
621
|
+
(-1e300 if vals[k] is None else vals[k]))
|
|
622
|
+
r = runs[best]
|
|
623
|
+
r.history[-1]["starts"] = vals
|
|
624
|
+
r.history[-1]["start"] = best
|
|
625
|
+
r.seconds = time.time() - t_all
|
|
626
|
+
return r
|
|
627
|
+
t0 = time.time()
|
|
628
|
+
mesh = domain.mesh
|
|
629
|
+
n = mesh.n_elem
|
|
630
|
+
pres = domain.preserve
|
|
631
|
+
design = ~pres
|
|
632
|
+
if (stroke is None) == (not tendons):
|
|
633
|
+
raise ValueError("drive the mechanism with a stroke or with tendons (one of them)")
|
|
634
|
+
if tendons:
|
|
635
|
+
pd, pv = np.zeros(0, np.int64), np.zeros(0)
|
|
636
|
+
else:
|
|
637
|
+
pd, pv = stroke_dofs(mesh, domain.loads[opts.input_case], domain.fixed, stroke)
|
|
638
|
+
from .contact import RigidObstacle
|
|
639
|
+
if isinstance(contact, RigidObstacle):
|
|
640
|
+
contact = RigidObject(contact)
|
|
641
|
+
elif isinstance(contact, (list, tuple)):
|
|
642
|
+
contact = Contacts(contact)
|
|
643
|
+
if contact is None:
|
|
644
|
+
lo = domain.loads[opts.output_case]
|
|
645
|
+
wts = np.abs(lo.reshape(-1, 3)).sum(axis=1) > 0
|
|
646
|
+
d = lo.reshape(-1, 3).sum(axis=0)
|
|
647
|
+
lvec = (wts[:, None] * (d / np.linalg.norm(d))[None, :]).ravel() / max(wts.sum(), 1)
|
|
648
|
+
springs = _spring_diag(lo, opts.k_out, mesh.n_dof)
|
|
649
|
+
springs[domain.fixed] = 0.0
|
|
650
|
+
else:
|
|
651
|
+
lvec, springs = None, np.zeros(mesh.n_dof)
|
|
652
|
+
model = _Model(mesh, nu, domain.fixed, pd, pv, springs, material, tendons, contact)
|
|
653
|
+
|
|
654
|
+
columns = None
|
|
655
|
+
if opts.extrusion is not None:
|
|
656
|
+
a = opts.extrusion
|
|
657
|
+
others = [i for i in range(3) if i != a]
|
|
658
|
+
ij = mesh.elem_ijk[:, others]
|
|
659
|
+
_, columns = np.unique(ij[:, 0] * (domain.dims[others[1]] + 1) + ij[:, 1], return_inverse=True)
|
|
660
|
+
h, hs = density_filter(domain, opts.filter_radius, pad=opts.extrusion is None)
|
|
661
|
+
ht = h.T.tocsr()
|
|
662
|
+
x = np.full(n, opts.volume_fraction)
|
|
663
|
+
if start:
|
|
664
|
+
# a seeded nudge of 1e-6 per cell: the path amplifies differences
|
|
665
|
+
# this small into different optima (rounding alone spreads the
|
|
666
|
+
# curling finger 5.5-8 mm), and nudges this small stay in the
|
|
667
|
+
# uniform start's neighbourhood; a visible ripple (+-0.05) landed
|
|
668
|
+
# in worse designs (3-5 mm)
|
|
669
|
+
x = x + 1e-6 * np.random.default_rng(start).uniform(-1.0, 1.0, n)
|
|
670
|
+
x[pres] = 1.0
|
|
671
|
+
target = opts.volume_fraction * design.sum()
|
|
672
|
+
beta, since = 1.0, 0
|
|
673
|
+
etas = [0.5] if not opts.robust else [0.5 + opts.robust, 0.5, 0.5 - opts.robust]
|
|
674
|
+
from .mma import MMA
|
|
675
|
+
mma = MMA(int(design.sum()), 1, xmin=0.0, xmax=1.0, move=opts.move)
|
|
676
|
+
history, u_prev, j0 = [], {}, None
|
|
677
|
+
x_ok, beta_ok, backtracks, beta_cap, stopped, refused = None, beta, 0, opts.beta_max, None, 0
|
|
678
|
+
it = 0
|
|
679
|
+
best, tpath = None, None
|
|
680
|
+
full = [t.tension for t in tendons or []]
|
|
681
|
+
# (servo tendons are displacement driven and need no ramp)
|
|
682
|
+
ts_ok = 0.08 if tendons and not any(isinstance(t, ServoTendon) for t in tendons) else 0.8
|
|
683
|
+
for it in range(1, opts.max_iterations + 1):
|
|
684
|
+
# tension continuation: the uniform grey start is far too soft for
|
|
685
|
+
# the full pull, so the tension rises 25% an iteration from 10% for
|
|
686
|
+
# as long as the design can take it
|
|
687
|
+
ts = min(1.0, ts_ok * 1.25)
|
|
688
|
+
real = None
|
|
689
|
+
for tries in range(8):
|
|
690
|
+
for t, tfull in zip(tendons or [], full):
|
|
691
|
+
if not isinstance(t, ServoTendon):
|
|
692
|
+
t.tension = tfull * ts
|
|
693
|
+
xt = _extrude((h @ x) / hs, columns, design)
|
|
694
|
+
try:
|
|
695
|
+
real, up = [], {}
|
|
696
|
+
for eta in etas:
|
|
697
|
+
xp = _project(xt, beta, eta)
|
|
698
|
+
xp[pres] = 1.0
|
|
699
|
+
j, dj, u = nonlinear_response(model, xp, opts.modulus, lvec, u_prev.get(eta), steps,
|
|
700
|
+
void_threshold, follow=follow_path)
|
|
701
|
+
up[eta] = u
|
|
702
|
+
real.append((j, dj * _dproject(xt, beta, eta)))
|
|
703
|
+
u_prev.update(up)
|
|
704
|
+
break
|
|
705
|
+
except RuntimeError:
|
|
706
|
+
real = None
|
|
707
|
+
if x_ok is None:
|
|
708
|
+
raise
|
|
709
|
+
backtracks += 1
|
|
710
|
+
if ts > ts_ok:
|
|
711
|
+
ts = ts_ok # too much pull for this design yet: hold the tension
|
|
712
|
+
elif beta > beta_ok:
|
|
713
|
+
# the sharper projection is what broke it: keep the last
|
|
714
|
+
# beta for now and try again later; stop sharpening
|
|
715
|
+
# after three refusals
|
|
716
|
+
beta, since, refused = beta_ok, 0, refused + 1
|
|
717
|
+
if refused >= 3:
|
|
718
|
+
beta_cap = beta_ok
|
|
719
|
+
else:
|
|
720
|
+
x = 0.5 * (x + x_ok) # backtrack toward the last design that converged
|
|
721
|
+
if real is None:
|
|
722
|
+
# no nearby design reaches the full stroke: keep the last one that did
|
|
723
|
+
x, beta, stopped = x_ok, beta_ok, "the stroke could not be reached from the last design"
|
|
724
|
+
break
|
|
725
|
+
x_ok, beta_ok, ts_ok = x.copy(), beta, ts
|
|
726
|
+
jb = real[len(etas) // 2][0]
|
|
727
|
+
if j0 is None:
|
|
728
|
+
# a grip starts at zero when the part does not reach the object yet
|
|
729
|
+
j0 = max(abs(jb), 0.05 * sum(full), 1e-9)
|
|
730
|
+
vals = np.array([r[0] / j0 for r in real])
|
|
731
|
+
if len(etas) == 1:
|
|
732
|
+
obj, g = vals[0], real[0][1] / j0
|
|
733
|
+
else:
|
|
734
|
+
q = 30.0
|
|
735
|
+
w = np.exp(-q * (vals - vals.min()))
|
|
736
|
+
obj = float(vals.min() - np.log(w.sum()) / q)
|
|
737
|
+
w /= w.sum()
|
|
738
|
+
g = sum(wk * gk for wk, (_, gk) in zip(w, real)) / j0
|
|
739
|
+
g = ht @ (_extrude(g, columns, design) / hs)
|
|
740
|
+
dv = ht @ (_extrude(_dproject(xt, beta, 0.5), columns, design) / hs)
|
|
741
|
+
xp = _project(xt, beta, 0.5)
|
|
742
|
+
xp[pres] = 1.0
|
|
743
|
+
vol = float(xp[design].sum())
|
|
744
|
+
xn = x.copy()
|
|
745
|
+
xn[design] = mma.update(x[design], -obj * 10.0, (-g * 10.0)[design],
|
|
746
|
+
np.array([vol / target - 1.0]), np.array([(dv / target)[design]]))
|
|
747
|
+
change = float(np.abs(xn - x).max())
|
|
748
|
+
x = xn
|
|
749
|
+
rec = {"iteration": it, "outputDisplacement": jb, "objective": obj, "beta": beta, "change": change,
|
|
750
|
+
"volume": float(xp[design].mean()), "backtracks": backtracks}
|
|
751
|
+
if tendons:
|
|
752
|
+
rec["tensionScale"] = ts
|
|
753
|
+
if len(etas) > 1:
|
|
754
|
+
rec["robustOutputs"] = [float(r[0]) for r in real]
|
|
755
|
+
if keep_best and beta >= beta_cap and ts >= 1.0 and vol <= target * 1.01:
|
|
756
|
+
# the design as printed, at every iteration of the sharpest
|
|
757
|
+
# phase: keep the best one seen
|
|
758
|
+
try:
|
|
759
|
+
jt, _, tpath = nonlinear_response(model, (xp > 0.5).astype(float), opts.modulus, lvec, tpath,
|
|
760
|
+
2 * steps, 0.5, follow=True)
|
|
761
|
+
rec["thresholdedOutput"] = float(jt)
|
|
762
|
+
if best is None or jt > best[0]:
|
|
763
|
+
best = (float(jt), x_ok.copy(), beta, it)
|
|
764
|
+
except (RuntimeError, ValueError):
|
|
765
|
+
tpath = None
|
|
766
|
+
history.append(rec)
|
|
767
|
+
if on_iter is not None:
|
|
768
|
+
on_iter(rec, xp)
|
|
769
|
+
since += 1
|
|
770
|
+
if beta < beta_cap and (since >= opts.beta_every or change < opts.tol * 2):
|
|
771
|
+
beta, since = min(beta * 2, beta_cap), 0
|
|
772
|
+
elif beta >= beta_cap and ts >= 1.0:
|
|
773
|
+
if settle:
|
|
774
|
+
# settle: shrink the move limit so the design converges
|
|
775
|
+
# instead of wandering round the optimum until the
|
|
776
|
+
# plateau test happens to stop it
|
|
777
|
+
mma.move = max(opts.move * 0.1, mma.move * 0.85)
|
|
778
|
+
if change < opts.tol:
|
|
779
|
+
break
|
|
780
|
+
if since >= 10 and len(history) > 10:
|
|
781
|
+
old = history[-11]["objective"]
|
|
782
|
+
if abs(obj - old) <= 2e-3 * abs(old):
|
|
783
|
+
break
|
|
784
|
+
for t, tfull in zip(tendons or [], full):
|
|
785
|
+
if not isinstance(t, ServoTendon):
|
|
786
|
+
t.tension = tfull
|
|
787
|
+
if stopped:
|
|
788
|
+
history[-1]["stopped"] = stopped
|
|
789
|
+
if beta_cap < opts.beta_max:
|
|
790
|
+
history[-1]["betaCap"] = beta_cap
|
|
791
|
+
xt = _extrude((h @ x) / hs, columns, design)
|
|
792
|
+
xp = _project(xt, beta, 0.5)
|
|
793
|
+
xp[pres] = 1.0
|
|
794
|
+
# the design as it will be printed: thresholded, void cells linear
|
|
795
|
+
try:
|
|
796
|
+
jt = nonlinear_response(model, (xp > 0.5).astype(float), opts.modulus, lvec, None, 2 * steps, 0.5,
|
|
797
|
+
follow=True)[0]
|
|
798
|
+
except (RuntimeError, ValueError):
|
|
799
|
+
jt = None
|
|
800
|
+
history[-1]["thresholdedOutput"] = None if jt is None else float(jt)
|
|
801
|
+
if best is not None and (jt is None or best[0] > jt):
|
|
802
|
+
# an earlier iterate of the sharpest phase prints better: return it
|
|
803
|
+
x, beta = best[1], best[2]
|
|
804
|
+
xt = _extrude((h @ x) / hs, columns, design)
|
|
805
|
+
xp = _project(xt, beta, 0.5)
|
|
806
|
+
xp[pres] = 1.0
|
|
807
|
+
history[-1]["thresholdedOutput"] = best[0]
|
|
808
|
+
history[-1]["keptIteration"] = best[3]
|
|
809
|
+
history[-1]["outputDisplacement"] = history[best[3] - 1]["outputDisplacement"]
|
|
810
|
+
return MechanismResult(x=x, xphys=xp, output_displacement=float(history[-1]["outputDisplacement"]),
|
|
811
|
+
iterations=it, seconds=time.time() - t0, history=history)
|