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.
Files changed (220) hide show
  1. loadpath/README.md +294 -0
  2. loadpath/SKILL.md +584 -0
  3. loadpath/__init__.py +17 -0
  4. loadpath/__main__.py +1202 -0
  5. loadpath/am.py +316 -0
  6. loadpath/amgfast.py +326 -0
  7. loadpath/assembly.py +985 -0
  8. loadpath/benchmarks/__init__.py +0 -0
  9. loadpath/benchmarks/beams.py +266 -0
  10. loadpath/benchmarks/cases/ge-bracket.json +380 -0
  11. loadpath/benchmarks/evaluate.py +172 -0
  12. loadpath/benchmarks/mechanisms.py +416 -0
  13. loadpath/benchmarks/problems.py +140 -0
  14. loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
  15. loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
  16. loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
  17. loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
  18. loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
  19. loadpath/benchmarks/reference.py +138 -0
  20. loadpath/benchmarks/run.py +301 -0
  21. loadpath/brep.py +1470 -0
  22. loadpath/bucklingopt.py +300 -0
  23. loadpath/buildsim.py +1490 -0
  24. loadpath/cadimport.py +904 -0
  25. loadpath/calibration.py +885 -0
  26. loadpath/cavities.py +211 -0
  27. loadpath/checks/__init__.py +0 -0
  28. loadpath/checks/dfm.py +354 -0
  29. loadpath/checks/exchanger.py +7 -0
  30. loadpath/checks/fea.py +212 -0
  31. loadpath/checks/fit.py +475 -0
  32. loadpath/checks/networks.py +260 -0
  33. loadpath/checks/opening.py +67 -0
  34. loadpath/checks/sdf_kit.py +7 -0
  35. loadpath/conjugate.py +267 -0
  36. loadpath/constrained.py +329 -0
  37. loadpath/contact.py +691 -0
  38. loadpath/coopt.py +342 -0
  39. loadpath/cores.py +163 -0
  40. loadpath/crease.py +1138 -0
  41. loadpath/critic.py +212 -0
  42. loadpath/crosscheck.py +1107 -0
  43. loadpath/cutcell.py +510 -0
  44. loadpath/data/lattice_buckling.json +439 -0
  45. loadpath/data/lattice_tensors.json +27449 -0
  46. loadpath/data/published_experiments.json +71 -0
  47. loadpath/decimate.py +177 -0
  48. loadpath/delivery.py +190 -0
  49. loadpath/directions.py +262 -0
  50. loadpath/domain.py +630 -0
  51. loadpath/drains.py +615 -0
  52. loadpath/durability.py +413 -0
  53. loadpath/editor.html +3135 -0
  54. loadpath/editor.py +610 -0
  55. loadpath/escape.py +354 -0
  56. loadpath/evals/__init__.py +0 -0
  57. loadpath/evals/baselines/coarse.json +1543 -0
  58. loadpath/evals/baselines/full.json +1497 -0
  59. loadpath/evals/brief_kinds/__init__.py +1 -0
  60. loadpath/evals/brief_kinds/heatx.py +387 -0
  61. loadpath/evals/brief_kinds/lattice.py +571 -0
  62. loadpath/evals/brief_kinds/multimat.py +593 -0
  63. loadpath/evals/briefs/assets/bracket.step +1201 -0
  64. loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
  65. loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
  66. loadpath/evals/briefs/assets/motor-body.stl +0 -0
  67. loadpath/evals/briefs/assets/sketch.png +0 -0
  68. loadpath/evals/briefs/base-adapter-fdm.json +492 -0
  69. loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
  70. loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
  71. loadpath/evals/briefs/camera-boom-mjf.json +339 -0
  72. loadpath/evals/briefs/camera-mount-pla.json +203 -0
  73. loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
  74. loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
  75. loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
  76. loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
  77. loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
  78. loadpath/evals/briefs/forearm-fdm.json +146 -0
  79. loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
  80. loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
  81. loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
  82. loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
  83. loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
  84. loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
  85. loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
  86. loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
  87. loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
  88. loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
  89. loadpath/evals/briefs/lighten-step-bracket.json +220 -0
  90. loadpath/evals/briefs/nema17-mount-stl.json +429 -0
  91. loadpath/evals/briefs/pillow-block-sls.json +220 -0
  92. loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
  93. loadpath/evals/briefs/rod-hook-pla.json +154 -0
  94. loadpath/evals/briefs/sensor-arm-resin.json +150 -0
  95. loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
  96. loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
  97. loadpath/evals/briefs/sketch-clevis.json +229 -0
  98. loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
  99. loadpath/evals/briefs/tool-flange-ti64.json +280 -0
  100. loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
  101. loadpath/evals/briefs/turret-fdm.json +281 -0
  102. loadpath/evals/briefs/upper-arm-sls.json +179 -0
  103. loadpath/evals/briefs/urdf-forearm.json +233 -0
  104. loadpath/evals/briefs/wrist-link-mjf.json +278 -0
  105. loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
  106. loadpath/evals/briefs.py +393 -0
  107. loadpath/evals/cases/arm-link-cast.json +157 -0
  108. loadpath/evals/cases/arm-link-driven.json +171 -0
  109. loadpath/evals/cases/arm-link-sandwich.json +315 -0
  110. loadpath/evals/cases/arm-link.json +111 -0
  111. loadpath/evals/cases/bell-crank.json +116 -0
  112. loadpath/evals/cases/grip-turret.json +479 -0
  113. loadpath/evals/cases/grip-upper-arm.json +253 -0
  114. loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
  115. loadpath/evals/cases/grip-wrist-yoke.json +831 -0
  116. loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
  117. loadpath/evals/cases/mbb-beam.json +61 -0
  118. loadpath/evals/cases/motor-body.stl +0 -0
  119. loadpath/evals/cases/motor-mount.json +265 -0
  120. loadpath/evals/cases/printed-hook.json +77 -0
  121. loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
  122. loadpath/evals/cases/wall-bracket.json +126 -0
  123. loadpath/evals/corpus.py +179 -0
  124. loadpath/evals/pareto.py +90 -0
  125. loadpath/evals/run.py +860 -0
  126. loadpath/examples/arm-link.json +25 -0
  127. loadpath/examples/bumpered-bracket-compression.json +225 -0
  128. loadpath/examples/bumpered-bracket-lattice.json +204 -0
  129. loadpath/examples/bumpered-bracket.json +34 -0
  130. loadpath/examples/gripper-finger-mm.json +185 -0
  131. loadpath/examples/heat-exchanger-counterflow.json +21 -0
  132. loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
  133. loadpath/examples/heat-exchanger-crossflow.json +20 -0
  134. loadpath/examples/pedal-lattice.json +23 -0
  135. loadpath/examples/pressure-plenum.json +12 -0
  136. loadpath/examples/shelf-bracket.json +27 -0
  137. loadpath/exchanger.py +831 -0
  138. loadpath/fasteners.py +199 -0
  139. loadpath/fem.py +668 -0
  140. loadpath/femops.py +70 -0
  141. loadpath/fieldjit.py +981 -0
  142. loadpath/fieldops.py +119 -0
  143. loadpath/fields.py +3697 -0
  144. loadpath/fieldtorch.py +464 -0
  145. loadpath/flow.py +984 -0
  146. loadpath/flowtopo.py +300 -0
  147. loadpath/gpu.py +521 -0
  148. loadpath/heatx.py +1067 -0
  149. loadpath/heatx_thermal.py +432 -0
  150. loadpath/homogenized.py +886 -0
  151. loadpath/ibrep.py +552 -0
  152. loadpath/impact.py +693 -0
  153. loadpath/infill.py +376 -0
  154. loadpath/interface_library.py +340 -0
  155. loadpath/interlock.py +186 -0
  156. loadpath/isolator.py +149 -0
  157. loadpath/knn.py +204 -0
  158. loadpath/lattice.py +1065 -0
  159. loadpath/lattice3mf.py +760 -0
  160. loadpath/lattice_buckling.py +245 -0
  161. loadpath/lattice_grade.py +325 -0
  162. loadpath/lattice_orient.py +595 -0
  163. loadpath/lattice_print.py +159 -0
  164. loadpath/lattice_tensors.py +558 -0
  165. loadpath/latticestep.py +1210 -0
  166. loadpath/manufacture.py +446 -0
  167. loadpath/material_library.py +489 -0
  168. loadpath/materials.py +144 -0
  169. loadpath/mcp_server.py +786 -0
  170. loadpath/mechanism.py +218 -0
  171. loadpath/mechanism_nl.py +811 -0
  172. loadpath/meshops.py +452 -0
  173. loadpath/mg.py +853 -0
  174. loadpath/mma.py +214 -0
  175. loadpath/modal.py +210 -0
  176. loadpath/multimat.py +1788 -0
  177. loadpath/narrowband.py +418 -0
  178. loadpath/nlkernel.py +241 -0
  179. loadpath/nonlinear.py +1095 -0
  180. loadpath/orient.py +334 -0
  181. loadpath/porous.py +994 -0
  182. loadpath/pressure.py +171 -0
  183. loadpath/preview.py +335 -0
  184. loadpath/printcheck.py +263 -0
  185. loadpath/printsetup.py +207 -0
  186. loadpath/processcheck.py +376 -0
  187. loadpath/quick.py +113 -0
  188. loadpath/recall.py +406 -0
  189. loadpath/reconstruct.py +797 -0
  190. loadpath/render.py +436 -0
  191. loadpath/resolved.py +189 -0
  192. loadpath/schema.py +1701 -0
  193. loadpath/sdf_kit.py +843 -0
  194. loadpath/selfx.py +292 -0
  195. loadpath/shapes.py +584 -0
  196. loadpath/sharp.py +717 -0
  197. loadpath/simp.py +1170 -0
  198. loadpath/sizing.py +312 -0
  199. loadpath/slicer.py +321 -0
  200. loadpath/spec.py +1072 -0
  201. loadpath/step.py +806 -0
  202. loadpath/struts.py +639 -0
  203. loadpath/study.py +1937 -0
  204. loadpath/tasks.py +822 -0
  205. loadpath/thermal.py +1691 -0
  206. loadpath/threemf.py +487 -0
  207. loadpath/tune.py +437 -0
  208. loadpath/validation.py +145 -0
  209. loadpath/verify.py +1215 -0
  210. loadpath/viewer.html +694 -0
  211. loadpath/viewer.py +446 -0
  212. loadpath/viscoelastic.py +106 -0
  213. loadpath/voxelprint.py +582 -0
  214. loadpath/voxels.py +402 -0
  215. loadpath-0.1.1.dist-info/METADATA +319 -0
  216. loadpath-0.1.1.dist-info/RECORD +220 -0
  217. loadpath-0.1.1.dist-info/WHEEL +5 -0
  218. loadpath-0.1.1.dist-info/entry_points.txt +2 -0
  219. loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
  220. 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}