Note
Go to the end to download the full example code.
Showcase for 3D thermal heat-sink
This example showcases the topology optimization of thermal loadcase for a heat-sink
The structure is subjected to a distributed heat load and the optimization seeks to minimize the thermal resistance. As the problem is 2-fold symmetric, only a quarter of the domain needs analysis and optimization. With post-processing the design can be mirrored to restore the complete domain (e.g. using Paraview).
This example is based on Multigrid preconditioned CG solver.
12 import numpy as np
13 import pymoto as pym
14
15 n = 128 # Resolution (# elements). Use powers of 2 for good GMG performance (e.g. 4, 8, 16, ...)
16 nx, ny, nz = n, n, 2*n # Domain sizes (# elements)
17 Lx = 0.1 # Length in (m), other dimensions are deduced from `ny` and `nz`
18 xmin = 1e-9 # Minimum density value
19 filter_radius = 3.0 # Filter radius
20 volfrac = 0.1 # Target volume fraction
21
22 if __name__ == "__main__":
23 print(__doc__)
24
25 # Setup domain
26 h = Lx/nx # Element size
27 domain = pym.VoxelDomain(nx, ny, nz, unitx=h, unity=h, unitz=h) # Setup domain object
28
29 # BC heat sink at bottom
30 boundary_dofs = domain.nodes[:-int(nx/2), :-int(ny/2), 0].flatten()
31
32 # Load vector on top section of domain
33 eidx_load = domain.elements[:, :, int(nz/4):].flatten()
34 nidx_load = domain.conn[eidx_load, :]
35 f = np.zeros(domain.nnodes)
36 np.add.at(f, nidx_load, 1.0)
37 f *= 1 / (4*np.sum(f)) # Normalize to 1/4W unit heat input (1W for the total symmetric domain)
38
39 # Make design vector and fill with initial values
40 sx = pym.Signal('x', state=np.ones(domain.nel) * volfrac)
41
42 # Padding elements for density filter (set densities to 0)
43 delem = int(np.ceil(filter_radius))
44 delem0 = delem - 1
45 eidx_xmax = domain.elements[-delem0:, :, :].flatten()
46 eidx_ymax = domain.elements[:, -delem0:, :].flatten()
47 eidx_zmax = domain.elements[:, :, -delem0:].flatten()
48 eidx_0 = np.unique(np.concatenate([eidx_xmax, eidx_ymax, eidx_zmax]))
49
50 # Start building the modular network
51 with pym.Network() as func:
52 # Set elements at the edge to zero to prevent the design 'sticking' to the boundary
53 sx0 = pym.SetValue(indices=eidx_0, value=0.0)(sx)
54
55 # Density filter
56 sxfilt = pym.FilterConv(domain, radius=filter_radius, xmax_bc=0, ymax_bc=0, zmax_bc=0)(sx0)
57 sx_analysis = sxfilt
58 sx_analysis.tag = 'design'
59
60 # SIMP material interpolation
61 sSIMP = pym.MathExpression(f"{xmin} + {1.0 - xmin}*inp0^3")(sx_analysis)
62
63 # System matrix assembly module
64 sK = pym.AssemblePoisson(domain, bc=boundary_dofs, material_property=237.)(sSIMP)
65
66 # Linear system solver. The linear solver can be chosen by uncommenting any of the following lines.
67 solver = None # Default (solver is automatically chosen based on matrix properties)
68 # solver = pym.solvers.SolverSparsePardiso() # Requires Intel MKL installed
69 # solver = pym.solvers.SolverSparseCholeskyCVXOPT() # Requires cvxopt installed
70 # solver = pym.solvers.SolverSparseCholeskyScikit() # Requires scikit installed
71
72 # Iterative solver: CG with geometric multigrid preconditioning
73 # Set up fine level multigrid operator
74 mg1 = pym.solvers.GeometricMultigrid(domain)
75 mgs = [mg1] # List of multigrid levels, starting with the finest grid
76 while True: # Setup multiple sub-levels
77 # Stop if any of the new dimensions is odd
78 if any([nn % 2 != 0 for nn in mgs[-1].sub_domain.size]):
79 break
80
81 # Stop if any of the coarse dimensions is smaller than 8
82 if any(mgs[-1].sub_domain.size < 8):
83 break
84
85 # Create a new coarser grid
86 mgs.append(pym.solvers.GeometricMultigrid(mgs[-1].sub_domain))
87 mgs[-2].inner_level = mgs[-1] # Set the inner level to the next coarser grid
88
89 print(f"Fine grid size: {domain.nelx} x {domain.nely} x {domain.nelz}")
90 print(f"Multigrid levels: {len(mgs)}")
91 print(f"Coarsest grid size: {mgs[-1].sub_domain.nelx} x {mgs[-1].sub_domain.nely} x {mgs[-1].sub_domain.nelz}")
92
93 # Set up the solver (comment out to use the default factorization, try this to see the difference in time)
94 solver = pym.solvers.CG(preconditioner=mg1, verbosity=0, tol=1e-5)
95
96 su = pym.LinSolve(hermitian=True, solver=solver)(sK, f)
97
98 # Output the design, deformation, and force field to a Paraview file
99 pym.WriteToVTI(domain, saveto='out/dat.vti')(sx_analysis, su)
100
101 # Compliance calculation c = f^T u
102 scompl = pym.EinSum('i,i->')(su, f)
103 scompl.tag = 'compliance'
104
105 # MMA needs correct scaling of the objective
106 sg0 = pym.Scaling(scaling=100.0)(scompl)
107 sg0.tag = "objective"
108
109 # Calculate the volume of the domain by adding all design densities together
110 svol = pym.EinSum('i->')(sx_analysis)
111 svol.tag = 'volume'
112
113 # Volume constraint
114 sg1 = pym.Scaling(scaling=10.0, maxval=volfrac*domain.nel)(svol)
115 sg1.tag = "volume constraint"
116
117 pym.PlotIter()(sg0, sg1) # Plot iteration history
118
119 # Do the optimization with MMA
120 # pym.minimize_mma([sx], [sg0, sg1], function=func)
121
122 # Do the optimization with OC
123 pym.minimize_oc(sx, sg0, function=func)
124
125 print("The optimization has finished!")
126 print(f"The final compliance value obtained is {scompl.state}")
127 print(f"The maximum displacement is {max(np.absolute(su.state))}")