Skip to content

Commit

Permalink
Add Maximum Weight Fractional Matching algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
oriyalperin committed Oct 18, 2023
1 parent 09e9f6d commit 20af1cb
Show file tree
Hide file tree
Showing 2 changed files with 296 additions and 0 deletions.
123 changes: 123 additions & 0 deletions networkz/algorithms/max_weight_fractional_matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import numpy as np
from scipy.optimize import linprog

import networkz as nx
from networkz import incidence_matrix


def maximum_weight_fractional_matching(G: nx.Graph, weight="weight", **linprog_options):
"""Returns the maximum-weight fractional matching of the wighted graph `G`.
A fractional graph is a graph in which every edge has a fraction [0,1]
such that the sum of fractions of edges adjacent to each vertex is at most 1.
A matching is a set of edges that do not share any nodes.
Define fw(e) for each edge e to be the multiplication of its weight and fraction.
A maximum-weight fractional matching is one with the maximum fw(e) sum of all e in E(G).
A fractional matching of maximum weight in a graph can be found by linear programming.
*If the edges are not weighted - the weight of each edge is 1.
Parameters
----------
G : NetworkX graph
Undirected weighted graph
weight : str
the name of the edge attribute that represents the weight of an edge.
linprog_options : dict
scipy.optimize.linprog options, None as default.
Returns
-------
F : dictionary
The fractions are returned as a dictionary, `frac`, such that
``frac[e] == f`` for edge `e` with fraction `f` (rounded to 3 decimals).
Examples
--------
In the weighted graph, G = (V,E).
>>> G = nx.Graph()
>>> G.add_nodes_from(["a1", "a2"])
>>> G.add_edge("a1", "a2", weight=3)
>>> F = maximum_weight_fractional_matching(G)
>>> print(F=={('a1', 'a2'): 1.0})
True
>>> F[('a1','a2')]
1.0
explanation: weight = 3
G = a1-----------a2
frac = 1.0
maximum_weight_fractional_matching(G) = a1-----------a2
The returned value is {('a1', 'a2'): 1.0}.
There is only one edge, so it gets the maximaum value.
another example:
>>> G = nx.Graph()
>>> G.add_nodes_from(["a1", "a2", "a3"])
>>> G.add_weighted_edges_from([("a1", "a2", 1), ("a1", "a3", 2), ("a2", "a3", 3)])
>>> F = maximum_weight_fractional_matching(G)
>>> print(F=={('a1', 'a2'): 0.5, ('a1', 'a3'): 0.5, ('a2', 'a3'): 0.5})
True
>>> F[('a2','a3')]
0.5
explanation: weight = 1
G = a1------------a2
\\ \\
weight = 2 \\ \\ weight = 3
\\ \\
\\ \\
a3
frac = 0.5
maximum_weight_fractional_matching(G) = a1------------a2
\\ \\
frac = 0.5 \\ \\ frac = 0.5
\\ \\
\\ \\
a3
The returned value is {('a1', 'a2'): 0.5, ('a1', 'a3'): 0.5, ('a2', 'a3'): 0.5}.
We want to find Max(x,y,z) S.T
a1: x +2y<=1
a2: x+3z<=1
a3: 2y+3z<=1
and
x,y,z<=1
we can solve it using the linprog function:
linprog(c, A_ub, b_ub, bounds, method='highs')
linprog solve the same problem, but it finds the Min(x,y,z) so if we want Max(x,y,z)
we can change our inqualities to be:
Min(x,y,z)
S.T
a1: x +2y>=-1
a2: x+3z>=-1
a3: 2y+3z>=-1
set bounds = (-1, 0)
and then take the result as ABS, like that - |Min(x,y,z)|
than we will get the solution for our original problem = {('a1', 'a2'): 0.5, ('a1', 'a3'): 0.5, ('a2', 'a3'): 0.5}
See Also
--------
linprog
References
----------
https://en.wikipedia.org/wiki/Fractional_matching
"""

if G.number_of_nodes() == 0 or G.number_of_edges() == 0:
return dict()
c = [G.edges[edge].get(weight, 1) for edge in G.edges]
b = [1] * len(G.nodes)
bounds = (-1, 0)
A = -incidence_matrix(G)
res = linprog(
c, A_ub=A, b_ub=b, bounds=bounds, method="highs", options=linprog_options
)
return dict(zip(G.edges, np.abs(np.round(res.x, 3))))
173 changes: 173 additions & 0 deletions networkz/algorithms/tests/test_max_weight_fractional_matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from random import randint

import pytest

np = pytest.importorskip("numpy")

import networkz as nx
import networkz.algorithms.max_weight_fractional_matching as mw


def get_max_weight_frac(res, G=nx.Graph()):
return sum(frac * G.edges[edge]["weight"] for edge, frac in res.items())


class TestMaximumWeightFractionalMatching:
def test_empty_graph(self):
G = nx.Graph()
res = mw.maximum_weight_fractional_matching(G)
assert {} == res

def test_graph_without_edges(self):
G = nx.Graph()
G.add_nodes_from([i for i in range(0, 10)])
res = mw.maximum_weight_fractional_matching(G)
assert {} == res

def test_simple_graph_without_weights(self):
G = nx.Graph()
G.add_nodes_from(["a1", "a2"])
G.add_edge("a1", "a2")
res = mw.maximum_weight_fractional_matching(G)
assert {("a1", "a2"): 1.0} == res

def test_simple_graph_with_weight(self):
G = nx.Graph()
G.add_nodes_from(["a1", "a2"])
G.add_edge("a1", "a2", weight=3)
res = mw.maximum_weight_fractional_matching(G)
assert {("a1", "a2"): 1.0} == res

def test_simple_graph_with_negative_weight(self):
G = nx.Graph()
G.add_nodes_from(["a1", "a2"])
G.add_edge("a1", "a2", weight=-1)
res = mw.maximum_weight_fractional_matching(G)
assert {("a1", "a2"): 0} == res

def test_3_nodes_graph_without_weights(self):
G = nx.Graph()
G.add_nodes_from(["a1", "a2", "a3"])
G.add_edges_from([("a1", "a2"), ("a1", "a3"), ("a2", "a3")])
res = mw.maximum_weight_fractional_matching(G)
max_weight = np.round(sum(frac for frac in res.values()), 3)
exp_val = {("a1", "a2"): 0.5, ("a1", "a3"): 0.5, ("a2", "a3"): 0.5}
weight = np.round(sum(frac for frac in exp_val.values()), 3)
assert weight == max_weight

def test_simple_graph_with_equal_weights(self):
G = nx.Graph()
G.add_nodes_from(["a1", "a2", "a3"])
G.add_weighted_edges_from([("a1", "a2", 5), ("a1", "a3", 5), ("a2", "a3", 5)])
res = mw.maximum_weight_fractional_matching(G)
max_weight = get_max_weight_frac(res, G)
exp_val = {("a1", "a2"): 0.5, ("a1", "a3"): 0.5, ("a2", "a3"): 0.5}
weight = get_max_weight_frac(exp_val, G)
assert weight == max_weight

def test_3_nodes_graph_1_3_weights(self):
G = nx.Graph()
G.add_nodes_from(["a1", "a2", "a3"])
G.add_weighted_edges_from([("a1", "a2", 1), ("a1", "a3", 2), ("a2", "a3", 3)])
res = mw.maximum_weight_fractional_matching(G)
max_weight = get_max_weight_frac(res, G)
exp_val = {("a1", "a2"): 0.5, ("a1", "a3"): 0.5, ("a2", "a3"): 0.5}
weight = get_max_weight_frac(exp_val, G)
assert weight == max_weight

def test_3_nodes_graph_with_weights(self):
G = nx.Graph()
G.add_nodes_from(["a1", "a2", "a3"])
G.add_weighted_edges_from([("a1", "a2", 1), ("a1", "a3", 2), ("a2", "a3", 4)])
res = mw.maximum_weight_fractional_matching(G)
max_weight = get_max_weight_frac(res, G)
exp_val = {("a1", "a2"): 0.0, ("a1", "a3"): 0.0, ("a2", "a3"): 1.0}
weight = get_max_weight_frac(exp_val, G)
assert weight == max_weight

def test_3_nodes_graph_with_negative_weight(self):
G = nx.Graph()
G.add_nodes_from(["a1", "a2", "a3"])
G.add_weighted_edges_from([("a1", "a2", -1), ("a1", "a3", 5), ("a2", "a3", 10)])
res = mw.maximum_weight_fractional_matching(G)
max_weight = get_max_weight_frac(res, G)
exp_val = {("a1", "a2"): 0.0, ("a1", "a3"): 0.0, ("a2", "a3"): 1.0}
weight = get_max_weight_frac(exp_val, G)
assert weight == max_weight

def test_7_nodes_graph_without_weights(self):
G = nx.Graph()
G.add_nodes_from([i for i in range(0, 7)])
G.add_edges_from(
[(0, 1), (0, 2), (0, 6), (0, 5), (1, 6), (2, 3), (2, 4), (2, 6), (3, 4)]
)
res = mw.maximum_weight_fractional_matching(G)
max_weight = np.round(sum(frac for frac in res.values()), 3)
exp_val = {
(0, 1): 0.0,
(0, 2): 0.0,
(0, 6): 0.0,
(0, 5): 1.0,
(1, 6): 1.0,
(2, 3): 0.5,
(2, 4): 0.5,
(2, 6): 0.0,
(3, 4): 0.5,
}
weight = np.round(sum(frac for frac in exp_val.values()), 3)
assert weight == max_weight

def test_7_nodes_graph_with_weights(self):
G = nx.Graph()
G.add_nodes_from([i for i in range(0, 7)])
G.add_weighted_edges_from(
[
(0, 1, 12),
(0, 2, 7),
(0, 6, 21),
(0, 5, 6),
(1, 6, 1),
(2, 3, 23),
(2, 4, 5),
(2, 6, 8),
(3, 4, 19),
]
)
res = mw.maximum_weight_fractional_matching(G)
max_weight = get_max_weight_frac(res, G)
exp_val = {
(0, 1): 0.0,
(0, 2): 0.0,
(0, 6): 1.0,
(0, 5): 0.0,
(1, 6): 0.0,
(2, 3): 0.5,
(2, 4): 0.5,
(2, 6): 0.0,
(3, 4): 0.5,
}
weight = get_max_weight_frac(exp_val, G)
assert weight == max_weight

def test_completes_graph_without_weights(self):
for i in range(0, 5):
n = randint(2, 30)
G = nx.complete_graph(n)
res = mw.maximum_weight_fractional_matching(G)
max_weight = np.round(sum(frac for frac in res.values()), 3)
weight = np.round((1 / (n - 1)) * len(G.edges()), 3)
assert weight == max_weight

def test_sum_of_weights_in_every_node_in_random_graph_is_less_than_1(self):
seed = 1
for i in range(0, 5):
G = nx.random_regular_graph(4, 6, seed)
res = mw.maximum_weight_fractional_matching(G)
for node in G.nodes:
sum_adj = 0
for edge in G.edges(node):
if edge in res:
sum_adj += res[edge]
else:
sum_adj += res[(edge[1], edge[0])]
assert sum_adj == 1

0 comments on commit 20af1cb

Please sign in to comment.