Natural frequencies#
The lowest natural frequencies of a cylinder are clustered, and the tests compute them with a deterministic and verified eigenvalue solver, which checks the residual of each eigenpair and, with Sylvester’s law of inertia, that no eigenvalue is missing:
def lowest_frequencies(K, M, k, rtol=1e-8):
r"""The ``k`` lowest `\omega_n^2` and modes of `\boldsymbol{K} \boldsymbol{u} = \omega_n^2
\boldsymbol{M} \boldsymbol{u}`, ``K`` positive definite, verified"""
# shift-invert with the shift -1, below the lowest eigenvalue: the
# largest eigenvalues mu of ([K] + [M])^-1 [M] give omega_n**2 = 1/mu - 1
lu = splu(csc_matrix(K + M))
op = LinearOperator(K.shape, matvec=lambda x: lu.solve(M @ x),
dtype=float)
mu, eigvecs = eigs(op, k=k, which='LM', tol=0, ncv=4*k,
v0=np.ones(K.shape[0]))
assert np.all(np.abs(mu.imag) <= 1e-10*np.abs(mu))
eigvals = 1/mu.real - 1
# real modes: remove the arbitrary complex phase of each vector
i = np.argmax(np.abs(eigvecs), axis=0)
phase = eigvecs[i, np.arange(k)]
eigvecs = (eigvecs*(np.abs(phase)/phase)).real
order = np.argsort(eigvals)
eigvals = eigvals[order]
eigvecs = eigvecs[:, order]
for lam, x in zip(eigvals, eigvecs.T):
Kx = K @ x
res = np.linalg.norm(Kx - lam*(M @ x))/np.linalg.norm(Kx)
assert res < rtol, 'eigenpair residual %.1e' % res
# the next eigenvalue, if any, is above the last one returned; the
# shift lies in the gap between them, away from a multiple eigenvalue
s = eigvals[-1]*(1 + 1e-6)
assert negative_pivots(K - s*M) == k, 'eigenvalues missing'
return eigvals, eigvecs
A closed isotropic cylinder with \(u\), \(v\) and \(w\) constrained at the nodes
of both ends. The mass matrix of update_M() requires the thickness
h and the density rho of each element:
def test_natural_frequency(plot=False):
# number of nodes
nx = 21 # axial, keep odd if you want a line of nodes exactly in the middle
ny = 56 # circumferential
# geometry
L = 0.8 # m
R = 0.4 # m
b = 2*pi*R # m
# material properties
E = 70e9 # Pa
nu = 0.33
h = 0.001 # m
rho = 2.7e3 # kg/m3
prop = isotropic_plate(thickness=h, E=E, nu=nu)
nids = 1 + np.arange(nx*(ny+1))
nids_mesh = nids.reshape(nx, ny+1)
# closing the cylinder by reassigning last row of node-ids
nids_mesh[:, -1] = nids_mesh[:, 0]
nids = np.unique(nids_mesh)
nid_pos = dict(zip(nids, np.arange(len(nids))))
xlin = np.linspace(0, L, nx)
ytmp = np.linspace(0, b, ny+1)
ylin = np.linspace(0, b-(ytmp[-1] - ytmp[-2]), ny)
xmesh, ymesh = np.meshgrid(xlin, ylin)
xmesh = xmesh.T
ymesh = ymesh.T
# getting nodes
ncoords = np.vstack((xmesh.flatten(), ymesh.flatten())).T
x = ncoords[:, 0]
y = ncoords[:, 1]
n1s = nids_mesh[:-1, :-1].flatten()
n2s = nids_mesh[1:, :-1].flatten()
n3s = nids_mesh[1:, 1:].flatten()
n4s = nids_mesh[:-1, 1:].flatten()
nint = 4
points, weights = get_points_weights(nint=nint)
num_elements = len(n1s)
print('num_elements', num_elements)
elements = []
N = DOF*nx*ny
Kr = np.zeros(KC0_SPARSE_SIZE*num_elements, dtype=INT)
Kc = np.zeros(KC0_SPARSE_SIZE*num_elements, dtype=INT)
Kv = np.zeros(KC0_SPARSE_SIZE*num_elements, dtype=DOUBLE)
Mr = np.zeros(M_SPARSE_SIZE*num_elements, dtype=INT)
Mc = np.zeros(M_SPARSE_SIZE*num_elements, dtype=INT)
Mv = np.zeros(M_SPARSE_SIZE*num_elements, dtype=DOUBLE)
init_k_KC0 = 0
init_k_M = 0
for n1, n2, n3, n4 in zip(n1s, n2s, n3s, n4s):
shell = BFSCCylinder(nint)
shell.c1 = DOF*nid_pos[n1]
shell.c2 = DOF*nid_pos[n2]
shell.c3 = DOF*nid_pos[n3]
shell.c4 = DOF*nid_pos[n4]
shell.R = R
shell.h = h
shell.rho = rho
shell.lex = L/(nx-1)
shell.ley = b/ny
assign_constant_ABD(shell, prop)
shell.init_k_KC0 = init_k_KC0
shell.init_k_M = init_k_M
update_KC0(shell, points, weights, Kr, Kc, Kv)
update_M(shell, Mr, Mc, Mv)
init_k_KC0 += KC0_SPARSE_SIZE
init_k_M += M_SPARSE_SIZE
elements.append(shell)
KC0 = coo_matrix((Kv, (Kr, Kc)), shape=(N, N)).tocsc()
M = coo_matrix((Mv, (Mr, Mc)), shape=(N, N)).tocsc()
print('structural matrices OK')
# applying boundary conditions
bk = np.zeros(N, dtype=bool)
# simply supports
checkSS = isclose(x, 0) | isclose(x, L)
bk[0::DOF] = checkSS
bk[3::DOF] = checkSS
bk[6::DOF] = checkSS
bu = ~bk # same as np.logical_not, defining unknown DOFs
# sub-matrices corresponding to unknown DOFs
Kuu = KC0[bu, :][:, bu]
Muu = M[bu, :][:, bu]
# solving for natural frequencies
num_eigenvalues = 16
# deterministic and verified, see natural_frequency_solver.py
eigvals, eigvecsu = lowest_frequencies(Kuu, Muu, num_eigenvalues)
omegan = eigvals**0.5
mode = 0
uu = eigvecsu[:, mode]
u = np.zeros(N, dtype=float)
u[bu] = uu
w = u[6::DOF].reshape(nx, ny)
print('omegan', omegan)
assert isclose(omegan[0], 1096.10818933, rtol=0.01)
if plot:
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from matplotlib import cm
plt.gca().set_aspect('equal')
levels = np.linspace(w.min(), w.max(), 300)
plt.contourf(xmesh, ymesh, w, levels=levels, cmap=cm.jet)
plt.colorbar()
plt.show()