alonso 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
alonso/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # Alonso: Approximate Vertex Cover Solver https://pypi.org/project/alonso
2
+ # Author: Frank Vega
3
+
4
+ __all__ = ["utils", "bipartite", "algorithm", "parser", "applogger", "test", "app", "batch"]
alonso/algorithm.py ADDED
@@ -0,0 +1,115 @@
1
+ # Created on 21/05/2025
2
+ # Author: Frank Vega
3
+
4
+ import itertools
5
+ from . import utils
6
+
7
+ import networkx as nx
8
+ from . import partition
9
+ from . import stable
10
+ from . import merge
11
+
12
+ def find_vertex_cover(graph):
13
+ """
14
+ Compute an approximate minimum vertex cover set for an undirected graph by transforming it into a chordal graph.
15
+
16
+ Args:
17
+ graph (nx.Graph): A NetworkX Graph object representing the input graph.
18
+
19
+ Returns:
20
+ set: A set of vertex indices representing the approximate minimum vertex cover set.
21
+ Returns an empty set if the graph is empty or has no edges.
22
+ """
23
+ # Validate that the input is a valid undirected NetworkX graph
24
+ if not isinstance(graph, nx.Graph):
25
+ raise ValueError("Input must be an undirected NetworkX Graph.")
26
+
27
+ # Handle trivial cases: return empty set for graphs with no nodes or no edges
28
+ if graph.number_of_nodes() == 0 or graph.number_of_edges() == 0:
29
+ return set() # No vertices or edges mean no cover is needed
30
+
31
+ # Create a working copy to avoid modifying the original graph
32
+ working_graph = graph.copy()
33
+
34
+ # Remove self-loops as they are irrelevant for vertex cover computation
35
+ working_graph.remove_edges_from(list(nx.selfloop_edges(working_graph)))
36
+
37
+ # Remove isolated nodes (degree 0) since they don't contribute to the vertex cover
38
+ working_graph.remove_nodes_from(list(nx.isolates(working_graph)))
39
+
40
+ # Return empty set if the cleaned graph has no nodes after removals
41
+ if working_graph.number_of_nodes() == 0:
42
+ return set()
43
+
44
+ # Partition edges into two subsets (E1, E2) using the Burr-Erdős-Lovász (1976) algorithm
45
+ # This step divides the graph into two claw-free subgraphs
46
+ # Complexity: O(m * (m * Δ * C + C^2)), where m is edges, Δ is maximum degree, C is number of claws
47
+ E1, E2 = partition.partition_edges_claw_free(working_graph)
48
+
49
+ # Compute minimum vertex cover for E1 using the Faenza, Oriolo & Stauffer (2011) algorithm
50
+ # This finds the maximum weighted stable set in the claw-free graph E1, whose complement is the vertex cover
51
+ # Complexity: O(n^3), where n is the number of nodes in the subgraph induced by E1
52
+ vertex_cover_1 = stable.minimum_vertex_cover_claw_free(E1)
53
+
54
+ # Compute minimum vertex cover for E2 using the same Faenza, Oriolo & Stauffer (2011) algorithm
55
+ # Complexity: O(n^3) for the subgraph induced by E2
56
+ vertex_cover_2 = stable.minimum_vertex_cover_claw_free(E2)
57
+
58
+ # Merge the two vertex covers from E1 and E2 to approximate the minimum vertex cover of the original graph
59
+ approximate_vertex_cover = merge.merge_vertex_covers(E1, E2, vertex_cover_1, vertex_cover_2)
60
+
61
+ # Create a residual graph containing edges not covered by the current vertex cover
62
+ residual_graph = nx.Graph()
63
+ for u, v in working_graph.edges():
64
+ if u not in approximate_vertex_cover and v not in approximate_vertex_cover:
65
+ residual_graph.add_edge(u, v) # Add edge if neither endpoint is in the cover
66
+
67
+ # Recursively find vertex cover for the residual graph to handle uncovered edges
68
+ residual_vertex_cover = find_vertex_cover(residual_graph)
69
+
70
+ # Combine the approximate vertex cover with the residual cover to ensure all edges are covered
71
+ return approximate_vertex_cover.union(residual_vertex_cover)
72
+
73
+ def find_vertex_cover_brute_force(graph):
74
+ """
75
+ Computes an exact minimum vertex cover in exponential time.
76
+
77
+ Args:
78
+ graph: A NetworkX Graph.
79
+
80
+ Returns:
81
+ A set of vertex indices representing the exact vertex cover, or None if the graph is empty.
82
+ """
83
+
84
+ if graph.number_of_nodes() == 0 or graph.number_of_edges() == 0:
85
+ return None
86
+
87
+ n_vertices = len(graph.nodes())
88
+
89
+ for k in range(1, n_vertices + 1): # Iterate through all possible sizes of the cover
90
+ for candidate in itertools.combinations(graph.nodes(), k):
91
+ cover_candidate = set(candidate)
92
+ if utils.is_vertex_cover(graph, cover_candidate):
93
+ return cover_candidate
94
+
95
+ return None
96
+
97
+
98
+
99
+ def find_vertex_cover_approximation(graph):
100
+ """
101
+ Computes an approximate vertex cover in polynomial time with an approximation ratio of at most 2 for undirected graphs.
102
+
103
+ Args:
104
+ graph: A NetworkX Graph.
105
+
106
+ Returns:
107
+ A set of vertex indices representing the approximate vertex cover, or None if the graph is empty.
108
+ """
109
+
110
+ if graph.number_of_nodes() == 0 or graph.number_of_edges() == 0:
111
+ return None
112
+
113
+ #networkx doesn't have a guaranteed minimum vertex cover function, so we use approximation
114
+ vertex_cover = nx.approximation.vertex_cover.min_weighted_vertex_cover(graph)
115
+ return vertex_cover
alonso/app.py ADDED
@@ -0,0 +1,98 @@
1
+ # Approximate Vertex Cover Solver
2
+ # Frank Vega
3
+ # May 21th, 2025
4
+
5
+ import argparse
6
+ import time
7
+
8
+ from . import algorithm
9
+ from . import parser
10
+ from . import applogger
11
+ from . import utils
12
+
13
+ def approximate_solution(inputFile, verbose=False, log=False, count=False, bruteForce=False, approximation=False):
14
+ """Finds an approximate vertex cover.
15
+
16
+ Args:
17
+ inputFile: Input file path.
18
+ verbose: Enable verbose output.
19
+ log: Enable file logging.
20
+ count: Measure the size of the vertex cover.
21
+ bruteForce: Enable brute force approach.
22
+ approximation: Enable an approximate approach within a ratio of at most 2.
23
+ """
24
+
25
+ logger = applogger.Logger(applogger.FileLogger() if (log) else applogger.ConsoleLogger(verbose))
26
+ # Read and parse a dimacs file
27
+ logger.info(f"Parsing the Input File started")
28
+ started = time.time()
29
+
30
+ graph = parser.read(inputFile)
31
+ filename = utils.get_file_name(inputFile)
32
+ logger.info(f"Parsing the Input File done in: {(time.time() - started) * 1000.0} milliseconds")
33
+
34
+ if approximation:
35
+ logger.info("An approximate Solution with an approximation ratio of at most 2 started")
36
+ started = time.time()
37
+
38
+ approximate_result = algorithm.find_vertex_cover_approximation(graph)
39
+
40
+ logger.info(f"An approximate Solution with an approximation ratio of at most 2 done in: {(time.time() - started) * 1000.0} milliseconds")
41
+
42
+ answer = utils.string_result_format(approximate_result, count)
43
+ output = f"{filename}: (approximation) {answer}"
44
+ utils.println(output, logger, log)
45
+
46
+ if bruteForce:
47
+ logger.info("A solution with an exponential-time complexity started")
48
+ started = time.time()
49
+
50
+ brute_force_result = algorithm.find_vertex_cover_brute_force(graph)
51
+
52
+ logger.info(f"A solution with an exponential-time complexity done in: {(time.time() - started) * 1000.0} milliseconds")
53
+
54
+ answer = utils.string_result_format(brute_force_result, count)
55
+ output = f"{filename}: (Brute Force) {answer}"
56
+ utils.println(output, logger, log)
57
+
58
+ logger.info("Our Algorithm with an approximate solution started")
59
+ started = time.time()
60
+
61
+ novel_result = algorithm.find_vertex_cover(graph)
62
+
63
+ logger.info(f"Our Algorithm with an approximate solution done in: {(time.time() - started) * 1000.0} milliseconds")
64
+
65
+ answer = utils.string_result_format(novel_result, count)
66
+ output = f"{filename}: {answer}"
67
+ utils.println(output, logger, log)
68
+ if novel_result and (bruteForce or approximation):
69
+ if bruteForce:
70
+ output = f"Exact Ratio (Alonso/Optimal): {len(novel_result)/len(brute_force_result)}"
71
+ elif approximation:
72
+ output = f"Upper Bound for Ratio (Alonso/Optimal): {2 * len(novel_result)/len(approximate_result)}"
73
+ utils.println(output, logger, log)
74
+
75
+ def main():
76
+
77
+ # Define the parameters
78
+ helper = argparse.ArgumentParser(prog="mvc", description='Compute an Approximate Vertex Cover for undirected graph encoded in DIMACS format.')
79
+ helper.add_argument('-i', '--inputFile', type=str, help='input file path', required=True)
80
+ helper.add_argument('-a', '--approximation', action='store_true', help='enable comparison with a polynomial-time approximation approach within a factor of at most 2')
81
+ helper.add_argument('-b', '--bruteForce', action='store_true', help='enable comparison with the exponential-time brute-force approach')
82
+ helper.add_argument('-c', '--count', action='store_true', help='calculate the size of the vertex cover')
83
+ helper.add_argument('-v', '--verbose', action='store_true', help='anable verbose output')
84
+ helper.add_argument('-l', '--log', action='store_true', help='enable file logging')
85
+ helper.add_argument('--version', action='version', version='%(prog)s 0.0.1')
86
+
87
+ # Initialize the parameters
88
+ args = helper.parse_args()
89
+ approximate_solution(args.inputFile,
90
+ verbose=args.verbose,
91
+ log=args.log,
92
+ count=args.count,
93
+ bruteForce=args.bruteForce,
94
+ approximation=args.approximation)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
alonso/applogger.py ADDED
@@ -0,0 +1,80 @@
1
+ import logging
2
+
3
+ class Logger:
4
+ """
5
+ A wrapper class for a logger object.
6
+ """
7
+
8
+ def __init__(self, logger):
9
+ """
10
+ Initializes the SatLogger with a logger object.
11
+
12
+ Args:
13
+ logger (logging.Logger): The underlying logger object to be used.
14
+ """
15
+ self.logger = logger
16
+
17
+ def info(self, msg, *args, **kwargs):
18
+ """
19
+ Logs an informational message using the underlying logger.
20
+
21
+ Args:
22
+ msg (str): The message to be logged.
23
+ *args: Additional arguments to be passed to the underlying logger's info method.
24
+ **kwargs: Additional keyword arguments to be passed to the underlying logger's info method.
25
+ """
26
+ self.logger.info(msg, *args, **kwargs)
27
+
28
+ class ConsoleLogger:
29
+ """
30
+ A simple logger class that logs to the console when enabled.
31
+ """
32
+
33
+ def __init__(self, log_enabled=True):
34
+ """
35
+ Initializes the ConsoleLogger with an optional flag for enabling logging.
36
+
37
+ Args:
38
+ log_enabled (bool, optional): Flag to enable or disable console logging. Defaults to True.
39
+ """
40
+ self.log_enabled = log_enabled
41
+
42
+ def info(self, msg, *args, **kwargs):
43
+ """
44
+ Logs a message to the console if enabled.
45
+
46
+ Args:
47
+ msg (str): The message to be logged.
48
+ *args: Additional arguments to be formatted with the message.
49
+ **kwargs: Additional keyword arguments (ignored for console logging).
50
+ """
51
+
52
+ if self.log_enabled:
53
+ print(msg.format(*args)) # Use f-strings or format method for cleaner formatting
54
+
55
+ class FileLogger:
56
+ """
57
+ A simple logger class that logs to a file.
58
+ """
59
+
60
+ def __init__(self, log_file="app.log", log_level=logging.INFO):
61
+ """
62
+ Initializes the FileLogger with an optional log file name and log level.
63
+
64
+ Args:
65
+ log_file (str, optional): The filename of the log file. Defaults to "app.log".
66
+ log_level (int, optional): The logging level. Defaults to logging.INFO.
67
+ """
68
+ logging.basicConfig(filename=log_file, level=log_level, format='%(asctime)s - %(levelname)s - %(message)s')
69
+ self.logger = logging.getLogger(__name__)
70
+
71
+ def info(self, msg, *args, **kwargs):
72
+ """
73
+ Logs an informational message to the file.
74
+
75
+ Args:
76
+ msg (str): The message to be logged.
77
+ *args: Additional arguments to be formatted with the message.
78
+ **kwargs: Additional keyword arguments (ignored for file logging).
79
+ """
80
+ self.logger.info(msg.format(*args))
alonso/batch.py ADDED
@@ -0,0 +1,53 @@
1
+ # Created on 21/05/2025
2
+ # Author: Frank Vega
3
+
4
+ import argparse
5
+ from . import utils
6
+ from . import app
7
+
8
+ def approximate_solutions(inputDirectory, verbose=False, log=False, count=False, bruteForce=False, approximation=False):
9
+ """Finds an approximate vertex cover for several instances.
10
+
11
+ Args:
12
+ inputDirectory: Input directory path.
13
+ verbose: Enable verbose output.
14
+ log: Enable file logging.
15
+ count: Measure the size of the vertex cover.
16
+ bruteForce: Enable brute force approach.
17
+ approximation: Enable an approximate approach within a ratio of at most 2.
18
+ """
19
+
20
+ file_names = utils.get_file_names(inputDirectory)
21
+
22
+ if file_names:
23
+ for file_name in file_names:
24
+ inputFile = f"{inputDirectory}/{file_name}"
25
+ print(f"Test: {inputDirectory}/{file_name}")
26
+ app.approximate_solution(inputFile, verbose, log, count, bruteForce, approximation)
27
+
28
+
29
+ def main():
30
+
31
+ # Define the parameters
32
+ helper = argparse.ArgumentParser(prog="batch_mvc", description="Compute an Approximate Vertex Cover for all undirected graphs encoded in DIMACS format and stored in a directory.")
33
+ helper.add_argument('-i', '--inputDirectory', type=str, help='Input directory path', required=True)
34
+ helper.add_argument('-a', '--approximation', action='store_true', help='enable comparison with a polynomial-time approximation approach within a factor of at most 2')
35
+ helper.add_argument('-b', '--bruteForce', action='store_true', help='enable comparison with the exponential-time brute-force approach')
36
+ helper.add_argument('-c', '--count', action='store_true', help='calculate the size of the vertex cover')
37
+ helper.add_argument('-v', '--verbose', action='store_true', help='anable verbose output')
38
+ helper.add_argument('-l', '--log', action='store_true', help='enable file logging')
39
+ helper.add_argument('--version', action='version', version='%(prog)s 0.0.1')
40
+
41
+
42
+ # Initialize the parameters
43
+ args = helper.parse_args()
44
+ approximate_solutions(args.inputDirectory,
45
+ verbose=args.verbose,
46
+ log=args.log,
47
+ count=args.count,
48
+ bruteForce=args.bruteForce,
49
+ approximation=args.approximation)
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
alonso/merge.py ADDED
@@ -0,0 +1,125 @@
1
+ def merge_vertex_covers(E1, E2, vertex_cover_1, vertex_cover_2):
2
+ """
3
+ Merge two vertex covers from edge partitions to get minimum vertex cover of G.
4
+
5
+ Args:
6
+ G: Graph represented as adjacency list/set of edges
7
+ E1, E2: Two partitions of edges E
8
+ vertex_cover_1: Vertex cover for subgraph induced by E1
9
+ vertex_cover_2: Vertex cover for subgraph induced by E2
10
+
11
+ Returns:
12
+ Merged vertex cover for the entire graph G
13
+ """
14
+ # All edges in the graph
15
+ all_edges = E1.union(E2)
16
+
17
+ # Initialize merge process
18
+ merged_cover = set()
19
+ covered_edges = set()
20
+
21
+ # Convert vertex covers to lists for merge-sort-like processing
22
+ candidates_1 = list(vertex_cover_1)
23
+ candidates_2 = list(vertex_cover_2)
24
+
25
+ i, j = 0, 0
26
+
27
+ # Merge process similar to merge sort
28
+ while i < len(candidates_1) or j < len(candidates_2):
29
+ # Calculate uncovered edges that each candidate can cover
30
+ uncovered_edges = all_edges - covered_edges
31
+
32
+ if not uncovered_edges:
33
+ break
34
+
35
+ # Get coverage count for remaining candidates
36
+ coverage_1 = 0
37
+ coverage_2 = 0
38
+
39
+ if i < len(candidates_1):
40
+ v1 = candidates_1[i]
41
+ coverage_1 = count_edges_covered_by_vertex(v1, uncovered_edges)
42
+
43
+ if j < len(candidates_2):
44
+ v2 = candidates_2[j]
45
+ coverage_2 = count_edges_covered_by_vertex(v2, uncovered_edges)
46
+
47
+ # Choose vertex that covers more uncovered edges (merge-sort comparison)
48
+ if i >= len(candidates_1):
49
+ # Only candidates from cover_2 remain
50
+ chosen_vertex = candidates_2[j]
51
+ j += 1
52
+ elif j >= len(candidates_2):
53
+ # Only candidates from cover_1 remain
54
+ chosen_vertex = candidates_1[i]
55
+ i += 1
56
+ elif coverage_1 >= coverage_2:
57
+ # Vertex from cover_1 covers more (or equal) uncovered edges
58
+ chosen_vertex = candidates_1[i]
59
+ i += 1
60
+ # Skip if same vertex exists in both covers
61
+ if j < len(candidates_2) and candidates_2[j] == chosen_vertex:
62
+ j += 1
63
+ else:
64
+ # Vertex from cover_2 covers more uncovered edges
65
+ chosen_vertex = candidates_2[j]
66
+ j += 1
67
+ # Skip if same vertex exists in both covers
68
+ if i < len(candidates_1) and candidates_1[i] == chosen_vertex:
69
+ i += 1
70
+
71
+ # Add chosen vertex to merged cover
72
+ if chosen_vertex not in merged_cover:
73
+ merged_cover.add(chosen_vertex)
74
+ # Update covered edges
75
+ newly_covered = get_edges_covered_by_vertex(chosen_vertex, uncovered_edges)
76
+ covered_edges.update(newly_covered)
77
+
78
+ return merged_cover
79
+
80
+
81
+ def count_edges_covered_by_vertex(vertex, edges):
82
+ """Count how many edges from the given set are covered by the vertex."""
83
+ count = 0
84
+ for edge in edges:
85
+ if vertex in edge:
86
+ count += 1
87
+ return count
88
+
89
+
90
+ def get_edges_covered_by_vertex(vertex, edges):
91
+ """Get all edges from the given set that are covered by the vertex."""
92
+ covered = set()
93
+ for edge in edges:
94
+ if vertex in edge:
95
+ covered.add(edge)
96
+ return covered
97
+
98
+
99
+ def find_vertex_cover_subgraph(edges):
100
+ """
101
+ Find a vertex cover for the subgraph induced by given edges.
102
+ This is a simplified greedy approach for demonstration.
103
+ """
104
+ cover = set()
105
+ uncovered_edges = set(edges)
106
+
107
+ while uncovered_edges:
108
+ # Find vertex that covers most uncovered edges
109
+ vertex_count = {}
110
+ for edge in uncovered_edges:
111
+ for vertex in edge:
112
+ vertex_count[vertex] = vertex_count.get(vertex, 0) + 1
113
+
114
+ # Choose vertex with maximum coverage
115
+ best_vertex = max(vertex_count.keys(), key=lambda v: vertex_count[v])
116
+ cover.add(best_vertex)
117
+
118
+ # Remove covered edges
119
+ to_remove = set()
120
+ for edge in uncovered_edges:
121
+ if best_vertex in edge:
122
+ to_remove.add(edge)
123
+ uncovered_edges -= to_remove
124
+
125
+ return cover
alonso/parser.py ADDED
@@ -0,0 +1,79 @@
1
+ import lzma
2
+ import bz2
3
+ import numpy as np
4
+ import scipy.sparse as sparse
5
+ import networkx as nx
6
+
7
+ from . import utils
8
+
9
+ def create_sparse_matrix_from_file(file):
10
+ """Creates a sparse matrix from a file containing a DIMACS format representation.
11
+
12
+ Args:
13
+ file: A file-like object (e.g., an opened file) containing the matrix data.
14
+
15
+ Returns:
16
+ A NetworkX Graph.
17
+
18
+ Raises:
19
+ ValueError: If the input matrix is not the correct DIMACS format.
20
+ """
21
+ graph = nx.Graph()
22
+ for i, line in enumerate(file):
23
+ line = line.strip() # Remove newline characters
24
+ if not line.startswith('c') and not line.startswith('p'):
25
+ edge = [np.int32(node) for node in line.split(' ') if node != 'e']
26
+ if len(edge) != 2 or min(edge[0], edge[1]) <= 0:
27
+ raise ValueError(f"The input file is not in the correct DIMACS format at line {i}")
28
+ elif graph.has_edge(edge[0] - 1, edge[1] - 1):
29
+ raise ValueError(f"The input file contains a repeated edge at line {i}")
30
+ else:
31
+ graph.add_edge(edge[0] - 1, edge[1] - 1)
32
+
33
+ return graph
34
+
35
+ def save_sparse_matrix_to_file(matrix, filename):
36
+ """
37
+ Writes a SciPy sparse matrix to a DIMACS format.
38
+
39
+ Args:
40
+ matrix: The SciPy sparse matrix.
41
+ filename: The name of the output text file.
42
+ """
43
+ rows, cols = matrix.nonzero()
44
+
45
+ with open(filename, 'w') as f:
46
+ f.write(f"p edge {matrix.shape[0]} {matrix.nnz // 2 - matrix.shape[0]//2}" + "\n")
47
+ for i, j in zip(rows, cols):
48
+ if i < j:
49
+ f.write(f"e {i + 1} {j + 1}" + "\n")
50
+
51
+
52
+ def read(filepath):
53
+ """Reads a file and returns its lines in an array format.
54
+
55
+ Args:
56
+ filepath: The path to the file.
57
+
58
+ Returns:
59
+ A NetworkX Graph.
60
+
61
+ Raises:
62
+ FileNotFoundError: If the file is not found.
63
+ """
64
+
65
+ try:
66
+ extension = utils.get_extension_without_dot(filepath)
67
+ if extension == 'xz' or extension == 'lzma':
68
+ with lzma.open(filepath, 'rt') as file:
69
+ matrix = create_sparse_matrix_from_file(file)
70
+ elif extension == 'bz2' or extension == 'bzip2':
71
+ with bz2.open(filepath, 'rt') as file:
72
+ matrix = create_sparse_matrix_from_file(file)
73
+ else:
74
+ with open(filepath, 'r') as file:
75
+ matrix = create_sparse_matrix_from_file(file)
76
+
77
+ return matrix
78
+ except FileNotFoundError:
79
+ raise FileNotFoundError(f"File not found: {filepath}")