Source code for pyrigi.framework

"""

Module for the functionality concerning frameworks.

.. currentmodule:: pyrigi.framework

Classes:

.. autosummary::

    Framework

"""

from __future__ import annotations
from typing import List, Dict, Union

from copy import deepcopy
from itertools import combinations
from random import randrange

import networkx as nx
import sympy as sp
import numpy as np

from sympy import Matrix, flatten, binomial

from pyrigi.data_type import (
    Vertex,
    Edge,
    Point,
    Stress,
    point_to_vector,
    Sequence,
    Coordinate,
)

from pyrigi.graph import Graph
from pyrigi.exception import LoopError
from pyrigi.graphDB import Complete as CompleteGraph
from pyrigi.misc import (
    doc_category,
    generate_category_tables,
    check_integrality_and_range,
    is_zero_vector,
    generate_two_orthonormal_vectors,
    eval_sympy_vector,
)

from typing import Optional

__doctest_requires__ = {
    ("Framework.generate_stl_bars",): ["trimesh", "manifold3d", "pathlib"]
}


[docs] class Framework(object): r""" This class provides the functionality for frameworks. Definitions ----------- * :prf:ref:`Framework <def-framework>` * :prf:ref:`Realization <def-realization>` Parameters ---------- graph: A graph without loops. realization: A dictionary mapping the vertices of the graph to points in $\RR^d$. The dimension ``d`` is retrieved from the points in realization. If ``graph`` is empty, and hence also the ``realization``, the dimension is set to 0 (:meth:`Framework.Empty` can be used to construct an empty framework with different dimension). Examples -------- >>> F = Framework(Graph([[0,1]]), {0:[1,2], 1:[0,5]}) >>> F Framework in 2-dimensional space consisting of: Graph with vertices [0, 1] and edges [[0, 1]] Realization {0:(1, 2), 1:(0, 5)} Notice that the realization of a vertex can be accessed using ``[ ]``: >>> F[0] Matrix([ [1], [2]]) TODO ---- Use :meth:`~.Framework.set_realization` in the constructor. METHODS Notes ----- Internally, the realization is represented as ``Dict[Vertex,Matrix]``. However, :meth:`~Framework.realization` can also return ``Dict[Vertex,Point]``. """ def __init__(self, graph: Graph, realization: Dict[Vertex, Point]) -> None: if not isinstance(graph, Graph): raise TypeError("The graph has to be an instance of class Graph.") if nx.number_of_selfloops(graph) > 0: raise LoopError() if not len(realization.keys()) == graph.number_of_nodes(): raise KeyError( "The length of realization has to be equal to " "the number of vertices of graph." ) if realization: self._dim = len(list(realization.values())[0]) else: self._dim = 0 for v in graph.nodes: if v not in realization: raise KeyError(f"Vertex {v} is not contained in the realization.") if not len(realization[v]) == self._dim: raise ValueError( f"The point {realization[v]} in the realization corresponding to " f"vertex {v} does not have the right dimension." ) self._realization = {v: Matrix(realization[v]) for v in graph.nodes} self._graph = deepcopy(graph)
[docs] def __str__(self) -> str: """Return the string representation.""" return ( self.__class__.__name__ + f" in {self.dim()}-dimensional space consisting of:\n{self._graph}\n" + "Realization {" + ", ".join( [ f"{v}:{tuple(self._realization[v])}" for v in self._graph.vertex_list() ] ) + "}" )
[docs] def __repr__(self) -> str: """Return the representation""" return self.__str__()
[docs] def __getitem__(self, vertex: Vertex) -> Matrix: """ Return the coordinates corresponding to the image of a given vertex under the realization map. Examples -------- >>> F = Framework(Graph([[0,1]]), {0:[1,2], 1:[0,5]}) >>> F[0] Matrix([ [1], [2]]) """ return self._realization[vertex]
[docs] @doc_category("Attribute getters") def dim(self) -> int: """Return the dimension of the framework.""" return self._dim
[docs] @doc_category("Attribute getters") def dimension(self) -> int: """Alias for :meth:`~Framework.dim`""" return self.dim()
[docs] @doc_category("Framework manipulation") def add_vertex(self, point: Point, vertex: Vertex = None) -> None: """ Add a vertex to the framework with the corresponding coordinates. If no vertex is provided (``None``), then the smallest, free integer is chosen instead. Parameters ---------- point: the realization of the new vertex vertex: the label of the new vertex Examples -------- >>> F = Framework.Empty(dim=2) >>> F.add_vertex((1.5,2), 'a') >>> F.add_vertex((3,1)) >>> F Framework in 2-dimensional space consisting of: Graph with vertices ['a', 1] and edges [] Realization {a:(1.50000000000000, 2), 1:(3, 1)} """ if vertex is None: candidate = self._graph.number_of_nodes() while candidate in self._graph.nodes: candidate += 1 vertex = candidate if vertex in self._graph.nodes: raise KeyError(f"Vertex {vertex} is already a vertex of the graph!") self._realization[vertex] = Matrix(point) self._graph.add_node(vertex)
[docs] @doc_category("Framework manipulation") def add_vertices(self, points: List[Point], vertices: List[Vertex] = []) -> None: r""" Add a list of vertices to the framework. Parameters ---------- points: List of points consisting of coordinates in $\RR^d$. It is checked that all points lie in the same ambient space. vertices: List of vertices. If the list of vertices is empty, we generate a vertex that is not yet taken with the method :meth:`add_vertex`. Else, the list of vertices needs to have the same length as the list of points. Examples -------- >>> F = Framework.Empty(dim=2) >>> F.add_vertices([(1.5,2), (3,1)], ['a',0]) >>> print(F) Framework in 2-dimensional space consisting of: Graph with vertices ['a', 0] and edges [] Realization {a:(1.50000000000000, 2), 0:(3, 1)} Notes ----- For each vertex that has to be added, :meth:`add_vertex` is called. """ if not (len(points) == len(vertices) or not vertices): raise IndexError("The vertex list does not have the correct length!") if not vertices: for point in points: self.add_vertex(point) else: for p, v in zip(points, vertices): self.add_vertex(p, v)
[docs] @doc_category("Framework manipulation") def add_edge(self, edge: Edge) -> None: """ Add an edge to the framework. Notes ----- This method only alters the graph attribute. """ self._graph._check_edge_format(edge) self._graph.add_edge(*(edge))
[docs] @doc_category("Framework manipulation") def add_edges(self, edges: List[Edge]) -> None: """ Add a list of edges to the framework. Notes ----- For each edge that has to be added, :meth:`add_edge` is called. """ for edge in edges: self.add_edge(edge)
[docs] @doc_category("Attribute getters") def graph(self) -> Graph: """ Return a copy of the underlying graph. Examples ---- >>> F = Framework.Random(Graph([(0,1), (1,2), (0,2)])) >>> F.graph() Graph with vertices [0, 1, 2] and edges [[0, 1], [0, 2], [1, 2]] """ return deepcopy(self._graph)
@doc_category("Other") def _plot_with_2D_realization( self, realization: Dict[Vertex, Point], inf_flex: Dict[Vertex, Sequence[Coordinate]] = None, vertex_color="#ff8c00", edge_width=1.5, **kwargs, ) -> None: """ Plot the graph of the framework with the given realization in the plane. For description of other parameters see :meth:`.Framework.plot`. Parameters ---------- realization: The realization in the plane used for plotting. inf_flex: Optional parameter for plotting an infinitesimal flex. We expect it to have the same format as `realization`: `Dict[Vertex, Point]`. """ self._graph.plot( placement=realization, vertex_color=vertex_color, edge_width=edge_width, inf_flex=inf_flex, **kwargs, ) @doc_category("Other") def _plot_using_projection_matrix( self, projection_matrix: Matrix, **kwargs, ) -> None: """ Plot the framework with the realization projected using the given matrix. For description of other parameters see :meth:`.Framework.plot`. Parameters ---------- projection_matrix: The matrix used for projection. The matrix must have dimensions ``(2, dim)``, where ``dim`` is the dimension of the framework. """ placement = {} for vertex, position in self.realization( as_points=False, numerical=True ).items(): placement[vertex] = np.dot(projection_matrix, np.array(position)) self._plot_with_2D_realization(placement, **kwargs)
[docs] @doc_category("Other") def plot2D( # noqa: C901 self, coordinates: Union[tuple, List] = None, inf_flex: Matrix | int | Dict[Vertex, Sequence[Coordinate]] = None, projection_matrix: Matrix = None, return_matrix: bool = False, random_seed: int = None, **kwargs, ) -> Optional[Matrix]: """ Plot this framework in 2D. If this framework is in dimensions higher than 2 and projection_matrix with coordinates are None a random projection matrix containing two orthonormal vectors is generated and used for projection into 2D. This matrix is then returned. For various formatting options, see :meth:`.Graph.plot`. Only coordinates or projection_matrix parameter can be used, not both! Parameters ---------- projection_matrix: The matrix used for projecting the placement of vertices only when they are in dimension higher than 2. The matrix must have dimensions (2, dim), where dim is the dimension of the currect placements of vertices. If None, a random projection matrix is generated. random_seed: The random seed used for generating the projection matrix. When the same value is provided, the framework will plot exactly same. coordinates: Indexes of two coordinates that will be used as the placement in 2D. inf_flex: Optional parameter for plotting a given infinitesimal flex. It is important to use the same vertex order as the one from :meth:`.Graph.vertex_list`. Alternatively, an ``int`` can be specified to choose the 0,1,2,...-th nontrivial infinitesimal flex for plotting. Lastly, a ``Dict[Vertex, Sequence[Coordinate]]`` can be provided, which maps the vertex labels to vectors (i.e. a sequence of coordinates). return_matrix: If True the matrix used for projection into 2D is returned. TODO ----- project the inf-flex as well in `_plot_using_projection_matrix`. """ inf_flex_pointwise = None if inf_flex is not None: if isinstance(inf_flex, int) and inf_flex >= 0: inf_flex_basis = self.nontrivial_inf_flexes() if inf_flex >= len(inf_flex_basis): raise IndexError( "The value of inf_flex exceeds " + "the dimension of the space " + "of infinitesimal flexes." ) inf_flex_pointwise = self._transform_inf_flex_to_pointwise( inf_flex_basis[inf_flex] ) elif isinstance(inf_flex, Matrix): inf_flex_pointwise = self._transform_inf_flex_to_pointwise(inf_flex) elif isinstance(inf_flex, dict) and all( isinstance(inf_flex[key], Sequence) for key in inf_flex.keys() ): inf_flex_pointwise = inf_flex else: raise TypeError("inf_flex does not have the correct Type.") if not self.is_dict_inf_flex(inf_flex_pointwise): raise ValueError( "The provided `inf_flex` is not an infinitesimal flex." ) if self._dim == 1: placement = {} for vertex, position in self.realization( as_points=True, numerical=True ).items(): placement[vertex] = np.append(np.array(position), 0) if inf_flex_pointwise is not None: inf_flex_pointwise = { v: (flex_v[0], 0) for v, flex_v in inf_flex_pointwise.items() } self._plot_with_2D_realization( placement, inf_flex=inf_flex_pointwise, **kwargs ) return if self._dim == 2: placement = self.realization(as_points=True, numerical=True) self._plot_with_2D_realization( placement, inf_flex=inf_flex_pointwise, **kwargs ) return # dim > 2 -> use projection to 2D if coordinates is not None: if ( not isinstance(coordinates, tuple) and not isinstance(coordinates, list) or len(coordinates) != 2 ): raise ValueError( "coordinates must have length 2!" + " Exactly Two coordinates are necessary for plotting in 2D." ) if np.max(coordinates) >= self._dim: raise ValueError( f"Index {np.max(coordinates)} out of range" + f" with placement in dim: {self._dim}." ) projection_matrix = np.zeros((2, self._dim)) projection_matrix[0, coordinates[0]] = 1 projection_matrix[1, coordinates[1]] = 1 if projection_matrix is not None: projection_matrix = np.array(projection_matrix) if projection_matrix.shape != (2, self._dim): raise ValueError( f"The projection matrix has wrong dimensions! \ {projection_matrix.shape} instead of (2, {self._dim})." ) if projection_matrix is None: projection_matrix = generate_two_orthonormal_vectors( self._dim, random_seed=random_seed ) projection_matrix = projection_matrix.T self._plot_using_projection_matrix(projection_matrix, **kwargs) if return_matrix: return projection_matrix
[docs] @doc_category("Other") def plot( self, **kwargs, ) -> None: """ Plot the framework. If the dimension of the framework is greater than 2, ``ValueError`` is raised, use :meth:`.Framework.plot2D` instead. For various formatting options, see :meth:`.Graph.plot`. TODO ---- Implement plotting in dimension 3 and better plotting for dimension 1 using ``connectionstyle`` """ if self._dim > 2: raise ValueError( "This framework is in higher dimension than 2!" + " For projection into 2D use F.plot2D()" ) self.plot2D(**kwargs)
[docs] @doc_category("Other") def to_tikz( self, vertex_style: Union(str, dict[str : list[Vertex]]) = "fvertex", edge_style: Union(str, dict[str : list[Edge]]) = "edge", label_style: str = "labelsty", figure_opts: str = "", vertex_in_labels: bool = False, vertex_out_labels: bool = False, default_styles: bool = True, ) -> str: r""" Create a TikZ code for the framework. Works for dimension 2 only. For using it in ``LaTeX`` you need to use the ``tikz`` package. Parameters ---------- vertex_style: If a single style is given as a string, then all vertices get this style. If a dictionary from styles to a list of vertices is given, vertices are put in style accordingly. The vertices missing in the dictionary do not get a style. edge_style: If a single style is given as a string, then all edges get this style. If a dictionary from styles to a list of edges is given, edges are put in style accordingly. The edges missing in the dictionary do not get a style. label_style: The style for labels that are placed next to vertices. figure_opts: Options for the tikzpicture environment. vertex_in_labels A bool on whether vertex names should be put as labels on the vertices. vertex_out_labels A bool on whether vertex names should be put next to vertices. default_styles A bool on whether default style definitions should be put to the options. Examples ---------- >>> G = Graph([(0, 1), (1, 2), (2, 3), (0, 3)]) >>> F=Framework(G,{0: [0, 0], 1: [1, 0], 2: [1, 1], 3: [0, 1]}) >>> print(F.to_tikz()) # doctest: +NORMALIZE_WHITESPACE \begin{tikzpicture}[fvertex/.style={circle,inner sep=0pt,minimum size=3pt,fill=white,draw=black,double=white,double distance=0.25pt,outer sep=1pt},edge/.style={line width=1.5pt,black!60!white}] \node[fvertex] (0) at (0, 0) {}; \node[fvertex] (1) at (1, 0) {}; \node[fvertex] (2) at (1, 1) {}; \node[fvertex] (3) at (0, 1) {}; \draw[edge] (0) to (1) (0) to (3) (1) to (2) (2) to (3); \end{tikzpicture} >>> print(F.to_tikz(vertex_in_labels=True)) # doctest: +NORMALIZE_WHITESPACE \begin{tikzpicture}[fvertex/.style={circle,inner sep=1pt,minimum size=3pt,fill=white,draw=black,double=white,double distance=0.25pt,outer sep=1pt,font=\scriptsize},edge/.style={line width=1.5pt,black!60!white}] \node[fvertex] (0) at (0, 0) {$0$}; \node[fvertex] (1) at (1, 0) {$1$}; \node[fvertex] (2) at (1, 1) {$2$}; \node[fvertex] (3) at (0, 1) {$3$}; \draw[edge] (0) to (1) (0) to (3) (1) to (2) (2) to (3); \end{tikzpicture} For more examples on formatting options, see also :meth:`.Graph.to_tikz`. """ # noqa: E501 # check dimension if self.dimension() != 2: raise ValueError( "TikZ code is only generated for frameworks in dimension 2." ) # strings for tikz styles if vertex_out_labels and default_styles: lstyle_str = r"labelsty/.style={font=\scriptsize,black!70!white}" else: lstyle_str = "" if vertex_style == "fvertex" and default_styles: if vertex_in_labels: vstyle_str = ( "fvertex/.style={circle,inner sep=1pt,minimum size=3pt," "fill=white,draw=black,double=white,double distance=0.25pt," r"outer sep=1pt,font=\scriptsize}" ) else: vstyle_str = ( "fvertex/.style={circle,inner sep=0pt,minimum size=3pt,fill=white," "draw=black,double=white,double distance=0.25pt,outer sep=1pt}" ) else: vstyle_str = "" if edge_style == "edge" and default_styles: estyle_str = "edge/.style={line width=1.5pt,black!60!white}" else: estyle_str = "" figure_str = [figure_opts, vstyle_str, estyle_str, lstyle_str] figure_str = [fs for fs in figure_str if fs != ""] figure_str = ",".join(figure_str) return self.graph().to_tikz( placement=self.realization(), figure_opts=figure_str, vertex_style=vertex_style, edge_style=edge_style, label_style=label_style, vertex_in_labels=vertex_in_labels, vertex_out_labels=vertex_out_labels, default_styles=False, )
[docs] @classmethod @doc_category("Class methods") def from_points(cls, points: List[Point]) -> Framework: """ Generate a framework from a list of points. The list of vertices of the underlying graph is taken to be ``[0,...,len(points)-1]``. The underlying graph has no edges. Parameters ---------- points: The realization of the framework that this method outputs is provided as a list of points. Examples -------- >>> F = Framework.from_points([(1,2), (2,3)]) >>> print(F) Framework in 2-dimensional space consisting of: Graph with vertices [0, 1] and edges [] Realization {0:(1, 2), 1:(2, 3)} """ vertices = range(len(points)) realization = {v: points[v] for v in vertices} return Framework(Graph.from_vertices(vertices), realization)
[docs] @classmethod @doc_category("Class methods") def Random( cls, graph: Graph, dim: int = 2, rand_range: int | List[int] = None ) -> Framework: """ Return a framework with random realization. Parameters ---------- graph: Graph on which the random realization should be constructed. rand_range: Sets the range of random numbers from which the realization is sampled. The format is either an interval ``(a,b)`` or a single integer ``a``, which produces the range ``(-a,a)``. Examples -------- >>> F = Framework.Random(Graph([(0,1), (1,2), (0,2)])) >>> print(F) # doctest: +SKIP Framework in 2-dimensional space consisting of: Graph with vertices [0, 1, 2] and edges [[0, 1], [0, 2], [1, 2]] Realization {0:(122, 57), 1:(27, 144), 2:(50, 98)} Notes ----- If ``rand_range=None``, then the range is set to ``(-10 * n^2 * d)``. TODO ---- Set the correct default range value. """ if not isinstance(dim, int) or dim < 1: raise TypeError( f"The dimension needs to be a positive integer, but is {dim}!" ) if rand_range is None: b = 10**4 * graph.number_of_nodes() ** 2 * dim a = -b if isinstance(rand_range, list): if not len(rand_range) == 2: raise ValueError("If `rand_range` is a list, it must be of length 2.") a, b = rand_range if isinstance(rand_range, int): if rand_range <= 0: raise ValueError("If `rand_range` is an int, it must be positive") b = rand_range a = -b realization = { vertex: [randrange(a, b) for _ in range(dim)] for vertex in graph.nodes } return Framework(graph, realization)
[docs] @classmethod @doc_category("Class methods") def Circular(cls, graph: Graph) -> Framework: """ Return the framework with a regular unit circle realization in the plane. Parameters ---------- graph: Underlying graph on which the framework is constructed. Examples ---- >>> import pyrigi.graphDB as graphs >>> F = Framework.Circular(graphs.CompleteBipartite(4, 2)) >>> print(F) Framework in 2-dimensional space consisting of: Graph with vertices [0, 1, 2, 3, 4, 5] and edges ... Realization {0:(1, 0), 1:(1/2, sqrt(3)/2), ... """ n = graph.number_of_nodes() return Framework( graph, { v: [sp.cos(2 * i * sp.pi / n), sp.sin(2 * i * sp.pi / n)] for i, v in enumerate(graph.vertex_list()) }, )
[docs] @classmethod @doc_category("Class methods") def Collinear(cls, graph: Graph, d: int = 1) -> Framework: """ Return the framework with a realization on the x-axis in the d-dimensional space. Parameters ---------- graph: Underlying graph on which the framework is constructed. Examples -------- >>> import pyrigi.graphDB as graphs >>> Framework.Collinear(graphs.Complete(3), d=2) Framework in 2-dimensional space consisting of: Graph with vertices [0, 1, 2] and edges [[0, 1], [0, 2], [1, 2]] Realization {0:(0, 0), 1:(1, 0), 2:(2, 0)} """ check_integrality_and_range(d, "dimension d", 1) return Framework( graph, { v: [i] + [0 for _ in range(d - 1)] for i, v in enumerate(graph.vertex_list()) }, )
[docs] @classmethod @doc_category("Class methods") def Simplicial(cls, graph: Graph, d: int = None) -> Framework: """ Return the framework with a realization on the d-simplex. Parameters ---------- graph: Underlying graph on which the framework is constructed. d: The dimension ``d`` has to be at least the number of vertices of the ``graph`` minus one. If ``d`` is not specified, then the least possible one is used. Examples ---- >>> F = Framework.Simplicial(Graph([(0,1), (1,2), (2,3), (0,3)]), 4); >>> F.realization(as_points=True) {0: [0, 0, 0, 0], 1: [1, 0, 0, 0], 2: [0, 1, 0, 0], 3: [0, 0, 1, 0]} >>> F = Framework.Simplicial(Graph([(0,1), (1,2), (2,3), (0,3)])); >>> F.realization(as_points=True) {0: [0, 0, 0], 1: [1, 0, 0], 2: [0, 1, 0], 3: [0, 0, 1]} """ if d is None: d = graph.number_of_nodes() - 1 check_integrality_and_range( d, "dimension d", max([1, graph.number_of_nodes() - 1]) ) return Framework( graph, { v: [1 if j == i - 1 else 0 for j in range(d)] for i, v in enumerate(graph.vertex_list()) }, )
[docs] @classmethod @doc_category("Class methods") def Empty(cls, dim: int = 2) -> Framework: """ Generate an empty framework. Parameters ---------- dim: a natural number that determines the dimension in which the framework is realized Examples ---- >>> F = Framework.Empty(dim=1); F Framework in 1-dimensional space consisting of: Graph with vertices [] and edges [] Realization {} """ if not isinstance(dim, int) or dim < 1: raise TypeError( f"The dimension needs to be a positive integer, but is {dim}!" ) F = Framework(graph=Graph(), realization={}) F._dim = dim return F
[docs] @classmethod @doc_category("Class methods") def Complete(cls, points: List[Point]) -> Framework: """ Generate a framework on the complete graph from a given list of points. The vertices of the underlying graph are taken to be the list ``[0,...,len(points)-1]``. Parameters ---------- points: The realization of the framework that this method outputs is provided as a list of points. Examples -------- >>> F = Framework.Complete([(1,),(2,),(3,),(4,)]); F Framework in 1-dimensional space consisting of: Graph with vertices [0, 1, 2, 3] and edges [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]] Realization {0:(1,), 1:(2,), 2:(3,), 3:(4,)} """ # noqa: E501 if not points: raise ValueError("The list of points cannot be empty.") Kn = CompleteGraph(len(points)) return Framework(Kn, {v: Matrix(p) for v, p in zip(Kn.nodes, points)})
[docs] @doc_category("Framework manipulation") def delete_vertex(self, vertex: Vertex) -> None: """ Delete a vertex from the framework. """ self._graph.delete_vertex(vertex) del self._realization[vertex]
[docs] @doc_category("Framework manipulation") def delete_vertices(self, vertices: List[Vertex]) -> None: """ Delete a list of vertices from the framework. """ for vertex in vertices: self.delete_vertex(vertex)
[docs] @doc_category("Framework manipulation") def delete_edge(self, edge: Edge) -> None: """ Delete an edge from the framework. """ self._graph.delete_edge(edge)
[docs] @doc_category("Framework manipulation") def delete_edges(self, edges: List[Edge]) -> None: """ Delete a list of edges from the framework. """ self._graph.delete_edges(edges)
[docs] @doc_category("Attribute getters") def realization( self, as_points: bool = False, numerical: bool = False ) -> Dict[Vertex, Point]: """ Return a copy of the realization. Parameters ---------- as_points: If ``True``, then the vertex positions type is Point, otherwise Matrix (default). numerical: If ``True``, the vertex positions are converted to floats. Examples -------- >>> F = Framework.Complete([(0,0), (1,0), (1,1)]) >>> F.realization(as_points=True) {0: [0, 0], 1: [1, 0], 2: [1, 1]} >>> F.realization() {0: Matrix([ [0], [0]]), 1: Matrix([ [1], [0]]), 2: Matrix([ [1], [1]])} Notes ----- The format returned by this method with ``as_points=True`` can be read by networkx. """ if not numerical: if not as_points: return deepcopy(self._realization) return { vertex: list(position) for vertex, position in self._realization.items() } else: if not as_points: { vertex: Matrix([float(p) for p in position]) for vertex, position in self._realization.items() } return { vertex: [float(p) for p in position] for vertex, position in self._realization.items() }
[docs] @doc_category("Framework properties") def is_quasi_injective( self, numerical: bool = False, tolerance: float = 1e-9 ) -> bool: """ Return whether the realization is :prf:ref:`quasi-injective <def-realization>`. Parameters ---------- numerical: Whether the check is symbolic (default) or numerical. tolerance: Used tolerance when checking numerically. Notes ----- For comparing whether two vectors are the same, :func:`.misc.is_zero_vector` is used. See its documentation for the description of the parameters. """ for u, v in self._graph.edges: edge_vector = self[u] - self[v] if is_zero_vector(edge_vector, numerical, tolerance): return False return True
[docs] @doc_category("Framework properties") def is_injective(self, numerical: bool = False, tolerance: float = 1e-9) -> bool: """ Return whether the realization is injective. Parameters ---------- numerical Whether the check is symbolic (default) or numerical. tolerance Used tolerance when checking numerically. Notes ----- For comparing whether two vectors are the same, :func:`.misc.is_zero_vector` is used. See its documentation for the description of the parameters. """ for u, v in combinations(self._graph.nodes, 2): edge_vector = self[u] - self[v] if is_zero_vector(edge_vector, numerical, tolerance): return False return True
[docs] @doc_category("Framework manipulation") def set_realization(self, realization: Dict[Vertex, Point]) -> None: r""" Change the realization of the framework. Parameters ---------- realization: a realization of the underlying graph of the framework Notes ----- It is assumed that the realization contains all vertices from the underlying graph. Furthermore, all points in the realization need to be contained in $\RR^d$ for a fixed $d$. Examples -------- >>> F = Framework.Complete([(0,0), (1,0), (1,1)]) >>> F.set_realization({vertex:(vertex,vertex+1) for vertex in F.graph().vertex_list()}) >>> print(F) Framework in 2-dimensional space consisting of: Graph with vertices [0, 1, 2] and edges [[0, 1], [0, 2], [1, 2]] Realization {0:(0, 1), 1:(1, 2), 2:(2, 3)} """ # noqa: E501 if not len(realization) == self._graph.number_of_nodes(): raise IndexError( "The realization does not contain the correct amount of vertices!" ) for v in self._graph.nodes: if v not in realization: raise KeyError("Vertex {vertex} is not a key of the given realization!") if not len(realization[v]) == self.dimension(): raise IndexError( f"The element {realization[v]} does not have " f"the dimension {self.dimension()}!" ) self._realization = {v: Matrix(realization[v]) for v in realization.keys()}
[docs] @doc_category("Framework manipulation") def set_vertex_pos(self, vertex: Vertex, point: Point) -> None: """ Change the coordinates of a single given vertex. Examples -------- >>> F = Framework.from_points([(0,0)]) >>> F.set_vertex_pos(0, (6,2)) >>> print(F) Framework in 2-dimensional space consisting of: Graph with vertices [0] and edges [] Realization {0:(6, 2)} """ if vertex not in self._realization: raise KeyError("Vertex {vertex} is not a key of the given realization!") if not len(point) == self.dimension(): raise IndexError( f"The point {point} does not have the dimension {self.dimension()}!" ) self._realization[vertex] = Matrix(point)
[docs] @doc_category("Framework manipulation") def set_vertex_positions_from_lists( self, vertices: List[Vertex], points: List[Point] ) -> None: """ Change the coordinates of a given list of vertices. Examples ---- >>> F = Framework.Complete([(0,0),(0,0),(1,0),(1,0)]); >>> F.realization(as_points=True) {0: [0, 0], 1: [0, 0], 2: [1, 0], 3: [1, 0]} >>> F.set_vertex_positions_from_lists([1,3], [(0,1),(1,1)]); >>> F.realization(as_points=True) {0: [0, 0], 1: [0, 1], 2: [1, 0], 3: [1, 1]} Notes ----- It is necessary that both lists have the same length. No vertex from ``vertices`` can be contained multiple times. We apply the method :meth:`~Framework.set_vertex_pos` to ``vertices`` and ``points``. """ if len(list(set(vertices))) != len(list(vertices)): raise ValueError("Multiple Vertices with the same name were found!") if not len(vertices) == len(points): raise IndexError( "The list of vertices does not have the same length as the list of points" ) self.set_vertex_positions({v: pos for v, pos in zip(vertices, points)})
[docs] @doc_category("Framework manipulation") def set_vertex_positions(self, subset_of_realization: Dict[Vertex, Point]): """ Change the coordinates of vertices given by a dictionary. Examples ---- >>> F = Framework.Complete([(0,0),(0,0),(1,0),(1,0)]); >>> F.realization(as_points=True) {0: [0, 0], 1: [0, 0], 2: [1, 0], 3: [1, 0]} >>> F.set_vertex_positions({1:(0,1),3:(1,1)}); >>> F.realization(as_points=True) {0: [0, 0], 1: [0, 1], 2: [1, 0], 3: [1, 1]} Notes ----- See :meth:`~Framework.set_vertex_pos`. """ for v, pos in subset_of_realization.items(): self.set_vertex_pos(v, pos)
[docs] @doc_category("Infinitesimal rigidity") def rigidity_matrix( self, vertex_order: List[Vertex] = None, edge_order: List[Edge] = None, ) -> Matrix: r""" Construct the rigidity matrix of the framework. Definitions ----------- * :prf:ref:`Rigidity matrix <def-rigidity-matrix>` Parameters ---------- vertex_order: A list of vertices, providing the ordering for the columns of the rigidity matrix. If none is provided, the list from :meth:`~Graph.vertex_list` is taken. edge_order: A list of edges, providing the ordering for the rows of the rigidity matrix. If none is provided, the list from :meth:`~Graph.edge_list` is taken. Examples -------- >>> F = Framework.Complete([(0,0),(2,0),(1,3)]) >>> F.rigidity_matrix() Matrix([ [-2, 0, 2, 0, 0, 0], [-1, -3, 0, 0, 1, 3], [ 0, 0, 1, -3, -1, 3]]) """ vertex_order = self._check_vertex_order(vertex_order) edge_order = self._check_edge_order(edge_order) # ``delta`` is responsible for distinguishing the edges (i,j) and (j,i) def delta(e, w): # the parameter e represents an edge # the parameter w represents a vertex if w == e[0]: return 1 if w == e[1]: return -1 return 0 return Matrix( [ flatten( [ delta(e, w) * (self._realization[e[0]] - self._realization[e[1]]) for w in vertex_order ] ) for e in edge_order ] )
[docs] def pinned_rigidity_matrix( self, pinned_vertices: Dict[Vertex, List[int]] = None, vertex_order: List[Vertex] = None, edge_order: List[Edge] = None, ) -> Matrix: r""" Construct the rigidity matrix of the framework. Parameters ---------- vertex_order: A list of vertices, providing the ordering for the columns of the rigidity matrix. edge_order: A list of edges, providing the ordering for the rows of the rigidity matrix. TODO ---- definition of pinned rigidity matrix, tests Examples -------- >>> F = Framework(Graph([[0, 1], [0, 2]]), {0: [0, 0], 1: [1, 0], 2: [1, 1]}) >>> F.pinned_rigidity_matrix() Matrix([ [-1, 0, 1, 0, 0, 0], [-1, -1, 0, 0, 1, 1], [ 1, 0, 0, 0, 0, 0], [ 0, 1, 0, 0, 0, 0], [ 0, 0, 1, 0, 0, 0]]) """ vertex_order = self._check_vertex_order(vertex_order) edge_order = self._check_edge_order(edge_order) rigidity_matrix = self.rigidity_matrix( vertex_order=vertex_order, edge_order=edge_order ) if pinned_vertices is None: freedom = self._dim * (self._dim + 1) // 2 pinned_vertices = {} upper = self._dim + 1 for v in vertex_order: upper -= 1 frozen_coord = [] for i in range(upper): if freedom > 0: frozen_coord.append(i) freedom -= 1 else: pinned_vertices[v] = frozen_coord break pinned_vertices[v] = frozen_coord else: number_pinned = sum([len(coord) for coord in pinned_vertices.values()]) if number_pinned > self._dim * (self._dim + 1) // 2: raise ValueError( "The maximal number of coordinates that" f"can be pinned is {self._dim * (self._dim + 1) // 2}, " f"but you provided {number_pinned}." ) for v in pinned_vertices: if min(pinned_vertices[v]) < 0 or max(pinned_vertices[v]) >= self._dim: raise ValueError("Coordinate indices out of range.") pinning_rows = [] for v in pinned_vertices: for coord in pinned_vertices[v]: idx = vertex_order.index(v) new_row = Matrix.zeros(1, self._dim * self._graph.number_of_nodes()) new_row[idx * self._dim + coord] = 1 pinning_rows.append(new_row) pinned_rigidity_matrix = Matrix.vstack(rigidity_matrix, *pinning_rows) return pinned_rigidity_matrix
[docs] @doc_category("Infinitesimal rigidity") def is_stress( self, stress: Stress, edge_order: List[Edge] = None, numerical: bool = False, tolerance=1e-9, ) -> bool: r""" Return whether a vector is a stress. Definitions ----------- :prf:ref:`Equilibrium stress <def-equilibrium-stress>` Parameters ---------- stress: A vector to be checked whether it is a stress of the framework. edge_order: A list of edges, providing the ordering for the entries of the ``stress``. If none is provided, the list from :meth:`~Graph.edge_list` is taken. numerical: A Boolean determining whether the evaluation of the product of the ``stress`` and the rigidity matrix is symbolic or numerical. tolerance: Absolute tolerance that is the threshold for acceptable equilibrium stresses. This parameter is used to determine the number of digits, to which accuracy the symbolic expressions are evaluated. Examples -------- >>> G = Graph([[0,1],[0,2],[0,3],[1,2],[2,3],[3,1]]) >>> pos = {0: (0, 0), 1: (0,1), 2: (-1,-1), 3: (1,-1)} >>> F = Framework(G, pos) >>> omega1 = [-8, -4, -4, 2, 2, 1] >>> F.is_stress(omega1) True >>> omega1[0] = 0 >>> F.is_stress(omega1) False """ edge_order = self._check_edge_order(edge_order=edge_order) return is_zero_vector( Matrix(stress).transpose() * self.rigidity_matrix(edge_order=edge_order), numerical=numerical, tolerance=tolerance, )
[docs] @doc_category("Infinitesimal rigidity") def stress_matrix( self, stress: Stress, edge_order: List[Edge] = None, vertex_order: List[Vertex] = None, ) -> Matrix: r""" Construct the stress matrix from a stress of from its support. The matrix order is the one from :meth:`~.Framework.vertex_list`. Definitions ----- * :prf:ref:`Stress Matrix <def-stress-matrix>` Parameters ---------- stress: A stress of the framework. edge_order: A list of edges, providing the ordering for the rows of the stress matrix. vertex_order: By listing vertices in the preferred order, the rigidity matrix can be computed in a way the user expects. Examples -------- >>> G = Graph([[0,1],[0,2],[0,3],[1,2],[2,3],[3,1]]) >>> pos = {0: (0, 0), 1: (0,1), 2: (-1,-1), 3: (1,-1)} >>> F = Framework(G, pos) >>> omega = [-8, -4, -4, 2, 2, 1] >>> F.stress_matrix(omega) Matrix([ [-16, 8, 4, 4], [ 8, -4, -2, -2], [ 4, -2, -1, -1], [ 4, -2, -1, -1]]) """ vertex_order = self._check_vertex_order(vertex_order) edge_order = self._check_edge_order(edge_order) if not self.is_stress(stress, edge_order=edge_order, numerical=True): raise ValueError( "The provided stress does not lie in the cokernel of the rigidity matrix!" ) # creation of a zero |V|x|V| matrix stress_matr = sp.zeros(len(self._graph)) v_to_i = {v: i for i, v in enumerate(vertex_order)} for edge, edge_stress in zip(edge_order, stress): for v in edge: stress_matr[v_to_i[v], v_to_i[v]] += edge_stress for e, stressval in zip(edge_order, stress): i, j = v_to_i[e[0]], v_to_i[e[1]] stress_matr[i, j] = -stressval stress_matr[j, i] = -stressval return stress_matr
[docs] @doc_category("Infinitesimal rigidity") def trivial_inf_flexes(self, vertex_order: List[Vertex] = None) -> List[Matrix]: r""" Return a basis of the vector subspace of trivial infinitesimal flexes. Definitions ----------- * :prf:ref:`Trivial infinitesimal flexes <def-trivial-inf-flex>` Parameters ---------- vertex_order: A list of vertices, providing the ordering for the entries of the infinitesimal flexes. Examples -------- >>> F = Framework.Complete([(0,0), (2,0), (0,2)]) >>> F.trivial_inf_flexes() [Matrix([ [1], [0], [1], [0], [1], [0]]), Matrix([ [0], [1], [0], [1], [0], [1]]), Matrix([ [ 0], [ 0], [ 0], [ 2], [-2], [ 0]])] """ vertex_order = self._check_vertex_order(vertex_order) dim = self._dim translations = [ Matrix.vstack(*[A for _ in vertex_order]) for A in Matrix.eye(dim).columnspace() ] basis_skew_symmetric = [] for i in range(1, dim): for j in range(i): A = Matrix.zeros(dim) A[i, j] = 1 A[j, i] = -1 basis_skew_symmetric += [A] inf_rot = [ Matrix.vstack(*[A * self._realization[v] for v in vertex_order]) for A in basis_skew_symmetric ] matrix_inf_flexes = Matrix.hstack(*(translations + inf_rot)) return matrix_inf_flexes.transpose().echelon_form().transpose().columnspace()
[docs] @doc_category("Infinitesimal rigidity") def nontrivial_inf_flexes(self, vertex_order: List[Vertex] = None) -> List[Matrix]: """ Return non-trivial infinitesimal flexes. Definitions ----------- :prf:ref:`Infinitesimal flex <def-inf-rigid-framework>` Parameters ---------- vertex_order: A list of vertices, providing the ordering for the entries of the infinitesimal flexes. If none is provided, the list from :meth:`~Graph.vertex_list` is taken. Examples -------- >>> import pyrigi.graphDB as graphs >>> F = Framework.Circular(graphs.CompleteBipartite(3, 3)) >>> F.nontrivial_inf_flexes() [Matrix([ [ 3/2], [-sqrt(3)/2], [ 1], [ 0], [ 0], [ 0], [ 3/2], [-sqrt(3)/2], [ 1], [ 0], [ 0], [ 0]])] Notes ----- See :meth:`~Framework.trivial_inf_flexes`. """ return self.inf_flexes(include_trivial=False, vertex_order=vertex_order)
[docs] @doc_category("Infinitesimal rigidity") def inf_flexes( self, include_trivial: bool = False, vertex_order: List[Vertex] = None ) -> List[Matrix]: r""" Return a basis of the space of infinitesimal flexes. Return a lift of a basis of the quotient of the vector space of infinitesimal flexes modulo trivial infinitesimal flexes, if ``include_trivial=False``. Return a basis of the vector space of infinitesimal flexes if ``include_trivial=True``. Else, return the entire kernel. Definitions ----------- * :prf:ref:`Infinitesimal flex <def-inf-flex>` Parameters ---------- include_trivial: Boolean that decides, whether the trivial flexes should be included (``True``) or not (``False``) vertex_order: A list of vertices, providing the ordering for the entries of the infinitesimal flexes. If none is provided, the list from :meth:`~Graph.vertex_list` is taken. Examples -------- >>> F = Framework.Complete([[0,0], [1,0], [1,1], [0,1]]) >>> F.delete_edges([(0,2), (1,3)]) >>> F.inf_flexes(include_trivial=False) [Matrix([ [1], [0], [1], [0], [0], [0], [0], [0]])] >>> F = Framework(Graph([[0, 1], [0, 3], [0, 4], [1, 3], [1, 4], [2, 3], [2, 4]]), {0: [0, 0], 1: [0, 1], 2: [0, 2], 3: [1, 2], 4: [-1, 2]}) >>> F.inf_flexes() [Matrix([ [0], [0], [0], [0], [0], [1], [0], [0], [0], [0]])] """ # noqa: E501 vertex_order = self._check_vertex_order(vertex_order) if include_trivial: return self.rigidity_matrix(vertex_order=vertex_order).nullspace() rigidity_matrix = self.rigidity_matrix(vertex_order=vertex_order) all_inf_flexes = rigidity_matrix.nullspace() trivial_inf_flexes = self.trivial_inf_flexes(vertex_order=vertex_order) s = len(trivial_inf_flexes) extend_basis_matrix = Matrix.hstack(*trivial_inf_flexes) tmp_matrix = Matrix.hstack(*trivial_inf_flexes) for v in all_inf_flexes: r = extend_basis_matrix.rank() tmp_matrix = Matrix.hstack(extend_basis_matrix, v) if not tmp_matrix.rank() == r: extend_basis_matrix = Matrix.hstack(extend_basis_matrix, v) basis = extend_basis_matrix.columnspace() return basis[s:]
[docs] @doc_category("Infinitesimal rigidity") def stresses(self, edge_order: List[Edge] = None) -> List[Matrix]: r""" Return a basis of the space of equilibrium stresses. Definitions ----------- :prf:ref:`Equilibrium stress <def-equilibrium-stress>` Parameters ---------- edge_order: A list of edges, providing the ordering for the entries of the stresses. If none is provided, the list from :meth:`~Graph.edge_list` is taken. Examples -------- >>> G = Graph([[0,1],[0,2],[0,3],[1,2],[2,3],[3,1]]) >>> pos = {0: (0, 0), 1: (0,1), 2: (-1,-1), 3: (1,-1)} >>> F = Framework(G, pos) >>> F.stresses() [Matrix([ [-8], [-4], [-4], [ 2], [ 2], [ 1]])] TODO ---- tests """ return self.rigidity_matrix(edge_order=edge_order).transpose().nullspace()
[docs] @doc_category("Infinitesimal rigidity") def rigidity_matrix_rank(self) -> int: """ Compute the rank of the rigidity matrix. Examples ---- >>> K4 = Framework.Complete([[0,0], [1,0], [1,1], [0,1]]) >>> K4.rigidity_matrix_rank() # the complete graph is a circuit 5 >>> K4.delete_edge([0,1]) >>> K4.rigidity_matrix_rank() # deleting a bar gives full rank 5 >>> K4.delete_edge([2,3]) >>> K4.rigidity_matrix_rank() #so now deleting an edge lowers the rank 4 """ return self.rigidity_matrix().rank()
[docs] @doc_category("Infinitesimal rigidity") def is_inf_rigid(self) -> bool: """ Check whether the given framework is infinitesimally rigid. The check is based on :meth:`~Framework.rigidity_matrix_rank`. Definitions ----- * :prf:ref:`Infinitesimal rigidity <def-inf-rigid-framework>` Examples ---- >>> from pyrigi import frameworkDB >>> F1 = frameworkDB.CompleteBipartite(4,4) >>> F1.is_inf_rigid() True >>> F2 = frameworkDB.Cycle(4,d=2) >>> F2.is_inf_rigid() False """ if self._graph.number_of_nodes() <= self._dim + 1: return self.rigidity_matrix_rank() == binomial( self._graph.number_of_nodes(), 2 ) else: return ( self.rigidity_matrix_rank() == self.dim() * self._graph.number_of_nodes() - binomial(self.dim() + 1, 2) )
[docs] @doc_category("Infinitesimal rigidity") def is_inf_flexible(self) -> bool: """ Check whether the given framework is infinitesimally flexible. See :meth:`~Framework.is_inf_rigid` """ return not self.is_inf_rigid()
[docs] @doc_category("Infinitesimal rigidity") def is_min_inf_rigid(self) -> bool: """ Check whether a framework is minimally infinitesimally rigid. Definitions ----- :prf:ref:`Minimal infinitesimal rigidity <def-min-rigid-framework>` Examples -------- >>> F = Framework.Complete([[0,0], [1,0], [1,1], [0,1]]) >>> F.is_min_inf_rigid() False >>> F.delete_edge((0,2)) >>> F.is_min_inf_rigid() True """ if not self.is_inf_rigid(): return False for edge in self._graph.edge_list(): self.delete_edge(edge) if self.is_inf_rigid(): self.add_edge(edge) return False self.add_edge(edge) return True
[docs] @doc_category("Infinitesimal rigidity") def is_independent(self) -> bool: """ Check whether the framework is :prf:ref:`independent <def-independent-framework>`. Examples -------- >>> F = Framework.Complete([[0,0], [1,0], [1,1], [0,1]]) >>> F.is_independent() False >>> F.delete_edge((0,2)) >>> F.is_independent() True """ return self.rigidity_matrix_rank() == self._graph.number_of_edges()
[docs] @doc_category("Infinitesimal rigidity") def is_dependent(self) -> bool: """ Check whether the framework is :prf:ref:`dependent <def-independent-framework>`. Notes ----- See also :meth:`~.Framework.is_independent`. """ return not self.is_independent()
[docs] @doc_category("Infinitesimal rigidity") def is_isostatic(self) -> bool: """ Check whether the framework is :prf:ref:`independent <def-independent-framework>` and :prf:ref:`infinitesimally rigid <def-inf-rigid-framework>`. """ return self.is_independent() and self.is_inf_rigid()
[docs] @doc_category("Waiting for implementation") def is_prestress_stable(self) -> bool: """ TODO ---- Implement """ raise NotImplementedError()
[docs] @doc_category("Infinitesimal rigidity") def is_redundantly_rigid(self) -> bool: """ Check if the framework is infinitesimally redundantly rigid. Definitions ----------- :prf:ref:`Redundant infinitesimal rigidity <def-redundantly-rigid-framework>` TODO ---- tests Examples -------- >>> F = Framework.Empty(dim=2) >>> F.add_vertices([(1,0), (1,1), (0,3), (-1,1)], ['a','b','c','d']) >>> F.add_edges([('a','b'), ('b','c'), ('c','d'), ('a','d'), ('a','c'), ('b','d')]) >>> F.is_redundantly_rigid() True >>> F.delete_edge(('a','c')) >>> F.is_redundantly_rigid() False """ # noqa: E501 for edge in self._graph.edge_list(): self.delete_edge(edge) if not self.is_inf_rigid(): self.add_edge(edge) return False self.add_edge(edge) return True
[docs] @doc_category("Framework properties") def is_congruent_realization( self, other_realization: Dict[Vertex, Point], numerical: bool = False, tolerance: float = 1e-9, ) -> bool: """ Return whether the given realization is congruent to self. Parameters ---------- other_realization The realization for checking the congruence. numerical Whether the check is symbolic (default) or numerical. tolerance Used tolerance when checking numerically. """ self._check_vertex_order(list(other_realization.keys())) for u, v in combinations(self._graph.nodes, 2): edge_vec = (self._realization[u]) - self._realization[v] dist_squared = (edge_vec.T * edge_vec)[0, 0] other_edge_vec = point_to_vector(other_realization[u]) - point_to_vector( other_realization[v] ) otherdist_squared = (other_edge_vec.T * other_edge_vec)[0, 0] difference = sp.simplify(dist_squared - otherdist_squared) if not difference.is_zero: if not numerical: return False elif numerical and sp.Abs(difference) > tolerance: return False return True
[docs] @doc_category("Framework properties") def is_congruent( self, other_framework: Framework, numerical: bool = False, tolerance: float = 1e-9, ) -> bool: """ Return whether the given framework is congruent to self. Parameters ---------- other_framework The framework for checking the congruence. numerical Whether the check is symbolic (default) or numerical. tolerance Used tolerance when checking numerically. """ if not nx.utils.graphs_equal(self._graph, other_framework._graph): raise ValueError("Underlying graphs are not same.") return self.is_congruent_realization( other_framework._realization, numerical, tolerance )
[docs] @doc_category("Framework properties") def is_equivalent_realization( self, other_realization: Dict[Vertex, Point], numerical: bool = False, tolerance: float = 1e-9, ) -> bool: """ Return whether the given realization is equivalent to self. Parameters ---------- other_realization The realization for checking the equivalence. numerical Whether the check is symbolic (default) or numerical. tolerance Used tolerance when checking numerically. """ self._check_vertex_order(list(other_realization.keys())) for u, v in self._graph.edges: edge_vec = self._realization[u] - self._realization[v] dist_squared = (edge_vec.T * edge_vec)[0, 0] other_edge_vec = point_to_vector(other_realization[u]) - point_to_vector( other_realization[v] ) otherdist_squared = (other_edge_vec.T * other_edge_vec)[0, 0] difference = sp.simplify(otherdist_squared - dist_squared) if not difference.is_zero: if not numerical: return False elif numerical and sp.Abs(difference) > tolerance: return False return True
[docs] @doc_category("Framework properties") def is_equivalent( self, other_framework: Framework, numerical: bool = False, tolerance: float = 1e-9, ) -> bool: """ Return whether the given framework is equivalent to self. Parameters ---------- other_framework The framework for checking the equivalence. numerical Whether the check is symbolic (default) or numerical. tolerance Used tolerance when checking numerically. """ if not nx.utils.graphs_equal(self._graph, other_framework._graph): raise ValueError("Underlying graphs are not same.") return self.is_equivalent_realization( other_framework._realization, numerical, tolerance )
[docs] @doc_category("Framework manipulation") def translate(self, vector: Point, inplace: bool = True) -> Union[None, Framework]: """ Translate the framework. Parameters ---------- vector Translation vector inplace If True (default), then this framework is translated. Otherwise, a new translated framework is returned. """ vector = point_to_vector(vector) if inplace: if vector.shape[0] != self.dim(): raise ValueError( "The dimension of the vector has to be the same as of the framework." ) for v in self._realization.keys(): self._realization[v] += vector return new_framework = deepcopy(self) new_framework.translate(vector, True) return new_framework
[docs] @doc_category("Framework manipulation") def rotate2D(self, angle: float, inplace: bool = True) -> Union[None, Framework]: """ Rotate the planar framework counter clockwise. Parameters ---------- angle Rotation angle inplace If True (default), then this framework is rotated. Otherwise, a new rotated framework is returned. """ if self.dim() != 2: raise ValueError("This realization is not in dimension 2!") rotation_matrix = Matrix( [[sp.cos(angle), -sp.sin(angle)], [sp.sin(angle), sp.cos(angle)]] ) if inplace: for v, pos in self._realization.items(): self._realization[v] = rotation_matrix * pos return new_framework = deepcopy(self) new_framework.rotate2D(angle, True) return new_framework
[docs] @doc_category("Other") def edge_lengths(self, numerical: bool = False) -> Dict[Edge, Coordinate]: """ Return the dictionary of the edge lengths. Parameters ------- numerical: If ``True``, numerical positions are used for the computation of the edge lengths. Examples -------- >>> G = Graph([(0,1), (1,2), (2,3), (0,3)]) >>> F = Framework(G, {0:[0,0], 1:[1,0], 2:[1,'1/2 * sqrt(5)'], 3:['1/2','4/3']}) >>> F.edge_lengths(numerical=False) {(0, 1): 1, (0, 3): sqrt(73)/6, (1, 2): sqrt(5)/2, (2, 3): sqrt((-4/3 + sqrt(5)/2)**2 + 1/4)} >>> F.edge_lengths(numerical=True) {(0, 1): 1.0, (0, 3): 1.4240006242195884, (1, 2): 1.118033988749895, (2, 3): 0.5443838790578374} """ # noqa: E501 if numerical: points = self.realization(as_points=True, numerical=True) return { tuple(pair): float( np.linalg.norm( np.array(points[pair[0]]) - np.array(points[pair[1]]) ) ) for pair in self._graph.edges } else: points = self.realization(as_points=True) return { tuple(pair): sp.sqrt( sum( [(v - w) ** 2 for v, w in zip(points[pair[0]], points[pair[1]])] ) ) for pair in self._graph.edges }
@staticmethod def _generate_stl_bar( holes_distance: float, holes_diameter: float, bar_width: float, bar_height: float, filename="bar.stl", ): """ Generate an STL file for a bar. The method uses Trimesh and Manifold3d packages to create a model of a bar with two holes at the ends. The bar is saved as an STL file. Parameters ---------- holes_distance : float Distance between the centers of the holes. holes_diameter : float Diameter of the holes. bar_width : float Width of the bar. bar_height : float Height of the bar. filename : str Name of the output STL file. Returns ------- bar_mesh : trimesh.base.Trimesh The bar as a Trimesh object. """ try: from trimesh.creation import box as trimesh_box from trimesh.creation import cylinder as trimesh_cylinder except ImportError: raise ImportError( "To create meshes of bars that can be exported as STL files, " "the packages 'trimesh' and 'manifold3d' are required. " "To install PyRigi including trimesh and manifold3d, " "run 'pip install pyrigi[meshing]'" ) if ( holes_distance <= 0 or holes_diameter <= 0 or bar_width <= 0 or bar_height <= 0 ): raise ValueError("Use only positive values for the parameters.") if bar_width <= holes_diameter: raise ValueError("The bar width must be greater than the holes diameter.") if holes_distance <= 2 * holes_diameter: raise ValueError( "The distance between the holes must be greater " "than twice the holes diameter." ) # Create the main bar as a box bar = trimesh_box(extents=[holes_distance, bar_width, bar_height]) # Define the positions of the holes (relative to the center of the bar) hole_position_1 = [-holes_distance / 2, 0, 0] hole_position_2 = [holes_distance / 2, 0, 0] # Create cylindrical shapes at the ends of the bar rounding_1 = trimesh_cylinder(radius=bar_width / 2, height=bar_height) rounding_1.apply_translation(hole_position_1) rounding_2 = trimesh_cylinder(radius=bar_width / 2, height=bar_height) rounding_2.apply_translation(hole_position_2) # Use boolean union to combine the bar and the roundings bar = bar.union([rounding_1, rounding_2]) # Create cylindrical holes hole_1 = trimesh_cylinder(radius=holes_diameter / 2, height=bar_height) hole_1.apply_translation(hole_position_1) hole_2 = trimesh_cylinder(radius=holes_diameter / 2, height=bar_height) hole_2.apply_translation(hole_position_2) # Use boolean subtraction to create holes in the bar bar_mesh = bar.difference([hole_1, hole_2]) # Export to STL bar_mesh.export(filename) return bar_mesh
[docs] @doc_category("Other") def generate_stl_bars( self, scale: float = 1.0, width_of_bars: float = 8.0, height_of_bars: float = 3.0, holes_diameter: float = 4.3, filename_prefix: str = "bar_", output_dir: str = "stl_output", ) -> None: """ Generate STL files for the bars of the framework. Generates STL files for the bars of the framework. The files are generated in the working folder. The naming convention for the files is ``bar_i-j.stl``, where i and j are the vertices of an edge. Parameters ---------- scale Scale factor for the lengths of the edges, default is 1.0. width_of_bars Width of the bars, default is 8.0 mm. height_of_bars Height of the bars, default is 3.0 mm. holes_diameter Diameter of the holes at the ends of the bars, default is 4.3 mm. filename_prefix Prefix for the filenames of the generated STL files, default is ``bar_``. output_dir Name or path of the folder where the STL files are saved, default is ``stl_output``. Relative to the working directory. Examples -------- >>> G = Graph([(0,1), (1,2), (2,3), (0,3)]) >>> F = Framework(G, {0:[0,0], 1:[1,0], 2:[1,'1/2 * sqrt(5)'], 3:[1/2,'4/3']}) >>> F.generate_stl_bars(scale=20) STL files for the bars have been generated in the chosen folder. """ from pathlib import Path as plPath # Create the folder if it does not exist folder_path = plPath(output_dir) if not folder_path.exists(): folder_path.mkdir(parents=True, exist_ok=True) edges_with_lengths = self.edge_lengths() for edge, length in edges_with_lengths.items(): scaled_length = length * scale f_name = ( output_dir + "/" + filename_prefix + str(edge[0]) + "-" + str(edge[1]) + ".stl" ) self._generate_stl_bar( holes_distance=scaled_length, holes_diameter=holes_diameter, bar_width=width_of_bars, bar_height=height_of_bars, filename=f_name, ) print("STL files for the bars have been generated in the chosen folder.")
@doc_category("Other") def _transform_inf_flex_to_pointwise( # noqa: C901 self, inf_flex: Matrix, vertex_order: List[Vertex] = None ) -> Dict[Vertex, Sequence[Coordinate]]: r""" Transform the natural data type of a flex (Matrix) to a dictionary that maps a vertex to a Sequence of coordinates (i.e. a vector). Parameters ---------- inf_flex: An infinitesimal flex in the form of a `Matrix`. vertex_order: If ``None``, the :meth:`.Graph.vertex_list` is taken as the vertex order. Notes ---- For example, this method can be used for generating an infinitesimal flex for plotting purposes. Examples ---- >>> F = Framework.from_points([(0,0), (1,0), (0,1)]) >>> F.add_edges([(0,1),(0,2)]) >>> flex = F.nontrivial_inf_flexes()[0] >>> F._transform_inf_flex_to_pointwise(flex) {0: [1, 0], 1: [1, 0], 2: [0, 0]} """ vertex_order = self._check_vertex_order(vertex_order) return { vertex_order[i]: [inf_flex[i * self.dim() + j] for j in range(self.dim())] for i in range(len(vertex_order)) }
[docs] @doc_category("Infinitesimal rigidity") def is_vector_inf_flex( self, inf_flex: List[Coordinate], vertex_order: List[Vertex] = None, numerical: bool = False, tolerance: float = 1e-9, ) -> bool: r""" Return whether a vector is an infinitesimal flex of the framework. Definitions ----------- :prf:ref:`Infinitesimal Flex <def-inf-flex>` :prf:ref:`Rigidity Matrix <def-rigidity-matrix>` Parameters ---------- inf_flex: An infinitesimal flex of the framework specified by a vector. vertex_order: A list of vertices specifying the order in which ``inf_flex`` is given. If none is provided, the list from :meth:`~Graph.vertex_list` is taken. numerical: A Boolean determining whether the evaluation of the product of the `inf_flex` and the rigidity matrix is symbolic or numerical. tolerance: Absolute tolerance that is the threshold for acceptable numerical flexes. This parameter is used to determine the number of digits, to which accuracy the symbolic expressions are evaluated. Examples -------- >>> from pyrigi import frameworkDB as fws >>> F = fws.Square() >>> q = [0,0,0,0,-2,0,-2,0] >>> F.is_vector_inf_flex(q) True >>> q[0] = 1 >>> F.is_vector_inf_flex(q) False >>> F = Framework.Complete([[0,0], [1,1]]) >>> F.is_vector_inf_flex(["sqrt(2)","-sqrt(2)",0,0], vertex_order=[1,0]) True """ vertex_order = self._check_vertex_order(vertex_order) return is_zero_vector( self.rigidity_matrix(vertex_order=vertex_order) * Matrix(inf_flex), numerical=numerical, tolerance=tolerance, )
[docs] @doc_category("Infinitesimal rigidity") def is_dict_inf_flex( self, vert_to_flex: Dict[Vertex, Sequence[Coordinate]], **kwargs ) -> bool: """ Return whether a dictionary specifies an infinitesimal flex of the framework. Definitions ----------- :prf:ref:`Infinitesimal flex <def-inf-flex>` Parameters ---------- vert_to_flex: Dictionary that maps the vertex labels to vectors of the same dimension as the framework is. Notes ----- See :meth:`.Framework.is_vector_inf_flex`. Examples -------- >>> F = Framework.Complete([[0,0], [1,1]]) >>> F.is_dict_inf_flex({0:[0,0], 1:[-1,1]}) True >>> F.is_dict_inf_flex({0:[0,0], 1:["sqrt(2)","-sqrt(2)"]}) True """ self._check_vertex_order(list(vert_to_flex.keys())) dict_to_list = [] for v in self._graph.vertex_list(): if v not in vert_to_flex: raise ValueError( f"Vertex {v} must be in the dictionary `vert_to_flex`." ) dict_to_list += list(vert_to_flex[v]) return self.is_vector_inf_flex( dict_to_list, vertex_order=self._graph.vertex_list(), **kwargs )
[docs] @doc_category("Infinitesimal rigidity") def is_vector_nontrivial_inf_flex( self, inf_flex: List[Coordinate], vertex_order: List[Vertex] = None, numerical: bool = False, tolerance: float = 1e-9, ) -> bool: r""" Return whether an infinitesimal flex is nontrivial. Definitions ----------- :prf:ref:`Nontrivial infinitesimal Flex <def-trivial-inf-flex>` Parameters ---------- inf_flex: An infinitesimal flex of the framework. vertex_order: A list of vertices specifying the order in which ``inf_flex`` is given. If none is provided, the list from :meth:`~Graph.vertex_list` is taken. numerical: A Boolean determining whether the evaluation of the product of the `inf_flex` and the rigidity matrix is symbolic or numerical. tolerance: Absolute tolerance that is the threshold for acceptable numerical flexes. This parameter is used to determine the number of digits, to which accuracy the symbolic expressions are evaluated. Notes ----- This is done by solving a linear system composed of a matrix `A` whose columns are given by a basis of the trivial flexes and the vector `b` given by the input flex. `b` is trivial if and only if there is a linear combination of the columns in `A` producing `b`. In other words, when there is a solution to `Ax=b`, then `b` is a trivial infinitesimal motion. Otherwise, `b` is nontrivial. In the `numerical=True` case we compute a least squares solution `x` of the overdetermined linear system and compare the values in `Ax` to the values in `b`. Examples -------- >>> from pyrigi import frameworkDB as fws >>> F = fws.Square() >>> q = [0,0,0,0,-2,0,-2,0] >>> F.is_vector_nontrivial_inf_flex(q) True >>> q = [1,-1,1,1,-1,1,-1,-1] >>> F.is_vector_inf_flex(q) True >>> F.is_vector_nontrivial_inf_flex(q) False """ vertex_order = self._check_vertex_order(vertex_order) if not self.is_vector_inf_flex( inf_flex, vertex_order=vertex_order, numerical=numerical, tolerance=tolerance, ): return False if not numerical: Q_trivial = Matrix.hstack( *(self.trivial_inf_flexes(vertex_order=vertex_order)) ) system = Q_trivial, Matrix(inf_flex) return sp.linsolve(system) == sp.EmptySet else: Q_trivial = np.array( [ eval_sympy_vector(flex, tolerance=tolerance) for flex in self.trivial_inf_flexes(vertex_order=vertex_order) ] ).transpose() b = np.array(eval_sympy_vector(inf_flex, tolerance=tolerance)).transpose() x = np.linalg.lstsq(Q_trivial, b, rcond=None)[0] return not is_zero_vector( np.dot(Q_trivial, x) - b, numerical=True, tolerance=tolerance )
[docs] @doc_category("Infinitesimal rigidity") def is_dict_nontrivial_inf_flex( self, vert_to_flex: Dict[Vertex, Sequence[Coordinate]], **kwargs ) -> bool: r""" Return whether a dictionary specifies an infinitesimal flex which is nontrivial. Definitions ----------- :prf:ref:`Nontrivial infinitesimal Flex <def-trivial-inf-flex>` Parameters ---------- inf_flex: An infinitesimal flex of the framework in the form of a dictionary. Notes ----- See :meth:`Framework.is_vector_nontrivial_inf_flex` for details, particularly concerning the possible parameters. Examples -------- >>> from pyrigi import frameworkDB as fws >>> F = fws.Square() >>> q = {0:[0,0], 1: [0,0], 2:[-2,0], 3:[-2,0]} >>> F.is_dict_nontrivial_inf_flex(q) True >>> q = {0:[1,-1], 1: [1,1], 2:[-1,1], 3:[-1,-1]} >>> F.is_dict_nontrivial_inf_flex(q) False """ self._check_vertex_order(list(vert_to_flex.keys())) dict_to_list = [] for v in self._graph.vertex_list(): if v not in vert_to_flex: raise ValueError( f"Vertex {v} must be in the dictionary `vert_to_flex`." ) dict_to_list += list(vert_to_flex[v]) return self.is_vector_nontrivial_inf_flex( dict_to_list, vertex_order=self._graph.vertex_list(), **kwargs )
[docs] @doc_category("Infinitesimal rigidity") def is_nontrivial_flex( self, inf_flex: List[Coordinate] | Dict[Vertex, Sequence[Coordinate]], **kwargs ) -> bool: """ Alias for :meth:`Framework.is_vector_nontrivial_inf_flex` and :meth:`Framework.is_dict_nontrivial_inf_flex`. Notes ----- We distinguish between instances of ``list`` and instances of ``dict`` to call one of the alias methods. """ if isinstance(inf_flex, list): return self.is_vector_nontrivial_inf_flex(inf_flex, **kwargs) elif isinstance(inf_flex, dict): return self.is_dict_nontrivial_inf_flex(inf_flex, **kwargs) else: raise TypeError( "The `inf_flex` must be specified either by a vector or a dictionary!" )
[docs] @doc_category("Infinitesimal rigidity") def is_vector_trivial_inf_flex(self, inf_flex: List[Coordinate], **kwargs) -> bool: r""" Return whether an infinitesimal flex is trivial. Definitions ----------- :prf:ref:`Trivial infinitesimal Flex <def-trivial-inf-flex>` Parameters ---------- inf_flex: An infinitesimal flex of the framework. Notes ----- See :meth:`Framework.is_nontrivial_vector_inf_flex` for details, particularly concerning the possible parameters. Examples -------- >>> from pyrigi import frameworkDB as fws >>> F = fws.Square() >>> q = [0,0,0,0,-2,0,-2,0] >>> F.is_vector_trivial_inf_flex(q) False >>> q = [1,-1,1,1,-1,1,-1,-1] >>> F.is_vector_trivial_inf_flex(q) True """ if not self.is_vector_inf_flex(inf_flex, **kwargs): return False return not self.is_vector_nontrivial_inf_flex(inf_flex, **kwargs)
[docs] @doc_category("Infinitesimal rigidity") def is_dict_trivial_inf_flex( self, vert_to_flex: Dict[Vertex, Sequence[Coordinate]], **kwargs ) -> bool: r""" Return whether an infinitesimal flex specified by a dictionary is trivial. Definitions ----------- :prf:ref:`Trivial infinitesimal flex <def-trivial-inf-flex>` Parameters ---------- inf_flex: An infinitesimal flex of the framework in the form of a dictionary. Notes ----- See :meth:`Framework.is_vector_trivial_inf_flex` for details, particularly concerning the possible parameters. Examples -------- >>> from pyrigi import frameworkDB as fws >>> F = fws.Square() >>> q = {0:[0,0], 1: [0,0], 2:[-2,0], 3:[-2,0]} >>> F.is_dict_trivial_inf_flex(q) False >>> q = {0:[1,-1], 1: [1,1], 2:[-1,1], 3:[-1,-1]} >>> F.is_dict_trivial_inf_flex(q) True """ self._check_vertex_order(list(vert_to_flex.keys())) dict_to_list = [] for v in self._graph.vertex_list(): if v not in vert_to_flex: raise ValueError( f"Vertex {v} must be in the dictionary `vert_to_flex`." ) dict_to_list += list(vert_to_flex[v]) return self.is_vector_trivial_inf_flex( dict_to_list, vertex_order=self._graph.vertex_list(), **kwargs )
[docs] @doc_category("Infinitesimal rigidity") def is_trivial_flex( self, inf_flex: List[Coordinate] | Dict[Vertex, Sequence[Coordinate]], **kwargs ) -> bool: """ Alias for :meth:`Framework.is_vector_trivial_inf_flex` and :meth:`Framework.is_dict_trivial_inf_flex`. Notes ----- We distinguish between instances of ``list`` and instances of ``dict`` to call one of the alias methods. """ if isinstance(inf_flex, list): return self.is_vector_trivial_inf_flex(inf_flex, **kwargs) elif isinstance(inf_flex, dict): return self.is_dict_trivial_inf_flex(inf_flex, **kwargs) else: raise TypeError( "The `inf_flex` must be specified either by a vector or a dictionary!" )
@doc_category("Other") def _check_vertex_order(self, vertex_order=List[Vertex]) -> List[Vertex]: """ Checks whether the provided `vertex_order` contains the same elements as the graph's vertex set. Parameters ---------- vertex_order: List of vertices in the preferred order Notes ----- Throws an error if the vertices in `vertex_order` do not agree with the underlying graphs's vertices. """ if vertex_order is None: return self._graph.vertex_list() else: if not self._graph.number_of_nodes() == len(vertex_order) or not set( self._graph.vertex_list() ) == set(vertex_order): raise ValueError( "New vertex set must contain exactly " + "the same vertices as the underlying graph!" ) return vertex_order @doc_category("Other") def _check_edge_order(self, edge_order=List[Vertex]) -> List[Edge]: """ Checks whether the provided `edge_order` contains the same elements as the graph's edge set. Parameters ---------- edge_order: List of edges in the preferred order Notes ----- Throws an error if the edges in `edge_order` do not agree with the underlying graphs's edges. """ if edge_order is None: return self._graph.edge_list() else: if not self._graph.number_of_edges() == len(edge_order) or not all( [ set(e) in [set(e) for e in edge_order] for e in self._graph.edge_list() ] ): raise ValueError( "edge_order must contain exactly the same edges as the graph!" ) return edge_order
Framework.__doc__ = Framework.__doc__.replace( "METHODS", generate_category_tables( Framework, 1, [ "Attribute getters", "Framework properties", "Class methods", "Framework manipulation", "Infinitesimal rigidity", "Other", "Waiting for implementation", ], include_all=False, ), )