diff --git a/CHANGELOG.md b/CHANGELOG.md index 632151d..9cbfa4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- **0.10.0** + - Features: + - `GrandIsoExecutor` and `NetworkXExecutor`: Support for `networkx.MultiGraph` and `networkx.MultiDiGraph` search through the use of the `multigraph_edge_match` executor argument (#107). + - Deprecations: + - Removed `dotmotif.dotmotif` method-style API (#107). - **0.9.2** (May 28 2021) - Features: - `GrandIsoExecutor`: Utilizes the node attribute matching flow available in grandiso≥2.0.0 to reduce complexity of attribute-heavy searches (#104) diff --git a/docs/reference/dotmotif/dotmotif.md b/docs/reference/dotmotif/dotmotif.md index 6774999..3fddfa5 100644 --- a/docs/reference/dotmotif/dotmotif.md +++ b/docs/reference/dotmotif/dotmotif.md @@ -23,7 +23,7 @@ Create a new dotmotif object. > - **parser** (`dotmotif.parsers.Parser`: `DEFAULT_MOTIF_PARSER`): The parser to use to parse the document. Defaults to the v2 parser. > - **exclude_automorphisms** (`bool`: `False`): Whether to exclude automorphism - variants of the motif when rturning results. + variants of the motif when returning results. > - **validators** (`List[Validator]`: `None`): A list of dotmotif.Validators to use when verifying the motif for correctness and executability. diff --git a/docs/reference/dotmotif/utils.py.md b/docs/reference/dotmotif/utils.py.md index d96c9d6..6112b4e 100644 --- a/docs/reference/dotmotif/utils.py.md +++ b/docs/reference/dotmotif/utils.py.md @@ -4,7 +4,7 @@ Draw a dotmotif motif object. ### Arguments -> - **dm** (`None`: `None`): dotmotif.DotMotif +> - **dm** (`None`: `None`): dotmotif.Motif > - **negative_edge_color** (`str`: `r`): Color used to represent negative edges > - **pos** (`dict`: `None`): The position to use. If unset, uses nx.spring_layout diff --git a/docs/reference/executors/GrandIsoExecutor.py.md b/docs/reference/executors/GrandIsoExecutor.py.md index bbb16cc..c5be458 100644 --- a/docs/reference/executors/GrandIsoExecutor.py.md +++ b/docs/reference/executors/GrandIsoExecutor.py.md @@ -1,8 +1,23 @@ +## *Class* `GrandIsoExecutor(NetworkXExecutor)` + + +A DotMotif executor that uses grandiso for subgraph monomorphism. + +This executor is dramatically fast than the NetworkX search, and is still a pure-Python implementation. + +[GrandIso](https://github.com/aplbrain/grandiso-networkx) + + + ## *Function* `find(self, motif, limit: int = None)` Find a motif in a larger graph. ### Arguments - motif (dotmotif.dotmotif) + motif (dotmotif.Motif) +> - **int** (`None`: `None`): None) + +### Returns + List[dict] diff --git a/docs/reference/executors/Neo4jExecutor.py.md b/docs/reference/executors/Neo4jExecutor.py.md index 5f83ed6..5150a47 100644 --- a/docs/reference/executors/Neo4jExecutor.py.md +++ b/docs/reference/executors/Neo4jExecutor.py.md @@ -72,21 +72,21 @@ You should usually ignore this, and use .find() instead. -## *Function* `count(self, motif: "dotmotif", limit=None) -> int` +## *Function* `count(self, motif: "dotmotif.Motif", limit=None) -> int` Count a motif in a larger graph. ### Arguments - motif (dotmotif.dotmotif) + motif (dotmotif.Motif) -## *Function* `find(self, motif: "dotmotif", limit=None, cursor=True)` +## *Function* `find(self, motif: "dotmotif.Motif", limit=None, cursor=True)` Find a motif in a larger graph. ### Arguments - motif (dotmotif.dotmotif) + motif (dotmotif.Motif) diff --git a/docs/reference/executors/NetworkXExecutor.py.md b/docs/reference/executors/NetworkXExecutor.py.md index feaa206..851cdc5 100644 --- a/docs/reference/executors/NetworkXExecutor.py.md +++ b/docs/reference/executors/NetworkXExecutor.py.md @@ -4,6 +4,12 @@ Check if a single edge satisfies the constraints. +## *Function* `_edge_satisfies_many_constraints_for_muligraph_any_edges(edge_attributes: dict, constraints: dict) -> List[Tuple[str, str, str]]` + + +Returns a subset of constraints that this edge matches, in the form (key, op, val). + + ## *Function* `_node_satisfies_constraints(node_attributes: dict, constraints: dict) -> bool` @@ -32,7 +38,7 @@ Create a new NetworkXExecutor. -## *Function* `count(self, motif: "dotmotif", limit: int = None)` +## *Function* `count(self, motif: "dotmotif.Motif", limit: int = None)` Count the occurrences of a motif in a graph. @@ -40,11 +46,11 @@ Count the occurrences of a motif in a graph. See NetworkXExecutor#find for more documentation. -## *Function* `find(self, motif: "dotmotif", limit: int = None)` +## *Function* `find(self, motif: "dotmotif.Motif", limit: int = None)` Find a motif in a larger graph. ### Arguments - motif (dotmotif.dotmotif) + motif (dotmotif.Motif) diff --git a/docs/reference/executors/NeuPrintExecutor.py.md b/docs/reference/executors/NeuPrintExecutor.py.md index 80d2e89..af4f111 100644 --- a/docs/reference/executors/NeuPrintExecutor.py.md +++ b/docs/reference/executors/NeuPrintExecutor.py.md @@ -42,26 +42,26 @@ You should usually ignore this, and use .find() instead. -## *Function* `count(self, motif: dotmotif, limit=None) -> int` +## *Function* `count(self, motif: Motif, limit=None) -> int` Count a motif in a larger graph. ### Arguments -> - **motif** (`dotmotif.dotmotif`: `None`): The motif to search for +> - **motif** (`dotmotif.Motif`: `None`): The motif to search for ### Returns > - **int** (`None`: `None`): The count of this motif in the host graph -## *Function* `find(self, motif: dotmotif, limit=None) -> pd.DataFrame` +## *Function* `find(self, motif: Motif, limit=None) -> pd.DataFrame` Find a motif in a larger graph. ### Arguments -> - **motif** (`dotmotif.dotmotif`: `None`): The motif to search for +> - **motif** (`dotmotif.Motif`: `None`): The motif to search for ### Returns > - **pd.DataFrame** (`None`: `None`): The results of the search diff --git a/docs/reference/ingest/ingest.md b/docs/reference/ingest/ingest.md index 161172d..e90a938 100644 --- a/docs/reference/ingest/ingest.md +++ b/docs/reference/ingest/ingest.md @@ -5,10 +5,10 @@ An abstract base class for import to the NetworkX format. -## *Class* `CSVEdgelistConverter(NetworkXConverter)` +## *Class* `EdgelistConverter(NetworkXConverter)` -A converter that takes an arbitrary CSV file on disk and converts it to a graph. +Convert an edgelist dataframe or CSV to a graph. diff --git a/docs/reference/tests/tests.md b/docs/reference/tests/tests.md new file mode 100644 index 0000000..e69de29 diff --git a/dotmotif/__init__.py b/dotmotif/__init__.py index c09468e..9ca8e26 100644 --- a/dotmotif/__init__.py +++ b/dotmotif/__init__.py @@ -119,7 +119,7 @@ def from_motif(self, cmd: str): return self - def from_nx(self, graph: nx.DiGraph) -> "dotmotif": + def from_nx(self, graph: nx.DiGraph) -> "Motif": """ Ingest directly from a graph. @@ -197,7 +197,7 @@ def save(self, fname: Union[str, IO[bytes]]) -> Union[str, IO[bytes]]: return fname @staticmethod - def load(fname: Union[str, IO[bytes]]) -> "dotmotif": + def load(fname: Union[str, IO[bytes]]) -> "Motif": """ Load the motif from a file on disk. @@ -215,6 +215,3 @@ def load(fname: Union[str, IO[bytes]]) -> "dotmotif": result = pickle.load(f) f.close() return result - - -dotmotif = Motif diff --git a/dotmotif/executors/GrandIsoExecutor.py b/dotmotif/executors/GrandIsoExecutor.py index 58a2b6e..6f9f835 100644 --- a/dotmotif/executors/GrandIsoExecutor.py +++ b/dotmotif/executors/GrandIsoExecutor.py @@ -83,10 +83,19 @@ def _node_attr_match_fn( is_node_attr_match=_node_attr_match_fn, ) + _edge_constraint_validator = ( + self._validate_edge_constraints if not self._host_is_multigraph + else ( + self._validate_multigraph_all_edge_constraints + if self._multigraph_edge_match == "all" + else self._validate_multigraph_any_edge_constraints + ) + ) + results = [] for r in graph_matches: if _doesnt_have_any_of_motifs_negative_edges(r) and ( - self._validate_edge_constraints( + _edge_constraint_validator( r, self.graph, motif.list_edge_constraints() ) # and self._validate_node_constraints( diff --git a/dotmotif/executors/Neo4jExecutor.py b/dotmotif/executors/Neo4jExecutor.py index f143349..aff11c6 100644 --- a/dotmotif/executors/Neo4jExecutor.py +++ b/dotmotif/executors/Neo4jExecutor.py @@ -1,5 +1,5 @@ """ -Copyright 2020 The Johns Hopkins University Applied Physics Laboratory. +Copyright 2021 The Johns Hopkins University Applied Physics Laboratory. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -280,12 +280,12 @@ def run(self, cypher: str, cursor=True): return self.G.run(cypher).to_table() return self.G.run(cypher) - def count(self, motif: "dotmotif", limit=None) -> int: + def count(self, motif: "dotmotif.Motif", limit=None) -> int: """ Count a motif in a larger graph. Arguments: - motif (dotmotif.dotmotif) + motif (dotmotif.Motif) """ qry = self.motif_to_cypher( @@ -295,12 +295,12 @@ def count(self, motif: "dotmotif", limit=None) -> int: qry += f" LIMIT {limit}" return int(self.G.run(qry).to_ndarray()) - def find(self, motif: "dotmotif", limit=None, cursor=True): + def find(self, motif: "dotmotif.Motif", limit=None, cursor=True): """ Find a motif in a larger graph. Arguments: - motif (dotmotif.dotmotif) + motif (dotmotif.Motif) """ qry = self.motif_to_cypher(motif, static_entity_labels=self._entity_labels) @@ -312,7 +312,7 @@ def find(self, motif: "dotmotif", limit=None, cursor=True): @staticmethod def motif_to_cypher( - motif: "dotmotif", count_only: bool = False, static_entity_labels: dict = None, + motif: "dotmotif.Motif", count_only: bool = False, static_entity_labels: dict = None, ) -> str: """ Output a query suitable for Cypher-compatible engines (e.g. Neo4j). diff --git a/dotmotif/executors/NetworkXExecutor.py b/dotmotif/executors/NetworkXExecutor.py index 2be7337..b9ffd78 100644 --- a/dotmotif/executors/NetworkXExecutor.py +++ b/dotmotif/executors/NetworkXExecutor.py @@ -14,9 +14,9 @@ limitations under the License.` """ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Tuple +import copy import networkx as nx -import pandas as pd from .Executor import Executor @@ -61,6 +61,27 @@ def _edge_satisfies_constraints(edge_attributes: dict, constraints: dict) -> boo return True +def _edge_satisfies_many_constraints_for_muligraph_any_edges(edge_attributes: dict, constraints: dict) -> List[Tuple[str, str, str]]: + """ + Returns a subset of constraints that this edge matches, in the form (key, op, val). + """ + matched_constraints = [] + for key, clist in constraints.items(): + for operator, values in clist.items(): + for value in values: + keyvalue_or_none = edge_attributes.get(key, None) + try: + operator_success = _OPERATORS[operator](keyvalue_or_none, value) + except TypeError: + # If you encounter a type error, that means the comparison + # could not possibly succeed + # # TODO: unless you tried a comparison + # against an undefined value (i.e. VALUE != undefined) + operator_success = False + if operator_success: + matched_constraints.append((key, operator, value)) + return matched_constraints + def _node_satisfies_constraints(node_attributes: dict, constraints: dict) -> bool: """ Check if a single node satisfies the constraints. @@ -89,6 +110,11 @@ def __init__(self, **kwargs) -> None: Arguments: graph (networkx.Graph) + multigraph_edge_match (str: 'any'): A string ('any' or 'all') that + determines how to match edges between nodes in the graph. If + 'any', then any edge between nodes can match the constraints + to satisfy the motif. If 'all', then all edges between nodes + must match the constraints to satisfy the motif. Returns: None @@ -101,6 +127,15 @@ def __init__(self, **kwargs) -> None: "You must pass a graph to the NetworkXExecutor constructor." ) + # Allow the user to set whether ALL edges should match when considering + # a multigraph or if ANY edges should be considered when matching. + # We only do this if the graph is a multigraph. + self._host_is_multigraph = False + if self.graph.is_multigraph(): + self._host_is_multigraph = True + self._multigraph_edge_match = kwargs.get("multigraph_edge_match", "any") + assert self._multigraph_edge_match in ("all", "any"), "_multigraph_edge_match must be one of 'all' or 'any'." + def _validate_node_constraints( self, node_isomorphism_map: dict, graph: nx.DiGraph, constraints: dict ) -> bool: @@ -200,7 +235,75 @@ def _validate_edge_constraints( return False return True - def count(self, motif: "dotmotif", limit: int = None): + def _validate_multigraph_all_edge_constraints( + self, node_isomorphism_map: dict, graph: nx.DiGraph, constraints: dict + ): + """ + Reuses logic from the simple _validate_edge_constraints case. + + Sole modification is that in the multigraph case, ALL edges must match + for an edge to be considered valid. If ANY of the edges between two + nodes mismatch the constraints, the mapping fails. + + """ + for (motif_U, motif_V), constraint_list in constraints.items(): + # Get graph nodes (from this isomorphism) + graph_u = node_isomorphism_map[motif_U] + graph_v = node_isomorphism_map[motif_V] + + # Check each edge in graph for constraints + for _, _, edge_attrs in graph.edges((graph_u, graph_v), data=True): + if not _edge_satisfies_constraints(edge_attrs, constraint_list): + # Fail fast + return False + return True + + def _validate_multigraph_any_edge_constraints( + self, node_isomorphism_map: dict, graph: nx.DiGraph, constraints: dict + ): + """ + Reuses logic from the simple _validate_edge_constraints case. + + Sole modification is that in the multigraph case, ANY edge can match + for an edge to be considered valid. If ANY of the edges between two + nodes match the constraints, the mapping succeeds. + + """ + for (motif_U, motif_V), constraint_list in constraints.items(): + # Get graph nodes (from this isomorphism) + graph_u = node_isomorphism_map[motif_U] + graph_v = node_isomorphism_map[motif_V] + + # check each edge in the graph for the constraints. + # if you find an edge that matches, REMOVE that constraint from + # the list and continue checking. + # if you get to the end of the list of edges and there are any + # constrains left, the mapping fails. + + # Check each edge in graph for constraints + constraint_list_copy = copy.deepcopy(constraint_list) + for _, _, edge_attrs in graph.edges((graph_u, graph_v), data=True): + matched_constraints = _edge_satisfies_many_constraints_for_muligraph_any_edges(edge_attrs, constraint_list_copy) + if matched_constraints: + # Remove matched constraints from the list + for constraint in matched_constraints: + (key, operator, value) = constraint + # remove `value` from the list of [key][operator]. + # if the list is empty, remove the entire key. + constraint_list_copy[key][operator].remove(value) + if len(constraint_list_copy[key][operator]) == 0: + del constraint_list_copy[key][operator] + if not constraint_list_copy[key]: + del constraint_list_copy[key] + + # if there are any constraints left over, the mapping failed + if len(constraint_list_copy) > 0: + return False + + return True + + + def count(self, motif: "dotmotif.Motif", limit: int = None): """ Count the occurrences of a motif in a graph. @@ -208,12 +311,12 @@ def count(self, motif: "dotmotif", limit: int = None): """ return len(self.find(motif, limit)) - def find(self, motif: "dotmotif", limit: int = None): + def find(self, motif: "dotmotif.Motif", limit: int = None): """ Find a motif in a larger graph. Arguments: - motif (dotmotif.dotmotif) + motif (dotmotif.Motif) """ # TODO: Can add constraints on iso node assignment. If we do this a @@ -263,12 +366,19 @@ def _doesnt_have_any_of_motifs_negative_edges(mapping): if _doesnt_have_any_of_motifs_negative_edges(mapping) ] + _edge_constraint_validator = ( + self._validate_edge_constraints if not self._host_is_multigraph else ( + self._validate_multigraph_all_edge_constraints + if self._multigraph_edge_match == "all" + else self._validate_multigraph_any_edge_constraints + ) + ) # Now, filter on attributes: res = [ r for r in results if ( - self._validate_edge_constraints( + _edge_constraint_validator( r, self.graph, motif.list_edge_constraints() ) and self._validate_node_constraints( diff --git a/dotmotif/executors/NeuPrintExecutor.py b/dotmotif/executors/NeuPrintExecutor.py index 604e815..b04013a 100644 --- a/dotmotif/executors/NeuPrintExecutor.py +++ b/dotmotif/executors/NeuPrintExecutor.py @@ -1,7 +1,7 @@ import pandas as pd from neuprint import Client -from .. import dotmotif +from .. import Motif from .Neo4jExecutor import Neo4jExecutor _LOOKUP = { @@ -71,12 +71,12 @@ def run(self, cypher: str) -> pd.DataFrame: """ return self.client.fetch_custom(cypher) - def count(self, motif: dotmotif, limit=None) -> int: + def count(self, motif: Motif, limit=None) -> int: """ Count a motif in a larger graph. Arguments: - motif (dotmotif.dotmotif): The motif to search for + motif (dotmotif.Motif): The motif to search for Returns: int: The count of this motif in the host graph @@ -91,12 +91,12 @@ def count(self, motif: dotmotif, limit=None) -> int: print(res) return int(res.to_numpy()) - def find(self, motif: dotmotif, limit=None) -> pd.DataFrame: + def find(self, motif: Motif, limit=None) -> pd.DataFrame: """ Find a motif in a larger graph. Arguments: - motif (dotmotif.dotmotif): The motif to search for + motif (dotmotif.Motif): The motif to search for Returns: pd.DataFrame: The results of the search @@ -109,7 +109,7 @@ def find(self, motif: dotmotif, limit=None) -> pd.DataFrame: @staticmethod def motif_to_cypher( - motif: dotmotif, count_only: bool = False, static_entity_labels: dict = None + motif: Motif, count_only: bool = False, static_entity_labels: dict = None ) -> str: """ Convert a motif to neuprint-flavored Cypher. diff --git a/dotmotif/executors/test_dm_cypher.py b/dotmotif/executors/test_dm_cypher.py index 7b88ef0..e84709c 100644 --- a/dotmotif/executors/test_dm_cypher.py +++ b/dotmotif/executors/test_dm_cypher.py @@ -47,7 +47,7 @@ class TestDotmotif_Cypher(unittest.TestCase): def test_cypher(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif(_DEMO_G_MIN) self.assertEqual( Neo4jExecutor.motif_to_cypher(dm).strip(), _DEMO_G_MIN_CYPHER.strip() @@ -56,7 +56,7 @@ def test_cypher(self): class TestDotmotif_edges_Cypher(unittest.TestCase): def test_cypher_edge_attributes(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif( """ A -> B [weight=4, area<=10] @@ -69,7 +69,7 @@ def test_cypher_edge_attributes(self): ) def test_cypher_edge_many_attributes(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif( """ A -> B [weight=4, area<=10, area<=20] @@ -83,7 +83,7 @@ def test_cypher_edge_many_attributes(self): # TODO # Issue with arbitrary ordering of inequalities # def test_cypher_edge_many_attributes_and_enforce_inequality(self): - # dm = dotmotif.dotmotif(enforce_inequality=True) + # dm = dotmotif.Motif(enforce_inequality=True) # dm.from_motif( # """ # A -> B [weight=4, area<=10, area<=20] @@ -99,7 +99,7 @@ def test_cypher_edge_many_attributes(self): class TestDotmotif_nodes_Cypher(unittest.TestCase): def test_cypher_node_attributes(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif( """ A -> B @@ -113,7 +113,7 @@ def test_cypher_node_attributes(self): ) def test_cypher_node_many_attributes(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif( """ A -> B @@ -128,7 +128,7 @@ def test_cypher_node_many_attributes(self): ) def test_cypher_node_same_node_many_attributes(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif( """ A -> B @@ -143,7 +143,7 @@ def test_cypher_node_same_node_many_attributes(self): ) def test_cypher_node_many_node_attributes(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif( """ A -> B @@ -158,7 +158,7 @@ def test_cypher_node_many_node_attributes(self): ) def test_cypher_negative_edge_and_inequality(self): - dm = dotmotif.dotmotif(enforce_inequality=True) + dm = dotmotif.Motif(enforce_inequality=True) dm.from_motif( """ A -> B @@ -180,7 +180,7 @@ def test_cypher_negative_edge_and_inequality(self): class TestDotmotif_nodes_edges_Cypher(unittest.TestCase): def test_cypher_node_and_edge_attributes(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif( """ A -> B [area != 10] @@ -201,7 +201,7 @@ def test_cypher_node_and_edge_attributes(self): class TestDynamicNodeConstraints(unittest.TestCase): def test_dynamic_constraints_in_cypher(self): - dm = dotmotif.dotmotif(enforce_inequality=True) + dm = dotmotif.Motif(enforce_inequality=True) dm.from_motif( """ A -> B @@ -217,7 +217,7 @@ def test_dynamic_constraints_in_cypher(self): class BugReports(unittest.TestCase): def test_fix_where_clause__github_35(self): - dm = dotmotif.dotmotif(enforce_inequality=True) + dm = dotmotif.Motif(enforce_inequality=True) dm.from_motif( """ A -> B diff --git a/dotmotif/executors/test_grandisoexecutor.py b/dotmotif/executors/test_grandisoexecutor.py index dd93acf..95cd88b 100644 --- a/dotmotif/executors/test_grandisoexecutor.py +++ b/dotmotif/executors/test_grandisoexecutor.py @@ -1,5 +1,6 @@ import unittest import dotmotif +from dotmotif import Motif from dotmotif.executors import GrandIsoExecutor from dotmotif.executors.NetworkXExecutor import ( _edge_satisfies_constraints, @@ -11,8 +12,7 @@ class TestSmallMotifs(unittest.TestCase): def test_edgecount_motif(self): - dm = dotmotif.dotmotif() - dm.from_motif("""A->B""") + dm = Motif("""A->B""") H = nx.DiGraph() H.add_edge("x", "y") @@ -23,8 +23,7 @@ def test_edgecount_motif(self): self.assertEqual(len(GrandIsoExecutor(graph=H).find(dm)), 2) def test_fullyconnected_triangle_motif(self): - dm = dotmotif.dotmotif() - dm.from_motif( + dm = Motif( """ A->B B->C @@ -41,8 +40,7 @@ def test_fullyconnected_triangle_motif(self): self.assertEqual(len(GrandIsoExecutor(graph=H).find(dm)), 3) def test_edge_attribute_equality(self): - dm = dotmotif.dotmotif() - dm.from_motif( + dm = Motif( """ A->B [weight==10, area==4] """ @@ -60,7 +58,7 @@ def test_one_instance(self): H.add_edge("x", "y", weight=1) H.add_edge("y", "z", weight=10) H.add_edge("z", "x", weight=5) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [weight>=11] """.strip() @@ -77,7 +75,7 @@ def test_two_instance(self): H.add_edge("a", "b", weight=1) H.add_edge("b", "c", weight=10) H.add_edge("c", "a", weight=5) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [weight>=7] """.strip() @@ -94,7 +92,7 @@ def test_triangle_two_instance(self): H.add_edge("a", "b", weight=1) H.add_edge("b", "c", weight=10) H.add_edge("c", "a", weight=5) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [weight>=7] B -> C @@ -109,7 +107,7 @@ def test_mini_example(self): H = nx.DiGraph() H.add_edge("y", "x", ATTRIBUTE=7) H.add_edge("y", "z", ATTRIBUTE=7) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [ATTRIBUTE>=7] """.strip() @@ -123,7 +121,7 @@ def test_node_and_edge_full_example(self): H.add_edge("X", "Y", weight=10) H.add_edge("Y", "Z", weight=9) H.add_edge("Z", "X", weight=8) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [weight>=7] """.strip() @@ -152,7 +150,7 @@ def test_automorphism_reduction(self): G.add_edge("X", "Z") G.add_edge("Y", "Z") - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> C B -> C @@ -164,7 +162,7 @@ def test_automorphism_reduction(self): res = GrandIsoExecutor(graph=G).find(motif) self.assertEqual(len(res), 2) - motif = dotmotif.dotmotif(exclude_automorphisms=True).from_motif( + motif = dotmotif.Motif(exclude_automorphisms=True).from_motif( """ A -> C B -> C @@ -182,7 +180,7 @@ def test_automorphism_auto(self): G.add_edge("X", "Z") G.add_edge("Y", "Z") - motif = dotmotif.dotmotif(exclude_automorphisms=True).from_motif( + motif = dotmotif.Motif(exclude_automorphisms=True).from_motif( """ A -> C B -> C @@ -198,7 +196,7 @@ def test_automorphism_notauto(self): G.add_edge("X", "Z") G.add_edge("Y", "Z") - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> C B -> C @@ -215,7 +213,7 @@ def test_automorphism_flag_triangle(self): G.add_edge("B", "C") G.add_edge("C", "A") - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B B -> C @@ -225,7 +223,7 @@ def test_automorphism_flag_triangle(self): res = GrandIsoExecutor(graph=G).find(motif) self.assertEqual(len(res), 3) - motif = dotmotif.dotmotif(exclude_automorphisms=True).from_motif( + motif = dotmotif.Motif(exclude_automorphisms=True).from_motif( """ A -> B B -> C @@ -254,7 +252,7 @@ def test_dynamic_constraints_zero_results(self): A -> B A.radius > B.radius """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = GrandIsoExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 0) @@ -275,7 +273,7 @@ def test_dynamic_constraints_one_result(self): A -> B A.radius > B.radius """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = GrandIsoExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 1) @@ -297,7 +295,7 @@ def test_dynamic_constraints_two_results(self): A -> B A.radius > B.radius """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = GrandIsoExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 2) @@ -321,7 +319,7 @@ def test_dynamic_constraints_in_macros_zero_results(self): macro(A, B) A -> B """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = GrandIsoExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 0) @@ -345,7 +343,7 @@ def test_dynamic_constraints_in_macros_one_result(self): macro(A, B) A -> B """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = GrandIsoExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 1) @@ -370,6 +368,6 @@ def test_dynamic_constraints_in_macros_two_result(self): macro(A, B) A -> B """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = GrandIsoExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 2) diff --git a/dotmotif/executors/test_neo4jexecutor.py b/dotmotif/executors/test_neo4jexecutor.py index de8b06f..6e5141d 100644 --- a/dotmotif/executors/test_neo4jexecutor.py +++ b/dotmotif/executors/test_neo4jexecutor.py @@ -19,7 +19,7 @@ def test_basic_node_attr(self): B -> C A === B """ - dm = dotmotif.dotmotif().from_motif(exp) + dm = dotmotif.Motif(exp) cypher = Neo4jExecutor.motif_to_cypher(dm) self.assertIn("id(A) < id(B)", cypher) @@ -28,6 +28,6 @@ def test_automatic_autos(self): A -> C B -> C """ - dm = dotmotif.dotmotif(exclude_automorphisms=True).from_motif(exp) + dm = dotmotif.Motif(exp, exclude_automorphisms=True) cypher = Neo4jExecutor.motif_to_cypher(dm) self.assertIn("id(A) < id(B)", cypher) diff --git a/dotmotif/executors/test_networkxexecutor.py b/dotmotif/executors/test_networkxexecutor.py index 02a0aff..fb1919e 100644 --- a/dotmotif/executors/test_networkxexecutor.py +++ b/dotmotif/executors/test_networkxexecutor.py @@ -196,8 +196,7 @@ def test_in_string(self): class TestSmallMotifs(unittest.TestCase): def test_edgecount_motif(self): - dm = dotmotif.dotmotif() - dm.from_motif("""A->B""") + dm = dotmotif.Motif("""A->B""") H = nx.DiGraph() H.add_edge("x", "y") @@ -208,8 +207,7 @@ def test_edgecount_motif(self): self.assertEqual(len(NetworkXExecutor(graph=H).find(dm)), 2) def test_fullyconnected_triangle_motif(self): - dm = dotmotif.dotmotif() - dm.from_motif( + dm = dotmotif.Motif( """ A->B B->C @@ -226,8 +224,7 @@ def test_fullyconnected_triangle_motif(self): self.assertEqual(len(NetworkXExecutor(graph=H).find(dm)), 3) def test_edge_attribute_equality(self): - dm = dotmotif.dotmotif() - dm.from_motif( + dm = dotmotif.Motif( """ A->B [weight==10, area==4] """ @@ -245,7 +242,7 @@ def test_one_instance(self): H.add_edge("x", "y", weight=1) H.add_edge("y", "z", weight=10) H.add_edge("z", "x", weight=5) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [weight>=11] """.strip() @@ -262,7 +259,7 @@ def test_two_instance(self): H.add_edge("a", "b", weight=1) H.add_edge("b", "c", weight=10) H.add_edge("c", "a", weight=5) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [weight>=7] """.strip() @@ -279,7 +276,7 @@ def test_triangle_two_instance(self): H.add_edge("a", "b", weight=1) H.add_edge("b", "c", weight=10) H.add_edge("c", "a", weight=5) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [weight>=7] B -> C @@ -294,7 +291,7 @@ def test_mini_example(self): H = nx.DiGraph() H.add_edge("y", "x", ATTRIBUTE=7) H.add_edge("y", "z", ATTRIBUTE=7) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [ATTRIBUTE>=7] """.strip() @@ -308,7 +305,7 @@ def test_node_and_edge_full_example(self): H.add_edge("X", "Y", weight=10) H.add_edge("Y", "Z", weight=9) H.add_edge("Z", "X", weight=8) - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B [weight>=7] """.strip() @@ -337,7 +334,7 @@ def test_automorphism_reduction(self): G.add_edge("X", "Z") G.add_edge("Y", "Z") - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> C B -> C @@ -349,7 +346,7 @@ def test_automorphism_reduction(self): res = NetworkXExecutor(graph=G).find(motif) self.assertEqual(len(res), 2) - motif = dotmotif.dotmotif(exclude_automorphisms=True).from_motif( + motif = dotmotif.Motif(exclude_automorphisms=True).from_motif( """ A -> C B -> C @@ -367,7 +364,7 @@ def test_automorphism_auto(self): G.add_edge("X", "Z") G.add_edge("Y", "Z") - motif = dotmotif.dotmotif(exclude_automorphisms=True).from_motif( + motif = dotmotif.Motif(exclude_automorphisms=True).from_motif( """ A -> C B -> C @@ -383,7 +380,7 @@ def test_automorphism_notauto(self): G.add_edge("X", "Z") G.add_edge("Y", "Z") - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> C B -> C @@ -400,7 +397,7 @@ def test_automorphism_flag_triangle(self): G.add_edge("B", "C") G.add_edge("C", "A") - motif = dotmotif.dotmotif().from_motif( + motif = dotmotif.Motif( """ A -> B B -> C @@ -410,7 +407,7 @@ def test_automorphism_flag_triangle(self): res = NetworkXExecutor(graph=G).find(motif) self.assertEqual(len(res), 3) - motif = dotmotif.dotmotif(exclude_automorphisms=True).from_motif( + motif = dotmotif.Motif(exclude_automorphisms=True).from_motif( """ A -> B B -> C @@ -439,7 +436,7 @@ def test_dynamic_constraints_zero_results(self): A -> B A.radius > B.radius """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = NetworkXExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 0) @@ -460,7 +457,7 @@ def test_dynamic_constraints_one_result(self): A -> B A.radius > B.radius """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = NetworkXExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 1) @@ -482,7 +479,7 @@ def test_dynamic_constraints_two_results(self): A -> B A.radius > B.radius """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = NetworkXExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 2) @@ -506,7 +503,7 @@ def test_dynamic_constraints_in_macros_zero_results(self): macro(A, B) A -> B """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = NetworkXExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 0) @@ -530,7 +527,7 @@ def test_dynamic_constraints_in_macros_one_result(self): macro(A, B) A -> B """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = NetworkXExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 1) @@ -555,7 +552,7 @@ def test_dynamic_constraints_in_macros_two_result(self): macro(A, B) A -> B """ - dm = dotmotif.dotmotif(parser=ParserV2) + dm = dotmotif.Motif(parser=ParserV2) res = NetworkXExecutor(graph=G).find(dm.from_motif(exp)) self.assertEqual(len(res), 2) diff --git a/dotmotif/executors/test_neuprintexecutor.py b/dotmotif/executors/test_neuprintexecutor.py index 0d22d88..a6b27ea 100644 --- a/dotmotif/executors/test_neuprintexecutor.py +++ b/dotmotif/executors/test_neuprintexecutor.py @@ -6,7 +6,7 @@ import unittest -from .. import dotmotif +from .. import Motif from .NeuPrintExecutor import NeuPrintExecutor if TOKEN: @@ -18,7 +18,7 @@ def test_can_get_version(self): class TestNeuPrintKnownMotifs(unittest.TestCase): def test_known_edge(self): - motif = dotmotif().from_motif( + motif = Motif().from_motif( """ input -> output input.type == "KCab-p" @@ -30,7 +30,7 @@ def test_known_edge(self): self.assertEqual(len(E.find(motif, limit=5)), 5) def test_bigger_motif(self): - motif = dotmotif().from_motif( + motif = Motif().from_motif( """ diedge(a, b) { a -> b diff --git a/dotmotif/parsers/v1/test_dm_parse.py b/dotmotif/parsers/v1/test_dm_parse.py index 7ba82ca..50dd5cf 100644 --- a/dotmotif/parsers/v1/test_dm_parse.py +++ b/dotmotif/parsers/v1/test_dm_parse.py @@ -17,29 +17,23 @@ def test_sanity(self): self.assertEqual(1, 1) def test_dm_parser(self): - dm = dotmotif.dotmotif() - dm.from_motif(_THREE_CYCLE) + dm = dotmotif.Motif(_THREE_CYCLE) self.assertEqual(len(dm._g.edges()), 3) self.assertEqual(len(dm._g.nodes()), 3) def test_dm_parser_actions(self): - dm = dotmotif.dotmotif() - dm.from_motif(_THREE_CYCLE) + dm = dotmotif.Motif(_THREE_CYCLE) self.assertEqual([e[2]["action"] for e in dm._g.edges(data=True)], ["SYN"] * 3) - dm = dotmotif.dotmotif() - dm.from_motif(_THREE_CYCLE_INH) + dm = dotmotif.Motif(_THREE_CYCLE_INH) self.assertEqual([e[2]["action"] for e in dm._g.edges(data=True)], ["INH"] * 3) def test_dm_parser_edge_exists(self): - dm = dotmotif.dotmotif() - dm.from_motif(_THREE_CYCLE) + dm = dotmotif.Motif(_THREE_CYCLE) self.assertEqual([e[2]["exists"] for e in dm._g.edges(data=True)], [True] * 3) - dm = dotmotif.dotmotif() - dm.from_motif(_THREE_CYCLE_NEG) + dm = dotmotif.Motif(_THREE_CYCLE_NEG) self.assertEqual([e[2]["exists"] for e in dm._g.edges(data=True)], [False] * 3) - dm = dotmotif.dotmotif() - dm.from_motif(_THREE_CYCLE_NEG_INH) + dm = dotmotif.Motif(_THREE_CYCLE_NEG_INH) self.assertEqual([e[2]["exists"] for e in dm._g.edges(data=True)], [False] * 3) diff --git a/dotmotif/parsers/v2/test_v2_parser.py b/dotmotif/parsers/v2/test_v2_parser.py index c681619..222b145 100644 --- a/dotmotif/parsers/v2/test_v2_parser.py +++ b/dotmotif/parsers/v2/test_v2_parser.py @@ -18,31 +18,25 @@ def test_sanity(self): self.assertEqual(1, 1) def test_dm_parser(self): - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(_THREE_CYCLE) + dm = dotmotif.Motif(_THREE_CYCLE) self.assertEqual(len(dm._g.edges()), 3) self.assertEqual(len(dm._g.nodes()), 3) def test_dm_parser_actions(self): - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(_THREE_CYCLE) + dm = dotmotif.Motif(_THREE_CYCLE) self.assertEqual([e[2]["action"] for e in dm._g.edges(data=True)], ["SYN"] * 3) - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(_THREE_CYCLE_INH) + dm = dotmotif.Motif(_THREE_CYCLE_INH) self.assertEqual([e[2]["action"] for e in dm._g.edges(data=True)], ["INH"] * 3) def test_dm_parser_edge_exists(self): - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(_THREE_CYCLE) + dm = dotmotif.Motif(_THREE_CYCLE) self.assertEqual([e[2]["exists"] for e in dm._g.edges(data=True)], [True] * 3) - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(_THREE_CYCLE_NEG) + dm = dotmotif.Motif(_THREE_CYCLE_NEG) self.assertEqual([e[2]["exists"] for e in dm._g.edges(data=True)], [False] * 3) - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(_THREE_CYCLE_NEG_INH) + dm = dotmotif.Motif(_THREE_CYCLE_NEG_INH) self.assertEqual([e[2]["exists"] for e in dm._g.edges(data=True)], [False] * 3) @@ -53,8 +47,7 @@ def test_macro_not_added(self): A -> B } """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm._g.edges()), 0) def test_simple_macro(self): @@ -64,8 +57,7 @@ def test_simple_macro(self): } edge(C, D) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm._g.edges()), 1) def test_simple_macro_construction(self): @@ -75,8 +67,7 @@ def test_simple_macro_construction(self): } edge(C, D) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) exp_edge = list(dm._g.edges(data=True))[0] self.assertEqual(exp_edge[0], "C") self.assertEqual(exp_edge[1], "D") @@ -89,8 +80,7 @@ def test_multiline_macro_construction(self): } dualedge(C, D) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) exp_edge = list(dm._g.edges(data=True))[0] self.assertEqual(exp_edge[0], "C") self.assertEqual(exp_edge[1], "D") @@ -105,8 +95,7 @@ def test_undefined_macro(self): """ # with self.assertRaises(ValueError): with self.assertRaises(Exception): - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dotmotif.Motif(exp) def test_wrong_args_macro(self): exp = """\ @@ -118,8 +107,7 @@ def test_wrong_args_macro(self): """ # with self.assertRaises(ValueError): with self.assertRaises(Exception): - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dotmotif.Motif(exp) def test_more_complex_macro(self): exp = """\ @@ -130,8 +118,7 @@ def test_more_complex_macro(self): } tri(C, D, E) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 3) @@ -145,8 +132,7 @@ def test_macro_reuse(self): tri(C, D, E) tri(F, G, H) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 6) @@ -168,8 +154,7 @@ def test_conflicting_macro_invalid_edge_throws(self): """ # with self.assertRaises(dotmotif.validators.DisagreeingEdgesValidatorError): with self.assertRaises(Exception): - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dotmotif.Motif(exp) def test_nested_macros(self): exp = """\ @@ -184,8 +169,7 @@ def test_nested_macros(self): } dualtri(foo, bar, baz) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 6) @@ -205,8 +189,7 @@ def test_deeply_nested_macros(self): } dualtri(foo, bar, baz) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 6) @@ -229,8 +212,7 @@ def test_clustercuss_macros_no_repeats(self): dualtri(foo, bar, baz) dualtri(foo, bar, baf) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 10) @@ -249,8 +231,7 @@ def test_comment_in_macro(self): dualedge(foo, bar) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 2) @@ -267,8 +248,7 @@ def test_combo_macro(self): dualedge(foo, bar) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 2) @@ -287,8 +267,7 @@ def test_comment_macro_inline(self): # standalone comment foo -> bar # inline comment """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 2) @@ -307,8 +286,7 @@ def test_alphanumeric_variables(self): # standalone comment foo_1 -> bar_2 # inline comment """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) edges = list(dm._g.edges(data=True)) self.assertEqual(len(edges), 2) self.assertEqual(list(dm._g.nodes()), ["foo_1", "bar_2"]) @@ -319,8 +297,7 @@ def test_alphanumeric_variables(self): L1 -> Tm3 L3 -> Mi9 """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(new_exp) + dm = dotmotif.Motif(new_exp) self.assertEqual(list(dm._g.nodes()), ["L1", "Mi1", "Tm3", "L3", "Mi9"]) @@ -329,8 +306,7 @@ def test_basic_edge_attr(self): exp = """\ Aa -> Ba [type == 1] """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm._g.edges()), 1) u, v, d = list(dm._g.edges(["Aa", "Bb"], data=True))[0] self.assertEqual(type(list(dm._g.nodes())[0]), str) @@ -341,8 +317,7 @@ def test_edge_multi_attr(self): exp = """\ Aa -> Ba [type != 1, type != 12] """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm._g.edges()), 1) u, v, d = list(dm._g.edges(data=True))[0] self.assertEqual(d["constraints"]["type"], {"!=": [1, 12]}) @@ -355,8 +330,7 @@ def test_edge_macro_attr(self): macro(X, Y) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm._g.edges()), 1) u, v, d = list(dm._g.edges(data=True))[0] self.assertEqual(d["constraints"]["type"], {"!=": [1, 12]}) @@ -369,8 +343,7 @@ def test_basic_node_attr(self): Aa.type = "excitatory" """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm.list_node_constraints()), 1) self.assertEqual(list(dm.list_node_constraints().keys()), ["Aa"]) @@ -381,8 +354,7 @@ def test_node_multi_attr(self): Aa.type = "excitatory" Aa.size = 4.5 """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm.list_node_constraints()), 1) self.assertEqual(len(dm.list_node_constraints()["Aa"]), 2) self.assertEqual(dm.list_node_constraints()["Aa"]["type"]["="], ["excitatory"]) @@ -396,8 +368,7 @@ def test_multi_node_attr(self): Aa.type = "excitatory" Ba.size=4.0 """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm.list_node_constraints()), 2) self.assertEqual(list(dm.list_node_constraints().keys()), ["Aa", "Ba"]) @@ -410,8 +381,7 @@ def test_node_macro_attr(self): Aaa -> Ba macro(Aaa) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm.list_node_constraints()), 1) self.assertEqual(list(dm.list_node_constraints().keys()), ["Aaa"]) @@ -424,8 +394,7 @@ def test_node_macro_attr(self): macro(Aaa) macro(Ba) """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm.list_node_constraints()), 2) self.assertEqual(list(dm.list_node_constraints().keys()), ["Aaa", "Ba"]) @@ -442,8 +411,7 @@ def test_dynamic_constraints(self): A -> B A.radius < B.radius """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm.list_dynamic_node_constraints()), 1) def test_dynamic_constraints_in_macro(self): @@ -460,6 +428,5 @@ def test_dynamic_constraints_in_macro(self): macro(A, B) A -> B """ - dm = dotmotif.dotmotif(parser=ParserV2) - dm.from_motif(exp) + dm = dotmotif.Motif(exp) self.assertEqual(len(dm.list_dynamic_node_constraints()), 1) diff --git a/dotmotif/tests/test_dm_flags.py b/dotmotif/tests/test_dm_flags.py index e46bad2..891a97f 100644 --- a/dotmotif/tests/test_dm_flags.py +++ b/dotmotif/tests/test_dm_flags.py @@ -23,14 +23,14 @@ def test_sanity(self): self.assertEqual(1, 1) def test_dm_parser_defaults(self): - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif(_DEMO_G_MIN) self.assertEqual( Neo4jExecutor.motif_to_cypher(dm).strip(), _DEMO_G_MIN_CYPHER.strip() ) def test_dm_parser_no_pretty_print(self): - dm = dotmotif.dotmotif(pretty_print=False) + dm = dotmotif.Motif(pretty_print=False) dm.from_motif(_DEMO_G_MIN) self.assertEqual( Neo4jExecutor.motif_to_cypher(dm).strip(), @@ -38,7 +38,7 @@ def test_dm_parser_no_pretty_print(self): ) def test_dm_parser_no_direction(self): - dm = dotmotif.dotmotif(ignore_direction=True) + dm = dotmotif.Motif(ignore_direction=True) dm.from_motif(_DEMO_G_MIN) self.assertEqual( Neo4jExecutor.motif_to_cypher(dm).strip(), @@ -46,7 +46,7 @@ def test_dm_parser_no_direction(self): ) def test_dm_parser_inequality(self): - dm = dotmotif.dotmotif(enforce_inequality=True) + dm = dotmotif.Motif(enforce_inequality=True) dm.from_motif(_DEMO_G_MIN) self.assertTrue( "A<>B" in Neo4jExecutor.motif_to_cypher(dm).strip() @@ -62,10 +62,10 @@ def test_dm_parser_inequality(self): ) def test_dm_parser_limit(self): - dm = dotmotif.dotmotif(limit=3) + dm = dotmotif.Motif(limit=3) dm.from_motif(_DEMO_G_MIN) self.assertTrue("LIMIT 3" in Neo4jExecutor.motif_to_cypher(dm).strip()) - dm = dotmotif.dotmotif() + dm = dotmotif.Motif() dm.from_motif(_DEMO_G_MIN) self.assertFalse("LIMIT" in Neo4jExecutor.motif_to_cypher(dm).strip()) @@ -76,7 +76,7 @@ def test_from_nx_import(self): g = nx.Graph() g.add_edge("A", "B") - dm = dotmotif.dotmotif().from_nx(g) + dm = dotmotif.Motif().from_nx(g) E = NetworkXExecutor(graph=G) self.assertEqual(len(E.find(dm)), 4) @@ -88,7 +88,7 @@ def test_from_nx_import(self): g = nx.Graph() g.add_edge("A", "B") - dm = dotmotif.dotmotif(ignore_direction=True).from_nx(g) + dm = dotmotif.Motif(ignore_direction=True).from_nx(g) E = NetworkXExecutor(graph=G) self.assertEqual(len(E.find(dm)), 4) diff --git a/dotmotif/tests/test_multigraphs.py b/dotmotif/tests/test_multigraphs.py new file mode 100644 index 0000000..f344247 --- /dev/null +++ b/dotmotif/tests/test_multigraphs.py @@ -0,0 +1,175 @@ + +""" + +## Multigraph Support + +The host graph argument to `NetworkXExecutor` and `GrandIsoExecutor` can be a +`nx.MultiDiGraph` or `nx.MultiGraph`. If the host graph is a `nx.MultiDiGraph`, +then the user can specify whether they would like to match all edges or any edge. + +If the user wants ALL edges between two nodes to satisfy the constraints (e.g., +`a -> b [size > 10]`) for all edges between nodes `a` and `b`) then they should +set `multigraph_edge_match = 'all'`. + +If the user wants the mapping to succeed if ANY edge between two nodes satisfies +the constraints (e.g., `a -> b [size > 10]` is true for at least one of the +edges between `a` and `b`) then they should set `multigraph_edge_match = 'any'`. + + +""" + +import networkx as nx +import pytest + +from dotmotif.executors.NetworkXExecutor import NetworkXExecutor +from dotmotif.executors.GrandIsoExecutor import GrandIsoExecutor +from dotmotif import Motif + +@pytest.mark.parametrize("executor", [NetworkXExecutor, GrandIsoExecutor]) +def test_multidigraph_all_edges(executor): + """ + Test that multigraph support works in GrandIso and NetworkX executors. + + Test setting `multigraph_match_all_edges = True`. + """ + + haystack = nx.MultiDiGraph() + haystack.add_edge("A", "B", size=10) + haystack.add_edge("A", "B", size=20) + + motif = Motif(""" + a -> b [size > 9] + """) + + results = executor(graph=haystack, multigraph_match_all_edges=True).find(motif) + + assert len(results) == 1 + + +@pytest.mark.parametrize("executor", [NetworkXExecutor, GrandIsoExecutor]) +def test_multigraph_any_edges(executor): + """ + Test that multigraph support works in GrandIso and NetworkX executors. + + Test setting `multigraph_match_all_edges = False`. + """ + + haystack = nx.MultiDiGraph() + haystack.add_edge("A", "B", size=10) + haystack.add_edge("A", "B", size=20) + + motif = Motif(""" + a -> b [size > 15] + """) + + results = executor(graph=haystack, multigraph_edge_match="any").find(motif) + + assert len(results) == 1 + + +@pytest.mark.parametrize("executor", [NetworkXExecutor, GrandIsoExecutor]) +def test_multigraph_basic(executor): + """ + Test that we can match the "basic" case where one attribute is specified + per edge, and one edge between two nodes satisfies it. + """ + + haystack = nx.MultiDiGraph() + haystack.add_edge("A", "B", size=10) + haystack.add_edge("A", "B", size=20) + haystack.add_edge("B", "C", size=20) + + motif = Motif(""" + a -> b [size > 15] + """) + + results = executor(graph=haystack, multigraph_edge_match="any").find(motif) + assert len(results) == 2 + results = executor(graph=haystack, multigraph_edge_match="all").find(motif) + assert len(results) == 1 + +@pytest.mark.parametrize("executor", [NetworkXExecutor, GrandIsoExecutor]) +def test_impossible_constraint_works_on_multigraph(executor): + """ + Tests that an "impossible" constraint on a simple graph works on a multigraph. + """ + + haystack = nx.MultiDiGraph() + haystack.add_edge("A", "B", size=10) + haystack.add_edge("A", "B", size=20) + haystack.add_edge("B", "C", size=30) + haystack.add_edge("B", "C", size=40) + + motif = Motif(""" + a -> b [size >= 15, size < 19] + """) + + results = executor(graph=haystack, multigraph_edge_match="any").find(motif) + assert len(results) == 1 + + results = executor(graph=haystack, multigraph_edge_match="all").find(motif) + assert len(results) == 0 + + results = executor(graph=nx.DiGraph(haystack)).find(motif) + assert len(results) == 0 + + +@pytest.mark.parametrize("executor", [NetworkXExecutor, GrandIsoExecutor]) +def test_complex_multigraph(executor): + """ + Tests that an "impossible" constraint on a simple graph works on a multigraph. + """ + + haystack = nx.MultiDiGraph() + haystack.add_edge("A", "B", size=10) + haystack.add_edge("A", "B", size=20) + haystack.add_edge("B", "C", size=30) + haystack.add_edge("B", "C", size=40) + haystack.add_edge("C", "A", size=50) + haystack.add_edge("C", "A", size=60) + + motif = Motif(""" + a -> b [size >= 15, size < 19] + b -> c [size > 20] + c -> a [size > 55] + """) + + results = executor(graph=haystack, multigraph_edge_match="any").find(motif) + assert len(results) == 2 + + results = executor(graph=haystack, multigraph_edge_match="all").find(motif) + assert len(results) == 0 + + results = executor(graph=nx.DiGraph(haystack)).find(motif) + assert len(results) == 0 + + +@pytest.mark.parametrize("executor", [NetworkXExecutor, GrandIsoExecutor]) +def test_complex_multigraph_fails(executor): + """ + Tests that an "impossible" constraint on a simple graph works on a multigraph. + """ + + haystack = nx.MultiDiGraph() + haystack.add_edge("A", "B", size=10) + haystack.add_edge("A", "B", size=20) + haystack.add_edge("B", "C", size=3) + haystack.add_edge("B", "C", size=4) + haystack.add_edge("C", "A", size=5) + haystack.add_edge("C", "A", size=6) + + motif = Motif(""" + a -> b [size > 15, size > 21] + b -> c [size > 20] + c -> a [size > 55] + """) + + results = executor(graph=haystack, multigraph_edge_match="any").find(motif) + assert len(results) == 0 + + results = executor(graph=haystack, multigraph_edge_match="all").find(motif) + assert len(results) == 0 + + results = executor(graph=nx.DiGraph(haystack)).find(motif) + assert len(results) == 0 + diff --git a/dotmotif/tests/test_utils.py b/dotmotif/tests/test_utils.py index 0509f83..ef54ff2 100644 --- a/dotmotif/tests/test_utils.py +++ b/dotmotif/tests/test_utils.py @@ -1,7 +1,7 @@ from unittest import TestCase import networkx as nx from ..utils import untype_string -from .. import dotmotif +from .. import Motif from tempfile import NamedTemporaryFile @@ -21,7 +21,7 @@ def test_untype_string_string(self): class TestSaveLoad(TestCase): def test_saveload(self): - m = dotmotif().from_motif( + m = Motif().from_motif( """ A -> B [type=6] """ @@ -29,7 +29,7 @@ def test_saveload(self): tf = NamedTemporaryFile() m.save(tf) tf.flush() - f = dotmotif.load(tf.name) + f = Motif.load(tf.name) self.assertTrue(nx.is_isomorphic(m._g, f._g)) self.assertEqual(m.list_edge_constraints(), f.list_edge_constraints()) self.assertEqual(m.list_node_constraints(), f.list_node_constraints()) diff --git a/dotmotif/utils.py b/dotmotif/utils.py index d74e888..7d75dda 100644 --- a/dotmotif/utils.py +++ b/dotmotif/utils.py @@ -25,7 +25,7 @@ def draw_motif(dm, negative_edge_color: str = "r", pos=None): Draw a dotmotif motif object. Arguments: - dm: dotmotif.DotMotif + dm: dotmotif.Motif negative_edge_color (str: r): Color used to represent negative edges pos (dict: None): The position to use. If unset, uses nx.spring_layout diff --git a/dotmotif/validators/test_dm_illegal_operators.py b/dotmotif/validators/test_dm_illegal_operators.py index 5deaadf..3db4781 100644 --- a/dotmotif/validators/test_dm_illegal_operators.py +++ b/dotmotif/validators/test_dm_illegal_operators.py @@ -8,22 +8,12 @@ class TestDotmotifIllegal(unittest.TestCase): """ def test_disagreeing_edges(self): - dm = dotmotif.dotmotif( + dm = dotmotif.Motif( validators=[dotmotif.validators.DisagreeingEdgesValidator()] ) - # with self.assertRaises(dotmotif.validators.DisagreeingEdgesValidatorError): with self.assertRaises(Exception): dm.from_motif("A -> B\nA !> B") def test_disagreeing_edges_ignored(self): - dm = dotmotif.dotmotif(validators=[]) + dm = dotmotif.Motif(validators=[]) dm.from_motif("A -+ B\nA !> B") - - # def test_cells_enforce_monotypy(self): - # dm = dotmotif.dotmotif(enforce_monotypy=True) - # with self.assertRaises(dotmotif.MotifError): - # dm.from_motif("A -> B\nA -| B") - - # def test_cells_enforce_monotypy_ignored(self): - # dm = dotmotif.dotmotif(enforce_monotypy=False) - # dm.from_motif("A -> B\nA -| B")