algorise's picture
download
raw
3.13 kB
"""Time-varying directed graphs and compatible row-/column-stochastic mixing matrices.
Follows the paper's Section 2 notation and Assumption 3.2: at each iteration k we
draw a directed ring (guarantees strong connectivity, Assumption 3.1) plus up to
`extra_edges` additional random directed edges (an Erdos-Renyi-style augmentation,
matching the paper's "ER model + directed rings" dynamic-topology construction in
Section 4). Degrees are capped so the resulting mixing weights admit a *uniform*
lower bound a, b > 0 across all k, as Assumption 3.2 requires.
"""
from __future__ import annotations
import numpy as np
class TimeVaryingDigraphs:
"""Generates a fresh strongly-connected directed graph each call to `step`,
and the row-stochastic (A) / column-stochastic (B) matrices compatible with it.
"""
def __init__(self, n: int, extra_edges: int, seed: int = 0):
self.n = n
self.extra_edges = extra_edges
self.rng = np.random.default_rng(seed)
# Uniform lower bounds guaranteed by construction (ring + <= extra_edges extra
# in-/out-edges per node => max in/out degree <= 1 + extra_edges, plus self-loop).
max_degree_plus_self = 2 + extra_edges
self.a_min = 1.0 / max_degree_plus_self
self.b_min = 1.0 / max_degree_plus_self
def step(self):
n = self.n
# Directed ring i -> i+1 (mod n) guarantees strong connectivity on its own;
# each node gets up to `extra_edges` extra random outgoing edges on top.
out_neighbors = [set() for _ in range(n)]
for i in range(n):
out_neighbors[i].add((i + 1) % n) # ring edge i -> i+1
for i in range(n):
possible = [j for j in range(n) if j != i and j not in out_neighbors[i]]
m = min(self.extra_edges, len(possible))
if m > 0:
chosen = self.rng.choice(possible, size=m, replace=False)
for j in chosen:
out_neighbors[i].add(int(j))
in_neighbors = [set() for _ in range(n)]
for i in range(n):
for j in out_neighbors[i]:
in_neighbors[j].add(i)
# Row-stochastic A: node i averages over in-neighbors + self (the "pull" step).
A = np.zeros((n, n))
for i in range(n):
support = sorted(in_neighbors[i] | {i})
w = 1.0 / len(support)
for j in support:
A[i, j] = w
# Column-stochastic B: node i splits its own weight across out-neighbors + self
# (the "push" step) -> column i sums to 1.
B = np.zeros((n, n))
for i in range(n):
support = sorted(out_neighbors[i] | {i})
w = 1.0 / len(support)
for j in support:
B[j, i] = w
assert np.allclose(A.sum(axis=1), 1.0)
assert np.allclose(B.sum(axis=0), 1.0)
return A, B
def static_ring_matrices(n: int, extra_edges: int, seed: int = 0):
"""A single fixed (non-time-varying) topology, for the Static_FAB ablation."""
gen = TimeVaryingDigraphs(n, extra_edges, seed)
return gen.step()

Xet Storage Details

Size:
3.13 kB
·
Xet hash:
415d391485e91e7aea5a2cda7336f7cc237f34839911ab87c89a97eb12b639bb

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.