Ejemplo n.º 1
0
def _triangle_xform(triangle):
    o = triangle[0]
    u = triangle[1] - o
    v = triangle[2] - o
    w = cross_vectors(u, v)
    v = cross_vectors(w, u)
    A = normalizerow(array([u, v, w])).T
    return o, A
Ejemplo n.º 2
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]
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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]
Ejemplo n.º 5
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]
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
def update_primal_from_dual(xy,
                            _xy,
                            free,
                            i_nbrs,
                            ij_e,
                            _C,
                            line_constraints=None,
                            target_lengths=[],
                            target_vectors=[],
                            leaves=[],
                            kmax=100):
    r"""Update the coordinates of the primal diagram using the coordinates of the corresponding dual diagram.
    This function apply to both sides, i.e. it can be used to update the form diagram from the geometry of the force
    diagram or to update the force diagram from the geometry of the force diagram.

    Parameters
    ----------
    xy : array-like
        XY coordinates of the vertices of the primal diagram.
    _xy : array-like
        XY coordinates of the vertices of the dual 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.
    line_constraints : list, optional
        Line constraints applied to the free nodes.
        Default is an ``None`` in which case no line constraints are considered.
    target_lengths : list, optional
        Target lengths / target forces of the edges.
        Default is an empty list, which considers that no target lengths are considered.
    target_vectors : list, optional
        Target vectors of the edges.
        Default is an empty list, which considers that no target vectors are considered.
    leaves : list, optional
        The leaves of the primal diagram.
        Default is an empty list, which considers that no leaves are considered.
    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. Or to update the force diagram geometrically to
    become reciprocal to the form diagram. The objective is to compute new locations
    for the vertices of the primal diagram such that the corrsponding lines of the
    primal and dual diagram are parallel while any geometric constraints imposed on
    the primal diagram are satisfied. Note that form, and force can assume position
    of primal and dual interchagable.

    The location of each vertex of the primal 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 dual
    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 primal 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 dual diagram.
        # the intersection is the point that minimises the distance to all connected
        # lines.
        for count in range(len(free)):
            i = free[count]
            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

                if target_lengths:
                    if target_lengths[ij_e[(i, j)]] == 0.0:
                        continue

                if target_vectors:
                    if target_vectors[ij_e[(i, j)]]:
                        n = array(target_vectors[ij_e[(i, j)]]).reshape(1, -1)

                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)

            if line_constraints:
                line = line_constraints[count]
                if line:
                    n_ = array(line.direction[:2]).reshape(1, 2)
                    r = I - n_.T.dot(n_)
                    pt = array(line.start[:2]).reshape(1, 2)
                    R += r
                    q += r.dot(pt.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 = lstsq(A, b)
        xy_lstsq = res[0].reshape((-1, 2), order='C')
        xy[free] = xy_lstsq

    # reconnect leaves
    for i in leaves:
        j = i_nbrs[i][0]
        xy[i] = xy[j] + xy0[i] - xy0[j]