Source code for pyrigi.framework._rigidity.stress

"""
This module provides functionality related to stresses of frameworks.
"""

from __future__ import annotations

import numpy as np
import sympy as sp
from sympy import Matrix

import pyrigi.graph._utils._input_check as _graph_input_check
from pyrigi._utils._zero_check import is_zero_vector
from pyrigi._utils.linear_algebra import _null_space
from pyrigi.data_type import (
    Edge,
    Number,
    Sequence,
    Stress,
    Vertex,
)
from pyrigi.framework._rigidity import infinitesimal as infinitesimal_rigidity
from pyrigi.framework.base import FrameworkBase
from pyrigi.graph import _general as graph_general


[docs] def is_dict_stress( framework: FrameworkBase, dict_stress: dict[Edge, Number], **kwargs ) -> bool: """ Return whether a dictionary specifies an equilibrium stress of the framework. Definitions ----------- :prf:ref:`Equilibrium Stress <def-equilibrium-stress>` Parameters ---------- framework: dict_stress: Dictionary that maps the edges to stress values. Examples -------- >>> F = Framework.Complete([[0,0], [1,0], ['1/2',0]]) >>> is_dict_stress(F, {(0,1):'-1/2', (0,2):1, (1,2):1}) True >>> is_dict_stress(F, {(0,1):1, (1,2):'-1/2', (0,2):1}) False Notes ----- See :func:`.is_vector_stress`. """ stress_edge_list = [tuple(e) for e in list(dict_stress.keys())] _graph_input_check.is_edge_order(framework._graph, stress_edge_list, "dict_stress") graph_edge_list = [tuple(e) for e in graph_general.edge_list(framework._graph)] dict_to_list = [] for e in graph_edge_list: dict_to_list += [ ( dict_stress[e] if e in stress_edge_list else dict_stress[tuple([e[1], e[0]])] ) ] return is_vector_stress( framework, dict_to_list, edge_order=graph_general.edge_list(framework._graph), **kwargs, )
[docs] def is_vector_stress( framework: FrameworkBase, stress: Sequence[Number], edge_order: Sequence[Edge] = None, numerical: bool = False, tolerance=1e-9, ) -> bool: r""" Return whether a vector is an equilibrium stress. Definitions ----------- :prf:ref:`Equilibrium stress <def-equilibrium-stress>` Parameters ---------- framework: 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] >>> is_stress(F, omega1) True >>> omega1[0] = 0 >>> is_stress(F, omega1) False >>> from pyrigi import frameworkDB >>> F = frameworkDB.Complete(5, dim=2) >>> stresses_F = stresses(F) >>> is_stress(F, stresses_F[0]) True """ framework._warn_numerical_coord(is_vector_stress, numerical) edge_order = _graph_input_check.is_edge_order( framework._graph, edge_order=edge_order ) return is_zero_vector( Matrix(stress).transpose() * infinitesimal_rigidity.rigidity_matrix(framework, edge_order=edge_order), numerical=numerical, tolerance=tolerance, )
[docs] def is_stress(framework: FrameworkBase, stress: Stress, **kwargs) -> bool: """ Alias for :func:`.is_vector_stress` and :func:`.is_dict_stress`. One of the alias methods is called depending on the type of the input. Parameters ---------- framework: stress """ if isinstance(stress, list | Matrix): return is_vector_stress(framework, stress, **kwargs) elif isinstance(stress, dict): return is_dict_stress(framework, stress, **kwargs) else: raise TypeError( "The `stress` must be specified either by a list/Matrix or a dictionary!" )
[docs] def stress_matrix( framework: FrameworkBase, stress: Stress, edge_order: Sequence[Edge] = None, vertex_order: Sequence[Vertex] = None, ) -> Matrix: r""" Construct the stress matrix of a stress. Definitions ----------- * :prf:ref:`Stress Matrix <def-stress-matrix>` Parameters ---------- framework: stress: A stress of the framework given as a vector. edge_order: A list of edges, providing the ordering of edges in ``stress``. If ``None``, :meth:`.Graph.edge_list` is assumed. vertex_order: Specification of row/column order of the stress matrix. If ``None``, :meth:`.Graph.vertex_list` is assumed. 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] >>> stress_matrix(F, omega) Matrix([ [-16, 8, 4, 4], [ 8, -4, -2, -2], [ 4, -2, -1, -1], [ 4, -2, -1, -1]]) """ vertex_order = _graph_input_check.is_vertex_order(framework._graph, vertex_order) edge_order = _graph_input_check.is_edge_order(framework._graph, edge_order) if not is_stress(framework, 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(framework._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] def stress_matrix_rank( framework: FrameworkBase, stress: Stress, edge_order: Sequence[Edge] = None, numerical: bool = False, tolerance: float = 1e-9, ) -> int: """ Return the rank of the stress matrix for a given stress. Definitions ----------- * :prf:ref:`Equilibrium stress <def-equilibrium-stress>` * :prf:ref:`Stress Matrix <def-stress-matrix>` Parameters ---------- framework: stress: A stress of the framework given as a vector. edge_order: A list of edges, providing the ordering of edges in ``stress``. If ``None``, :meth:`.Graph.edge_list` is assumed. numerical: If ``True``, the rank of the stress matrix with entries as floats is computed. *Warning:* For ``numerical=True`` the numerical rank computation may produce different results than the computation over exact coordinates. tolerance: Numerical tolerance used for computing the rank. Examples -------- >>> F = Framework.Complete([(0, 0), (0, 1), (-1, -1), (1, -1)]) >>> omega = [-8, -4, -4, 2, 2, 1] >>> is_stress(F, omega) True >>> stress_matrix(F, omega) Matrix([ [-16, 8, 4, 4], [ 8, -4, -2, -2], [ 4, -2, -1, -1], [ 4, -2, -1, -1]]) >>> stress_matrix_rank(F, omega) 1 """ framework._warn_numerical_coord(stress_matrix_rank, numerical) if numerical: return np.linalg.matrix_rank( np.array( stress_matrix(framework, stress=stress, edge_order=edge_order) ).astype(np.float64), tol=tolerance, ) return stress_matrix(framework, stress=stress, edge_order=edge_order).rank()
[docs] def stresses( framework: FrameworkBase, edge_order: Sequence[Edge] = None, numerical: bool = False, tolerance: float = 1e-9, ) -> list[Matrix] | list[list[float]]: r""" Return a basis of the space of equilibrium stresses. Definitions ----------- :prf:ref:`Equilibrium stress <def-equilibrium-stress>` Parameters ---------- framework: 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. numerical: Determines whether the output is symbolic (default) or numerical. tolerance: Used tolerance when computing the stresses numerically. 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) >>> stresses(F) [Matrix([ [-8], [-4], [-4], [ 2], [ 2], [ 1]])] """ framework._warn_numerical_coord(stresses, numerical) if not numerical: return ( infinitesimal_rigidity.rigidity_matrix(framework, edge_order=edge_order) .transpose() .nullspace() ) F = FrameworkBase( framework._graph, framework.realization(as_points=True, numerical=True) ) equilibrium_stresses = _null_space( np.array( infinitesimal_rigidity.rigidity_matrix(F, edge_order=edge_order).transpose() ).astype(np.float64), tolerance=tolerance, ) return [ list(equilibrium_stresses[:, i]) for i in range(equilibrium_stresses.shape[1]) ]
def _transform_stress_to_edgewise( framework: FrameworkBase, stress: Matrix, edge_order: Sequence[Edge] = None ) -> dict[Edge, Number]: r""" Transform the natural data type of a stress (``Matrix``) to a dictionary that maps an edge to a coordinate. Parameters ---------- framework: stress: An equilibrium stress in the form of a ``Matrix``. edge_order: If ``None``, the :meth:`.Graph.edge_list` is taken as the edge order. Examples ---- >>> F = Framework.Complete([(0,0),(1,0),(1,1),(0,1)]) >>> stress = stresses(F)[0] >>> from pyrigi.framework._rigidity.stress import _transform_stress_to_edgewise >>> _transform_stress_to_edgewise(F, stress) {(0, 1): 1, (0, 2): -1, (0, 3): 1, (1, 2): 1, (1, 3): -1, (2, 3): 1} Notes ---- For example, this function can be used for generating an equilibrium stress for plotting purposes. """ edge_order = _graph_input_check.is_edge_order(framework._graph, edge_order) return {tuple(edge_order[i]): stress[i] for i in range(len(edge_order))}