Note
Go to the end to download the full example code.
Robust formulation
The robust formulation ensures that the design is tolerant for manufacturing deviations and enables control on minimum feature size
It is implemented using Heaviside projections on the design, generating three variants: a nominal design, an eroded design and a dilated design. The worst-case of these three designs is used in the objective function. Note that for a compliance minimization problem, the eroded design is always the worst. Hence it is sometimes called the ‘poor-man’s robust formulation’. The ‘full’ version of the robust method can be found in Full robust formulation, which evaluates each of the eroded, nominal, and dilated designs.
In this example, the pymoto.MathExpression module is used extensively to automatically generate the
sensitivities of the Heaviside projection filters.
- References:
Wang, F., Lazarov, B. S., & Sigmund, O. (2011). On projection methods, convergence and robust formulations in topology optimization. Structural and multidisciplinary optimization, 43, 767-784. DOI: https://doi.org/10.1007/s00158-010-0602-y
23 import numpy as np
24 import pymoto as pym
25
26 nx, ny = 100, 40
27 xmin = 1e-5
28 filter_radius = 5.0
29 volfrac = 0.5
30
31
32 class Continuation(pym.Module):
33 """ Module that generates a continuated value """
34
35 def __init__(self, start=0.0, stop=1.0, nsteps=80, stepstart=10, interval=10):
36 self.startval = start
37 self.endval = stop
38 self.interval = interval
39 self.dval = (stop - start) / nsteps
40 self.nstart = stepstart
41 self.iter = -1
42 self.val = self.startval
43
44 def __call__(self):
45 maxval = max(self.startval, self.endval)
46 minval = min(self.startval, self.endval)
47 if self.iter % self.interval == 0: # Only update value every `interval` iterations
48 self.val = np.clip(self.startval + self.dval * (self.iter - self.nstart), minval, maxval)
49
50 self.iter += 1
51 return self.val
52
53
54 if __name__ == "__main__":
55 print(__doc__)
56
57 # --- SETUP ---
58 # Generate a grid
59 domain = pym.VoxelDomain(nx, ny)
60
61 # Chose which physics to solve: structural or thermal
62 physics = "structural" # "strucural" or "thermal"
63 # (the thermal problem with robust is a lot harder for the optimizer)
64 if physics == "structural":
65 # STRUCTURAL
66 # Calculate boundary dof indices
67 boundary_nodes = domain.get_nodenumber(0, np.arange(ny + 1))
68 boundary_dofs = domain.get_dofnumber(boundary_nodes, np.arange(2), 2).flatten()
69
70 # Generate a force vector
71 force_dofs = 2 * domain.get_nodenumber(nx, ny // 2)
72 ndof = 2 # Number of displacements per node
73
74 # Compute element stiffness matrix
75 nu, E = 0.3, 1.0
76 lx, ly, lz = 1.0, 1.0, 1.0
77 c = ly / lx
78 ka = (4 * c) * (1 - nu)
79 kc = (4 / c) * (1 - nu)
80 kd = (2 * c) * (1 - 2 * nu)
81 kb = (2 / c) * (1 - 2 * nu)
82 kf = 6 * nu - 3 / 2
83 ke = E * lz / (12 * (1 + nu) * (1 - 2 * nu))
84
85 el = ke * np.array([
86 [ka + kb, -3 / 2, ka / 2 - kb, kf, -ka + kb / 2, -kf, -ka / 2 - kb / 2, 3 / 2],
87 [-3 / 2, kc + kd, -kf, -kc + kd / 2, kf, kc / 2 - kd, 3 / 2, -kc / 2 - kd / 2],
88 [ka / 2 - kb, -kf, ka + kb, 3 / 2, -ka / 2 - kb / 2, -3 / 2, -ka + kb / 2, kf],
89 [kf, -kc + kd / 2, 3 / 2, kc + kd, -3 / 2, -kc / 2 - kd / 2, -kf, kc / 2 - kd],
90 [-ka + kb / 2, kf, -ka / 2 - kb / 2, -3 / 2, ka + kb, 3 / 2, ka / 2 - kb, -kf],
91 [-kf, kc / 2 - kd, -3 / 2, -kc / 2 - kd / 2, 3 / 2, kc + kd, kf, -kc + kd / 2],
92 [-ka / 2 - kb / 2, 3 / 2, -ka + kb / 2, -kf, ka / 2 - kb, kf, ka + kb, -3 / 2],
93 [3 / 2, -kc / 2 - kd / 2, kf, kc / 2 - kd, -kf, -kc + kd / 2, -3 / 2, kc + kd],
94 ])
95
96 elif physics == "thermal":
97 # THERMAL
98 # Get dof numbers at the boundary
99 boundary_dofs = domain.get_nodenumber(0, np.arange(ny // 4, (3 * ny) // 4))
100
101 # Make a force vector
102 force_dofs = domain.get_nodenumber(*np.meshgrid(np.arange(nx // 4, nx + 1), np.arange(ny + 1)))
103 ndof = 1 # Number of dofs per node
104
105 # Element conductivity matrix
106 el = 1 / 6 * np.array([
107 [8, -2, -2, -4],
108 [-2, 8, -4, -2],
109 [-2, -4, 8, -2],
110 [-4, -2, -2, 8]
111 ])
112 else:
113 raise RuntimeError("Unknown physics: {}".format(physics))
114
115 # In this example the filter is (virtually) padded with zeros, around the boundary condition we don't want that to
116 # happen. This vector sets the elements for which no padding is applied.
117 pad = domain.elements[:int(filter_radius)+1, :].flatten()
118
119 # Make force and design vector, and fill with initial values
120 f = np.zeros(domain.nnodes * ndof)
121 f[force_dofs] = 1.0
122
123 sx = pym.Signal('x', state=np.ones(domain.nel) * volfrac)
124
125 # Start building the modular network
126 with pym.Network() as fn:
127 # Filter
128 sxfilt = pym.DensityFilter(domain=domain, radius=filter_radius, nonpadding=pad)(sx)
129 sxfilt.tag = "filtered design"
130
131 # Heaviside projections
132 sBeta = Continuation(start=1e-3, stop=40.0, stepstart=10, nsteps=80, interval=5)()
133 sBeta.tag = "beta"
134
135 pym.Print()(sBeta) # Print the beta value each iteration
136
137 etaDi, etaNo, etaEr = 0.4, 0.5, 0.6
138
139 heaviside = "(tanh(inp1 * {0}) + tanh(inp1 * (inp0 - {0}))) / (tanh(inp1 * {0}) + tanh(inp1 * (1 - {0})))"
140 sxNom = pym.MathExpression(heaviside.format(etaNo))(sxfilt, sBeta)
141 sxEr = pym.MathExpression(heaviside.format(etaEr))(sxfilt, sBeta)
142 sxDi = pym.MathExpression(heaviside.format(etaDi))(sxfilt, sBeta)
143
144 sxNom.tag = "nominal"
145 sxEr.tag = "eroded"
146 sxDi.tag = "dilated"
147
148 # SIMP material interpolation
149 sSIMP = pym.MathExpression(f"{xmin} + {1-xmin}*inp0^3")(sxEr)
150
151 # Add stiffness assembly module
152 sK = pym.AssembleGeneral(domain, element_matrix=el, bc=boundary_dofs)(sSIMP)
153
154 # Linear system solver
155 su = pym.LinSolve()(sK, f)
156
157 # Compliance calculation
158 sc = pym.EinSum('i,i->')(su, f)
159
160 # Objective scaling
161 sg0 = pym.Scaling(scaling=100.0)(sc)
162 sg0.tag = "objective"
163
164 # Plot design
165 pym.PlotDomain(domain, saveto="out/design")(sxNom)
166
167 # Volume
168 s_volume = pym.EinSum('i->')(sxNom)
169
170 # Volume constraint
171 s_gvol = pym.Scaling(scaling=10, maxval=volfrac * domain.nel)(s_volume)
172 s_gvol.tag = "Volume constraint"
173
174 pym.PlotIter()(sg0)
175
176 # --- OPTIMIZATION ---
177 # pym.finite_difference(func, sx, sg0) # Note that the FD won't be correct due to the continuation changing values
178 pym.minimize_mma(sx, [sg0, s_gvol], maxit=120)