Repository: networkx/networkx

Issue:
Minimum weight perfect bipartite matching
I'm wondering if there would be appetite for an algorithm to solve the problem of finding minimum weight perfect bipartite matchings in bipartite graphs: Let *G* = ((*U*, *V*), *E*) be a weighted bipartite graph. We then want to find a perfect matching, i.e. one of cardinality min(|*U*|, |*V*|), so that the sum over all weights in the matching is minimal. This is also known as the [linear assignment problem](https://en.wikipedia.org/wiki/Assignment_problem).

There are algorithms for solving several related problems in NetworkX already, but I didn't seem to be able to find one specialized to this problem. Some related algorithms are the following:

* The linear assignment problem is a special case of a minimum cost flow problem, obtained by adding a source incident to all vertices in *U* and a sink incident to all vertices in *V*, and letting the demand for these two vertices be min(|*U*|, |*V*|) with appropriate signs. This could then be solved using `networkx.algorithms.flow.min_cost_flow`. A specialized algorithm would take advantage of the bipartite structure to improve performance. Moreover, as far as I can tell, `min_cost_flow` assumes that all weights are integral, which we wouldn't have to here.
* On the other hand, `networkx.algorithms.bipartite.maximum_matching` can detect if a feasible solution exists, i.e. a matching with the desired cardinality, but it doesn't take into account the weights.
* `networkx.algorithms.matching.max_weight_matching` takes weights into account but operates on arbitrary graphs and does not require that the matching be perfect in the bipartite case.

A large number of different algorithms exist to solve the problem. Assuming that the inclusion of an algorithm is relevant, the first thing I'm wondering is the following: SciPy already has an efficient implementation of an algorithm to solve the problem in [`scipy.optimize.linear_sum_assignment`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linear_sum_assignment.html), and since NetworkX already depends on SciPy, it would be only a few lines of code to simply make use of the SciPy implementation. However, as NetworkX is generally focused on bundling Python implementations of its algorithms, simply making use of the SciPy implementation might be going against the spirit of the library, and it would be preferable to include a Python version of one of the algorithms instead?


Discussion:
In some cases, NetworkX provides his own Python implementation **and** a Numpy based implementation. For instance, this occurs for the Floyd-Warshall algorithm with a very smart and efficient implementation (see timings below).


By the way, another very interesting option is Numba + Numpy I'm experimenting for the moment: it provides sometimes still faster execution than  C++. Some  Floyd-Warshall  algorithm timings (in seconds) for a graph with parameters n=800, m=100000:

```
Networkx                        : 82.66 
Python (no dict)                : 34.58
Python 
(no dict and multiprocessing)   : 21
Numpy from Networkx             : 1.67 (no predecessors calculation) 
Numba-Numpy-nx                  : 1.33
Graph-tool                      : 0.91 
Numba-Numpy-nx-paral            : 0.5
Scipy (Cython)                  : 0.50 
C++                             : 0.30
Numba                           : 0.24
C++ threads                     : 0.14
Numba parallel                  : 0.11
C++ openMP                      : 0.09
```

I tried Numba under GPU but didn't get nice results (I'm new to GPU programming).



In an ideal world we would have a scipy and base python implementation of each algorithm, but it is certainly fine to just implement a scipy version. The vast majority of our users have scipy available. Those that don't would not be able to use the function. We could leave it at that, or implement a python version. But since you suggest the Scipy version would be very straightforward I think it is reasonable to start with that and add a pure python version if you think it would be helpful.
Thanks. The algorithms generally aren't particularly complicated, but if I were to do it, I would probably just port what's already in SciPy. I think it's hard to tell how helpful that would be. I definitely understand the value in pure-Python implementations. You know the user base best.

And yes, it would be straightforward to adopt the SciPy version; really it amounts to the following:

```python
from scipy.optimize import linear_sum_assignment

def minimum_weight_matching(G, top_nodes=None):
    left, right = nx.bipartite.sets(G, top_nodes)
    # Ensure that the graph is complete This is currently a requirement in the underlying
    # optimization algorithm from SciPy, but the constraint will be removed in SciPy 1.4.0,
    # at which point it may also be removed here.
    if len(G.edges()) != len(left)*len(right):
        raise ValueError('The bipartite graph must be complete.')
    U = list(left)
    V = list(right)
    weights = nx.bipartite.biadjacency_matrix(G, row_order=U, column_order=V).toarray()
    assignment = linear_sum_assignment(weights)
    return {U[u]: V[v] for u, v in zip(*assignment)}
```

One downside is, as mentioned in the comment, that the requirement of completeness will only be lifted in the upcoming version of SciPy.

