From 03cb168a6b814499d0dc0b0cc92c2d9204aacae1 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 15 Jan 2024 13:25:33 +0100 Subject: [PATCH 1/5] merge classes and update tests --- src/compas/datastructures/__init__.py | 2 - src/compas/datastructures/graph/__init__.py | 0 src/compas/datastructures/graph/graph.py | 1814 ---------------- src/compas/datastructures/network/network.py | 1964 ++++++++++++++++-- tests/compas/datastructures/test_graph.py | 164 -- tests/compas/datastructures/test_network.py | 115 +- 6 files changed, 1957 insertions(+), 2102 deletions(-) delete mode 100644 src/compas/datastructures/graph/__init__.py delete mode 100644 src/compas/datastructures/graph/graph.py delete mode 100644 tests/compas/datastructures/test_graph.py diff --git a/src/compas/datastructures/__init__.py b/src/compas/datastructures/__init__.py index 6afb1650bb7a..136e4fbb0e8b 100644 --- a/src/compas/datastructures/__init__.py +++ b/src/compas/datastructures/__init__.py @@ -55,7 +55,6 @@ # Class APIs # ============================================================================= -from .graph.graph import Graph from .network.network import Network from .halfedge.halfedge import HalfEdge from .mesh.mesh import Mesh @@ -69,7 +68,6 @@ __all__ = [ "Datastructure", - "Graph", "CellNetwork", "Network", "HalfEdge", diff --git a/src/compas/datastructures/graph/__init__.py b/src/compas/datastructures/graph/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/compas/datastructures/graph/graph.py b/src/compas/datastructures/graph/graph.py deleted file mode 100644 index 1d32f51771e1..000000000000 --- a/src/compas/datastructures/graph/graph.py +++ /dev/null @@ -1,1814 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -from random import sample -from ast import literal_eval -from itertools import combinations - -from compas.topology import breadth_first_traverse -from compas.topology import connected_components - -from compas.datastructures.datastructure import Datastructure -from compas.datastructures.attributes import NodeAttributeView -from compas.datastructures.attributes import EdgeAttributeView - - -class Graph(Datastructure): - """Base graph data structure for describing the topological relationships between nodes connected by edges. - - Parameters - ---------- - default_node_attributes : dict[str, Any], optional - Default values for node attributes. - default_edge_attributes : dict[str, Any], optional - Default values for edge attributes. - **kwargs : dict, optional - Additional keyword arguments are passed to the base class, and will be stored in the :attr:`attributes` attribute. - - Attributes - ---------- - default_node_attributes : dict[str, Any] - dictionary containing default values for the attributes of nodes. - It is recommended to add a default to this dictionary using :meth:`update_default_node_attributes` - for every node attribute used in the data structure. - default_edge_attributes : dict[str, Any] - dictionary containing default values for the attributes of edges. - It is recommended to add a default to this dictionary using :meth:`update_default_edge_attributes` - for every edge attribute used in the data structure. - - See Also - -------- - :class:`compas.datastructures.Network` - - """ - - DATASCHEMA = { - "type": "object", - "properties": { - "dna": {"type": "object"}, - "dea": {"type": "object"}, - "node": { - "type": "object", - "additionalProperties": {"type": "object"}, - }, - "edge": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {"type": "object"}, - }, - }, - "max_node": {"type": "integer", "minimum": -1}, - }, - "required": [ - "dna", - "dea", - "node", - "edge", - "max_node", - ], - } - - def __init__(self, default_node_attributes=None, default_edge_attributes=None, **kwargs): - super(Graph, self).__init__(**kwargs) - self._max_node = -1 - self.node = {} - self.edge = {} - self.adjacency = {} - self.default_node_attributes = {} - self.default_edge_attributes = {} - if default_node_attributes: - self.default_node_attributes.update(default_node_attributes) - if default_edge_attributes: - self.default_edge_attributes.update(default_edge_attributes) - - def __str__(self): - tpl = "" - return tpl.format(self.number_of_nodes(), self.number_of_edges()) - - # -------------------------------------------------------------------------- - # Data - # -------------------------------------------------------------------------- - - @property - def data(self): - data = { - "dna": self.default_node_attributes, - "dea": self.default_edge_attributes, - "node": {}, - "edge": {}, - "max_node": self._max_node, - } - for key in self.node: - data["node"][repr(key)] = self.node[key] - for u in self.edge: - ru = repr(u) - data["edge"][ru] = {} - for v in self.edge[u]: - rv = repr(v) - data["edge"][ru][rv] = self.edge[u][v] - return data - - @classmethod - def from_data(cls, data): - dna = data.get("dna") or {} - dea = data.get("dea") or {} - node = data.get("node") or {} - edge = data.get("edge") or {} - - graph = cls(default_node_attributes=dna, default_edge_attributes=dea) - - for node, attr in iter(node.items()): - node = literal_eval(node) - graph.add_node(key=node, attr_dict=attr) - - for u, nbrs in iter(edge.items()): - u = literal_eval(u) - for v, attr in iter(nbrs.items()): - v = literal_eval(v) - graph.add_edge(u, v, attr_dict=attr) - - graph._max_node = data.get("max_node", graph._max_node) - - return graph - - # -------------------------------------------------------------------------- - # Properties - # -------------------------------------------------------------------------- - - # -------------------------------------------------------------------------- - # Constructors - # -------------------------------------------------------------------------- - - @classmethod - def from_edges(cls, edges): - """Create a new graph instance from information about the edges. - - Parameters - ---------- - edges : list[tuple[hashable, hashable]] - The edges of the graph as pairs of node identifiers. - - Returns - ------- - :class:`compas.datastructures.Graph` - - See Also - -------- - :meth:`from_networkx` - - """ - graph = cls() - for u, v in edges: - if u not in graph.node: - graph.add_node(u) - if v not in graph.node: - graph.add_node(v) - graph.add_edge(u, v) - - @classmethod - def from_networkx(cls, graph): - """Create a new graph instance from a NetworkX DiGraph instance. - - Parameters - ---------- - graph : networkx.DiGraph - NetworkX instance of a directed graph. - - Returns - ------- - :class:`compas.datastructures.Graph` - - See Also - -------- - :meth:`to_networkx` - :meth:`from_edges` - - """ - g = cls() - g.attributes.update(graph.graph) - - for node in graph.nodes(): - g.add_node(node, **graph.nodes[node]) - - for edge in graph.edges(): - g.add_edge(*edge, **graph.edges[edge]) - - return g - - def to_networkx(self): - """Create a new NetworkX graph instance from a graph. - - Returns - ------- - networkx.DiGraph - A newly created NetworkX DiGraph. - - See Also - -------- - :meth:`from_networkx` - - """ - import networkx as nx - - G = nx.DiGraph() - G.graph.update(self.attributes) # type: ignore - - for node, attr in self.nodes(data=True): - G.add_node(node, **attr) # type: ignore - - for edge, attr in self.edges(data=True): - G.add_edge(*edge, **attr) - - return G - - # -------------------------------------------------------------------------- - # Helpers - # -------------------------------------------------------------------------- - - def clear(self): - """Clear all the network data. - - Returns - ------- - None - - """ - del self.node - del self.edge - del self.adjacency - self.node = {} - self.edge = {} - self.adjacency = {} - - def node_sample(self, size=1): - """Get a list of identifiers of a random set of n nodes. - - Parameters - ---------- - size : int, optional - The size of the sample. - - Returns - ------- - list[hashable] - The identifiers of the nodes. - - See Also - -------- - :meth:`edge_sample` - - """ - return sample(list(self.nodes()), size) - - def edge_sample(self, size=1): - """Get the identifiers of a set of random edges. - - Parameters - ---------- - size : int, optional - The size of the sample. - - Returns - ------- - list[tuple[hashable, hashable]] - The identifiers of the random edges. - - See Also - -------- - :meth:`node_sample` - - """ - return sample(list(self.edges()), size) - - def node_index(self): - """Returns a dictionary that maps node identifiers to their corresponding index in a node list or array. - - Returns - ------- - dict[hashable, int] - A dictionary of node-index pairs. - - See Also - -------- - :meth:`index_node` - :meth:`edge_index` - - """ - return {key: index for index, key in enumerate(self.nodes())} - - def index_node(self): - """Returns a dictionary that maps the indices of a node list to keys in a node dictionary. - - Returns - ------- - dict[int, hashable] - A dictionary of index-node pairs. - - See Also - -------- - :meth:`node_index` - :meth:`index_edge` - - """ - return dict(enumerate(self.nodes())) - - def edge_index(self): - """Returns a dictionary that maps edge identifiers (i.e. pairs of vertex identifiers) - to the corresponding edge index in a list or array of edges. - - Returns - ------- - dict[tuple[hashable, hashable], int] - A dictionary of uv-index pairs. - - See Also - -------- - :meth:`index_edge` - :meth:`node_index` - - """ - return {(u, v): index for index, (u, v) in enumerate(self.edges())} - - def index_edge(self): - """Returns a dictionary that maps edges in a list to the corresponding - vertex identifier pairs. - - Returns - ------- - dict[int, tuple[hashable, hashable]] - A dictionary of index-uv pairs. - - See Also - -------- - :meth:`edge_index` - :meth:`index_node` - - """ - return dict(enumerate(self.edges())) - - # -------------------------------------------------------------------------- - # Builders - # -------------------------------------------------------------------------- - - def add_node(self, key=None, attr_dict=None, **kwattr): - """Add a node and specify its attributes (optional). - - Parameters - ---------- - key : hashable, optional - An identifier for the node. - Defaults to None, in which case an identifier of type int is automatically generated. - attr_dict : dict[str, Any], optional - A dictionary of vertex attributes. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - hashable - The identifier of the node. - - See Also - -------- - :meth:`add_edge` - :meth:`delete_node` - - Notes - ----- - If no key is provided for the node, one is generated - automatically. An automatically generated key increments the highest - integer key in use by 1. - - Examples - -------- - >>> graph = Graph() - >>> node = graph.add_node() - >>> node - 0 - - """ - if key is None: - key = self._max_node = self._max_node + 1 - try: - if key > self._max_node: - self._max_node = key - except (ValueError, TypeError): - pass - - if key not in self.node: - self.node[key] = {} - self.edge[key] = {} - self.adjacency[key] = {} - attr = attr_dict or {} - attr.update(kwattr) - self.node[key].update(attr) - return key - - def add_edge(self, u, v, attr_dict=None, **kwattr): - """Add an edge and specify its attributes. - - Parameters - ---------- - u : hashable - The identifier of the first node of the edge. - v : hashable - The identifier of the second node of the edge. - attr_dict : dict[str, Any], optional - A dictionary of edge attributes. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - tuple[hashable, hashable] - The identifier of the edge. - - See Also - -------- - :meth:`add_node` - :meth:`delete_edge` - - Examples - -------- - >>> - - """ - attr = attr_dict or {} - attr.update(kwattr) - if u not in self.node: - u = self.add_node(u) - if v not in self.node: - v = self.add_node(v) - data = self.edge[u].get(v, {}) - data.update(attr) - self.edge[u][v] = data - if v not in self.adjacency[u]: - self.adjacency[u][v] = None - if u not in self.adjacency[v]: - self.adjacency[v][u] = None - return u, v - - # -------------------------------------------------------------------------- - # Modifiers - # -------------------------------------------------------------------------- - - def delete_node(self, key): - """Delete a node from the graph. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - None - - See Also - -------- - :meth:`delete_edge` - :meth:`add_node` - - Examples - -------- - >>> - - """ - if key in self.edge: - del self.edge[key] - if key in self.adjacency: - del self.adjacency[key] - if key in self.node: - del self.node[key] - for u in list(self.edge): - for v in list(self.edge[u]): - if v == key: - del self.edge[u][v] - for u in self.adjacency: - for v in list(self.adjacency[u]): - if v == key: - del self.adjacency[u][v] - - def delete_edge(self, edge): - """Delete an edge from the network. - - Parameters - ---------- - edge : tuple[hashable, hashable] - The identifier of the edge as a pair of node identifiers. - - Returns - ------- - None - - See Also - -------- - :meth:`delete_node` - :meth:`add_edge` - - Examples - -------- - >>> - - """ - u, v = edge - - if u in self.edge and v in self.edge[u]: - del self.edge[u][v] - - if u == v: # invalid edge - del self.adjacency[u][v] - elif v not in self.edge or u not in self.edge[v]: - del self.adjacency[u][v] - del self.adjacency[v][u] - # else: an edge in an opposite direction exists, we don't want to delete the adjacency - - # -------------------------------------------------------------------------- - # Info - # -------------------------------------------------------------------------- - - def summary(self): - """Return a summary of the graph. - - Returns - ------- - str - The formatted summary. - - """ - tpl = "\n".join( - [ - "{} summary", - "=" * (len(self.name) + len(" summary")), - "- nodes: {}", - "- edges: {}", - ] - ) - return tpl.format(self.name, self.number_of_nodes(), self.number_of_edges()) - - def number_of_nodes(self): - """Compute the number of nodes of the graph. - - Returns - ------- - int - The number of nodes. - - See Also - -------- - :meth:`number_of_edges` - - """ - return len(list(self.nodes())) - - def number_of_edges(self): - """Compute the number of edges of the graph. - - Returns - ------- - int - The number of edges. - - See Also - -------- - :meth:`number_of_nodes` - - """ - return len(list(self.edges())) - - def is_connected(self): - """Verify that the network is connected. - - - Returns - ------- - bool - True, if the network is connected. - False, otherwise. - - Notes - ----- - A network is connected if for every two vertices a path exists connecting them. - - Examples - -------- - >>> import compas - >>> from compas.datastructures import Network - >>> network = Network.from_obj(compas.get('lines.obj')) - >>> network.is_connected() - True - - """ - if self.number_of_nodes() == 0: - return False - nodes = breadth_first_traverse(self.adjacency, self.node_sample(size=1)[0]) - return len(nodes) == self.number_of_nodes() - - # -------------------------------------------------------------------------- - # Accessors - # -------------------------------------------------------------------------- - - def nodes(self, data=False): - """Iterate over the nodes of the network. - - Parameters - ---------- - data : bool, optional - If True, yield the node attributes in addition to the node identifiers. - - Yields - ------ - hashable | tuple[hashable, dict[str, Any]] - If `data` is False, the next node identifier. - If `data` is True, the next node as a (key, attr) tuple. - - See Also - -------- - :meth:`nodes_where`, :meth:`nodes_where_predicate` - :meth:`edges`, :meth:`edges_where`, :meth:`edges_where_predicate` - - """ - for key in self.node: - if not data: - yield key - else: - yield key, self.node_attributes(key) - - def nodes_where(self, conditions=None, data=False, **kwargs): - """Get nodes for which a certain condition or set of conditions is true. - - Parameters - ---------- - conditions : dict, optional - A set of conditions in the form of key-value pairs. - The keys should be attribute names. The values can be attribute - values or ranges of attribute values in the form of min/max pairs. - data : bool, optional - If True, yield the node attributes in addition to the node identifiers. - - Yields - ------ - hashable | tuple[hashable, dict[str, Any]] - If `data` is False, the next node that matches the condition. - If `data` is True, the next node and its attributes. - - See Also - -------- - :meth:`nodes`, :meth:`nodes_where_predicate` - :meth:`edges`, :meth:`edges_where`, :meth:`edges_where_predicate` - - """ - conditions = conditions or {} - conditions.update(kwargs) - - for key, attr in self.nodes(True): - is_match = True - attr = attr or {} - - for name, value in conditions.items(): - method = getattr(self, name, None) - - if callable(method): - val = method(key) - if isinstance(val, list): - if value not in val: - is_match = False - break - break - if isinstance(value, (tuple, list)): - minval, maxval = value - if val < minval or val > maxval: - is_match = False - break - else: - if value != val: - is_match = False - break - - else: - if name not in attr: - is_match = False - break - if isinstance(attr[name], list): - if value not in attr[name]: - is_match = False - break - break - if isinstance(value, (tuple, list)): - minval, maxval = value - if attr[name] < minval or attr[name] > maxval: - is_match = False - break - else: - if value != attr[name]: - is_match = False - break - - if is_match: - if data: - yield key, attr - else: - yield key - - def nodes_where_predicate(self, predicate, data=False): - """Get nodes for which a certain condition or set of conditions is true using a lambda function. - - Parameters - ---------- - predicate : callable - The condition you want to evaluate. - The callable takes 2 parameters: the node identifier and the node attributes, and should return True or False. - data : bool, optional - If True, yield the node attributes in addition to the node identifiers. - - Yields - ------ - hashable | tuple[hashable, dict[str, Any]] - If `data` is False, the next node that matches the condition. - If `data` is True, the next node and its attributes. - - See Also - -------- - :meth:`nodes`, :meth:`nodes_where` - :meth:`edges`, :meth:`edges_where`, :meth:`edges_where_predicate` - - Examples - -------- - >>> - - """ - for key, attr in self.nodes(True): - if predicate(key, attr): - if data: - yield key, attr - else: - yield key - - def edges(self, data=False): - """Iterate over the edges of the network. - - Parameters - ---------- - data : bool, optional - If True, yield the edge attributes in addition to the edge identifiers. - - Yields - ------ - tuple[hashable, hashable] | tuple[tuple[hashable, hashable], dict[str, Any]] - If `data` is False, the next edge identifier (u, v). - If `data` is True, the next edge identifier and its attributes as a ((u, v), attr) tuple. - - See Also - -------- - :meth:`edges_where`, :meth:`edges_where_predicate` - :meth:`nodes`, :meth:`nodes_where`, :meth:`nodes_where_predicate` - - """ - for u, nbrs in iter(self.edge.items()): - for v, attr in iter(nbrs.items()): - if data: - yield (u, v), attr - else: - yield u, v - - def edges_where(self, conditions=None, data=False, **kwargs): - """Get edges for which a certain condition or set of conditions is true. - - Parameters - ---------- - conditions : dict, optional - A set of conditions in the form of key-value pairs. - The keys should be attribute names. The values can be attribute - values or ranges of attribute values in the form of min/max pairs. - data : bool, optional - If True, yield the edge attributes in addition to the edge identifiers. - **kwargs : dict[str, Any], optional - Additional conditions provided as named function arguments. - - Yields - ------ - tuple[hashable, hashable] | tuple[tuple[hashable, hashable], dict[str, Any]] - If `data` is False, the next edge identifier (u, v). - If `data` is True, the next edge identifier and its attributes as a ((u, v), attr) tuple. - - See Also - -------- - :meth:`edges`, :meth:`edges_where_predicate` - :meth:`nodes`, :meth:`nodes_where`, :meth:`nodes_where_predicate` - - """ - conditions = conditions or {} - conditions.update(kwargs) - - for key in self.edges(): - is_match = True - - attr = self.edge_attributes(key) or {} - - for name, value in conditions.items(): - method = getattr(self, name, None) - - if method and callable(method): - val = method(key) - elif name in attr: - val = attr[name] - else: - is_match = False - break - - if isinstance(val, list): - if value not in val: - is_match = False - break - elif isinstance(value, (tuple, list)): - minval, maxval = value - if val < minval or val > maxval: - is_match = False - break - else: - if value != val: - is_match = False - break - - if is_match: - if data: - yield key, attr - else: - yield key - - def edges_where_predicate(self, predicate, data=False): - """Get edges for which a certain condition or set of conditions is true using a lambda function. - - Parameters - ---------- - predicate : callable - The condition you want to evaluate. - The callable takes 2 parameters: - an edge identifier (tuple of node identifiers) and edge attributes, - and should return True or False. - data : bool, optional - If True, yield the edge attributes in addition to the edge attributes. - - Yields - ------ - tuple[hashable, hashable] | tuple[tuple[hashable, hashable], dict[str, Any]] - If `data` is False, the next edge identifier (u, v). - If `data` is True, the next edge identifier and its attributes as a ((u, v), attr) tuple. - - See Also - -------- - :meth:`edges`, :meth:`edges_where` - :meth:`nodes`, :meth:`nodes_where`, :meth:`nodes_where_predicate` - - Examples - -------- - >>> - - """ - for key, attr in self.edges(True): - if predicate(key, attr): - if data: - yield key, attr - else: - yield key - - # -------------------------------------------------------------------------- - # default attributes - # -------------------------------------------------------------------------- - - def update_default_node_attributes(self, attr_dict=None, **kwattr): - """Update the default node attributes. - - Parameters - ---------- - attr_dict : dict[str, Any], optional - A dictionary of attributes with their default values. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - None - - See Also - -------- - :meth:`update_default_edge_attributes` - - """ - if not attr_dict: - attr_dict = {} - attr_dict.update(kwattr) - self.default_node_attributes.update(attr_dict) - - def update_default_edge_attributes(self, attr_dict=None, **kwattr): - """Update the default edge attributes. - - Parameters - ---------- - attr_dict : dict[str, Any], optional - A dictionary of attributes with their default values. - **kwattr : dict[str, Any], optional - A dictionary of additional attributes compiled of remaining named arguments. - - Returns - ------- - None - - See Also - -------- - :meth:`update_default_node_attributes` - - """ - if not attr_dict: - attr_dict = {} - attr_dict.update(kwattr) - self.default_edge_attributes.update(attr_dict) - - update_dna = update_default_node_attributes - update_dea = update_default_edge_attributes - - # -------------------------------------------------------------------------- - # Node attributes - # -------------------------------------------------------------------------- - - def node_attribute(self, key, name, value=None): - """Get or set an attribute of a node. - - Parameters - ---------- - key : hashable - The node identifier. - name : str - The name of the attribute - value : obj, optional - The value of the attribute. - - Returns - ------- - obj or None - The value of the attribute, - or None when the function is used as a "setter". - - Raises - ------ - KeyError - If the node does not exist. - - See Also - -------- - :meth:`unset_node_attribute` - :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` - :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` - - """ - if key not in self.node: - raise KeyError(key) - if value is not None: - self.node[key][name] = value - return - if name in self.node[key]: - return self.node[key][name] - else: - if name in self.default_node_attributes: - return self.default_node_attributes[name] - - def unset_node_attribute(self, key, name): - """Unset the attribute of a node. - - Parameters - ---------- - key : int - The node identifier. - name : str - The name of the attribute. - - Raises - ------ - KeyError - If the node does not exist. - - See Also - -------- - :meth:`node_attribute` - - Notes - ----- - Unsetting the value of a node attribute implicitly sets it back to the value - stored in the default node attribute dict. - - """ - if name in self.node[key]: - del self.node[key][name] - - def node_attributes(self, key, names=None, values=None): - """Get or set multiple attributes of a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - names : list[str], optional - A list of attribute names. - values : list[Any], optional - A list of attribute values. - - Returns - ------- - dict[str, Any] | list[Any] | None - If the parameter `names` is empty, - the function returns a dictionary of all attribute name-value pairs of the node. - If the parameter `names` is not empty, - the function returns a list of the values corresponding to the requested attribute names. - The function returns None if it is used as a "setter". - - Raises - ------ - KeyError - If the node does not exist. - - See Also - -------- - :meth:`node_attribute`, :meth:`nodes_attribute`, :meth:`nodes_attributes` - :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` - - """ - if key not in self.node: - raise KeyError(key) - if names and values is not None: - # use it as a setter - for name, value in zip(names, values): - self.node[key][name] = value - return - # use it as a getter - if not names: - # return all node attributes as a dict - return NodeAttributeView(self.default_node_attributes, self.node[key]) - values = [] - for name in names: - if name in self.node[key]: - values.append(self.node[key][name]) - elif name in self.default_node_attributes: - values.append(self.default_node_attributes[name]) - else: - values.append(None) - return values - - def nodes_attribute(self, name, value=None, keys=None): - """Get or set an attribute of multiple nodes. - - Parameters - ---------- - name : str - The name of the attribute. - value : obj, optional - The value of the attribute. - keys : list[hashable], optional - A list of node identifiers. - - Returns - ------- - list[Any] | None - The value of the attribute for each node, - or None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the nodes does not exist. - - See Also - -------- - :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attributes` - :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` - - """ - if not keys: - keys = self.nodes() - if value is not None: - for key in keys: - self.node_attribute(key, name, value) - return - return [self.node_attribute(key, name) for key in keys] - - def nodes_attributes(self, names=None, values=None, keys=None): - """Get or set multiple attributes of multiple nodes. - - Parameters - ---------- - names : list[str], optional - The names of the attribute. - values : list[Any], optional - The values of the attributes. - keys : list[hashable], optional - A list of node identifiers. - - Returns - ------- - list[dict[str, Any]] | list[list[Any]] | None - If the parameter `names` is None, - the function returns a list containing an attribute dict per node. - If the parameter `names` is not None, - the function returns a list containing a list of attribute values per node corresponding to the provided attribute names. - The function returns None if it is used as a "setter". - - Raises - ------ - KeyError - If any of the nodes does not exist. - - See Also - -------- - :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute` - :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` - - """ - if not keys: - keys = self.nodes() - if values: - for key in keys: - self.node_attributes(key, names, values) - return - return [self.node_attributes(key, names) for key in keys] - - # -------------------------------------------------------------------------- - # Edge attributes - # -------------------------------------------------------------------------- - - def edge_attribute(self, key, name, value=None): - """Get or set an attribute of an edge. - - Parameters - ---------- - key : tuple[hashable, hashable] - The identifier of the edge as a pair of node identifiers. - name : str - The name of the attribute. - value : obj, optional - The value of the attribute. - - Returns - ------- - object | None - The value of the attribute, or None when the function is used as a "setter". - - Raises - ------ - KeyError - If the edge does not exist. - - See Also - -------- - :meth:`unset_edge_attribute` - :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` - :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` - - """ - u, v = key - if u not in self.edge or v not in self.edge[u]: - raise KeyError(key) - attr = self.edge[u][v] - if value is not None: - attr[name] = value - return - if name in attr: - return attr[name] - if name in self.default_edge_attributes: - return self.default_edge_attributes[name] - - def unset_edge_attribute(self, key, name): - """Unset the attribute of an edge. - - Parameters - ---------- - key : tuple[hashable, hashable] - The edge identifier. - name : str - The name of the attribute. - - Returns - ------- - None - - Raises - ------ - KeyError - If the edge does not exist. - - See Also - -------- - :meth:`edge_attribute` - - Notes - ----- - Unsetting the value of an edge attribute implicitly sets it back to the value - stored in the default edge attribute dict. - - """ - u, v = key - if u not in self.edge or v not in self.edge[u]: - raise KeyError(key) - attr = self.edge[u][v] - if name in attr: - del attr[name] - - def edge_attributes(self, key, names=None, values=None): - """Get or set multiple attributes of an edge. - - Parameters - ---------- - key : tuple[hashable, hashable] - The identifier of the edge. - names : list[str], optional - A list of attribute names. - values : list[Any], optional - A list of attribute values. - - Returns - ------- - dict[str, Any] | list[Any] | None - If the parameter `names` is empty, a dictionary of all attribute name-value pairs of the edge. - If the parameter `names` is not empty, a list of the values corresponding to the provided names. - None if the function is used as a "setter". - - Raises - ------ - KeyError - If the edge does not exist. - - See Also - -------- - :meth:`edge_attribute`, :meth:`edges_attribute`, :meth:`edges_attributes` - :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` - - """ - u, v = key - if u not in self.edge or v not in self.edge[u]: - raise KeyError(key) - if names and values: - # use it as a setter - for name, value in zip(names, values): - self.edge_attribute(key, name, value) - return - # use it as a getter - if not names: - # get the entire attribute dict - return EdgeAttributeView(self.default_edge_attributes, self.edge[u][v]) - # get only the values of the named attributes - values = [] - for name in names: - value = self.edge_attribute(key, name) - values.append(value) - return values - - def edges_attribute(self, name, value=None, keys=None): - """Get or set an attribute of multiple edges. - - Parameters - ---------- - name : str - The name of the attribute. - value : obj, optional - The value of the attribute. - keys : list[tuple[hashable, hashable]], optional - A list of edge identifiers. - - Returns - ------- - list[Any] | None - A list containing the value per edge of the requested attribute, - or None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the edges does not exist. - - See Also - -------- - :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attributes` - :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` - - """ - if not keys: - keys = self.edges() - if value is not None: - for key in keys: - self.edge_attribute(key, name, value) - return - return [self.edge_attribute(key, name) for key in keys] - - def edges_attributes(self, names=None, values=None, keys=None): - """Get or set multiple attributes of multiple edges. - - Parameters - ---------- - names : list[str], optional - The names of the attribute. - values : list[Any], optional - The values of the attributes. - keys : list[tuple[hashable, hashable]], optional - A list of edge identifiers. - - Returns - ------- - list[dict[str, Any]] | list[list[Any]] | None - If `names` is empty, - a list containing per edge an attribute dict with all attributes of the edge. - If `names` is not empty, - a list containing per edge a list of attribute values corresponding to the requested names. - None if the function is used as a "setter". - - Raises - ------ - KeyError - If any of the edges does not exist. - - See Also - -------- - :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute` - :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` - - """ - if not keys: - keys = self.edges() - if values: - for key in keys: - self.edge_attributes(key, names, values) - return - return [self.edge_attributes(key, names) for key in keys] - - # -------------------------------------------------------------------------- - # Node topology - # -------------------------------------------------------------------------- - - def has_node(self, key): - """Verify if a specific node is present in the network. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - bool - True or False. - - See Also - -------- - :meth:`has_edge` - - """ - return key in self.node - - def is_leaf(self, key): - """Verify if a node is a leaf. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - bool - True or False. - - See Also - -------- - :meth:`leaves` - :meth:`is_node_connected` - - Notes - ----- - A node is a *leaf* if it has only one neighbor. - - """ - return self.degree(key) == 1 - - def leaves(self): - """Return all leaves of the network. - - Returns - ------- - list[hashable] - A list of node identifiers. - - """ - return [key for key in self.nodes() if self.is_leaf(key)] - - def is_node_connected(self, key): - """Verify if a specific node is connected. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - bool - True or False. - - See Also - -------- - :meth:`is_leaf` - - """ - return self.degree(key) > 0 - - def neighbors(self, key): - """Return the neighbors of a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - list[hashable] - A list of node identifiers. - - See Also - -------- - :meth:`neighbors_out`, :meth:`neighbors_in` - :meth:`neighborhood` - - """ - return list(self.adjacency[key]) - - def neighborhood(self, key, ring=1): - """Return the nodes in the neighborhood of a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - ring : int, optional - The size of the neighborhood. - - Returns - ------- - list[hashable] - A list of node identifiers. - - See Also - -------- - :meth:`neighbors` - - """ - nbrs = set(self.neighbors(key)) - i = 1 - while True: - if i == ring: - break - temp = [] - for nbr in nbrs: - temp += self.neighbors(nbr) - nbrs.update(temp) - i += 1 - if key in nbrs: - nbrs.remove(key) - return list(nbrs) - - def neighbors_out(self, key): - """Return the outgoing neighbors of a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - list[hashable] - A list of node identifiers. - - See Also - -------- - :meth:`neighbors`, :meth:`neighbors_in` - - """ - return list(self.edge[key]) - - def neighbors_in(self, key): - """Return the incoming neighbors of a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - list[hashable] - A list of node identifiers. - - See Also - -------- - :meth:`neighbors`, :meth:`neighbors_out` - - """ - return list(set(self.adjacency[key]) - set(self.edge[key])) - - def degree(self, key): - """Return the number of neighbors of a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - int - The number of neighbors of the node. - - See Also - -------- - :meth:`degree_out`, :meth:`degree_in` - - """ - return len(self.neighbors(key)) - - def degree_out(self, key): - """Return the number of outgoing neighbors of a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - int - The number of outgoing neighbors of the node. - - See Also - -------- - :meth:`degree`, :meth:`degree_in` - - """ - return len(self.neighbors_out(key)) - - def degree_in(self, key): - """Return the numer of incoming neighbors of a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - int - The number of incoming neighbors of the node. - - See Also - -------- - :meth:`degree`, :meth:`degree_out` - - """ - return len(self.neighbors_in(key)) - - def node_edges(self, key): - """Return the edges connected to a node. - - Parameters - ---------- - key : hashable - The identifier of the node. - - Returns - ------- - list[tuple[hashable, hashable]] - The edges connected to the node. - - """ - edges = [] - for nbr in self.neighbors(key): - if nbr in self.edge[key]: - edges.append((key, nbr)) - else: - edges.append((nbr, key)) - return edges - - # -------------------------------------------------------------------------- - # Edge topology - # -------------------------------------------------------------------------- - - def has_edge(self, edge, directed=True): - """Verify if the network contains a specific edge. - - Parameters - ---------- - edge : tuple[hashable, hashable] - The identifier of the edge as a pair of node identifiers. - directed : bool, optional - If True, the direction of the edge is taken into account. - - Returns - ------- - bool - True if the edge is present, False otherwise. - - See Also - -------- - :meth:`has_node` - - """ - u, v = edge - if directed: - return u in self.edge and v in self.edge[u] - return (u in self.edge and v in self.edge[u]) or (v in self.edge and u in self.edge[v]) - - # -------------------------------------------------------------------------- - # Other Methods - # -------------------------------------------------------------------------- - - def connected_nodes(self): - """Get groups of connected nodes. - - Returns - ------- - list[list[hashable]] - - See Also - -------- - :meth:`connected_edges` - - """ - return connected_components(self.adjacency) - - def connected_edges(self): - """Get groups of connected edges. - - Returns - ------- - list[list[tuple[hashable, hashable]]] - - See Also - -------- - :meth:`connected_nodes` - - """ - return [[(u, v) for u in nodes for v in self.neighbors(u) if u < v] for nodes in self.connected_nodes()] - - def exploded(self): - """Explode the graph into its connected components. - - Returns - ------- - list[:class:`compas.datastructures.Graph`] - - """ - cls = type(self) - graphs = [] - for nodes in self.connected_nodes(): - edges = [(u, v) for u in nodes for v in self.neighbors(u) if u < v] - graph = cls( - default_node_attributes=self.default_node_attributes, - default_edge_attributes=self.default_edge_attributes, - ) - for node in nodes: - graph.add_node(node, attr_dict=self.node_attributes(node)) - for u, v in edges: - graph.add_edge(u, v, attr_dict=self.edge_attributes((u, v))) - graphs.append(graph) - return graphs - - def complement(self): - """Generate the complement of a graph. - - The complement of a graph G is the graph H with the same vertices - but whose edges consists of the edges not present in the graph G [1]_. - - Returns - ------- - :class:`compas.datastructures.Graph` - The complement graph. - - References - ---------- - .. [1] Wolfram MathWorld. *Graph complement*. - Available at: http://mathworld.wolfram.com/GraphComplement.html. - - Examples - -------- - >>> import compas - >>> from compas.datastructures import Network - >>> network = Network.from_obj(compas.get('lines.obj')) - >>> complement = network.complement() - >>> any(complement.has_edge(u, v, directed=False) for u, v in network.edges()) - False - - """ - cls = type(self) - - graph = cls( - default_node_attributes=self.default_node_attributes, - default_edge_attributes=self.default_edge_attributes, - ) - for node in self.nodes(): - graph.add_node(node, attr_dict=self.node_attributes(node)) - - for u, v in combinations(self.nodes(), 2): - if not self.has_edge((u, v), directed=False): - graph.add_edge(u, v, attr_dict=self.edge_attributes((u, v))) - - return graph - - # -------------------------------------------------------------------------- - # Matrices - # -------------------------------------------------------------------------- - - def adjacency_matrix(self, rtype="array"): - """Creates a node adjacency matrix from a Network datastructure. - - Parameters - ---------- - rtype : Literal['array', 'csc', 'csr', 'coo', 'list'], optional - Format of the result. - - Returns - ------- - array_like - Constructed adjacency matrix. - - """ - from compas.topology import adjacency_matrix - - node_index = self.node_index() - adjacency = [[node_index[nbr] for nbr in self.neighbors(key)] for key in self.nodes()] - return adjacency_matrix(adjacency, rtype=rtype) - - def connectivity_matrix(self, rtype="array"): - """Creates a connectivity matrix from a Network datastructure. - - Parameters - ---------- - rtype : Literal['array', 'csc', 'csr', 'coo', 'list'], optional - Format of the result. - - Returns - ------- - array_like - Constructed connectivity matrix. - - """ - from compas.topology import connectivity_matrix - - node_index = self.node_index() - edges = [(node_index[u], node_index[v]) for u, v in self.edges()] - return connectivity_matrix(edges, rtype=rtype) - - def degree_matrix(self, rtype="array"): - """Creates a degree matrix from a Network datastructure. - - Parameters - ---------- - rtype : Literal['array', 'csc', 'csr', 'coo', 'list'], optional - Format of the result. - - Returns - ------- - array_like - Constructed degree matrix. - - """ - from compas.topology import degree_matrix - - node_index = self.node_index() - adjacency = [[node_index[nbr] for nbr in self.neighbors(key)] for key in self.nodes()] - return degree_matrix(adjacency, rtype=rtype) - - def laplacian_matrix(self, normalize=False, rtype="array"): - """Creates a Laplacian matrix from a Network datastructure. - - Parameters - ---------- - normalize : bool, optional - If True, normalize the entries such that the value on the diagonal is 1. - rtype : Literal['array', 'csc', 'csr', 'coo', 'list'], optional - Format of the result. - - Returns - ------- - array_like - Constructed Laplacian matrix. - - Notes - ----- - ``d = L.dot(xyz)`` is currently a vector that points from the centroid to the node. - Therefore ``c = xyz - d``. By changing the signs in the laplacian, the dsiplacement - vectors could be used in a more natural way ``c = xyz + d``. - - """ - from compas.topology import laplacian_matrix - - node_index = self.node_index() - edges = [(node_index[u], node_index[v]) for u, v in self.edges()] - return laplacian_matrix(edges, normalize=normalize, rtype=rtype) diff --git a/src/compas/datastructures/network/network.py b/src/compas/datastructures/network/network.py index ab989d8f87b8..e89a4e2f7c29 100644 --- a/src/compas/datastructures/network/network.py +++ b/src/compas/datastructures/network/network.py @@ -2,6 +2,10 @@ from __future__ import absolute_import from __future__ import division +from random import sample +from ast import literal_eval +from itertools import combinations + import compas if compas.PY2: @@ -23,8 +27,12 @@ from compas.geometry import scale_vector from compas.geometry import transform_points from compas.topology import astar_shortest_path +from compas.topology import breadth_first_traverse +from compas.topology import connected_components -from compas.datastructures import Graph +from compas.datastructures.datastructure import Datastructure +from compas.datastructures.attributes import NodeAttributeView +from compas.datastructures.attributes import EdgeAttributeView from .operations.split import network_split_edge from .operations.join import network_join_edges @@ -40,8 +48,8 @@ from .duality import network_find_cycles -class Network(Graph): - """Geometric implementation of an edge graph. +class Network(Datastructure): + """Data structure for describing the relationships between nodes connected by edges. Parameters ---------- @@ -52,8 +60,46 @@ class Network(Graph): **kwargs : dict, optional Additional attributes to add to the network. + Attributes + ---------- + default_node_attributes : dict[str, Any] + dictionary containing default values for the attributes of nodes. + It is recommended to add a default to this dictionary using :meth:`update_default_node_attributes` + for every node attribute used in the data structure. + default_edge_attributes : dict[str, Any] + dictionary containing default values for the attributes of edges. + It is recommended to add a default to this dictionary using :meth:`update_default_edge_attributes` + for every edge attribute used in the data structure. + """ + DATASCHEMA = { + "type": "object", + "properties": { + "dna": {"type": "object"}, + "dea": {"type": "object"}, + "node": { + "type": "object", + "additionalProperties": {"type": "object"}, + }, + "edge": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {"type": "object"}, + }, + }, + "max_node": {"type": "integer", "minimum": -1}, + }, + "required": [ + "dna", + "dea", + "node", + "edge", + "max_node", + ], + } + split_edge = network_split_edge join_edges = network_join_edges smooth = network_smooth_centroid @@ -69,32 +115,128 @@ class Network(Graph): find_cycles = network_find_cycles def __init__(self, default_node_attributes=None, default_edge_attributes=None, **kwargs): - _default_node_attributes = {"x": 0.0, "y": 0.0, "z": 0.0} - _default_edge_attributes = {} + super(Network, self).__init__(**kwargs) + self._max_node = -1 + self.node = {} + self.edge = {} + self.adjacency = {} + self.default_node_attributes = {"x": 0.0, "y": 0.0, "z": 0.0} + self.default_edge_attributes = {} if default_node_attributes: - _default_node_attributes.update(default_node_attributes) + self.default_node_attributes.update(default_node_attributes) if default_edge_attributes: - _default_edge_attributes.update(default_edge_attributes) - super(Network, self).__init__( - default_node_attributes=_default_node_attributes, default_edge_attributes=_default_edge_attributes, **kwargs - ) + self.default_edge_attributes.update(default_edge_attributes) def __str__(self): tpl = "" return tpl.format(self.number_of_nodes(), self.number_of_edges()) # -------------------------------------------------------------------------- - # customisation + # Data # -------------------------------------------------------------------------- - # -------------------------------------------------------------------------- - # special properties - # -------------------------------------------------------------------------- + @property + def data(self): + data = { + "dna": self.default_node_attributes, + "dea": self.default_edge_attributes, + "node": {}, + "edge": {}, + "max_node": self._max_node, + } + for key in self.node: + data["node"][repr(key)] = self.node[key] + for u in self.edge: + ru = repr(u) + data["edge"][ru] = {} + for v in self.edge[u]: + rv = repr(v) + data["edge"][ru][rv] = self.edge[u][v] + return data + + @classmethod + def from_data(cls, data): + dna = data.get("dna") or {} + dea = data.get("dea") or {} + node = data.get("node") or {} + edge = data.get("edge") or {} + + graph = cls(default_node_attributes=dna, default_edge_attributes=dea) + + for node, attr in iter(node.items()): + node = literal_eval(node) + graph.add_node(key=node, attr_dict=attr) + + for u, nbrs in iter(edge.items()): + u = literal_eval(u) + for v, attr in iter(nbrs.items()): + v = literal_eval(v) + graph.add_edge(u, v, attr_dict=attr) + + graph._max_node = data.get("max_node", graph._max_node) + + return graph # -------------------------------------------------------------------------- - # constructors + # Constructors # -------------------------------------------------------------------------- + @classmethod + def from_edges(cls, edges): + """Create a new graph instance from information about the edges. + + Parameters + ---------- + edges : list[tuple[hashable, hashable]] + The edges of the graph as pairs of node identifiers. + + Returns + ------- + :class:`compas.datastructures.Graph` + + See Also + -------- + :meth:`from_networkx` + + """ + graph = cls() + for u, v in edges: + if u not in graph.node: + graph.add_node(u) + if v not in graph.node: + graph.add_node(v) + graph.add_edge(u, v) + + @classmethod + def from_networkx(cls, graph): + """Create a new graph instance from a NetworkX DiGraph instance. + + Parameters + ---------- + graph : networkx.DiGraph + NetworkX instance of a directed graph. + + Returns + ------- + :class:`compas.datastructures.Graph` + + See Also + -------- + :meth:`to_networkx` + :meth:`from_edges` + + """ + g = cls() + g.attributes.update(graph.graph) + + for node in graph.nodes(): + g.add_node(node, **graph.nodes[node]) + + for edge in graph.edges(): + g.add_edge(*edge, **graph.edges[edge]) + + return g + @classmethod def from_obj(cls, filepath, precision=None): """Construct a network from the data contained in an OBJ file. @@ -239,7 +381,7 @@ def from_pointcloud(cls, cloud, degree=3): return network # -------------------------------------------------------------------------- - # converters + # Converters # -------------------------------------------------------------------------- def to_obj(self): @@ -315,10 +457,157 @@ def to_nodes_and_edges(self): edges = [(key_index[u], key_index[v]) for u, v in self.edges()] return nodes, edges + def to_networkx(self): + """Create a new NetworkX graph instance from a graph. + + Returns + ------- + networkx.DiGraph + A newly created NetworkX DiGraph. + + See Also + -------- + :meth:`from_networkx` + + """ + import networkx as nx + + G = nx.DiGraph() + G.graph.update(self.attributes) # type: ignore + + for node, attr in self.nodes(data=True): + G.add_node(node, **attr) # type: ignore + + for edge, attr in self.edges(data=True): + G.add_edge(*edge, **attr) + + return G + # -------------------------------------------------------------------------- - # helpers + # Helpers # -------------------------------------------------------------------------- + def clear(self): + """Clear all the network data. + + Returns + ------- + None + + """ + del self.node + del self.edge + del self.adjacency + self.node = {} + self.edge = {} + self.adjacency = {} + + def node_sample(self, size=1): + """Get a list of identifiers of a random set of n nodes. + + Parameters + ---------- + size : int, optional + The size of the sample. + + Returns + ------- + list[hashable] + The identifiers of the nodes. + + See Also + -------- + :meth:`edge_sample` + + """ + return sample(list(self.nodes()), size) + + def edge_sample(self, size=1): + """Get the identifiers of a set of random edges. + + Parameters + ---------- + size : int, optional + The size of the sample. + + Returns + ------- + list[tuple[hashable, hashable]] + The identifiers of the random edges. + + See Also + -------- + :meth:`node_sample` + + """ + return sample(list(self.edges()), size) + + def node_index(self): + """Returns a dictionary that maps node identifiers to their corresponding index in a node list or array. + + Returns + ------- + dict[hashable, int] + A dictionary of node-index pairs. + + See Also + -------- + :meth:`index_node` + :meth:`edge_index` + + """ + return {key: index for index, key in enumerate(self.nodes())} + + def index_node(self): + """Returns a dictionary that maps the indices of a node list to keys in a node dictionary. + + Returns + ------- + dict[int, hashable] + A dictionary of index-node pairs. + + See Also + -------- + :meth:`node_index` + :meth:`index_edge` + + """ + return dict(enumerate(self.nodes())) + + def edge_index(self): + """Returns a dictionary that maps edge identifiers (i.e. pairs of vertex identifiers) + to the corresponding edge index in a list or array of edges. + + Returns + ------- + dict[tuple[hashable, hashable], int] + A dictionary of uv-index pairs. + + See Also + -------- + :meth:`index_edge` + :meth:`node_index` + + """ + return {(u, v): index for index, (u, v) in enumerate(self.edges())} + + def index_edge(self): + """Returns a dictionary that maps edges in a list to the corresponding + vertex identifier pairs. + + Returns + ------- + dict[int, tuple[hashable, hashable]] + A dictionary of index-uv pairs. + + See Also + -------- + :meth:`edge_index` + :meth:`index_node` + + """ + return dict(enumerate(self.edges())) + def node_gkey(self, precision=None): """Returns a dictionary that maps node identifiers to the corresponding *geometric key* up to a certain precision. @@ -370,191 +659,1436 @@ def gkey_node(self, precision=None): return {gkey(xyz(key), precision): key for key in self.nodes()} # -------------------------------------------------------------------------- - # builders + # Builders # -------------------------------------------------------------------------- - # -------------------------------------------------------------------------- - # modifiers - # -------------------------------------------------------------------------- + def add_node(self, key=None, attr_dict=None, **kwattr): + """Add a node and specify its attributes (optional). - # -------------------------------------------------------------------------- - # info - # -------------------------------------------------------------------------- + Parameters + ---------- + key : hashable, optional + An identifier for the node. + Defaults to None, in which case an identifier of type int is automatically generated. + attr_dict : dict[str, Any], optional + A dictionary of vertex attributes. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. - # -------------------------------------------------------------------------- - # accessors - # -------------------------------------------------------------------------- + Returns + ------- + hashable + The identifier of the node. - def shortest_path(self, u, v): - """Find the shortest path between two nodes using the A* algorithm. + See Also + -------- + :meth:`add_edge` + :meth:`delete_node` + + Notes + ----- + If no key is provided for the node, one is generated + automatically. An automatically generated key increments the highest + integer key in use by 1. + + Examples + -------- + >>> graph = Graph() + >>> node = graph.add_node() + >>> node + 0 + + """ + if key is None: + key = self._max_node = self._max_node + 1 + try: + if key > self._max_node: + self._max_node = key + except (ValueError, TypeError): + pass + + if key not in self.node: + self.node[key] = {} + self.edge[key] = {} + self.adjacency[key] = {} + attr = attr_dict or {} + attr.update(kwattr) + self.node[key].update(attr) + return key + + def add_edge(self, u, v, attr_dict=None, **kwattr): + """Add an edge and specify its attributes. Parameters ---------- u : hashable - The identifier of the start node. + The identifier of the first node of the edge. v : hashable - The identifier of the end node. + The identifier of the second node of the edge. + attr_dict : dict[str, Any], optional + A dictionary of edge attributes. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. Returns ------- - list[hashable] | None - The path from root to goal, or None, if no path exists between the vertices. + tuple[hashable, hashable] + The identifier of the edge. See Also -------- - :meth:`compas.topology.astar_shortest_path` - - """ - return astar_shortest_path(self.adjacency, u, v) - - # -------------------------------------------------------------------------- - # node attributes - # -------------------------------------------------------------------------- - - # -------------------------------------------------------------------------- - # edge attributes - # -------------------------------------------------------------------------- + :meth:`add_node` + :meth:`delete_edge` - # -------------------------------------------------------------------------- - # node topology - # -------------------------------------------------------------------------- + Examples + -------- + >>> - # -------------------------------------------------------------------------- - # edge topology - # -------------------------------------------------------------------------- + """ + attr = attr_dict or {} + attr.update(kwattr) + if u not in self.node: + u = self.add_node(u) + if v not in self.node: + v = self.add_node(v) + data = self.edge[u].get(v, {}) + data.update(attr) + self.edge[u][v] = data + if v not in self.adjacency[u]: + self.adjacency[u][v] = None + if u not in self.adjacency[v]: + self.adjacency[v][u] = None + return u, v # -------------------------------------------------------------------------- - # node geometry + # Modifiers # -------------------------------------------------------------------------- - def node_coordinates(self, key, axes="xyz"): - """Return the coordinates of a node. + def delete_node(self, key): + """Delete a node from the graph. Parameters ---------- key : hashable The identifier of the node. - axes : str, optional - The components of the node coordinates to return. Returns ------- - list[float] - The coordinates of the node. + None See Also -------- - :meth:`node_point`, :meth:`node_laplacian`, :meth:`node_neighborhood_centroid` + :meth:`delete_edge` + :meth:`add_node` - """ - return [self.node[key][axis] for axis in axes] + Examples + -------- + >>> - def node_point(self, node): - """Return the point of a node. + """ + if key in self.edge: + del self.edge[key] + if key in self.adjacency: + del self.adjacency[key] + if key in self.node: + del self.node[key] + for u in list(self.edge): + for v in list(self.edge[u]): + if v == key: + del self.edge[u][v] + for u in self.adjacency: + for v in list(self.adjacency[u]): + if v == key: + del self.adjacency[u][v] + + def delete_edge(self, edge): + """Delete an edge from the network. Parameters ---------- - node : hashable - The identifier of the node. + edge : tuple[hashable, hashable] + The identifier of the edge as a pair of node identifiers. Returns ------- - :class:`compas.geometry.Point` - The point of the node. + None See Also -------- - :meth:`node_coordinates`, :meth:`node_laplacian`, :meth:`node_neighborhood_centroid` + :meth:`delete_node` + :meth:`add_edge` + + Examples + -------- + >>> """ - return Point(*self.node_coordinates(node)) + u, v = edge - def node_laplacian(self, key): - """Return the vector from the node to the centroid of its 1-ring neighborhood. + if u in self.edge and v in self.edge[u]: + del self.edge[u][v] - Parameters - ---------- - key : hashable - The identifier of the node. + if u == v: # invalid edge + del self.adjacency[u][v] + elif v not in self.edge or u not in self.edge[v]: + del self.adjacency[u][v] + del self.adjacency[v][u] + # else: an edge in an opposite direction exists, we don't want to delete the adjacency + + # -------------------------------------------------------------------------- + # Info + # -------------------------------------------------------------------------- + + def summary(self): + """Return a summary of the graph. Returns ------- - :class:`compas.geometry.Vector` - The laplacian vector. + str + The formatted summary. + + """ + tpl = "\n".join( + [ + "{} summary", + "=" * (len(self.name) + len(" summary")), + "- nodes: {}", + "- edges: {}", + ] + ) + return tpl.format(self.name, self.number_of_nodes(), self.number_of_edges()) + + def number_of_nodes(self): + """Compute the number of nodes of the graph. + + Returns + ------- + int + The number of nodes. See Also -------- - :meth:`node_coordinates`, :meth:`node_point`, :meth:`node_neighborhood_centroid` + :meth:`number_of_edges` """ - c = centroid_points([self.node_coordinates(nbr) for nbr in self.neighbors(key)]) - p = self.node_coordinates(key) - return Vector(*subtract_vectors(c, p)) - - def node_neighborhood_centroid(self, key): - """Return the computed centroid of the neighboring nodes. + return len(list(self.nodes())) - Parameters - ---------- - key : hashable - The identifier of the node. + def number_of_edges(self): + """Compute the number of edges of the graph. Returns ------- - :class:`compas.geometry.Point` - The point at the centroid. + int + The number of edges. See Also -------- - :meth:`node_coordinates`, :meth:`node_point`, :meth:`node_laplacian` + :meth:`number_of_nodes` """ - return Point(*centroid_points([self.node_coordinates(nbr) for nbr in self.neighbors(key)])) - - # -------------------------------------------------------------------------- - # edge geometry - # -------------------------------------------------------------------------- + return len(list(self.edges())) - def edge_coordinates(self, edge, axes="xyz"): - """Return the coordinates of the start and end point of an edge. + def is_connected(self): + """Verify that the network is connected. - Parameters - ---------- - edge : tuple[hashable, hashable] - The identifier of the edge. - axes : str, optional - The axes along which the coordinates should be included. Returns ------- - tuple[list[float], list[float]] - The coordinates of the start point. - The coordinates of the end point. + bool + True, if the network is connected. + False, otherwise. - See Also + Notes + ----- + A network is connected if for every two vertices a path exists connecting them. + + Examples -------- - :meth:`edge_point`, :meth:`edge_start`, :meth:`edge_end`, :meth:`edge_midpoint` + >>> import compas + >>> from compas.datastructures import Network + >>> network = Network.from_obj(compas.get('lines.obj')) + >>> network.is_connected() + True """ - u, v = edge - return self.node_coordinates(u, axes=axes), self.node_coordinates(v, axes=axes) + if self.number_of_nodes() == 0: + return False + nodes = breadth_first_traverse(self.adjacency, self.node_sample(size=1)[0]) + return len(nodes) == self.number_of_nodes() - def edge_start(self, edge): - """Return the start point of an edge. + # -------------------------------------------------------------------------- + # Accessors + # -------------------------------------------------------------------------- + + def nodes(self, data=False): + """Iterate over the nodes of the network. Parameters ---------- - edge : tuple[hashable, hashable] - The identifier of the edge. + data : bool, optional + If True, yield the node attributes in addition to the node identifiers. - Returns - ------- - :class:`compas.geometry.Point` - The start point of the edge. + Yields + ------ + hashable | tuple[hashable, dict[str, Any]] + If `data` is False, the next node identifier. + If `data` is True, the next node as a (key, attr) tuple. See Also -------- - :meth:`edge_point`, :meth:`edge_end`, :meth:`edge_midpoint` + :meth:`nodes_where`, :meth:`nodes_where_predicate` + :meth:`edges`, :meth:`edges_where`, :meth:`edges_where_predicate` + + """ + for key in self.node: + if not data: + yield key + else: + yield key, self.node_attributes(key) + + def nodes_where(self, conditions=None, data=False, **kwargs): + """Get nodes for which a certain condition or set of conditions is true. + + Parameters + ---------- + conditions : dict, optional + A set of conditions in the form of key-value pairs. + The keys should be attribute names. The values can be attribute + values or ranges of attribute values in the form of min/max pairs. + data : bool, optional + If True, yield the node attributes in addition to the node identifiers. + + Yields + ------ + hashable | tuple[hashable, dict[str, Any]] + If `data` is False, the next node that matches the condition. + If `data` is True, the next node and its attributes. + + See Also + -------- + :meth:`nodes`, :meth:`nodes_where_predicate` + :meth:`edges`, :meth:`edges_where`, :meth:`edges_where_predicate` + + """ + conditions = conditions or {} + conditions.update(kwargs) + + for key, attr in self.nodes(True): + is_match = True + attr = attr or {} + + for name, value in conditions.items(): + method = getattr(self, name, None) + + if callable(method): + val = method(key) + if isinstance(val, list): + if value not in val: + is_match = False + break + break + if isinstance(value, (tuple, list)): + minval, maxval = value + if val < minval or val > maxval: + is_match = False + break + else: + if value != val: + is_match = False + break + + else: + if name not in attr: + is_match = False + break + if isinstance(attr[name], list): + if value not in attr[name]: + is_match = False + break + break + if isinstance(value, (tuple, list)): + minval, maxval = value + if attr[name] < minval or attr[name] > maxval: + is_match = False + break + else: + if value != attr[name]: + is_match = False + break + + if is_match: + if data: + yield key, attr + else: + yield key + + def nodes_where_predicate(self, predicate, data=False): + """Get nodes for which a certain condition or set of conditions is true using a lambda function. + + Parameters + ---------- + predicate : callable + The condition you want to evaluate. + The callable takes 2 parameters: the node identifier and the node attributes, and should return True or False. + data : bool, optional + If True, yield the node attributes in addition to the node identifiers. + + Yields + ------ + hashable | tuple[hashable, dict[str, Any]] + If `data` is False, the next node that matches the condition. + If `data` is True, the next node and its attributes. + + See Also + -------- + :meth:`nodes`, :meth:`nodes_where` + :meth:`edges`, :meth:`edges_where`, :meth:`edges_where_predicate` + + Examples + -------- + >>> + + """ + for key, attr in self.nodes(True): + if predicate(key, attr): + if data: + yield key, attr + else: + yield key + + def edges(self, data=False): + """Iterate over the edges of the network. + + Parameters + ---------- + data : bool, optional + If True, yield the edge attributes in addition to the edge identifiers. + + Yields + ------ + tuple[hashable, hashable] | tuple[tuple[hashable, hashable], dict[str, Any]] + If `data` is False, the next edge identifier (u, v). + If `data` is True, the next edge identifier and its attributes as a ((u, v), attr) tuple. + + See Also + -------- + :meth:`edges_where`, :meth:`edges_where_predicate` + :meth:`nodes`, :meth:`nodes_where`, :meth:`nodes_where_predicate` + + """ + for u, nbrs in iter(self.edge.items()): + for v, attr in iter(nbrs.items()): + if data: + yield (u, v), attr + else: + yield u, v + + def edges_where(self, conditions=None, data=False, **kwargs): + """Get edges for which a certain condition or set of conditions is true. + + Parameters + ---------- + conditions : dict, optional + A set of conditions in the form of key-value pairs. + The keys should be attribute names. The values can be attribute + values or ranges of attribute values in the form of min/max pairs. + data : bool, optional + If True, yield the edge attributes in addition to the edge identifiers. + **kwargs : dict[str, Any], optional + Additional conditions provided as named function arguments. + + Yields + ------ + tuple[hashable, hashable] | tuple[tuple[hashable, hashable], dict[str, Any]] + If `data` is False, the next edge identifier (u, v). + If `data` is True, the next edge identifier and its attributes as a ((u, v), attr) tuple. + + See Also + -------- + :meth:`edges`, :meth:`edges_where_predicate` + :meth:`nodes`, :meth:`nodes_where`, :meth:`nodes_where_predicate` + + """ + conditions = conditions or {} + conditions.update(kwargs) + + for key in self.edges(): + is_match = True + + attr = self.edge_attributes(key) or {} + + for name, value in conditions.items(): + method = getattr(self, name, None) + + if method and callable(method): + val = method(key) + elif name in attr: + val = attr[name] + else: + is_match = False + break + + if isinstance(val, list): + if value not in val: + is_match = False + break + elif isinstance(value, (tuple, list)): + minval, maxval = value + if val < minval or val > maxval: + is_match = False + break + else: + if value != val: + is_match = False + break + + if is_match: + if data: + yield key, attr + else: + yield key + + def edges_where_predicate(self, predicate, data=False): + """Get edges for which a certain condition or set of conditions is true using a lambda function. + + Parameters + ---------- + predicate : callable + The condition you want to evaluate. + The callable takes 2 parameters: + an edge identifier (tuple of node identifiers) and edge attributes, + and should return True or False. + data : bool, optional + If True, yield the edge attributes in addition to the edge attributes. + + Yields + ------ + tuple[hashable, hashable] | tuple[tuple[hashable, hashable], dict[str, Any]] + If `data` is False, the next edge identifier (u, v). + If `data` is True, the next edge identifier and its attributes as a ((u, v), attr) tuple. + + See Also + -------- + :meth:`edges`, :meth:`edges_where` + :meth:`nodes`, :meth:`nodes_where`, :meth:`nodes_where_predicate` + + Examples + -------- + >>> + + """ + for key, attr in self.edges(True): + if predicate(key, attr): + if data: + yield key, attr + else: + yield key + + def shortest_path(self, u, v): + """Find the shortest path between two nodes using the A* algorithm. + + Parameters + ---------- + u : hashable + The identifier of the start node. + v : hashable + The identifier of the end node. + + Returns + ------- + list[hashable] | None + The path from root to goal, or None, if no path exists between the vertices. + + See Also + -------- + :meth:`compas.topology.astar_shortest_path` + + """ + return astar_shortest_path(self.adjacency, u, v) + + # -------------------------------------------------------------------------- + # Default attributes + # -------------------------------------------------------------------------- + + def update_default_node_attributes(self, attr_dict=None, **kwattr): + """Update the default node attributes. + + Parameters + ---------- + attr_dict : dict[str, Any], optional + A dictionary of attributes with their default values. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + None + + See Also + -------- + :meth:`update_default_edge_attributes` + + """ + if not attr_dict: + attr_dict = {} + attr_dict.update(kwattr) + self.default_node_attributes.update(attr_dict) + + def update_default_edge_attributes(self, attr_dict=None, **kwattr): + """Update the default edge attributes. + + Parameters + ---------- + attr_dict : dict[str, Any], optional + A dictionary of attributes with their default values. + **kwattr : dict[str, Any], optional + A dictionary of additional attributes compiled of remaining named arguments. + + Returns + ------- + None + + See Also + -------- + :meth:`update_default_node_attributes` + + """ + if not attr_dict: + attr_dict = {} + attr_dict.update(kwattr) + self.default_edge_attributes.update(attr_dict) + + update_dna = update_default_node_attributes + update_dea = update_default_edge_attributes + + # -------------------------------------------------------------------------- + # Node attributes + # -------------------------------------------------------------------------- + + def node_attribute(self, key, name, value=None): + """Get or set an attribute of a node. + + Parameters + ---------- + key : hashable + The node identifier. + name : str + The name of the attribute + value : obj, optional + The value of the attribute. + + Returns + ------- + obj or None + The value of the attribute, + or None when the function is used as a "setter". + + Raises + ------ + KeyError + If the node does not exist. + + See Also + -------- + :meth:`unset_node_attribute` + :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` + :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` + + """ + if key not in self.node: + raise KeyError(key) + if value is not None: + self.node[key][name] = value + return + if name in self.node[key]: + return self.node[key][name] + else: + if name in self.default_node_attributes: + return self.default_node_attributes[name] + + def unset_node_attribute(self, key, name): + """Unset the attribute of a node. + + Parameters + ---------- + key : int + The node identifier. + name : str + The name of the attribute. + + Raises + ------ + KeyError + If the node does not exist. + + See Also + -------- + :meth:`node_attribute` + + Notes + ----- + Unsetting the value of a node attribute implicitly sets it back to the value + stored in the default node attribute dict. + + """ + if name in self.node[key]: + del self.node[key][name] + + def node_attributes(self, key, names=None, values=None): + """Get or set multiple attributes of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + names : list[str], optional + A list of attribute names. + values : list[Any], optional + A list of attribute values. + + Returns + ------- + dict[str, Any] | list[Any] | None + If the parameter `names` is empty, + the function returns a dictionary of all attribute name-value pairs of the node. + If the parameter `names` is not empty, + the function returns a list of the values corresponding to the requested attribute names. + The function returns None if it is used as a "setter". + + Raises + ------ + KeyError + If the node does not exist. + + See Also + -------- + :meth:`node_attribute`, :meth:`nodes_attribute`, :meth:`nodes_attributes` + :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` + + """ + if key not in self.node: + raise KeyError(key) + if names and values is not None: + # use it as a setter + for name, value in zip(names, values): + self.node[key][name] = value + return + # use it as a getter + if not names: + # return all node attributes as a dict + return NodeAttributeView(self.default_node_attributes, self.node[key]) + values = [] + for name in names: + if name in self.node[key]: + values.append(self.node[key][name]) + elif name in self.default_node_attributes: + values.append(self.default_node_attributes[name]) + else: + values.append(None) + return values + + def nodes_attribute(self, name, value=None, keys=None): + """Get or set an attribute of multiple nodes. + + Parameters + ---------- + name : str + The name of the attribute. + value : obj, optional + The value of the attribute. + keys : list[hashable], optional + A list of node identifiers. + + Returns + ------- + list[Any] | None + The value of the attribute for each node, + or None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the nodes does not exist. + + See Also + -------- + :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attributes` + :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` + + """ + if not keys: + keys = self.nodes() + if value is not None: + for key in keys: + self.node_attribute(key, name, value) + return + return [self.node_attribute(key, name) for key in keys] + + def nodes_attributes(self, names=None, values=None, keys=None): + """Get or set multiple attributes of multiple nodes. + + Parameters + ---------- + names : list[str], optional + The names of the attribute. + values : list[Any], optional + The values of the attributes. + keys : list[hashable], optional + A list of node identifiers. + + Returns + ------- + list[dict[str, Any]] | list[list[Any]] | None + If the parameter `names` is None, + the function returns a list containing an attribute dict per node. + If the parameter `names` is not None, + the function returns a list containing a list of attribute values per node corresponding to the provided attribute names. + The function returns None if it is used as a "setter". + + Raises + ------ + KeyError + If any of the nodes does not exist. + + See Also + -------- + :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute` + :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` + + """ + if not keys: + keys = self.nodes() + if values: + for key in keys: + self.node_attributes(key, names, values) + return + return [self.node_attributes(key, names) for key in keys] + + # -------------------------------------------------------------------------- + # Edge attributes + # -------------------------------------------------------------------------- + + def edge_attribute(self, key, name, value=None): + """Get or set an attribute of an edge. + + Parameters + ---------- + key : tuple[hashable, hashable] + The identifier of the edge as a pair of node identifiers. + name : str + The name of the attribute. + value : obj, optional + The value of the attribute. + + Returns + ------- + object | None + The value of the attribute, or None when the function is used as a "setter". + + Raises + ------ + KeyError + If the edge does not exist. + + See Also + -------- + :meth:`unset_edge_attribute` + :meth:`edge_attributes`, :meth:`edges_attribute`, :meth:`edges_attributes` + :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` + + """ + u, v = key + if u not in self.edge or v not in self.edge[u]: + raise KeyError(key) + attr = self.edge[u][v] + if value is not None: + attr[name] = value + return + if name in attr: + return attr[name] + if name in self.default_edge_attributes: + return self.default_edge_attributes[name] + + def unset_edge_attribute(self, key, name): + """Unset the attribute of an edge. + + Parameters + ---------- + key : tuple[hashable, hashable] + The edge identifier. + name : str + The name of the attribute. + + Returns + ------- + None + + Raises + ------ + KeyError + If the edge does not exist. + + See Also + -------- + :meth:`edge_attribute` + + Notes + ----- + Unsetting the value of an edge attribute implicitly sets it back to the value + stored in the default edge attribute dict. + + """ + u, v = key + if u not in self.edge or v not in self.edge[u]: + raise KeyError(key) + attr = self.edge[u][v] + if name in attr: + del attr[name] + + def edge_attributes(self, key, names=None, values=None): + """Get or set multiple attributes of an edge. + + Parameters + ---------- + key : tuple[hashable, hashable] + The identifier of the edge. + names : list[str], optional + A list of attribute names. + values : list[Any], optional + A list of attribute values. + + Returns + ------- + dict[str, Any] | list[Any] | None + If the parameter `names` is empty, a dictionary of all attribute name-value pairs of the edge. + If the parameter `names` is not empty, a list of the values corresponding to the provided names. + None if the function is used as a "setter". + + Raises + ------ + KeyError + If the edge does not exist. + + See Also + -------- + :meth:`edge_attribute`, :meth:`edges_attribute`, :meth:`edges_attributes` + :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` + + """ + u, v = key + if u not in self.edge or v not in self.edge[u]: + raise KeyError(key) + if names and values: + # use it as a setter + for name, value in zip(names, values): + self.edge_attribute(key, name, value) + return + # use it as a getter + if not names: + # get the entire attribute dict + return EdgeAttributeView(self.default_edge_attributes, self.edge[u][v]) + # get only the values of the named attributes + values = [] + for name in names: + value = self.edge_attribute(key, name) + values.append(value) + return values + + def edges_attribute(self, name, value=None, keys=None): + """Get or set an attribute of multiple edges. + + Parameters + ---------- + name : str + The name of the attribute. + value : obj, optional + The value of the attribute. + keys : list[tuple[hashable, hashable]], optional + A list of edge identifiers. + + Returns + ------- + list[Any] | None + A list containing the value per edge of the requested attribute, + or None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the edges does not exist. + + See Also + -------- + :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attributes` + :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` + + """ + if not keys: + keys = self.edges() + if value is not None: + for key in keys: + self.edge_attribute(key, name, value) + return + return [self.edge_attribute(key, name) for key in keys] + + def edges_attributes(self, names=None, values=None, keys=None): + """Get or set multiple attributes of multiple edges. + + Parameters + ---------- + names : list[str], optional + The names of the attribute. + values : list[Any], optional + The values of the attributes. + keys : list[tuple[hashable, hashable]], optional + A list of edge identifiers. + + Returns + ------- + list[dict[str, Any]] | list[list[Any]] | None + If `names` is empty, + a list containing per edge an attribute dict with all attributes of the edge. + If `names` is not empty, + a list containing per edge a list of attribute values corresponding to the requested names. + None if the function is used as a "setter". + + Raises + ------ + KeyError + If any of the edges does not exist. + + See Also + -------- + :meth:`edge_attribute`, :meth:`edge_attributes`, :meth:`edges_attribute` + :meth:`node_attribute`, :meth:`node_attributes`, :meth:`nodes_attribute`, :meth:`nodes_attributes` + + """ + if not keys: + keys = self.edges() + if values: + for key in keys: + self.edge_attributes(key, names, values) + return + return [self.edge_attributes(key, names) for key in keys] + + # -------------------------------------------------------------------------- + # Node topology + # -------------------------------------------------------------------------- + + def has_node(self, key): + """Verify if a specific node is present in the network. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + bool + True or False. + + See Also + -------- + :meth:`has_edge` + + """ + return key in self.node + + def is_leaf(self, key): + """Verify if a node is a leaf. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + bool + True or False. + + See Also + -------- + :meth:`leaves` + :meth:`is_node_connected` + + Notes + ----- + A node is a *leaf* if it has only one neighbor. + + """ + return self.degree(key) == 1 + + def leaves(self): + """Return all leaves of the network. + + Returns + ------- + list[hashable] + A list of node identifiers. + + """ + return [key for key in self.nodes() if self.is_leaf(key)] + + def is_node_connected(self, key): + """Verify if a specific node is connected. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + bool + True or False. + + See Also + -------- + :meth:`is_leaf` + + """ + return self.degree(key) > 0 + + def neighbors(self, key): + """Return the neighbors of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + list[hashable] + A list of node identifiers. + + See Also + -------- + :meth:`neighbors_out`, :meth:`neighbors_in` + :meth:`neighborhood` + + """ + return list(self.adjacency[key]) + + def neighborhood(self, key, ring=1): + """Return the nodes in the neighborhood of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + ring : int, optional + The size of the neighborhood. + + Returns + ------- + list[hashable] + A list of node identifiers. + + See Also + -------- + :meth:`neighbors` + + """ + nbrs = set(self.neighbors(key)) + i = 1 + while True: + if i == ring: + break + temp = [] + for nbr in nbrs: + temp += self.neighbors(nbr) + nbrs.update(temp) + i += 1 + if key in nbrs: + nbrs.remove(key) + return list(nbrs) + + def neighbors_out(self, key): + """Return the outgoing neighbors of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + list[hashable] + A list of node identifiers. + + See Also + -------- + :meth:`neighbors`, :meth:`neighbors_in` + + """ + return list(self.edge[key]) + + def neighbors_in(self, key): + """Return the incoming neighbors of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + list[hashable] + A list of node identifiers. + + See Also + -------- + :meth:`neighbors`, :meth:`neighbors_out` + + """ + return list(set(self.adjacency[key]) - set(self.edge[key])) + + def degree(self, key): + """Return the number of neighbors of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + int + The number of neighbors of the node. + + See Also + -------- + :meth:`degree_out`, :meth:`degree_in` + + """ + return len(self.neighbors(key)) + + def degree_out(self, key): + """Return the number of outgoing neighbors of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + int + The number of outgoing neighbors of the node. + + See Also + -------- + :meth:`degree`, :meth:`degree_in` + + """ + return len(self.neighbors_out(key)) + + def degree_in(self, key): + """Return the numer of incoming neighbors of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + int + The number of incoming neighbors of the node. + + See Also + -------- + :meth:`degree`, :meth:`degree_out` + + """ + return len(self.neighbors_in(key)) + + def node_edges(self, key): + """Return the edges connected to a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + list[tuple[hashable, hashable]] + The edges connected to the node. + + """ + edges = [] + for nbr in self.neighbors(key): + if nbr in self.edge[key]: + edges.append((key, nbr)) + else: + edges.append((nbr, key)) + return edges + + # -------------------------------------------------------------------------- + # Edge topology + # -------------------------------------------------------------------------- + + def has_edge(self, edge, directed=True): + """Verify if the network contains a specific edge. + + Parameters + ---------- + edge : tuple[hashable, hashable] + The identifier of the edge as a pair of node identifiers. + directed : bool, optional + If True, the direction of the edge is taken into account. + + Returns + ------- + bool + True if the edge is present, False otherwise. + + See Also + -------- + :meth:`has_node` + + """ + u, v = edge + if directed: + return u in self.edge and v in self.edge[u] + return (u in self.edge and v in self.edge[u]) or (v in self.edge and u in self.edge[v]) + + # -------------------------------------------------------------------------- + # Node geometry + # -------------------------------------------------------------------------- + + def node_coordinates(self, key, axes="xyz"): + """Return the coordinates of a node. + + Parameters + ---------- + key : hashable + The identifier of the node. + axes : str, optional + The components of the node coordinates to return. + + Returns + ------- + list[float] + The coordinates of the node. + + See Also + -------- + :meth:`node_point`, :meth:`node_laplacian`, :meth:`node_neighborhood_centroid` + + """ + return [self.node[key][axis] for axis in axes] + + def node_point(self, node): + """Return the point of a node. + + Parameters + ---------- + node : hashable + The identifier of the node. + + Returns + ------- + :class:`compas.geometry.Point` + The point of the node. + + See Also + -------- + :meth:`node_coordinates`, :meth:`node_laplacian`, :meth:`node_neighborhood_centroid` + + """ + return Point(*self.node_coordinates(node)) + + def node_laplacian(self, key): + """Return the vector from the node to the centroid of its 1-ring neighborhood. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + :class:`compas.geometry.Vector` + The laplacian vector. + + See Also + -------- + :meth:`node_coordinates`, :meth:`node_point`, :meth:`node_neighborhood_centroid` + + """ + c = centroid_points([self.node_coordinates(nbr) for nbr in self.neighbors(key)]) + p = self.node_coordinates(key) + return Vector(*subtract_vectors(c, p)) + + def node_neighborhood_centroid(self, key): + """Return the computed centroid of the neighboring nodes. + + Parameters + ---------- + key : hashable + The identifier of the node. + + Returns + ------- + :class:`compas.geometry.Point` + The point at the centroid. + + See Also + -------- + :meth:`node_coordinates`, :meth:`node_point`, :meth:`node_laplacian` + + """ + return Point(*centroid_points([self.node_coordinates(nbr) for nbr in self.neighbors(key)])) + + # -------------------------------------------------------------------------- + # Edge geometry + # -------------------------------------------------------------------------- + + def edge_coordinates(self, edge, axes="xyz"): + """Return the coordinates of the start and end point of an edge. + + Parameters + ---------- + edge : tuple[hashable, hashable] + The identifier of the edge. + axes : str, optional + The axes along which the coordinates should be included. + + Returns + ------- + tuple[list[float], list[float]] + The coordinates of the start point. + The coordinates of the end point. + + See Also + -------- + :meth:`edge_point`, :meth:`edge_start`, :meth:`edge_end`, :meth:`edge_midpoint` + + """ + u, v = edge + return self.node_coordinates(u, axes=axes), self.node_coordinates(v, axes=axes) + + def edge_start(self, edge): + """Return the start point of an edge. + + Parameters + ---------- + edge : tuple[hashable, hashable] + The identifier of the edge. + + Returns + ------- + :class:`compas.geometry.Point` + The start point of the edge. + + See Also + -------- + :meth:`edge_point`, :meth:`edge_end`, :meth:`edge_midpoint` """ return self.node_point(edge[0]) @@ -716,7 +2250,7 @@ def edge_length(self, edge): return distance_point_point(a, b) # -------------------------------------------------------------------------- - # transformations + # Transformations # -------------------------------------------------------------------------- def transform(self, transformation): @@ -736,3 +2270,191 @@ def transform(self, transformation): points = transform_points(nodes, transformation) for point, node in zip(points, self.nodes()): self.node_attributes(node, "xyz", point) + + # -------------------------------------------------------------------------- + # Other Methods + # -------------------------------------------------------------------------- + + def connected_nodes(self): + """Get groups of connected nodes. + + Returns + ------- + list[list[hashable]] + + See Also + -------- + :meth:`connected_edges` + + """ + return connected_components(self.adjacency) + + def connected_edges(self): + """Get groups of connected edges. + + Returns + ------- + list[list[tuple[hashable, hashable]]] + + See Also + -------- + :meth:`connected_nodes` + + """ + return [[(u, v) for u in nodes for v in self.neighbors(u) if u < v] for nodes in self.connected_nodes()] + + def exploded(self): + """Explode the graph into its connected components. + + Returns + ------- + list[:class:`compas.datastructures.Graph`] + + """ + cls = type(self) + graphs = [] + for nodes in self.connected_nodes(): + edges = [(u, v) for u in nodes for v in self.neighbors(u) if u < v] + graph = cls( + default_node_attributes=self.default_node_attributes, + default_edge_attributes=self.default_edge_attributes, + ) + for node in nodes: + graph.add_node(node, attr_dict=self.node_attributes(node)) + for u, v in edges: + graph.add_edge(u, v, attr_dict=self.edge_attributes((u, v))) + graphs.append(graph) + return graphs + + def complement(self): + """Generate the complement of a graph. + + The complement of a graph G is the graph H with the same vertices + but whose edges consists of the edges not present in the graph G [1]_. + + Returns + ------- + :class:`compas.datastructures.Graph` + The complement graph. + + References + ---------- + .. [1] Wolfram MathWorld. *Graph complement*. + Available at: http://mathworld.wolfram.com/GraphComplement.html. + + Examples + -------- + >>> import compas + >>> from compas.datastructures import Network + >>> network = Network.from_obj(compas.get('lines.obj')) + >>> complement = network.complement() + >>> any(complement.has_edge(u, v, directed=False) for u, v in network.edges()) + False + + """ + cls = type(self) + + graph = cls( + default_node_attributes=self.default_node_attributes, + default_edge_attributes=self.default_edge_attributes, + ) + for node in self.nodes(): + graph.add_node(node, attr_dict=self.node_attributes(node)) + + for u, v in combinations(self.nodes(), 2): + if not self.has_edge((u, v), directed=False): + graph.add_edge(u, v, attr_dict=self.edge_attributes((u, v))) + + return graph + + # -------------------------------------------------------------------------- + # Matrices + # -------------------------------------------------------------------------- + + def adjacency_matrix(self, rtype="array"): + """Creates a node adjacency matrix from a Network datastructure. + + Parameters + ---------- + rtype : Literal['array', 'csc', 'csr', 'coo', 'list'], optional + Format of the result. + + Returns + ------- + array_like + Constructed adjacency matrix. + + """ + from compas.topology import adjacency_matrix + + node_index = self.node_index() + adjacency = [[node_index[nbr] for nbr in self.neighbors(key)] for key in self.nodes()] + return adjacency_matrix(adjacency, rtype=rtype) + + def connectivity_matrix(self, rtype="array"): + """Creates a connectivity matrix from a Network datastructure. + + Parameters + ---------- + rtype : Literal['array', 'csc', 'csr', 'coo', 'list'], optional + Format of the result. + + Returns + ------- + array_like + Constructed connectivity matrix. + + """ + from compas.topology import connectivity_matrix + + node_index = self.node_index() + edges = [(node_index[u], node_index[v]) for u, v in self.edges()] + return connectivity_matrix(edges, rtype=rtype) + + def degree_matrix(self, rtype="array"): + """Creates a degree matrix from a Network datastructure. + + Parameters + ---------- + rtype : Literal['array', 'csc', 'csr', 'coo', 'list'], optional + Format of the result. + + Returns + ------- + array_like + Constructed degree matrix. + + """ + from compas.topology import degree_matrix + + node_index = self.node_index() + adjacency = [[node_index[nbr] for nbr in self.neighbors(key)] for key in self.nodes()] + return degree_matrix(adjacency, rtype=rtype) + + def laplacian_matrix(self, normalize=False, rtype="array"): + """Creates a Laplacian matrix from a Network datastructure. + + Parameters + ---------- + normalize : bool, optional + If True, normalize the entries such that the value on the diagonal is 1. + rtype : Literal['array', 'csc', 'csr', 'coo', 'list'], optional + Format of the result. + + Returns + ------- + array_like + Constructed Laplacian matrix. + + Notes + ----- + ``d = L.dot(xyz)`` is currently a vector that points from the centroid to the node. + Therefore ``c = xyz - d``. By changing the signs in the laplacian, the dsiplacement + vectors could be used in a more natural way ``c = xyz + d``. + + """ + from compas.topology import laplacian_matrix + + node_index = self.node_index() + edges = [(node_index[u], node_index[v]) for u, v in self.edges()] + return laplacian_matrix(edges, normalize=normalize, rtype=rtype) diff --git a/tests/compas/datastructures/test_graph.py b/tests/compas/datastructures/test_graph.py deleted file mode 100644 index fe8fcbba79a7..000000000000 --- a/tests/compas/datastructures/test_graph.py +++ /dev/null @@ -1,164 +0,0 @@ -import pytest -import json -import compas - -from compas.datastructures import Graph - - -# ============================================================================== -# Fixtures -# ============================================================================== - - -@pytest.fixture -def graph(): - edges = [(0, 1), (0, 2), (0, 3), (0, 4)] - graph = Graph() - for u, v in edges: - graph.add_edge(u, v) - return graph - - -# ============================================================================== -# Basics -# ============================================================================== - -# ============================================================================== -# Data -# ============================================================================== - - -def test_graph_data(graph): - other = Graph.from_data(json.loads(json.dumps(graph.data))) - - assert graph.data == other.data - assert graph.default_node_attributes == other.default_node_attributes - assert graph.default_edge_attributes == other.default_edge_attributes - assert graph.number_of_nodes() == other.number_of_nodes() - assert graph.number_of_edges() == other.number_of_edges() - - if not compas.IPY: - assert Graph.validate_data(graph.data) - assert Graph.validate_data(other.data) - - -# ============================================================================== -# Constructors -# ============================================================================== - -# ============================================================================== -# Properties -# ============================================================================== - -# ============================================================================== -# Accessors -# ============================================================================== - -# ============================================================================== -# Builders -# ============================================================================== - -# ============================================================================== -# Modifiers -# ============================================================================== - - -def test_graph_invalid_edge_delete(): - graph = Graph() - node = graph.add_node() - edge = graph.add_edge(node, node) - graph.delete_edge(edge) - assert graph.has_edge(edge) is False - - -def test_graph_opposite_direction_edge_delete(): - graph = Graph() - node_a = graph.add_node() - node_b = graph.add_node() - edge_a = graph.add_edge(node_a, node_b) - edge_b = graph.add_edge(node_b, node_a) - graph.delete_edge(edge_a) - assert graph.has_edge(edge_a) is False - assert graph.has_edge(edge_b) is True - - -# ============================================================================== -# Samples -# ============================================================================== - - -def test_graph_node_sample(graph): - for node in graph.node_sample(): - assert graph.has_node(node) - for node in graph.node_sample(size=graph.number_of_nodes()): - assert graph.has_node(node) - - -def test_graph_edge_sample(graph): - for edge in graph.edge_sample(): - assert graph.has_edge(edge) - for edge in graph.edge_sample(size=graph.number_of_edges()): - assert graph.has_edge(edge) - - -# ============================================================================== -# Attributes -# ============================================================================== - - -def test_graph_default_node_attributes(): - graph = Graph(name="test", default_node_attributes={"a": 1, "b": 2}) - for node in graph.nodes(): - assert graph.node_attribute(node, name="a") == 1 - assert graph.node_attribute(node, name="b") == 2 - graph.node_attribute(node, name="a", value=3) - assert graph.node_attribute(node, name="a") == 3 - - -def test_graph_default_edge_attributes(): - graph = Graph(name="test", default_edge_attributes={"a": 1, "b": 2}) - for edge in graph.edges(): - assert graph.edge_attribute(edge, name="a") == 1 - assert graph.edge_attribute(edge, name="b") == 2 - graph.edge_attribute(edge, name="a", value=3) - assert graph.edge_attribute(edge, name="a") == 3 - - -# ============================================================================== -# Conversion -# ============================================================================== - - -def test_graph_to_networkx(): - if compas.IPY: - return - - g = Graph() - g.attributes["name"] = "DiGraph" - g.attributes["val"] = (0, 0, 0) - g.add_node(0) - g.add_node(1, weight=1.2, height="test") - g.add_node(2, x=1, y=1, z=0) - - g.add_edge(0, 1, attr_value=10) - g.add_edge(1, 2) - - nxg = g.to_networkx() - - assert nxg.graph["name"] == "DiGraph", "Graph attributes must be preserved" # type: ignore - assert nxg.graph["val"] == (0, 0, 0), "Graph attributes must be preserved" # type: ignore - assert set(nxg.nodes()) == set(g.nodes()), "Node sets must match" - assert nxg.nodes[1]["weight"] == 1.2, "Node attributes must be preserved" - assert nxg.nodes[1]["height"] == "test", "Node attributes must be preserved" - assert nxg.nodes[2]["x"] == 1, "Node attributes must be preserved" - - assert set(nxg.edges()) == set(((0, 1), (1, 2))), "Edge sets must match" - assert nxg.edges[0, 1]["attr_value"] == 10, "Edge attributes must be preserved" - - g2 = Graph.from_networkx(nxg) - - assert g.number_of_nodes() == g2.number_of_nodes() - assert g.number_of_edges() == g2.number_of_edges() - assert g2.edge_attribute((0, 1), "attr_value") == 10 - assert g2.attributes["name"] == "DiGraph", "Graph attributes must be preserved" - assert g2.attributes["val"] == (0, 0, 0), "Graph attributes must be preserved" diff --git a/tests/compas/datastructures/test_network.py b/tests/compas/datastructures/test_network.py index ca0957cfb120..1bf5fad86375 100644 --- a/tests/compas/datastructures/test_network.py +++ b/tests/compas/datastructures/test_network.py @@ -15,6 +15,15 @@ BASE_FOLDER = os.path.dirname(__file__) +@pytest.fixture +def network(): + edges = [(0, 1), (0, 2), (0, 3), (0, 4)] + network = Network() + for u, v in edges: + network.add_edge(u, v) + return network + + @pytest.fixture def planar_network(): return Network.from_obj(os.path.join(BASE_FOLDER, "fixtures", "planar.obj")) @@ -82,7 +91,21 @@ def test_network_from_pointcloud(): # ============================================================================== -def test_network_data(): +def test_network_data1(network): + other = Network.from_data(json.loads(json.dumps(network.data))) + + assert network.data == other.data + assert network.default_node_attributes == other.default_node_attributes + assert network.default_edge_attributes == other.default_edge_attributes + assert network.number_of_nodes() == other.number_of_nodes() + assert network.number_of_edges() == other.number_of_edges() + + if not compas.IPY: + assert Network.validate_data(network.data) + assert Network.validate_data(other.data) + + +def test_network_data2(): cloud = Pointcloud.from_bounds(random.random(), random.random(), random.random(), random.randint(10, 100)) network = Network.from_pointcloud(cloud=cloud, degree=3) other = Network.from_data(json.loads(json.dumps(network.data))) @@ -119,18 +142,108 @@ def test_add_node(): # Modifiers # ============================================================================== + +def test_network_invalid_edge_delete(): + network = Network() + node = network.add_node() + edge = network.add_edge(node, node) + network.delete_edge(edge) + assert network.has_edge(edge) is False + + +def test_network_opposite_direction_edge_delete(): + network = Network() + node_a = network.add_node() + node_b = network.add_node() + edge_a = network.add_edge(node_a, node_b) + edge_b = network.add_edge(node_b, node_a) + network.delete_edge(edge_a) + assert network.has_edge(edge_a) is False + assert network.has_edge(edge_b) is True + + # ============================================================================== # Samples # ============================================================================== + +def test_network_node_sample(network): + for node in network.node_sample(): + assert network.has_node(node) + for node in network.node_sample(size=network.number_of_nodes()): + assert network.has_node(node) + + +def test_network_edge_sample(network): + for edge in network.edge_sample(): + assert network.has_edge(edge) + for edge in network.edge_sample(size=network.number_of_edges()): + assert network.has_edge(edge) + + # ============================================================================== # Attributes # ============================================================================== + +def test_network_default_node_attributes(): + network = Network(name="test", default_node_attributes={"a": 1, "b": 2}) + for node in network.nodes(): + assert network.node_attribute(node, name="a") == 1 + assert network.node_attribute(node, name="b") == 2 + network.node_attribute(node, name="a", value=3) + assert network.node_attribute(node, name="a") == 3 + + +def test_network_default_edge_attributes(): + network = Network(name="test", default_edge_attributes={"a": 1, "b": 2}) + for edge in network.edges(): + assert network.edge_attribute(edge, name="a") == 1 + assert network.edge_attribute(edge, name="b") == 2 + network.edge_attribute(edge, name="a", value=3) + assert network.edge_attribute(edge, name="a") == 3 + + # ============================================================================== # Conversion # ============================================================================== + +def test_network_to_networkx(): + if compas.IPY: + return + + g = Network() + g.attributes["name"] = "DiGraph" + g.attributes["val"] = (0, 0, 0) + g.add_node(0) + g.add_node(1, weight=1.2, height="test") + g.add_node(2, x=1, y=1, z=0) + + g.add_edge(0, 1, attr_value=10) + g.add_edge(1, 2) + + nxg = g.to_networkx() + + assert nxg.graph["name"] == "DiGraph", "Network attributes must be preserved" # type: ignore + assert nxg.graph["val"] == (0, 0, 0), "Network attributes must be preserved" # type: ignore + assert set(nxg.nodes()) == set(g.nodes()), "Node sets must match" + assert nxg.nodes[1]["weight"] == 1.2, "Node attributes must be preserved" + assert nxg.nodes[1]["height"] == "test", "Node attributes must be preserved" + assert nxg.nodes[2]["x"] == 1, "Node attributes must be preserved" + + assert set(nxg.edges()) == set(((0, 1), (1, 2))), "Edge sets must match" + assert nxg.edges[0, 1]["attr_value"] == 10, "Edge attributes must be preserved" + + g2 = Network.from_networkx(nxg) + + assert g.number_of_nodes() == g2.number_of_nodes() + assert g.number_of_edges() == g2.number_of_edges() + assert g2.edge_attribute((0, 1), "attr_value") == 10 + assert g2.attributes["name"] == "DiGraph", "Network attributes must be preserved" + assert g2.attributes["val"] == (0, 0, 0), "Network attributes must be preserved" + + # ============================================================================== # Methods # ============================================================================== From 9702d009c61d0a5143e23c3613131f7d069f2835 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 15 Jan 2024 13:27:07 +0100 Subject: [PATCH 2/5] log --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee95cf723f2..220c85260658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* Merged `compas.datastructures.Graph` into `compas.datastructures.Network`. + ### Removed +* Removed `compas.datastructures.Graph` + ## [2.0.0-beta.2] 2024-01-12 ### Added From 1995ac5aa8e09b478820cd4959eae53d3e4d0dc8 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 15 Jan 2024 13:39:26 +0100 Subject: [PATCH 3/5] local setup wrongly configured --- .../datastructures/assembly/assembly.py | 42 +++++++++---------- tests/compas/data/test_dataschema.py | 14 +++---- tests/compas/topology/test_traversal.py | 3 +- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/compas/datastructures/assembly/assembly.py b/src/compas/datastructures/assembly/assembly.py index dd2d8328ded0..a41ec125d76d 100644 --- a/src/compas/datastructures/assembly/assembly.py +++ b/src/compas/datastructures/assembly/assembly.py @@ -3,7 +3,7 @@ from __future__ import division from compas.datastructures import Datastructure -from compas.datastructures import Graph +from compas.datastructures import Network from .exceptions import AssemblyError @@ -19,8 +19,8 @@ class Assembly(Datastructure): ---------- attributes : dict[str, Any] General attributes of the data structure that will be included in the data dict and serialization. - graph : :class:`compas.datastructures.Graph` - The graph that is used under the hood to store the parts and their connections. + network : :class:`compas.datastructures.Network` + The network that is used under the hood to store the parts and their connections. See Also -------- @@ -34,21 +34,21 @@ class Assembly(Datastructure): "type": "object", "properties": { "attributes": {"type": "object"}, - "graph": Graph.DATASCHEMA, + "network": Network.DATASCHEMA, }, - "required": ["graph"], + "required": ["network"], } def __init__(self, name=None, **kwargs): super(Assembly, self).__init__() self.attributes = {"name": name or "Assembly"} self.attributes.update(kwargs) - self.graph = Graph() + self.network = Network() self._parts = {} def __str__(self): tpl = "" - return tpl.format(self.graph.number_of_nodes(), self.graph.number_of_edges()) + return tpl.format(self.network.number_of_nodes(), self.network.number_of_edges()) # ========================================================================== # Data @@ -58,14 +58,14 @@ def __str__(self): def data(self): return { "attributes": self.attributes, - "graph": self.graph.data, + "network": self.network.data, } @classmethod def from_data(cls, data): assembly = cls() assembly.attributes.update(data["attributes"] or {}) - assembly.graph = Graph.from_data(data["graph"]) + assembly.network = Network.from_data(data["network"]) assembly._parts = {part.guid: part.key for part in assembly.parts()} # type: ignore return assembly @@ -107,12 +107,12 @@ def add_part(self, part, key=None, **kwargs): Returns ------- int | str - The identifier of the part in the current assembly graph. + The identifier of the part in the current assembly network. """ if part.guid in self._parts: raise AssemblyError("Part already added to the assembly") - key = self.graph.add_node(key=key, part=part, **kwargs) + key = self.network.add_node(key=key, part=part, **kwargs) part.key = key self._parts[part.guid] = part.key return key @@ -143,9 +143,9 @@ def add_connection(self, a, b, **kwargs): error_msg = "Both parts have to be added to the assembly before a connection can be created." if a.key is None or b.key is None: raise AssemblyError(error_msg) - if not self.graph.has_node(a.key) or not self.graph.has_node(b.key): + if not self.network.has_node(a.key) or not self.network.has_node(b.key): raise AssemblyError(error_msg) - return self.graph.add_edge(a.key, b.key, **kwargs) + return self.network.add_edge(a.key, b.key, **kwargs) def delete_part(self, part): """Remove a part from the assembly. @@ -161,7 +161,7 @@ def delete_part(self, part): """ del self._parts[part.guid] - self.graph.delete_node(key=part.key) + self.network.delete_node(key=part.key) def delete_connection(self, edge): """Delete a connection between two parts. @@ -176,7 +176,7 @@ def delete_connection(self, edge): None """ - self.graph.delete_edge(edge=edge) + self.network.delete_edge(edge=edge) def parts(self): """The parts of the assembly. @@ -187,8 +187,8 @@ def parts(self): The individual parts of the assembly. """ - for node in self.graph.nodes(): - yield self.graph.node_attribute(node, "part") + for node in self.network.nodes(): + yield self.network.node_attribute(node, "part") def connections(self, data=False): """Iterate over the connections between the parts. @@ -205,7 +205,7 @@ def connections(self, data=False): If `data` is True, the next connector identifier and its attributes as a ((u, v), attr) tuple. """ - return self.graph.edges(data) + return self.network.edges(data) def find(self, guid): """Find a part in the assembly by its GUID. @@ -228,7 +228,7 @@ def find(self, guid): if key is None: return None - return self.graph.node_attribute(key, "part") + return self.network.node_attribute(key, "part") def find_by_key(self, key): """Find a part in the assembly by its key. @@ -245,7 +245,7 @@ def find_by_key(self, key): or None if the part can't be found. """ - if key not in self.graph.node: + if key not in self.network.node: return None - return self.graph.node_attribute(key, "part") + return self.network.node_attribute(key, "part") diff --git a/tests/compas/data/test_dataschema.py b/tests/compas/data/test_dataschema.py index 34fe7285d22f..4eefef69e851 100644 --- a/tests/compas/data/test_dataschema.py +++ b/tests/compas/data/test_dataschema.py @@ -20,7 +20,7 @@ from compas.geometry import Torus from compas.geometry import Pointcloud -from compas.datastructures import Graph +from compas.datastructures import Network from compas.datastructures import HalfEdge if not compas.IPY: @@ -593,7 +593,7 @@ def test_schema_pointcloud_invalid(pointcloud): Pointcloud.validate_data(pointcloud) @pytest.mark.parametrize( - "graph", + "network", [ { "dna": {}, @@ -618,11 +618,11 @@ def test_schema_pointcloud_invalid(pointcloud): }, ], ) - def test_schema_graph_valid(graph): - Graph.validate_data(graph) + def test_schema_network_valid(network): + Network.validate_data(network) @pytest.mark.parametrize( - "graph", + "network", [ { "dna": {}, @@ -663,9 +663,9 @@ def test_schema_graph_valid(graph): }, ], ) - def test_schema_graph_invalid(graph): + def test_schema_network_invalid(network): with pytest.raises(jsonschema.exceptions.ValidationError): - Graph.validate_data(graph) + Network.validate_data(network) @pytest.mark.parametrize( "halfedge", diff --git a/tests/compas/topology/test_traversal.py b/tests/compas/topology/test_traversal.py index 5bb8f1352d6f..8817e273ad8a 100644 --- a/tests/compas/topology/test_traversal.py +++ b/tests/compas/topology/test_traversal.py @@ -1,4 +1,3 @@ -from compas.datastructures import Graph from compas.datastructures import Mesh from compas.datastructures import Network from compas.geometry import Box, Frame @@ -49,7 +48,7 @@ def test_astar_shortest_path_mesh(): def test_astar_lightest_path(): - g = Graph() + g = Network() for i in range(4): g.add_node(i) g.add_edge(0, 1) From 7f7098c11a1a4c3c8c0b527c8f610d10adbba1a6 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 15 Jan 2024 15:18:05 +0100 Subject: [PATCH 4/5] renamed everything to graph and added network as alias --- CHANGELOG.md | 4 +- .../api/compas.datastructures.CellNetwork.rst | 3 +- docs/api/compas.datastructures.Graph.rst | 199 +++++++++++++++++ docs/api/compas.datastructures.Network.rst | 199 ----------------- docs/api/compas.datastructures.rst | 2 +- docs/api/compas.scene.rst | 2 +- docs/api/compas_blender.scene.rst | 2 +- docs/api/compas_ghpython.scene.rst | 2 +- docs/api/compas_rhino.scene.rst | 2 +- ...s.rst => basics.datastructures.graphs.rst} | 2 +- docs/userguide/basics.datastructures.rst | 2 +- docs/userguide/scene.py | 7 +- src/compas/datastructures/__init__.py | 12 +- .../datastructures/assembly/assembly.py | 44 ++-- .../cell_network/cell_network.py | 17 +- .../{network => graph}/__init__.py | 0 .../{network => graph}/duality.py | 68 +++--- .../{network/network.py => graph/graph.py} | 170 +++++++------- .../{network => graph}/operations/__init__.py | 0 .../{network => graph}/operations/join.py | 68 +++--- .../{network => graph}/operations/split.py | 34 +-- .../{network => graph}/planarity.py | 130 +++++------ .../{network => graph}/smoothing.py | 16 +- src/compas/datastructures/mesh/mesh.py | 8 +- src/compas/scene/__init__.py | 4 +- .../{networkobject.py => graphobject.py} | 46 ++-- src/compas/scene/meshobject.py | 2 +- src/compas/scene/volmeshobject.py | 2 +- src/compas/topology/combinatorics.py | 14 +- src/compas/topology/matrices.py | 4 +- src/compas/topology/traversal.py | 12 +- src/compas_blender/scene/__init__.py | 8 +- .../{networkobject.py => graphobject.py} | 38 ++-- src/compas_ghpython/scene/__init__.py | 8 +- .../{networkobject.py => graphobject.py} | 20 +- src/compas_ghpython/utilities/__init__.py | 4 +- src/compas_ghpython/utilities/drawing.py | 14 +- src/compas_rhino/scene/__init__.py | 8 +- .../{networkobject.py => graphobject.py} | 42 ++-- src/compas_rhino/utilities/layers.py | 10 +- tests/compas/compas_api.json | 56 ++--- tests/compas/compas_api_ipy.json | 48 ++-- tests/compas/data/test_dataschema.py | 14 +- tests/compas/data/test_json.py | 6 +- tests/compas/datastructures/test_network.py | 210 +++++++++--------- tests/compas/donttest_api_completeness.py | 2 +- tests/compas/topology/test_traversal.py | 10 +- 47 files changed, 788 insertions(+), 787 deletions(-) create mode 100644 docs/api/compas.datastructures.Graph.rst delete mode 100644 docs/api/compas.datastructures.Network.rst rename docs/userguide/{basics.datastructures.networks.rst => basics.datastructures.graphs.rst} (94%) rename src/compas/datastructures/{network => graph}/__init__.py (100%) rename src/compas/datastructures/{network => graph}/duality.py (71%) rename src/compas/datastructures/{network/network.py => graph/graph.py} (94%) rename src/compas/datastructures/{network => graph}/operations/__init__.py (100%) rename src/compas/datastructures/{network => graph}/operations/join.py (63%) rename src/compas/datastructures/{network => graph}/operations/split.py (59%) rename src/compas/datastructures/{network => graph}/planarity.py (59%) rename src/compas/datastructures/{network => graph}/smoothing.py (73%) rename src/compas/scene/{networkobject.py => graphobject.py} (81%) rename src/compas_blender/scene/{networkobject.py => graphobject.py} (86%) rename src/compas_ghpython/scene/{networkobject.py => graphobject.py} (73%) rename src/compas_rhino/scene/{networkobject.py => graphobject.py} (88%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 220c85260658..12a579de193b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added `compas.dtastructures.Network` as alias of `compas.datastructures.Graph`. + ### Changed * Merged `compas.datastructures.Graph` into `compas.datastructures.Network`. ### Removed -* Removed `compas.datastructures.Graph` +* Removed `compas.datastructures.Network`. ## [2.0.0-beta.2] 2024-01-12 diff --git a/docs/api/compas.datastructures.CellNetwork.rst b/docs/api/compas.datastructures.CellNetwork.rst index 6680a9e68830..b5d3449f781b 100644 --- a/docs/api/compas.datastructures.CellNetwork.rst +++ b/docs/api/compas.datastructures.CellNetwork.rst @@ -149,14 +149,13 @@ Topology ~CellNetwork.halfface_opposite_cell ~CellNetwork.halfface_opposite_halfface ~CellNetwork.halfface_vertex_ancestor - ~CellNetwork.halfface_vertex_descendant + ~CellNetwork.halfface_vertex_descendent ~CellNetwork.halfface_vertices ~CellNetwork.has_edge ~CellNetwork.has_halfface ~CellNetwork.has_vertex ~CellNetwork.is_cell_on_boundary ~CellNetwork.is_edge_on_boundary - ~CellNetwork.is_halfedge_on_boundary ~CellNetwork.is_halfface_on_boundary ~CellNetwork.is_valid ~CellNetwork.is_vertex_on_boundary diff --git a/docs/api/compas.datastructures.Graph.rst b/docs/api/compas.datastructures.Graph.rst new file mode 100644 index 000000000000..0c41402455c4 --- /dev/null +++ b/docs/api/compas.datastructures.Graph.rst @@ -0,0 +1,199 @@ +****************************************************************************** +Graph +****************************************************************************** + +.. currentmodule:: compas.datastructures + +.. autoclass:: Graph + +Methods +======= + +Constructors +------------ + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.from_edges + ~Graph.from_json + ~Graph.from_lines + ~Graph.from_networkx + ~Graph.from_nodes_and_edges + ~Graph.from_obj + ~Graph.from_pointcloud + +Conversions +----------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.to_json + ~Graph.to_lines + ~Graph.to_networkx + ~Graph.to_obj + ~Graph.to_points + +Builders and Modifiers +---------------------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.add_edge + ~Graph.add_node + ~Graph.delete_edge + ~Graph.delete_node + ~Graph.join_edges + ~Graph.split_edge + +Accessors +--------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.edge_sample + ~Graph.edges + ~Graph.edges_where + ~Graph.edges_where_predicate + ~Graph.node_sample + ~Graph.nodes + ~Graph.nodes_where + ~Graph.nodes_where_predicate + +Attributes +---------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.edge_attribute + ~Graph.edge_attributes + ~Graph.edges_attribute + ~Graph.edges_attributes + ~Graph.node_attribute + ~Graph.node_attributes + ~Graph.nodes_attribute + ~Graph.nodes_attributes + ~Graph.update_default_edge_attributes + ~Graph.update_default_node_attributes + ~Graph.unset_edge_attribute + ~Graph.unset_node_attribute + +Topology +-------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.complement + ~Graph.connected_nodes + ~Graph.connected_edges + ~Graph.degree + ~Graph.degree_out + ~Graph.degree_in + ~Graph.exploded + ~Graph.has_edge + ~Graph.has_node + ~Graph.is_leaf + ~Graph.is_node_connected + ~Graph.neighborhood + ~Graph.neighbors + ~Graph.neighbors_in + ~Graph.neighbors_out + ~Graph.node_edges + ~Graph.number_of_edges + ~Graph.number_of_nodes + +Geometry +-------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.edge_coordinates + ~Graph.edge_direction + ~Graph.edge_end + ~Graph.edge_length + ~Graph.edge_line + ~Graph.edge_midpoint + ~Graph.edge_point + ~Graph.edge_start + ~Graph.edge_vector + ~Graph.node_coordinates + ~Graph.node_point + ~Graph.node_laplacian + ~Graph.node_neighborhood_centroid + ~Graph.transform + ~Graph.transformed + +Paths +----- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.shortest_path + +Planarity +--------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.count_crossings + ~Graph.embed_in_plane + ~Graph.find_crossings + ~Graph.find_cycles + ~Graph.is_crossed + ~Graph.is_planar + ~Graph.is_planar_embedding + ~Graph.is_xy + +Matrices +-------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.adjacency_matrix + ~Graph.connectivity_matrix + ~Graph.degree_matrix + ~Graph.laplacian_matrix + +Mappings +-------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.gkey_node + ~Graph.node_gkey + ~Graph.node_index + ~Graph.edge_index + ~Graph.index_node + ~Graph.index_edge + +Utilities +--------- + +.. autosummary:: + :toctree: generated/ + :nosignatures: + + ~Graph.summary + ~Graph.copy + ~Graph.clear diff --git a/docs/api/compas.datastructures.Network.rst b/docs/api/compas.datastructures.Network.rst deleted file mode 100644 index 6ed4b72647fc..000000000000 --- a/docs/api/compas.datastructures.Network.rst +++ /dev/null @@ -1,199 +0,0 @@ -****************************************************************************** -Network -****************************************************************************** - -.. currentmodule:: compas.datastructures - -.. autoclass:: Network - -Methods -======= - -Constructors ------------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.from_edges - ~Network.from_json - ~Network.from_lines - ~Network.from_networkx - ~Network.from_nodes_and_edges - ~Network.from_obj - ~Network.from_pointcloud - -Conversions ------------ - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.to_json - ~Network.to_lines - ~Network.to_networkx - ~Network.to_obj - ~Network.to_points - -Builders and Modifiers ----------------------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.add_edge - ~Network.add_node - ~Network.delete_edge - ~Network.delete_node - ~Network.join_edges - ~Network.split_edge - -Accessors ---------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.edge_sample - ~Network.edges - ~Network.edges_where - ~Network.edges_where_predicate - ~Network.node_sample - ~Network.nodes - ~Network.nodes_where - ~Network.nodes_where_predicate - -Attributes ----------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.edge_attribute - ~Network.edge_attributes - ~Network.edges_attribute - ~Network.edges_attributes - ~Network.node_attribute - ~Network.node_attributes - ~Network.nodes_attribute - ~Network.nodes_attributes - ~Network.update_default_edge_attributes - ~Network.update_default_node_attributes - ~Network.unset_edge_attribute - ~Network.unset_node_attribute - -Topology --------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.complement - ~Network.connected_nodes - ~Network.connected_edges - ~Network.degree - ~Network.degree_out - ~Network.degree_in - ~Network.exploded - ~Network.has_edge - ~Network.has_node - ~Network.is_leaf - ~Network.is_node_connected - ~Network.neighborhood - ~Network.neighbors - ~Network.neighbors_in - ~Network.neighbors_out - ~Network.node_edges - ~Network.number_of_edges - ~Network.number_of_nodes - -Geometry --------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.edge_coordinates - ~Network.edge_direction - ~Network.edge_end - ~Network.edge_length - ~Network.edge_line - ~Network.edge_midpoint - ~Network.edge_point - ~Network.edge_start - ~Network.edge_vector - ~Network.node_coordinates - ~Network.node_point - ~Network.node_laplacian - ~Network.node_neighborhood_centroid - ~Network.transform - ~Network.transformed - -Paths ------ - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.shortest_path - -Planarity ---------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.count_crossings - ~Network.embed_in_plane - ~Network.find_crossings - ~Network.find_cycles - ~Network.is_crossed - ~Network.is_planar - ~Network.is_planar_embedding - ~Network.is_xy - -Matrices --------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.adjacency_matrix - ~Network.connectivity_matrix - ~Network.degree_matrix - ~Network.laplacian_matrix - -Mappings --------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.gkey_node - ~Network.node_gkey - ~Network.node_index - ~Network.edge_index - ~Network.index_node - ~Network.index_edge - -Utilities ---------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ~Network.summary - ~Network.copy - ~Network.clear diff --git a/docs/api/compas.datastructures.rst b/docs/api/compas.datastructures.rst index 13d45f814587..f51f1be65912 100644 --- a/docs/api/compas.datastructures.rst +++ b/docs/api/compas.datastructures.rst @@ -18,7 +18,7 @@ Classes :maxdepth: 1 :titlesonly: - compas.datastructures.Network + compas.datastructures.Graph compas.datastructures.Mesh compas.datastructures.CellNetwork compas.datastructures.Tree diff --git a/docs/api/compas.scene.rst b/docs/api/compas.scene.rst index 942b0eeef978..86aec900c8ec 100644 --- a/docs/api/compas.scene.rst +++ b/docs/api/compas.scene.rst @@ -22,7 +22,7 @@ Classes GeometryObject MeshObject - NetworkObject + GraphObject NoSceneObjectContextError Scene SceneObject diff --git a/docs/api/compas_blender.scene.rst b/docs/api/compas_blender.scene.rst index 0b8a598c40d5..6f10b8f9283a 100644 --- a/docs/api/compas_blender.scene.rst +++ b/docs/api/compas_blender.scene.rst @@ -29,7 +29,7 @@ Classes FrameObject LineObject MeshObject - NetworkObject + GraphObject PointObject PointcloudObject PolygonObject diff --git a/docs/api/compas_ghpython.scene.rst b/docs/api/compas_ghpython.scene.rst index bcfa0053be8f..87f7258b9257 100644 --- a/docs/api/compas_ghpython.scene.rst +++ b/docs/api/compas_ghpython.scene.rst @@ -30,7 +30,7 @@ Classes GHSceneObject LineObject MeshObject - NetworkObject + GraphObject PointObject PolygonObject PolyhedronObject diff --git a/docs/api/compas_rhino.scene.rst b/docs/api/compas_rhino.scene.rst index 0a147af50172..23c322d98c0b 100644 --- a/docs/api/compas_rhino.scene.rst +++ b/docs/api/compas_rhino.scene.rst @@ -30,7 +30,7 @@ Classes FrameObject LineObject MeshObject - NetworkObject + GraphObject PlaneObject PointObject PolygonObject diff --git a/docs/userguide/basics.datastructures.networks.rst b/docs/userguide/basics.datastructures.graphs.rst similarity index 94% rename from docs/userguide/basics.datastructures.networks.rst rename to docs/userguide/basics.datastructures.graphs.rst index 74df68e79b59..2db0da7b9191 100644 --- a/docs/userguide/basics.datastructures.networks.rst +++ b/docs/userguide/basics.datastructures.graphs.rst @@ -1,3 +1,3 @@ ******************************************************************************** -Networks +Graphs ******************************************************************************** diff --git a/docs/userguide/basics.datastructures.rst b/docs/userguide/basics.datastructures.rst index 419c7c4cd0f0..dddfd52f3eb0 100644 --- a/docs/userguide/basics.datastructures.rst +++ b/docs/userguide/basics.datastructures.rst @@ -7,7 +7,7 @@ Datastructures :titlesonly: :caption: Data Structures - basics.datastructures.networks + basics.datastructures.graphs basics.datastructures.meshes basics.datastructures.cells basics.datastructures.trees diff --git a/docs/userguide/scene.py b/docs/userguide/scene.py index 6a8e384cdde6..39ead4d4722e 100644 --- a/docs/userguide/scene.py +++ b/docs/userguide/scene.py @@ -14,10 +14,11 @@ from compas.geometry import Sphere from compas.geometry import Torus from compas.geometry import Vector + # from compas.geometry import Plane from compas.datastructures import Mesh -from compas.datastructures import Network +from compas.datastructures import Graph from compas.datastructures import VolMesh from compas.geometry import Translation @@ -44,7 +45,7 @@ mesh = Mesh.from_polyhedron(8) -network = Network.from_nodes_and_edges([(0, 0, 0), (0, -1.5, 0), (-1, 1, 0), (1, 1, 0)], [(0, 1), (0, 2), (0, 3)]) +graph = Graph.from_nodes_and_edges([(0, 0, 0), (0, -1.5, 0), (-1, 1, 0), (1, 1, 0)], [(0, 1), (0, 2), (0, 3)]) volmesh = VolMesh.from_meshgrid(1, 1, 1, 2, 2, 2) @@ -68,7 +69,7 @@ # scene.add(plane) scene.add(mesh) -scene.add(network) +scene.add(graph) scene.add(volmesh) diff --git a/src/compas/datastructures/__init__.py b/src/compas/datastructures/__init__.py index 136e4fbb0e8b..de8338863ab8 100644 --- a/src/compas/datastructures/__init__.py +++ b/src/compas/datastructures/__init__.py @@ -11,11 +11,7 @@ # Graphs # ============================================================================= -# ============================================================================= -# Networks -# ============================================================================= - -from .network.planarity import network_embed_in_plane_proxy # noqa: F401 +from .graph.planarity import graph_embed_in_plane_proxy # noqa: F401 # ============================================================================= # Halfedges @@ -55,7 +51,7 @@ # Class APIs # ============================================================================= -from .network.network import Network +from .graph.graph import Graph from .halfedge.halfedge import HalfEdge from .mesh.mesh import Mesh from .halfface.halfface import HalfFace @@ -66,10 +62,12 @@ from .cell_network.cell_network import CellNetwork from .tree.tree import Tree, TreeNode +Network = Graph + __all__ = [ "Datastructure", "CellNetwork", - "Network", + "Graph", "HalfEdge", "Mesh", "HalfFace", diff --git a/src/compas/datastructures/assembly/assembly.py b/src/compas/datastructures/assembly/assembly.py index a41ec125d76d..e639052fc41e 100644 --- a/src/compas/datastructures/assembly/assembly.py +++ b/src/compas/datastructures/assembly/assembly.py @@ -3,7 +3,7 @@ from __future__ import division from compas.datastructures import Datastructure -from compas.datastructures import Network +from compas.datastructures import Graph from .exceptions import AssemblyError @@ -19,12 +19,12 @@ class Assembly(Datastructure): ---------- attributes : dict[str, Any] General attributes of the data structure that will be included in the data dict and serialization. - network : :class:`compas.datastructures.Network` - The network that is used under the hood to store the parts and their connections. + graph : :class:`compas.datastructures.Graph` + The graph that is used under the hood to store the parts and their connections. See Also -------- - :class:`compas.datastructures.Network` + :class:`compas.datastructures.Graph` :class:`compas.datastructures.Mesh` :class:`compas.datastructures.VolMesh` @@ -34,21 +34,21 @@ class Assembly(Datastructure): "type": "object", "properties": { "attributes": {"type": "object"}, - "network": Network.DATASCHEMA, + "graph": Graph.DATASCHEMA, }, - "required": ["network"], + "required": ["graph"], } def __init__(self, name=None, **kwargs): super(Assembly, self).__init__() self.attributes = {"name": name or "Assembly"} self.attributes.update(kwargs) - self.network = Network() + self.graph = Graph() self._parts = {} def __str__(self): tpl = "" - return tpl.format(self.network.number_of_nodes(), self.network.number_of_edges()) + return tpl.format(self.graph.number_of_nodes(), self.graph.number_of_edges()) # ========================================================================== # Data @@ -58,14 +58,14 @@ def __str__(self): def data(self): return { "attributes": self.attributes, - "network": self.network.data, + "graph": self.graph.data, } @classmethod def from_data(cls, data): assembly = cls() assembly.attributes.update(data["attributes"] or {}) - assembly.network = Network.from_data(data["network"]) + assembly.graph = Graph.from_data(data["graph"]) assembly._parts = {part.guid: part.key for part in assembly.parts()} # type: ignore return assembly @@ -107,12 +107,12 @@ def add_part(self, part, key=None, **kwargs): Returns ------- int | str - The identifier of the part in the current assembly network. + The identifier of the part in the current assembly graph. """ if part.guid in self._parts: raise AssemblyError("Part already added to the assembly") - key = self.network.add_node(key=key, part=part, **kwargs) + key = self.graph.add_node(key=key, part=part, **kwargs) part.key = key self._parts[part.guid] = part.key return key @@ -143,9 +143,9 @@ def add_connection(self, a, b, **kwargs): error_msg = "Both parts have to be added to the assembly before a connection can be created." if a.key is None or b.key is None: raise AssemblyError(error_msg) - if not self.network.has_node(a.key) or not self.network.has_node(b.key): + if not self.graph.has_node(a.key) or not self.graph.has_node(b.key): raise AssemblyError(error_msg) - return self.network.add_edge(a.key, b.key, **kwargs) + return self.graph.add_edge(a.key, b.key, **kwargs) def delete_part(self, part): """Remove a part from the assembly. @@ -161,7 +161,7 @@ def delete_part(self, part): """ del self._parts[part.guid] - self.network.delete_node(key=part.key) + self.graph.delete_node(key=part.key) def delete_connection(self, edge): """Delete a connection between two parts. @@ -176,7 +176,7 @@ def delete_connection(self, edge): None """ - self.network.delete_edge(edge=edge) + self.graph.delete_edge(edge=edge) def parts(self): """The parts of the assembly. @@ -187,8 +187,8 @@ def parts(self): The individual parts of the assembly. """ - for node in self.network.nodes(): - yield self.network.node_attribute(node, "part") + for node in self.graph.nodes(): + yield self.graph.node_attribute(node, "part") def connections(self, data=False): """Iterate over the connections between the parts. @@ -205,7 +205,7 @@ def connections(self, data=False): If `data` is True, the next connector identifier and its attributes as a ((u, v), attr) tuple. """ - return self.network.edges(data) + return self.graph.edges(data) def find(self, guid): """Find a part in the assembly by its GUID. @@ -228,7 +228,7 @@ def find(self, guid): if key is None: return None - return self.network.node_attribute(key, "part") + return self.graph.node_attribute(key, "part") def find_by_key(self, key): """Find a part in the assembly by its key. @@ -245,7 +245,7 @@ def find_by_key(self, key): or None if the part can't be found. """ - if key not in self.network.node: + if key not in self.graph.node: return None - return self.network.node_attribute(key, "part") + return self.graph.node_attribute(key, "part") diff --git a/src/compas/datastructures/cell_network/cell_network.py b/src/compas/datastructures/cell_network/cell_network.py index 36e8ed6f0a44..ceda4bb10171 100644 --- a/src/compas/datastructures/cell_network/cell_network.py +++ b/src/compas/datastructures/cell_network/cell_network.py @@ -4,7 +4,7 @@ from compas.datastructures import HalfFace from compas.datastructures import Mesh -from compas.datastructures import Network +from compas.datastructures import Graph from compas.geometry import Line from compas.geometry import Point from compas.geometry import Polygon @@ -1227,17 +1227,18 @@ def data(self, data): # -------------------------------------------------------------------------- def to_network(self): - """Convert the cell network to a network. + """Convert the cell network to a graph. Returns ------- - :class:`compas.datastructures.Network` - A network object. + :class:`compas.datastructures.Graph` + A graph object. + """ - network = Network() + graph = Graph() for vertex, attr in self.vertices(data=True): x, y, z = self.vertex_coordinates(vertex) - network.add_node(key=vertex, x=x, y=y, z=z, attr_dict=attr) + graph.add_node(key=vertex, x=x, y=y, z=z, attr_dict=attr) for (u, v), attr in self.edges(data=True): - network.add_edge(u, v, attr_dict=attr) - return network + graph.add_edge(u, v, attr_dict=attr) + return graph diff --git a/src/compas/datastructures/network/__init__.py b/src/compas/datastructures/graph/__init__.py similarity index 100% rename from src/compas/datastructures/network/__init__.py rename to src/compas/datastructures/graph/__init__.py diff --git a/src/compas/datastructures/network/duality.py b/src/compas/datastructures/graph/duality.py similarity index 71% rename from src/compas/datastructures/network/duality.py rename to src/compas/datastructures/graph/duality.py index 994254694b72..2786296650f5 100644 --- a/src/compas/datastructures/network/duality.py +++ b/src/compas/datastructures/graph/duality.py @@ -12,13 +12,13 @@ PI2 = 2.0 * pi -def network_find_cycles(network, breakpoints=None): - """Find the faces of a network. +def graph_find_cycles(graph, breakpoints=None): + """Find the faces of a graph. Parameters ---------- - network : :class:`compas.datastructures.Network` - The network object. + graph : :class:`compas.datastructures.Graph` + The graph object. breakpoints : list, optional The vertices at which to break the found faces. @@ -32,75 +32,75 @@ def network_find_cycles(network, breakpoints=None): Warnings -------- This algorithms is essentially a wall follower (a type of maze-solving algorithm). - It relies on the geometry of the network to be repesented as a planar, + It relies on the geometry of the graph to be repesented as a planar, straight-line embedding. It determines an ordering of the neighboring vertices - around each vertex, and then follows the *walls* of the network, always + around each vertex, and then follows the *walls* of the graph, always taking turns in the same direction. """ if not breakpoints: breakpoints = [] - for u, v in network.edges(): - network.adjacency[u][v] = None - network.adjacency[v][u] = None + for u, v in graph.edges(): + graph.adjacency[u][v] = None + graph.adjacency[v][u] = None - network_sort_neighbors(network) + graph_sort_neighbors(graph) - leaves = list(network.leaves()) + leaves = list(graph.leaves()) if leaves: - key_xy = list(zip(leaves, network.nodes_attributes("xy", keys=leaves))) + key_xy = list(zip(leaves, graph.nodes_attributes("xy", keys=leaves))) else: - key_xy = list(zip(network.nodes(), network.nodes_attributes("xy"))) + key_xy = list(zip(graph.nodes(), graph.nodes_attributes("xy"))) u = sorted(key_xy, key=lambda x: (x[1][1], x[1][0]))[0][0] cycles = {} found = {} ckey = 0 - v = network_node_find_first_neighbor(network, u) - cycle = network_find_edge_cycle(network, (u, v)) + v = graph_node_find_first_neighbor(graph, u) + cycle = graph_find_edge_cycle(graph, (u, v)) frozen = frozenset(cycle) found[frozen] = ckey cycles[ckey] = cycle for a, b in pairwise(cycle + cycle[:1]): - network.adjacency[a][b] = ckey + graph.adjacency[a][b] = ckey ckey += 1 - for u, v in network.edges(): - if network.adjacency[u][v] is None: - cycle = network_find_edge_cycle(network, (u, v)) + for u, v in graph.edges(): + if graph.adjacency[u][v] is None: + cycle = graph_find_edge_cycle(graph, (u, v)) frozen = frozenset(cycle) if frozen not in found: found[frozen] = ckey cycles[ckey] = cycle ckey += 1 for a, b in pairwise(cycle + cycle[:1]): - network.adjacency[a][b] = found[frozen] - if network.adjacency[v][u] is None: - cycle = network_find_edge_cycle(network, (v, u)) + graph.adjacency[a][b] = found[frozen] + if graph.adjacency[v][u] is None: + cycle = graph_find_edge_cycle(graph, (v, u)) frozen = frozenset(cycle) if frozen not in found: found[frozen] = ckey cycles[ckey] = cycle ckey += 1 for a, b in pairwise(cycle + cycle[:1]): - network.adjacency[a][b] = found[frozen] + graph.adjacency[a][b] = found[frozen] cycles = _break_cycles(cycles, breakpoints) return cycles -def network_node_find_first_neighbor(network, key): - nbrs = network.neighbors(key) +def graph_node_find_first_neighbor(graph, key): + nbrs = graph.neighbors(key) if len(nbrs) == 1: return nbrs[0] ab = [-1.0, -1.0, 0.0] - a = network.node_coordinates(key, "xyz") + a = graph.node_coordinates(key, "xyz") b = [a[0] + ab[0], a[1] + ab[1], 0] angles = [] for nbr in nbrs: - c = network.node_coordinates(nbr, "xyz") + c = graph.node_coordinates(nbr, "xyz") ac = [c[0] - a[0], c[1] - a[1], 0] alpha = angle_vectors(ab, ac) if is_ccw_xy(a, b, c, True): @@ -109,14 +109,14 @@ def network_node_find_first_neighbor(network, key): return nbrs[angles.index(min(angles))] -def network_sort_neighbors(network, ccw=True): +def graph_sort_neighbors(graph, ccw=True): sorted_neighbors = {} - xyz = {key: network.node_coordinates(key) for key in network.nodes()} - for key in network.nodes(): - nbrs = network.neighbors(key) + xyz = {key: graph.node_coordinates(key) for key in graph.nodes()} + for key in graph.nodes(): + nbrs = graph.neighbors(key) sorted_neighbors[key] = node_sort_neighbors(key, nbrs, xyz, ccw=ccw) for key, nbrs in sorted_neighbors.items(): - network.node_attribute(key, "neighbors", nbrs[::-1]) + graph.node_attribute(key, "neighbors", nbrs[::-1]) return sorted_neighbors @@ -149,12 +149,12 @@ def node_sort_neighbors(key, nbrs, xyz, ccw=True): return ordered -def network_find_edge_cycle(network, edge): +def graph_find_edge_cycle(graph, edge): u, v = edge cycle = [u] while True: cycle.append(v) - nbrs = network.node_attribute(v, "neighbors") + nbrs = graph.node_attribute(v, "neighbors") nbr = nbrs[nbrs.index(u) - 1] u, v = v, nbr if v == cycle[0]: diff --git a/src/compas/datastructures/network/network.py b/src/compas/datastructures/graph/graph.py similarity index 94% rename from src/compas/datastructures/network/network.py rename to src/compas/datastructures/graph/graph.py index e89a4e2f7c29..1354da76f71d 100644 --- a/src/compas/datastructures/network/network.py +++ b/src/compas/datastructures/graph/graph.py @@ -34,21 +34,21 @@ from compas.datastructures.attributes import NodeAttributeView from compas.datastructures.attributes import EdgeAttributeView -from .operations.split import network_split_edge -from .operations.join import network_join_edges +from .operations.split import graph_split_edge +from .operations.join import graph_join_edges -from .planarity import network_is_crossed -from .planarity import network_is_planar -from .planarity import network_is_planar_embedding -from .planarity import network_is_xy -from .planarity import network_count_crossings -from .planarity import network_find_crossings -from .planarity import network_embed_in_plane -from .smoothing import network_smooth_centroid -from .duality import network_find_cycles +from .planarity import graph_is_crossed +from .planarity import graph_is_planar +from .planarity import graph_is_planar_embedding +from .planarity import graph_is_xy +from .planarity import graph_count_crossings +from .planarity import graph_find_crossings +from .planarity import graph_embed_in_plane +from .smoothing import graph_smooth_centroid +from .duality import graph_find_cycles -class Network(Datastructure): +class Graph(Datastructure): """Data structure for describing the relationships between nodes connected by edges. Parameters @@ -58,7 +58,7 @@ class Network(Datastructure): default_edge_attributes: dict, optional Default values for edge attributes. **kwargs : dict, optional - Additional attributes to add to the network. + Additional attributes to add to the graph. Attributes ---------- @@ -100,22 +100,22 @@ class Network(Datastructure): ], } - split_edge = network_split_edge - join_edges = network_join_edges - smooth = network_smooth_centroid + split_edge = graph_split_edge + join_edges = graph_join_edges + smooth = graph_smooth_centroid - is_crossed = network_is_crossed - is_planar = network_is_planar - is_planar_embedding = network_is_planar_embedding - is_xy = network_is_xy - count_crossings = network_count_crossings - find_crossings = network_find_crossings - embed_in_plane = network_embed_in_plane + is_crossed = graph_is_crossed + is_planar = graph_is_planar + is_planar_embedding = graph_is_planar_embedding + is_xy = graph_is_xy + count_crossings = graph_count_crossings + find_crossings = graph_find_crossings + embed_in_plane = graph_embed_in_plane - find_cycles = network_find_cycles + find_cycles = graph_find_cycles def __init__(self, default_node_attributes=None, default_edge_attributes=None, **kwargs): - super(Network, self).__init__(**kwargs) + super(Graph, self).__init__(**kwargs) self._max_node = -1 self.node = {} self.edge = {} @@ -128,7 +128,7 @@ def __init__(self, default_node_attributes=None, default_edge_attributes=None, * self.default_edge_attributes.update(default_edge_attributes) def __str__(self): - tpl = "" + tpl = "" return tpl.format(self.number_of_nodes(), self.number_of_edges()) # -------------------------------------------------------------------------- @@ -196,7 +196,7 @@ def from_edges(cls, edges): See Also -------- - :meth:`from_networkx` + :meth:`from_graphx` """ graph = cls() @@ -239,7 +239,7 @@ def from_networkx(cls, graph): @classmethod def from_obj(cls, filepath, precision=None): - """Construct a network from the data contained in an OBJ file. + """Construct a graph from the data contained in an OBJ file. Parameters ---------- @@ -250,8 +250,8 @@ def from_obj(cls, filepath, precision=None): Returns ------- - :class:`compas.datastructures.Network` - A network object. + :class:`compas.datastructures.Graph` + A graph object. See Also -------- @@ -260,20 +260,20 @@ def from_obj(cls, filepath, precision=None): :class:`compas.files.OBJ` """ - network = cls() + graph = cls() obj = OBJ(filepath, precision) obj.read() nodes = obj.vertices edges = obj.lines for i, (x, y, z) in enumerate(nodes): # type: ignore - network.add_node(i, x=x, y=y, z=z) + graph.add_node(i, x=x, y=y, z=z) for edge in edges: # type: ignore - network.add_edge(*edge) - return network + graph.add_edge(*edge) + return graph @classmethod def from_lines(cls, lines, precision=None): - """Construct a network from a set of lines represented by their start and end point coordinates. + """Construct a graph from a set of lines represented by their start and end point coordinates. Parameters ---------- @@ -285,8 +285,8 @@ def from_lines(cls, lines, precision=None): Returns ------- - :class:`compas.datastructures.Network` - A network object. + :class:`compas.datastructures.Graph` + A graph object. See Also -------- @@ -294,7 +294,7 @@ def from_lines(cls, lines, precision=None): :meth:`from_obj`, :meth:`from_nodes_and_edges`, :meth:`from_pointcloud` """ - network = cls() + graph = cls() edges = [] node = {} for line in lines: @@ -308,16 +308,16 @@ def from_lines(cls, lines, precision=None): key_index = dict((k, i) for i, k in enumerate(iter(node))) for key, xyz in iter(node.items()): i = key_index[key] - network.add_node(i, x=xyz[0], y=xyz[1], z=xyz[2]) + graph.add_node(i, x=xyz[0], y=xyz[1], z=xyz[2]) for u, v in edges: i = key_index[u] j = key_index[v] - network.add_edge(i, j) - return network + graph.add_edge(i, j) + return graph @classmethod def from_nodes_and_edges(cls, nodes, edges): - """Construct a network from nodes and edges. + """Construct a graph from nodes and edges. Parameters ---------- @@ -327,8 +327,8 @@ def from_nodes_and_edges(cls, nodes, edges): Returns ------- - :class:`compas.datastructures.Network` - A network object. + :class:`compas.datastructures.Graph` + A graph object. See Also -------- @@ -336,23 +336,23 @@ def from_nodes_and_edges(cls, nodes, edges): :meth:`from_obj`, :meth:`from_lines`, :meth:`from_pointcloud` """ - network = cls() + graph = cls() if isinstance(nodes, Mapping): for key, (x, y, z) in nodes.items(): - network.add_node(key, x=x, y=y, z=z) + graph.add_node(key, x=x, y=y, z=z) else: for i, (x, y, z) in enumerate(nodes): - network.add_node(i, x=x, y=y, z=z) + graph.add_node(i, x=x, y=y, z=z) for u, v in edges: - network.add_edge(u, v) + graph.add_edge(u, v) - return network + return graph @classmethod def from_pointcloud(cls, cloud, degree=3): - """Construct a network from random connections between the points of a pointcloud. + """Construct a graph from random connections between the points of a pointcloud. Parameters ---------- @@ -363,8 +363,8 @@ def from_pointcloud(cls, cloud, degree=3): Returns ------- - :class:`compas.datastructures.Network` - A network object. + :class:`compas.datastructures.Graph` + A graph object. See Also -------- @@ -372,20 +372,20 @@ def from_pointcloud(cls, cloud, degree=3): :meth:`from_obj`, :meth:`from_lines`, :meth:`from_nodes_and_edges` """ - network = cls() + graph = cls() for x, y, z in cloud: - network.add_node(x=x, y=y, z=z) - for u in network.nodes(): - for v in network.node_sample(size=degree): - network.add_edge(u, v) - return network + graph.add_node(x=x, y=y, z=z) + for u in graph.nodes(): + for v in graph.node_sample(size=degree): + graph.add_edge(u, v) + return graph # -------------------------------------------------------------------------- # Converters # -------------------------------------------------------------------------- def to_obj(self): - """Write the network to an OBJ file. + """Write the graph to an OBJ file. Parameters ---------- @@ -405,12 +405,12 @@ def to_obj(self): raise NotImplementedError def to_points(self): - """Return the coordinates of the network. + """Return the coordinates of the graph. Returns ------- list[list[float]] - A list with the coordinates of the vertices of the network. + A list with the coordinates of the vertices of the graph. See Also -------- @@ -421,7 +421,7 @@ def to_points(self): return [self.node_coordinates(key) for key in self.nodes()] def to_lines(self): - """Return the lines of the network as pairs of start and end point coordinates. + """Return the lines of the graph as pairs of start and end point coordinates. Returns ------- @@ -437,7 +437,7 @@ def to_lines(self): return [self.edge_coordinates(edge) for edge in self.edges()] def to_nodes_and_edges(self): - """Return the nodes and edges of a network. + """Return the nodes and edges of a graph. Returns ------- @@ -488,7 +488,7 @@ def to_networkx(self): # -------------------------------------------------------------------------- def clear(self): - """Clear all the network data. + """Clear all the graph data. Returns ------- @@ -802,7 +802,7 @@ def delete_node(self, key): del self.adjacency[u][v] def delete_edge(self, edge): - """Delete an edge from the network. + """Delete an edge from the graph. Parameters ---------- @@ -889,25 +889,25 @@ def number_of_edges(self): return len(list(self.edges())) def is_connected(self): - """Verify that the network is connected. + """Verify that the graph is connected. Returns ------- bool - True, if the network is connected. + True, if the graph is connected. False, otherwise. Notes ----- - A network is connected if for every two vertices a path exists connecting them. + A graph is connected if for every two vertices a path exists connecting them. Examples -------- >>> import compas - >>> from compas.datastructures import Network - >>> network = Network.from_obj(compas.get('lines.obj')) - >>> network.is_connected() + >>> from compas.datastructures import Graph + >>> graph = Graph.from_obj(compas.get('lines.obj')) + >>> graph.is_connected() True """ @@ -921,7 +921,7 @@ def is_connected(self): # -------------------------------------------------------------------------- def nodes(self, data=False): - """Iterate over the nodes of the network. + """Iterate over the nodes of the graph. Parameters ---------- @@ -1057,7 +1057,7 @@ def nodes_where_predicate(self, predicate, data=False): yield key def edges(self, data=False): - """Iterate over the edges of the network. + """Iterate over the edges of the graph. Parameters ---------- @@ -1676,7 +1676,7 @@ def edges_attributes(self, names=None, values=None, keys=None): # -------------------------------------------------------------------------- def has_node(self, key): - """Verify if a specific node is present in the network. + """Verify if a specific node is present in the graph. Parameters ---------- @@ -1721,7 +1721,7 @@ def is_leaf(self, key): return self.degree(key) == 1 def leaves(self): - """Return all leaves of the network. + """Return all leaves of the graph. Returns ------- @@ -1933,7 +1933,7 @@ def node_edges(self, key): # -------------------------------------------------------------------------- def has_edge(self, edge, directed=True): - """Verify if the network contains a specific edge. + """Verify if the graph contains a specific edge. Parameters ---------- @@ -2254,7 +2254,7 @@ def edge_length(self, edge): # -------------------------------------------------------------------------- def transform(self, transformation): - """Transform all nodes of the network. + """Transform all nodes of the graph. Parameters ---------- @@ -2345,10 +2345,10 @@ def complement(self): Examples -------- >>> import compas - >>> from compas.datastructures import Network - >>> network = Network.from_obj(compas.get('lines.obj')) - >>> complement = network.complement() - >>> any(complement.has_edge(u, v, directed=False) for u, v in network.edges()) + >>> from compas.datastructures import Graph + >>> graph = Graph.from_obj(compas.get('lines.obj')) + >>> complement = graph.complement() + >>> any(complement.has_edge(u, v, directed=False) for u, v in graph.edges()) False """ @@ -2372,7 +2372,7 @@ def complement(self): # -------------------------------------------------------------------------- def adjacency_matrix(self, rtype="array"): - """Creates a node adjacency matrix from a Network datastructure. + """Creates a node adjacency matrix from a Graph datastructure. Parameters ---------- @@ -2392,7 +2392,7 @@ def adjacency_matrix(self, rtype="array"): return adjacency_matrix(adjacency, rtype=rtype) def connectivity_matrix(self, rtype="array"): - """Creates a connectivity matrix from a Network datastructure. + """Creates a connectivity matrix from a Graph datastructure. Parameters ---------- @@ -2412,7 +2412,7 @@ def connectivity_matrix(self, rtype="array"): return connectivity_matrix(edges, rtype=rtype) def degree_matrix(self, rtype="array"): - """Creates a degree matrix from a Network datastructure. + """Creates a degree matrix from a Graph datastructure. Parameters ---------- @@ -2432,7 +2432,7 @@ def degree_matrix(self, rtype="array"): return degree_matrix(adjacency, rtype=rtype) def laplacian_matrix(self, normalize=False, rtype="array"): - """Creates a Laplacian matrix from a Network datastructure. + """Creates a Laplacian matrix from a Graph datastructure. Parameters ---------- diff --git a/src/compas/datastructures/network/operations/__init__.py b/src/compas/datastructures/graph/operations/__init__.py similarity index 100% rename from src/compas/datastructures/network/operations/__init__.py rename to src/compas/datastructures/graph/operations/__init__.py diff --git a/src/compas/datastructures/network/operations/join.py b/src/compas/datastructures/graph/operations/join.py similarity index 63% rename from src/compas/datastructures/network/operations/join.py rename to src/compas/datastructures/graph/operations/join.py index ee26bcebacbc..9a06ecbdf36d 100644 --- a/src/compas/datastructures/network/operations/join.py +++ b/src/compas/datastructures/graph/operations/join.py @@ -6,20 +6,20 @@ from compas.utilities import pairwise -def network_join_edges(network, key): +def graph_join_edges(graph, key): """Join the edges incidental on the given node, if there are exactly two incident edges. Parameters ---------- - network : :class:`compas.geometry.Network` - A network data structure. + graph : :class:`compas.geometry.Graph` + A graph data structure. key : hashable The node identifier. Returns ------- None - The network is modified in place. + The graph is modified in place. Notes ----- @@ -28,39 +28,39 @@ def network_join_edges(network, key): Therefore, the new edge has only default edge attributes. """ - nbrs = network.vertex_neighbors(key) + nbrs = graph.vertex_neighbors(key) if len(nbrs) != 2: return a, b = nbrs - if a in network.edge[key]: - del network.edge[key][a] + if a in graph.edge[key]: + del graph.edge[key][a] else: - del network.edge[a][key] - del network.halfedge[key][a] - del network.halfedge[a][key] - if b in network.edge[key]: - del network.edge[key][b] + del graph.edge[a][key] + del graph.halfedge[key][a] + del graph.halfedge[a][key] + if b in graph.edge[key]: + del graph.edge[key][b] else: - del network.edge[b][key] - del network.halfedge[key][b] - del network.halfedge[b][key] - del network.vertex[key] - del network.halfedge[key] - del network.edge[key] + del graph.edge[b][key] + del graph.halfedge[key][b] + del graph.halfedge[b][key] + del graph.vertex[key] + del graph.halfedge[key] + del graph.edge[key] # set attributes based on average of two joining edges? - network.add_edge((a, b)) + graph.add_edge((a, b)) -def network_polylines(network, splits=None): - """Join network edges into polylines. +def graph_polylines(graph, splits=None): + """Join graph edges into polylines. - The polylines stop at points with a valency different from 2 in the network of line. + The polylines stop at points with a valency different from 2 in the graph of line. Optional splits can be included. Parameters ---------- - network : Network - A network. + graph : Graph + A graph. splits : sequence[[float, float, float] | :class:`compas.geometry.Point`], optional List of point coordinates for polyline splits. @@ -76,7 +76,7 @@ def network_polylines(network, splits=None): where a ... f are different point coordinates. This will result in the following polylines (a, b, c), (c, d) and (c, e, f). - >>> from compas.datastructures import Network + >>> from compas.datastructures import Graph >>> a = [0., 0., 0.] >>> b = [1., 0., 0.] >>> c = [2., 0., 0.] @@ -84,8 +84,8 @@ def network_polylines(network, splits=None): >>> e = [3., 0., 0.] >>> f = [4., 0., 0.] >>> lines = [(a, b), (b, c), (c, d), (c, e), (e, f)] - >>> network = Network.from_lines(lines) - >>> len(network_polylines(network)) == 3 + >>> graph = Graph.from_lines(lines) + >>> len(graph_polylines(graph)) == 3 True """ @@ -95,7 +95,7 @@ def network_polylines(network, splits=None): stop_geom_keys = set([TOL.geometric_key(xyz) for xyz in splits]) polylines = [] - edges_to_visit = set(network.edges()) + edges_to_visit = set(graph.edges()) # initiate a polyline from an unvisited edge while len(edges_to_visit) > 0: @@ -105,18 +105,18 @@ def network_polylines(network, splits=None): while polyline[0] != polyline[-1]: # ... or until both end are non-two-valent vertices if ( - len(network.neighbors(polyline[-1])) != 2 - or TOL.geometric_key(network.node_coordinates(polyline[-1])) in stop_geom_keys + len(graph.neighbors(polyline[-1])) != 2 + or TOL.geometric_key(graph.node_coordinates(polyline[-1])) in stop_geom_keys ): polyline = list(reversed(polyline)) if ( - len(network.neighbors(polyline[-1])) != 2 - or TOL.geometric_key(network.node_coordinates(polyline[-1])) in stop_geom_keys + len(graph.neighbors(polyline[-1])) != 2 + or TOL.geometric_key(graph.node_coordinates(polyline[-1])) in stop_geom_keys ): break # add next edge - polyline.append([nbr for nbr in network.neighbors(polyline[-1]) if nbr != polyline[-2]][0]) + polyline.append([nbr for nbr in graph.neighbors(polyline[-1]) if nbr != polyline[-2]][0]) # delete polyline edges from the list of univisted edges for u, v in pairwise(polyline): @@ -127,4 +127,4 @@ def network_polylines(network, splits=None): polylines.append(polyline) - return [[network.node_coordinates(vkey) for vkey in polyline] for polyline in polylines] + return [[graph.node_coordinates(vkey) for vkey in polyline] for polyline in polylines] diff --git a/src/compas/datastructures/network/operations/split.py b/src/compas/datastructures/graph/operations/split.py similarity index 59% rename from src/compas/datastructures/network/operations/split.py rename to src/compas/datastructures/graph/operations/split.py index 3eaebd3a523f..6e1c64e858db 100644 --- a/src/compas/datastructures/network/operations/split.py +++ b/src/compas/datastructures/graph/operations/split.py @@ -3,7 +3,7 @@ from __future__ import division -def network_split_edge(network, edge, t=0.5): +def graph_split_edge(graph, edge, t=0.5): """Split and edge by inserting a node along its length. Parameters @@ -23,11 +23,11 @@ def network_split_edge(network, edge, t=0.5): ValueError If `t` is not in the range 0-1. Exception - If the edge is not part of the network. + If the edge is not part of the graph. """ u, v = edge - if not network.has_edge(u, v): + if not graph.has_edge(u, v): return if t <= 0.0: @@ -36,28 +36,28 @@ def network_split_edge(network, edge, t=0.5): raise ValueError("t should be smaller than 1.0.") # the split node - x, y, z = network.edge_point(edge, t) - w = network.add_node(x=x, y=y, z=z) + x, y, z = graph.edge_point(edge, t) + w = graph.add_node(x=x, y=y, z=z) - network.add_edge((u, w)) - network.add_edge((w, v)) + graph.add_edge((u, w)) + graph.add_edge((w, v)) - if v in network.edge[u]: - del network.edge[u][v] - elif u in network.edge[v]: - del network.edge[v][u] + if v in graph.edge[u]: + del graph.edge[u][v] + elif u in graph.edge[v]: + del graph.edge[v][u] else: raise Exception # split half-edge UV - network.adjacency[u][w] = None - network.adjacency[w][v] = None - del network.adjacency[u][v] + graph.adjacency[u][w] = None + graph.adjacency[w][v] = None + del graph.adjacency[u][v] # split half-edge VU - network.adjacency[v][w] = None - network.adjacency[w][u] = None - del network.adjacency[v][u] + graph.adjacency[v][w] = None + graph.adjacency[w][u] = None + del graph.adjacency[v][u] # return the key of the split node return w diff --git a/src/compas/datastructures/network/planarity.py b/src/compas/datastructures/graph/planarity.py similarity index 59% rename from src/compas/datastructures/network/planarity.py rename to src/compas/datastructures/graph/planarity.py index aec0c9c1614a..7e6dfa113937 100644 --- a/src/compas/datastructures/network/planarity.py +++ b/src/compas/datastructures/graph/planarity.py @@ -15,40 +15,40 @@ from compas.geometry._core.predicates_2 import is_intersection_segment_segment_xy -def network_embed_in_plane_proxy(data, fixed=None, straightline=True): - from compas.datastructures import Network +def graph_embed_in_plane_proxy(data, fixed=None, straightline=True): + from compas.datastructures import Graph - network = Network.from_data(data) - network_embed_in_plane(network, fixed=fixed, straightline=straightline) - return network.to_data() + graph = Graph.from_data(data) + graph_embed_in_plane(graph, fixed=fixed, straightline=straightline) + return graph.to_data() -def network_is_crossed(network): - """Verify if a network has crossing edges. +def graph_is_crossed(graph): + """Verify if a graph has crossing edges. Parameters ---------- - network : :class:`compas.datastructures.Network` - A network object. + graph : :class:`compas.datastructures.Graph` + A graph object. Returns ------- bool - True if the network has at least one pair of crossing edges. + True if the graph has at least one pair of crossing edges. False otherwise. Notes ----- - This algorithm assumes that the network lies in the XY plane. + This algorithm assumes that the graph lies in the XY plane. """ - for (u1, v1), (u2, v2) in product(network.edges(), network.edges()): + for (u1, v1), (u2, v2) in product(graph.edges(), graph.edges()): if u1 == u2 or v1 == v2 or u1 == v2 or u2 == v1: continue - a = network.node_attributes(u1, "xy") - b = network.node_attributes(v1, "xy") - c = network.node_attributes(u2, "xy") - d = network.node_attributes(v2, "xy") + a = graph.node_attributes(u1, "xy") + b = graph.node_attributes(v1, "xy") + c = graph.node_attributes(u2, "xy") + d = graph.node_attributes(v2, "xy") if is_intersection_segment_segment_xy((a, b), (c, d)): return True return False @@ -67,13 +67,13 @@ def _are_edges_crossed(edges, vertices): return False -def network_count_crossings(network): - """Count the number of crossings (pairs of crossing edges) in the network. +def graph_count_crossings(graph): + """Count the number of crossings (pairs of crossing edges) in the graph. Parameters ---------- - network : :class:`compas.datastructures.Network` - A network object. + graph : :class:`compas.datastructures.Graph` + A graph object. Returns ------- @@ -82,19 +82,19 @@ def network_count_crossings(network): Notes ----- - This algorithm assumes that the network lies in the XY plane. + This algorithm assumes that the graph lies in the XY plane. """ - return len(network_find_crossings(network)) + return len(graph_find_crossings(graph)) -def network_find_crossings(network): - """Identify all pairs of crossing edges in a network. +def graph_find_crossings(graph): + """Identify all pairs of crossing edges in a graph. Parameters ---------- - network : :class:`compas.datastructures.Network` - A network object. + graph : :class:`compas.datastructures.Graph` + A graph object. Returns ------- @@ -103,33 +103,33 @@ def network_find_crossings(network): Notes ----- - This algorithm assumes that the network lies in the XY plane. + This algorithm assumes that the graph lies in the XY plane. """ crossings = set() - for (u1, v1), (u2, v2) in product(network.edges(), network.edges()): + for (u1, v1), (u2, v2) in product(graph.edges(), graph.edges()): if u1 == u2 or v1 == v2 or u1 == v2 or u2 == v1: continue if ((u1, v1), (u2, v2)) in crossings: continue if ((u2, v2), (u1, v1)) in crossings: continue - a = network.node_attributes(u1, "xy") - b = network.node_attributes(v1, "xy") - c = network.node_attributes(u2, "xy") - d = network.node_attributes(v2, "xy") + a = graph.node_attributes(u1, "xy") + b = graph.node_attributes(v1, "xy") + c = graph.node_attributes(u2, "xy") + d = graph.node_attributes(v2, "xy") if is_intersection_segment_segment_xy((a, b), (c, d)): crossings.add(((u1, v1), (u2, v2))) return list(crossings) -def network_is_xy(network): - """Verify that a network lies in the XY plane. +def graph_is_xy(graph): + """Verify that a graph lies in the XY plane. Parameters ---------- - network : :class:`compas.datastructures.Network` - A network object. + graph : :class:`compas.datastructures.Graph` + A graph object. Returns ------- @@ -139,27 +139,27 @@ def network_is_xy(network): """ z = None - for key in network.nodes(): + for key in graph.nodes(): if z is None: - z = network.node_attribute(key, "z") or 0.0 + z = graph.node_attribute(key, "z") or 0.0 else: - if z != network.node_attribute(key, "z") or 0.0: + if z != graph.node_attribute(key, "z") or 0.0: return False return True -def network_is_planar(network): - """Check if the network is planar. +def graph_is_planar(graph): + """Check if the graph is planar. Parameters ---------- - network : :class:`compas.datastructures.Network` - A network object. + graph : :class:`compas.datastructures.Graph` + A graph object. Returns ------- bool - True if the network is planar. + True if the graph is planar. False otherwise. Raises @@ -169,8 +169,8 @@ def network_is_planar(network): Notes ----- - A network is planar if it can be drawn in the plane without crossing edges. - If a network is planar, it can be shown that an embedding of the network in + A graph is planar if it can be drawn in the plane without crossing edges. + If a graph is planar, it can be shown that an embedding of the graph in the plane exists, and, furthermore, that straight-line embedding in the plane exists. @@ -181,35 +181,35 @@ def network_is_planar(network): print("NetworkX is not installed.") raise - nxgraph = network.to_networkx() + nxgraph = graph.to_networkx() return nx.is_planar(nxgraph) -def network_is_planar_embedding(network): - """Verify that a network is embedded in the plane without crossing edges. +def graph_is_planar_embedding(graph): + """Verify that a graph is embedded in the plane without crossing edges. Parameters ---------- - network : :class:`compas.datastructures.Network` - A network object. + graph : :class:`compas.datastructures.Graph` + A graph object. Returns ------- bool - True if the network is embedded in the plane without crossing edges. + True if the graph is embedded in the plane without crossing edges. Fase otherwise. """ - return network_is_planar(network) and network_is_xy(network) and not network_is_crossed(network) + return graph_is_planar(graph) and graph_is_xy(graph) and not graph_is_crossed(graph) -def network_embed_in_plane(network, fixed=None, straightline=True): - """Embed the network in the plane. +def graph_embed_in_plane(graph, fixed=None, straightline=True): + """Embed the graph in the plane. Parameters ---------- - network : :class:`compas.datastructures.Network` - A network object. + graph : :class:`compas.datastructures.Graph` + A graph object. fixed : [hashable, hashable], optional Two fixed points. straightline : bool, optional @@ -233,14 +233,14 @@ def network_embed_in_plane(network, fixed=None, straightline=True): print("NetworkX is not installed. Get NetworkX at https://networkx.github.io/.") raise - x = network.nodes_attribute("x") - y = network.nodes_attribute("y") + x = graph.nodes_attribute("x") + y = graph.nodes_attribute("y") xmin, xmax = min(x), max(x) ymin, ymax = min(y), max(y) xspan = xmax - xmin yspan = ymax - ymin - edges = [(u, v) for u, v in network.edges() if not network.is_leaf(u) and not network.is_leaf(v)] + edges = [(u, v) for u, v in graph.edges() if not graph.is_leaf(u) and not graph.is_leaf(v)] is_embedded = False pos = {} @@ -262,8 +262,8 @@ def network_embed_in_plane(network, fixed=None, straightline=True): if fixed: a, b = fixed - p0 = network.node_attributes(a, "xy") - p1 = network.node_attributes(b, "xy") + p0 = graph.node_attributes(a, "xy") + p1 = graph.node_attributes(b, "xy") p2 = pos[b] vec0 = subtract_vectors_xy(p1, p0) vec1 = subtract_vectors_xy(pos[b], pos[a]) @@ -290,9 +290,9 @@ def network_embed_in_plane(network, fixed=None, straightline=True): pos[key][0] += t[0] pos[key][1] += t[1] - # update network node coordinates - for key in network.nodes(): + # update graph node coordinates + for key in graph.nodes(): if key in pos: - network.node_attributes(key, "xy", pos[key]) + graph.node_attributes(key, "xy", pos[key]) return True diff --git a/src/compas/datastructures/network/smoothing.py b/src/compas/datastructures/graph/smoothing.py similarity index 73% rename from src/compas/datastructures/network/smoothing.py rename to src/compas/datastructures/graph/smoothing.py index 271fd53d2827..c91349bfb850 100644 --- a/src/compas/datastructures/network/smoothing.py +++ b/src/compas/datastructures/graph/smoothing.py @@ -5,15 +5,15 @@ from compas.geometry import centroid_points -def network_smooth_centroid(network, fixed=None, kmax=100, damping=0.5, callback=None, callback_args=None): - """Smooth a network by moving every free node to the centroid of its neighbors. +def graph_smooth_centroid(graph, fixed=None, kmax=100, damping=0.5, callback=None, callback_args=None): + """Smooth a graph by moving every free node to the centroid of its neighbors. Parameters ---------- - network : Mesh - A network object. + graph : Mesh + A graph object. fixed : list, optional - The fixed nodes of the network. + The fixed nodes of the graph. kmax : int, optional The maximum number of iterations. damping : float, optional @@ -41,15 +41,15 @@ def network_smooth_centroid(network, fixed=None, kmax=100, damping=0.5, callback fixed = set(fixed) for k in range(kmax): - key_xyz = {key: network.node_coordinates(key) for key in network.nodes()} + key_xyz = {key: graph.node_coordinates(key) for key in graph.nodes()} - for key, attr in network.nodes(True): + for key, attr in graph.nodes(True): if key in fixed: continue x, y, z = key_xyz[key] - cx, cy, cz = centroid_points([key_xyz[nbr] for nbr in network.neighbors(key)]) + cx, cy, cz = centroid_points([key_xyz[nbr] for nbr in graph.neighbors(key)]) attr["x"] += damping * (cx - x) attr["y"] += damping * (cy - y) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 9d41369231da..d30e065ed803 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -358,11 +358,11 @@ def from_lines(cls, lines, delete_boundary_face=False, precision=None): # type: A mesh object. """ - from compas.datastructures import Network + from compas.datastructures import Graph - network = Network.from_lines(lines, precision=precision) - vertices = network.to_points() - faces = network.find_cycles() + graph = Graph.from_lines(lines, precision=precision) + vertices = graph.to_points() + faces = graph.find_cycles() mesh = cls.from_vertices_and_faces(vertices, faces) if delete_boundary_face: mesh.delete_face(0) diff --git a/src/compas/scene/__init__.py b/src/compas/scene/__init__.py index 773901cac8fd..14431c21faf8 100644 --- a/src/compas/scene/__init__.py +++ b/src/compas/scene/__init__.py @@ -12,7 +12,7 @@ from .exceptions import NoSceneObjectContextError from .sceneobject import SceneObject from .meshobject import MeshObject -from .networkobject import NetworkObject +from .graphobject import GraphObject from .geometryobject import GeometryObject from .volmeshobject import VolMeshObject @@ -31,7 +31,7 @@ "NoSceneObjectContextError", "SceneObject", "MeshObject", - "NetworkObject", + "GraphObject", "GeometryObject", "VolMeshObject", "Scene", diff --git a/src/compas/scene/networkobject.py b/src/compas/scene/graphobject.py similarity index 81% rename from src/compas/scene/networkobject.py rename to src/compas/scene/graphobject.py index 85f2818fd50f..f25987e1a6cc 100644 --- a/src/compas/scene/networkobject.py +++ b/src/compas/scene/graphobject.py @@ -9,21 +9,21 @@ from .descriptors.colordict import ColorDictAttribute -class NetworkObject(SceneObject): - """Scene object for drawing network data structures. +class GraphObject(SceneObject): + """Scene object for drawing graph data structures. Parameters ---------- - network : :class:`compas.datastructures.Network` - A COMPAS network. + graph : :class:`compas.datastructures.Graph` + A COMPAS graph. Attributes ---------- - network : :class:`compas.datastructures.Network` - The COMPAS network associated with the scene object. + graph : :class:`compas.datastructures.Graph` + The COMPAS graph associated with the scene object. node_xyz : dict[hashable, list[float]] Mapping between nodes and their view coordinates. - The default view coordinates are the actual coordinates of the nodes of the network. + The default view coordinates are the actual coordinates of the nodes of the graph. nodecolor : :class:`compas.colors.ColorDict` Mapping between nodes and RGB color values. edgecolor : :class:`compas.colors.ColorDict` @@ -47,11 +47,11 @@ class NetworkObject(SceneObject): nodecolor = ColorDictAttribute() edgecolor = ColorDictAttribute() - def __init__(self, network, **kwargs): - super(NetworkObject, self).__init__(item=network, **kwargs) - self._network = None + def __init__(self, graph, **kwargs): + super(GraphObject, self).__init__(item=graph, **kwargs) + self._graph = None self._node_xyz = None - self.network = network + self.graph = graph self.nodecolor = kwargs.get("nodecolor", self.color) self.edgecolor = kwargs.get("edgecolor", self.color) self.nodesize = kwargs.get("nodesize", 1.0) @@ -60,12 +60,12 @@ def __init__(self, network, **kwargs): self.show_edges = kwargs.get("show_edges", True) @property - def network(self): - return self._network + def graph(self): + return self._graph - @network.setter - def network(self, network): - self._network = network + @graph.setter + def graph(self, graph): + self._graph = graph self._transformation = None self._node_xyz = None @@ -81,9 +81,9 @@ def transformation(self, transformation): @property def node_xyz(self): if self._node_xyz is None: - points = self.network.nodes_attributes("xyz") # type: ignore + points = self.graph.nodes_attributes("xyz") # type: ignore points = transform_points(points, self.worldtransformation) - self._node_xyz = dict(zip(self.network.nodes(), points)) # type: ignore + self._node_xyz = dict(zip(self.graph.nodes(), points)) # type: ignore return self._node_xyz @node_xyz.setter @@ -92,7 +92,7 @@ def node_xyz(self, node_xyz): @abstractmethod def draw_nodes(self, nodes=None, color=None, text=None): - """Draw the nodes of the network. + """Draw the nodes of the graph. Parameters ---------- @@ -117,7 +117,7 @@ def draw_nodes(self, nodes=None, color=None, text=None): @abstractmethod def draw_edges(self, edges=None, color=None, text=None): - """Draw the edges of the network. + """Draw the edges of the graph. Parameters ---------- @@ -141,7 +141,7 @@ def draw_edges(self, edges=None, color=None, text=None): raise NotImplementedError def clear_nodes(self): - """Clear the nodes of the network. + """Clear the nodes of the graph. Returns ------- @@ -151,7 +151,7 @@ def clear_nodes(self): raise NotImplementedError def clear_edges(self): - """Clear the edges of the network. + """Clear the edges of the graph. Returns ------- @@ -161,7 +161,7 @@ def clear_edges(self): raise NotImplementedError def clear(self): - """Clear the nodes and the edges of the network. + """Clear the nodes and the edges of the graph. Returns ------- diff --git a/src/compas/scene/meshobject.py b/src/compas/scene/meshobject.py index 00bdd9045674..80224f24083f 100644 --- a/src/compas/scene/meshobject.py +++ b/src/compas/scene/meshobject.py @@ -45,7 +45,7 @@ class MeshObject(SceneObject): See Also -------- - :class:`compas.scene.NetworkObject` + :class:`compas.scene.GraphObject` :class:`compas.scene.VolMeshObject` """ diff --git a/src/compas/scene/volmeshobject.py b/src/compas/scene/volmeshobject.py index 9c074715446c..3ed16fd2618b 100644 --- a/src/compas/scene/volmeshobject.py +++ b/src/compas/scene/volmeshobject.py @@ -51,7 +51,7 @@ class VolMeshObject(SceneObject): See Also -------- - :class:`compas.scene.NetworkObject` + :class:`compas.scene.GraphObject` :class:`compas.scene.MeshObject` """ diff --git a/src/compas/topology/combinatorics.py b/src/compas/topology/combinatorics.py index eb7e6808669f..a5d5e1d802b9 100644 --- a/src/compas/topology/combinatorics.py +++ b/src/compas/topology/combinatorics.py @@ -27,7 +27,7 @@ def vertex_coloring(adjacency): Notes ----- This algorithms works on any data structure that can be interpreted as a graph, e.g. - networks, meshes, volmeshes, etc.. + graphs, meshes, volmeshes, etc.. For more info, see [1]_. @@ -38,17 +38,17 @@ def vertex_coloring(adjacency): Warnings -------- - This is a greedy algorithm, so it might be slow for large networks. + This is a greedy algorithm, so it might be slow for large graphs. Examples -------- >>> import compas - >>> from compas.datastructures import Network - >>> network = Network.from_obj(compas.get('lines.obj')) - >>> key_color = vertex_coloring(network.adjacency) - >>> key = network.get_any_node() + >>> from compas.datastructures import Graph + >>> graph = Graph.from_obj(compas.get('lines.obj')) + >>> key_color = vertex_coloring(graph.adjacency) + >>> key = graph.get_any_node() >>> color = key_color[key] - >>> any(key_color[nbr] == color for nbr in network.neighbors(key)) + >>> any(key_color[nbr] == color for nbr in graph.neighbors(key)) False """ diff --git a/src/compas/topology/matrices.py b/src/compas/topology/matrices.py index 86e04b56a184..863c9c9c794b 100644 --- a/src/compas/topology/matrices.py +++ b/src/compas/topology/matrices.py @@ -125,7 +125,7 @@ def connectivity_matrix(edges, rtype="array"): Notes ----- - The connectivity matrix encodes how edges in a network are connected + The connectivity matrix encodes how edges in a graph are connected together. Each row represents an edge and has 1 and -1 inserted into the columns for the start and end nodes. @@ -273,7 +273,7 @@ def laplacian_matrix(edges, normalize=False, rtype="array"): # def mass_matrix(Ct, ks, q=0, c=1, tiled=True): -# r"""Creates a network's nodal mass matrix. +# r"""Creates a graph's nodal mass matrix. # Parameters # ---------- diff --git a/src/compas/topology/traversal.py b/src/compas/topology/traversal.py index f5af0b20f983..b577cd014d64 100644 --- a/src/compas/topology/traversal.py +++ b/src/compas/topology/traversal.py @@ -40,7 +40,7 @@ def depth_first_ordering(adjacency, root): Notes ----- - Return all nodes of a connected component containing `root` of a network + Return all nodes of a connected component containing `root` of a graph represented by an adjacency dictionary. This implementation uses a "to visit" stack. The principle of a stack @@ -59,7 +59,7 @@ def depth_first_ordering(adjacency, root): stack, the entire structure has been traversed. Note that this returns a depth-first spanning tree of a connected component - of the network. + of the graph. """ adjacency = {key: set(nbrs) for key, nbrs in iter(adjacency.items())} @@ -167,7 +167,7 @@ def breadth_first_ordering(adjacency, root): the list of nodes to visit. By appending the neighbors to the end of the list of nodes to visit, - and by visiting the nodes at the start of the list first, the network is + and by visiting the nodes at the start of the list first, the graph is traversed in *breadth-first* order. """ @@ -313,7 +313,7 @@ def breadth_first_tree(adjacency, root): def shortest_path(adjacency, root, goal): - """Find the shortest path between two vertices of a network. + """Find the shortest path between two vertices of a graph. Parameters ---------- @@ -448,8 +448,8 @@ def astar_shortest_path(graph, root, goal): Parameters ---------- - graph : :class:`compas.datastructures.Network` | :class:`compas.datastructures.Mesh` - A network or mesh data structure. + graph : :class:`compas.datastructures.Graph` | :class:`compas.datastructures.Mesh` + A graph or mesh data structure. root : hashable The identifier of the starting node. goal : hashable diff --git a/src/compas_blender/scene/__init__.py b/src/compas_blender/scene/__init__.py index 8a75753b206f..8d0ba1694e2f 100644 --- a/src/compas_blender/scene/__init__.py +++ b/src/compas_blender/scene/__init__.py @@ -27,7 +27,7 @@ from compas.geometry import Torus from compas.geometry import Vector from compas.datastructures import Mesh -from compas.datastructures import Network +from compas.datastructures import Graph from compas.datastructures import VolMesh from .sceneobject import BlenderSceneObject @@ -40,7 +40,7 @@ from .frameobject import FrameObject from .lineobject import LineObject from .meshobject import MeshObject -from .networkobject import NetworkObject +from .graphobject import GraphObject from .planeobject import PlaneObject from .pointobject import PointObject from .pointcloudobject import PointcloudObject @@ -75,7 +75,7 @@ def register_scene_objects(): register(Frame, FrameObject, context="Blender") register(Line, LineObject, context="Blender") register(Mesh, MeshObject, context="Blender") - register(Network, NetworkObject, context="Blender") + register(Graph, GraphObject, context="Blender") register(Plane, PlaneObject, context="Blender") register(Point, PointObject, context="Blender") register(Pointcloud, PointcloudObject, context="Blender") @@ -101,7 +101,7 @@ def register_scene_objects(): "FrameObject", "LineObject", "MeshObject", - "NetworkObject", + "GraphObject", "PlaneObject", "PointObject", "PointcloudObject", diff --git a/src/compas_blender/scene/networkobject.py b/src/compas_blender/scene/graphobject.py similarity index 86% rename from src/compas_blender/scene/networkobject.py rename to src/compas_blender/scene/graphobject.py index 48da9aa5d818..4e8e8e1e0312 100644 --- a/src/compas_blender/scene/networkobject.py +++ b/src/compas_blender/scene/graphobject.py @@ -8,28 +8,28 @@ import bpy # type: ignore import compas_blender -from compas.datastructures import Network +from compas.datastructures import Graph from compas.colors import Color from compas.geometry import Line -from compas.scene import NetworkObject as BaseSceneObject +from compas.scene import GraphObject as BaseSceneObject from .sceneobject import BlenderSceneObject from compas_blender import conversions -class NetworkObject(BlenderSceneObject, BaseSceneObject): - """Scene object for drawing network data structures in Blender. +class GraphObject(BlenderSceneObject, BaseSceneObject): + """Scene object for drawing graph data structures in Blender. Parameters ---------- - network : :class:`compas.datastructures.Network` - A COMPAS network. + graph : :class:`compas.datastructures.Graph` + A COMPAS graph. """ - def __init__(self, network: Network, **kwargs: Any): - super().__init__(network=network, **kwargs) + def __init__(self, graph: Graph, **kwargs: Any): + super().__init__(graph=graph, **kwargs) self.nodeobjects = [] self.edgeobjects = [] @@ -88,7 +88,7 @@ def draw( nodecolor: Optional[Union[Color, Dict[int, Color]]] = None, edgecolor: Optional[Union[Color, Dict[Tuple[int, int], Color]]] = None, ) -> list[bpy.types.Object]: - """Draw the network. + """Draw the graph. Parameters ---------- @@ -142,10 +142,10 @@ def draw_nodes( self.nodecolor = color - for node in nodes or self.network.nodes(): # type: ignore - name = f"{self.network.name}.node.{node}" # type: ignore + for node in nodes or self.graph.nodes(): # type: ignore + name = f"{self.graph.name}.node.{node}" # type: ignore color = self.nodecolor[node] # type: ignore - point = self.network.nodes_attributes("xyz")[node] # type: ignore + point = self.graph.nodes_attributes("xyz")[node] # type: ignore # there is no such thing as a sphere data block bpy.ops.mesh.primitive_uv_sphere_add(location=point, radius=radius, segments=u, ring_count=v) @@ -183,11 +183,11 @@ def draw_edges( self.edgecolor = color - for u, v in edges or self.network.edges(): # type: ignore - name = f"{self.network.name}.edge.{u}-{v}" # type: ignore + for u, v in edges or self.graph.edges(): # type: ignore + name = f"{self.graph.name}.edge.{u}-{v}" # type: ignore color = self.edgecolor[u, v] # type: ignore curve = conversions.line_to_blender_curve( - Line(self.network.nodes_attributes("xyz")[u], self.network.nodes_attributes("xyz")[v]) + Line(self.graph.nodes_attributes("xyz")[u], self.graph.nodes_attributes("xyz")[v]) ) obj = self.create_object(curve, name=name) @@ -219,8 +219,8 @@ def draw_edges( # for node in self.node_text: # labels.append( # { - # "pos": self.network.nodes_attributes("xyz")[node], - # "name": f"{self.network.name}.nodelabel.{node}", + # "pos": self.graph.nodes_attributes("xyz")[node], + # "name": f"{self.graph.name}.nodelabel.{node}", # "text": self.node_text[node], # "color": self.nodecolor[node], # } @@ -247,8 +247,8 @@ def draw_edges( # u, v = edge # labels.append( # { - # "pos": centroid_points([self.network.nodes_attributes("xyz")[u], self.network.nodes_attributes("xyz")[v]]), - # "name": f"{self.network.name}.edgelabel.{u}-{v}", + # "pos": centroid_points([self.graph.nodes_attributes("xyz")[u], self.graph.nodes_attributes("xyz")[v]]), + # "name": f"{self.graph.name}.edgelabel.{u}-{v}", # "text": self.edge_text[edge], # "color": self.edgecolor[edge], # } diff --git a/src/compas_ghpython/scene/__init__.py b/src/compas_ghpython/scene/__init__.py index eeb6d8435d74..39386224e21e 100644 --- a/src/compas_ghpython/scene/__init__.py +++ b/src/compas_ghpython/scene/__init__.py @@ -28,7 +28,7 @@ from compas.geometry import Brep from compas.datastructures import Mesh -from compas.datastructures import Network +from compas.datastructures import Graph from compas.datastructures import VolMesh from .sceneobject import GHSceneObject @@ -42,7 +42,7 @@ from .frameobject import FrameObject from .lineobject import LineObject from .meshobject import MeshObject -from .networkobject import NetworkObject +from .graphobject import GraphObject from .planeobject import PlaneObject from .pointobject import PointObject from .polygonobject import PolygonObject @@ -78,7 +78,7 @@ def register_scene_objects(): register(Frame, FrameObject, context="Grasshopper") register(Line, LineObject, context="Grasshopper") register(Mesh, MeshObject, context="Grasshopper") - register(Network, NetworkObject, context="Grasshopper") + register(Graph, GraphObject, context="Grasshopper") register(Plane, PlaneObject, context="Grasshopper") register(Point, PointObject, context="Grasshopper") register(Polygon, PolygonObject, context="Grasshopper") @@ -105,7 +105,7 @@ def register_scene_objects(): "FrameObject", "LineObject", "MeshObject", - "NetworkObject", + "GraphObject", "PlaneObject", "PointObject", "PolygonObject", diff --git a/src/compas_ghpython/scene/networkobject.py b/src/compas_ghpython/scene/graphobject.py similarity index 73% rename from src/compas_ghpython/scene/networkobject.py rename to src/compas_ghpython/scene/graphobject.py index bcb138b8fbd0..9329b69204ab 100644 --- a/src/compas_ghpython/scene/networkobject.py +++ b/src/compas_ghpython/scene/graphobject.py @@ -4,27 +4,27 @@ from compas_rhino import conversions -from compas.scene import NetworkObject as BaseNetworkObject +from compas.scene import GraphObject as BaseGraphObject from .sceneobject import GHSceneObject -class NetworkObject(GHSceneObject, BaseNetworkObject): - """Scene object for drawing network data structures. +class GraphObject(GHSceneObject, BaseGraphObject): + """Scene object for drawing graph data structures. Parameters ---------- - network : :class:`compas.datastructures.Network` - A COMPAS network. + graph : :class:`compas.datastructures.Graph` + A COMPAS graph. **kwargs : dict, optional Additional keyword arguments. """ - def __init__(self, network, **kwargs): - super(NetworkObject, self).__init__(network=network, **kwargs) + def __init__(self, graph, **kwargs): + super(GraphObject, self).__init__(graph=graph, **kwargs) def draw(self): - """Draw the entire network with default color settings. + """Draw the entire graph with default color settings. Returns ------- @@ -50,7 +50,7 @@ def draw_nodes(self, nodes=None): """ points = [] - for node in nodes or self.network.nodes(): # type: ignore + for node in nodes or self.graph.nodes(): # type: ignore points.append(conversions.point_to_rhino(self.node_xyz[node])) return points @@ -71,7 +71,7 @@ def draw_edges(self, edges=None): """ lines = [] - for edge in edges or self.network.edges(): # type: ignore + for edge in edges or self.graph.edges(): # type: ignore lines.append(conversions.line_to_rhino((self.node_xyz[edge[0]], self.node_xyz[edge[1]]))) return lines diff --git a/src/compas_ghpython/utilities/__init__.py b/src/compas_ghpython/utilities/__init__.py index eb74b3fcf605..6946dffbc0dc 100644 --- a/src/compas_ghpython/utilities/__init__.py +++ b/src/compas_ghpython/utilities/__init__.py @@ -12,7 +12,7 @@ draw_pipes, draw_spheres, draw_mesh, - draw_network, + draw_graph, draw_circles, draw_brep, ) @@ -31,7 +31,7 @@ "draw_pipes", "draw_spheres", "draw_mesh", - "draw_network", + "draw_graph", "draw_circles", "draw_brep", "list_to_ghtree", diff --git a/src/compas_ghpython/utilities/drawing.py b/src/compas_ghpython/utilities/drawing.py index 18523940b465..b02f391988dd 100644 --- a/src/compas_ghpython/utilities/drawing.py +++ b/src/compas_ghpython/utilities/drawing.py @@ -403,12 +403,12 @@ def draw_mesh(vertices, faces, color=None, vertex_normals=None, texture_coordina return mesh -def draw_network(network): - """Draw a network in Grasshopper. +def draw_graph(graph): + """Draw a graph in Grasshopper. Parameters ---------- - network : :class:`compas.datastructures.Network` + graph : :class:`compas.datastructures.Graph` Returns ------- @@ -418,11 +418,11 @@ def draw_network(network): """ points = [] - for key in network.nodes(): - points.append({"pos": network.node_coordinates(key)}) + for key in graph.nodes(): + points.append({"pos": graph.node_coordinates(key)}) lines = [] - for u, v in network.edges(): - lines.append({"start": network.node_coordinates(u), "end": network.node_coordinates(v)}) + for u, v in graph.edges(): + lines.append({"start": graph.node_coordinates(u), "end": graph.node_coordinates(v)}) points_rg = draw_points(points) lines_rg = draw_lines(lines) diff --git a/src/compas_rhino/scene/__init__.py b/src/compas_rhino/scene/__init__.py index f2fb63de6e66..07a5c78c2c3c 100644 --- a/src/compas_rhino/scene/__init__.py +++ b/src/compas_rhino/scene/__init__.py @@ -30,7 +30,7 @@ from compas.geometry import Brep from compas.datastructures import Mesh -from compas.datastructures import Network +from compas.datastructures import Graph from compas.datastructures import VolMesh import compas_rhino @@ -55,7 +55,7 @@ from .torusobject import TorusObject from .meshobject import MeshObject -from .networkobject import NetworkObject +from .graphobject import GraphObject from .volmeshobject import VolMeshObject from .curveobject import CurveObject @@ -92,7 +92,7 @@ def register_scene_objects(): register(Sphere, SphereObject, context="Rhino") register(Torus, TorusObject, context="Rhino") register(Mesh, MeshObject, context="Rhino") - register(Network, NetworkObject, context="Rhino") + register(Graph, GraphObject, context="Rhino") register(VolMesh, VolMeshObject, context="Rhino") register(Curve, CurveObject, context="Rhino") register(Surface, SurfaceObject, context="Rhino") @@ -119,7 +119,7 @@ def register_scene_objects(): "SphereObject", "TorusObject", "MeshObject", - "NetworkObject", + "GraphObject", "VolMeshObject", "CurveObject", "SurfaceObject", diff --git a/src/compas_rhino/scene/networkobject.py b/src/compas_rhino/scene/graphobject.py similarity index 88% rename from src/compas_rhino/scene/networkobject.py rename to src/compas_rhino/scene/graphobject.py index 5fff2c491220..31e7f1cd2c5f 100644 --- a/src/compas_rhino/scene/networkobject.py +++ b/src/compas_rhino/scene/graphobject.py @@ -9,7 +9,7 @@ from compas.geometry import Line from compas.geometry import Cylinder from compas.geometry import Sphere -from compas.scene import NetworkObject as BaseNetworkObject +from compas.scene import GraphObject as BaseGraphObject from compas_rhino.conversions import point_to_rhino from compas_rhino.conversions import line_to_rhino from compas_rhino.conversions import sphere_to_rhino @@ -18,20 +18,20 @@ from ._helpers import attributes -class NetworkObject(RhinoSceneObject, BaseNetworkObject): - """Scene object for drawing network data structures. +class GraphObject(RhinoSceneObject, BaseGraphObject): + """Scene object for drawing graph data structures. Parameters ---------- - network : :class:`compas.datastructures.Network` - A COMPAS network. + graph : :class:`compas.datastructures.Graph` + A COMPAS graph. **kwargs : dict, optional Additional keyword arguments. """ - def __init__(self, network, **kwargs): - super(NetworkObject, self).__init__(network=network, **kwargs) + def __init__(self, graph, **kwargs): + super(GraphObject, self).__init__(graph=graph, **kwargs) # ========================================================================== # clear @@ -45,7 +45,7 @@ def clear(self): None """ - guids = compas_rhino.get_objects(name="{}.*".format(self.network.name)) # type: ignore + guids = compas_rhino.get_objects(name="{}.*".format(self.graph.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_nodes(self): @@ -56,7 +56,7 @@ def clear_nodes(self): None """ - guids = compas_rhino.get_objects(name="{}.node.*".format(self.network.name)) # type: ignore + guids = compas_rhino.get_objects(name="{}.node.*".format(self.graph.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) def clear_edges(self): @@ -67,7 +67,7 @@ def clear_edges(self): None """ - guids = compas_rhino.get_objects(name="{}.edge.*".format(self.network.name)) # type: ignore + guids = compas_rhino.get_objects(name="{}.edge.*".format(self.graph.name)) # type: ignore compas_rhino.delete_objects(guids, purge=True) # ========================================================================== @@ -81,7 +81,7 @@ def draw( nodecolor=None, edgecolor=None, ): - """Draw the network using the chosen visualisation settings. + """Draw the graph using the chosen visualisation settings. Parameters ---------- @@ -129,8 +129,8 @@ def draw_nodes(self, nodes=None, color=None, group=None): self.nodecolor = color - for node in nodes or self.network.nodes(): # type: ignore - name = "{}.node.{}".format(self.network.name, node) # type: ignore + for node in nodes or self.graph.nodes(): # type: ignore + name = "{}.node.{}".format(self.graph.name, node) # type: ignore attr = attributes(name=name, color=self.nodecolor[node], layer=self.layer) # type: ignore point = point_to_rhino(self.node_xyz[node]) @@ -169,11 +169,11 @@ def draw_edges(self, edges=None, color=None, group=None, show_direction=False): arrow = "end" if show_direction else None self.edgecolor = color - for edge in edges or self.network.edges(): # type: ignore + for edge in edges or self.graph.edges(): # type: ignore u, v = edge color = self.edgecolor[edge] # type: ignore - name = "{}.edge.{}-{}".format(self.network.name, u, v) # type: ignore + name = "{}.edge.{}-{}".format(self.graph.name, u, v) # type: ignore attr = attributes(name=name, color=color, layer=self.layer, arrow=arrow) # type: ignore line = Line(self.node_xyz[u], self.node_xyz[v]) @@ -217,7 +217,7 @@ def draw_nodelabels(self, text, color=None, group=None, fontheight=10, fontface= self.nodecolor = color for node in text: - name = "{}.node.{}.label".format(self.network.name, node) # type: ignore + name = "{}.node.{}.label".format(self.graph.name, node) # type: ignore attr = attributes(name=name, color=self.nodecolor[node], layer=self.layer) # type: ignore point = point_to_rhino(self.node_xyz[node]) @@ -264,7 +264,7 @@ def draw_edgelabels(self, text, color=None, group=None, fontheight=10, fontface= u, v = edge color = self.edgecolor[edge] # type: ignore - name = "{}.edge.{}-{}.label".format(self.network.name, u, v) # type: ignore + name = "{}.edge.{}-{}.label".format(self.graph.name, u, v) # type: ignore attr = attributes(name=name, color=color, layer=self.layer) line = Line(self.node_xyz[u], self.node_xyz[v]) @@ -287,7 +287,7 @@ def draw_edgelabels(self, text, color=None, group=None, fontheight=10, fontface= # ============================================================================= def draw_spheres(self, radius, color=None, group=None): - """Draw spheres at the vertices of the network. + """Draw spheres at the vertices of the graph. Parameters ---------- @@ -309,7 +309,7 @@ def draw_spheres(self, radius, color=None, group=None): self.nodecolor = color for node in radius: - name = "{}.node.{}.sphere".format(self.network.name, node) # type: ignore + name = "{}.node.{}.sphere".format(self.graph.name, node) # type: ignore color = self.nodecolor[node] # type: ignore attr = attributes(name=name, color=color, layer=self.layer) @@ -325,7 +325,7 @@ def draw_spheres(self, radius, color=None, group=None): return guids def draw_pipes(self, radius, color=None, group=None): - """Draw pipes around the edges of the network. + """Draw pipes around the edges of the graph. Parameters ---------- @@ -347,7 +347,7 @@ def draw_pipes(self, radius, color=None, group=None): self.edgecolor = color for edge in radius: - name = "{}.edge.{}-{}.pipe".format(self.network.name, *edge) # type: ignore + name = "{}.edge.{}-{}.pipe".format(self.graph.name, *edge) # type: ignore color = self.edgecolor[edge] # type: ignore attr = attributes(name=name, color=color, layer=self.layer) diff --git a/src/compas_rhino/utilities/layers.py b/src/compas_rhino/utilities/layers.py index 16e63659fa4e..b833ba6a96f8 100644 --- a/src/compas_rhino/utilities/layers.py +++ b/src/compas_rhino/utilities/layers.py @@ -118,7 +118,7 @@ def create_layers_from_paths(names, separator="::"): * COMPAS * Datastructures * Mesh - * Network + * Graph * Geometry * Point * Vector @@ -127,7 +127,7 @@ def create_layers_from_paths(names, separator="::"): create_layers_from_paths([ "COMPAS::Datastructures::Mesh", - "COMPAS::Datastructures::Network", + "COMPAS::Datastructures::Graph", "COMPAS::Geometry::Point", "COMPAS::Geometry::Vector", ]) @@ -158,7 +158,7 @@ def create_layers_from_dict(layers): layers = {'COMPAS', {'layers': { 'Datastructures': {'color': (255, 0, 0), 'layers': { 'Mesh': {}, - 'Network': {} + 'Graph': {} }}, 'Geometry': {'color': (0, 0, 255),'layers': { 'Point': {}, @@ -307,11 +307,11 @@ def delete_layers(layers): -------- .. code-block:: python - layers = {'COMPAS': {'layers': {'Datastructures': {'layers': {'Mesh': {}, 'Network': {}}}}}} + layers = {'COMPAS': {'layers': {'Datastructures': {'layers': {'Mesh': {}, 'Graph': {}}}}}} create_layers(layers) - delete_layers(['COMPAS::Datastructures::Network']) + delete_layers(['COMPAS::Datastructures::Graph']) delete_layers({'COMPAS': {'layers': {'Datastructures': {'layers': {'Mesh': {}}}}}}) """ diff --git a/tests/compas/compas_api.json b/tests/compas/compas_api.json index 74592bd0ef08..50bdb070d8a0 100644 --- a/tests/compas/compas_api.json +++ b/tests/compas/compas_api.json @@ -50,7 +50,7 @@ "Assembly", "AssemblyError", "BaseMesh", - "BaseNetwork", + "BaseGraph", "BaseVolMesh", "Datastructure", "Feature", @@ -60,7 +60,7 @@ "HalfEdge", "HalfFace", "Mesh", - "Network", + "Graph", "ParametricFeature", "Part", "VolMesh", @@ -132,31 +132,31 @@ "mesh_weld", "meshes_join", "meshes_join_and_weld", - "network_adjacency_matrix", - "network_complement", - "network_connectivity_matrix", - "network_count_crossings", - "network_degree_matrix", - "network_disconnected_edges", - "network_disconnected_nodes", - "network_embed_in_plane", - "network_embed_in_plane_proxy", - "network_explode", - "network_find_crossings", - "network_find_cycles", - "network_is_connected", - "network_is_crossed", - "network_is_planar", - "network_is_planar_embedding", - "network_is_xy", - "network_join_edges", - "network_laplacian_matrix", - "network_polylines", - "network_shortest_path", - "network_smooth_centroid", - "network_split_edge", - "network_transform", - "network_transformed", + "graph_adjacency_matrix", + "graph_complement", + "graph_connectivity_matrix", + "graph_count_crossings", + "graph_degree_matrix", + "graph_disconnected_edges", + "graph_disconnected_nodes", + "graph_embed_in_plane", + "graph_embed_in_plane_proxy", + "graph_explode", + "graph_find_crossings", + "graph_find_cycles", + "graph_is_connected", + "graph_is_crossed", + "graph_is_planar", + "graph_is_planar_embedding", + "graph_is_xy", + "graph_join_edges", + "graph_laplacian_matrix", + "graph_polylines", + "graph_shortest_path", + "graph_smooth_centroid", + "graph_split_edge", + "graph_transform", + "graph_transformed", "trimesh_collapse_edge", "trimesh_cotangent_laplacian_matrix", "trimesh_descent", @@ -648,4 +648,4 @@ "yellow" ] } -} +} \ No newline at end of file diff --git a/tests/compas/compas_api_ipy.json b/tests/compas/compas_api_ipy.json index 28abcc97ebe7..efc776f6e77f 100644 --- a/tests/compas/compas_api_ipy.json +++ b/tests/compas/compas_api_ipy.json @@ -50,7 +50,7 @@ "Assembly", "AssemblyError", "BaseMesh", - "BaseNetwork", + "BaseGraph", "BaseVolMesh", "Datastructure", "Feature", @@ -60,7 +60,7 @@ "HalfEdge", "HalfFace", "Mesh", - "Network", + "Graph", "ParametricFeature", "Part", "VolMesh", @@ -120,27 +120,27 @@ "mesh_weld", "meshes_join", "meshes_join_and_weld", - "network_complement", - "network_count_crossings", - "network_disconnected_edges", - "network_disconnected_nodes", - "network_embed_in_plane", - "network_embed_in_plane_proxy", - "network_explode", - "network_find_crossings", - "network_find_cycles", - "network_is_connected", - "network_is_crossed", - "network_is_planar", - "network_is_planar_embedding", - "network_is_xy", - "network_join_edges", - "network_polylines", - "network_shortest_path", - "network_smooth_centroid", - "network_split_edge", - "network_transform", - "network_transformed", + "graph_complement", + "graph_count_crossings", + "graph_disconnected_edges", + "graph_disconnected_nodes", + "graph_embed_in_plane", + "graph_embed_in_plane_proxy", + "graph_explode", + "graph_find_crossings", + "graph_find_cycles", + "graph_is_connected", + "graph_is_crossed", + "graph_is_planar", + "graph_is_planar_embedding", + "graph_is_xy", + "graph_join_edges", + "graph_polylines", + "graph_shortest_path", + "graph_smooth_centroid", + "graph_split_edge", + "graph_transform", + "graph_transformed", "trimesh_collapse_edge", "trimesh_face_circle", "trimesh_gaussian_curvature", @@ -578,4 +578,4 @@ "yellow" ] } -} +} \ No newline at end of file diff --git a/tests/compas/data/test_dataschema.py b/tests/compas/data/test_dataschema.py index 4eefef69e851..91ba9bdb3ba5 100644 --- a/tests/compas/data/test_dataschema.py +++ b/tests/compas/data/test_dataschema.py @@ -20,7 +20,7 @@ from compas.geometry import Torus from compas.geometry import Pointcloud -from compas.datastructures import Network +from compas.datastructures import Graph from compas.datastructures import HalfEdge if not compas.IPY: @@ -593,7 +593,7 @@ def test_schema_pointcloud_invalid(pointcloud): Pointcloud.validate_data(pointcloud) @pytest.mark.parametrize( - "network", + "graph", [ { "dna": {}, @@ -618,11 +618,11 @@ def test_schema_pointcloud_invalid(pointcloud): }, ], ) - def test_schema_network_valid(network): - Network.validate_data(network) + def test_schema_graphalid(graph): + Graph.validate_data(graph) @pytest.mark.parametrize( - "network", + "graph", [ { "dna": {}, @@ -663,9 +663,9 @@ def test_schema_network_valid(network): }, ], ) - def test_schema_network_invalid(network): + def test_schema_graph_invalid(graph): with pytest.raises(jsonschema.exceptions.ValidationError): - Network.validate_data(network) + Graph.validate_data(graph) @pytest.mark.parametrize( "halfedge", diff --git a/tests/compas/data/test_json.py b/tests/compas/data/test_json.py index 26d9b0994a5c..f6cd2f8f2b9f 100644 --- a/tests/compas/data/test_json.py +++ b/tests/compas/data/test_json.py @@ -3,7 +3,7 @@ import compas from compas.datastructures import Mesh -from compas.datastructures import Network +from compas.datastructures import Graph from compas.datastructures import VolMesh from compas.geometry import Box from compas.geometry import Frame @@ -42,8 +42,8 @@ def test_json_xform(): assert before.guid == after.guid -def test_json_network(): - before = Network() +def test_json_graph(): + before = Graph() a = before.add_node() b = before.add_node() before.add_edge(a, b) diff --git a/tests/compas/datastructures/test_network.py b/tests/compas/datastructures/test_network.py index 1bf5fad86375..ab92b6e76bc7 100644 --- a/tests/compas/datastructures/test_network.py +++ b/tests/compas/datastructures/test_network.py @@ -5,7 +5,7 @@ import pytest import compas -from compas.datastructures import Network +from compas.datastructures import Graph from compas.geometry import Pointcloud # ============================================================================== @@ -16,42 +16,42 @@ @pytest.fixture -def network(): +def graph(): edges = [(0, 1), (0, 2), (0, 3), (0, 4)] - network = Network() + graph = Graph() for u, v in edges: - network.add_edge(u, v) - return network + graph.add_edge(u, v) + return graph @pytest.fixture -def planar_network(): - return Network.from_obj(os.path.join(BASE_FOLDER, "fixtures", "planar.obj")) +def planar_graph(): + return Graph.from_obj(os.path.join(BASE_FOLDER, "fixtures", "planar.obj")) @pytest.fixture -def non_planar_network(): - return Network.from_obj(os.path.join(BASE_FOLDER, "fixtures", "non-planar.obj")) +def non_planar_graph(): + return Graph.from_obj(os.path.join(BASE_FOLDER, "fixtures", "non-planar.obj")) @pytest.fixture -def k5_network(): - network = Network() - network.add_edge("a", "b") - network.add_edge("a", "c") - network.add_edge("a", "d") - network.add_edge("a", "e") +def k5_graph(): + graph = Graph() + graph.add_edge("a", "b") + graph.add_edge("a", "c") + graph.add_edge("a", "d") + graph.add_edge("a", "e") - network.add_edge("b", "c") - network.add_edge("b", "d") - network.add_edge("b", "e") + graph.add_edge("b", "c") + graph.add_edge("b", "d") + graph.add_edge("b", "e") - network.add_edge("c", "d") - network.add_edge("c", "e") + graph.add_edge("c", "d") + graph.add_edge("c", "e") - network.add_edge("d", "e") + graph.add_edge("d", "e") - return network + return graph # ============================================================================== @@ -70,20 +70,20 @@ def k5_network(): compas.get("grid_irregular.obj"), ], ) -def test_network_from_obj(filepath): - network = Network.from_obj(filepath) - assert network.number_of_nodes() > 0 - assert network.number_of_edges() > 0 - assert len(list(network.nodes())) == network._max_node + 1 - assert network.is_connected() +def test_graph_from_obj(filepath): + graph = Graph.from_obj(filepath) + assert graph.number_of_nodes() > 0 + assert graph.number_of_edges() > 0 + assert len(list(graph.nodes())) == graph._max_node + 1 + assert graph.is_connected() -def test_network_from_pointcloud(): +def test_graph_from_pointcloud(): cloud = Pointcloud.from_bounds(random.random(), random.random(), random.random(), random.randint(10, 100)) - network = Network.from_pointcloud(cloud=cloud, degree=3) - assert network.number_of_nodes() == len(cloud) - for node in network.nodes(): - assert network.degree(node) >= 3 + graph = Graph.from_pointcloud(cloud=cloud, degree=3) + assert graph.number_of_nodes() == len(cloud) + for node in graph.nodes(): + assert graph.degree(node) >= 3 # ============================================================================== @@ -91,30 +91,30 @@ def test_network_from_pointcloud(): # ============================================================================== -def test_network_data1(network): - other = Network.from_data(json.loads(json.dumps(network.data))) +def test_graph_data1(graph): + other = Graph.from_data(json.loads(json.dumps(graph.data))) - assert network.data == other.data - assert network.default_node_attributes == other.default_node_attributes - assert network.default_edge_attributes == other.default_edge_attributes - assert network.number_of_nodes() == other.number_of_nodes() - assert network.number_of_edges() == other.number_of_edges() + assert graph.data == other.data + assert graph.default_node_attributes == other.default_node_attributes + assert graph.default_edge_attributes == other.default_edge_attributes + assert graph.number_of_nodes() == other.number_of_nodes() + assert graph.number_of_edges() == other.number_of_edges() if not compas.IPY: - assert Network.validate_data(network.data) - assert Network.validate_data(other.data) + assert Graph.validate_data(graph.data) + assert Graph.validate_data(other.data) -def test_network_data2(): +def test_graph_data2(): cloud = Pointcloud.from_bounds(random.random(), random.random(), random.random(), random.randint(10, 100)) - network = Network.from_pointcloud(cloud=cloud, degree=3) - other = Network.from_data(json.loads(json.dumps(network.data))) + graph = Graph.from_pointcloud(cloud=cloud, degree=3) + other = Graph.from_data(json.loads(json.dumps(graph.data))) - assert network.data == other.data + assert graph.data == other.data if not compas.IPY: - assert Network.validate_data(network.data) - assert Network.validate_data(other.data) + assert Graph.validate_data(graph.data) + assert Graph.validate_data(other.data) # ============================================================================== @@ -131,11 +131,11 @@ def test_network_data2(): def test_add_node(): - network = Network() - assert network.add_node(1) == 1 - assert network.add_node("1", x=0, y=0, z=0) == "1" - assert network.add_node(2) == 2 - assert network.add_node(0, x=1) == 0 + graph = Graph() + assert graph.add_node(1) == 1 + assert graph.add_node("1", x=0, y=0, z=0) == "1" + assert graph.add_node(2) == 2 + assert graph.add_node(0, x=1) == 0 # ============================================================================== @@ -143,23 +143,23 @@ def test_add_node(): # ============================================================================== -def test_network_invalid_edge_delete(): - network = Network() - node = network.add_node() - edge = network.add_edge(node, node) - network.delete_edge(edge) - assert network.has_edge(edge) is False +def test_graph_invalid_edge_delete(): + graph = Graph() + node = graph.add_node() + edge = graph.add_edge(node, node) + graph.delete_edge(edge) + assert graph.has_edge(edge) is False -def test_network_opposite_direction_edge_delete(): - network = Network() - node_a = network.add_node() - node_b = network.add_node() - edge_a = network.add_edge(node_a, node_b) - edge_b = network.add_edge(node_b, node_a) - network.delete_edge(edge_a) - assert network.has_edge(edge_a) is False - assert network.has_edge(edge_b) is True +def test_graph_opposite_direction_edge_delete(): + graph = Graph() + node_a = graph.add_node() + node_b = graph.add_node() + edge_a = graph.add_edge(node_a, node_b) + edge_b = graph.add_edge(node_b, node_a) + graph.delete_edge(edge_a) + assert graph.has_edge(edge_a) is False + assert graph.has_edge(edge_b) is True # ============================================================================== @@ -167,18 +167,18 @@ def test_network_opposite_direction_edge_delete(): # ============================================================================== -def test_network_node_sample(network): - for node in network.node_sample(): - assert network.has_node(node) - for node in network.node_sample(size=network.number_of_nodes()): - assert network.has_node(node) +def test_graph_node_sample(graph): + for node in graph.node_sample(): + assert graph.has_node(node) + for node in graph.node_sample(size=graph.number_of_nodes()): + assert graph.has_node(node) -def test_network_edge_sample(network): - for edge in network.edge_sample(): - assert network.has_edge(edge) - for edge in network.edge_sample(size=network.number_of_edges()): - assert network.has_edge(edge) +def test_graph_edge_sample(graph): + for edge in graph.edge_sample(): + assert graph.has_edge(edge) + for edge in graph.edge_sample(size=graph.number_of_edges()): + assert graph.has_edge(edge) # ============================================================================== @@ -186,22 +186,22 @@ def test_network_edge_sample(network): # ============================================================================== -def test_network_default_node_attributes(): - network = Network(name="test", default_node_attributes={"a": 1, "b": 2}) - for node in network.nodes(): - assert network.node_attribute(node, name="a") == 1 - assert network.node_attribute(node, name="b") == 2 - network.node_attribute(node, name="a", value=3) - assert network.node_attribute(node, name="a") == 3 +def test_graph_default_node_attributes(): + graph = Graph(name="test", default_node_attributes={"a": 1, "b": 2}) + for node in graph.nodes(): + assert graph.node_attribute(node, name="a") == 1 + assert graph.node_attribute(node, name="b") == 2 + graph.node_attribute(node, name="a", value=3) + assert graph.node_attribute(node, name="a") == 3 -def test_network_default_edge_attributes(): - network = Network(name="test", default_edge_attributes={"a": 1, "b": 2}) - for edge in network.edges(): - assert network.edge_attribute(edge, name="a") == 1 - assert network.edge_attribute(edge, name="b") == 2 - network.edge_attribute(edge, name="a", value=3) - assert network.edge_attribute(edge, name="a") == 3 +def test_graph_default_edge_attributes(): + graph = Graph(name="test", default_edge_attributes={"a": 1, "b": 2}) + for edge in graph.edges(): + assert graph.edge_attribute(edge, name="a") == 1 + assert graph.edge_attribute(edge, name="b") == 2 + graph.edge_attribute(edge, name="a", value=3) + assert graph.edge_attribute(edge, name="a") == 3 # ============================================================================== @@ -213,7 +213,7 @@ def test_network_to_networkx(): if compas.IPY: return - g = Network() + g = Graph() g.attributes["name"] = "DiGraph" g.attributes["val"] = (0, 0, 0) g.add_node(0) @@ -225,8 +225,8 @@ def test_network_to_networkx(): nxg = g.to_networkx() - assert nxg.graph["name"] == "DiGraph", "Network attributes must be preserved" # type: ignore - assert nxg.graph["val"] == (0, 0, 0), "Network attributes must be preserved" # type: ignore + assert nxg.graph["name"] == "DiGraph", "Graph attributes must be preserved" # type: ignore + assert nxg.graph["val"] == (0, 0, 0), "Graph attributes must be preserved" # type: ignore assert set(nxg.nodes()) == set(g.nodes()), "Node sets must match" assert nxg.nodes[1]["weight"] == 1.2, "Node attributes must be preserved" assert nxg.nodes[1]["height"] == "test", "Node attributes must be preserved" @@ -235,13 +235,13 @@ def test_network_to_networkx(): assert set(nxg.edges()) == set(((0, 1), (1, 2))), "Edge sets must match" assert nxg.edges[0, 1]["attr_value"] == 10, "Edge attributes must be preserved" - g2 = Network.from_networkx(nxg) + g2 = Graph.from_networkx(nxg) assert g.number_of_nodes() == g2.number_of_nodes() assert g.number_of_edges() == g2.number_of_edges() assert g2.edge_attribute((0, 1), "attr_value") == 10 - assert g2.attributes["name"] == "DiGraph", "Network attributes must be preserved" - assert g2.attributes["val"] == (0, 0, 0), "Network attributes must be preserved" + assert g2.attributes["name"] == "DiGraph", "Graph attributes must be preserved" + assert g2.attributes["val"] == (0, 0, 0), "Graph attributes must be preserved" # ============================================================================== @@ -249,14 +249,14 @@ def test_network_to_networkx(): # ============================================================================== -def test_non_planar(k5_network, non_planar_network): +def test_non_planar(k5_graph, non_planar_graph): if not compas.IPY: - assert k5_network.is_planar() is not True - assert non_planar_network.is_planar() is not True + assert k5_graph.is_planar() is not True + assert non_planar_graph.is_planar() is not True -def test_planar(k5_network, planar_network): +def test_planar(k5_graph, planar_graph): if not compas.IPY: - k5_network.delete_edge(("a", "b")) # Delete (a, b) edge to make K5 planar - assert k5_network.is_planar() is True - assert planar_network.is_planar() is True + k5_graph.delete_edge(("a", "b")) # Delete (a, b) edge to make K5 planar + assert k5_graph.is_planar() is True + assert planar_graph.is_planar() is True diff --git a/tests/compas/donttest_api_completeness.py b/tests/compas/donttest_api_completeness.py index 7db2f3429d1c..e782572a9e5d 100644 --- a/tests/compas/donttest_api_completeness.py +++ b/tests/compas/donttest_api_completeness.py @@ -101,7 +101,7 @@ # for name in compas_api[packmod]: # if name in [ # "BaseMesh", -# "BaseNetwork", +# "BaseGraph", # "BaseVolMesh", # "Datastructure", # "Graph", diff --git a/tests/compas/topology/test_traversal.py b/tests/compas/topology/test_traversal.py index 8817e273ad8a..7c4c51e5c23e 100644 --- a/tests/compas/topology/test_traversal.py +++ b/tests/compas/topology/test_traversal.py @@ -1,12 +1,12 @@ from compas.datastructures import Mesh -from compas.datastructures import Network +from compas.datastructures import Graph from compas.geometry import Box, Frame from compas.topology import astar_shortest_path from compas.topology.traversal import astar_lightest_path def test_astar_shortest_path(): - n = Network() + n = Graph() a = n.add_node(x=1, y=2, z=0) b = n.add_node(x=3, y=1, z=0) n.add_edge(a, b) @@ -15,7 +15,7 @@ def test_astar_shortest_path(): def test_astar_shortest_path_cycle(): - n = Network() + n = Graph() a = n.add_node(x=1, y=0, z=0) b = n.add_node(x=2, y=0, z=0) c = n.add_node(x=3, y=0, z=0) @@ -31,7 +31,7 @@ def test_astar_shortest_path_cycle(): def test_astar_shortest_path_disconnected(): - n = Network() + n = Graph() a = n.add_node(x=1, y=0, z=0) b = n.add_node(x=2, y=0, z=0) c = n.add_node(x=3, y=0, z=0) @@ -48,7 +48,7 @@ def test_astar_shortest_path_mesh(): def test_astar_lightest_path(): - g = Network() + g = Graph() for i in range(4): g.add_node(i) g.add_edge(0, 1) From ea8a04a7cbd2b00b68919e5a693ee805f0e63121 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 15 Jan 2024 18:55:25 +0100 Subject: [PATCH 5/5] small fixes --- CHANGELOG.md | 2 +- src/compas/datastructures/graph/graph.py | 2 +- tests/compas/data/test_dataschema.py | 2 +- tests/compas/datastructures/{test_network.py => test_graph.py} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename tests/compas/datastructures/{test_network.py => test_graph.py} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a579de193b..38552efbc2f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -* Merged `compas.datastructures.Graph` into `compas.datastructures.Network`. +* Merged `compas.datastructures.Network` into `compas.datastructures.Graph`. ### Removed diff --git a/src/compas/datastructures/graph/graph.py b/src/compas/datastructures/graph/graph.py index 1354da76f71d..74a802b1a059 100644 --- a/src/compas/datastructures/graph/graph.py +++ b/src/compas/datastructures/graph/graph.py @@ -196,7 +196,7 @@ def from_edges(cls, edges): See Also -------- - :meth:`from_graphx` + :meth:`from_networkx` """ graph = cls() diff --git a/tests/compas/data/test_dataschema.py b/tests/compas/data/test_dataschema.py index 91ba9bdb3ba5..34fe7285d22f 100644 --- a/tests/compas/data/test_dataschema.py +++ b/tests/compas/data/test_dataschema.py @@ -618,7 +618,7 @@ def test_schema_pointcloud_invalid(pointcloud): }, ], ) - def test_schema_graphalid(graph): + def test_schema_graph_valid(graph): Graph.validate_data(graph) @pytest.mark.parametrize( diff --git a/tests/compas/datastructures/test_network.py b/tests/compas/datastructures/test_graph.py similarity index 99% rename from tests/compas/datastructures/test_network.py rename to tests/compas/datastructures/test_graph.py index ab92b6e76bc7..7f54d49cb59d 100644 --- a/tests/compas/datastructures/test_network.py +++ b/tests/compas/datastructures/test_graph.py @@ -209,7 +209,7 @@ def test_graph_default_edge_attributes(): # ============================================================================== -def test_network_to_networkx(): +def test_graph_to_networkx(): if compas.IPY: return