Skip to content

Function reference

The CALFEM manual is the primary source for conceptual descriptions of the standard function groups: material, element, system, graphics, utility, and matrix functions. This page is the generated Python API reference for the installed package.

Core functions

CALFEM Core module

Contains all the functions implementing CALFEM standard functionality.

Copyright (c) Division of Structural Mechanics and Division of Solid Mechanics, Lund University.

assem(edof, K, Ke, f=None, fe=None)

Assemble element matrices Ke (and fe) into the global stiffness matrix K (and global force vector f).

Parameters

edof : array_like DOF topology array. K : array_like The global stiffness matrix. Ke : array_like Element stiffness matrix. f : array_like, optional The global force vector. fe : array_like, optional Element force vector.

Returns

K : ndarray The updated global stiffness matrix. f : ndarray, optional The updated global force vector, if f and fe are provided.

Source code in src/calfem/core.py
def assem(edof: ArrayLike, K: Union[NDArray[np.floating], csr_matrix, csc_matrix, lil_matrix], Ke: ArrayLike, f: Optional[ArrayLike] = None, fe: Optional[ArrayLike] = None) -> Union[NDArray[np.floating], Tuple[NDArray[np.floating], NDArray[np.floating]]]:
    """
    Assemble element matrices Ke (and fe) into the global stiffness matrix K (and global force vector f).

    Parameters
    ----------
    edof : array_like
        DOF topology array.
    K : array_like
        The global stiffness matrix.
    Ke : array_like
        Element stiffness matrix.
    f : array_like, optional
        The global force vector.
    fe : array_like, optional
        Element force vector.

    Returns
    -------
    K : ndarray
        The updated global stiffness matrix.
    f : ndarray, optional
        The updated global force vector, if f and fe are provided.
    """

    import scipy.sparse as sp
    import numpy as np

    # Handle sparse matrices case
    if isinstance(K, sp.lil_matrix) or isinstance(K, sp.csr_matrix) or isinstance(K, sp.csc_matrix):
        if edof.ndim == 1:
            idx = edof-1
            # Convert to COO format for efficient modification
            if not isinstance(K, sp.lil_matrix):
                K = K.tolil()

            # Manually update each element
            for i in range(len(idx)):
                for j in range(len(idx)):
                    K[idx[i], idx[j]] += Ke[i, j]

            if (not f is None) and (not fe is None):
                fe_array = np.asarray(fe)
                if fe_array.ndim == 2 and fe_array.shape[1] > 1:
                    fe_shaped = fe_array.reshape(-1, 1)
                else:
                    fe_shaped = fe_array
                f[idx] += fe_shaped.flatten()
        else:
            for row in edof:
                idx = row-1
                # Convert to LIL format for efficient modification
                if not isinstance(K, sp.lil_matrix):
                    K = K.tolil()

                # Manually update each element    
                for i in range(len(idx)):
                    for j in range(len(idx)):
                        K[idx[i], idx[j]] += Ke[i, j]

                if (not f is None) and (not fe is None):
                    fe_array = np.asarray(fe)
                    if fe_array.ndim == 2 and fe_array.shape[1] > 1:
                        fe_shaped = fe_array.reshape(-1, 1)
                    else:
                        fe_shaped = fe_array
                    f[idx] += fe_shaped.flatten()
    else:
        # Original code for dense matrices
        if edof.ndim == 1:
            idx = edof-1
            K[np.ix_(idx, idx)] = K[np.ix_(idx, idx)] + Ke

            if (not f is None) and (not fe is None):
                fe_array = np.asarray(fe)
                if fe_array.ndim == 2 and fe_array.shape[1] > 1:
                    fe_shaped = fe_array.reshape(-1, 1)
                else:
                    fe_shaped = fe_array
                f[np.ix_(idx)] = f[np.ix_(idx)] + fe_shaped
        else:
            for row in edof:
                idx = row-1
                K[np.ix_(idx, idx)] = K[np.ix_(idx, idx)] + Ke
                if (not f is None) and (not fe is None):
                    fe_array = np.asarray(fe)
                    if fe_array.ndim == 2 and fe_array.shape[1] > 1:
                        fe_shaped = fe_array.reshape(-1, 1)
                    else:
                        fe_shaped = fe_array
                    f[np.ix_(idx)] = f[np.ix_(idx)] + fe_shaped

    if f is None:
        return K
    else:
        return K, f

bar1e(ex, ep, eq=None)

Compute the stiffness matrix (and optionally load vector) for a 1D bar element.

Parameters

ex : array_like Element node coordinates [x1, x2]. ep : array_like Element properties [E, A], where E is Young's modulus and A is cross-sectional area. eq : array_like, optional Distributed load [qX].

Returns

Ke : ndarray Bar stiffness matrix, shape (2, 2). fe : ndarray, optional Element load vector, shape (2, 1), if eq is not None.

Examples

bar1e([0, 2], [210e9, 0.01]) array([[ 1.05e+09, -1.05e+09], [-1.05e+09, 1.05e+09]])

History

LAST MODIFIED: O Dahlblom 2015-10-22 O Dahlblom 2022-11-14 (Python version)

Source code in src/calfem/core.py
def bar1e(ex: ArrayLike, ep: ArrayLike, eq: Optional[ArrayLike] = None) -> Union[NDArray[np.floating], Tuple[NDArray[np.floating], NDArray[np.floating]]]:
    """
    Compute the stiffness matrix (and optionally load vector) for a 1D bar element.

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ep : array_like
        Element properties [E, A], where E is Young's modulus and A is cross-sectional area.
    eq : array_like, optional
        Distributed load [qX].

    Returns
    -------
    Ke : ndarray
        Bar stiffness matrix, shape (2, 2).
    fe : ndarray, optional
        Element load vector, shape (2, 1), if eq is not None.

    Examples
    --------
    >>> bar1e([0, 2], [210e9, 0.01])
    array([[ 1.05e+09, -1.05e+09],
           [-1.05e+09,  1.05e+09]])

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-10-22
                   O Dahlblom   2022-11-14 (Python version)
    """
    E, A = ep
    DEA=E*A

    qX=0.
    if not eq is None:
        qX=eq[0]

    x1, x2 = ex
    dx = x2-x1
    L = abs(dx)

    Ke = DEA/L*np.array([
        [1, -1],
        [-1, 1]
    ])

    fe = qX*L*np.array([1/2, 1/2]).reshape(2,1)

    if eq is None:
        return Ke
    else:
        return Ke, fe

bar1s(ex, ep, ed, eq=None, nep=None)

Compute section forces in a 1D bar element.

Parameters

ex : array_like Element node coordinates [x1, x2]. ep : array_like Element properties [E, A], where E is Young's modulus and A is cross-sectional area. ed : array_like Element displacement vector [u1, u2]. eq : array_like, optional Distributed load [qX]. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 1). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 1), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

Examples

bar1s([0, 2], [210e9, 0.01], [0.0, 0.001]) array([[1.05e+06], [1.05e+06]])

History

LAST MODIFIED: O Dahlblom 2021-02-25 O Dahlblom 2022-11-14 (Python version)

Source code in src/calfem/core.py
def bar1s(ex, ep, ed, eq=None, nep=None):
    """
    Compute section forces in a 1D bar element.

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ep : array_like
        Element properties [E, A], where E is Young's modulus and A is cross-sectional area.
    ed : array_like
        Element displacement vector [u1, u2].
    eq : array_like, optional
        Distributed load [qX].
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 1).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 1), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    Examples
    --------
    >>> bar1s([0, 2], [210e9, 0.01], [0.0, 0.001])
    array([[1.05e+06],
           [1.05e+06]])

    History
    -------
    LAST MODIFIED: O Dahlblom  2021-02-25
                   O Dahlblom  2022-11-14 (Python version)
    """
    E, A = ep
    DEA=E*A

    qX=0.
    if not eq is None:  
       qX=eq[0] 

    ne=2
    if nep != None: 
       ne=nep

    x1, x2 = ex
    dx = x2-x1
    L = abs(dx)

    a1 = ed.reshape(2,1)

    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 

    C1a = C1 @ a1

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a

    if DEA != 0:
       u = u -(X**2-L*X)*qX/(2*DEA)
       du = du -(2*X-L)*qX/(2*DEA)

    N = DEA*du
    es = N
    edi=u
    eci=X

    if nep is None:
        return es
    else:
        return es, edi, eci

bar1we(ex, ep, eq=None)

Compute the stiffness matrix (and optionally load vector) for a 1D bar element with axial springs.

Parameters

ex : array_like Element node coordinates [x1, x2]. ep : array_like Element properties [E, A, kX], where E is Young's modulus, A is cross-sectional area, and kX is axial spring stiffness. eq : array_like, optional Distributed load [qX].

Returns

Ke : ndarray Bar stiffness matrix, shape (2, 2). fe : ndarray, optional Element load vector, shape (2, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2015-12-17 O Dahlblom 2022-10-19 (Python version)

Source code in src/calfem/core.py
def bar1we(ex, ep, eq=None):
    """
    Compute the stiffness matrix (and optionally load vector) for a 1D bar element with axial springs.

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ep : array_like
        Element properties [E, A, kX], where E is Young's modulus, A is cross-sectional area, and kX is axial spring stiffness.
    eq : array_like, optional
        Distributed load [qX].

    Returns
    -------
    Ke : ndarray
        Bar stiffness matrix, shape (2, 2).
    fe : ndarray, optional
        Element load vector, shape (2, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-12-17
                   O Dahlblom   2022-10-19 (Python version)
    """
    E, A, kX = ep
    DEA = E*A;

    qX = 0.
    if not eq is None:
        qX = eq[0]

    x1, x2 = ex
    dx = x2-x1
    L = abs(dx)

    K1 = DEA/L*np.array([
        [1, -1],
        [-1, 1]
    ])

    K2 = kX*L/6*np.array([
        [2,  1],
        [1,  2]
    ])

    Ke = K1+K2

    fe = qX*L*np.array([1/2, 1/2]).reshape(2,1)

    if eq is None:
        return Ke
    else:
        return Ke, fe

bar1ws(ex, ep, ed, eq=None, nep=None)

Compute section forces in a 1D bar element with axial springs.

Parameters

ex : array_like Element node coordinates [x1, x2]. ep : array_like Element properties [E, A, kX], where E is Young's modulus, A is cross-sectional area, and kX is axial spring stiffness. ed : array_like Element displacement vector [u1, u2]. eq : array_like, optional Distributed load [qX]. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 1). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 1), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2021-02-25 O Dahlblom 2022-11-14 (Python version)

Source code in src/calfem/core.py
def bar1ws(ex, ep, ed, eq=None, nep=None):
    """
    Compute section forces in a 1D bar element with axial springs.

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ep : array_like
        Element properties [E, A, kX], where E is Young's modulus, A is cross-sectional area, and kX is axial spring stiffness.
    ed : array_like
        Element displacement vector [u1, u2].
    eq : array_like, optional
        Distributed load [qX].
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 1).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 1), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom  2021-02-25
                   O Dahlblom  2022-11-14 (Python version)
    """
    E, A, kX = ep
    DEA = E*A

    qX = 0.
    if not eq is None:  
       qX = eq[0] 

    ne = 2
    if nep != None: 
       ne = nep

    x1, x2 = ex
    dx = x2-x1
    L = abs(dx)

    a1 = ed.reshape(2,1)

    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 

    C1a = C1 @ a1

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a

    if DEA != 0:
       u = u +kX/DEA*np.concatenate(((X**2-L*X)/2, (X**3-L**2*X)/6),1) @ C1a-(X**2-L*X)*qX/(2*DEA)
       du = du +kX/DEA*np.concatenate(((2*X-L)/2, (3*X**2-L**2)/6),1) @ C1a-(2*X-L)*qX/(2*DEA)

    N = DEA*du
    es = N
    edi = u
    eci = X

    if nep is None:
        return es
    else:
        return es, edi, eci

bar2e(ex, ey, ep, eq=None)

Compute the element stiffness matrix (and optionally load vector) for a 2D bar element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A], where E is Young's modulus and A is cross-sectional area. eq : array_like, optional Distributed load [qX].

Returns

Ke : ndarray Bar stiffness matrix, shape (4, 4). fe : ndarray, optional Element load vector, shape (4, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2015-10-20 O Dahlblom 2022-11-16 (Python version)

Source code in src/calfem/core.py
def bar2e(ex, ey, ep, eq=None):
    """
    Compute the element stiffness matrix (and optionally load vector) for a 2D bar element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A], where E is Young's modulus and A is cross-sectional area.
    eq : array_like, optional
        Distributed load [qX].

    Returns
    -------
    Ke : ndarray
        Bar stiffness matrix, shape (4, 4).
    fe : ndarray, optional
        Element load vector, shape (4, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-10-20
                   O Dahlblom   2022-11-16 (Python version)
    """
    E, A = ep
    DEA = E*A

    qX = 0.
    if not eq is None:
        qX=eq[0]

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    Kle = DEA/L*np.array([
        [1, -1],
        [-1, 1]
    ])

    fle = qX*L*np.array([1/2, 1/2]).reshape(2,1)

    nxX=dx/L
    nyX=dy/L
    G = np.array([
        [nxX, nyX,   0,   0],  
        [  0,   0, nxX, nyX]
    ])

    Ke = G.T @ Kle @ G   
    fe = G.T @ fle

    if eq is None:
        return Ke
    else:
        return Ke, fe

bar2ge(ex, ey, ep, QX)

Compute the element stiffness matrix for a 2D bar element with additional axial force QX.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A], where E is Young's modulus and A is cross-sectional area. QX : float Additional axial force.

Returns

Ke : ndarray Bar stiffness matrix, shape (4, 4).

History

LAST MODIFIED: O Dahlblom 2015-12-17 O Dahlblom 2022-11-16 (Python version)

Source code in src/calfem/core.py
def bar2ge(ex, ey, ep, QX):
    """
    Compute the element stiffness matrix for a 2D bar element with additional axial force QX.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A], where E is Young's modulus and A is cross-sectional area.
    QX : float
        Additional axial force.

    Returns
    -------
    Ke : ndarray
        Bar stiffness matrix, shape (4, 4).

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-12-17
                   O Dahlblom   2022-11-16 (Python version)
    """
    E, A = ep
    DEA = E*A

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    K0le = DEA/L*np.array([
        [ 1,  0, -1,  0],
        [ 0,  0,  0,  0],
        [-1,  0,  1,  0],
        [ 0,  0,  0,  0]
    ])          

    Ksle = QX/L*np.array([
        [ 0,  0,  0,  0],
        [ 0,  1,  0, -1],
        [ 0,  0,  0,  0],
        [ 0, -1,  0,  1]
    ])

    Kle = K0le + Ksle

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L

    G = np.array([
        [nxX, nyX,   0,   0],
        [nxY, nyY,   0,   0],  
        [  0,   0, nxX, nyX],
        [  0,   0, nxY, nyY]
    ])

    Ke = G.T @ Kle @ G   

    return Ke

bar2gs(ex, ey, ep, ed, nep=None)

Compute normal force and axial force in a 2D bar element (bar2ge).

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A], where E is Young's modulus and A is cross-sectional area. ed : array_like Element displacement vector [u1, ..., u4]. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 1). QX : float Axial force. edi : ndarray, optional Element displacements at evaluation points, shape (nep, 1), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2015-10-20 O Dahlblom 2022-11-16 (Python version)

Source code in src/calfem/core.py
def bar2gs(ex, ey, ep, ed, nep=None):
    """
    Compute normal force and axial force in a 2D bar element (bar2ge).

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A], where E is Young's modulus and A is cross-sectional area.
    ed : array_like
        Element displacement vector [u1, ..., u4].
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 1).
    QX : float
        Axial force.
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 1), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom  2015-10-20
                   O Dahlblom  2022-11-16 (Python version)
    """
    E, A = ep
    DEA = E*A

    ne=2
    if nep != None: 
       ne=nep

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L

    G = np.array([
        [nxX, nyX,   0,   0],
        [nxY, nyY,   0,   0],  
        [  0,   0, nxX, nyX],
        [  0,   0, nxY, nyY]
    ])

    edl = G @ ed.reshape(4,1)
    a1 = np.array([
        edl[0],
        edl[2]
    ])

    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 
    C1a = C1 @ a1

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a

    N = DEA*du
    QX = N[0]
    es = N
    edi=u
    eci=X

    if nep is None:
        return es, QX
    else:
        return es, QX, edi, eci

bar2s(ex, ey, ep, ed, eq=None, nep=None)

Compute normal force in a 2D bar element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A], where E is Young's modulus and A is cross-sectional area. ed : array_like Element displacement vector [u1, ..., u4]. eq : array_like, optional Distributed load [qX]. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 1). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 1), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2015-12-04 O Dahlblom 2022-11-16 (Python version)

Source code in src/calfem/core.py
def bar2s(ex, ey, ep, ed, eq=None, nep=None):
    """
    Compute normal force in a 2D bar element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A], where E is Young's modulus and A is cross-sectional area.
    ed : array_like
        Element displacement vector [u1, ..., u4].
    eq : array_like, optional
        Distributed load [qX].
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 1).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 1), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom  2015-12-04
                   O Dahlblom  2022-11-16 (Python version)
    """
    E, A = ep
    DEA = E*A

    qX = 0.
    if not eq is None:  
       qX = eq[0] 

    ne = 2
    if nep != None: 
       ne=nep

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    nxX = dx/L
    nyX = dy/L

    G = np.array([
        [nxX, nyX,   0,   0],  
        [  0,   0, nxX, nyX]
    ])

    a1 = G @ ed.reshape(4,1)

    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 

    C1a = C1 @ a1

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a

    if DEA != 0:
       u = u -(X**2-L*X)*qX/(2*DEA)
       du = du -(2*X-L)*qX/(2*DEA)

    N = DEA*du
    es = N
    edi = u
    eci = X

    if nep is None:
        return es
    else:
        return es, edi, eci

bar3e(ex, ey, ez, ep, eq=None)

Compute the element stiffness matrix (and optionally load vector) for a 3D bar element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ez : array_like Element node z-coordinates [z1, z2]. ep : array_like Element properties [E, A], where E is Young's modulus and A is cross-sectional area. eq : array_like, optional Distributed load [qX].

Returns

Ke : ndarray Bar stiffness matrix, shape (6, 6). fe : ndarray, optional Element load vector, shape (6, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2015-10-19 O Dahlblom 2022-11-18 (Python version)

Source code in src/calfem/core.py
def bar3e(ex, ey, ez, ep, eq=None):
    """
    Compute the element stiffness matrix (and optionally load vector) for a 3D bar element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ez : array_like
        Element node z-coordinates [z1, z2].
    ep : array_like
        Element properties [E, A], where E is Young's modulus and A is cross-sectional area.
    eq : array_like, optional
        Distributed load [qX].

    Returns
    -------
    Ke : ndarray
        Bar stiffness matrix, shape (6, 6).
    fe : ndarray, optional
        Element load vector, shape (6, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-10-19
                   O Dahlblom   2022-11-18 (Python version)
    """
    E, A = ep
    DEA=E*A

    qX=0.
    if not eq is None:
        qX=eq[0]

    x1, x2 = ex
    y1, y2 = ey
    z1, z2 = ez
    dx = x2-x1
    dy = y2-y1
    dz = z2-z1
    L = np.sqrt(dx*dx+dy*dy+dz*dz)

    Kle = DEA/L*np.array([
        [1, -1],
        [-1, 1]
    ])

    fle = qX*L*np.array([1/2, 1/2]).reshape(2,1)

    nxX=dx/L
    nyX=dy/L
    nzX=dz/L
    G = np.array([
        [nxX, nyX, nzX,  0,   0,   0],  
        [  0,   0,   0, nxX, nyX, nzX]
    ])

    Ke = G.T @ Kle @ G   
    fe = G.T @ fle

    if eq is None:
        return Ke
    else:
        return Ke, fe

bar3s(ex, ey, ez, ep, ed, eq=None, nep=None)

Compute normal force in a 3D bar element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ez : array_like Element node z-coordinates [z1, z2]. ep : array_like Element properties [E, A], where E is Young's modulus and A is cross-sectional area. ed : array_like Element displacement vector [u1, ..., u6]. eq : array_like, optional Distributed load [qX]. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 1). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 1), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2021-09-01 O Dahlblom 2022-11-18 (Python version)

Source code in src/calfem/core.py
def bar3s(ex, ey, ez, ep, ed, eq=None, nep=None):
    """
    Compute normal force in a 3D bar element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ez : array_like
        Element node z-coordinates [z1, z2].
    ep : array_like
        Element properties [E, A], where E is Young's modulus and A is cross-sectional area.
    ed : array_like
        Element displacement vector [u1, ..., u6].
    eq : array_like, optional
        Distributed load [qX].
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 1).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 1), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom  2021-09-01
                   O Dahlblom  2022-11-18 (Python version)
    """
    E, A = ep
    DEA = E*A

    qX = 0.
    if not eq is None:  
       qX = eq[0] 

    ne = 2
    if nep != None: 
       ne = nep

    x1, x2 = ex
    y1, y2 = ey
    z1, z2 = ez
    dx = x2-x1
    dy = y2-y1
    dz = z2-z1
    L = np.sqrt(dx*dx+dy*dy+dz*dz)

    nxX = dx/L
    nyX = dy/L
    nzX  =dz/L

    G = np.array([
        [nxX, nyX, nzX,   0,   0,   0],  
        [  0,   0,   0, nxX, nyX, nzX]
    ])

    a1 = G @ ed.reshape(6,1)

    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 

    C1a = C1 @ a1

    X = np.linspace(0., L+L/(ne-1), ne).reshape(ne,1) 
    #X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a

    if DEA != 0:
       u = u -(X**2-L*X)*qX/(2*DEA)
       du = du -(2*X-L)*qX/(2*DEA)

    N = DEA*du
    es = N
    edi=u
    eci=X

    if nep is None:
        return es
    else:
        return es, edi, eci

beam1e(ex, ep, eq=None)

Compute the stiffness matrix (and optionally load vector) for a 1D beam element.

Parameters

ex : array_like Element node coordinates [x1, x2]. ep : array_like Element properties [E, I], where E is Young's modulus and I is moment of inertia. eq : array_like, optional Distributed load [qY].

Returns

Ke : ndarray Beam stiffness matrix, shape (4, 4). fe : ndarray, optional Element load vector, shape (4, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2019-01-09 O Dahlblom 2022-10-25 (Python version)

Source code in src/calfem/core.py
def beam1e(ex, ep, eq=None):
    """
    Compute the stiffness matrix (and optionally load vector) for a 1D beam element.

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ep : array_like
        Element properties [E, I], where E is Young's modulus and I is moment of inertia.
    eq : array_like, optional
        Distributed load [qY].

    Returns
    -------
    Ke : ndarray
        Beam stiffness matrix, shape (4, 4).
    fe : ndarray, optional
        Element load vector, shape (4, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2019-01-09
                   O Dahlblom   2022-10-25 (Python version)
    """
    E, I = ep
    DEI = E*I

    qY = 0.
    if not eq is None:
        qY = eq[0]

    x1, x2 = ex
    dx = x2-x1
    L = abs(dx)

    Ke = DEI/L**3*np.array([
        [12, 6*L, -12, 6*L],
        [6*L,  4*L**2, -6*L,  2*L**2],
        [-12, -6*L,  12, -6*L],
        [6*L,  2*L**2,  -6*L, 4*L**2]
    ])

    fe = qY*np.array([L/2, L**2/12, L/2, -L**2/12]).reshape(4,1)

    if eq is None:
        return Ke
    else:
        return Ke, fe

beam1s(ex, ep, ed, eq=None, nep=None)

Compute section forces in a 1D beam element (beam1e).

Parameters

ex : array_like Element node coordinates [x1, x2]. ep : array_like Element properties [E, I], where E is Young's modulus and I is moment of inertia. ed : array_like Element displacement vector [u1, ..., u4]. eq : array_like, optional Distributed loads [qy], local directions. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 2). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 1), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2021-09-01 O Dahlblom 2022-10-25 (Python version)

Source code in src/calfem/core.py
def beam1s(ex, ep, ed, eq=None, nep=None):
    """
    Compute section forces in a 1D beam element (beam1e).

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ep : array_like
        Element properties [E, I], where E is Young's modulus and I is moment of inertia.
    ed : array_like
        Element displacement vector [u1, ..., u4].
    eq : array_like, optional
        Distributed loads [qy], local directions.
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 2).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 1), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom  2021-09-01
                   O Dahlblom  2022-10-25 (Python version)
    """

    E, I = ep
    DEI = E*I

    qY=0.

    if not eq is None:  
       qY = eq[0] 

    ne = 2
    if nep != None: 
       ne = nep

    x1, x2 = ex
    dx = x2-x1
    L = abs(dx)

    a2 = ed.reshape(4,1)

    C2 = np.array([
        [1.,      0.,    0.,     0.],
        [0.,      1.,    0.,     0.],
        [-3/L**2, -2/L,  3/L**2, -1/L],
        [2/L**3,  1/L**2, -2/L**3, 1/L**2]
    ]) 

    C2a = C2 @ a2

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1)   
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    v = np.concatenate((one,  X, X**2, X**3), 1) @ C2a
#   dv = np.concatenate((zero,  one, 2*X, 3*X**2), 1) @ C2a
    d2v = np.concatenate((zero, zero, 2*one, 6*X), 1) @ C2a
    d3v = np.concatenate((zero, zero, zero, 6*one), 1) @ C2a

    if DEI != 0:
       v = v+(X**4 - 2*L*X**3 + L**2*X**2)*qY/(24*DEI)
#      dv = dv+(2*X**3 - 3*L*X**2 + L**2*X)*qY/(12*DEI)
       d2v = d2v+(6*X**2 - 6*L*X + L**2*one)*qY/(12*DEI)
       d3v = d3v+(2*X - L*one)*qY/(2*DEI)

    M = DEI*d2v
    V = -DEI*d3v 
    es = np.concatenate((V, M), 1)
    edi = v
    eci = X

    if nep is None:
        return es
    else:
        return es, edi, eci

beam1we(ex, ep, eq=None)

Compute the stiffness matrix (and optionally load vector) for a 1D beam element on elastic foundation.

Parameters

ex : array_like Element node coordinates [x1, x2]. ep : array_like Element properties [E, I, kY], where E is Young's modulus, I is moment of inertia, and kY is transversal foundation stiffness. eq : array_like, optional Distributed load [qY].

Returns

Ke : ndarray Beam stiffness matrix, shape (4, 4). fe : ndarray, optional Element load vector, shape (4, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2016-02-17 O Dahlblom 2022-10-18 (Python version)

Source code in src/calfem/core.py
def beam1we(ex, ep, eq=None):
    """
    Compute the stiffness matrix (and optionally load vector) for a 1D beam element on elastic foundation.

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ep : array_like
        Element properties [E, I, kY], where E is Young's modulus, I is moment of inertia, and kY is transversal foundation stiffness.
    eq : array_like, optional
        Distributed load [qY].

    Returns
    -------
    Ke : ndarray
        Beam stiffness matrix, shape (4, 4).
    fe : ndarray, optional
        Element load vector, shape (4, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2016-02-17
                   O Dahlblom   2022-10-18 (Python version)
    """
    E, I, kY =ep
    DEI = E*I;

    qY = 0
    if not eq is None:
        qY = eq[0]

    x1, x2 = ex
    dx = x2-x1
    L = abs(dx)

    K0 = DEI/L**3*np.array([
        [12,  6*L,    -12,  6*L],
        [6*L, 4*L**2, -6*L, 2*L**2],
        [-12, -6*L,   12,   -6*L],
        [6*L, 2*L**2, -6*L, 4*L**2]
    ])    

    Ks = kY*L/420*np.array([
        [156,   22*L,    54,   -13*L],
        [22*L,  4*L**2,  13*L, -3*L**2],
        [54,    13*L,    156,  -22*L],
        [-13*L, -3*L**2, -22*L, 4*L**2]
    ])

    Ke = K0+Ks

    fe = qY*np.array([L/2, L**2/12, L/2, -L**2/12]).reshape(4,1)
    if eq is None:
        return Ke
    else:
        return Ke, fe

beam1ws(ex, ep, ed, eq=None, nep=None)

Compute section forces in a 1D beam element on elastic foundation (beam1we).

Parameters

ex : array_like Element node coordinates [x1, x2]. ep : array_like Element properties [E, I, kY], where E is Young's modulus, I is moment of inertia, and kY is transversal foundation stiffness. ed : array_like Element displacement vector [u1, ..., u4]. eq : array_like, optional Distributed loads [qy], local directions. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 2). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 1), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2021-09-01 O Dahlblom 2022-10-18 (Python version)

Source code in src/calfem/core.py
def beam1ws(ex, ep, ed, eq=None, nep=None):
    """
    Compute section forces in a 1D beam element on elastic foundation (beam1we).

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ep : array_like
        Element properties [E, I, kY], where E is Young's modulus, I is moment of inertia, and kY is transversal foundation stiffness.
    ed : array_like
        Element displacement vector [u1, ..., u4].
    eq : array_like, optional
        Distributed loads [qy], local directions.
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 2).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 1), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom  2021-09-01
                   O Dahlblom  2022-10-18 (Python version)
    """
    E, I, kY = ep
    DEI = E*I

    qY = 0.

    if not eq is None:  
       qY = eq[0] 

    ne = 2
    if nep != None: 
        ne = nep

    x1, x2 = ex
    dx = x2-x1
    L = abs(dx)

    a2 = ed.reshape(4,1)  

    C2 = np.array([
        [1.,      0.,    0.,     0.],
        [0.,      1.,    0.,     0.],
        [-3/L**2, -2/L,  3/L**2, -1/L],
        [2/L**3,  1/L**2, -2/L**3, 1/L**2]
    ]) 

    C2a = C2 @ a2

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    v = np.concatenate((one,  X, X**2, X**3), 1) @ C2a
    d2v = np.concatenate((zero, zero, 2*one, 6*X), 1) @ C2a
    d3v = np.concatenate((zero, zero, zero, 6*one), 1) @ C2a

    if DEI != 0:
       v = v - kY/DEI*np.concatenate((
       (X**4 - 2*L*X**3 + L**2*X**2)/24,
       (X**5 - 3*L**2*X**3 + 2*L**3*X**2)/120,
       (X**6 - 4*L**3*X**3 + 3*L**4*X**2)/360, 
       (X**7 - 5*L**4*X**3 + 4*L**5*X**2)/840), 1) @ C2a + \
       (X**4 - 2*L*X**3 + L**2*X**2)*qY/(24*DEI)
       d2v = d2v - kY/DEI*np.concatenate((
       (6*X**2 - 6*L*X + L**2*one)/12, 
       (10*X**3 - 9*L**2*X + 2*L**3*one)/60, 
       (5*X**4 - 4*L**3*X + L**4*one)/60, 
       (21*X**5 - 15*L**4*X + 4*L**5*one)/420),1) @ C2a + \
       (6*X**2 - 6*L*X + L**2*one)*qY/(12*DEI)
       d3v = d3v - kY/DEI*np.concatenate((
       (2*X - L*one)/2,
       (10*X**2 - 3*L**2)/20, 
       (5*X**3 - L**3*one)/15,
       (7*X**4 - L**4*one)/28), 1) @ C2a + \
       (2*X - L*one)*qY/(2*DEI)
    M = DEI*d2v
    V = -DEI*d3v 
    es = np.concatenate((V, M), 1)
    edi = v
    eci = X

    if nep is None:
        return es
    else:
        return es, edi, eci

beam2crd(ex=None, ey=None, ed=None, mag=None)

Calculate the element continuous displacements for multiple identical 2D Bernoulli beam elements.

Parameters

ex : array_like, optional Element node x-coordinates. ey : array_like, optional Element node y-coordinates. ed : array_like, optional Element displacement matrix. mag : float, optional Magnification factor.

Returns

excd : ndarray Continuous x-coordinates. eycd : ndarray Continuous y-coordinates.

History

LAST MODIFIED: P-E AUSTRELL 1993-10-15

Source code in src/calfem/core.py
def beam2crd(ex=None, ey=None, ed=None, mag=None):
    """
    Calculate the element continuous displacements for multiple identical 2D Bernoulli beam elements.

    Parameters
    ----------
    ex : array_like, optional
        Element node x-coordinates.
    ey : array_like, optional
        Element node y-coordinates.
    ed : array_like, optional
        Element displacement matrix.
    mag : float, optional
        Magnification factor.

    Returns
    -------
    excd : ndarray
        Continuous x-coordinates.
    eycd : ndarray
        Continuous y-coordinates.

    History
    -------
    LAST MODIFIED: P-E AUSTRELL 1993-10-15
    """

    nie, ned = ed.shape

    n_coords = 21

    excd = np.zeros([nie, n_coords])
    eycd = np.zeros([nie, n_coords])

    for i in range(nie):
        b = np.array([ex[i, 1] - ex[i, 0], ey[i, 1] - ey[i, 0]])
        L = np.sqrt(b @ np.transpose(b))
        n = b / L

        G = np.array([
            [n[0], n[1], 0, 0, 0, 0],
            [-n[1], n[0], 0, 0, 0, 0],
            [0, 0, 1, 0, 0, 0],
            [0, 0, 0, n[0], n[1], 0],
            [0, 0, 0, - n[1], n[0], 0],
            [0, 0, 0, 0, 0, 1]
        ])

        d = np.transpose(ed[i, :])
        dl = G @ d
        xl = np.transpose(np.linspace(0, L, n_coords))
        one = np.ones(xl.shape)

        Cis = np.array([
            [-1, 1],
            [L, 0]
        ]) / L

        ds = np.array([dl[0], dl[3]]).reshape(2, 1)
        xl_one = np.transpose(np.vstack((xl, one)))
        ul = np.transpose(xl_one@Cis@ds)  # [20x1][2]

        Cib = np.array([
            [12, 6 * L, - 12, 6 * L],
            [- 6 * L, - 4 * L ** 2, 6 * L, - 2 * L ** 2],
            [0, L ** 3, 0, 0],
            [L ** 3, 0, 0, 0]
        ]) / L ** 3

        db = np.array([dl[1], dl[2], dl[4], dl[5]]).reshape(4, 1)
        vl = np.transpose(np.transpose(
            np.vstack((xl**3/6, xl**2/2, xl, one)))@Cib@db)

        cld = np.vstack((ul, vl))
        A = np.array([
            [n[0], -n[1]],
            [n[1], n[0]]
        ])
        cd = A@cld

        # [2,1] x [1,20] + [2 x 1] x [1 x 20]
        #    [2 x 20]    +      [2 x 20]

        AA = A[:, 0].reshape(2, 1)
        XL = xl.reshape(1, n_coords)
        xyc = AA@XL + np.array([[ex[i, 0]], [ey[i, 0]]]
                               )@one.reshape(1, n_coords)

        excd[i, :] = xyc[0, :]+mag*cd[0, :]
        eycd[i, :] = xyc[1, :]+mag*cd[1, :]

    return excd, eycd

beam2crd_old(ex, ey, ed, mag)

Calculate the element continuous displacements for multiple identical 2D Bernoulli beam elements.

Parameters

ex : array_like Element node x-coordinates. ey : array_like Element node y-coordinates. ed : array_like Element displacement matrix. mag : float Magnification factor.

Returns

excd : ndarray Continuous x-coordinates. eycd : ndarray Continuous y-coordinates.

History

LAST MODIFIED: P-E AUSTRELL 1993-10-15 J Lindemann 2021-12-30 (Python)

Source code in src/calfem/core.py
def beam2crd_old(ex, ey, ed, mag):
    """
    Calculate the element continuous displacements for multiple identical 2D Bernoulli beam elements.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates.
    ey : array_like
        Element node y-coordinates.
    ed : array_like
        Element displacement matrix.
    mag : float
        Magnification factor.

    Returns
    -------
    excd : ndarray
        Continuous x-coordinates.
    eycd : ndarray
        Continuous y-coordinates.

    History
    -------
    LAST MODIFIED: P-E AUSTRELL 1993-10-15
                   J Lindemann 2021-12-30 (Python)
    """
    nie, ned = ed.shape

    excd = np.zeros([nie, 20])
    eycd = np.zeros([nie, 20])

    for i in range(nie):

        b = np.array([ex[i, 1]-ex[i, 0], ey[i, 1]-ey[i, 0]])
        L = np.sqrt(b@np.transpose(b)).item()
        n = b/L

        G = np.array([
            [n[0], n[1],  0,    0,    0,   0],
            [-n[1], n[0],  0,    0,    0,   0],
            [0,    0,     1,    0,    0,   0],
            [0,    0,     0,   n[0], n[1], 0],
            [0,    0,     0,  -n[1], n[0], 0],
            [0,    0,     0,    0,    0,   1]
        ])

        d = ed[i, :]
        dl = G @ d

        xl = np.linspace(0.0, L, 20)
        one = np.ones(xl.shape)

        Cis = np.array([
            [-1.0, 1.0],
            [L, 0.0]
        ]) / L

        ds = np.array([dl[0], dl[3]]).reshape(2, 1)

        xl_one = np.transpose(np.vstack((xl, one)))

        ul = np.transpose(xl_one@Cis@ds)  # [20x1][2]

        Cib = np.array([
            [12,      6*L,  -12,    6*L],
            [-6*L, -4*L**2,  6*L, -2*L**2],
            [0,      L**3,    0,     0],
            [L**3,      0,     0,     0]
        ])/L**3

        db = np.array([dl[1], dl[2], dl[4], dl[5]]).reshape(4, 1)
        vl = np.transpose(np.transpose(
            np.vstack((xl**3/6, xl**2/2, xl, one)))@Cib@db)

        cld = np.vstack((ul, vl))
        A = np.array([
            [n[0], -n[1]],
            [n[1], n[0]]
        ])
        cd = A@cld

        # [2,1] x [1,20] + [2 x 1] x [1 x 20]
        #    [2 x 20]    +      [2 x 20]

        AA = A[:, 0].reshape(2, 1)
        XL = xl.reshape(1, 20)
        xyc = AA@XL + np.array([[ex[i, 0]], [ey[i, 0]]])@one.reshape(1, 20)

        excd[i, :] = xyc[0, :]+mag*cd[0, :]
        eycd[i, :] = xyc[1, :]+mag*cd[1, :]

    return excd, eycd

beam2de(ex, ey, ep)

Calculate the stiffness matrix Ke, mass matrix Me, and optionally damping matrix Ce for a 2D elastic Bernoulli beam element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I, m, (a, b)], where E is Young's modulus, A is cross-sectional area, I is moment of inertia, m is mass per unit length, and a, b are optional damping coefficients where Ce = aMe + bKe.

Returns

Ke : ndarray Element stiffness matrix, shape (6, 6). Me : ndarray Element mass matrix, shape (6, 6). Ce : ndarray, optional Element damping matrix, shape (6, 6), if damping coefficients are provided.

History

LAST MODIFIED: K Persson 1995-08-23 O Dahlblom 2022-12-08 (Python version)

Source code in src/calfem/core.py
def beam2de(ex, ey, ep):
    """
    Calculate the stiffness matrix Ke, mass matrix Me, and optionally damping matrix Ce for a 2D elastic Bernoulli beam element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I, m, (a, b)], where E is Young's modulus, A is cross-sectional area,
        I is moment of inertia, m is mass per unit length, and a, b are optional damping coefficients
        where Ce = a*Me + b*Ke.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (6, 6).
    Me : ndarray
        Element mass matrix, shape (6, 6).
    Ce : ndarray, optional
        Element damping matrix, shape (6, 6), if damping coefficients are provided.

    History
    -------
    LAST MODIFIED: K Persson    1995-08-23
                   O Dahlblom   2022-12-08 (Python version)
    """
    b = np.array([
        [ex[1]-ex[0]],
        [ey[1]-ey[0]]
    ])
    L = np.sqrt(b.T @ b).item(0)
    n = np.array(b/L).reshape(2,)

    a = 0
    b = 0
    if np.size(ep) == 4:
        E, A, I, m = ep
    elif np.size(ep) == 6:
        E, A, I, m, a, b = ep

    print ("E, A, I, m, a, b, L")    
    print (E, A, I, m, a, b, L) 


    Kle = np.array([
        [E*A/L, 0,           0,         -E*A/L, 0,           0],
        [0,     12*E*I/L**3, 6*E*I/L**2, 0,    -12*E*I/L**3, 6*E*I/L**2],
        [0,     6*E*I/L**2,  4*E*I/L,    0,    -6*E*I/L**2,  2*E*I/L],
        [-E*A/L, 0,           0,          E*A/L, 0,           0],
        [0,    -12*E*I/L**3, -6*E*I/L**2, 0,     12*E*I/L**3, -6*E*I/L**2],
        [0,     6*E*I/L**2,  2*E*I/L,    0,    -6*E*I/L**2,  4*E*I/L]
    ])

    Mle = m*L/420*np.array([
        [140, 0,    0,      70,  0,    0],
        [0,   156,  22*L,   0,   54,  -13*L],
        [0,   22*L, 4*L**2, 0,   13*L, -3*L**2],
        [70,  0,    0,      140, 0,    0],
        [0,   54,   13*L,   0,   156, -22*L],
        [0,  -13*L, -3*L**2, 0,  -22*L, 4*L**2]
    ])

    Cle = a*Mle+b*Kle

    G = np.array([
        [n[0], n[1], 0, 0,    0,    0],
        [-n[1], n[0], 0, 0,    0,    0],
        [0,    0,    1, 0,    0,    0],
        [0,    0,    0, n[0], n[1], 0],
        [0,    0,    0, -n[1], n[0], 0],
        [0,    0,    0, 0,    0,    1]
    ])

    Ke = G.T @ Kle @ G
    Me = G.T @ Mle @ G
    Ce = G.T @ Cle @ G

    if np.size(ep) == 4:
        return Ke, Me
    elif np.size(ep) == 6:
        return Ke, Me, Ce

beam2ds(ex, ey, ep, ed, ev, ea)

Calculate element forces for multiple identical 2D Bernoulli beam elements in dynamic analysis.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I, m, (a, b)], where E is Young's modulus, A is cross-sectional area, I is moment of inertia, m is mass per unit length, and a, b are optional damping coefficients where Ce = aMe + bKe. ed : array_like Element displacement matrix. ev : array_like Element velocity matrix. ea : array_like Element acceleration matrix.

Returns

es : ndarray Element forces in local directions, shape (nie, 6). Each row contains [-N1, -V1, -M1, N2, V2, M2].

History

LAST MODIFIED: K Persson 1995-08-23 O Dahlblom 2022-12-08 (Python version)

Source code in src/calfem/core.py
def beam2ds(ex, ey, ep, ed, ev, ea):
    """
    Calculate element forces for multiple identical 2D Bernoulli beam elements in dynamic analysis.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I, m, (a, b)], where E is Young's modulus, A is cross-sectional area,
        I is moment of inertia, m is mass per unit length, and a, b are optional damping coefficients
        where Ce = a*Me + b*Ke.
    ed : array_like
        Element displacement matrix.
    ev : array_like
        Element velocity matrix.
    ea : array_like
        Element acceleration matrix.

    Returns
    -------
    es : ndarray
        Element forces in local directions, shape (nie, 6).
        Each row contains [-N1, -V1, -M1, N2, V2, M2].

    History
    -------
    LAST MODIFIED: K Persson    1995-08-23
                   O Dahlblom   2022-12-08 (Python version)
    """
    b = np.array([
        [ex[1]-ex[0]],
        [ey[1]-ey[0]]
    ])
    L = np.sqrt(b.T @ b).item(0)
    n = np.array(b/L).reshape(2,)

    a = 0
    b = 0
    if np.size(ep) == 4:
        E, A, I, m = ep
    elif np.size(ep) == 6:
        E, A, I, m, a, b = ep

    Kle = np.array([
        [E*A/L, 0,           0,         -E*A/L, 0,           0],
        [0,     12*E*I/L**3, 6*E*I/L**2, 0,    -12*E*I/L**3, 6*E*I/L**2],
        [0,     6*E*I/L**2,  4*E*I/L,    0,    -6*E*I/L**2,  2*E*I/L],
        [-E*A/L, 0,           0,          E*A/L, 0,           0],
        [0,    -12*E*I/L**3, -6*E*I/L**2, 0,     12*E*I/L**3, -6*E*I/L**2],
        [0,     6*E*I/L**2,  2*E*I/L,    0,    -6*E*I/L**2,  4*E*I/L]
    ])

    Mle = m*L/420*np.array([
        [140, 0,    0,      70,  0,    0],
        [0,   156,  22*L,   0,   54,  -13*L],
        [0,   22*L, 4*L**2, 0,   13*L, -3*L**2],
        [70,  0,    0,      140, 0,    0],
        [0,   54,   13*L,   0,   156, -22*L],
        [0,  -13*L, -3*L**2, 0,  -22*L, 4*L**2]
    ])

    Cle = a*Mle+b*Kle

    G = np.array([
        [n[0], n[1], 0, 0,    0,    0],
        [-n[1], n[0], 0, 0,    0,    0],
        [0,    0,    1, 0,    0,    0],
        [0,    0,    0, n[0], n[1], 0],
        [0,    0,    0, -n[1], n[0], 0],
        [0,    0,    0, 0,    0,    1]
    ])

    nie, ned = ed.shape
    es = np.array(np.zeros((nie, 6)))
    for i in range(nie):
       d = ed[i,:].reshape(6,1)
       v = ev[i,:].reshape(6,1)
       a = ea[i,:].reshape(6,1)
       es[i,:] = (Kle @ G @ d + Cle @ G @ v + Mle @ G @ a).T

#   [nie,ned]=size(ed);
#   for i=1:nie
#        d=ed(i,:)';
#        v=ev(i,:)';
#        a=ea(i,:)';
#        es(i,:)=(Kle*G*d+Cle*G*v+Mle*G*a)';

    return es

beam2e(ex, ey, ep, eq=None)

Compute the stiffness matrix (and optionally load vector) for a 2D beam element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia. eq : array_like, optional Distributed loads [qX, qY], local directions.

Returns

Ke : ndarray Element stiffness matrix, shape (6, 6). fe : ndarray, optional Element load vector, shape (6, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2015-08-17 O Dahlblom 2022-11-21 (Python version)

Source code in src/calfem/core.py
def beam2e(ex: ArrayLike, ey: ArrayLike, ep: ArrayLike, eq: Optional[ArrayLike] = None) -> Union[NDArray[np.floating], Tuple[NDArray[np.floating], NDArray[np.floating]]]:
    """
    Compute the stiffness matrix (and optionally load vector) for a 2D beam element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia.
    eq : array_like, optional
        Distributed loads [qX, qY], local directions.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (6, 6).
    fe : ndarray, optional
        Element load vector, shape (6, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-08-17
                   O Dahlblom   2022-11-21 (Python version)
    """
    E, A, I = ep
    DEA = E*A
    DEI = E*I

    qX = 0.
    qY = 0.
    if not eq is None:
        qX, qY = eq

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    Kle = np.array([
        [ DEA/L,           0.,          0., -DEA/L,           0.,          0.],
        [    0.,  12*DEI/L**3,  6*DEI/L**2,     0., -12*DEI/L**3,  6*DEI/L**2],
        [    0.,   6*DEI/L**2,     4*DEI/L,     0.,  -6*DEI/L**2,     2*DEI/L],
        [-DEA/L,           0.,          0.,  DEA/L,           0.,          0.],
        [    0., -12*DEI/L**3, -6*DEI/L**2,     0.,  12*DEI/L**3, -6*DEI/L**2],
        [    0.,   6*DEI/L**2,     2*DEI/L,     0.,  -6*DEI/L**2,     4*DEI/L]
    ])

    fle = L*np.array([qX/2, qY/2, qY*L/12, qX/2, qY/2, -qY*L/12]).reshape(6,1)

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    Ke = G.T @ Kle @ G
    fe = G.T @ fle

    if eq is None:
        return Ke
    else:
        return Ke, fe

beam2ge(ex, ey, ep, QX, eq=None)

Compute the element stiffness matrix (and optionally load vector) for a 2D beam element with geometric nonlinearity.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia. QX : float Axial force in the beam. eq : array_like, optional Distributed transverse load [qY].

Returns

Ke : ndarray Element stiffness matrix, shape (6, 6). fe : ndarray, optional Element load vector, shape (6, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2015-12-17 O Dahlblom 2022-12-08 (Python version)

Source code in src/calfem/core.py
def beam2ge(ex, ey, ep, QX, eq=None):
    """
    Compute the element stiffness matrix (and optionally load vector) for a 2D beam element with geometric nonlinearity.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia.
    QX : float
        Axial force in the beam.
    eq : array_like, optional
        Distributed transverse load [qY].

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (6, 6).
    fe : ndarray, optional
        Element load vector, shape (6, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-12-17
                   O Dahlblom   2022-12-08 (Python version)
    """
    E, A, I = ep
    DEA = E*A
    DEI = E*I

    if eq != None:
        if np.size(eq) > 1:
            error("eq should be a scalar !!!")
            return
        else:
            qY = eq[0]
    else:
        qY = 0

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    K0le = np.array([
        [ DEA/L,           0.,          0., -DEA/L,           0.,          0.],
        [    0.,  12*DEI/L**3,  6*DEI/L**2,     0., -12*DEI/L**3,  6*DEI/L**2],
        [    0.,   6*DEI/L**2,     4*DEI/L,     0.,  -6*DEI/L**2,     2*DEI/L],
        [-DEA/L,           0.,          0.,  DEA/L,           0.,          0.],
        [    0., -12*DEI/L**3, -6*DEI/L**2,     0.,  12*DEI/L**3, -6*DEI/L**2],
        [    0.,   6*DEI/L**2,     2*DEI/L,     0.,  -6*DEI/L**2,     4*DEI/L]
    ])

    Ksle = QX/(30*L)*np.array([
        [   0.,   0.,     0.,   0.,   0.,     0.],
        [   0.,  36.,    3*L,   0., -36.,    3*L],
        [   0.,  3*L, 4*L**2,   0., -3*L,  -L**2],
        [   0.,   0.,     0.,   0.,   0.,     0.],
        [   0., -36.,   -3*L,   0.,  36.,   -3*L],
        [   0.,  3*L,  -L**2,   0., -3*L, 4*L**2]
    ])

    fle = qY*L*np.array([0, 1/2, L/12, 0, 1/2, -L/12]).reshape(6,1)

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    Kle = K0le+Ksle 
    Ke = G.T @ Kle @ G
    fe = G.T @ fle

    if eq is None:
        return Ke
    else:
        return Ke, fe

beam2gs(ex, ey, ep, ed, QX, eq=None, nep=None)

Calculate section forces in a 2D nonlinear beam element (beam2ge).

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia. ed : array_like Element displacement vector [u1, ..., u6]. QX : float Axial force in the beam. eq : array_like, optional Distributed transverse load [qY]. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 3). QX : float Axial force. edi : ndarray, optional Element displacements at evaluation points, shape (nep, 2), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2021-09-01 O Dahlblom 2022-12-06 (Python version)

Source code in src/calfem/core.py
def beam2gs(ex, ey, ep, ed, QX, eq=None, nep=None):
    """
    Calculate section forces in a 2D nonlinear beam element (beam2ge).

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia.
    ed : array_like
        Element displacement vector [u1, ..., u6].
    QX : float
        Axial force in the beam.
    eq : array_like, optional
        Distributed transverse load [qY].
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 3).
    QX : float
        Axial force.
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 2), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom   2021-09-01
                   O Dahlblom   2022-12-06 (Python version)
    """
    E, A, I = ep
    DEA = E*A
    DEI = E*I

    if eq != None:
        if np.size(eq) > 1:
            error("eq should be a scalar !!!")
            return
        else:
            qY = eq[0]
    else:
        qY = 0

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    ne = 2
    if nep != None: 
       ne = nep

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    edl = G @ ed.reshape(6,1)

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    a1 = np.array([
        edl[0],
        edl[3]
    ])
    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 
    C1a = C1 @ a1

    a2 = np.array([
        edl[1],
        edl[2],
        edl[4],
        edl[5]
    ])
    C2 = np.array([
        [1.,      0.,    0.,     0.],
        [0.,      1.,    0.,     0.],
        [-3/L**2, -2/L,  3/L**2, -1/L],
        [2/L**3,  1/L**2, -2/L**3, 1/L**2]
    ]) 
    C2a = C2 @ a2

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a
    v = np.concatenate((one,  X, X**2, X**3), 1) @ C2a
    dv = np.concatenate((zero,  one, 2*X, 3*X**2), 1) @ C2a
    d2v = np.concatenate((zero, zero, 2*one, 6*X), 1) @ C2a
    d3v = np.concatenate((zero, zero, zero, 6*one), 1) @ C2a
    if DEI != 0:
       v = v + QX/DEI*np.concatenate((zero, zero, (X**4-2*L*X**3+L**2*X**2)/12, (X**5-3*L**2*X**3+2*L**3*X**2)/20), 1) @ C2a + \
       (X**4 - 2*L*X**3 + L**2*X**2)*qY/(24*DEI)
       dv = dv + QX/DEI*np.concatenate((zero, zero, (2*X**3-3*L*X**2+L**2*X)/6, (5*X**4-9*L**2*X**2+4*L**3*X)/20), 1) @ C2a + \
       (2*X**3 - 3*L*X**2 + L**2*X)*qY/(12*DEI)
       d2v = d2v + QX/DEI*np.concatenate((zero, zero, (6*X**2-6*L*X+L**2*one)/6, (10*X**3-9*L**2*X+2*L**3*one)/10), 1) @ C2a + \
       (6*X**2 - 6*L*X + L**2*one)*qY/(12*DEI)
       d3v = d3v + QX/DEI*np.concatenate((zero, zero, (2*X-L*one), (30*X**2-9*L**2*one)/10), 1) @ C2a + \
       (2*X - L*one)*qY/(2*DEI)

    QX = DEA*du.item(0)
    M = DEI*d2v
    V = -DEI*d3v
    N=QX+dv*V
    es = np.concatenate((N, V, M), 1)
    edi = np.concatenate((u, v), 1)
    eci = X

    if nep is None:
        return es, QX
    else:
        return es, QX, edi, eci

beam2gxe(ex, ey, ep, QX, eq=None)

Compute the element stiffness matrix (and optionally load vector) for a 2D beam element with geometric nonlinearity (exact solution).

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia. QX : float Axial force in the beam. eq : array_like, optional Distributed transverse load.

Returns

Ke : ndarray Element stiffness matrix, shape (6, 6). fe : ndarray, optional Element load vector, shape (6, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2021-06-21 O Dahlblom 2022-12-06 (Python version)

Source code in src/calfem/core.py
def beam2gxe(ex, ey, ep, QX, eq=None):
    """
    Compute the element stiffness matrix (and optionally load vector) for a 2D beam element with geometric nonlinearity (exact solution).

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia.
    QX : float
        Axial force in the beam.
    eq : array_like, optional
        Distributed transverse load.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (6, 6).
    fe : ndarray, optional
        Element load vector, shape (6, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2021-06-21
                   O Dahlblom   2022-12-06 (Python version)
    """
    E, A, I = ep
    DEA = E*A
    DEI = E*I

    if eq != None:
        if np.size(eq) > 1:
            error("eq should be a scalar !!!")
            return
        else:
            qY = eq[0]
    else:
        qY = 0

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    eps = 1e-12

    if QX < -eps*DEI/L**2: 
        kL = np.sqrt(-QX/DEI)*L
        f1 = (kL/2)/np.tan(kL/2)
        f2 = kL**2/(12*(1-f1))
        f3 = f1/4+3*f2/4
        f4 = -f1/2+3*f2/2
        f5 = f1*f2
        h = 6*(2/kL**2-(1+np.cos(kL))/(kL*np.sin(kL)))
    elif QX > eps*DEI/L**2:
        kL = np.sqrt(QX/DEI)*L
        f1 = (kL/2)/np.tanh(kL/2)
        f2 = -(1/12.)*kL**2/(1-f1)
        f3 = f1/4+3*f2/4
        f4 = -f1/2+3*f2/2
        f5 = f1*f2
        h = -6*(2/kL**2-(1+np.cosh(kL))/(kL*np.sinh(kL)))
    else:
        f1 = f2 = f3 = f4 = f5 = h = 1

    Kle = np.array([
         [DEA/L,              0.,             0., -DEA/L, 0.,                          0.],
        [    0.,  12*DEI*f5/L**3,  6*DEI*f2/L**2,     0., -12*DEI*f5/L**3,  6*DEI*f2/L**2],
        [    0.,   6*DEI*f2/L**2,     4*DEI*f3/L,     0.,  -6*DEI*f2/L**2,     2*DEI*f4/L],
        [-DEA/L,              0.,             0.,  DEA/L,              0.,             0.],
        [    0., -12*DEI*f5/L**3, -6*DEI*f2/L**2,     0.,  12*DEI*f5/L**3, -6*DEI*f2/L**2],
            [0.,   6*DEI*f2/L**2,     2*DEI*f4/L,     0.,  -6*DEI*f2/L**2,     4*DEI*f3/L]
    ])

    fle = qY*L*np.array([0., 1/2., L*h/12, 0., 1/2., -L*h/12]).reshape(6,1)

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    Ke = G.T @ Kle @ G
    fe = G.T @ fle

    if eq is None:
        return Ke
    else:
        return Ke, fe

beam2gxs(ex, ey, ep, ed, QX, eq=None, nep=None)

Calculate section forces in a 2D nonlinear beam element (beam2gxe).

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia. ed : array_like Element displacement vector [u1, ..., u6]. QX : float Axial force in the beam. eq : array_like, optional Distributed transverse load. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 3). QX : float Axial force. edi : ndarray, optional Element displacements at evaluation points, shape (nep, 2), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2021-06-21 O Dahlblom 2022-12-06 (Python version)

Source code in src/calfem/core.py
def beam2gxs(ex, ey, ep, ed, QX, eq=None, nep=None):
    """

    Calculate section forces in a 2D nonlinear beam element (beam2gxe).

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia.
    ed : array_like
        Element displacement vector [u1, ..., u6].
    QX : float
        Axial force in the beam.
    eq : array_like, optional
        Distributed transverse load.
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 3).
    QX : float
        Axial force.
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 2), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom   2021-06-21
                   O Dahlblom   2022-12-06 (Python version)
    """
    E, A, I = ep
    DEA = E*A
    DEI = E*I

    if eq != None:
        if np.size(eq) > 1:
            error("eq should be a scalar !!!")
            return
        else:
            qY = eq[0]
    else:
        qY = 0

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    ne = 2
    if nep != None: 
       ne = nep

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    edl = G @ ed.reshape(6,1)

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    a1 = np.array([
        edl[0],
        edl[3]
    ])
    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 
    C1a = C1 @ a1
    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a

    a2 = np.array([
        edl[1],
        edl[2],
        edl[4],
        edl[5]
    ])

    eps = 1e-12

    if QX < -eps*DEI/L**2:
        k = np.sqrt(-QX/DEI)
        kL = k*L
        C2 = 1/(k*(-2*(1-np.cos(kL))+kL*np.sin(kL)))*np.array([
            [k*(kL*np.sin(kL)+np.cos(kL)-1), -kL*np.cos(kL)+np.sin(kL), -k*(1-np.cos(kL)), -np.sin(kL)+kL],
            [-(k**2)*np.sin(kL), -k*(1-np.cos(kL)), (k**2)*np.sin(kL), -k*(1-np.cos(kL))],
            [-k*(1-np.cos(kL)), kL*np.cos(kL)-np.sin(kL),  k*(1-np.cos(kL)), np.sin(kL)-kL],
            [k*np.sin(kL), (kL*np.sin(kL)+np.cos(kL)-1), -k*np.sin(kL), (1-np.cos(kL))]
        ]) 

        C2a = C2 @ a2
        v = np.concatenate((one,  X, np.cos(k*X), np.sin(k*X)), 1) @ C2a
        dv = np.concatenate((zero, one, -k*np.sin(k*X), k*np.cos(k*X)), 1) @ C2a
        d2v = np.concatenate((zero, zero, -k**2*np.cos(k*X), -k**2*np.sin(k*X)), 1) @ C2a
        d3v = np.concatenate((zero, zero, k**3*np.sin(k*X), -k**3*np.cos(k*X)), 1) @ C2a
        if DEI != 0:
          v = v+qY*L**4/(2*DEI)*((1+np.cos(kL))/(kL**3*np.sin(kL))*(-1+np.cos(k*X))+np.sin(k*X)/kL**3+X*(-1+X/L)/(kL**2*L))
          dv = dv+qY*L**3/(2*DEI)*((1+np.cos(kL))/(kL**2*np.sin(kL))*(-np.sin(k*X))+np.cos(k*X)/kL**2+(-1+2*X/L)/kL**2)
          d2v = d2v+qY*L**2/(2*DEI)*((1+np.cos(kL))/(kL*np.sin(kL))*(-np.cos(k*X))-np.sin(k*X)/kL+2/(kL**2)) 
          d3v = d3v+qY*L/(2*DEI)*((1+np.cos(kL))/np.sin(kL)*(np.sin(k*X))-np.cos(k*X))
    elif QX > eps*DEI/L**2:
        k = np.sqrt(QX/DEI)
        kL = k*L
        C2 = 1/(k*(-2*(1-np.cosh(kL))-kL*np.sinh(kL)))*np.array([
            [k*(-kL*np.sinh(kL)+np.cosh(kL)-1), -kL*np.cosh(kL)+np.sinh(kL), -k*(1-np.cosh(kL)), -np.sinh(kL)+kL],
            [(k**2)*np.sinh(kL), -k*(1-np.cosh(kL)), -(k**2)*np.sinh(kL), -k*(1-np.cosh(kL))],
            [-k*(1-np.cosh(kL)), kL*np.cosh(kL)-np.sinh(kL),  k*(1-np.cosh(kL)), np.sinh(kL)-kL],
            [-k*np.sinh(kL), (-kL*np.sinh(kL)+np.cosh(kL)-1), k*np.sinh(kL), (1-np.cosh(kL))]
        ]) 
        C2a = C2 @ a2
        v = np.concatenate((one,  X, np.cosh(k*X), np.sinh(k*X)), 1) @ C2a
        dv = np.concatenate((zero, one, k*np.sinh(k*X), k*np.cosh(k*X)), 1) @ C2a
        d2v = np.concatenate((zero, zero, k**2*np.cosh(k*X), k**2*np.sinh(k*X)), 1) @ C2a
        d3v = np.concatenate((zero, zero, k**3*np.sinh(k*X), k**3*np.cosh(k*X)), 1) @ C2a
        if DEI != 0:
          v = v+qY*L**4/(2*DEI)*((1+np.cosh(kL))/(kL**3*np.sinh(kL))*(-1+np.cosh(k*X))-np.sinh(k*X)/kL**3+X*(1-X/L)/(kL**2*L))
          dv = dv+qY*L**3/(2*DEI)*((1+np.cosh(kL))/(kL**2*np.sinh(kL))*(np.sinh(k*X))-np.cosh(k*X)/kL**2+(1-2*X/L)/kL**2)
          d2v = d2v+qY*L**2/(2*DEI)*((1+np.cosh(kL))/(kL*np.sinh(kL))*(np.cosh(k*X))-np.sinh(k*X)/kL-2/(kL**2)) 
          d3v = d3v+qY*L/(2*DEI)*((1+np.cosh(kL))/np.sinh(kL)*(np.sinh(k*X))-np.cosh(k*X))
    else:
        C2 = np.array([
            [1.,      0.,    0.,     0.],
            [0.,      1.,    0.,     0.],
            [-3/L**2, -2/L,  3/L**2, -1/L],
            [2/L**3,  1/L**2, -2/L**3, 1/L**2]
        ]) 
        C2a = C2 @ a2
        v = np.concatenate((one,  X, X**2, X**3), 1) @ C2a
        dv = np.concatenate((zero,  one, 2*X, 3*X**2), 1) @ C2a
        d2v = np.concatenate((zero, zero, 2*one, 6*X), 1) @ C2a
        d3v = np.concatenate((zero, zero, zero, 6*one), 1) @ C2a
        if DEI != 0:
           v = v+(X**4 - 2*L*X**3 + L**2*X**2)*qY/(24*DEI)
           dv = dv+(2*X**3 -3*L*X**2 +L**2*X)*qY/(12*DEI)
           d2v = d2v +(6*X**2 - 6*L*X + L**2*one)*qY/(12*DEI)
           d3v = d3v +(2*X - L*one)*qY/(2*DEI)

    QX = DEA*du.item(0)
    M = DEI*d2v
    V = -DEI*d3v
    N=QX+dv*V
    es = np.concatenate((N, V, M), 1)
    edi = np.concatenate((u, v), 1)
    eci = X

    if nep is None:
        return es, QX
    else:
        return es, QX, edi, eci

beam2s(ex, ey, ep, ed, eq=None, nep=None)

Compute section forces in a 2D beam element (beam2e).

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia. ed : array_like Element displacement vector [u1, ..., u6]. eq : array_like, optional Distributed loads [qX, qY], local directions. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 3). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 2), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2015-08-17 O Dahlblom 2022-11-21 (Python version)

Source code in src/calfem/core.py
def beam2s(ex, ey, ep, ed, eq=None, nep=None):
    """
    Compute section forces in a 2D beam element (beam2e).

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I], where E is Young's modulus, A is cross-sectional area, and I is moment of inertia.
    ed : array_like
        Element displacement vector [u1, ..., u6].
    eq : array_like, optional
        Distributed loads [qX, qY], local directions.
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 3).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 2), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-08-17
                   O Dahlblom   2022-11-21 (Python version)
    """
    E, A, I = ep
    DEA = E*A
    DEI = E*I

    qX = 0.
    qY = 0.
    if not eq is None:
        qX, qY = eq

    ne=2
    if nep != None: 
       ne=nep

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    edl = G @ ed.reshape(6,1)

    a1 = np.array([
        edl[0],
        edl[3]
    ])
    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 
    C1a = C1 @ a1

    a2 = np.array([
        edl[1],
        edl[2],
        edl[4],
        edl[5]
    ])
    C2 = np.array([
        [1.,      0.,    0.,     0.],
        [0.,      1.,    0.,     0.],
        [-3/L**2, -2/L,  3/L**2, -1/L],
        [2/L**3,  1/L**2, -2/L**3, 1/L**2]
    ]) 
    C2a = C2 @ a2

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a
    if DEA != 0:
       u = u -(X**2-L*X)*qX/(2*DEA)
       du = du -(2*X-L)*qX/(2*DEA)

    v = np.concatenate((one,  X, X**2, X**3), 1) @ C2a
#   dv = np.concatenate((zero,  one, 2*X, 3*X**2), 1) @ C2a
    d2v=np.concatenate((zero, zero, 2*one, 6*X), 1) @ C2a
    d3v = np.concatenate((zero, zero, zero, 6*one), 1) @ C2a
    if DEI != 0:
       v = v+(X**4 - 2*L*X**3 + L**2*X**2)*qY/(24*DEI)
#      dv = dv+(2*X**3 - 3*L*X**2 + L**2*X)*qY/(12*DEI)
       d2v = d2v+(6*X**2 - 6*L*X + L**2*one)*qY/(12*DEI)
       d3v = d3v+(2*X - L*one)*qY/(2*DEI)

    N = DEA*du
    M = DEI*d2v
    V = -DEI*d3v 
    es = np.concatenate((N, V, M), 1)
    edi = np.concatenate((u, v), 1)
    eci = X

    if nep is None:
        return es
    else:
        return es, edi, eci

beam2te(ex, ey, ep, eq=None)

Compute the stiffness matrix (and optionally load vector) for a 2D Timoshenko beam element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, Gm, A, I, ks], where E is Young's modulus, Gm is shear modulus, A is cross-sectional area, I is moment of inertia, and ks is shear correction factor. eq : array_like, optional Distributed loads in local directions [qX, qY].

Returns

Ke : ndarray Element stiffness matrix, shape (6, 6). fe : ndarray, optional Element load vector, shape (6, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2021-11-05 O Dahlblom 2022-12-08 (Python version)

Source code in src/calfem/core.py
def beam2te(ex, ey, ep, eq=None):
    """
    Compute the stiffness matrix (and optionally load vector) for a 2D Timoshenko beam element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, Gm, A, I, ks], where E is Young's modulus, Gm is shear modulus, 
        A is cross-sectional area, I is moment of inertia, and ks is shear correction factor.
    eq : array_like, optional
        Distributed loads in local directions [qX, qY].

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (6, 6).
    fe : ndarray, optional
        Element load vector, shape (6, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2021-11-05
                   O Dahlblom   2022-12-08 (Python version)
    """
    E, Gm, A, I, ks = ep
    DEA = E*A
    DEI = E*I
    DGAks = Gm*A*ks

    qX = 0.
    qY = 0.
    if not eq is None:
        qX, qY = eq

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)
    m = (12*DEI)/(L**2*DGAks)
    f1=1/(1+m)
    f2=f1*(1+m/4)
    f3=f1*(1-m/2)


    Kle = np.array([
        [ DEA/L,              0.,             0., -DEA/L,              0.,              0.],
        [    0.,  12*DEI*f1/L**3,  6*DEI*f1/L**2,     0., -12*DEI*f1/L**3,   6*DEI*f1/L**2],
        [    0.,   6*DEI*f1/L**2,     4*DEI*f2/L,     0.,  -6*DEI*f1/L**2,      2*DEI*f3/L],
        [-DEA/L,              0.,             0.,  DEA/L,              0.,              0.],
        [    0., -12*DEI*f1/L**3, -6*DEI*f1/L**2,     0.,   12*DEI*f1/L**3, -6*DEI*f1/L**2],
        [    0.,   6*DEI*f1/L**2,     2*DEI*f3/L,     0.,   -6*DEI*f1/L**2,     4*DEI*f2/L]
    ])

    fle = L*np.array([qX/2, qY/2, qY*L/12, qX/2, qY/2, -qY*L/12]).reshape(6,1)

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    Ke = G.T @ Kle @ G
    fe = G.T @ fle

    if eq is None:
        return Ke
    else:
        return Ke, fe

beam2ts(ex, ey, ep, ed, eq=None, nep=None)

Compute section forces in a 2D Timoshenko beam element (beam2te).

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, G, A, I, ks], where E is Young's modulus, G is shear modulus, A is cross-sectional area, I is moment of inertia, and ks is shear correction factor. ed : array_like Element displacement vector [u1, ..., u6]. eq : array_like, optional Distributed loads in local directions [qx, qy]. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces in local directions at evaluation points, shape (nep, 3). Each row contains [N, V, M] (normal force, shear force, moment). edi : ndarray, optional Element displacements in local directions at evaluation points, shape (nep, 3), if nep is given. Each row contains [u, v, theta]. Note: For Timoshenko beam element, the rotation of the cross section is not equal to dv/dx. eci : ndarray, optional Local x-coordinates of the evaluation points, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2021-11-05 O Dahlblom 2022-12-08 (Python version)

Source code in src/calfem/core.py
def beam2ts(ex, ey, ep, ed, eq=None, nep=None):
    """
    Compute section forces in a 2D Timoshenko beam element (beam2te).

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, G, A, I, ks], where E is Young's modulus, G is shear modulus,
        A is cross-sectional area, I is moment of inertia, and ks is shear correction factor.
    ed : array_like
        Element displacement vector [u1, ..., u6].
    eq : array_like, optional
        Distributed loads in local directions [qx, qy].
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces in local directions at evaluation points, shape (nep, 3).
        Each row contains [N, V, M] (normal force, shear force, moment).
    edi : ndarray, optional
        Element displacements in local directions at evaluation points, shape (nep, 3), if nep is given.
        Each row contains [u, v, theta]. Note: For Timoshenko beam element, the rotation of the 
        cross section is not equal to dv/dx.
    eci : ndarray, optional
        Local x-coordinates of the evaluation points, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom   2021-11-05
                   O Dahlblom   2022-12-08 (Python version)
    """
    E, Gm, A, I, ks = ep
    DEA = E*A
    DEI = E*I
    DGAks = Gm*A*ks
    alpha=DEI/DGAks

    qX = 0.
    qY = 0.
    if not eq is None:
        qX, qY = eq

    ne=2
    if nep != None: 
       ne=nep

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    edl = G @ ed.reshape(6,1)

    a1 = np.array([
        edl[0],
        edl[3]
    ])
    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 
    C1a = C1 @ a1

    a2 = np.array([
        edl[1],
        edl[2],
        edl[4],
        edl[5]
    ])
    C2 = 1/(L**2+12*alpha)*np.array([
        [L**2+12*alpha,             0.,         0.,           0.],
        [  -12*alpha/L,   L**2+6*alpha, 12*alpha/L,     -6*alpha],
        [          -3., -2*L-6*alpha/L,         3., -L+6*alpha/L],
        [          2/L,             1.,       -2/L,           1.]
    ]) 
    C2a = C2 @ a2

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a
    if DEA != 0:
       u = u -(X**2-L*X)*qX/(2*DEA)
       du = du -(2*X-L)*qX/(2*DEA)

    v = np.concatenate((one,  X, X**2, X**3), 1) @ C2a
    dv = np.concatenate((zero,  one, 2*X, 3*X**2), 1) @ C2a
    theta = np.concatenate((zero, one, 2*X, 3*X**2+6*alpha), 1) @ C2a
    dtheta=np.concatenate((zero, zero, 2*one, 6*X), 1) @ C2a
    if DEI != 0:
       v = v+(X**4 - 2*L*X**3 + L**2*X**2)*qY/(24*DEI)+(-X**2/2+L*X/2)*qY/DGAks
       dv = dv+(2*X**3 - 3*L*X**2 + L**2*X)*qY/(12*DEI)+(-X+L/2)*qY/DGAks
       theta = theta+(2*X**3-3*L*X**2+L**2*X)*qY/(12*DEI)
       dtheta = dtheta+(6*X**2-6*L*X+L**2)*qY/(12*DEI)

    N = DEA*du
    M = DEI*dtheta
    V = DGAks*(dv-theta) 
    es = np.concatenate((N, V, M), 1)
    edi = np.concatenate((u, v, theta), 1)
    eci = X

    if nep is None:
        return es
    else:
        return es, edi, eci

beam2we(ex, ey, ep, eq=None)

Compute the stiffness matrix (and optionally load vector) for a 2D beam element on elastic foundation.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I, kX, kY], where E is Young's modulus, A is cross-sectional area, I is moment of inertia, kX is axial foundation stiffness, and kY is transversal foundation stiffness. eq : array_like, optional Distributed loads [qX, qY], local directions.

Returns

Ke : ndarray Element stiffness matrix, shape (6, 6). fe : ndarray, optional Element load vector, shape (6, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2015-08-07 O Dahlblom 2022-11-21 (Python version)

Source code in src/calfem/core.py
def beam2we(ex, ey, ep, eq=None):
    """
    Compute the stiffness matrix (and optionally load vector) for a 2D beam element on elastic foundation.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I, kX, kY], where E is Young's modulus, A is cross-sectional area, I is moment of inertia, kX is axial foundation stiffness, and kY is transversal foundation stiffness.
    eq : array_like, optional
        Distributed loads [qX, qY], local directions.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (6, 6).
    fe : ndarray, optional
        Element load vector, shape (6, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-08-07
                   O Dahlblom   2022-11-21 (Python version)
    """
    E, A, I, kX, kY = ep
    DEA = E*A
    DEI = E*I

    qX = 0
    qY = 0
    if not eq is None:
        qX, qY = eq

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    K0 = np.array([
        [ DEA/L,           0.,          0., -DEA/L,           0.,          0.],
        [    0.,  12*DEI/L**3,  6*DEI/L**2,     0., -12*DEI/L**3,  6*DEI/L**2],
        [    0.,   6*DEI/L**2,     4*DEI/L,     0.,  -6*DEI/L**2,     2*DEI/L],
        [-DEA/L,           0.,          0.,  DEA/L,           0.,          0.],
        [    0., -12*DEI/L**3, -6*DEI/L**2,     0.,  12*DEI/L**3, -6*DEI/L**2],
        [     0.,  6*DEI/L**2.,     2*DEI/L,     0.,  -6*DEI/L**2,     4*DEI/L]
    ])

    Ks = L/420*np.array([
        [140*kX, 0,       0,          70*kX,  0,       0],
        [0,      156*kY,  22*kY*L,    0,      54*kY,  -13*kY*L],
        [0,      22*kY*L, 4*kY*L**2,  0,      13*kY*L, -3*kY*L**2],
        [70*kX,  0,       0,          140*kX, 0,       0],
        [0,      54*kY,   13*kY*L,    0,      156*kY, -22*kY*L],
        [0,     -13*kY*L, -3*kY*L**2, 0,     -22*kY*L, 4*kY*L**2]
    ])

    Kle = K0+Ks

    fle = L*np.array([qX/2, qY/2, qY*L/12, qX/2, qY/2, -qY*L/12]).reshape(6,1)

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    Ke = G.T @ Kle @ G
    fe = G.T @ fle

    if eq is None:
        return Ke
    else:
        return Ke, fe

beam2ws(ex, ey, ep, ed, eq=None, nep=None)

Compute section forces in a 2D beam element on elastic foundation.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ep : array_like Element properties [E, A, I, kX, kY], where E is Young's modulus, A is cross-sectional area, I is moment of inertia, kX is axial foundation stiffness, and kY is transversal foundation stiffness. ed : array_like Element displacement vector [u1, ..., u6]. eq : array_like, optional Distributed loads [qX, qY], local directions. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 3). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 2), if nep is given. eci : ndarray, optional Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2022-09-30 O Dahlblom 2022-11-21 (Python version)

Source code in src/calfem/core.py
def beam2ws(ex, ey, ep, ed, eq=None, nep=None):
    """
    Compute section forces in a 2D beam element on elastic foundation.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ep : array_like
        Element properties [E, A, I, kX, kY], where E is Young's modulus, A is cross-sectional area, I is moment of inertia, kX is axial foundation stiffness, and kY is transversal foundation stiffness.
    ed : array_like
        Element displacement vector [u1, ..., u6].
    eq : array_like, optional
        Distributed loads [qX, qY], local directions.
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 3).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 2), if nep is given.
    eci : ndarray, optional
        Evaluation points on the local x-axis, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom   2022-09-30
                   O Dahlblom   2022-11-21 (Python version)
    """
    E, A, I, kX, kY = ep
    DEA = E*A
    DEI = E*I

    qX = 0
    qY = 0
    if not eq is None:
        qX, qY = eq

    x1, x2 = ex
    y1, y2 = ey
    dx = x2-x1
    dy = y2-y1
    L = np.sqrt(dx*dx+dy*dy)

    ne = 2
    if nep != None: 
       ne = nep

    nxX = dx/L
    nyX = dy/L
    nxY = -dy/L
    nyY = dx/L
    G = np.array([
        [nxX, nyX,   0,   0,   0,   0],
        [nxY, nyY,   0,   0,   0,   0],
        [  0,   0,   1,   0,   0,   0],
        [  0,   0,   0, nxX, nyX,   0],
        [  0,   0,   0, nxY, nyY,   0],
        [  0,   0,   0,   0,   0,   1]
    ])

    edl = G @ ed.reshape(6,1)

    a1 = np.array([
        edl[0],
        edl[3]
    ])
    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 
    C1a = C1 @ a1

    a2 = np.array([
        edl[1],
        edl[2],
        edl[4],
        edl[5]
    ])
    C2 = np.array([
        [1.,      0.,    0.,     0.],
        [0.,      1.,    0.,     0.],
        [-3/L**2, -2/L,  3/L**2, -1/L],
        [2/L**3,  1/L**2, -2/L**3, 1/L**2]
    ]) 
    C2a = C2 @ a2

    X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a
    if DEA != 0:
       u = u +kX/DEA*np.concatenate(((X**2-L*X)/2, (X**3-L**2*X)/6),1) @ C1a-(X**2-L*X)*qX/(2*DEA)
       du = du +kX/DEA*np.concatenate(((2*X-L)/2, (3*X**2-L**2)/6),1) @ C1a-(2*X-L)*qX/(2*DEA)

    v = np.concatenate((one,  X, X**2, X**3), 1) @ C2a
#   dv = np.concatenate((zero,  one, 2*X, 3*X**2), 1) @ C2a
    d2v = np.concatenate((zero, zero, 2*one, 6*X), 1) @ C2a
    d3v = np.concatenate((zero, zero, zero, 6*one), 1) @ C2a
    if DEI != 0:
       v = v - kY/DEI*np.concatenate((
       (X**4 - 2*L*X**3 + L**2*X**2)/24,
       (X**5 - 3*L**2*X**3 + 2*L**3*X**2)/120,
       (X**6 - 4*L**3*X**3 + 3*L**4*X**2)/360, 
       (X**7 - 5*L**4*X**3 + 4*L**5*X**2)/840), 1) @ C2a + \
       (X**4 - 2*L*X**3 + L**2*X**2)*qY/(24*DEI)
       d2v = d2v - kY/DEI*np.concatenate((
       (6*X**2 - 6*L*X + L**2*one)/12, 
       (10*X**3 - 9*L**2*X + 2*L**3*one)/60, 
       (5*X**4 - 4*L**3*X + L**4*one)/60, 
       (21*X**5 - 15*L**4*X + 4*L**5*one)/420),1) @ C2a + \
       (6*X**2 - 6*L*X + L**2*one)*qY/(12*DEI)
       d3v = d3v - kY/DEI*np.concatenate((
       (2*X - L*one)/2,
       (10*X**2 - 3*L**2)/20, 
       (5*X**3 - L**3*one)/15,
       (7*X**4 - L**4*one)/28), 1) @ C2a + \
       (2*X - L*one)*qY/(2*DEI)

    N = DEA*du
    M = DEI*d2v
    V = -DEI*d3v 
    es = np.concatenate((N, V, M), 1)
    edi = np.concatenate((u, v), 1)
    eci = X

    if nep is None:
        return es
    else:
        return es, edi, eci

beam3e(ex, ey, ez, eo, ep, eq=None)

Calculate the stiffness matrix (and optionally load vector) for a 3D elastic Bernoulli beam element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ez : array_like Element node z-coordinates [z1, z2]. eo : array_like Orientation of local z-axis [xz, yz, zz]. ep : array_like Element properties [E, G, A, Iy, Iz, Kv], where E is Young's modulus, G is shear modulus, A is cross-sectional area, Iy is moment of inertia about local y-axis, Iz is moment of inertia about local z-axis, and Kv is Saint-Venant's torsion constant. eq : array_like, optional Distributed loads in local directions [qX, qY, qZ, qW].

Returns

Ke : ndarray Element stiffness matrix, shape (12, 12). fe : ndarray, optional Element load vector, shape (12, 1), if eq is not None.

History

LAST MODIFIED: O Dahlblom 2015-10-19 O Dahlblom 2022-11-21 (Python version)

Source code in src/calfem/core.py
def beam3e(ex, ey, ez, eo, ep, eq=None):
    """
    Calculate the stiffness matrix (and optionally load vector) for a 3D elastic Bernoulli beam element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ez : array_like
        Element node z-coordinates [z1, z2].
    eo : array_like
        Orientation of local z-axis [xz, yz, zz].
    ep : array_like
        Element properties [E, G, A, Iy, Iz, Kv], where E is Young's modulus, G is shear modulus,
        A is cross-sectional area, Iy is moment of inertia about local y-axis, 
        Iz is moment of inertia about local z-axis, and Kv is Saint-Venant's torsion constant.
    eq : array_like, optional
        Distributed loads in local directions [qX, qY, qZ, qW].

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (12, 12).
    fe : ndarray, optional
        Element load vector, shape (12, 1), if eq is not None.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-10-19
                   O Dahlblom   2022-11-21 (Python version)
    """
    E, Gs, A, Iy, Iz, Kv = ep
    DEA = E*A 
    DEIz = E*Iz
    DEIy = E*Iy
    DGK = Gs*Kv

    qX = 0.
    qY = 0.
    qZ = 0.
    qW = 0.
    if not eq is None:
        qX, qY, qZ, qW = eq

    x1, x2 = ex
    y1, y2 = ey
    z1, z2 = ez
    dx = x2-x1
    dy = y2-y1
    dz = z2-z1
    L = np.sqrt(dx*dx+dy*dy+dz*dz)

    a = DEA/L
    b = 12*DEIz/L**3
    c = 6*DEIz/L**2
    d = 12*DEIy/L**3
    e = 6*DEIy/L**2
    f = DGK/L
    g = 2*DEIy/L
    h = 2*DEIz/L

    Kle = np.array([
        [a, 0, 0, 0, 0,   0,  -a, 0, 0, 0, 0,   0],
        [0, b, 0, 0, 0,   c,   0, -b, 0, 0, 0,   c],
        [0, 0, d, 0, -e,   0,   0, 0, -d, 0, -e,   0],
        [0, 0, 0, f, 0,   0,   0, 0, 0, -f, 0,   0],
        [0, 0, -e, 0, 2*g, 0,   0, 0, e, 0, g,   0],
        [0, c, 0, 0, 0,   2*h, 0, -c, 0, 0, 0,   h],
        [-a, 0, 0, 0, 0,   0,   a, 0, 0, 0, 0,   0],
        [0, -b, 0, 0, 0,  -c,   0, b, 0, 0, 0,  -c],
        [0, 0, -d, 0, e,   0,   0, 0, d, 0, e,   0],
        [0, 0, 0, -f, 0,   0,   0, 0, 0, f, 0,   0],
        [0, 0, -e, 0, g,   0,   0, 0, e, 0, 2*g, 0],
        [0, c, 0, 0, 0,   h,   0, -c, 0, 0, 0,   2*h]
    ])

    fle = L/2*np.array([qX, qY, qZ, qW, -qZ*L/6, qY*L/6,
                     qX, qY, qZ, qW, qZ*L/6, -qY*L/6]).reshape(12,1)

    n1 = np.array([dx, dy, dz])/L

    lc = np.sqrt(eo @ eo.T)
    eo1= np.array(eo/lc)
    n2a = np.cross(eo1,n1)
    n2al = np.sqrt(n2a @ n2a.T)
    n2=n2a/n2al

    n3 = np.cross(n1,n2)

    G = np.array([
        [n1[0], n1[1], n1[2], 0,     0,     0,
            0,     0,     0,     0,     0,     0],
        [n2[0], n2[1], n2[2], 0,     0,     0,
            0,     0,     0,     0,     0,     0],
        [n3[0], n3[1], n3[2], 0,     0,     0,
            0,     0,     0,     0,     0,     0],
        [0,     0,     0,     n1[0], n1[1], n1[2],
            0,     0,     0,     0,     0,     0],
        [0,     0,     0,     n2[0], n2[1], n2[2],
            0,     0,     0,     0,     0,     0],
        [0,     0,     0,     n3[0], n3[1], n3[2],
            0,     0,     0,     0,     0,     0],
        [0,     0,     0,     0,     0,     0,
            n1[0], n1[1], n1[2], 0,     0,     0],
        [0,     0,     0,     0,     0,     0,
            n2[0], n2[1], n2[2], 0,     0,     0],
        [0,     0,     0,     0,     0,     0,
            n3[0], n3[1], n3[2], 0,     0,     0],
        [0,     0,     0,     0,     0,     0,     0,
            0,     0,     n1[0], n1[1], n1[2]],
        [0,     0,     0,     0,     0,     0,     0,
            0,     0,     n2[0], n2[1], n2[2]],
        [0,     0,     0,     0,     0,     0,
            0,     0,     0,     n3[0], n3[1], n3[2]]
    ])

    Ke = G.T @ Kle @ G
    fe = G.T @ fle

    if eq is None:
        return Ke
    else:
        return Ke, fe

beam3s(ex, ey, ez, eo, ep, ed, eq=None, nep=None)

Calculate the variation of section forces and displacements along a 3D beam element.

Parameters

ex : array_like Element node x-coordinates [x1, x2]. ey : array_like Element node y-coordinates [y1, y2]. ez : array_like Element node z-coordinates [z1, z2]. eo : array_like Orientation of local z-axis [xz, yz, zz]. ep : array_like Element properties [E, G, A, Iy, Iz, Kv], where E is Young's modulus, G is shear modulus, A is cross-sectional area, Iy is moment of inertia about local y-axis, Iz is moment of inertia about local z-axis, and Kv is Saint-Venant's torsion constant. ed : array_like Element displacement vector [u1, ..., u12]. eq : array_like, optional Distributed loads in local directions [qX, qY, qZ, qW]. nep : int, optional Number of evaluation points (default is 2).

Returns

es : ndarray Section forces at evaluation points, shape (nep, 6). Each row contains [N, Vy, Vz, T, My, Mz] (normal force, shear forces, torque, moments). edi : ndarray, optional Element displacements at evaluation points, shape (nep, 4), if nep is given. Each row contains [u, v, w, phi] (displacements and rotation). eci : ndarray, optional Local x-coordinates of the evaluation points, shape (nep, 1), if nep is given.

History

LAST MODIFIED: O Dahlblom 2015-10-19 O Dahlblom 2022-11-23 (Python version)

Source code in src/calfem/core.py
def beam3s(ex, ey, ez, eo, ep, ed, eq=None, nep=None):
    """
    Calculate the variation of section forces and displacements along a 3D beam element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2].
    ey : array_like
        Element node y-coordinates [y1, y2].
    ez : array_like
        Element node z-coordinates [z1, z2].
    eo : array_like
        Orientation of local z-axis [xz, yz, zz].
    ep : array_like
        Element properties [E, G, A, Iy, Iz, Kv], where E is Young's modulus, G is shear modulus,
        A is cross-sectional area, Iy is moment of inertia about local y-axis, 
        Iz is moment of inertia about local z-axis, and Kv is Saint-Venant's torsion constant.
    ed : array_like
        Element displacement vector [u1, ..., u12].
    eq : array_like, optional
        Distributed loads in local directions [qX, qY, qZ, qW].
    nep : int, optional
        Number of evaluation points (default is 2).

    Returns
    -------
    es : ndarray
        Section forces at evaluation points, shape (nep, 6).
        Each row contains [N, Vy, Vz, T, My, Mz] (normal force, shear forces, torque, moments).
    edi : ndarray, optional
        Element displacements at evaluation points, shape (nep, 4), if nep is given.
        Each row contains [u, v, w, phi] (displacements and rotation).
    eci : ndarray, optional
        Local x-coordinates of the evaluation points, shape (nep, 1), if nep is given.

    History
    -------
    LAST MODIFIED: O Dahlblom   2015-10-19
                   O Dahlblom   2022-11-23 (Python version)
    """
    E, Gs, A, Iy, Iz, Kv = ep
    DEA = E*A 
    DEIz = E*Iz
    DEIy = E*Iy
    DGK = Gs*Kv

    qX = 0.
    qY = 0.
    qZ = 0.
    qW = 0.
    if not eq is None:
        qX, qY, qZ, qW = eq

    ne = 2
    if nep != None:
        ne = nep

    x1, x2 = ex
    y1, y2 = ey
    z1, z2 = ez
    dx = x2-x1
    dy = y2-y1
    dz = z2-z1
    L = np.sqrt(dx*dx+dy*dy+dz*dz)
    n1 = np.array([dx, dy, dz])/L

    lc = np.sqrt(eo @ eo.T)
    eo1= np.array(eo/lc)
    n2a = np.cross(eo1,n1)
    n2al = np.sqrt(n2a @ n2a.T)
    n2=n2a/n2al

    n3 = np.cross(n1,n2)

    G = np.array([
        [n1[0], n1[1], n1[2], 0,     0,     0,
            0,     0,     0,     0,     0,     0],
        [n2[0], n2[1], n2[2], 0,     0,     0,
            0,     0,     0,     0,     0,     0],
        [n3[0], n3[1], n3[2], 0,     0,     0,
            0,     0,     0,     0,     0,     0],
        [0,     0,     0,     n1[0], n1[1], n1[2],
            0,     0,     0,     0,     0,     0],
        [0,     0,     0,     n2[0], n2[1], n2[2],
            0,     0,     0,     0,     0,     0],
        [0,     0,     0,     n3[0], n3[1], n3[2],
            0,     0,     0,     0,     0,     0],
        [0,     0,     0,     0,     0,     0,
            n1[0], n1[1], n1[2], 0,     0,     0],
        [0,     0,     0,     0,     0,     0,
            n2[0], n2[1], n2[2], 0,     0,     0],
        [0,     0,     0,     0,     0,     0,
            n3[0], n3[1], n3[2], 0,     0,     0],
        [0,     0,     0,     0,     0,     0,     0,
            0,     0,     n1[0], n1[1], n1[2]],
        [0,     0,     0,     0,     0,     0,     0,
            0,     0,     n2[0], n2[1], n2[2]],
        [0,     0,     0,     0,     0,     0,
            0,     0,     0,     n3[0], n3[1], n3[2]]
    ])

    edl = G @ ed.reshape(12,1)

    a1 = np.array([
        edl[0],
        edl[6]
    ])
    C1 = np.array([
        [1.,      0.],
        [-1/L,   1/L]
    ]) 
    C1a = C1 @ a1

    a2 = np.array([
        edl[1],
        edl[5],
        edl[7],
        edl[11]
    ])
    C2 = np.array([
        [1.,      0.,    0.,     0.],
        [0.,      1.,    0.,     0.],
        [-3/L**2, -2/L,  3/L**2, -1/L],
        [2/L**3,  1/L**2, -2/L**3, 1/L**2]
    ]) 
    C2a = C2 @ a2

    a3 = np.array([
        edl[2],
        -edl[4],
        edl[8],
        -edl[10]
    ])
    C3 = C2
    C3a = C3 @ a3

    a4 = np.array([
        edl[3],
        edl[9]
    ])
    C4 = C1
    C4a = C4 @ a4

    X = np.linspace(0., L+L/(ne-1), ne).reshape(ne,1) 
    #X = np.arange(0., L+L/(ne-1), L/(ne-1)).reshape(ne,1) 
    zero = np.zeros(ne).reshape(ne,1)    
    one = np.ones(ne).reshape(ne,1)

    u = np.concatenate((one,  X), 1) @ C1a
    du = np.concatenate((zero,  one), 1) @ C1a
    if DEA != 0:
       u = u-(X**2-L*X)*qX/(2*DEA)
       du = du-(2*X-L)*qX/(2*DEA)

    v = np.concatenate((one,  X, X**2, X**3), 1) @ C2a
    d2v=np.concatenate((zero, zero, 2*one, 6*X), 1) @ C2a
    d3v = np.concatenate((zero, zero, zero, 6*one), 1) @ C2a
    if DEIz != 0:
       v = v+(X**4 - 2*L*X**3 + L**2*X**2)*qY/(24*DEIz)
       d2v = d2v+(6*X**2 - 6*L*X + L**2*one)*qY/(12*DEIz)
       d3v = d3v+(2*X - L*one)*qY/(2*DEIz)

    w = np.concatenate((one,  X, X**2, X**3), 1) @ C3a
    d2w = np.concatenate((zero, zero, 2*one, 6*X), 1) @ C3a
    d3w = np.concatenate((zero, zero, zero, 6*one), 1) @ C3a
    if DEIy != 0:
       w = w+(X**4 - 2*L*X**3 + L**2*X**2)*qZ/(24*DEIy)
       d2w = d2w+(6*X**2 - 6*L*X + L**2*one)*qZ/(12*DEIy)
       d3w = d3w+(2*X - L*one)*qZ/(2*DEIy)

    fi = np.concatenate((one,  X), 1) @ C4a
    dfi = np.concatenate((zero,  one), 1) @ C4a
    if DGK != 0:
       fi = fi-(X**2-L*X)*qW/(2*DGK)
       dfi = dfi-(2*X-L)*qW/(2*DGK)
    N = DEA*du
    Mz = DEIz*d2v
    Vy = -DEIz*d3v 
    My = -DEIy*d2w
    Vz = -DEIy*d3w
    T = DGK*dfi
    es = np.concatenate((N, Vy, Vz, T, My, Mz), 1)
    edi = np.concatenate((u, v, w, fi), 1)
    eci = X

    if nep is None:
        return es
    else:
        return es, edi, eci

c_mul(a, b)

Multiply two integers with 32-bit overflow handling.

Parameters

a : int First integer. b : int Second integer.

Returns

int Product of a and b with 32-bit overflow handling.

Source code in src/calfem/core.py
def c_mul(a, b):
    """
    Multiply two integers with 32-bit overflow handling.

    Parameters
    ----------
    a : int
        First integer.
    b : int
        Second integer.

    Returns
    -------
    int
        Product of a and b with 32-bit overflow handling.
    """
    return eval(hex((int(a) * b) & 0xFFFFFFFF)[:-1])

check_length(v, length, error_string)

Check if the input has the specified length, raise ValueError if not.

Parameters

v : object Object to check (must support len()).

Source code in src/calfem/core.py
def check_length(v, length, error_string):
    """
    Check if the input has the specified length, raise ValueError if not.

    Parameters
    ----------
    v : object
        Object to check (must support len()).
    """

    fname = sys._getframe(1).f_code.co_name

    if len(v) != length:
        raise ValueError("%s (%s)" % (error_string, fname))

check_list_array(v, error_string)

Check if the input is a list or numpy array, raise TypeError if not.

Parameters

v : object Object to check. error_string : str Error message to display if check fails.

Source code in src/calfem/core.py
def check_list_array(v, error_string):
    """
    Check if the input is a list or numpy array, raise TypeError if not.

    Parameters
    ----------
    v : object
        Object to check.
    error_string : str
        Error message to display if check fails.
    """
    fname = sys._getframe(1).f_code.co_name
    if (type(v) != list) and (type(v) != np.ndarray):
        raise TypeError("%s (%s)" % (error_string, fname))

coord_extract(edof, coords, dofs, nen=-1)

Create element coordinate matrices ex, ey, ez from edof coord and dofs matrices.

Parameters

edof : array_like Element topology array, shape (nel, nen * nnd), where nel is number of elements, nen is number of element nodes, and nnd is number of node DOFs. coords : array_like Node coordinates array, shape (ncoords, ndims), where ncoords is number of coordinates and ndims is node dimensions. dofs : array_like DOF array, shape (ncoords, nnd), where nnd is number of node DOFs. nen : int, optional Number of element nodes. If -1, calculated from edof and dofs.

Returns

ex : ndarray Element x-coordinates, returned if ndims = 1. ex, ey : tuple of ndarray Element x and y coordinates, returned if ndims = 2. ex, ey, ez : tuple of ndarray Element x, y and z coordinates, returned if ndims = 3.

Source code in src/calfem/core.py
def coord_extract(edof, coords, dofs, nen=-1):
    """
    Create element coordinate matrices ex, ey, ez from edof coord and dofs matrices.

    Parameters
    ----------
    edof : array_like
        Element topology array, shape (nel, nen * nnd), where nel is number of elements,
        nen is number of element nodes, and nnd is number of node DOFs.
    coords : array_like
        Node coordinates array, shape (ncoords, ndims), where ncoords is number of coordinates
        and ndims is node dimensions.
    dofs : array_like
        DOF array, shape (ncoords, nnd), where nnd is number of node DOFs.
    nen : int, optional
        Number of element nodes. If -1, calculated from edof and dofs.

    Returns
    -------
    ex : ndarray
        Element x-coordinates, returned if ndims = 1.
    ex, ey : tuple of ndarray
        Element x and y coordinates, returned if ndims = 2.
    ex, ey, ez : tuple of ndarray
        Element x, y and z coordinates, returned if ndims = 3.
    """

    # Create dictionary with dof indices

    dofDict = {}
    nDofs = np.size(dofs, 1)
    nElements = np.size(edof, 0)
    n_element_dofs = np.size(edof, 1)
    nDimensions = np.size(coords, 1)
    nElementDofs = np.size(edof, 1)

    if nen == -1:
        nElementNodes = int(nElementDofs/nDofs)
    else:
        nElementNodes = nen

    if nElementNodes*nDofs != n_element_dofs:
        nDofs = nElementNodes*nDofs - n_element_dofs
        user_warning(
            "dofs/edof mismatch. Using %d dofs per node when indexing." % nDofs)

    idx = 0
    for dof in dofs:
        #dofDict[dofHash(dof)] = idx
        dofDict[hash(tuple(dof[0:nDofs]))] = idx
        idx += 1

    # Loop over edof and extract element coords

    ex = np.zeros((nElements, nElementNodes))
    ey = np.zeros((nElements, nElementNodes))
    ez = np.zeros((nElements, nElementNodes))

    elementIdx = 0
    for etopo in edof:
        for i in range(nElementNodes):
            i0 = i*nDofs
            i1 = i*nDofs+nDofs-1
            dof = []
            if i0 == i1:
                dof = [etopo[i*nDofs]]
            else:
                dof = etopo[i*nDofs:(i*nDofs+nDofs)]

            nodeCoord = coords[dofDict[hash(tuple(dof[0:nDofs]))]]

            if nDimensions >= 1:
                ex[elementIdx, i] = nodeCoord[0]
            if nDimensions >= 2:
                ey[elementIdx, i] = nodeCoord[1]
            if nDimensions >= 3:
                ez[elementIdx, i] = nodeCoord[2]

        elementIdx += 1

    if nDimensions == 1:
        return ex

    if nDimensions == 2:
        return ex, ey

    if nDimensions == 3:
        return ex, ey, ez

create_dofs(nCoords, nDof)

Create degree of freedom (DOF) array.

Parameters

nCoords : int Number of coordinates (nodes). nDof : int Number of degrees of freedom per coordinate.

Returns

ndarray DOF array, shape (nCoords, nDof), with sequential DOF numbering starting from 1.

Source code in src/calfem/core.py
def create_dofs(nCoords: int, nDof: int) -> NDArray[np.integer]:
    """
    Create degree of freedom (DOF) array.

    Parameters
    ----------
    nCoords : int
        Number of coordinates (nodes).
    nDof : int
        Number of degrees of freedom per coordinate.

    Returns
    -------
    ndarray
        DOF array, shape (nCoords, nDof), with sequential DOF numbering starting from 1.
    """
    return np.arange(nCoords*nDof).reshape(nCoords, nDof)+1

disable_friendly_errors()

Disable friendly error logging and restore the previous exception hook.

Source code in src/calfem/core.py
def disable_friendly_errors():
    """
    Disable friendly error logging and restore the previous exception hook.
    """
    sys.excepthook = __prev_exception_hook

dof_hash(dof)

Compute a hash value for a degree of freedom array.

Parameters

dof : array_like Degree of freedom array.

Returns

int Hash value for the DOF array.

Source code in src/calfem/core.py
def dof_hash(dof):
    """
    Compute a hash value for a degree of freedom array.

    Parameters
    ----------
    dof : array_like
        Degree of freedom array.

    Returns
    -------
    int
        Hash value for the DOF array.
    """
    if len(dof) == 1:
        return dof[0]
    value = 0x345678
    for item in dof:
        value = c_mul(1000003, value) ^ hash(item)
    value = value ^ len(dof)
    if value == -1:
        value = -2
    return value

effmises(es, ptype)

Calculate effective von mises stresses.

Parameters

es : array_like Element stress matrix [[sigx, sigy, [sigz], tauxy], [...]], one row for each element. ptype : int Analysis type: 1 : plane stress 2 : plane strain 3 : axisymmetry 4 : three dimensional

Returns

eseff : ndarray Effective stress array [eseff_0, ..., eseff_nel-1].

Source code in src/calfem/core.py
def effmises(es, ptype):
    """
    Calculate effective von mises stresses.

    Parameters
    ----------
    es : array_like
        Element stress matrix [[sigx, sigy, [sigz], tauxy], [...]], one row for each element.
    ptype : int
        Analysis type:
        1 : plane stress
        2 : plane strain
        3 : axisymmetry
        4 : three dimensional

    Returns
    -------
    eseff : ndarray
        Effective stress array [eseff_0, ..., eseff_nel-1].
    """

    nel = np.size(es, 0)
    escomps = np.size(es, 1)

    eseff = np.zeros([nel])

    if ptype == 1:
        sigxx = es[:, 0]
        sigyy = es[:, 1]
        sigxy = es[:, 2]
        eseff = np.sqrt(sigxx*sigxx+sigyy*sigyy-sigxx*sigyy+3*sigxy*sigxy)
        return eseff

eigen(K, M, b=None)

Solve the generalized eigenvalue problem |K-LM|X = 0, considering boundary conditions.

Parameters

K : array_like Global stiffness matrix, shape (ndof, ndof). M : array_like Global mass matrix, shape (ndof, ndof). b : array_like, optional Boundary condition vector, shape (nbc, 1).

Returns

L : ndarray Eigenvalue vector, shape (ndof-nbc, 1). X : ndarray Eigenvectors, shape (ndof, ndof-nbc).

Source code in src/calfem/core.py
def eigen(K: ArrayLike, M: ArrayLike, b: Optional[ArrayLike] = None) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
    """
    Solve the generalized eigenvalue problem |K-LM|X = 0, considering boundary conditions.

    Parameters
    ----------
    K : array_like
        Global stiffness matrix, shape (ndof, ndof).
    M : array_like
        Global mass matrix, shape (ndof, ndof).
    b : array_like, optional
        Boundary condition vector, shape (nbc, 1).

    Returns
    -------
    L : ndarray
        Eigenvalue vector, shape (ndof-nbc, 1).
    X : ndarray
        Eigenvectors, shape (ndof, ndof-nbc).
    """
    nd, _ = K.shape
    if b is not None:
        fdof = np.setdiff1d(np.arange(nd), b-1)
        D, X1 = eig(K[np.ix_(fdof,fdof)], M[np.ix_(fdof,fdof)])
        D = np.real(D)
        nfdof, _ = X1.shape
        for j in range(nfdof):
            mnorm = np.sqrt(X1[:,j].T@M[np.ix_(fdof,fdof)]@X1[:,j])
            X1[:,j] /= mnorm
        s_order = np.argsort(D)
        L = np.sort(D)
        X2 = np.zeros(X1.shape)
        for ind,j in enumerate(s_order):
            X2[:,ind] = X1[:,j]
        X = np.zeros((nd,nfdof))
        X[fdof,:] = X2
        return L, X
    else:
        D, X1 = eig(K, M)
        D = np.real(D)
        for j in range(nd):
            mnorm = np.sqrt(X1[:,j].T@M@X1[:,j])
            X1[:,j] /= mnorm
        s_order = np.argsort(D)
        L = np.sort(D)
        X = np.copy(X1)
        for ind,j in enumerate(s_order):
            X[:,ind] = X1[:,j]
        return L, X

enable_friendly_errors()

Enable friendly error logging by setting a custom exception hook.

Source code in src/calfem/core.py
def enable_friendly_errors():
    """
    Enable friendly error logging by setting a custom exception hook.
    """
    def __friendly_exception_hook(exc_type, exc_value, exc_traceback):
        if issubclass(exc_type, KeyboardInterrupt):
            sys.__excepthook__(exc_type, exc_value, exc_traceback)
            return
        cflog.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
        sys.exit(1)

    global __prev_exception_hook
    __prev_exception_hook = sys.excepthook
    sys.excepthook = __friendly_exception_hook

error(msg)

Log an error message.

Parameters

str

Error message to log.

Source code in src/calfem/core.py
def error(msg: str) -> None:
    """
    Log an error message.

    Parameters
    ----------

    msg : str
        Error message to log.
    """
    cflog.error(" calfem.core: "+msg)

extract_eldisp(edof, a)

Extract element displacements from the global displacement vector according to the topology matrix edof.

Parameters

a : array_like The global displacement vector. edof : array_like DOF topology array.

Returns

ed : ndarray Element displacement array.

Source code in src/calfem/core.py
def extract_eldisp(edof, a):
    """
    Extract element displacements from the global displacement vector according to the topology matrix edof.

    Parameters
    ----------
    a : array_like
        The global displacement vector.
    edof : array_like
        DOF topology array.

    Returns
    -------
    ed : ndarray
        Element displacement array.
    """

    ed = None

    if edof.ndim == 1:
        nDofs = len(edof)
        ed = np.zeros([nDofs])
        idx = edof-1
        ed[:] = a[np.ix_(idx)].T
    else:
        nElements = edof.shape[0]
        nDofs = edof.shape[1]
        ed = np.zeros([nElements, nDofs])
        i = 0
        for row in edof:
            idx = row-1
            ed[i, :] = a[np.ix_(idx)].T
            i += 1

    return ed

flw2i4e(ex, ey, ep, D, eq=None)

Compute element stiffness (conductivity) matrix for 4 node isoparametric field element.

Parameters

ex : array_like Element coordinates [x1, x2, x3, x4]. ey : array_like Element coordinates [y1, y2, y3, y4]. ep : array_like Element properties [t, ir], where t is thickness and ir is integration rule. D : array_like Constitutive matrix [[kxx, kxy], [kyx, kyy]]. eq : float, optional Heat supply per unit volume.

Returns

Ke : ndarray Element 'stiffness' matrix, shape (4, 4). fe : ndarray, optional Element load vector, shape (4, 1), if eq is not None.

Source code in src/calfem/core.py
def flw2i4e(ex, ey, ep, D, eq=None):
    """
    Compute element stiffness (conductivity) matrix for 4 node isoparametric field element.

    Parameters
    ----------
    ex : array_like
        Element coordinates [x1, x2, x3, x4].
    ey : array_like
        Element coordinates [y1, y2, y3, y4].
    ep : array_like
        Element properties [t, ir], where t is thickness and ir is integration rule.
    D : array_like
        Constitutive matrix [[kxx, kxy], [kyx, kyy]].
    eq : float, optional
        Heat supply per unit volume.

    Returns
    -------
    Ke : ndarray
        Element 'stiffness' matrix, shape (4, 4).
    fe : ndarray, optional
        Element load vector, shape (4, 1), if eq is not None.
    """
    t = ep[0]
    ir = ep[1]
    ngp = ir*ir

    if eq is None:
        q = 0
    else:
        q = eq

    if ir == 1:
        g1 = 0.0
        w1 = 2.0
        gp = np.array([[g1, g1]])  # Make this explicitly 2D
        w = np.array([[w1, w1]])   # Make this explicitly 2D
    elif ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.array([
            [-g1, -g1],
            [g1, -g1],
            [-g1, g1],
            [g1, g1]
        ])
        w = np.array([
            [w1, w1],
            [w1, w1],
            [w1, w1],
            [w1, w1]
        ])
    elif ir == 3:
        g1 = 0.774596669241483
        g2 = 0.
        w1 = 0.555555555555555
        w2 = 0.888888888888888
        gp = np.array([
            [-g1, -g1],
            [-g2, -g1],
            [g1, -g1],
            [-g1, g2],
            [g2, g2],
            [g1, g2],
            [-g1, g1],
            [g2, g1],
            [g1, g1]
        ])
        w = np.array([
            [w1, w1],
            [w2, w1],
            [w1, w1],
            [w1, w2],
            [w2, w2],
            [w1, w2],
            [w1, w1],
            [w2, w1],
            [w1, w1]
        ])
    else:
        info("Used number of integration points not implemented")
        return

    # Make sure w is a 2D array
    w = np.asarray(w)
    if w.ndim == 1:
        w = w.reshape(-1, 1)

    # Compute the weight products - handle both matrix and array cases safely
    if w.shape[1] >= 2:
        wp = w[:, 0] * w[:, 1]
    else:
        # Handle the case where w might only have one column
        wp = w[:, 0]

    xsi = gp[:, 0]
    eta = gp[:, 1]
    r2 = ngp*2

    # Calculate shape functions and derivatives
    N = np.multiply((1-xsi), (1-eta))/4.
    N = np.column_stack((N, np.multiply((1+xsi), (1-eta))/4.))
    N = np.column_stack((N, np.multiply((1+xsi), (1+eta))/4.))
    N = np.column_stack((N, np.multiply((1-xsi), (1+eta))/4.))

    dNr = np.zeros((r2, 4))
    dNr[0:r2:2, 0] = -(1-eta)/4.
    dNr[0:r2:2, 1] = (1-eta)/4.
    dNr[0:r2:2, 2] = (1+eta)/4.
    dNr[0:r2:2, 3] = -(1+eta)/4.
    dNr[1:r2+1:2, 0] = -(1-xsi)/4.
    dNr[1:r2+1:2, 1] = -(1+xsi)/4.
    dNr[1:r2+1:2, 2] = (1+xsi)/4.
    dNr[1:r2+1:2, 3] = (1-xsi)/4.

    Ke1 = np.zeros((4, 4))
    fe1 = np.zeros((4, 1))

    # Convert ex and ey to arrays and combine for JT calculation
    ex_array = np.asarray(ex).reshape(-1, 1)
    ey_array = np.asarray(ey).reshape(-1, 1)
    coords = np.hstack([ex_array, ey_array])

    JT = dNr @ coords

    for i in range(ngp):
        indx = np.array([2*(i+1)-1, 2*(i+1)])
        detJ = np.linalg.det(JT[indx-1, :])
        if detJ < 10*np.finfo(float).eps:
            info("Jacobi determinant == 0")
        JTinv = np.linalg.inv(JT[indx-1, :])
        B = JTinv @ dNr[indx-1, :]

        Ke1 = Ke1 + B.T @ D @ B * detJ * wp[i]
        fe1 = fe1 + N[i, :].reshape(-1, 1) * detJ * wp[i]

    if eq is None:
        return Ke1 * t
    else:
        return Ke1 * t, fe1 * t * q

flw2i4s(ex, ey, ep, D, ed)

Compute flows or corresponding quantities in the 4 node isoparametric element.

Parameters

array_like

Element coordinates [x1, x2, x3, x4].

ey : array_like Element coordinates [y1, y2, y3, y4]. ep : array_like Element properties [t, ir], where t is thickness and ir is integration rule. D : array_like Constitutive matrix [[kxx, kxy], [kyx, kyy]]. ed : array_like Element nodal values [u1, u2, u3, u4].

Returns

ndarray

Element flows [[qx, qy], [.., ..]].

et : ndarray Element gradients [[qx, qy], [..., ..]]. eci : ndarray Gauss point location vector [[ix1, iy1], [..., ...], [ix(nint), iy(nint)]].

Source code in src/calfem/core.py
def flw2i4s(ex, ey, ep, D, ed):
    """
    Compute flows or corresponding quantities in the 4 node isoparametric element.

    Parameters
    ----------

    ex : array_like
        Element coordinates [x1, x2, x3, x4].
    ey : array_like
        Element coordinates [y1, y2, y3, y4].
    ep : array_like
        Element properties [t, ir], where t is thickness and ir is integration rule.
    D : array_like
        Constitutive matrix [[kxx, kxy], [kyx, kyy]].
    ed : array_like
        Element nodal values [u1, u2, u3, u4].

    Returns
    -------

    es : ndarray
        Element flows [[qx, qy], [.., ..]].
    et : ndarray
        Element gradients [[qx, qy], [..., ..]].
    eci : ndarray
        Gauss point location vector [[ix1, iy1], [..., ...], [ix(nint), iy(nint)]].
    """
    t = ep[0]
    ir = ep[1]
    ngp = ir*ir

    if ir == 1:
        g1 = 0.0
        w1 = 2.0
        gp = np.array([[g1, g1]])  # Make this explicitly 2D
        w = np.array([[w1, w1]])   # Make this explicitly 2D
    elif ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.array([
            [-g1, -g1],
            [g1, -g1],
            [-g1, g1],
            [g1, g1]
        ])
        w = np.array([
            [w1, w1],
            [w1, w1],
            [w1, w1],
            [w1, w1]
        ])
    elif ir == 3:
        g1 = 0.774596669241483
        g2 = 0.
        w1 = 0.555555555555555
        w2 = 0.888888888888888
        gp = np.array([
            [-g1, -g1],
            [-g2, -g1],
            [g1, -g1],
            [-g1, g2],
            [g2, g2],
            [g1, g2],
            [-g1, g1],
            [g2, g1],
            [g1, g1]
        ])
        w = np.array([
            [w1, w1],
            [w2, w1],
            [w1, w1],
            [w1, w2],
            [w2, w2],
            [w1, w2],
            [w1, w1],
            [w2, w1],
            [w1, w1]
        ])
    else:
        info("Used number of integration points not implemented")

    # Make sure w is a 2D array
    w = np.asarray(w)
    if w.ndim == 1:
        w = w.reshape(-1, 1)

    # We don't need wp for this function, so no need to compute it

    xsi = gp[:, 0]
    eta = gp[:, 1]
    r2 = ngp*2

    N = np.multiply((1-xsi), (1-eta))/4.
    N = np.column_stack((N, np.multiply((1+xsi), (1-eta))/4.))
    N = np.column_stack((N, np.multiply((1+xsi), (1+eta))/4.))
    N = np.column_stack((N, np.multiply((1-xsi), (1+eta))/4.))

    dNr = np.zeros((r2, 4))
    dNr[0:r2:2, 0] = -(1-eta)/4.
    dNr[0:r2:2, 1] = (1-eta)/4.
    dNr[0:r2:2, 2] = (1+eta)/4.
    dNr[0:r2:2, 3] = -(1+eta)/4.
    dNr[1:r2+1:2, 0] = -(1-xsi)/4.
    dNr[1:r2+1:2, 1] = -(1+xsi)/4.
    dNr[1:r2+1:2, 2] = (1+xsi)/4.
    dNr[1:r2+1:2, 3] = (1-xsi)/4.

    # Calculate Gauss point locations (for output)
    # Use numpy arrays instead of matrix multiplication
    ex_array = np.asarray(ex).reshape(-1, 1)
    ey_array = np.asarray(ey).reshape(-1, 1)
    coords = np.hstack([ex_array, ey_array])

    eci = N @ coords

    # Ensure ed is properly shaped for calculations
    if np.ndim(ed) == 1:
        ed = np.array([ed])

    # Get the number of rows in ed
    red = ed.shape[0]

    JT = dNr @ coords

    es = np.zeros((ngp*red, 2))
    et = np.zeros((ngp*red, 2))

    for i in range(ngp):
        indx = np.array([2*(i+1)-1, 2*(i+1)])
        detJ = np.linalg.det(JT[indx-1, :])
        if detJ < 10*np.finfo(float).eps:
            info("Jacobi determinant == 0")
        JTinv = np.linalg.inv(JT[indx-1, :])
        B = JTinv @ dNr[indx-1, :]

        # Process each row of ed
        for j in range(red):
            # Ensure ed row is a column vector for matrix multiplications
            ed_row = np.asarray(ed[j]).reshape(-1, 1)  # (8,1)
            # Compute flow (es) and gradient (et)
            p1 = -D @ B @ ed_row    # (2,1)
            p2 = B @ ed_row         # (2,1)
            # Flatten to shape (2,) before assigning to row slices
            es[i + j*ngp, :] = p1.flatten()
            et[i + j*ngp, :] = p2.flatten()

    return es, et, eci

flw2i8e(ex, ey, ep, D, eq=None)

Compute element stiffness (conductivity) matrix for 8 node isoparametric field element.

Parameters

array_like

Element coordinates [x1, ..., x8].

ey : array_like Element coordinates [y1, ..., y8]. ep : array_like Element properties [t, ir], where t is thickness and ir is integration rule. D : array_like Constitutive matrix [[kxx, kxy], [kyx, kyy]]. eq : float, optional Heat supply per unit volume.

Returns

ndarray

Element 'stiffness' matrix, shape (8, 8).

fe : ndarray, optional Element load vector, shape (8, 1), if eq is not None.

Source code in src/calfem/core.py
def flw2i8e(ex, ey, ep, D, eq=None):
    """
    Compute element stiffness (conductivity) matrix for 8 node isoparametric field element.

    Parameters
    ----------

    ex : array_like
        Element coordinates [x1, ..., x8].
    ey : array_like
        Element coordinates [y1, ..., y8].
    ep : array_like
        Element properties [t, ir], where t is thickness and ir is integration rule.
    D : array_like
        Constitutive matrix [[kxx, kxy], [kyx, kyy]].
    eq : float, optional
        Heat supply per unit volume.

    Returns
    -------

    Ke : ndarray
        Element 'stiffness' matrix, shape (8, 8).
    fe : ndarray, optional
        Element load vector, shape (8, 1), if eq is not None.
    """
    t = ep[0]
    ir = ep[1]
    ngp = ir*ir

    if eq is None:
        q = 0
    else:
        q = eq

    if ir == 1:
        g1 = 0.0
        w1 = 2.0
        gp = np.array([[g1, g1]])  # Make this explicitly 2D
        w = np.array([[w1, w1]])   # Make this explicitly 2D
    elif ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.array([
            [-g1, -g1],
            [g1, -g1],
            [-g1, g1],
            [g1, g1]
        ])
        w = np.array([
            [w1, w1],
            [w1, w1],
            [w1, w1],
            [w1, w1]
        ])
    elif ir == 3:
        g1 = 0.774596669241483
        g2 = 0.
        w1 = 0.555555555555555
        w2 = 0.888888888888888
        gp = np.array([
            [-g1, -g1],
            [-g2, -g1],
            [g1, -g1],
            [-g1, g2],
            [g2, g2],
            [g1, g2],
            [-g1, g1],
            [g2, g1],
            [g1, g1]
        ])
        w = np.array([
            [w1, w1],
            [w2, w1],
            [w1, w1],
            [w1, w2],
            [w2, w2],
            [w1, w2],
            [w1, w1],
            [w2, w1],
            [w1, w1]
        ])
    else:
        info("Used number of integration points not implemented")
        return

    # Make sure w is a 2D array
    w = np.asarray(w)
    if w.ndim == 1:
        w = w.reshape(-1, 1)

    # Compute the weight products - handle both matrix and array cases safely
    if w.shape[1] >= 2:
        wp = w[:, 0] * w[:, 1]
    else:
        # Handle the case where w might only have one column
        wp = w[:, 0]

    xsi = gp[:, 0]
    eta = gp[:, 1]
    r2 = ngp*2

    N = np.multiply(np.multiply(-(1-xsi), (1-eta)), (1+xsi+eta))/4.
    N = np.column_stack((
        N, 
        np.multiply(np.multiply(-(1+xsi), (1-eta)), (1-xsi+eta))/4.
    ))
    N = np.column_stack((
        N,
        np.multiply(np.multiply(-(1+xsi), (1+eta)), (1-xsi-eta))/4.
    ))
    N = np.column_stack((
        N,
        np.multiply(np.multiply(-(1-xsi), (1+eta)), (1+xsi-eta))/4.
    ))
    N = np.column_stack((
        N,
        np.multiply((1-np.multiply(xsi, xsi)), (1-eta))/2.
    ))
    N = np.column_stack((
        N,
        np.multiply((1+xsi), (1-np.multiply(eta, eta)))/2.
    ))
    N = np.column_stack((
        N,
        np.multiply((1-np.multiply(xsi, xsi)), (1+eta))/2.
    ))
    N = np.column_stack((
        N,
        np.multiply((1-xsi), (1-np.multiply(eta, eta)))/2.
    ))

    dNr = np.zeros((r2, 8))
    dNr[0:r2:2, 0] = -(-np.multiply((1-eta), (1+xsi+eta)) +
                       np.multiply((1-xsi), (1-eta)))/4.
    dNr[0:r2:2, 1] = -(np.multiply((1-eta), (1-xsi+eta)) -
                       np.multiply((1+xsi), (1-eta)))/4.
    dNr[0:r2:2, 2] = -(np.multiply((1+eta), (1-xsi-eta)) -
                       np.multiply((1+xsi), (1+eta)))/4.
    dNr[0:r2:2, 3] = -(-np.multiply((1+eta), (1+xsi-eta)) +
                       np.multiply((1-xsi), (1+eta)))/4.
    dNr[0:r2:2, 4] = -np.multiply(xsi, (1-eta))
    dNr[0:r2:2, 5] = (1-np.multiply(eta, eta))/2.
    dNr[0:r2:2, 6] = -np.multiply(xsi, (1+eta))
    dNr[0:r2:2, 7] = -(1-np.multiply(eta, eta))/2.
    dNr[1:r2+1:2, 0] = -(-np.multiply((1-xsi), (1+xsi+eta)) +
                         np.multiply((1-xsi), (1-eta)))/4.
    dNr[1:r2+1:2, 1] = -(-np.multiply((1+xsi), (1-xsi+eta)) +
                         np.multiply((1+xsi), (1-eta)))/4.
    dNr[1:r2+1:2, 2] = -(np.multiply((1+xsi), (1-xsi-eta)) -
                         np.multiply((1+xsi), (1+eta)))/4.
    dNr[1:r2+1:2, 3] = -(np.multiply((1-xsi), (1+xsi-eta)) -
                         np.multiply((1-xsi), (1+eta)))/4.
    dNr[1:r2+1:2, 4] = -(1-np.multiply(xsi, xsi))/2.
    dNr[1:r2+1:2, 5] = -np.multiply(eta, (1+xsi))
    dNr[1:r2+1:2, 6] = (1-np.multiply(xsi, xsi))/2.
    dNr[1:r2+1:2, 7] = -np.multiply(eta, (1-xsi))

    Ke1 = np.zeros((8, 8))
    fe1 = np.zeros((8, 1))

    # Convert ex and ey to arrays and combine for JT calculation
    ex_array = np.asarray(ex).reshape(-1, 1)
    ey_array = np.asarray(ey).reshape(-1, 1)
    coords = np.hstack([ex_array, ey_array])

    JT = dNr @ coords

    for i in range(ngp):
        indx = np.array([2*(i+1)-1, 2*(i+1)])
        detJ = np.linalg.det(JT[indx-1, :])
        if detJ < 10*np.finfo(float).eps:
            info("Jacobideterminanten lika med noll!")
        JTinv = np.linalg.inv(JT[indx-1, :])
        B = JTinv @ dNr[indx-1, :]
        Ke1 = Ke1 + B.T @ D @ B * detJ * wp[i]
        fe1 = fe1 + N[i, :].reshape(-1, 1) * detJ * wp[i]

    if eq != None:
        return Ke1*t, fe1*t*q
    else:
        return Ke1*t

flw2i8s(ex, ey, ep, D, ed)

Compute flows or corresponding quantities in the 8 node isoparametric element.

Parameters

array_like

Element coordinates [x1, x2, x3, ..., x8].

ey : array_like Element coordinates [y1, y2, y3, ..., y8]. ep : array_like Element properties [t, ir], where t is thickness and ir is integration rule. D : array_like Constitutive matrix [[kxx, kxy], [kyx, kyy]]. ed : array_like Element nodal values [u1, ..., u8].

Returns

ndarray

Element flows [[qx, qy], [.., ..]].

et : ndarray Element gradients [[qx, qy], [.., ..]]. eci : ndarray Gauss point location vector [[ix1, iy1], [..., ...], [ix(nint), iy(nint)]].

Source code in src/calfem/core.py
def flw2i8s(ex, ey, ep, D, ed):
    """
    Compute flows or corresponding quantities in the 8 node isoparametric element.

    Parameters
    ----------

    ex : array_like
        Element coordinates [x1, x2, x3, ..., x8].
    ey : array_like
        Element coordinates [y1, y2, y3, ..., y8].
    ep : array_like
        Element properties [t, ir], where t is thickness and ir is integration rule.
    D : array_like
        Constitutive matrix [[kxx, kxy], [kyx, kyy]].
    ed : array_like
        Element nodal values [u1, ..., u8].

    Returns
    -------

    es : ndarray
        Element flows [[qx, qy], [.., ..]].
    et : ndarray
        Element gradients [[qx, qy], [.., ..]].
    eci : ndarray
        Gauss point location vector [[ix1, iy1], [..., ...], [ix(nint), iy(nint)]].
    """
    t = ep[0]
    ir = ep[1]
    ngp = ir*ir

    if ir == 1:
        g1 = 0.0
        w1 = 2.0
        gp = np.array([[g1, g1]])  # Make this explicitly 2D
        w = np.array([[w1, w1]])   # Make this explicitly 2D
    elif ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.array([
            [-g1, -g1],
            [g1, -g1],
            [-g1, g1],
            [g1, g1]
        ])
        w = np.array([
            [w1, w1],
            [w1, w1],
            [w1, w1],
            [w1, w1]
        ])
    elif ir == 3:
        g1 = 0.774596669241483
        g2 = 0.
        w1 = 0.555555555555555
        w2 = 0.888888888888888
        gp = np.array([
            [-g1, -g1],
            [-g2, -g1],
            [g1, -g1],
            [-g1, g2],
            [g2, g2],
            [g1, g2],
            [-g1, g1],
            [g2, g1],
            [g1, g1]
        ])
        w = np.array([
            [w1, w1],
            [w2, w1],
            [w1, w1],
            [w1, w2],
            [w2, w2],
            [w1, w2],
            [w1, w1],
            [w2, w1],
            [w1, w1]
        ])
    else:
        info("Used number of integration points not implemented")
        return

    # We don't need wp for this function, so no need to compute it

    xsi = gp[:, 0]
    eta = gp[:, 1]
    r2 = ngp*2

    N = np.multiply(np.multiply(-(1-xsi), (1-eta)), (1+xsi+eta))/4.
    N = np.column_stack((
        N, 
        np.multiply(np.multiply(-(1+xsi), (1-eta)), (1-xsi+eta))/4.
    ))
    N = np.column_stack((
        N,
        np.multiply(np.multiply(-(1+xsi), (1+eta)), (1-xsi-eta))/4.
    ))
    N = np.column_stack((
        N,
        np.multiply(np.multiply(-(1-xsi), (1+eta)), (1+xsi-eta))/4.
    ))
    N = np.column_stack((
        N,
        np.multiply((1-np.multiply(xsi, xsi)), (1-eta))/2.
    ))
    N = np.column_stack((
        N,
        np.multiply((1+xsi), (1-np.multiply(eta, eta)))/2.
    ))
    N = np.column_stack((
        N,
        np.multiply((1-np.multiply(xsi, xsi)), (1+eta))/2.
    ))
    N = np.column_stack((
        N,
        np.multiply((1-xsi), (1-np.multiply(eta, eta)))/2.
    ))

    dNr = np.zeros((r2, 8))
    dNr[0:r2:2, 0] = -(-np.multiply((1-eta), (1+xsi+eta)) +
                       np.multiply((1-xsi), (1-eta)))/4.
    dNr[0:r2:2, 1] = -(np.multiply((1-eta), (1-xsi+eta)) -
                       np.multiply((1+xsi), (1-eta)))/4.
    dNr[0:r2:2, 2] = -(np.multiply((1+eta), (1-xsi-eta)) -
                       np.multiply((1+xsi), (1+eta)))/4.
    dNr[0:r2:2, 3] = -(-np.multiply((1+eta), (1+xsi-eta)) +
                       np.multiply((1-xsi), (1+eta)))/4.
    dNr[0:r2:2, 4] = -np.multiply(xsi, (1-eta))
    dNr[0:r2:2, 5] = (1-np.multiply(eta, eta))/2.
    dNr[0:r2:2, 6] = -np.multiply(xsi, (1+eta))
    dNr[0:r2:2, 7] = -(1-np.multiply(eta, eta))/2.
    dNr[1:r2+1:2, 0] = -(-np.multiply((1-xsi), (1+xsi+eta)) +
                         np.multiply((1-xsi), (1-eta)))/4.
    dNr[1:r2+1:2, 1] = -(-np.multiply((1+xsi), (1-xsi+eta)) +
                         np.multiply((1+xsi), (1-eta)))/4.
    dNr[1:r2+1:2, 2] = -(np.multiply((1+xsi), (1-xsi-eta)) -
                         np.multiply((1+xsi), (1+eta)))/4.
    dNr[1:r2+1:2, 3] = -(np.multiply((1-xsi), (1+xsi-eta)) -
                         np.multiply((1-xsi), (1+eta)))/4.
    dNr[1:r2+1:2, 4] = -(1-np.multiply(xsi, xsi))/2.
    dNr[1:r2+1:2, 5] = -np.multiply(eta, (1+xsi))
    dNr[1:r2+1:2, 6] = (1-np.multiply(xsi, xsi))/2.
    dNr[1:r2+1:2, 7] = -np.multiply(eta, (1-xsi))

    # Calculate Gauss point locations (for output)
    # Use numpy arrays instead of matrix multiplication
    ex_array = np.asarray(ex).reshape(-1, 1)
    ey_array = np.asarray(ey).reshape(-1, 1)
    coords = np.hstack([ex_array, ey_array])

    eci = N @ coords

    # Ensure ed is properly shaped for calculations
    if np.ndim(ed) == 1:
        ed = np.array([ed])

    # Get the number of rows in ed
    red = ed.shape[0]

    JT = dNr @ coords

    es = np.zeros((ngp*red, 2))
    et = np.zeros((ngp*red, 2))

    for i in range(ngp):
        indx = np.array([2*(i+1)-1, 2*(i+1)])
        detJ = np.linalg.det(JT[indx-1, :])
        if detJ < 10*np.finfo(float).eps:
            info("Jacobi determinant == 0")
        JTinv = np.linalg.inv(JT[indx-1, :])
        B = JTinv @ dNr[indx-1, :]

        # Process each row of ed
        for j in range(red):
            # Ensure row vector is column shaped (n,1)
            ed_row = np.asarray(ed[j]).reshape(-1, 1)
            # Flow vector (negative conductivity * grad)
            p1 = -D @ B @ ed_row   # shape (2,1) or MatrixCompat
            # Gradient vector
            p2 = B @ ed_row        # shape (2,1) or MatrixCompat
            # Convert to numpy 1D arrays (handles MatrixCompat or ndarray)
            p1_arr = np.asarray(p1, dtype=float).flatten()
            p2_arr = np.asarray(p2, dtype=float).flatten()
            # Assign first two components (expected size 2)
            es[i + j*ngp, :] = p1_arr[:2]
            et[i + j*ngp, :] = p2_arr[:2]

    return es, et, eci

flw2qe(ex, ey, ep, D, eq=None)

Compute element stiffness (conductivity) matrix for a quadrilateral field element. This function calculates the element stiffness matrix and optionally the load vector for a 4-node quadrilateral element used in 2D heat flow analysis. The quadrilateral is divided into 4 triangular sub-elements using the centroid as a common node.

Parameters

ex : array_like Element x-coordinates [x1, x2, x3, x4] for the 4 corner nodes. ey : array_like Element y-coordinates [y1, y2, y3, y4] for the 4 corner nodes. ep : array_like Element properties [t] where t is the element thickness. D : array_like Constitutive matrix (2x2) for heat conductivity: [[kxx, kxy], [kyx, kyy]] where kxx, kyy are conductivities in x and y directions, and kxy, kyx are cross-conductivities. eq : float, optional Heat supply per unit volume. If None, only stiffness matrix is computed. Default is None.

Returns

Ke : ndarray Element stiffness matrix (4x4) for the quadrilateral element. fe : ndarray, optional Element load vector (4x1). Only returned when eq is provided.

Source code in src/calfem/core.py
def flw2qe(ex, ey, ep, D, eq=None):
    """
    Compute element stiffness (conductivity) matrix for a quadrilateral field element.
    This function calculates the element stiffness matrix and optionally the load vector
    for a 4-node quadrilateral element used in 2D heat flow analysis. The quadrilateral
    is divided into 4 triangular sub-elements using the centroid as a common node.

    Parameters
    ----------
    ex : array_like
        Element x-coordinates [x1, x2, x3, x4] for the 4 corner nodes.
    ey : array_like
        Element y-coordinates [y1, y2, y3, y4] for the 4 corner nodes.
    ep : array_like
        Element properties [t] where t is the element thickness.
    D : array_like
        Constitutive matrix (2x2) for heat conductivity:
        [[kxx, kxy],
         [kyx, kyy]]
        where kxx, kyy are conductivities in x and y directions,
        and kxy, kyx are cross-conductivities.
    eq : float, optional
        Heat supply per unit volume. If None, only stiffness matrix is computed.
        Default is None.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix (4x4) for the quadrilateral element.
    fe : ndarray, optional
        Element load vector (4x1). Only returned when eq is provided.
    """
    xc = sum(ex)/4.
    yc = sum(ey)/4.

    K = np.zeros((5, 5))
    f = np.zeros((5, 1))

    if eq is None:
        k1 = flw2te([ex[0], ex[1], xc], [ey[0], ey[1], yc], ep, D)
        K = assem(np.array([1, 2, 5]), K, k1)
        k1 = flw2te([ex[1], ex[2], xc], [ey[1], ey[2], yc], ep, D)
        K = assem(np.array([2, 3, 5]), K, k1)
        k1 = flw2te([ex[2], ex[3], xc], [ey[2], ey[3], yc], ep, D)
        K = assem(np.array([3, 4, 5]), K, k1)
        k1 = flw2te([ex[3], ex[0], xc], [ey[3], ey[0], yc], ep, D)
        K = assem(np.array([4, 1, 5]), K, k1)
    else:
        k1, f1 = flw2te([ex[0], ex[1], xc], [ey[0], ey[1], yc], ep, D, eq)
        K, f = assem(np.array([1, 2, 5]), K, k1, f, f1)
        k1, f1 = flw2te([ex[1], ex[2], xc], [ey[1], ey[2], yc], ep, D, eq)
        K, f = assem(np.array([2, 3, 5]), K, k1, f, f1)
        k1, f1 = flw2te([ex[2], ex[3], xc], [ey[2], ey[3], yc], ep, D, eq)
        K, f = assem(np.array([3, 4, 5]), K, k1, f, f1)
        k1, f1 = flw2te([ex[3], ex[0], xc], [ey[3], ey[0], yc], ep, D, eq)
        K, f = assem(np.array([4, 1, 5]), K, k1, f, f1)
    Ke1, fe1 = statcon(K, f, np.array([5]))

    Ke = Ke1
    fe = fe1

    if eq is None:
        return Ke
    else:
        return Ke, fe

flw2qs(ex, ey, ep, D, ed, eq=None)

Compute flows or corresponding quantities in the quadrilateral field element.

Parameters

array_like

Element node x-coordinates [x1, x2, x3, x4].

ey : array_like Element node y-coordinates [y1, y2, y3, y4]. ep : array_like Element properties [t], where t is element thickness. D : array_like Constitutive matrix [[kxx, kxy], [kyx, kyy]]. ed : array_like Element nodal values [[u1, u2, u3, u4], [.., .., .., ..]], where u1,u2,u3,u4 are nodal values.

Returns

ndarray

Element flows [[qx, qy], [.., ..]].

et : ndarray Element gradients [[gx, gy], [.., ..]].

Source code in src/calfem/core.py
def flw2qs(ex, ey, ep, D, ed, eq=None):
    """
    Compute flows or corresponding quantities in the quadrilateral field element.

    Parameters
    ----------

    ex : array_like
        Element node x-coordinates [x1, x2, x3, x4].
    ey : array_like
        Element node y-coordinates [y1, y2, y3, y4].
    ep : array_like
        Element properties [t], where t is element thickness.
    D : array_like
        Constitutive matrix [[kxx, kxy], [kyx, kyy]].
    ed : array_like
        Element nodal values [[u1, u2, u3, u4], [.., .., .., ..]], where u1,u2,u3,u4 are nodal values.

    Returns
    -------

    es : ndarray
        Element flows [[qx, qy], [.., ..]].
    et : ndarray
        Element gradients [[gx, gy], [.., ..]].
    """
    K = np.zeros((5, 5))
    f = np.zeros((5, 1))

    xm = sum(ex)/4
    ym = sum(ey)/4

    if eq is None:
        q = 0
    else:
        q = eq

    En = np.array([
        [1, 2, 5],
        [2, 3, 5],
        [3, 4, 5],
        [4, 1, 5]
    ])
    ex1 = np.array([ex[0], ex[1], xm])
    ey1 = np.array([ey[0], ey[1], ym])
    ex2 = np.array([ex[1], ex[2], xm])
    ey2 = np.array([ey[1], ey[2], ym])
    ex3 = np.array([ex[2], ex[3], xm])
    ey3 = np.array([ey[2], ey[3], ym])
    ex4 = np.array([ex[3], ex[0], xm])
    ey4 = np.array([ey[3], ey[0], ym])

    if eq is None:
        k1 = flw2te(ex1, ey1, ep, D)
        K = assem(En[0], K, k1)
        k1 = flw2te(ex2, ey2, ep, D)
        K = assem(En[1], K, k1)
        k1 = flw2te(ex3, ey3, ep, D)
        K = assem(En[2], K, k1)
        k1 = flw2te(ex4, ey4, ep, D)
        K = assem(En[3], K, k1)
    else:
        k1, f1 = flw2te(ex1, ey1, ep, D, q)
        K, f = assem(En[0], K, k1, f, f1)
        k1, f1 = flw2te(ex2, ey2, ep, D, q)
        K, f = assem(En[1], K, k1, f, f1)
        k1, f1 = flw2te(ex3, ey3, ep, D, q)
        K, f = assem(En[2], K, k1, f, f1)
        k1, f1 = flw2te(ex4, ey4, ep, D, q)
        K, f = assem(En[3], K, k1, f, f1)

    if ed.ndim == 1:
        ed = np.array([ed])

    ni, nj = np.shape(ed)

    a = np.zeros((5, ni))
    for i in range(ni):
        a[np.ix_(range(5), [i])], r = np.asarray(
            solveq(K, f, np.arange(1, 5), ed[i]))

    s1, t1 = flw2ts(ex1, ey1, D, a[np.ix_(En[0, :]-1, np.arange(ni))].T)
    s2, t2 = flw2ts(ex2, ey2, D, a[np.ix_(En[1, :]-1, np.arange(ni))].T)
    s3, t3 = flw2ts(ex3, ey3, D, a[np.ix_(En[2, :]-1, np.arange(ni))].T)
    s4, t4 = flw2ts(ex4, ey4, D, a[np.ix_(En[3, :]-1, np.arange(ni))].T)

    es = (s1+s2+s3+s4)/4.
    et = (t1+t2+t3+t4)/4.

    return es, et

flw2te(ex, ey, ep, D, eq=None)

Compute element stiffness (conductivity) matrix for a triangular field element.

Parameters

ex : array_like Element node x-coordinates [x1, x2, x3]. ey : array_like Element node y-coordinates [y1, y2, y3]. ep : array_like Element properties [t], where t is the element thickness. D : array_like Constitutive matrix [[kxx, kxy], [kyx, kyy]]. eq : float, optional Heat supply per unit volume.

Returns

Ke : ndarray Element 'stiffness' matrix, shape (3, 3). fe : ndarray Element load vector, shape (3, 1).

Source code in src/calfem/core.py
def flw2te(ex, ey, ep, D, eq=None):
    """
    Compute element stiffness (conductivity) matrix for a triangular field element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2, x3].
    ey : array_like
        Element node y-coordinates [y1, y2, y3].
    ep : array_like
        Element properties [t], where t is the element thickness.
    D : array_like
        Constitutive matrix [[kxx, kxy], [kyx, kyy]].
    eq : float, optional
        Heat supply per unit volume.

    Returns
    -------
    Ke : ndarray
        Element 'stiffness' matrix, shape (3, 3).
    fe : ndarray
        Element load vector, shape (3, 1).
    """
    t = ep[0]
    if eq is None:
        eq = 0.

    exm = np.asmatrix(ex)
    eym = np.asmatrix(ey)
    C = np.asmatrix(np.hstack([np.ones((3, 1)), exm.T, eym.T]))
    B = np.matrix([
        [0., 1., 0.],
        [0., 0., 1.]
    ])*C.I
    A = 0.5*np.linalg.det(C)

    Ke = B.T*D*B*t*A
    fe = np.matrix([[1., 1., 1.]]).T*eq*A*t/3

    if eq == 0.:
        return Ke
    else:
        return Ke, fe

flw2ts(ex, ey, D, ed)

Compute flows or corresponding quantities in the triangular field element.

Parameters

ex : array_like Element node x-coordinates [x1, x2, x3]. ey : array_like Element node y-coordinates [y1, y2, y3]. D : array_like Constitutive matrix [[kxx, kxy], [kyx, kyy]]. ed : array_like Element nodal values [u1, u2, u3], one row per element.

Returns

es : ndarray Element flows, shape (n_elem, 2) or (2,) for single element. et : ndarray Element gradients, shape (n_elem, 2) or (2,) for single element.

Source code in src/calfem/core.py
def flw2ts(ex, ey, D, ed):
    """
    Compute flows or corresponding quantities in the triangular field element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2, x3].
    ey : array_like
        Element node y-coordinates [y1, y2, y3].
    D : array_like
        Constitutive matrix [[kxx, kxy], [kyx, kyy]].
    ed : array_like
        Element nodal values [u1, u2, u3], one row per element.

    Returns
    -------
    es : ndarray
        Element flows, shape (n_elem, 2) or (2,) for single element.
    et : ndarray
        Element gradients, shape (n_elem, 2) or (2,) for single element.
    """

    if len(ex.shape) > 1:
        qs = np.zeros([ex.shape[0], 2])
        qt = np.zeros([ex.shape[0], 2])
        row = 0
        for exr, eyr, edr in zip(ex, ey, ed):
            exm = np.asarray(exr).reshape(-1)  # Ensure 1D array
            eym = np.asarray(eyr).reshape(-1)  # Ensure 1D array
            edm = np.asarray(edr).reshape(-1)  # Ensure 1D array

            # Create C matrix with consistent dimensions
            ones_col = np.ones((3, 1))
            # Reshape column vectors properly
            ex_col = exm.reshape(-1, 1)
            ey_col = eym.reshape(-1, 1)

            # Stack horizontally with consistent 2D shapes
            C = np.hstack([ones_col, ex_col, ey_col])

            B = np.array([
                [0., 1., 0.],
                [0., 0., 1.]
            ]) @ np.linalg.inv(C)

            qs[row, :] = (-D @ B @ edm.T).T
            qt[row, :] = (B @ edm.T).T
            row += 1

        return qs, qt
    else:
        exm = np.asarray(ex).reshape(-1)  # Ensure 1D array
        eym = np.asarray(ey).reshape(-1)  # Ensure 1D array
        edm = np.asarray(ed).reshape(-1)  # Ensure 1D array

        # Create C matrix with consistent dimensions
        ones_col = np.ones((3, 1))
        # Reshape column vectors properly
        ex_col = exm.reshape(-1, 1)
        ey_col = eym.reshape(-1, 1)

        # Stack horizontally with consistent 2D shapes
        C = np.hstack([ones_col, ex_col, ey_col])

        B = np.array([
            [0., 1., 0.],
            [0., 0., 1.]
        ]) @ np.linalg.inv(C)

        qs = -D @ B @ edm.T
        qt = B @ edm.T

        return qs.T, qt.T

flw3i8e(ex, ey, ez, ep, D, eq=None)

Compute element stiffness (conductivity) matrix for 8 node isoparametric field element.

Parameters

array_like

Element node x-coordinates [x1, x2, x3, ..., x8].

ey : array_like Element node y-coordinates [y1, y2, y3, ..., y8]. ez : array_like Element node z-coordinates [z1, z2, z3, ..., z8]. ep : array_like Element properties [ir], where ir is integration rule. D : array_like Constitutive matrix [[kxx, kxy, kxz], [kyx, kyy, kyz], [kzx, kzy, kzz]]. eq : float, optional Heat supply per unit volume.

Returns

ndarray

Element 'stiffness' matrix, shape (8, 8).

fe : ndarray, optional Element load vector, shape (8, 1), if eq is not None.

Source code in src/calfem/core.py
def flw3i8e(ex, ey, ez, ep, D, eq=None):
    """
    Compute element stiffness (conductivity) matrix for 8 node isoparametric field element.

    Parameters
    ----------

    ex : array_like
        Element node x-coordinates [x1, x2, x3, ..., x8].
    ey : array_like
        Element node y-coordinates [y1, y2, y3, ..., y8].
    ez : array_like
        Element node z-coordinates [z1, z2, z3, ..., z8].
    ep : array_like
        Element properties [ir], where ir is integration rule.
    D : array_like
        Constitutive matrix [[kxx, kxy, kxz], [kyx, kyy, kyz], [kzx, kzy, kzz]].
    eq : float, optional
        Heat supply per unit volume.

    Returns
    -------

    Ke : ndarray
        Element 'stiffness' matrix, shape (8, 8).
    fe : ndarray, optional
        Element load vector, shape (8, 1), if eq is not None.
    """
    ir = ep[0]
    ngp = ir*ir*ir

    if eq is None:
        q = 0
    else:
        q = eq

    if ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.array([
            [-1, -1, -1],
            [1, -1, -1],
            [1, 1, -1],
            [-1, 1, -1],
            [-1, -1, 1],
            [1, -1, 1],
            [1, 1, 1],
            [-1, 1, 1]
        ])*g1
        w = np.ones((8, 3))*w1
    elif ir == 3:
        g1 = 0.774596669241483
        g2 = 0.
        w1 = 0.555555555555555
        w2 = 0.888888888888888
        gp = np.zeros((27, 3))
        w = np.zeros((27, 3))
        I1 = np.array([-1, 0, 1, -1, 0, 1, -1, 0, 1])
        I2 = np.array([0, -1, 0, 0, 1, 0, 0, 1, 0])
        gp[:, 0] = np.repeat(I1, 3).reshape(27, 1).flatten()
        I2_tiled = np.tile(I2, 3).reshape(27, 1)
        gp[:, 0] = gp[:, 0]*g1 + I2_tiled.flatten()*g2
        I1 = np.abs(I1)
        I2 = np.abs(I2)
        w[:, 0] = np.repeat(I1, 3).reshape(27, 1).flatten()*w1
        w[:, 0] = w[:, 0] + np.tile(I2, 3).reshape(27, 1).flatten()*w2
        I1 = np.repeat([-1, -1, -1, 0, 0, 0, 1, 1, 1], 3)
        I2 = np.repeat([0, 0, 0, 1, 1, 1, 0, 0, 0], 3)
        gp[:, 1] = I1*g1
        gp[:, 1] = gp[:, 1] + I2*g2
        I1 = np.abs(I1)
        I2 = np.abs(I2)
        w[:, 1] = I1*w1
        w[:, 1] = w[:, 1] + I2*w2
        I1 = np.tile([-1, -1, -1, -1, -1, -1, -1, -1, -1], 3)
        I2 = np.tile([0, 0, 0, 0, 0, 0, 0, 0, 0], 3)
        I3 = np.abs(I1)
        gp[:, 2] = I1*g1
        gp[:, 2] = gp[:, 2] + I2*g2
        w[:, 2] = I3*w1
        w[:, 2] = w[:, 2] + I2*w2
    else:
        info("Used number of integration points not implemented")
        return

    wp = w[:, 0]*w[:, 1]*w[:, 2]

    xsi = gp[:, 0]
    eta = gp[:, 1]
    zet = gp[:, 2]
    r2 = ngp*3

    N = np.zeros((ngp, 8))
    dNr = np.zeros((r2, 8))

    N[:, 0] = (1-xsi)*(1-eta)*(1-zet)/8
    N[:, 1] = (1+xsi)*(1-eta)*(1-zet)/8
    N[:, 2] = (1+xsi)*(1+eta)*(1-zet)/8
    N[:, 3] = (1-xsi)*(1+eta)*(1-zet)/8
    N[:, 4] = (1-xsi)*(1-eta)*(1+zet)/8
    N[:, 5] = (1+xsi)*(1-eta)*(1+zet)/8
    N[:, 6] = (1+xsi)*(1+eta)*(1+zet)/8
    N[:, 7] = (1-xsi)*(1+eta)*(1+zet)/8

    dNr[0:r2+1:3, 0] = -(1-eta)*(1-zet)
    dNr[0:r2+1:3, 1] = (1-eta)*(1-zet)
    dNr[0:r2+1:3, 2] = (1+eta)*(1-zet)
    dNr[0:r2+1:3, 3] = -(1+eta)*(1-zet)
    dNr[0:r2+1:3, 4] = -(1-eta)*(1+zet)
    dNr[0:r2+1:3, 5] = (1-eta)*(1+zet)
    dNr[0:r2+1:3, 6] = (1+eta)*(1+zet)
    dNr[0:r2+1:3, 7] = -(1+eta)*(1+zet)
    dNr[1:r2+2:3, 0] = -(1-xsi)*(1-zet)
    dNr[1:r2+2:3, 1] = -(1+xsi)*(1-zet)
    dNr[1:r2+2:3, 2] = (1+xsi)*(1-zet)
    dNr[1:r2+2:3, 3] = (1-xsi)*(1-zet)
    dNr[1:r2+2:3, 4] = -(1-xsi)*(1+zet)
    dNr[1:r2+2:3, 5] = -(1+xsi)*(1+zet)
    dNr[1:r2+2:3, 6] = (1+xsi)*(1+zet)
    dNr[1:r2+2:3, 7] = (1-xsi)*(1+zet)
    dNr[2:r2+3:3, 0] = -(1-xsi)*(1-eta)
    dNr[2:r2+3:3, 1] = -(1+xsi)*(1-eta)
    dNr[2:r2+3:3, 2] = -(1+xsi)*(1+eta)
    dNr[2:r2+3:3, 3] = -(1-xsi)*(1+eta)
    dNr[2:r2+3:3, 4] = (1-xsi)*(1-eta)
    dNr[2:r2+3:3, 5] = (1+xsi)*(1-eta)
    dNr[2:r2+3:3, 6] = (1+xsi)*(1+eta)
    dNr[2:r2+3:3, 7] = (1-xsi)*(1+eta)

    dNr = dNr/8.0

    Ke = np.zeros((8, 8))
    fe = np.zeros((8, 1))

    ex = np.asarray(ex).reshape((8, 1))
    ey = np.asarray(ey).reshape((8, 1))
    ez = np.asarray(ez).reshape((8, 1))
    coords = np.hstack((ex, ey, ez))

    JT = dNr @ coords

    eps = np.finfo(float).eps

    for i in range(ngp):
        indx = [i*3, i*3+1, i*3+2]
        detJ = np.linalg.det(JT[indx, :])
        if detJ < 10*eps:
            info("Jacobi determinant equal or less than zero!")
        JTinv = np.linalg.inv(JT[indx, :])
        dNx = JTinv @ dNr[indx, :]

        B = np.zeros((3, 8))
        B[0, :] = dNx[0, :]
        B[1, :] = dNx[1, :]
        B[2, :] = dNx[2, :]

        Ke = Ke + B.T @ D @ B * detJ * wp[i]
        fe = fe + (N[i, :].reshape(-1, 1) * detJ * wp[i]).reshape(-1, 1)

    if eq is not None:
        return Ke, fe*q
    else:
        return Ke

flw3i8s(ex, ey, ez, ep, D, ed)

Compute flows or corresponding quantities in the 8 node (3-dim) isoparametric field element.

Parameters

ex : array_like Element node x-coordinates [x1, x2, x3, ..., x8]. ey : array_like Element node y-coordinates [y1, y2, y3, ..., y8]. ez : array_like Element node z-coordinates [z1, z2, z3, ..., z8]. ep : array_like Element properties [ir], where ir is integration rule. D : array_like Constitutive matrix [[kxx, kxy, kxz], [kyx, kyy, kyz], [kzx, kzy, kzz]]. ed : array_like Element nodal values [[u1, ..., u8], [.., ..., ..]].

Returns

es : ndarray Element flows [[qx, qy, qz], [.., .., ..]]. et : ndarray Element gradients [[qx, qy, qz], [.., .., ..]]. eci : ndarray Gauss point location vector [[ix1, iy1, iz1], [..., ..., ...], [ix(nint), iy(nint), iz(nint)]].

Source code in src/calfem/core.py
def flw3i8s(ex, ey, ez, ep, D, ed):
    """
    Compute flows or corresponding quantities in the 8 node (3-dim) isoparametric field element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2, x3, ..., x8].
    ey : array_like
        Element node y-coordinates [y1, y2, y3, ..., y8].
    ez : array_like
        Element node z-coordinates [z1, z2, z3, ..., z8].
    ep : array_like
        Element properties [ir], where ir is integration rule.
    D : array_like
        Constitutive matrix [[kxx, kxy, kxz], [kyx, kyy, kyz], [kzx, kzy, kzz]].
    ed : array_like
        Element nodal values [[u1, ..., u8], [.., ..., ..]].

    Returns
    -------
    es : ndarray
        Element flows [[qx, qy, qz], [.., .., ..]].
    et : ndarray
        Element gradients [[qx, qy, qz], [.., .., ..]].
    eci : ndarray
        Gauss point location vector [[ix1, iy1, iz1], [..., ..., ...], [ix(nint), iy(nint), iz(nint)]].
    """
    ir = ep[0]
    ngp = ir*ir*ir

    if ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.array([
            [-1, -1, -1],
            [1, -1, -1],
            [1, 1, -1],
            [-1, 1, -1],
            [-1, -1, 1],
            [1, -1, 1],
            [1, 1, 1],
            [-1, 1, 1]
        ])*g1
        w = np.ones((8, 3))*w1
    elif ir == 3:
        g1 = 0.774596669241483
        g2 = 0.
        w1 = 0.555555555555555
        w2 = 0.888888888888888
        gp = np.zeros((27, 3))
        w = np.zeros((27, 3))
        I1 = np.array([-1, 0, 1, -1, 0, 1, -1, 0, 1])
        I2 = np.array([0, -1, 0, 0, 1, 0, 0, 1, 0])
        gp[:, 0] = np.repeat(I1, 3).reshape(27, 1).flatten()
        I2_tiled = np.tile(I2, 3).reshape(27, 1)
        gp[:, 0] = gp[:, 0]*g1 + I2_tiled.flatten()*g2
        I1 = np.abs(I1)
        I2 = np.abs(I2)
        w[:, 0] = np.repeat(I1, 3).reshape(27, 1).flatten()*w1
        w[:, 0] = w[:, 0] + np.tile(I2, 3).reshape(27, 1).flatten()*w2
        I1 = np.repeat([-1, -1, -1, 0, 0, 0, 1, 1, 1], 3)
        I2 = np.repeat([0, 0, 0, 1, 1, 1, 0, 0, 0], 3)
        gp[:, 1] = I1*g1
        gp[:, 1] = gp[:, 1] + I2*g2
        I1 = np.abs(I1)
        I2 = np.abs(I2)
        w[:, 1] = I1*w1
        w[:, 1] = w[:, 1] + I2*w2
        I1 = np.tile([-1, -1, -1, -1, -1, -1, -1, -1, -1], 3)
        I2 = np.tile([0, 0, 0, 0, 0, 0, 0, 0, 0], 3)
        I3 = np.abs(I1)
        gp[:, 2] = I1*g1
        gp[:, 2] = gp[:, 2] + I2*g2
        w[:, 2] = I3*w1
        w[:, 2] = w[:, 2] + I2*w2
    else:
        info("Used number of integration points not implemented")
        return

    wp = w[:, 0]*w[:, 1]*w[:, 2]

    xsi = gp[:, 0]
    eta = gp[:, 1]
    zet = gp[:, 2]
    r2 = ngp*3

    N = np.zeros((ngp, 8))
    dNr = np.zeros((r2, 8))

    N[:, 0] = (1-xsi)*(1-eta)*(1-zet)/8
    N[:, 1] = (1+xsi)*(1-eta)*(1-zet)/8
    N[:, 2] = (1+xsi)*(1+eta)*(1-zet)/8
    N[:, 3] = (1-xsi)*(1+eta)*(1-zet)/8
    N[:, 4] = (1-xsi)*(1-eta)*(1+zet)/8
    N[:, 5] = (1+xsi)*(1-eta)*(1+zet)/8
    N[:, 6] = (1+xsi)*(1+eta)*(1+zet)/8
    N[:, 7] = (1-xsi)*(1+eta)*(1+zet)/8

    dNr[0:r2+1:3, 0] = -(1-eta)*(1-zet)
    dNr[0:r2+1:3, 1] = (1-eta)*(1-zet)
    dNr[0:r2+1:3, 2] = (1+eta)*(1-zet)
    dNr[0:r2+1:3, 3] = -(1+eta)*(1-zet)
    dNr[0:r2+1:3, 4] = -(1-eta)*(1+zet)
    dNr[0:r2+1:3, 5] = (1-eta)*(1+zet)
    dNr[0:r2+1:3, 6] = (1+eta)*(1+zet)
    dNr[0:r2+1:3, 7] = -(1+eta)*(1+zet)
    dNr[1:r2+2:3, 0] = -(1-xsi)*(1-zet)
    dNr[1:r2+2:3, 1] = -(1+xsi)*(1-zet)
    dNr[1:r2+2:3, 2] = (1+xsi)*(1-zet)
    dNr[1:r2+2:3, 3] = (1-xsi)*(1-zet)
    dNr[1:r2+2:3, 4] = -(1-xsi)*(1+zet)
    dNr[1:r2+2:3, 5] = -(1+xsi)*(1+zet)
    dNr[1:r2+2:3, 6] = (1+xsi)*(1+zet)
    dNr[1:r2+2:3, 7] = (1-xsi)*(1+zet)
    dNr[2:r2+3:3, 0] = -(1-xsi)*(1-eta)
    dNr[2:r2+3:3, 1] = -(1+xsi)*(1-eta)
    dNr[2:r2+3:3, 2] = -(1+xsi)*(1+eta)
    dNr[2:r2+3:3, 3] = -(1-xsi)*(1+eta)
    dNr[2:r2+3:3, 4] = (1-xsi)*(1-eta)
    dNr[2:r2+3:3, 5] = (1+xsi)*(1-eta)
    dNr[2:r2+3:3, 6] = (1+xsi)*(1+eta)
    dNr[2:r2+3:3, 7] = (1-xsi)*(1+eta)

    dNr = dNr/8.0

    # Calculate Gauss point locations (for output)
    ex = np.asarray(ex).reshape((8, 1))
    ey = np.asarray(ey).reshape((8, 1))
    ez = np.asarray(ez).reshape((8, 1))
    coords = np.hstack((ex, ey, ez))

    eci = N @ coords

    # Ensure ed is properly shaped for calculations
    if np.ndim(ed) == 1:
        ed = np.array([ed])

    # Get the number of rows in ed
    red = ed.shape[0]

    JT = dNr @ coords

    es = np.zeros((ngp*red, 3))
    et = np.zeros((ngp*red, 3))

    eps = np.finfo(float).eps

    for i in range(ngp):
        indx = [i*3, i*3+1, i*3+2]
        detJ = np.linalg.det(JT[indx, :])
        if detJ < 10*eps:
            info("Jacobi determinant equal or less than zero!")
        JTinv = np.linalg.inv(JT[indx, :])
        dNx = JTinv @ dNr[indx, :]

        B = np.zeros((3, 8))
        B[0, :] = dNx[0, :]
        B[1, :] = dNx[1, :]
        B[2, :] = dNx[2, :]

        # Process each row of ed
        for j in range(red):
            p1 = -D @ B @ ed[j].T
            p2 = B @ ed[j].T
            es[i + j*ngp, :] = p1.T
            et[i + j*ngp, :] = p2.T

    return es, et, eci

gfunc(G, dt)

Form vector with function values at equally spaced points by linear interpolation.

Parameters

G : array_like Time-function pairs [t_i, g_i], where t_i is time i and g_i is g(t_i), shape (np, 2). dt : float Time step.

Returns

t : ndarray 1-D vector with equally spaced time points. g : ndarray 1-D vector with corresponding function values.

Source code in src/calfem/core.py
def gfunc(G,dt):
    """
    Form vector with function values at equally spaced points by linear interpolation.

    Parameters
    ----------
    G : array_like
        Time-function pairs [t_i, g_i], where t_i is time i and g_i is g(t_i), shape (np, 2).
    dt : float
        Time step.

    Returns
    -------
    t : ndarray
        1-D vector with equally spaced time points.
    g : ndarray
        1-D vector with corresponding function values.
    """
    ti = np.arange(G[0,0],G[-1,0]+dt,dt)
    g1 = np.interp(ti,G[:,0],G[:,1])
    return ti, g1

hooke(ptype, E, v)

Calculate the material matrix for a linear elastic and isotropic material.

Parameters

ptype : int Analysis type: 1 : plane stress 2 : plane strain 3 : axisymmetry 4 : three dimensional E : float Young's modulus. v : float Poisson's ratio.

Returns

D : ndarray Material matrix.

Source code in src/calfem/core.py
def hooke(ptype, E, v):
    """
    Calculate the material matrix for a linear elastic and isotropic material.

    Parameters
    ----------
    ptype : int
        Analysis type:
        1 : plane stress
        2 : plane strain
        3 : axisymmetry
        4 : three dimensional
    E : float
        Young's modulus.
    v : float
        Poisson's ratio.

    Returns
    -------
    D : ndarray
        Material matrix.
    """

    if ptype == 1:
        D = E*np.matrix(
            [[1, v, 0],
             [v, 1, 0],
             [0, 0, (1-v)/2]]
        )/(1-v**2)
    elif ptype == 2:
        D = E/(1+v)*np.matrix(
            [[1-v, v, v, 0],
             [v, 1-v, v, 0],
             [v, v, 1-v, 0],
             [0, 0, 0, (1-2*v)/2]]
        )/(1-2*v)
    elif ptype == 3:
        D = E/(1+v)*np.matrix(
            [[1-v, v, v, 0],
             [v, 1-v, v, 0],
             [v, v, 1-v, 0],
             [0, 0, 0, (1-2*v)/2]]
        )/(1-2*v)
    elif ptype == 4:
        D = E*np.matrix(
            [[1-v, v, v, 0, 0, 0],
             [v, 1-v, v, 0, 0, 0],
             [v, v, 1-v, 0, 0, 0],
             [0, 0, 0, (1-2*v)/2, 0, 0],
             [0, 0, 0, 0, (1-2*v)/2, 0],
             [0, 0, 0, 0, 0, (1-2*v)/2]]
        )/(1+v)/(1-2*v)
    else:
        info("ptype not supported.")

    return D

info(msg)

Log an informational message.

Parameters

msg : str Informational message to log.

Source code in src/calfem/core.py
def info(msg: str) -> None:
    """
    Log an informational message.

    Parameters
    ----------
    msg : str
        Informational message to log.
    """
    cflog.info(" calfem.core: "+msg)        

plani4e(ex, ey, ep, D, eq=None)

Calculate the stiffness matrix for a 4 node isoparametric element in plane strain or plane stress.

Parameters

ex : array_like Element coordinates [x1, x2, x3, x4]. ey : array_like Element coordinates [y1, y2, y3, y4]. ep : array_like Element properties [ptype, t, ir], where ptype is analysis type, t is thickness, and ir is integration rule. D : array_like Constitutive matrix. eq : array_like, optional Body force vector [bx, by], where bx, by are body forces in x, y directions.

Returns

Ke : ndarray Element stiffness matrix, shape (8, 8). fe : ndarray, optional Equivalent nodal forces, shape (8, 1), if eq is provided.

Source code in src/calfem/core.py
def plani4e(ex, ey, ep, D, eq=None):
    """
    Calculate the stiffness matrix for a 4 node isoparametric element in plane strain or plane stress.

    Parameters
    ----------
    ex : array_like
        Element coordinates [x1, x2, x3, x4].
    ey : array_like
        Element coordinates [y1, y2, y3, y4].
    ep : array_like
        Element properties [ptype, t, ir], where ptype is analysis type, t is thickness, and ir is integration rule.
    D : array_like
        Constitutive matrix.
    eq : array_like, optional
        Body force vector [bx, by], where bx, by are body forces in x, y directions.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (8, 8).
    fe : ndarray, optional
        Equivalent nodal forces, shape (8, 1), if eq is provided.
    """
    ptype = ep[0]
    t = ep[1]
    ir = ep[2]
    ngp = ir*ir
    if eq is None:
        q = np.zeros((2, 1))
    else:
        q = np.reshape(eq, (2, 1))
#--------- gauss points --------------------------------------
    if ir == 1:
        g1 = 0.0
        w1 = 2.0
        gp = np.matrix([g1, g1])
        w = np.matrix([w1, w1])
    elif ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.matrix([
            [-g1, -g1],
            [g1, -g1],
            [-g1, g1],
            [g1, g1]])
        w = np.matrix([
            [w1, w1],
            [w1, w1],
            [w1, w1],
            [w1, w1]])
    elif ir == 3:
        g1 = 0.774596669241483
        g2 = 0.
        w1 = 0.555555555555555
        w2 = 0.888888888888888
        gp = np.matrix([
            [-g1, -g1],
            [-g2, -g1],
            [g1, -g1],
            [-g1, g2],
            [g2, g2],
            [g1, g2],
            [-g1, g1],
            [g2, g1],
            [g1, g1]])
        w = np.matrix([
            [w1, w1],
            [w2, w1],
            [w1, w1],
            [w1, w2],
            [w2, w2],
            [w1, w2],
            [w1, w1],
            [w2, w1],
            [w1, w1]])
    else:
        info("Used number of integrat     ion points not implemented")
    wp = np.multiply(w[:, 0], w[:, 1])
    xsi = gp[:, 0]
    eta = gp[:, 1]
    r2 = ngp*2
    # Shape Functions
    N = np.multiply((1-xsi), (1-eta))/4.
    N = np.append(N, np.multiply((1+xsi), (1-eta))/4., axis=1)
    N = np.append(N, np.multiply((1+xsi), (1+eta))/4., axis=1)
    N = np.append(N, np.multiply((1-xsi), (1+eta))/4., axis=1)

    dNr = np.matrix(np.zeros((r2, 4)))
    dNr[0:r2:2, 0] = -(1-eta)/4.
    dNr[0:r2:2, 1] = (1-eta)/4.
    dNr[0:r2:2, 2] = (1+eta)/4.
    dNr[0:r2:2, 3] = -(1+eta)/4.
    dNr[1:r2+1:2, 0] = -(1-xsi)/4.
    dNr[1:r2+1:2, 1] = -(1+xsi)/4.
    dNr[1:r2+1:2, 2] = (1+xsi)/4.
    dNr[1:r2+1:2, 3] = (1-xsi)/4.

#
    Ke1 = np.matrix(np.zeros((8, 8)))
    fe1 = np.matrix(np.zeros((8, 1)))
    JT = dNr*np.matrix([ex, ey]).T
    # --------- plane stress --------------------------------------
    if ptype == 1:
        colD = np.shape(D)[0]
        if colD > 3:
            Cm = np.linalg.inv(D)
            Dm = np.linalg.inv(Cm[np.ix_([0, 1, 3], [0, 1, 3])])
        else:
            Dm = D
#
        B = np.matrix(np.zeros((3, 8)))
        N2 = np.matrix(np.zeros((2, 8)))
        for i in range(ngp):
            indx = np.array([2*(i+1)-1, 2*(i+1)])
            detJ = np.linalg.det(JT[indx-1, :])
            if detJ < 10*np.finfo(float).eps:
                info("Jacobi determinant equal or less than zero!")
            JTinv = np.linalg.inv(JT[indx-1, :])
            dNx = JTinv*dNr[indx-1, :]
#
            index_array_even = np.array([0, 2, 4, 6])
            index_array_odd = np.array([1, 3, 5, 7])
#
            counter = 0
            for index in index_array_even:
                B[0, index] = dNx[0, counter]
                B[2, index] = dNx[1, counter]
                N2[0, index] = N[i, counter]
                counter = counter+1
#
            counter = 0
            for index in index_array_odd:
                B[1, index] = dNx[1, counter]
                B[2, index] = dNx[0, counter]
                N2[1, index] = N[i, counter]
                counter = counter+1
#
            Ke1 = Ke1+B.T*Dm*B*detJ*wp[i].item()*t
            fe1 = fe1 + N2.T * q * detJ * wp[i].item() * t

        return Ke1, fe1
#--------- plane strain --------------------------------------
    elif ptype == 2:
        #
        colD = np.shape(D)[0]
        if colD > 3:
            Dm = D[np.ix_([0, 1, 3], [0, 1, 3])]
        else:
            Dm = D
#
        B = np.matrix(np.zeros((3, 8)))
        N2 = np.matrix(np.zeros((2, 8)))
        for i in range(ngp):
            indx = np.array([2*(i+1)-1, 2*(i+1)])
            detJ = np.linalg.det(JT[indx-1, :])
            if detJ < 10*np.finfo(float).eps:
                info("Jacobideterminant equal or less than zero!")
            JTinv = np.linalg.inv(JT[indx-1, :])
            dNx = JTinv*dNr[indx-1, :]
#
            index_array_even = np.array([0, 2, 4, 6])
            index_array_odd = np.array([1, 3, 5, 7])
#
            counter = 0
            for index in index_array_even:
                #
                B[0, index] = dNx[0, counter]
                B[2, index] = dNx[1, counter]
                N2[0, index] = N[i, counter]
#
                counter = counter+1
#
            counter = 0
            for index in index_array_odd:
                B[1, index] = dNx[1, counter]
                B[2, index] = dNx[0, counter]
                N2[1, index] = N[i, counter]
                counter = counter+1
#
            Ke1 = Ke1 + B.T * Dm * B * detJ * wp[i].item() * t
            fe1 = fe1+N2.T*q*detJ*wp[i].item()*t
        return Ke1, fe1
    else:
        info("Error ! Check first argument, ptype=1 or 2 allowed")

planqe(ex, ey, ep, D, eq=None)

Calculate the stiffness matrix for a quadrilateral plane stress or plane strain element.

Parameters

ex : array_like Element coordinates [x1, x2, x3, x4]. ey : array_like Element coordinates [y1, y2, y3, y4]. ep : array_like Element properties [ptype, t], where ptype is analysis type and t is element thickness. D : array_like Constitutive matrix. eq : array_like, optional Body force vector [bx, by], where bx, by are body forces in x, y directions.

Returns

Ke : ndarray Element stiffness matrix, shape (8, 8). fe : ndarray, optional Equivalent nodal forces, if eq is provided.

Source code in src/calfem/core.py
def planqe(ex, ey, ep, D, eq=None):
    """
    Calculate the stiffness matrix for a quadrilateral plane stress or plane strain element.

    Parameters
    ----------
    ex : array_like
        Element coordinates [x1, x2, x3, x4].
    ey : array_like
        Element coordinates [y1, y2, y3, y4].
    ep : array_like
        Element properties [ptype, t], where ptype is analysis type and t is element thickness.
    D : array_like
        Constitutive matrix.
    eq : array_like, optional
        Body force vector [bx, by], where bx, by are body forces in x, y directions.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (8, 8).
    fe : ndarray, optional
        Equivalent nodal forces, if eq is provided.
    """
    K = np.zeros((10, 10))
    f = np.zeros((10, 1))

    xm = sum(ex)/4.
    ym = sum(ey)/4.

    b1 = eq if eq is not None else np.array([[0], [0]])

    # Make sure b1 is properly shaped for plante
    b1 = np.asarray(b1).flatten()

    # Create element coordinates for triangular subelements
    ex1 = np.array([ex[0], ex[1], xm])
    ey1 = np.array([ey[0], ey[1], ym])
    ex2 = np.array([ex[1], ex[2], xm])
    ey2 = np.array([ey[1], ey[2], ym])
    ex3 = np.array([ex[2], ex[3], xm])
    ey3 = np.array([ey[2], ey[3], ym])
    ex4 = np.array([ex[3], ex[0], xm])
    ey4 = np.array([ey[3], ey[0], ym])

    # Create element matrices for each subelement
    ke1, fe1 = plante(ex1, ey1, ep, D, b1)
    # Convert fe1 to column vector if needed
    fe1 = np.asarray(fe1).reshape(-1, 1)
    K, f = assem(np.array([1, 2, 3, 4, 9, 10]), K, ke1, f, fe1)

    ke1, fe1 = plante(ex2, ey2, ep, D, b1)
    # Convert fe1 to column vector if needed
    fe1 = np.asarray(fe1).reshape(-1, 1)
    K, f = assem(np.array([3, 4, 5, 6, 9, 10]), K, ke1, f, fe1)

    ke1, fe1 = plante(ex3, ey3, ep, D, b1)
    # Convert fe1 to column vector if needed
    fe1 = np.asarray(fe1).reshape(-1, 1)
    K, f = assem(np.array([5, 6, 7, 8, 9, 10]), K, ke1, f, fe1)

    ke1, fe1 = plante(ex4, ey4, ep, D, b1)
    # Convert fe1 to column vector if needed
    fe1 = np.asarray(fe1).reshape(-1, 1)
    K, f = assem(np.array([7, 8, 1, 2, 9, 10]), K, ke1, f, fe1)

    # Static condensation
    Ke, fe = statcon(K, f, np.array([[9], [10]]))

    if eq is None:
        return Ke
    else:
        return Ke, fe

planqs(ex, ey, ep, D, ed, eq=None)

Calculate element normal and shear stress for a quadrilateral plane stress or plane strain element.

Parameters

ex : array_like Element node x-coordinates [x1, x2, x3, x4]. ey : array_like Element node y-coordinates [y1, y2, y3, y4]. ep : array_like Element properties [ptype, t], where ptype is analysis type and t is thickness. D : array_like Constitutive matrix. ed : array_like Element displacement vector [u1, u2, ..., u8]. eq : array_like, optional Body force vector [bx, by], where bx, by are body forces in x, y directions.

Returns

es : ndarray Element stress array [sigx, sigy, (sigz), tauxy]. et : ndarray Element strain array [epsx, epsy, (epsz), gamxy].

Source code in src/calfem/core.py
def planqs(ex, ey, ep, D, ed, eq=None):
    """
    Calculate element normal and shear stress for a quadrilateral plane stress or plane strain element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2, x3, x4].
    ey : array_like
        Element node y-coordinates [y1, y2, y3, y4].
    ep : array_like
        Element properties [ptype, t], where ptype is analysis type and t is thickness.
    D : array_like
        Constitutive matrix.
    ed : array_like
        Element displacement vector [u1, u2, ..., u8].
    eq : array_like, optional
        Body force vector [bx, by], where bx, by are body forces in x, y directions.

    Returns
    -------
    es : ndarray
        Element stress array [sigx, sigy, (sigz), tauxy].
    et : ndarray
        Element strain array [epsx, epsy, (epsz), gamxy].
    """

    # Convert inputs to arrays for consistency
    ex = np.asarray(ex).flatten()
    ey = np.asarray(ey).flatten()
    ed = np.asarray(ed).flatten()

    if len(ex) != 4 or len(ey) != 4 or len(ed) != 8:
        raise ValueError(
            'Error ! PLANQS: only one element at the time (ex, ey, ed must be arrays with 4, 4, and 8 elements)')

    K = np.zeros((10, 10))
    f = np.zeros((10, 1))

    xm = sum(ex)/4.
    ym = sum(ey)/4.

    b1 = eq if eq is not None else np.array([[0], [0]])

    ex1 = np.array([ex[0], ex[1], xm])
    ey1 = np.array([ey[0], ey[1], ym])
    ex2 = np.array([ex[1], ex[2], xm])
    ey2 = np.array([ey[1], ey[2], ym])
    ex3 = np.array([ex[2], ex[3], xm])
    ey3 = np.array([ey[2], ey[3], ym])
    ex4 = np.array([ex[3], ex[0], xm])
    ey4 = np.array([ey[3], ey[0], ym])

    ke1, fe1 = plante(ex1, ey1, ep, D, b1)
    K, f = assem(np.array([1, 2, 3, 4, 9, 10]), K, ke1, f, fe1)
    ke1, fe1 = plante(ex2, ey2, ep, D, b1)
    K, f = assem(np.array([3, 4, 5, 6, 9, 10]), K, ke1, f, fe1)
    ke1, fe1 = plante(ex3, ey3, ep, D, b1)
    K, f = assem(np.array([5, 6, 7, 8, 9, 10]), K, ke1, f, fe1)
    ke1, fe1 = plante(ex4, ey4, ep, D, b1)
    K, f = assem(np.array([7, 8, 1, 2, 9, 10]), K, ke1, f, fe1)

    A1 = 0.5 * np.linalg.det(np.vstack([
        np.ones(3),
        ex1,
        ey1
    ]).T)

    A2 = 0.5 * np.linalg.det(np.vstack([
        np.ones(3),
        ex2,
        ey2
    ]).T)

    A3 = 0.5 * np.linalg.det(np.vstack([
        np.ones(3),
        ex3,
        ey3
    ]).T)

    A4 = 0.5 * np.linalg.det(np.vstack([
        np.ones(3),
        ex4,
        ey4
    ]).T)

    Atot = A1+A2+A3+A4

    a, _ = solveq(K, f, np.array(range(1, 9)), ed)

    # Create proper element displacement arrays for each triangular sub-element
    ed1 = np.array([a[0,0], a[1,0], a[2,0], a[3,0], a[8,0], a[9,0]])
    ed2 = np.array([a[2,0], a[3,0], a[4,0], a[5,0], a[8,0], a[9,0]])
    ed3 = np.array([a[4,0], a[5,0], a[6,0], a[7,0], a[8,0], a[9,0]])
    ed4 = np.array([a[6,0], a[7,0], a[0,0], a[1,0], a[8,0], a[9,0]])

    s1, t1 = plants(ex1, ey1, ep, D, ed1)
    s2, t2 = plants(ex2, ey2, ep, D, ed2)
    s3, t3 = plants(ex3, ey3, ep, D, ed3)
    s4, t4 = plants(ex4, ey4, ep, D, ed4)

    es = (s1*A1+s2*A2+s3*A3+s4*A4)/Atot
    et = (t1*A1+t2*A2+t3*A3+t4*A4)/Atot

    return es[0], et[0]

plante(ex, ey, ep, D, eq=None)

Calculate the stiffness matrix for a triangular plane stress or plane strain element.

Parameters

ex : array_like Element coordinates [x1, x2, x3]. ey : array_like Element coordinates [y1, y2, y3]. ep : array_like Element properties [ptype, t], where ptype is analysis type and t is thickness. D : array_like Constitutive matrix. eq : array_like, optional Body force vector [bx, by], where bx, by are body forces in x, y directions.

Returns

Ke : ndarray Element stiffness matrix, shape (6, 6). fe : ndarray, optional Equivalent nodal forces, shape (6, 1), if eq is given.

Source code in src/calfem/core.py
def plante(ex, ey, ep, D, eq=None):
    """
    Calculate the stiffness matrix for a triangular plane stress or plane strain element.

    Parameters
    ----------
    ex : array_like
        Element coordinates [x1, x2, x3].
    ey : array_like
        Element coordinates [y1, y2, y3].
    ep : array_like
        Element properties [ptype, t], where ptype is analysis type and t is thickness.
    D : array_like
        Constitutive matrix.
    eq : array_like, optional
        Body force vector [bx, by], where bx, by are body forces in x, y directions.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (6, 6).
    fe : ndarray, optional
        Equivalent nodal forces, shape (6, 1), if eq is given.
    """

    ptype, t = ep

    bx = 0.0
    by = 0.0

    if not eq is None:
        bx = eq[0]
        by = eq[1]

    # Ensure arrays have correct dimensions for operations
    ex_array = np.asarray(ex).reshape(-1, 1)
    ey_array = np.asarray(ey).reshape(-1, 1)
    ones_col = np.ones((3, 1))

    # Construct C matrix with proper dimensions
    C = np.array([
        [1, ex[0], ey[0], 0,     0,     0],
        [0,     0,     0, 1, ex[0], ey[0]],
        [1, ex[1], ey[1], 0,     0,     0],
        [0,     0,     0, 1, ex[1], ey[1]],
        [1, ex[2], ey[2], 0,     0,     0],
        [0,     0,     0, 1, ex[2], ey[2]]
    ])

    # Calculate area using a properly shaped array
    A_matrix = np.hstack([ones_col, ex_array, ey_array])
    A = 0.5 * np.linalg.det(A_matrix)

    # --------- plane stress --------------------------------------

    if ptype == 1:
        B = np.array([
            [0, 1, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 1],
            [0, 0, 1, 0, 1, 0]
        ]) @ np.linalg.inv(C)

        colD = D.shape[1]

        if colD > 3:
            Cm = np.linalg.inv(D)
            Dm = np.linalg.inv(Cm[np.ix_((0, 1, 3), (0, 1, 3))])
        else:
            Dm = D

        Ke = B.T @ Dm @ B * A * t
        fe = A/3 * np.array([bx, by, bx, by, bx, by]).reshape(-1, 1) * t

        if eq is None:
            return Ke
        else:
            return Ke, fe.T

    #--------- plane strain --------------------------------------

    elif ptype == 2:
        B = np.array([
            [0, 1, 0, 0, 0, 0, ],
            [0, 0, 0, 0, 0, 1, ],
            [0, 0, 1, 0, 1, 0, ]
        ]) @ np.linalg.inv(C)

        colD = D.shape[1]

        if colD > 3:
            Dm = D[np.ix_((0, 1, 3), (0, 1, 3))]
        else:
            Dm = D

        Ke = B.T @ Dm @ B * A * t
        fe = A/3 * np.array([bx, by, bx, by, bx, by]).reshape(-1, 1) * t

        if eq is None:
            return Ke
        else:
            return Ke, fe.T

    else:
        info("Error ! Check first argument, ptype=1 or 2 allowed")
        if eq is None:
            return None
        else:
            return None, None

plantf(ex, ey, ep, es)

Compute internal element force vector in a triangular element in plane stress or plane strain.

Parameters

ex : array_like Element node x-coordinates [x1, x2, x3]. ey : array_like Element node y-coordinates [y1, y2, y3]. ep : array_like Element properties [ptype, t], where ptype is analysis type and t is thickness. es : array_like Element stress matrix [[sigx, sigy, [sigz], tauxy], [...]], one row for each element.

Returns

fe : ndarray Internal force vector [[f1], [f2], ..., [f8]].

Source code in src/calfem/core.py
def plantf(ex, ey, ep, es):
    """
    Compute internal element force vector in a triangular element in plane stress or plane strain.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2, x3].
    ey : array_like
        Element node y-coordinates [y1, y2, y3].
    ep : array_like
        Element properties [ptype, t], where ptype is analysis type and t is thickness.
    es : array_like
        Element stress matrix [[sigx, sigy, [sigz], tauxy], [...]], one row for each element.

    Returns
    -------
    fe : ndarray
        Internal force vector [[f1], [f2], ..., [f8]].
    """

    ptype, t = ep

    stress = np.asarray(es, dtype=float).reshape(-1)

    if stress.size == 4:
        stress = stress[[0, 1, 3]]
    elif stress.size != 3:
        raise ValueError("es must contain 3 or 4 stress components")

    stress = stress.reshape(3, 1)

    #--------- plane stress --------------------------------------

    if ptype == 1:

        C = np.matrix([
            [1, ex[0], ey[0], 0, 0,     0],
            [0, 0,     0,     1, ex[0], ey[0]],
            [1, ex[1], ey[1], 0, 0,     0],
            [0, 0,     0,     1, ex[1], ey[1]],
            [1, ex[2], ey[2], 0, 0,     0],
            [0, 0,     0,     1, ex[2], ey[2]]
        ])

        A = 0.5*np.linalg.det(np.matrix([
            [1, ex[0], ey[0]],
            [1, ex[1], ey[1]],
            [1, ex[2], ey[2]]
        ]))

        B = np.matrix([
            [0, 1, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 1],
            [0, 0, 1, 0, 1, 0]
        ])*np.linalg.inv(C)

        ef = A*t*(B.T @ stress)

        return np.reshape(np.asarray(ef), 6)

    #--------- plane strain --------------------------------------

    elif ptype == 2:

        C = np.matrix([
            [1, ex[0], ey[0], 0, 0,     0],
            [0, 0,     0,     1, ex[0], ey[0]],
            [1, ex[1], ey[1], 0, 0,     0],
            [0, 0,     0,     1, ex[1], ey[1]],
            [1, ex[2], ey[2], 0, 0,     0],
            [0, 0,     0,     1, ex[2], ey[2]]
        ])

        A = 0.5*np.linalg.det(np.matrix([
            [1, ex[0], ey[0]],
            [1, ex[1], ey[1]],
            [1, ex[2], ey[2]]
        ]))

        B = np.matrix([
            [0, 1, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 1],
            [0, 0, 1, 0, 1, 0]
        ])*np.linalg.inv(C)

        ef = A*t*(B.T @ stress)

        return np.reshape(np.asarray(ef), (6,1))

    else:
        info("Error ! Check first argument, ptype=1 or 2 allowed")
        return None

plants(ex, ey, ep, D, ed)

Calculate element normal and shear stress for a triangular plane stress or plane strain element.

Parameters

ex : array_like Element node x-coordinates [x1, x2, x3]. ey : array_like Element node y-coordinates [y1, y2, y3]. ep : array_like Element properties [ptype, t], where ptype is analysis type and t is thickness. D : array_like Constitutive matrix. ed : array_like Element displacement vector [u1, u2, ..., u6], one row for each element.

Returns

es : ndarray Element stress matrix, one row for each element. Each row contains [sigx, sigy, [sigz], tauxy]. et : ndarray Element strain matrix, one row for each element. Each row contains [epsx, epsy, [epsz], gamxy].

Source code in src/calfem/core.py
def plants(ex, ey, ep, D, ed):
    """
    Calculate element normal and shear stress for a triangular plane stress or plane strain element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2, x3].
    ey : array_like
        Element node y-coordinates [y1, y2, y3].
    ep : array_like
        Element properties [ptype, t], where ptype is analysis type and t is thickness.
    D : array_like
        Constitutive matrix.
    ed : array_like
        Element displacement vector [u1, u2, ..., u6], one row for each element.

    Returns
    -------
    es : ndarray
        Element stress matrix, one row for each element.
        Each row contains [sigx, sigy, [sigz], tauxy].
    et : ndarray
        Element strain matrix, one row for each element.
        Each row contains [epsx, epsy, [epsz], gamxy].
    """

    ptype = ep[0]

    # Ensure all inputs are proper arrays
    ex = np.asarray(ex)
    ey = np.asarray(ey)
    ed = np.asarray(ed)

    if np.ndim(ex) == 1:
        ex = np.array([ex])
    if np.ndim(ey) == 1:
        ey = np.array([ey])
    if np.ndim(ed) == 1:
        ed = np.array([ed])

    rowed = ed.shape[0]
    rowex = ex.shape[0]

    # --------- plane stress --------------------------------------

    if ptype == 1:

        colD = D.shape[1]

        if colD > 3:
            Cm = np.linalg.inv(D)
            Dm = np.linalg.inv(Cm[np.ix_((0, 1, 3), (0, 1, 3))])
        else:
            Dm = D

        incie = 0

        if rowex == 1:
            incie = 0
        else:
            incie = 1

        et = np.zeros([rowed, colD])
        es = np.zeros([rowed, colD])

        ie = 0

        for i in range(rowed):
            # Create C matrix for element
            ex1 = ex[ie, :].flatten()
            ey1 = ey[ie, :].flatten()

            # Construct the C matrix properly
            C = np.array([
                [1, ex1[0], ey1[0], 0, 0, 0],
                [0, 0, 0, 1, ex1[0], ey1[0]],
                [1, ex1[1], ey1[1], 0, 0, 0],
                [0, 0, 0, 1, ex1[1], ey1[1]],
                [1, ex1[2], ey1[2], 0, 0, 0],
                [0, 0, 0, 1, ex1[2], ey1[2]]
            ])

            # Create the element B matrix
            B = np.array([
                [0, 1, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 1],
                [0, 0, 1, 0, 1, 0]]) @ np.linalg.inv(C)

            # Make sure element displacements are the right shape
            element_disp = ed[ie, :].flatten()

            # Calculate strains
            ee = B @ element_disp.reshape(-1, 1)

            if colD > 3:
                ss = np.zeros([colD, 1])
                ss[[0, 1, 3]] = Dm @ ee
                ee = Cm @ ss
            else:
                ss = Dm @ ee

            et[ie, :] = ee.T.flatten()
            es[ie, :] = ss.T.flatten()

            ie = ie + incie

        return es, et

    # --------- plane strain --------------------------------------
    elif ptype == 2:  # Implementation by LAPM
        colD = D.shape[1]
        incie = 0

        if rowex == 1:
            incie = 0
        else:
            incie = 1

        et = np.zeros([rowed, colD])
        es = np.zeros([rowed, colD])

        ie = 0

        ee = np.zeros([colD, 1])

        for i in range(rowed):
            # Create C matrix for element
            ex1 = ex[ie, :].flatten()
            ey1 = ey[ie, :].flatten()

            # Construct the C matrix properly
            C = np.array([
                [1, ex1[0], ey1[0], 0, 0, 0],
                [0, 0, 0, 1, ex1[0], ey1[0]],
                [1, ex1[1], ey1[1], 0, 0, 0],
                [0, 0, 0, 1, ex1[1], ey1[1]],
                [1, ex1[2], ey1[2], 0, 0, 0],
                [0, 0, 0, 1, ex1[2], ey1[2]]
            ])

            # Create the element B matrix
            B = np.array([
                [0, 1, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 1],
                [0, 0, 1, 0, 1, 0]]) @ np.linalg.inv(C)

            # Make sure element displacements are the right shape
            element_disp = ed[ie, :].flatten()

            # Calculate strains
            e = B @ element_disp.reshape(-1, 1)

            if colD > 3:
                ee[[0, 1, 3]] = e
            else:
                ee = e

            et[ie, :] = ee.T.flatten()
            es[ie, :] = (D @ ee).T.flatten()

            ie = ie + incie

        return es, et

    else:
        print("Error ! Check first argument, ptype=1 or 2 allowed")
        return None

platre(ex, ey, ep, D, eq=None)

Calculate the stiffness matrix for a rectangular plate element.

NOTE! Element sides must be parallel to the coordinate axis.

Parameters

ex : array_like Element coordinates [x1, x2, x3, x4]. ey : array_like Element coordinates [y1, y2, y3, y4]. ep : array_like Element properties [t], where t is thickness. D : array_like Constitutive matrix for plane stress. eq : array_like, optional Load per unit area [qz].

Returns

Ke : ndarray Element stiffness matrix, shape (12, 12). fe : ndarray, optional Equivalent nodal forces, shape (12, 1), if eq is not None.

Source code in src/calfem/core.py
def platre(ex, ey, ep, D, eq=None):
    """
    Calculate the stiffness matrix for a rectangular plate element.

    NOTE! Element sides must be parallel to the coordinate axis.

    Parameters
    ----------
    ex : array_like
        Element coordinates [x1, x2, x3, x4].
    ey : array_like
        Element coordinates [y1, y2, y3, y4].
    ep : array_like
        Element properties [t], where t is thickness.
    D : array_like
        Constitutive matrix for plane stress.
    eq : array_like, optional
        Load per unit area [qz].

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix, shape (12, 12).
    fe : ndarray, optional
        Equivalent nodal forces, shape (12, 1), if eq is not None.
    """
    Lx = (ex[2]-ex[0]).astype(float)
    Ly = (ey[2]-ey[0]).astype(float)
    t = ep[0]

    D = t**3/12.*D

    A1 = Ly/(Lx**3)
    A2 = Lx/(Ly**3)
    A3 = 1/Lx/Ly
    A4 = Ly/(Lx**2)
    A5 = Lx/(Ly**2)
    A6 = 1/Lx
    A7 = 1/Ly
    A8 = Ly/Lx
    A9 = Lx/Ly

    C1 = 4*A1*D[0, 0]+4*A2*D[1, 1]+2*A3*D[0, 1]+5.6*A3*D[2, 2]
    C2 = -4*A1*D[0, 0]+2*A2*D[1, 1]-2*A3*D[0, 1]-5.6*A3*D[2, 2]
    C3 = 2*A1*D[0, 0]-4*A2*D[1, 1]-2*A3*D[0, 1]-5.6*A3*D[2, 2]
    C4 = -2*A1*D[0, 0]-2*A2*D[1, 1]+2*A3*D[0, 1]+5.6*A3*D[2, 2]
    C5 = 2*A5*D[1, 1]+A6*D[0, 1]+0.4*A6*D[2, 2]
    C6 = 2*A4*D[0, 0]+A7*D[0, 1]+0.4*A7*D[2, 2]

    C7 = 2*A5*D[1, 1]+0.4*A6*D[2, 2]
    C8 = 2*A4*D[0, 0]+0.4*A7*D[2, 2]
    C9 = A5*D[1, 1]-A6*D[0, 1]-0.4*A6*D[2, 2]
    C10 = A4*D[0, 0]-A7*D[0, 1]-0.4*A7*D[2, 2]
    C11 = A5*D[1, 1]-0.4*A6*D[2, 2]
    C12 = A4*D[0, 0]-0.4*A7*D[2, 2]

    C13 = 4/3.*A9*D[1, 1]+8/15.*A8*D[2, 2]
    C14 = 4/3.*A8*D[0, 0]+8/15.*A9*D[2, 2]
    C15 = 2/3.*A9*D[1, 1]-8/15.*A8*D[2, 2]
    C16 = 2/3.*A8*D[0, 0]-8/15.*A9*D[2, 2]
    C17 = 2/3.*A9*D[1, 1]-2/15.*A8*D[2, 2]
    C18 = 2/3.*A8*D[0, 0]-2/15.*A9*D[2, 2]
    C19 = 1/3.*A9*D[1, 1]+2/15.*A8*D[2, 2]
    C20 = 1/3.*A8*D[0, 0]+2/15.*A9*D[2, 2]
    C21 = D[0, 1]

    Keq = np.matrix(np.zeros((12, 12)))
    Keq[0, 0:13] = C1, C5, -C6, C2, C9, -C8, C4, C11, -C12, C3, C7, -C10
    Keq[1, 1:13] = C13, -C21, C9, C15, 0, -C11, C19, 0, -C7, C17, 0
    Keq[2, 2:13] = C14, C8, 0, C18, C12, 0, C20, -C10, 0, C16
    Keq[3, 3:13] = C1, C5, C6, C3, C7, C10, C4, C11, C12
    Keq[4, 4:13] = C13, C21, -C7, C17, 0, -C11, C19, 0
    Keq[5, 5:13] = C14, C10, 0, C16, -C12, 0, C20
    Keq[6, 6:13] = C1, -C5, C6, C2, -C9, C8
    Keq[7, 7:13] = C13, -C21, -C9, C15, 0
    Keq[8, 8:13] = C14, -C8, 0, C18
    Keq[9, 9:13] = C1, -C5, -C6
    Keq[10, 10:13] = C13, C21
    Keq[11, 11] = C14
    Keq = Keq.T+Keq-np.diag(np.diag(Keq))

    if eq != None:
        q = eq
        R1 = q*Lx*Ly/4
        R2 = q*Lx*Ly**2/24
        R3 = q*Ly*Lx**2/24

        feq = np.matrix([R1, R2, -R3, R1, R2, R3, R1, -R2, R3, R1, -R2, -R3])

    if eq != None:
        return Keq, feq
    else:
        return Keq

red(A, b)

Reduce the size of a square matrix by omitting rows and columns.

Algorithm for reducing the size of a square matrix A by omitting rows and columns defined by the matrix b.

Parameters

A : array_like Unreduced square matrix, shape (nd, nd). b : array_like Boundary condition matrix containing DOF indices to remove, shape (nbc, 1) or (nbc,), where nbc is the number of constraints.

Returns

B : ndarray Reduced matrix with rows and columns removed.

Examples

K = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) bc = np.array([[2]]) # Remove row/column 2 (1-indexed) K_red = red(K, bc)

Source code in src/calfem/core.py
def red(A: ArrayLike, b: ArrayLike) -> NDArray[np.floating]:
    """
    Reduce the size of a square matrix by omitting rows and columns.

    Algorithm for reducing the size of a square matrix A by omitting 
    rows and columns defined by the matrix b.

    Parameters
    ----------
    A : array_like
        Unreduced square matrix, shape (nd, nd).
    b : array_like
        Boundary condition matrix containing DOF indices to remove,
        shape (nbc, 1) or (nbc,), where nbc is the number of constraints.

    Returns
    -------
    B : ndarray
        Reduced matrix with rows and columns removed.

    Examples
    --------
    >>> K = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    >>> bc = np.array([[2]])  # Remove row/column 2 (1-indexed)
    >>> K_red = red(K, bc)
    """
    A_arr = np.asarray(A)
    b_arr = np.asarray(b)

    nd = A_arr.shape[0]
    fdof = np.arange(1, nd + 1)

    # Extract prescribed DOFs from first column
    if b_arr.ndim == 1:
        pdof = b_arr
    else:
        pdof = b_arr[:, 0]

    # Remove prescribed DOFs from free DOFs (convert to 0-indexed)
    fdof = np.delete(fdof, pdof - 1)

    # Extract reduced matrix (convert back to 0-indexed for numpy)
    B = A_arr[np.ix_(fdof - 1, fdof - 1)]

    return B

soli8e(ex, ey, ez, ep, D, eqp=None)

Calculate the stiffness matrix (and optionally load vector) for an 8-node (brick) isoparametric element.

Parameters

ex : array_like Element node x-coordinates [x1, x2, x3, ..., x8]. ey : array_like Element node y-coordinates [y1, y2, y3, ..., y8]. ez : array_like Element node z-coordinates [z1, z2, z3, ..., z8]. ep : array_like Element properties [ir], where ir is the integration rule. D : array_like Constitutive matrix. eqp : array_like, optional Body force vector [bx, by, bz], where bx, by, bz are body forces in x, y, z directions.

Returns

Ke : ndarray Element stiffness matrix. fe : ndarray, optional Equivalent nodal forces, if eqp is not None.

History

LAST MODIFIED: M Ristinmaa 1995-10-25 J Lindemann 2022-01-24 (Python version)

Source code in src/calfem/core.py
def soli8e(ex, ey, ez, ep, D, eqp=None):
    """
    Calculate the stiffness matrix (and optionally load vector) for an 8-node (brick) isoparametric element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2, x3, ..., x8].
    ey : array_like
        Element node y-coordinates [y1, y2, y3, ..., y8].
    ez : array_like
        Element node z-coordinates [z1, z2, z3, ..., z8].
    ep : array_like
        Element properties [ir], where ir is the integration rule.
    D : array_like
        Constitutive matrix.
    eqp : array_like, optional
        Body force vector [bx, by, bz], where bx, by, bz are body forces in x, y, z directions.

    Returns
    -------
    Ke : ndarray
        Element stiffness matrix.
    fe : ndarray, optional
        Equivalent nodal forces, if eqp is not None.

    History
    -------
    LAST MODIFIED: M Ristinmaa   1995-10-25
                   J Lindemann   2022-01-24 (Python version)
    """
    ir = ep[0]
    ngp = ir*ir*ir

    if eqp is None:
        eq = np.zeros((3, 1))
    else:
        eq = eqp

    if ir == 1:
        g1 = 0.0
        w1 = 2.0
        gp = np.array([g1, g1, g1]).reshape(1, 3)
        w = np.array([w1, w1, w1]).reshape(1, 3)
    elif ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.zeros((8, 3))
        w = np.zeros((8, 3))
        gp[:, 0] = np.array([-1, 1, 1, -1, -1, 1, 1, -1])*g1
        w[:, 0] = np.array([1, 1, 1, 1, 1, 1, 1, 1])*w1
        gp[:, 1] = np.array([-1, -1, 1, 1, -1, -1, 1, 1])*g1
        w[:, 1] = np.array([1, 1, 1, 1, 1, 1, 1, 1])*w1
        gp[:, 2] = np.array([-1, -1, -1, -1, 1, 1, 1, 1])*g1
        w[:, 2] = np.array([1, 1, 1, 1, 1, 1, 1, 1])*w1
    else:
        g1 = 0.774596669241483,
        g2 = 0.0
        w1 = 0.555555555555555
        w2 = 0.888888888888888

        gp = np.zeros((27, 3))
        w = np.zeros((27, 3))

        I1 = np.array([-1, 0, 1, -1, 0, 1, -1, 0, 1]).reshape(1, 9)
        I2 = np.array([0, -1, 0, 0, 1, 0, 0, 1, 0]).reshape(1, 9)

        gp[:, 0] = np.concatenate((I1, I1, I1), axis=1)*g1
        gp[:, 0] = np.concatenate((I2, I2, I2), axis=1)*g2 + gp[:, 0]

        I1 = np.abs(I1)
        I2 = np.abs(I2)

        w[:, 0] = np.concatenate((I1, I1, I1), axis=1)*w1
        w[:, 0] = np.concatenate((I2, I2, I2), axis=1)*w2 + w[:, 0]

        I1 = np.array([-1, -1, -1, 0, 0, 0, 1, 1, 1]).reshape(1, 9)
        I2 = np.array([0, 0, 0, 1, 1, 1, 0, 0, 0]).reshape(1, 9)

        gp[:, 1] = np.concatenate((I1, I1, I1), axis=1)*g1
        gp[:, 1] = np.concatenate((I2, I2, I2), axis=1)*g2 + gp[:, 1]

        I1 = np.abs(I1)
        I2 = np.abs(I2)

        w[:, 1] = np.concatenate((I1, I1, I1), axis=1)*w1
        w[:, 1] = np.concatenate((I2, I2, I2), axis=1)*w2 + w[:, 1]

        I1 = np.array([-1, -1, -1, -1, -1, -1, -1, -1, -1]).reshape(1, 9)
        I2 = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]).reshape(1, 9)
        I3 = np.abs(I1)

        gp[:, 2] = np.concatenate((I1, I2, I3), axis=1)*g1
        gp[:, 2] = np.concatenate((I2, I3, I2), axis=1)*g2 + gp[:, 2]

        w[:, 2] = np.concatenate((I3, I2, I3), axis=1)*w1
        w[:, 2] = np.concatenate((I2, I3, I2), axis=1)*w2 + w[:, 2]

    wp = w[:, 0]*w[:, 1]*w[:, 2]

    xsi = gp[:, 0]
    eta = gp[:, 1]
    zet = gp[:, 2]
    r2 = ngp*3

    N = np.zeros((ngp, 8))
    dNr = np.zeros((r2, 8))

    N[:, 0] = (1-xsi)*(1-eta)*(1-zet)/8
    N[:, 1] = (1+xsi)*(1-eta)*(1-zet)/8
    N[:, 2] = (1+xsi)*(1+eta)*(1-zet)/8
    N[:, 3] = (1-xsi)*(1+eta)*(1-zet)/8
    N[:, 4] = (1-xsi)*(1-eta)*(1+zet)/8
    N[:, 5] = (1+xsi)*(1-eta)*(1+zet)/8
    N[:, 6] = (1+xsi)*(1+eta)*(1+zet)/8
    N[:, 7] = (1-xsi)*(1+eta)*(1+zet)/8

    dNr[0:r2+1:3, 0] = -(1-eta)*(1-zet)
    dNr[0:r2+1:3, 1] = (1-eta)*(1-zet)
    dNr[0:r2+1:3, 2] = (1+eta)*(1-zet)
    dNr[0:r2+1:3, 3] = -(1+eta)*(1-zet)
    dNr[0:r2+1:3, 4] = -(1-eta)*(1+zet)
    dNr[0:r2+1:3, 5] = (1-eta)*(1+zet)
    dNr[0:r2+1:3, 6] = (1+eta)*(1+zet)
    dNr[0:r2+1:3, 7] = -(1+eta)*(1+zet)
    dNr[1:r2+2:3, 0] = -(1-xsi)*(1-zet)
    dNr[1:r2+2:3, 1] = -(1+xsi)*(1-zet)
    dNr[1:r2+2:3, 2] = (1+xsi)*(1-zet)
    dNr[1:r2+2:3, 3] = (1-xsi)*(1-zet)
    dNr[1:r2+2:3, 4] = -(1-xsi)*(1+zet)
    dNr[1:r2+2:3, 5] = -(1+xsi)*(1+zet)
    dNr[1:r2+2:3, 6] = (1+xsi)*(1+zet)
    dNr[1:r2+2:3, 7] = (1-xsi)*(1+zet)
    dNr[2:r2+3:3, 0] = -(1-xsi)*(1-eta)
    dNr[2:r2+3:3, 1] = -(1+xsi)*(1-eta)
    dNr[2:r2+3:3, 2] = -(1+xsi)*(1+eta)
    dNr[2:r2+3:3, 3] = -(1-xsi)*(1+eta)
    dNr[2:r2+3:3, 4] = (1-xsi)*(1-eta)
    dNr[2:r2+3:3, 5] = (1+xsi)*(1-eta)
    dNr[2:r2+3:3, 6] = (1+xsi)*(1+eta)
    dNr[2:r2+3:3, 7] = (1-xsi)*(1+eta)

    dNr = dNr/8.0

    Ke = np.zeros((24, 24))
    fe = np.zeros((24, 1))

    ex = np.asarray(ex).reshape((8, 1))
    ey = np.asarray(ey).reshape((8, 1))
    ez = np.asarray(ez).reshape((8, 1))

    JT = dNr@np.concatenate((ex, ey, ez), axis=1)

    eps = np.finfo(float).eps

    for i in range(ngp):
        indx = [i*3, i*3+1, i*3+2]
        detJ = np.linalg.det(JT[indx, :])
        if detJ < 10*eps:
            print('Jacobideterminant equal or less than zero!')
        JTinv = np.linalg.inv(JT[indx, :])
        dNx = JTinv@dNr[indx, :]

        B = np.zeros((6, 24))
        N2 = np.zeros((3, 24))

        B[0, 0:24:3] = dNx[0, :]
        B[1, 1:25:3] = dNx[1, :]
        B[2, 2:26:3] = dNx[2, :]
        B[3, 0:24:3] = dNx[1, :]
        B[3, 1:25:3] = dNx[0, :]
        B[4, 0:24:3] = dNx[2, :]
        B[4, 2:26:3] = dNx[0, :]
        B[5, 1:25:3] = dNx[2, :]
        B[5, 2:26:3] = dNx[1, :]

        N2[0, 0:24:3] = N[i, :]
        N2[1, 1:25:3] = N[i, :]
        N2[2, 2:26:3] = N[i, :]

        Ke = Ke + (np.transpose(B)@D@B)*detJ*wp[i]
        fe = fe + (np.transpose(N2)@eq)*detJ*wp[i]

    if eqp != None:
        return Ke, fe
    else:
        return Ke

soli8s(ex, ey, ez, ep, D, ed)

Calculate element normal and shear stress for an 8-node (brick) isoparametric element.

Parameters

ex : array_like Element node x-coordinates [x1, x2, x3, ..., x8]. ey : array_like Element node y-coordinates [y1, y2, y3, ..., y8]. ez : array_like Element node z-coordinates [z1, z2, z3, ..., z8]. ep : array_like Element properties [Ir], where Ir is the integration rule. D : array_like Constitutive matrix. ed : array_like Element displacement vector [u1, u2, ..., u24].

Returns

es : ndarray Element stress matrix, one row for each integration point. Each row contains [sigx, sigy, sigz, sigxy, sigyz, sigxz]. et : ndarray Element strain matrix, one row for each integration point. Each row contains [epsx, epsy, epsz, epsxy, epsyz, epsxz].

History

LAST MODIFIED: M Ristinmaa 1995-10-25 J Lindemann 2022-02-23 (Python version)

Source code in src/calfem/core.py
def soli8s(ex, ey, ez, ep, D, ed):
    """
    Calculate element normal and shear stress for an 8-node (brick) isoparametric element.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates [x1, x2, x3, ..., x8].
    ey : array_like
        Element node y-coordinates [y1, y2, y3, ..., y8].
    ez : array_like
        Element node z-coordinates [z1, z2, z3, ..., z8].
    ep : array_like
        Element properties [Ir], where Ir is the integration rule.
    D : array_like
        Constitutive matrix.
    ed : array_like
        Element displacement vector [u1, u2, ..., u24].

    Returns
    -------
    es : ndarray
        Element stress matrix, one row for each integration point.
        Each row contains [sigx, sigy, sigz, sigxy, sigyz, sigxz].
    et : ndarray
        Element strain matrix, one row for each integration point.
        Each row contains [epsx, epsy, epsz, epsxy, epsyz, epsxz].

    History
    -------
    LAST MODIFIED: M Ristinmaa   1995-10-25
                   J Lindemann   2022-02-23 (Python version)
    """

    ir = ep[0]
    ngp = ir*ir*ir

    ir = ep[0]
    ngp = ir*ir*ir

    if ir == 1:
        g1 = 0.0
        w1 = 2.0
        gp = np.array([g1, g1, g1]).reshape(1, 3)
        w = np.array([w1, w1, w1]).reshape(1, 3)
    elif ir == 2:
        g1 = 0.577350269189626
        w1 = 1
        gp = np.zeros((8, 3))
        w = np.zeros((8, 3))
        gp[:, 0] = np.array([-1, 1, 1, -1, -1, 1, 1, -1])*g1
        w[:, 0] = np.array([1, 1, 1, 1, 1, 1, 1, 1])*w1
        gp[:, 1] = np.array([-1, -1, 1, 1, -1, -1, 1, 1])*g1
        w[:, 1] = np.array([1, 1, 1, 1, 1, 1, 1, 1])*w1
        gp[:, 2] = np.array([-1, -1, -1, -1, 1, 1, 1, 1])*g1
        w[:, 2] = np.array([1, 1, 1, 1, 1, 1, 1, 1])*w1
    else:
        g1 = 0.774596669241483,
        g2 = 0.0
        w1 = 0.555555555555555
        w2 = 0.888888888888888

        gp = np.zeros((27, 3))
        w = np.zeros((27, 3))

        I1 = np.array([-1, 0, 1, -1, 0, 1, -1, 0, 1]).reshape(1, 9)
        I2 = np.array([0, -1, 0, 0, 1, 0, 0, 1, 0]).reshape(1, 9)

        gp[:, 0] = np.concatenate((I1, I1, I1), axis=1)*g1
        gp[:, 0] = np.concatenate((I2, I2, I2), axis=1)*g2 + gp[:, 0]

        I1 = np.abs(I1)
        I2 = np.abs(I2)

        w[:, 0] = np.concatenate((I1, I1, I1), axis=1)*w1
        w[:, 0] = np.concatenate((I2, I2, I2), axis=1)*w2 + w[:, 0]

        I1 = np.array([-1, -1, -1, 0, 0, 0, 1, 1, 1]).reshape(1, 9)
        I2 = np.array([0, 0, 0, 1, 1, 1, 0, 0, 0]).reshape(1, 9)

        gp[:, 1] = np.concatenate((I1, I1, I1), axis=1)*g1
        gp[:, 1] = np.concatenate((I2, I2, I2), axis=1)*g2 + gp[:, 1]

        I1 = np.abs(I1)
        I2 = np.abs(I2)

        w[:, 1] = np.concatenate((I1, I1, I1), axis=1)*w1
        w[:, 1] = np.concatenate((I2, I2, I2), axis=1)*w2 + w[:, 1]

        I1 = np.array([-1, -1, -1, -1, -1, -1, -1, -1, -1]).reshape(1, 9)
        I2 = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]).reshape(1, 9)
        I3 = np.abs(I1)

        gp[:, 2] = np.concatenate((I1, I2, I3), axis=1)*g1
        gp[:, 2] = np.concatenate((I2, I3, I2), axis=1)*g2 + gp[:, 2]

        w[:, 2] = np.concatenate((I3, I2, I3), axis=1)*w1
        w[:, 2] = np.concatenate((I2, I3, I2), axis=1)*w2 + w[:, 2]

    wp = w[:, 0]*w[:, 1]*w[:, 2]

    xsi = gp[:, 0]
    eta = gp[:, 1]
    zet = gp[:, 2]
    r2 = ngp*3

    N = np.zeros((ngp, 8))
    dNr = np.zeros((r2, 8))

    N[:, 0] = (1-xsi)*(1-eta)*(1-zet)/8
    N[:, 1] = (1+xsi)*(1-eta)*(1-zet)/8
    N[:, 2] = (1+xsi)*(1+eta)*(1-zet)/8
    N[:, 3] = (1-xsi)*(1+eta)*(1-zet)/8
    N[:, 4] = (1-xsi)*(1-eta)*(1+zet)/8
    N[:, 5] = (1+xsi)*(1-eta)*(1+zet)/8
    N[:, 6] = (1+xsi)*(1+eta)*(1+zet)/8
    N[:, 7] = (1-xsi)*(1+eta)*(1+zet)/8

    dNr[0:r2+1:3, 0] = -(1-eta)*(1-zet)
    dNr[0:r2+1:3, 1] = (1-eta)*(1-zet)
    dNr[0:r2+1:3, 2] = (1+eta)*(1-zet)
    dNr[0:r2+1:3, 3] = -(1+eta)*(1-zet)
    dNr[0:r2+1:3, 4] = -(1-eta)*(1+zet)
    dNr[0:r2+1:3, 5] = (1-eta)*(1+zet)
    dNr[0:r2+1:3, 6] = (1+eta)*(1+zet)
    dNr[0:r2+1:3, 7] = -(1+eta)*(1+zet)
    dNr[1:r2+2:3, 0] = -(1-xsi)*(1-zet)
    dNr[1:r2+2:3, 1] = -(1+xsi)*(1-zet)
    dNr[1:r2+2:3, 2] = (1+xsi)*(1-zet)
    dNr[1:r2+2:3, 3] = (1-xsi)*(1-zet)
    dNr[1:r2+2:3, 4] = -(1-xsi)*(1+zet)
    dNr[1:r2+2:3, 5] = -(1+xsi)*(1+zet)
    dNr[1:r2+2:3, 6] = (1+xsi)*(1+zet)
    dNr[1:r2+2:3, 7] = (1-xsi)*(1+zet)
    dNr[2:r2+3:3, 0] = -(1-xsi)*(1-eta)
    dNr[2:r2+3:3, 1] = -(1+xsi)*(1-eta)
    dNr[2:r2+3:3, 2] = -(1+xsi)*(1+eta)
    dNr[2:r2+3:3, 3] = -(1-xsi)*(1+eta)
    dNr[2:r2+3:3, 4] = (1-xsi)*(1-eta)
    dNr[2:r2+3:3, 5] = (1+xsi)*(1-eta)
    dNr[2:r2+3:3, 6] = (1+xsi)*(1+eta)
    dNr[2:r2+3:3, 7] = (1-xsi)*(1+eta)

    dNr = dNr/8.0

    ex = np.asarray(ex).reshape((8, 1))
    ey = np.asarray(ey).reshape((8, 1))
    ez = np.asarray(ez).reshape((8, 1))

    JT = dNr@np.concatenate((ex, ey, ez), axis=1)

    eps = np.finfo(float).eps

    eci = N@np.concatenate((ex, ey, ez), axis=1)
    et = np.zeros((ngp, 6))
    es = np.zeros((ngp, 6))

    ed = ed.reshape(1, 24)

    for i in range(ngp):
        indx = [i*3, i*3+1, i*3+2]
        detJ = np.linalg.det(JT[indx, :])
        if detJ < 10*eps:
            print('Jacobideterminant equal or less than zero!')
        JTinv = np.linalg.inv(JT[indx, :])
        dNx = JTinv@dNr[indx, :]

        B = np.zeros((6, 24))
        N2 = np.zeros((3, 24))

        B[0, 0:24:3] = dNx[0, :]
        B[1, 1:25:3] = dNx[1, :]
        B[2, 2:26:3] = dNx[2, :]
        B[3, 0:24:3] = dNx[1, :]
        B[3, 1:25:3] = dNx[0, :]
        B[4, 0:24:3] = dNx[2, :]
        B[4, 2:26:3] = dNx[0, :]
        B[5, 1:25:3] = dNx[2, :]
        B[5, 2:26:3] = dNx[1, :]

        N2[0, 0:24:3] = N[i, :]
        N2[1, 1:25:3] = N[i, :]
        N2[2, 2:26:3] = N[i, :]

        # [6x24] x [24,1]
        ee = B@np.transpose(ed)

        et[i, :] = ee.reshape(6,)
        es[i, :] = (D@ee).reshape(6,)

    return et, es, eci

solveq(K, f, bcPrescr=None, bcVal=None)

Solve static FE-equations considering boundary conditions.

Parameters

K : array_like Global stiffness matrix, shape (nd, nd). f : array_like Global load vector, shape (nd, 1). bcPrescr : array_like 1-dim integer array containing prescribed dofs. bcVal : array_like, optional 1-dim float array containing prescribed values. If not given all prescribed dofs are assumed 0.

Returns

a : ndarray Solution including boundary values, shape (nd, 1). r : ndarray Reaction force vector, shape (nd, 1).

Source code in src/calfem/core.py
def solveq(K, f, bcPrescr=None, bcVal=None):
    """
    Solve static FE-equations considering boundary conditions.

    Parameters
    ----------
    K : array_like
        Global stiffness matrix, shape (nd, nd).
    f : array_like
        Global load vector, shape (nd, 1).
    bcPrescr : array_like
        1-dim integer array containing prescribed dofs.
    bcVal : array_like, optional
        1-dim float array containing prescribed values.
        If not given all prescribed dofs are assumed 0.

    Returns
    -------
    a : ndarray
        Solution including boundary values, shape (nd, 1).
    r : ndarray
        Reaction force vector, shape (nd, 1).
    """

    if bcPrescr is None:
        return np.array(np.linalg.solve(K, f))

    nDofs = K.shape[0]
    nPdofs = bcPrescr.shape[0]

    if bcVal is None:
        bcVal = np.zeros([nPdofs], 'd')

    bc = np.ones(nDofs, 'bool')
    bcDofs = np.arange(nDofs)

    bc[np.ix_(bcPrescr-1)] = False
    bcDofs = bcDofs[bc]

    # Ensure bcVal is a column vector
    bcVal_col = np.array(bcVal).reshape(-1, 1)

    # Compute fsys with correct broadcasting
    fsys = f[bcDofs] - K[np.ix_((bcDofs), (bcPrescr-1))] @ bcVal_col
    asys = np.linalg.solve(K[np.ix_((bcDofs), (bcDofs))], fsys)

    a = np.zeros([nDofs, 1])
    a[np.ix_(bcPrescr-1)] = bcVal_col
    a[np.ix_(bcDofs)] = asys.reshape(-1, 1)

    Q = K @ a - f

    return (a, Q)

spring1e(ep)

Compute the element stiffness matrix for a spring element.

Parameters

ep : array_like Spring stiffness or analog quantity [k].

Returns

Ke : ndarray Spring stiffness matrix, shape (2, 2).

Examples

spring1e(100) array([[ 100, -100], [-100, 100]])

History

LAST MODIFIED: P-E Austrell 1994-11-02 O Dahlblom 2022-11-15 (Python version)

Source code in src/calfem/core.py
def spring1e(ep: ArrayLike) -> NDArray[np.floating]:
    """
    Compute the element stiffness matrix for a spring element.

    Parameters
    ----------
    ep : array_like
        Spring stiffness or analog quantity [k].

    Returns
    -------
    Ke : ndarray
        Spring stiffness matrix, shape (2, 2).

    Examples
    --------
    >>> spring1e(100)
    array([[ 100, -100],
           [-100,  100]])

    History
    -------
    LAST MODIFIED: P-E Austrell 1994-11-02
                   O Dahlblom   2022-11-15 (Python version)
    """
    k = ep  

    Ke = k * np.array([
        [1, -1],
        [-1, 1]
    ])

    return Ke

spring1s(ep, ed)

Compute the element force in a spring element.

Parameters

ep : array_like Spring stiffness or analog quantity [k]. ed : array_like Element displacement vector [u1, u2].

Returns

es : float Element force.

Examples

spring1s(100, [0.1, 0.2]) 10.0

History

LAST MODIFIED: P-E Austrell 1994-11-02 O Dahlblom 2022-11-14 (Python version)

Source code in src/calfem/core.py
def spring1s(ep: ArrayLike, ed: ArrayLike) -> float:
    """
    Compute the element force in a spring element.

    Parameters
    ----------
    ep : array_like
        Spring stiffness or analog quantity [k].
    ed : array_like
        Element displacement vector [u1, u2].

    Returns
    -------
    es : float
        Element force.

    Examples
    --------
    >>> spring1s(100, [0.1, 0.2])
    10.0

    History
    -------
    LAST MODIFIED: P-E Austrell 1994-11-02
                   O Dahlblom   2022-11-14 (Python version)
    """
    k = ep

    N = k*(ed[1]-ed[0])
    es = N

    return es

spsolveq(K, f, bcPrescr, bcVal=None)

Solve static FE-equations considering boundary conditions.

Parameters

K : array_like Global stiffness matrix, shape (nd, nd). f : array_like Global load vector, shape (nd, 1). bcPrescr : array_like 1-dim integer array containing prescribed dofs. bcVal : array_like, optional 1-dim float array containing prescribed values. If not given all prescribed dofs are assumed 0.

Returns

a : ndarray Solution including boundary values, shape (nd, 1). Q : ndarray Reaction force vector, shape (nd, 1).

Source code in src/calfem/core.py
def spsolveq(K, f, bcPrescr, bcVal=None):
    """
    Solve static FE-equations considering boundary conditions.

    Parameters
    ----------
    K : array_like
        Global stiffness matrix, shape (nd, nd).
    f : array_like
        Global load vector, shape (nd, 1).
    bcPrescr : array_like
        1-dim integer array containing prescribed dofs.
    bcVal : array_like, optional
        1-dim float array containing prescribed values.
        If not given all prescribed dofs are assumed 0.

    Returns
    -------
    a : ndarray
        Solution including boundary values, shape (nd, 1).
    Q : ndarray
        Reaction force vector, shape (nd, 1).
    """

    nDofs = K.shape[0]
    nPdofs = bcPrescr.shape[0]

    if bcVal is None:
        bcVal = np.zeros([nPdofs], 'd')

    bc = np.ones(nDofs, 'bool')
    bcDofs = np.arange(nDofs)

    bc[np.ix_(bcPrescr-1)] = False
    bcDofs = bcDofs[bc]

    bcVal_m = np.array(bcVal).reshape(-1, 1)

    info("Preparing system matrix...")

    mask = np.ones(K.shape[0], dtype=bool)
    mask[bcDofs] = False

    info("step 1... converting K->CSR")
    Kcsr = K.asformat("csr")
    info("step 2... Kt")
    #Kt1 = K[bcDofs]
    #Kt = Kt1[:,bcPrescr]
    Kt = K[np.ix_((bcDofs), (bcPrescr-1))]
    info("step 3... fsys")
    fsys = f[bcDofs] - Kt @ bcVal_m
    info("step 4... Ksys")
    Ksys1 = Kcsr[bcDofs]
    Ksys = Ksys1[:, bcDofs]
    #Ksys = Kcsr[np.ix_((bcDofs),(bcDofs))]
    info("done...")

    info("Solving system...")
    asys = dsolve.spsolve(Ksys, fsys)

    info("Reconstructing full a...")
    a = np.zeros([nDofs, 1])
    a[np.ix_(bcPrescr-1)] = bcVal_m
    # asys is 1D, ensure shape (n,1) for assignment
    a[np.ix_(bcDofs)] = np.array(asys).reshape(-1, 1)

    Q = K @ a - f
    info("done...")
    return (a, Q)

statcon(K, f, cd)

Condensation of static FE-equations according to the vector cd.

Parameters

K : array_like Global stiffness matrix, shape (nd, nd). f : array_like Global load vector, shape (nd, 1). cd : array_like Vector containing dof's to be eliminated, shape (nc, 1), where nc is number of condensed dof's.

Returns

K1 : ndarray Condensed stiffness matrix, shape (nd-nc, nd-nc). f1 : ndarray Condensed load vector, shape (nd-nc, 1).

Source code in src/calfem/core.py
def statcon(K, f, cd):
    """
    Condensation of static FE-equations according to the vector cd.

    Parameters
    ----------
    K : array_like
        Global stiffness matrix, shape (nd, nd).
    f : array_like
        Global load vector, shape (nd, 1).
    cd : array_like
        Vector containing dof's to be eliminated, shape (nc, 1), where nc is number of condensed dof's.

    Returns
    -------
    K1 : ndarray
        Condensed stiffness matrix, shape (nd-nc, nd-nc).
    f1 : ndarray
        Condensed load vector, shape (nd-nc, 1).
    """
    nd = K.shape[0]

    # Ensure cd is properly shaped (flatten to 1D array)
    cd = np.asarray(cd).flatten()
    # Adjust for 0-based indexing
    cd = (cd-1).astype(int)

    # Create indices for free (a) and constrained (b) dofs
    aindx = np.setdiff1d(np.arange(nd), cd)
    bindx = cd

    # Extract submatrices
    Kaa = K[np.ix_(aindx, aindx)]
    Kab = K[np.ix_(aindx, bindx)]
    Kbb = K[np.ix_(bindx, bindx)]

    # Extract portions of force vector
    f = np.asarray(f)
    if f.ndim > 1 and f.shape[1] > 1:
        # Handle matrix-like f
        f = f.reshape(-1, 1)

    fa = f[aindx]
    fb = f[bindx]

    # Calculate the condensed matrices
    Kbb_inv = np.linalg.inv(Kbb)
    K1 = Kaa - Kab @ Kbb_inv @ Kab.T
    f1 = fa - Kab @ Kbb_inv @ fb

    return K1, f1

step1(K, C, f, a0, bc, ip, times, dofs)

Algorithm for dynamic solution of first-order FE equations considering boundary conditions.

Parameters

K : array_like Conductivity matrix, shape (ndof, ndof). C : array_like Capacity matrix, shape (ndof, ndof). f : array_like Load vector, shape (ndof, nstep + 1). If shape (ndof, 1), the values are kept constant during time integration. a0 : array_like Initial vector a(0), shape (ndof, 1). bc : array_like Boundary condition matrix, shape (nbc, nstep + 2). where nbc = number of prescribed degrees of freedom (either constant or time-dependent). The first column contains the numbers of the prescribed degrees of freedom and the subsequent columns contain the time history. If shape (nbc, 2), the values from the second column are kept constant during time integration. ip : array_like Array [dt, tottime, alpha], where dt is the size of the time increment, tottime is the total time, alpha is time integration constant. Frequently used values of alpha are: alpha=0: forward difference; forward Euler, alpha=1/2: trapezoidal rule; Crank-Nicholson alpha=1: backward difference; backward Euler times : array_like Array [t(i) ...] of times at which output should be written to a and da. dofs : array_like Array [dof(i) ...] of degree of freedom numbers for which history output should be written to ahist and dahist.

Returns

modelhist : dict Dictionary containing solution history for the whole model with keys:

- 'a' : ndarray
    Values of a at all timesteps, alternatively at times specified in 'times',
    shape (ndof, nstep + 1) or (ndof, ntimes).
- 'da' : ndarray
    Values of da at all timesteps, alternatively at times specified in 'times',
    shape (ndof, nstep + 1) or (ndof, ntimes).

dofhist : dict Dictionary containing solution history for the degrees of freedom selected in 'dofs' with keys:

- 'a' : ndarray
    Time history of a at the dofs specified in 'dofs',
    shape (ndof, nstep + 1).
- 'da' : ndarray
    Time history of da at the dofs specified in 'dofs',
    shape (ndof, nstep + 1).
Source code in src/calfem/core.py
def step1(K,C,f,a0,bc,ip,times,dofs):
    """
    Algorithm for dynamic solution of first-order FE equations considering boundary conditions.

    Parameters
    ----------
    K : array_like
        Conductivity matrix, shape (ndof, ndof).
    C : array_like
        Capacity matrix, shape (ndof, ndof).
    f : array_like
        Load vector, shape (ndof, nstep + 1).
        If shape (ndof, 1), the values are kept constant during time integration.
    a0 : array_like
        Initial vector a(0), shape (ndof, 1).
    bc : array_like
        Boundary condition matrix, shape (nbc, nstep + 2).
        where nbc = number of prescribed degrees of freedom (either constant or time-dependent).
        The first column contains the numbers of the prescribed degrees of freedom
        and the subsequent columns contain the time history.
        If shape (nbc, 2), the values from the second column are kept constant
        during time integration.
    ip : array_like
        Array [dt, tottime, alpha], where
        dt is the size of the time increment,
        tottime is the total time,
        alpha is time integration constant.
        Frequently used values of alpha are:
        alpha=0: forward difference; forward Euler,
        alpha=1/2: trapezoidal rule; Crank-Nicholson
        alpha=1: backward difference; backward Euler
    times : array_like
        Array [t(i) ...] of times at which output should be written to a and da.
    dofs : array_like
        Array [dof(i) ...] of degree of freedom numbers for which history output
        should be written to ahist and dahist.

    Returns
    -------
    modelhist : dict
        Dictionary containing solution history for the whole model with keys:

        - 'a' : ndarray
            Values of a at all timesteps, alternatively at times specified in 'times',
            shape (ndof, nstep + 1) or (ndof, ntimes).
        - 'da' : ndarray
            Values of da at all timesteps, alternatively at times specified in 'times',
            shape (ndof, nstep + 1) or (ndof, ntimes).
    dofhist : dict
        Dictionary containing solution history for the degrees of freedom selected in 'dofs' with keys:

        - 'a' : ndarray
            Time history of a at the dofs specified in 'dofs',
            shape (ndof, nstep + 1).
        - 'da' : ndarray
            Time history of da at the dofs specified in 'dofs',
            shape (ndof, nstep + 1).
    """
    ndof, _ = K.shape
    dt, tottime, alpha = ip
    a1 = (1-alpha)*dt
    a2 = alpha*dt

    nstep = 1
    if np.array(f).any():
        _, ncf = f.shape
        if ncf>1:
            nstep = ncf-1

    if np.array(bc).any():
        _, ncb = bc.shape
        if ncb>2:
            nstep = ncb-2
        bound = 1
    if not np.array(bc).any():
        bound = 0

    ns = int(tottime/dt)
    if (ns < nstep or nstep==1):
        nstep=ns

    tf = np.zeros((ndof,nstep+1))
    if np.array(f).any():
        if ncf==1:
            tf = f[:,0].reshape(-1,1)@np.ones((1,nstep+1))
        if ncf>1:
            tf = np.copy(f)

    modelhist = {}
    sa=0
    if not np.array(times).any():
        ntimes=0
        sa=1
        modelhist['a'] = np.zeros((ndof,nstep+1))
        modelhist['da'] = np.zeros((ndof,nstep+1))
    else:
        ntimes = len(times)
        if ntimes:
            sa=2
            modelhist['a'] = np.zeros((ndof,ntimes))
            modelhist['da'] = np.zeros((ndof,ntimes))

    dofhist = {}
    if np.array(dofs).all():
        ndofs = len(dofs)
        if ndofs:
            dofhist['a'] = np.zeros((ndofs,nstep+1))
            dofhist['da'] = np.zeros((ndofs,nstep+1))
    else:
        ndofs=0

    itime = 0

    # Calculate initial second time derivative d2a0
    da0 = np.linalg.solve(C,tf[:,0].reshape(-1,1) - K@a0)
    # Save initial values
    if sa==1:
        modelhist['a'][:,0] = a0.ravel()
        modelhist['da'][:,0] = da0.ravel()
    elif sa==2:
        if times[itime]==0:
            modelhist['a'][:,itime] = a0.ravel()
            modelhist['da'][:,itime] = da0.ravel()
            itime += 1

    if ndofs:
        dofhist['a'][:,0] = a0[np.ix_(dofs-1)].ravel()
        dofhist['da'][:,0] = da0[np.ix_(dofs-1)].ravel()

    # Reduce matrices due to bcs
    tempa = np.zeros((ndof,1))
    tempda = np.zeros((ndof,1))
    fdof=np.arange(1,ndof+1).astype(int)
    if bound:
        nrb, ncb = bc.shape
        if ncb==2:
            pa = bc[:,1].reshape(-1,1)@np.ones((1,nstep+1))
            pda = np.zeros((nrb,nstep+1))
        elif ncb>2:
            pa = np.copy(bc[:,1:])
            pda1 = (pa[:,1]-pa[:,0])/dt
            pdarest = (pa[:,1:] - pa[:,0:-1])/dt
            pda = np.hstack((pda1.reshape(-1,1),pdarest))
        pdof = np.copy(bc[:,0]).astype(int)
        fdof = np.setdiff1d(fdof,pdof).astype(int) - 1
        pdof -= 1 #adjusting for indexing starting from 0
        Keff = C[np.ix_(fdof,fdof)] + a2*K[np.ix_(fdof,fdof)]
    else:
        fdof -= 1 #adjusting for indexing starting from 0
        Keff = C + a2*K

    L, U = lu(Keff,permute_l=True)
    anew = a0[np.ix_(fdof)]
    danew = da0[np.ix_(fdof)]

    # Iterate over time steps
    for j in range(1,nstep+1):
        time = dt*j
        aold = np.copy(anew)
        daold = np.copy(danew)
        apred = aold + a1*daold
        if not bound:
            reff = tf[:,j].reshape(-1,1) - K@apred
        else:
            pdeff = C[np.ix_(fdof,pdof)]@pda[:,j].reshape(-1,1) + K[np.ix_(fdof,pdof)]@pa[:,j].reshape(-1,1)
            reff = tf[np.ix_(fdof),j].reshape(-1,1) - K[np.ix_(fdof,fdof)]@apred - pdeff
        y = np.linalg.solve(L,reff)
        danew = np.linalg.solve(U,y)
        anew = apred + a2*danew
        # Save to modelhist and dofhist
        if bound:
            tempa[np.ix_(pdof)] = pa[:,j].reshape(-1,1)
            tempda[np.ix_(pdof)] = pda[:,j].reshape(-1,1)
        tempa[np.ix_(fdof)] = anew
        tempda[np.ix_(fdof)] = danew
        if sa==1:
            modelhist['a'][:,j] = tempa.ravel()
            modelhist['da'][:,j] = tempda.ravel()
        elif sa==2:
            if ntimes and itime < ntimes:
                if time >= times[itime]:
                    modelhist['a'][:,itime] = tempa.ravel()
                    modelhist['da'][:,itime] = tempda.ravel()
                    itime += 1
            if ndofs:
                dofhist['a'][:,j] = tempa[np.ix_(dofs-1)].ravel()
                dofhist['da'][:,j] = tempda[np.ix_(dofs-1)].ravel()

    return modelhist, dofhist

step2(K, C, M, f, a0, da0, bc, ip, times, dofs)

Algorithm for dynamic solution of second-order FE equations considering boundary conditions.

Parameters

K : array_like Global stiffness matrix, shape (ndof, ndof). C : array_like Global damping matrix, shape (ndof, ndof). If there is no damping in the system, simply set C=[]. M : array_like Global mass matrix, shape (ndof, ndof). f : array_like Global load vector, shape (ndof, nstep + 1). If shape (ndof, 1), the values are kept constant during time integration. a0 : array_like Initial displacement vector a(0), shape (ndof, 1). da0 : array_like Initial velocity vector v(0), shape (ndof, 1). bc : array_like Boundary condition matrix, shape (nbc, nstep + 2). where nbc = number of prescribed degrees of freedom (either constant or time-dependent). The first column contains the numbers of the prescribed degrees of freedom and the subsequent columns contain the time history. If shape (nbc, 2), the values from the second column are kept constant during time integration. ip : array_like Array [dt, tottime, alpha, delta], where dt is the size of the time increment, tottime is the total time, alpha and delta are time integration constants for the Newmark family of methods. Frequently used values of alpha and delta are: alpha=1/4, delta=1/2: average acceleration (trapezoidal) rule, alpha=1/6, delta=1/2: linear acceleration, alpha=0, delta=1/2: central difference. times : array_like Array [t(i) ...] of times at which output should be written to a, da and d2a. dofs : array_like Array [dof(i) ...] of degree of freedom numbers for which history output should be written to ahist, dahist and d2ahist.

Returns

modelhist : dict Dictionary containing solution history for the whole model with keys:

- 'a' : ndarray
    Displacement values at all timesteps, alternatively at times specified in 'times',
    shape (ndof, nstep + 1) or (ndof, ntimes).
- 'da' : ndarray
    Velocity values at all timesteps, alternatively at times specified in 'times',
    shape (ndof, nstep + 1) or (ndof, ntimes).
- 'd2a' : ndarray
    Acceleration values at all timesteps, alternatively at times specified in 'times',
    shape (ndof, nstep + 1) or (ndof, ntimes).

dofhist : dict Dictionary containing solution history for the degrees of freedom selected in 'dofs' with keys:

- 'a' : ndarray
    Displacement time history at the dofs specified in 'dofs',
    shape (ndof, nstep + 1).
- 'da' : ndarray
    Velocity time history at the dofs specified in 'dofs',
    shape (ndof, nstep + 1).
- 'd2a' : ndarray
    Acceleration time history at the dofs specified in 'dofs',
    shape (ndof, nstep + 1).
Source code in src/calfem/core.py
def step2(K,C,M,f,a0,da0,bc,ip,times,dofs):
    """
    Algorithm for dynamic solution of second-order FE equations considering boundary conditions.

    Parameters
    ----------
    K : array_like
        Global stiffness matrix, shape (ndof, ndof).
    C : array_like
        Global damping matrix, shape (ndof, ndof).
        If there is no damping in the system, simply set C=[].
    M : array_like
        Global mass matrix, shape (ndof, ndof).
    f : array_like
        Global load vector, shape (ndof, nstep + 1).
        If shape (ndof, 1), the values are kept constant during time integration.
    a0 : array_like
        Initial displacement vector a(0), shape (ndof, 1).
    da0 : array_like
        Initial velocity vector v(0), shape (ndof, 1).
    bc : array_like
        Boundary condition matrix, shape (nbc, nstep + 2).
        where nbc = number of prescribed degrees of freedom (either constant or time-dependent).
        The first column contains the numbers of the prescribed degrees of freedom
        and the subsequent columns contain the time history.
        If shape (nbc, 2), the values from the second column are kept constant
        during time integration.
    ip : array_like
        Array [dt, tottime, alpha, delta], where
        dt is the size of the time increment,
        tottime is the total time,
        alpha and delta are time integration constants for the Newmark family of methods.
        Frequently used values of alpha and delta are:
        alpha=1/4, delta=1/2: average acceleration (trapezoidal) rule,
        alpha=1/6, delta=1/2: linear acceleration,
        alpha=0, delta=1/2: central difference.
    times : array_like
        Array [t(i) ...] of times at which output should be written to a, da and d2a.
    dofs : array_like
        Array [dof(i) ...] of degree of freedom numbers for which history output
        should be written to ahist, dahist and d2ahist.

    Returns
    -------
    modelhist : dict
        Dictionary containing solution history for the whole model with keys:

        - 'a' : ndarray
            Displacement values at all timesteps, alternatively at times specified in 'times',
            shape (ndof, nstep + 1) or (ndof, ntimes).
        - 'da' : ndarray
            Velocity values at all timesteps, alternatively at times specified in 'times',
            shape (ndof, nstep + 1) or (ndof, ntimes).
        - 'd2a' : ndarray
            Acceleration values at all timesteps, alternatively at times specified in 'times',
            shape (ndof, nstep + 1) or (ndof, ntimes).
    dofhist : dict
        Dictionary containing solution history for the degrees of freedom selected in 'dofs' with keys:

        - 'a' : ndarray
            Displacement time history at the dofs specified in 'dofs',
            shape (ndof, nstep + 1).
        - 'da' : ndarray
            Velocity time history at the dofs specified in 'dofs',
            shape (ndof, nstep + 1).
        - 'd2a' : ndarray
            Acceleration time history at the dofs specified in 'dofs',
            shape (ndof, nstep + 1).
    """
    ndof, _ = K.shape
    if not np.array(C).any():
        C = np.zeros((ndof,ndof))
    dt, tottime, alpha, delta = ip
    b1 = dt*dt*0.5*(1-2*alpha)
    b2 = (1-delta)*dt
    b3 = delta*dt
    b4 = alpha*dt*dt

    nstep = 1
    if np.array(f).any():
        _, ncf = f.shape
        if ncf>1:
            nstep = ncf-1

    if np.array(bc).any():
        _, ncb = bc.shape
        if ncb>2:
            nstep = ncb-2
        bound = 1
    if not np.array(bc).any():
        bound = 0

    ns = int(tottime/dt)
    if (ns < nstep or nstep==1):
        nstep=ns

    tf = np.zeros((ndof,nstep+1))
    if np.array(f).any():
        if ncf==1:
            tf = f[:,0].reshape(-1,1)@np.ones((1,nstep+1))
        if ncf>1:
            tf = np.copy(f)

    modelhist = {}
    sa=0
    if not np.array(times).any():
        ntimes=0
        sa=1
        modelhist['a'] = np.zeros((ndof,nstep+1))
        modelhist['da'] = np.zeros((ndof,nstep+1))
        modelhist['d2a'] = np.zeros((ndof,nstep+1))
    else:
        ntimes = len(times)
        if ntimes:
            sa=2
            modelhist['a'] = np.zeros((ndof,ntimes))
            modelhist['da'] = np.zeros((ndof,ntimes))
            modelhist['d2a'] = np.zeros((ndof,ntimes))

    dofhist = {}
    if np.array(dofs).all():
        ndofs = len(dofs)
        if ndofs:
            dofhist['a'] = np.zeros((ndofs,nstep+1))
            dofhist['da'] = np.zeros((ndofs,nstep+1))
            dofhist['d2a'] = np.zeros((ndofs,nstep+1))
    else:
        ndofs=0

    itime = 0

    # Calculate initial second time derivative d2a0
    d2a0 = np.linalg.solve(M,tf[:,0].reshape(-1,1) - C@da0 - K@a0)
    # Save initial values
    if sa==1:
        modelhist['a'][:,0] = a0.ravel()
        modelhist['da'][:,0] = da0.ravel()
        modelhist['d2a'][:,0] = d2a0.ravel()
    elif sa==2:
        if times[itime]==0:
            modelhist['a'][:,itime] = a0.ravel()
            modelhist['da'][:,itime] = da0.ravel()
            modelhist['d2a'][:,itime] = d2a0.ravel()
            itime += 1

    if ndofs:
        dofhist['a'][:,0] = a0[np.ix_(dofs-1)].ravel()
        dofhist['da'][:,0] = da0[np.ix_(dofs-1)].ravel()
        dofhist['d2a'][:,0] = d2a0[np.ix_(dofs-1)].ravel()

    # Reduce matrices due to bcs
    tempa = np.zeros((ndof,1))
    tempda = np.zeros((ndof,1))
    tempd2a = np.zeros((ndof,1))
    fdof=np.arange(1,ndof+1).astype(int)
    if bound:
        nrb, ncb = bc.shape
        if ncb==2:
            pa = bc[:,1].reshape(-1,1)@np.ones((1,nstep+1))
            pda = np.zeros((nrb,nstep+1))
        elif ncb>2:
            pa = np.copy(bc[:,1:])
            pda1 = (pa[:,1]-pa[:,0])/dt
            pdarest = (pa[:,1:] - pa[:,0:-1])/dt
            pda = np.hstack((pda1.reshape(-1,1),pdarest))
        pdof = np.copy(bc[:,0]).astype(int)
        fdof = np.setdiff1d(fdof,pdof).astype(int) - 1
        pdof -= 1 #adjusting for indexing starting from 0
        Keff = M[np.ix_(fdof,fdof)] + b3*C[np.ix_(fdof,fdof)] +b4*K[np.ix_(fdof,fdof)]
    else:
        fdof -= 1 #adjusting for indexing starting from 0
        Keff = M + b3*C + b4*K

    L, U = lu(Keff,permute_l=True)
    anew = a0[np.ix_(fdof)]
    danew = da0[np.ix_(fdof)]
    d2anew = d2a0[np.ix_(fdof)]

    # Iterate over time steps
    for j in range(1,nstep+1):
        time = dt*j
        aold = np.copy(anew)
        daold = np.copy(danew)
        d2aold = np.copy(d2anew)
        apred = aold + dt*daold + b1*d2aold
        dapred = daold + b2*d2aold
        if not bound:
            reff = tf[:,j].reshape(-1,1) - C@dapred - K@apred
        else:
            pdeff = C[np.ix_(fdof,pdof)]@pda[:,j].reshape(-1,1) + K[np.ix_(fdof,pdof)]@pa[:,j].reshape(-1,1)
            reff = tf[np.ix_(fdof),j].reshape(-1,1) - C[np.ix_(fdof,fdof)]@dapred - K[np.ix_(fdof,fdof)]@apred - pdeff
        y = np.linalg.solve(L,reff)
        d2anew = np.linalg.solve(U,y)
        anew = apred + b4*d2anew
        danew = dapred + b3*d2anew
        # Save to modelhist and dofhist
        if bound:
            tempa[np.ix_(pdof)] = pa[:,j].reshape(-1,1)
            tempda[np.ix_(pdof)] = pda[:,j].reshape(-1,1)
        tempa[np.ix_(fdof)] = anew
        tempda[np.ix_(fdof)] = danew
        tempd2a[np.ix_(fdof)] = d2anew
        if sa==1:
            modelhist['a'][:,j] = tempa.ravel()
            modelhist['da'][:,j] = tempda.ravel()
            modelhist['d2a'][:,j] = tempd2a.ravel()
        elif sa==2:
            if ntimes and itime < ntimes:
                if time >= times[itime]:
                    modelhist['a'][:,itime] = tempa.ravel()
                    modelhist['da'][:,itime] = tempda.ravel()
                    modelhist['d2a'][:,itime] = tempd2a.ravel()
                    itime += 1
            if ndofs:
                dofhist['a'][:,j] = tempa[np.ix_(dofs-1)].ravel()
                dofhist['da'][:,j] = tempda[np.ix_(dofs-1)].ravel()
                dofhist['d2a'][:,j] = tempd2a[np.ix_(dofs-1)].ravel()

    return modelhist, dofhist

stress2nodal(eseff, edof)

Convert element effective stresses to nodal effective stresses.

Parameters:

eseff  = [eseff_0 .. eseff_nel-1] 
edof   = [dof topology array]

Returns:

ev:     element value array [[ev_0_0 ev_0_1 ev_0_nen-1 ]
                              ..
                              ev_nel-1_0 ev_nel-1_1 ev_nel-1_nen-1]
Source code in src/calfem/core.py
def stress2nodal(eseff, edof):
    """
    Convert element effective stresses to nodal effective
    stresses.

    Parameters:

        eseff  = [eseff_0 .. eseff_nel-1] 
        edof   = [dof topology array]

    Returns:

        ev:     element value array [[ev_0_0 ev_0_1 ev_0_nen-1 ]
                                      ..
                                      ev_nel-1_0 ev_nel-1_1 ev_nel-1_nen-1]

    """

    values = np.zeros(edof.max())
    elnodes = int(np.size(edof, 1) / 2)

    for etopo, eleseff in zip(edof, eseff):
        values[etopo-1] = values[etopo-1] + eleseff / elnodes

    evtemp = extractEldisp(edof, values)
    ev = evtemp[:, range(0, elnodes*2, 2)]

    return ev

user_warning(msg)

Print a user warning message.

Parameters

str

Warning message to display.

Source code in src/calfem/core.py
def user_warning(msg: str) -> None:
    """
    Print a user warning message.

    Parameters
    ----------

    msg : str
        Warning message to display.
    """
    fname = sys._getframe(1).f_code.co_name
    print("Warning: %s (%s)" % (msg, fname))

Geometry functions

CALFEM Geometry module

Contains functions and classes for describing geometry.

Geometry

Instances of GeoData can hold geometric data and be passed to GmshMesher in pycalfem_Mesh to mesh the geometry.

Source code in src/calfem/geometry.py
 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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
class Geometry:
    """
    Instances of GeoData can hold geometric data and be passed to 
    GmshMesher in pycalfem_Mesh to mesh the geometry.
    """

    def __init__(self):
        self.points = {}  # dict of [[x, y, z], elSize, marker]
        # dict of [curvTypestring, [p1, p2, ... pn], marker, elementsOnCurve, distributionString, distributionVal]
        self.curves = {}
        # dict of [SurfaceTypeString, [c1, c2 ... cn], [[c1, c2 ... cm], ... [c1, ... ck]], ID, marker, is_structured]. c means curve-ID.
        self.surfaces = {}
        # dict of [[s1, s2 ..], [[s1,s2...],[s1,s2..],..], ID, marker, is_structured]
        self.volumes = {}
        # This is automatically set to True if a 3D point is added.
        self.is3D = False
        self._pointIDspecified = False
        self._nextPointID = 0
        self._curveIDspecified = False
        self._nextcurveID = 0
        self._surfaceIDspecified = False
        self._nextsurfaceID = 0
        self._volumeIDspecified = False
        self._nextvolumeID = 0

    def removePoint(self, ID):
        '''Removes the point with this ID'''
        self.points.pop(ID)

    def removeCurve(self, ID):
        '''Removes the curve with this ID'''
        self.curves.pop(ID)

    def removeSurface(self, ID):
        '''Removes the surface with this ID'''
        self.surfaces.pop(ID)

    def removeVolume(self, ID):
        '''Removes the volume with this ID'''
        self.volumes.pop(ID)

    def getPointCoords(self, IDs=None):
        '''
        Returns an N-by-3 list of point coordinates if the parameter is
        a list of IDs. If the parameter is just a single integer then 
        a single coordinate (simple 3-element list) is returned.
        If the parameter is undefined (or None) all point coords will be returned
        '''
        if IDs == None:
            return [p[0] for p in self.points.values()]
        try:
            pointCoords = [self.points[pID][0] for pID in IDs]
        except TypeError:  # IDs was not iterable. Probably just a single ID.
            pointCoords = self.points[IDs][0]
        return pointCoords

    def pointsOnCurves(self, IDs):
        '''
        Returns a list of all geometric points (not nodes) on the curves
        specified in IDs. IDs may be an integer or a list of integers.
        '''
        return self._subentitiesOnEntities(IDs, self.curves, 1)

    def stuffOnSurfaces(self, IDs):
        '''
        Returns lists of all geometric points and curves on the surfaces
        specified in IDs. IDs may be an integer or a list of integers
        '''
        curveSet = self._subentitiesOnEntities(
            IDs, self.surfaces, 1)  # Curves on the outer edges
        curveSet.update(self._subentityHolesOnEntities(
            IDs, self.surfaces, 2))  # Curves on the holes
        # Points on the curves of these surfaces.
        pointList = self.pointsOnCurves(curveSet)
        return pointList, list(curveSet)

    def stuffOnVolumes(self, IDs):
        '''
        Returns lists of all geometric points, curves, and surfaces on the volumes
        specified in IDs. IDs may be an integer or a list of integers
        '''
        surfaceSet = self._subentitiesOnEntities(IDs, self.surfaces, 0)
        surfaceSet.update(self._subentitiesOnEntities(IDs, self.surfaces, 1))
        pointList, curveList = self.stuffOnSurfaces(surfaceSet)
        return pointList, curveList, list(surfaceSet)

    def _subentitiesOnEntities(self, IDs, entityDict, index):
        '''
        Duplicate code. Gets the IDs of the subentities that
        make up an entity, i.e. the points that define a curve or
        the curves that define a surface. Note that only the outer
        subentities of surfaces and volumes can be extracted with
        this function. For holes use _subentityHolesOnEntities().
        '''
        theSet = set()
        try:
            for ID in IDs:
                theSet.update(entityDict[ID][index])
        except TypeError:  # IDs is not iterable, so it is probably a single ID
            theSet.update(entityDict[IDs][index])
        return theSet

    def _subentityHolesOnEntities(self, IDs, entityDict, index):
        '''Duplicate code. Does the same thing as _subentitiesOnEntities(), but for holes'''
        theSet = set()
        try:
            for ID in IDs:
                for hole in entityDict[ID][index]:
                    theSet.update(hole)
        except TypeError:  # IDs is not iterable, so it is probably a single ID
            for hole in entityDict[IDs][index]:
                theSet.update(hole)
        return theSet

    def points(self, points, markers=None, ids=None, elSizes=None):
        '''
        Add points from a numpy-array
        '''
        nPoints, dims = points.shape

        if dims == 2:
            for row in points:
                self.addPoint(row.tolist())
        elif dims == 3:
            for row in points:
                self.addPoint(row.tolist())

    def point(self, coord, ID=None, marker=0, el_size=1):
        """
        Adds a point.

        Parameters
        ----------
        coord : list
            [x, y] or [x, y, z]. List, not array.
        ID : int, optional
            Positive integer ID of this point. If left unspecified the
            point will be assigned the smallest unused point-ID.
            It is recommended to specify all point-IDs or none.
        marker : int, optional
            Marker applied to this point. Default 0.
            It is not a good idea to apply non-zero markers to points
            that are control points on B-splines or center points on 
            circles/ellipses, since this can lead to "loose" nodes
            that are not part of any elements.
        el_size : float, optional
            The size of elements at this point. Default 1. Use to make
            a mesh denser or sparser here. Only affects unstructured
            meshes.
        """
        if len(coord) == 3:  # A 3D point is inserted.
            self.is3D = True
        else:  # The point is in 2D (we assume)
            # Pad with a 0. (we store points as 3D points for consistency's sake)
            coord = coord+[0]

        if ID == None:  # ID is not specified. Get an ID for this point:
            ID = self._getNewPointID()
        else:
            self._pointIDspecified = True

        self.points[ID] = [coord, el_size, marker]

    def bounding_box_2d(self):
        """Calculate bounding box geometry"""

        min_x = 1e300
        max_x = -1e300
        min_y = 1e300
        max_y = -1e300

        for point_id in self.points.keys():
            p = self.points[point_id]
            if p[0][0] > max_x:
                max_x = p[0][0]
            if p[0][0] < min_x:
                min_x = p[0][0]
            if p[0][1] > max_y:
                max_y = p[0][1]
            if p[0][1] < min_y:
                min_y = p[0][1]

        return min_x, max_x, min_y, max_y

    def splines(self, points):
        '''
        Add splines from numpy array
        '''
        nPoints, dims = points.shape

        if dims == 2:
            for row in points:
                self.addSpline(row.tolist())
        elif dims == 3:
            for row in points:
                splineDef = row.tolist()
                self.addSpline(splineDef[:-1], marker=splineDef[2])

    def spline(self, points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None):
        """
        Adds a Spline curve

        Parameters
        ----------
        points : list
            List of indices of control points that make a Spline
            [p1, p2, ... , pn]
        ID : int, optional
            Positive integer ID of this curve. If left unspecified the
            curve will be assigned the smallest unused curve-ID.
            It is recommended to specify all curve-IDs or none.
        marker : int, optional
            Marker applied to this curve. Default 0.
        el_on_curve : int, optional
            Elements on curve. 
            The number of element edges that will be distributed
            along this curve. Only works for structured meshes.
        el_distrib_type : str, optional
            Either "bump" or "progression". 
            Determines how the density of elements vary along the curve
            for structured meshes. Only works for structured meshes.
            el_on_curve and el_distrib_val must be be defined if this param
            is used.
        el_distrib_val : float, optional
            Determines how severe the element distribution is.
            Only works for structured meshes. el_on_curve and 
            el_distrib_type must be be defined if this param is used.

            bump:
            Smaller value means elements are bunched up at the edges
            of the curve, larger means bunched in the middle.

            progression:
            The edge of each element along this curve (from starting
            point to end) will be larger than the preceding one by 
            this factor.
            el_distrib_val = 2 meaning for example that each line element 
            in the series will be twice as long as the preceding one.
            el_distrib_val < 1 makes each element smaller than the 
            preceeding one.
        """
        self._addCurve("Spline", points, ID, marker, el_on_curve,
                       el_distrib_type, el_distrib_val)

    def bspline(self, points, ID=None, marker=0, el_on_curve=None,  el_distrib_type=None, el_distrib_val=None):
        """
        Adds a B-Spline curve

        Parameters
        ----------
        points : list
            List of indices of control points that make a B-spline
            [p1, p2, ... , pn]
        ID : int, optional
            Positive integer ID of this curve. If left unspecified the
            curve will be assigned the smallest unused curve-ID.
            It is recommended to specify all curve-IDs or none.
        marker : int, optional
            Marker applied to this curve. Default 0.
        el_on_curve : int, optional
            Elements on curve. 
            The number of element edges that will be distributed
            along this curve. Only works for structured meshes.
        el_distrib_type : str, optional
            Either "bump" or "progression". 
            Determines how the density of elements vary along the curve
            for structured meshes. Only works for structured meshes.
            el_on_curve and el_distrib_val must be be defined if this param
            is used.
        el_distrib_val : float, optional
            Determines how severe the element distribution is.
            Only works for structured meshes. el_on_curve and 
            el_distrib_type must be be defined if this param is used.

            bump:
            Smaller value means elements are bunched up at the edges
            of the curve, larger means bunched in the middle.

            progression:
            The edge of each element along this curve (from starting
            point to end) will be larger than the preceding one by 
            this factor.
            el_distrib_val = 2 meaning for example that each line element 
            in the series will be twice as long as the preceding one.
            el_distrib_val < 1 makes each element smaller than the 
            preceeding one.
        """
        self._addCurve("BSpline", points, ID, marker,
                       el_on_curve, el_distrib_type, el_distrib_val)

    def circle(self, points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None):
        """
        Adds a Circle arc curve.

        Parameters
        ----------
        points : list
            List of 3 indices of point that make a circle arc smaller
            than Pi.
            [startpoint, centerpoint, endpoint]
        ID : int, optional
            Positive integer ID of this curve. If left unspecified the
            curve will be assigned the smallest unused curve-ID.
            It is recommended to specify all curve-IDs or none.
        marker : int, optional
            Marker applied to this curve. Default 0.
        el_on_curve : int, optional
            Elements on curve.
            The number of element edges that will be distributed
            along this curve. Only works for structured meshes.
        el_distrib_type : str, optional
            Either "bump" or "progression". 
            Determines how the density of elements vary along the curve
            for structured meshes. Only works for structured meshes.
            el_on_curve and el_distrib_val must be be defined if this param
            is used.
        el_distrib_val : float, optional
            Determines how severe the element distribution is.
            Only works for structured meshes. el_on_curve and 
            el_distrib_type must be be defined if this param is used.

            bump:
            Smaller value means elements are bunched up at the edges
            of the curve, larger means bunched in the middle.

            progression:
            The edge of each element along this curve (from starting
            point to end) will be larger than the preceding one by 
            this factor.
            el_distrib_val = 2 meaning for example that each line element 
            in the series will be twice as long as the preceding one.
            el_distrib_val < 1 makes each element smaller than the 
            preceeding one.
        """
        if len(points) != 3:
            raise IndexError(
                "Circle: points must be a list of 3 positive integers denoting point indices")
        self._addCurve("Circle", points, ID, marker, el_on_curve,
                       el_distrib_type, el_distrib_val)

    def ellipse(self, points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None):
        """
        Adds a Ellipse arc curve.

        Parameters
        ----------
        points : list
            List of 4 indices of point that make a ellipse arc smaller
            than Pi.
            [startpoint, centerpoint, mAxisPoint, endpoint]
            Startpoint is the starting point of the arc.
            Centerpoint is the point at the center of the ellipse.
            MAxisPoint is any point on the major axis of the ellipse.
            Endpoint is the end point of the arc.
        ID : int, optional
            Positive integer ID of this curve. If left unspecified the
            curve will be assigned the smallest unused curve-ID.
            It is recommended to specify all curve-IDs or none.
        marker : int, optional
            Marker applied to this curve. Default 0.
        el_on_curve : int, optional
            Elements on curve. 
            The number of element edges that will be distributed
            along this curve. Only works for structured meshes.
        el_distrib_type : str, optional
            Either "bump" or "progression". 
            Determines how the density of elements vary along the curve
            for structured meshes. Only works for structured meshes.
            el_on_curve and el_distrib_val must be be defined if this param
            is used.
        el_distrib_val : float, optional
            Determines how severe the element distribution is.
            Only works for structured meshes. el_on_curve and 
            el_distrib_type must be be defined if this param is used.

            bump:
            Smaller value means elements are bunched up at the edges
            of the curve, larger means bunched in the middle.

            progression:
            The edge of each element along this curve (from starting
            point to end) will be larger than the preceding one by 
            this factor.
            el_distrib_val = 2 meaning for example that each line element 
            in the series will be twice as long as the preceding one.
            el_distrib_val < 1 makes each element smaller than the 
            preceeding one.
        """
        if len(points) != 4:
            raise IndexError(
                "Ellipse: points must be a list of 4 positive integers denoting point indices")
        self._addCurve("Ellipse", points, ID, marker,
                       el_on_curve, el_distrib_type, el_distrib_val)

    def _addCurve(self, name, points, ID, marker, el_on_curve, el_distrib_type, el_distrib_val):
        '''Duplicate code goes here!'''
        if ID == None:
            ID = self._getNewCurveID()
        else:
            self._curveIDspecified = True

        if el_distrib_type != None:
            # transform into lowercase except the first letter upper case.
            el_distrib_type = el_distrib_type.lower().title()
            if el_distrib_type not in ["Bump", "Progression"]:
                raise ValueError(
                    "el_distrib_type must be a string, either \"bump\" or \"progression\". Curve with ID=%i was incorrect." % ID)
            if el_distrib_val == None:
                raise ValueError(
                    "If el_distrib_type is defined then el_distrib_val must be given a (float) value")

        self.curves[ID] = [name, points, marker,
                           el_on_curve, el_distrib_type, el_distrib_val]

    def surface(self, outer_loop, holes=[], ID=None, marker=0):
        """
        Adds a plane surface (flat).

        Parameters
        ----------
        outer_loop : list
            List of curve IDs that make up the outer boundary of
            the surface. The curves must lie in the same plane.
        holes : list, optional
            List of lists of curve IDs that make up the inner
            boundaries of the surface. The curves must lie in the
            same plane. Default [].
        ID : int, optional
            Positive integer ID of this surface. If left unspecified
            the surface will be assigned the smallest unused surface-ID.
            It is recommended to specify all surface-IDs or none.
        marker : int, optional
            Marker applied to this surface. Default 0.
        """
        # TODO: Possibly check if outer_loop is an actual loop and if the holes are correct.
        self._addSurf("Plane Surface", outer_loop, holes,
                      ID, marker, is_structured=False)

    def ruled_surface(self, outer_loop, ID=None, marker=0):
        """
        Adds a Ruled Surface (bent surface).

        Parameters
        ----------
        outer_loop : list
            List of 3 or 4 curve IDs that make up the boundary of
            the surface.
        ID : int, optional
            Positive integer ID of this surface. If left unspecified
            the surface will be assigned the smallest unused surface-ID.
            It is recommended to specify all surface-IDs or none.
        marker : int, optional
            Marker applied to this surface. Default 0.
        """
        if len(outer_loop) not in [3, 4]:
            raise IndexError(
                "Ruled Surface: outer_loop must be a list of 3 or 4 positive integers denoting curve indices")
        self._addSurf("Surface", outer_loop, [],
                      ID, marker, is_structured=False)

    def struct_surface(self, outer_loop, ID=None, marker=0):
        """
        Adds a Structured Surface.

        Parameters
        ----------
        outer_loop : list
            List of 4 curve IDs that make up the boundary of
            the surface. The curves must be structured, i.e. their
            parameter 'elOnCurv' must be defined.
        ID : int, optional
            Positive integer ID of this surface. If left unspecified
            the surface will be assigned the smallest unused surface-ID.
            It is recommended to specify all surface-IDs or none.
        marker : int, optional
            Marker applied to this surface. Default 0.
        """
        self._checkIfProperStructuredQuadBoundary(outer_loop, ID)
        self._addSurf("Surface", outer_loop, [],
                      ID, marker, is_structured=True)

    def _addSurf(self, name, outer_loop, holes, ID, marker, is_structured):
        '''For duplicate code'''
        # TODO: check if the curves in outer_loop actually exist. Maybe print a warning.
        if ID == None:
            ID = self._getNewSurfaceID()
        else:
            self._surfaceIDspecified = True

        # Catch the easy mistake of making holes a list of ints rather than a list of lists of ints.
        for hole in holes:
            try:
                [h for h in hole]
            except TypeError:
                raise TypeError(
                    "Hole " + str(hole) + " is not iterable. Parameter holes must be a list of lists of integers")

        self.surfaces[ID] = [name, outer_loop,
                             holes, ID, marker, is_structured]

    def volume(self, outer_surfaces, holes=[], ID=None, marker=0):
        """
        Adds a Volume

        Parameters
        ----------
        outer_surfaces : list
            List of surface IDs that make up the outer boundary of
            the volume.
        holes : list, optional
            List of lists of surface IDs that make up the inner
            boundaries of the volume. Default [].
        ID : int, optional
            Positive integer ID of this volume. If left unspecified
            the volume will be assigned the smallest unused volume-ID.
            It is recommended to specify all volume-IDs or none.
        marker : int, optional
            Marker applied to this volume. Default 0.
        """
        self._addVolume(outer_surfaces, holes, ID, marker, is_structured=False)

    def struct_volume(self, outer_surfaces, ID=None, marker=0):
        """
        Adds a Structured Volume

        Parameters
        ----------
        outer_surfaces : list
            List of surface IDs that make up the outer boundary of
            the volume. The surfaces must be Structured Surfaces.
        ID : int, optional
            Positive integer ID of this volume. If left unspecified
            the volume will be assigned the smallest unused volume-ID.
            It is recommended to specify all volume-IDs or none.
        marker : int, optional
            Marker applied to this volume. Default 0.
        """
        # TODO: Check input. (see if surfaces are structured)
        self._addVolume(outer_surfaces, [], ID, marker, is_structured=True)

    def _addVolume(self,  outer_surfaces, holes, ID, marker, is_structured):
        '''Duplicate code'''
        # TODO: Check input (outer_surfaces and holes[i] must be closed surfaces)
        if ID == None:
            ID = self._getNewVolumeID()
        else:
            self._volumeIDspecified = True
        self.volumes[ID] = [outer_surfaces, holes, ID, marker, is_structured]

    def pointMarker(self, ID, marker):
        self.setPointMarker(ID, marker)

    def setPointMarker(self, ID, marker):
        '''Sets the marker of the point with the ID'''
        self.points[ID][2] = marker

    def curveMarker(self, ID, marker):
        self.setCurveMarker(ID, marker)

    def setCurveMarker(self, ID, marker):
        '''Sets the marker of the curve with the ID'''
        self.curves[ID][2] = marker

    def surfaceMarker(self, ID, marker):
        self.setSurfaceMarker(ID, marker)

    def setSurfaceMarker(self, ID, marker):
        '''Sets the marker of the surface with the ID'''
        self.surfaces[ID][4] = marker

    def setVolumeMarker(self, ID, marker):
        '''Sets the marker of the volume with the ID'''
        self.volumes[ID][3] = marker

    def _checkIfProperStructuredQuadBoundary(self, outer_loop, ID):
        '''Checks if the four edges of a quad-shaped superelement exist and
        are correct, i.e el_on_curve of opposite curves are equal.'''
        if len(outer_loop) != 4:
            raise IndexError(
                "Structured Surface: outer_loop must be a list of 4 positive integers denoting curve indices")

        try:
            c0 = self.curves[outer_loop[0]]
            c1 = self.curves[outer_loop[1]]
            c2 = self.curves[outer_loop[2]]
            c3 = self.curves[outer_loop[3]]
        except KeyError:
            raise KeyError(
                "Structured Surface: Attempted construction of StructuredSurface with ID=%s from a curve that does not exist" % ID)

        if None in [c0, c1, c2, c3]:
            raise Exception(
                "Attempted to create structured surface from non-structured boundary curves.")

        # Check if the number of elements on opposite curves match.
        if(c0[-3] != c2[-3] or c1[-3] != c3[-3]):
            raise Exception("Structured Surface: The outer_loop of StructuredSurface %i is not properly " +
                            "constructed. The reason could be that the number of elements (elOnCurv) on " +
                            "opposite pairs of curves are different")

    def _getNewPointID(self):
        if not self._pointIDspecified:
            self._nextPointID += 1
            return self._nextPointID - 1
        else:
            return self._smallestFreeKey(self.points)

    def _getNewCurveID(self):
        if not self._curveIDspecified:
            self._nextcurveID += 1
            return self._nextcurveID - 1
        else:
            return self._smallestFreeKey(self.curves)

    def _getNewSurfaceID(self):
        if not self._surfaceIDspecified:
            self._nextsurfaceID += 1
            return self._nextsurfaceID - 1
        else:
            return self._smallestFreeKey(self.surfaces)

    def _getNewVolumeID(self):
        if not self._volumeIDspecified:
            self._nextvolumeID += 1
            return self._nextvolumeID - 1
        else:
            return self._smallestFreeKey(self.volumes)

    def _smallestFreeKey(self, dictionary):
        '''Finds the smallest unused key in the dict.'''
        sortedkeys = sorted(dictionary)
        for i in range(len(dictionary)):
            if sortedkeys[i] != i:
                return i

    addPoints = points
    addPoint = point
    addSpline = spline
    line = spline
    addBSpline = bspline
    addCircle = circle
    addEllipse = ellipse
    addSurface = surface
    addRuledSurface = ruled_surface
    ruledSurface = ruled_surface
    ruled_surf = ruled_surface
    structuredSurface = struct_surface
    structured_surface = struct_surface
    addStructuredSurface = struct_surface
    addVolume = volume
    addStructuredVolume = struct_volume
    structuredVolume = struct_volume
    structured_volume = struct_volume
    get_point_coords = getPointCoords
    curve_marker = curveMarker
    line_marker = curveMarker

bounding_box_2d()

Calculate bounding box geometry

Source code in src/calfem/geometry.py
def bounding_box_2d(self):
    """Calculate bounding box geometry"""

    min_x = 1e300
    max_x = -1e300
    min_y = 1e300
    max_y = -1e300

    for point_id in self.points.keys():
        p = self.points[point_id]
        if p[0][0] > max_x:
            max_x = p[0][0]
        if p[0][0] < min_x:
            min_x = p[0][0]
        if p[0][1] > max_y:
            max_y = p[0][1]
        if p[0][1] < min_y:
            min_y = p[0][1]

    return min_x, max_x, min_y, max_y

bspline(points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None)

Adds a B-Spline curve

Parameters

points : list List of indices of control points that make a B-spline [p1, p2, ... , pn] ID : int, optional Positive integer ID of this curve. If left unspecified the curve will be assigned the smallest unused curve-ID. It is recommended to specify all curve-IDs or none. marker : int, optional Marker applied to this curve. Default 0. el_on_curve : int, optional Elements on curve. The number of element edges that will be distributed along this curve. Only works for structured meshes. el_distrib_type : str, optional Either "bump" or "progression". Determines how the density of elements vary along the curve for structured meshes. Only works for structured meshes. el_on_curve and el_distrib_val must be be defined if this param is used. el_distrib_val : float, optional Determines how severe the element distribution is. Only works for structured meshes. el_on_curve and el_distrib_type must be be defined if this param is used.

bump:
Smaller value means elements are bunched up at the edges
of the curve, larger means bunched in the middle.

progression:
The edge of each element along this curve (from starting
point to end) will be larger than the preceding one by 
this factor.
el_distrib_val = 2 meaning for example that each line element 
in the series will be twice as long as the preceding one.
el_distrib_val < 1 makes each element smaller than the 
preceeding one.
Source code in src/calfem/geometry.py
def bspline(self, points, ID=None, marker=0, el_on_curve=None,  el_distrib_type=None, el_distrib_val=None):
    """
    Adds a B-Spline curve

    Parameters
    ----------
    points : list
        List of indices of control points that make a B-spline
        [p1, p2, ... , pn]
    ID : int, optional
        Positive integer ID of this curve. If left unspecified the
        curve will be assigned the smallest unused curve-ID.
        It is recommended to specify all curve-IDs or none.
    marker : int, optional
        Marker applied to this curve. Default 0.
    el_on_curve : int, optional
        Elements on curve. 
        The number of element edges that will be distributed
        along this curve. Only works for structured meshes.
    el_distrib_type : str, optional
        Either "bump" or "progression". 
        Determines how the density of elements vary along the curve
        for structured meshes. Only works for structured meshes.
        el_on_curve and el_distrib_val must be be defined if this param
        is used.
    el_distrib_val : float, optional
        Determines how severe the element distribution is.
        Only works for structured meshes. el_on_curve and 
        el_distrib_type must be be defined if this param is used.

        bump:
        Smaller value means elements are bunched up at the edges
        of the curve, larger means bunched in the middle.

        progression:
        The edge of each element along this curve (from starting
        point to end) will be larger than the preceding one by 
        this factor.
        el_distrib_val = 2 meaning for example that each line element 
        in the series will be twice as long as the preceding one.
        el_distrib_val < 1 makes each element smaller than the 
        preceeding one.
    """
    self._addCurve("BSpline", points, ID, marker,
                   el_on_curve, el_distrib_type, el_distrib_val)

circle(points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None)

Adds a Circle arc curve.

Parameters

points : list List of 3 indices of point that make a circle arc smaller than Pi. [startpoint, centerpoint, endpoint] ID : int, optional Positive integer ID of this curve. If left unspecified the curve will be assigned the smallest unused curve-ID. It is recommended to specify all curve-IDs or none. marker : int, optional Marker applied to this curve. Default 0. el_on_curve : int, optional Elements on curve. The number of element edges that will be distributed along this curve. Only works for structured meshes. el_distrib_type : str, optional Either "bump" or "progression". Determines how the density of elements vary along the curve for structured meshes. Only works for structured meshes. el_on_curve and el_distrib_val must be be defined if this param is used. el_distrib_val : float, optional Determines how severe the element distribution is. Only works for structured meshes. el_on_curve and el_distrib_type must be be defined if this param is used.

bump:
Smaller value means elements are bunched up at the edges
of the curve, larger means bunched in the middle.

progression:
The edge of each element along this curve (from starting
point to end) will be larger than the preceding one by 
this factor.
el_distrib_val = 2 meaning for example that each line element 
in the series will be twice as long as the preceding one.
el_distrib_val < 1 makes each element smaller than the 
preceeding one.
Source code in src/calfem/geometry.py
def circle(self, points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None):
    """
    Adds a Circle arc curve.

    Parameters
    ----------
    points : list
        List of 3 indices of point that make a circle arc smaller
        than Pi.
        [startpoint, centerpoint, endpoint]
    ID : int, optional
        Positive integer ID of this curve. If left unspecified the
        curve will be assigned the smallest unused curve-ID.
        It is recommended to specify all curve-IDs or none.
    marker : int, optional
        Marker applied to this curve. Default 0.
    el_on_curve : int, optional
        Elements on curve.
        The number of element edges that will be distributed
        along this curve. Only works for structured meshes.
    el_distrib_type : str, optional
        Either "bump" or "progression". 
        Determines how the density of elements vary along the curve
        for structured meshes. Only works for structured meshes.
        el_on_curve and el_distrib_val must be be defined if this param
        is used.
    el_distrib_val : float, optional
        Determines how severe the element distribution is.
        Only works for structured meshes. el_on_curve and 
        el_distrib_type must be be defined if this param is used.

        bump:
        Smaller value means elements are bunched up at the edges
        of the curve, larger means bunched in the middle.

        progression:
        The edge of each element along this curve (from starting
        point to end) will be larger than the preceding one by 
        this factor.
        el_distrib_val = 2 meaning for example that each line element 
        in the series will be twice as long as the preceding one.
        el_distrib_val < 1 makes each element smaller than the 
        preceeding one.
    """
    if len(points) != 3:
        raise IndexError(
            "Circle: points must be a list of 3 positive integers denoting point indices")
    self._addCurve("Circle", points, ID, marker, el_on_curve,
                   el_distrib_type, el_distrib_val)

ellipse(points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None)

Adds a Ellipse arc curve.

Parameters

points : list List of 4 indices of point that make a ellipse arc smaller than Pi. [startpoint, centerpoint, mAxisPoint, endpoint] Startpoint is the starting point of the arc. Centerpoint is the point at the center of the ellipse. MAxisPoint is any point on the major axis of the ellipse. Endpoint is the end point of the arc. ID : int, optional Positive integer ID of this curve. If left unspecified the curve will be assigned the smallest unused curve-ID. It is recommended to specify all curve-IDs or none. marker : int, optional Marker applied to this curve. Default 0. el_on_curve : int, optional Elements on curve. The number of element edges that will be distributed along this curve. Only works for structured meshes. el_distrib_type : str, optional Either "bump" or "progression". Determines how the density of elements vary along the curve for structured meshes. Only works for structured meshes. el_on_curve and el_distrib_val must be be defined if this param is used. el_distrib_val : float, optional Determines how severe the element distribution is. Only works for structured meshes. el_on_curve and el_distrib_type must be be defined if this param is used.

bump:
Smaller value means elements are bunched up at the edges
of the curve, larger means bunched in the middle.

progression:
The edge of each element along this curve (from starting
point to end) will be larger than the preceding one by 
this factor.
el_distrib_val = 2 meaning for example that each line element 
in the series will be twice as long as the preceding one.
el_distrib_val < 1 makes each element smaller than the 
preceeding one.
Source code in src/calfem/geometry.py
def ellipse(self, points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None):
    """
    Adds a Ellipse arc curve.

    Parameters
    ----------
    points : list
        List of 4 indices of point that make a ellipse arc smaller
        than Pi.
        [startpoint, centerpoint, mAxisPoint, endpoint]
        Startpoint is the starting point of the arc.
        Centerpoint is the point at the center of the ellipse.
        MAxisPoint is any point on the major axis of the ellipse.
        Endpoint is the end point of the arc.
    ID : int, optional
        Positive integer ID of this curve. If left unspecified the
        curve will be assigned the smallest unused curve-ID.
        It is recommended to specify all curve-IDs or none.
    marker : int, optional
        Marker applied to this curve. Default 0.
    el_on_curve : int, optional
        Elements on curve. 
        The number of element edges that will be distributed
        along this curve. Only works for structured meshes.
    el_distrib_type : str, optional
        Either "bump" or "progression". 
        Determines how the density of elements vary along the curve
        for structured meshes. Only works for structured meshes.
        el_on_curve and el_distrib_val must be be defined if this param
        is used.
    el_distrib_val : float, optional
        Determines how severe the element distribution is.
        Only works for structured meshes. el_on_curve and 
        el_distrib_type must be be defined if this param is used.

        bump:
        Smaller value means elements are bunched up at the edges
        of the curve, larger means bunched in the middle.

        progression:
        The edge of each element along this curve (from starting
        point to end) will be larger than the preceding one by 
        this factor.
        el_distrib_val = 2 meaning for example that each line element 
        in the series will be twice as long as the preceding one.
        el_distrib_val < 1 makes each element smaller than the 
        preceeding one.
    """
    if len(points) != 4:
        raise IndexError(
            "Ellipse: points must be a list of 4 positive integers denoting point indices")
    self._addCurve("Ellipse", points, ID, marker,
                   el_on_curve, el_distrib_type, el_distrib_val)

getPointCoords(IDs=None)

Returns an N-by-3 list of point coordinates if the parameter is a list of IDs. If the parameter is just a single integer then a single coordinate (simple 3-element list) is returned. If the parameter is undefined (or None) all point coords will be returned

Source code in src/calfem/geometry.py
def getPointCoords(self, IDs=None):
    '''
    Returns an N-by-3 list of point coordinates if the parameter is
    a list of IDs. If the parameter is just a single integer then 
    a single coordinate (simple 3-element list) is returned.
    If the parameter is undefined (or None) all point coords will be returned
    '''
    if IDs == None:
        return [p[0] for p in self.points.values()]
    try:
        pointCoords = [self.points[pID][0] for pID in IDs]
    except TypeError:  # IDs was not iterable. Probably just a single ID.
        pointCoords = self.points[IDs][0]
    return pointCoords

point(coord, ID=None, marker=0, el_size=1)

Adds a point.

Parameters

coord : list [x, y] or [x, y, z]. List, not array. ID : int, optional Positive integer ID of this point. If left unspecified the point will be assigned the smallest unused point-ID. It is recommended to specify all point-IDs or none. marker : int, optional Marker applied to this point. Default 0. It is not a good idea to apply non-zero markers to points that are control points on B-splines or center points on circles/ellipses, since this can lead to "loose" nodes that are not part of any elements. el_size : float, optional The size of elements at this point. Default 1. Use to make a mesh denser or sparser here. Only affects unstructured meshes.

Source code in src/calfem/geometry.py
def point(self, coord, ID=None, marker=0, el_size=1):
    """
    Adds a point.

    Parameters
    ----------
    coord : list
        [x, y] or [x, y, z]. List, not array.
    ID : int, optional
        Positive integer ID of this point. If left unspecified the
        point will be assigned the smallest unused point-ID.
        It is recommended to specify all point-IDs or none.
    marker : int, optional
        Marker applied to this point. Default 0.
        It is not a good idea to apply non-zero markers to points
        that are control points on B-splines or center points on 
        circles/ellipses, since this can lead to "loose" nodes
        that are not part of any elements.
    el_size : float, optional
        The size of elements at this point. Default 1. Use to make
        a mesh denser or sparser here. Only affects unstructured
        meshes.
    """
    if len(coord) == 3:  # A 3D point is inserted.
        self.is3D = True
    else:  # The point is in 2D (we assume)
        # Pad with a 0. (we store points as 3D points for consistency's sake)
        coord = coord+[0]

    if ID == None:  # ID is not specified. Get an ID for this point:
        ID = self._getNewPointID()
    else:
        self._pointIDspecified = True

    self.points[ID] = [coord, el_size, marker]

points(points, markers=None, ids=None, elSizes=None)

Add points from a numpy-array

Source code in src/calfem/geometry.py
def points(self, points, markers=None, ids=None, elSizes=None):
    '''
    Add points from a numpy-array
    '''
    nPoints, dims = points.shape

    if dims == 2:
        for row in points:
            self.addPoint(row.tolist())
    elif dims == 3:
        for row in points:
            self.addPoint(row.tolist())

pointsOnCurves(IDs)

Returns a list of all geometric points (not nodes) on the curves specified in IDs. IDs may be an integer or a list of integers.

Source code in src/calfem/geometry.py
def pointsOnCurves(self, IDs):
    '''
    Returns a list of all geometric points (not nodes) on the curves
    specified in IDs. IDs may be an integer or a list of integers.
    '''
    return self._subentitiesOnEntities(IDs, self.curves, 1)

removeCurve(ID)

Removes the curve with this ID

Source code in src/calfem/geometry.py
def removeCurve(self, ID):
    '''Removes the curve with this ID'''
    self.curves.pop(ID)

removePoint(ID)

Removes the point with this ID

Source code in src/calfem/geometry.py
def removePoint(self, ID):
    '''Removes the point with this ID'''
    self.points.pop(ID)

removeSurface(ID)

Removes the surface with this ID

Source code in src/calfem/geometry.py
def removeSurface(self, ID):
    '''Removes the surface with this ID'''
    self.surfaces.pop(ID)

removeVolume(ID)

Removes the volume with this ID

Source code in src/calfem/geometry.py
def removeVolume(self, ID):
    '''Removes the volume with this ID'''
    self.volumes.pop(ID)

ruled_surface(outer_loop, ID=None, marker=0)

Adds a Ruled Surface (bent surface).

Parameters

outer_loop : list List of 3 or 4 curve IDs that make up the boundary of the surface. ID : int, optional Positive integer ID of this surface. If left unspecified the surface will be assigned the smallest unused surface-ID. It is recommended to specify all surface-IDs or none. marker : int, optional Marker applied to this surface. Default 0.

Source code in src/calfem/geometry.py
def ruled_surface(self, outer_loop, ID=None, marker=0):
    """
    Adds a Ruled Surface (bent surface).

    Parameters
    ----------
    outer_loop : list
        List of 3 or 4 curve IDs that make up the boundary of
        the surface.
    ID : int, optional
        Positive integer ID of this surface. If left unspecified
        the surface will be assigned the smallest unused surface-ID.
        It is recommended to specify all surface-IDs or none.
    marker : int, optional
        Marker applied to this surface. Default 0.
    """
    if len(outer_loop) not in [3, 4]:
        raise IndexError(
            "Ruled Surface: outer_loop must be a list of 3 or 4 positive integers denoting curve indices")
    self._addSurf("Surface", outer_loop, [],
                  ID, marker, is_structured=False)

setCurveMarker(ID, marker)

Sets the marker of the curve with the ID

Source code in src/calfem/geometry.py
def setCurveMarker(self, ID, marker):
    '''Sets the marker of the curve with the ID'''
    self.curves[ID][2] = marker

setPointMarker(ID, marker)

Sets the marker of the point with the ID

Source code in src/calfem/geometry.py
def setPointMarker(self, ID, marker):
    '''Sets the marker of the point with the ID'''
    self.points[ID][2] = marker

setSurfaceMarker(ID, marker)

Sets the marker of the surface with the ID

Source code in src/calfem/geometry.py
def setSurfaceMarker(self, ID, marker):
    '''Sets the marker of the surface with the ID'''
    self.surfaces[ID][4] = marker

setVolumeMarker(ID, marker)

Sets the marker of the volume with the ID

Source code in src/calfem/geometry.py
def setVolumeMarker(self, ID, marker):
    '''Sets the marker of the volume with the ID'''
    self.volumes[ID][3] = marker

spline(points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None)

Adds a Spline curve

Parameters

points : list List of indices of control points that make a Spline [p1, p2, ... , pn] ID : int, optional Positive integer ID of this curve. If left unspecified the curve will be assigned the smallest unused curve-ID. It is recommended to specify all curve-IDs or none. marker : int, optional Marker applied to this curve. Default 0. el_on_curve : int, optional Elements on curve. The number of element edges that will be distributed along this curve. Only works for structured meshes. el_distrib_type : str, optional Either "bump" or "progression". Determines how the density of elements vary along the curve for structured meshes. Only works for structured meshes. el_on_curve and el_distrib_val must be be defined if this param is used. el_distrib_val : float, optional Determines how severe the element distribution is. Only works for structured meshes. el_on_curve and el_distrib_type must be be defined if this param is used.

bump:
Smaller value means elements are bunched up at the edges
of the curve, larger means bunched in the middle.

progression:
The edge of each element along this curve (from starting
point to end) will be larger than the preceding one by 
this factor.
el_distrib_val = 2 meaning for example that each line element 
in the series will be twice as long as the preceding one.
el_distrib_val < 1 makes each element smaller than the 
preceeding one.
Source code in src/calfem/geometry.py
def spline(self, points, ID=None, marker=0, el_on_curve=None, el_distrib_type=None, el_distrib_val=None):
    """
    Adds a Spline curve

    Parameters
    ----------
    points : list
        List of indices of control points that make a Spline
        [p1, p2, ... , pn]
    ID : int, optional
        Positive integer ID of this curve. If left unspecified the
        curve will be assigned the smallest unused curve-ID.
        It is recommended to specify all curve-IDs or none.
    marker : int, optional
        Marker applied to this curve. Default 0.
    el_on_curve : int, optional
        Elements on curve. 
        The number of element edges that will be distributed
        along this curve. Only works for structured meshes.
    el_distrib_type : str, optional
        Either "bump" or "progression". 
        Determines how the density of elements vary along the curve
        for structured meshes. Only works for structured meshes.
        el_on_curve and el_distrib_val must be be defined if this param
        is used.
    el_distrib_val : float, optional
        Determines how severe the element distribution is.
        Only works for structured meshes. el_on_curve and 
        el_distrib_type must be be defined if this param is used.

        bump:
        Smaller value means elements are bunched up at the edges
        of the curve, larger means bunched in the middle.

        progression:
        The edge of each element along this curve (from starting
        point to end) will be larger than the preceding one by 
        this factor.
        el_distrib_val = 2 meaning for example that each line element 
        in the series will be twice as long as the preceding one.
        el_distrib_val < 1 makes each element smaller than the 
        preceeding one.
    """
    self._addCurve("Spline", points, ID, marker, el_on_curve,
                   el_distrib_type, el_distrib_val)

splines(points)

Add splines from numpy array

Source code in src/calfem/geometry.py
def splines(self, points):
    '''
    Add splines from numpy array
    '''
    nPoints, dims = points.shape

    if dims == 2:
        for row in points:
            self.addSpline(row.tolist())
    elif dims == 3:
        for row in points:
            splineDef = row.tolist()
            self.addSpline(splineDef[:-1], marker=splineDef[2])

struct_surface(outer_loop, ID=None, marker=0)

Adds a Structured Surface.

Parameters

outer_loop : list List of 4 curve IDs that make up the boundary of the surface. The curves must be structured, i.e. their parameter 'elOnCurv' must be defined. ID : int, optional Positive integer ID of this surface. If left unspecified the surface will be assigned the smallest unused surface-ID. It is recommended to specify all surface-IDs or none. marker : int, optional Marker applied to this surface. Default 0.

Source code in src/calfem/geometry.py
def struct_surface(self, outer_loop, ID=None, marker=0):
    """
    Adds a Structured Surface.

    Parameters
    ----------
    outer_loop : list
        List of 4 curve IDs that make up the boundary of
        the surface. The curves must be structured, i.e. their
        parameter 'elOnCurv' must be defined.
    ID : int, optional
        Positive integer ID of this surface. If left unspecified
        the surface will be assigned the smallest unused surface-ID.
        It is recommended to specify all surface-IDs or none.
    marker : int, optional
        Marker applied to this surface. Default 0.
    """
    self._checkIfProperStructuredQuadBoundary(outer_loop, ID)
    self._addSurf("Surface", outer_loop, [],
                  ID, marker, is_structured=True)

struct_volume(outer_surfaces, ID=None, marker=0)

Adds a Structured Volume

Parameters

outer_surfaces : list List of surface IDs that make up the outer boundary of the volume. The surfaces must be Structured Surfaces. ID : int, optional Positive integer ID of this volume. If left unspecified the volume will be assigned the smallest unused volume-ID. It is recommended to specify all volume-IDs or none. marker : int, optional Marker applied to this volume. Default 0.

Source code in src/calfem/geometry.py
def struct_volume(self, outer_surfaces, ID=None, marker=0):
    """
    Adds a Structured Volume

    Parameters
    ----------
    outer_surfaces : list
        List of surface IDs that make up the outer boundary of
        the volume. The surfaces must be Structured Surfaces.
    ID : int, optional
        Positive integer ID of this volume. If left unspecified
        the volume will be assigned the smallest unused volume-ID.
        It is recommended to specify all volume-IDs or none.
    marker : int, optional
        Marker applied to this volume. Default 0.
    """
    # TODO: Check input. (see if surfaces are structured)
    self._addVolume(outer_surfaces, [], ID, marker, is_structured=True)

stuffOnSurfaces(IDs)

Returns lists of all geometric points and curves on the surfaces specified in IDs. IDs may be an integer or a list of integers

Source code in src/calfem/geometry.py
def stuffOnSurfaces(self, IDs):
    '''
    Returns lists of all geometric points and curves on the surfaces
    specified in IDs. IDs may be an integer or a list of integers
    '''
    curveSet = self._subentitiesOnEntities(
        IDs, self.surfaces, 1)  # Curves on the outer edges
    curveSet.update(self._subentityHolesOnEntities(
        IDs, self.surfaces, 2))  # Curves on the holes
    # Points on the curves of these surfaces.
    pointList = self.pointsOnCurves(curveSet)
    return pointList, list(curveSet)

stuffOnVolumes(IDs)

Returns lists of all geometric points, curves, and surfaces on the volumes specified in IDs. IDs may be an integer or a list of integers

Source code in src/calfem/geometry.py
def stuffOnVolumes(self, IDs):
    '''
    Returns lists of all geometric points, curves, and surfaces on the volumes
    specified in IDs. IDs may be an integer or a list of integers
    '''
    surfaceSet = self._subentitiesOnEntities(IDs, self.surfaces, 0)
    surfaceSet.update(self._subentitiesOnEntities(IDs, self.surfaces, 1))
    pointList, curveList = self.stuffOnSurfaces(surfaceSet)
    return pointList, curveList, list(surfaceSet)

surface(outer_loop, holes=[], ID=None, marker=0)

Adds a plane surface (flat).

Parameters

outer_loop : list List of curve IDs that make up the outer boundary of the surface. The curves must lie in the same plane. holes : list, optional List of lists of curve IDs that make up the inner boundaries of the surface. The curves must lie in the same plane. Default []. ID : int, optional Positive integer ID of this surface. If left unspecified the surface will be assigned the smallest unused surface-ID. It is recommended to specify all surface-IDs or none. marker : int, optional Marker applied to this surface. Default 0.

Source code in src/calfem/geometry.py
def surface(self, outer_loop, holes=[], ID=None, marker=0):
    """
    Adds a plane surface (flat).

    Parameters
    ----------
    outer_loop : list
        List of curve IDs that make up the outer boundary of
        the surface. The curves must lie in the same plane.
    holes : list, optional
        List of lists of curve IDs that make up the inner
        boundaries of the surface. The curves must lie in the
        same plane. Default [].
    ID : int, optional
        Positive integer ID of this surface. If left unspecified
        the surface will be assigned the smallest unused surface-ID.
        It is recommended to specify all surface-IDs or none.
    marker : int, optional
        Marker applied to this surface. Default 0.
    """
    # TODO: Possibly check if outer_loop is an actual loop and if the holes are correct.
    self._addSurf("Plane Surface", outer_loop, holes,
                  ID, marker, is_structured=False)

volume(outer_surfaces, holes=[], ID=None, marker=0)

Adds a Volume

Parameters

outer_surfaces : list List of surface IDs that make up the outer boundary of the volume. holes : list, optional List of lists of surface IDs that make up the inner boundaries of the volume. Default []. ID : int, optional Positive integer ID of this volume. If left unspecified the volume will be assigned the smallest unused volume-ID. It is recommended to specify all volume-IDs or none. marker : int, optional Marker applied to this volume. Default 0.

Source code in src/calfem/geometry.py
def volume(self, outer_surfaces, holes=[], ID=None, marker=0):
    """
    Adds a Volume

    Parameters
    ----------
    outer_surfaces : list
        List of surface IDs that make up the outer boundary of
        the volume.
    holes : list, optional
        List of lists of surface IDs that make up the inner
        boundaries of the volume. Default [].
    ID : int, optional
        Positive integer ID of this volume. If left unspecified
        the volume will be assigned the smallest unused volume-ID.
        It is recommended to specify all volume-IDs or none.
    marker : int, optional
        Marker applied to this volume. Default 0.
    """
    self._addVolume(outer_surfaces, holes, ID, marker, is_structured=False)

Mesh functions

CALFEM Mesh module

Contains functions and classes for generating meshes from geometries.

GmshMeshGenerator

Meshes geometry in GeoData objects or geo-files by calling the Gmsh executable. This is done when the function create() is called.

Source code in src/calfem/mesh.py
 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
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
class GmshMeshGenerator:
    """
    Meshes geometry in GeoData objects or geo-files by calling the Gmsh executable.
    This is done when the function create() is called.
    """

    _GMSH_ALGORITHMS_2D = {
        "meshadapt": 1,
        "auto": 2,
        "initial2d": 3,
        "del2d": 5,
        "front2d": 6,
        "delquad": 8,
        "quadqs": 11,
    }

    _GMSH_ALGORITHMS_3D = {
        "del3d": 1,
        "initial3d": 3,
        "front3d": 4,
        "mmg3d": 7,
        "hxt": 10,
    }    

    def __init__(self, geometry, el_type=2, el_size_factor=1, dofs_per_node=1,
                 gmsh_exec_path=None, clcurv=False,
                 min_size=None, max_size=None, meshing_algorithm=None,
                 additional_options='', mesh_dir='', return_boundary_elements=False):
        """        
        Parameters
        ----------
        geometry : GeoData or str
            GeoData instance or string containing path to .geo-file

        el_type : int
            Element type and order. See gmsh manual for details.

        el_size_factor : float
            Factor by which the element sizes are multiplied.

        dofs_per_node : int
            Number of degrees of freedom per node.

        gmsh_exec_path : str, optional
            File path to where the gmsh executable is located.

        clcurv : bool
            Set to true to make elements smaller at high curvatures. 
            (Experimental option according to the gmsh manual)

        min_size : float, optional
            Minimum element size

        max_size : float, optional
            Maximum element size

        meshing_algorithm : str, optional
            Select mesh algorithm ('meshadapt', 'del2d',
            'front2d',  'del3d', 'front3d', ...). 
            See the gmsh manual for more info.

        return_boundary_elements : bool
            Flag for returning dictionary with boundary element
            information. Useful for applying loads on boundary.

        additional_options : str
            String containing additional command line args for gmsh.
            Use this if a gmsh option is not covered by the above 
            parameters (See section 3.3 in the gmsh manual for a 
            list of options)):
        """
        self.geometry = geometry
        self.el_type = el_type
        self.el_size_factor = el_size_factor
        self.dofs_per_node = dofs_per_node
        self.gmsh_exec_path = gmsh_exec_path
        self.clcurv = clcurv
        self.min_size = min_size
        self.max_size = max_size
        self.meshing_algorithm = meshing_algorithm
        self.additional_options = additional_options
        self.mesh_dir = mesh_dir
        self.return_boundary_elements = return_boundary_elements
        self.gmsh_options = {}
        self.gmsh_verbosity = 0

        # gmsh elements that have rectangle faces
        self._ElementsWithQuadFaces = [3, 5, 10, 12, 16, 17, 92, 93]
        self._2ndOrderElms = [8,  9, 10, 11, 12,
                              13, 14, 16, 17, 18,
                              19]
        self._2dOrderIncompleteElms = [9, 11, 13, 14,
                                       16, 17, 18, 19]
        # Apart from 16 the 2nd orders are totally untested. Only 16 (8-node quad)
        # is implemented in pycalfem though, so it does not matter.

        self.use_gmsh_module = True
        self.remove_gmsh_signal_handler = True
        self.initialize_gmsh = True

    def create(self, is3D=False, dim=3):
        """
        Meshes a surface or volume defined by the geometry in geoData.

        Parameters
        ----------
        is3D : bool, optional
            Optional parameter that only needs to be set if geometry
            is loaded from a geo-file, i.e. if geoData is a path string.
            Default False.

        Returns
        -------
        coords : ndarray
            Node coordinates

            [[n0_x, n0_y, n0_z],
            [   ...           ],
            [nn_x, nn_y, nn_z]]

        edof : ndarray
            Element topology

            [[el0_dof1, ..., el0_dofn],
            [          ...          ],
            [eln_dof1, ..., eln_dofn]]

        dofs : ndarray
            Node dofs

            [[n0_dof1, ..., n0_dofn],
            [         ...         ],
            [nn_dof1, ..., nn_dofn]]

        bdofs : dict
            Boundary dofs. Dictionary containing lists of dofs for
            each boundary marker. Dictionary key = marker id.

        elementmarkers : list
            List of integer markers. Row i contains the marker of
            element i. Markers are similar to boundary markers and
            can be used to identify in which region an element lies.

        boundaryElements : dict, optional
            Returned if self.return_boundary_elements is true.
            Contains dictionary with boundary elements. The keys are markers
            and the values are lists of elements for that marker.

        Notes
        -----
        Running this function also creates object variables:

        nodesOnCurve : dict
            Dictionary containing lists of node-indices. Key is a 
            curve-ID and the value is a list of indices of all nodes
            on that curve, including its end points.

        nodesOnSurface : dict
            Dictionary containing lists of node-indices. Key is a
            surface-ID and the value is a list of indices of the nodes
            on that surface, including its boundary.

        nodesOnVolume : dict
            Dictionary containing lists of node-indices. Key is a
            volume-ID and the value is a list of indices of the nodes
            in that volume, including its surface. 
        """
        # Nodes per element for different element types:
        # (taken from Chapter 9, page 89 of the gmsh manual)
        nodesPerElmDict = {1: 2,   2: 3,   3: 4,   4: 4,   5: 8,
                           6: 6,   7: 5,   8: 3,   9: 6,  10: 9,
                           11: 10, 12: 27, 13: 18, 14: 14, 15: 1,
                           16: 8,  17: 20, 18: 15, 19: 13, 20: 9,
                           21: 10, 22: 12, 23: 15, 24: 15, 25: 21,
                           26: 4,  27: 5,  28: 6,  29: 20, 30: 35,
                           31: 56, 92: 64, 93: 125}
        nodesPerElement = nodesPerElmDict[self.el_type]

        # Check for GMSH executable
        #
        # Consider using the gmsh_extension module

        if not self.use_gmsh_module:
            gmshExe = self.gmsh_exec_path
            if gmshExe == None:
                gmshExe = None
                if sys.platform == "win32":
                    gmshExe = which("gmsh.bat")
                    if gmshExe == None:
                        gmshExe = which("gmsh.exe")
                else:
                    gmshExe = which("gmsh")
            else:
                if not os.path.exists(gmshExe):
                    gmshExe = os.path.join(
                        os.getcwd(), self.gmsh_exec_path)  # Try relative path
                    if not os.path.exists(gmshExe):
                        gmshExe = None  # Relative path didnt work either

            if gmshExe == None:
                raise IOError(
                    "Error: Could not find GMSH. Please make sure that the \\GMSH executable is available on the search path (PATH).")
            else:
                cflog.info(" GMSH -> %s" % gmshExe)
        else:
            cflog.info(" GMSH -> Python-module")

        # Create a temporary directory for GMSH

        oldStyleTempDir = False

        if self.mesh_dir != "":
            tempMeshDir = self.mesh_dir
            if not os.path.exists(tempMeshDir):
                os.mkdir(tempMeshDir)
        else:
            tempMeshDir = tempfile.mkdtemp()

        # If geometry data is given as a .geo file we will just pass it on to gmsh later.

        if type(self.geometry) is str:
            geoFilePath = self.geometry

            # In this case geoData is a path string, so the dimension must be supplied by the user.

            dim = 3 if is3D else 2
            if not os.path.exists(geoFilePath):
                geoFilePath = os.path.join(
                    os.getcwd(), geoFilePath)  # Try relative path
                if not os.path.exists(geoFilePath):
                    raise IOError(
                        "Error: Could not find geo-file " + geoFilePath)
        else:

            # Get the dimension of the model from geoData.

            dim = 3 if self.geometry.is3D else 2

            if oldStyleTempDir:
                if not os.path.exists("./gmshMeshTemp"):
                    os.mkdir("./gmshMeshTemp")
                geoFilePath = os.path.normpath(os.path.join(
                    os.getcwd(), "gmshMeshTemp/tempGeometry.geo"))  # "gmshMeshTemp/tempGeometry.geo"
            else:
                geoFilePath = os.path.normpath(
                    os.path.join(tempMeshDir, 'tempGeometry.geo'))

            with open(geoFilePath, "w") as self.geofile:
                self._writeGeoFile()  # Write geoData to file

        if oldStyleTempDir:

            # Filepath to the msh-file that will be generated.

            mshFileName = os.path.normpath(os.path.join(
                os.getcwd(), 'gmshMeshTemp/meshFile.msh'))
        else:
            mshFileName = os.path.normpath(
                os.path.join(tempMeshDir, 'meshFile.msh'))

        # construct options string:

        options = ""
        options += ' -' + str(dim)
        options += ' -clscale ' + str(self.el_size_factor)  # scale factor
        options += ' -o \"%s\"' % mshFileName
        options += ' -clcurv' if self.clcurv else ''
        options += ' -clmin ' + \
            str(self.min_size) if self.min_size is not None else ''
        options += ' -clmax ' + \
            str(self.max_size) if self.max_size is not None else ''
        options += ' -algo ' + self.meshing_algorithm if self.meshing_algorithm is not None else ''
        options += ' -order 2' if self.el_type in self._2ndOrderElms else ''
        options += ' -format msh22'
        options += ' -v 5'
        options += ' ' + self.additional_options

        # Execute gmsh

        if self.use_gmsh_module:

            # Meshing using gmsh extension module

            if self.initialize_gmsh:
                gmsh.initialize(sys.argv, interruptible=False)

            gmsh.option.setNumber("General.Verbosity", self.gmsh_verbosity)

            # This is a hack to enable the use of gmsh in
            # a separate thread.

            if self.remove_gmsh_signal_handler:
                gmsh.oldsig = None

            # Load .geo file

            gmsh.open(geoFilePath)
            gmsh.model.geo.synchronize()

            # Set meshing options

            if self.el_type in self._2ndOrderElms:
                gmsh.option.setNumber("Mesh.ElementOrder", 2)

            if self.meshing_algorithm is not None:
                algorithm = str(self.meshing_algorithm).lower()

                if algorithm in self._GMSH_ALGORITHMS_3D:
                    gmsh.option.setNumber(
                        "Mesh.Algorithm3D",
                        self._GMSH_ALGORITHMS_3D[algorithm],
                    )
                elif algorithm in self._GMSH_ALGORITHMS_2D:
                    gmsh.option.setNumber(
                        "Mesh.Algorithm",
                        self._GMSH_ALGORITHMS_2D[algorithm],
                    )
                else:
                    valid = sorted(
                        list(self._GMSH_ALGORITHMS_2D.keys())
                        + list(self._GMSH_ALGORITHMS_3D.keys())
                    )
                    raise ValueError(
                        f"Unknown meshing_algorithm '{self.meshing_algorithm}'. "
                        f"Valid values are: {', '.join(valid)}"
                    )

            gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
            gmsh.option.setNumber("Mesh.MeshSizeFactor", self.el_size_factor)

            if self.clcurv is not None:
                gmsh.option.setNumber(
                    "Mesh.MeshSizeFromCurvature", self.clcurv)

            if self.min_size is not None:
                gmsh.option.setNumber('Mesh.MeshSizeMin', self.min_size)
            if self.max_size is not None:
                gmsh.option.setNumber('Mesh.MeshSizeMax', self.max_size)

            for mesh_option in self.gmsh_options.keys():
                gmsh.option.setNumber(mesh_option, self.gmsh_options[mesh_option])

            # Generate mesh

            gmsh.model.mesh.generate(dim)

            # Write .msh file

            gmsh.write(mshFileName)

            # Close extension module

            if self.initialize_gmsh:
                gmsh.finalize()
        else:
            gmshExe = os.path.normpath(gmshExe)
            info("GMSH binary: "+gmshExe)

            output = subprocess.Popen(r'"%s" "%s" %s' % (
                gmshExe, geoFilePath, options), shell=True, stdout=subprocess.PIPE).stdout.read()

        # Read generated msh file:
        # print("Opening msh file " + mshFileName)#TEMP

        with open(mshFileName, 'r') as mshFile:

            info(" Mesh file  : "+mshFileName)

            # print("Reading msh file...")

            ln = mshFile.readline()
            while(ln != '$Nodes\n'):  # Read until we find the nodes
                ln = mshFile.readline()
            nbrNodes = int(mshFile.readline())
            allNodes = np.zeros([nbrNodes, dim], 'd')
            for i in range(nbrNodes):
                line = list(map(float, mshFile.readline().split()))

                # Grab the coordinates (1:3 if 2D, 1:4 if 3D)

                allNodes[i, :] = line[1:dim+1]

            while(mshFile.readline() != '$Elements\n'):  # Read until we find the elements
                pass

            # The nbr of elements (including marker elements).

            nbrElements = int(mshFile.readline())
            elements = []
            elementmarkers = []

            # temp dictionary of sets. Key:MarkerID. Value:Set. The sets will be converted to lists.

            bdofs = {}
            boundaryElements = {}

            # nodeOnPoint = {}  #dictionary pointID : nodeNumber

            self.nodesOnCurve = {}  # dictionary lineID  : set of [nodeNumber]
            # dictionary surfID  : set of [nodeNumber]
            self.nodesOnSurface = {}
            self.nodesOnVolume = {}  # dictionary volID   : set of [nodeNumber]

            # Read all elements (points, surfaces, etc):

            for i in range(nbrElements):
                line = list(map(int, mshFile.readline().split()))
                eType = line[1]  # second int is the element type.
                # Third int is the nbr of tags on this element.
                nbrTags = line[2]
                marker = line[3]  # Fourth int (first tag) is the marker.

                # Fifth int  is the ID of the geometric entity (points, curves, etc) that the element belongs to

                entityID = line[4]

                # The rest after tags are node indices.

                nodes = line[3+nbrTags: len(line)]

                # If the element type is the kind of element we are looking for:

                if(eType == self.el_type):

                    # Add the nodes of the elements to the list.

                    elements.append(nodes)

                    # Add element marker. It is used for keeping track of elements (thickness, heat-production and such)

                    elementmarkers.append(marker)
                else:  # If the element is not a "real" element we store its node at marker in bdof instead:
                    _insertInSetDict(bdofs, marker, nodes)

                    # We also store the full information as 'boundary elements'

                    _insertBoundaryElement(
                        boundaryElements, eType, marker, nodes)

                # if eType == 15: #If point. Commmented away because points only make elements if they have non-zero markers, so nodeOnPoint is not very useful.
                #    nodeOnPoint[entityID-1] = nodes[0] #insert node into nodeOnPoint. (ID-1 because we want 0-based indices)

                if eType in [1, 8, 26, 27, 28]:  # If line

                    # insert nodes into nodesOnCurve

                    _insertInSetDict(self.nodesOnCurve, entityID -
                                     1, _offsetIndices(nodes, -1))
                elif eType in [2, 3, 9, 10, 16, 20, 21, 22, 23, 24, 25]:  # If surfaceelement

                    # insert nodes into nodesOnSurface

                    _insertInSetDict(self.nodesOnSurface, entityID -
                                     1, _offsetIndices(nodes, -1))
                else:

                    # if volume element.

                    _insertInSetDict(self.nodesOnVolume, entityID -
                                     1, _offsetIndices(nodes, -1))

            elements = np.array(elements)
            for key in bdofs.keys():  # Convert the sets of boundary nodes to lists.
                bdofs[key] = list(bdofs[key])
            for key in self.nodesOnCurve.keys():  # Convert set to list
                self.nodesOnCurve[key] = list(self.nodesOnCurve[key])
            for key in self.nodesOnSurface.keys():  # Convert set to list
                self.nodesOnSurface[key] = list(self.nodesOnSurface[key])
            for key in self.nodesOnVolume.keys():  # Convert set to list
                self.nodesOnVolume[key] = list(self.nodesOnVolume[key])

        # Remove temporary mesh directory if not explicetly specified.

        if self.mesh_dir == "":
            shutil.rmtree(tempMeshDir)

        dofs = createdofs(np.size(allNodes, 0), self.dofs_per_node)

        if self.dofs_per_node > 1:  # This if-chunk copied from pycalfem_utils.py
            self.topo = elements
            expandedElements = np.zeros(
                (np.size(elements, 0), nodesPerElement*self.dofs_per_node), 'i')
            elIdx = 0
            for elementTopo in elements:
                for i in range(nodesPerElement):
                    expandedElements[elIdx, i*self.dofs_per_node:(
                        i*self.dofs_per_node+self.dofs_per_node)] = dofs[elementTopo[i]-1, :]
                elIdx += 1

            for keyID in bdofs.keys():
                bVerts = bdofs[keyID]
                bVertsNew = []
                for i in range(len(bVerts)):
                    for j in range(self.dofs_per_node):
                        bVertsNew.append(dofs[bVerts[i]-1][j])
                bdofs[keyID] = bVertsNew

            if self.return_boundary_elements:
                return allNodes, np.asarray(expandedElements), dofs, bdofs, elementmarkers, boundaryElements
            return allNodes, np.asarray(expandedElements), dofs, bdofs, elementmarkers

        if self.return_boundary_elements:
            return allNodes, elements, dofs, bdofs, elementmarkers, boundaryElements
        return allNodes, elements, dofs, bdofs, elementmarkers

    def _writeGeoFile(self):

        # key is marker, value is a list of point indices (0-based) with that marker

        pointMarkers = {}
        curveMarkers = {}
        surfaceMarkers = {}
        volumeMarkers = {}

        # WRITE POINTS:

        for ID, [coords, elSize, marker] in self.geometry.points.items():
            self.geofile.write("Point(%i) = {%s};\n" % (
                ID+1, _formatList(coords + [elSize])))
            _insertInSetDict(pointMarkers, marker, ID)

        # WRITE CURVES:

        for ID, [curveName, points, marker, elOnCurve, distributionString, distributionVal] in self.geometry.curves.items():
            self.geofile.write("%s(%i) = {%s};\n" % (
                curveName, ID+1, _formatList(points, 1)))

            # Transfinite Line{2} = 20 Using Bump 0.05;

            if elOnCurve != None:
                distribution = "" if distributionString == None else "Using %s %f" % (
                    distributionString, distributionVal)
                self.geofile.write("Transfinite Line{%i} = %i %s;\n" % (
                    ID+1, elOnCurve+1, distribution))

                # +1 on elOnCurve because gmsh actually takes the number of nodes on the curve, not elements on the curve.

            _insertInSetDict(curveMarkers, marker, ID)

        # WRITE SURFACES:

        for ID, [surfName, outerLoop, holes, ID, marker, isStructured] in self.geometry.surfaces.items():

            # First we write line loops for the surface edge and holes (if there are any holes):

            self._writeLineLoop(outerLoop, ID+1)
            holeIDs = []
            for hole, i in zip(holes, range(len(holes))):

                # Create a hopefully unique ID-number for the line loop: Like 10015 or 1540035
                # (If gmsh uses 32-bit integers for IDs then IDs over 214'748 will break)

                holeID = 10000 * (ID+1) + 10 * i + 5
                self._writeLineLoop(hole, holeID)
                holeIDs.append(holeID)

            # Second, we write the surface itself:
            # If we have hole we want to include them in the surface.

            holeString = "" if not holeIDs else ", " + _formatList(holeIDs)

            # Like "Plane Surface(2) = {4, 2, 6, 8}

            self.geofile.write("%s(%i) = {%s%s};\n" % (
                surfName, ID+1, ID+1, holeString))

            # Lastly, we make the surface transfinite if it is a structured surface:

            if isStructured:
                cornerPoints = set()

                # Find the corner points. This is possibly unnecessary since Gmsh can do this automatically.

                for c in outerLoop:
                    curvePoints = self.geometry.curves[c][1]
                    cornerPoints.add(curvePoints[0])
                    cornerPoints.add(curvePoints[-1])
                cornerPoints = list(cornerPoints)
                self.geofile.write("Transfinite Surface{%i} = {%s};\n" % (
                    ID+1, _formatList(cornerPoints, 1)))  # Like Transfinite Surface{1} = {1,2,3,4};

                # Transfinite Surface has an optional argument (about triangle orientation) that is not implemented here.

            _insertInSetDict(surfaceMarkers, marker, ID)

        # WRITE VOLUMES:

        for ID, [outerLoop, holes, ID, marker, isStructured] in self.geometry.volumes.items():

            # Surface loops for the volume boundary and holes (if any):

            self._writeSurfaceLoop(outerLoop, ID+1)
            holeIDs = []
            for hole, i in zip(holes, range(len(holes))):

                # ID-number for the hole surface loop

                holeID = 10000 * (ID+1) + 10 * i + 7
                self._writeSurfaceLoop(hole, holeID)
                holeIDs.append(holeID)

            # Write the volume itself:
            # If we have hole we want to include them in the surface.

            holeString = "" if not holeIDs else " , " + _formatList(holeIDs)

            # Like "Plane Surface(2) = {4, 2, 6, 8}

            self.geofile.write(
                "Volume(%i) = {%s%s};\n" % (ID+1, ID+1, holeString))

            # Lastly, we make the volume transfinite if it is a structured volume:

            if isStructured:
                self.geofile.write("Transfinite Volume{%i} = {};\n" % (ID+1))

                # We don't find the corner points of the structured volume like we did with the surfaces. Gmsh can actually find the corners automatically.

            _insertInSetDict(volumeMarkers, marker, ID)

        # MAYBE MAKE QUADS:

        if(self.el_type in self._ElementsWithQuadFaces):  # If we have quads surfaces on the elements
            self.geofile.write("Mesh.RecombineAll = 1;\n")

        # WRITE POINT MARKERS:

        for marker, IDlist in pointMarkers.items():
            if marker != 0:
                self.geofile.write("Physical Point(%i) = {%s};\n" % (
                    marker, _formatList(IDlist, 1)))

        # WRITE CURVE MARKERS:

        for marker, IDlist in curveMarkers.items():
            self.geofile.write("Physical Line(%i) = {%s};\n" % (
                marker, _formatList(IDlist, 1)))

        # WRITE SURFACE MARKERS:

        for marker, IDlist in surfaceMarkers.items():
            self.geofile.write("Physical Surface(%i) = {%s};\n" % (
                marker, _formatList(IDlist, 1)))

        # WRITE SURFACE MARKERS:
        for marker, IDlist in volumeMarkers.items():
            self.geofile.write("Physical Volume(%i) = {%s};\n" % (
                marker, _formatList(IDlist, 1)))

        # If the element type is of an incomplete second order type
        # (i.e it is an 2nd order element without nodes in the middle of the element face),
        # then we need to specify this in the geo-file:

        if self.el_type in self._2dOrderIncompleteElms:
            self.geofile.write("Mesh.SecondOrderIncomplete=1;\n")

    def _writeLineLoop(self, lineIndices, loopID):

        # endPoints is used to keep track of at which points the curves start and end (i.e the direction of the curves)

        endPoints = []

        # lineIndices is a list of curve indices (0-based here, but 1-based later in the method)

        for i in lineIndices:
            curvePoints = self.geometry.curves[i][1]
            endPoints.append([curvePoints[0], curvePoints[-1]])

        # We need the indices to be 1-based rather than 0-based in the next loop. (Some indices will be preceded by a minus-sign)

        lineIndices = _offsetIndices(lineIndices, 1)
        isFirstLine = True
        nbrLinesinLoop = len(lineIndices)

        # In this loop we reverse the direction of some lines in the LineLoop to make them conform to the format that Gmsh expects.

        for k in range(nbrLinesinLoop):
            if isFirstLine and nbrLinesinLoop > 1:
                isFirstLine = False

                # If last point of the first line exists in the endpoints of the second line... Do nothing

                if endPoints[0][1] in endPoints[1]:
                    pass

                # Else if the first point in the first line exists in the endpoints of the second line:

                elif endPoints[0][0] in endPoints[1]:
                    endPoints[0].reverse()
                    lineIndices[0] *= -1  # Reverse the direction of the line
                else:
                    raise Exception(
                        "ERROR: The first curve of line-loop %i does not link up to the subsequent curve" % loopID)
            elif endPoints[k][0] == endPoints[k-1][1]:
                pass
            elif endPoints[k][1] == endPoints[k-1][1]:
                endPoints[k].reverse()
                lineIndices[k] *= -1  # Reverse the direction of the line
            else:
                raise Exception(
                    "ERROR: The %i th curve (starting from 0) of a line-loop %i does not link up with the preceding curve" % (k, loopID))
            if k == nbrLinesinLoop-1 and endPoints[k][1] != endPoints[0][0]:
                # If last line AND the last point of the last curve not equal the first point of the first curve:
                raise Exception(
                    "ERROR: The last curve of a line-loop %i does not join up with the first curve" % loopID)

        # If the model is in 2D we need to make all line loops counter-clockwise so surface normals point in the positive z-direction.

        if not self.geometry.is3D:
            lineIndices = self._makeCounterClockwise(lineIndices)

        self.geofile.write("Line Loop(%i) = {%s};\n" % (loopID, _formatList(
            lineIndices)))  # (lineIndices are alreay 1-based here)

    def _makeCounterClockwise(self, lineIndices):
        '''If the lineIndices describe a line loop that is not counterclockwise,
        this function will return a counterclockwise version of lineIndices
        (i.e. all indices multiplied by -1).
        lineIndices is a list of integers (1-based line indices) that may be negative, but not 0'''

        # Method described at http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order

        summa = 0.0  # Counter-clockwise if the sum ends up negative.
        for index in lineIndices:
            sign = -1 if index < 0 else 1
            # Make a copy of the line index that is positive and 0-based.
            realIndex = sign*index - 1
            curveType = self.geometry.curves[realIndex][0]
            pointIDs = self.geometry.curves[realIndex][1]
            if curveType in ['Spline', 'BSpline']:
                points = [self.geometry.points[ID][0]
                          for ID in pointIDs]  # [[x,y,z], [x,y,z], ...]
                # Reverse the order of the points if the curve direction is reversed.
                points = points if sign == 1 else points[::-1]
                # For every point along the curve except the last:
                for i in range(len(pointIDs)-1):
                    # (x2-x1)(y2+y1).
                    summa += (points[i+1][0] - points[i][0]) * \
                        (points[i+1][1] + points[i][1])
            elif curveType == 'Circle':
                # We will find a point 'd' on the middle of the circle arc, and use a-d-c as approximation of the arc.
                # 3-by-3 array. The rows are start-center-end points and columns are x,y,z.
                points = np.array([self.geometry.points[ID][0]
                                   for ID in pointIDs])
                # Reverse the order of the points if the curve direction is reversed.
                points = points if sign == 1 else points[::-1]
                a = points[0, :]  # start
                b = points[1, :]  # center
                c = points[2, :]  # end
                r = np.linalg.norm(a-b)  # radius
                d = b + r * (a + 2*b + c) / np.linalg.norm(a + 2*b + c)
                approxArc = np.vstack((a, d, c))
                for i in range(len(approxArc)-1):
                    # (x2-x1)(y2+y1).
                    summa += (approxArc[i+1, 0] - approxArc[i, 0]) * \
                        (approxArc[i+1, 1] + approxArc[i, 1])
            elif curveType == 'Ellipse':
                # We will find a point 'd' near the middle of the circle arc, and use a-d-c as approximation of the arc.
                # The only difference from the circle above, is that the radius at d is approximated as the mean distance between
                # the center and the two other points.
                # 4-by-3 array. The rows are start-center-majAxis-end points and columns are x,y,z.
                points = np.array([self.geometry.points[ID][0]
                                   for ID in pointIDs])
                # skip the major axis point (row 2)
                points = points[[0, 1, 3], :]
                # Reverse the order of the points if the curve direction is reversed.
                points = points if sign == 1 else points[::-1]
                a = points[0, :]  # start
                b = points[1, :]  # center
                c = points[2, :]  # end
                r = (np.linalg.norm(a-b) + np.linalg.norm(c-b)) / \
                    2  # approximate radius
                d = b + r * (a + 2*b + c) / np.linalg.norm(a + 2*b + c)
                approxArc = np.vstack((a, d, c))
                for i in range(len(approxArc)-1):
                    # (x2-x1)(y2+y1).
                    summa += (approxArc[i+1, 0] - approxArc[i, 0]) * \
                        (approxArc[i+1, 1] + approxArc[i, 1])
        # If the sum is positive the loop (closed polygon) is clockwise, so reverse the direction of all curves:
        if summa > 0:
            lineIndices = [-x for x in lineIndices]
        return lineIndices

    def _writeSurfaceLoop(self, outerLoop, ID):
        self.geofile.write("Surface Loop(%i) = {%s};\n" % (
            ID, _formatList(outerLoop, 1)))

    # --- Compatibility properties

    @property
    def elType(self):
        return self.el_type

    @elType.setter
    def elType(self, value):
        self.el_type = value

    @property
    def elSizeFactor(self):
        return self.el_size_factor

    @elSizeFactor.setter
    def elSizeFactor(self, value):
        self.el_size_factor = value

    @property
    def dofsPerNode(self):
        return self.dofs_per_node

    @dofsPerNode.setter
    def dofsPerNode(self, value):
        self.dofs_per_node = value

    @property
    def gmshExecPath(self):
        return self.gmsh_exec_path

    @gmshExecPath.setter
    def gmshExecPath(self, value):
        self.gmsh_exec_path = value

    @property
    def minSize(self):
        return self.min_size

    @minSize.setter
    def minSize(self, value):
        self.min_size = value

    @property
    def maxSize(self):
        return self.max_size

    @maxSize.setter
    def maxSize(self, value):
        self.max_size = value

    @property
    def meshingAlgorithm(self):
        return self.meshing_algorithm

    @meshingAlgorithm.setter
    def meshingAlgorithm(self, value):
        self.meshing_algorithm = value

    @property
    def additionalOptions(self):
        return self.additional_options

    @additionalOptions.setter
    def additionalOptions(self, value):
        self.additional_options = value

    @property
    def meshDir(self):
        return self.mesh_dir

    @meshDir.setter
    def meshDir(self, value):
        self.mesh_dir = value

    @property
    def returnBoundaryElements(self):
        return self.return_boundary_elements

    @returnBoundaryElements.setter
    def returnBoundaryElements(self, value):
        self.return_boundary_elements = value

__init__(geometry, el_type=2, el_size_factor=1, dofs_per_node=1, gmsh_exec_path=None, clcurv=False, min_size=None, max_size=None, meshing_algorithm=None, additional_options='', mesh_dir='', return_boundary_elements=False)

Parameters

geometry : GeoData or str GeoData instance or string containing path to .geo-file

int

Element type and order. See gmsh manual for details.

float

Factor by which the element sizes are multiplied.

int

Number of degrees of freedom per node.

str, optional

File path to where the gmsh executable is located.

bool

Set to true to make elements smaller at high curvatures. (Experimental option according to the gmsh manual)

float, optional

Minimum element size

float, optional

Maximum element size

str, optional

Select mesh algorithm ('meshadapt', 'del2d', 'front2d', 'del3d', 'front3d', ...). See the gmsh manual for more info.

bool

Flag for returning dictionary with boundary element information. Useful for applying loads on boundary.

str

String containing additional command line args for gmsh. Use this if a gmsh option is not covered by the above parameters (See section 3.3 in the gmsh manual for a list of options)):

Source code in src/calfem/mesh.py
def __init__(self, geometry, el_type=2, el_size_factor=1, dofs_per_node=1,
             gmsh_exec_path=None, clcurv=False,
             min_size=None, max_size=None, meshing_algorithm=None,
             additional_options='', mesh_dir='', return_boundary_elements=False):
    """        
    Parameters
    ----------
    geometry : GeoData or str
        GeoData instance or string containing path to .geo-file

    el_type : int
        Element type and order. See gmsh manual for details.

    el_size_factor : float
        Factor by which the element sizes are multiplied.

    dofs_per_node : int
        Number of degrees of freedom per node.

    gmsh_exec_path : str, optional
        File path to where the gmsh executable is located.

    clcurv : bool
        Set to true to make elements smaller at high curvatures. 
        (Experimental option according to the gmsh manual)

    min_size : float, optional
        Minimum element size

    max_size : float, optional
        Maximum element size

    meshing_algorithm : str, optional
        Select mesh algorithm ('meshadapt', 'del2d',
        'front2d',  'del3d', 'front3d', ...). 
        See the gmsh manual for more info.

    return_boundary_elements : bool
        Flag for returning dictionary with boundary element
        information. Useful for applying loads on boundary.

    additional_options : str
        String containing additional command line args for gmsh.
        Use this if a gmsh option is not covered by the above 
        parameters (See section 3.3 in the gmsh manual for a 
        list of options)):
    """
    self.geometry = geometry
    self.el_type = el_type
    self.el_size_factor = el_size_factor
    self.dofs_per_node = dofs_per_node
    self.gmsh_exec_path = gmsh_exec_path
    self.clcurv = clcurv
    self.min_size = min_size
    self.max_size = max_size
    self.meshing_algorithm = meshing_algorithm
    self.additional_options = additional_options
    self.mesh_dir = mesh_dir
    self.return_boundary_elements = return_boundary_elements
    self.gmsh_options = {}
    self.gmsh_verbosity = 0

    # gmsh elements that have rectangle faces
    self._ElementsWithQuadFaces = [3, 5, 10, 12, 16, 17, 92, 93]
    self._2ndOrderElms = [8,  9, 10, 11, 12,
                          13, 14, 16, 17, 18,
                          19]
    self._2dOrderIncompleteElms = [9, 11, 13, 14,
                                   16, 17, 18, 19]
    # Apart from 16 the 2nd orders are totally untested. Only 16 (8-node quad)
    # is implemented in pycalfem though, so it does not matter.

    self.use_gmsh_module = True
    self.remove_gmsh_signal_handler = True
    self.initialize_gmsh = True

create(is3D=False, dim=3)

Meshes a surface or volume defined by the geometry in geoData.

Parameters

is3D : bool, optional Optional parameter that only needs to be set if geometry is loaded from a geo-file, i.e. if geoData is a path string. Default False.

Returns

coords : ndarray Node coordinates

[[n0_x, n0_y, n0_z],
[   ...           ],
[nn_x, nn_y, nn_z]]
ndarray

Element topology

[[el0_dof1, ..., el0_dofn], [ ... ], [eln_dof1, ..., eln_dofn]]

ndarray

Node dofs

[[n0_dof1, ..., n0_dofn], [ ... ], [nn_dof1, ..., nn_dofn]]

dict

Boundary dofs. Dictionary containing lists of dofs for each boundary marker. Dictionary key = marker id.

list

List of integer markers. Row i contains the marker of element i. Markers are similar to boundary markers and can be used to identify in which region an element lies.

dict, optional

Returned if self.return_boundary_elements is true. Contains dictionary with boundary elements. The keys are markers and the values are lists of elements for that marker.

Notes

Running this function also creates object variables:

dict

Dictionary containing lists of node-indices. Key is a curve-ID and the value is a list of indices of all nodes on that curve, including its end points.

dict

Dictionary containing lists of node-indices. Key is a surface-ID and the value is a list of indices of the nodes on that surface, including its boundary.

dict

Dictionary containing lists of node-indices. Key is a volume-ID and the value is a list of indices of the nodes in that volume, including its surface.

Source code in src/calfem/mesh.py
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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
def create(self, is3D=False, dim=3):
    """
    Meshes a surface or volume defined by the geometry in geoData.

    Parameters
    ----------
    is3D : bool, optional
        Optional parameter that only needs to be set if geometry
        is loaded from a geo-file, i.e. if geoData is a path string.
        Default False.

    Returns
    -------
    coords : ndarray
        Node coordinates

        [[n0_x, n0_y, n0_z],
        [   ...           ],
        [nn_x, nn_y, nn_z]]

    edof : ndarray
        Element topology

        [[el0_dof1, ..., el0_dofn],
        [          ...          ],
        [eln_dof1, ..., eln_dofn]]

    dofs : ndarray
        Node dofs

        [[n0_dof1, ..., n0_dofn],
        [         ...         ],
        [nn_dof1, ..., nn_dofn]]

    bdofs : dict
        Boundary dofs. Dictionary containing lists of dofs for
        each boundary marker. Dictionary key = marker id.

    elementmarkers : list
        List of integer markers. Row i contains the marker of
        element i. Markers are similar to boundary markers and
        can be used to identify in which region an element lies.

    boundaryElements : dict, optional
        Returned if self.return_boundary_elements is true.
        Contains dictionary with boundary elements. The keys are markers
        and the values are lists of elements for that marker.

    Notes
    -----
    Running this function also creates object variables:

    nodesOnCurve : dict
        Dictionary containing lists of node-indices. Key is a 
        curve-ID and the value is a list of indices of all nodes
        on that curve, including its end points.

    nodesOnSurface : dict
        Dictionary containing lists of node-indices. Key is a
        surface-ID and the value is a list of indices of the nodes
        on that surface, including its boundary.

    nodesOnVolume : dict
        Dictionary containing lists of node-indices. Key is a
        volume-ID and the value is a list of indices of the nodes
        in that volume, including its surface. 
    """
    # Nodes per element for different element types:
    # (taken from Chapter 9, page 89 of the gmsh manual)
    nodesPerElmDict = {1: 2,   2: 3,   3: 4,   4: 4,   5: 8,
                       6: 6,   7: 5,   8: 3,   9: 6,  10: 9,
                       11: 10, 12: 27, 13: 18, 14: 14, 15: 1,
                       16: 8,  17: 20, 18: 15, 19: 13, 20: 9,
                       21: 10, 22: 12, 23: 15, 24: 15, 25: 21,
                       26: 4,  27: 5,  28: 6,  29: 20, 30: 35,
                       31: 56, 92: 64, 93: 125}
    nodesPerElement = nodesPerElmDict[self.el_type]

    # Check for GMSH executable
    #
    # Consider using the gmsh_extension module

    if not self.use_gmsh_module:
        gmshExe = self.gmsh_exec_path
        if gmshExe == None:
            gmshExe = None
            if sys.platform == "win32":
                gmshExe = which("gmsh.bat")
                if gmshExe == None:
                    gmshExe = which("gmsh.exe")
            else:
                gmshExe = which("gmsh")
        else:
            if not os.path.exists(gmshExe):
                gmshExe = os.path.join(
                    os.getcwd(), self.gmsh_exec_path)  # Try relative path
                if not os.path.exists(gmshExe):
                    gmshExe = None  # Relative path didnt work either

        if gmshExe == None:
            raise IOError(
                "Error: Could not find GMSH. Please make sure that the \\GMSH executable is available on the search path (PATH).")
        else:
            cflog.info(" GMSH -> %s" % gmshExe)
    else:
        cflog.info(" GMSH -> Python-module")

    # Create a temporary directory for GMSH

    oldStyleTempDir = False

    if self.mesh_dir != "":
        tempMeshDir = self.mesh_dir
        if not os.path.exists(tempMeshDir):
            os.mkdir(tempMeshDir)
    else:
        tempMeshDir = tempfile.mkdtemp()

    # If geometry data is given as a .geo file we will just pass it on to gmsh later.

    if type(self.geometry) is str:
        geoFilePath = self.geometry

        # In this case geoData is a path string, so the dimension must be supplied by the user.

        dim = 3 if is3D else 2
        if not os.path.exists(geoFilePath):
            geoFilePath = os.path.join(
                os.getcwd(), geoFilePath)  # Try relative path
            if not os.path.exists(geoFilePath):
                raise IOError(
                    "Error: Could not find geo-file " + geoFilePath)
    else:

        # Get the dimension of the model from geoData.

        dim = 3 if self.geometry.is3D else 2

        if oldStyleTempDir:
            if not os.path.exists("./gmshMeshTemp"):
                os.mkdir("./gmshMeshTemp")
            geoFilePath = os.path.normpath(os.path.join(
                os.getcwd(), "gmshMeshTemp/tempGeometry.geo"))  # "gmshMeshTemp/tempGeometry.geo"
        else:
            geoFilePath = os.path.normpath(
                os.path.join(tempMeshDir, 'tempGeometry.geo'))

        with open(geoFilePath, "w") as self.geofile:
            self._writeGeoFile()  # Write geoData to file

    if oldStyleTempDir:

        # Filepath to the msh-file that will be generated.

        mshFileName = os.path.normpath(os.path.join(
            os.getcwd(), 'gmshMeshTemp/meshFile.msh'))
    else:
        mshFileName = os.path.normpath(
            os.path.join(tempMeshDir, 'meshFile.msh'))

    # construct options string:

    options = ""
    options += ' -' + str(dim)
    options += ' -clscale ' + str(self.el_size_factor)  # scale factor
    options += ' -o \"%s\"' % mshFileName
    options += ' -clcurv' if self.clcurv else ''
    options += ' -clmin ' + \
        str(self.min_size) if self.min_size is not None else ''
    options += ' -clmax ' + \
        str(self.max_size) if self.max_size is not None else ''
    options += ' -algo ' + self.meshing_algorithm if self.meshing_algorithm is not None else ''
    options += ' -order 2' if self.el_type in self._2ndOrderElms else ''
    options += ' -format msh22'
    options += ' -v 5'
    options += ' ' + self.additional_options

    # Execute gmsh

    if self.use_gmsh_module:

        # Meshing using gmsh extension module

        if self.initialize_gmsh:
            gmsh.initialize(sys.argv, interruptible=False)

        gmsh.option.setNumber("General.Verbosity", self.gmsh_verbosity)

        # This is a hack to enable the use of gmsh in
        # a separate thread.

        if self.remove_gmsh_signal_handler:
            gmsh.oldsig = None

        # Load .geo file

        gmsh.open(geoFilePath)
        gmsh.model.geo.synchronize()

        # Set meshing options

        if self.el_type in self._2ndOrderElms:
            gmsh.option.setNumber("Mesh.ElementOrder", 2)

        if self.meshing_algorithm is not None:
            algorithm = str(self.meshing_algorithm).lower()

            if algorithm in self._GMSH_ALGORITHMS_3D:
                gmsh.option.setNumber(
                    "Mesh.Algorithm3D",
                    self._GMSH_ALGORITHMS_3D[algorithm],
                )
            elif algorithm in self._GMSH_ALGORITHMS_2D:
                gmsh.option.setNumber(
                    "Mesh.Algorithm",
                    self._GMSH_ALGORITHMS_2D[algorithm],
                )
            else:
                valid = sorted(
                    list(self._GMSH_ALGORITHMS_2D.keys())
                    + list(self._GMSH_ALGORITHMS_3D.keys())
                )
                raise ValueError(
                    f"Unknown meshing_algorithm '{self.meshing_algorithm}'. "
                    f"Valid values are: {', '.join(valid)}"
                )

        gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
        gmsh.option.setNumber("Mesh.MeshSizeFactor", self.el_size_factor)

        if self.clcurv is not None:
            gmsh.option.setNumber(
                "Mesh.MeshSizeFromCurvature", self.clcurv)

        if self.min_size is not None:
            gmsh.option.setNumber('Mesh.MeshSizeMin', self.min_size)
        if self.max_size is not None:
            gmsh.option.setNumber('Mesh.MeshSizeMax', self.max_size)

        for mesh_option in self.gmsh_options.keys():
            gmsh.option.setNumber(mesh_option, self.gmsh_options[mesh_option])

        # Generate mesh

        gmsh.model.mesh.generate(dim)

        # Write .msh file

        gmsh.write(mshFileName)

        # Close extension module

        if self.initialize_gmsh:
            gmsh.finalize()
    else:
        gmshExe = os.path.normpath(gmshExe)
        info("GMSH binary: "+gmshExe)

        output = subprocess.Popen(r'"%s" "%s" %s' % (
            gmshExe, geoFilePath, options), shell=True, stdout=subprocess.PIPE).stdout.read()

    # Read generated msh file:
    # print("Opening msh file " + mshFileName)#TEMP

    with open(mshFileName, 'r') as mshFile:

        info(" Mesh file  : "+mshFileName)

        # print("Reading msh file...")

        ln = mshFile.readline()
        while(ln != '$Nodes\n'):  # Read until we find the nodes
            ln = mshFile.readline()
        nbrNodes = int(mshFile.readline())
        allNodes = np.zeros([nbrNodes, dim], 'd')
        for i in range(nbrNodes):
            line = list(map(float, mshFile.readline().split()))

            # Grab the coordinates (1:3 if 2D, 1:4 if 3D)

            allNodes[i, :] = line[1:dim+1]

        while(mshFile.readline() != '$Elements\n'):  # Read until we find the elements
            pass

        # The nbr of elements (including marker elements).

        nbrElements = int(mshFile.readline())
        elements = []
        elementmarkers = []

        # temp dictionary of sets. Key:MarkerID. Value:Set. The sets will be converted to lists.

        bdofs = {}
        boundaryElements = {}

        # nodeOnPoint = {}  #dictionary pointID : nodeNumber

        self.nodesOnCurve = {}  # dictionary lineID  : set of [nodeNumber]
        # dictionary surfID  : set of [nodeNumber]
        self.nodesOnSurface = {}
        self.nodesOnVolume = {}  # dictionary volID   : set of [nodeNumber]

        # Read all elements (points, surfaces, etc):

        for i in range(nbrElements):
            line = list(map(int, mshFile.readline().split()))
            eType = line[1]  # second int is the element type.
            # Third int is the nbr of tags on this element.
            nbrTags = line[2]
            marker = line[3]  # Fourth int (first tag) is the marker.

            # Fifth int  is the ID of the geometric entity (points, curves, etc) that the element belongs to

            entityID = line[4]

            # The rest after tags are node indices.

            nodes = line[3+nbrTags: len(line)]

            # If the element type is the kind of element we are looking for:

            if(eType == self.el_type):

                # Add the nodes of the elements to the list.

                elements.append(nodes)

                # Add element marker. It is used for keeping track of elements (thickness, heat-production and such)

                elementmarkers.append(marker)
            else:  # If the element is not a "real" element we store its node at marker in bdof instead:
                _insertInSetDict(bdofs, marker, nodes)

                # We also store the full information as 'boundary elements'

                _insertBoundaryElement(
                    boundaryElements, eType, marker, nodes)

            # if eType == 15: #If point. Commmented away because points only make elements if they have non-zero markers, so nodeOnPoint is not very useful.
            #    nodeOnPoint[entityID-1] = nodes[0] #insert node into nodeOnPoint. (ID-1 because we want 0-based indices)

            if eType in [1, 8, 26, 27, 28]:  # If line

                # insert nodes into nodesOnCurve

                _insertInSetDict(self.nodesOnCurve, entityID -
                                 1, _offsetIndices(nodes, -1))
            elif eType in [2, 3, 9, 10, 16, 20, 21, 22, 23, 24, 25]:  # If surfaceelement

                # insert nodes into nodesOnSurface

                _insertInSetDict(self.nodesOnSurface, entityID -
                                 1, _offsetIndices(nodes, -1))
            else:

                # if volume element.

                _insertInSetDict(self.nodesOnVolume, entityID -
                                 1, _offsetIndices(nodes, -1))

        elements = np.array(elements)
        for key in bdofs.keys():  # Convert the sets of boundary nodes to lists.
            bdofs[key] = list(bdofs[key])
        for key in self.nodesOnCurve.keys():  # Convert set to list
            self.nodesOnCurve[key] = list(self.nodesOnCurve[key])
        for key in self.nodesOnSurface.keys():  # Convert set to list
            self.nodesOnSurface[key] = list(self.nodesOnSurface[key])
        for key in self.nodesOnVolume.keys():  # Convert set to list
            self.nodesOnVolume[key] = list(self.nodesOnVolume[key])

    # Remove temporary mesh directory if not explicetly specified.

    if self.mesh_dir == "":
        shutil.rmtree(tempMeshDir)

    dofs = createdofs(np.size(allNodes, 0), self.dofs_per_node)

    if self.dofs_per_node > 1:  # This if-chunk copied from pycalfem_utils.py
        self.topo = elements
        expandedElements = np.zeros(
            (np.size(elements, 0), nodesPerElement*self.dofs_per_node), 'i')
        elIdx = 0
        for elementTopo in elements:
            for i in range(nodesPerElement):
                expandedElements[elIdx, i*self.dofs_per_node:(
                    i*self.dofs_per_node+self.dofs_per_node)] = dofs[elementTopo[i]-1, :]
            elIdx += 1

        for keyID in bdofs.keys():
            bVerts = bdofs[keyID]
            bVertsNew = []
            for i in range(len(bVerts)):
                for j in range(self.dofs_per_node):
                    bVertsNew.append(dofs[bVerts[i]-1][j])
            bdofs[keyID] = bVertsNew

        if self.return_boundary_elements:
            return allNodes, np.asarray(expandedElements), dofs, bdofs, elementmarkers, boundaryElements
        return allNodes, np.asarray(expandedElements), dofs, bdofs, elementmarkers

    if self.return_boundary_elements:
        return allNodes, elements, dofs, bdofs, elementmarkers, boundaryElements
    return allNodes, elements, dofs, bdofs, elementmarkers

create_mesh(geometry, el_type=2, el_size_factor=1, dofs_per_node=1, gmsh_exec_path=None, clcurv=False, min_size=None, max_size=None, meshing_algorithm=None, additional_options='')

Create a mesh for the given geometry using GMSH. This function serves as a convenient wrapper around the GmshMeshGenerator class to generate finite element meshes from geometric definitions. Parameters


geometry : object The geometry object defining the domain to be meshed. el_type : int, optional Element type identifier. Default is 2. el_size_factor : float, optional Factor controlling the element size. Default is 1. dofs_per_node : int, optional Number of degrees of freedom per node. Default is 1. gmsh_exec_path : str, optional Path to the GMSH executable. If None, uses system default. Default is None. clcurv : bool, optional Enable/disable curved element generation. Default is False. min_size : float, optional Minimum element size constraint. Default is None. max_size : float, optional Maximum element size constraint. Default is None. meshing_algorithm : int, optional GMSH meshing algorithm identifier. Default is None. additional_options : str, optional Additional GMSH options as a string. Default is ''. Returns


mesh : object The generated mesh object containing nodes, elements, and connectivity information. Examples


mesh = create_mesh(geometry, el_type=3, el_size_factor=0.5) mesh = create_mesh(geometry, min_size=0.1, max_size=1.0)

Source code in src/calfem/mesh.py
def create_mesh(geometry, el_type=2, el_size_factor=1, dofs_per_node=1,gmsh_exec_path=None,    
                clcurv=False, min_size=None, max_size=None, meshing_algorithm=None,             additional_options=''):
    """
    Create a mesh for the given geometry using GMSH.
    This function serves as a convenient wrapper around the GmshMeshGenerator class
    to generate finite element meshes from geometric definitions.
    Parameters
    ----------
    geometry : object
        The geometry object defining the domain to be meshed.
    el_type : int, optional
        Element type identifier. Default is 2.
    el_size_factor : float, optional
        Factor controlling the element size. Default is 1.
    dofs_per_node : int, optional
        Number of degrees of freedom per node. Default is 1.
    gmsh_exec_path : str, optional
        Path to the GMSH executable. If None, uses system default. Default is None.
    clcurv : bool, optional
        Enable/disable curved element generation. Default is False.
    min_size : float, optional
        Minimum element size constraint. Default is None.
    max_size : float, optional
        Maximum element size constraint. Default is None.
    meshing_algorithm : int, optional
        GMSH meshing algorithm identifier. Default is None.
    additional_options : str, optional
        Additional GMSH options as a string. Default is ''.
    Returns
    -------
    mesh : object
        The generated mesh object containing nodes, elements, and connectivity information.
    Examples
    --------
    >>> mesh = create_mesh(geometry, el_type=3, el_size_factor=0.5)
    >>> mesh = create_mesh(geometry, min_size=0.1, max_size=1.0)
    """

    meshGen = GmshMeshGenerator(geometry, el_type, el_size_factor, dofs_per_node,
                                gmsh_exec_path, clcurv, min_size, max_size, meshing_algorithm,
                                additional_options)

    return meshGen.create()

error(msg)

Log error message

Source code in src/calfem/mesh.py
def error(msg):
    """Log error message"""
    cflog.error(msg)

info(msg)

Log information message

Source code in src/calfem/mesh.py
def info(msg):
    """Log information message"""
    cflog.info(msg)

trimesh2d(vertices, segments=None, holes=None, maxArea=None, quality=True, dofs_per_node=1, logFilename='tri.log', triangleExecutablePath=None)

Triangulates an area described by a number vertices (vertices) and a set of segments that describes a closed polygon.

Parameters:

vertices            array [nVertices x 2] with vertices describing the geometry.

                    [[v0_x, v0_y],
                     [   ...    ],
                     [vn_x, vn_y]]

segments            array [nSegments x 3] with segments describing the geometry.

                    [[s0_v0, s0_v1,marker],
                     [        ...        ],
                     [sn_v0, sn_v1,marker]]

holes               [Not currently used]

maxArea             Maximum area for triangle. (None)

quality             If true, triangles are prevented having angles < 30 degrees. (True)

dofs_per_node         Number of degrees of freedom per node.

logFilename         Filename for triangle output ("tri.log")

Returns:

coords              Node coordinates

                    [[n0_x, n0_y],
                     [   ...    ],
                     [nn_x, nn_y]]

edof                Element topology

                    [[el0_dof1, ..., el0_dofn],
                     [          ...          ],
                     [eln_dof1, ..., eln_dofn]]

dofs                Node dofs

                    [[n0_dof1, ..., n0_dofn],
                     [         ...         ],
                     [nn_dof1, ..., nn_dofn]]

bdofs               Boundary dofs. Dictionary containing lists of dofs for
                    each boundary marker. Dictionary key = marker id.
Source code in src/calfem/mesh.py
def trimesh2d(vertices, segments=None, holes=None, maxArea=None, quality=True, dofs_per_node=1, logFilename="tri.log", triangleExecutablePath=None):
    """
    Triangulates an area described by a number vertices (vertices) and a set
    of segments that describes a closed polygon. 

    Parameters:

        vertices            array [nVertices x 2] with vertices describing the geometry.

                            [[v0_x, v0_y],
                             [   ...    ],
                             [vn_x, vn_y]]

        segments            array [nSegments x 3] with segments describing the geometry.

                            [[s0_v0, s0_v1,marker],
                             [        ...        ],
                             [sn_v0, sn_v1,marker]]

        holes               [Not currently used]

        maxArea             Maximum area for triangle. (None)

        quality             If true, triangles are prevented having angles < 30 degrees. (True)

        dofs_per_node         Number of degrees of freedom per node.

        logFilename         Filename for triangle output ("tri.log")

    Returns:

        coords              Node coordinates

                            [[n0_x, n0_y],
                             [   ...    ],
                             [nn_x, nn_y]]

        edof                Element topology

                            [[el0_dof1, ..., el0_dofn],
                             [          ...          ],
                             [eln_dof1, ..., eln_dofn]]

        dofs                Node dofs

                            [[n0_dof1, ..., n0_dofn],
                             [         ...         ],
                             [nn_dof1, ..., nn_dofn]]

        bdofs               Boundary dofs. Dictionary containing lists of dofs for
                            each boundary marker. Dictionary key = marker id.

    """

    # Check for triangle executable

    triangleExecutable = triangleExecutablePath

    if triangleExecutable == None:
        triangleExecutable = ""
        if sys.platform == "win32":
            triangleExecutable = which("triangle.exe")
        else:
            triangleExecutable = which("triangle")
    else:
        if not os.path.exists(triangleExecutable):
            triangleExecutable = None

    if triangleExecutable == None:
        error("Error: Could not find triangle. Please make sure that the \ntriangle executable is available on the search path (PATH).")
        return None, None, None, None

    # Create triangle options

    options = ""

    if maxArea != None:
        options += "-a%f " % maxArea + " "
    if quality:
        options += "-q"

    # Set initial variables

    nSegments = 0
    nHoles = 0
    nAttribs = 0
    nBoundaryMarkers = 1
    nVertices = len(vertices)

    # All files are created as temporary files

    if not os.path.exists("./trimesh.temp"):
        os.mkdir("./trimesh.temp")

    filename = "./trimesh.temp/polyfile.poly"

    if not segments is None:
        nSegments = len(segments)

    if not holes is None:
        nHoles = len(holes)

    # Create a .poly file

    polyFile = open(filename, "w")
    polyFile.write("%d 2 %d \n" % (nVertices, nAttribs))

    i = 0

    for vertex in vertices:
        polyFile.write("%d %g %g\n" % (i, vertex[0], vertex[1]))
        i = i + 1

    polyFile.write("%d %d \n" % (nSegments, nBoundaryMarkers))

    i = 0

    for segment in segments:
        polyFile.write("%d %d %d %d\n" %
                       (i, segment[0], segment[1], segment[2]))
        i = i + 1

    polyFile.write("0\n")

    polyFile.close()

    # Execute triangle

    os.system("%s %s %s > tri.log" % (triangleExecutable, options, filename))

    # Read results from triangle

    strippedName = os.path.splitext(filename)[0]

    nodeFilename = "%s.1.node" % strippedName
    elementFilename = "%s.1.ele" % strippedName
    polyFilename = "%s.1.poly" % strippedName

    # Read vertices

    allVertices = None
    boundaryVertices = {}

    if os.path.exists(nodeFilename):
        nodeFile = open(nodeFilename, "r")
        nodeInfo = list(map(int, nodeFile.readline().split()))

        nNodes = nodeInfo[0]

        allVertices = np.zeros([nNodes, 2], 'd')

        for i in range(nNodes):
            vertexRow = list(map(float, nodeFile.readline().split()))

            boundaryMarker = int(vertexRow[3])

            if not (boundaryMarker in boundaryVertices):
                boundaryVertices[boundaryMarker] = []

            allVertices[i, :] = [vertexRow[1], vertexRow[2]]
            boundaryVertices[boundaryMarker].append(i+1)

        nodeFile.close()

    # Read elements

    elements = []

    if os.path.exists(elementFilename):
        elementFile = open(elementFilename, "r")
        elementInfo = list(map(int, elementFile.readline().split()))

        nElements = elementInfo[0]

        elements = np.zeros([nElements, 3], 'i')

        for i in range(nElements):
            elementRow = list(map(int, elementFile.readline().split()))
            elements[i, :] = [elementRow[1]+1,
                              elementRow[2]+1, elementRow[3]+1]

        elementFile.close()

    # Add dofs in edof and bcVerts

    dofs = cfc.createdofs(np.size(allVertices, 0), dofs_per_node)

    if dofs_per_node > 1:
        expandedElements = np.zeros(
            (np.size(elements, 0), 3*dofs_per_node), 'i')
        dofs = cfc.createdofs(np.size(allVertices, 0), dofs_per_node)

        elIdx = 0

        for elementTopo in elements:
            for i in range(3):
                expandedElements[elIdx, i*dofs_per_node:(
                    i*dofs_per_node+dofs_per_node)] = dofs[elementTopo[i]-1, :]
            elIdx += 1

        for bVertIdx in boundaryVertices.keys():
            bVert = boundaryVertices[bVertIdx]
            bVertNew = []
            for i in range(len(bVert)):
                for j in range(dofs_per_node):
                    bVertNew.append(dofs[bVert[i]-1][j])

            boundaryVertices[bVertIdx] = bVertNew

        return allVertices, np.asarray(expandedElements), dofs, boundaryVertices

    return allVertices, elements, dofs, boundaryVertices

Utility functions

This is a utility module for the CALFEM Python library. It contains various utility functions that is used throughout the library. It includes functions for reading and writing files, applying boundary conditions, displaying messages, and exporting data in different formats.

apply_bc(boundaryDofs, bcPrescr, bcVal, marker, value=0.0, dimension=0)

Apply boundary condition to bcPresc and bcVal matrices. For 2D problems with 2 dofs per node.

Parameters

boundaryDofs : dict Dictionary with boundary dofs. bcPresc : array_like 1-dim integer array containing prescribed dofs. bcVal : array_like 1-dim float array containing prescribed values. marker : int Boundary marker to assign boundary condition. value : float, optional Value to assign boundary condition. If not given 0.0 is assigned. dimension : int, optional dimension to apply bc. 0 - all, 1 - x, 2 - y

Returns

bcPresc : array_like Updated 1-dim integer array containing prescribed dofs. bcVal : array_like Updated 1-dim float array containing prescribed values.

Source code in src/calfem/utils.py
def apply_bc(boundaryDofs, bcPrescr, bcVal, marker, value=0.0, dimension=0):
    """
    Apply boundary condition to bcPresc and bcVal matrices. For 2D problems
    with 2 dofs per node.

    Parameters
    ----------
    boundaryDofs : dict
        Dictionary with boundary dofs.
    bcPresc : array_like
        1-dim integer array containing prescribed dofs.
    bcVal : array_like
        1-dim float array containing prescribed values.
    marker : int
        Boundary marker to assign boundary condition.
    value : float, optional
        Value to assign boundary condition.
        If not given 0.0 is assigned.
    dimension : int, optional
        dimension to apply bc. 0 - all, 1 - x, 2 - y

    Returns
    -------
    bcPresc : array_like
        Updated 1-dim integer array containing prescribed dofs.
    bcVal : array_like
        Updated 1-dim float array containing prescribed values.
    """

    if marker in boundaryDofs:
        if (dimension == 0):
            bcAdd = np.array(boundaryDofs[marker])
            bcAddVal = np.ones([np.size(bcAdd)])*value
        elif dimension in [1, 2]:
            bcAdd = boundaryDofs[marker][(dimension-1)::2]
            bcAddVal = np.ones([np.size(bcAdd)])*value
        else:
            print("Error: wrong dimension, ", dimension)

        newBcPrescr, prescrIdx = np.unique(
            np.hstack([bcPrescr, bcAdd]), return_index=True)
        newBcVal = np.hstack([bcVal, bcAddVal])[prescrIdx]

        return newBcPrescr, newBcVal
    else:
        print("Error: Boundary marker", marker, "does not exist.")

apply_bc_3d(boundaryDofs, bcPrescr, bcVal, marker, value=0.0, dimension=0)

Apply boundary condition to bcPresc and bcVal matrices. For 3D problems with 3 dofs per node.

Parameters

boundaryDofs : dict Dictionary with boundary dofs. bcPrescr : array_like 1-dim integer array containing prescribed dofs. bcVal : array_like 1-dim float array containing prescribed values. marker : int Boundary marker to assign boundary condition. value : float, optional Value to assign boundary condition. If not given 0.0 is assigned. dimension : int, optional dimension to apply bc. 0 - all, 1 - x, 2 - y, 3 - z

Returns

bcPrescr : array_like Updated 1-dim integer array containing prescribed dofs. bcVal : array_like Updated 1-dim float array containing prescribed values.

Source code in src/calfem/utils.py
def apply_bc_3d(boundaryDofs, bcPrescr, bcVal, marker, value=0.0, dimension=0):
    """
    Apply boundary condition to bcPresc and bcVal matrices. For 3D problems
    with 3 dofs per node.

    Parameters
    ----------
    boundaryDofs : dict
        Dictionary with boundary dofs.
    bcPrescr : array_like
        1-dim integer array containing prescribed dofs.
    bcVal : array_like
        1-dim float array containing prescribed values.
    marker : int
        Boundary marker to assign boundary condition.
    value : float, optional
        Value to assign boundary condition.
        If not given 0.0 is assigned.
    dimension : int, optional
        dimension to apply bc. 0 - all, 1 - x, 2 - y,
        3 - z

    Returns
    -------
    bcPrescr : array_like
        Updated 1-dim integer array containing prescribed dofs.
    bcVal : array_like
        Updated 1-dim float array containing prescribed values.
    """

    if marker in boundaryDofs:
        if (dimension == 0):
            bcAdd = np.array(boundaryDofs[marker])
            bcAddVal = np.ones([np.size(bcAdd)])*value
        elif dimension in [1, 2, 3]:
            bcAdd = boundaryDofs[marker][(dimension-1)::3]
            bcAddVal = np.ones([np.size(bcAdd)])*value
        else:
            print("Error: wrong dimension, ", dimension)

        newBcPrescr, prescrIdx = np.unique(
            np.hstack([bcPrescr, bcAdd]), return_index=True)
        newBcVal = np.hstack([bcVal, bcAddVal])[prescrIdx]

        return newBcPrescr, newBcVal
    else:
        print("Error: Boundary marker", marker, "does not exist.")

apply_bc_node(nodeIdx, dofs, bcPrescr, bcVal, value=0.0, dimension=0)

Apply boundary conditions to a specific node. This function adds boundary condition prescriptions and values for a given node to existing boundary condition arrays.

Parameters

nodeIdx : int Index of the node to apply boundary conditions to. dofs : array_like Degrees of freedom array. Can be 1D (for single DOF per node) or 2D (for multiple DOFs per node). bcPrescr : array_like Existing array of prescribed boundary condition DOF indices. bcVal : array_like Existing array of prescribed boundary condition values. value : float, optional Value to prescribe for the boundary condition. Default is 0.0. dimension : int, optional Dimension/direction to apply BC. If 0, applies to all DOFs of the node. If 1, 2, or 3, applies to specific dimension (1-indexed). Default is 0.

Returns

tuple of numpy.ndarray A tuple containing: - Updated prescribed DOF indices array (bcPrescr concatenated with new DOFs) - Updated prescribed values array (bcVal concatenated with new values)

Notes

When dimension=0, boundary conditions are applied to all degrees of freedom for the specified node. When dimension is 1, 2, or 3, the boundary condition is applied only to that specific dimension (using 1-based indexing).

Examples

Apply BC to all DOFs of node 5 with value 0.0

bc_dofs, bc_vals = apply_bc_node(5, dofs, [], [], 0.0, 0)

Apply BC to x-direction (dimension 1) of node 10 with value 5.0

bc_dofs, bc_vals = apply_bc_node(10, dofs, bc_dofs, bc_vals, 5.0, 1)

Source code in src/calfem/utils.py
def apply_bc_node(nodeIdx, dofs, bcPrescr, bcVal, value=0.0, dimension=0):
    """
    Apply boundary conditions to a specific node.
    This function adds boundary condition prescriptions and values for a given node
    to existing boundary condition arrays.

    Parameters
    ----------
    nodeIdx : int
        Index of the node to apply boundary conditions to.
    dofs : array_like
        Degrees of freedom array. Can be 1D (for single DOF per node) or 2D 
        (for multiple DOFs per node).
    bcPrescr : array_like
        Existing array of prescribed boundary condition DOF indices.
    bcVal : array_like
        Existing array of prescribed boundary condition values.
    value : float, optional
        Value to prescribe for the boundary condition. Default is 0.0.
    dimension : int, optional
        Dimension/direction to apply BC. If 0, applies to all DOFs of the node.
        If 1, 2, or 3, applies to specific dimension (1-indexed). Default is 0.

    Returns
    -------
    tuple of numpy.ndarray
        A tuple containing:
        - Updated prescribed DOF indices array (bcPrescr concatenated with new DOFs)
        - Updated prescribed values array (bcVal concatenated with new values)

    Notes
    -----
    When dimension=0, boundary conditions are applied to all degrees of freedom
    for the specified node. When dimension is 1, 2, or 3, the boundary condition
    is applied only to that specific dimension (using 1-based indexing).

    Examples
    --------
    >>> # Apply BC to all DOFs of node 5 with value 0.0
    >>> bc_dofs, bc_vals = apply_bc_node(5, dofs, [], [], 0.0, 0)
    >>> 
    >>> # Apply BC to x-direction (dimension 1) of node 10 with value 5.0
    >>> bc_dofs, bc_vals = apply_bc_node(10, dofs, bc_dofs, bc_vals, 5.0, 1)
    """

    if (dimension == 0):
        bcAdd = np.asarray(dofs[nodeIdx])
        bcAddVal = np.ones([np.size(bcAdd)])*value
    elif dimension in [1, 2, 3]:
        bcAdd = np.asarray(dofs[nodeIdx, dimension-1])
        bcAddVal = np.ones([np.size(bcAdd)])*value
    else:
        print("Error: wrong dimension, ", dimension)

    return np.hstack([bcPrescr, bcAdd]), np.hstack([bcVal, bcAddVal])

apply_force(boundaryDofs, f, marker, value=0.0, dimension=0)

Apply boundary force to f matrix. The value is added to all boundaryDofs defined by marker. Applicable to 2D problems with 2 dofs per node.

Parameters:

boundaryDofs        Dictionary with boundary dofs.
f                   force matrix.
marker              Boundary marker to assign boundary condition.
value               Value to assign boundary condition.
                    If not given 0.0 is assigned.
dimension           dimension to apply force. 0 - all, 1 - x, 2 - y
Source code in src/calfem/utils.py
def apply_force(boundaryDofs, f, marker, value=0.0, dimension=0):
    """
    Apply boundary force to f matrix. The value is
    added to all boundaryDofs defined by marker. Applicable
    to 2D problems with 2 dofs per node.

    Parameters:

        boundaryDofs        Dictionary with boundary dofs.
        f                   force matrix.
        marker              Boundary marker to assign boundary condition.
        value               Value to assign boundary condition.
                            If not given 0.0 is assigned.
        dimension           dimension to apply force. 0 - all, 1 - x, 2 - y

    """

    if marker in boundaryDofs:
        if dimension == 0:
            f[np.asarray(boundaryDofs[marker])-1] += value
        elif dimension in [1, 2]:
            f[np.asarray(boundaryDofs[marker][(dimension-1)::2])-1] += value
        else:
            print("Error: The dimension, ", dimension, ", is invalid")
    else:
        print("Error: Boundary marker", marker, "does not exist.")

apply_force_3d(boundaryDofs, f, marker, value=0.0, dimension=0)

Apply boundary force to f matrix for 3D problems. The value is added to all boundaryDofs defined by marker. Applicable to 3D problems with 3 degrees of freedom per node. Parameters


boundaryDofs : dict Dictionary with boundary degrees of freedom. f : numpy.ndarray Force matrix to be modified. marker : int or str Boundary marker to identify which boundary condition to apply. value : float, optional Value to add to the force matrix at specified boundary DOFs. Default is 0.0. dimension : int, optional Dimension to apply force: * 0 - all dimensions (default) * 1 - x-direction only * 2 - y-direction only
* 3 - z-direction only Notes


If the specified marker does not exist in boundaryDofs, an error message is printed. If an invalid dimension is specified (not 0, 1, 2, or 3), an error message is printed. Examples


boundaryDofs = {1: [1, 2, 3, 4, 5, 6]} f = np.zeros(6) apply_force_3d(boundaryDofs, f, 1, value=100.0, dimension=1)

Applies force of 100.0 in x-direction to DOFs 1, 4

Source code in src/calfem/utils.py
def apply_force_3d(boundaryDofs, f, marker, value=0.0, dimension=0):
    """
    Apply boundary force to f matrix for 3D problems.
    The value is added to all boundaryDofs defined by marker. Applicable
    to 3D problems with 3 degrees of freedom per node.
    Parameters
    ----------
    boundaryDofs : dict
        Dictionary with boundary degrees of freedom.
    f : numpy.ndarray
        Force matrix to be modified.
    marker : int or str
        Boundary marker to identify which boundary condition to apply.
    value : float, optional
        Value to add to the force matrix at specified boundary DOFs.
        Default is 0.0.
    dimension : int, optional
        Dimension to apply force:
        * 0 - all dimensions (default)
        * 1 - x-direction only
        * 2 - y-direction only  
        * 3 - z-direction only
    Notes
    -----
    If the specified marker does not exist in boundaryDofs, an error message
    is printed. If an invalid dimension is specified (not 0, 1, 2, or 3),
    an error message is printed.
    Examples
    --------
    >>> boundaryDofs = {1: [1, 2, 3, 4, 5, 6]}
    >>> f = np.zeros(6)
    >>> apply_force_3d(boundaryDofs, f, 1, value=100.0, dimension=1)
    # Applies force of 100.0 in x-direction to DOFs 1, 4
    """

    if marker in boundaryDofs:
        if dimension == 0:
            f[np.asarray(boundaryDofs[marker])-1] += value
        elif dimension in [1, 2, 3]:
            f[np.asarray(boundaryDofs[marker][(dimension-1)::3])-1] += value
        else:
            print("Error: The dimension, ", dimension, ", is invalid")
    else:
        print("Error: Boundary marker", marker, "does not exist.")

apply_force_node(nodeIdx, dofs, f, value=0.0, dimension=0)

Apply a force to a specific node in the finite element model. This function adds a force value to the global force vector at the degrees of freedom corresponding to a specified node. The force can be applied to all DOFs of the node or to a specific dimension. Parameters


nodeIdx : int Index of the node where the force is to be applied. dofs : array_like Degrees of freedom array that maps nodes to their DOF indices in the global system. Can be 1D (for single DOF per node) or 2D (for multiple DOFs per node). f : array_like Global force vector where the force will be added. value : float, optional Magnitude of the force to be applied. Default is 0.0. dimension : int, optional Specific dimension/DOF to apply the force to. If 0, applies to all DOFs of the node. If 1 or higher, applies to the specified dimension (1-indexed). Default is 0. Notes


  • When dimension=0, the force is applied to all DOFs of the node (assumes 1D dofs array)
  • When dimension>=1, the force is applied to the specific dimension of the node (assumes 2D dofs array with shape [node, dimension])
  • The dimension parameter uses 1-based indexing (dimension=1 corresponds to first DOF) Examples

Apply force to all DOFs of node 5

apply_force_node(5, dofs, f, value=100.0, dimension=0)

Apply force to x-direction (dimension 1) of node 3

apply_force_node(3, dofs, f, value=50.0, dimension=1)

Source code in src/calfem/utils.py
def apply_force_node(nodeIdx, dofs, f, value=0.0, dimension=0):
    """
    Apply a force to a specific node in the finite element model.
    This function adds a force value to the global force vector at the degrees of freedom
    corresponding to a specified node. The force can be applied to all DOFs of the node
    or to a specific dimension.
    Parameters
    ----------
    nodeIdx : int
        Index of the node where the force is to be applied.
    dofs : array_like
        Degrees of freedom array that maps nodes to their DOF indices in the global system.
        Can be 1D (for single DOF per node) or 2D (for multiple DOFs per node).
    f : array_like
        Global force vector where the force will be added.
    value : float, optional
        Magnitude of the force to be applied. Default is 0.0.
    dimension : int, optional
        Specific dimension/DOF to apply the force to. If 0, applies to all DOFs of the node.
        If 1 or higher, applies to the specified dimension (1-indexed). Default is 0.
    Notes
    -----
    - When dimension=0, the force is applied to all DOFs of the node (assumes 1D dofs array)
    - When dimension>=1, the force is applied to the specific dimension of the node
      (assumes 2D dofs array with shape [node, dimension])
    - The dimension parameter uses 1-based indexing (dimension=1 corresponds to first DOF)
    Examples
    --------
    >>> # Apply force to all DOFs of node 5
    >>> apply_force_node(5, dofs, f, value=100.0, dimension=0)
    >>> # Apply force to x-direction (dimension 1) of node 3
    >>> apply_force_node(3, dofs, f, value=50.0, dimension=1)
    """

    if (dimension == 0):
        f[dofs[nodeIdx]] += value
    elif (dimension == 1):
        f[dofs[nodeIdx, dimension-1]] += value
    else:
        f[dofs[nodeIdx, dimension-1]] += value

apply_force_total(boundaryDofs, f, marker, value=0.0, dimension=0)

Apply boundary force to f matrix. Total force, value, is distributed over all boundaryDofs defined by marker. Applicable to 2D problems with 2 dofs per node.

Parameters

boundaryDofs : dict Dictionary with boundary dofs. f : array_like Force matrix. marker : int Boundary marker to assign boundary condition. value : float, optional Total force value to assign boundary condition. If not given 0.0 is assigned. dimension : int, optional Dimension to apply force. 0 - all, 1 - x, 2 - y

Source code in src/calfem/utils.py
def apply_force_total(boundaryDofs, f, marker, value=0.0, dimension=0):
    """
    Apply boundary force to f matrix. Total force, value, is
    distributed over all boundaryDofs defined by marker. Applicable
    to 2D problems with 2 dofs per node.

    Parameters
    ----------
    boundaryDofs : dict
        Dictionary with boundary dofs.
    f : array_like
        Force matrix.
    marker : int
        Boundary marker to assign boundary condition.
    value : float, optional
        Total force value to assign boundary condition.
        If not given 0.0 is assigned.
    dimension : int, optional
        Dimension to apply force. 0 - all, 1 - x, 2 - y

    """

    if marker in boundaryDofs:
        if dimension == 0:
            nDofs = len(boundaryDofs[marker])
            valuePerDof = value / nDofs
            f[np.asarray(boundaryDofs[marker])-1] += valuePerDof
        elif dimension in [1, 2]:
            nDofs = len(boundaryDofs[marker][(dimension-1)::2])
            valuePerDof = value / nDofs
            f[np.asarray(boundaryDofs[marker][(dimension-1)::2]) -
              1] += valuePerDof
        else:
            print("Error: The dimension, ", dimension, ", is invalid")
    else:
        print("Error: Boundary marker", marker, "does not exist.")

apply_force_total_3d(boundaryDofs, f, marker, value=0.0, dimension=0)

Apply boundary force to f matrix. Total force, value, is distributed over all boundaryDofs defined by marker. Applicable to 3D problems with 3 dofs per node.

Parameters

boundaryDofs : dict Dictionary with boundary dofs. f : array_like Force matrix. marker : int Boundary marker to assign boundary condition. value : float, optional Total force value to assign boundary condition. If not given 0.0 is assigned. dimension : int, optional Dimension to apply force. 0 - all, 1 - x, 2 - y, 3 - z

Source code in src/calfem/utils.py
def apply_force_total_3d(boundaryDofs, f, marker, value=0.0, dimension=0):
    """
    Apply boundary force to f matrix. Total force, value, is
    distributed over all boundaryDofs defined by marker. Applicable
    to 3D problems with 3 dofs per node.

    Parameters
    ----------
    boundaryDofs : dict
        Dictionary with boundary dofs.
    f : array_like
        Force matrix.
    marker : int
        Boundary marker to assign boundary condition.
    value : float, optional
        Total force value to assign boundary condition.
        If not given 0.0 is assigned.
    dimension : int, optional
        Dimension to apply force. 0 - all, 1 - x, 2 - y,
        3 - z

    """

    if marker in boundaryDofs:
        if dimension == 0:
            nDofs = len(boundaryDofs[marker])
            valuePerDof = value / nDofs
            f[np.asarray(boundaryDofs[marker])-1] += valuePerDof
        elif dimension in [1, 2, 3]:
            nDofs = len(boundaryDofs[marker][(dimension-1)::3])
            valuePerDof = value / nDofs
            f[np.asarray(boundaryDofs[marker][(dimension-1)::3]) -
              1] += valuePerDof
        else:
            print("Error: The dimension, ", dimension, ", is invalid")
    else:
        print("Error: Boundary marker", marker, "does not exist.")

apply_traction_linear_element(boundaryElements, coords, dofs, F, marker, q)

Apply traction on part of boundary with marker.

q is added to all boundaryDofs defined by marker. Applicable to 2D problems with 2 dofs per node. The function works with linear line elements. (elm-type 1 in GMSH).

Parameters

boundaryElements : dict Dictionary with boundary elements, the key is a marker and the values are lists of elements. coords : array_like Coordinates matrix dofs : array_like Dofs matrix F : array_like force matrix. marker : int Boundary marker to assign boundary condition. q : array_like Value to assign boundary condition. shape = [qx qy] in global coordinates

Source code in src/calfem/utils.py
def apply_traction_linear_element(boundaryElements, coords, dofs, F, marker, q):
    """
    Apply traction on part of boundary with marker.

    q is added to all boundaryDofs defined by marker. Applicable
    to 2D problems with 2 dofs per node. The function works with linear
    line elements. (elm-type 1 in GMSH).

    Parameters
    ----------
    boundaryElements : dict
        Dictionary with boundary elements, the key is a marker and the values are lists of elements.
    coords : array_like
        Coordinates matrix
    dofs : array_like
        Dofs matrix
    F : array_like
        force matrix.
    marker : int
        Boundary marker to assign boundary condition.
    q : array_like
        Value to assign boundary condition.
        shape = [qx qy] in global coordinates

    """
    if marker not in boundaryElements:
        print("Error: Boundary marker", marker, "does not exist.")
        return
    for element in boundaryElements[marker]:
        if element['elm-type'] != 1:
            print("Error: Wrong element type.")
            return

    q = np.matrix(q).T

    # Integration points and weights:
    Xi = [-1/np.sqrt(3), 1/np.sqrt(3)]
    W = [1, 1]

    # Shape functions:
    def N1(x): return 1-(1+x)/2
    def N2(x): return (1+x)/2

    for element in boundaryElements[marker]:
        # Loop through integration points:
        f = np.zeros([4, 1])
        for xi, w in zip(Xi, W):
            N = np.matrix([[N1(xi),      0,  N2(xi),       0],
                           [0, N1(xi),        0, N2(xi)]])
            # The minus one is since the nodes in node-number-list start at 1...
            coord = coords[np.array(element['node-number-list'])-1]
            v1 = coord[0, :]
            v2 = coord[1, :]
            J = np.linalg.norm(v1-v2) / 2
            f += w * N.T * q * J

        # Minus one since dofs start at 1...
        idx = dofs[np.array(element['node-number-list'])-1, :].flatten()-1
        F[idx] += f

calc_bar_displ_limits(a, coords, edof, dofs)

Calculate max and min global displacements for bars.

Parameters

a : array_like Global displacement array with 3 dofs / node. coords : array_like Node coordinates. edof : array_like Bar topology. dofs : array_like Node dofs.

Returns

tuple Tuple containing (min_displ, max_displ).

Source code in src/calfem/utils.py
def calc_bar_displ_limits(a, coords, edof, dofs):
    """
    Calculate max and min global displacements for bars.

    Parameters
    ----------
    a : array_like
        Global displacement array with 3 dofs / node.
    coords : array_like
        Node coordinates.
    edof : array_like
        Bar topology.
    dofs : array_like
        Node dofs.

    Returns
    -------
    tuple
        Tuple containing (min_displ, max_displ).
    """

    if edof.shape[0]>0:

        ex, ey, ez = cfc.coord_extract(edof, coords, dofs)
        ed = cfc.extract_eldisp(edof, a)

        coords, topo, node_dofs = convert_to_node_topo(edof, ex, ey, ez, n_dofs_per_node=3, ignore_first=False)

        min_displ = 1e300
        max_displ = -1e300

        for el_topo in topo:
            d0 = np.array(a[node_dofs[el_topo[0]][:3]-1]).flatten()
            d1 = np.array(a[node_dofs[el_topo[1]][:3]-1]).flatten()

            l_d0 = np.linalg.norm(d0)
            l_d1 = np.linalg.norm(d1)

            if l_d0>max_displ:
                max_displ = l_d0
            if l_d1>max_displ:
                max_displ = l_d1
            if l_d0<min_displ:
                min_displ = l_d0
            if l_d1<min_displ:
                min_displ = l_d1

        return min_displ, max_displ

    else:

        return 0.0, 0.0

calc_beam_displ_limits(a, coords, edof, dofs)

Calculate max and min displacements for beams.

Parameters

a : array_like Global displacement array with 6 dofs / node. coords : array_like Node coordinates. edof : array_like Beam topology. dofs : array_like Node dofs.

Returns

tuple Tuple containing (min_displ, max_displ).

Source code in src/calfem/utils.py
def calc_beam_displ_limits(a, coords, edof, dofs):
    """
    Calculate max and min displacements for beams.

    Parameters
    ----------
    a : array_like
        Global displacement array with 6 dofs / node.
    coords : array_like
        Node coordinates.
    edof : array_like
        Beam topology.
    dofs : array_like
        Node dofs.

    Returns
    -------
    tuple
        Tuple containing (min_displ, max_displ).
    """

    if edof.shape[0]>0:

        ex, ey, ez = cfc.coord_extract(edof, coords, dofs)
        ed = cfc.extract_eldisp(edof, a)

        coords, topo, node_dofs = convert_to_node_topo(edof, ex, ey, ez, n_dofs_per_node=6, ignore_first=False)

        min_displ = 1e300
        max_displ = -1e300

        for el_topo in topo:
            d0 = np.array(a[node_dofs[el_topo[0]][:3]-1]).flatten()
            d1 = np.array(a[node_dofs[el_topo[1]][:3]-1]).flatten()

            l_d0 = np.linalg.norm(d0) 
            l_d1 = np.linalg.norm(d1) 

            if l_d0>max_displ:
                max_displ = l_d0
            if l_d1>max_displ:
                max_displ = l_d1
            if l_d0<min_displ:
                min_displ = l_d0
            if l_d1<min_displ:
                min_displ = l_d1

        return min_displ, max_displ

    else: 

        return 0.0, 0.0

calc_limits(coords)

Calculate max an min limits of 3d coordinates

Source code in src/calfem/utils.py
def calc_limits(coords):
    """Calculate max an min limits of 3d coordinates"""

    if coords.shape[0]>0:

        max_x = np.max(coords[:,0])
        min_x = np.min(coords[:,0])
        max_y = np.max(coords[:,1])
        min_y = np.min(coords[:,1])
        max_z = np.max(coords[:,2])
        min_z = np.min(coords[:,2])

        return max_x, min_x, max_y, min_y, max_z, min_z
    else:
        return 0.0, 0.0, 0.0, 0.0, 0.0, 0.0

calc_size(coords)

Calculate max and min sizes of 3d coordinates.

Source code in src/calfem/utils.py
def calc_size(coords):
    """Calculate max and min sizes of 3d coordinates."""

    max_x, min_x, max_y, min_y, max_z, min_z = calc_limits(coords)
    lx = max_x - min_x
    ly = max_y - min_y
    lz = max_z - min_z

    return lx, ly, lz

convert_to_node_topo(edof, ex, ey, ez, n_dofs_per_node=3, ignore_first=True)

Routine to convert dof based topology and element coordinates to node based topology required for visualisation with VTK and other visualisation frameworks

Parameters

edof : array_like Element topology [nel x (n_dofs_per_node)|(n_dofs_per_node+1)*n_nodes ] ex : array_like Element x coordinates [nel x n_nodes] ey : array_like Element y coordinates [nel x n_nodes] ez : array_like Element z coordinates [nel x n_nodes] n_dofs_per_node : int, optional Number of dofs per node. Default is 3. ignore_first : bool, optional Ignore first column of edof. Default is True.

Returns

coords : numpy.ndarray Array of node coordinates. [n_nodes x 3] topo : numpy.ndarray Node topology. [nel x n_nodes] node_dofs : numpy.ndarray Dofs for each node. [n_nodes x n_dofs_per_node]

Source code in src/calfem/utils.py
def convert_to_node_topo(edof, ex, ey, ez, n_dofs_per_node=3, ignore_first=True):
    """
    Routine to convert dof based topology and element coordinates to node based
    topology required for visualisation with VTK and other visualisation frameworks

    Parameters
    ----------
    edof : array_like
        Element topology [nel x (n_dofs_per_node)|(n_dofs_per_node+1)*n_nodes ]
    ex : array_like
        Element x coordinates [nel x n_nodes]
    ey : array_like
        Element y coordinates [nel x n_nodes]
    ez : array_like
        Element z coordinates [nel x n_nodes]
    n_dofs_per_node : int, optional
        Number of dofs per node. Default is 3.
    ignore_first : bool, optional
        Ignore first column of edof. Default is True.

    Returns
    -------
    coords : numpy.ndarray
        Array of node coordinates. [n_nodes x 3]
    topo : numpy.ndarray
        Node topology. [nel x n_nodes]
    node_dofs : numpy.ndarray
        Dofs for each node. [n_nodes x n_dofs_per_node]
    """

    node_hash_coords = {}
    node_hash_numbers = {}
    node_hash_dofs = {}
    el_hash_dofs = []

    nel, cols = edof.shape

    if ignore_first:
        tot_dofs = cols-1
    else:
        tot_dofs = cols

    n_nodes = int(tot_dofs / n_dofs_per_node)

    for elx, ely, elz, dofs in zip(ex, ey, ez, edof):

        if ignore_first:
            el_dofs = dofs[1:]
        else:
            el_dofs = dofs

        # 0 1 2  3 4 5  6 7 8  9 12 11 

        el_dof = np.zeros((n_nodes, n_dofs_per_node), dtype=int)
        el_hash_topo = []

        for i in range(n_nodes):
            el_dof[i] = el_dofs[ (i*n_dofs_per_node):((i+1)*n_dofs_per_node) ]
            node_hash_coords[hash(tuple(el_dof[i]))] = [elx[i], ely[i], elz[i]]
            node_hash_numbers[hash(tuple(el_dof[i]))] = -1
            node_hash_dofs[hash(tuple(el_dof[i]))] = el_dof[i]
            el_hash_topo.append(hash(tuple(el_dof[i])))

        el_hash_dofs.append(el_hash_topo)

    coord_count = 0

    coords = []
    node_dofs = []

    for node_hash in node_hash_numbers.keys():
        node_hash_numbers[node_hash] = coord_count
        node_dofs.append(list(node_hash_dofs[node_hash]))
        coord_count +=1

        coords.append(node_hash_coords[node_hash])

    topo = []

    for el_hashes in el_hash_dofs:
        el_topo = []
        for i in range(n_nodes):
            el_topo.append(node_hash_numbers[el_hashes[i]])

        topo.append(el_topo)

    return np.array(coords), np.array(topo), np.array(node_dofs)

disp_array(a, headers=[], fmt='.4e', tablefmt='psql', showindex=False)

Print a numpy array in a nice way.

Source code in src/calfem/utils.py
def disp_array(a, headers=[], fmt=".4e", tablefmt="psql", showindex=False):
    """
    Print a numpy array in a nice way.
    """
    if type_of_script() == 'jupyter':
        display(tab.tabulate(np.asarray(a), tablefmt="html", floatfmt=".4e", showindex=showindex, headers=headers))
    else:
        print(tab.tabulate(np.asarray(a), tablefmt=tablefmt, floatfmt=fmt, showindex=showindex, headers=headers))

export_vtk_stress(filename, coords, topo, a=None, el_scalar=None, el_vec1=None, el_vec2=None)

Export mesh and results for a 2D stress problem.

Parameters

filename : str Filename of vtk-file coords : numpy.ndarray Element coordinates topo : numpy.ndarray Element topology (not dof topology). mesh.topo. a : numpy.ndarray, optional Element displacements 2-dof el_scalar : list, optional Scalar values for each element el_vec1 : list, optional Vector value for each element el_vec2 : list, optional Vector value for each element

Source code in src/calfem/utils.py
def export_vtk_stress(filename, coords, topo, a=None, el_scalar=None, el_vec1=None, el_vec2=None):
    """
    Export mesh and results for a 2D stress problem.

    Parameters
    ----------
    filename : str
        Filename of vtk-file
    coords : numpy.ndarray
        Element coordinates
    topo : numpy.ndarray
        Element topology (not dof topology). mesh.topo.
    a : numpy.ndarray, optional
        Element displacements 2-dof
    el_scalar : list, optional
        Scalar values for each element
    el_vec1 : list, optional
        Vector value for each element
    el_vec2 : list, optional
        Vector value for each element
    """

    points = np.zeros([coords.shape[0], 3], dtype=np.float64)
    points[:,0:2] = coords
    points = points.tolist()
    polygons = (topo-1).tolist()

    displ = []

    point_data = None
    scalars = None
    vectors1 = None
    vectors2 = None
    cell_data = None

    if a is not None:
        for i in range(0, len(a), 2):
            displ.append([a[i].item(), a[i+1].item(), 0.0])

        point_data = vtk.PointData(vtk.Vectors(displ, name="displacements"))

    if el_scalar is not None:
        print("Adding cell scalars...")
        scalars = vtk.Scalars(el_scalar, name="scalar")
    if el_vec1 is not None:
        print("Adding cell vector 1...")
        vectors1 = vtk.Vectors(el_vec1, name="principal1")
    if el_vec2 is not None:
        print("Adding cell vector 2...")
        vectors2 = vtk.Vectors(el_vec2, name="principal2")

    if el_scalar is not None and el_vec1 is None and el_vec2 is None:
        print("Exporting celldata, el_scalar...")
        cell_data = vtk.CellData(scalars)
    if el_scalar is not None and el_vec1 is None and el_vec2 is not None:
        print("Exporting celldata, el_scalar, el_vec2...")
        cell_data = vtk.CellData(scalars, vectors2)
    if el_scalar is not None and el_vec1 is not None and el_vec2 is None:
        print("Exporting celldata, el_scalar, el_vec1...")
        cell_data = vtk.CellData(scalars, vectors1)
    if el_scalar is not None and el_vec1 is not None and el_vec2 is not None:
        print("Exporting celldata, el_scalar, el_vec1, el_vec2...")
        cell_data = vtk.CellData(scalars, vectors1, vectors2)
    if el_scalar is None and el_vec1 is None and el_vec2 is not None:
        print("Exporting celldata, el_vec2...")
        cell_data = vtk.CellData(vectors2)
    if el_scalar is None and el_vec1 is not None and el_vec2 is None:
        print("Exporting celldata, el_vec1...")
        cell_data = vtk.CellData(vectors1)
    if el_scalar is None and el_vec1 is not None and el_vec2 is None:
        print("Exporting celldata, el_vec1, el_vec2...")
        cell_data = vtk.CellData(vectors1, vectors2)

    structure = vtk.PolyData(points=points, polygons=polygons)

    if cell_data is not None and point_data is not None:
        print("VTK includes cell_data and point_data")
        vtk_data = vtk.VtkData(structure, cell_data, point_data)
    if cell_data is None and point_data is not None:
        print("VTK includes point_data")
        vtk_data = vtk.VtkData(structure, point_data)
    if cell_data is None and point_data is None:
        print("VTK includes only structure")
        vtk_data = vtk.VtkData(structure)

    vtk_data.tofile("exm6.vtk", "ascii")

load_arrays(name)

Load arrays from file.

Source code in src/calfem/utils.py
def load_arrays(name):
    """Load arrays from file."""

    with open(name, 'rb') as file:
        coords = pickle.load(file)
        edof = pickle.load(file)
        dofs = pickle.load(file)
        bdofs = pickle.load(file)
        elementmarkers = pickle.load(file)
        boundaryElements = pickle.load(file)
        markerDict = pickle.load(file)

    return coords, edof, dofs, bdofs, elementmarkers, boundaryElements, markerDict

load_geometry(name)

Loads a geometry from a file.

Source code in src/calfem/utils.py
def load_geometry(name):
    """Loads a geometry from a file."""

    with open(name, 'rb') as file:
        test = pickle.load(file)
    return test

load_mesh(name)

Load a mesh from file.

Source code in src/calfem/utils.py
def load_mesh(name):
    """Load a mesh from file."""

    with open(name, 'rb') as file:
        mesh = pickle.load(file)
    return mesh

read_float(f)

Read a row from file, f, and return a list of floats.

Source code in src/calfem/utils.py
def read_float(f):
    """
    Read a row from file, f, and return a list of floats.
    """
    return list(map(float, f.readline().split()))

read_int(f)

Read a row from file, f, and return a list of integers.

Source code in src/calfem/utils.py
def read_int(f):
    """
    Read a row from file, f, and return a list of integers.
    """
    return list(map(int, f.readline().split()))

read_single_float(f)

Read a single float from a row in file f. All other values on row are discarded.

Source code in src/calfem/utils.py
def read_single_float(f):
    """
    Read a single float from a row in file f. All other values on row are discarded.
    """
    return readFloat(f)[0]

read_single_int(f)

Read a single integer from a row in file f. All other values on row are discarded.

Source code in src/calfem/utils.py
def read_single_int(f):
    """
    Read a single integer from a row in file f. All other values on row are discarded.
    """
    return readInt(f)[0]

save_arrays(coords, edof, dofs, bdofs, elementmarkers, boundaryElements, markerDict, name='unnamed_arrays')

Save arrays to file.

Source code in src/calfem/utils.py
def save_arrays(coords, edof, dofs, bdofs, elementmarkers, boundaryElements, markerDict, name="unnamed_arrays"):
    """Save arrays to file."""

    if not name.endswith(".cfma"):
        name = name + ".cfma"
    with open(name, 'wb') as file:
        pickle.dump(coords, file)
        pickle.dump(edof, file)
        pickle.dump(dofs, file)
        #for key in bdofs.items():
        #    print(key, markerDict[key])
        pickle.dump(bdofs, file)
        pickle.dump(elementmarkers, file)
        pickle.dump(boundaryElements, file)
        pickle.dump(markerDict, file)

save_geometry(g, name='unnamed_geometry')

Save a geometry to file.

Source code in src/calfem/utils.py
def save_geometry(g, name="unnamed_geometry"):
    """Save a geometry to file."""

    if not name.endswith(".cfg"):
        name = name + ".cfg"
    with open(name, 'wb') as file:
        pickle.dump(g, file)

save_matlab_arrays(coords, edof, dofs, bdofs, elementmarkers, boundaryElements, markerDict, name='Untitled')

Save arrays as MATLAB .mat files.

Source code in src/calfem/utils.py
def save_matlab_arrays(coords, edof, dofs, bdofs, elementmarkers, boundaryElements, markerDict, name="Untitled"):
    """Save arrays as MATLAB .mat files."""

    if not name.endswith(".mat"):
        name = name + ".mat"
    saveDict = {}
    saveDict["coords"] = coords.astype('double')

    # Convert to CALFEM Edof definition with element number as first index

    new_column = np.arange(1, np.size(edof, 0) + 1)[:, np.newaxis]
    edof = np.append(new_column, edof, axis=1)

    saveDict["edof"] = edof.astype('double')
    saveDict["dofs"] = dofs.astype('double')
    # bdofs = {str(k): v for k, v in bdofs.items()} # MATLAB struct needs keys as strings
    print(bdofs)
    print(markerDict)
    newBdof = {}
    for index, bdofs in bdofs.items():
        print(index, bdofs)
        if index == 0:
            newBdof["None"] = bdofs
        else:
            newBdof[markerDict[index]] = bdofs

    saveDict["bdofs"] = newBdof
    elementmarkers = np.asarray(elementmarkers)

    # To avoid problems with one indexing in MATLAB

    elementmarkers = elementmarkers + 1
    saveDict["elementmarkers"] = elementmarkers
    scipy.io.savemat(name, saveDict)

save_mesh(mesh, name='Untitled')

Save a mesh to file.

Source code in src/calfem/utils.py
def save_mesh(mesh, name="Untitled"):
    """Save a mesh to file."""

    if not name.endswith(".cfm"):
        name = name + ".cfm"
    with open(name, 'wb') as file:
        pickle.dump(mesh, file)

scalfact2(ex, ey, ed, rat=0.2)

Determine scale factor for drawing computational results, such as displacements, section forces or flux.

Parameters

ex : array_like Element node x coordinates ey : array_like
Element node y coordinates ed : array_like Element displacement matrix or section force matrix rat : float, optional Relation between illustrated quantity and element size. Default is 0.2.

Returns

float Scale factor for drawing computational results

Source code in src/calfem/utils.py
def scalfact2(ex, ey, ed, rat=0.2):
    """
    Determine scale factor for drawing computational results, such as 
    displacements, section forces or flux.

    Parameters
    ----------
    ex : array_like
        Element node x coordinates
    ey : array_like  
        Element node y coordinates
    ed : array_like
        Element displacement matrix or section force matrix
    rat : float, optional
        Relation between illustrated quantity and element size. 
        Default is 0.2.

    Returns
    -------
    float
        Scale factor for drawing computational results
    """
    # nen:   number of element nodes
    # nel:   number of elements
    nen = -1
    if ex.shape != ey.shape:
        print("ex and ey shapes do not match.")
        return 1.0

    dlmax = 0.
    edmax = 1.

    if np.linalg.matrix_rank(ex) == 1:
        nen = ex.shape[0]
        nel = 1
        dxmax = max(ex.T.max(0)-ex.T.min(0))  # axis 0, return vector
        dymax = max(ey.T.max(0)-ey.T.min(0))
        dlmax = max(dxmax, dymax)
        edmax = abs(ed).max()
    else:
        nen = ex.shape[1]
        nel = ex.shape[0]
        dxmax = max(ex.T.max(0)-ex.T.min(0))
        dymax = max(ey.T.max(0)-ey.T.min(0))
        dlmax = max(dxmax, dymax)
        edmax = abs(ed).max()

    k = rat
    return k*dlmax/edmax

str_disp_array(a, headers=[], fmt='.4e', tablefmt='psql', showindex=False)

Return a numpy array in a nice way as a string.

Source code in src/calfem/utils.py
def str_disp_array(a, headers=[], fmt=".4e", tablefmt="psql", showindex=False):
    """
    Return a numpy array in a nice way as a string.
    """
    return tab.tabulate(np.asarray(a), tablefmt=tablefmt, floatfmt=fmt, showindex=showindex, headers=headers)

which(filename)

Return complete path to executable given by filename.

Source code in src/calfem/utils.py
def which(filename):
    """
    Return complete path to executable given by filename.
    """
    if not ('PATH' in os.environ) or os.environ['PATH'] == '':
        p = os.defpath
    else:
        p = os.environ['PATH']

    pathlist = p.split(os.pathsep)
    pathlist.insert(0, ".")
    pathlist.insert(0, "/bin")
    pathlist.insert(0, "/usr/bin")
    pathlist.insert(0, "/opt/local/bin")
    pathlist.insert(0, "/usr/local/bin")
    pathlist.insert(0, "/Applications/Gmsh.app/Contents/MacOS")

    # Add paths from site-packages

    for path in sys.path:
        if "site-packages" in path:
            pathlist.insert(0, path)

    for path in pathlist:
        f = os.path.join(path, filename)

        if os.access(f, os.X_OK):
            return f

    return None

Visualisation functions (Matplotlib)

CALFEM Visualisation module (matplotlib)

Contains all the functions implementing visualisation routines.

axis(*args, **kwargs)

Define axis of figure (Matplotlib passthrough)

Source code in src/calfem/vis_mpl.py
def axis(*args, **kwargs):
    """Define axis of figure (Matplotlib passthrough)"""
    plt.axis(*args, **kwargs)

camera3d()

Get visvis 3D camera.

Source code in src/calfem/vis_mpl.py
def camera3d():
    """Get visvis 3D camera."""
    return None

ce2vf(coords, edof, dofs_per_node, el_type)

Convert coordinates and element topology to vertices and faces for visualization. Extracts vertices, faces and vertices per face from input data for use in visualization routines. Handles both 2D and 3D coordinate systems and various element types including triangular, quadrilateral, tetrahedral and hexahedral elements.

Parameters

coords : ndarray Node coordinates array of shape (n_nodes, 2) for 2D or (n_nodes, 3) for 3D. Contains the spatial coordinates of all nodes in the mesh. edof : ndarray Element degrees of freedom array of shape (n_elements, dofs_per_element). Contains the connectivity information for each element. dofs_per_node : int Number of degrees of freedom per node. Used to extract node numbers from the edof array. el_type : int Element type identifier: - 2: Triangular elements - 3: Quadrilateral elements
- 4: Tetrahedral elements - 5: Hexahedral elements - 16: 8-node quadrilateral elements

Returns

verts : ndarray Vertex coordinates array of shape (n_nodes, 3). For 2D input, z-coordinates are padded with zeros. faces : ndarray Face connectivity array of shape (n_faces, vertices_per_face) containing node indices that define each face. For 3D elements, this decomposes volume elements into their constituent faces. vertices_per_face : int Number of vertices per face (3 for triangular faces, 4 for quadrilateral faces). is_3d : bool Flag indicating whether the problem is 3D (True) or 2D (False).

Raises

ValueError If coords array doesn't have 2 or 3 columns, or if el_type is not supported.

Notes

For 3D volume elements (tetrahedra and hexahedra), the function decomposes each element into its constituent faces using predefined connectivity matrices. The node ordering follows the Gmsh manual conventions. For 8-node quadrilateral elements (el_type=16), only the first 4 corner nodes are used to define the face.

Source code in src/calfem/vis_mpl.py
def ce2vf(coords, edof, dofs_per_node, el_type):
    """
    Convert coordinates and element topology to vertices and faces for visualization.
    Extracts vertices, faces and vertices per face from input data for use in 
    visualization routines. Handles both 2D and 3D coordinate systems and various
    element types including triangular, quadrilateral, tetrahedral and hexahedral
    elements.

    Parameters
    ----------
    coords : ndarray
        Node coordinates array of shape (n_nodes, 2) for 2D or (n_nodes, 3) for 3D.
        Contains the spatial coordinates of all nodes in the mesh.
    edof : ndarray
        Element degrees of freedom array of shape (n_elements, dofs_per_element).
        Contains the connectivity information for each element.
    dofs_per_node : int
        Number of degrees of freedom per node. Used to extract node numbers
        from the edof array.
    el_type : int
        Element type identifier:
        - 2: Triangular elements
        - 3: Quadrilateral elements  
        - 4: Tetrahedral elements
        - 5: Hexahedral elements
        - 16: 8-node quadrilateral elements

    Returns
    -------
    verts : ndarray
        Vertex coordinates array of shape (n_nodes, 3). For 2D input, z-coordinates
        are padded with zeros.
    faces : ndarray
        Face connectivity array of shape (n_faces, vertices_per_face) containing
        node indices that define each face. For 3D elements, this decomposes
        volume elements into their constituent faces.
    vertices_per_face : int
        Number of vertices per face (3 for triangular faces, 4 for quadrilateral faces).
    is_3d : bool
        Flag indicating whether the problem is 3D (True) or 2D (False).

    Raises
    ------
    ValueError
        If coords array doesn't have 2 or 3 columns, or if el_type is not supported.

    Notes
    -----
    For 3D volume elements (tetrahedra and hexahedra), the function decomposes
    each element into its constituent faces using predefined connectivity matrices.
    The node ordering follows the Gmsh manual conventions.
    For 8-node quadrilateral elements (el_type=16), only the first 4 corner nodes
    are used to define the face.
    """

    if np.shape(coords)[1] == 2:
        is_3d = False
        # pad with zeros to make 3D
        verts = np.hstack((coords, np.zeros([np.shape(coords)[0], 1])))
    elif np.shape(coords)[1] == 3:
        is_3d = True
        verts = coords
    else:
        raise ValueError("coords must be N-by-2 or N-by-3 array")

    if el_type in [2, 4]:  # elements with triangular faces
        vertices_per_face = 3
    elif el_type in [3, 5, 16]:  # elements with rectangular faces
        vertices_per_face = 4
    else:  # [NOTE] This covers all element types available in CALFEM plus tetrahedrons. If more element types are added it is necessary to include them here and below.
        raise ValueError("element type not implemented")

    faces = (edof[:, 0::dofs_per_node] - 1) / dofs_per_node
    # 'faces' here are actually lists of nodes in elements, not in faces necessarily if the elements are in 3D. This case is handled below.

    if el_type in [4, 5]:  # if hexahedrons or tetrahedrons:
        if el_type == 5:
            G = np.array(
                [
                    [0, 3, 2, 1],
                    [0, 1, 5, 4],
                    [4, 5, 6, 7],
                    [2, 6, 5, 1],
                    [2, 3, 7, 6],
                    [0, 4, 7, 3],
                ]
            )  # G is an array that is used to decomposes hexahedrons into its component faces.
        # The numbers are from the node orders (see p94 in the Gmsh manual) and each row makes one face.
        elif el_type == 4:
            G = np.array(
                [[0, 1, 2], [0, 3, 2], [1, 3, 2], [0, 3, 1]]
            )  # This G decomposes tetrahedrons into faces
        faces = np.vstack([faces[i, G] for i in range(faces.shape[0])])
    elif el_type == 16:  # if 8-node-quads:
        # The first 4 nodes are the corners of the high order quad.
        faces = faces[:, 0:4]

    return verts, np.asarray(faces, dtype=int), vertices_per_face, is_3d

clf()

Clear visvis figure

Source code in src/calfem/vis_mpl.py
def clf():
    """Clear visvis figure"""
    plt.clf()

close(fig=None)

Close visvis figure

Source code in src/calfem/vis_mpl.py
def close(fig=None):
    """Close visvis figure"""
    if fig == None:
        plt.close()
    else:
        plt.close(fig)

close_all()

Close all visvis windows.

Source code in src/calfem/vis_mpl.py
def close_all():
    """Close all visvis windows."""
    plt.close("all")

colorbar(**kwargs)

Add a colorbar to current figure

Source code in src/calfem/vis_mpl.py
def colorbar(**kwargs):
    """Add a colorbar to current figure"""
    global cfv_def_mappable
    if cfv_def_mappable != None:
        cbar = plt.colorbar(mappable=cfv_def_mappable, ax=plt.gca(), **kwargs)
        cfv_def_mappable = None
        return cbar
    else:
        return plt.colorbar(**kwargs)

create_ordered_polys(geom, N=10)

Creates ordered polygons from the geometry definition. This function processes geometry surfaces by converting their constituent curves into ordered polygon representations. Each curve is discretized into N points based on its type (Spline, BSpline, Circle, or Ellipse), and the resulting polygons are ordered such that consecutive curves share endpoints.

Parameters

geom : object Geometry object containing surfaces and curves definitions. Must have 'surfaces' and 'curves' attributes, and a 'get_point_coords' method. N : int, optional Number of points to use for curve discretization (default is 10). Note: This parameter is overridden to 10 within the function.

Returns

list of numpy.ndarray List of ordered polygons, where each polygon is a numpy array of shape (n_points, 3) representing the coordinates of points forming the polygon boundary. Each polygon corresponds to a surface in the geometry.

Notes

  • The function assumes curves can be connected end-to-end to form closed polygons
  • Curves are automatically flipped if needed to maintain proper ordering
  • Only processes the outer boundary of surfaces (holes are ignored)
  • Supported curve types: Spline, BSpline, Circle, Ellipse
Source code in src/calfem/vis_mpl.py
def create_ordered_polys(geom, N=10):
    """
    Creates ordered polygons from the geometry definition.
    This function processes geometry surfaces by converting their constituent curves
    into ordered polygon representations. Each curve is discretized into N points
    based on its type (Spline, BSpline, Circle, or Ellipse), and the resulting
    polygons are ordered such that consecutive curves share endpoints.

    Parameters
    ----------
    geom : object
        Geometry object containing surfaces and curves definitions. Must have
        'surfaces' and 'curves' attributes, and a 'get_point_coords' method.
    N : int, optional
        Number of points to use for curve discretization (default is 10).
        Note: This parameter is overridden to 10 within the function.

    Returns
    -------
    list of numpy.ndarray
        List of ordered polygons, where each polygon is a numpy array of shape
        (n_points, 3) representing the coordinates of points forming the polygon
        boundary. Each polygon corresponds to a surface in the geometry.

    Notes
    -----
    - The function assumes curves can be connected end-to-end to form closed polygons
    - Curves are automatically flipped if needed to maintain proper ordering
    - Only processes the outer boundary of surfaces (holes are ignored)
    - Supported curve types: Spline, BSpline, Circle, Ellipse
    """

    N = 10

    o_polys = []

    for id, (surf_name, curve_ids, holes, _, _, _) in geom.surfaces.items():
        polygon = np.empty((0, 3), float)

        polys = []

        for curve_id in curve_ids:
            curve_name, curve_points, _, _, _, _ = geom.curves[curve_id]
            points = geom.get_point_coords(curve_points)

            if curve_name == "Spline":
                P = _catmullspline(points, N)
            if curve_name == "BSpline":
                P = _bspline(points, N)
            if curve_name == "Circle":
                P = _circleArc(*points, pointsOnCurve=N)
            if curve_name == "Ellipse":
                P = _ellipseArc(*points, pointsOnCurve=N)

            polys.append(P)

        ordered_polys = []

        ordered_polys.append(polys.pop())

        while len(polys) != 0:
            p0 = ordered_polys[-1]
            for p in polys:
                if np.allclose(p0[-1], p[0]):
                    ordered_polys.append(polys.pop())
                    break
                elif np.allclose(p0[-1], p[-1]):
                    ordered_polys.append(np.flipud(polys.pop()))
                    break

        for p in ordered_polys:
            polygon = np.concatenate((polygon, p))

        o_polys.append(polygon)

    return o_polys

dispbeam2(ex, ey, edi, plotpar=[2, 1, 1], sfac=None)

Draw the displacement diagram for a two dimensional beam element.

Parameters

ex : array_like Element node coordinates [x1, x2]. ey : array_like Element node coordinates [y1, y2]. edi : array_like Matrix containing the displacements in Nbr evaluation points along the beam. Shape: [[u1, v1], [u2, v2], ...]. plotpar : list, optional Plot parameters [linetype, linecolour, nodemark]. Default [2, 1, 1].

- linetype: 1=solid, 2=dashed, 3=dotted
- linecolour: 1=black, 2=blue, 3=magenta, 4=red
- nodemark: 0=no mark, 1=circle, 2=star, 3=point

sfac : float, optional Scale factor for displacements. If None, auto magnification is used.

Returns

float or None Scale factor for displacements when sfac is None.

Notes

Default if sfac and plotpar is left out is auto magnification and dashed black lines with circles at nodes -> plotpar=[1 1 1]

O Dahlblom 2015-11-18

O Dahlblom 2023-01-31 (Python)

Copyright (c) Division of Structural Mechanics and Division of Solid Mechanics. Lund University

Source code in src/calfem/vis_mpl.py
def dispbeam2(ex, ey, edi, plotpar=[2, 1, 1], sfac=None):
    """
    Draw the displacement diagram for a two dimensional beam element.

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ey : array_like
        Element node coordinates [y1, y2].
    edi : array_like
        Matrix containing the displacements in Nbr evaluation points along the beam.
        Shape: [[u1, v1], [u2, v2], ...].
    plotpar : list, optional
        Plot parameters [linetype, linecolour, nodemark]. Default [2, 1, 1].

        - linetype: 1=solid, 2=dashed, 3=dotted
        - linecolour: 1=black, 2=blue, 3=magenta, 4=red
        - nodemark: 0=no mark, 1=circle, 2=star, 3=point
    sfac : float, optional
        Scale factor for displacements. If None, auto magnification is used.

    Returns
    -------
    float or None
        Scale factor for displacements when sfac is None.

    Notes
    -----
    Default if sfac and plotpar is left out is auto magnification
    and dashed black lines with circles at nodes -> plotpar=[1 1 1]

    LAST MODIFIED: O Dahlblom  2015-11-18
                   O Dahlblom  2023-01-31 (Python)

    Copyright (c)  Division of Structural Mechanics and
                   Division of Solid Mechanics.
                   Lund University
    """
    if ex.shape != ey.shape:
        raise ValueError("Check size of ex, ey dimensions.")

    rows, cols = edi.shape
    if cols != 2:
        raise ValueError("Check size of edi dimension.")
    Nbr = rows

    x1, x2 = ex
    y1, y2 = ey
    dx = x2 - x1
    dy = y2 - y1
    L = np.sqrt(dx * dx + dy * dy)
    nxX = dx / L
    nyX = dy / L
    n = np.array([nxX, nyX])

    line_color, line_style, node_color, node_style = pltstyle2(plotpar)

    if sfac is None:
        sfac = (0.1 * L) / (np.max(abs(edi)))

    eci = np.linspace(0.0, L, Nbr)

    edi1 = edi * sfac
    # From local x-coordinates to global coordinates of the beam element.
    A = np.zeros(2 * Nbr).reshape(Nbr, 2)
    A[0, 0] = ex[0]
    A[0, 1] = ey[0]
    for i in range(1, Nbr):
        A[i, 0] = A[0, 0] + eci[i] * n[0]
        A[i, 1] = A[0, 1] + eci[i] * n[1]

    for i in range(0, Nbr):
        A[i, 0] = A[i, 0] + edi1[i, 0] * n[0] - edi1[i, 1] * n[1]
        A[i, 1] = A[i, 1] + edi1[i, 0] * n[1] + edi1[i, 1] * n[0]
    xc = np.array(A[:, 0])
    yc = np.array(A[:, 1])

    plt.axis("equal")
    plt.plot(xc, yc, color=line_color, linewidth=1)

    A1 = np.array([A[0, 0], A[Nbr - 1, 0]]).reshape(1, 2)
    A2 = np.array([A[0, 1], A[Nbr - 1, 1]]).reshape(1, 2)
    draw_node_circles(A1, A2, color=node_color, filled=False, marker_type=node_style)

draw_displacements(a, coords, edof, dofs_per_node, el_type, draw_undisplaced_mesh=False, magnfac=-1.0, magscale=0.25, title=None, color=(0, 0, 0), node_color=(0, 0, 0))

Draws scalar element values in 2D or 3D. Returns the world object elementsWobject that represents the mesh.

Parameters

ev : array_like An N-by-1 array or a list of scalars. The Scalar values of the elements. ev[i] should be the value of element i. coords : array_like An N-by-2 or N-by-3 array. Row i contains the x,y,z coordinates of node i. edof : array_like An E-by-L array. Element topology. (E is the number of elements and L is the number of dofs per element) dofs_per_node : int Dofs per node. el_type : int Element Type. See Gmsh manual for details. Usually 2 for triangles or 3 for quadrangles. displacements : array_like An N-by-2 or N-by-3 array. Row i contains the x,y,z displacements of node i. axes : matplotlib.axes.Axes Matlotlib Axes. The Axes where the model will be drawn. If unspecified the current Axes will be used, or a new Axes will be created if none exist. draw_undisplaced_mesh : bool True if the wire of the undisplaced mesh should be drawn on top of the displaced mesh. Default False. Use only if displacements != None. magnfac : float Magnification factor. Displacements are multiplied by this value. Use this to make small displacements more visible. title : str Changes title of the figure. Default "Element Values".

Source code in src/calfem/vis_mpl.py
def draw_displacements(
    a,
    coords,
    edof,
    dofs_per_node,
    el_type,
    draw_undisplaced_mesh=False,
    magnfac=-1.0,
    magscale=0.25,
    title=None,
    color=(0, 0, 0),
    node_color=(0, 0, 0),
):
    """
    Draws scalar element values in 2D or 3D. Returns the world object
    elementsWobject that represents the mesh.

    Parameters
    ----------
    ev : array_like
        An N-by-1 array or a list of scalars. The Scalar values of the elements. ev[i] should be the value of element i.
    coords : array_like
        An N-by-2 or N-by-3 array. Row i contains the x,y,z coordinates of node i.
    edof : array_like
        An E-by-L array. Element topology. (E is the number of elements and L is the number of dofs per element)
    dofs_per_node : int
        Dofs per node.
    el_type : int
        Element Type. See Gmsh manual for details. Usually 2 for triangles or 3 for quadrangles.
    displacements : array_like
        An N-by-2 or N-by-3 array. Row i contains the x,y,z  displacements of node i.
    axes : matplotlib.axes.Axes
        Matlotlib Axes. The Axes where the model will be drawn. If unspecified the current Axes will be used, or a new Axes will be created if none exist.
    draw_undisplaced_mesh : bool
        True if the wire of the undisplaced mesh should be drawn on top of the displaced mesh. Default False. Use only if displacements != None.
    magnfac : float
        Magnification factor. Displacements are multiplied by this value. Use this to make small displacements more visible.
    title : str
        Changes title of the figure. Default "Element Values".
    """

    if draw_undisplaced_mesh:
        draw_mesh(coords, edof, dofs_per_node, el_type, color=(0.8, 0.8, 0.8))

    if a is not None:
        if a.shape[1] != coords.shape[1]:
            a = np.reshape(a, (-1, coords.shape[1]))

            x_max = np.max(coords[:, 0])
            x_min = np.min(coords[:, 0])

            y_max = np.max(coords[:, 1])
            y_min = np.min(coords[:, 1])

            x_size = x_max - x_min
            y_size = y_max - y_min

            if x_size > y_size:
                max_size = x_size
            else:
                max_size = y_size

            if magnfac < 0:
                magnfac = 0.25 * max_size

            coords = np.asarray(coords + magnfac * a)

    verts, faces, vertices_per_face, is_3d = ce2vf(coords, edof, dofs_per_node, el_type)

    y = verts[:, 0]
    z = verts[:, 1]

    values = []

    def quatplot(y, z, quatrangles, values=[], ax=None, **kwargs):
        if not ax:
            ax = plt.gca()
        yz = np.c_[y, z]
        v = yz[quatrangles]
        pc = matplotlib.collections.PolyCollection(v, **kwargs)

        ax.add_collection(pc)
        ax.autoscale()
        return pc

    ax = plt.gca()
    ax.set_aspect("equal")

    pc = quatplot(
        y, z, faces, values, ax=ax, edgecolor=(0.3, 0.3, 0.3), facecolor="none"
    )

    if title != None:
        ax.set(title=title)

draw_element_values(values, coords, edof, dofs_per_node, el_type, displacements=None, draw_elements=True, draw_undisplaced_mesh=False, magnfac=1.0, title=None, color=(0, 0, 0), node_color=(0, 0, 0))

Draws scalar element values in 2D or 3D.

Parameters

values : array_like An N-by-1 array or a list of scalars. The Scalar values of the elements. ev[i] should be the value of element i. coords : array_like An N-by-2 or N-by-3 array. Row i contains the x,y,z coordinates of node i. edof : array_like An E-by-L array. Element topology. (E is the number of elements and L is the number of dofs per element) dofs_per_node : int Dofs per node. el_type : int Element Type. See Gmsh manual for details. Usually 2 for triangles or 3 for quadrangles. displacements : array_like, optional An N-by-2 or N-by-3 array. Row i contains the x,y,z displacements of node i. draw_elements : bool, optional True if mesh wire should be drawn. Default True. draw_undisplaced_mesh : bool, optional True if the wire of the undisplaced mesh should be drawn on top of the displaced mesh. Default False. Use only if displacements != None. magnfac : float, optional Magnification factor. Displacements are multiplied by this value. Use this to make small displacements more visible. title : str, optional Changes title of the figure. Default "Element Values". color : tuple or str, optional Color of the wire. node_color : tuple or str, optional Color of the nodes.

Source code in src/calfem/vis_mpl.py
def draw_element_values(
    values,
    coords,
    edof,
    dofs_per_node,
    el_type,
    displacements=None,
    draw_elements=True,
    draw_undisplaced_mesh=False,
    magnfac=1.0,
    title=None,
    color=(0, 0, 0),
    node_color=(0, 0, 0),
):
    """
    Draws scalar element values in 2D or 3D.

    Parameters
    ----------
    values : array_like
        An N-by-1 array or a list of scalars. The Scalar values of the elements. ev[i] should be the value of element i.
    coords : array_like
        An N-by-2 or N-by-3 array. Row i contains the x,y,z coordinates of node i.
    edof : array_like
        An E-by-L array. Element topology. (E is the number of elements and L is the number of dofs per element)
    dofs_per_node : int
        Dofs per node.
    el_type : int
        Element Type. See Gmsh manual for details. Usually 2 for triangles or 3 for quadrangles.
    displacements : array_like, optional
        An N-by-2 or N-by-3 array. Row i contains the x,y,z displacements of node i.
    draw_elements : bool, optional
        True if mesh wire should be drawn. Default True.
    draw_undisplaced_mesh : bool, optional
        True if the wire of the undisplaced mesh should be drawn on top of the displaced mesh. Default False. Use only if displacements != None.
    magnfac : float, optional
        Magnification factor. Displacements are multiplied by this value. Use this to make small displacements more visible.
    title : str, optional
        Changes title of the figure. Default "Element Values".
    color : tuple or str, optional
        Color of the wire.
    node_color : tuple or str, optional
        Color of the nodes.
    """

    if draw_undisplaced_mesh:
        draw_mesh(coords, edof, dofs_per_node, el_type, color=(0.5, 0.5, 0.5))

    if displacements is not None:
        if displacements.shape[1] != coords.shape[1]:
            displacements = np.reshape(displacements, (-1, coords.shape[1]))
            coords = np.asarray(coords + magnfac * displacements)

    verts, faces, vertices_per_face, is_3d = ce2vf(coords, edof, dofs_per_node, el_type)

    y = verts[:, 0]
    z = verts[:, 1]

    def quatplot(y, z, quatrangles, values=[], ax=None, **kwargs):
        if not ax:
            ax = plt.gca()
        yz = np.c_[y, z]
        v = yz[quatrangles]
        pc = matplotlib.collections.PolyCollection(v, **kwargs)

        pc.set_array(np.asarray(values))
        ax.add_collection(pc)
        ax.autoscale()
        return pc

    fig = plt.gcf()
    ax = plt.gca()
    ax.set_aspect("equal")

    if draw_elements:
        pc = quatplot(y, z, faces, values, ax=ax, edgecolor=color)
    else:
        pc = quatplot(y, z, faces, values, ax=ax, edgecolor=None)

    set_mappable(pc)

    if title != None:
        ax.set(title=title)

draw_elements(ex, ey, title='', color=(0, 0, 0), face_color=(0.8, 0.8, 0.8), node_color=(0, 0, 0), line_style='solid', filled=False, closed=True, show_nodes=False)

Draws wire mesh of model in 2D or 3D. Returns the Mesh object that represents the mesh.

Parameters

ex : ndarray Element x-coordinates array. ey : ndarray Element y-coordinates array. title : str, optional Changes title of the figure. Default "". color : tuple or str, optional Color of the wire. Defaults to black (0,0,0). Can also be given as a character in 'rgbycmkw'. face_color : tuple or str, optional Color of the faces. Defaults to (0.8,0.8,0.8). Parameter filled must be True or faces will not be drawn at all. node_color : tuple or str, optional Color of the nodes. Defaults to black (0,0,0). line_style : str, optional Line style for drawing. Default "solid". filled : bool, optional Faces will be drawn if True. Otherwise only the wire is drawn. Default False. closed : bool, optional Whether elements should be drawn as closed polygons. Default True. show_nodes : bool, optional Whether to show nodes as markers. Default False.

Source code in src/calfem/vis_mpl.py
def draw_elements(
    ex,
    ey,
    title="",
    color=(0, 0, 0),
    face_color=(0.8, 0.8, 0.8),
    node_color=(0, 0, 0),
    line_style="solid",
    filled=False,
    closed=True,
    show_nodes=False,
):
    """
    Draws wire mesh of model in 2D or 3D. Returns the Mesh object that represents
    the mesh.

    Parameters
    ----------
    ex : ndarray
        Element x-coordinates array.
    ey : ndarray
        Element y-coordinates array.
    title : str, optional
        Changes title of the figure. Default "".
    color : tuple or str, optional
        Color of the wire. Defaults to black (0,0,0). Can also be given as a character in 'rgbycmkw'.
    face_color : tuple or str, optional
        Color of the faces. Defaults to (0.8,0.8,0.8). Parameter filled must be True or faces will not be drawn at all.
    node_color : tuple or str, optional
        Color of the nodes. Defaults to black (0,0,0).
    line_style : str, optional
        Line style for drawing. Default "solid".
    filled : bool, optional
        Faces will be drawn if True. Otherwise only the wire is drawn. Default False.
    closed : bool, optional
        Whether elements should be drawn as closed polygons. Default True.
    show_nodes : bool, optional
        Whether to show nodes as markers. Default False.
    """

    if ex.ndim != 1:
        nnodes = ex.shape[1]
        nel = ex.shape[0]
    else:
        nnodes = ex.shape[0]
        nel = 1

    polys = []

    for elx, ely in zip(ex, ey):
        pg = np.zeros((nnodes, 2))
        pg[:, 0] = elx
        pg[:, 1] = ely

        polys.append(pg)

    ax = plt.gca()

    if filled:
        pc = matplotlib.collections.PolyCollection(
            polys,
            facecolor=face_color,
            edgecolor=color,
            linestyle=line_style,
            closed=closed,
        )
    else:
        pc = matplotlib.collections.PolyCollection(
            polys,
            facecolor="none",
            edgecolor=color,
            linestyle=line_style,
            closed=closed,
        )

    ax.add_collection(pc)
    ax.autoscale()

    ax.set_aspect("equal")

    if title != None:
        ax.set(title=title)

draw_geometry(geometry, draw_points=True, label_points=True, label_curves=True, title=None, font_size=11, N=20, rel_margin=0.05, draw_axis=False, axes=None)

Draws the geometry (points and curves) in geoData.

Parameters

geometry : object GeoData object. Geodata contains geometric information of the model. draw_points : bool, optional If True points will be drawn. Default True. label_points : bool, optional If True Points will be labeled. The format is: ID[marker]. If a point has marker==0 only the ID is written. Default True. label_curves : bool, optional If True Curves will be labeled. The format is: ID(elementsOnCurve)[marker]. Default True. title : str, optional Title for the plot. Default None. font_size : int, optional Size of the text in the text labels. Default 11. N : int, optional The number of discrete points per curve segment. Default 20. Increase for smoother curves. Decrease for better performance. rel_margin : float, optional Extra spacing between geometry and axis. Default 0.05. draw_axis : bool, optional Whether to draw the axis frame. Default False. axes : matplotlib.axes.Axes, optional Matplotlib Axes. The Axes where the model will be drawn. If unspecified the current Axes will be used, or a new Axes will be created if none exist. Default None.

Source code in src/calfem/vis_mpl.py
def draw_geometry(
    geometry,
    draw_points=True,
    label_points=True,
    label_curves=True,
    title=None,
    font_size=11,
    N=20,
    rel_margin=0.05,
    draw_axis=False,
    axes=None,
):
    """
    Draws the geometry (points and curves) in geoData.

    Parameters
    ----------
    geometry : object
        GeoData object. Geodata contains geometric information of the model.
    draw_points : bool, optional
        If True points will be drawn. Default True.
    label_points : bool, optional
        If True Points will be labeled. The format is: ID[marker]. If a point has marker==0 only the ID is written. Default True.
    label_curves : bool, optional
        If True Curves will be labeled. The format is: ID(elementsOnCurve)[marker]. Default True.
    title : str, optional
        Title for the plot. Default None.
    font_size : int, optional
        Size of the text in the text labels. Default 11.
    N : int, optional
        The number of discrete points per curve segment. Default 20. Increase for smoother curves. Decrease for better performance.
    rel_margin : float, optional
        Extra spacing between geometry and axis. Default 0.05.
    draw_axis : bool, optional
        Whether to draw the axis frame. Default False.
    axes : matplotlib.axes.Axes, optional
        Matplotlib Axes. The Axes where the model will be drawn. If unspecified the current Axes will be used, or a new Axes will be created if none exist. Default None.
    """

    if axes is None:
        ax = plt.gca()
    else:
        ax = axes

    ax.set_aspect("equal")
    ax.set_frame_on(draw_axis)

    if draw_points:
        P = np.array(geometry.getPointCoords())  # M-by-3 list of M points.
        # plotArgs = {'mc':'r', 'mw':5, 'lw':0, 'ms':'o', 'axesAdjust':False, 'axes':axes}
        plotArgs = {"marker": "o", "ls": ""}
        if geometry.is3D:
            plt.plot(P[:, 0], P[:, 1], P[:, 2], **plotArgs)
        else:
            plt.plot(P[:, 0], P[:, 1], **plotArgs)

        if label_points:  # Write text label at the points:
            # [[x, y, z], elSize, marker]
            for ID, (xyz, el_size, marker) in geometry.points.items():
                text = "  " + str(ID) + ("[%s]" % marker if marker != 0 else "")
                plt.text(xyz[0], xyz[1], text, fontsize=font_size, color=(0.5, 0, 0.5))

    for ID, (
        curveName,
        pointIDs,
        marker,
        elementsOnCurve,
        _,
        _,
    ) in geometry.curves.items():
        points = geometry.getPointCoords(pointIDs)
        if curveName == "Spline":
            P = _catmullspline(points, N)
        if curveName == "BSpline":
            P = _bspline(points, N)
        if curveName == "Circle":
            P = _circleArc(*points, pointsOnCurve=N)
        if curveName == "Ellipse":
            P = _ellipseArc(*points, pointsOnCurve=N)
        # plotArgs = {'lc':'k', 'ms':None, 'axesAdjust':False, 'axes':axes} #Args for plot style. Black lines with no symbols at points.

        # Args for plot style. Black lines with no symbols at points.
        plotArgs = {"color": "black"}

        if geometry.is3D:
            plt.plot(P[:, 0], P[:, 1], P[:, 2], **plotArgs)
        else:
            plt.plot(P[:, 0], P[:, 1], **plotArgs)

        if label_curves:
            # Sort of midpoint along the curve. Where the text goes.
            midP = P[int(P.shape[0] * 7.0 / 12), :].tolist()
            # Create the text for the curve. Includes ID, elementsOnCurve, and marker:
            text = " " + str(ID)
            text += "(%s)" % (elementsOnCurve) if elementsOnCurve is not None else ""
            # Something like "4(5)[8]"
            text += "[%s]" % (marker) if marker != 0 else ""
            plt.text(midP[0], midP[1], text, fontsize=font_size)

    if title != None:
        plt.title(title)

    min_x, max_x, min_y, max_y = geometry.bounding_box_2d()

    g_width = max_x - min_x
    g_height = max_y - min_y

    if g_width > g_height:
        margin = rel_margin * g_width
    else:
        margin = rel_margin * g_height

    bottom, top = ax.get_ylim()
    left, right = ax.get_xlim()
    ax.set_ylim(bottom - margin, top + margin)
    ax.set_xlim(left - margin, right + margin)

draw_mesh(coords, edof, dofs_per_node, el_type, title=None, color=(0, 0, 0), face_color=(0.8, 0.8, 0.8), node_color=(0, 0, 0), filled=False, show_nodes=False)

Draws wire mesh of model in 2D or 3D. Returns the Mesh object that represents the mesh.

Parameters

coords : ndarray An N-by-2 or N-by-3 array. Row i contains the x,y,z coordinates of node i. edof : ndarray An E-by-L array. Element topology. (E is the number of elements and L is the number of dofs per element) dofs_per_nodes : int Integer. Dofs per node. el_type : int Integer. Element Type. See Gmsh manual for details. Usually 2 for triangles or 3 for quadrangles. axes : matplotlib.axes.Axes, optional Matplotlib Axes. The Axes where the model will be drawn. If unspecified the current Axes will be used, or a new Axes will be created if none exist. axes_adjust : bool, optional Boolean. True if the view should be changed to show the whole model. Default True. title : str, optional String. Changes title of the figure. Default "Mesh". color : tuple or str, optional 3-tuple or char. Color of the wire. Defaults to black (0,0,0). Can also be given as a character in 'rgbycmkw'. face_color : tuple or str, optional 3-tuple or char. Color of the faces. Defaults to white (1,1,1). Parameter filled must be True or faces will not be drawn at all. filled : bool, optional Boolean. Faces will be drawn if True. Otherwise only the wire is drawn. Default False.

Source code in src/calfem/vis_mpl.py
def draw_mesh(
    coords,
    edof,
    dofs_per_node,
    el_type,
    title=None,
    color=(0, 0, 0),
    face_color=(0.8, 0.8, 0.8),
    node_color=(0, 0, 0),
    filled=False,
    show_nodes=False,
):
    """
    Draws wire mesh of model in 2D or 3D. Returns the Mesh object that represents
    the mesh.

    Parameters
    ----------
    coords : ndarray
        An N-by-2 or N-by-3 array. Row i contains the x,y,z coordinates of node i.
    edof : ndarray
        An E-by-L array. Element topology. (E is the number of elements and L is the number of dofs per element)
    dofs_per_nodes : int
        Integer. Dofs per node.
    el_type : int
        Integer. Element Type. See Gmsh manual for details. Usually 2 for triangles or 3 for quadrangles.
    axes : matplotlib.axes.Axes, optional
        Matplotlib Axes. The Axes where the model will be drawn. If unspecified the current Axes will be used, or a new Axes will be created if none exist.
    axes_adjust : bool, optional
        Boolean. True if the view should be changed to show the whole model. Default True.
    title : str, optional
        String. Changes title of the figure. Default "Mesh".
    color : tuple or str, optional
        3-tuple or char. Color of the wire. Defaults to black (0,0,0). Can also be given as a character in 'rgbycmkw'.
    face_color : tuple or str, optional
        3-tuple or char. Color of the faces. Defaults to white (1,1,1). Parameter filled must be True or faces will not be drawn at all.
    filled : bool, optional
        Boolean. Faces will be drawn if True. Otherwise only the wire is drawn. Default False.
    """

    verts, faces, vertices_per_face, is_3d = ce2vf(coords, edof, dofs_per_node, el_type)

    y = verts[:, 0]
    z = verts[:, 1]

    values = np.zeros(faces.shape[0], float)

    def quatplot(y, z, quatrangles, values=[], ax=None, **kwargs):
        if not ax:
            ax = plt.gca()
        yz = np.c_[y, z]
        v = yz[quatrangles]
        if filled:
            pc = matplotlib.collections.PolyCollection(
                v, facecolor=face_color, **kwargs
            )
        else:
            pc = matplotlib.collections.PolyCollection(v, facecolor="none", **kwargs)

        ax.add_collection(pc)
        ax.autoscale()
        return pc

    ax = plt.gca()
    ax.set_aspect("equal")

    pc = quatplot(y, z, faces, values, ax=ax, edgecolor=color)

    if show_nodes:
        ax.plot(y, z, marker="o", ls="", color=node_color)

    if title != None:
        ax.set(title=title)

draw_nodal_values_contour(values, coords, edof, levels=12, title=None, dofs_per_node=None, el_type=None, draw_elements=False)

Draw contour plot of nodal values on a triangulated mesh.

Parameters

values : array_like Nodal values to be plotted as contours. coords : array_like Coordinates of nodes in the mesh, shape (n_nodes, 2). edof : array_like Element degrees of freedom connectivity matrix. levels : int, optional Number of contour levels to draw, default is 12. title : str, optional Title for the plot, default is None. dofs_per_node : int, optional Number of degrees of freedom per node, required if draw_elements is True. el_type : str, optional Element type, required if draw_elements is True. draw_elements : bool, optional Whether to draw the mesh elements on top of contours, default is False.

Notes

The function creates a triangulated contour plot using matplotlib's tricontour. If draw_elements is True, both dofs_per_node and el_type must be specified to draw the mesh overlay.

The plot uses equal aspect ratio and displays contours of the provided nodal values interpolated over the triangulated mesh.

Source code in src/calfem/vis_mpl.py
def draw_nodal_values_contour(
    values,
    coords,
    edof,
    levels=12,
    title=None,
    dofs_per_node=None,
    el_type=None,
    draw_elements=False,
):      
    """
    Draw contour plot of nodal values on a triangulated mesh.

    Parameters
    ----------
    values : array_like
        Nodal values to be plotted as contours.
    coords : array_like
        Coordinates of nodes in the mesh, shape (n_nodes, 2).
    edof : array_like
        Element degrees of freedom connectivity matrix.
    levels : int, optional
        Number of contour levels to draw, default is 12.
    title : str, optional
        Title for the plot, default is None.
    dofs_per_node : int, optional
        Number of degrees of freedom per node, required if draw_elements is True.
    el_type : str, optional
        Element type, required if draw_elements is True.
    draw_elements : bool, optional
        Whether to draw the mesh elements on top of contours, default is False.

    Notes
    -----
    The function creates a triangulated contour plot using matplotlib's tricontour.
    If draw_elements is True, both dofs_per_node and el_type must be specified
    to draw the mesh overlay.

    The plot uses equal aspect ratio and displays contours of the provided
    nodal values interpolated over the triangulated mesh.
    """
    edof_tri = topo_to_tri(edof)

    ax = plt.gca()
    ax.set_aspect("equal")

    x, y = coords.T
    v = np.asarray(values)
    plt.tricontour(x, y, edof_tri - 1, v.ravel(), levels)

    if draw_elements:
        if dofs_per_node != None and el_type != None:
            draw_mesh(coords, edof, dofs_per_node, el_type, color=(0.2, 0.2, 0.2))
        else:
            info("dofs_per_node and el_type must be specified to draw the mesh.")

    if title != None:
        ax.set(title=title)

draw_nodal_values_contourf(values, coords, edof, levels=12, title=None, dofs_per_node=None, el_type=None, draw_elements=False)

Draw filled contour plot of nodal values on a finite element mesh. This function creates a filled contour plot (tricontourf) to visualize scalar values at the nodes of a finite element mesh. The contours are interpolated over triangular elements derived from the mesh topology. Parameters


values : array_like Nodal values to be plotted as contours. Should have one value per node. coords : array_like Node coordinates array with shape (n_nodes, 2) where each row contains [x, y] coordinates of a node. edof : array_like Element topology array defining the connectivity between elements and degrees of freedom/nodes. levels : int, optional Number of contour levels to draw. Default is 12. title : str, optional Title for the plot. If None, no title is set. Default is None. dofs_per_node : int, optional Number of degrees of freedom per node. Required if draw_elements is True. Default is None. el_type : str, optional Element type identifier. Required if draw_elements is True. Default is None. draw_elements : bool, optional Whether to overlay the mesh elements on the contour plot. If True, dofs_per_node and el_type must be specified. Default is False. Notes


  • The function uses matplotlib's tricontourf for creating filled contours
  • Element topology is converted to triangular connectivity using topo_to_tri
  • The plot aspect ratio is set to 'equal' for proper geometric representation
  • If draw_elements is True but required parameters are missing, an info message is displayed Examples

coords = np.array([[0, 0], [1, 0], [0.5, 1]]) edof = np.array([[1, 2, 3]]) values = np.array([1.0, 2.0, 1.5]) draw_nodal_values_contourf(values, coords, edof, levels=10, title="Temperature")

Source code in src/calfem/vis_mpl.py
def draw_nodal_values_contourf(
    values,
    coords,
    edof,
    levels=12,
    title=None,
    dofs_per_node=None,
    el_type=None,
    draw_elements=False,
):
    """
    Draw filled contour plot of nodal values on a finite element mesh.
    This function creates a filled contour plot (tricontourf) to visualize scalar values
    at the nodes of a finite element mesh. The contours are interpolated over triangular
    elements derived from the mesh topology.
    Parameters
    ----------
    values : array_like
        Nodal values to be plotted as contours. Should have one value per node.
    coords : array_like
        Node coordinates array with shape (n_nodes, 2) where each row contains
        [x, y] coordinates of a node.
    edof : array_like
        Element topology array defining the connectivity between elements and
        degrees of freedom/nodes.
    levels : int, optional
        Number of contour levels to draw. Default is 12.
    title : str, optional
        Title for the plot. If None, no title is set. Default is None.
    dofs_per_node : int, optional
        Number of degrees of freedom per node. Required if draw_elements is True.
        Default is None.
    el_type : str, optional
        Element type identifier. Required if draw_elements is True. Default is None.
    draw_elements : bool, optional
        Whether to overlay the mesh elements on the contour plot. If True,
        dofs_per_node and el_type must be specified. Default is False.
    Notes
    -----
    - The function uses matplotlib's tricontourf for creating filled contours
    - Element topology is converted to triangular connectivity using topo_to_tri
    - The plot aspect ratio is set to 'equal' for proper geometric representation
    - If draw_elements is True but required parameters are missing, an info message is displayed
    Examples
    --------
    >>> coords = np.array([[0, 0], [1, 0], [0.5, 1]])
    >>> edof = np.array([[1, 2, 3]])
    >>> values = np.array([1.0, 2.0, 1.5])
    >>> draw_nodal_values_contourf(values, coords, edof, levels=10, title="Temperature")
    """

    edof_tri = topo_to_tri(edof)

    ax = plt.gca()
    ax.set_aspect("equal")

    x, y = coords.T
    v = np.asarray(values)
    plt.tricontourf(x, y, edof_tri - 1, v.ravel(), levels)

    if draw_elements:
        if dofs_per_node != None and el_type != None:
            draw_mesh(coords, edof, dofs_per_node, el_type, color=(0.2, 0.2, 0.2))
        else:
            info("dofs_per_node and el_type must be specified to draw the mesh.")

    if title != None:
        ax.set(title=title)

draw_nodal_values_shaded(values, coords, edof, title=None, dofs_per_node=None, el_type=None, draw_elements=False)

Draw shaded contour plot of nodal values using triangular interpolation. This function creates a shaded contour plot where nodal values are interpolated across triangular elements using Gouraud shading. The visualization shows smooth color gradients representing the variation of values across the mesh. Parameters


values : array-like Nodal values to be plotted. Should have one value per node. coords : array-like Node coordinates as a 2D array with shape (n_nodes, 2) where each row contains [x, y] coordinates. edof : array-like Element degrees of freedom connectivity matrix. Each row defines the nodes that belong to an element. title : str, optional Title for the plot. If None, no title is displayed. dofs_per_node : int, optional Number of degrees of freedom per node. Required if draw_elements is True. el_type : str, optional Element type identifier. Required if draw_elements is True. draw_elements : bool, default False If True, overlays the mesh elements on the contour plot. Requires dofs_per_node and el_type to be specified. Notes


  • The function uses matplotlib's tripcolor with Gouraud shading for smooth interpolation between nodal values
  • Element topology is converted to triangular format using topo_to_tri()
  • If draw_elements is True but dofs_per_node or el_type are not provided, an informational message is displayed and the mesh is not drawn
  • The plot aspect ratio is automatically set to equal for proper visualization Examples

coords = np.array([[0, 0], [1, 0], [1, 1], [0, 1]]) edof = np.array([[1, 2, 3], [1, 3, 4]]) values = np.array([0.0, 1.0, 1.5, 0.5]) draw_nodal_values_shaded(values, coords, edof, title="Temperature Distribution")

Source code in src/calfem/vis_mpl.py
def draw_nodal_values_shaded(
    values,
    coords,
    edof,
    title=None,
    dofs_per_node=None,
    el_type=None,
    draw_elements=False,
):        
    """
    Draw shaded contour plot of nodal values using triangular interpolation.
    This function creates a shaded contour plot where nodal values are interpolated
    across triangular elements using Gouraud shading. The visualization shows smooth
    color gradients representing the variation of values across the mesh.
    Parameters
    ----------
    values : array-like
        Nodal values to be plotted. Should have one value per node.
    coords : array-like
        Node coordinates as a 2D array with shape (n_nodes, 2) where each row
        contains [x, y] coordinates.
    edof : array-like
        Element degrees of freedom connectivity matrix. Each row defines the
        nodes that belong to an element.
    title : str, optional
        Title for the plot. If None, no title is displayed.
    dofs_per_node : int, optional
        Number of degrees of freedom per node. Required if draw_elements is True.
    el_type : str, optional
        Element type identifier. Required if draw_elements is True.
    draw_elements : bool, default False
        If True, overlays the mesh elements on the contour plot. Requires
        dofs_per_node and el_type to be specified.
    Notes
    -----
    - The function uses matplotlib's tripcolor with Gouraud shading for smooth
      interpolation between nodal values
    - Element topology is converted to triangular format using topo_to_tri()
    - If draw_elements is True but dofs_per_node or el_type are not provided,
      an informational message is displayed and the mesh is not drawn
    - The plot aspect ratio is automatically set to equal for proper visualization
    Examples
    --------
    >>> coords = np.array([[0, 0], [1, 0], [1, 1], [0, 1]])
    >>> edof = np.array([[1, 2, 3], [1, 3, 4]])
    >>> values = np.array([0.0, 1.0, 1.5, 0.5])
    >>> draw_nodal_values_shaded(values, coords, edof, title="Temperature Distribution")
    """

    edof_tri = topo_to_tri(edof)

    ax = plt.gca()
    ax.set_aspect("equal")

    x, y = coords.T
    v = np.asarray(values)
    plt.tripcolor(x, y, edof_tri - 1, v.ravel(), shading="gouraud")

    if draw_elements:
        if dofs_per_node != None and el_type != None:
            draw_mesh(coords, edof, dofs_per_node, el_type, color=(0.2, 0.2, 0.2))
        else:
            info("dofs_per_node and el_type must be specified to draw the mesh.")

    if title != None:
        ax.set(title=title)

draw_node_circles(ex, ey, title='', color=(0, 0, 0), face_color=(0.8, 0.8, 0.8), filled=False, marker_type='o')

Draws wire mesh of model in 2D or 3D. Returns the Mesh object that represents the mesh.

Parameters

ex : ndarray Element x-coordinates array. ey : ndarray Element y-coordinates array. title : str, optional Changes title of the figure. Default "". color : tuple or str, optional Color of the wire. Defaults to black (0,0,0). Can also be given as a character in 'rgbycmkw'. face_color : tuple or str, optional Color of the faces. Defaults to (0.8,0.8,0.8). Parameter filled must be True or faces will not be drawn at all. filled : bool, optional Faces will be drawn if True. Otherwise only the wire is drawn. Default False. marker_type : str, optional Marker type for drawing. Default "o".

Source code in src/calfem/vis_mpl.py
def draw_node_circles(
    ex,
    ey,
    title="",
    color=(0, 0, 0),
    face_color=(0.8, 0.8, 0.8),
    filled=False,
    marker_type="o",
):
    """
    Draws wire mesh of model in 2D or 3D. Returns the Mesh object that represents
    the mesh.

    Parameters
    ----------
    ex : ndarray
        Element x-coordinates array.
    ey : ndarray
        Element y-coordinates array.
    title : str, optional
        Changes title of the figure. Default "".
    color : tuple or str, optional
        Color of the wire. Defaults to black (0,0,0). Can also be given as a character in 'rgbycmkw'.
    face_color : tuple or str, optional
        Color of the faces. Defaults to (0.8,0.8,0.8). Parameter filled must be True or faces will not be drawn at all.
    filled : bool, optional
        Faces will be drawn if True. Otherwise only the wire is drawn. Default False.
    marker_type : str, optional
        Marker type for drawing. Default "o".
    """

    nel = ex.shape[0]
    nnodes = ex.shape[1]

    nodes = []

    x = []
    y = []

    for elx, ely in zip(ex, ey):
        for xx, yy in zip(elx, ely):
            x.append(xx)
            y.append(yy)

    ax = plt.gca()

    if filled:
        ax.scatter(x, y, color=color, marker=marker_type)
    else:
        ax.scatter(x, y, edgecolor=color, color="none", marker=marker_type)

    ax.autoscale()

    ax.set_aspect("equal")

    if title != None:
        ax.set(title=title)

draw_ordered_polys(o_polys)

Draw ordered polygons on the current matplotlib axes.

This function takes a collection of ordered polygons and renders them as patches on the current matplotlib axes. Each polygon is drawn with an orange face color and a line width of 1.

Parameters

o_polys : array-like A collection of polygons where each polygon is represented as a numpy array with shape (n_vertices, 2) or (n_vertices, 3+). Only the first two columns (x, y coordinates) are used for drawing.

Notes

  • The function uses the current matplotlib axes (plt.gca())
  • All polygons are drawn with orange face color and line width of 1
  • Only the first two columns of each polygon array are used for coordinates
  • The function requires matplotlib.pyplot, matplotlib.path, and matplotlib.patches to be imported as plt, mpp, and patches respectively

Examples

import numpy as np import matplotlib.pyplot as plt

Create a simple triangle

triangle = np.array([[0, 0], [1, 0], [0.5, 1], [0, 0]]) draw_ordered_polys([triangle]) plt.show()

Source code in src/calfem/vis_mpl.py
def draw_ordered_polys(o_polys):
    """
    Draw ordered polygons on the current matplotlib axes.

    This function takes a collection of ordered polygons and renders them as patches
    on the current matplotlib axes. Each polygon is drawn with an orange face color
    and a line width of 1.

    Parameters
    ----------
    o_polys : array-like
        A collection of polygons where each polygon is represented as a numpy array
        with shape (n_vertices, 2) or (n_vertices, 3+). Only the first two columns
        (x, y coordinates) are used for drawing.

    Notes
    -----
    - The function uses the current matplotlib axes (plt.gca())
    - All polygons are drawn with orange face color and line width of 1
    - Only the first two columns of each polygon array are used for coordinates
    - The function requires matplotlib.pyplot, matplotlib.path, and matplotlib.patches
      to be imported as plt, mpp, and patches respectively

    Examples
    --------
    >>> import numpy as np
    >>> import matplotlib.pyplot as plt
    >>> # Create a simple triangle
    >>> triangle = np.array([[0, 0], [1, 0], [0.5, 1], [0, 0]])
    >>> draw_ordered_polys([triangle])
    >>> plt.show()
    """
    for poly in o_polys:
        ax = plt.gca()
        path = mpp.Path(poly[:, 0:2])
        patch = patches.PathPatch(path, facecolor="orange", lw=1)
        ax.add_patch(patch)

eldisp2(ex, ey, ed, plotpar=[2, 1, 1], sfac=None)

Draw the deformed 2D mesh for a number of elements of the same type.

Supported elements are: - 1: bar element - 2: beam element
- 3: triangular 3 node element - 4: quadrilateral 4 node element - 5: 8-node isoparametric element

Parameters

ex : array_like Element x-coordinates array where nen is number of element nodes and nel is number of elements. ey : array_like Element y-coordinates array where nen is number of element nodes and nel is number of elements. ed : array_like Element displacement matrix. plotpar : list, optional Plot parameters [linetype, linecolor, nodemark]. Default [2, 1, 1].

- linetype: 1=solid, 2=dashed, 3=dotted
- linecolor: 1=black, 2=blue, 3=magenta, 4=red
- nodemark: 1=circle, 2=star, 0=no mark

sfac : float, optional Scale factor for displacements. If None, auto magnification is used.

Returns

float or None Scale factor for displacements when sfac is None.

Notes

Default if sfac and plotpar is left out is auto magnification and dashed black lines with circles at nodes -> plotpar=[2 1 1]

O Dahlblom 2004-10-01

J Lindemann 2021-12-30 (Python)

Copyright (c) Division of Structural Mechanics and Division of Solid Mechanics. Lund University

Source code in src/calfem/vis_mpl.py
def eldisp2(ex, ey, ed, plotpar=[2, 1, 1], sfac=None):
    """
    Draw the deformed 2D mesh for a number of elements of the same type.

    Supported elements are:
    - 1: bar element
    - 2: beam element  
    - 3: triangular 3 node element
    - 4: quadrilateral 4 node element
    - 5: 8-node isoparametric element

    Parameters
    ----------
    ex : array_like
        Element x-coordinates array where nen is number of element nodes
        and nel is number of elements.
    ey : array_like
        Element y-coordinates array where nen is number of element nodes
        and nel is number of elements.
    ed : array_like
        Element displacement matrix.
    plotpar : list, optional
        Plot parameters [linetype, linecolor, nodemark]. Default [2, 1, 1].

        - linetype: 1=solid, 2=dashed, 3=dotted
        - linecolor: 1=black, 2=blue, 3=magenta, 4=red
        - nodemark: 1=circle, 2=star, 0=no mark
    sfac : float, optional
        Scale factor for displacements. If None, auto magnification is used.

    Returns
    -------
    float or None
        Scale factor for displacements when sfac is None.

    Notes
    -----
    Default if sfac and plotpar is left out is auto magnification
    and dashed black lines with circles at nodes -> plotpar=[2 1 1]

    LAST MODIFIED: O Dahlblom 2004-10-01
                   J Lindemann 2021-12-30 (Python)

    Copyright (c)  Division of Structural Mechanics and
                   Division of Solid Mechanics.
                   Lund University
    """

    if ex.shape == ey.shape:
        if ex.ndim != 1:
            nen = ex.shape[1]

            if ed.shape[0] != ex.shape[0]:
                raise ValueError("Check size of ed/ex dimensions.")

            ned = ed.shape[1]
        else:
            nen = ex.shape[0]
            ned = ed.shape[0]

            ex = ex.reshape(1, nen)
            ey = ey.reshape(1, nen)
            ed = ed.reshape(1, ned)
    else:
        raise ValueError("Check size of ex, ey dimensions.")

    dx_max = float(np.max(ex)) - float(np.min(ex))
    dy_max = float(np.max(ey)) - float(np.min(ey))
    dl_max = max(dx_max, dy_max)
    ed_max = float(np.max(np.max(np.abs(ed))))
    krel = 0.1

    if sfac is None:
        sfac = krel * dl_max / ed_max

    k = sfac

    plt.axis("equal")

    line_color, line_style, node_color, node_style = pltstyle2(plotpar)

    if nen == 2:
        if ned == 4:
            x = np.transpose(ex + k * ed[:, [0, 2]])
            y = np.transpose(ey + k * ed[:, [1, 3]])
            xc = np.transpose(x)
            yc = np.transpose(y)
        elif ned == 6:
            x = np.transpose(ex + k * ed[:, [0, 3]])
            y = np.transpose(ey + k * ed[:, [1, 4]])
            exc, eyc = beam2crd(ex, ey, ed, k)
            xc = exc
            yc = eyc
    elif nen == 3:
        pass
    elif nen == 4:
        pass
    elif nen == 8:
        pass
    else:
        print("Error: Element type is not supported.")
        return

    draw_elements(
        xc, yc, color=line_color, line_style=line_style, filled=False, closed=False
    )

    if node_style != "":
        draw_node_circles(x, y, color=node_color, filled=False, marker_type=node_style)

eldraw2(ex, ey, plotpar=[1, 2, 1], elnum=[])

Draw the undeformed 2D mesh for a number of elements of the same type.

Supported elements are: 1) -> bar element 2) -> beam el. 3) -> triangular 3 node el. 4) -> quadrilateral 4 node el. 5) -> 8-node isopar. element

Parameters

ex, ey : array_like Element node coordinates arrays where nen is number of element nodes and nel is number of elements. plotpar : list, optional Plot parameters [linetype, linecolor, nodemark]. Default [1, 2, 1].

- linetype: 1=solid, 2=dashed, 3=dotted
- linecolor: 1=black, 2=blue, 3=magenta, 4=red  
- nodemark: 0=no mark, 1=circle, 2=star

elnum : array_like, optional Element numbers.

Notes

Default is solid white lines with circles at nodes.

Source code in src/calfem/vis_mpl.py
def eldraw2(ex, ey, plotpar=[1, 2, 1], elnum=[]):
    """
    Draw the undeformed 2D mesh for a number of elements of the same type.

    Supported elements are:
    1) -> bar element              2) -> beam el.
    3) -> triangular 3 node el.    4) -> quadrilateral 4 node el.
    5) -> 8-node isopar. element

    Parameters
    ----------
    ex, ey : array_like
        Element node coordinates arrays where nen is number of element nodes
        and nel is number of elements.
    plotpar : list, optional
        Plot parameters [linetype, linecolor, nodemark]. Default [1, 2, 1].

        - linetype: 1=solid, 2=dashed, 3=dotted
        - linecolor: 1=black, 2=blue, 3=magenta, 4=red  
        - nodemark: 0=no mark, 1=circle, 2=star
    elnum : array_like, optional
        Element numbers.

    Notes
    -----
    Default is solid white lines with circles at nodes.
    """

    if ex.shape == ey.shape:
        if ex.ndim != 1:
            nen = ex.shape[1]
        else:
            nen = ex.shape[0]

            ex = ex.reshape(1, nen)
            ey = ey.reshape(1, nen)
    else:
        raise ValueError("Check size of ex, ey dimensions.")

    line_type = plotpar[0]
    line_color = plotpar[1]
    node_mark = plotpar[2]

    # Translate CALFEM plotpar to visvis

    if line_type == 1:
        mpl_line_style = "solid"
    elif line_type == 2:
        mpl_line_style = (0, (5, 5))
    elif line_type == 3:
        mpl_line_style = "dotted"

    if line_color == 1:
        mpl_line_color = (0, 0, 0)  # 'k'
    elif line_color == 2:
        mpl_line_color = (0, 0, 1)  # 'b'
    elif line_color == 3:
        mpl_line_color = (1, 0, 1)  # 'm'
    elif line_color == 4:
        mpl_line_color = (1, 0, 0)  # 'r'

    if node_mark == 1:
        mpl_node_mark = "o"
    elif node_mark == 2:
        mpl_node_mark = "x"
    elif node_mark == 0:
        mpl_node_mark = ""

    plt.axis("equal")

    draw_element_numbers = False

    if len(elnum) == ex.shape[0]:
        draw_element_numbers = True

    draw_elements(ex, ey, color=mpl_line_color, line_style=mpl_line_style, filled=False)
    if mpl_node_mark != "":
        draw_node_circles(
            ex, ey, color=mpl_line_color, filled=False, marker_type=mpl_node_mark
        )

    return None

error(msg)

Log error message

Source code in src/calfem/vis_mpl.py
def error(msg):
    """Log error message"""
    cflog.error(msg)

figure(figure=None, show=True, fig_size=(6, 5.33))

Create a visvis figure with extras.

Source code in src/calfem/vis_mpl.py
def figure(figure=None, show=True, fig_size=(6, 5.33)):
    """Create a visvis figure with extras."""
    f = None

    if figure == None:
        f = plt.figure(figsize=fig_size)
    else:
        try:
            f = plt.figure(figure.number)
        except:
            f = plt.figure(figsize=fig_size)

    if f is not None:
        g_figures.append(f)

    return f

figure_class()

Return visvis Figure class.

Source code in src/calfem/vis_mpl.py
def figure_class():
    """Return visvis Figure class."""
    return None

gca()

Get current axis of the current visvis figure.

Source code in src/calfem/vis_mpl.py
def gca():
    """Get current axis of the current visvis figure."""
    return plt.gca()

info(msg)

Log information message

Source code in src/calfem/vis_mpl.py
def info(msg):
    """Log information message"""
    cflog.info(msg)

pltstyle(plotpar)

Define linetype, linecolor and markertype character codes.

Parameters

plotpar : list Plot parameters [linetype, linecolor, nodemark]

- linetype : int
    1 -> solid, 2 -> dashed, 3 -> dotted
- linecolor : int  
    1 -> black, 2 -> blue, 3 -> magenta, 4 -> red
- nodemark : int
    1 -> circle, 2 -> star, 0 -> no mark

Returns

s1 : str Linetype and color for mesh lines s2 : str Type and color for node markers

Notes

LAST MODIFIED: Ola Dahlblom 2004-09-15 Copyright (c) Division of Structural Mechanics and Division of Solid Mechanics. Lund University

Source code in src/calfem/vis_mpl.py
def pltstyle(plotpar):
    """
    Define linetype, linecolor and markertype character codes.

    Parameters
    ----------
    plotpar : list
        Plot parameters [linetype, linecolor, nodemark]

        - linetype : int
            1 -> solid, 2 -> dashed, 3 -> dotted
        - linecolor : int  
            1 -> black, 2 -> blue, 3 -> magenta, 4 -> red
        - nodemark : int
            1 -> circle, 2 -> star, 0 -> no mark

    Returns
    -------
    s1 : str
        Linetype and color for mesh lines
    s2 : str
        Type and color for node markers

    Notes
    -----
    LAST MODIFIED: Ola Dahlblom 2004-09-15
    Copyright (c)  Division of Structural Mechanics and
                   Division of Solid Mechanics.
                   Lund University
    """
    if type(plotpar) != list:
        raise TypeError("plotpar should be a list.")
    if len(plotpar) != 3:
        raise ValueError("plotpar needs to be a list of 3 values.")

    p1, p2, p3 = plotpar

    s1 = ""
    s2 = ""

    if p1 == 1:
        s1 += "-"
    elif p1 == 2:
        s1 += "--"
    elif p1 == 3:
        s1 += ":"
    else:
        raise ValueError("Invalid value for plotpar[0].")

    if p2 == 1:
        s1 += "k"
    elif p2 == 2:
        s1 += "b"
    elif p2 == 3:
        s1 += "m"
    elif p2 == 4:
        s1 += "r"
    else:
        raise ValueError("Invalid value for plotpar[1].")

    if p3 == 1:
        s2 = "ko"
    elif p3 == 2:
        s2 = "k*"
    elif p3 == 3:
        s2 = "k."
    else:
        raise ValueError("Invalid value for plotpar[2].")

    return s1, s2

pltstyle2(plotpar)

Define linetype, linecolor and markertype character codes.

Parameters

plotpar : list Plot parameters [linetype, linecolor, nodemark]

- linetype : int
    1 -> solid, 2 -> dashed, 3 -> dotted
- linecolor : int  
    1 -> black, 2 -> blue, 3 -> magenta, 4 -> red
- nodemark : int
    1 -> circle, 2 -> star, 0 -> no mark

Returns

line_color : tuple RGB color tuple for mesh lines line_style : str or tuple Line style for mesh lines node_color : tuple RGB color tuple for node markers node_type : str Marker type for nodes

Notes

LAST MODIFIED: Ola Dahlblom 2004-09-15 Copyright (c) Division of Structural Mechanics and Division of Solid Mechanics. Lund University

Source code in src/calfem/vis_mpl.py
def pltstyle2(plotpar):
    """
    Define linetype, linecolor and markertype character codes.

    Parameters
    ----------
    plotpar : list
        Plot parameters [linetype, linecolor, nodemark]

        - linetype : int
            1 -> solid, 2 -> dashed, 3 -> dotted
        - linecolor : int  
            1 -> black, 2 -> blue, 3 -> magenta, 4 -> red
        - nodemark : int
            1 -> circle, 2 -> star, 0 -> no mark

    Returns
    -------
    line_color : tuple
        RGB color tuple for mesh lines
    line_style : str or tuple
        Line style for mesh lines
    node_color : tuple
        RGB color tuple for node markers
    node_type : str
        Marker type for nodes

    Notes
    -----
    LAST MODIFIED: Ola Dahlblom 2004-09-15
    Copyright (c)  Division of Structural Mechanics and
                   Division of Solid Mechanics.
                   Lund University
    """

    cfc.check_list_array(plotpar, "plotpar needs to be a list or an array of 3 values.")
    cfc.check_length(plotpar, 3, "plotpar needs to contain 3 values.")

    p1, p2, p3 = plotpar

    s1 = ""
    s2 = ""

    line_style = ""
    line_color = ""
    node_color = ""
    node_type = ""

    if p1 == 1:
        line_style = "solid"
    elif p1 == 2:
        line_style = (0, (5, 5))
    elif p1 == 3:
        line_style = "dotted"
    else:
        raise ValueError("Invalid value for plotpar[0].")

    if p2 == 1:
        line_color = (0, 0, 0)
    elif p2 == 2:
        line_color = (0, 0, 1)
    elif p2 == 3:
        line_color = (1, 0, 1)
    elif p2 == 4:
        line_color = (1, 0, 0)
    else:
        raise ValueError("Invalid value for plotpar[1].")


    if p3 == 0:
        node_color = ""
        node_type = ""
    elif p3 == 1:
        node_color = (0, 0, 0)
        node_type = "o"
    elif p3 == 2:
        node_color = (0, 0, 0)
        node_type = "*"
    elif p3 == 3:
        node_color = (0, 0, 0)
        node_type = "."
    else:
        raise ValueError("Invalid value for plotpar[2].")

    return line_color, line_style, node_color, node_type

point_in_geometry(o_polys, point)

Check if a point is inside any of the given polygons.

Parameters

o_polys : list List of polygon arrays, where each polygon is represented as a numpy array with coordinates in the first two columns (x, y coordinates). point : array-like A point represented as [x, y] coordinates to test for containment.

Returns

bool True if the point is inside any of the polygons, False otherwise.

Notes

This function uses matplotlib's Path.contains_points() method to perform the point-in-polygon test. The function returns True as soon as the point is found to be inside any polygon (short-circuit evaluation).

Source code in src/calfem/vis_mpl.py
def point_in_geometry(o_polys, point):
    """
    Check if a point is inside any of the given polygons.

    Parameters
    ----------
    o_polys : list
        List of polygon arrays, where each polygon is represented as a numpy array
        with coordinates in the first two columns (x, y coordinates).
    point : array-like
        A point represented as [x, y] coordinates to test for containment.

    Returns
    -------
    bool
        True if the point is inside any of the polygons, False otherwise.

    Notes
    -----
    This function uses matplotlib's Path.contains_points() method to perform
    the point-in-polygon test. The function returns True as soon as the point
    is found to be inside any polygon (short-circuit evaluation).
    """
    for poly in o_polys:
        path = mpp.Path(poly[:, 0:2])
        inside = path.contains_points([point])

        if inside:
            return True

    return False

scalfact2(ex, ey, ed, rat=0.2)

Determine scale factor for drawing computational results, such as displacements, section forces or flux.

Parameters

ex : array_like Element node x-coordinates. ey : array_like Element node y-coordinates. ed : array_like Element displacement matrix or section force matrix. rat : float, optional Relation between illustrated quantity and element size. If not specified, 0.2 is used.

Returns

float Scale factor for drawing.

Notes

LAST MODIFIED: O Dahlblom 2004-09-15 J Lindemann 2021-12-29 (Python)

Copyright (c) Division of Structural Mechanics and Division of Solid Mechanics. Lund University

Source code in src/calfem/vis_mpl.py
def scalfact2(ex, ey, ed, rat=0.2):
    """
    Determine scale factor for drawing computational results, such as
    displacements, section forces or flux.

    Parameters
    ----------
    ex : array_like
        Element node x-coordinates.
    ey : array_like
        Element node y-coordinates.
    ed : array_like
        Element displacement matrix or section force matrix.
    rat : float, optional
        Relation between illustrated quantity and element size.
        If not specified, 0.2 is used.

    Returns
    -------
    float
        Scale factor for drawing.

    Notes
    -----
    LAST MODIFIED: O Dahlblom  2004-09-15
                   J Lindemann 2021-12-29 (Python)

    Copyright (c)  Division of Structural Mechanics and
                   Division of Solid Mechanics.
                   Lund University
    """

    if ex.shape == ey.shape:
        if ex.ndim != 1:
            nen = ex.shape[1]
        else:
            nen = ex.shape[0]
    else:
        raise ValueError("Check size of ex, ey dimensions.")

    dx_max = float(np.max(ex)) - float(np.min(ex))
    dy_max = float(np.max(ey)) - float(np.min(ey))
    dl_max = max(dx_max, dy_max)
    ed_max = float(np.max(np.max(np.abs(ed))))

    # dxmax=max(max(ex')-min(ex')); dymax=max(max(ey')-min(ey'));
    # dlmax=max(dxmax,dymax);
    # edmax=max(max(abs(ed)));

    k = rat

    return k * dl_max / ed_max

scalgraph2(sfac, magnitude, plotpar=2)

Draw a graphic scale.

Parameters

sfac : float Scale factor. magnitude : array_like The graphic scale has a length equivalent to Ref and starts at coordinates (x,y). Can be: - [Ref] : scale reference, starts at (0, -0.5) - [Ref, x, y] : scale reference with starting coordinates plotpar : int, optional Line color. Default is 2. - 1 : black - 2 : blue
- 3 : magenta - 4 : red

Notes

LAST MODIFIED: O Dahlblom 2015-12-02 O Dahlblom 2023-01-23 (Python)

Copyright (c) Division of Structural Mechanics and Division of Solid Mechanics. Lund University

Source code in src/calfem/vis_mpl.py
def scalgraph2(sfac, magnitude, plotpar=2):
    """
    Draw a graphic scale.

    Parameters
    ----------
    sfac : float
        Scale factor.
    magnitude : array_like
        The graphic scale has a length equivalent to Ref and starts at 
        coordinates (x,y). Can be:
        - [Ref] : scale reference, starts at (0, -0.5)
        - [Ref, x, y] : scale reference with starting coordinates
    plotpar : int, optional
        Line color. Default is 2.
        - 1 : black
        - 2 : blue  
        - 3 : magenta
        - 4 : red

    Notes
    -----
    LAST MODIFIED: O Dahlblom  2015-12-02
                   O Dahlblom  2023-01-23 (Python)

    Copyright (c)  Division of Structural Mechanics and
                   Division of Solid Mechanics.
                   Lund University
    """
    cols = len(magnitude)
    if cols != 1 and cols != 3:
        raise ValueError("Check size of magnitude input argument.")
    if cols == 1:
        N = magnitude
        x = 0
        y = -0.5
    if cols == 3:
        N, x, y = magnitude

    L = N * sfac

    if plotpar == 1:
        line_color = (0, 0, 0)
    elif plotpar == 2:
        line_color = (0, 0, 1)
    elif plotpar == 3:
        line_color = (1, 0, 1)
    elif plotpar == 4:
        line_color = (1, 0, 0)
    else:
        raise ValueError("Invalid value for plotpar[1].")

    plt.plot([x, (x + L)], [y, y], color=line_color, linewidth=1)
    plt.plot([x, x], [(y - L / 20), (y + L / 20)], color=line_color, linewidth=1)
    plt.plot(
        [(x + L), (x + L)], [(y - L / 20), (y + L / 20)], color=line_color, linewidth=1
    )
    plt.text(x + L * 1.1, (y - L / 20), str(N))

secforce2(ex, ey, es, plotpar=[2, 1], sfac=None, eci=None)

Draw section force diagram for a two dimensional bar or beam element.

Parameters

ex : array_like Element node coordinates [x1, x2]. ey : array_like Element node coordinates [y1, y2]. es : array_like Vector containing the section force in Nbr evaluation points along the element. Shape: [S1, S2, ...]. plotpar : list, optional Plot parameters [linecolour, elementcolour]. Default [2, 1].

- linecolour: 1=black, 2=blue, 3=magenta, 4=red
- elementcolour: 1=black, 2=blue, 3=magenta, 4=red

sfac : float, optional Scale factor for section force diagrams. If None, auto scaling is used. eci : array_like, optional Local x-coordinates of the evaluation points (Nbr). If not given, the evaluation points are assumed to be uniformly distributed.

Returns

float or None Scale factor for section forces when sfac is None.

Notes

LAST MODIFIED: O Dahlblom 2019-12-16 O Dahlblom 2023-01-31 (Python)

Copyright (c) Division of Structural Mechanics and Division of Solid Mechanics. Lund University

Source code in src/calfem/vis_mpl.py
def secforce2(ex, ey, es, plotpar=[2, 1], sfac=None, eci=None):
    """
    Draw section force diagram for a two dimensional bar or beam element.

    Parameters
    ----------
    ex : array_like
        Element node coordinates [x1, x2].
    ey : array_like
        Element node coordinates [y1, y2].
    es : array_like
        Vector containing the section force in Nbr evaluation points along the element.
        Shape: [S1, S2, ...].
    plotpar : list, optional
        Plot parameters [linecolour, elementcolour]. Default [2, 1].

        - linecolour: 1=black, 2=blue, 3=magenta, 4=red
        - elementcolour: 1=black, 2=blue, 3=magenta, 4=red
    sfac : float, optional
        Scale factor for section force diagrams. If None, auto scaling is used.
    eci : array_like, optional
        Local x-coordinates of the evaluation points (Nbr). If not given, 
        the evaluation points are assumed to be uniformly distributed.

    Returns
    -------
    float or None
        Scale factor for section forces when sfac is None.

    Notes
    -----
    LAST MODIFIED: O Dahlblom  2019-12-16
                   O Dahlblom  2023-01-31 (Python)

    Copyright (c)  Division of Structural Mechanics and
                   Division of Solid Mechanics.
                   Lund University
    """
    if ex.shape != ey.shape:
        raise ValueError("Check size of ex, ey dimensions.")

    c = len(es)
    Nbr = c

    x1, x2 = ex
    y1, y2 = ey
    dx = x2 - x1
    dy = y2 - y1
    L = np.sqrt(dx * dx + dy * dy)
    nxX = dx / L
    nyX = dy / L
    n = np.array([nxX, nyX])

    if sfac is None:
        sfac = (0.2 * L) / max(abs(es))

    if eci is None:
        eci = np.linspace(0.0, L, Nbr)

    p1 = plotpar[0]
    if p1 == 1:
        line_color = (0, 0, 0)
    elif p1 == 2:
        line_color = (0, 0, 1)
    elif p1 == 3:
        line_color = (1, 0, 1)
    elif p1 == 4:
        line_color = (1, 0, 0)
    else:
        raise ValueError("Invalid value for plotpar[1].")
    line_style = "solid"

    p2 = plotpar[1]
    if p2 == 1:
        line_color1 = (0, 0, 0)
    elif p2 == 2:
        line_color1 = (0, 0, 1)
    elif p2 == 3:
        line_color1 = (1, 0, 1)
    elif p2 == 4:
        line_color1 = (1, 0, 0)
    else:
        raise ValueError("Invalid value for plotpar[1].")

    a = len(eci)
    if a != c:
        raise ValueError("Check size of eci dimension.")

    es = es * sfac

    # From local x-coordinates to global coordinates of the element
    A = np.zeros(2 * Nbr).reshape(Nbr, 2)
    A[0, 0] = ex[0]
    A[0, 1] = ey[0]
    for i in range(Nbr):
        A[i, 0] = A[0, 0] + eci[i] * n[0]
        A[i, 1] = A[0, 1] + eci[i] * n[1]

    B = np.array(A)

    # Plot diagram
    for i in range(0, Nbr):
        A[i, 0] = A[i, 0] + es[i] * n[1]
        A[i, 1] = A[i, 1] - es[i] * n[0]

    xc = np.array(A[:, 0])
    yc = np.array(A[:, 1])

    plt.axis("equal")
    plt.plot(xc, yc, color=line_color, linewidth=1)

    # Plot stripes in diagram
    xs = np.zeros(2)
    ys = np.zeros(2)
    for i in range(Nbr):
        xs[0] = B[i, 0]
        xs[1] = A[i, 0]
        ys[0] = B[i, 1]
        ys[1] = A[i, 1]
        plt.plot(xs, ys, color=line_color, linewidth=1)

    # Plot element
    plt.plot(ex, ey, color=line_color1, linewidth=2)

show()

Use in Qt applications

Source code in src/calfem/vis_mpl.py
def show():
    """Use in Qt applications"""
    plt.show()

show_and_wait()

Wait for plot to show

Source code in src/calfem/vis_mpl.py
def show_and_wait():
    """Wait for plot to show"""
    global cfv_block_at_show

    if "CFV_NO_BLOCK" in os.environ:
        plt.show(block=False)
    else:
        plt.show(block=cfv_block_at_show)

show_and_wait_mpl()

Wait for plot to show

Source code in src/calfem/vis_mpl.py
def show_and_wait_mpl():
    """Wait for plot to show"""
    plt.show()

subplot(*args)

Create a visvis subplot.

Source code in src/calfem/vis_mpl.py
def subplot(*args):
    """Create a visvis subplot."""
    return plt.subplot(*args)

title(*args, **kwargs)

Define title of figure (Matplotlib passthrough)

Source code in src/calfem/vis_mpl.py
def title(*args, **kwargs):
    """Define title of figure (Matplotlib passthrough)"""
    plt.title(*args, **kwargs)

topo_to_tri(edof)

Convert element topology to triangular elements for visualization. This function converts different element topologies (triangular, quadrilateral, and 8-node elements) into triangular elements suitable for mesh visualization and plotting.

Parameters

edof : numpy.ndarray Element topology array where each row represents an element and columns represent the node indices. Supported shapes: - (n, 3): Triangular elements (returned as-is) - (n, 4): Quadrilateral elements (split into 2 triangles each) - (n, 8): 8-node elements (split into 6 triangles each)

Returns

numpy.ndarray Triangular element topology array with shape (m, 3) where m depends on the input topology: - Triangular input: m = n (no change) - Quadrilateral input: m = 2n - 8-node input: m = 6n

Raises

Error If the element topology is not supported (i.e., edof.shape[1] is not 3, 4, or 8).

Notes

  • For quadrilateral elements, the splitting pattern creates two triangles using nodes [0,1,2] and [2,3,0]
  • For 8-node elements, the splitting creates 6 triangles to represent the element faces for 3D visualization
Source code in src/calfem/vis_mpl.py
def topo_to_tri(edof):
    """
    Convert element topology to triangular elements for visualization.
    This function converts different element topologies (triangular, quadrilateral, 
    and 8-node elements) into triangular elements suitable for mesh visualization 
    and plotting.

    Parameters
    ----------
    edof : numpy.ndarray
        Element topology array where each row represents an element and columns 
        represent the node indices. Supported shapes:
        - (n, 3): Triangular elements (returned as-is)
        - (n, 4): Quadrilateral elements (split into 2 triangles each)
        - (n, 8): 8-node elements (split into 6 triangles each)

    Returns
    -------
    numpy.ndarray
        Triangular element topology array with shape (m, 3) where m depends 
        on the input topology:
        - Triangular input: m = n (no change)
        - Quadrilateral input: m = 2*n
        - 8-node input: m = 6*n

    Raises
    ------
    Error
        If the element topology is not supported (i.e., edof.shape[1] is not 3, 4, or 8).

    Notes
    -----
    - For quadrilateral elements, the splitting pattern creates two triangles 
      using nodes [0,1,2] and [2,3,0]
    - For 8-node elements, the splitting creates 6 triangles to represent 
      the element faces for 3D visualization
    """

    if edof.shape[1] == 3:
        return edof
    elif edof.shape[1] == 4:
        new_edof = np.zeros((edof.shape[0] * 2, 3), int)
        new_edof[0::2, 0] = edof[:, 0]
        new_edof[0::2, 1] = edof[:, 1]
        new_edof[0::2, 2] = edof[:, 2]
        new_edof[1::2, 0] = edof[:, 2]
        new_edof[1::2, 1] = edof[:, 3]
        new_edof[1::2, 2] = edof[:, 0]
        return new_edof
    elif edof.shape[1] == 8:
        new_edof = np.zeros((edof.shape[0] * 6, 3), int)
        new_edof[0::6, 0] = edof[:, 0]
        new_edof[0::6, 1] = edof[:, 4]
        new_edof[0::6, 2] = edof[:, 7]
        new_edof[1::6, 0] = edof[:, 4]
        new_edof[1::6, 1] = edof[:, 1]
        new_edof[1::6, 2] = edof[:, 5]
        new_edof[2::6, 0] = edof[:, 5]
        new_edof[2::6, 1] = edof[:, 2]
        new_edof[2::6, 2] = edof[:, 6]
        new_edof[3::6, 0] = edof[:, 6]
        new_edof[3::6, 1] = edof[:, 3]
        new_edof[3::6, 2] = edof[:, 7]
        new_edof[4::6, 0] = edof[:, 4]
        new_edof[4::6, 1] = edof[:, 6]
        new_edof[4::6, 2] = edof[:, 7]
        new_edof[5::6, 0] = edof[:, 4]
        new_edof[5::6, 1] = edof[:, 5]
        new_edof[5::6, 2] = edof[:, 6]
        return new_edof
    else:
        error("Element topology not supported.")