Anyway, I'm happy to provide a PR if you think this would be useful.
Yes, I think this would be useful. You should add the function, some simple unit-tests and/or smoke tests and a link in the doc/reference files. You might want to look at another module that uses scipy to see how we arrange the testing so the tests get skipped if scipy is not present.
Thanks!
Thanks for the kick start; I'll look into it. Do you have a favorite example of a module that does things just right? Otherwise I'll just go hunting.
```networkx/algorithms/bipartitie/spectral.py``` has the code that handles imports well for testing.


Resolving patch:
diff --git a/networkx/algorithms/bipartite/matching.py b/networkx/algorithms/bipartite/matching.py
--- a/networkx/algorithms/bipartite/matching.py
+++ b/networkx/algorithms/bipartite/matching.py
@@ -1,6 +1,7 @@
 # matching.py - bipartite graph maximum matching algorithms
 #
-# Copyright 2015 Jeffrey Finkelstein <jeffrey.finkelstein@gmail.com>.
+# Copyright 2015 Jeffrey Finkelstein <jeffrey.finkelstein@gmail.com>,
+# Copyright 2019 Søren Fuglede Jørgensen
 #
 # This file is part of NetworkX.
 #
@@ -17,8 +18,8 @@
 # Portions of this module use code from David Eppstein's Python Algorithms and
 # Data Structures (PADS) library, which is dedicated to the public domain (for
 # proof, see <http://www.ics.uci.edu/~eppstein/PADS/ABOUT-PADS.txt>).
-"""Provides functions for computing a maximum cardinality matching in a
-bipartite graph.
+"""Provides functions for computing maximum cardinality matchings and minimum
+weight full matchings in a bipartite graph.
 
 If you don't care about the particular implementation of the maximum matching
 algorithm, simply use the :func:`maximum_matching`. If you do care, you can
@@ -40,15 +41,21 @@
 The dictionary returned by :func:`maximum_matching` includes a mapping for
 vertices in both the left and right vertex sets.
 
