Coverage for /opt/conda/lib/python3.14/site-packages/medil/_ecc_algorithms.py: 86%
116 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 02:29 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 02:29 +0000
1"""Implementations of edge clique cover finding algorithms."""
3from collections import deque
5import numpy as np
7from ._graph import UndirectedDependenceGraph
10def _find_clique_min_cover(graph, verbose=False):
11 """Returns the clique-minimum edge clique cover.
13 Parameters
14 ----------
15 graph : np.array
16 Adjacency matrix for undirected graph.
18 verbose : bool, optional
19 Wether or not to print verbose output.
21 Returns
22 -------
23 the_cover: np.array
24 Biadjacency matrix representing edge clique cover.
26 See Also
27 --------
28 _graph.UndirectedDependenceGraph : Defines auxilliary data structure
29 and reduction rules used by this
30 algorithm.
32 Notes
33 -----
34 This is an implementation of the algorithm described in
35 :cite:`Gramm_2009`.
37 """
38 graph = UndirectedDependenceGraph(graph, verbose)
39 try:
40 graph.make_aux()
41 except ValueError:
42 print("The input graph doesn't appear to have any edges!")
43 return graph.adj_matrix
45 num_cliques = 1
46 the_cover = None
47 if verbose:
48 # find bound for cliques in solution
49 max_intersect_num = graph.num_vertices**2 // 4
50 if max_intersect_num < graph.num_edges:
51 p = graph.n_choose_2(graph.num_vertices) - graph.num_edges
52 t = int(np.sqrt(p))
53 max_intersect_num = p + t if p > 0 else 1
54 print("solution has at most {} cliques.".format(max_intersect_num))
55 while the_cover is None:
56 if verbose:
57 print(
58 "\ntesting for solutions with {}/{} cliques".format(
59 num_cliques, max_intersect_num
60 )
61 )
62 the_cover = _branch(graph, num_cliques, the_cover, iteration=0, iteration_max=3)
63 num_cliques += 1
65 return _add_isolated_verts(the_cover)
68def _branch(graph, k_num_cliques, the_cover, iteration, iteration_max):
69 """Helper function for `find_clique_min_cover()`.
71 Describing the solution search space as a tree.
72 This function tests whether the given node is a solution, and it branches if not.
74 Parameters
75 ----------
76 graph : UndirectedDependenceGraph()
77 Class for representing undirected graph and auxilliary data used in edge clique cover algorithm.
79 k_num_cliques : int
80 Current depth of search; number of cliques in cover being testet for solution.
82 the_cover : np.array
83 Biadjacency matrix representing (possibly partial) edge clique cover.
85 iteration: current iteration
87 iteration_max: maximum number of iteration_max
89 Returns
90 -------
91 2d numpy array or None
92 Biadjacency matrix representing (complete) edge clique cover or None if cover is only partial.
94 """
96 iteration = iteration + 1
97 branch_graph = graph.reducible_copy()
98 branch_graph.the_cover = the_cover
99 branch_graph.cover_edges()
101 if branch_graph.num_edges == 0:
102 return branch_graph.reconstruct_cover(the_cover)
104 branch_graph.reduzieren(k_num_cliques)
105 k_num_cliques = branch_graph.k_num_cliques
107 if k_num_cliques < 0:
108 return None
110 if branch_graph.num_edges == 0: # equiv to len(branch_graph.extant_edges_idx)==0
111 return (
112 branch_graph.the_cover
113 ) # not in paper, but speeds it up slightly; or rather return None?
115 chosen_nbrhood = branch_graph.choose_nbrhood()
116 # print("num cliques: {}".format(len([x for x in _max_cliques(chosen_nbrhood)])))
117 for clique_nodes in _max_cliques(chosen_nbrhood):
118 if len(clique_nodes) == 1: # then this vert has been rmed; quirk of max_cliques
119 continue
120 clique = np.zeros(branch_graph.unreduced.num_vertices, dtype=int)
121 clique[clique_nodes] = 1
122 union = (
123 clique.reshape(1, -1)
124 if branch_graph.the_cover is None
125 else np.vstack((branch_graph.the_cover, clique))
126 )
128 # print(iteration)
129 if iteration > iteration_max:
130 return branch_graph.the_cover
132 the_cover_prime = _branch(
133 branch_graph,
134 k_num_cliques - 1,
135 union,
136 iteration,
137 iteration_max=iteration_max,
138 )
139 if the_cover_prime is not None:
140 return the_cover_prime
141 return None
144def _max_cliques(nbrhood):
145 """Adaptation of NetworkX code for finding all maximal cliques.
147 Parameters
148 ----------
149 nbrhood : np.array
150 Adjacency matrix for undirected (sub)graph.
152 Returns
153 -------
154 generator
155 set of all maximal cliques
157 Notes
158 -----
159 Pieced together from nx.from_numpy_array and nx.find_cliques, which
160 is output sensitive.
162 """
164 if len(nbrhood) == 0:
165 return
167 # convert adjacency matrix to nx style graph
168 adj = {
169 u: {v for v in np.nonzero(nbrhood[u])[0] if v != u} for u in range(len(nbrhood))
170 }
171 Q = [None]
173 subg = set(range(len(nbrhood)))
174 cand = set(range(len(nbrhood)))
175 u = max(subg, key=lambda u: len(cand & adj[u]))
176 ext_u = cand - adj[u]
177 stack = []
179 try:
180 while True:
181 if ext_u:
182 q = ext_u.pop()
183 cand.remove(q)
184 Q[-1] = q
185 adj_q = adj[q]
186 subg_q = subg & adj_q
187 if not subg_q:
188 yield Q[:]
189 else:
190 cand_q = cand & adj_q
191 if cand_q:
192 stack.append((subg, cand, ext_u))
193 Q.append(None)
194 subg = subg_q
195 cand = cand_q
196 u = max(subg, key=lambda u: len(cand & adj[u]))
197 ext_u = cand - adj[u]
198 else:
199 Q.pop()
200 subg, cand, ext_u = stack.pop()
201 except IndexError:
202 pass
203 # note: max_cliques is a generator, so it's consumed after being
204 # looped through once
207def _add_isolated_verts(cover):
208 cover = cover.astype(bool)
209 iso_vert_idx = np.flatnonzero(cover.sum(0) == 0)
210 num_rows = len(iso_vert_idx)
211 num_cols = cover.shape[1]
212 iso_vert_cover = np.zeros((num_rows, num_cols), bool)
213 iso_vert_cover[np.arange(num_rows), iso_vert_idx] = True
214 return np.vstack((cover, iso_vert_cover))
217def _find_heuristic_1pc(graph):
218 num_meas = len(graph)
220 # nx_graph = nx.from_numpy_array(graph)
221 # indep_set =
222 # list(nx.approximation.maximum_independent_set(nx_graph))
224 indep_sets = _max_cliques(np.logical_not(graph))
225 max_indep_set = next(indep_sets)
226 for indep_set in indep_sets:
227 if len(indep_set) > len(max_indep_set):
228 max_indep_set = indep_set
230 num_latents = len(max_indep_set)
232 the_cover = np.zeros((num_latents, num_meas), bool)
233 the_cover[np.arange(num_latents), max_indep_set] = True
235 for idx, node in enumerate(max_indep_set):
236 nbrs = np.flatnonzero(graph[node])
237 the_cover[idx, nbrs] = True
239 uncovered_edges = deque(
240 {
241 tuple(edge)
242 for edge in np.argwhere(np.triu(graph, 1))
243 if not np.logical_and(the_cover[:, edge[0]], the_cover[:, edge[1]]).any()
244 }
245 )
247 while bool(uncovered_edges):
248 u, v = uncovered_edges.popleft()
249 if the_cover[:, u].any():
250 the_cover[np.argmax(the_cover[:, u]), v] = True
251 elif the_cover[:, v].any():
252 the_cover[np.argmax(the_cover[:, v]), u] = True
253 else:
254 uncovered_edges.append((u, v))
256 recon = the_cover.T @ the_cover
257 if np.logical_not(graph <= recon).any():
258 raise Exception(f"Problem with `find_heuristic_1pc()` for input {graph}")
260 return the_cover