Skip to content

Commit

Permalink
max weight matching (#1602)
Browse files Browse the repository at this point in the history
* Added max_weight_matching

* Made weight optional

* fmt

* mostly working Mapping output

* add python wrappers for Matching and improve the Matching output

* cleanup, add tests and docs

* fix non-deterministic order

---------

Co-authored-by: Lucas Jeub <[email protected]>
  • Loading branch information
miratepuffin and ljeub-pometry authored Nov 13, 2024
1 parent 8dce659 commit b6f020c
Show file tree
Hide file tree
Showing 16 changed files with 1,957 additions and 23 deletions.
8 changes: 4 additions & 4 deletions python/python/raphtory/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ class Graph(GraphView):
num_shards (int, optional): The number of locks to use in the storage to allow for multithreaded updates.
"""

def __new__(self, num_shards: Optional[int] = None):
def __new__(self, num_shards: Optional[int] = None) -> Graph:
"""Create and return a new object. See help(type) for accurate signature."""

def __reduce__(self): ...
Expand Down Expand Up @@ -3426,7 +3426,7 @@ class Nodes(object):
class PersistentGraph(GraphView):
"""A temporal graph that allows edges and nodes to be deleted."""

def __new__(self):
def __new__(self) -> PersistentGraph:
"""Create and return a new object. See help(type) for accurate signature."""

def __reduce__(self): ...
Expand Down Expand Up @@ -4009,7 +4009,7 @@ class Prop(object):
def __ne__(self, value):
"""Return self!=value."""

def __new__(self, name):
def __new__(self, name) -> Prop:
"""Create and return a new object. See help(type) for accurate signature."""

def any(self, values):
Expand Down Expand Up @@ -4103,7 +4103,7 @@ class PyGraphEncoder(object):
"""Call self as a function."""

def __getstate__(self): ...
def __new__(self):
def __new__(self) -> PyGraphEncoder:
"""Create and return a new object. See help(type) for accurate signature."""

def __setstate__(self): ...
Expand Down
112 changes: 112 additions & 0 deletions python/python/raphtory/algorithms/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,78 @@ from raphtory.typing import *
from datetime import datetime
from pandas import DataFrame

class Matching(object):
"""A Matching (i.e., a set of edges that do not share any nodes)"""

def __bool__(self):
"""True if self else False"""

def __contains__(self, key):
"""Return bool(key in self)."""

def __iter__(self):
"""Implement iter(self)."""

def __len__(self):
"""Return len(self)."""

def __repr__(self):
"""Return repr(self)."""

def dst(self, src: InputNode) -> Optional[Node]:
"""
Get the matched destination node for a source node
Arguments:
src (InputNode): The source node
Returns:
Optional[Node]: The matched destination node if it exists
"""

def edge_for_dst(self, dst: InputNode) -> Optional[Edge]:
"""
Get the matched edge for a destination node
Arguments:
dst (InputNode): The source node
Returns:
Optional[Edge]: The matched edge if it exists
"""

def edge_for_src(self, src: InputNode) -> Optional[Edge]:
"""
Get the matched edge for a source node
Arguments:
src (InputNode): The source node
Returns:
Optional[Edge]: The matched edge if it exists
"""

def edges(self) -> Edges:
"""
Get a view of the matched edges
Returns:
Edges: The edges in the matching
"""

def src(self, dst: InputNode) -> Optional[Node]:
"""
Get the matched source node for a destination node
Arguments:
dst (InputNode): The destination node
Returns:
Optional[Node]: The matched source node if it exists
"""

def all_local_reciprocity(g: GraphView):
"""
Local reciprocity - measure of the symmetry of relationships associated with a node
Expand Down Expand Up @@ -405,6 +477,46 @@ def max_out_degree(g: GraphView):
int : value of the largest outdegree
"""

def max_weight_matching(
graph: GraphView,
weight_prop: Optional[str] = None,
max_cardinality: bool = True,
verify_optimum_flag: bool = False,
) -> Matching:
"""
Compute a maximum-weighted matching in the general undirected weighted
graph given by "edges". If `max_cardinality` is true, only
maximum-cardinality matchings are considered as solutions.
The algorithm is based on "Efficient Algorithms for Finding Maximum
Matching in Graphs" by Zvi Galil, ACM Computing Surveys, 1986.
Based on networkx implementation
<https://github.com/networkx/networkx/blob/3351206a3ce5b3a39bb2fc451e93ef545b96c95b/networkx/algorithms/matching.py>
With reference to the standalone protoype implementation from:
<http://jorisvr.nl/article/maximum-matching>
<http://jorisvr.nl/files/graphmatching/20130407/mwmatching.py>
The function takes time O(n**3)
Arguments:
graph (GraphView): The graph to compute the maximum weight matching for
weight_prop (str, optional): The property on the edge to use for the weight. If not
provided,
max_cardinality (bool): If set to true compute the maximum-cardinality matching
with maximum weight among all maximum-cardinality matchings. Defaults to True.
verify_optimum_flag (bool): If true prior to returning an additional routine
to verify the optimal solution was found will be run after computing
the maximum weight matching. If it's true and the found matching is not
an optimal solution this function will panic. This option should
normally be only set true during testing. Defaults to False.
Returns:
Matching: The matching
"""

def min_degree(g: GraphView) -> int:
"""
Returns the smallest degree found in the graph
Expand Down
20 changes: 12 additions & 8 deletions python/python/raphtory/graphql/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class GraphServer(object):
otlp_agent_port=None,
otlp_tracing_service_name=None,
config_path=None,
):
) -> GraphServer:
"""Create and return a new object. See help(type) for accurate signature."""

