Gurobi / gurobi-optimods

Data-driven APIs for common optimization tasks
https://gurobi-optimods.readthedocs.io
Apache License 2.0
153 stars 32 forks source link

Enable multiple edges in the same direction between two nodes #157

Closed jsfreischuetz closed 4 months ago

jsfreischuetz commented 4 months ago

Enable multiple edges in the same direction between two nodes

Description

Currently the solver does not support multiple edges of between two nodes in the same direction. This PR implements this feature for pandas and networkx.

scipy, because of the matrix representation, requires some more complex changes to support it. For now this is left out as it is somewhat obvious to the user that this would not be supported for this structure.

fixes #155

Implements based on feedback from #156

Checklist

jsfreischuetz commented 4 months ago

@simonbowly I have implemented with a more minimal implementation as requested.

torressa commented 4 months ago

Hi @jsfreischuetz thanks for the updated PR! I would still like to keep the original output types and internal behaviour as much as possible, only change if the input is using multiple edges. Something like:

--- a/src/gurobi_optimods/min_cost_flow.py
+++ b/src/gurobi_optimods/min_cost_flow.py
@@ -63,9 +63,12 @@ def min_cost_flow_pandas(

         source_label, target_label = arc_data.index.names

-        arc_data = (
-            arc_data.reset_index()
-        )  # This is a workaround for duplicate entries being disallowed in gurobipy_pandas
+        multigraph = False
+        # This is a workaround for duplicate entries being disallowed in gurobipy_pandas
+        if arc_data.index.has_duplicates:
+            arc_data = arc_data.reset_index()
+            multigraph = True
+
         arc_df = arc_data.gppd.add_vars(model, ub="capacity", obj="cost", name="flow")

         balance_df = (
@@ -88,9 +91,9 @@ def min_cost_flow_pandas(
         if model.Status in [GRB.INFEASIBLE, GRB.INF_OR_UNBD]:
             raise ValueError("Unsatisfiable flows")

-        arc_df = arc_df.set_index(
-            ["source", "target"]
-        )  # Repair index that was reset above
+        if multigraph:
+            # Repair index that was reset above
+            arc_df = arc_df.set_index([source_label, target_label])
         return model.ObjVal, arc_df["flow"].gppd.X

@@ -179,6 +182,8 @@ def min_cost_flow_networkx(G, *, create_env):
         f"Solving min-cost flow with {len(G.nodes)} nodes and {len(G.edges)} edges"
     )
     with create_env() as env, gp.Model(env=env) as model:
+        multigraph = isinstance(G, nx.MultiGraph)
+
         G = nx.MultiDiGraph(G)

         edges, capacities, costs = gp.multidict(
@@ -221,7 +226,7 @@ def min_cost_flow_networkx(G, *, create_env):
             raise ValueError("Unsatisfiable flows")

         # Create a new Graph with selected edges in the matching
-        resulting_flow = nx.MultiDiGraph()
+        resulting_flow = nx.MultiDiGraph() if multigraph else nx.DiGraph()
         resulting_flow.add_nodes_from(nodes)
         resulting_flow.add_edges_from(
             [(edge[0], edge[1], {"flow": v.X}) for edge, v in x.items() if v.X > 0.1]
torressa commented 4 months ago

Also, we can use the same functions to check the expected output by changing from a dictionary to a (sorted) list

--- a/tests/test_graph_utils.py
+++ b/tests/test_graph_utils.py
@@ -1,10 +1,18 @@
 import numpy as np

+def _sort_key(x):
+    return str(x)
+
+
 def check_solution_pandas(solution, candidates):
     # Checks whether the solution (`pd.Series`) matches any of the list of
     # candidates (containing `dict`)
-    if any(solution.to_dict() == c for c in candidates):
+    if any(
+        sorted(list(zip(solution.index.to_list(), solution.to_list())), key=_sort_key)
+        == sorted(c, key=_sort_key)
+        for c in candidates
+    ):
         return True
     return False

@@ -21,30 +29,10 @@ def check_solution_scipy(solution, candidates):
 def check_solution_networkx(solution, candidates):
     # Checks whether the solution (`nx.DiGraph`) matches any of the list of
     # candidates (containing tuples dict `{(i, j): data}`)
-    sol_dict = {(i, j): d for i, j, d in solution.edges(data=True)}
-    if any(sol_dict == c for c in candidates):
-        return True
-    return False
-
-
-def check_solution_pandas_multi(solution, candidates):
-    # Checks whether the solution (`pd.Series`) matches any of the list of
-    # candidates (containing `pd.Series`)
-    if any(solution.reset_index().equals(c.reset_index()) for c in candidates):
+    sol_list = sorted(
+        [((i, j), data["flow"]) for i, j, data in solution.edges(data=True)],
+        key=_sort_key,
+    )
+    if any(sol_list == sorted(c, key=_sort_key) for c in candidates):
         return True
     return False
-
-
-def check_solution_networkx_multi(solution, candidates):
-    # Checks whether the solution (`nx.DiGraph`) matches any of the list of
-    # candidates (containing tuples dict `{(i, j): data}`)
-    for candidate in candidates:
-
-        def edge_sort(row):
-            return (str(row[0]), str(row[1]))
-
-        if sorted(candidate, key=edge_sort) == sorted(
-            list(solution.edges(data=True)), key=edge_sort
-        ):
-            return True
-    return False
diff --git a/tests/test_min_cost_flow.py b/tests/test_min_cost_flow.py
index 33b00ca..df04752 100644
--- a/tests/test_min_cost_flow.py
+++ b/tests/test_min_cost_flow.py
@@ -15,9 +15,7 @@ import gurobi_optimods.min_cost_flow as mcf

 from .test_graph_utils import (
     check_solution_networkx,
-    check_solution_networkx_multi,
     check_solution_pandas,
-    check_solution_pandas_multi,
     check_solution_scipy,
 )

@@ -93,7 +91,13 @@ class TestMinCostFlow(unittest.TestCase):
         cost, sol = mcf.min_cost_flow_pandas(edge_data, node_data)
         sol = sol[sol > 0]
         self.assertEqual(cost, 31)
-        candidate = {(0, 1): 1.0, (0, 2): 1.0, (1, 3): 1.0, (2, 4): 2.0, (4, 5): 2.0}
+        candidate = [
+            ((0, 1), 1.0),
+            ((0, 2), 1.0),
+            ((1, 3), 1.0),
+            ((2, 4), 2.0),
+            ((4, 5), 2.0),
+        ]
         self.assertIsInstance(sol, pd.Series)
         self.assertTrue(check_solution_pandas(sol, [candidate]))

@@ -125,13 +129,13 @@ class TestMinCostFlow(unittest.TestCase):
         G = datasets.simple_graph_networkx()
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 31)
-        expected = {
-            (0, 1): {"flow": 1.0},
-            (0, 2): {"flow": 1.0},
-            (1, 3): {"flow": 1.0},
-            (2, 4): {"flow": 2.0},
-            (4, 5): {"flow": 2.0},
-        }
+        expected = [
+            ((0, 1), 1.0),
+            ((0, 2), 1.0),
+            ((1, 3), 1.0),
+            ((2, 4), 2.0),
+            ((4, 5), 2.0),
+        ]
         self.assertIsInstance(sol, nx.Graph)
         self.assertTrue(check_solution_networkx(sol, [expected]))

@@ -141,13 +145,13 @@ class TestMinCostFlow(unittest.TestCase):
         G = nx.relabel_nodes(G, {0: "s", 5: "t"})
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 31)
-        expected = {
-            ("s", 1): {"flow": 1.0},
-            ("s", 2): {"flow": 1.0},
-            (1, 3): {"flow": 1.0},
-            (2, 4): {"flow": 2.0},
-            (4, "t"): {"flow": 2.0},
-        }
+        expected = [
+            (("s", 1), 1.0),
+            (("s", 2), 1.0),
+            ((1, 3), 1.0),
+            ((2, 4), 2.0),
+            ((4, "t"), 2.0),
+        ]
         self.assertIsInstance(sol, nx.Graph)
         self.assertTrue(check_solution_networkx(sol, [expected]))

@@ -158,24 +162,24 @@ class TestMinCostFlow2(unittest.TestCase):
         cost, sol = mcf.min_cost_flow_pandas(edge_data, node_data)
         sol = sol[sol > 0]
         self.assertEqual(cost, 150)
-        candidate = {
-            (0, 1): 12.0,
-            (0, 2): 8.0,
-            (1, 3): 4.0,
-            (1, 2): 8.0,
-            (2, 3): 15.0,
-            (2, 4): 1.0,
-            (3, 4): 14.0,
-        }
-        candidate2 = {
-            (0, 1): 12.0,
-            (0, 2): 8.0,
-            (1, 3): 4.0,
-            (1, 2): 8.0,
-            (2, 3): 11.0,
-            (2, 4): 5.0,
-            (3, 4): 10.0,
-        }
+        candidate = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 15.0),
+            ((2, 4), 1.0),
+            ((3, 4), 14.0),
+        ]
+        candidate2 = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 11.0),
+            ((2, 4), 5.0),
+            ((3, 4), 10.0),
+        ]
         self.assertTrue(check_solution_pandas(sol, [candidate, candidate2]))

     def test_scipy(self):