+Similarly, :func:`minimum_weight_full_matching` produces, for a complete
+weighted bipartite graph, a matching whose cardinality is the cardinality of
+the smaller of the two partitions, and for which the sum of the weights of the
+edges included in the matching is minimal.
+
 """
 import collections
 import itertools
 
+from networkx.algorithms.bipartite.matrix import biadjacency_matrix
 from networkx.algorithms.bipartite import sets as bipartite_sets
 import networkx as nx
 
 __all__ = ['maximum_matching', 'hopcroft_karp_matching', 'eppstein_matching',
-           'to_vertex_cover']
+           'to_vertex_cover', 'minimum_weight_full_matching']
 
 INFINITY = float('inf')
 
@@ -87,7 +94,6 @@ def hopcroft_karp_matching(G, top_nodes=None):
 
     Notes
     -----
-
     This function is implemented with the `Hopcroft--Karp matching algorithm
     <https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm>`_ for
     bipartite graphs.
@@ -205,7 +211,6 @@ def eppstein_matching(G, top_nodes=None):
 
     Notes
     -----
-
     This function is implemented with David Eppstein's version of the algorithm
     Hopcroft--Karp algorithm (see :func:`hopcroft_karp_matching`), which
     originally appeared in the `Python Algorithms and Data Structures library
@@ -406,7 +411,6 @@ def to_vertex_cover(G, matching, top_nodes=None):
 
     Parameters
     ----------
-
     G : NetworkX graph
 
       Undirected bipartite graph
@@ -426,7 +430,6 @@ def to_vertex_cover(G, matching, top_nodes=None):
 
     Returns
     -------
-
     vertex_cover : :class:`set`
 
       The minimum vertex cover in `G`.
@@ -442,7 +445,6 @@ def to_vertex_cover(G, matching, top_nodes=None):
 
     Notes
     -----
-
     This function is implemented using the procedure guaranteed by `Konig's
     theorem
     <https://en.wikipedia.org/wiki/K%C3%B6nig%27s_theorem_%28graph_theory%29>`_,
@@ -483,3 +485,96 @@ def to_vertex_cover(G, matching, top_nodes=None):
 #:
 #: This function is simply an alias for :func:`hopcroft_karp_matching`.
 maximum_matching = hopcroft_karp_matching
+
+
+def minimum_weight_full_matching(G, top_nodes=None, weight='weight'):
+    """Returns the minimum weight full matching of the bipartite graph `G`.
+
+    Let :math:`G = ((U, V), E)` be a complete weighted bipartite graph with
+    real weights :math:`w : E \to \mathbb{R}`. This function then produces
+    a maximum matching :math:`M \subseteq E` which, since the graph is
+    assumed to be complete, has cardinality
+   
+    .. math::
+       \lvert M \rvert = \min(\lvert U \rvert, \lvert V \rvert),
+
+    and which minimizes the sum of the weights of the edges included in the
+    matching, :math:`\sum_{e \in M} w(e)`.
+    
+    When :math:`\lvert U \rvert = \lvert V \rvert`, this is commonly
+    referred to as a perfect matching; here, since we allow
+    :math:`\lvert U \rvert` and :math:`\lvert V \rvert` to differ, we
+    follow Karp[1]_ and refer to the matching as *full*.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+      Undirected bipartite graph
+
+    top_nodes : container
+
+      Container with all nodes in one bipartite node set. If not supplied
+      it will be computed.
+
+    weight : string, optional (default='weight')
+
+       The edge data key used to provide each value in the matrix.
+
+    Returns
+    -------
+    matches : dictionary
+
+      The matching is returned as a dictionary, `matches`, such that
+      ``matches[v] == w`` if node `v` is matched to node `w`. Unmatched
+      nodes do not occur as a key in matches.
+
+    Raises
+    ------
+    ValueError : Exception
+
+      Raised if the input bipartite graph is not complete.
+
+    ImportError : Exception
+
+      Raised if SciPy is not available.
+
+    Notes
+    -----
+    The problem of determining a minimum weight full matching is also known as
+    the rectangular linear assignment problem. This implementation defers the
+    calculation of the assignment to SciPy.
+
+    References
+    ----------
+    .. [1] Richard Manning Karp:
+       An algorithm to Solve the m x n Assignment Problem in Expected Time
+       O(mn log n).
+       Networks, 10(2):143–152, 1980.
+
+    """
+    try:
+        import scipy.optimize
+    except ImportError:
+        raise ImportError('minimum_weight_full_matching requires SciPy: ' +
+                          'https://scipy.org/')
+    left, right = nx.bipartite.sets(G, top_nodes)
+    # Ensure that the graph is complete. This is currently a requirement in
+    # the underlying  optimization algorithm from SciPy, but the constraint
+    # will be removed in SciPy 1.4.0, at which point it can also be removed
+    # here.
+    for (u, v) in itertools.product(left, right):
+        # As the graph is undirected, make sure to check for edges in
+        # both directions
+        if (u, v) not in G.edges() and (v, u) not in G.edges():
+            raise ValueError('The bipartite graph must be complete.')
+    U = list(left)
+    V = list(right)
+    weights = biadjacency_matrix(G, row_order=U,
+                                 column_order=V, weight=weight).toarray()
+    left_matches = scipy.optimize.linear_sum_assignment(weights)
+    d = {U[u]: V[v] for u, v in zip(*left_matches)}
+    # d will contain the matching from edges in left to right; we need to
+    # add the ones from right to left as well.
+    d.update({v: u for u, v in d.items()})
+    return d


Test patch:
diff --git a/networkx/algorithms/bipartite/tests/test_matching.py b/networkx/algorithms/bipartite/tests/test_matching.py
--- a/networkx/algorithms/bipartite/tests/test_matching.py
+++ b/networkx/algorithms/bipartite/tests/test_matching.py
@@ -1,6 +1,7 @@
 # test_matching.py - unit tests for bipartite matching algorithms
 #
-# Copyright 2015 Jeffrey Finkelstein <jeffrey.finkelstein@gmail.com>.
+# Copyright 2015 Jeffrey Finkelstein <jeffrey.finkelstein@gmail.com>,
+# Copyright 2019 Søren Fuglede Jørgensen
 #
 # This file is part of NetworkX.
 #
@@ -11,11 +12,13 @@
 
 import networkx as nx
 
+from nose import SkipTest
 from nose.tools import assert_true, assert_equal, raises
 
 from networkx.algorithms.bipartite.matching import eppstein_matching
 from networkx.algorithms.bipartite.matching import hopcroft_karp_matching
 from networkx.algorithms.bipartite.matching import maximum_matching
+from networkx.algorithms.bipartite.matching import minimum_weight_full_matching
 from networkx.algorithms.bipartite.matching import to_vertex_cover
 
 
@@ -196,3 +199,106 @@ def test_eppstein_matching():
     matching = eppstein_matching(G)
     assert_true(len(matching) == len(maximum_matching(G)))
     assert all(x in set(matching.keys()) for x in set(matching.values()))
+
+
+class TestMinimumWeightFullMatching(object):
+
+    @classmethod
+    def setupClass(cls):
+        global scipy
+        try:
+            import scipy.optimize
+        except ImportError:
+            raise SkipTest('SciPy not available.')
+
+    def test_minimum_weight_full_matching_square(self):
+        G = nx.complete_bipartite_graph(3, 3)
+        G.add_edge(0, 3, weight=400)
+        G.add_edge(0, 4, weight=150)
+        G.add_edge(0, 5, weight=400)
+        G.add_edge(1, 3, weight=400)
+        G.add_edge(1, 4, weight=450)
+        G.add_edge(1, 5, weight=600)
+        G.add_edge(2, 3, weight=300)
+        G.add_edge(2, 4, weight=225)
+        G.add_edge(2, 5, weight=300)
+        matching = minimum_weight_full_matching(G)
+        assert_equal(matching, {0: 4, 1: 3, 2: 5, 4: 0, 3: 1, 5: 2})
+
+    def test_minimum_weight_full_matching_smaller_left(self):
+        G = nx.complete_bipartite_graph(3, 4)
+        G.add_edge(0, 3, weight=400)
+        G.add_edge(0, 4, weight=150)
+        G.add_edge(0, 5, weight=400)
+        G.add_edge(0, 6, weight=1)
+        G.add_edge(1, 3, weight=400)
+        G.add_edge(1, 4, weight=450)
+        G.add_edge(1, 5, weight=600)
+        G.add_edge(1, 6, weight=2)
+        G.add_edge(2, 3, weight=300)
+        G.add_edge(2, 4, weight=225)
+        G.add_edge(2, 5, weight=290)
+        G.add_edge(2, 6, weight=3)
+        matching = minimum_weight_full_matching(G)
+        assert_equal(matching, {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1})
+
+    def test_minimum_weight_full_matching_smaller_top_nodes_right(self):
+        G = nx.complete_bipartite_graph(3, 4)
+        G.add_edge(0, 3, weight=400)
+        G.add_edge(0, 4, weight=150)
+        G.add_edge(0, 5, weight=400)
+        G.add_edge(0, 6, weight=1)
+        G.add_edge(1, 3, weight=400)
+        G.add_edge(1, 4, weight=450)
+        G.add_edge(1, 5, weight=600)
+        G.add_edge(1, 6, weight=2)
+        G.add_edge(2, 3, weight=300)
+        G.add_edge(2, 4, weight=225)
+        G.add_edge(2, 5, weight=290)
+        G.add_edge(2, 6, weight=3)
+        matching = minimum_weight_full_matching(G, top_nodes=[3, 4, 5, 6])
+        assert_equal(matching, {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1})
+
+    def test_minimum_weight_full_matching_smaller_right(self):
+        G = nx.complete_bipartite_graph(4, 3)
+        G.add_edge(0, 4, weight=400)
+        G.add_edge(0, 5, weight=400)
+        G.add_edge(0, 6, weight=300)
+        G.add_edge(1, 4, weight=150)
+        G.add_edge(1, 5, weight=450)
+        G.add_edge(1, 6, weight=225)
+        G.add_edge(2, 4, weight=400)
+        G.add_edge(2, 5, weight=600)
+        G.add_edge(2, 6, weight=290)
+        G.add_edge(3, 4, weight=1)
+        G.add_edge(3, 5, weight=2)
+        G.add_edge(3, 6, weight=3)
+        matching = minimum_weight_full_matching(G)
+        assert_equal(matching, {1: 4, 2: 6, 3: 5, 4: 1, 5: 3, 6: 2})
+
+    def test_minimum_weight_full_matching_negative_weights(self):
+        G = nx.complete_bipartite_graph(2, 2)
+        G.add_edge(0, 2, weight=-2)
+        G.add_edge(0, 3, weight=0.2)
+        G.add_edge(1, 2, weight=-2)
+        G.add_edge(1, 3, weight=0.3)
+        matching = minimum_weight_full_matching(G)
+        assert_equal(matching, {0: 3, 1: 2, 2: 1, 3: 0})
+
+    def test_minimum_weight_full_matching_different_weight_key(self):
+        G = nx.complete_bipartite_graph(2, 2)
+        G.add_edge(0, 2, mass=2)
+        G.add_edge(0, 3, mass=0.2)
+        G.add_edge(1, 2, mass=1)
+        G.add_edge(1, 3, mass=2)
+        matching = minimum_weight_full_matching(G, weight='mass')
+        assert_equal(matching, {0: 3, 1: 2, 2: 1, 3: 0})
+
+    @raises(ValueError)
+    def test_minimum_weight_full_matching_requires_complete_input(self):
+        G = nx.Graph()
+        G.add_nodes_from([1, 2, 3, 4], bipartite=0)
+        G.add_nodes_from(['a', 'b', 'c'], bipartite=1)
+        G.add_edges_from([(1, 'a'), (1, 'b'), (2, 'b'),
+                          (2, 'c'), (3, 'c'), (4, 'a')])
+        minimum_weight_full_matching(G)