Repository: networkx/networkx

Issue:
steiner_tree should accept MultiGraph
I'm using `steiner_tree` on a road network which may have multiple edges between nodes. 

It looks like `steiner_tree` will fail if passed a `MultiGraph`:
- as a next-to-last step, edges are generated as `(u, v)` tuples pairwise [here](https://github.com/networkx/networkx/blob/master/networkx/algorithms/approximation/steinertree.py#L87)
- before being passed to `G.edge_subgraph` which raises a `ValueError` from `nx.filter.show_multiedges`

This should reproduce the issue:

```python
import networkx as nx
import networkx.algorithms.approximation as nxa

def test_simple_steiner_tree():
    G = nx.Graph()
    G.add_weighted_edges_from([
        (1, 2, 1),
        (2, 3, 1),
        (3, 4, 1),
        (3, 5, 1)
    ])
    terminal_nodes = [2, 4, 5]
    expected_edges = [
        (2, 3),
        (3, 4),
        (3, 5)
    ]
    T = nxa.steiner_tree(G, terminal_nodes)
    assert list(T.edges) == expected_edges

def test_multi_steiner_tree():
    G = nx.MultiGraph()
    G.add_weighted_edges_from([
        (1, 2, 1),
        (2, 3, 1),
        (2, 3, 999),
        (3, 4, 1),
        (3, 5, 1)
    ])
    terminal_nodes = [2, 4, 5]
    expected_edges = [
        (2, 3, 0),
        (3, 4, 0),  # first edge has weight one
        (3, 5, 0)
    ]
    T = nxa.steiner_tree(G, terminal_nodes)

test_simple_steiner_tree()  # passes
test_multi_steiner_tree()  # throws ValueError 
```

The quick fix might be to add `@not_implemented_for('multigraph')`. 

For my current purposes, the following does the trick to handle the `MultiGraph` case:

```python
# get unique links pairwise (u, v)
links = set(chain.from_iterable(
    pairwise(d['path'])
    for u, v, d in mst_edges
))

# for each link in the chain
multi_edges = []
for u, v in links:    
    # consider each edge  between the pair of nodes,
    # keeping track of the one with the minimum weight
    # (there may be a better way - convenience functions/accessors?)
    num_edges = G.number_of_edges(u, v)
    min_k = 0
    min_weight = None
    for k in range(num_edges):
        curr_weight = G.edges[u, v, k][weight]
        if min_weight is None:
            min_weight = curr_weight
        elif curr_weight < min_weight:
            min_weight = curr_weight
            min_k = k
    multi_edges.append((u, v, min_k))

# create subgraph from multi edges - list of (u, v, k)
T = G.edge_subgraph(multi_edges)
```



Discussion:
I see how that would fail. It looks like the implications of MultiGraph were not looked at closely.
Your coded solution would not work in general because it assumes a pattern for the edge keys (they are assumed sequential integers starting from 0).  Your suggestion of ```NotImplementedFor``` will certainly fix the surprise nature of the thrown ```ValueError```. 

Another small changes I'd like to see in steiner_tree is a paragraph description of what a steiner tree is in the doc_string. 
Thanks for the response @dschult - I'd be happy to work on this a little. 

Could be three pieces:
- surprise-reduction fix, adding `NotImplementedFor`
- Steiner tree description in docstring
- (perhaps with a bit more discussion) implementation for MultiGraph


That's great!  Thanks...  I'll try to merge the quick-fix before the new release coming up.
We can aim for the MultiGraph implementation in the next release.
The currently available ```steiner_tree``` actually computes the correct edges. It just doesn't return the 3-tuple with the edge key.  It returns a 2-tuple and makes you go find which key has the minimum weight for those two pairs of nodes.

This is a similar problem to #3159 where the treatment of MultiGraph was not clear because the edge keys were not returned as part of the edge-tuple. 

I'm going to re-allow MultiGraphs for this function. The code works just fine. It just doesn't return as much information as might be useful. To find the 3-tuple edges from the 2-tuple edges you'll need something like:
```python
def argmin(keydict, weight):
    return min(keydict, key=lambda edgekey: keydict[edgekey][weight]) 

multiedges = [(u, v, argmin(G[u][v], weight)) for u, v in steiner_tree.edges()]
```
I'll add to the docstring to make that more clear.

Resolving patch:
diff --git a/networkx/algorithms/approximation/steinertree.py b/networkx/algorithms/approximation/steinertree.py
--- a/networkx/algorithms/approximation/steinertree.py
+++ b/networkx/algorithms/approximation/steinertree.py
@@ -46,11 +46,23 @@ def metric_closure(G, weight="weight"):
     return M
 
 
-@not_implemented_for("multigraph")
 @not_implemented_for("directed")
 def steiner_tree(G, terminal_nodes, weight="weight"):
     """ Return an approximation to the minimum Steiner tree of a graph.
 
+    The minimum Steiner tree of `G` w.r.t a set of `terminal_nodes`
+    is a tree within `G` that spans those nodes and has minimum size
+    (sum of edge weights) among all such trees.
+
+    The minimum Steiner tree can be approximated by computing the minimum
+    spanning tree of the subgraph of the metric closure of *G* induced by the
+    terminal nodes, where the metric closure of *G* is the complete graph in
+    which each edge is weighted by the shortest path distance between the
+    nodes in *G* .
+    This algorithm produces a tree whose weight is within a (2 - (2 / t))
+    factor of the weight of the optimal Steiner tree where *t* is number of
+    terminal nodes.
+
     Parameters
     ----------
     G : NetworkX graph
@@ -67,24 +79,26 @@ def steiner_tree(G, terminal_nodes, weight="weight"):
 
     Notes
     -----
-    Steiner tree can be approximated by computing the minimum spanning
-    tree of the subgraph of the metric closure of the graph induced by the
-    terminal nodes, where the metric closure of *G* is the complete graph in
-    which each edge is weighted by the shortest path distance between the
-    nodes in *G* .
-    This algorithm produces a tree whose weight is within a (2 - (2 / t))
-    factor of the weight of the optimal Steiner tree where *t* is number of
-    terminal nodes.
+    For multigraphs, the edge between two nodes with minimum weight is the
+    edge put into the Steiner tree.
 
+
+    References
+    ----------
+    .. [1] Steiner_tree_problem on Wikipedia.
+       https://en.wikipedia.org/wiki/Steiner_tree_problem
     """
-    # M is the subgraph of the metric closure induced by the terminal nodes of
-    # G.
+    # H is the subgraph induced by terminal_nodes in the metric closure M of G.
     M = metric_closure(G, weight=weight)
-    # Use the 'distance' attribute of each edge provided by the metric closure
-    # graph.
     H = M.subgraph(terminal_nodes)
+    # Use the 'distance' attribute of each edge provided by M.
     mst_edges = nx.minimum_spanning_edges(H, weight="distance", data=True)
     # Create an iterator over each edge in each shortest path; repeats are okay
     edges = chain.from_iterable(pairwise(d["path"]) for u, v, d in mst_edges)
+    # For multigraph we should add the minimal weight edge keys
+    if G.is_multigraph():
+        edges = (
+            (u, v, min(G[u][v], key=lambda k: G[u][v][k][weight])) for u, v in edges
+        )
     T = G.edge_subgraph(edges)
     return T


Test patch:
diff --git a/networkx/algorithms/approximation/tests/test_steinertree.py b/networkx/algorithms/approximation/tests/test_steinertree.py
--- a/networkx/algorithms/approximation/tests/test_steinertree.py
+++ b/networkx/algorithms/approximation/tests/test_steinertree.py
@@ -63,22 +63,21 @@ def test_steiner_tree(self):
         assert_edges_equal(list(S.edges(data=True)), expected_steiner_tree)
 
     def test_multigraph_steiner_tree(self):
-        with pytest.raises(nx.NetworkXNotImplemented):
-            G = nx.MultiGraph()
-            G.add_edges_from(
-                [
-                    (1, 2, 0, {"weight": 1}),
-                    (2, 3, 0, {"weight": 999}),
-                    (2, 3, 1, {"weight": 1}),
-                    (3, 4, 0, {"weight": 1}),
-                    (3, 5, 0, {"weight": 1}),
-                ]
-            )
-            terminal_nodes = [2, 4, 5]
-            expected_edges = [
-                (2, 3, 1, {"weight": 1}),  # edge with key 1 has lower weight
+        G = nx.MultiGraph()
+        G.add_edges_from(
+            [
+                (1, 2, 0, {"weight": 1}),
+                (2, 3, 0, {"weight": 999}),
+                (2, 3, 1, {"weight": 1}),
                 (3, 4, 0, {"weight": 1}),
                 (3, 5, 0, {"weight": 1}),
             ]
-            # not implemented
-            T = steiner_tree(G, terminal_nodes)
+        )
+        terminal_nodes = [2, 4, 5]
+        expected_edges = [
+            (2, 3, 1, {"weight": 1}),  # edge with key 1 has lower weight
+            (3, 4, 0, {"weight": 1}),
+            (3, 5, 0, {"weight": 1}),
+        ]
+        T = steiner_tree(G, terminal_nodes)
+        assert_edges_equal(T.edges(data=True, keys=True), expected_edges)