1
0
Fork 0

Improve performance of verification code

This commit is contained in:
Joris van Rantwijk 2023-02-08 21:29:56 +01:00
parent 291d3ead8b
commit 8bef12559a
1 changed files with 152 additions and 79 deletions

View File

@ -79,12 +79,10 @@ def maximum_weight_matching(
(x, y) for (x, y, _w) in edges if ctx.vertex_mate[x] == y] (x, y) for (x, y, _w) in edges if ctx.vertex_mate[x] == y]
# Verify that the matching is optimal. # Verify that the matching is optimal.
# This only works reliably for integer weights. # This is just a safeguard; the verification will always pass unless
# Verification is a redundant step; if the matching algorithm is correct, # there is a bug in the matching algorithm.
# verification will always pass. # Verification only works reliably for integer weights.
if graph.integer_weights: if graph.integer_weights:
# TODO : pass selection of data to verification
# passing the whole context does not inspire trust that this is an independent verification
_verify_optimum(ctx) _verify_optimum(ctx)
return pairs return pairs
@ -1649,106 +1647,181 @@ class _MatchingContext:
return (augmenting_path is not None) return (augmenting_path is not None)
# TODO : clean up this whole mess def _verify_blossom_edges(
ctx: _MatchingContext,
blossom: _NonTrivialBlossom,
edge_slack_2x: list[int|float]
) -> None:
"""Descend down the blossom tree to find edges that are contained
in blossoms.
Adjust the slack of all contained edges to account for the dual variables
of its containing blossoms.
On the way down, keep track of the sum of dual variables of
the containing blossoms.
On the way up, keep track of the total number of matched edges
in the subblossoms. Then check that all blossoms with non-zero
dual variable are "full".
Raises:
AssertionError: If a blossom with non-zero dual is not full.
"""
num_vertex = ctx.graph.num_vertex
# For each vertex "x",
# "vertex_depth[x]" is the depth of the smallest blossom on
# the current descent path that contains "x".
vertex_depth: list[int] = num_vertex * [0]
# Keep track of the sum of blossom duals at each depth along
# the current descent path.
path_sum_dual: list[int|float] = [0]
# Keep track of the number of matched edges at each depth along
# the current descent path.
path_num_matched: list[int] = [0]
# Use an explicit stack to avoid deep recursion.
stack: list[tuple[_NonTrivialBlossom, int]] = [(blossom, -1)]
while stack:
(blossom, p) = stack[-1]
depth = len(stack)
if p == -1:
# We just entered this sub-blossom.
# Update the depth of all vertices in this sub-blossom.
for x in blossom.vertices():
vertex_depth[x] = depth
# Calculate the sub of blossoms at the current depth.
path_sum_dual.append(path_sum_dual[-1] + blossom.dual_var)
# Initialize the number of matched edges at the current depth.
path_num_matched.append(0)
p += 1
if p < len(blossom.subblossoms):
# Update the sub-blossom pointer at the current level.
stack[-1] = (blossom, p + 1)
# Examine the next sub-blossom at the current level.
sub = blossom.subblossoms[p]
if isinstance(sub, _NonTrivialBlossom):
# Prepare to descent into the selected sub-blossom and
# scan it recursively.
stack.append((sub, -1))
else:
# Handle this trivial sub-blossom.
# Scan its adjacent edges and find the smallest blossom
# that contains each edge.
for e in ctx.graph.adjacent_edges[sub.base_vertex]:
(x, y, _w) = ctx.graph.edges[e]
# Only process edges that are ordered out from this
# sub-blossom. This ensures that we process each edge in
# the blossom only once.
if x == sub.base_vertex:
edge_depth = vertex_depth[y]
if edge_depth > 0:
# This edge is contained in an ancestor blossom.
# Update its slack.
edge_slack_2x[e] += 2 * path_sum_dual[edge_depth]
# Update the number of matched edges in ancestor.
if ctx.vertex_mate[x] == y:
path_num_matched[edge_depth] += 1
else:
# We are now leaving the current sub-blossom.
# Count the number of vertices inside this blossom.
blossom_vertices = blossom.vertices()
blossom_num_vertex = len(blossom_vertices)
# Check that all blossoms with positive dual are "full".
# A blossom is full if all except one of its vertices are
# matched to another vertex in the blossom.
if blossom.dual_var > 0:
blossom_num_matched = path_num_matched[depth]
assert blossom_num_vertex == 2 * blossom_num_matched + 1
# Update the number of matched edges in the parent blossom to
# take into account the matched edges in this blossom.
path_num_matched[depth - 1] += path_num_matched[depth]
# Revert the depth of the vertices in this sub-blossom.
for x in blossom_vertices:
vertex_depth[x] = depth - 1
# Trim the descending path.
path_sum_dual.pop()
path_num_matched.pop()
# Remove the current blossom from the stack.
# We thus continue our scan of the parent blossom.
stack.pop()
def _verify_optimum(ctx: _MatchingContext) -> None: def _verify_optimum(ctx: _MatchingContext) -> None:
"""Verify that the optimum solution has been found. """Verify that the optimum solution has been found.
This function takes time O(m * n). This function takes time O(n**2).
TODO : really ??
Raises: Raises:
AssertionError: If the solution is not optimal. AssertionError: If the solution is not optimal.
""" """
num_vertex = ctx.graph.num_vertex num_vertex = ctx.graph.num_vertex
num_edge = len(ctx.graph.edges)
vertex_mate = ctx.vertex_mate # Double-check that each matched edge actually exists in the graph.
vertex_dual_var_2x = ctx.vertex_dual_2x
# Double-check that each matching edge actually exists in the graph.
num_matched_vertex = 0 num_matched_vertex = 0
for x in range(num_vertex): for x in range(num_vertex):
if vertex_mate[x] != -1: if ctx.vertex_mate[x] != -1:
assert ctx.vertex_mate[ctx.vertex_mate[x]] == x
num_matched_vertex += 1 num_matched_vertex += 1
num_matched_edge = 0 num_matched_edge = 0
for (x, y, _w) in ctx.graph.edges: for (x, y, _w) in ctx.graph.edges:
if vertex_mate[x] == y: if ctx.vertex_mate[x] == y:
num_matched_edge += 1 num_matched_edge += 1
assert num_matched_vertex == 2 * num_matched_edge assert num_matched_vertex == 2 * num_matched_edge
# Check that all dual variables are non-negative. # Check that all dual variables are non-negative.
assert min(vertex_dual_var_2x) >= 0 assert min(ctx.vertex_dual_2x) >= 0
for blossom in ctx.nontrivial_blossom: for blossom in ctx.nontrivial_blossom:
assert blossom.dual_var >= 0 assert blossom.dual_var >= 0
# Count the number of vertices in each blossom. # Calculate the slack of each edge.
blossom_nvertex = {id(blossom): 0 for blossom in ctx.nontrivial_blossom} # A correction will be needed for edges inside blossoms.
for x in range(num_vertex): edge_slack_2x: list[int|float] = [
b = ctx.trivial_blossom[x] ctx.vertex_dual_2x[x] + ctx.vertex_dual_2x[y] - 2 * w
while b.parent is not None: for (x, y, w) in ctx.graph.edges]
b = b.parent
blossom_nvertex[id(b)] += 1
# Calculate slack of each edge. # Descend down each top-level blossom.
# Also count the number of matched edges in each blossom. # Adjust edge slacks to account for the duals of its containing blossoms.
blossom_nmatched = {id(blossom): 0 for blossom in ctx.nontrivial_blossom} # And check that blossoms with non-zero dual are full.
# This takes total time O(n**2).
for (x, y, w) in ctx.graph.edges:
# List blossoms that contain vertex "x".
xblossoms = []
bx = ctx.trivial_blossom[x]
while bx.parent is not None:
bx = bx.parent
xblossoms.append(bx)
# List blossoms that contain vertex "y".
yblossoms = []
by = ctx.trivial_blossom[y]
while by.parent is not None:
by = by.parent
yblossoms.append(by)
# List blossoms that contain the edge (x, y).
edge_blossoms: list[_NonTrivialBlossom] = []
for (bx, by) in zip(reversed(xblossoms), reversed(yblossoms)):
if bx is not by:
break
edge_blossoms.append(bx)
# Calculate edge slack =
# dual[x] + dual[y] - weight
# + sum(blossom.dual_var for "blossom" containing the edge)
#
# Multiply weights by 2 to ensure integer values.
slack = vertex_dual_var_2x[x] + vertex_dual_var_2x[y] - 2 * w
slack += 2 * sum(blossom.dual_var for blossom in edge_blossoms)
# Check that all edges have non-negative slack.
assert slack >= 0
# Check that all matched edges have zero slack.
if vertex_mate[x] == y:
assert slack == 0
# Update number of matched edges in each blossom.
if vertex_mate[x] == y:
for b in edge_blossoms:
blossom_nmatched[id(b)] += 1
# Check that all unmatched vertices have zero dual.
for x in range(num_vertex):
if vertex_mate[x] == -1:
assert vertex_dual_var_2x[x] == 0
# Check that all blossoms with positive dual are "full".
# A blossom is full if all except one of its vertices are matched
# to another vertex in the same blossom.
for blossom in ctx.nontrivial_blossom: for blossom in ctx.nontrivial_blossom:
if blossom.dual_var > 0: if blossom.parent is None:
assert blossom_nvertex[id(blossom)] == 2 * blossom_nmatched[id(blossom)] + 1 _verify_blossom_edges(ctx, blossom, edge_slack_2x)
# We now know the correct slack of each edge.
# Check that all edges have non-negative slack.
assert min(edge_slack_2x) >= 0
# Check that all matched edges have zero slack.
for e in range(num_edge):
(x, y, _w) = ctx.graph.edges[e]
if ctx.vertex_mate[x] == y:
assert edge_slack_2x[e] == 0
# Optimum solution confirmed. # Optimum solution confirmed.