Boundary conditions and loads#
In femlabpy, a finite element model eventually becomes a global linear system with known and unknown degrees of freedom. The solver needs two things before that system can be used directly: the external load vector and the boundary-condition handling that removes or constrains rigid body motion.
where \(\mathbf{K}\) is the global stiffness matrix, \(\mathbf{u}\) is the vector of nodal unknowns, and \(\mathbf{P}\) is the global load vector. Before the system is solved, the unconstrained equations are usually singular, so the code must first apply loads and then enforce constraints in a way that matches the stored DOF layout.
This chapter follows the actual implementation in loads.py and boundary.py. The important point is that the public helpers do not hide the array shape: they expect the same column conventions used everywhere else in the codebase, and the theory here explains why.
2.1 Applying external loads#
The global load vector \(\mathbf{P}\) accumulates all external forces applied to the structure. In this repository the most common case is a nodal load table where each row begins with a 1-based node index and the remaining columns are the force components in DOF order.
The helpers in src/femlabpy/loads.py do two different things:
setload(p, P) replaces the current values at the listed DOFs.
addload(p, P) accumulates additional values at the same DOFs.
That distinction matters in assembly workflows. Use setload when the input table is the full load definition for the problem. Use addload when several tables or source terms need to be combined into one vector.
Load table layout#
import numpy as np
from femlabpy import init
from femlabpy.loads import setload, addload
_, p, _ = init(nn=3, dof=2)
# [node, Fx, Fy]
P = np.array([
[1, 0.0, -100.0],
[3, 50.0, 0.0],
])
p = setload(p, P)
p = addload(p, np.array([[3, 0.0, -25.0]]))
2.2 Dirichlet boundary conditions#
Dirichlet boundary conditions specify known values of the primary field variable at certain nodes or components:
Mathematical formulation#
Consider the partitioned system:
where subscript \(f\) denotes free DOFs and \(c\) denotes constrained DOFs with \(\mathbf{u}_c = \bar{\mathbf{u}}_c\).
Condensed system: The free DOFs satisfy:
The term \(\mathbf{K}_{fc} \bar{\mathbf{u}}_c\) transfers the effect of prescribed displacements to the right-hand side.
Reaction forces: After solving for \(\mathbf{u}_f\):
Penalty method alternative#
Instead of elimination, constrained DOFs can be enforced with a large spring:
where \(k_s \gg \max(K_{ii})\). This approximately enforces \(u_i \approx \bar{u}_i\).
Implementation in setbc#
The code uses a hybrid approach that maintains matrix symmetry:
Compute scale: \(k_s = 0.1 \cdot \max_i |K_{ii}|\)
Transfer coupling to RHS: \(\mathbf{P} \leftarrow \mathbf{P} - \mathbf{K}_{:,j} \bar{u}_j\)
Zero row and column: \(K_{j,:} = 0\), \(K_{:,j} = 0\)
Set diagonal and load: \(K_{jj} = k_s\), \(P_j = k_s \bar{u}_j\)
This preserves symmetry and gives exact constraint enforcement.
2.3 Multi-point constraints (Lagrange multipliers)#
Multi-point constraints couple multiple DOFs:
where \(\mathbf{G}\) is the \(m \times n\) constraint matrix and \(\mathbf{Q}\) is the \(m \times 1\) constraint RHS.
Variational formulation#
The constrained minimization problem:
The Lagrangian is:
Stationarity conditions:
Saddle-point system#
The KKT conditions form the saddle-point system:
Physical interpretation of \(\boldsymbol{\lambda}\):
The Lagrange multipliers represent the constraint forces. From the first equation:
The term \(-\mathbf{G}^T\boldsymbol{\lambda}\) is the reaction force required to enforce the constraint.
Numerical conditioning#
The saddle-point matrix is indefinite. For numerical stability, solve_lag_general scales the constraint equations:
where \(s \sim 0.01 \cdot \max_i |K_{ii}|\) balances the constraint rows with the stiffness entries.
After solving, the multipliers are rescaled: \(\boldsymbol{\lambda} = s \cdot \bar{\boldsymbol{\lambda}}\)
Example: rigid link constraint#
This example shows the same structure as the implementation: one equilibrium solve, one constraint equation, and one multiplier that reports the transmitted constraint force.
import numpy as np
def solve_lag_general(K, p, G, Q):
scale = 1.0e-2 * np.max(np.abs(np.diag(K)))
Gbar = scale * G
Qbar = scale * Q
# Build the saddle-point matrix
Kbar = np.block([
[K, Gbar.T],
[Gbar, np.zeros((G.shape[0], G.shape[0]))]
])
# Build the augmented right-hand side
pbar = np.vstack([p, Qbar])
# Solve the augmented system
augmented = np.linalg.solve(Kbar, pbar)
u = augmented[:K.shape[0]]
lagrange = augmented[K.shape[0]:] * scale
return u, lagrange
def main():
K = np.array([
[1500.0, -500.0],
[-500.0, 500.0]
])
p = np.array([[0.0],
[100.0]])
G = np.array([[1.0, -1.0]])
Q = np.array([[0.0]])
u, lagrange = solve_lag_general(K, p, G, Q)
print("Displacements:")
print(f"u_1 = {u[0,0]:.5f}")
print(f"u_2 = {u[1,0]:.5f}")
print("\nConstraint Force (Lagrange Multiplier):")
print(f"lambda = {lagrange[0,0]:.5f}")
if __name__ == "__main__":
main()
The key outcome is that the constraint is enforced without guessing a penalty stiffness. That makes the method useful whenever the exact multiplier value matters, such as in periodic constraints or multi-point tie conditions.
Summary#
femlabpy uses direct elimination for standard fixed DOFs and Lagrange multipliers for general linear constraints. The two paths share the same global indexing rules, so the boundary-condition code stays consistent with the rest of the assembly pipeline.