@@ -197,24 +201,24 @@ class TestMinCostFlow2(unittest.TestCase):
         G = load_graph2_networkx()
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 150)
-        candidate = {
-            (0, 1): {"flow": 12.0},
-            (0, 2): {"flow": 8.0},
-            (1, 2): {"flow": 8.0},
-            (1, 3): {"flow": 4.0},
-            (2, 3): {"flow": 11.0},
-            (2, 4): {"flow": 5.0},
-            (3, 4): {"flow": 10.0},
-        }
-        candidate2 = {
-            (0, 1): {"flow": 12.0},
-            (0, 2): {"flow": 8.0},
-            (1, 3): {"flow": 4.0},
-            (1, 2): {"flow": 8.0},
-            (2, 3): {"flow": 15.0},
-            (2, 4): {"flow": 1.0},
-            (3, 4): {"flow": 14.0},
-        }
+        candidate = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 2), 8.0),
+            ((1, 3), 4.0),
+            ((2, 3), 11.0),
+            ((2, 4), 5.0),
+            ((3, 4), 10.0),
+        ]
+        candidate2 = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 15.0),
+            ((2, 4), 1.0),
+            ((3, 4), 14.0),
+        ]
         self.assertTrue(check_solution_networkx(sol, [candidate, candidate2]))

     @unittest.skipIf(nx is None, "networkx is not installed")
