Repository: networkx/networkx

Issue:
Directed edges in MultiDiGraph not handled correctly by subgraph monomorphism
### Current Behavior

A directed graph without edges should not have a subgraph monomorphic to a digraph with one edge.
However, current implementation says it is monomorphic.

### Expected Behavior

It should not be considered monomorphic.

### Steps to Reproduce

import networkx as nx

G = nx.MultiGraph()
G.add_node("A", label="A")
G.add_node("B", label="B")
H = nx.MultiGraph([["A", "B"]]) # one edge
matcher = nx.isomorphism.GraphMatcher(G, H)
print(matcher.subgraph_is_monomorphic()) ## False, as expected

G = nx.MultiDiGraph()
G.add_node("A", label="A")
G.add_node("B", label="B")
H = nx.MultiDiGraph([["A", "B"]]) # one edge
matcher = nx.isomorphism.GraphMatcher(G, H)
print(matcher.subgraph_is_monomorphic()) ## True, should be false

### Environment

Ubuntu 22.04 inside Windows 10 WSL

Python version: Python 3.12.3
NetworkX version: 3.4.2

### Additional context

My suspicion as to what is going on: 

Say the edge is A -> B. I suspect A is mapped first and added into core_1. A has no mapped neighbor yet that needs to check the edge existence, so it's fine. And when B is mapped and added, it has no (outgoing) neighbors, so the edge is also not checked. The program never notices that the "monomorphism" is missing the directed edge.



Discussion:
Be careful to use the right class for your matcher:
GraphMatcher is for undirected non-multiedge graphs

If you use `MultiDiGraphMatcher` (or even `DiGraphMatcher` since you don't have any multiedges) for the directed case, it returns False as expected.

I guess this shows that we could/should check if the input graphs are the right directed/multiedge form in the `__init__` method for each class. I think this would become mute once we switch completely to a function oriented interface: `nx.is_monomorphic` but that doesn't exist yet.




Patch:
diff --git a/networkx/algorithms/isomorphism/isomorphvf2.py b/networkx/algorithms/isomorphism/isomorphvf2.py
index cb0c84fa82c..587503a9dee 100644
--- a/networkx/algorithms/isomorphism/isomorphvf2.py
+++ b/networkx/algorithms/isomorphism/isomorphvf2.py
@@ -146,6 +146,8 @@
 
 import sys
 
+import networkx as nx
+
 __all__ = ["GraphMatcher", "DiGraphMatcher"]
 
 
@@ -172,6 +174,22 @@ def __init__(self, G1, G2):
         >>> G2 = nx.path_graph(4)
         >>> GM = isomorphism.GraphMatcher(G1, G2)
         """
+        if G1.is_directed() != G2.is_directed():
+            raise nx.NetworkXError("G1 and G2 must have the same directedness")
+
+        is_directed_matcher = self._is_directed_matcher()
+        if not is_directed_matcher and (G1.is_directed() or G2.is_directed()):
+            raise nx.NetworkXError(
+                "(Multi-)GraphMatcher() not defined for directed graphs. "
+                "Use (Multi-)DiGraphMatcher() instead."
+            )
+
+        if is_directed_matcher and not (G1.is_directed() and G2.is_directed()):
+            raise nx.NetworkXError(
+                "(Multi-)DiGraphMatcher() not defined for undirected graphs. "
+                "Use (Multi-)GraphMatcher() instead."
+            )
+
         self.G1 = G1
         self.G2 = G2
         self.G1_nodes = set(G1.nodes())
@@ -191,6 +209,9 @@ def __init__(self, G1, G2):
         # Initialize state
         self.initialize()
 
+    def _is_directed_matcher(self):
+        return False
+
     def reset_recursion_limit(self):
         """Restores the recursion limit."""
         # TODO:
@@ -620,6 +641,9 @@ def __init__(self, G1, G2):
         """
         super().__init__(G1, G2)
 
+    def _is_directed_matcher(self):
+        return True
+
     def candidate_pairs_iter(self):
         """Iterator over candidate pairs of nodes in G1 and G2."""
 


Test patch:
diff --git a/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py b/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py
index d6697a6d14b..9a351edef92 100644
--- a/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py
+++ b/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py
@@ -6,6 +6,8 @@
 import random
 import struct
 
+import pytest
+
 import networkx as nx
 from networkx.algorithms import isomorphism as iso
 
@@ -215,6 +217,34 @@ def test_multiedge():
             assert gm.subgraph_is_monomorphic()
 
 
+@pytest.mark.parametrize("G1", [nx.Graph(), nx.MultiGraph()])
+@pytest.mark.parametrize("G2", [nx.Graph(), nx.MultiGraph()])
+def test_matcher_raises(G1, G2):
+    undirected_matchers = [iso.GraphMatcher, iso.MultiGraphMatcher]
+    directed_matchers = [iso.DiGraphMatcher, iso.MultiDiGraphMatcher]
+
+    for matcher in undirected_matchers:
+        matcher(G1, G2)
+
+        msg = r"\(Multi-\)GraphMatcher\(\) not defined for directed graphs"
+        with pytest.raises(nx.NetworkXError, match=msg):
+            matcher(G1.to_directed(), G2.to_directed())
+
+    for matcher in directed_matchers:
+        matcher(G1.to_directed(), G2.to_directed())
+
+        msg = r"\(Multi-\)DiGraphMatcher\(\) not defined for undirected graphs"
+        with pytest.raises(nx.NetworkXError, match=msg):
+            matcher(G1, G2)
+
+    for matcher in undirected_matchers + directed_matchers:
+        msg = r"G1 and G2 must have the same directedness"
+        with pytest.raises(nx.NetworkXError, match=msg):
+            matcher(G1, G2.to_directed())
+        with pytest.raises(nx.NetworkXError, match=msg):
+            matcher(G1.to_directed(), G2)
+
+
 def test_selfloop():
     # Simple test for graphs with selfloops
     edges = [