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/partition.py ADDED
@@ -0,0 +1,216 @@
1
+ import networkx as nx
2
+ from typing import Tuple, Set, List
3
+ from collections import defaultdict
4
+ import itertools
5
+ import mendive.algorithm as claws
6
+
7
+ class BurrErdosLovaszPartitioner:
8
+ """
9
+ Implementation of the Burr-Erdős-Lovász (1976) algorithm for partitioning
10
+ edges of a graph into two subsets such that each subset induces a k-star-free subgraph.
11
+
12
+ For k=3 (claw-free case), we partition edges to avoid 3-stars in each partition.
13
+
14
+ The algorithm works by maintaining two edge sets and for each new edge,
15
+ assigning it to the partition where it creates the fewest violations,
16
+ then locally repairing any violations that arise.
17
+ """
18
+
19
+ def __init__(self, graph: nx.Graph, k: int = 3):
20
+ self.graph = graph.copy()
21
+ self.k = k # k=3 for claw-free (3-star-free)
22
+ self.n = len(graph.nodes())
23
+
24
+ def partition_edges(self) -> Tuple[Set[Tuple[int, int]], Set[Tuple[int, int]]]:
25
+ """
26
+ Partition edges into two k-star-free subgraphs using BEL algorithm.
27
+ """
28
+ E1 = set()
29
+ E2 = set()
30
+
31
+ # Convert edges to consistent format
32
+ edges = [(min(u, v), max(u, v)) for u, v in self.graph.edges()]
33
+
34
+ # Main algorithm: assign each edge to minimize violations
35
+ for edge in edges:
36
+ # Count violations if we add edge to E1
37
+ violations_E1 = self._count_violations_after_adding(E1, edge)
38
+ violations_E2 = self._count_violations_after_adding(E2, edge)
39
+
40
+ # Add to partition with fewer violations
41
+ if violations_E1 <= violations_E2:
42
+ E1.add(edge)
43
+ # Repair violations in E1
44
+ E1 = self._repair_violations(E1)
45
+ else:
46
+ E2.add(edge)
47
+ # Repair violations in E2
48
+ E2 = self._repair_violations(E2)
49
+
50
+ return E1, E2
51
+
52
+ def _count_violations_after_adding(self, edge_set: Set[Tuple[int, int]],
53
+ new_edge: Tuple[int, int]) -> int:
54
+ """Count number of k-stars that would be created by adding new_edge."""
55
+ temp_set = edge_set | {new_edge}
56
+ return self._count_k_stars(temp_set)
57
+
58
+ def _is_claw_free(self, edge_set: Set[Tuple[int, int]]) -> bool:
59
+ """Count number of k-stars in the edge set."""
60
+ if not edge_set:
61
+ return True
62
+
63
+ # Build graph from edge set
64
+ G = nx.Graph()
65
+ G.add_edges_from(edge_set)
66
+
67
+ claw = claws.find_claw_coordinates(G, first_claw=True)
68
+ if claw is None:
69
+ return True
70
+ else:
71
+ return False
72
+
73
+
74
+ def _count_k_stars(self, edge_set: Set[Tuple[int, int]]) -> int:
75
+ """Count number of k-stars in the edge set."""
76
+ if not edge_set:
77
+ return 0
78
+
79
+ # Build graph from edge set
80
+ G = nx.Graph()
81
+ G.add_edges_from(edge_set)
82
+
83
+ k_star_count = 0
84
+
85
+ if self.k == 3:
86
+ all_claws = claws.find_claw_coordinates(G, first_claw=False)
87
+ if all_claws is not None:
88
+ k_star_count = len(all_claws)
89
+ else:
90
+ # Check each vertex as potential center of k-star
91
+ for center in G.nodes():
92
+ neighbors = list(G.neighbors(center))
93
+ if len(neighbors) >= self.k:
94
+ # Count k-subsets of neighbors that form independent sets
95
+ for k_subset in itertools.combinations(neighbors, self.k):
96
+ # Check if this k-subset is independent
97
+ is_independent = True
98
+ for i in range(self.k):
99
+ for j in range(i + 1, self.k):
100
+ if G.has_edge(k_subset[i], k_subset[j]):
101
+ is_independent = False
102
+ break
103
+ if not is_independent:
104
+ break
105
+
106
+ if is_independent:
107
+ k_star_count += 1
108
+
109
+ return k_star_count
110
+
111
+ def _repair_violations(self, edge_set: Set[Tuple[int, int]]) -> Set[Tuple[int, int]]:
112
+ """
113
+ Repair k-star violations in edge_set by removing minimal edges.
114
+ This is the key part of the BEL algorithm.
115
+ """
116
+ if not edge_set:
117
+ return edge_set
118
+
119
+ current_set = edge_set.copy()
120
+
121
+ while True:
122
+ if self.k == 3:
123
+ is_claw_free = self._is_claw_free(current_set)
124
+ if is_claw_free:
125
+ break # No more violations
126
+ violations = self._find_k_stars(current_set)
127
+ else:
128
+ violations = self._find_k_stars(current_set)
129
+ if not violations:
130
+ break # No more violations
131
+
132
+ # Find the edge that appears in most violations
133
+ edge_violation_count = defaultdict(int)
134
+ for k_star in violations:
135
+ center, leaves = k_star
136
+ # Each k-star consists of k edges from center to leaves
137
+ for leaf in leaves:
138
+ edge = (min(center, leaf), max(center, leaf))
139
+ if edge in current_set:
140
+ edge_violation_count[edge] += 1
141
+
142
+ if not edge_violation_count:
143
+ break
144
+
145
+ # Remove the edge that appears in most violations
146
+ most_violating_edge = max(edge_violation_count.keys(),
147
+ key=lambda e: edge_violation_count[e])
148
+ current_set.remove(most_violating_edge)
149
+
150
+ return current_set
151
+
152
+ def _find_k_stars(self, edge_set: Set[Tuple[int, int]]) -> List[Tuple[int, Tuple]]:
153
+ """Find all k-stars in the edge set. Returns list of (center, leaves) tuples."""
154
+ if not edge_set:
155
+ return []
156
+
157
+ # Build graph from edge set
158
+ G = nx.Graph()
159
+ G.add_edges_from(edge_set)
160
+
161
+ k_stars = []
162
+
163
+ if self.k == 3:
164
+ all_claws = claws.find_claw_coordinates(G, first_claw=False)
165
+ if all_claws is not None:
166
+ for subset in all_claws:
167
+ subgraph = G.subgraph(subset)
168
+ sorted_nodes = sorted(list(subset), key=lambda x: subgraph.degree(x))
169
+ center = sorted_nodes.pop()
170
+ k_subset = tuple(sorted_nodes)
171
+ k_stars.append((center, k_subset))
172
+ else:
173
+ # Check each vertex as potential center of k-star
174
+ for center in G.nodes():
175
+ neighbors = list(G.neighbors(center))
176
+ if len(neighbors) >= self.k:
177
+ # Find all k-subsets of neighbors that form independent sets
178
+ for k_subset in itertools.combinations(neighbors, self.k):
179
+ # Check if this k-subset is independent
180
+ is_independent = True
181
+ for i in range(self.k):
182
+ for j in range(i + 1, self.k):
183
+ if G.has_edge(k_subset[i], k_subset[j]):
184
+ is_independent = False
185
+ break
186
+ if not is_independent:
187
+ break
188
+
189
+ if is_independent:
190
+ k_stars.append((center, k_subset))
191
+
192
+ return k_stars
193
+
194
+ def verify_partition(self, E1: Set[Tuple[int, int]], E2: Set[Tuple[int, int]]) -> Tuple[bool, bool]:
195
+ """Verify that both partitions are k-star-free."""
196
+ if self.k == 3:
197
+ return (self._is_claw_free(E1), self._is_claw_free(E2))
198
+ else:
199
+ return (self._count_k_stars(E1) == 0, self._count_k_stars(E2) == 0)
200
+
201
+
202
+ def partition_edges_claw_free(G: nx.Graph) -> Tuple[Set[Tuple[int, int]], Set[Tuple[int, int]]]:
203
+ """
204
+ Partition edges of graph G into two sets such that each induces a claw-free subgraph.
205
+
206
+ Implementation of Burr, Erdős, Lovász (1976) algorithm for k=3 (claw-free case).
207
+
208
+ Args:
209
+ G: Undirected NetworkX graph
210
+
211
+ Returns:
212
+ (E1, E2): Two edge sets that induce claw-free subgraphs
213
+ """
214
+ partitioner = BurrErdosLovaszPartitioner(G, k=3)
215
+ return partitioner.partition_edges()
216
+
alonso/stable.py ADDED
@@ -0,0 +1,186 @@
1
+ from collections import defaultdict
2
+ import networkx as nx
3
+
4
+ class FaenzaOrioloStaufferAlgorithm:
5
+ """
6
+ Implementation of the Faenza, Oriolo & Stauffer (2011) algorithm
7
+ for finding maximum weighted stable set in claw-free graphs.
8
+ """
9
+
10
+ def __init__(self, edges, weights=None):
11
+ """
12
+ Initialize the algorithm with graph edges and optional vertex weights.
13
+
14
+ Args:
15
+ edges: List of tuples representing edges
16
+ weights: Dictionary mapping vertices to weights (default: all weights = 1)
17
+ """
18
+ self.edges = [(u, v) for u, v in edges]
19
+ self.vertices = set()
20
+ for u, v in self.edges:
21
+ self.vertices.add(u)
22
+ self.vertices.add(v)
23
+ self.graph = nx.Graph()
24
+ self.graph.add_edges_from(self.edges)
25
+ self.n = len(self.vertices)
26
+ if weights is None:
27
+ self.weights = {v: 1 for v in self.vertices}
28
+ else:
29
+ self.weights = {k: v for k, v in weights.items()}
30
+
31
+ # Build adjacency list
32
+ self.adj = defaultdict(set)
33
+ for u, v in self.edges:
34
+ self.adj[u].add(v)
35
+ self.adj[v].add(u)
36
+
37
+ def find_stable_set_decomposition(self):
38
+ """
39
+ Find a stable set decomposition of the graph.
40
+ This is a key step in the FOS algorithm.
41
+ """
42
+ # For simplicity, we'll use a greedy approach to find stable sets
43
+ # In the full algorithm, this would use more sophisticated decomposition
44
+ remaining_vertices = set(self.vertices)
45
+ stable_sets = []
46
+
47
+ while remaining_vertices:
48
+ # Greedy maximal stable set
49
+ current_stable = set()
50
+ vertices_to_check = list(remaining_vertices)
51
+
52
+ for v in vertices_to_check:
53
+ if v in remaining_vertices:
54
+ # Check if v can be added to current stable set
55
+ can_add = True
56
+ for u in current_stable:
57
+ if u in self.adj[v]:
58
+ can_add = False
59
+ break
60
+
61
+ if can_add:
62
+ current_stable.add(v)
63
+ remaining_vertices.remove(v)
64
+
65
+ if current_stable:
66
+ stable_sets.append(current_stable)
67
+
68
+ return stable_sets
69
+
70
+ def solve_weighted_stable_set_on_clique(self, clique_vertices):
71
+ """
72
+ Solve maximum weighted stable set on a clique (trivial: pick heaviest vertex).
73
+ """
74
+ if not clique_vertices:
75
+ return set(), 0
76
+
77
+ best_vertex = max(clique_vertices, key=lambda v: self.weights[v])
78
+ return {best_vertex}, self.weights[best_vertex]
79
+
80
+ def is_clique(self, vertices):
81
+ """Check if given vertices form a clique."""
82
+ G = self.graph.subgraph(vertices)
83
+ n = G.number_of_nodes()
84
+ if n < 2:
85
+ return True
86
+ e = G.number_of_edges()
87
+ max_edges = (n * (n - 1)) / 2
88
+ return e == max_edges
89
+
90
+ def find_maximum_weighted_stable_set(self):
91
+ """
92
+ Main algorithm to find maximum weighted stable set.
93
+ This implements the core FOS algorithm structure.
94
+ """
95
+ # Base cases
96
+ if not self.vertices:
97
+ return set(), 0
98
+
99
+ if len(self.vertices) == 1:
100
+ v = next(iter(self.vertices))
101
+ return {v}, self.weights[v]
102
+
103
+ # Check if graph is a clique
104
+ if self.is_clique(self.vertices):
105
+ return self.solve_weighted_stable_set_on_clique(self.vertices)
106
+
107
+ # For claw-free graphs, we use dynamic programming approach
108
+ # This is a simplified version of the full FOS algorithm
109
+ return self._dp_solve()
110
+
111
+ def _dp_solve(self):
112
+ """
113
+ Dynamic programming solution for claw-free graphs.
114
+ This implements the polynomial-time algorithm structure from FOS.
115
+ """
116
+ # Convert to networkx for easier manipulation
117
+ G = nx.Graph()
118
+ G.add_nodes_from(self.vertices)
119
+ G.add_edges_from(self.edges)
120
+
121
+ # Use complement graph approach for stable set = clique in complement
122
+ G_complement = nx.complement(G)
123
+
124
+ # Find all maximal cliques in complement (these are maximal stable sets in original)
125
+ maximal_stable_sets = list(nx.find_cliques(G_complement))
126
+
127
+ # Find the one with maximum weight
128
+ best_stable_set = set()
129
+ best_weight = 0
130
+
131
+ for stable_set in maximal_stable_sets:
132
+ weight = sum(self.weights[v] for v in stable_set)
133
+ if weight > best_weight:
134
+ best_weight = weight
135
+ best_stable_set = set(stable_set)
136
+
137
+ # Also check individual vertices
138
+ for v in self.vertices:
139
+ if self.weights[v] > best_weight:
140
+ best_weight = self.weights[v]
141
+ best_stable_set = {v}
142
+
143
+ return best_stable_set, best_weight
144
+
145
+ def verify_stable_set(self, stable_set):
146
+ """Verify that the given set is indeed a stable set."""
147
+ stable_list = list(stable_set)
148
+ for i in range(len(stable_list)):
149
+ for j in range(i + 1, len(stable_list)):
150
+ if stable_list[j] in self.adj[stable_list[i]]:
151
+ return False, f"Vertices {stable_list[i]} and {stable_list[j]} are adjacent"
152
+ return True, "Valid stable set"
153
+
154
+
155
+ def solve_maximum_weighted_stable_set(edges, weights=None):
156
+ """
157
+ Solve maximum weighted stable set problem using FOS algorithm.
158
+
159
+ Args:
160
+ edges: List of edges as tuples
161
+ weights: Dictionary of vertex weights (optional, defaults to 1 for all)
162
+
163
+ Returns:
164
+ tuple: (stable_set, weight)
165
+ """
166
+ algorithm = FaenzaOrioloStaufferAlgorithm(edges, weights)
167
+ return algorithm.find_maximum_weighted_stable_set()
168
+
169
+ def minimum_vertex_cover_claw_free(edges, weights=None):
170
+ """
171
+ Solve minimum vertex cover problem using FOS algorithm.
172
+
173
+ Args:
174
+ edges: List of edges as tuples
175
+ weights: Dictionary of vertex weights (optional, defaults to 1 for all)
176
+
177
+ Returns:
178
+ - A minimum vertex cover
179
+ """
180
+ algorithm = FaenzaOrioloStaufferAlgorithm(edges, weights)
181
+ stable_set, _ = algorithm.find_maximum_weighted_stable_set()
182
+ G = nx.Graph()
183
+ G.add_edges_from(edges)
184
+ minimum_vertex_cover = set(G.nodes) - stable_set
185
+ return minimum_vertex_cover
186
+
alonso/test.py ADDED
@@ -0,0 +1,118 @@
1
+ # Created on 21/05/2025
2
+ # Author: Frank Vega
3
+
4
+ import time
5
+ import argparse
6
+ import math
7
+ import networkx as nx
8
+
9
+ from . import algorithm
10
+ from . import applogger
11
+ from . import parser
12
+ from . import utils
13
+
14
+ def restricted_float(x):
15
+ try:
16
+ x = float(x)
17
+ except ValueError:
18
+ raise argparse.ArgumentTypeError("%r not a floating-point literal" % (x,))
19
+
20
+ if x < 0.0 or x > 1.0:
21
+ raise argparse.ArgumentTypeError("%r not in range [0.0, 1.0]"%(x,))
22
+ return x
23
+
24
+ def main():
25
+
26
+ # Define the parameters
27
+ helper = argparse.ArgumentParser(prog="test_mvc", description="The Alonso Testing Application using randomly generated, large sparse matrices.")
28
+ helper.add_argument('-d', '--dimension', type=int, help="an integer specifying the dimensions of the square matrices", required=True)
29
+ helper.add_argument('-n', '--num_tests', type=int, default=5, help="an integer specifying the number of tests to run")
30
+ helper.add_argument('-s', '--sparsity', type=restricted_float, default=0.95, help="sparsity of the matrices (0.0 for dense, close to 1.0 for very sparse)")
31
+ helper.add_argument('-a', '--approximation', action='store_true', help='enable comparison with a polynomial-time approximation approach within a factor of at most 2')
32
+ helper.add_argument('-b', '--bruteForce', action='store_true', help='enable comparison with the exponential-time brute-force approach')
33
+ helper.add_argument('-c', '--count', action='store_true', help='calculate the size of the vertex cover')
34
+ helper.add_argument('-w', '--write', action='store_true', help='write the generated random matrix to a file in the current directory')
35
+ helper.add_argument('-v', '--verbose', action='store_true', help='anable verbose output')
36
+ helper.add_argument('-l', '--log', action='store_true', help='enable file logging')
37
+ helper.add_argument('--version', action='version', version='%(prog)s 0.0.1')
38
+
39
+ # Initialize the parameters
40
+ args = helper.parse_args()
41
+ num_tests = args.num_tests
42
+ matrix_shape = (args.dimension, args.dimension)
43
+ sparsity = args.sparsity
44
+ logger = applogger.Logger(applogger.FileLogger() if (args.log) else applogger.ConsoleLogger(args.verbose))
45
+ hash_string = utils.generate_short_hash(6 + math.ceil(math.log2(num_tests))) if args.write else None
46
+ count = args.count
47
+ bruteForce = args.bruteForce
48
+ approximation = args.approximation
49
+ # Perform the tests
50
+ for i in range(num_tests):
51
+
52
+ logger.info(f"Creating Matrix {i + 1}")
53
+
54
+ sparse_matrix = utils.random_matrix_tests(matrix_shape, sparsity)
55
+
56
+ if sparse_matrix is None:
57
+ continue
58
+
59
+ graph = utils.sparse_matrix_to_graph(sparse_matrix)
60
+ logger.info(f"Matrix shape: {sparse_matrix.shape}")
61
+ logger.info(f"Number of non-zero elements: {sparse_matrix.nnz}")
62
+ logger.info(f"Sparsity: {1 - (sparse_matrix.nnz / (sparse_matrix.shape[0] * sparse_matrix.shape[1]))}")
63
+
64
+ if approximation:
65
+ logger.info("An approximate Solution with an approximation ratio of at most 2 started")
66
+ started = time.time()
67
+
68
+ approximate_result = algorithm.find_vertex_cover_approximation(graph)
69
+
70
+ logger.info(f"An approximate Solution with an approximation ratio of at most 2 done in: {(time.time() - started) * 1000.0} milliseconds")
71
+
72
+ answer = utils.string_result_format(approximate_result, count)
73
+ output = f"{i + 1}-approximation Test: {answer}"
74
+ utils.println(output, logger, args.log)
75
+
76
+ if bruteForce:
77
+ logger.info("A solution with an exponential-time complexity started")
78
+ started = time.time()
79
+
80
+ brute_force_result = algorithm.find_vertex_cover_brute_force(graph)
81
+
82
+ logger.info(f"A solution with an exponential-time complexity done in: {(time.time() - started) * 1000.0} milliseconds")
83
+
84
+ answer = utils.string_result_format(brute_force_result, count)
85
+ output = f"{i + 1}-Brute Force Test: {answer}"
86
+ utils.println(output, logger, args.log)
87
+
88
+
89
+ logger.info("Our Algorithm with an approximate solution started")
90
+ started = time.time()
91
+
92
+ novel_result = algorithm.find_vertex_cover(graph)
93
+
94
+ logger.info(f"Our Algorithm with an approximate solution done in: {(time.time() - started) * 1000.0} milliseconds")
95
+
96
+ answer = utils.string_result_format(novel_result, count)
97
+ output = f"{i + 1}-Alonso Test: {answer}"
98
+ utils.println(output, logger, args.log)
99
+
100
+ if novel_result and (bruteForce or approximation):
101
+ if bruteForce:
102
+ output = f"Exact Ratio (Alonso/Optimal): {len(novel_result)/len(brute_force_result)}"
103
+ elif approximation:
104
+ output = f"Upper Bound for Ratio (Alonso/Optimal): {2 * len(novel_result)/len(approximate_result)}"
105
+ utils.println(output, logger, args.log)
106
+
107
+
108
+ if args.write:
109
+ output = f"Saving Matrix Test {i + 1}"
110
+ utils.println(output, logger, args.log)
111
+
112
+ filename = f"sparse_matrix_{i + 1}_{hash_string}"
113
+ parser.save_sparse_matrix_to_file(sparse_matrix, filename)
114
+ output = f"Matrix Test {i + 1} written to file {filename}."
115
+ utils.println(output, logger, args.log)
116
+
117
+ if __name__ == "__main__":
118
+ main()