Tutorial: topology optimization using pyfe3d#

Date: 14 of October 2024

Author: Saullo G. P. Castro

Cite this tutorial as:

Castro, SGP (2024). General-purpose finite element solver based on Python and Cython (Version 0.5.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.6573489.

Importing Python modules#

[1]:
import numpy as np
from numpy import isclose
from scipy.sparse.linalg import cg
from scipy.sparse import coo_matrix
from scipy.optimize import minimize

from pyfe3d.shellprop_utils import isotropic_plate
from pyfe3d import Quad4R, Quad4RData, Quad4RProbe, INT, DOUBLE, DOF

Creating mesh#

[2]:
data = Quad4RData()
probe = Quad4RProbe()
nx = 39
ny = 39
N = DOF*nx*ny

a = 3.0
b = 3.0

c = 3
E0 = 203.e9 # Pa
nu = 0.33

h = 0.003 # m

xtmp = np.linspace(0, a, nx)
ytmp = np.linspace(0, b, ny)
xmesh, ymesh = np.meshgrid(xtmp, ytmp)
ncoords = np.vstack((xmesh.T.flatten(), ymesh.T.flatten(), np.zeros_like(ymesh.T.flatten()))).T

x = ncoords[:, 0]
y = ncoords[:, 1]
z = ncoords[:, 2]
ncoords_flatten = ncoords.flatten()

nids = 1 + np.arange(ncoords.shape[0])
nid_pos = dict(zip(nids, np.arange(len(nids))))
nids_mesh = nids.reshape(nx, ny)
n1s = nids_mesh[:-1, :-1].flatten()
n2s = nids_mesh[1:, :-1].flatten()
n3s = nids_mesh[1:, 1:].flatten()
n4s = nids_mesh[:-1, 1:].flatten()

num_elements = len(n1s)
print('# number of elements', num_elements)

nodes = list(zip(n1s, n2s, n3s, n4s))

# number of elements 1444

Calculate the stiffness matrix for a vector of design variables “rho”#

The Young moduli of every element is calculated using the power law: \(E=\rho^c E_0\), where \(E_0\) is the original Young modulus.

[3]:
def calc_K(rho):
    E = rho**c*E0

    KC0r = np.zeros(data.KC0_SPARSE_SIZE*num_elements, dtype=INT)
    KC0c = np.zeros(data.KC0_SPARSE_SIZE*num_elements, dtype=INT)
    KC0v = np.zeros(data.KC0_SPARSE_SIZE*num_elements, dtype=DOUBLE)

    quads = []
    init_k_KC0 = 0
    i = 0
    for n1, n2, n3, n4 in zip(n1s, n2s, n3s, n4s):
        prop = isotropic_plate(thickness=h, E=E[i], nu=nu, calc_scf=True)
        i += 1
        pos1 = nid_pos[n1]
        pos2 = nid_pos[n2]
        pos3 = nid_pos[n3]
        pos4 = nid_pos[n4]
        r1 = ncoords[pos1]
        r2 = ncoords[pos2]
        r3 = ncoords[pos3]
        normal = np.cross(r2 - r1, r3 - r2)[2]
        assert normal > 0
        quad = Quad4R(probe)
        quad.n1 = n1
        quad.n2 = n2
        quad.n3 = n3
        quad.n4 = n4
        quad.c1 = DOF*nid_pos[n1]
        quad.c2 = DOF*nid_pos[n2]
        quad.c3 = DOF*nid_pos[n3]
        quad.c4 = DOF*nid_pos[n4]
        quad.init_k_KC0 = init_k_KC0
        quad.update_rotation_matrix(ncoords_flatten)
        quad.update_probe_xe(ncoords_flatten)
        quad.update_KC0(KC0r, KC0c, KC0v, prop)
        quads.append(quad)
        init_k_KC0 += data.KC0_SPARSE_SIZE

    KC0 = coo_matrix((KC0v, (KC0r, KC0c)), shape=(N, N)).tocsc()

    return KC0

Sensitivity of the stiffness matrix with respect to the vector “rho”#

The local support of \(\rho_i\) in vector \(\vec{\rho} = \{\rho_1, \rho_2, \dots, \rho_i, \dots, \rho_N\}\) makes it easy to calculate the sensitivity.

\[\frac{\partial E}{\partial \rho}\big\rvert_i = c \rho_i^{c-1} E_0\]
[4]:
def dKdrho(rho, i):
    dEdrho = c*rho[i]**(c-1)*E0
    prop = isotropic_plate(thickness=h, E=dEdrho, nu=nu, calc_scf=True)

    KC0r = np.zeros(1*data.KC0_SPARSE_SIZE, dtype=INT)
    KC0c = np.zeros(1*data.KC0_SPARSE_SIZE, dtype=INT)
    KC0v = np.zeros(1*data.KC0_SPARSE_SIZE, dtype=DOUBLE)

    n1, n2, n3, n4 = nodes[i]
    pos1 = nid_pos[n1]
    pos2 = nid_pos[n2]
    pos3 = nid_pos[n3]
    pos4 = nid_pos[n4]
    r1 = ncoords[pos1]
    r2 = ncoords[pos2]
    r3 = ncoords[pos3]
    normal = np.cross(r2 - r1, r3 - r2)[2]
    assert normal > 0
    quad = Quad4R(probe)
    quad.n1 = n1
    quad.n2 = n2
    quad.n3 = n3
    quad.n4 = n4
    quad.c1 = DOF*nid_pos[n1]
    quad.c2 = DOF*nid_pos[n2]
    quad.c3 = DOF*nid_pos[n3]
    quad.c4 = DOF*nid_pos[n4]
    quad.init_k_KC0 = 0
    quad.update_rotation_matrix(ncoords_flatten)
    quad.update_probe_xe(ncoords_flatten)
    quad.update_KC0(KC0r, KC0c, KC0v, prop)

    KC0 = coo_matrix((KC0v, (KC0r, KC0c)), shape=(N, N)).tocsc()

    return KC0


Applying boundary conditions and forces#

[5]:
bk = np.zeros(N, dtype=bool)
check = isclose(x, 0) & (isclose(y, 0.) | isclose(y, b))
bk[0::DOF] = check
bk[1::DOF] = check
# making it a 2D membrane problem
bk[2::DOF] = True
bk[3::DOF] = True
bk[4::DOF] = True

bu = ~bk

# applying load along u at x=a
# nodes at vertices get 1/2 of the force
fext = np.zeros(N)
ftotal = -1000.
print('ftotal', ftotal)
# at x=0
check = isclose(x, a) & isclose(y, 0)
fext[1::DOF][check] = ftotal

ftotal -1000.0

Performing topology optimization#

[6]:
uu = None
K = None

def jac(rho):
    """Adjoint gradient of the compliance with respect to each density in 'rho'
    """
    global uu, K
    # K = calc_K(rho=rho)
    # Kuu = K[bu, :][:, bu]
    u = np.zeros(N)
    # PREC = np.max(1/Kuu.diagonal())
    # uu, info = cg(PREC*Kuu, PREC*fext[bu], atol=1e-3)
    u[bu] = uu
    dCdrho = np.zeros(num_elements)
    for i in range(num_elements):
        dCdrho[i] = - u.T @ dKdrho(rho, i) @ u
    return dCdrho


def rel_volume(rho):
    target_rel_volume = 0.4
    constr = target_rel_volume - np.sum(rho)/num_elements
    return constr


def objective(rho):
    global uu, K
    K = calc_K(rho)
    Kuu = K[bu, :][:, bu]
    PREC = np.max(1/Kuu.diagonal())
    uu, info = cg(PREC*Kuu, PREC*fext[bu], atol=1e-3)
    compl = uu.T @ Kuu @ uu
    print('# obj', compl)
    return compl


constraints = [
    {'type': 'ineq', 'fun': rel_volume},
]
bounds = [[1.e-6, 1.] for _ in range(num_elements)]

rho = np.ones(num_elements)
topopt = minimize(objective, rho, jac=jac, method='SLSQP',
               constraints=constraints, bounds=bounds)
# obj 0.03656858898675277
# obj 0.5547638094651913
# obj 0.35106069976731524
# obj 0.3215979475255099
# obj 0.26155065221967616
# obj 0.2447341311076434
# obj 0.22158155519550016
# obj 0.2110542607798679
# obj 0.20017326095359275
# obj 0.1899385104265735
# obj 0.18559066078665587
# obj 0.17579991247268809
# obj 0.1695368390646748
# obj 0.16614085821270308
# obj 0.16067894839400845
# obj 0.15745805351723577
# obj 0.1543229529344281
# obj 0.14997568672655667
# obj 0.14805508446344015
# obj 0.1443873346907818
# obj 0.1420002577011633
# obj 0.1387533680825178
# obj 0.13659894670685266
# obj 0.1336785942032554
# obj 0.1302100407756137
# obj 0.12576457530613608
# obj 0.12055160240888262
# obj 0.11620300114017221
# obj 0.11263125359507889
/Users/saullogiovanip/miniconda3/lib/python3.12/site-packages/scipy/optimize/_slsqp_py.py:434: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds
  fx = wrapped_fun(x)
# obj 0.11144236537240935
/Users/saullogiovanip/miniconda3/lib/python3.12/site-packages/scipy/optimize/_slsqp_py.py:438: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds
  g = append(wrapped_grad(x), 0.0)
/Users/saullogiovanip/miniconda3/lib/python3.12/site-packages/scipy/optimize/_slsqp_py.py:498: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds
  a_ieq = vstack([con['jac'](x, *con['args'])
# obj 0.1093976103320863
# obj 0.10641891141655721
# obj 0.10528727474306264
# obj 0.10298874900519467
# obj 0.1007075298824823
# obj 0.09893335389752553
# obj 0.09774328677980391
# obj 0.09603914335961679
# obj 0.09477669903683783
# obj 0.0933967618079123
# obj 0.09213737826774447
# obj 0.09103602206543925
# obj 0.08992870637624946
# obj 0.08876037150415103
# obj 0.08761904956984673
# obj 0.08676634286601623
# obj 0.08586449260199747
# obj 0.08463077613202426
# obj 0.08368662929320665
# obj 0.08274835885449637
# obj 0.08183925637394532
# obj 0.0810026586568174
# obj 0.07992119435209558
# obj 0.07887065589514056
# obj 0.07808017107093869
# obj 0.07737506257498095
# obj 0.07671351488737689
# obj 0.07580524529586612
# obj 0.07490326203365288
# obj 0.0741322844147335
# obj 0.07333014135170007
# obj 0.07272968991453943
# obj 0.07221843513446832
# obj 0.07179850909572436
# obj 0.07134393524717482
# obj 0.07054215800944728
# obj 0.07010145418080924
# obj 0.0695357918453487
# obj 0.06910654031075446
# obj 0.06868069049809973
# obj 0.06835049872745673
# obj 0.06796273315002309
# obj 0.06756604867367567
# obj 0.06704438597089588
# obj 0.06655132368916959
# obj 0.0662428799388423
# obj 0.06577538774620953
# obj 0.06537434481286232
# obj 0.06503839635940929
# obj 0.06472260842541358
# obj 0.0643952744313543
# obj 0.06411250516329453
# obj 0.06389062591491629
# obj 0.06371647857097114
# obj 0.06351433452917071
# obj 0.06331996038445666
# obj 0.06305710309293343
# obj 0.06285521671617096
# obj 0.06264059770003866
# obj 0.06240530621406128
# obj 0.0622018210575791
# obj 0.061958487925665655
# obj 0.06170946050663415
# obj 0.06146190725572331
# obj 0.06123275687579674
# obj 0.06100521185653299
# obj 0.0608189804290589
# obj 0.06062682961043829
# obj 0.06047445116501475
# obj 0.060249869402383985

Checking the result of the topology optimization#

[7]:
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.cm as cm

xcg = np.zeros((num_elements, 3))
i = 0
for n1, n2, n3, n4 in zip(n1s, n2s, n3s, n4s):
    pos1 = nid_pos[n1]
    pos2 = nid_pos[n2]
    pos3 = nid_pos[n3]
    pos4 = nid_pos[n4]
    r1 = ncoords[pos1]
    r2 = ncoords[pos2]
    r3 = ncoords[pos3]
    r4 = ncoords[pos4]

    xcg[i] = (r1 + r2 + r3 + r4)/4
    i += 1

plt.clf()
plt.scatter(xcg[:, 0], xcg[:, 1], c=topopt.x, vmin=0, vmax=1)
plt.colorbar()
plt.show()
_images/tut_topology_14_0.png
[ ]: