algorise's picture
download
raw
9.14 kB
"""
Synthetic "online shortest path with a long-term delay constraint" testbed,
built to mirror the structure of the paper's own experiments (Section 7 /
Appendix E: online shortest path over network measurements, comparing
against a fixed-horizon + doubling-trick baseline). We use a synthetic
layered graph and synthetic non-stationary/adversarial edge traffic instead
of the paper's RIPE-Atlas-derived dataset (no network dataset access is
required here), while keeping the same problem structure: a decision set
that is a probability simplex over candidate paths, linear round costs
(path latency-under-tolls) and linear round constraints (path delay budget).
Scale: N=16 nodes in 4 layers (width 4), 64 source-sink paths, K=48
candidate paths sub-sampled as the action set, T up to 40,000 rounds -- not
a toy size for a convex-optimization OCO experiment (this is exactly the
"vector of size K, T iterations" regime the paper's own Theorem statements
are about), and each full run costs well under a second of CPU time.
"""
from __future__ import annotations
import numpy as np
class PathNetwork:
def __init__(self, layer_width: int = 4, n_layers: int = 3, n_paths: int = 48, seed: int = 0):
rng = np.random.default_rng(seed)
# layers: source(1) -> L1(w) -> L2(w) -> ... -> Ln(w) -> sink(1)
self.layer_width = layer_width
self.n_layers = n_layers
layers = [1] + [layer_width] * n_layers + [1]
# enumerate all source-sink paths as sequences of node indices per layer
choices = [range(w) for w in layers[1:-1]]
import itertools
all_paths = list(itertools.product(*choices))
rng.shuffle(all_paths)
self.paths = all_paths[:n_paths]
self.K = len(self.paths)
self.layers = layers
# base (mean) edge cost / delay per (layer, from_idx, to_idx)
self.n_edge_layers = n_layers + 1
self.base_cost = [rng.uniform(0.5, 2.0, size=(layers[i], layers[i + 1])) for i in range(self.n_edge_layers)]
self.base_delay = [rng.uniform(0.5, 2.0, size=(layers[i], layers[i + 1])) for i in range(self.n_edge_layers)]
# per-edge drift phase/frequency for smooth non-stationarity
self.cost_phase = [rng.uniform(0, 2 * np.pi, size=b.shape) for b in self.base_cost]
self.delay_phase = [rng.uniform(0, 2 * np.pi, size=b.shape) for b in self.base_delay]
self.rng = rng
def _edge_path_indices(self, path):
# returns list of (layer, i, j) edges along this path (source and sink are singleton layers)
seq = [0] + list(path) + [0]
return list(enumerate(zip(seq[:-1], seq[1:])))
def edge_values(self, t: int, noise_scale: float, spike_prob: float, spike_scale: float, rng: np.random.Generator):
"""Return (cost_edges, delay_edges): lists of matrices, one per edge-layer,
for round t. Smooth drift + gaussian noise + rare adversarial spikes."""
cost_edges = []
delay_edges = []
for li in range(self.n_edge_layers):
drift_c = 0.5 * np.sin(0.01 * t + self.cost_phase[li])
drift_d = 0.5 * np.sin(0.013 * t + self.delay_phase[li] + 1.0)
noise_c = rng.normal(0, noise_scale, size=self.base_cost[li].shape)
noise_d = rng.normal(0, noise_scale, size=self.base_delay[li].shape)
spikes_c = (rng.uniform(size=self.base_cost[li].shape) < spike_prob) * spike_scale
spikes_d = (rng.uniform(size=self.base_delay[li].shape) < spike_prob) * spike_scale
c = np.clip(self.base_cost[li] + drift_c + noise_c + spikes_c, 0.05, None)
d = np.clip(self.base_delay[li] + drift_d + noise_d + spikes_d, 0.05, None)
cost_edges.append(c)
delay_edges.append(d)
return cost_edges, delay_edges
def path_vectors(self, cost_edges, delay_edges):
c_vec = np.zeros(self.K)
d_vec = np.zeros(self.K)
for k, path in enumerate(self.paths):
edges = self._edge_path_indices(path)
c_vec[k] = sum(cost_edges[li][i, j] for li, (i, j) in edges)
d_vec[k] = sum(delay_edges[li][i, j] for li, (i, j) in edges)
return c_vec, d_vec
def make_round_sequence(
net: PathNetwork,
T: int,
G: float,
delay_budget_quantile: float = 0.55,
noise_scale: float = 0.15,
spike_prob: float = 0.01,
spike_scale: float = 3.0,
seed: int = 1,
):
"""Precompute T rounds of (c_t, d_t) path-cost/path-delay vectors, each
rescaled to have L2 norm exactly G (so the Lipschitz constant of every
round's linear cost/constraint function over the simplex is exactly G,
letting us check the paper's constants exactly rather than up to unknown
slack). Returns c_seq (T,K), d_seq (T,K), and the delay budget Delta.
"""
rng = np.random.default_rng(seed)
c_raw = np.zeros((T, net.K))
d_raw = np.zeros((T, net.K))
for t in range(1, T + 1):
cost_edges, delay_edges = net.edge_values(t, noise_scale, spike_prob, spike_scale, rng)
c_vec, d_vec = net.path_vectors(cost_edges, delay_edges)
c_raw[t - 1] = c_vec
d_raw[t - 1] = d_vec
# delay budget: a quantile of the raw per-path delays, so the constraint
# is neither vacuous nor infeasible for every path
Delta = float(np.quantile(d_raw, delay_budget_quantile))
# Guarantee path 0 is a "reliable low-delay route": its raw delay is kept
# well under Delta in every single round. This guarantees the globally
# feasible set {x : g_t(x)<=0 for all t} is non-empty (contains at least
# the vertex e_0), so the static-regret LP comparator below is always
# solvable -- without this, T rounds of independent random constraints
# can jointly rule out every fixed action.
safe_level = 0.15 * Delta
d_raw[:, 0] = safe_level + rng.uniform(0, 0.02 * max(Delta, 1e-6), size=T)
d_centered = d_raw - Delta
def rescale(mat):
norms = np.linalg.norm(mat, axis=1, keepdims=True)
norms = np.maximum(norms, 1e-9)
return mat / norms * G
c_seq = rescale(c_raw)
d_seq = rescale(d_centered)
return c_seq, d_seq
def iid_adversarial_sequence(K: int, T: int, G: float, seed: int, violation_rate: float = 0.5):
"""A harder, structure-free instance: i.i.d. random unit-norm cost and
constraint vectors each round (no smooth drift to exploit). This is the
classic hard case for online linear optimization -- even the best FIXED
action in hindsight is itself a noisy quantity, which is what forces
Theta(sqrt(t)) regret for *any* algorithm and lets us see the claimed
growth rate directly, rather than the near-zero regret a trackable,
slowly-drifting environment produces."""
rng = np.random.default_rng(seed)
c_raw = rng.standard_normal((T, K))
d_raw = rng.standard_normal((T, K))
Delta = float(np.quantile(d_raw, violation_rate))
d_raw[:, 0] = np.quantile(d_raw, 0.05) * np.ones(T) # guaranteed-safe column
d_centered = d_raw - Delta
def rescale(mat):
norms = np.linalg.norm(mat, axis=1, keepdims=True)
return mat / np.maximum(norms, 1e-9) * G
return rescale(c_raw), rescale(d_centered)
def block_adversarial_sequence(K: int, T: int, block_size: int, G: float, seed: int, violation_rate: float = 0.5):
"""Piecewise-constant adversarial sequence: cost/constraint vectors are
redrawn i.i.d. every `block_size` rounds and held fixed within a block.
This gives a direct, controllable knob on the comparator's path length
P_T for the dynamic-regret experiment: the per-round best-response
minimizer only moves between blocks, so smaller `block_size` (more,
smaller blocks) yields larger P_T, while larger `block_size` yields
smaller P_T -- without needing thousands of independent LP solves
(only one LP per unique block is needed, since the optimum is constant
within a block).
"""
rng = np.random.default_rng(seed)
n_blocks = int(np.ceil(T / block_size))
c_blocks = rng.standard_normal((n_blocks, K))
d_blocks = rng.standard_normal((n_blocks, K))
Delta = float(np.quantile(d_blocks, violation_rate))
d_blocks[:, 0] = np.quantile(d_blocks, 0.05) # guaranteed-safe column, all blocks
d_blocks = d_blocks - Delta
def rescale(mat):
norms = np.linalg.norm(mat, axis=1, keepdims=True)
return mat / np.maximum(norms, 1e-9) * G
c_blocks = rescale(c_blocks)
d_blocks = rescale(d_blocks)
block_id = np.repeat(np.arange(n_blocks), block_size)[:T]
c_seq = c_blocks[block_id]
d_seq = d_blocks[block_id]
return c_seq, d_seq, block_id, c_blocks, d_blocks
def cost_constraint_fns(c_seq: np.ndarray, d_seq: np.ndarray):
"""Build cost_fn(t,x)->(f,grad) and constraint_fn(t,x)->(g,grad) closures
for linear round functions f_t(x)=<c_t,x>, g_t(x)=<d_t,x> (already budget-centered)."""
def cost_fn(t, x):
c = c_seq[t - 1]
return float(np.dot(c, x)), c
def constraint_fn(t, x):
d = d_seq[t - 1]
return float(np.dot(d, x)), d
return cost_fn, constraint_fn

Xet Storage Details

Size:
9.14 kB
·
Xet hash:
f77c09b6050b523b5ebc19729484eff928b3a4a750f016471d31a1ed0d945acc

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