def run(self, port: int = 1736, timeout_ms: int = 180000):
Expand Down Expand Up @@ -147,7 +147,7 @@ class GraphqlGraphs(object):
class RaphtoryClient(object):
"""A client for handling GraphQL operations in the context of Raphtory."""

def __new__(self, url):
def __new__(self, url) -> RaphtoryClient:
"""Create and return a new object. See help(type) for accurate signature."""

def copy_graph(self, path, new_path):
Expand Down Expand Up @@ -268,7 +268,7 @@ class RaphtoryClient(object):
"""

class RemoteEdge(object):
def __new__(self, path, client, src, dst):
def __new__(self, path, client, src, dst) -> RemoteEdge:
"""Create and return a new object. See help(type) for accurate signature."""

def add_constant_properties(
Expand Down Expand Up @@ -323,11 +323,13 @@ class RemoteEdge(object):
"""

class RemoteEdgeAddition(object):
def __new__(self, src, dst, layer=None, constant_properties=None, updates=None):
def __new__(
self, src, dst, layer=None, constant_properties=None, updates=None
) -> RemoteEdgeAddition:
"""Create and return a new object. See help(type) for accurate signature."""

class RemoteGraph(object):
def __new__(self, path, client):
def __new__(self, path, client) -> RemoteGraph:
"""Create and return a new object. See help(type) for accurate signature."""

def add_constant_properties(self, properties: dict):
Expand Down Expand Up @@ -456,7 +458,7 @@ class RemoteGraph(object):
"""

class RemoteNode(object):
def __new__(self, path, client, id):
def __new__(self, path, client, id) -> RemoteNode:
"""Create and return a new object. See help(type) for accurate signature."""

def add_constant_properties(self, properties: Dict[str, Prop]):
Expand Down Expand Up @@ -501,11 +503,13 @@ class RemoteNode(object):
"""

class RemoteNodeAddition(object):
def __new__(self, name, node_type=None, constant_properties=None, updates=None):
def __new__(
self, name, node_type=None, constant_properties=None, updates=None
) -> RemoteNodeAddition:
"""Create and return a new object. See help(type) for accurate signature."""

class RemoteUpdate(object):
def __new__(self, time, properties=None):
def __new__(self, time, properties=None) -> RemoteUpdate:
"""Create and return a new object. See help(type) for accurate signature."""

class RunningGraphServer(object):
Expand Down
2 changes: 1 addition & 1 deletion python/python/raphtory/vectors/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ from datetime import datetime
from pandas import DataFrame

class Document(object):
def __new__(self, content, life=None):
def __new__(self, content, life=None) -> Document:
"""Create and return a new object. See help(type) for accurate signature."""

def __repr__(self):
Expand Down
2 changes: 2 additions & 0 deletions python/scripts/gen-stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ def gen_class(cls: type, name) -> str:
# Get __init__ signature from class info
signature = cls_signature(cls)
if signature is not None:
if obj_name == "__new__":
signature = signature.replace(return_annotation=name)
entities.append(
gen_fn(
entity,
Expand Down
27 changes: 27 additions & 0 deletions python/tests/test_algorithms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
import pandas as pd
import pandas.core.frame

from raphtory import Graph, PersistentGraph
from raphtory import algorithms
from raphtory import graph_loader
Expand Down Expand Up @@ -518,3 +519,29 @@ def test_temporal_SEIR():
for i, (n, v) in enumerate(res):
assert n == g.node(i + 1)
assert v.infected == i


def test_max_weight_matching():
g = Graph()
g.add_edge(0, 1, 2, {"weight": 5})
g.add_edge(0, 2, 3, {"weight": 11})
g.add_edge(0, 3, 4, {"weight": 5})

# Run max weight matching with max cardinality set to false
max_weight = algorithms.max_weight_matching(g, "weight", False)
max_cardinality = algorithms.max_weight_matching(g, "weight")

assert len(max_weight) == 1
assert len(max_cardinality) == 2
assert (2, 3) in max_weight
assert (1, 2) in max_cardinality
assert (3, 4) in max_cardinality

assert max_weight.edges().id == [(2, 3)]
assert sorted(max_cardinality.edges().id) == [(1, 2), (3, 4)]

assert max_weight.src(3).id == 2
assert max_weight.src(2) is None

assert max_weight.dst(2).id == 3
assert max_weight.dst(3) is None
26 changes: 26 additions & 0 deletions raphtory-api/src/core/entities/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,32 @@ pub enum GID {
U64(u64),
Str(String),
}
impl PartialEq<str> for GID {
fn eq(&self, other: &str) -> bool {
match self {
GID::U64(_) => false,
GID::Str(id) => id == other,
}
}
}

impl PartialEq<String> for GID {
fn eq(&self, other: &String) -> bool {
match self {
GID::U64(_) => false,
GID::Str(id) => id == other,
}
}
}

impl PartialEq<u64> for GID {
fn eq(&self, other: &u64) -> bool {
match self {
GID::Str(_) => false,
GID::U64(id) => id == other,
}
}
}

impl Default for GID {
fn default() -> Self {
Expand Down
Loading

0 comments on commit b6f020c

Please sign in to comment.