@@ -223,23 +227,23 @@ class TestMinCostFlow2(unittest.TestCase):
         G = nx.relabel_nodes(G, {0: "s", 4: "t"})
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 150)
-        candidate = {
-            ("s", 1): {"flow": 12.0},
-            ("s", 2): {"flow": 8.0},
-            (1, 2): {"flow": 8.0},
-            (1, 3): {"flow": 4.0},
-            (2, 3): {"flow": 11.0},
-            (2, "t"): {"flow": 5.0},
-            (3, "t"): {"flow": 10.0},
-        }
+        candidate = [
+            (("s", 1), 12.0),
+            (("s", 2), 8.0),
+            ((1, 2), 8.0),
+            ((1, 3), 4.0),
+            ((2, 3), 11.0),
+            ((2, "t"), 5.0),
+            ((3, "t"), 10.0),
+        ]
         candidate2 = {
-            ("s", 1): {"flow": 12.0},
-            ("s", 2): {"flow": 8.0},
-            (1, 3): {"flow": 4.0},
-            (1, 2): {"flow": 8.0},
-            (2, 3): {"flow": 15.0},
-            (2, "t"): {"flow": 1.0},
-            (3, "t"): {"flow": 14.0},
+            (("s", 1), 12.0),
+            (("s", 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 15.0),
+            ((2, "t"), 1.0),
+            ((3, "t"), 14.0),
         }
         self.assertTrue(check_solution_networkx(sol, [candidate, candidate2]))

@@ -251,15 +255,17 @@ class TestMinCostFlow3(unittest.TestCase):
         sol = sol[sol > 0]
         self.assertEqual(cost, 49.0)

-        candidate = pd.DataFrame(
-            {
-                "source": [0, 0, 1, 1, 2, 2, 3, 2],
-                "target": [1, 2, 3, 2, 3, 4, 4, 3],
-                "flow": [12.0, 8.0, 4.0, 8.0, 10.0, 5.0, 10.0, 1.0],
-            }
-        ).set_index(["source", "target"])
-
-        self.assertTrue(check_solution_pandas_multi(sol, [candidate]))
+        candidate = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 10.0),
+            ((2, 4), 5.0),
+            ((3, 4), 10.0),
+            ((2, 3), 1.0),
+        ]
+        self.assertTrue(check_solution_pandas(sol, [candidate]))

     @unittest.skipIf(nx is None, "networkx is not installed")
     def test_networkx(self):
