Repository: sagemath/sage

Issue:
RuntimeError in Graph.is_planar for some directed graphs with allow_multiple_edges set
### Is there an existing issue for this?

- [X] I have searched the existing issues for a bug report that matches the one I want to file, without success.

### Did you read the documentation and troubleshoot guide?

- [X] I have read the documentation and troubleshoot guide

### Environment

```markdown
- **OS**: Debian 11
- **Sage Version**: 9.7
```


### Steps To Reproduce

Run
```python
def foo (G):
  print (G.is_planar())
  G.allow_multiple_edges(True)
  print ('has_multiple_edges ?', G.has_multiple_edges())
  print (G.is_planar())

foo (Graph())
foo (graphs.PetersenGraph())
foo (Graph().to_directed())
foo (graphs.PetersenGraph().to_directed())
```

### Expected Behavior

```python
True
has_multiple_edges ? False
True
False
has_multiple_edges ? False
False
True
has_multiple_edges ? False
True
False
has_multiple_edges ? False
False
```

### Actual Behavior

```python
True
has_multiple_edges ? False
True
False
has_multiple_edges ? False
False
True
has_multiple_edges ? False
True
False
has_multiple_edges ? False
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Input In [1], in <cell line: 1>()
----> 1 get_ipython().run_line_magic('runfile', 'bug_is_planar.sage')

File ~/src/sage-9.7/local/var/lib/sage/venv-python3.10.5/lib/python3.10/site-packages/IPython/core/interactiveshell.py:2305, in InteractiveShell.run_line_magic(self, magic_name, line, _stack_depth)
   2303     kwargs['local_ns'] = self.get_local_scope(stack_depth)
   2304 with self.builtin_trap:
-> 2305     result = fn(*args, **kwargs)
   2306 return result

File ~/src/sage-9.7/src/sage/repl/ipython_extension.py:121, in SageMagics.runfile(self, s)
     97 @line_magic
     98 def runfile(self, s):
     99     r"""
    100     Execute the code contained in the file ``s``.
    101 
   (...)
    119         sage: shell.quit()
    120     """
--> 121     return self.shell.ex(load_wrap(s, attach=False))

File ~/src/sage-9.7/local/var/lib/sage/venv-python3.10.5/lib/python3.10/site-packages/IPython/core/interactiveshell.py:2684, in InteractiveShell.ex(self, cmd)
   2682 """Execute a normal python statement in user namespace."""
   2683 with self.builtin_trap:
-> 2684     exec(cmd, self.user_global_ns, self.user_ns)

File <string>:1, in <module>

File ~/src/sage-9.7/src/sage/repl/load.py:272, in load(filename, globals, attach)
    270             add_attached_file(fpath)
    271         with open(fpath) as f:
--> 272             exec(preparse_file(f.read()) + "\n", globals)
    273 elif ext == '.spyx' or ext == '.pyx':
    274     if attach:

File <string>:10, in <module>

File <string>:5, in foo(G)

File ~/src/sage-9.7/src/sage/graphs/generic_graph.py:5259, in GenericGraph.is_planar(self, on_embedding, kuratowski, set_embedding, set_pos)
   5257 if hasattr(G, '_immutable'):
   5258     G = copy(G)
-> 5259 planar = is_planar(G, kuratowski=kuratowski, set_pos=set_pos, set_embedding=set_embedding)
   5260 if kuratowski:
   5261     bool_result = planar[0]

File ~/src/sage-9.7/src/sage/graphs/planarity.pyx:155, in sage.graphs.planarity.is_planar()
    153 
    154     if status == NOTOK:
--> 155         raise RuntimeError("status is not ok")
    156     elif status == NONEMBEDDABLE:
    157         # Kuratowski subgraph isolator

RuntimeError: status is not ok

```

### Additional Information

