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 +4 -0
- alonso/algorithm.py +115 -0
- alonso/app.py +98 -0
- alonso/applogger.py +80 -0
- alonso/batch.py +53 -0
- alonso/merge.py +125 -0
- alonso/parser.py +79 -0
- alonso/partition.py +216 -0
- alonso/stable.py +186 -0
- alonso/test.py +118 -0
- alonso/utils.py +257 -0
- alonso-0.0.1.dist-info/METADATA +267 -0
- alonso-0.0.1.dist-info/RECORD +17 -0
- alonso-0.0.1.dist-info/WHEEL +5 -0
- alonso-0.0.1.dist-info/entry_points.txt +4 -0
- alonso-0.0.1.dist-info/licenses/LICENSE +21 -0
- alonso-0.0.1.dist-info/top_level.txt +1 -0
alonso/__init__.py
ADDED
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}")
|