-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.py
More file actions
363 lines (302 loc) · 12.9 KB
/
utils.py
File metadata and controls
363 lines (302 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
"""
Helper functions and other auxilliary routines commonly used when implementing SSP.
Most of these routines were ported directly from meep. Since autograd is no longer
maintained, but jax _is_ maintained, we swap out autograd for jax.
TODO: Most of these routines are only compatible with 2D and need extra work in order
to generalize to arbitrary dimensions.
"""
from typing import Union, List, Tuple
import numpy as np
# Use jax as our autograd engine
from jax import numpy as jnp
ArrayLikeType = Union[List, Tuple, np.ndarray]
def _centered(arr: np.ndarray, newshape: ArrayLikeType) -> np.ndarray:
"""Formats the output of an FFT to center the zero-frequency component.
A helper function borrowed from SciPy:
https://github.com/scipy/scipy/blob/v1.4.1/scipy/signal/signaltools.py#L263-L270
Args:
arr: output array from an FFT operation.
newshape: 1d array with two elements (integers) specifying the dimensions
of the array to be returned.
Returns:
The input array with the zero-frequency component as the central element.
"""
newshape = np.asarray(newshape)
currshape = np.array(arr.shape)
startind = (currshape - newshape) // 2
endind = startind + newshape
myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]
return arr[tuple(myslice)]
def _quarter_to_full_kernel(arr: np.ndarray, pad_to: np.ndarray) -> np.ndarray:
"""Constructs the full kernel from its nonnegative quadrant.
Args:
arr: 2d input array representing the nonnegative quadrant of a
filter kernel with C4v symmetry.
pad_to: 1d array with two elements (integers) specifying the size
of the zero padding.
Returns:
The complete kernel.
"""
pad_size = pad_to - 2 * np.array(arr.shape) + 1
top = np.zeros((pad_size[0], arr.shape[1]))
bottom = np.zeros((pad_size[0], arr.shape[1] - 1))
middle = np.zeros((pad_to[0], pad_size[1]))
top_left = arr[:, :]
top_right = jnp.flipud(arr[1:, :])
bottom_left = jnp.fliplr(arr[:, 1:])
bottom_right = jnp.flipud(
jnp.fliplr(arr[1:, 1:])
) # equivalent to flip, but flip is incompatible with autograd
return jnp.concatenate(
(
jnp.concatenate((top_left, top, top_right)),
middle,
jnp.concatenate((bottom_left, bottom, bottom_right)),
),
axis=1,
)
def _edge_pad(arr: np.ndarray, pad: np.ndarray) -> np.ndarray:
"""Border-pads the edges of an array.
Used to preprocess the design weights prior to convolution with the filter.
Border padding an image will set the value of each padded pixel equal to
the value of the nearest pixel in the image. Used to implement feature-
preserving convolution filters that prevent unwanted edge effects.
Args:
arr: 2d array whose borders contain the values to use for padding
pad: 2x2 array of integers indicating the size
of the borders to pad the array with.
Returns:
A 2d array with border padding.
"""
# fill sides
left = jnp.tile(arr[0, :], (pad[0][0], 1))
right = jnp.tile(arr[-1, :], (pad[0][1], 1))
top = jnp.tile(arr[:, 0], (pad[1][0], 1)).transpose()
bottom = jnp.tile(arr[:, -1], (pad[1][1], 1)).transpose()
# fill corners
top_left = jnp.tile(arr[0, 0], (pad[0][0], pad[1][0]))
top_right = jnp.tile(arr[-1, 0], (pad[0][1], pad[1][0]))
bottom_left = jnp.tile(arr[0, -1], (pad[0][0], pad[1][1]))
bottom_right = jnp.tile(arr[-1, -1], (pad[0][1], pad[1][1]))
if pad[0][0] > 0 and pad[0][1] > 0 and pad[1][0] > 0 and pad[1][1] > 0:
return jnp.concatenate(
(
jnp.concatenate((top_left, top, top_right)),
jnp.concatenate((left, arr, right)),
jnp.concatenate((bottom_left, bottom, bottom_right)),
),
axis=1,
)
elif pad[0][0] == 0 and pad[0][1] == 0 and pad[1][0] > 0 and pad[1][1] > 0:
return jnp.concatenate((top, arr, bottom), axis=1)
elif pad[0][0] > 0 and pad[0][1] > 0 and pad[1][0] == 0 and pad[1][1] == 0:
return jnp.concatenate((left, arr, right), axis=0)
elif pad[0][0] == 0 and pad[0][1] == 0 and pad[1][0] == 0 and pad[1][1] == 0:
return arr
else:
raise ValueError("At least one of the padding numbers is invalid.")
def convolve_design_weights_and_kernel(
x: np.ndarray, h: np.ndarray, periodic_axes: ArrayLikeType = None
) -> np.ndarray:
"""Convolves the design weights with the kernel.
Uses a 2d FFT to perform the convolution operation. This approach is
typically faster than a direct calculation. It also preserves the shape
of the input and output arrays. The design weights are border-padded
prior to the FFT to preserve features on the edges of the design region.
Args:
x: 2d design weights.
h: filter kernel. Must be same size as `x`
periodic_axes: list of axes (x, y = 0, 1) that are to be treated as
periodic. Default is None (all axes are non-periodic).
Returns:
The convolution of the design weights with the kernel as a 2d array.
"""
(sx, sy) = x.shape
if periodic_axes is None:
h = _quarter_to_full_kernel(h, 3 * np.array([sx, sy]))
x = _edge_pad(x, ((sx, sx), (sy, sy)))
else:
(kx, ky) = h.shape
npx = int(
np.ceil((2 * kx - 1) / sx)
) # 2*kx-1 is the size of a complete kernel in the x direction
npy = int(
np.ceil((2 * ky - 1) / sy)
) # 2*ky-1 is the size of a complete kernel in the y direction
if npx % 2 == 0:
npx += 1 # Ensure npx is an odd number
if npy % 2 == 0:
npy += 1 # Ensure npy is an odd number
periodic_axes = np.array(periodic_axes)
# Repeat the design pattern in periodic directions according to
# the kernel size
x = jnp.tile(
x, (npx if 0 in periodic_axes else 1, npy if 1 in periodic_axes else 1)
)
jnpdx = 0 if 0 in periodic_axes else sx
jnpdy = 0 if 1 in periodic_axes else sy
x = _edge_pad(
x, ((jnpdx, jnpdx), (jnpdy, jnpdy))
) # pad only in nonperiodic directions
h = _quarter_to_full_kernel(
h,
np.array(
[
npx * sx if 0 in periodic_axes else 3 * sx,
npy * sy if 1 in periodic_axes else 3 * sy,
]
),
)
h = h / jnp.sum(h) # Normalize the kernel
return _centered(
jnp.real(jnp.fft.ifft2(jnp.fft.fft2(x) * jnp.fft.fft2(h))), (sx, sy)
)
def _get_resolution(resolution: ArrayLikeType) -> tuple:
"""Converts input design-grid resolution to the acceptable format.
Args:
resolution: number of list of numbers representing design-grid
resolution, allowing anisotropic resolution.
Returns:
A two-element tuple composed of the resolution in x and y directions.
"""
if isinstance(resolution, (tuple, list, np.ndarray)):
if len(resolution) == 2:
return resolution
elif len(resolution) == 1:
return resolution[0], resolution[0]
else:
raise ValueError(
"The dimension of the design-grid resolution is incorrect."
)
elif isinstance(resolution, (int, float)):
return resolution, resolution
else:
raise ValueError("The input for design-grid resolution is invalid.")
def mesh_grid(
radius: float,
Lx: float,
Ly: float,
resolution: ArrayLikeType,
periodic_axes: ArrayLikeType = None,
) -> tuple:
"""Obtains the numbers of grid points and the coordinates of the grid
of the design region.
Args:
radius: filter radius (in Meep units).
Lx: length of design region in X direction (in Meep units).
Ly: length of design region in Y direction (in Meep units).
resolution: resolution of the design grid (not the Meep grid
resolution).
periodic_axes: list of axes (x, y = 0, 1) that are to be treated as
periodic. Default is None (all axes are non-periodic).
Returns:
A four-element tuple composed of the numbers of grid points and
the coordinates of the grid.
"""
resolution = _get_resolution(resolution)
Nx = int(round(Lx * resolution[0])) + 1
Ny = int(round(Ly * resolution[1])) + 1
if Nx <= 1 and Ny <= 1:
raise AssertionError(
"The grid size is improper. Check the size and resolution of the design region."
)
xv = np.arange(0, Lx / 2, 1 / resolution[0]) if resolution[0] > 0 else [0]
yv = np.arange(0, Ly / 2, 1 / resolution[1]) if resolution[1] > 0 else [0]
# If the design weights are periodic in a direction,
# the size of the kernel in that direction needs to be adjusted
# according to the filter radius.
if periodic_axes is not None:
periodic_axes = np.array(periodic_axes)
if 0 in periodic_axes:
xv = (
jnp.arange(0, jnp.ceil(2 * radius / Lx) * Lx / 2, 1 / resolution[0])
if resolution[0] > 0
else [0]
)
if 1 in periodic_axes:
yv = (
jnp.arange(0, jnp.ceil(2 * radius / Ly) * Ly / 2, 1 / resolution[1])
if resolution[1] > 0
else [0]
)
X, Y = np.meshgrid(xv, yv, sparse=True, indexing="ij")
return Nx, Ny, X, Y
def conic_filter(
x: np.ndarray,
radius: float,
Lx: float,
Ly: float,
resolution: ArrayLikeType,
periodic_axes: ArrayLikeType = None,
) -> np.ndarray:
"""A linear conic (or "hat") filter.
Ref: B.S. Lazarov, F. Wang, & O. Sigmund, Length scale and
manufacturability in density-based topology optimization.
Archive of Applied Mechanics, 86(1-2), pp. 189-218 (2016).
Args:
x: 2d design weights.
radius: filter radius (in Meep units).
Lx: length of design region in X direction (in Meep units).
Ly: length of design region in Y direction (in Meep units).
resolution: resolution of the design grid (not the Meep grid
resolution).
periodic_axes: list of axes (x, y = 0, 1) that are to be treated as
periodic. Default is None (all axes are non-periodic).
Returns:
The filtered design weights.
"""
Nx, Ny, X, Y = mesh_grid(radius, Lx, Ly, resolution, periodic_axes)
x = x.reshape(Nx, Ny) # Ensure the input is 2d
h = jnp.where(
X**2 + Y**2 < radius**2, (1 - np.sqrt(abs(X**2 + Y**2)) / radius), 0
)
return convolve_design_weights_and_kernel(x, h, periodic_axes)
def tanh_projection(x: np.ndarray, beta: float, eta: float) -> np.ndarray:
"""Sigmoid projection filter.
Ref: F. Wang, B. S. Lazarov, & O. Sigmund, On projection methods,
convergence and robust formulations in topology optimization.
Structural and Multidisciplinary Optimization, 43(6), pp. 767-784 (2011).
Args:
x: 2d design weights to be filtered.
beta: thresholding parameter in the range [0, inf]. Determines the
degree of binarization of the output.
eta: threshold point in the range [0, 1].
Returns:
The filtered design weights.
"""
if beta == jnp.inf:
# Note that backpropagating through here can produce NaNs. So we
# manually specify the step function to keep the gradient clean.
return jnp.where(x > eta, 1.0, 0.0)
else:
return (jnp.tanh(beta * eta) + jnp.tanh(beta * (x - eta))) / (
jnp.tanh(beta * eta) + jnp.tanh(beta * (1 - eta))
)
def get_conic_radius_from_eta_e(b, eta_e):
"""Calculates the corresponding filter radius given the minimum length scale (b)
and the desired eroded threshold point (eta_e).
Args:
b : float
Desired minimum length scale.
eta_e : float
Eroded threshold point (1-eta)
Returns:
The conic filter radius.
References
[1] Qian, X., & Sigmund, O. (2013). Topological design of electromechanical actuators with
robustness toward over-and under-etching. Computer Methods in Applied
Mechanics and Engineering, 253, 237-251.
[2] Wang, F., Lazarov, B. S., & Sigmund, O. (2011). On projection methods, convergence and
robust formulations in topology optimization. Structural and Multidisciplinary
Optimization, 43(6), 767-784.
[3] Lazarov, B. S., Wang, F., & Sigmund, O. (2016). Length scale and manufacturability in
density-based topology optimization. Archive of Applied Mechanics, 86(1-2), 189-218.
"""
if (eta_e >= 0.5) and (eta_e < 0.75):
return b / (2 * np.sqrt(eta_e - 0.5))
elif (eta_e >= 0.75) and (eta_e <= 1):
return b / (2 - 2 * np.sqrt(1 - eta_e))
else:
raise ValueError(
"The erosion threshold point (eta_e) must be between 0.5 and 1."
)