Skip to content

Commit 6b25531

Browse files
committed
Virtual nodes separated
1 parent 578ce92 commit 6b25531

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

src/tensor_network_viz/_core/layout/body.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import math
6+
from collections import defaultdict
67
from collections.abc import Iterable
78

89
import numpy as np
@@ -173,6 +174,7 @@ def _compute_component_layout_2d(
173174
)
174175

175176
_snap_virtual_nodes_to_barycenters(component, positions)
177+
_spread_colocated_virtual_hubs_2d(component, positions)
176178
_place_trimmed_leaf_nodes_2d(component, positions)
177179
return _center_positions(positions, node_ids=node_ids)
178180

@@ -244,6 +246,46 @@ def _snap_virtual_nodes_to_barycenters(
244246
)
245247

246248

249+
def _spread_colocated_virtual_hubs_2d(
250+
component: _LayoutComponent,
251+
positions: NodePositions,
252+
) -> None:
253+
"""Separate virtual hubs that share the same visible neighbors (identical barycenters)."""
254+
groups: defaultdict[frozenset[int], list[int]] = defaultdict(list)
255+
graph_nx = component.contraction_graph
256+
for vid in component.virtual_node_ids:
257+
groups[frozenset(graph_nx.neighbors(vid))].append(vid)
258+
259+
spacing = float(_VIRTUAL_HUB_MIN_SEPARATION)
260+
for neighbor_set, vids in groups.items():
261+
if len(vids) < 2:
262+
continue
263+
neighbors = sorted(neighbor_set, key=lambda nid: float(positions[nid][0]))
264+
base = np.mean(
265+
np.stack([positions[nid] for nid in neighbors]),
266+
axis=0,
267+
)
268+
if len(neighbors) >= 2:
269+
p0 = np.asarray(positions[neighbors[0]], dtype=float).reshape(-1)[:2]
270+
p1 = np.asarray(positions[neighbors[-1]], dtype=float).reshape(-1)[:2]
271+
chord = p1 - p0
272+
chord_len = float(np.linalg.norm(chord))
273+
if chord_len > 1e-9:
274+
perp = np.array([-chord[1], chord[0]], dtype=float) / chord_len
275+
else:
276+
perp = np.array([0.0, 1.0], dtype=float)
277+
else:
278+
perp = np.array([0.0, 1.0], dtype=float)
279+
280+
vids_sorted = sorted(vids)
281+
n_v = len(vids_sorted)
282+
offsets = np.linspace(-0.5 * (n_v - 1), 0.5 * (n_v - 1), n_v)
283+
for vid, off in zip(vids_sorted, offsets, strict=True):
284+
pos = np.asarray(positions[vid], dtype=float).copy()
285+
pos[:2] = base[:2] + perp * spacing * float(off)
286+
positions[vid] = pos
287+
288+
247289
def _place_trimmed_leaf_nodes_2d(
248290
component: _LayoutComponent,
249291
positions: NodePositions,
@@ -1104,5 +1146,6 @@ def _orthogonal_unit(vector: Vector) -> Vector:
11041146
"_segment_point_min_distance_sq_3d",
11051147
"_segments_cross_2d",
11061148
"_snap_virtual_nodes_to_barycenters",
1149+
"_spread_colocated_virtual_hubs_2d",
11071150
"_used_axis_directions",
11081151
]

src/tensor_network_viz/_core/layout/parameters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
_LAYOUT_BOND_CURVE_NEAR_PAIR_REF: float = 0.28
2121
_LAYOUT_BOND_CURVE_SAMPLES: int = 24
2222
_COMPONENT_GAP: float = 1.4
23+
# Minimum spacing (layout units) between virtual hyperedge hubs that share the same tensor neighbors
24+
# (otherwise they collapse to the same barycenter).
25+
_VIRTUAL_HUB_MIN_SEPARATION: float = 0.38
2326
_LAYER_SPACING: float = 0.55
2427
_LAYER_SEQUENCE: tuple[int, ...] = (0, 1, -1, 2, -2, 3, -3)
2528

@@ -30,6 +33,7 @@
3033
"_FREE_DIR_OVERLAP_THRESHOLD",
3134
"_FREE_DIR_SAMPLES_2D",
3235
"_LAYER_SEQUENCE",
36+
"_VIRTUAL_HUB_MIN_SEPARATION",
3337
"_LAYER_SPACING",
3438
"_LAYOUT_BOND_CURVE_NEAR_PAIR_REF",
3539
"_LAYOUT_BOND_CURVE_OFFSET_FACTOR",

0 commit comments

Comments
 (0)