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/flowtopo.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flow-aware topology optimization of heat sinks: the design decides where the
|
|
3
|
+
air goes, and the optimizer is charged for it.
|
|
4
|
+
|
|
5
|
+
thermal.py optimizes a sink with a fixed convection coefficient on its
|
|
6
|
+
surface. That rewards surface wherever it is, so on an air-cooled sink in a
|
|
7
|
+
duct it grows a dense pin field that chokes its own flow: rated with real
|
|
8
|
+
flow at a fixed fan pressure, it came out 25% worse than a uniform gyroid
|
|
9
|
+
and 20% worse than plain plate fins (docs/BENCHMARKS.md). Here the model has
|
|
10
|
+
the flow in it, at the cost of a coarser flow physics:
|
|
11
|
+
|
|
12
|
+
- flow: Darcy through the whole duct (Borrvall & Petersson's porosity
|
|
13
|
+
interpolation: inverse permeability alpha(rho) = alpha_f + (alpha_s -
|
|
14
|
+
alpha_f) rho (1 + q) / (rho + q)), driven by a fixed pressure drop
|
|
15
|
+
between the duct's inlet and outlet faces. Darcy has no wall friction, so
|
|
16
|
+
the open duct's alpha_f is a calibration: set it so a known design (plate
|
|
17
|
+
fins) passes what the lattice Boltzmann rating says it does at that
|
|
18
|
+
pressure;
|
|
19
|
+
- heat: conduction in metal and air (k(rho) = k_f + (k_s - k_f) rho^3) and
|
|
20
|
+
advection by the Darcy flux, first-order upwind, the air entering at 0 and
|
|
21
|
+
leaving through the outlet face; heat in on given cells;
|
|
22
|
+
- objective: the mean temperature of the heated cells; one volume
|
|
23
|
+
constraint; density filter, Heaviside projection with continuation, MMA.
|
|
24
|
+
|
|
25
|
+
The gradient is the adjoint through both solves: the heat adjoint lambda
|
|
26
|
+
(A^T lambda = dJ/dT), then the flow adjoint mu (K mu = -d(lambda^T A T)/dp),
|
|
27
|
+
since the design moves the temperature both by conducting and by steering
|
|
28
|
+
the flow. Checked against finite differences in tests/test_flowtopo.py.
|
|
29
|
+
|
|
30
|
+
Box grids only (the Sink of bench/lattice_heatsink.py): `design`, `solid`,
|
|
31
|
+
`fluid` are bool masks on one grid; everything else is an adiabatic,
|
|
32
|
+
impermeable wall. Units: m inside, mm at the edges, Pa, W, K.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import time
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
import scipy.sparse as sp
|
|
41
|
+
from scipy import ndimage
|
|
42
|
+
from scipy.sparse.linalg import splu
|
|
43
|
+
|
|
44
|
+
from .mma import MMA
|
|
45
|
+
|
|
46
|
+
__all__ = ["FlowSink", "FlowTopoOptions", "optimize_flow_thermal", "calibrate_alpha"]
|
|
47
|
+
|
|
48
|
+
Q_RAMP = 0.1 # Borrvall-Petersson q: how grey cells resist flow
|
|
49
|
+
ALPHA_RATIO = 1e4 # solid's inverse permeability over open duct's
|
|
50
|
+
K_PENALTY = 3.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FlowSink:
|
|
54
|
+
"""The two linear problems and their adjoints on one voxel grid."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, design, solid, fluid, heat, h, k_solid, fluid_props, dp, alpha_f, flow_axis=0):
|
|
57
|
+
self.shape = design.shape
|
|
58
|
+
self.live = design | solid | fluid
|
|
59
|
+
self.idx = -np.ones(self.shape, np.int64)
|
|
60
|
+
self.n = int(self.live.sum())
|
|
61
|
+
self.idx[self.live] = np.arange(self.n)
|
|
62
|
+
self.design = design[self.live]
|
|
63
|
+
self.fixed_rho = np.where(solid[self.live], 1.0, 0.0)
|
|
64
|
+
self.heat = heat[self.live]
|
|
65
|
+
self.hm = h * 1e-3
|
|
66
|
+
self.ks, self.kf = k_solid, fluid_props.k
|
|
67
|
+
self.rc = fluid_props.rho * fluid_props.cp
|
|
68
|
+
self.dp = float(dp)
|
|
69
|
+
self.alpha_f = float(alpha_f)
|
|
70
|
+
a, b = [], []
|
|
71
|
+
for ax in range(3):
|
|
72
|
+
s0 = [slice(None)] * 3
|
|
73
|
+
s1 = [slice(None)] * 3
|
|
74
|
+
s0[ax] = slice(0, -1)
|
|
75
|
+
s1[ax] = slice(1, None)
|
|
76
|
+
both = self.live[tuple(s0)] & self.live[tuple(s1)]
|
|
77
|
+
a.append(self.idx[tuple(s0)][both])
|
|
78
|
+
b.append(self.idx[tuple(s1)][both])
|
|
79
|
+
self.fa, self.fb = np.concatenate(a), np.concatenate(b)
|
|
80
|
+
lo = [slice(None)] * 3
|
|
81
|
+
hi = [slice(None)] * 3
|
|
82
|
+
lo[flow_axis], hi[flow_axis] = 0, -1
|
|
83
|
+
self.inlet = self.idx[tuple(lo)][fluid[tuple(lo)]]
|
|
84
|
+
self.outlet = self.idx[tuple(hi)][fluid[tuple(hi)]]
|
|
85
|
+
|
|
86
|
+
# -- interpolations
|
|
87
|
+
ratio = ALPHA_RATIO
|
|
88
|
+
|
|
89
|
+
def alpha(self, r):
|
|
90
|
+
return self.alpha_f * (1.0 + (self.ratio - 1.0) * r * (1 + Q_RAMP) / (r + Q_RAMP))
|
|
91
|
+
|
|
92
|
+
def dalpha(self, r):
|
|
93
|
+
return self.alpha_f * (self.ratio - 1.0) * (1 + Q_RAMP) * Q_RAMP / (r + Q_RAMP) ** 2
|
|
94
|
+
|
|
95
|
+
def k(self, r):
|
|
96
|
+
return self.kf + (self.ks - self.kf) * r ** K_PENALTY
|
|
97
|
+
|
|
98
|
+
def dk(self, r):
|
|
99
|
+
return (self.ks - self.kf) * K_PENALTY * r ** (K_PENALTY - 1)
|
|
100
|
+
|
|
101
|
+
def rho(self, xd):
|
|
102
|
+
r = self.fixed_rho.copy()
|
|
103
|
+
r[self.design] = xd
|
|
104
|
+
return r
|
|
105
|
+
|
|
106
|
+
# -- solves
|
|
107
|
+
def solve(self, r):
|
|
108
|
+
n, fa, fb, hm = self.n, self.fa, self.fb, self.hm
|
|
109
|
+
al = self.alpha(r)
|
|
110
|
+
G = 2.0 * hm / (al[fa] + al[fb]) # m^3/(s Pa) per face (Darcy, mu in alpha)
|
|
111
|
+
Gin = 2.0 * hm / al[self.inlet]
|
|
112
|
+
Gout = 2.0 * hm / al[self.outlet]
|
|
113
|
+
K = sp.coo_matrix((np.concatenate([G, G, -G, -G, Gin, Gout]),
|
|
114
|
+
(np.concatenate([fa, fb, fa, fb, self.inlet, self.outlet]),
|
|
115
|
+
np.concatenate([fa, fb, fb, fa, self.inlet, self.outlet]))), shape=(n, n)).tocsc()
|
|
116
|
+
bvec = np.zeros(n)
|
|
117
|
+
np.add.at(bvec, self.inlet, Gin * self.dp)
|
|
118
|
+
Klu = splu(K)
|
|
119
|
+
p = Klu.solve(bvec)
|
|
120
|
+
Q = G * (p[fa] - p[fb])
|
|
121
|
+
F = self.rc * Q
|
|
122
|
+
Fo = self.rc * Gout * p[self.outlet]
|
|
123
|
+
kk = self.k(r)
|
|
124
|
+
g = 2.0 * kk[fa] * kk[fb] / (kk[fa] + kk[fb]) * hm
|
|
125
|
+
fp, fm = np.maximum(F, 0.0), np.maximum(-F, 0.0)
|
|
126
|
+
rows = np.concatenate([fa, fb, fa, fb, fa, fa, fb, fb])
|
|
127
|
+
cols = np.concatenate([fb, fa, fa, fb, fa, fb, fb, fa])
|
|
128
|
+
vals = np.concatenate([-g, -g, g, g, fp, -fm, fm, -fp])
|
|
129
|
+
A = sp.coo_matrix((vals, (rows, cols)), shape=(n, n)).tocsr()
|
|
130
|
+
A = (A + sp.coo_matrix((Fo, (self.outlet, self.outlet)), shape=(n, n))).tocsc()
|
|
131
|
+
q = np.zeros(n)
|
|
132
|
+
q[self.heat] = 1.0 / self.heat.sum() # 1 W; T scales with power
|
|
133
|
+
Alu = splu(A)
|
|
134
|
+
T = Alu.solve(q)
|
|
135
|
+
return dict(r=r, al=al, G=G, Gin=Gin, Gout=Gout, p=p, F=F, Fo=Fo, kk=kk, g=g, T=T, Klu=Klu, Alu=Alu,
|
|
136
|
+
flow=float(Gin @ (self.dp - p[self.inlet])))
|
|
137
|
+
|
|
138
|
+
def objective(self, s):
|
|
139
|
+
"""Mean temperature rise of the heated cells, K per W."""
|
|
140
|
+
return float(s["T"][self.heat].mean())
|
|
141
|
+
|
|
142
|
+
def gradient(self, s):
|
|
143
|
+
"""dJ/drho on every live cell (the adjoint of both solves)."""
|
|
144
|
+
fa, fb, hm, rc = self.fa, self.fb, self.hm, self.rc
|
|
145
|
+
T, p, F = s["T"], s["p"], s["F"]
|
|
146
|
+
dJdT = np.zeros(self.n)
|
|
147
|
+
dJdT[self.heat] = 1.0 / self.heat.sum()
|
|
148
|
+
lam = s["Alu"].solve(dJdT, trans="T")
|
|
149
|
+
dl = lam[fa] - lam[fb]
|
|
150
|
+
up = np.where(F >= 0, T[fa], T[fb])
|
|
151
|
+
sf = dl * up # d(lam^T A T)/dF per face
|
|
152
|
+
so = lam[self.outlet] * T[self.outlet]
|
|
153
|
+
# flow adjoint: K mu = -w, w = d(lam^T A T)/dp
|
|
154
|
+
w = np.zeros(self.n)
|
|
155
|
+
np.add.at(w, fa, rc * sf * s["G"])
|
|
156
|
+
np.add.at(w, fb, -rc * sf * s["G"])
|
|
157
|
+
np.add.at(w, self.outlet, rc * so * s["Gout"])
|
|
158
|
+
mu = s["Klu"].solve(-w)
|
|
159
|
+
r, kk, al = s["r"], s["kk"], s["al"]
|
|
160
|
+
grad = np.zeros(self.n)
|
|
161
|
+
# conduction
|
|
162
|
+
term = (T[fa] - T[fb]) * dl
|
|
163
|
+
ka, kb = kk[fa], kk[fb]
|
|
164
|
+
dga = 2.0 * kb ** 2 / (ka + kb) ** 2 * hm
|
|
165
|
+
dgb = 2.0 * ka ** 2 / (ka + kb) ** 2 * hm
|
|
166
|
+
dk = self.dk(r)
|
|
167
|
+
np.add.at(grad, fa, -term * dga * dk[fa])
|
|
168
|
+
np.add.at(grad, fb, -term * dgb * dk[fb])
|
|
169
|
+
# flow conductances (through F at fixed p, and the flow residual)
|
|
170
|
+
da = self.dalpha(r)
|
|
171
|
+
dG = -2.0 * hm / (al[fa] + al[fb]) ** 2 # dG/dalpha_a = dG/dalpha_b
|
|
172
|
+
dpf = p[fa] - p[fb]
|
|
173
|
+
cf = -(sf * rc * dpf + (mu[fa] - mu[fb]) * dpf) * dG
|
|
174
|
+
np.add.at(grad, fa, cf * da[fa])
|
|
175
|
+
np.add.at(grad, fb, cf * da[fb])
|
|
176
|
+
i, o = self.inlet, self.outlet
|
|
177
|
+
dGi = -2.0 * hm / al[i] ** 2
|
|
178
|
+
dGo = -2.0 * hm / al[o] ** 2
|
|
179
|
+
np.add.at(grad, i, -(mu[i] * (p[i] - self.dp)) * dGi * da[i])
|
|
180
|
+
np.add.at(grad, o, -(so * rc * p[o] + mu[o] * p[o]) * dGo * da[o])
|
|
181
|
+
return grad
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def calibrate_alpha(solid_of_design, fluid_of_design, h, dp, flow_lpm, flow_axis=0):
|
|
185
|
+
"""alpha_f for which a given (crisp) design passes `flow_lpm` at `dp` Pa
|
|
186
|
+
in the Darcy model. Darcy is linear and the solid barely conducts flow,
|
|
187
|
+
so one solve at alpha_f = 1 and a rescale does it."""
|
|
188
|
+
from .heatx_thermal import FLUIDS
|
|
189
|
+
|
|
190
|
+
fs = FlowSink(np.zeros_like(solid_of_design), solid_of_design, fluid_of_design, solid_of_design, h,
|
|
191
|
+
1.0, FLUIDS["air"], dp, 1.0, flow_axis)
|
|
192
|
+
s = fs.solve(fs.fixed_rho)
|
|
193
|
+
return s["flow"] / (flow_lpm / 60000.0)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class FlowTopoOptions:
|
|
198
|
+
volume_fraction: float
|
|
199
|
+
filter_radius: float = 2.0 # mm
|
|
200
|
+
max_iterations: int = 120
|
|
201
|
+
beta_max: float = 16.0
|
|
202
|
+
beta_every: int = 15
|
|
203
|
+
move: float = 0.1
|
|
204
|
+
# The solid's flow resistance grows from 10x the open duct's to
|
|
205
|
+
# ALPHA_RATIO over this many iterations. At full contrast from the first
|
|
206
|
+
# iteration a uniform grey start chokes the duct (alpha at rho 0.2 is
|
|
207
|
+
# ~7000x open), the flow and its gradient vanish, and the run settled at
|
|
208
|
+
# 13.7 K/W where plate fins read 4.2 in the same model.
|
|
209
|
+
ratio_iterations: int = 60
|
|
210
|
+
x0: np.ndarray | None = None # warm start, densities on the design cells
|
|
211
|
+
# hold the volume exactly (two-sided), for comparisons at equal metal;
|
|
212
|
+
# otherwise it is an upper bound and the optimizer may use less (more
|
|
213
|
+
# open duct, more flow)
|
|
214
|
+
equal_volume: bool = False
|
|
215
|
+
beta0: float = 1.0 # projection sharpness at the start (raise it for a crisp warm start)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _cone(radius_cells):
|
|
219
|
+
r = int(np.floor(radius_cells))
|
|
220
|
+
g = np.mgrid[-r:r + 1, -r:r + 1, -r:r + 1]
|
|
221
|
+
w = np.maximum(0.0, radius_cells - np.sqrt((g ** 2).sum(0)))
|
|
222
|
+
return w
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _project(x, beta, eta=0.5):
|
|
226
|
+
d = np.tanh(beta * eta) + np.tanh(beta * (1 - eta))
|
|
227
|
+
return (np.tanh(beta * eta) + np.tanh(beta * (x - eta))) / d
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _dproject(x, beta, eta=0.5):
|
|
231
|
+
d = np.tanh(beta * eta) + np.tanh(beta * (1 - eta))
|
|
232
|
+
return beta * (1 - np.tanh(beta * (x - eta)) ** 2) / d
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def optimize_flow_thermal(fs: FlowSink, design_grid: np.ndarray, h: float, opts: FlowTopoOptions,
|
|
236
|
+
on_iter=None) -> dict:
|
|
237
|
+
"""Lay out volume_fraction of the design cells to minimize the heated
|
|
238
|
+
cells' mean temperature at the fixed pressure drop. Returns {"x": filtered
|
|
239
|
+
and projected density on the grid (design cells), "objective", "history"}."""
|
|
240
|
+
t0 = time.time()
|
|
241
|
+
w = _cone(opts.filter_radius / h)
|
|
242
|
+
norm = ndimage.convolve(design_grid.astype(float), w, mode="constant")
|
|
243
|
+
norm = np.where(design_grid, norm, 1.0)
|
|
244
|
+
sel = design_grid
|
|
245
|
+
nd = int(sel.sum())
|
|
246
|
+
|
|
247
|
+
def filt(xd):
|
|
248
|
+
g = np.zeros(design_grid.shape)
|
|
249
|
+
g[sel] = xd
|
|
250
|
+
return (ndimage.convolve(g, w, mode="constant") / norm)[sel]
|
|
251
|
+
|
|
252
|
+
def filt_t(gd):
|
|
253
|
+
g = np.zeros(design_grid.shape)
|
|
254
|
+
g[sel] = gd
|
|
255
|
+
return ndimage.convolve(g / norm, w, mode="constant")[sel]
|
|
256
|
+
|
|
257
|
+
# live-cell order of the design cells, matching design_grid[sel] order
|
|
258
|
+
order = fs.idx[sel]
|
|
259
|
+
x = np.full(nd, opts.volume_fraction) if opts.x0 is None else np.clip(np.asarray(opts.x0, float), 0, 1)
|
|
260
|
+
mma = MMA(nd, 2 if opts.equal_volume else 1, move=opts.move)
|
|
261
|
+
beta, since = float(opts.beta0), 0
|
|
262
|
+
hist = []
|
|
263
|
+
for it in range(1, opts.max_iterations + 1):
|
|
264
|
+
frac = 1.0 if opts.ratio_iterations <= 0 else min(1.0, (it - 1) / opts.ratio_iterations)
|
|
265
|
+
fs.ratio = 10.0 ** (1.0 + (np.log10(ALPHA_RATIO) - 1.0) * frac)
|
|
266
|
+
xt = filt(x)
|
|
267
|
+
xp = _project(xt, beta)
|
|
268
|
+
r = fs.fixed_rho.copy()
|
|
269
|
+
r[order] = xp
|
|
270
|
+
s = fs.solve(r)
|
|
271
|
+
J = fs.objective(s)
|
|
272
|
+
gr = fs.gradient(s)[order]
|
|
273
|
+
dJ = filt_t(gr * _dproject(xt, beta))
|
|
274
|
+
vol = xp.mean() / opts.volume_fraction - 1.0
|
|
275
|
+
dv = filt_t(_dproject(xt, beta) / (nd * opts.volume_fraction))
|
|
276
|
+
if it == 1 or frac < 1.0:
|
|
277
|
+
# the model stiffens under the optimizer while the ratio climbs
|
|
278
|
+
scale = 1.0 / max(abs(J), 1e-12)
|
|
279
|
+
if opts.equal_volume:
|
|
280
|
+
xn = mma.update(x, J * scale, dJ * scale, np.array([vol, -vol]), np.stack([dv, -dv]))
|
|
281
|
+
else:
|
|
282
|
+
xn = mma.update(x, J * scale, dJ * scale, np.array([vol]), dv[None, :])
|
|
283
|
+
change = float(np.abs(xn - x).max())
|
|
284
|
+
x = xn
|
|
285
|
+
since += 1
|
|
286
|
+
rec = {"iteration": it, "objective": J, "flowLpm": s["flow"] * 60000.0, "volume": float(xp.mean()),
|
|
287
|
+
"beta": beta, "change": change, "seconds": time.time() - t0}
|
|
288
|
+
hist.append(rec)
|
|
289
|
+
if on_iter:
|
|
290
|
+
on_iter(rec)
|
|
291
|
+
if beta < opts.beta_max and (since >= opts.beta_every or change < 0.01):
|
|
292
|
+
beta, since = min(2 * beta, opts.beta_max), 0
|
|
293
|
+
elif beta >= opts.beta_max and change < 0.005 and frac >= 1.0 and it >= 10:
|
|
294
|
+
break
|
|
295
|
+
xt = filt(x)
|
|
296
|
+
xp = _project(xt, beta)
|
|
297
|
+
g = np.zeros(design_grid.shape)
|
|
298
|
+
g[sel] = xp
|
|
299
|
+
fs.ratio = ALPHA_RATIO
|
|
300
|
+
return {"x": g, "objective": hist[-1]["objective"], "history": hist, "seconds": time.time() - t0}
|