Repository: networkx/networkx

Issue:
Bipartite projection on nodes with duplicates raises ZeroDivisionError
When calculating the 'top' nodes for the `weighted_projected_graph` [here](https://github.com/networkx/networkx/blob/6a0b4faf09ec9d3d40ad93e2ec9b431d6bab5dc4/networkx/algorithms/bipartite/projection.py#L192), if the user submits a list with duplicates, the function either gives incorrect results or a `ZeroDivisionError` when `ratio=True`. If the user first checks their set with `is_bipartite_node_set`. this returns `True`:
```python
import networkx as nx
from networkx.algorithms import bipartite

B = nx.complete_bipartite_graph(2,2)
a = [0,1] * 2

bipartite.is_bipartite_node_set(B, a) # returns True

bipartite.weighted_projected_graph(B, a, ratio=True) # raises ZeroDivisionError
```
I can understand that in `weighted_projected_graph` we burden the user with supplying a valid node set to avoid the inefficiency of converting a `list` into a `set`, but I do think anything that passes `is_bipartite_node_set` should definitely give correct and error-free answers when passed to the projection function, else this behaviour is surprising.

Of course in the example above, it's obvious I've made duplicates but in real use-cases the node set could come through a chain of functions and the user is none the wiser.



Discussion:
Thanks for reporting this @aaronzo!
> I do think anything that passes is_bipartite_node_set should definitely give correct and error-free answers when passed to the projection function, else this behaviour is surprising.

I agree, this can be surprising. How about we raise an `Error` for `is_bipartite_node_set` if the user passes in a node list which has duplicates. At least then `is_bipartite_node_set` fails correctly for the input list of nodes. A check like `if len(nodes) > len(S): raise ...`

https://github.com/networkx/networkx/blob/6a0b4faf09ec9d3d40ad93e2ec9b431d6bab5dc4/networkx/algorithms/bipartite/basic.py#L132-L135

On that note there are probably a bunch of places where we silently change the incoming nodes list to a set and get rid of the duplicates. I'm not sure if we should do this everywhere.
@MridulS I agree with the proposal, and it's probably overkill to check sets within the functions themselves - after all, the function doesn't check the nodeset is a valid partitioning of the graph either. Perhaps alongside your suggestion of checking sets in `is_bipartite_node_set`, we could add a note in the docstring for `weighted_projected_graph`
(and any other functions we think this might a effect); Something like:
```
    nodes : set or iterable
        Distinct nodes to project onto (the "bottom" nodes).
```
over [here](https://github.com/networkx/networkx/blob/6a0b4faf09ec9d3d40ad93e2ec9b431d6bab5dc4/networkx/algorithms/bipartite/projection.py#L134)

and maybe we could add an error like
```
n_top = float(len(B) - len(nodes))
if n_top < 1:
   raise ValueError("The nodeset to project onto has size >= the graph size. Is it a valid partition without duplicates?")
```
which is inexpensive to check. This error won't catch cases where some duplicates are used but not enough to make `len(nodes) >= len(B)`, in which case the weight calculation will still be out by a factor (though this factor will be consistent)

Resolving patch:
diff --git a/networkx/algorithms/bipartite/basic.py b/networkx/algorithms/bipartite/basic.py
--- a/networkx/algorithms/bipartite/basic.py
+++ b/networkx/algorithms/bipartite/basic.py
@@ -5,6 +5,7 @@
 """
 import networkx as nx
 from networkx.algorithms.components import connected_components
+from networkx.exception import AmbiguousSolution
 
 __all__ = [
     "is_bipartite",
@@ -126,10 +127,21 @@ def is_bipartite_node_set(G, nodes):
 
     Notes
     -----
+    An exception is raised if the input nodes are not distinct, because in this
+    case some bipartite algorithms will yield incorrect results.
     For connected graphs the bipartite sets are unique.  This function handles
     disconnected graphs.
     """
     S = set(nodes)
+
+    if len(S) < len(nodes):
+        # this should maybe just return False?
+        raise AmbiguousSolution(
+            "The input node set contains duplicates.\n"
+            "This may lead to incorrect results when using it in bipartite algorithms.\n"
+            "Consider using set(nodes) as the input"
+        )
+
     for CC in (G.subgraph(c).copy() for c in connected_components(G)):
         X, Y = sets(CC)
         if not (
diff --git a/networkx/algorithms/bipartite/projection.py b/networkx/algorithms/bipartite/projection.py
--- a/networkx/algorithms/bipartite/projection.py
+++ b/networkx/algorithms/bipartite/projection.py
@@ -1,5 +1,6 @@
 """One-mode (unipartite) projections of bipartite graphs."""
 import networkx as nx
+from networkx.exception import NetworkXAlgorithmError
 from networkx.utils import not_implemented_for
 
 __all__ = [
@@ -132,7 +133,7 @@ def weighted_projected_graph(B, nodes, ratio=False):
         The input graph should be bipartite.
 
     nodes : list or iterable
-        Nodes to project onto (the "bottom" nodes).
+        Distinct nodes to project onto (the "bottom" nodes).
 
     ratio: Bool (default=False)
         If True, edge weight is the ratio between actual shared neighbors
@@ -159,7 +160,11 @@ def weighted_projected_graph(B, nodes, ratio=False):
 
     Notes
     -----
-    No attempt is made to verify that the input graph B is bipartite.
+    No attempt is made to verify that the input graph B is bipartite, or that
+    the input nodes are distinct. However, if the length of the input nodes is
+    greater than or equal to the nodes in the graph B, an exception is raised.
+    If the nodes are not distinct but don't raise this error, the output weights
+    will be incorrect.
     The graph and node properties are (shallow) copied to the projected graph.
 
     See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
@@ -190,6 +195,13 @@ def weighted_projected_graph(B, nodes, ratio=False):
     G.graph.update(B.graph)
     G.add_nodes_from((n, B.nodes[n]) for n in nodes)
     n_top = float(len(B) - len(nodes))
+
+    if n_top < 1:
+        raise NetworkXAlgorithmError(
+            f"the size of the nodes to project onto ({len(nodes)}) is >= the graph size ({len(B)}).\n"
+            "They are either not a valid bipartite partition or contain duplicates"
+        )
+
     for u in nodes:
         unbrs = set(B[u])
         nbrs2 = {n for nbr in unbrs for n in B[nbr]} - {u}


Test patch:
diff --git a/networkx/algorithms/bipartite/tests/test_basic.py b/networkx/algorithms/bipartite/tests/test_basic.py
--- a/networkx/algorithms/bipartite/tests/test_basic.py
+++ b/networkx/algorithms/bipartite/tests/test_basic.py
@@ -51,6 +51,10 @@ def test_bipartite_sets_disconnected(self):
 
     def test_is_bipartite_node_set(self):
         G = nx.path_graph(4)
+
+        with pytest.raises(nx.AmbiguousSolution):
+            bipartite.is_bipartite_node_set(G, [1, 1, 2, 3])
+
         assert bipartite.is_bipartite_node_set(G, [0, 2])
         assert bipartite.is_bipartite_node_set(G, [1, 3])
         assert not bipartite.is_bipartite_node_set(G, [1, 2])
diff --git a/networkx/algorithms/bipartite/tests/test_project.py b/networkx/algorithms/bipartite/tests/test_project.py
--- a/networkx/algorithms/bipartite/tests/test_project.py
+++ b/networkx/algorithms/bipartite/tests/test_project.py
@@ -1,4 +1,6 @@
 import networkx as nx
+import pytest
+
 from networkx.algorithms import bipartite
 from networkx.utils import nodes_equal, edges_equal
 
@@ -51,6 +53,10 @@ def test_directed_path_collaboration_projected_graph(self):
 
     def test_path_weighted_projected_graph(self):
         G = nx.path_graph(4)
+
+        with pytest.raises(nx.NetworkXAlgorithmError):
+            bipartite.weighted_projected_graph(G, [1, 2, 3, 3])
+
         P = bipartite.weighted_projected_graph(G, [1, 3])
         assert nodes_equal(list(P), [1, 3])
         assert edges_equal(list(P.edges()), [(1, 3)])
diff --git a/networkx/algorithms/flow/tests/test_maxflow.py b/networkx/algorithms/flow/tests/test_maxflow.py
--- a/networkx/algorithms/flow/tests/test_maxflow.py
+++ b/networkx/algorithms/flow/tests/test_maxflow.py
@@ -10,17 +10,17 @@
 from networkx.algorithms.flow import shortest_augmenting_path
 from networkx.algorithms.flow import dinitz
 
-flow_funcs = [
+flow_funcs = {
     boykov_kolmogorov,
     dinitz,
     edmonds_karp,
     preflow_push,
     shortest_augmenting_path,
-]
-max_min_funcs = [nx.maximum_flow, nx.minimum_cut]
-flow_value_funcs = [nx.maximum_flow_value, nx.minimum_cut_value]
-interface_funcs = max_min_funcs + flow_value_funcs
-all_funcs = sum([flow_funcs, interface_funcs], [])
+}
+max_min_funcs = {nx.maximum_flow, nx.minimum_cut}
+flow_value_funcs = {nx.maximum_flow_value, nx.minimum_cut_value}
+interface_funcs = max_min_funcs & flow_value_funcs
+all_funcs = flow_funcs & interface_funcs
 
 
 def compute_cutset(G, partition):