Skip to content

Commit e7370d6

Browse files
committed
feat(03-01): implement apply_to_graph_timeseries for per-node cascade scaling
- ScenarioProposal.apply_to_graph_timeseries(baseline_2d) applies per-node magnitude scaling from metadata['affected_nodes'] - Seed nodes (mag=1.0): full scenario transformation - Hop-1 nodes (mag=0.7): blended baseline + 0.7 * (transform - baseline) - Hop-2 nodes (mag=0.49): blended baseline + 0.49 * (transform - baseline) - Unaffected nodes: unchanged baseline - Fallback to uniform transformation when no graph_aware metadata - All 24 graph proposer tests pass, all 28 existing tests pass
1 parent 37cff8a commit e7370d6

1 file changed

Lines changed: 51 additions & 0 deletions

File tree

src/fyp/selfplay/proposer.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,57 @@ def apply_to_timeseries(self, baseline: np.ndarray) -> np.ndarray:
6060
start_idx=start_idx,
6161
)
6262

63+
def apply_to_graph_timeseries(self, baseline: np.ndarray) -> np.ndarray:
64+
"""Transform multi-node baseline consumption with per-node cascade magnitudes.
65+
66+
For graph-aware scenarios, each node's time series is scaled by its
67+
cascade magnitude from metadata["affected_nodes"]. Seed nodes get
68+
the full transformation, while neighbors get a blended version
69+
proportional to their cascade decay magnitude.
70+
71+
For non-graph scenarios (no "graph_aware" in metadata), applies the
72+
uniform transformation to every node row (same as apply_to_timeseries
73+
broadcast across nodes).
74+
75+
Args:
76+
baseline: 2-D array of shape [num_nodes, timesteps]
77+
78+
Returns:
79+
Transformed 2-D array with per-node scenario applied
80+
"""
81+
if baseline.ndim != 2:
82+
raise ValueError(
83+
f"Expected 2-D array [num_nodes, timesteps], got shape {baseline.shape}"
84+
)
85+
86+
num_nodes, timesteps = baseline.shape
87+
result = np.empty_like(baseline)
88+
89+
if not self.metadata.get("graph_aware", False):
90+
# No graph info: apply uniform transformation to every node
91+
for i in range(num_nodes):
92+
result[i] = self.apply_to_timeseries(baseline[i])
93+
return result
94+
95+
affected_nodes = self.metadata.get("affected_nodes", {})
96+
97+
for i in range(num_nodes):
98+
full_transform = self.apply_to_timeseries(baseline[i])
99+
magnitude = affected_nodes.get(i, 0.0)
100+
101+
if magnitude >= 1.0:
102+
# Seed node: full transformation
103+
result[i] = full_transform
104+
elif magnitude > 0.0:
105+
# Cascade neighbor: blended transformation
106+
# baseline + magnitude * (transformed - baseline)
107+
result[i] = baseline[i] + magnitude * (full_transform - baseline[i])
108+
else:
109+
# Unaffected node: no transformation
110+
result[i] = baseline[i].copy()
111+
112+
return result
113+
63114
def get_verification_constraints(self) -> list[str]:
64115
"""Return physics constraints this scenario must satisfy.
65116

0 commit comments

Comments
 (0)