For those curious about the implementation details of the dynamic relaxation algorithm in the COMPAS framework, we show the general structure of code and their overall logic here. Looking through the code of compas.numerical.dr.dr.py:
Firstly, general helper variables, functions, and classes are defined at the start:
# Butcher Tableau for Runge-Kutta integration schemeK = [ [0.0], [0.5,0.5], [0.5,0.0,0.5], [1.0,0.0,0.0,1.0],]classCoeff():"""Damping coefficients."""def__init__(self,c): self.c = c self.a = (1- c *0.5) / (1+ c *0.5) self.b =0.5* (1+ self.a)defnorm_vector(vector): ...defnorm_vectors(vectors): ...defadjacency_from_edges(edges): ...
Next, inside the body of the dr function, the input arguments are pre-processed and default values are assigned to the remaining iteration parameters:
# --------------------------------------------------------------------------# attribute lists# -------------------------------------------------------------------------- X = vertices # m P = loads # kN Q_pre = qpre or [0.0for _ inrange(e)] # kN/m F_pre = fpre or [0.0for _ inrange(e)] # kN L_pre = lpre or [0.0for _ inrange(e)] # m L_init = linit or [0.0for _ inrange(e)] # m E = E or [0.0for _ inrange(e)] # kN/mm2 => GPa radius = radius or [0.0for _ inrange(e)] # mm# --------------------------------------------------------------------------# sectional properties# -------------------------------------------------------------------------- A = [3.14159* r **2for r in radius] # mm2 EA = [_e * _a for _e, _a inzip(E, A)] # kN# --------------------------------------------------------------------------# initial values# -------------------------------------------------------------------------- Q = [1.0for _ inrange(e)] L = [sum((X[i][axis] - X[j][axis]) **2for axis in (0, 1, 2))**0.5for i, j initer(edges)]# if none of the initial lengths are set,# set the initial lengths to the current lengths if all(l == 0 for l in L_init): L_init = L[:] F = [q * l for q, l inzip(Q, L)] M = [sum(0.5* dt **2* Q[ij_e[(i, j)]] for j in i_nbrs[i])for i inrange(n)] V = [[0.0,0.0,0.0] for _ inrange(n)] R = [[0.0,0.0,0.0] for _ inrange(n)] dX = [[0.0,0.0,0.0] for _ inrange(n)]
Next, an update_residuals and Runge-Kutta integration function are declared. The latter is nothing more than a refined method for deriving the nodal residuals at each step. Runge-Kutta integration evaluates multiple forward steps, and returns a weighted average of the residuals for each forward step. Depending on the number of forward steps (2 or 4), different weighting factors are used. This gives a faster and more robust convergence. For a concise mathematical background, those interested are referred here.
The logic core of the algorithm is defined in the iteration section. It can be seen that the different edge target inputs (force density, force, length and strain) all compete through their summation into one force density per edge. Hence, the algorithm allows to input these various edge target variables, but providing competing values for them all at once may give unexpected and/or uncontrollable results.
# --------------------------------------------------------------------------# iteration# --------------------------------------------------------------------------for k inrange(kmax):# get force density contribution form each source Q_fpre = [f / l if l else0for f, l inzip(F_pre, L)] Q_lpre = [f / l if l else0for f, l inzip(F, L_pre)] Q_ea = [ea * (l - linit) / (linit * l) if (l * linit)else0for ea, l, linit inzip(EA, L, L_init)]# set total force density as sum from each source Q = [qq + qf + ql + qea for qq, qf, ql, qeainzip(Q_pre, Q_fpre, Q_lpre, Q_ea)]# set fictitious nodal mass M = [sum(0.5* dt **2* Q[ij_e[(i, j)]] for j in i_nbrs[i])for i inrange(n)]# store geometry of previous iteration X0 =deepcopy(X)# get damped nodal velocities V0 = [[ca * V[i][axis] for axis in (0,1,2)] for i inrange(n)]# get damped nodal accelerations from Runge-Kutta integration dV =runge_kutta(X0, V0, steps=2)# update nodal coordinatesfor i in free: V[i]= [V0[i][axis] + dV[i][axis] for axis in (0,1,2)] dX[i]= [V[i][axis] * dt for axis in (0,1,2)] X[i]= [X0[i][axis] + dX[i][axis] for axis in (0,1,2)]# update edge lengths, forces, nodal residuals L = [sum((X[i][axis] - X[j][axis]) **2for axis in (0, 1, 2))**0.5for i, j initer(edges)] F = [q * l for q, l inzip(Q, L)]update_R()# stopping critertia crit1 =max(norm_vectors([R[i] for i in free])) crit2 =max(norm_vectors([dX[i] for i in free]))# callbackif callback:callback(k, X, (crit1, crit2), callback_args)# convergenceif crit1 < tol1:breakif crit2 < tol2:break# --------------------------------------------------------------------------# update# --------------------------------------------------------------------------update_R()return X, Q, F, L, R
One of the stopping criteria is defined as before: the maximum residual reaching under a certain threshold. An alternative criterion is provided as a kind of energy convergence: the maximum nodal displacement is a measure of how much kinematic energy there is in the dynamic system.
Finally, the updated nested lists of coordinates, force densities, edge lengths and residuals are returned.