def update_angle_deviations(self): """Compute the angle deviation with the corresponding edge in the FormDiagram. """ for edge in self.edges(): edge_ = self.primal_edge(edge) uv = self.edge_vector(*edge) uv_ = self.primal.edge_vector(*edge_) a = angle_vectors_xy(uv, cross_vectors(uv_, (0, 0, 1)), deg=True) self.edge_attribute(edge, '_a', a) self.primal.edge_attribute(edge_, '_a', a)
def check_deviations(form, force, tol=10e-3): """Checks whether the form and force diagrams are indeed reciprocal, i.e. have their corresponding edges parallel. Parameters ---------- form: compas_ags.diagrams.FormDiagram The form diagram to check deviations. force: compas_ags.diagrams.ForceDiagram The force diagram to check deviations. tol: float (10e-3) The tolerance allowd for the deviations. Returns ------- checked : bool Return whether of not the diagram passes the check with no deviations greater than the tolerance. """ edges_form = list(form.edges()) edges_force = force.ordered_edges(form) checked = True for i in range(len(edges_form)): pt0, pt1 = form.edge_coordinates(edges_form[i][0], edges_form[i][1]) _pt0, _pt1 = force.edge_coordinates(edges_force[i][0], edges_force[i][1]) a = angle_vectors_xy(subtract_vectors(pt1, pt0), subtract_vectors(_pt1, _pt0), deg=True) if a < tol or a > 180 - tol: a = 0.0 else: checked = False a = min(a, 180 - a) form.edge_attribute(edges_form[i], 'a', a) return checked
def network_embed_in_plane(network, fix=None, straightline=True): """Embed the network in the plane. Parameters ---------- network : Network A network object. fix : list (None) Two fixed points. straightline : bool (True) Embed using straight lines. Returns ------- bool True if the embedding was successful. False otherwise. Raises ------ ImportError If NetworkX is not installed. Warning ------- This function uses the Python package NetworkX. NetworkX can be *pip installed*. Examples -------- .. plot:: :include-source: import compas from compas.datastructures import Network from compas.topology import network_embed_in_plane from compas.plotters import NetworkPlotter network = Network.from_obj(compas.get('fink.obj')) embedding = network.copy() fix = (1, 12) if network_embed_in_plane(embedding, fix=fix): plotter = NetworkPlotter(embedding) plotter.draw_lines([{'start': network.vertex_coordinates(u, 'xy'), 'end': network.vertex_coordinates(v, 'xy'), 'color': '#cccccc'} for u, v in network.edges()]) plotter.draw_vertices(radius=0.3, text={key: key for key in embedding.vertices()}, facecolor={key: '#ff0000' for key in fix}) plotter.draw_edges() plotter.show() """ try: import networkx as nx except ImportError: print( "NetworkX is not installed. Get NetworkX at https://networkx.github.io/." ) raise x = network.get_vertices_attribute('x') y = network.get_vertices_attribute('y') xmin, xmax = min(x), max(x) ymin, ymax = min(y), max(y) xspan = xmax - xmin yspan = ymax - ymin edges = [(u, v) for u, v in network.edges() if not network.is_vertex_leaf(u) and not network.is_vertex_leaf(v) ] is_embedded = False count = 100 while count: graph = nx.Graph(edges) pos = nx.spring_layout(graph, 2, iterations=100, scale=max(xspan, yspan)) if not _network_are_edges_crossed(edges, pos): is_embedded = True break count -= 1 if not is_embedded: return False if fix: a, b = fix p0 = network.vertex_coordinates(a, 'xy') p1 = network.vertex_coordinates(b, 'xy') p2 = pos[b] vec0 = [ network.vertex[b][axis] - network.vertex[a][axis] for axis in 'xy' ] vec1 = [pos[b][axis] - pos[a][axis] for axis in (0, 1)] # rotate a = angle_vectors_xy(vec0, vec1) if is_ccw_xy(p0, p1, p2): a = 2 * pi - a cosa = cos(a) sina = sin(a) for key in pos: x, y = pos[key] pos[key][0] = cosa * x - sina * y pos[key][1] = sina * x + cosa * y # scale l0 = (vec0[0]**2 + vec0[1]**2)**0.5 l1 = (vec1[0]**2 + vec1[1]**2)**0.5 scale = l0 / l1 for key in pos: pos[key][0] *= scale pos[key][1] *= scale # translate t = network.vertex[fix[0]]['x'] - pos[fix[0]][0], network.vertex[ fix[0]]['y'] - pos[fix[0]][1] for key in pos: pos[key][0] += t[0] pos[key][1] += t[1] # update network vertex coordinates for key in network.vertices(): if key in pos: network.vertex[key]['x'] = pos[key][0] network.vertex[key]['y'] = pos[key][1] return True
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 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]
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 form_update_from_force(form, force, kmax=100): r"""Update the form diagram after a modification of the force diagram. Parameters ---------- form : FormDiagram The form diagram to update. force : ForceDiagram The force diagram on which the update is based. Returns ------- None The form and force diagram are updated in-place. Notes ----- Compute the geometry of the form diagram from the geometry of the form diagram and some constraints (location of fixed points). Since both diagrams are reciprocal, the coordinates of each vertex of the form diagram can be expressed as the intersection of three or more lines parallel to the corresponding edges of the force diagram. Essentially, this boils down to solving the following problem: .. math:: \mathbf{A}\mathbf{x} = \mathbf{b} with :math:`\mathbf{A}` the coefficients of the x and y-coordinate of the , :math:`\mathbf{x}` the coordinates of the vertices of the form diagram, in *Fortran* order (first all x-coordinates, then all y-coordinates), and :math:`\mathbf{b}` .... Examples -------- >>> """ # -------------------------------------------------------------------------- # form diagram # -------------------------------------------------------------------------- vertex_index = form.vertex_index() edge_index = form.edge_index() i_j = { index: [vertex_index[nbr] for nbr in form.vertex_neighbors(vertex)] for index, vertex in enumerate(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') # -------------------------------------------------------------------------- # constraints # -------------------------------------------------------------------------- leaves = [vertex_index[vertex] for vertex in form.leaves()] fixed = [vertex_index[vertex] for vertex in form.fixed()] free = list( set(range(form.number_of_vertices())) - set(fixed) - set(leaves)) # -------------------------------------------------------------------------- # force diagram # -------------------------------------------------------------------------- _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') # -------------------------------------------------------------------------- # compute the coordinates of thet *free* vertices # as a function of the fixed vertices and the previous coordinates of the *free* vertices # re-add the leaves and leaf-edges # -------------------------------------------------------------------------- update_form_from_force(xy, _xy, free, leaves, i_j, ij_e, _C, kmax=kmax) # -------------------------------------------------------------------------- # update # -------------------------------------------------------------------------- uv = C.dot(xy) _uv = _C.dot(_xy) angles = [angle_vectors_xy(a, b, deg=True) for a, b in zip(uv, _uv)] lengths = normrow(uv) forces = normrow(_uv) q = forces / lengths # -------------------------------------------------------------------------- # update form diagram # -------------------------------------------------------------------------- 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] < 90: attr['f'] = forces[index, 0] attr['q'] = q[index, 0] else: attr['f'] = -forces[index, 0] attr['q'] = -q[index, 0] # -------------------------------------------------------------------------- # update force diagram # -------------------------------------------------------------------------- for edge, attr in force.edges(True): index = _edge_index[edge] attr['a'] = angles[index] attr['l'] = forces[index, 0]
def network_embed_in_plane(network, fixed=None, straightline=True): """Embed the network in the plane. Parameters ---------- network : Network A network object. fixed : list (None) Two fixed points. straightline : bool (True) Embed using straight lines. Returns ------- bool True if the embedding was successful. False otherwise. Raises ------ ImportError If NetworkX is not installed. Examples -------- >>> """ try: import networkx as nx except ImportError: print( "NetworkX is not installed. Get NetworkX at https://networkx.github.io/." ) raise x = network.nodes_attribute('x') y = network.nodes_attribute('y') xmin, xmax = min(x), max(x) ymin, ymax = min(y), max(y) xspan = xmax - xmin yspan = ymax - ymin edges = [(u, v) for u, v in network.edges() if not network.is_leaf(u) and not network.is_leaf(v)] is_embedded = False count = 100 while count: graph = nx.Graph(edges) pos = nx.spring_layout(graph, iterations=100, scale=max(xspan, yspan)) if not _are_edges_crossed(edges, pos): is_embedded = True break count -= 1 if not is_embedded: return False if fixed: a, b = fixed p0 = network.node_attributes(a, 'xy') p1 = network.node_attributes(b, 'xy') p2 = pos[b] vec0 = subtract_vectors_xy(p1, p0) vec1 = subtract_vectors_xy(pos[b], pos[a]) # rotate angle = angle_vectors_xy(vec0, vec1) if is_ccw_xy(p0, p1, p2): angle = 2 * pi - angle cosa = cos(angle) sina = sin(angle) for key in pos: x, y = pos[key] pos[key][0] = cosa * x - sina * y pos[key][1] = sina * x + cosa * y # scale l0 = (vec0[0]**2 + vec0[1]**2)**0.5 l1 = (vec1[0]**2 + vec1[1]**2)**0.5 scale = l0 / l1 for key in pos: pos[key][0] *= scale pos[key][1] *= scale # translate t = subtract_vectors_xy(p0, pos[a]) for key in pos: pos[key][0] += t[0] pos[key][1] += t[1] # update network node coordinates for key in network.nodes(): if key in pos: network.node_attributes(key, 'xy', pos[key]) return True
def form_update_from_force_direct(form, force): r"""Update the form diagram after a modification of the force diagram. Compute the geometry of the form diagram from the geometry of the force diagram and some constraints (location of fixed points). The form diagram is computed by formulating the reciprocal relationships to the approach in described in AGS. In order to include the constraints, the reciprocal force densities and form diagram coordinates are solved for at the same time, by formulating the equation system: .. math:: \mathbf{M}\mathbf{X} = \mathbf{r} with :math:`\mathbf{M}` containing the coefficients of the system of equations including constraints, :math:`\mathbf{X}` the coordinates of the vertices of the form diagram and the reciprocal force densities, in *Fortran* order (first all :math:`\mathbf{x}`-coordinates, then all :math:`\mathbf{y}`-coordinates, then all reciprocal force densities, :math:`\mathbf{q}^{-1}`), and :math:`\mathbf{r}` contains the residual (all zeroes except for the constraint rows). The addition of constraints reduces the number of independent edges, which must be identified during the solving procedure. Additionally, the algorithm fails if any force density is zero (corresponding to a zero-length edge in the force diagram) or if it is over-constrained. Parameters ---------- form : compas_ags.diagrams.formdiagram.FormDiagram The form diagram to update. force : compas_bi_ags.diagrams.forcediagram.ForceDiagram The force diagram on which the update is based. """ # -------------------------------------------------------------------------- # form diagram # -------------------------------------------------------------------------- k_i = form.key_index() # i_j = {i: [k_i[n] for n in form.vertex_neighbours(k)] for i, k in enumerate(form.vertices())} uv_e = form.uv_index() ij_e = {(k_i[u], k_i[v]): uv_e[(u, v)] for u, v in uv_e} edges = [(k_i[u], k_i[v]) for u, v in form.edges()] C = connectivity_matrix(edges, 'array') # add opposite edges for convenience... ij_e.update({(k_i[v], k_i[u]): uv_e[(u, v)] for u, v in uv_e}) 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)) # -------------------------------------------------------------------------- # force diagram # -------------------------------------------------------------------------- _i_k = {index: key for index, key in enumerate(force.vertices())} _xy = array(force.xy(), dtype=float64) _edges = force.ordered_edges(form) _uv_e = {(_i_k[i], _i_k[j]): e for e, (i, j) in enumerate(_edges)} _vcount = force.number_of_vertices() _C = connectivity_matrix(_edges, 'array') _e_v = force.external_vertices(form) _free = list(set(range(_vcount)) - set(_e_v)) # -------------------------------------------------------------------------- # compute the coordinates of the form based on the force diagram # with linear constraints # -------------------------------------------------------------------------- # Compute dual equilibrium matrix and Laplacian matrix import numpy as np _E = equilibrium_matrix(_C, _xy, _free, 'array') L = laplacian_matrix(edges, normalize=False, rtype='array') # Get dual coordinate difference vectors _uv = _C.dot(_xy) _U = np.diag(_uv[:, 0]) _V = np.diag(_uv[:, 1]) # Get reciprocal force densities from compas_bi_ags.utilities.errorhandler import SolutionError if any(abs(q) < 1e-14): raise SolutionError( 'Found zero force density, direct solution not possible.') q = np.divide(1, q) # Formulate the equation system z = np.zeros(L.shape) z2 = np.zeros((_E.shape[0], L.shape[1])) M = np.bmat([[L, z, -C.T.dot(_U)], [z, L, -C.T.dot(_V)], [z2, z2, _E]]) rhs = np.zeros((M.shape[0], 1)) X = np.vstack((matrix(xy)[:, 0], matrix(xy)[:, 1], matrix(q)[:, 0])) # Add constraints constraint_rows, res = force.compute_constraints(form, M) M = np.vstack((M, constraint_rows)) rhs = np.vstack((rhs, res)) # Get independent variables from compas_bi_ags.utilities.helpers import get_independent_stress, check_solutions nr_free_vars, free_vars, dependent_vars = get_independent_stress(M) #k, m = dof(M) #ind = nonpivots(rref(M)) # Partition system Mid = M[:, free_vars] Md = M[:, dependent_vars] Xid = X[free_vars] # Check that solution exists check_solutions(M, rhs) # Solve Xd = np.asarray(np.linalg.lstsq(Md, rhs - Mid * Xid)[0]) X[dependent_vars] = Xd # Store solution nx = xy.shape[0] ny = xy.shape[0] xy[:, 0] = X[:nx].T xy[:, 1] = X[nx:(nx + ny)].T # -------------------------------------------------------------------------- # update # -------------------------------------------------------------------------- uv = C.dot(xy) _uv = _C.dot(_xy) a = [angle_vectors_xy(uv[i], _uv[i]) for i in range(len(edges))] l = normrow(uv) _l = normrow(_uv) q = _l / l # -------------------------------------------------------------------------- # update form diagram # -------------------------------------------------------------------------- for key, attr in form.vertices(True): index = k_i[key] attr['x'] = xy[index, 0] attr['y'] = xy[index, 1] for u, v, attr in form.edges(True): e = uv_e[(u, v)] attr['l'] = l[e, 0] attr['a'] = a[e] if a[e] < 90: attr['f'] = _l[e, 0] attr['q'] = q[e, 0] else: attr['f'] = -_l[e, 0] attr['q'] = -q[e, 0] # -------------------------------------------------------------------------- # update force diagram # -------------------------------------------------------------------------- for u, v, attr in force.edges(True): e = _uv_e[(u, v)] attr['a'] = a[e] attr['l'] = _l[e, 0]
def horizontal_nodal(form, force, alpha=100, kmax=100, callback=None): r"""Compute horizontal equilibrium using a node-per-node approach. Parameters ---------- form : compas_tna.diagrams.FormDiagram The form diagram. force : compas_tna.diagrams.ForceDiagram The force diagram. alpha : float, optional 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, optional Maximum number of iterations. Default is ``100``. callback : callable, optional A callback function to be called at every iteration of the parallelisation algorithm. The callback should take the current iterand, the coordinates of the form diagram, and the coordinates of the force diagram as input parameters. Default is ``None``. Returns ------- None Notes ----- This function will update the form and force diagram instead of returning a result. The relationship between force densities (``q``), horizontal forces (``h``), and lengths (``l``) is the following: .. math:: Q_{i} &= \frac{F_{i, thrust}}{L_{i, thrust}} \\ &= \frac{H_{i, form}}{L_{i, form}} \\ &= scale * \frac{L_{i, force}}{L_{i, form}} """ alpha = float(alpha) / 100.0 alpha = max(0, min(1, alpha)) # -------------------------------------------------------------------------- # form diagram # -------------------------------------------------------------------------- k_i = form.key_index() i_nbrs = {k_i[key]: [k_i[nbr] for nbr in form.vertex_neighbors(key)] for key in form.vertices()} fixed = set(list(form.anchors()) + list(form.fixed())) fixed = [k_i[key] for key in fixed] xy = form.vertices_attributes('xy') edges = list(form.edges_where({'_is_edge': True})) lmin = form.edges_attribute('lmin', keys=edges) lmax = form.edges_attribute('lmax', keys=edges) hmin = form.edges_attribute('hmin', keys=edges) hmax = form.edges_attribute('hmax', keys=edges) flipmask = [-1.0 if form.edge_attribute(edge, '_is_tension') else 1.0 for edge in edges] uv_i = form.uv_index() ij_e = {(k_i[u], k_i[v]): index for (u, v), index in iter(uv_i.items())} edges = [[k_i[u], k_i[v]] for u, v in edges] # -------------------------------------------------------------------------- # force diagram # -------------------------------------------------------------------------- _k_i = force.key_index() _i_nbrs = {_k_i[key]: [_k_i[nbr] for nbr in force.vertex_neighbors(key)] for key in force.vertices()} _fixed = list(force.fixed()) _fixed = [_k_i[key] for key in _fixed] _xy = force.vertices_attributes('xy') _edges = force.ordered_edges(form) _uv_i = {uv: index for index, uv in enumerate(_edges)} _ij_e = {(_k_i[u], _k_i[v]): index for (u, v), index in iter(_uv_i.items())} _lmin = force.edges_attribute('lmin', keys=_edges) _lmax = force.edges_attribute('lmax', keys=_edges) _edges = [[_k_i[u], _k_i[v]] for u, v in _edges] scale = force.attributes.get('scale', 1.0) # -------------------------------------------------------------------------- # rotate force diagram to make it parallel to the form diagram # use CCW direction (opposite of cycle direction) # -------------------------------------------------------------------------- _x, _y = zip(*_xy) _xy[:] = [list(item) for item in zip([-_ for _ in _y], _x)] # -------------------------------------------------------------------------- # 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 = [[factor * (xy[j][0] - xy[i][0]), factor * (xy[j][1] - xy[i][1])] for (i, j), factor in zip(edges, flipmask)] _uv = [[_xy[j][0] - _xy[i][0], _xy[j][1] - _xy[i][1]] for i, j in _edges] lengths = [(dx**2 + dy**2)**0.5 for dx, dy in uv] forces = [(dx**2 + dy**2)**0.5 for dx, dy in _uv] # -------------------------------------------------------------------------- # the target vectors # -------------------------------------------------------------------------- form_targets = [[alpha * v[0] / l, alpha * v[1] / l] if l else [0, 0] for v, l in zip(uv, lengths)] force_targets = [[(1 - alpha) * v[0] / l, (1 - alpha) * v[1] / l] if l else [0, 0] for v, l in zip(_uv, forces)] targets = [[a[0] + b[0], a[1] + b[1]] for a, b in zip(form_targets, force_targets)] # -------------------------------------------------------------------------- # proper force bounds # -------------------------------------------------------------------------- hmin[:] = [_ / scale for _ in hmin] hmax[:] = [_ / scale for _ in hmax] _lmin[:] = [max(a, b) for a, b in zip(hmin, _lmin)] _lmax[:] = [min(a, b) for a, b in zip(hmax, _lmax)] # -------------------------------------------------------------------------- # parallelise # -------------------------------------------------------------------------- if alpha < 1: parallelise_edges(xy, edges, targets, i_nbrs, ij_e, fixed=fixed, kmax=kmax, lmin=lmin, lmax=lmax) if alpha > 0: parallelise_edges(_xy, _edges, targets, _i_nbrs, _ij_e, fixed=_fixed, kmax=kmax, lmin=_lmin, lmax=_lmax, callback=callback) # -------------------------------------------------------------------------- # update the coordinate difference vectors # -------------------------------------------------------------------------- uv = [[xy[j][0] - xy[i][0], xy[j][1] - xy[i][1]] for i, j in edges] _uv = [[_xy[j][0] - _xy[i][0], _xy[j][1] - _xy[i][1]] for i, j in _edges] lengths = [(dx**2 + dy**2)**0.5 for dx, dy in uv] forces = [(dx**2 + dy**2)**0.5 for dx, dy in _uv] # -------------------------------------------------------------------------- # compute the force densities # -------------------------------------------------------------------------- forces[:] = [f * factor for f, factor in zip(forces, flipmask)] q = [f / l for f, l in zip(forces, lengths)] # -------------------------------------------------------------------------- # rotate the force diagram 90 degrees in CW direction # this way the relation between the two diagrams is easier to read # -------------------------------------------------------------------------- _x, _y = zip(*_xy) _xy[:] = [list(item) for item in zip(_y, [-_ for _ in _x])] # -------------------------------------------------------------------------- # angle deviations # note that this does not account for flipped edges! # -------------------------------------------------------------------------- angles = [angle_vectors_xy(uv[i], _uv[i], deg=True) for i in range(len(edges))] # -------------------------------------------------------------------------- # update form # -------------------------------------------------------------------------- for key in form.vertices(): i = k_i[key] form.vertex_attributes(key, 'xy', xy[i]) for uv in form.edges_where({'_is_edge': True}): i = uv_i[uv] form.edge_attributes(uv, ['q', '_f', '_l', '_a'], [q[i], forces[i], lengths[i], angles[i]]) # -------------------------------------------------------------------------- # update force # -------------------------------------------------------------------------- for key in force.vertices(): i = _k_i[key] force.vertex_attributes(key, 'xy', _xy[i]) for (u, v) in force.edges(): if (u, v) in _uv_i: i = _uv_i[(u, v)] else: i = _uv_i[(v, u)] force.edge_attributes((u, v), ['_l', '_a'], [forces[i], angles[i]])
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 ------- None 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. """ k_i = form.key_index() i_j = dict((i, [k_i[n] for n in form.vertex_neighbors(k)]) for i, k in enumerate(form.vertices())) uv_e = form.uv_index() ij_e = dict(((k_i[u], k_i[v]), uv_e[(u, v)]) for u, v in uv_e) xy = array(form.xy(), dtype=float64) edges = [(k_i[u], k_i[v]) for u, v in form.edges()] C = connectivity_matrix(edges, 'csr') # add opposite edges for convenience... ij_e.update(dict(((k_i[v], k_i[u]), uv_e[(u, v)]) for u, v in uv_e)) leaves = [k_i[key] for key in form.leaves()] fixed = [k_i[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 k_i[u] not in leaves and k_i[v] not in leaves ] _k_i = dict((key, index) for index, key in enumerate(force.vertices())) _i_k = dict((index, key) for index, key in enumerate(force.vertices())) _xy = array(force.xy(), dtype=float64) _edges = force.ordered_edges(form) _C = connectivity_matrix(_edges, 'csr') _uv_e = dict(((_i_k[i], _i_k[j]), e) for e, (i, j) in enumerate(_edges)) _uv_e.update({(v, u): _uv_e[u, v] for u, v in _uv_e}) _free = [key for key, attr in force.vertices(True) if attr['is_param']] _free = [_k_i[key] for key in _free] 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) x0 = _xy[_free, 0] # bounds = [(None, 0) for i in range(len(_free))] res = minimize( objfunc, x0, method=algo, tol=1e-12, # bounds=bounds, options={'maxiter': 1000}) uv = C.dot(xy) _uv = _C.dot(_xy) a = [angle_vectors_xy(uv[i], _uv[i]) for i in range(len(edges))] l = normrow(uv) _l = normrow(_uv) q = _l / l for key, attr in form.vertices(True): index = k_i[key] attr['x'] = xy[index, 0] attr['y'] = xy[index, 1] for (u, v), attr in form.edges(True): e = uv_e[(u, v)] attr['l'] = l[e, 0] attr['a'] = a[e] if (a[e] - 3.14159)**2 < 0.25 * 3.14159: attr['f'] = -_l[e, 0] attr['q'] = -q[e, 0] else: attr['f'] = _l[e, 0] attr['q'] = q[e, 0] for key, attr in force.vertices(True): index = _k_i[key] attr['x'] = _xy[index, 0] attr['y'] = _xy[index, 1] for (u, v), attr in force.edges(True): e = _uv_e[(u, v)] attr['a'] = a[e] attr['l'] = _l[e, 0]
def form_update_from_force(form, force, kmax=100): r"""Update the form diagram after a modification of the force diagram. Parameters ---------- form : FormDiagram The form diagram to update. force : ForceDiagram The force diagram on which the update is based. Returns ------- None The form and force diagram are updated in-place. Notes ----- Compute the geometry of the form diagram from the geometry of the form diagram and some constraints (location of fixed points). Since both diagrams are reciprocal, the coordinates of each vertex of the form diagram can be expressed as the intersection of three or more lines parallel to the corresponding edges of the force diagram. Essentially, this boils down to solving the following problem: .. math:: \mathbf{A}\mathbf{x} = \mathbf{b} with :math:`\mathbf{A}` the coefficients of the x and y-coordinate of the , :math:`\mathbf{x}` the coordinates of the vertices of the form diagram, in *Fortran* order (first all x-coordinates, then all y-coordinates), and :math:`\mathbf{b}` .... Examples -------- .. code-block:: python """ # -------------------------------------------------------------------------- # form diagram # -------------------------------------------------------------------------- k_i = form.key_index() i_j = { i: [k_i[n] for n in form.vertex_neighbors(k)] for i, k in enumerate(form.vertices()) } uv_e = form.uv_index() ij_e = {(k_i[u], k_i[v]): uv_e[(u, v)] for u, v in uv_e} xy = array(form.xy(), dtype=float64) edges = [(k_i[u], k_i[v]) for u, v in form.edges()] C = connectivity_matrix(edges, 'csr') # add opposite edges for convenience... ij_e.update({(k_i[v], k_i[u]): uv_e[(u, v)] for u, v in uv_e}) # -------------------------------------------------------------------------- # constraints # -------------------------------------------------------------------------- leaves = [k_i[key] for key in form.leaves()] fixed = [k_i[key] for key in form.fixed()] free = list( set(range(form.number_of_vertices())) - set(fixed) - set(leaves)) # -------------------------------------------------------------------------- # force diagram # -------------------------------------------------------------------------- _i_k = {index: key for index, key in enumerate(force.vertices())} _xy = array(force.xy(), dtype=float64) _edges = force.ordered_edges(form) _uv_e = {(_i_k[i], _i_k[j]): e for e, (i, j) in enumerate(_edges)} _C = connectivity_matrix(_edges, 'csr') # -------------------------------------------------------------------------- # compute the coordinates of thet *free* vertices # as a function of the fixed vertices and the previous coordinates of the *free* vertices # re-add the leaves and leaf-edges # -------------------------------------------------------------------------- update_form_from_force(xy, _xy, free, leaves, i_j, ij_e, _C, kmax=kmax) # -------------------------------------------------------------------------- # update # -------------------------------------------------------------------------- uv = C.dot(xy) _uv = _C.dot(_xy) a = [angle_vectors_xy(uv[i], _uv[i], deg=True) for i in range(len(edges))] l = normrow(uv) _l = normrow(_uv) q = _l / l # -------------------------------------------------------------------------- # update form diagram # -------------------------------------------------------------------------- for key, attr in form.vertices(True): index = k_i[key] attr['x'] = xy[index, 0] attr['y'] = xy[index, 1] for u, v, attr in form.edges(True): e = uv_e[(u, v)] attr['l'] = l[e, 0] attr['a'] = a[e] if a[e] < 90: attr['f'] = _l[e, 0] attr['q'] = q[e, 0] else: attr['f'] = -_l[e, 0] attr['q'] = -q[e, 0] # -------------------------------------------------------------------------- # update force diagram # -------------------------------------------------------------------------- for u, v, attr in force.edges(True): e = _uv_e[(u, v)] attr['a'] = a[e] attr['l'] = _l[e, 0]