Source code for qbiocode.apps.quvine.views.views
# 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.
import networkx as nx
import numpy as np
[docs]
def constrained_view(
G,
root,
num_views,
max_nodes,
max_edges,
max_degree,
rng,
degree_norm=False,
degree_alpha=0.5,
eps=1e-6,
):
"""
Build constrained ego-views with optional degree-normalized expansion.
Parameters
----------
degree_norm : bool
If True, neighbors are sampled with probability proportional to
(deg + eps)^(-degree_alpha)
degree_alpha : float
Strength of degree downweighting (0.5 recommended)
"""
neighbors = list(G.neighbors(root))
# Isolated root: no edges to traverse, no valid view possible.
# This can happen if the full graph G has isolated nodes (e.g. last-resort
# random fill in subgraph.py). Return no views so the walk loop skips it.
if not neighbors:
return []
views = []
def sample_frontier(nodes):
if not degree_norm or len(nodes) == 0:
rng.shuffle(nodes)
return nodes
degrees = np.array([G.degree(v) for v in nodes], dtype=float)
weights = 1.0 / np.power(degrees + eps, degree_alpha)
weights /= weights.sum()
# sample without replacement
return list(rng.choice(nodes, size=len(nodes), replace=False, p=weights))
for _ in range(num_views):
view_nodes = {root}
view_edges = 0
frontier = sample_frontier(neighbors.copy())
while frontier:
candidate = frontier.pop()
if candidate in view_nodes:
continue
# count incremental edges only
new_edges = sum(
1 for v in view_nodes if G.has_edge(candidate, v)
)
if view_edges + new_edges > max_edges:
continue
if new_edges > max_degree:
continue
view_nodes.add(candidate)
view_edges += new_edges
# expand frontier
for nbr in G.neighbors(candidate):
if nbr in view_nodes:
continue
if not degree_norm:
frontier.append(nbr)
else:
# probabilistic degree-normalized frontier growth
deg = G.degree(nbr)
p_keep = 1.0 / np.power(deg + eps, degree_alpha)
if rng.random() < min(1.0, p_keep):
frontier.append(nbr)
if len(view_nodes) >= max_nodes or view_edges >= max_edges:
break
# Only keep views where root has at least one neighbour (len > 1).
# A {root}-only view can arise when all frontier candidates fail the
# constraint checks; passing it to a quantum walk causes a crash.
if len(view_nodes) > 1:
views.append(view_nodes)
return views
# def constrained_view(G,
# root,
# num_views,
# max_nodes,
# max_edges,
# max_degree,
# rng
# ):
# neighbors = list(G.neighbors(root))
# views = []
# for _ in range(num_views):
# view_nodes = {root}
# view_edges = 0
# #randomize expansion order
# frontier = list(neighbors)
# rng.shuffle(frontier)
# while frontier:
# candidate_node = frontier.pop()
# if candidate_node in view_nodes:
# continue
# #compute incremental edges only
# new_edges = sum(1 for v in view_nodes if G.has_edge(candidate_node, v))
# if view_edges + new_edges > max_edges:
# continue
# #local degree check
# if new_edges > max_degree:
# continue
# view_nodes.add(candidate_node)
# view_edges += new_edges
# for neighbor in G.neighbors(candidate_node):
# if neighbor not in view_nodes:
# frontier.append(neighbor)
# #enforce sizes
# if len(view_nodes) >= max_nodes or view_edges >= max_edges:
# break
# views.append(view_nodes)
# return views