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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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().