Source code for qbiocode.apps.quvine.reproducibility.method_adapters

# 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.

"""
Method Adapters for Reproducible Benchmarking Pipeline

This module provides adapter functions that wrap all 42 QuVINE methods
to work with the reproducible pipeline interface.

Each adapter:
1. Accepts pre-generated graph and split
2. Uses the provided canonical seed
3. Returns standardized metrics
4. Does NOT modify input data or create its own splits
"""

import numpy as np
import networkx as nx
from typing import Dict, Any, Optional, List
from sklearn.metrics import (
    accuracy_score, f1_score, roc_auc_score, 
    average_precision_score, precision_recall_curve
)
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
import logging

logger = logging.getLogger(__name__)


# ============================================================================
# Utility Functions
# ============================================================================

[docs] def train_classifier( embeddings: np.ndarray, train_idx: np.ndarray, val_idx: np.ndarray, test_idx: np.ndarray, labels: np.ndarray, seed: int ) -> Dict[str, float]: """ Train a logistic regression classifier on embeddings. Parameters ---------- embeddings : np.ndarray Node embeddings [N, d] train_idx : np.ndarray Training indices val_idx : np.ndarray Validation indices test_idx : np.ndarray Test indices labels : np.ndarray Node labels seed : int Random seed Returns ------- dict Classification metrics """ # Standardize features scaler = StandardScaler() X_train = scaler.fit_transform(embeddings[train_idx]) X_val = scaler.transform(embeddings[val_idx]) X_test = scaler.transform(embeddings[test_idx]) y_train = labels[train_idx] y_val = labels[val_idx] y_test = labels[test_idx] # Train classifier clf = LogisticRegression( max_iter=1000, random_state=seed, solver='lbfgs' ) clf.fit(X_train, y_train) # Predictions y_pred_test = clf.predict(X_test) # Metrics metrics = { "accuracy": float(accuracy_score(y_test, y_pred_test)), "f1_macro": float(f1_score(y_test, y_pred_test, average='macro')), "f1_micro": float(f1_score(y_test, y_pred_test, average='micro')), "f1_weighted": float(f1_score(y_test, y_pred_test, average='weighted')), "num_train": len(train_idx), "num_val": len(val_idx), "num_test": len(test_idx), "num_classes": int(labels.max() + 1) } return metrics
[docs] def evaluate_node_ranking( embeddings: np.ndarray, seed_nodes: List[int], target_nodes: List[int], k_values: List[int] = [10, 20, 50] ) -> Dict[str, float]: """ Evaluate node ranking task. Parameters ---------- embeddings : np.ndarray Node embeddings [N, d] seed_nodes : list Seed node indices target_nodes : list Target node indices k_values : list K values for precision@k Returns ------- dict Ranking metrics """ # Compute centroid of seed nodes seed_centroid = embeddings[seed_nodes].mean(axis=0) # Compute similarities to all nodes similarities = embeddings @ seed_centroid # Rank nodes by similarity ranked_indices = np.argsort(-similarities) # Compute metrics target_set = set(target_nodes) metrics = {} for k in k_values: top_k = set(ranked_indices[:k].tolist()) hits = len(top_k & target_set) precision_at_k = hits / k if k > 0 else 0.0 metrics[f"precision@{k}"] = float(precision_at_k) # Mean reciprocal rank mrr = 0.0 for target in target_nodes: rank = np.where(ranked_indices == target)[0] if len(rank) > 0: mrr += 1.0 / (rank[0] + 1) mrr /= len(target_nodes) if target_nodes else 1.0 metrics["mrr"] = float(mrr) metrics["num_seed_nodes"] = len(seed_nodes) metrics["num_target_nodes"] = len(target_nodes) return metrics
# ============================================================================ # Classical Baseline Adapters # ============================================================================
[docs] def run_node2vec_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for Node2Vec method.""" from qbiocode.apps.quvine.baselines import run_node2vec config = config or {} nodes = list(G.nodes()) # Generate embeddings embeddings = run_node2vec( graph=G, nodes=nodes, dimensions=config.get('dimensions', 128), walk_length=config.get('walk_length', 80), num_walks=config.get('num_walks', 10), p=config.get('p', 1.0), q=config.get('q', 1.0), window=config.get('window', 10), min_count=config.get('min_count', 1), workers=config.get('workers', 4), seed=seed ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_netmf_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for NetMF method.""" from qbiocode.apps.quvine.baselines import run_netmf config = config or {} nodes = list(G.nodes()) # Generate embeddings embeddings = run_netmf( graph=G, nodes=nodes, dimensions=config.get('dimensions', 128), window_size=config.get('window_size', 10), negative=config.get('negative', 1), rank=config.get('rank', None), use_svd=config.get('use_svd', True), seed=seed ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_graphsage_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for GraphSAGE method.""" from qbiocode.apps.quvine.baselines import run_graphsage config = config or {} nodes = list(G.nodes()) # Generate embeddings embeddings = run_graphsage( graph=G, nodes=nodes, dimensions=config.get('dimensions', 128), hidden_dim=config.get('hidden_dim', 256), n_layers=config.get('n_layers', 2), epochs=config.get('epochs', 50), lr=config.get('lr', 0.01), neg_samples=config.get('neg_samples', 5), seed=seed, device=config.get('device', 'cpu') ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_appnp_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for APPNP method.""" from qbiocode.apps.quvine.baselines import run_appnp config = config or {} nodes = list(G.nodes()) # Generate embeddings embeddings = run_appnp( graph=G, nodes=nodes, dimensions=config.get('dimensions', 128), hidden_dim=config.get('hidden_dim', 64), n_layers=config.get('n_layers', 2), alpha=config.get('alpha', 0.1), K=config.get('K', 10), dropout=config.get('dropout', 0.5), lr=config.get('lr', 0.01), weight_decay=config.get('weight_decay', 5e-4), epochs=config.get('epochs', 200), seed=seed, device=config.get('device', 'cpu') ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
# ============================================================================ # QuVINE SGNS Methods (Quantum Walk + Skip-Gram) # ============================================================================
[docs] def run_quvine_sgns( G: nx.Graph, walk_type: str, config: Optional[Dict] = None, seed: int = 42 ) -> np.ndarray: """ Run QuVINE SGNS embedding with specified walk type. Parameters ---------- G : nx.Graph Input graph walk_type : str Type of walk: 'rwr', 'ctqw', or 'dtqw' config : dict, optional Configuration parameters seed : int Random seed Returns ------- np.ndarray Node embeddings (n_nodes x embedding_dim) """ from qbiocode.apps.quvine.walks.rwr import generate_RWR_pagerank_walks from qbiocode.apps.quvine.walks.ctqw import generate_CTQW_walks from qbiocode.apps.quvine.walks.dtqw import generate_DTQW_walks from qbiocode.apps.quvine.corpus.builder import CorpusBuilder from qbiocode.apps.quvine.embedding.word2vec import corpus_to_embedding config = config or {} nodes = list(G.nodes()) # Convert nodes to strings for Word2Vec node_to_str = {n: str(n) for n in nodes} str_to_node = {str(n): n for n in nodes} G_str = nx.relabel_nodes(G, node_to_str, copy=True) nodes_str = [node_to_str[n] for n in nodes] # Set random seed rng = np.random.default_rng(seed) # Walk parameters num_walks = config.get('num_walks', 10) walk_length = config.get('walk_length', 80) # Generate walks for all nodes corpus_builder = CorpusBuilder() for node_str in nodes_str: try: if walk_type == 'rwr': walks = generate_RWR_pagerank_walks( G=G_str, root=node_str, view_nodes=None, num_walks=num_walks, walk_length=walk_length, restart_prob=config.get('restart_prob', 0.15), max_iter=config.get('max_iter', 100), rng=rng ) elif walk_type == 'ctqw': walks = generate_CTQW_walks( G=G_str, root=node_str, view_nodes=None, num_walks=num_walks, walk_length=walk_length, time=config.get('time', 1.0), steps=config.get('steps', 20), rng=rng ) elif walk_type == 'dtqw': walks = generate_DTQW_walks( G=G_str, root=node_str, view_nodes=None, num_walks=num_walks, walk_length=walk_length, steps=config.get('steps', 25), coin=config.get('coin', 'grover'), rng=rng ) else: raise ValueError(f"Unknown walk type: {walk_type}") # Add walks to corpus corpus_builder.add(node_str, walks) except Exception as e: logger.warning(f"Walk generation failed for node {node_str}: {e}") continue # Build corpus corpus = corpus_builder.build() # Train SGNS embeddings embeddings = corpus_to_embedding( corpus=corpus, nodes=nodes_str, vector_size=config.get('dimensions', 128), window=config.get('window', 10), sg=1, # Skip-gram negative=config.get('negative', 5), min_count=config.get('min_count', 0), workers=config.get('workers', 4), epochs=config.get('epochs', 5) ) return embeddings
[docs] def run_quvine_rwr_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for QuVINE RWR (Random Walk with Restart + SGNS).""" config = config or {} # Generate embeddings using RWR walks embeddings = run_quvine_sgns(G, 'rwr', config, seed) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_quvine_ctqw_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for QuVINE CTQW (Continuous-Time Quantum Walk + SGNS).""" config = config or {} # Generate embeddings using CTQW walks embeddings = run_quvine_sgns(G, 'ctqw', config, seed) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_quvine_dtqw_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for QuVINE DTQW (Discrete-Time Quantum Walk + SGNS).""" config = config or {} # Generate embeddings using DTQW walks embeddings = run_quvine_sgns(G, 'dtqw', config, seed) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
# ============================================================================ # Filter Methods (Baseline and QuVINE-calibrated) # ============================================================================
[docs] def run_baseline_filter_heat_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for baseline heat kernel filter (no quantum calibration).""" from qbiocode.apps.quvine.embedding.quantum_filters import generate_baseline_heat_embedding config = config or {} # Generate embeddings embeddings = generate_baseline_heat_embedding( G=G, embedding_dim=config.get('embedding_dim', 128), scale=config.get('scale', 1.0), use_features=False, normalize=True, random_state=seed ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_baseline_filter_poly_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for baseline polynomial filter (no quantum calibration).""" from qbiocode.apps.quvine.embedding.quantum_filters import generate_baseline_poly_embedding config = config or {} # Generate embeddings embeddings = generate_baseline_poly_embedding( G=G, embedding_dim=config.get('embedding_dim', 128), order=config.get('order', 4), use_features=False, normalize=True, random_state=seed ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def generate_quantum_targets_from_walks( G: nx.Graph, walk_type: str, config: Dict, seed: int, n_samples: int = 10 ) -> List[Dict]: """ Generate quantum walk targets for filter calibration. Samples subnetworks and computes quantum walk distributions. Returns targets with integer node IDs (not strings). """ from qbiocode.apps.quvine.walks.rwr import get_RWR_pagerank_scores from qbiocode.apps.quvine.walks.ctqw import generate_ctqw_hiperwalk_scores from qbiocode.apps.quvine.walks.dtqw import get_coined_hiperwalk_scores nodes = list(G.nodes()) rng = np.random.default_rng(seed) q_targets = [] # Sample random centers (use original integer node IDs) centers = rng.choice(nodes, size=min(n_samples, len(nodes)), replace=False) for center in centers: # Sample local neighborhood neighbors = list(nx.single_source_shortest_path_length(G, center, cutoff=2).keys()) if len(neighbors) < 3: continue try: if walk_type == 'rwr': scores = get_RWR_pagerank_scores( G, center, restart_prob=config.get('restart_prob', 0.15), view_nodes=set(neighbors), max_iter=config.get('max_iter', 100) ) elif walk_type == 'ctqw': scores = generate_ctqw_hiperwalk_scores( G, center, view_nodes=set(neighbors), steps=config.get('steps', 20), time=config.get('time', 1.0) ) elif walk_type == 'dtqw': scores = get_coined_hiperwalk_scores( G, center, view_nodes=set(neighbors), steps=config.get('steps', 25), coin=config.get('coin', 'grover') ) else: raise ValueError(f"Unknown walk type: {walk_type}") # Normalize to probability distribution nodes_in_view = list(scores.keys()) probs = np.array([scores[n] for n in nodes_in_view]) probs = probs / probs.sum() if probs.sum() > 0 else probs q_targets.append({ 'nodes': nodes_in_view, 'center': center, 'pQ': probs }) except Exception as e: logger.warning(f"Failed to generate quantum target for center {center}: {e}") continue return q_targets
[docs] def run_filter_rwr_heat_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for RWR + heat kernel filter.""" from qbiocode.apps.quvine.embedding.quantum_filters import ( get_laplacian, calibrate_heat_kernel, apply_heat_filter ) config = config or {} # Generate quantum targets from RWR walks q_targets = generate_quantum_targets_from_walks(G, 'rwr', config, seed) if not q_targets: logger.warning("No quantum targets generated, falling back to baseline") from qbiocode.apps.quvine.embedding.quantum_filters import generate_baseline_heat_embedding embeddings = generate_baseline_heat_embedding( G, embedding_dim=config.get('embedding_dim', 128), random_state=seed ) else: # Manual implementation to work around tuple unpacking bug np.random.seed(seed) N = G.number_of_nodes() # Get Laplacian (unpack tuple properly) L, nodelist, node_to_idx = get_laplacian(G, normalize=True) # Calibrate heat kernel t_grid = np.logspace(-2, 2, 40) _, t_star = calibrate_heat_kernel(L, q_targets, t_grid, node_to_idx, loss='l2') # Generate random features embedding_dim = config.get('embedding_dim', 128) rng = np.random.default_rng(seed) X = rng.normal(size=(N, embedding_dim)) norm = np.linalg.norm(X, axis=1, keepdims=True) X = X / np.maximum(norm, 1e-12) # Apply heat kernel filter embeddings = apply_heat_filter(L, X, t_star) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_filter_rwr_poly_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for RWR + polynomial filter.""" from qbiocode.apps.quvine.embedding.quantum_filters import generate_quvine_poly_embedding config = config or {} # Generate quantum targets from RWR walks q_targets = generate_quantum_targets_from_walks(G, 'rwr', config, seed) if not q_targets: logger.warning("No quantum targets generated, falling back to baseline") from qbiocode.apps.quvine.embedding.quantum_filters import generate_baseline_poly_embedding embeddings = generate_baseline_poly_embedding(G, embedding_dim=config.get('embedding_dim', 128), random_state=seed) else: # Generate embeddings with quantum calibration embeddings = generate_quvine_poly_embedding( G=G, q_targets=q_targets, K=config.get('K', 4), ridge=config.get('ridge', 1e-6), embedding_dim=config.get('embedding_dim', 128), use_features=False, normalize=True, random_state=seed ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_filter_ctqw_heat_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for CTQW + heat kernel filter.""" from qbiocode.apps.quvine.embedding.quantum_filters import ( get_laplacian, calibrate_heat_kernel, apply_heat_filter ) config = config or {} # Generate quantum targets from CTQW walks q_targets = generate_quantum_targets_from_walks(G, 'ctqw', config, seed) if not q_targets: logger.warning("No quantum targets generated, falling back to baseline") from qbiocode.apps.quvine.embedding.quantum_filters import generate_baseline_heat_embedding embeddings = generate_baseline_heat_embedding( G, embedding_dim=config.get('embedding_dim', 128), random_state=seed ) else: # Manual implementation to work around tuple unpacking bug np.random.seed(seed) N = G.number_of_nodes() # Get Laplacian (unpack tuple properly) L, nodelist, node_to_idx = get_laplacian(G, normalize=True) # Calibrate heat kernel t_grid = np.logspace(-2, 2, 40) _, t_star = calibrate_heat_kernel(L, q_targets, t_grid, node_to_idx, loss='l2') # Generate random features embedding_dim = config.get('embedding_dim', 128) rng = np.random.default_rng(seed) X = rng.normal(size=(N, embedding_dim)) norm = np.linalg.norm(X, axis=1, keepdims=True) X = X / np.maximum(norm, 1e-12) # Apply heat kernel filter embeddings = apply_heat_filter(L, X, t_star) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_filter_ctqw_poly_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for CTQW + polynomial filter.""" from qbiocode.apps.quvine.embedding.quantum_filters import generate_quvine_poly_embedding config = config or {} # Generate quantum targets from CTQW walks q_targets = generate_quantum_targets_from_walks(G, 'ctqw', config, seed) if not q_targets: logger.warning("No quantum targets generated, falling back to baseline") from qbiocode.apps.quvine.embedding.quantum_filters import generate_baseline_poly_embedding embeddings = generate_baseline_poly_embedding(G, embedding_dim=config.get('embedding_dim', 128), random_state=seed) else: # Generate embeddings with quantum calibration embeddings = generate_quvine_poly_embedding( G=G, q_targets=q_targets, K=config.get('K', 4), ridge=config.get('ridge', 1e-6), embedding_dim=config.get('embedding_dim', 128), use_features=False, normalize=True, random_state=seed ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
# ============================================================================ # GAT Methods (Graph Attention Networks with various input features) # ============================================================================
[docs] def run_gat_adapter( G: nx.Graph, split: Dict[str, Any], task: str, method_name: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """ Generic GAT adapter that handles all 12 GAT variants. GAT variants differ only in their input features: - gat_baseline: raw structural features - gat_heat/poly: fixed filter parameters - gat_rwr/ctqw/dtqw: walk-based features - gat_*_heat/poly: quantum-calibrated filters """ from qbiocode.apps.quvine.baselines.gat import generate_gat_embedding_by_method_name, GATConfig, TrainConfig config = config or {} # Prepare quantum targets if needed for calibrated variants ctqw_targets = None dtqw_targets = None rwr_targets = None if 'ctqw' in method_name and ('heat' in method_name or 'poly' in method_name): # Need CTQW targets for calibration ctqw_targets = generate_quantum_targets_from_walks(G, 'ctqw', config, seed) elif 'dtqw' in method_name and ('heat' in method_name or 'poly' in method_name): # Need DTQW targets for calibration dtqw_targets = generate_quantum_targets_from_walks(G, 'dtqw', config, seed) elif 'rwr' in method_name and ('heat' in method_name or 'poly' in method_name): # Need RWR targets for calibration rwr_targets = generate_quantum_targets_from_walks(G, 'rwr', config, seed) # Set up GAT configuration gat_config = GATConfig( hidden_dim=config.get('hidden_dim', 64), output_dim=config.get('embedding_dim', 128), num_layers=config.get('num_layers', 2), heads=config.get('heads', 4), dropout=config.get('dropout', 0.5), attention_dropout=config.get('attention_dropout', 0.2), negative_slope=config.get('negative_slope', 0.2), residual=config.get('residual', True) ) # Set up training configuration train_config = TrainConfig( epochs=config.get('epochs', 200), lr=config.get('lr', 5e-3), weight_decay=config.get('weight_decay', 5e-4), patience=config.get('patience', 25), edge_batch_size=config.get('edge_batch_size', 4096), val_edge_fraction=config.get('val_edge_fraction', 0.1), device=config.get('device', 'cpu'), random_state=seed, verbose=config.get('verbose', False) ) # Generate embeddings embeddings = generate_gat_embedding_by_method_name( G=G, method_name=method_name, embedding_dim=config.get('embedding_dim', 128), nodelist=list(G.nodes()), ctqw_targets=ctqw_targets, dtqw_targets=dtqw_targets, rwr_targets=rwr_targets, heat_t=config.get('heat_t', 1.0), poly_K=config.get('poly_K', 4), rwr_alpha=config.get('rwr_alpha', 0.15), gat_config=gat_config, train_config=train_config ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
# Create individual adapters for each GAT variant
[docs] def run_gat_baseline_adapter(G, split, task, config=None, seed=42): """GAT with raw structural features.""" return run_gat_adapter(G, split, task, 'gat_baseline', config, seed)
[docs] def run_gat_heat_adapter(G, split, task, config=None, seed=42): """GAT with fixed heat kernel features.""" return run_gat_adapter(G, split, task, 'gat_heat', config, seed)
[docs] def run_gat_poly_adapter(G, split, task, config=None, seed=42): """GAT with fixed polynomial features.""" return run_gat_adapter(G, split, task, 'gat_poly', config, seed)
[docs] def run_gat_rwr_adapter(G, split, task, config=None, seed=42): """GAT with RWR walk features.""" return run_gat_adapter(G, split, task, 'gat_rwr', config, seed)
[docs] def run_gat_ctqw_adapter(G, split, task, config=None, seed=42): """GAT with direct CTQW features.""" return run_gat_adapter(G, split, task, 'gat_ctqw', config, seed)
[docs] def run_gat_dtqw_adapter(G, split, task, config=None, seed=42): """GAT with direct DTQW features.""" return run_gat_adapter(G, split, task, 'gat_dtqw', config, seed)
[docs] def run_gat_rwr_heat_adapter(G, split, task, config=None, seed=42): """GAT with RWR-calibrated heat kernel features.""" return run_gat_adapter(G, split, task, 'gat_rwr_heat', config, seed)
[docs] def run_gat_rwr_poly_adapter(G, split, task, config=None, seed=42): """GAT with RWR-calibrated polynomial features.""" return run_gat_adapter(G, split, task, 'gat_rwr_poly', config, seed)
[docs] def run_gat_ctqw_heat_adapter(G, split, task, config=None, seed=42): """GAT with CTQW-calibrated heat kernel features.""" return run_gat_adapter(G, split, task, 'gat_ctqw_heat', config, seed)
[docs] def run_gat_ctqw_poly_adapter(G, split, task, config=None, seed=42): """GAT with CTQW-calibrated polynomial features.""" return run_gat_adapter(G, split, task, 'gat_ctqw_poly', config, seed)
[docs] def run_gat_dtqw_heat_adapter(G, split, task, config=None, seed=42): """GAT with DTQW-calibrated heat kernel features.""" return run_gat_adapter(G, split, task, 'gat_dtqw_heat', config, seed)
[docs] def run_gat_dtqw_poly_adapter(G, split, task, config=None, seed=42): """GAT with DTQW-calibrated polynomial features.""" return run_gat_adapter(G, split, task, 'gat_dtqw_poly', config, seed)
# ============================================================================ # GraphGPS Methods (Graph GPS Transformer with various input features) # ============================================================================
[docs] def run_graphgps_adapter( G: nx.Graph, split: Dict[str, Any], task: str, method_name: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """ Generic GraphGPS adapter that handles all 12 GraphGPS variants. GraphGPS variants differ only in their input features (same as GAT): - graphgps_baseline: raw structural features - graphgps_heat/poly: fixed filter parameters - graphgps_rwr/ctqw/dtqw: walk-based features - graphgps_*_heat/poly: quantum-calibrated filters """ from qbiocode.apps.quvine.baselines.graphgps import generate_graphgps_embedding_by_method_name, GraphGPSConfig, TrainConfig config = config or {} # Prepare quantum targets if needed for calibrated variants ctqw_targets = None dtqw_targets = None rwr_targets = None if 'ctqw' in method_name and ('heat' in method_name or 'poly' in method_name): ctqw_targets = generate_quantum_targets_from_walks(G, 'ctqw', config, seed) elif 'dtqw' in method_name and ('heat' in method_name or 'poly' in method_name): dtqw_targets = generate_quantum_targets_from_walks(G, 'dtqw', config, seed) elif 'rwr' in method_name and ('heat' in method_name or 'poly' in method_name): rwr_targets = generate_quantum_targets_from_walks(G, 'rwr', config, seed) # Set up GraphGPS configuration gps_config = GraphGPSConfig( hidden_dim=config.get('hidden_dim', 64), output_dim=config.get('embedding_dim', 128), num_layers=config.get('num_layers', 2), heads=config.get('heads', 4), dropout=config.get('dropout', 0.2), attn_dropout=config.get('attn_dropout', 0.2), local_gnn=config.get('local_gnn', 'gcn'), attn_type=config.get('attn_type', 'multihead'), use_layer_norm=config.get('use_layer_norm', True), activation=config.get('activation', 'relu'), lap_pe_dim=config.get('lap_pe_dim', 0), standardize_features=config.get('standardize_features', True) ) # Set up training configuration train_config = TrainConfig( task='link_reconstruction', epochs=config.get('epochs', 200), lr=config.get('lr', 5e-3), weight_decay=config.get('weight_decay', 5e-4), patience=config.get('patience', 30), edge_batch_size=config.get('edge_batch_size', 8192), val_edge_fraction=config.get('val_edge_fraction', 0.1), device=config.get('device', 'cpu'), random_state=seed, verbose=config.get('verbose', False) ) # Generate embeddings embeddings = generate_graphgps_embedding_by_method_name( G=G, method_name=method_name, embedding_dim=config.get('embedding_dim', 128), nodelist=list(G.nodes()), ctqw_targets=ctqw_targets, dtqw_targets=dtqw_targets, rwr_targets=rwr_targets, heat_t=config.get('heat_t', 1.0), poly_K=config.get('poly_K', 4), rwr_alpha=config.get('rwr_alpha', 0.15), gps_config=gps_config, train_config=train_config ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
# Create individual adapters for each GraphGPS variant
[docs] def run_graphgps_baseline_adapter(G, split, task, config=None, seed=42): """GraphGPS with raw structural features.""" return run_graphgps_adapter(G, split, task, 'graphgps_baseline', config, seed)
[docs] def run_graphgps_heat_adapter(G, split, task, config=None, seed=42): """GraphGPS with fixed heat kernel features.""" return run_graphgps_adapter(G, split, task, 'graphgps_heat', config, seed)
[docs] def run_graphgps_poly_adapter(G, split, task, config=None, seed=42): """GraphGPS with fixed polynomial features.""" return run_graphgps_adapter(G, split, task, 'graphgps_poly', config, seed)
[docs] def run_graphgps_rwr_adapter(G, split, task, config=None, seed=42): """GraphGPS with RWR walk features.""" return run_graphgps_adapter(G, split, task, 'graphgps_rwr', config, seed)
[docs] def run_graphgps_ctqw_adapter(G, split, task, config=None, seed=42): """GraphGPS with direct CTQW features.""" return run_graphgps_adapter(G, split, task, 'graphgps_ctqw', config, seed)
[docs] def run_graphgps_dtqw_adapter(G, split, task, config=None, seed=42): """GraphGPS with direct DTQW features.""" return run_graphgps_adapter(G, split, task, 'graphgps_dtqw', config, seed)
[docs] def run_graphgps_rwr_heat_adapter(G, split, task, config=None, seed=42): """GraphGPS with RWR-calibrated heat kernel features.""" return run_graphgps_adapter(G, split, task, 'graphgps_rwr_heat', config, seed)
[docs] def run_graphgps_rwr_poly_adapter(G, split, task, config=None, seed=42): """GraphGPS with RWR-calibrated polynomial features.""" return run_graphgps_adapter(G, split, task, 'graphgps_rwr_poly', config, seed)
[docs] def run_graphgps_ctqw_heat_adapter(G, split, task, config=None, seed=42): """GraphGPS with CTQW-calibrated heat kernel features.""" return run_graphgps_adapter(G, split, task, 'graphgps_ctqw_heat', config, seed)
[docs] def run_graphgps_ctqw_poly_adapter(G, split, task, config=None, seed=42): """GraphGPS with CTQW-calibrated polynomial features.""" return run_graphgps_adapter(G, split, task, 'graphgps_ctqw_poly', config, seed)
[docs] def run_graphgps_dtqw_heat_adapter(G, split, task, config=None, seed=42): """GraphGPS with DTQW-calibrated heat kernel features.""" return run_graphgps_adapter(G, split, task, 'graphgps_dtqw_heat', config, seed)
[docs] def run_graphgps_dtqw_poly_adapter(G, split, task, config=None, seed=42): """GraphGPS with DTQW-calibrated polynomial features.""" return run_graphgps_adapter(G, split, task, 'graphgps_dtqw_poly', config, seed)
# ============================================================================ # Additional Classical Baselines # ============================================================================
[docs] def run_baseline_gcnmf_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """Adapter for baseline GCN-MF (no quantum calibration).""" from qbiocode.apps.quvine.baselines.gcn_mf import generate_baseline_gcnmf_embedding config = config or {} # Generate embeddings embeddings = generate_baseline_gcnmf_embedding( G=G, embedding_dim=config.get('embedding_dim', 128), hidden_dim=config.get('hidden_dim', 64), mf_dim=config.get('mf_dim', 64), n_layers=config.get('n_layers', 2), epochs=config.get('epochs', 200), lr=config.get('lr', 0.01), weight_decay=config.get('weight_decay', 5e-4), random_state=seed, device=config.get('device', 'cpu') ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_baseline_filter_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """ Adapter for generic baseline filter (defaults to heat kernel). This is the generic 'baseline_filter' method that can be configured to use either heat or polynomial filters via config['filter_type']. Defaults to heat kernel for backward compatibility. """ from qbiocode.apps.quvine.embedding.quantum_filters import generate_baseline_filter_embedding config = config or {} filter_type = config.get('filter_type', 'heat') # Generate embeddings using the generic baseline filter function embeddings = generate_baseline_filter_embedding( G=G, filter_type=filter_type, t=config.get('t', 1.0), K=config.get('K', 4), embedding_dim=config.get('embedding_dim', 128), use_features=False, normalize=config.get('normalize', True), random_state=seed ) # Evaluate based on task if task == "node_classification": return train_classifier( embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
# ============================================================================ # Fusion Methods # ============================================================================
[docs] def run_rwr_fusion_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """ Fusion method combining RWR-based embeddings. Combines: - quvine_rwr (SGNS with RWR) - filter_rwr_heat (RWR-calibrated heat kernel) - filter_rwr_poly (RWR-calibrated polynomial) Uses SVD-based fusion to combine the three views. """ from qbiocode.apps.quvine.fusion.fuse import fuse_embeddings_svd, _prep_blocks from qbiocode.apps.quvine.embedding.quantum_filters import ( get_laplacian, calibrate_heat_kernel, apply_heat_filter, calibrate_polynomial_filter, apply_polynomial_filter, generate_baseline_heat_embedding, generate_baseline_poly_embedding ) config = config or {} embedding_dim = config.get('embedding_dim', 128) # Generate three RWR-based embeddings emb_sgns = run_quvine_sgns(G, 'rwr', config, seed) # Generate quantum targets q_targets = generate_quantum_targets_from_walks(G, 'rwr', config, seed) if q_targets: np.random.seed(seed) N = G.number_of_nodes() L, nodelist, node_to_idx = get_laplacian(G, normalize=True) # Generate random features rng = np.random.default_rng(seed) X = rng.normal(size=(N, embedding_dim)) norm = np.linalg.norm(X, axis=1, keepdims=True) X = X / np.maximum(norm, 1e-12) # Calibrate and apply heat filter t_grid = np.logspace(-2, 2, 40) _, t_star = calibrate_heat_kernel(L, q_targets, t_grid, node_to_idx, loss='l2') emb_heat = apply_heat_filter(L, X, t_star) # Calibrate and apply poly filter result = calibrate_polynomial_filter(L, q_targets, K=config.get('K', 4), node_to_idx=node_to_idx, ridge=1e-6) poly_coeffs = result[0] if isinstance(result, tuple) else result emb_poly = apply_polynomial_filter(L, X, poly_coeffs) else: emb_heat = generate_baseline_heat_embedding(G, embedding_dim=embedding_dim, random_state=seed) emb_poly = generate_baseline_poly_embedding(G, embedding_dim=embedding_dim, order=config.get('K', 4), random_state=seed) # Fuse the three views embeddings_list = _prep_blocks([emb_sgns, emb_heat, emb_poly]) fused_embeddings = fuse_embeddings_svd(embeddings_list, k=embedding_dim) # Evaluate based on task if task == "node_classification": return train_classifier( fused_embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( fused_embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( fused_embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_ctqw_fusion_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """ Fusion method combining CTQW-based embeddings. Combines: - quvine_ctqw (SGNS with CTQW) - filter_ctqw_heat (CTQW-calibrated heat kernel) - filter_ctqw_poly (CTQW-calibrated polynomial) Uses SVD-based fusion to combine the three views. """ from qbiocode.apps.quvine.fusion.fuse import fuse_embeddings_svd, _prep_blocks from qbiocode.apps.quvine.embedding.quantum_filters import ( get_laplacian, calibrate_heat_kernel, apply_heat_filter, calibrate_polynomial_filter, apply_polynomial_filter, generate_baseline_heat_embedding, generate_baseline_poly_embedding ) config = config or {} embedding_dim = config.get('embedding_dim', 128) # Generate three CTQW-based embeddings emb_sgns = run_quvine_sgns(G, 'ctqw', config, seed) # Generate quantum targets q_targets = generate_quantum_targets_from_walks(G, 'ctqw', config, seed) if q_targets: np.random.seed(seed) N = G.number_of_nodes() L, nodelist, node_to_idx = get_laplacian(G, normalize=True) # Generate random features rng = np.random.default_rng(seed) X = rng.normal(size=(N, embedding_dim)) norm = np.linalg.norm(X, axis=1, keepdims=True) X = X / np.maximum(norm, 1e-12) # Calibrate and apply heat filter t_grid = np.logspace(-2, 2, 40) _, t_star = calibrate_heat_kernel(L, q_targets, t_grid, node_to_idx, loss='l2') emb_heat = apply_heat_filter(L, X, t_star) # Calibrate and apply poly filter result = calibrate_polynomial_filter(L, q_targets, K=config.get('K', 4), node_to_idx=node_to_idx, ridge=1e-6) poly_coeffs = result[0] if isinstance(result, tuple) else result emb_poly = apply_polynomial_filter(L, X, poly_coeffs) else: emb_heat = generate_baseline_heat_embedding(G, embedding_dim=embedding_dim, random_state=seed) emb_poly = generate_baseline_poly_embedding(G, embedding_dim=embedding_dim, order=config.get('K', 4), random_state=seed) # Fuse the three views embeddings_list = _prep_blocks([emb_sgns, emb_heat, emb_poly]) fused_embeddings = fuse_embeddings_svd(embeddings_list, k=embedding_dim) # Evaluate based on task if task == "node_classification": return train_classifier( fused_embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( fused_embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( fused_embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
[docs] def run_dtqw_fusion_adapter( G: nx.Graph, split: Dict[str, Any], task: str, config: Optional[Dict] = None, seed: int = 42 ) -> Dict[str, float]: """ Fusion method combining DTQW-based embeddings. Combines: - quvine_dtqw (SGNS with DTQW) - Baseline heat kernel (as DTQW doesn't have filter variants) - Baseline polynomial (as DTQW doesn't have filter variants) Uses SVD-based fusion to combine the three views. Note: DTQW doesn't have dedicated filter variants, so we use baseline filters. """ from qbiocode.apps.quvine.embedding.quantum_filters import ( generate_baseline_heat_embedding, generate_baseline_poly_embedding ) from qbiocode.apps.quvine.fusion.fuse import fuse_embeddings_svd, _prep_blocks config = config or {} embedding_dim = config.get('embedding_dim', 128) # Generate DTQW-based SGNS embedding using existing helper emb_sgns = run_quvine_sgns(G, 'dtqw', config, seed) # Generate baseline filter embeddings (DTQW doesn't have calibrated filters) emb_heat = generate_baseline_heat_embedding( G=G, embedding_dim=embedding_dim, scale=config.get('scale', 1.0), use_features=False, normalize=True, random_state=seed ) emb_poly = generate_baseline_poly_embedding( G=G, embedding_dim=embedding_dim, order=config.get('order', 4), use_features=False, normalize=True, random_state=seed ) # Fuse the three views embeddings_list = _prep_blocks([emb_sgns, emb_heat, emb_poly]) fused_embeddings = fuse_embeddings_svd(embeddings_list, k=embedding_dim) # Evaluate based on task if task == "node_classification": return train_classifier( fused_embeddings, np.array(split["train_idx"]), np.array(split["val_idx"]), np.array(split["test_idx"]), np.array(split["labels"]), seed ) elif task == "link_prediction": return evaluate_link_prediction( fused_embeddings, split["test_edges"], split["neg_test_edges"] ) elif task == "node_ranking": return evaluate_node_ranking( fused_embeddings, split["seed_nodes"], split["target_nodes"] ) else: raise ValueError(f"Unknown task: {task}")
# ============================================================================ # Method Registry # ============================================================================ METHOD_ADAPTERS = { # Classical baselines "node2vec": run_node2vec_adapter, "netmf": run_netmf_adapter, "graphsage": run_graphsage_adapter, "appnp": run_appnp_adapter, # QuVINE SGNS methods "quvine_rwr": run_quvine_rwr_adapter, "quvine_ctqw": run_quvine_ctqw_adapter, "quvine_dtqw": run_quvine_dtqw_adapter, # Baseline filter methods (no quantum calibration) "baseline_filter_heat": run_baseline_filter_heat_adapter, "baseline_filter_poly": run_baseline_filter_poly_adapter, # QuVINE filter methods (quantum-calibrated) "filter_rwr_heat": run_filter_rwr_heat_adapter, "filter_rwr_poly": run_filter_rwr_poly_adapter, "filter_ctqw_heat": run_filter_ctqw_heat_adapter, "filter_ctqw_poly": run_filter_ctqw_poly_adapter, # GAT variants (12 methods) "gat_baseline": run_gat_baseline_adapter, "gat_heat": run_gat_heat_adapter, "gat_poly": run_gat_poly_adapter, "gat_rwr": run_gat_rwr_adapter, "gat_ctqw": run_gat_ctqw_adapter, "gat_dtqw": run_gat_dtqw_adapter, "gat_rwr_heat": run_gat_rwr_heat_adapter, "gat_rwr_poly": run_gat_rwr_poly_adapter, "gat_ctqw_heat": run_gat_ctqw_heat_adapter, "gat_ctqw_poly": run_gat_ctqw_poly_adapter, "gat_dtqw_heat": run_gat_dtqw_heat_adapter, "gat_dtqw_poly": run_gat_dtqw_poly_adapter, # GraphGPS variants (12 methods) "graphgps_baseline": run_graphgps_baseline_adapter, "graphgps_heat": run_graphgps_heat_adapter, "graphgps_poly": run_graphgps_poly_adapter, "graphgps_rwr": run_graphgps_rwr_adapter, "graphgps_ctqw": run_graphgps_ctqw_adapter, "graphgps_dtqw": run_graphgps_dtqw_adapter, "graphgps_rwr_heat": run_graphgps_rwr_heat_adapter, "graphgps_rwr_poly": run_graphgps_rwr_poly_adapter, "graphgps_ctqw_heat": run_graphgps_ctqw_heat_adapter, "graphgps_ctqw_poly": run_graphgps_ctqw_poly_adapter, "graphgps_dtqw_heat": run_graphgps_dtqw_heat_adapter, "graphgps_dtqw_poly": run_graphgps_dtqw_poly_adapter, # Additional classical baselines "baseline_gcnmf": run_baseline_gcnmf_adapter, "baseline_filter": run_baseline_filter_adapter, # Fusion methods (combine multiple embedding views) "rwr_fusion": run_rwr_fusion_adapter, "ctqw_fusion": run_ctqw_fusion_adapter, "dtqw_fusion": run_dtqw_fusion_adapter, }
[docs] def get_method_adapter(method_name: str): """ Get the adapter function for a method. Parameters ---------- method_name : str Name of the method Returns ------- callable Adapter function Raises ------ NotImplementedError If method is not yet implemented """ if method_name not in METHOD_ADAPTERS: raise NotImplementedError( f"Method '{method_name}' not yet implemented. " f"Available methods: {list(METHOD_ADAPTERS.keys())}" ) return METHOD_ADAPTERS[method_name]