The problem seems to comes from the call to `gp_Embed` in the `is_planar` function in Cython ([source code](https://github.com/sagemath/sage/blob/develop/src/sage/graphs/planarity.pyx#L148))


Discussion:
I agree that it seems to come from the call to `gp_Embed`. We can also get it with smaller graphs.

```py
sage: G = graphs.CompleteGraph(3)
sage: G.to_directed().is_planar()
True
sage: G.allow_multiple_edges(True)
sage: G.to_directed().is_planar()
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
...
RuntimeError: status is not ok
sage: G.add_edge(0, 1)
sage: G.to_directed().is_planar()
True
```

Resolving patch:
diff --git a/src/sage/graphs/generators/smallgraphs.py b/src/sage/graphs/generators/smallgraphs.py
--- a/src/sage/graphs/generators/smallgraphs.py
+++ b/src/sage/graphs/generators/smallgraphs.py
@@ -912,7 +912,7 @@ def Balaban11Cage(embedding=1):
         sage: g3 = graphs.Balaban11Cage(embedding=3)                                    # needs networkx
         sage: g1.show(figsize=[10,10])          # long time                             # needs sage.plot
         sage: g2.show(figsize=[10,10])          # long time                             # needs networkx sage.plot
-        sage: g3.show(figsize=[10,10])          # long time                             # needs sage.plot
+        sage: g3.show(figsize=[10,10])          # long time                             # needs networkx sage.plot
 
     Proof that the embeddings are the same graph::
 
diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py
--- a/src/sage/graphs/generic_graph.py
+++ b/src/sage/graphs/generic_graph.py
@@ -5399,7 +5399,8 @@ def is_planar(self, on_embedding=None, kuratowski=False, set_embedding=False, se
 
             sage: k43 = graphs.CompleteBipartiteGraph(4, 3)
             sage: result = k43.is_planar(kuratowski=True); result
-            (False, Graph on 6 vertices)
+            (False,
+             Kuratowski subgraph of (Complete bipartite graph of order 4+3): Graph on 6 vertices)
             sage: result[1].is_isomorphic(graphs.CompleteBipartiteGraph(3, 3))
             True
 
@@ -5412,19 +5413,61 @@ def is_planar(self, on_embedding=None, kuratowski=False, set_embedding=False, se
             Traceback (most recent call last):
             ...
             NotImplementedError: cannot compute with embeddings of
-            multiple-edged or looped graphs
+             multiple-edged or looped graphs
+            sage: G.is_planar(set_embedding=True)
+            Traceback (most recent call last):
+            ...
+            NotImplementedError: cannot compute with embeddings of
+             multiple-edged or looped graphs
+            sage: G.is_planar(kuratowski=True)
+            (True, None)
             sage: G.is_planar(set_pos=True)
+            True
+            sage: sorted(G.get_pos().items())
+            [(0, [0, 0]), (1, [0, 1])]
+
+        Digraphs with multiple edges or loops or pairs of opposite arcs are
+        partially supported (:trac:`35152`)::
+
+            sage: D = digraphs.Complete(3)
+            sage: D.is_planar()
+            True
+            sage: D.is_planar(set_pos=True)
+            True
+            sage: sorted(D.get_pos().items())
+            [(0, [0, 1]), (1, [1, 1]), (2, [1, 0])]
+            sage: D.is_planar(on_embedding={})
             Traceback (most recent call last):
             ...
             NotImplementedError: cannot compute with embeddings of
-            multiple-edged or looped graphs
-            sage: G.is_planar(set_embedding=True)
+             digraphs with pairs of opposite arcs
+            sage: D.is_planar(set_embedding=True)
             Traceback (most recent call last):
             ...
             NotImplementedError: cannot compute with embeddings of
-            multiple-edged or looped graphs
-            sage: G.is_planar(kuratowski=True)
+             digraphs with pairs of opposite arcs
+            sage: D.is_planar(kuratowski=True)
             (True, None)
+            sage: D.allow_multiple_edges(True)
+            sage: D.add_edges(D.edges(sort=False))
+            sage: D.allow_loops(True)
+            sage: D.add_edges((u, u) for u in D)
+            sage: D.is_planar()
+            True
+            sage: D.is_planar(kuratowski=True)
+            (True, None)
+            sage: D.is_planar(set_pos=True)
+            True
+            sage: D.is_planar(set_embedding=True)
+            Traceback (most recent call last):
+            ...
+            NotImplementedError: cannot compute with embeddings of
+             multiple-edged or looped graphs
+            sage: D.is_planar(on_embedding={})
+            Traceback (most recent call last):
+            ...
+            NotImplementedError: cannot compute with embeddings of
+             multiple-edged or looped graphs
 
         ::
 
@@ -5485,31 +5528,35 @@ def is_planar(self, on_embedding=None, kuratowski=False, set_embedding=False, se
             if self.order() > 4 and self.size() > 3 * self.order() - 6:
                 return False
 
-        if self.has_multiple_edges() or self.has_loops():
-            if set_embedding or (on_embedding is not None) or set_pos:
+        if set_embedding or (on_embedding is not None):
+            # So far, working with embeddings is not working properly when a
+            # (di)graph has multiple edges or loops, or when a digraph has pairs
+            # of opposite arcs
+            if self.has_multiple_edges() or self.has_loops():
                 raise NotImplementedError("cannot compute with embeddings of multiple-edged or looped graphs")
-            else:
-                return self.to_simple().is_planar(kuratowski=kuratowski)
+            elif (self.is_directed() and
+                      any(self.has_edge(v, u) for u, v in self.edge_iterator(labels=False))):
+                raise NotImplementedError("cannot compute with embeddings of digraphs with pairs of opposite arcs")
 
         if on_embedding is not None:
             self._check_embedding_validity(on_embedding, boolean=False)
             return (0 == self.genus(minimal=False, set_embedding=False, on_embedding=on_embedding))
+
+        # We take the underlying undirected and simple graph
+        G = self.to_simple(to_undirected=True, immutable=False)
+        # And check if it is planar
+        from sage.graphs.planarity import is_planar
+        planar = is_planar(G, kuratowski=kuratowski, set_pos=set_pos, set_embedding=set_embedding)
+        if kuratowski:
+            bool_result = planar[0]
         else:
-            from sage.graphs.planarity import is_planar
-            G = self.to_undirected()
-            if hasattr(G, '_immutable'):
-                G = copy(G)
-            planar = is_planar(G, kuratowski=kuratowski, set_pos=set_pos, set_embedding=set_embedding)
-            if kuratowski:
-                bool_result = planar[0]
-            else:
-                bool_result = planar
-            if bool_result:
-                if set_pos:
-                    self._pos = G._pos
-                if set_embedding:
-                    self._embedding = G._embedding
-            return planar
+            bool_result = planar
+        if bool_result:
+            if set_pos:
+                self._pos = G._pos
+            if set_embedding:
+                self._embedding = G._embedding
+        return planar
 
     def is_circular_planar(self, on_embedding=None, kuratowski=False,
                            set_embedding=True, boundary=None,
@@ -5591,7 +5638,7 @@ def is_circular_planar(self, on_embedding=None, kuratowski=False,
             sage: g439.is_circular_planar(boundary=[1, 2, 3, 4])
             False
             sage: g439.is_circular_planar(kuratowski=True, boundary=[1, 2, 3, 4])
-            (False, Graph on 8 vertices)
+            (False, Kuratowski subgraph of (): Graph on 8 vertices)
             sage: g439.is_circular_planar(kuratowski=True, boundary=[1, 2, 3])
             (True, None)
             sage: g439.get_embedding()


Test patch: