forked from ariel-research/networkz
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Maximum Weight Fractional Matching algorithm
- Loading branch information
1 parent
09e9f6d
commit 20af1cb
Showing
2 changed files
with
296 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
173
networkz/algorithms/tests/test_max_weight_fractional_matching.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |