Source code for qbiocode.apps.quvine.data.sparsify

# Copyright 2026, IBM Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable, Literal, Optional, Set, Tuple, List, Dict

import networkx as nx
import numpy as np


[docs] def materialize_undirected_simple_graph(G: nx.Graph) -> nx.Graph: """ Return a fully materialized undirected simple nx.Graph (no views, no DiGraph internals). This avoids the weird KeyErrors you were seeing with adjacency traversal. Preserves graph-level metadata and node attributes. """ H = nx.Graph() # Copy graph-level metadata H.graph.update(G.graph) # nodes with attributes H.add_nodes_from(G.nodes(data=True)) # edges() - if input is directed/multi, nx.Graph() will collapse direction/multiedges H.add_edges_from(list(G.edges())) return H
[docs] def edge_triangle_support_scores(G: nx.Graph, candidate_edges: List[Tuple]) -> List[Tuple[Tuple, int]]: """ Score edges by triangle support (# common neighbors). Uses adjacency dict access only. """ # Ensure fully materialized to avoid any internal weirdness G = materialize_undirected_simple_graph(G) node_set = set(G.nodes()) # Precompute neighbor sets via adjacency (safe) neigh: Dict = {u: set(G.adj[u].keys()) for u in node_set} scored: List[Tuple[Tuple, int]] = [] for u, v in candidate_edges: if u not in node_set or v not in node_set: continue scored.append(((u, v), len(neigh[u].intersection(neigh[v])))) return scored
[docs] def sparsify_edges_biological( G: nx.Graph, retain_ratio: float, max_degree: int, rng: np.random.Generator, scoring: Literal["common_neighbors", "prefer_low_degree"] = "common_neighbors", ) -> nx.Graph: """ Edge-only sparsification: degree-capped selection prioritizing biologically coherent edges. No BFS, no connectivity checks, no nx.is_connected/connected_components. """ if not (0 < retain_ratio <= 1): raise ValueError("retain_ratio must be in (0, 1].") if max_degree <= 0: raise ValueError("max_degree must be positive.") G = materialize_undirected_simple_graph(G) if G.number_of_nodes() == 0: return G m0 = G.number_of_edges() target_edges = int(round(retain_ratio * m0)) # Guarantee we don't request a negative/zero edge budget target_edges = max(0, target_edges) edges = list(G.edges()) if scoring == "common_neighbors": # Score all edges scored = edge_triangle_support_scores(G, edges) rng.shuffle(scored) # tie-breaking scored.sort(key=lambda x: x[1], reverse=True) sorted_edges = [e for (e, _) in scored] elif scoring == "prefer_low_degree": rng.shuffle(edges) edges.sort(key=lambda e: (G.degree[e[0]] + G.degree[e[1]])) sorted_edges = edges else: raise ValueError(f"Unknown scoring method: {scoring}") H = nx.Graph() H.add_nodes_from(G.nodes()) # Degree-capped edge addition for u, v in sorted_edges: if H.number_of_edges() >= target_edges: break # nodes must exist (should, but guard anyway) if u not in H or v not in H: continue if H.degree[u] >= max_degree or H.degree[v] >= max_degree: continue if H.has_edge(u, v): continue H.add_edge(u, v) return H