@@ -267,14 +273,14 @@ class TestMinCostFlow3(unittest.TestCase):
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 49.0)
         candidate = [
-            (0, 1, {"flow": 12.0}),
-            (0, 2, {"flow": 8.0}),
-            (1, 3, {"flow": 4.0}),
-            (1, 2, {"flow": 8.0}),
-            (2, 3, {"flow": 10.0}),
-            (2, 3, {"flow": 1.0}),
-            (2, 4, {"flow": 5.0}),
-            (3, 4, {"flow": 10.0}),
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 10.0),
+            ((2, 3), 1.0),
+            ((2, 4), 5.0),
+            ((3, 4), 10.0),
         ]

-        self.assertTrue(check_solution_networkx_multi(sol, [candidate]))
+        self.assertTrue(check_solution_networkx(sol, [candidate]))
torressa commented 4 months ago

@jsfreischuetz Thanks for the updated PR! Just made some changes to keep the previous outputs the same (not always returning a MultiGraph) and some further clean up. Feel free to check that it all still works as expected

jsfreischuetz commented 4 months ago

Also, we can use the same functions to check the expected output by changing from a dictionary to a (sorted) list

--- a/tests/test_graph_utils.py
+++ b/tests/test_graph_utils.py
@@ -1,10 +1,18 @@
 import numpy as np

+def _sort_key(x):
+    return str(x)
+
+
 def check_solution_pandas(solution, candidates):
     # Checks whether the solution (`pd.Series`) matches any of the list of
     # candidates (containing `dict`)
-    if any(solution.to_dict() == c for c in candidates):
+    if any(
+        sorted(list(zip(solution.index.to_list(), solution.to_list())), key=_sort_key)
+        == sorted(c, key=_sort_key)
+        for c in candidates
+    ):
         return True
     return False

@@ -21,30 +29,10 @@ def check_solution_scipy(solution, candidates):
 def check_solution_networkx(solution, candidates):
     # Checks whether the solution (`nx.DiGraph`) matches any of the list of
     # candidates (containing tuples dict `{(i, j): data}`)
-    sol_dict = {(i, j): d for i, j, d in solution.edges(data=True)}
-    if any(sol_dict == c for c in candidates):
-        return True
-    return False
-
-
-def check_solution_pandas_multi(solution, candidates):
-    # Checks whether the solution (`pd.Series`) matches any of the list of
-    # candidates (containing `pd.Series`)
-    if any(solution.reset_index().equals(c.reset_index()) for c in candidates):
+    sol_list = sorted(
+        [((i, j), data["flow"]) for i, j, data in solution.edges(data=True)],
+        key=_sort_key,
+    )
+    if any(sol_list == sorted(c, key=_sort_key) for c in candidates):
         return True
     return False
-
-
-def check_solution_networkx_multi(solution, candidates):
-    # Checks whether the solution (`nx.DiGraph`) matches any of the list of
-    # candidates (containing tuples dict `{(i, j): data}`)
-    for candidate in candidates:
-
-        def edge_sort(row):
-            return (str(row[0]), str(row[1]))
-
-        if sorted(candidate, key=edge_sort) == sorted(
-            list(solution.edges(data=True)), key=edge_sort
-        ):
-            return True
-    return False
diff --git a/tests/test_min_cost_flow.py b/tests/test_min_cost_flow.py
index 33b00ca..df04752 100644
--- a/tests/test_min_cost_flow.py
+++ b/tests/test_min_cost_flow.py
@@ -15,9 +15,7 @@ import gurobi_optimods.min_cost_flow as mcf

 from .test_graph_utils import (
     check_solution_networkx,
-    check_solution_networkx_multi,
     check_solution_pandas,
-    check_solution_pandas_multi,
     check_solution_scipy,
 )

@@ -93,7 +91,13 @@ class TestMinCostFlow(unittest.TestCase):
         cost, sol = mcf.min_cost_flow_pandas(edge_data, node_data)
         sol = sol[sol > 0]
         self.assertEqual(cost, 31)
-        candidate = {(0, 1): 1.0, (0, 2): 1.0, (1, 3): 1.0, (2, 4): 2.0, (4, 5): 2.0}
+        candidate = [
+            ((0, 1), 1.0),
+            ((0, 2), 1.0),
+            ((1, 3), 1.0),
+            ((2, 4), 2.0),
+            ((4, 5), 2.0),
+        ]
         self.assertIsInstance(sol, pd.Series)
         self.assertTrue(check_solution_pandas(sol, [candidate]))

