From 38374e293f31bcb3ba325de0648ac883443bb56e Mon Sep 17 00:00:00 2001 From: Joris van Rantwijk Date: Sat, 11 Feb 2023 22:39:46 +0100 Subject: [PATCH] Code to generate test graphs --- tests/generate/make_graphs.sh | 66 ++++++ tests/generate/make_random_graph.py | 131 ++++++++++++ tests/generate/make_slow_graph.py | 301 ++++++++++++++++++++++++++++ 3 files changed, 498 insertions(+) create mode 100755 tests/generate/make_graphs.sh create mode 100644 tests/generate/make_random_graph.py create mode 100644 tests/generate/make_slow_graph.py diff --git a/tests/generate/make_graphs.sh b/tests/generate/make_graphs.sh new file mode 100755 index 0000000..fafcc4c --- /dev/null +++ b/tests/generate/make_graphs.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# +# Generate a set of test graphs. +# + +set -e + +SRCDIR="$(dirname $0)" + +# Usage: gen_random SEED N M +gen_random() { + python3 $SRCDIR/make_random_graph.py --seed $1 $2 $3 \ + > graph_rnd_s${1}_n${2}_m${3}.gr +} + +# Usage: gen_slow STRUCTURE N M +gen_slow() { + python3 $SRCDIR/make_slow_graph.py --structure $1 $2 \ + > graph_slow_${1}_n${2}_m${3}.gr +} + + +gen_random 101 250 31125 +gen_random 102 250 31125 +gen_random 103 250 3952 +gen_random 104 250 3952 +gen_random 105 250 2500 +gen_random 106 250 2500 + +gen_random 111 500 124750 +gen_random 112 500 124750 +gen_random 113 500 11180 +gen_random 114 500 11180 +gen_random 115 500 5000 +gen_random 116 500 5000 + +gen_random 121 1000 499500 +gen_random 122 1000 499500 +gen_random 123 1000 31622 +gen_random 124 1000 31622 +gen_random 125 1000 10000 +gen_random 126 1000 10000 + +gen_random 133 2000 89442 +gen_random 134 2000 89442 +gen_random 135 2000 20000 +gen_random 136 2000 20000 + +gen_random 143 5000 353553 +gen_random 144 5000 353553 +gen_random 145 5000 50000 +gen_random 146 5000 50000 + +gen_random 155 10000 100000 +gen_random 156 10000 100000 + +gen_slow dense 252 4095 +gen_slow dense 500 15875 +gen_slow dense 1000 63000 +gen_slow sparse 252 312 +gen_slow sparse 500 622 +gen_slow sparse 1004 1252 +gen_slow sparse 2004 2502 +gen_slow sparse 5004 6252 +gen_slow sparse 10004 12502 + diff --git a/tests/generate/make_random_graph.py b/tests/generate/make_random_graph.py new file mode 100644 index 0000000..18e4339 --- /dev/null +++ b/tests/generate/make_random_graph.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +""" +Generate a random graph in DIMACS format. +""" + +from __future__ import annotations + +import sys +import argparse +import random +from typing import TextIO + + +def write_dimacs_graph( + f: TextIO, + edges: list[tuple[int, int, int|float]] + ) -> None: + """Write a graph in DIMACS edge list format.""" + + num_vertex = 1 + max(max(x, y) for (x, y, _w) in edges) + num_edge = len(edges) + + print(f"p edge {num_vertex} {num_edge}", file=f) + + integer_weights = all(isinstance(w, int) for (_x, _y, w) in edges) + + for (x, y, w) in edges: + if integer_weights: + print(f"e {x+1} {y+1} {w}", file=f) + else: + print(f"e {x+1} {y+1} {w:.12g}", file=f) + + +def make_random_graph( + n: int, + m: int, + max_weight: float, + float_weights: bool, + rng: random.Random + ) -> list[tuple[int, int, int|float]]: + """Generate a random graph with random edge weights.""" + + edge_set: set[tuple[int, int]] = set() + + if 3 * m < n * (n - 2) // 2: + # Simply add random edges until we have enough. + while len(edge_set) < m: + x = rng.randint(0, n - 2) + y = rng.randint(x + 1, n - 1) + edge_set.add((x, y)) + + else: + # We need a very dense graph. + # Generate all edge candidates and choose a random subset. + edge_candidates = [(x, y) + for x in range(n - 1) + for y in range(x + 1, n)] + rng.shuffle(edge_candidates) + edge_set.update(edge_candidates[:m]) + + edges: list[tuple[int, int, int|float]] = [] + for (x, y) in sorted(edge_set): + w: int|float + if float_weights: + w = rng.uniform(1.0e-8, max_weight) + else: + w = rng.randint(1, int(max_weight)) + edges.append((x, y, w)) + + return edges + + +def main() -> int: + """Main program.""" + + parser = argparse.ArgumentParser() + parser.description = "Generate a random graph in DIMACS format." + + parser.add_argument("--seed", + action="store", + type=int, + help="random seed") + parser.add_argument("--maxweight", + action="store", + type=float, + default=1000000, + help="maximum edge weight") + parser.add_argument("--float", + action="store_true", + help="use floating point edge weights") + parser.add_argument("n", + action="store", + type=int, + help="number of vertices") + parser.add_argument("m", + action="store", + type=int, + help="number of edges") + + args = parser.parse_args() + + if args.n < 2: + print("ERROR: Number of vertices must be >= 2", file=sys.stderr) + return 1 + + if args.m < 1: + print("ERROR: Number of edges must be >= 1", file=sys.stderr) + return 1 + + if args.m > args.n * (args.n - 1) // 2: + print("ERROR: Too many edges", file=sys.stderr) + return 1 + + if args.maxweight < 1.0e-6: + print("ERROR: Invalid maximum edge weight", file=sys.stderr) + return 1 + + if args.seed is None: + rng = random.Random() + else: + rng = random.Random(args.seed) + + edges = make_random_graph(args.n, args.m, args.maxweight, args.float, rng) + write_dimacs_graph(sys.stdout, edges) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/generate/make_slow_graph.py b/tests/generate/make_slow_graph.py new file mode 100644 index 0000000..6515635 --- /dev/null +++ b/tests/generate/make_slow_graph.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 + +""" +Generate a graph that is "difficult" for the O(n**3) matching algorithm. + +Output in DIMACS format. +""" + +from __future__ import annotations + +import sys +import argparse +from typing import TextIO + + +count_make_blossom = [0] +count_delta_step = [0] + + +def patch_matching_code() -> None: + """Patch the matching code to count events.""" + + import max_weight_matching + + orig_make_blossom = max_weight_matching._MatchingContext.make_blossom + orig_substage_calc_dual_delta = ( + max_weight_matching._MatchingContext.substage_calc_dual_delta) + + def stub_make_blossom(*args, **kwargs): + count_make_blossom[0] += 1 + return orig_make_blossom(*args, **kwargs) + + def stub_substage_calc_dual_delta(*args, **kwargs): + count_delta_step[0] += 1 + ret = orig_substage_calc_dual_delta(*args, **kwargs) +# print("DELTA", ret) + return ret + + max_weight_matching._MatchingContext.make_blossom = stub_make_blossom + max_weight_matching._MatchingContext.substage_calc_dual_delta = ( + stub_substage_calc_dual_delta) + + +def run_max_weight_matching( + edges: list[tuple[int, int, int]] + ) -> tuple[list[tuple[int, int]], int, int]: + """Run the matching algorithm and count subroutine calls.""" + import max_weight_matching + + count_make_blossom[0] = 0 + count_delta_step[0] = 0 + + pairs = max_weight_matching.maximum_weight_matching(edges) + return (pairs, count_make_blossom[0], count_delta_step[0]) + + +def write_dimacs_graph( + f: TextIO, + edges: list[tuple[int, int, int]] + ) -> None: + """Write a graph in DIMACS edge list format.""" + + num_vertex = 1 + max(max(x, y) for (x, y, _w) in edges) + num_edge = len(edges) + + print(f"p edge {num_vertex} {num_edge}", file=f) + + for (x, y, w) in edges: + print(f"e {x+1} {y+1} {w}", file=f) + + +def make_dense_slow_graph(n: int) -> list[tuple[int, int, int]]: + """Generate a dense (not complete) graph with N vertices. + + N must be divisible by 4. + + Number of edges = M = (N**2/16 + N/2). + Number of delta steps required to solve the matching = (M - 1). + """ + + assert n % 4 == 0 + + edges: list[tuple[int, int, int]] = [] + + num_peripheral_pairs = n // 4 + num_central_pairs = n // 4 + max_weight = 2 * num_central_pairs * (num_peripheral_pairs + 1) + + # Peripheral pairs will be matched up first. + for i in range(num_peripheral_pairs): + x = 2 * i + y = x + 1 + w = max_weight - i + edges.append((x, y, w)) + + # Then for each central pair: + for k in range(num_central_pairs): + + x = 2 * num_peripheral_pairs + 2 * k + + # Central pair discovers all peripheral pairs. + for i in range(num_peripheral_pairs): + y = 2 * i + if k % 2 == 0: + w = max_weight - (1 + k // 2) * (num_peripheral_pairs + 1) + 1 + else: + w = max_weight - (1 + k // 2) * (num_peripheral_pairs + 1) - i + edges.append((x, y, w)) + + # Then this central pair gets matched. + y = x + 1 + w = max_weight - (k + 1) * (2 * num_peripheral_pairs + 1) + edges.append((x, y, w)) + + return edges + + +def make_sparse_slow_graph(n: int) -> list[tuple[int, int, int]]: + """Generate a sparse graph with N vertices. + + N must be 4 modulo 8. + + Number of edges = M = (5/4 * N - 3). + Number of delta steps required to solve the matching ~ (N**2 / 16). + """ + + assert n >= 12 + assert n % 8 == 4 + + num_p_pairs = (n - 4) // 4 + num_q_pairs = num_p_pairs // 2 + + # + # Graph structure: + # + # O O O O O + # | | | | | (P pairs of nodes) + # | | | | | (one edge in each pair) + # O___ O O O ___O + # \_ \ |\ / \ _/| / / + # \ | _/ __/ | _/ (2 * P edges) + # \_ | _/ __/ | _/ (connecting both coupling pairs + # \ | / __/ \ | / to each top-layer pair) + # \|/__/ \ \|/ + # O------/ \--\--O + # | | (2 coupling pairs) + # | | (1 edge in each pair) + # O O + # / \ / \ (2 * Q edges) + # / \ / \ (connecting each coupling pair + # / \ / \ to the Q pairs below it) + # O O O O + # | | | | (2 groups of Q pairs of nodes) + # | | | | (1 edge in each pair) + # O O O O + # + # Plan: + # - First, match each pair in the top layer. + # - Then, pull all edges between the top-layer and the + # left coupling pair tight (without matching anything). + # - Then match the left coupling pair. + # - Then pull all edges between the top-layer and the + # right coupling pair tight. (This will loosen the edges + # between the top-layer and the left coupling pair.) + # - Then match the right coupling pair. + # - Then alternate between the bottom left group and bottom right group + # of pairs: + # - Pick a new pair from the selected bottom group. + # - Pull the edge to its coupling pair tight. + # - Pull all edges between the coupling pair and the top-layer tight. + # (This will loosen the edges between the top-layer and the other + # coupling pair.) + # - Match the selected bottom pair. + # + # The trick is to assign edge weights that force the matching + # algorithm to execute the plan as descibed above. + # + + edges: list[tuple[int, int, int]] = [] + + max_weight = 16 * (num_q_pairs + 1) * (num_p_pairs + 2) + + # Make the top pairs. + for i in range(num_p_pairs): + x = 2 * i + y = 2 * i + 1 + w = max_weight - i + edges.append((x, y, w)) + + # Make the coupling pairs. + for k in range(2): + + # Connect the coupling pair to the top layer. + for i in range(num_p_pairs): + x = 2 * i + 1 + y = 2 * num_p_pairs + 2 * k + if k == 0: + w = max_weight - num_p_pairs + else: + w = max_weight - num_p_pairs - 1 - i + edges.append((x, y, w)) + + # Make the internal edge in the coupling pair. + x = 2 * num_p_pairs + 2 * k + y = 2 * num_p_pairs + 2 * k + 1 + if k == 0: + w = max_weight - 2 * num_p_pairs - 1 + else: + w = max_weight - 4 * num_p_pairs - 2 + edges.append((x, y, w)) + + # Make the bottom groups. + for k in range(2): + + # Connect the coupling pair to the bottom layer. + for i in range(num_q_pairs): + x = 2 * num_p_pairs + 2 * k + 1 + y = 2 * num_p_pairs + 4 + 2 * num_q_pairs * k + 2 * i + w = (max_weight + - (2 * i + k) * (2 * num_p_pairs + 4) + - 4 * num_p_pairs + 1) + edges.append((x, y, w)) + + # Make the pairs in this half of the bottom layer. + for i in range(num_q_pairs): + x = 2 * num_p_pairs + 4 + 2 * num_q_pairs * k + 2 * i + y = 2 * num_p_pairs + 4 + 2 * num_q_pairs * k + 2 * i + 1 + w = (max_weight + - (2 * i + k + 1) * 8 * num_p_pairs + - (2 * i + k) * 12) + edges.append((x, y, w)) + + return edges + + +def main() -> int: + """Main program.""" + + parser = argparse.ArgumentParser() + parser.description = "Generate a difficult graph." + + parser.add_argument("--structure", + action="store", + choices=("sparse", "dense"), + default="sparse", + help="choose graph structure") + parser.add_argument("--check", + action="store_true", + help="solve the matching and count delta steps") + parser.add_argument("n", + action="store", + type=int, + help="number of vertices") + + args = parser.parse_args() + + if args.check: + patch_matching_code() + + if args.structure == "sparse": + + if args.n < 12: + print("ERROR: Number of vertices must be >= 12", file=sys.stderr) + return 1 + + if args.n % 8 != 4: + print("ERROR: Number of vertices must be 4 modulo 8", + file=sys.stderr) + return 1 + + edges = make_sparse_slow_graph(args.n) + + elif args.structure == "dense": + + if args.n < 4: + print("ERROR: Number of vertices must be >= 4", file=sys.stderr) + return 1 + + if args.n % 4 != 0: + print("ERROR: Number of vertices must be divisible by 4", + file=sys.stderr) + return 1 + + edges = make_dense_slow_graph(args.n) + + else: + assert False + + if args.check: + (pairs, num_blossom, num_delta) = run_max_weight_matching(edges) + print(f"n={args.n} m={len(edges)} " + f"nblossom={num_blossom} ndelta={num_delta}", + file=sys.stderr) + + write_dimacs_graph(sys.stdout, edges) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())