def compute_internal_work_compression(form, force): """Compute the work done by the internal compressive forces of a structure. Parameters ---------- form : FormDiagram The form diagram. force : ForceDiagram The force diagram. Returns ------- float The internal work done by the compressive forces in a structure. Notes ----- ... References ---------- ... See Also -------- :func:`compute_internal_work_tension` Examples -------- .. code-block:: python # """ k_i = form.key_index() xy = array(form.xy(), dtype=float64) edges = [(k_i[u], k_i[v]) for u, v in form.edges()] C = connectivity_matrix(edges, 'csr') q = array(form.q(), dtype=float64).reshape((-1, 1)) _xy = force.xy() _edges = force.ordered_edges(form) _C = connectivity_matrix(_edges, 'csr') leaves = set(form.leaves()) internal = [ i for i, (u, v) in enumerate(form.edges()) if u not in leaves and v not in leaves ] compression = [i for i in internal if q[i, 0] < 0] l = normrow(C.dot(xy)) _l = normrow(_C.dot(_xy)) li = l[compression] _li = _l[compression] return li.T.dot(_li)[0, 0]
def objfunc(_x): _xy[_free, 0] = _x update_primal_from_dual(xy, _xy, free, leaves, i_j, ij_e, _C) length = normrow(C.dot(xy)) force = normrow(_C.dot(_xy)) lp = length[internal].T.dot(force[internal])[0, 0] print(lp) return(lp)
def _beam_shear(S, X, inds, indi, indf, EIx, EIy): """ Generate the beam nodal shear forces Sx, Sy and Sz. Parameters: S (array): Nodal shear force array. X (array): Co-ordinates of nodes. inds (list): Indices of beam element start nodes. indi (list): Indices of beam element intermediate nodes. indf (list): Indices of beam element finish nodes beams. EIx (array): Nodal EIx flexural stiffnesses. EIy (array): Nodal EIy flexural stiffnesses. Returns: array: Updated beam nodal shears. """ S *= 0 Xs = X[inds, :] Xi = X[indi, :] Xf = X[indf, :] Qa = Xi - Xs Qb = Xf - Xi Qc = Xf - Xs Qn = cross(Qa, Qb) mu = 0.5 * (Xf - Xs) La = normrow(Qa) Lb = normrow(Qb) Lc = normrow(Qc) LQn = normrow(Qn) Lmu = normrow(mu) a = arccos((La**2 + Lb**2 - Lc**2) / (2 * La * Lb)) k = 2 * sin(a) / Lc ex = Qn / tile(LQn, (1, 3)) # temporary simplification ez = mu / tile(Lmu, (1, 3)) ey = cross(ez, ex) K = tile(k / LQn, (1, 3)) * Qn Kx = tile(sum(K * ex, 1)[:, newaxis], (1, 3)) * ex Ky = tile(sum(K * ey, 1)[:, newaxis], (1, 3)) * ey Mc = EIx * Kx + EIy * Ky cma = cross(Mc, Qa) cmb = cross(Mc, Qb) ua = cma / tile(normrow(cma), (1, 3)) ub = cmb / tile(normrow(cmb), (1, 3)) c1 = cross(Qa, ua) c2 = cross(Qb, ub) Lc1 = normrow(c1) Lc2 = normrow(c2) Ms = sum(Mc**2, 1)[:, newaxis] Sa = ua * tile(Ms * Lc1 / (La * sum(Mc * c1, 1)[:, newaxis]), (1, 3)) Sb = ub * tile(Ms * Lc2 / (Lb * sum(Mc * c2, 1)[:, newaxis]), (1, 3)) Sa[isnan(Sa)] = 0 Sb[isnan(Sb)] = 0 S[inds, :] += Sa S[indi, :] -= Sa + Sb S[indf, :] += Sb # Add node junction duplication for when elements cross each other # mu[0, :] = -1.25*x[0, :] + 1.5*x[1, :] - 0.25*x[2, :] # mu[-1, :] = 0.25*x[-3, :] - 1.5*x[-2, :] + 1.25*x[-1, :] return S
def objfunc(_x): _xy[_free, 0] = _x update_form_from_force(xy, _xy, free, leaves, i_j, ij_e, _C) l = normrow(C.dot(xy)) _l = normrow(_C.dot(_xy)) li = l[internal] _li = _l[internal] lp = li.T.dot(_li)[0, 0] print(lp) return (lp)
def _beam_shear(S, X, inds, indi, indf, EIx, EIy): S *= 0 Xs = X[inds, :] Xi = X[indi, :] Xf = X[indf, :] Qa = Xi - Xs Qb = Xf - Xi Qc = Xf - Xs Qn = cross(Qa, Qb) mu = 0.5 * (Xf - Xs) La = normrow(Qa) Lb = normrow(Qb) Lc = normrow(Qc) LQn = normrow(Qn) Lmu = normrow(mu) a = arccos((La**2 + Lb**2 - Lc**2) / (2 * La * Lb)) k = 2 * sin(a) / Lc ex = Qn / tile(LQn, (1, 3)) # temporary simplification ez = mu / tile(Lmu, (1, 3)) ey = cross(ez, ex) K = tile(k / LQn, (1, 3)) * Qn Kx = tile(sum(K * ex, 1)[:, newaxis], (1, 3)) * ex Ky = tile(sum(K * ey, 1)[:, newaxis], (1, 3)) * ey Mc = EIx * Kx + EIy * Ky cma = cross(Mc, Qa) cmb = cross(Mc, Qb) ua = cma / tile(normrow(cma), (1, 3)) ub = cmb / tile(normrow(cmb), (1, 3)) c1 = cross(Qa, ua) c2 = cross(Qb, ub) Lc1 = normrow(c1) Lc2 = normrow(c2) Ms = sum(Mc**2, 1)[:, newaxis] Sa = ua * tile(Ms * Lc1 / (La * sum(Mc * c1, 1)[:, newaxis]), (1, 3)) Sb = ub * tile(Ms * Lc2 / (Lb * sum(Mc * c2, 1)[:, newaxis]), (1, 3)) Sa[isnan(Sa)] = 0 Sb[isnan(Sb)] = 0 S[inds, :] += Sa S[indi, :] -= Sa + Sb S[indf, :] += Sb # Add node junction duplication for when elements cross each other # mu[0, :] = -1.25*x[0, :] + 1.5*x[1, :] - 0.25*x[2, :] # mu[-1, :] = 0.25*x[-3, :] - 1.5*x[-2, :] + 1.25*x[-1, :] return S
def icp_numpy(source, target, tol=1e-3): """Align two point clouds using the Iterative Closest Point (ICP) method. Parameters ---------- source : list of point The source data. target : list of point The target data. tol : float, optional Tolerance for finding matches. Default is ``1e-3``. Returns ------- The transformed points Notes ----- First we align the source with the target cloud using the frames resulting from a PCA of each of the clouds, simply by calculating a frame-to-frame transformation. This initial alignment is used to establish an initial correspondence between the points of the two clouds. Then we iteratively improve the alignment by computing successive "best-fit" transformations using SVD of the cross-covariance matrix of the two data sets. During this iterative process, we continuously update the correspondence between the point clouds by finding the closest point in the target to each of the source points. The algorithm terminates when the alignment error is below a specified tolerance. Examples -------- >>> """ A = asarray(source) B = asarray(target) origin, axes, _ = pca_numpy(A) A_frame = Frame(origin, axes[0], axes[1]) origin, axes, _ = pca_numpy(B) B_frame = Frame(origin, axes[0], axes[1]) X = Transformation.from_frame_to_frame(A_frame, B_frame) A = transform_points_numpy(A, X) for i in range(20): D = cdist(A, B, 'euclidean') closest = argmin(D, axis=1) if norm(normrow(A - B[closest])) < tol: break X = bestfit_transform(A, B[closest]) A = transform_points_numpy(A, X) return A, X
def trimesh_vertexarea_matrix(mesh): """Compute the n x n diagonal matrix of per-vertex voronoi areas. Parameters ---------- mesh : :class:`compas.datastructures.Mesh` The triangle mesh data structure. Returns ------- sparse matrix The diagonal voronoi area matrix. Examples -------- >>> from compas.datastructures import Mesh >>> mesh = Mesh.from_polygons([[[0, 0, 0], [1, 0, 0], [0, 1, 0]]]) >>> A = trimesh_vertexarea_matrix(mesh) >>> A.diagonal().tolist() [0.1666, 0.1666, 0.1666] """ key_index = mesh.key_index() xyz = asarray(mesh.vertices_attributes('xyz'), dtype=float) tris = asarray([[key_index[key] for key in mesh.face_vertices(fkey)] for fkey in mesh.faces()], dtype=int) e1 = xyz[tris[:, 1]] - xyz[tris[:, 0]] e2 = xyz[tris[:, 2]] - xyz[tris[:, 0]] n = cross(e1, e2) a = 0.5 * normrow(n).ravel() a3 = a / 3.0 area = zeros(xyz.shape[0]) for i in (0, 1, 2): b = bincount(tris[:, i], a3) area[:len(b)] += b return spdiags(area, 0, xyz.shape[0], xyz.shape[0])
def compute_external_work(form, force): """Compute the external work of a structure. The external work done by a structure is equal to the work done by the external forces. This is equal to the sum of the dot products of the force vectors and the vectors defined by the force application point and a fixed arbitrary point. Parameters ---------- form : FormDiagram The form diagram. force : ForceDiagram The force diagram. Returns ------- float The external work done by the structure. Examples -------- >>> """ vertex_index = form.vertex_index() xy = array(form.xy(), dtype=float64) edges = [(vertex_index[u], vertex_index[v]) for u, v in form.edges()] C = connectivity_matrix(edges, 'csr') _vertex_index = force.vertex_index() _xy = force.xy() _edges = force.ordered_edges(form) _edges[:] = [(_vertex_index[u], _vertex_index[v]) for u, v in _edges] _C = connectivity_matrix(_edges, 'csr') leaves = set(form.leaves()) external = [ i for i, (u, v) in enumerate(form.edges()) if u in leaves or v in leaves ] lengths = normrow(C.dot(xy)) forces = normrow(_C.dot(_xy)) return lengths[external].T.dot(forces[external])[0, 0]
def trimesh_vertexarea_matrix(mesh): """Compute the n x n diagonal matrix of per-vertex voronoi areas. Parameters ---------- mesh : compas.datastructures.Mesh The triangle mesh data structure. Returns ------- sparse matrix The diagonal voronoi area matrix. Examples -------- .. plot:: :include-source: import compas from compas.datastructures import Mesh from compas.datastructures import mesh_quads_to_triangles from compas.datastructures import trimesh_vertexarea_matrix from compas_plotters import MeshPlotter mesh = Mesh.from_obj(compas.get('faces.obj')) mesh_quads_to_triangles(mesh) A = trimesh_vertexarea_matrix(mesh) area = A.diagonal().tolist() plotter = MeshPlotter(mesh, tight=True) plotter.draw_vertices( text={key: "{:.1f}".format(area[i]) for i, key in enumerate(mesh.vertices())}, radius=0.2 ) plotter.draw_edges() plotter.draw_faces() plotter.show() """ key_index = mesh.key_index() xyz = asarray(mesh.get_vertices_attributes('xyz'), dtype=float) tris = asarray([[key_index[key] for key in mesh.face_vertices(fkey)] for fkey in mesh.faces()], dtype=int) e1 = xyz[tris[:, 1]] - xyz[tris[:, 0]] e2 = xyz[tris[:, 2]] - xyz[tris[:, 0]] n = cross(e1, e2) a = 0.5 * normrow(n).ravel() a3 = a / 3.0 area = zeros(xyz.shape[0]) for i in (0, 1, 2): b = bincount(tris[:, i], a3) area[:len(b)] += b return spdiags(area, 0, xyz.shape[0], xyz.shape[0])
def _beam_shear(S, X, inds, indi, indf, EIx, EIy): S *= 0 Xs = X[inds, :] Xi = X[indi, :] Xf = X[indf, :] Qa = Xi - Xs Qb = Xf - Xi Qc = Xf - Xs Qn = cross(Qa, Qb) mu = 0.5 * (Xf - Xs) La = normrow(Qa) Lb = normrow(Qb) Lc = normrow(Qc) LQn = normrow(Qn) Lmu = normrow(mu) a = arccos((La**2 + Lb**2 - Lc**2) / (2 * La * Lb)) k = 2 * sin(a) / Lc ex = Qn / tile(LQn, (1, 3)) ez = mu / tile(Lmu, (1, 3)) ey = cross(ez, ex) K = tile(k / LQn, (1, 3)) * Qn Kx = tile(sum(K * ex, 1)[:, newaxis], (1, 3)) * ex Ky = tile(sum(K * ey, 1)[:, newaxis], (1, 3)) * ey Mc = EIx * Kx + EIy * Ky cma = cross(Mc, Qa) cmb = cross(Mc, Qb) ua = cma / tile(normrow(cma), (1, 3)) ub = cmb / tile(normrow(cmb), (1, 3)) c1 = cross(Qa, ua) c2 = cross(Qb, ub) Lc1 = normrow(c1) Lc2 = normrow(c2) Ms = sum(Mc**2, 1)[:, newaxis] Sa = ua * tile(Ms * Lc1 / (La * sum(Mc * c1, 1)[:, newaxis]), (1, 3)) Sb = ub * tile(Ms * Lc2 / (Lb * sum(Mc * c2, 1)[:, newaxis]), (1, 3)) Sa[isnan(Sa)] = 0 Sb[isnan(Sb)] = 0 S[inds, :] += Sa S[indi, :] -= Sa + Sb S[indf, :] += Sb return S
def compute_internal_work_compression(form, force): """Compute the work done by the internal compressive forces of a structure. Parameters ---------- form : FormDiagram The form diagram. force : ForceDiagram The force diagram. Returns ------- float The internal work done by the compressive forces in a structure. Examples -------- >>> """ vertex_index = form.vertex_index() xy = array(form.xy(), dtype=float64) edges = [(vertex_index[u], vertex_index[v]) for u, v in form.edges()] C = connectivity_matrix(edges, 'csr') q = array(form.q(), dtype=float64).reshape((-1, 1)) _vertex_index = force.vertex_index() _xy = force.xy() _edges = force.ordered_edges(form) _edges[:] = [(_vertex_index[u], _vertex_index[v]) for u, v in _edges] _C = connectivity_matrix(_edges, 'csr') leaves = set(form.leaves()) internal = [ i for i, (u, v) in enumerate(form.edges()) if u not in leaves and v not in leaves ] compression = [i for i in internal if q[i, 0] < 0] lengths = normrow(C.dot(xy)) forces = normrow(_C.dot(_xy)) return lengths[compression].T.dot(forces[compression])[0, 0]
def parallelise_nodal(xy, C, targets, i_nbrs, ij_e, fixed=None, kmax=100, lmin=None, lmax=None): fixed = fixed or [] fixed = set(fixed) n = xy.shape[0] for k in range(kmax): xy0 = xy.copy() uv = C.dot(xy) l = normrow(uv) # noqa: E741 if lmin is not None and lmax is not None: apply_bounds(l, lmin, lmax) for j in range(n): if j in fixed: continue nbrs = i_nbrs[j] xy[j, :] = 0.0 for i in nbrs: if (i, j) in ij_e: e = ij_e[(i, j)] t = targets[e] elif (j, i) in ij_e: e = ij_e[(j, i)] t = -targets[e] else: continue xy[j] += xy0[i] + l[e, 0] * t # add damping factor? xy[j] /= len(nbrs) for (i, j) in ij_e: e = ij_e[(i, j)] if l[e, 0] == 0.0: a = xy[i] b = xy[j] c = 0.5 * (a + b) xy[i] = c[:] xy[j] = c[:]
def form_update_q_from_qind(form): """Update the force densities of the dependent edges of a form diagram using the values of the independent ones. Parameters ---------- form : FormDiagram The form diagram. Returns ------- None The updated force densities are stored as attributes of the edges of the form diagram. Examples -------- .. code-block:: python # """ k_i = form.key_index() uv_index = form.uv_index() vcount = form.number_of_vertices() ecount = form.number_of_edges() fixed = form.leaves() fixed = [k_i[key] for key in fixed] free = list(set(range(vcount)) - set(fixed)) ind = [ index for index, (u, v, attr) in enumerate(form.edges(True)) if attr['is_ind'] ] dep = list(set(range(ecount)) - set(ind)) edges = [(k_i[u], k_i[v]) for u, v in form.edges()] xy = array(form.xy(), dtype=float64).reshape((-1, 2)) q = array(form.q(), dtype=float64).reshape((-1, 1)) C = connectivity_matrix(edges, 'csr') E = equilibrium_matrix(C, xy, free, 'csr') update_q_from_qind(E, q, dep, ind) uv = C.dot(xy) l = normrow(uv) f = q * l for u, v, attr in form.edges(True): index = uv_index[(u, v)] attr['q'] = q[index, 0] attr['f'] = f[index, 0] attr['l'] = l[index, 0]
def form_update_q_from_qind(form): """Update the force densities of the dependent edges of a form diagram using the values of the independent ones. Parameters ---------- form : FormDiagram The form diagram. Returns ------- None The updated force densities are stored as attributes of the edges of the form diagram. Examples -------- >>> """ vertex_index = form.vertex_index() edge_index = form.edge_index() vcount = form.number_of_vertices() ecount = form.number_of_edges() fixed = form.leaves() fixed = [vertex_index[vertex] for vertex in fixed] free = list(set(range(vcount)) - set(fixed)) ind = [edge_index[edge] for edge in form.ind()] dep = list(set(range(ecount)) - set(ind)) edges = [(vertex_index[u], vertex_index[v]) for u, v in form.edges()] xy = array(form.xy(), dtype=float64).reshape((-1, 2)) q = array(form.q(), dtype=float64).reshape((-1, 1)) C = connectivity_matrix(edges, 'csr') E = equilibrium_matrix(C, xy, free, 'csr') update_q_from_qind(E, q, dep, ind) uv = C.dot(xy) lengths = normrow(uv) forces = q * lengths for edge in form.edges(): index = edge_index[edge] form.edge_attributes( edge, ['q', 'f', 'l'], [q[index, 0], forces[index, 0], lengths[index, 0]])
def update_form_from_force(xy, _xy, free, leaves, i_nbrs, ij_e, _C, kmax=100): r"""Update the coordinates of a form diagram using the coordinates of the corresponding force diagram. Parameters ---------- xy : array-like XY coordinates of the vertices of the form diagram. _xy : array-like XY coordinates of the vertices of the force diagram. free : list The free vertices of the form diagram. leaves : list The leaves of the form diagram. i_nbrs : list of list of int Vertex neighbours per vertex. ij_e : dict Edge index for every vertex pair. _C : sparse matrix in csr format The connectivity matrix of the force diagram. kmax : int, optional Maximum number of iterations. Default is ``100``. Returns ------- None The vertex coordinates are modified in-place. Notes ----- This function should be used to update the form diagram after modifying the geometry of the force diagram. The objective is to compute new locations for the vertices of the form diagram such that the corrsponding lines of the form and force diagram are parallel while any geometric constraints imposed on the form diagram are satisfied. The location of each vertex of the form diagram is computed as the intersection of the lines connected to it. Each of the connected lines is based at the connected neighbouring vertex and taken parallel to the corresponding line in the force diagram. For a point :math:`\mathbf{p}`, which is the least-squares intersection of *K* lines, with every line *j* defined by a point :math:`\mathbf{a}_{j}` on the line and a direction vector :math:`\mathbf{n}_{j}`, we can write .. math:: \mathbf{R} \mathbf{p} = \mathbf{q} with .. math:: \mathbf{R} = \displaystyle\sum_{j=1}^{K}(\mathbf{I} - \mathbf{n}_{j}\mathbf{n}_{j}^{T}) \quad,\quad \mathbf{q} = \displaystyle\sum_{j=1}^{K}(\mathbf{I} - \mathbf{n}_{j}\mathbf{n}_{j}^{T})\mathbf{a}_{j} This system of linear equations can be solved using the normal equations .. math:: \mathbf{p} = (\mathbf{R}^{T}\mathbf{R})^{-1}\mathbf{R}^{T}\mathbf{q} Examples -------- >>> """ _uv = _C.dot(_xy) _t = normalizerow(_uv) I = eye(2, dtype=float64) # noqa: E741 xy0 = array(xy, copy=True) A = zeros((2 * len(free), 2 * len(free)), dtype=float64) b = zeros((2 * len(free), 1), dtype=float64) # update the free vertices for k in range(kmax): row = 0 # in order for the two diagrams to have parallel corresponding edges, # each free vertex location of the form diagram is computed as the intersection # of the connected lines. each of these lines is based at the corresponding # connected neighbouring vertex and taken parallel to the corresponding # edge in the force diagram. # the intersection is the point that minimises the distance to all connected # lines. for i in free: R = zeros((2, 2), dtype=float64) q = zeros((2, 1), dtype=float64) # add line constraints based on connected edges for j in i_nbrs[i]: if j in leaves: continue n = _t[ij_e[(i, j)], None] # the direction of the line (the line parallel to the corresponding line in the force diagram) _l = _uv[ij_e[(i, j)], None] if normrow(_l)[0, 0] < 0.001: continue r = I - n.T.dot(n) # projection into the orthogonal space of the direction vector a = xy[j, None] # a point on the line (the neighbour of the vertex) R += r q += r.dot(a.T) A[row: row + 2, row: row + 2] = R b[row: row + 2] = q row += 2 # p = solve(R.T.dot(R), R.T.dot(q)) # xy[i] = p.reshape((-1, 2), order='C') # res = solve(A.T.dot(A), A.T.dot(b)) # xy[free] = res.reshape((-1, 2), order='C') res = lstsq(A, b) xy[free] = res[0].reshape((-1, 2), order='C') # reconnect leaves for i in leaves: j = i_nbrs[i][0] xy[i] = xy[j] + xy0[i] - xy0[j]
def dr_numpy(vertices, edges, fixed, loads, qpre, fpre, lpre, linit, E, radius, callback=None, callback_args=None, **kwargs): """Implementation of the dynamic relaxation method for form findong and analysis of articulated networks of axial-force members. Parameters ---------- vertices : list XYZ coordinates of the vertices. edges : list Connectivity of the vertices. fixed : list Indices of the fixed vertices. loads : list XYZ components of the loads on the vertices. qpre : list Prescribed force densities in the edges. fpre : list Prescribed forces in the edges. lpre : list Prescribed lengths of the edges. linit : list Initial length of the edges. E : list Stiffness of the edges. radius : list Radius of the edges. callback : callable, optional User-defined function that is called at every iteration. callback_args : tuple, optional Additional arguments passed to the callback. Notes ----- For more info, see [1]_. References ---------- .. [1] De Laet L., Veenendaal D., Van Mele T., Mollaert M. and Block P., *Bending incorporated: designing tension structures by integrating bending-active elements*, Proceedings of Tensinet Symposium 2013,Istanbul, Turkey, 2013. Examples -------- .. plot:: :include-source: import compas from compas.datastructures import Network from compas_plotters import NetworkPlotter from compas.numerical import dr_numpy dva = { 'is_fixed': False, 'x': 0.0, 'y': 0.0, 'z': 0.0, 'px': 0.0, 'py': 0.0, 'pz': 0.0, 'rx': 0.0, 'ry': 0.0, 'rz': 0.0, } dea = { 'qpre': 1.0, 'fpre': 0.0, 'lpre': 0.0, 'linit': 0.0, 'E': 0.0, 'radius': 0.0, } network = Network.from_obj(compas.get('lines.obj')) network.update_default_vertex_attributes(dva) network.update_default_edge_attributes(dea) for key, attr in network.vertices(True): attr['is_fixed'] = network.vertex_degree(key) == 1 for index, (u, v, attr) in enumerate(network.edges(True)): attr['qpre'] = index + 1 k_i = network.key_index() vertices = network.get_vertices_attributes(('x', 'y', 'z')) edges = [(k_i[u], k_i[v]) for u, v in network.edges()] fixed = [k_i[key] for key in network.vertices_where({'is_fixed': True})] loads = network.get_vertices_attributes(('px', 'py', 'pz')) qpre = network.get_edges_attribute('qpre') fpre = network.get_edges_attribute('fpre') lpre = network.get_edges_attribute('lpre') linit = network.get_edges_attribute('linit') E = network.get_edges_attribute('E') radius = network.get_edges_attribute('radius') lines = [] for u, v in network.edges(): lines.append({ 'start': network.vertex_coordinates(u, 'xy'), 'end' : network.vertex_coordinates(v, 'xy'), 'color': '#cccccc', 'width': 1.0 }) plotter = NetworkPlotter(network) plotter.draw_lines(lines) xyz, q, f, l, r = dr_numpy(vertices, edges, fixed, loads, qpre, fpre, lpre, linit, E, radius) for key, attr in network.vertices(True): index = k_i[key] attr['x'] = xyz[index, 0] attr['y'] = xyz[index, 1] attr['z'] = xyz[index, 2] plotter.draw_vertices( facecolor={key: '#ff0000' for key in network.vertices_where({'is_fixed': True})}) plotter.draw_edges() plotter.show() """ # -------------------------------------------------------------------------- # callback # -------------------------------------------------------------------------- if callback: assert callable(callback), 'The provided callback is not callable.' # -------------------------------------------------------------------------- # configuration # -------------------------------------------------------------------------- kmax = kwargs.get('kmax', 10000) dt = kwargs.get('dt', 1.0) tol1 = kwargs.get('tol1', 1e-3) tol2 = kwargs.get('tol2', 1e-6) coeff = Coeff(kwargs.get('c', 0.1)) ca = coeff.a cb = coeff.b # -------------------------------------------------------------------------- # attribute lists # -------------------------------------------------------------------------- num_v = len(vertices) num_e = len(edges) free = list(set(range(num_v)) - set(fixed)) # -------------------------------------------------------------------------- # attribute arrays # -------------------------------------------------------------------------- x = array(vertices, dtype=float).reshape((-1, 3)) # m p = array(loads, dtype=float).reshape((-1, 3)) # kN qpre = array(qpre, dtype=float).reshape((-1, 1)) fpre = array(fpre, dtype=float).reshape((-1, 1)) # kN lpre = array(lpre, dtype=float).reshape((-1, 1)) # m linit = array(linit, dtype=float).reshape((-1, 1)) # m E = array(E, dtype=float).reshape((-1, 1)) # kN/mm2 => GPa radius = array(radius, dtype=float).reshape((-1, 1)) # mm # -------------------------------------------------------------------------- # sectional properties # -------------------------------------------------------------------------- A = 3.14159 * radius ** 2 # mm2 EA = E * A # kN # -------------------------------------------------------------------------- # create the connectivity matrices # after spline edges have been aligned # -------------------------------------------------------------------------- C = connectivity_matrix(edges, 'csr') Ct = C.transpose() Ci = C[:, free] Cit = Ci.transpose() Ct2 = Ct.copy() Ct2.data **= 2 # -------------------------------------------------------------------------- # if none of the initial lengths are set, # set the initial lengths to the current lengths # -------------------------------------------------------------------------- if all(linit == 0): linit = normrow(C.dot(x)) # -------------------------------------------------------------------------- # initial values # -------------------------------------------------------------------------- q = ones((num_e, 1), dtype=float) l = normrow(C.dot(x)) # noqa: E741 f = q * l v = zeros((num_v, 3), dtype=float) r = zeros((num_v, 3), dtype=float) # -------------------------------------------------------------------------- # helpers # -------------------------------------------------------------------------- def rk(x0, v0, steps=2): def a(t, v): dx = v * t x[free] = x0[free] + dx[free] # update residual forces r[free] = p[free] - D.dot(x) return cb * r / mass if steps == 1: return a(dt, v0) if steps == 2: B = [0.0, 1.0] K0 = dt * a(K[0][0] * dt, v0) K1 = dt * a(K[1][0] * dt, v0 + K[1][1] * K0) dv = B[0] * K0 + B[1] * K1 return dv if steps == 4: B = [1. / 6., 1. / 3., 1. / 3., 1. / 6.] K0 = dt * a(K[0][0] * dt, v0) K1 = dt * a(K[1][0] * dt, v0 + K[1][1] * K0) K2 = dt * a(K[2][0] * dt, v0 + K[2][1] * K0 + K[2][2] * K1) K3 = dt * a(K[3][0] * dt, v0 + K[3][1] * K0 + K[3][2] * K1 + K[3][3] * K2) dv = B[0] * K0 + B[1] * K1 + B[2] * K2 + B[3] * K3 return dv raise NotImplementedError # -------------------------------------------------------------------------- # start iterating # -------------------------------------------------------------------------- for k in range(kmax): # print(k) q_fpre = fpre / l q_lpre = f / lpre q_EA = EA * (l - linit) / (linit * l) q_lpre[isinf(q_lpre)] = 0 q_lpre[isnan(q_lpre)] = 0 q_EA[isinf(q_EA)] = 0 q_EA[isnan(q_EA)] = 0 q = qpre + q_fpre + q_lpre + q_EA Q = diags([q[:, 0]], [0]) D = Cit.dot(Q).dot(C) mass = 0.5 * dt ** 2 * Ct2.dot(qpre + q_fpre + q_lpre + EA / linit) # RK x0 = x.copy() v0 = ca * v.copy() dv = rk(x0, v0, steps=4) v[free] = v0[free] + dv[free] dx = v * dt x[free] = x0[free] + dx[free] # update u = C.dot(x) l = normrow(u) # noqa: E741 f = q * l r = p - Ct.dot(Q).dot(u) # crits crit1 = norm(r[free]) crit2 = norm(dx[free]) # callback if callback: callback(k, x, [crit1, crit2], callback_args) # convergence if crit1 < tol1: break if crit2 < tol2: break return x, q, f, l, r
def update_xyz_numpy(mesh): """Find the equilibrium shape of a mesh for the given force densities. Parameters ---------- mesh : compas_fofin.datastructures.Cablenet The mesh to equilibriate. Returns ------- None The function updates the input mesh and returns nothing. """ k_i = mesh.key_index() fixed = mesh.vertices_where({'is_anchor': True}) fixed = [k_i[key] for key in fixed] free = list(set(range(mesh.number_of_vertices())) - set(fixed)) xyz = array(mesh.vertices_attributes('xyz'), dtype=float64) p = array(mesh.vertices_attributes(('px', 'py', 'pz')), dtype=float64) edges = [(k_i[u], k_i[v]) for u, v in mesh.edges_where({'is_edge': True})] q = array( [attr['q'] for key, attr in mesh.edges_where({'is_edge': True}, True)], dtype=float64).reshape((-1, 1)) density = mesh.attributes['density'] calculate_sw = SelfweightCalculator(mesh, density=density) if density: sw = calculate_sw(xyz) p[:, 2] = -sw[:, 0] C = connectivity_matrix(edges, 'csr') Ci = C[:, free] Cf = C[:, fixed] Ct = C.transpose() Cit = Ci.transpose() Q = diags([q.flatten()], [0]) A = Cit.dot(Q).dot(Ci) b = p[free] - Cit.dot(Q).dot(Cf).dot(xyz[fixed]) xyz[free] = spsolve(A, b) if density: sw = calculate_sw(xyz) p[:, 2] = -sw[:, 0] l = normrow(C.dot(xyz)) f = q * l r = p - Ct.dot(Q).dot(C).dot(xyz) for key, attr in mesh.vertices(True): index = k_i[key] attr['x'] = xyz[index, 0] attr['y'] = xyz[index, 1] attr['z'] = xyz[index, 2] attr['rx'] = r[index, 0] attr['ry'] = r[index, 1] attr['rz'] = r[index, 2] for index, (key, attr) in enumerate(mesh.edges_where({'is_edge': True}, True)): attr['q'] = q[index, 0] attr['f'] = f[index, 0] attr['l'] = l[index, 0]
def fn(dofs, *args): network, Xt, tol, ds = args X = update(dofs=dofs, network=network, tol=tol, plot=False, Xt=Xt, ds=ds) ind = argmin(cdist(X, Xt), axis=1) return 1000 * mean(normrow(X - Xt[ind, :]))
def _create_arrays(network): """ Create arrays for dynamic relaxation solver. Parameters: network (obj): Network to analyse. Returns: array: Nodal co-ordinates x, y, z. array: Constraint conditions Bx, By, Bz. array: Nodal loads Px, Py, Pz. array: Resultant nodal loads. array: Shear force components Sx, Sy, Sz. array: Nodal velocities Vx, Vy, Vz. array: Edge Young's moduli. array: Edge areas. array: Connectivity matrix. array: Transposed connectivity matrix. array: Edge initial forces. array: Edge initial lengths. list: Compression only edges indices. list: Tension only edges indices. array: Network edges' start points. array: Network edges' end points. array: Mass matrix. array: Edge axial stiffnesses. """ # Vertices n = network.number_of_vertices() B = zeros((n, 3)) P = zeros((n, 3)) X = zeros((n, 3)) S = zeros((n, 3)) V = zeros((n, 3)) k_i = network.key_index() for key in network.vertices(): i = k_i[key] vertex = network.vertex[key] B[i, :] = vertex.get('B', [1, 1, 1]) P[i, :] = vertex.get('P', [0, 0, 0]) X[i, :] = [vertex[j] for j in 'xyz'] Pn = normrow(P) # Edges uv_i = network.uv_index() edges = list(network.edges()) m = len(edges) u = zeros(m, dtype=int64) v = zeros(m, dtype=int64) E = zeros((m, 1)) A = zeros((m, 1)) s0 = zeros((m, 1)) l0 = zeros((m, 1)) ind_c = [] ind_t = [] for c, uv in enumerate(edges): ui, vi = uv i = uv_i[(ui, vi)] edge = network.edge[ui][vi] E[i] = edge.get('E', 0) A[i] = edge.get('A', 0) l0[i] = edge.get('l0', network.edge_length(ui, vi)) s0[i] = edge.get('s0', 0) u[c] = k_i[ui] v[c] = k_i[vi] ct = edge.get('ct', None) if ct == 'c': ind_c.append(i) elif ct == 't': ind_t.append(i) f0 = s0 * A ks = E * A / l0 q0 = f0 / l0 # Faces (testing) # if network.face: # for face in faces: # fdata = network.facedata[face] # Eh = fdata.get('E', 0) # th = fdata.get('t', 0) # Ah = network.face_area(face) # for ui, vi in network.face_edges(face): # i = uv_i[(ui, vi)] # ks[i] += 1.5 * Eh * Ah * th / l0[i]**2 # Arrays C = connectivity_matrix([[k_i[ui], k_i[vi]] for ui, vi in edges], 'csr') Ct = C.transpose() M = mass_matrix(Ct=Ct, ks=ks, q=q0, c=1, tiled=False) return X, B, P, Pn, S, V, E, A, C, Ct, f0, l0, ind_c, ind_t, u, v, M, ks
def drx_solver(tol, steps, factor, C, Ct, X, ks, l0, f0, ind_c, ind_t, P, S, B, M, V, refresh, beams, inds, indi, indf, EIx, EIy, callback, **kwargs): """ NumPy and SciPy dynamic relaxation solver. Parameters: tol (float): Tolerance limit. steps (int): Maximum number of steps. factor (float): Convergence factor. C (array): Connectivity matrix. Ct (array): Transposed connectivity matrix. X (array): Nodal co-ordinates. ks (array): Initial edge axial stiffnesses. l0 (array): Initial edge lengths. f0 (array): Initial edge forces. ind_c (list): Indices of compression only edges. ind_t (list): Indices of tension only edges. P (array): Nodal loads Px, Py, Pz. S (array): Shear forces Sx, Sy, Sz. B (array): Constraint conditions. M (array): Mass matrix. V (array): Nodal velocities Vx, Vy, Vz. refresh (int): Update progress every n steps. beams (bool): Dictionary of beam information. inds (list): Indices of beam element start nodes. indi (list): Indices of beam element intermediate nodes. indf (list): Indices of beam element finish nodes beams. EIx (array): Nodal EIx flexural stiffnesses. EIy (array): Nodal EIy flexural stiffnesses. callback (obj): Callback function. Returns: array: Updated nodal co-ordinates. array: Updated forces. array: Updated lengths. """ res = 1000 * tol ts, Uo = 0, 0 M = factor * tile(M, (1, 3)) while (ts <= steps) and (res > tol): uvw, l = uvw_lengths(C, X) f = f0 + ks * (l - l0) if ind_t: f[ind_t] *= f[ind_t] > 0 if ind_c: f[ind_c] *= f[ind_c] < 0 if beams: S = _beam_shear(S, X, inds, indi, indf, EIx, EIy) q = f / l qt = tile(q, (1, 3)) R = (P - S - Ct.dot(uvw * qt)) * B res = mean(normrow(R)) V += R / M Un = sum(M * V**2) if Un < Uo: V *= 0 Uo = Un X += V if refresh: if (ts % refresh == 0) or (res < tol): print('Step:{0} Residual:{1:.3g}'.format(ts, res)) if callback: callback(X, **kwargs) ts += 1 return X, f, l
def mesh_geodesic_distances_numpy(mesh, sources, m=1.0): """Compute geodesic from the vertices of a mesh to given source vertices. Parameters ---------- mesh : compas.datastructures.Mesh A mesh instance. sources : list A list of vertex identifiers from which the distances should be calculated. m : float (1.0) ? Returns ------- array Distance values. """ Lc = trimesh_cotangent_laplacian_matrix(mesh) key_index = mesh.key_index() vertices = mesh.vertices_attributes('xyz') faces = [[key_index[key] for key in mesh.face_vertices(fkey)] for fkey in mesh.faces()] V = array(vertices) F = array(faces, dtype=int) e01 = V[F[:, 1]] - V[F[:, 0]] e12 = V[F[:, 2]] - V[F[:, 1]] e20 = V[F[:, 0]] - V[F[:, 2]] normal = cross(e01, e12) A2 = normrow(normal) A3 = A2.ravel() / 6 VA = zeros(V.shape[0]) for i in (0, 1, 2): b = bincount(F[:, i], A3) VA[:len(b)] += b VA = spdiags(VA, 0, V.shape[0], V.shape[0]) h = mean([normrow(e01), normrow(e12), normrow(e20)]) t = m * h**2 u0 = zeros(V.shape[0]) u0[sources] = 1.0 # A = VA - t * Lc # print(A.sum(axis=1)) u = splu((VA - t * Lc).tocsc()).solve(u0) unit = normal / A2 unit_e01 = cross(unit, e01) unit_e12 = cross(unit, e12) unit_e20 = cross(unit, e20) grad_u = (unit_e01 * u[F[:, 2], None] + unit_e12 * u[F[:, 0], None] + unit_e20 * u[F[:, 1], None]) / A2 X = -grad_u / normrow(grad_u) div_X = zeros(V.shape[0]) for i1, i2, i3 in [(0, 1, 2), (1, 2, 0), (2, 0, 1)]: v1 = F[:, i1] v2 = F[:, i2] v3 = F[:, i3] e1 = V[v2] - V[v1] e2 = V[v3] - V[v1] e0 = V[v3] - V[v2] a = 1 / tan(arccos(sum(normalizerow(-e2) * normalizerow(-e0), axis=1))) b = 1 / tan(arccos(sum(normalizerow(-e1) * normalizerow(+e0), axis=1))) div_X += bincount(v1, 0.5 * (a * sum(e1 * X, axis=1) + b * sum(e2 * X, axis=1)), minlength=V.shape[0]) # print(Lc.sum(axis=1)) phi = splu(Lc.tocsc()).solve(div_X) phi -= phi.min() return phi
def fd_numpy(vertices, edges, fixed, q, loads, **kwargs): """Implementation of the force density method to compute equilibrium of axial force networks. Parameters ---------- vertices : list XYZ coordinates of the vertices of the network edges : list Edges between vertices represented by fixed : list Indices of fixed vertices. q : list Force density of edges. loads : list XYZ components of the loads on the vertices. Returns ------- xyz : array XYZ coordinates of the equilibrium geometry. q : array Force densities in the edges. f : array Forces in the edges. l : array Lengths of the edges r : array Residual forces. Notes ----- For more info, see [1]_ References ---------- .. [1] Schek H., *The Force Density Method for Form Finding and Computation of General Networks*, Computer Methods in Applied Mechanics and Engineering 3: 115-134, 1974. Examples -------- .. plot:: :include-source: import compas from compas.datastructures import Mesh from compas.plotters import MeshPlotter from compas.numerical import fd_numpy from compas.utilities import i_to_black # make a mesh # add default attributes for form finding mesh = Mesh.from_obj(compas.get('faces.obj')) mesh.update_default_vertex_attributes({'is_anchor': False, 'px': 0.0, 'py': 0.0, 'pz': 0.0}) mesh.update_default_edge_attributes({'q': 1.0}) # identify the anchors # move two anchors up to create anticlastic boundary conditions for key, attr in mesh.vertices(True): attr['is_anchor'] = mesh.vertex_degree(key) == 2 if key in (18, 35): attr['z'] = 5.0 # preprocess k_i = mesh.key_index() xyz = mesh.get_vertices_attributes(('x', 'y', 'z')) loads = mesh.get_vertices_attributes(('px', 'py', 'pz')) q = mesh.get_edges_attribute('q') fixed = mesh.vertices_where({'is_anchor': True}) fixed = [k_i[k] for k in fixed] edges = [(k_i[u], k_i[v]) for u, v in mesh.edges()] # compute equilibrium # update the mesh geometry xyz, q, f, l, r = fd_numpy(xyz, edges, fixed, q, loads) for key, attr in mesh.vertices(True): index = k_i[key] attr['x'] = xyz[index, 0] attr['y'] = xyz[index, 1] attr['z'] = xyz[index, 2] # visualisae the result # color the vertices according to their elevation plotter = MeshPlotter(mesh) zmax = max(mesh.get_vertices_attribute('z')) plotter.draw_vertices( facecolor={key: i_to_black(attr['z'] / zmax) for key, attr in mesh.vertices(True)} ) plotter.draw_faces() plotter.draw_edges() plotter.show() """ v = len(vertices) free = list(set(range(v)) - set(fixed)) xyz = asarray(vertices, dtype=float).reshape((-1, 3)) q = asarray(q, dtype=float).reshape((-1, 1)) p = asarray(loads, dtype=float).reshape((-1, 3)) C = connectivity_matrix(edges, 'csr') Ci = C[:, free] Cf = C[:, fixed] Ct = C.transpose() Cit = Ci.transpose() Q = diags([q.flatten()], [0]) A = Cit.dot(Q).dot(Ci) b = p[free] - Cit.dot(Q).dot(Cf).dot(xyz[fixed]) xyz[free] = spsolve(A, b) l = normrow(C.dot(xyz)) f = q * l r = p - Ct.dot(Q).dot(C).dot(xyz) return xyz, q, f, l, r
def horizontal(form, force, alpha=100.0, kmax=100, display=False): r"""Compute horizontal equilibrium. Parameters ---------- form : compas_tna.diagrams.formdiagram.FormDiagram force : compas_tna.diagrams.forcediagram.ForceDiagram alpha : float Weighting factor for computation of the target vectors (the default is 100.0, which implies that the target vectors are the edges of the form diagram). If 0.0, the target vectors are the edges of the force diagram. kmax : int Maximum number of iterations (the default is 100). display : bool Display information about the current iteration (the default is False). Notes ----- This implementation is based on the following formulation .. math:: \mathbf{C}^{T} \mathbf{C} \mathbf{xy} = \mathbf{C}^{T} \mathbf{t} with :math:`\mathbf{C}` the connectivity matrix and :math:`\mathbf{t}` the target vectors. """ # -------------------------------------------------------------------------- # alpha == 1 : form diagram fixed # alpha == 0 : force diagram fixed # -------------------------------------------------------------------------- alpha = max(0., min(1., float(alpha) / 100.0)) # -------------------------------------------------------------------------- # form diagram # -------------------------------------------------------------------------- k_i = form.key_index() uv_i = form.uv_index() fixed = set(list(form.anchors()) + list(form.fixed())) fixed = [k_i[key] for key in fixed] edges = [[k_i[u], k_i[v]] for u, v in form.edges_where({'_is_edge': True})] xy = array(form.vertices_attributes('xy'), dtype=float64) lmin = array([ attr.get('lmin', 1e-7) for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float64).reshape((-1, 1)) lmax = array([ attr.get('lmax', 1e+7) for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float64).reshape((-1, 1)) fmin = array([ attr.get('fmin', 1e-7) for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float64).reshape((-1, 1)) fmax = array([ attr.get('fmax', 1e+7) for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float64).reshape((-1, 1)) C = connectivity_matrix(edges, 'csr') Ct = C.transpose() CtC = Ct.dot(C) # -------------------------------------------------------------------------- # force diagram # -------------------------------------------------------------------------- _k_i = force.key_index() _uv_i = force.uv_index(form=form) _fixed = list(force.fixed()) _fixed = [_k_i[key] for key in _fixed] _fixed = _fixed or [0] _edges = force.ordered_edges(form) _xy = array(force.vertices_attributes('xy'), dtype=float64) _C = connectivity_matrix(_edges, 'csr') _Ct = _C.transpose() _Ct_C = _Ct.dot(_C) # -------------------------------------------------------------------------- # rotate force diagram to make it parallel to the form diagram # use CCW direction (opposite of cycle direction) # -------------------------------------------------------------------------- _xy[:] = rot90(_xy, +1.0) # -------------------------------------------------------------------------- # make the diagrams parallel to a target vector # that is the (alpha) weighted average of the directions of corresponding # edges of the two diagrams # -------------------------------------------------------------------------- uv = C.dot(xy) _uv = _C.dot(_xy) l = normrow(uv) _l = normrow(_uv) t = alpha * normalizerow(uv) + (1 - alpha) * normalizerow(_uv) # parallelise # add the outer loop to the parallelise function for k in range(kmax): # apply length bounds apply_bounds(l, lmin, lmax) apply_bounds(_l, fmin, fmax) # print, if allowed if display: print(k) if alpha != 1.0: # if emphasis is not entirely on the form # update the form diagram xy = parallelise_sparse(CtC, Ct.dot(l * t), xy, fixed, 'CtC') uv = C.dot(xy) l = normrow(uv) if alpha != 0.0: # if emphasis is not entirely on the force # update the force diagram _xy = parallelise_sparse(_Ct_C, _Ct.dot(_l * t), _xy, _fixed, '_Ct_C') _uv = _C.dot(_xy) _l = normrow(_uv) # -------------------------------------------------------------------------- # compute the force densities # -------------------------------------------------------------------------- f = _l q = (f / l).astype(float64) # -------------------------------------------------------------------------- # rotate the force diagram 90 degrees in CW direction # this way the relation between the two diagrams is easier to read # -------------------------------------------------------------------------- _xy[:] = rot90(_xy, -1.0) # -------------------------------------------------------------------------- # angle deviations # note that this does not account for flipped edges! # -------------------------------------------------------------------------- a = [angle_vectors_xy(uv[i], _uv[i], deg=True) for i in range(len(edges))] # -------------------------------------------------------------------------- # update form # -------------------------------------------------------------------------- for key, attr in form.vertices(True): i = k_i[key] attr['x'] = xy[i, 0] attr['y'] = xy[i, 1] for (u, v), attr in form.edges_where({'_is_edge': True}, True): i = uv_i[(u, v)] attr['q'] = q[i, 0] attr['_f'] = f[i, 0] attr['_l'] = l[i, 0] attr['_a'] = a[i] # -------------------------------------------------------------------------- # update force # -------------------------------------------------------------------------- for key, attr in force.vertices(True): i = _k_i[key] attr['x'] = _xy[i, 0] attr['y'] = _xy[i, 1] for (u, v), attr in force.edges_where({'_is_edge': True}, True): i = _uv_i[(u, v)] attr['_l'] = _l[i, 0]
def vertical_from_bbox(form, factor=5.0, kmax=100, tol=1e-3, density=1.0, display=True): # -------------------------------------------------------------------------- # FormDiagram # -------------------------------------------------------------------------- k_i = form.key_index() uv_i = form.uv_index() vcount = len(form.vertex) anchors = list(form.anchors()) fixed = list(form.fixed()) fixed = set(anchors + fixed) fixed = [k_i[key] for key in fixed] free = list(set(range(vcount)) - set(fixed)) edges = [(k_i[u], k_i[v]) for u, v in form.edges_where({'is_edge': True})] xyz = array(form.get_vertices_attributes('xyz'), dtype=float64) thick = array(form.get_vertices_attribute('t'), dtype=float64).reshape( (-1, 1)) p = array(form.get_vertices_attributes(('px', 'py', 'pz')), dtype=float64) q = [ attr.get('q', 1.0) for u, v, attr in form.edges_where({'is_edge': True}, True) ] q = array(q, dtype=float64).reshape((-1, 1)) C = connectivity_matrix(edges, 'csr') Ci = C[:, free] Cf = C[:, fixed] Cit = Ci.transpose() Ct = C.transpose() # -------------------------------------------------------------------------- # original data # -------------------------------------------------------------------------- p0 = array(p, copy=True) q0 = array(q, copy=True) # -------------------------------------------------------------------------- # load updater # -------------------------------------------------------------------------- update_loads = LoadUpdater(form, p0, thickness=thick, density=density) # -------------------------------------------------------------------------- # scale # -------------------------------------------------------------------------- (xmin, ymin, zmin), (xmax, ymax, zmax) = form.bbox() d = ((xmax - xmin)**2 + (ymax - ymin)**2)**0.5 scale = d / factor # -------------------------------------------------------------------------- # vertical # -------------------------------------------------------------------------- q = scale * q0 Q = diags([q.ravel()], [0]) update_z(xyz, Q, C, p, free, fixed, update_loads, tol=tol, kmax=kmax, display=display) # -------------------------------------------------------------------------- # update # -------------------------------------------------------------------------- l = normrow(C.dot(xyz)) f = q * l r = Ct.dot(Q).dot(C).dot(xyz) - p sw = p - p0 # -------------------------------------------------------------------------- # form # -------------------------------------------------------------------------- for key, attr in form.vertices(True): index = k_i[key] attr['z'] = xyz[index, 2] attr['rx'] = r[index, 0] attr['ry'] = r[index, 1] attr['rz'] = r[index, 2] attr['sw'] = sw[index, 2] for u, v, attr in form.edges_where({'is_edge': True}, True): index = uv_i[(u, v)] attr['f'] = f[index, 0] attr['l'] = l[index, 0] return scale
def dr_numpy(vertices, edges, fixed, loads, qpre, fpre, lpre, linit, E, radius, callback=None, callback_args=None, **kwargs): """Implementation of the dynamic relaxation method for form findong and analysis of articulated networks of axial-force members. Parameters ---------- vertices : list XYZ coordinates of the vertices. edges : list Connectivity of the vertices. fixed : list Indices of the fixed vertices. loads : list XYZ components of the loads on the vertices. qpre : list Prescribed force densities in the edges. fpre : list Prescribed forces in the edges. lpre : list Prescribed lengths of the edges. linit : list Initial length of the edges. E : list Stiffness of the edges. radius : list Radius of the edges. callback : callable, optional User-defined function that is called at every iteration. callback_args : tuple, optional Additional arguments passed to the callback. Returns ------- xyz : array XYZ coordinates of the equilibrium geometry. q : array Force densities in the edges. f : array Forces in the edges. l : array Lengths of the edges r : array Residual forces. Notes ----- For more info, see [1]_. References ---------- .. [1] De Laet L., Veenendaal D., Van Mele T., Mollaert M. and Block P., *Bending incorporated: designing tension structures by integrating bending-active elements*, Proceedings of Tensinet Symposium 2013,Istanbul, Turkey, 2013. Examples -------- >>> """ # -------------------------------------------------------------------------- # callback # -------------------------------------------------------------------------- if callback: assert callable(callback), 'The provided callback is not callable.' # -------------------------------------------------------------------------- # configuration # -------------------------------------------------------------------------- kmax = kwargs.get('kmax', 10000) dt = kwargs.get('dt', 1.0) tol1 = kwargs.get('tol1', 1e-3) tol2 = kwargs.get('tol2', 1e-6) coeff = Coeff(kwargs.get('c', 0.1)) ca = coeff.a cb = coeff.b # -------------------------------------------------------------------------- # attribute lists # -------------------------------------------------------------------------- num_v = len(vertices) num_e = len(edges) free = list(set(range(num_v)) - set(fixed)) # -------------------------------------------------------------------------- # attribute arrays # -------------------------------------------------------------------------- x = array(vertices, dtype=float).reshape((-1, 3)) # m p = array(loads, dtype=float).reshape((-1, 3)) # kN qpre = array(qpre, dtype=float).reshape((-1, 1)) fpre = array(fpre, dtype=float).reshape((-1, 1)) # kN lpre = array(lpre, dtype=float).reshape((-1, 1)) # m linit = array(linit, dtype=float).reshape((-1, 1)) # m E = array(E, dtype=float).reshape((-1, 1)) # kN/mm2 => GPa radius = array(radius, dtype=float).reshape((-1, 1)) # mm # -------------------------------------------------------------------------- # sectional properties # -------------------------------------------------------------------------- A = 3.14159 * radius**2 # mm2 EA = E * A # kN # -------------------------------------------------------------------------- # create the connectivity matrices # after spline edges have been aligned # -------------------------------------------------------------------------- C = connectivity_matrix(edges, 'csr') Ct = C.transpose() Ci = C[:, free] Cit = Ci.transpose() Ct2 = Ct.copy() Ct2.data **= 2 # -------------------------------------------------------------------------- # if none of the initial lengths are set, # set the initial lengths to the current lengths # -------------------------------------------------------------------------- if all(linit == 0): linit = normrow(C.dot(x)) # -------------------------------------------------------------------------- # initial values # -------------------------------------------------------------------------- q = ones((num_e, 1), dtype=float) l = normrow(C.dot(x)) # noqa: E741 f = q * l v = zeros((num_v, 3), dtype=float) r = zeros((num_v, 3), dtype=float) # -------------------------------------------------------------------------- # helpers # -------------------------------------------------------------------------- def rk(x0, v0, steps=2): def a(t, v): dx = v * t x[free] = x0[free] + dx[free] # update residual forces r[free] = p[free] - D.dot(x) return cb * r / mass if steps == 1: return a(dt, v0) if steps == 2: B = [0.0, 1.0] K0 = dt * a(K[0][0] * dt, v0) K1 = dt * a(K[1][0] * dt, v0 + K[1][1] * K0) dv = B[0] * K0 + B[1] * K1 return dv if steps == 4: B = [1. / 6., 1. / 3., 1. / 3., 1. / 6.] K0 = dt * a(K[0][0] * dt, v0) K1 = dt * a(K[1][0] * dt, v0 + K[1][1] * K0) K2 = dt * a(K[2][0] * dt, v0 + K[2][1] * K0 + K[2][2] * K1) K3 = dt * a(K[3][0] * dt, v0 + K[3][1] * K0 + K[3][2] * K1 + K[3][3] * K2) dv = B[0] * K0 + B[1] * K1 + B[2] * K2 + B[3] * K3 return dv raise NotImplementedError # -------------------------------------------------------------------------- # start iterating # -------------------------------------------------------------------------- for k in range(kmax): # print(k) q_fpre = fpre / l q_lpre = f / lpre q_EA = EA * (l - linit) / (linit * l) q_lpre[isinf(q_lpre)] = 0 q_lpre[isnan(q_lpre)] = 0 q_EA[isinf(q_EA)] = 0 q_EA[isnan(q_EA)] = 0 q = qpre + q_fpre + q_lpre + q_EA Q = diags([q[:, 0]], [0]) D = Cit.dot(Q).dot(C) mass = 0.5 * dt**2 * Ct2.dot(qpre + q_fpre + q_lpre + EA / linit) # RK x0 = x.copy() v0 = ca * v.copy() dv = rk(x0, v0, steps=4) v[free] = v0[free] + dv[free] dx = v * dt x[free] = x0[free] + dx[free] # update u = C.dot(x) l = normrow(u) # noqa: E741 f = q * l r = p - Ct.dot(Q).dot(u) # crits crit1 = norm(r[free]) crit2 = norm(dx[free]) # callback if callback: callback(k, x, [crit1, crit2], callback_args) # convergence if crit1 < tol1: break if crit2 < tol2: break return x, q, f, l, r
def optimise_loadpath(form, force, algo='COBYLA'): """Optimise the loadpath using the parameters of the force domain. The parameters of the force domain are the coordinates of the vertices of the force diagram. Parameters ---------- form : FormDiagram The form diagram. force : ForceDiagram The force diagram. algo : {'COBYLA', L-BFGS-B', 'SLSQ', 'MMA', 'GMMA'}, optional The optimisation algorithm. Returns ------- form: :class:`FormDiagram` The optimised form diagram. force: :class:`ForceDiagram` The optimised force diagram. Notes ----- In many cases, the number of paramters of the force domain involved in generating new solutions in the form domain is smaller than when using the elements of the form diagram directly. For example, the loadpath of a bridge with a compression arch can be optimised using only the x-coordinates of the vertices of the force diagram corresponding to internal spaces formed by the segments of the arch, the corresponding deck elements, and the hangers connecting them. Any solution generated by these parameters will be in equilibrium and automatically have a horizontal bridge deck. Although the *BRG* algorithm is the preferred choice, since it is (should be) tailored to the problem of optimising loadpaths using the domain of the force diagram, it does not have a stable and/or efficient implementation. The main problem is the generation of the form diagram, based on a given force diagram. For example, when edge forces flip from tension to compression, and vice versa, parallelisation is no longer effective. """ vertex_index = form.vertex_index() edge_index = form.edge_index() i_j = {vertex_index[vertex]: [vertex_index[nbr] for nbr in form.vertex_neighbors(vertex)] for vertex in form.vertices()} ij_e = {(vertex_index[u], vertex_index[v]): edge_index[u, v] for u, v in edge_index} ij_e.update({(vertex_index[v], vertex_index[u]): edge_index[u, v] for u, v in edge_index}) xy = array(form.xy(), dtype=float64) edges = [(vertex_index[u], vertex_index[v]) for u, v in form.edges()] C = connectivity_matrix(edges, 'csr') leaves = [vertex_index[key] for key in form.leaves()] fixed = [vertex_index[key] for key in form.fixed()] free = list(set(range(form.number_of_vertices())) - set(fixed) - set(leaves)) internal = [i for i, (u, v) in enumerate(form.edges()) if vertex_index[u] not in leaves and vertex_index[v] not in leaves] _vertex_index = force.vertex_index() _edge_index = force.edge_index(form) _edge_index.update({(v, u): _edge_index[u, v] for u, v in _edge_index}) _xy = array(force.xy(), dtype=float64) _edges = force.ordered_edges(form) _edges[:] = [(_vertex_index[u], _vertex_index[v]) for u, v in _edges] _C = connectivity_matrix(_edges, 'csr') _free = [key for key, attr in force.vertices(True) if attr['is_param']] _free = [_vertex_index[key] for key in _free] def objfunc(_x): _xy[_free, 0] = _x update_primal_from_dual(xy, _xy, free, leaves, i_j, ij_e, _C) length = normrow(C.dot(xy)) force = normrow(_C.dot(_xy)) lp = length[internal].T.dot(force[internal])[0, 0] print(lp) return(lp) x0 = _xy[_free, 0] result = minimize(objfunc, x0, method=algo, tol=1e-12, options={'maxiter': 1000}) # noqa: F841 uv = C.dot(xy) _uv = _C.dot(_xy) angles = [angle_vectors_xy(a, b) for a, b in zip(uv, _uv)] lengths = normrow(uv) forces = normrow(_uv) q = forces / lengths for vertex, attr in form.vertices(True): index = vertex_index[vertex] attr['x'] = xy[index, 0] attr['y'] = xy[index, 1] for edge, attr in form.edges(True): index = edge_index[edge] attr['l'] = lengths[index, 0] attr['a'] = angles[index] if (angles[index] - 3.14159) ** 2 < 0.25 * 3.14159: attr['f'] = - forces[index, 0] attr['q'] = - q[index, 0] else: attr['f'] = forces[index, 0] attr['q'] = q[index, 0] for vertex, attr in force.vertices(True): index = _vertex_index[vertex] attr['x'] = _xy[index, 0] attr['y'] = _xy[index, 1] for edge, attr in force.edges(True): index = _edge_index[edge] attr['a'] = angles[index] attr['l'] = forces[index, 0] return form, force
def vertical_from_zmax(form, zmax, kmax=100, xtol=1e-2, rtol=1e-3, density=1.0, display=True): """For the given form and force diagram, compute the scale of the force diagram for which the highest point of the thrust network is equal to a specified value. Parameters ---------- form : compas_tna.diagrams.formdiagram.FormDiagram The form diagram force : compas_tna.diagrams.forcediagram.ForceDiagram The corresponding force diagram. zmax : float The maximum height of the thrust network (the default is None, which implies that the maximum height will be equal to a quarter of the diagonal of the bounding box of the form diagram). kmax : int The maximum number of iterations for computing vertical equilibrium (the default is 100). tol : float The stopping criterion. density : float The density for computation of the self-weight of the thrust network (the default is 1.0). Set this to 0.0 to ignore self-weight and only consider specified point loads. display : bool If True, information about the current iteration will be displayed. """ xtol2 = xtol**2 # -------------------------------------------------------------------------- # FormDiagram # -------------------------------------------------------------------------- k_i = form.key_index() uv_i = form.uv_index() vcount = len(form.vertex) anchors = list(form.anchors()) fixed = list(form.fixed()) fixed = set(anchors + fixed) fixed = [k_i[key] for key in fixed] free = list(set(range(vcount)) - set(fixed)) edges = [(k_i[u], k_i[v]) for u, v in form.edges_where({'is_edge': True})] xyz = array(form.get_vertices_attributes('xyz'), dtype=float64) thick = array(form.get_vertices_attribute('t'), dtype=float64).reshape( (-1, 1)) p = array(form.get_vertices_attributes(('px', 'py', 'pz')), dtype=float64) q = [ attr.get('q', 1.0) for u, v, attr in form.edges_where({'is_edge': True}, True) ] q = array(q, dtype=float64).reshape((-1, 1)) C = connectivity_matrix(edges, 'csr') Ci = C[:, free] Cf = C[:, fixed] Cit = Ci.transpose() Ct = C.transpose() # -------------------------------------------------------------------------- # original data # -------------------------------------------------------------------------- p0 = array(p, copy=True) q0 = array(q, copy=True) # -------------------------------------------------------------------------- # load updater # -------------------------------------------------------------------------- update_loads = LoadUpdater(form, p0, thickness=thick, density=density) # -------------------------------------------------------------------------- # scale to zmax # note that zmax should not exceed scale * diagonal # -------------------------------------------------------------------------- scale = 1.0 for k in range(kmax): if display: print(k) update_loads(p, xyz) q = scale * q0 Q = diags([q.ravel()], [0]) A = Cit.dot(Q).dot(Ci) b = p[free, 2] - Cit.dot(Q).dot(Cf).dot(xyz[fixed, 2]) xyz[free, 2] = spsolve(A, b) z = max(xyz[free, 2]) res2 = (z - zmax)**2 if res2 < xtol2: break scale = scale * (z / zmax) # -------------------------------------------------------------------------- # vertical # -------------------------------------------------------------------------- q = scale * q0 Q = diags([q.ravel()], [0]) res = update_z(xyz, Q, C, p, free, fixed, update_loads, tol=rtol, kmax=kmax, display=display) # -------------------------------------------------------------------------- # update # -------------------------------------------------------------------------- l = normrow(C.dot(xyz)) f = q * l r = Ct.dot(Q).dot(C).dot(xyz) - p sw = p - p0 # -------------------------------------------------------------------------- # form # -------------------------------------------------------------------------- for key, attr in form.vertices(True): index = k_i[key] attr['z'] = xyz[index, 2] attr['rx'] = r[index, 0] attr['ry'] = r[index, 1] attr['rz'] = r[index, 2] attr['sw'] = sw[index, 2] for u, v, attr in form.edges_where({'is_edge': True}, True): index = uv_i[(u, v)] attr['f'] = f[index, 0] attr['l'] = l[index, 0] return scale
def mesh_geodesic_distances(mesh, sources, m=1.0): Lc = trimesh_cotangent_laplacian_matrix(mesh) key_index = mesh.key_index() vertices = mesh.get_vertices_attributes('xyz') faces = [[key_index[key] for key in mesh.face_vertices(fkey)] for fkey in mesh.faces()] V = array(vertices) F = array(faces, dtype=int) # Laplacian matrix with symmetric cotangent weights # W = empty(0) # I = empty(0, dtype=int) # J = empty(0, dtype=int) # for i1, i2, i3 in [(0, 1, 2), (1, 2, 0), (2, 0, 1)]: # v1 = F[:, i1] # v2 = F[:, i2] # v3 = F[:, i3] # e1 = V[v2] - V[v1] # e2 = V[v3] - V[v1] # cotan = 0.5 * sum(e1 * e2, axis=1) / normrow(cross(e1, e2)).ravel() # W = append(W, cotan) # I = append(I, v2) # J = append(J, v3) # W = append(W, cotan) # I = append(I, v3) # J = append(J, v2) # Lc = csr_matrix((W, (I, J)), shape=(V.shape[0], V.shape[0])) # Lc = Lc - spdiags(Lc * ones(V.shape[0]), 0, V.shape[0], V.shape[0]) # Step I # Heat *u* is allowed to diffuse for a brief period of time (t) e01 = V[F[:, 1]] - V[F[:, 0]] e12 = V[F[:, 2]] - V[F[:, 1]] e20 = V[F[:, 0]] - V[F[:, 2]] normal = cross(e01, e12) A2 = normrow(normal) A3 = A2.ravel() / 6 VA = zeros(V.shape[0]) for i in (0, 1, 2): b = bincount(F[:, i], A3) VA[:len(b)] += b VA = spdiags(VA, 0, V.shape[0], V.shape[0]) h = mean([normrow(e01), normrow(e12), normrow(e20)]) t = m * h ** 2 u0 = zeros(V.shape[0]) u0[sources] = 1.0 A = VA - t * Lc print(A.sum(axis=1)) u = splu((VA - t * Lc).tocsc()).solve(u0) # u = spsolve(VA - t * Lc, u0) # A = VA - t * Lc # b = u0 # u = spsolve(A.transpose().dot(A), A.transpose().dot(b)) unit = normal / A2 unit_e01 = cross(unit, e01) unit_e12 = cross(unit, e12) unit_e20 = cross(unit, e20) grad_u = ( unit_e01 * u[F[:, 2], None] + unit_e12 * u[F[:, 0], None] + unit_e20 * u[F[:, 1], None] ) / A2 X = - grad_u / normrow(grad_u) div_X = zeros(V.shape[0]) for i1, i2, i3 in [(0, 1, 2), (1, 2, 0), (2, 0, 1)]: v1 = F[:, i1] v2 = F[:, i2] v3 = F[:, i3] e1 = V[v2] - V[v1] e2 = V[v3] - V[v1] e0 = V[v3] - V[v2] a = 1 / tan(arccos(sum(normalizerow(-e2) * normalizerow(-e0), axis=1))) b = 1 / tan(arccos(sum(normalizerow(-e1) * normalizerow(+e0), axis=1))) div_X += bincount( v1, 0.5 * (a * sum(e1 * X, axis=1) + b * sum(e2 * X, axis=1)), minlength=V.shape[0] ) print(Lc.sum(axis=1)) phi = splu(Lc.tocsc()).solve(div_X) # phi = spsolve(Lc, div_X) # A = Lc # b = div_X # phi = spsolve(A.transpose().dot(A), A.transpose().dot(b)) phi -= phi.min() return phi
def vertical_from_q(form, scale=1.0, density=1.0, kmax=100, tol=1e-3, display=True): """Compute vertical equilibrium from the force densities of the independent edges. Parameters ---------- form : FormDiagram The form diagram scale : float The scale of the horizontal forces. Default is ``1.0``. density : float, optional The density for computation of the self-weight of the thrust network. Set this to 0.0 to ignore self-weight and only consider specified point loads. Default is ``1.0``. kmax : int, optional The maximum number of iterations for computing vertical equilibrium. Default is ``100``. tol : float The stopping criterion. Default is ``0.001``. display : bool Display information about the current iteration. Default is ``True``. """ k_i = form.key_index() uv_i = form.uv_index() vcount = form.number_of_vertices() anchors = list(form.anchors()) fixed = list(form.fixed()) fixed = set(anchors + fixed) fixed = [k_i[key] for key in fixed] edges = [(k_i[u], k_i[v]) for u, v in form.edges_where({'is_edge': True})] free = list(set(range(vcount)) - set(fixed)) xyz = array(form.get_vertices_attributes('xyz'), dtype=float64) thick = array(form.get_vertices_attribute('t'), dtype=float64).reshape( (-1, 1)) p = array(form.get_vertices_attributes(('px', 'py', 'pz')), dtype=float64) q = [ attr.get('q', 1.0) for u, v, attr in form.edges_where({'is_edge': True}, True) ] q = array(q, dtype=float64).reshape((-1, 1)) C = connectivity_matrix(edges, 'csr') # -------------------------------------------------------------------------- # original data # -------------------------------------------------------------------------- p0 = array(p, copy=True) q0 = array(q, copy=True) # -------------------------------------------------------------------------- # load updater # -------------------------------------------------------------------------- update_loads = LoadUpdater(form, p0, thickness=thick, density=density) # -------------------------------------------------------------------------- # update forcedensity based on given q[ind] # -------------------------------------------------------------------------- q = scale * q0 Q = diags([q.ravel()], [0]) # -------------------------------------------------------------------------- # compute vertical # -------------------------------------------------------------------------- update_z(xyz, Q, C, p, free, fixed, update_loads, tol=tol, kmax=kmax, display=display) # -------------------------------------------------------------------------- # update # -------------------------------------------------------------------------- l = normrow(C.dot(xyz)) f = q * l r = C.transpose().dot(Q).dot(C).dot(xyz) - p sw = p - p0 # -------------------------------------------------------------------------- # form # -------------------------------------------------------------------------- for key, attr in form.vertices(True): index = k_i[key] attr['z'] = xyz[index, 2] attr['rx'] = r[index, 0] attr['ry'] = r[index, 1] attr['rz'] = r[index, 2] attr['sw'] = sw[index, 2] for u, v, attr in form.edges_where({'is_edge': True}, True): index = uv_i[(u, v)] attr['f'] = f[index, 0] attr['l'] = l[index, 0]
def horizontal_nodal(form, force, alpha=100, kmax=100, display=False): """Compute horizontal equilibrium using a node-per-node approach. Parameters ---------- form : compas_tna.diagrams.FormDiagram force : compas_tna.diagrams.ForceDiagram alpha : float Weighting factor for computation of the target vectors (the default is 100.0, which implies that the target vectors are the edges of the form diagram). If 0.0, the target vectors are the edges of the force diagram. kmax : int Maximum number of iterations (the default is 100). display : bool Display information about the current iteration (the default is False). """ alpha = float(alpha) / 100.0 alpha = max(0., min(1., alpha)) # -------------------------------------------------------------------------- # form diagram # -------------------------------------------------------------------------- k_i = form.key_index() uv_i = form.uv_index() i_nbrs = { k_i[key]: [k_i[nbr] for nbr in form.vertex_neighbors(key)] for key in form.vertices() } ij_e = {(k_i[u], k_i[v]): index for (u, v), index in iter(uv_i.items())} fixed = set(list(form.anchors()) + list(form.fixed())) fixed = [k_i[key] for key in fixed] edges = [[k_i[u], k_i[v]] for u, v in form.edges_where({'_is_edge': True})] lmin = array([ attr.get('lmin', 1e-7) for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float64).reshape((-1, 1)) lmax = array([ attr.get('lmax', 1e+7) for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float64).reshape((-1, 1)) fmin = array([ attr.get('fmin', 1e-7) for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float64).reshape((-1, 1)) fmax = array([ attr.get('fmax', 1e+7) for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float64).reshape((-1, 1)) flipmask = array([ 1.0 if not attr['_is_tension'] else -1.0 for key, attr in form.edges_where({'_is_edge': True}, True) ], dtype=float).reshape((-1, 1)) xy = array(form.vertices_attributes('xy'), dtype=float64) C = connectivity_matrix(edges, 'csr') # -------------------------------------------------------------------------- # force diagram # -------------------------------------------------------------------------- _k_i = force.key_index() _uv_i = force.uv_index(form=form) _i_nbrs = { _k_i[key]: [_k_i[nbr] for nbr in force.vertex_neighbors(key)] for key in force.vertices() } _ij_e = {(_k_i[u], _k_i[v]): index for (u, v), index in iter(_uv_i.items())} _fixed = list(force.fixed()) _fixed = [_k_i[key] for key in _fixed] _fixed = _fixed or [0] _edges = force.ordered_edges(form) _xy = array(force.vertices_attributes('xy'), dtype=float64) _C = connectivity_matrix(_edges, 'csr') # -------------------------------------------------------------------------- # rotate force diagram to make it parallel to the form diagram # use CCW direction (opposite of cycle direction) # -------------------------------------------------------------------------- _xy[:] = rot90(_xy, +1.0) # -------------------------------------------------------------------------- # make the diagrams parallel to a target vector # that is the (alpha) weighted average of the directions of corresponding # edges of the two diagrams # -------------------------------------------------------------------------- uv = flipmask * C.dot(xy) _uv = _C.dot(_xy) l = normrow(uv) _l = normrow(_uv) # -------------------------------------------------------------------------- # the target vectors # -------------------------------------------------------------------------- targets = alpha * normalizerow(uv) + (1 - alpha) * normalizerow(_uv) # -------------------------------------------------------------------------- # parallelise # -------------------------------------------------------------------------- if alpha < 1: parallelise_nodal(xy, C, targets, i_nbrs, ij_e, fixed=fixed, kmax=kmax, lmin=lmin, lmax=lmax) if alpha > 0: parallelise_nodal(_xy, _C, targets, _i_nbrs, _ij_e, kmax=kmax, lmin=fmin, lmax=fmax) # -------------------------------------------------------------------------- # update the coordinate difference vectors # -------------------------------------------------------------------------- uv = C.dot(xy) _uv = _C.dot(_xy) l = normrow(uv) _l = normrow(_uv) # -------------------------------------------------------------------------- # compute the force densities # -------------------------------------------------------------------------- f = flipmask * _l q = (f / l).astype(float64) # -------------------------------------------------------------------------- # rotate the force diagram 90 degrees in CW direction # this way the relation between the two diagrams is easier to read # -------------------------------------------------------------------------- _xy[:] = rot90(_xy, -1.0) # -------------------------------------------------------------------------- # angle deviations # note that this does not account for flipped edges! # -------------------------------------------------------------------------- a = [angle_vectors_xy(uv[i], _uv[i], deg=True) for i in range(len(edges))] # -------------------------------------------------------------------------- # update form # -------------------------------------------------------------------------- for key, attr in form.vertices(True): i = k_i[key] attr['x'] = xy[i, 0] attr['y'] = xy[i, 1] for (u, v), attr in form.edges_where({'_is_edge': True}, True): i = uv_i[(u, v)] attr['q'] = q[i, 0] attr['_f'] = f[i, 0] attr['_l'] = l[i, 0] attr['_a'] = a[i] # -------------------------------------------------------------------------- # update force # -------------------------------------------------------------------------- for key, attr in force.vertices(True): i = _k_i[key] attr['x'] = _xy[i, 0] attr['y'] = _xy[i, 1] for (u, v), attr in force.edges(True): if (u, v) in _uv_i: i = _uv_i[(u, v)] attr['_l'] = _l[i, 0] elif (v, u) in _uv_i: i = _uv_i[(v, u)] attr['_l'] = _l[i, 0]