@@ -125,13 +129,13 @@ class TestMinCostFlow(unittest.TestCase):
         G = datasets.simple_graph_networkx()
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 31)
-        expected = {
-            (0, 1): {"flow": 1.0},
-            (0, 2): {"flow": 1.0},
-            (1, 3): {"flow": 1.0},
-            (2, 4): {"flow": 2.0},
-            (4, 5): {"flow": 2.0},
-        }
+        expected = [
+            ((0, 1), 1.0),
+            ((0, 2), 1.0),
+            ((1, 3), 1.0),
+            ((2, 4), 2.0),
+            ((4, 5), 2.0),
+        ]
         self.assertIsInstance(sol, nx.Graph)
         self.assertTrue(check_solution_networkx(sol, [expected]))

@@ -141,13 +145,13 @@ class TestMinCostFlow(unittest.TestCase):
         G = nx.relabel_nodes(G, {0: "s", 5: "t"})
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 31)
-        expected = {
-            ("s", 1): {"flow": 1.0},
-            ("s", 2): {"flow": 1.0},
-            (1, 3): {"flow": 1.0},
-            (2, 4): {"flow": 2.0},
-            (4, "t"): {"flow": 2.0},
-        }
+        expected = [
+            (("s", 1), 1.0),
+            (("s", 2), 1.0),
+            ((1, 3), 1.0),
+            ((2, 4), 2.0),
+            ((4, "t"), 2.0),
+        ]
         self.assertIsInstance(sol, nx.Graph)
         self.assertTrue(check_solution_networkx(sol, [expected]))

@@ -158,24 +162,24 @@ class TestMinCostFlow2(unittest.TestCase):
         cost, sol = mcf.min_cost_flow_pandas(edge_data, node_data)
         sol = sol[sol > 0]
         self.assertEqual(cost, 150)
-        candidate = {
-            (0, 1): 12.0,
-            (0, 2): 8.0,
-            (1, 3): 4.0,
-            (1, 2): 8.0,
-            (2, 3): 15.0,
-            (2, 4): 1.0,
-            (3, 4): 14.0,
-        }
-        candidate2 = {
-            (0, 1): 12.0,
-            (0, 2): 8.0,
-            (1, 3): 4.0,
-            (1, 2): 8.0,
-            (2, 3): 11.0,
-            (2, 4): 5.0,
-            (3, 4): 10.0,
-        }
+        candidate = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 15.0),
+            ((2, 4), 1.0),
+            ((3, 4), 14.0),
+        ]
+        candidate2 = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 11.0),
+            ((2, 4), 5.0),
+            ((3, 4), 10.0),
+        ]
         self.assertTrue(check_solution_pandas(sol, [candidate, candidate2]))

     def test_scipy(self):
@@ -197,24 +201,24 @@ class TestMinCostFlow2(unittest.TestCase):
         G = load_graph2_networkx()
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 150)
-        candidate = {
-            (0, 1): {"flow": 12.0},
-            (0, 2): {"flow": 8.0},
-            (1, 2): {"flow": 8.0},
-            (1, 3): {"flow": 4.0},
-            (2, 3): {"flow": 11.0},
-            (2, 4): {"flow": 5.0},
-            (3, 4): {"flow": 10.0},
-        }
-        candidate2 = {
-            (0, 1): {"flow": 12.0},
-            (0, 2): {"flow": 8.0},
-            (1, 3): {"flow": 4.0},
-            (1, 2): {"flow": 8.0},
-            (2, 3): {"flow": 15.0},
-            (2, 4): {"flow": 1.0},
-            (3, 4): {"flow": 14.0},
-        }
+        candidate = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 2), 8.0),
+            ((1, 3), 4.0),
+            ((2, 3), 11.0),
+            ((2, 4), 5.0),
+            ((3, 4), 10.0),
+        ]
+        candidate2 = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 15.0),
+            ((2, 4), 1.0),
+            ((3, 4), 14.0),
+        ]
         self.assertTrue(check_solution_networkx(sol, [candidate, candidate2]))

     @unittest.skipIf(nx is None, "networkx is not installed")
