diff --git a/pymathics/graph/__main__.py b/pymathics/graph/__main__.py index 8ddd0e4..6b2f01b 100644 --- a/pymathics/graph/__main__.py +++ b/pymathics/graph/__main__.py @@ -11,6 +11,7 @@ from mathics.builtin.base import Builtin, AtomBuiltin from mathics.builtin.box.graphics import GraphicsBox +from mathics.builtin.box.inout import _BoxedString from mathics.builtin.patterns import Matcher from mathics.core.atoms import Integer, Integer0, Integer1, Real from mathics.core.convert.expression import ListExpression @@ -27,7 +28,6 @@ SymbolRGBColor, SymbolRule, ) - from inspect import isgenerator WL_MARKER_TO_NETWORKX = { @@ -416,6 +416,7 @@ def _normalize_edges(edges): class Graph(Atom): + class_head_name = "Pymathics`Graph" options = DEFAULT_GRAPH_OPTIONS @@ -423,17 +424,31 @@ def __init__(self, G, **kwargs): super(Graph, self).__init__() self.G = G + def __hash__(self): + return hash(("Pymathics`Graph", self.G)) + + def __str__(self): + return "-Graph-" + + def atom_to_boxes(self, f, evaluation) -> _BoxedString: + return _BoxedString("-Graph-") + + def default_format(self, evaluation, form): + return "-Graph-" + + def do_format(self, evaluation, form): + return self + @property def edges(self): return self.G.edges - @property - def vertices(self): - return self.G.nodes - def empty(self): return len(self.G) == 0 + def is_loop_free(self): + return not any(True for _ in nx.nodes_with_selfloops(self.G)) + # networkx graphs can't be for mixed def is_mixed_graph(self): return False @@ -442,100 +457,85 @@ def is_mixed_graph(self): def is_multigraph(self): return isinstance(self.G, (nx.MultiDiGraph, nx.MultiGraph)) - def is_loop_free(self): - return not any(True for _ in nx.nodes_with_selfloops(self.G)) - - def __str__(self): - return "-Graph-" - - def do_copy(self): - return Graph(self.G) - def get_sort_key(self, pattern_sort=False): if pattern_sort: return super(Graph, self).get_sort_key(True) else: return hash(self) - def default_format(self, evaluation, form): - return "-Graph-" + @property + def vertices(self): + return self.G.nodes - def same(self, other): - return isinstance(other, Graph) and self.G == other.G - # FIXME - # self.properties == other.properties - # self.options == other.options - # self.highlights == other.highlights - def to_python(self, *args, **kwargs): - return self.G +class _Collection(object): + def __init__(self, expressions, properties=None, index=None): + self.expressions = expressions + self.properties = properties if properties else None + self.index = index - def __hash__(self): - return hash(("Graph", self.G)) # FIXME self.properties, ... + def clone(self): + properties = self.properties + return _Collection( + self.expressions[:], properties[:] if properties else None, None + ) - def atom_to_boxes(self, form, evaluation): - return Expression(SymbolGraphBox, self, form) + def filter(self, expressions): + index = self.get_index() + return [expr for expr in expressions if expr in index] - def boxes_to_xml(self, **options): - # Figure out what to do here. - return "-Graph-XML-" + def extend(self, expressions, properties): + if properties: + if self.properties is None: + self.properties = [None] * len(self.expressions) + self.properties.extend(properties) + self.expressions.extend(expressions) + self.index = None + return expressions - def get_property(self, element, name): - if element.get_head_name() in ("System`DirectedEdge", "System`UndirectedEdge"): - x = self.edges.get_property(element, name) - if x is None: - x = self.vertices.get_property(element, name) - return x + def delete(self, expressions): + index = self.get_index() + trash = set(index[x] for x in expressions) + deleted = [self.expressions[i] for i in trash] + self.expressions = [x for i, x in enumerate(self.expressions) if i not in trash] + self.properties = [x for i, x in enumerate(self.properties) if i not in trash] + self.index = None + return deleted - def delete_edges(self, edges_to_delete): - G = self.G.copy() - directed = G.is_directed() - - edges_to_delete = list(_normalize_edges(edges_to_delete)) - # FIXME: edges_to_delete is needs to be a tuple. tuples - # are edges in networkx - edges_to_delete = [edge for edge in self.edges if edge in edges_to_delete] - - for edge in edges_to_delete: - if edge.has_form("DirectedEdge", 2): - if directed: - u, v = edge.elements - G.remove_edge(u, v) - elif edge.has_form("UndirectedEdge", 2): - u, v = edge.elements - if directed: - G.remove_edge(u, v) - G.remove_edge(v, u) - else: - G.remove_edge(u, v) - - edges = self.edges.clone() - edges.delete(edges_to_delete) - - return Graph(G) - - def update_weights(self, evaluation): - weights = None - G = self.G - - if self.is_multigraph(): - for u, v, k, w in G.edges.data( - "System`EdgeWeight", default=None, keys=True - ): - data = G.get_edge_data(u, v, key=k) - w = data.get() - if w is not None: - w = w.evaluate(evaluation).to_mpmath() - G[u][v][k]["WEIGHT"] = w - weights = "WEIGHT" + def data(self): + return self.expressions, list(self.get_properties()) + + def get_index(self): + index = self.index + if index is None: + index = dict((v, i) for i, v in enumerate(self.expressions)) + self.index = index + return index + + def get_properties(self): + if self.properties: + for p in self.properties: + yield p else: - for u, v, w in G.edges.data("System`EdgeWeight", default=None): - if w is not None: - w = w.evaluate(evaluation).to_mpmath() - G[u][v]["WEIGHT"] = w - weights = "WEIGHT" + for _ in range(len(self.expressions)): + yield None - return weights + def get_sorted(self): + index = self.get_index() + return lambda c: sorted(c, key=lambda v: index[v]) + + def get_property(self, element, name): + properties = self.properties + if properties is None: + return None + index = self.get_index() + i = index.get(element) + if i is None: + return None + p = properties[i] + if p is None: + return None + return p.get(name) def _is_connected(G): @@ -1754,9 +1754,7 @@ def apply(self, graph, evaluation, options): def degrees(graph): degrees = dict(list(graph.G.degree(graph.vertices))) - return ListExpression( - *[Integer(degrees.get(v, 0)) for v in graph.vertices] - ) + return ListExpression(*[Integer(degrees.get(v, 0)) for v in graph.vertices]) return self._evaluate_atom(graph, options, degrees) diff --git a/pymathics/graph/generators.py b/pymathics/graph/generators.py index 4f25d9a..b9e5fe1 100644 --- a/pymathics/graph/generators.py +++ b/pymathics/graph/generators.py @@ -9,11 +9,12 @@ from pymathics.graph.__main__ import ( Graph, + SymbolUndirectedEdge, _NetworkXBuiltin, _convert_networkx_graph, _graph_from_list, - has_directed_option, _process_graph_options, + has_directed_option, nx, ) @@ -43,7 +44,7 @@ def graph_helper( else graph_generator_func(*args, **kwargs) ) except MemoryError: - evaluation.message(self.get_name(), "mem", expression) + evaluation.message("Graph", "mem", evaluation) return None if graph_layout and not options["System`GraphLayout"].get_string_value(): options["System`GraphLayout"] = String(graph_layout) @@ -287,7 +288,7 @@ def apply(self, k, n, expression, evaluation, options): n_int = n.get_int_value() k_int = k.get_int_value() - new_n_int = int(((k_int ** n_int) - 1) / (k_int - 1)) + new_n_int = int(((k_int**n_int) - 1) / (k_int - 1)) return f_r_t_apply(self, k, Integer(new_n_int), expression, evaluation, options) # FIXME: can be done with rules? @@ -296,7 +297,7 @@ def apply_2(self, n, expression, evaluation, options): n_int = n.get_int_value() - new_n_int = int(2 ** n_int) - 1 + new_n_int = int(2**n_int) - 1 return f_r_t_apply( self, Integer(2), Integer(new_n_int), expression, evaluation, options ) @@ -585,13 +586,13 @@ class PathGraph(_NetworkXBuiltin): = -Graph- """ - def apply(self, l, evaluation, options): + def apply(self, element, evaluation, options): "PathGraph[l_List, OptionsPattern[%(name)s]]" - leaves = l.leaves + elements = element.elements def edges(): - for u, v in zip(leaves, leaves[1:]): - yield Expression("UndirectedEdge", u, v) + for u, v in zip(elements, elements[1:]): + yield Expression(SymbolUndirectedEdge, u, v) g = _graph_from_list(edges(), options) g.G.graph_layout = ( @@ -617,7 +618,7 @@ def _generate(self, n, m, k, evaluation, options): py_k = k.get_int_value() is_directed = has_directed_option(options) - with RandomEnv(evaluation) as rand: + with RandomEnv(evaluation) as _: for _ in range(py_k): # seed = rand.randint(0, 2 ** 63 - 1) # 2**63 is too large G = nx.gnm_random_graph(py_n, py_m, directed=is_directed) diff --git a/pymathics/graph/version.py b/pymathics/graph/version.py index 09b4136..848ccdb 100644 --- a/pymathics/graph/version.py +++ b/pymathics/graph/version.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- @@ -6,4 +5,4 @@ # well as importing into Python. That's why there is no # space around "=" below. # fmt: off -__version__="2.3.1.dev0" +__version__="5.0.0.dev0"