Multigrid preconditioned CG solver

Significant speedups can be achieved using CG with a multigrid preconditioner to solve the linear system of equations

The speedup enables large-scale topology optimization problems, which is achieved by projecting the finite element problem onto a series of coarser grids, and only factorizing the finite element matrix at the coarsest grid. The solution is then interpolated back to the fine grid. This process is used as preconditioner for the CG iterations, enabling fast convergence and optimization in 3D.

This example is based on Compliance minimization and only differs in the solver used. The user has full control over the number of multigrid levels and their internal settings (see pymoto.solvers.GeometricMultigrid and pymoto.solvers.CG for more details).

References:

Amir, O., Aage, N., & Lazarov, B. S. (2014). On multigrid-CG for efficient topology optimization. Structural and Multidisciplinary Optimization, 49(5), 815-829. DOI: https://doi.org/10.1007/s00158-013-1015-5

 21 import numpy as np
 22 import pymoto as pym
 23
 24 # nx, ny, nz = 32, 64, 64  # Domain sizes (# elements). Set nz to zero for the 2D problem, nz > 0 runs a 3D problem
 25 nx, ny, nz = 512, 256, 0  # 2D
 26 xmin = 1e-9
 27 filter_radius = 2.0
 28 volfrac = 0.5
 29 thermal = False  # True = Static thermal analysis; False = Static mechanical analysis will be done
 30
 31 if __name__ == "__main__":
 32     print(__doc__)
 33
 34     if nz == 0:  # 2D analysis
 35         # Generate a grid
 36         domain = pym.VoxelDomain(nx, ny)
 37
 38         if thermal:
 39             ndof = 1  # Number of dofs per node
 40             # Get dof numbers at the boundary
 41             boundary_dofs = domain.nodes[0, ny//4:((ny + 1) - ny // 4)].flatten()
 42
 43             # Make a force vector
 44             force_dofs = domain.nodes[1:, :].flatten()
 45
 46         else:  # Mechanical
 47             ndof = 2
 48             # Calculate boundary dof indices
 49             boundary_nodes = domain.nodes[0, :].flatten()
 50             boundary_dofs = domain.get_dofnumber(boundary_nodes, ndof=ndof)
 51
 52             # Which dofs to put a force on? The 1 is added for a force in y-direction (x-direction would be zero)
 53             force_dofs = ndof * domain.nodes[nx, ny//2] + 1
 54
 55     else:
 56         domain = pym.VoxelDomain(nx, ny, nz)
 57         boundary_nodes = domain.nodes[0, :, :].flatten()
 58
 59         if thermal:
 60             boundary_dofs = boundary_nodes
 61             force_dofs = domain.nodes[1:, :, :].flatten()
 62             ndof = 1
 63
 64         else:  # Mechanical
 65             ndof = 3
 66             boundary_dofs = domain.get_dofnumber(boundary_nodes, ndof=ndof)
 67             force_dofs = ndof * domain.get_nodenumber(nx, ny // 2, nz // 2) + 2  # Z-direction
 68
 69     if domain.nnodes > 1e+6:
 70         print("Too many nodes :(")  # Safety, to prevent overloading the memory in your machine
 71         exit()
 72
 73     # Generate a force vector
 74     f = np.zeros(domain.nnodes * ndof)
 75     f[force_dofs] = 1.0  # Uniform force of 1.0 at all selected dofs
 76
 77     # Make design vector and fill with initial values
 78     sx = pym.Signal('x', state=np.ones(domain.nel) * volfrac)
 79
 80     # Start building the modular network
 81     with pym.Network(print_timing=True) as func:
 82         # Filter
 83         sxfilt = pym.FilterConv(domain, radius=filter_radius)(sx)
 84         sx_analysis = sxfilt
 85         sx_analysis.tag = 'design'
 86
 87         # Show the design on the screen as it optimizes (plotting domain in 3D may take significant time)
 88         if nz == 0:
 89             pym.PlotDomain(domain, saveto="out/design", clim=[0, 1])(sx_analysis)
 90
 91         # SIMP material interpolation
 92         sSIMP = pym.MathExpression(f"{xmin} + {1.0 - xmin}*inp0^3")(sx_analysis)
 93
 94         # System matrix assembly module
 95         if thermal:
 96             sK = pym.AssemblePoisson(domain, bc=boundary_dofs)(sSIMP)
 97         else:
 98             sK = pym.AssembleStiffness(domain, bc=boundary_dofs)(sSIMP)
 99
100         # Linear system solver. The linear solver can be chosen by uncommenting any of the following lines.
101         solver = None  # Default (solver is automatically chosen based on matrix properties)
102         # solver = pym.solvers.SolverSparsePardiso()  # Requires Intel MKL installed
103         # solver = pym.solvers.SolverSparseCholeskyCVXOPT()  # Requires cvxopt installed
104         # solver = pym.solvers.SolverSparseCholeskyScikit()  # Requires scikit installed
105
106         ''' Iterative solver: CG with geometric multigrid preconditioning '''
107         # Set up fine level multigrid operator
108         mg1 = pym.solvers.GeometricMultigrid(domain)
109         mgs = [mg1] # List of multigrid levels, starting with the finest grid
110         while True:  # Setup multiple sub-levels
111             # Stop if any of the new dimensions is odd
112             if any([nn % 2 != 0 for nn in mgs[-1].sub_domain.size]):
113                 break
114
115             # Stop if any of the coarse dimensions is smaller than 8
116             if any(mgs[-1].sub_domain.size < 8):
117                 break
118
119             # Create a new coarser grid
120             mgs.append(pym.solvers.GeometricMultigrid(mgs[-1].sub_domain))
121             mgs[-2].inner_level = mgs[-1]  # Set the inner level to the next coarser grid
122
123         print(f"Multigrid levels: {len(mgs)}")
124         print(f"Coarsest grid size: {mgs[-1].sub_domain.nelx} x {mgs[-1].sub_domain.nely} " +
125               (f"x {mgs[-1].sub_domain.nelz}" if nz > 0 else ""))
126
127         # Set up the solver (comment out to use the default factorization, try this to see the difference in time)
128         solver = pym.solvers.CG(preconditioner=mg1, verbosity=1, tol=1e-5)
129
130         ''' From here the rest of the example is identical to ex_compliance.py '''
131         su = pym.LinSolve(hermitian=True, solver=solver)(sK, f)
132
133         # Output the design, deformation, and force field to a Paraview file
134         pym.WriteToVTI(domain, saveto='out/dat.vti')(sx_analysis, su, f)
135
136         # Compliance calculation c = f^T u
137         scompl = pym.EinSum('i,i->')(su, f)
138         scompl.tag = 'compliance'
139
140         # MMA needs correct scaling of the objective
141         sg0 = pym.Scaling(scaling=100.0)(scompl)
142         sg0.tag = "objective"
143
144         # Calculate the volume of the domain by adding all design densities together
145         svol = pym.EinSum('i->')(sx_analysis)
146         svol.tag = 'volume'
147
148         # Volume constraint
149         sg1 = pym.MathExpression(f'10*(inp0/{domain.nel} - {volfrac})')(svol)
150         sg1.tag = "volume constraint"
151
152     # Maybe you want to check the design-sensitivities?
153     do_finite_difference = False
154     if do_finite_difference:
155         pym.finite_difference(sx, [sg0, sg1], function=func, dx=1e-4)
156         exit()
157
158     with func:
159         pym.PlotIter()(sg0, sg1)  # Plot iteration history
160
161     # Do the optimization with MMA
162     pym.minimize_mma([sx], [sg0, sg1], function=func)
163
164     # Do the optimization with OC
165     # pym.minimize_oc(sx, sg0, function=func)
166
167     # Here you can do some post processing
168     print("The optimization has finished!")
169     print(f"The final compliance value obtained is {scompl.state}")
170     print(f"The maximum {'temperature' if thermal else 'displacement'} is {max(np.absolute(su.state))}")

Gallery generated by Sphinx-Gallery