@@ -223,23 +227,23 @@ class TestMinCostFlow2(unittest.TestCase):
         G = nx.relabel_nodes(G, {0: "s", 4: "t"})
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 150)
-        candidate = {
-            ("s", 1): {"flow": 12.0},
-            ("s", 2): {"flow": 8.0},
-            (1, 2): {"flow": 8.0},
-            (1, 3): {"flow": 4.0},
-            (2, 3): {"flow": 11.0},
-            (2, "t"): {"flow": 5.0},
-            (3, "t"): {"flow": 10.0},
-        }
+        candidate = [
+            (("s", 1), 12.0),
+            (("s", 2), 8.0),
+            ((1, 2), 8.0),
+            ((1, 3), 4.0),
+            ((2, 3), 11.0),
+            ((2, "t"), 5.0),
+            ((3, "t"), 10.0),
+        ]
         candidate2 = {
-            ("s", 1): {"flow": 12.0},
-            ("s", 2): {"flow": 8.0},
-            (1, 3): {"flow": 4.0},
-            (1, 2): {"flow": 8.0},
-            (2, 3): {"flow": 15.0},
-            (2, "t"): {"flow": 1.0},
-            (3, "t"): {"flow": 14.0},
+            (("s", 1), 12.0),
+            (("s", 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 15.0),
+            ((2, "t"), 1.0),
+            ((3, "t"), 14.0),
         }
         self.assertTrue(check_solution_networkx(sol, [candidate, candidate2]))

@@ -251,15 +255,17 @@ class TestMinCostFlow3(unittest.TestCase):
         sol = sol[sol > 0]
         self.assertEqual(cost, 49.0)

-        candidate = pd.DataFrame(
-            {
-                "source": [0, 0, 1, 1, 2, 2, 3, 2],
-                "target": [1, 2, 3, 2, 3, 4, 4, 3],
-                "flow": [12.0, 8.0, 4.0, 8.0, 10.0, 5.0, 10.0, 1.0],
-            }
-        ).set_index(["source", "target"])
-
-        self.assertTrue(check_solution_pandas_multi(sol, [candidate]))
+        candidate = [
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 10.0),
+            ((2, 4), 5.0),
+            ((3, 4), 10.0),
+            ((2, 3), 1.0),
+        ]
+        self.assertTrue(check_solution_pandas(sol, [candidate]))

     @unittest.skipIf(nx is None, "networkx is not installed")
     def test_networkx(self):
@@ -267,14 +273,14 @@ class TestMinCostFlow3(unittest.TestCase):
         cost, sol = mcf.min_cost_flow_networkx(G)
         self.assertEqual(cost, 49.0)
         candidate = [
-            (0, 1, {"flow": 12.0}),
-            (0, 2, {"flow": 8.0}),
-            (1, 3, {"flow": 4.0}),
-            (1, 2, {"flow": 8.0}),
-            (2, 3, {"flow": 10.0}),
-            (2, 3, {"flow": 1.0}),
-            (2, 4, {"flow": 5.0}),
-            (3, 4, {"flow": 10.0}),
+            ((0, 1), 12.0),
+            ((0, 2), 8.0),
+            ((1, 3), 4.0),
+            ((1, 2), 8.0),
+            ((2, 3), 10.0),
+            ((2, 3), 1.0),
+            ((2, 4), 5.0),
+            ((3, 4), 10.0),
         ]

-        self.assertTrue(check_solution_networkx_multi(sol, [candidate]))
+        self.assertTrue(check_solution_networkx(sol, [candidate]))

I had this code in a previous commit, but was trying to limit changes, but thanks!

jsfreischuetz commented 4 months ago

@jsfreischuetz Thanks for the updated PR! Just made some changes to keep the previous outputs the same (not always returning a MultiGraph) and some further clean up. Feel free to check that it all still works as expected

Everything seems to be working properly

torressa commented 4 months ago

I had this code in a previous commit, but was trying to limit changes, but thanks!

Ah cool, sorry, did not realise. I think it makes sense, it is safer than dictionaries as the order can sometimes differ leading to randomly failing tests. I can revert it if Simon is not happy with it.

@simonbowly please review, sorry about the numerous changes to the tests but I think it is better like this

simonbowly commented 4 months ago

Thanks @jsfreischuetz and @torressa! I'll release the changes as version 2.2, just want to take a look at another open PR first and hopefully merge and release that in the same go.