Compare alternative implementations ("approaches") for phase gradient and phase curvature and record their differences across topologies. This lets us "see what happens" when using naive linear methods vs canonical circular (wrapped) methods on S1.
Outputs JSONL to a specified path with per-run metrics including correlations and mean absolute deviations between approaches.
Usage (PowerShell):
python benchmarks/field_methods_battery.py
--export results/field_methods_battery.jsonl
--topologies ring,ws,scale_free --sizes 30 --runs 5 --seed 42
"""Field Methods Battery
=========================
Compare alternative implementations ("approaches") for phase gradient and
phase curvature and record their differences across topologies. This lets
us "see what happens" when using naive linear methods vs canonical
circular (wrapped) methods on S1.
Outputs JSONL to a specified path with per-run metrics including
correlations and mean absolute deviations between approaches.
Usage (PowerShell):
python benchmarks/field_methods_battery.py \
--export results/field_methods_battery.jsonl \
--topologies ring,ws,scale_free --sizes 30 --runs 5 --seed 42
"""
from __future__ import annotations
import argparse
import json
import math
import random
import sys as _sys
# Ensure local src is importable
from pathlib import Path
from pathlib import Path as _Path
from typing import Any, Dict, Iterable
import networkx as nx
import numpy as np
_ROOT = _Path(__file__).resolve().parents[1]
_SRC = _ROOT / "src"
if str(_SRC) not in _sys.path:
_sys.path.insert(0, str(_SRC))
from tnfr.physics.fields import ( # type: ignore # noqa: E402
compute_phase_curvature,
compute_phase_gradient,
)
def make_graph(topology: str, n: int, seed: int) -> nx.Graph:
random.seed(seed)
if topology == "ring":
return nx.cycle_graph(n)
if topology == "ws":
return nx.watts_strogatz_graph(n, k=min(4, n - 1), p=0.3, seed=seed)
if topology == "scale_free":
return nx.scale_free_graph(n, seed=seed).to_undirected()
if topology == "grid":
import math as _m
side = max(2, int(_m.sqrt(n)))
G = nx.grid_2d_graph(side, side)
mapping = {node: i for i, node in enumerate(G.nodes())}
return nx.relabel_nodes(G, mapping)
if topology == "tree":
G = nx.balanced_tree(r=2, h=max(1, int(math.log2(n))))
if G.number_of_nodes() > n:
nodes_to_remove = list(G.nodes())[n:]
G.remove_nodes_from(nodes_to_remove)
return G
raise ValueError(f"Unknown topology: {topology}")
def init_state(G: nx.Graph, seed: int) -> None:
random.seed(seed)
for node in G.nodes:
# Phases spread to hit wrap-around edge cases
G.nodes[node]["phase"] = random.uniform(0.0, 2 * math.pi)
G.nodes[node]["theta"] = G.nodes[node]["phase"]
# Alternative (naive) methods -------------------------------------------------
def naive_phase_gradient(G: nx.Graph) -> Dict[Any, float]:
grad: Dict[Any, float] = {}
for i in G.nodes():
neigh = list(G.neighbors(i))
if not neigh:
grad[i] = 0.0
continue
phi_i = float(G.nodes[i].get("phase", 0.0))
diffs = [abs(phi_i - float(G.nodes[j].get("phase", 0.0))) for j in neigh]
grad[i] = float(np.mean(diffs))
return grad
def naive_phase_curvature(G: nx.Graph) -> Dict[Any, float]:
curv: Dict[Any, float] = {}
for i in G.nodes():
neigh = list(G.neighbors(i))
if not neigh:
curv[i] = 0.0
continue
phi_i = float(G.nodes[i].get("phase", 0.0))
mean_lin = float(np.mean([float(G.nodes[j].get("phase", 0.0)) for j in neigh]))
curv[i] = float(phi_i - mean_lin)
return curv
# Metrics --------------------------------------------------------------------
def compare_fields(a: Dict[Any, float], b: Dict[Any, float]) -> Dict[str, float]:
nodes = list(set(a.keys()) & set(b.keys()))
if not nodes:
return {"corr": 0.0, "mad": 0.0, "rmse": 0.0}
va = np.array([a[n] for n in nodes], dtype=float)
vb = np.array([b[n] for n in nodes], dtype=float)
if len(nodes) >= 3:
C = np.corrcoef(va, vb)
corr = float(C[0, 1])
else:
corr = 0.0
mad = float(np.mean(np.abs(va - vb)))
rmse = float(np.sqrt(np.mean((va - vb) ** 2)))
return {"corr": corr, "mad": mad, "rmse": rmse}
# CLI ------------------------------------------------------------------------
def parse_args(argv: Iterable[str]) -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Field Methods Battery (compare naive vs canonical)"
)
p.add_argument("--topologies", type=str, default="ring,ws,scale_free")
p.add_argument("--sizes", type=str, default="30")
p.add_argument("--runs", type=int, default=5)
p.add_argument("--seed", type=int, default=42)
p.add_argument("--export", type=str, default="results/field_methods_battery.jsonl")
return p.parse_args(list(argv))
def main(argv: Iterable[str]) -> int:
args = parse_args(argv)
random.seed(args.seed)
topologies = [t.strip() for t in args.topologies.split(",") if t.strip()]
sizes = [int(s) for s in args.sizes.split(",") if s.strip()]
out_path = Path(args.export)
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("w", encoding="utf-8") as f_out:
for topo in topologies:
for n in sizes:
for run in range(args.runs):
seed = args.seed + run + n
G = make_graph(topo, n, seed)
init_state(G, seed + 99)
# Canonical
grad_can = compute_phase_gradient(G)
curv_can = compute_phase_curvature(G)
# Naive
grad_nv = naive_phase_gradient(G)
curv_nv = naive_phase_curvature(G)
cmp_grad = compare_fields(grad_can, grad_nv)
cmp_curv = compare_fields(curv_can, curv_nv)
rec = {
"task": "field_methods_battery",
"topology": topo,
"n": n,
"seed": seed,
"cmp_grad": cmp_grad,
"cmp_curv": cmp_curv,
}
f_out.write(json.dumps(rec) + "\n")
print(f"Field methods battery complete. Output: {out_path}")
return 0
if __name__ == "__main__": # pragma: no cover
import sys
raise SystemExit(main(sys.argv[1:]))