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)

Gallery generated by Sphinx-Gallery