Assembling a model#

bfsccylinder computes the contributions of one element; the mesh, the global matrices, the boundary conditions and the loads are written by the user with NumPy and SciPy, as in the tests of the repository, see Linear static analysis. The functions and the sizes below are those of the Donnell-type element; the module bfsccylinder.bfsccylinder_sanders, imported from bfsccylinder.sanders, has the same names, see Donnell-type and Sanders-type kinematics.

Mesh of a closed cylinder#

A cylinder of length \(L\) and radius \(R\) is divided into nx - 1 elements along \(x\) and ny elements around the circumference, of sides lex = L/(nx - 1) and ley = 2*pi*R/ny. The tests number nx by ny + 1 nodes with \(x\) varying slowest and close the cylinder by giving the last column of nodes the numbers of the first one, before taking the four nodes of each element from the corners of the array, in the order of The element and its degrees of freedom:

nids = 1 + np.arange(nx*(ny+1))
nids_mesh = nids.reshape(nx, ny+1)
nids_mesh[:, -1] = nids_mesh[:, 0]
nids = np.unique(nids_mesh)
nid_pos = dict(zip(nids, np.arange(len(nids))))

n1s = nids_mesh[:-1, :-1].flatten()
n2s = nids_mesh[1:, :-1].flatten()
n3s = nids_mesh[1:, 1:].flatten()
n4s = nids_mesh[:-1, 1:].flatten()

with nid_pos mapping each node identification number to its index, such that c1 = DOF*nid_pos[n1], and so on, and the model has N = DOF*nx*ny degrees of freedom. The elements of the last column share their nodes 3 and 4 with the nodes 2 and 1 of the first column, and the continuity of the degrees of freedom closes the shell around the circumference.

Element loop and sparse arrays#

Each function update_K* and update_M() writes the entries of one element in three 1-D arrays, the row indices, the column indices and the values, at the positions init_k to init_k + SPARSE_SIZE - 1, with init_k the attribute of the element for that matrix and SPARSE_SIZE the number of entries of one element:

Function

Position attribute

Entries per element

update_KC0()

init_k_KC0

KC0_SPARSE_SIZE

update_KCNL()

init_k_KCNL

KCNL_SPARSE_SIZE

update_KG(), update_KG_constant_stress()

init_k_KG

KG_SPARSE_SIZE

update_M()

init_k_M

M_SPARSE_SIZE

update_KCfollower()

init_k_KCfollower

KCfollower_SPARSE_SIZE

The sizes of the Sanders-type element are larger for KCNL, KG, M and KCfollower, see bfsccylinder.bfsccylinder_sanders; the code is the same when the sizes are taken from the namespace of the element used. For num_elements elements the arrays have SPARSE_SIZE*num_elements entries, of type INT for the indices and DOUBLE for the values, and the element of index k gets init_k = k*SPARSE_SIZE, e.g. by increasing a counter by SPARSE_SIZE in the element loop:

nint = 4
points, weights = get_points_weights(nint=nint)
KC0r = np.zeros(KC0_SPARSE_SIZE*num_elements, dtype=INT)
KC0c = np.zeros(KC0_SPARSE_SIZE*num_elements, dtype=INT)
KC0v = np.zeros(KC0_SPARSE_SIZE*num_elements, dtype=DOUBLE)
init_k_KC0 = 0
for n1, n2, n3, n4 in zip(n1s, n2s, n3s, n4s):
    shell = BFSCCylinder(nint)
    shell.c1 = DOF*nid_pos[n1]
    ...
    shell.R = R
    shell.lex = L/(nx-1)
    shell.ley = 2*pi*R/ny
    assign_constant_ABD(shell, prop)
    shell.init_k_KC0 = init_k_KC0
    update_KC0(shell, points, weights, KC0r, KC0c, KC0v)
    init_k_KC0 += KC0_SPARSE_SIZE

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

scipy.sparse.coo_matrix sums the values at repeated positions, the entries of the nodes shared by several elements. INT, DOUBLE, DOF and the sizes are given by the bfsccylinder and bfsccylinder.sanders namespaces. The elements write the entries of both triangles of each matrix, not only an upper triangle, and the matrices that are symmetric come out symmetric after the sum.

The functions overwrite the indices but add the values to the existing ones. Arrays reused for a new configuration, e.g. KCNL and KG at each iteration of a nonlinear analysis, are zeroed first, KCNLv *= 0; the indices do not change for a fixed mesh.

The vectors are assembled without position attributes: update_fint() and update_fext_follower() add the vector of one element directly to a global vector of size N, at the positions of its nodes, and the functions that depend on the displacements read them from a global vector u of size N.

Boundary conditions and loads#

The tests constrain degrees of freedom by partitioning the global system: a boolean array bk marks the constrained degrees of freedom, selected with the slices of The element and its degrees of freedom and the coordinates of the nodes, and bu = ~bk the unknown ones, e.g. for \(u = v = w = 0\) at the nodes of both ends:

bk = np.zeros(N, dtype=bool)
checkSS = isclose(x, 0) | isclose(x, L)
bk[0::DOF] = checkSS
bk[3::DOF] = checkSS
bk[6::DOF] = checkSS
bu = ~bk

Kuu = KC0[bu, :][:, bu]

A concentrated load is a value of the global force vector at a degree of freedom of a node, e.g. f[6::DOF][check] = fmid for a normal force, and an axial force at the nodes of an edge, e.g. fext[0::DOF][checkTopEdge] = -load/ny, see Nonlinear static analysis. The force vector of a pressure is given by update_fext_follower(), at \(\boldsymbol{u} = \boldsymbol{0}\) the dead pressure load, see Follower pressure.

A prescribed displacement, e.g. the axial shortening of one end, is a known value uk = u[bk] of a constrained degree of freedom, which moves to the right-hand side of the reduced system, \(\boldsymbol{K}_{uu}\boldsymbol{u}_u = -\boldsymbol{K}_{uk}\bar{\boldsymbol{u}}_k\), and whose reaction forces are \(\boldsymbol{F}_k = \boldsymbol{K}_{uk}^\top\boldsymbol{u}_u + \boldsymbol{K}_{kk}\bar{\boldsymbol{u}}_k\):

Kuk = KC0[bu, :][:, bk]
Kkk = KC0[bk, :][:, bk]
fu = -Kuk*uk
...
fk = Kuk.T*uu + Kkk*uk

see Linear buckling analysis. The solution of the reduced system is expanded to the size of the model, u[bu] = uu, before it is given to the functions that depend on the displacements, e.g. update_KG().