Skip to content

Commit 93e0396

Browse files
committed
topology generator
1 parent 9819d7f commit 93e0396

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

examples/topologies_examples.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""
2+
Topology gallery for the 10 representative scenarios used in power/FPR analysis.
3+
4+
For each scenario, displays:
5+
- Ground-truth effect mask (binary/weighted)
6+
- Uncorrected signed t-statistic from simulated two-sample data
7+
8+
Generation parameters match fpr_config_local.yaml / sweep_config_power_local.yaml:
9+
n_nodes=60, n_modules=4, intra_corr=0.3, inter_corr=0.05, noise_level=0.05
10+
11+
Usage:
12+
python examples/topologies_examples.py
13+
python examples/topologies_examples.py --output-dir results/topologies
14+
python examples/topologies_examples.py --effect-size 0.5 --n-samples 50
15+
"""
16+
from __future__ import annotations
17+
18+
import argparse
19+
import sys
20+
from pathlib import Path
21+
from typing import List, Optional
22+
23+
import numpy as np
24+
25+
sys.path.append(str(Path(__file__).parent.parent))
26+
27+
from examples.sim_topology_examples import TopologyDatasetGenerator, _draw_module_boundaries
28+
from tfnbs.pairwise_stats import compute_t_stat
29+
30+
# The 10 scenarios used in power analysis and FPR calibration configs.
31+
SCENARIOS: List[str] = [
32+
"within_module_dense",
33+
"between_modules_dense",
34+
"partial_bipartite_between_modules",
35+
"gradient_core_periphery_within_module",
36+
"scattered_cross_block",
37+
"hub",
38+
"rich_club",
39+
"cross_block_connected_chain",
40+
"chain",
41+
"fragmented_within_module",
42+
]
43+
44+
# Short display names for plot titles.
45+
DISPLAY_NAMES = {
46+
"within_module_dense": "Within-module dense",
47+
"between_modules_dense": "Between-modules dense",
48+
"partial_bipartite_between_modules": "Partial bipartite",
49+
"gradient_core_periphery_within_module": "Core-periphery gradient",
50+
"scattered_cross_block": "Scattered cross-block",
51+
"hub": "Hub (star)",
52+
"rich_club": "Rich club",
53+
"cross_block_connected_chain": "Cross-block chain",
54+
"chain": "Chain",
55+
"fragmented_within_module": "Fragmented within-module",
56+
}
57+
58+
59+
def plot_topology_gallery(
60+
n_nodes: int = 60,
61+
n_modules: int = 4,
62+
intra_corr: float = 0.3,
63+
inter_corr: float = 0.05,
64+
noise_level: float = 0.05,
65+
effect_size: float = 0.3,
66+
n_samples: int = 30,
67+
seed: int = 42,
68+
output_dir: Path = Path("examples/output"),
69+
) -> Path:
70+
"""Generate a 10x2 gallery figure: ground truth + t-stat for each scenario."""
71+
import matplotlib
72+
matplotlib.use("Agg")
73+
import matplotlib.pyplot as plt
74+
75+
gen = TopologyDatasetGenerator(
76+
n_nodes=n_nodes,
77+
n_modules=n_modules,
78+
intra_corr=intra_corr,
79+
inter_corr=inter_corr,
80+
noise_level=noise_level,
81+
seed=seed,
82+
)
83+
84+
nrows = 5
85+
ncols = 4 # 2 scenarios per row × 2 panels (GT + t-stat)
86+
fig, axes = plt.subplots(nrows, ncols, figsize=(16, 20))
87+
88+
for idx, scenario_name in enumerate(SCENARIOS):
89+
row = idx // 2
90+
col_base = (idx % 2) * 2 # 0 or 2
91+
92+
ds = gen.generate(
93+
scenario_name,
94+
effect_size=effect_size,
95+
n_samples=n_samples,
96+
time_points=30,
97+
)
98+
99+
g1_z, g2_z = ds.fisher_z()
100+
t_dict = compute_t_stat(g1_z, g2_z, test_type="two-sample")
101+
t_signed = t_dict["g2>g1"] - t_dict["g1>g2"]
102+
103+
effect_gt = ds.effect_mask * ds.effect_size
104+
n_edges = int(np.sum(ds.effect_mask != 0) // 2)
105+
display_name = DISPLAY_NAMES.get(scenario_name, scenario_name)
106+
107+
# Ground truth panel
108+
ax_gt = axes[row, col_base]
109+
max_gt = float(np.max(np.abs(effect_gt))) if np.any(effect_gt) else 1.0
110+
im_gt = ax_gt.imshow(effect_gt, cmap="seismic", vmin=-max_gt, vmax=max_gt)
111+
ax_gt.set_title(f"{display_name}\nGT ({n_edges} edges)", fontsize=9)
112+
_draw_module_boundaries(ax_gt, ds.net_labels)
113+
fig.colorbar(im_gt, ax=ax_gt, fraction=0.046, pad=0.04)
114+
115+
# T-stat panel
116+
ax_t = axes[row, col_base + 1]
117+
max_t = float(np.max(np.abs(t_signed))) if np.any(t_signed) else 1.0
118+
im_t = ax_t.imshow(t_signed, cmap="seismic", vmin=-max_t, vmax=max_t)
119+
ax_t.set_title(f"{display_name}\nt-stat (uncorrected)", fontsize=9)
120+
_draw_module_boundaries(ax_t, ds.net_labels)
121+
fig.colorbar(im_t, ax=ax_t, fraction=0.046, pad=0.04)
122+
123+
for ax in (ax_gt, ax_t):
124+
ax.set_xticks([])
125+
ax.set_yticks([])
126+
127+
fig.suptitle(
128+
f"Topology scenarios | effect_size={effect_size} | "
129+
f"n_samples={n_samples} | {n_nodes} nodes, {n_modules} modules",
130+
fontsize=13,
131+
y=0.995,
132+
)
133+
plt.tight_layout(rect=[0, 0, 1, 0.98])
134+
135+
output_dir = Path(output_dir)
136+
output_dir.mkdir(parents=True, exist_ok=True)
137+
out_path = output_dir / f"topologies_gallery_es{effect_size:g}_n{n_samples}.png"
138+
fig.savefig(out_path, dpi=150, bbox_inches="tight")
139+
plt.close(fig)
140+
return out_path
141+
142+
143+
def main(argv: Optional[List[str]] = None) -> int:
144+
parser = argparse.ArgumentParser(
145+
description="Plot ground-truth masks and t-stats for the 10 representative topologies."
146+
)
147+
parser.add_argument("--n-nodes", type=int, default=60)
148+
parser.add_argument("--n-modules", type=int, default=4)
149+
parser.add_argument("--intra-corr", type=float, default=0.3)
150+
parser.add_argument("--inter-corr", type=float, default=0.05)
151+
parser.add_argument("--noise-level", type=float, default=0.05)
152+
parser.add_argument("--effect-size", type=float, default=0.3)
153+
parser.add_argument("--n-samples", type=int, default=30)
154+
parser.add_argument("--seed", type=int, default=42)
155+
parser.add_argument(
156+
"--output-dir",
157+
type=Path,
158+
default=Path(__file__).parent.parent / "results" / "topologies",
159+
)
160+
args = parser.parse_args(argv)
161+
162+
out_path = plot_topology_gallery(
163+
n_nodes=args.n_nodes,
164+
n_modules=args.n_modules,
165+
intra_corr=args.intra_corr,
166+
inter_corr=args.inter_corr,
167+
noise_level=args.noise_level,
168+
effect_size=args.effect_size,
169+
n_samples=args.n_samples,
170+
seed=args.seed,
171+
output_dir=args.output_dir,
172+
)
173+
print(f"Saved: {out_path}")
174+
return 0
175+
176+
177+
if __name__ == "__main__":
178+
raise SystemExit(main())

0 commit comments

Comments
 (0)