shun hace 2 meses
padre
commit
803969bfe4

+ 4 - 0
add_linkselfie/.gitignore

@@ -0,0 +1,4 @@
+__pycache__/
+*.pdf
+outputs/
+.DS_Store

BIN
add_linkselfie/__pycache__/evaluation.cpython-38.pyc


+ 20 - 0
add_linkselfie/dump.txt

@@ -0,0 +1,20 @@
+
+add_linkselfieの主要構成
+
+
+evaluation.py
+main.py
+network.py
+nb_protocol.py
+	/metrics
+		widths.py
+	/viz
+		plots.py
+	/outputs
+	
+	/schedulers
+		greedy_scheduler.py
+  		__init__.py
+		lnaive_nb.py
+		lnaive_scheduler.py
+  		lonline_nb.py

+ 4 - 39
add_linkselfie/evalationmemo.txt

@@ -1,42 +1,7 @@
-ノードS〜ノードD 間のリンク集合 L だけでなく、
-ノードS〜ノードD_n間のリンク集合 L_n (1 <= n <= N, N はノードSの隣接ノード数)がそれぞれ入力される。
-ノードS〜ノードD_n間の重要度 I_n (0〜1 の値) が入力として与えられる。
-総バウンスコスト C が入力として与えられる。
-この時、ノードS〜ノードD_n のノードペアの中で、
-K 個のノードペア (S, D_s_1), (S, D_s_2), ... (S, D_s_K) における
-忠実度が最大のリンクをそれぞれ発見する。
-ただし、発見するリンクのノードペア数 K は、
-重要度と忠実度の積の総和、つまり
-I = \sum_{k = 1}^K I_s_k * F_s_k 
-が最大となるように定める。
-ここで F_n は (S, D_n) 間において忠実度の最大のリンクの忠実度である。
-
-A. この拡張問題は、複数のノードペア (S, Dₙ) に対して、限られたリソース(バウンスコスト)の中で、重要なノードペアの
-高忠実度リンクを選定する最適化問題です。以下に、この問題の形式的な定義を記述します。
-
-
---------------------------------------------------------
-
-この拡張問題を最適化問題として扱うにはどうすればいいのかを考えました
-->
-全リンクの I_d × (UB - LB) の総和 (重要度を幅に乗算)を最小化する最適化
-問題とする?方向性を考えてます
-
-ここで、UB,LBは信頼区間の上界と下界です。
-たくさん測定をするとUB-LBは小さくなります
-
-現時点では提案手法は予想とは裏腹にnaiveよりも性能が低いという結果にな
-りました。(提案手法では需要が高い宛先の中で忠実度が中程度のリンクに資
-源を割かないため)
-
-
-全リンクではなく、各宛先の最大推定忠実度のI_d × (UB - LB)
-7) plot_minwidthsum_perpair_weighted_vs_budget_{NOISE}.pdf
-のみで提案手法が性能が優れているという結果です。
-
-
-
-現段階で新しく実装している評価指標
+[出力PDF一覧と内容まとめ]
+(注) {NOISE} は main.py の noise_model_list の要素名に置換されます (例: Depolar)。
+PDF はカレントディレクトリに保存。幅系グラフは delta=0.1 (既定) を使用。
+pickle は ./outputs/ 以下に保存されるもののみ明記しています。
 
 1) plot_accuracy_vs_budget_{NOISE}.pdf
    - X軸: 目標予算 (Budget target)

+ 377 - 657
add_linkselfie/evaluation.py

@@ -1,20 +1,31 @@
-# evaluation.py
-# Run evaluation and plot figures
+# evaluation.py — Run shared sweep once; all plots aggregate from cache (Py3.8-safe)
+
 import math
 import os
 import pickle
 import time
 import shutil
+import json
+import hashlib
 
 import matplotlib.pyplot as plt
 import numpy as np
 from cycler import cycler
 
-from algorithms import benchmark_using_algorithm  # may be used elsewhere
+# metrics / viz を外出し(UNIX的分離)
+from metrics.widths import (
+    ci_radius_hoeffding,
+    sum_weighted_widths_all_links,
+    sum_weighted_min_widths_perpair,
+    sum_widths_all_links,
+    sum_minwidths_perpair,
+)
+from viz.plots import mean_ci95, plot_with_ci_band
+
 from network import QuantumNetwork
-from schedulers import run_scheduler  # パッケージ化したものを使う
+from schedulers import run_scheduler  # スケジューラ呼び出し
 
-# ---- Matplotlib style (IEEE-ish) ----
+# ---- Matplotlib style(互換性重視: hex色 & 無難な記号類)----
 plt.rc("font", family="Times New Roman")
 plt.rc("font", size=20)
 default_cycler = (
@@ -24,7 +35,6 @@ default_cycler = (
 )
 plt.rc("axes", prop_cycle=default_cycler)
 
-
 # =========================
 # Fidelity generators
 # =========================
@@ -40,7 +50,6 @@ def generate_fidelity_list_avg_gap(path_num):
     assert len(result) == path_num
     return result
 
-
 def generate_fidelity_list_fix_gap(path_num, gap, fidelity_max=1):
     result = []
     fidelity = fidelity_max
@@ -50,40 +59,30 @@ def generate_fidelity_list_fix_gap(path_num, gap, fidelity_max=1):
     assert len(result) == path_num
     return result
 
-
 def generate_fidelity_list_random(path_num, alpha=0.95, beta=0.85, variance=0.1):
-    """Generate `path_num` links.
-    u_1 = alpha, u_i = beta for all i = 2, 3, ..., n.
-    Fidelity_i ~ N(u_i, variance), clipped to [0.8, 1].
-    Ensure the top-1 gap is large enough (> 0.02) for termination guarantees.
-    """
+    """Generate `path_num` links with a guaranteed top-1 gap."""
     while True:
         mean = [alpha] + [beta] * (path_num - 1)
         result = []
         for i in range(path_num):
             mu = mean[i]
-            # Sample a Gaussian random variable and make sure its value is in the valid range
+            # [0.8, 1.0] の範囲に入るまでサンプリング
             while True:
                 r = np.random.normal(mu, variance)
-                # Depolarizing noise and amplitude damping noise models require fidelity >= 0.5
-                # Be conservative: require >= 0.8
                 if 0.8 <= r <= 1.0:
                     break
             result.append(r)
         assert len(result) == path_num
         sorted_res = sorted(result, reverse=True)
-        # To guarantee the termination of algorithms, we require that the gap is large enough
         if sorted_res[0] - sorted_res[1] > 0.02:
             return result
 
-
 # =========================
-# Progress helpers (LinkSelfie風)
+# Progress helpers
 # =========================
 def _start_timer():
     return {"t0": time.time(), "last": time.time()}
 
-
 def _tick(timer):
     now = time.time()
     dt_total = now - timer["t0"]
@@ -91,765 +90,486 @@ def _tick(timer):
     timer["last"] = now
     return dt_total, dt_step
 
-
 def _log(msg):
     print(msg, flush=True)
 
-
 # =========================
-# Plots
+# Shared sweep (cache) helpers with file lock
 # =========================
-def plot_accuracy_vs_budget(
-    budget_list,          # e.g., [1000, 2000, 3000, ...] (x-axis)
-    scheduler_names,      # e.g., ["LNaive", "Greedy", ...]
-    noise_model,          # e.g., "Depolar"
-    node_path_list,       # e.g., [5, 5, 5]
-    importance_list,      # e.g., [0.4, 0.7, 1.0] (not used here, but kept for interface)
-    bounces=(1, 2, 3, 4),
-    repeat=10,
-    verbose=True,
-    print_every=1,
-):
-    file_name = f"plot_accuracy_vs_budget_{noise_model}"
+def _sweep_signature(budget_list, scheduler_names, noise_model,
+                     node_path_list, importance_list, bounces, repeat):
+    payload = {
+        "budget_list": list(budget_list),
+        "scheduler_names": list(scheduler_names),
+        "noise_model": str(noise_model),
+        "node_path_list": list(node_path_list),
+        "importance_list": list(importance_list),
+        "bounces": list(bounces),
+        "repeat": int(repeat),
+        "version": 1,
+    }
+    sig = hashlib.md5(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()[:10]
+    return payload, sig
+
+def _shared_sweep_path(noise_model, sig):
     root_dir = os.path.dirname(os.path.abspath(__file__))
-    output_dir = os.path.join(root_dir, "outputs")
-    os.makedirs(output_dir, exist_ok=True)
-    file_path = os.path.join(output_dir, f"{file_name}.pickle")
-
-    if os.path.exists(file_path):
-        _log("Pickle data exists, skip simulation and plot the data directly.")
-        _log("To rerun, delete the pickle in `outputs`.")
-        with open(file_path, "rb") as f:
-            payload = pickle.load(f)
-            budget_list = payload["budget_list"]
-            results = payload["results"]
-    else:
-        results = {name: {"accs": [[] for _ in budget_list]} for name in scheduler_names}
+    outdir = os.path.join(root_dir, "outputs")
+    os.makedirs(outdir, exist_ok=True)
+    return os.path.join(outdir, f"shared_sweep_{noise_model}_{sig}.pickle")
+
+def _run_or_load_shared_sweep(
+    budget_list, scheduler_names, noise_model,
+    node_path_list, importance_list,
+    bounces=(1,2,3,4), repeat=10,
+    verbose=True, print_every=1,
+):
+    config, sig = _sweep_signature(budget_list, scheduler_names, noise_model,
+                                   node_path_list, importance_list, bounces, repeat)
+    cache_path = _shared_sweep_path(noise_model, sig)
+    lock_path  = cache_path + ".lock"
+    STALE_LOCK_SECS = 6 * 60 * 60        # 6時間無更新ならロック回収
+    HEARTBEAT_EVERY = 5.0                # 生成側のロック更新間隔(秒)
+
+    # 既存キャッシュがあれば即ロード
+    if os.path.exists(cache_path):
+        if verbose: _log(f"[shared] Load cached sweep: {os.path.basename(cache_path)}")
+        with open(cache_path, "rb") as f:
+            return pickle.load(f)
+
+    # --- ロック獲得(初回生成は1プロセスのみ)---
+    got_lock = False
+    while True:
+        try:
+            fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
+            os.close(fd)
+            got_lock = True
+            break
+        except FileExistsError:
+            # 他プロセスが生成中:完成を待つ(タイムアウトなし)
+            if os.path.exists(cache_path):
+                with open(cache_path, "rb") as f:
+                    return pickle.load(f)
+
+            # スタックロック検出:長時間 mtime 更新がない場合は回収
+            try:
+                age = time.time() - os.path.getmtime(lock_path)
+            except OSError:
+                age = 0
+            if age > STALE_LOCK_SECS:
+                if verbose: _log("[shared] Stale lock detected. Removing...")
+                try: os.remove(lock_path)
+                except FileNotFoundError: pass
+                continue
+
+            # 進捗待ち
+            if verbose: _log("[shared] Waiting for cache to be ready...")
+            time.sleep(1.0)
+
+    try:
+        if verbose: _log(f"[shared] Run sweep and cache to: {os.path.basename(cache_path)}")
+
+        data = {name: {k: [] for k in range(len(budget_list))} for name in scheduler_names}
+        last_hb = time.time()
+
         for k, C_total in enumerate(budget_list):
-            timer = _start_timer()
-            if verbose:
-                _log(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===")
+            if verbose: _log(f"=== [SHARED {noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===")
+
             for r in range(repeat):
                 if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    _log(f"  [repeat {r+1}/{repeat}] generating topology …")
-                # 1リピート = 1トポロジ(全スケジューラで共有)
+                    _log(f"  [repeat {r+1}/{repeat}]")
+
+                # ハートビート(ロックの mtime を更新して“生存”を伝える)
+                now = time.time()
+                if now - last_hb >= HEARTBEAT_EVERY:
+                    try: os.utime(lock_path, None)
+                    except FileNotFoundError: pass
+                    last_hb = now
+
+                # 1リピート = 1トポロジ
                 fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
 
                 def network_generator(path_num, pair_idx):
                     return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
 
                 for name in scheduler_names:
-                    if verbose and ((r + 1) % print_every == 0 or r == 0):
-                        _log(f"    - {name}: running …")
-                    per_pair_results, _ = run_scheduler(
+                    per_pair_results, total_cost, per_pair_details = run_scheduler(
                         node_path_list=node_path_list,
                         importance_list=importance_list,
                         scheduler_name=name,
                         bounces=list(bounces),
                         C_total=int(C_total),
                         network_generator=network_generator,
+                        return_details=True,
                     )
-                    acc = (
-                        float(np.mean([1.0 if c else 0.0 for (c, _cost, _bf) in per_pair_results]))
-                        if per_pair_results
-                        else 0.0
-                    )
-                    results[name]["accs"][k].append(acc)
-                    if verbose and ((r + 1) % print_every == 0 or r == 0):
-                        _log(f"      -> acc={acc:.3f}")
-            if verbose:
-                tot, _ = _tick(timer)
-                _log(f"=== done Budget={C_total} | elapsed {tot:.1f}s ===")
-
-        with open(file_path, "wb") as f:
-            pickle.dump({"budget_list": list(budget_list), "results": results}, f)
-
-    # --- Plot ---
+                    data[name][k].append({
+                        "per_pair_results": per_pair_results,
+                        "per_pair_details": per_pair_details,
+                        "total_cost": total_cost,
+                    })
+
+        payload = {"config": config, "budget_list": list(budget_list), "data": data}
+
+        # アトミック書き込み
+        tmp = cache_path + ".tmp"
+        with open(tmp, "wb") as f:
+            pickle.dump(payload, f, protocol=pickle.HIGHEST_PROTOCOL)
+        os.replace(tmp, cache_path)
+
+        return payload
+
+    finally:
+        if got_lock:
+            try: os.remove(lock_path)
+            except FileNotFoundError: pass
+# =========================
+# 1) Accuracy: 平均のみ(CIなし)
+# =========================
+def plot_accuracy_vs_budget(
+    budget_list, scheduler_names, noise_model,
+    node_path_list, importance_list,
+    bounces=(1,2,3,4), repeat=10,
+    verbose=True, print_every=1,
+):
+    file_name = f"plot_accuracy_vs_budget_{noise_model}"
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    outdir = os.path.join(root_dir, "outputs")
+    os.makedirs(outdir, exist_ok=True)
+
+    payload = _run_or_load_shared_sweep(
+        budget_list, scheduler_names, noise_model,
+        node_path_list, importance_list,
+        bounces=bounces, repeat=repeat,
+        verbose=verbose, print_every=print_every,
+    )
+
+    results = {name: {"accs": [[] for _ in budget_list]} for name in scheduler_names}
+    for name in scheduler_names:
+        for k in range(len(budget_list)):
+            for run in payload["data"][name][k]:
+                per_pair_results = run["per_pair_results"]
+                acc = float(np.mean([1.0 if c else 0.0 for (c, _cost, _bf) in per_pair_results])) if per_pair_results else 0.0
+                results[name]["accs"][k].append(acc)
+
+    # plot
     plt.rc("axes", prop_cycle=default_cycler)
     fig, ax = plt.subplots()
-    x = list(budget_list)
+    xs = list(budget_list)
     for name, data in results.items():
         avg_accs = [float(np.mean(v)) if v else 0.0 for v in data["accs"]]
-        label = name.replace("Vanilla NB", "VanillaNB").replace("Succ. Elim. NB", "SuccElimNB")
-        ax.plot(x, avg_accs, linewidth=2.0, label=label)
-
+        label = name.replace("Vanilla NB","VanillaNB").replace("Succ. Elim. NB","SuccElimNB")
+        ax.plot(xs, avg_accs, linewidth=2.0, label=label)
     ax.set_xlabel("Total Budget (C)")
     ax.set_ylabel("Average Correctness")
-    ax.grid(True)
-    ax.legend(title="Scheduler", fontsize=14, title_fontsize=18)
+    ax.grid(True); ax.legend(title="Scheduler", fontsize=14, title_fontsize=18)
     plt.tight_layout()
-    pdf_name = f"{file_name}.pdf"
-    plt.savefig(pdf_name)
-    if shutil.which("pdfcrop"):
-        os.system(f"pdfcrop {pdf_name} {pdf_name}")
-    _log(f"Saved: {pdf_name}")
-
+    pdf = f"{file_name}.pdf"
+    plt.savefig(pdf); 
+    if shutil.which("pdfcrop"): os.system(f"pdfcrop {pdf} {pdf}")
+    _log(f"Saved: {pdf}")
 
+# =========================
+# 2) Value vs Used(x=実コスト平均)
+# =========================
 def plot_value_vs_used(
-    budget_list,
-    scheduler_names,
-    noise_model,
-    node_path_list,
-    importance_list,
-    bounces=(1, 2, 3, 4),
-    repeat=10,
-    verbose=True,
-    print_every=1,
+    budget_list, scheduler_names, noise_model,
+    node_path_list, importance_list,
+    bounces=(1,2,3,4), repeat=10,
+    verbose=True, print_every=1,
 ):
-    """x = 実コスト平均(used)で描く版。旧 plot_value_vs_budget と同等の挙動。"""
     file_name = f"plot_value_vs_used_{noise_model}"
     root_dir = os.path.dirname(os.path.abspath(__file__))
-    output_dir = os.path.join(root_dir, "outputs")
-    os.makedirs(output_dir, exist_ok=True)
-
-    results = {
-        name: {"values": [[] for _ in budget_list], "costs": [[] for _ in budget_list]}
-        for name in scheduler_names
-    }
-
-    for k, C_total in enumerate(budget_list):
-        timer = _start_timer()
-        if verbose:
-            _log(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===")
-
-        # 1リピート = 1トポロジ(全スケジューラで共有)
-        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
-
-        def network_generator(path_num, pair_idx):
-            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
-
-        for r in range(repeat):
-            if verbose and ((r + 1) % print_every == 0 or r == 0):
-                _log(f"  [repeat {r+1}/{repeat}]")
-            for name in scheduler_names:
-                if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    _log(f"    - {name}: running …")
-                per_pair_results, total_cost, per_pair_details = run_scheduler(
-                    node_path_list=node_path_list,
-                    importance_list=importance_list,
-                    scheduler_name=name,
-                    bounces=list(bounces),
-                    C_total=int(C_total),
-                    network_generator=network_generator,
-                    return_details=True,
-                )
-                # 価値の合成
+    outdir = os.path.join(root_dir, "outputs")
+    os.makedirs(outdir, exist_ok=True)
+
+    payload = _run_or_load_shared_sweep(
+        budget_list, scheduler_names, noise_model,
+        node_path_list, importance_list,
+        bounces=bounces, repeat=repeat,
+        verbose=verbose, print_every=print_every,
+    )
+
+    results = {name: {"values": [[] for _ in budget_list], "costs": [[] for _ in budget_list]} for name in scheduler_names}
+    for name in scheduler_names:
+        for k in range(len(budget_list)):
+            for run in payload["data"][name][k]:
+                per_pair_details = run["per_pair_details"]
+                total_cost = int(run["total_cost"])
+                # value = Σ_d I_d Σ_l est(d,l) * alloc(d,l)
                 value = 0.0
-                for d, details in enumerate(per_pair_details):
-                    alloc = details.get("alloc_by_path", {})
-                    est = details.get("est_fid_by_path", {})
+                for d, det in enumerate(per_pair_details):
+                    alloc = det.get("alloc_by_path", {})
+                    est   = det.get("est_fid_by_path", {})
                     inner = sum(float(est.get(l, 0.0)) * int(b) for l, b in alloc.items())
-                    value += float(importance_list[d]) * inner
-
+                    I = float(importance_list[d]) if d < len(importance_list) else 1.0
+                    value += I * inner
                 results[name]["values"][k].append(float(value))
-                results[name]["costs"][k].append(int(total_cost))
-                if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    _log(f"      -> used={total_cost}, value={value:.2f}")
-
-        if verbose:
-            tot, _ = _tick(timer)
-            _log(f"=== done Budget={C_total} | elapsed {tot:.1f}s ===")
+                results[name]["costs"][k].append(total_cost)
 
-    # --- Plot (x = 実コスト平均) ---
+    # plot
     plt.rc("axes", prop_cycle=default_cycler)
     fig, ax = plt.subplots()
-    for name, data in results.items():
-        xs = [float(np.mean(v)) if v else 0.0 for v in data["costs"]]
-        ys = [float(np.mean(v)) if v else 0.0 for v in data["values"]]
-        ax.plot(xs, ys, linewidth=2.0, marker="o", label=name)
-
+    for name, dat in results.items():
+        xs = [float(np.mean(v)) if v else 0.0 for v in dat["costs"]]
+        ys = [float(np.mean(v)) if v else 0.0 for v in dat["values"]]
+        label = name.replace("Vanilla NB","VanillaNB").replace("Succ. Elim. NB","SuccElimNB")
+        ax.plot(xs, ys, linewidth=2.0, marker="o", label=label)
     ax.set_xlabel("Total Measured Cost (used)")
     ax.set_ylabel("Total Value (Σ I_d Σ f̂_{d,l}·B_{d,l})")
-    ax.grid(True)
-    ax.legend(title="Scheduler")
+    ax.grid(True); ax.legend(title="Scheduler")
     plt.tight_layout()
-    pdf_name = f"{file_name}.pdf"
-    plt.savefig(pdf_name)
-    if shutil.which("pdfcrop"):
-        os.system(f"pdfcrop {pdf_name} {pdf_name}")
-    _log(f"Saved: {pdf_name}")
-
+    pdf = f"{file_name}.pdf"
+    plt.savefig(pdf); 
+    if shutil.which("pdfcrop"): os.system(f"pdfcrop {pdf} {pdf}")
+    _log(f"Saved: {pdf}")
 
+# =========================
+# 3) Value vs Budget target(x=目標予算)
+# =========================
 def plot_value_vs_budget_target(
-    budget_list,
-    scheduler_names,
-    noise_model,
-    node_path_list,
-    importance_list,
-    bounces=(1, 2, 3, 4),
-    repeat=10,
-    verbose=True,
-    print_every=1,
+    budget_list, scheduler_names, noise_model,
+    node_path_list, importance_list,
+    bounces=(1,2,3,4), repeat=10,
+    verbose=True, print_every=1,
 ):
-    """x = 目標予算(指定した budget_list をそのまま x 軸に)で描く版。"""
     file_name = f"plot_value_vs_budget_target_{noise_model}"
     root_dir = os.path.dirname(os.path.abspath(__file__))
-    output_dir = os.path.join(root_dir, "outputs")
-    os.makedirs(output_dir, exist_ok=True)
-
-    results = {
-        name: {"values": [[] for _ in budget_list], "costs": [[] for _ in budget_list]}
-        for name in scheduler_names
-    }
-
-    for k, C_total in enumerate(budget_list):
-        timer = _start_timer()
-        if verbose:
-            _log(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===")
-
-        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
-
-        def network_generator(path_num, pair_idx):
-            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
-
-        for r in range(repeat):
-            if verbose and ((r + 1) % print_every == 0 or r == 0):
-                _log(f"  [repeat {r+1}/{repeat}]")
-            for name in scheduler_names:
-                if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    _log(f"    - {name}: running …")
-                per_pair_results, total_cost, per_pair_details = run_scheduler(
-                    node_path_list=node_path_list,
-                    importance_list=importance_list,
-                    scheduler_name=name,
-                    bounces=list(bounces),
-                    C_total=int(C_total),
-                    network_generator=network_generator,
-                    return_details=True,
-                )
+    outdir = os.path.join(root_dir, "outputs")
+    os.makedirs(outdir, exist_ok=True)
+
+    payload = _run_or_load_shared_sweep(
+        budget_list, scheduler_names, noise_model,
+        node_path_list, importance_list,
+        bounces=bounces, repeat=repeat,
+        verbose=verbose, print_every=print_every,
+    )
+
+    results = {name: {"values": [[] for _ in budget_list]} for name in scheduler_names}
+    for name in scheduler_names:
+        for k in range(len(budget_list)):
+            for run in payload["data"][name][k]:
+                per_pair_details = run["per_pair_details"]
                 value = 0.0
-                for d, details in enumerate(per_pair_details):
-                    alloc = details.get("alloc_by_path", {})
-                    est = details.get("est_fid_by_path", {})
+                for d, det in enumerate(per_pair_details):
+                    alloc = det.get("alloc_by_path", {})
+                    est   = det.get("est_fid_by_path", {})
                     inner = sum(float(est.get(l, 0.0)) * int(b) for l, b in alloc.items())
-                    value += float(importance_list[d]) * inner
-
+                    I = float(importance_list[d]) if d < len(importance_list) else 1.0
+                    value += I * inner
                 results[name]["values"][k].append(float(value))
-                results[name]["costs"][k].append(int(total_cost))
-                if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    _log(f"      -> used={total_cost}, value={value:.2f}")
-
-        if verbose:
-            tot, _ = _tick(timer)
-            _log(f"=== done Budget={C_total} | elapsed {tot:.1f}s ===")
 
-    # --- Plot (x = 目標予算) ---
+    # plot
     plt.rc("axes", prop_cycle=default_cycler)
     fig, ax = plt.subplots()
-    x = list(budget_list)
-    for name, data in results.items():
-        ys = [float(np.mean(v)) if v else 0.0 for v in data["values"]]
-        ax.plot(x, ys, linewidth=2.0, marker="o", label=name)
-
+    xs = list(budget_list)
+    for name, dat in results.items():
+        ys = [float(np.mean(v)) if v else 0.0 for v in dat["values"]]
+        label = name.replace("Vanilla NB","VanillaNB").replace("Succ. Elim. NB","SuccElimNB")
+        ax.plot(xs, ys, linewidth=2.0, marker="o", label=label)
     ax.set_xlabel("Budget (target)")
     ax.set_ylabel("Total Value (Σ I_d Σ f̂_{d,l}·B_{d,l})")
-    ax.grid(True)
-    ax.legend(title="Scheduler")
+    ax.grid(True); ax.legend(title="Scheduler")
     plt.tight_layout()
-    pdf_name = f"{file_name}.pdf"
-    plt.savefig(pdf_name)
-    if shutil.which("pdfcrop"):
-        os.system(f"pdfcrop {pdf_name} {pdf_name}")
-    _log(f"Saved: {pdf_name}")
-
+    pdf = f"{file_name}.pdf"
+    plt.savefig(pdf); 
+    if shutil.which("pdfcrop"): os.system(f"pdfcrop {pdf} {pdf}")
+    _log(f"Saved: {pdf}")
 
 # =========================
-# CI width helpers and plots
+# 4) 幅(UB-LB)Unweighted: 全リンク総和
 # =========================
-def _ci_radius_hoeffding(n: int, delta: float = 0.1) -> float:
-    if n <= 0:
-        return 1.0
-    return math.sqrt(0.5 * math.log(2.0 / delta) / n)
-
-
-# =========================
-# Width-sum metrics (new)
-# =========================
-
-def _sum_widths_all_links(per_pair_details, delta: float = 0.1) -> float:
-    """
-    すべてのペア・すべてのリンクについて、(UB - LB) を合計。
-    est が無いリンクはスキップ(=寄与0)。測定していないリンクは数えません。
-    """
-    total = 0.0
-    for det in per_pair_details:
-        alloc = det.get("alloc_by_path", {})  # n = 測定回数
-        est   = det.get("est_fid_by_path", {})  # 標本平均
-        for pid, m in est.items():
-            n = int(alloc.get(pid, 0))
-            rad = _ci_radius_hoeffding(n, delta)
-            lb = max(0.0, float(m) - rad)
-            ub = min(1.0, float(m) + rad)
-            total += (ub - lb)
-    return float(total)
-
-
-def _sum_min_widths_per_pair(per_pair_details, delta: float = 0.1) -> float:
-    """
-    ペアごとにリンクの (UB - LB) を算出し、その「最小値」を取り、全ペアで合計。
-    est が空のペアは保守的に 1.0 を加算(“全く分からない”幅として扱う)。
-    """
-    s = 0.0
-    for det in per_pair_details:
-        alloc = det.get("alloc_by_path", {})
-        est   = det.get("est_fid_by_path", {})
-        if not est:
-            s += 1.0
-            continue
-        widths = []
-        for pid, m in est.items():
-            n = int(alloc.get(pid, 0))
-            rad = _ci_radius_hoeffding(n, delta)
-            lb = max(0.0, float(m) - rad)
-            ub = min(1.0, float(m) + rad)
-            widths.append(ub - lb)
-        s += (min(widths) if widths else 1.0)
-    return float(s)
-
-
 def plot_widthsum_alllinks_vs_budget(
-    budget_list,
-    scheduler_names,
-    noise_model,
-    node_path_list,
-    importance_list,
-    bounces=(1, 2, 3, 4),
-    repeat=10,
-    delta=0.1,
-    verbose=True,
-    print_every=1,
+    budget_list, scheduler_names, noise_model,
+    node_path_list, importance_list,
+    bounces=(1,2,3,4), repeat=10, delta=0.1,
+    verbose=True, print_every=1,
 ):
-    """
-    y = 全リンク(UB-LB)総和 の平均 ±95%CI を、x = 目標予算 で描画。
-    生データは outputs/plot_widthsum_alllinks_vs_budget_*.pickle に保存。
-    """
     file_name = f"plot_widthsum_alllinks_vs_budget_{noise_model}"
     root_dir = os.path.dirname(os.path.abspath(__file__))
-    output_dir = os.path.join(root_dir, "outputs")
-    os.makedirs(output_dir, exist_ok=True)
+    outdir = os.path.join(root_dir, "outputs")
+    os.makedirs(outdir, exist_ok=True)
 
-    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+    payload = _run_or_load_shared_sweep(
+        budget_list, scheduler_names, noise_model,
+        node_path_list, importance_list,
+        bounces=bounces, repeat=repeat,
+        verbose=verbose, print_every=print_every,
+    )
 
-    for k, C_total in enumerate(budget_list):
-        if verbose:
-            print(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===", flush=True)
-
-        # 1リピート=1トポロジ(全スケジューラ共有)
-        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
-
-        def network_generator(path_num, pair_idx):
-            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
-
-        for r in range(repeat):
-            if verbose and ((r + 1) % print_every == 0 or r == 0):
-                print(f"  [repeat {r+1}/{repeat}]", flush=True)
-
-            for name in scheduler_names:
-                per_pair_results, total_cost, per_pair_details = run_scheduler(
-                    node_path_list=node_path_list,
-                    importance_list=importance_list,
-                    scheduler_name=name,
-                    bounces=list(bounces),
-                    C_total=int(C_total),
-                    network_generator=network_generator,
-                    return_details=True,
-                )
-                v = _sum_widths_all_links(per_pair_details, delta=delta)
+    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+    for name in scheduler_names:
+        for k in range(len(budget_list)):
+            for run in payload["data"][name][k]:
+                per_pair_details = run["per_pair_details"]
+                v = sum_widths_all_links(per_pair_details, delta=delta)
                 results[name]["sums"][k].append(v)
-                if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    print(f"    - {name}: sum_alllinks={v:.4f} (used={total_cost})", flush=True)
-
-    # --- Save raw data (.pickle) ---
-    file_path = os.path.join(output_dir, f"{file_name}.pickle")
-    with open(file_path, "wb") as f:
-        pickle.dump({"budget_list": list(budget_list), "results": results}, f)
-    print(f"Saved pickle: {file_path}")
 
-    # --- Plot mean ± 95% CI across repeats ---
+    # plot (mean ± 95%CI)
     plt.rc("axes", prop_cycle=default_cycler)
     fig, ax = plt.subplots()
-    x = list(budget_list)
-    for name, data in results.items():
+    xs = list(budget_list)
+    for name, dat in results.items():
         means, halfs = [], []
-        for vals in data["sums"]:
-            m, h = mean_ci95(vals)
-            means.append(m); halfs.append(h)
+        for vals in dat["sums"]:
+            m, h = mean_ci95(vals); means.append(m); halfs.append(h)
         means = np.asarray(means); halfs = np.asarray(halfs)
-        ax.plot(x, means, linewidth=2.0, marker="o", label=name)
-        ax.fill_between(x, means - halfs, means + halfs, alpha=0.25)
-
+        label = name.replace("Vanilla NB","VanillaNB").replace("Succ. Elim. NB","SuccElimNB")
+        ax.plot(xs, means, linewidth=2.0, marker="o", label=label)
+        ax.fill_between(xs, means - halfs, means + halfs, alpha=0.25)
     ax.set_xlabel("Budget (target)")
     ax.set_ylabel("Sum of (UB - LB) over all links")
-    ax.grid(True)
-    ax.legend(title="Scheduler")
+    ax.grid(True); ax.legend(title="Scheduler")
     plt.tight_layout()
-    pdf_name = f"{file_name}.pdf"
-    plt.savefig(pdf_name)
-    if shutil.which("pdfcrop"):
-        os.system(f"pdfcrop {pdf_name} {pdf_name}")
-    print(f"Saved: {pdf_name}")
-
+    pdf = f"{file_name}.pdf"
+    plt.savefig(pdf); 
+    if shutil.which("pdfcrop"): os.system(f"pdfcrop {pdf} {pdf}")
+    _log(f"Saved: {pdf}")
 
+# =========================
+# 5) 幅(UB-LB)Unweighted: ペア最小幅の総和
+# =========================
 def plot_minwidthsum_perpair_vs_budget(
-    budget_list,
-    scheduler_names,
-    noise_model,
-    node_path_list,
-    importance_list,
-    bounces=(1, 2, 3, 4),
-    repeat=10,
-    delta=0.1,
-    verbose=True,
-    print_every=1,
+    budget_list, scheduler_names, noise_model,
+    node_path_list, importance_list,
+    bounces=(1,2,3,4), repeat=10, delta=0.1,
+    verbose=True, print_every=1,
 ):
-    """
-    y = ペアごとの (UB-LB) 最小値の合計 の平均 ±95%CI、x = 目標予算。
-    生データは outputs/plot_minwidthsum_perpair_vs_budget_*.pickle に保存。
-    """
     file_name = f"plot_minwidthsum_perpair_vs_budget_{noise_model}"
     root_dir = os.path.dirname(os.path.abspath(__file__))
-    output_dir = os.path.join(root_dir, "outputs")
-    os.makedirs(output_dir, exist_ok=True)
+    outdir = os.path.join(root_dir, "outputs")
+    os.makedirs(outdir, exist_ok=True)
 
-    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+    payload = _run_or_load_shared_sweep(
+        budget_list, scheduler_names, noise_model,
+        node_path_list, importance_list,
+        bounces=bounces, repeat=repeat,
+        verbose=verbose, print_every=print_every,
+    )
 
-    for k, C_total in enumerate(budget_list):
-        if verbose:
-            print(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===", flush=True)
-
-        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
-
-        def network_generator(path_num, pair_idx):
-            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
-
-        for r in range(repeat):
-            if verbose and ((r + 1) % print_every == 0 or r == 0):
-                print(f"  [repeat {r+1}/{repeat}]", flush=True)
-
-            for name in scheduler_names:
-                per_pair_results, total_cost, per_pair_details = run_scheduler(
-                    node_path_list=node_path_list,
-                    importance_list=importance_list,
-                    scheduler_name=name,
-                    bounces=list(bounces),
-                    C_total=int(C_total),
-                    network_generator=network_generator,
-                    return_details=True,
-                )
-                v = _sum_min_widths_per_pair(per_pair_details, delta=delta)
+    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+    for name in scheduler_names:
+        for k in range(len(budget_list)):
+            for run in payload["data"][name][k]:
+                per_pair_details = run["per_pair_details"]
+                v = sum_minwidths_perpair(per_pair_details, delta=delta)
                 results[name]["sums"][k].append(v)
-                if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    print(f"    - {name}: sum_min_perpair={v:.4f} (used={total_cost})", flush=True)
 
-    # --- Save raw data (.pickle) ---
-    file_path = os.path.join(output_dir, f"{file_name}.pickle")
-    with open(file_path, "wb") as f:
-        pickle.dump({"budget_list": list(budget_list), "results": results}, f)
-    print(f"Saved pickle: {file_path}")
-
-    # --- Plot mean ± 95% CI across repeats ---
+    # plot (mean ± 95%CI)
     plt.rc("axes", prop_cycle=default_cycler)
     fig, ax = plt.subplots()
-    x = list(budget_list)
-    for name, data in results.items():
+    xs = list(budget_list)
+    for name, dat in results.items():
         means, halfs = [], []
-        for vals in data["sums"]:
-            m, h = mean_ci95(vals)
-            means.append(m); halfs.append(h)
+        for vals in dat["sums"]:
+            m, h = mean_ci95(vals); means.append(m); halfs.append(h)
         means = np.asarray(means); halfs = np.asarray(halfs)
-        ax.plot(x, means, linewidth=2.0, marker="o", label=name)
-        ax.fill_between(x, means - halfs, means + halfs, alpha=0.25)
-
+        label = name.replace("Vanilla NB","VanillaNB").replace("Succ. Elim. NB","SuccElimNB")
+        ax.plot(xs, means, linewidth=2.0, marker="o", label=label)
+        ax.fill_between(xs, means - halfs, means + halfs, alpha=0.25)
     ax.set_xlabel("Budget (target)")
     ax.set_ylabel("Sum over pairs of min (UB - LB)")
-    ax.grid(True)
-    ax.legend(title="Scheduler")
+    ax.grid(True); ax.legend(title="Scheduler")
     plt.tight_layout()
-    pdf_name = f"{file_name}.pdf"
-    plt.savefig(pdf_name)
-    if shutil.which("pdfcrop"):
-        os.system(f"pdfcrop {pdf_name} {pdf_name}")
-    print(f"Saved: {pdf_name}")
-
-
+    pdf = f"{file_name}.pdf"
+    plt.savefig(pdf); 
+    if shutil.which("pdfcrop"): os.system(f"pdfcrop {pdf} {pdf}")
+    _log(f"Saved: {pdf}")
 
 # =========================
-# Weighted width-sum metrics (add-on)
+# 6) 幅(UB-LB)Weighted: 全リンク I_d·幅 総和
 # =========================
-
-def _sum_weighted_widths_all_links(per_pair_details, importance_list, delta: float = 0.1) -> float:
-    """
-    すべてのペア・すべてのリンクの (UB-LB) に、ペア重要度 I_d を掛けて合計。
-    importance_list[d] が無ければ I_d=1.0 として扱う。
-    """
-    total = 0.0
-    for d, det in enumerate(per_pair_details):
-        I = float(importance_list[d]) if d < len(importance_list) else 1.0
-        alloc = det.get("alloc_by_path", {})
-        est   = det.get("est_fid_by_path", {})
-        for pid, m in est.items():
-            n = int(alloc.get(pid, 0))
-            rad = _ci_radius_hoeffding(n, delta)
-            lb = max(0.0, float(m) - rad)
-            ub = min(1.0, float(m) + rad)
-            total += I * (ub - lb)
-    return float(total)
-
-
-def _sum_weighted_min_widths_per_pair(per_pair_details, importance_list, delta: float = 0.1) -> float:
-    """
-    ペア d ごとに min_l (UB-LB) を計算し、I_d を掛けて全ペアで合計。
-    est が空のペアは保守的に幅=1.0 として I_d*1.0 を加算。
-    """
-    s = 0.0
-    for d, det in enumerate(per_pair_details):
-        I = float(importance_list[d]) if d < len(importance_list) else 1.0
-        alloc = det.get("alloc_by_path", {})
-        est   = det.get("est_fid_by_path", {})
-        if not est:
-            s += I * 1.0
-            continue
-        widths = []
-        for pid, m in est.items():
-            n = int(alloc.get(pid, 0))
-            rad = _ci_radius_hoeffding(n, delta)
-            lb = max(0.0, float(m) - rad)
-            ub = min(1.0, float(m) + rad)
-            widths.append(ub - lb)
-        s += I * (min(widths) if widths else 1.0)
-    return float(s)
-
-
 def plot_widthsum_alllinks_weighted_vs_budget(
-    budget_list,
-    scheduler_names,
-    noise_model,
-    node_path_list,
-    importance_list,
-    bounces=(1, 2, 3, 4),
-    repeat=10,
-    delta=0.1,
-    verbose=True,
-    print_every=1,
+    budget_list, scheduler_names, noise_model,
+    node_path_list, importance_list,
+    bounces=(1,2,3,4), repeat=10, delta=0.1,
+    verbose=True, print_every=1,
 ):
-    """
-    y = Σ_d Σ_l I_d·(UB-LB) の平均 ±95%CI、x = 目標予算。
-    生データは outputs/plot_widthsum_alllinks_weighted_vs_budget_*.pickle に保存。
-    """
     file_name = f"plot_widthsum_alllinks_weighted_vs_budget_{noise_model}"
     root_dir = os.path.dirname(os.path.abspath(__file__))
-    output_dir = os.path.join(root_dir, "outputs")
-    os.makedirs(output_dir, exist_ok=True)
+    outdir = os.path.join(root_dir, "outputs")
+    os.makedirs(outdir, exist_ok=True)
 
-    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+    payload = _run_or_load_shared_sweep(
+        budget_list, scheduler_names, noise_model,
+        node_path_list, importance_list,
+        bounces=bounces, repeat=repeat,
+        verbose=verbose, print_every=print_every,
+    )
 
-    for k, C_total in enumerate(budget_list):
-        if verbose:
-            print(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===", flush=True)
-
-        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
-
-        def network_generator(path_num, pair_idx):
-            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
-
-        for r in range(repeat):
-            if verbose and ((r + 1) % print_every == 0 or r == 0):
-                print(f"  [repeat {r+1}/{repeat}]", flush=True)
-
-            for name in scheduler_names:
-                per_pair_results, total_cost, per_pair_details = run_scheduler(
-                    node_path_list=node_path_list,
-                    importance_list=importance_list,
-                    scheduler_name=name,
-                    bounces=list(bounces),
-                    C_total=int(C_total),
-                    network_generator=network_generator,
-                    return_details=True,
-                )
-                v = _sum_weighted_widths_all_links(per_pair_details, importance_list, delta=delta)
+    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+    for name in scheduler_names:
+        for k in range(len(budget_list)):
+            for run in payload["data"][name][k]:
+                per_pair_details = run["per_pair_details"]
+                v = sum_weighted_widths_all_links(per_pair_details, importance_list, delta=delta)
                 results[name]["sums"][k].append(v)
-                if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    print(f"    - {name}: wsum_alllinks={v:.4f} (used={total_cost})", flush=True)
-
-    # --- Save raw data (.pickle) ---
-    file_path = os.path.join(output_dir, f"{file_name}.pickle")
-    with open(file_path, "wb") as f:
-        pickle.dump({"budget_list": list(budget_list), "results": results}, f)
-    print(f"Saved pickle: {file_path}")
 
-    # --- Plot mean ± 95% CI ---
+    # plot (mean ± 95%CI)
     plt.rc("axes", prop_cycle=default_cycler)
     fig, ax = plt.subplots()
-    x = list(budget_list)
-    for name, data in results.items():
+    xs = list(budget_list)
+    for name, dat in results.items():
         means, halfs = [], []
-        for vals in data["sums"]:
-            m, h = mean_ci95(vals)
-            means.append(m); halfs.append(h)
+        for vals in dat["sums"]:
+            m, h = mean_ci95(vals); means.append(m); halfs.append(h)
         means = np.asarray(means); halfs = np.asarray(halfs)
-        ax.plot(x, means, linewidth=2.0, marker="o", label=name)
-        ax.fill_between(x, means - halfs, means + halfs, alpha=0.25)
-
+        label = name.replace("Vanilla NB","VanillaNB").replace("Succ. Elim. NB","SuccElimNB")
+        ax.plot(xs, means, linewidth=2.0, marker="o", label=label)
+        ax.fill_between(xs, means - halfs, means + halfs, alpha=0.25)
     ax.set_xlabel("Budget (target)")
-    ax.set_ylabel("Weighted sum of (UB - LB) over all links (× I_d)")
-    ax.grid(True); ax.legend(title="Scheduler")
+    ax.set_ylabel("Weighted Sum of Widths  Σ_d Σ_l I_d (UB - LB)")
+    ax.grid(True); ax.legend(title="Scheduler", fontsize=14, title_fontsize=18)
     plt.tight_layout()
-    pdf_name = f"{file_name}.pdf"
-    plt.savefig(pdf_name)
-    if shutil.which("pdfcrop"):
-        os.system(f"pdfcrop {pdf_name} {pdf_name}")
-    print(f"Saved: {pdf_name}")
-
+    pdf = f"{file_name}.pdf"
+    plt.savefig(pdf); 
+    if shutil.which("pdfcrop"): os.system(f"pdfcrop {pdf} {pdf}")
+    _log(f"Saved: {pdf}")
 
+# =========================
+# 7) 幅(UB-LB)Weighted: ペアごとの I_d·最小幅 総和
+# =========================
 def plot_minwidthsum_perpair_weighted_vs_budget(
-    budget_list,
-    scheduler_names,
-    noise_model,
-    node_path_list,
-    importance_list,
-    bounces=(1, 2, 3, 4),
-    repeat=10,
-    delta=0.1,
-    verbose=True,
-    print_every=1,
+    budget_list, scheduler_names, noise_model,
+    node_path_list, importance_list,
+    bounces=(1,2,3,4), repeat=10, delta=0.1,
+    verbose=True, print_every=1,
 ):
-    """
-    y = Σ_d I_d·min_l(UB-LB) の平均 ±95%CI、x = 目標予算。
-    生データは outputs/plot_minwidthsum_perpair_weighted_vs_budget_*.pickle に保存。
-    """
     file_name = f"plot_minwidthsum_perpair_weighted_vs_budget_{noise_model}"
     root_dir = os.path.dirname(os.path.abspath(__file__))
-    output_dir = os.path.join(root_dir, "outputs")
-    os.makedirs(output_dir, exist_ok=True)
+    outdir = os.path.join(root_dir, "outputs")
+    os.makedirs(outdir, exist_ok=True)
 
-    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+    payload = _run_or_load_shared_sweep(
+        budget_list, scheduler_names, noise_model,
+        node_path_list, importance_list,
+        bounces=bounces, repeat=repeat,
+        verbose=verbose, print_every=print_every,
+    )
 
-    for k, C_total in enumerate(budget_list):
-        if verbose:
-            print(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===", flush=True)
-
-        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
-
-        def network_generator(path_num, pair_idx):
-            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
-
-        for r in range(repeat):
-            if verbose and ((r + 1) % print_every == 0 or r == 0):
-                print(f"  [repeat {r+1}/{repeat}]", flush=True)
-
-            for name in scheduler_names:
-                per_pair_results, total_cost, per_pair_details = run_scheduler(
-                    node_path_list=node_path_list,
-                    importance_list=importance_list,
-                    scheduler_name=name,
-                    bounces=list(bounces),
-                    C_total=int(C_total),
-                    network_generator=network_generator,
-                    return_details=True,
-                )
-                v = _sum_weighted_min_widths_per_pair(per_pair_details, importance_list, delta=delta)
+    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+    for name in scheduler_names:
+        for k in range(len(budget_list)):
+            for run in payload["data"][name][k]:
+                per_pair_details = run["per_pair_details"]
+                v = sum_weighted_min_widths_perpair(per_pair_details, importance_list, delta=delta)
                 results[name]["sums"][k].append(v)
-                if verbose and ((r + 1) % print_every == 0 or r == 0):
-                    print(f"    - {name}: wsum_min_perpair={v:.4f} (used={total_cost})", flush=True)
 
-    # --- Save raw data (.pickle) ---
-    file_path = os.path.join(output_dir, f"{file_name}.pickle")
-    with open(file_path, "wb") as f:
-        pickle.dump({"budget_list": list(budget_list), "results": results}, f)
-    print(f"Saved pickle: {file_path}")
-
-    # --- Plot mean ± 95% CI ---
+    # plot (mean ± 95%CI)
     plt.rc("axes", prop_cycle=default_cycler)
     fig, ax = plt.subplots()
-    x = list(budget_list)
-    for name, data in results.items():
+    xs = list(budget_list)
+    for name, dat in results.items():
         means, halfs = [], []
-        for vals in data["sums"]:
-            m, h = mean_ci95(vals)
-            means.append(m); halfs.append(h)
+        for vals in dat["sums"]:
+            m, h = mean_ci95(vals); means.append(m); halfs.append(h)
         means = np.asarray(means); halfs = np.asarray(halfs)
-        ax.plot(x, means, linewidth=2.0, marker="o", label=name)
-        ax.fill_between(x, means - halfs, means + halfs, alpha=0.25)
-
+        label = name.replace("Vanilla NB","VanillaNB").replace("Succ. Elim. NB","SuccElimNB")
+        ax.plot(xs, means, linewidth=2.0, marker="o", label=label)
+        ax.fill_between(xs, means - halfs, means + halfs, alpha=0.25)
     ax.set_xlabel("Budget (target)")
     ax.set_ylabel("Weighted sum over pairs of min (UB - LB) (× I_d)")
     ax.grid(True); ax.legend(title="Scheduler")
     plt.tight_layout()
-    pdf_name = f"{file_name}.pdf"
-    plt.savefig(pdf_name)
-    if shutil.which("pdfcrop"):
-        os.system(f"pdfcrop {pdf_name} {pdf_name}")
-    print(f"Saved: {pdf_name}")
-
-
-# =========================
-# 95%CI helpers (repeats 可変対応)
-# =========================
-# 小 n 用の簡易表(両側95%、df = n-1)
-_T95 = {
-    1: 12.706,
-    2: 4.303,
-    3: 3.182,
-    4: 2.776,
-    5: 2.571,
-    6: 2.447,
-    7: 2.365,
-    8: 2.306,
-    9: 2.262,
-    10: 2.228,
-    11: 2.201,
-    12: 2.179,
-    13: 2.160,
-    14: 2.145,
-    15: 2.131,
-    16: 2.120,
-    17: 2.110,
-    18: 2.101,
-    19: 2.093,
-    20: 2.086,
-    21: 2.080,
-    22: 2.074,
-    23: 2.069,
-    24: 2.064,
-    25: 2.060,
-    26: 2.056,
-    27: 2.052,
-    28: 2.048,
-    29: 2.045,
-}
-
-
-def tcrit_95(n: int) -> float:
-    """repeats=n に対する両側95% t臨界値 (df=n-1)。n<2 は 0 を返す。"""
-    if n <= 1:
-        return 0.0
-    df = n - 1
-    if df in _T95:
-        return _T95[df]
-    if df >= 30:
-        return 1.96  # 正規近似
-    return 2.13  # 小 n 保守値
-
-
-def mean_ci95(vals):
-    """同一 budget 上の値列 vals(可変 n)に対して (mean, halfwidth) を返す。"""
-    arr = np.asarray(vals, dtype=float)
-    n = len(arr)
-    if n == 0:
-        return 0.0, 0.0
-    if n == 1:
-        return float(arr[0]), 0.0
-    mean = float(arr.mean())
-    s = float(arr.std(ddof=1))
-    half = tcrit_95(n) * (s / math.sqrt(n))
-    return mean, half
-
-
-def _plot_with_ci_band(ax, xs, mean, half, *, label, line_kwargs=None, band_kwargs=None):
-    line_kwargs = {} if line_kwargs is None else dict(line_kwargs)
-    band_kwargs = {"alpha": 0.25} | ({} if band_kwargs is None else dict(band_kwargs))
-    ax.plot(xs, mean, label=label, **line_kwargs)
-    ax.fill_between(xs, mean - half, mean + half, **band_kwargs)
-
-
-
+    pdf = f"{file_name}.pdf"
+    plt.savefig(pdf); 
+    if shutil.which("pdfcrop"): os.system(f"pdfcrop {pdf} {pdf}")
+    _log(f"Saved: {pdf}")

+ 852 - 0
add_linkselfie/evaluationold.py

@@ -0,0 +1,852 @@
+# evaluation.py
+# Run evaluation and plot figures
+import math
+import os
+import pickle
+import time
+import shutil
+
+import matplotlib.pyplot as plt
+import numpy as np
+from cycler import cycler
+
+from algorithms import benchmark_using_algorithm  # may be used elsewhere
+from network import QuantumNetwork
+from schedulers import run_scheduler  # パッケージ化したものを使う
+
+# ---- Matplotlib style (IEEE-ish) ----
+plt.rc("font", family="Times New Roman")
+plt.rc("font", size=20)
+default_cycler = (
+    cycler(color=["#4daf4a", "#377eb8", "#e41a1c", "#984ea3", "#ff7f00", "#a65628"])
+    + cycler(marker=["s", "v", "o", "x", "*", "+"])
+    + cycler(linestyle=[":", "--", "-", "-.", "--", ":"])
+)
+plt.rc("axes", prop_cycle=default_cycler)
+
+
+# =========================
+# Fidelity generators
+# =========================
+def generate_fidelity_list_avg_gap(path_num):
+    result = []
+    fidelity_max = 1
+    fidelity_min = 0.9
+    gap = (fidelity_max - fidelity_min) / path_num
+    fidelity = fidelity_max
+    for _ in range(path_num):
+        result.append(fidelity)
+        fidelity -= gap
+    assert len(result) == path_num
+    return result
+
+
+def generate_fidelity_list_fix_gap(path_num, gap, fidelity_max=1):
+    result = []
+    fidelity = fidelity_max
+    for _ in range(path_num):
+        result.append(fidelity)
+        fidelity -= gap
+    assert len(result) == path_num
+    return result
+
+
+def generate_fidelity_list_random(path_num, alpha=0.95, beta=0.85, variance=0.1):
+    """Generate `path_num` links.
+    u_1 = alpha, u_i = beta for all i = 2, 3, ..., n.
+    Fidelity_i ~ N(u_i, variance), clipped to [0.8, 1].
+    Ensure the top-1 gap is large enough (> 0.02) for termination guarantees.
+    """
+    while True:
+        mean = [alpha] + [beta] * (path_num - 1)
+        result = []
+        for i in range(path_num):
+            mu = mean[i]
+            # Sample a Gaussian random variable and make sure its value is in the valid range
+            while True:
+                r = np.random.normal(mu, variance)
+                # Depolarizing noise and amplitude damping noise models require fidelity >= 0.5
+                # Be conservative: require >= 0.8
+                if 0.8 <= r <= 1.0:
+                    break
+            result.append(r)
+        assert len(result) == path_num
+        sorted_res = sorted(result, reverse=True)
+        # To guarantee the termination of algorithms, we require that the gap is large enough
+        if sorted_res[0] - sorted_res[1] > 0.02:
+            return result
+
+
+# =========================
+# Progress helpers (LinkSelfie風)
+# =========================
+def _start_timer():
+    return {"t0": time.time(), "last": time.time()}
+
+
+def _tick(timer):
+    now = time.time()
+    dt_total = now - timer["t0"]
+    dt_step = now - timer["last"]
+    timer["last"] = now
+    return dt_total, dt_step
+
+
+def _log(msg):
+    print(msg, flush=True)
+
+
+# =========================
+# Plots
+# =========================
+def plot_accuracy_vs_budget(
+    budget_list,          # e.g., [1000, 2000, 3000, ...] (x-axis)
+    scheduler_names,      # e.g., ["LNaive", "Greedy", ...]
+    noise_model,          # e.g., "Depolar"
+    node_path_list,       # e.g., [5, 5, 5]
+    importance_list,      # e.g., [0.4, 0.7, 1.0] (not used here, but kept for interface)
+    bounces=(1, 2, 3, 4),
+    repeat=10,
+    verbose=True,
+    print_every=1,
+):
+    file_name = f"plot_accuracy_vs_budget_{noise_model}"
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    output_dir = os.path.join(root_dir, "outputs")
+    os.makedirs(output_dir, exist_ok=True)
+    file_path = os.path.join(output_dir, f"{file_name}.pickle")
+
+    if os.path.exists(file_path):
+        _log("Pickle data exists, skip simulation and plot the data directly.")
+        _log("To rerun, delete the pickle in `outputs`.")
+        with open(file_path, "rb") as f:
+            payload = pickle.load(f)
+            budget_list = payload["budget_list"]
+            results = payload["results"]
+    else:
+        results = {name: {"accs": [[] for _ in budget_list]} for name in scheduler_names}
+        for k, C_total in enumerate(budget_list):
+            timer = _start_timer()
+            if verbose:
+                _log(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===")
+            for r in range(repeat):
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    _log(f"  [repeat {r+1}/{repeat}] generating topology …")
+                # 1リピート = 1トポロジ(全スケジューラで共有)
+                fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
+
+                def network_generator(path_num, pair_idx):
+                    return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
+
+                for name in scheduler_names:
+                    if verbose and ((r + 1) % print_every == 0 or r == 0):
+                        _log(f"    - {name}: running …")
+                    per_pair_results, _ = run_scheduler(
+                        node_path_list=node_path_list,
+                        importance_list=importance_list,
+                        scheduler_name=name,
+                        bounces=list(bounces),
+                        C_total=int(C_total),
+                        network_generator=network_generator,
+                    )
+                    acc = (
+                        float(np.mean([1.0 if c else 0.0 for (c, _cost, _bf) in per_pair_results]))
+                        if per_pair_results
+                        else 0.0
+                    )
+                    results[name]["accs"][k].append(acc)
+                    if verbose and ((r + 1) % print_every == 0 or r == 0):
+                        _log(f"      -> acc={acc:.3f}")
+            if verbose:
+                tot, _ = _tick(timer)
+                _log(f"=== done Budget={C_total} | elapsed {tot:.1f}s ===")
+
+        with open(file_path, "wb") as f:
+            pickle.dump({"budget_list": list(budget_list), "results": results}, f)
+
+    # --- Plot ---
+    plt.rc("axes", prop_cycle=default_cycler)
+    fig, ax = plt.subplots()
+    x = list(budget_list)
+    for name, data in results.items():
+        avg_accs = [float(np.mean(v)) if v else 0.0 for v in data["accs"]]
+        label = name.replace("Vanilla NB", "VanillaNB").replace("Succ. Elim. NB", "SuccElimNB")
+        ax.plot(x, avg_accs, linewidth=2.0, label=label)
+
+    ax.set_xlabel("Total Budget (C)")
+    ax.set_ylabel("Average Correctness")
+    ax.grid(True)
+    ax.legend(title="Scheduler", fontsize=14, title_fontsize=18)
+    plt.tight_layout()
+    pdf_name = f"{file_name}.pdf"
+    plt.savefig(pdf_name)
+    if shutil.which("pdfcrop"):
+        os.system(f"pdfcrop {pdf_name} {pdf_name}")
+    _log(f"Saved: {pdf_name}")
+
+
+def plot_value_vs_used(
+    budget_list,
+    scheduler_names,
+    noise_model,
+    node_path_list,
+    importance_list,
+    bounces=(1, 2, 3, 4),
+    repeat=10,
+    verbose=True,
+    print_every=1,
+):
+    """x = 実コスト平均(used)で描く版。旧 plot_value_vs_budget と同等の挙動。"""
+    file_name = f"plot_value_vs_used_{noise_model}"
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    output_dir = os.path.join(root_dir, "outputs")
+    os.makedirs(output_dir, exist_ok=True)
+
+    results = {
+        name: {"values": [[] for _ in budget_list], "costs": [[] for _ in budget_list]}
+        for name in scheduler_names
+    }
+
+    for k, C_total in enumerate(budget_list):
+        timer = _start_timer()
+        if verbose:
+            _log(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===")
+
+        # 1リピート = 1トポロジ(全スケジューラで共有)
+        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
+
+        def network_generator(path_num, pair_idx):
+            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
+
+        for r in range(repeat):
+            if verbose and ((r + 1) % print_every == 0 or r == 0):
+                _log(f"  [repeat {r+1}/{repeat}]")
+            for name in scheduler_names:
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    _log(f"    - {name}: running …")
+                per_pair_results, total_cost, per_pair_details = run_scheduler(
+                    node_path_list=node_path_list,
+                    importance_list=importance_list,
+                    scheduler_name=name,
+                    bounces=list(bounces),
+                    C_total=int(C_total),
+                    network_generator=network_generator,
+                    return_details=True,
+                )
+                # 価値の合成
+                value = 0.0
+                for d, details in enumerate(per_pair_details):
+                    alloc = details.get("alloc_by_path", {})
+                    est = details.get("est_fid_by_path", {})
+                    inner = sum(float(est.get(l, 0.0)) * int(b) for l, b in alloc.items())
+                    value += float(importance_list[d]) * inner
+
+                results[name]["values"][k].append(float(value))
+                results[name]["costs"][k].append(int(total_cost))
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    _log(f"      -> used={total_cost}, value={value:.2f}")
+
+        if verbose:
+            tot, _ = _tick(timer)
+            _log(f"=== done Budget={C_total} | elapsed {tot:.1f}s ===")
+
+    # --- Plot (x = 実コスト平均) ---
+    plt.rc("axes", prop_cycle=default_cycler)
+    fig, ax = plt.subplots()
+    for name, data in results.items():
+        xs = [float(np.mean(v)) if v else 0.0 for v in data["costs"]]
+        ys = [float(np.mean(v)) if v else 0.0 for v in data["values"]]
+        ax.plot(xs, ys, linewidth=2.0, marker="o", label=name)
+
+    ax.set_xlabel("Total Measured Cost (used)")
+    ax.set_ylabel("Total Value (Σ I_d Σ f̂_{d,l}·B_{d,l})")
+    ax.grid(True)
+    ax.legend(title="Scheduler")
+    plt.tight_layout()
+    pdf_name = f"{file_name}.pdf"
+    plt.savefig(pdf_name)
+    if shutil.which("pdfcrop"):
+        os.system(f"pdfcrop {pdf_name} {pdf_name}")
+    _log(f"Saved: {pdf_name}")
+
+
+def plot_value_vs_budget_target(
+    budget_list,
+    scheduler_names,
+    noise_model,
+    node_path_list,
+    importance_list,
+    bounces=(1, 2, 3, 4),
+    repeat=10,
+    verbose=True,
+    print_every=1,
+):
+    """x = 目標予算(指定した budget_list をそのまま x 軸に)で描く版。"""
+    file_name = f"plot_value_vs_budget_target_{noise_model}"
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    output_dir = os.path.join(root_dir, "outputs")
+    os.makedirs(output_dir, exist_ok=True)
+
+    results = {
+        name: {"values": [[] for _ in budget_list], "costs": [[] for _ in budget_list]}
+        for name in scheduler_names
+    }
+
+    for k, C_total in enumerate(budget_list):
+        timer = _start_timer()
+        if verbose:
+            _log(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===")
+
+        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
+
+        def network_generator(path_num, pair_idx):
+            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
+
+        for r in range(repeat):
+            if verbose and ((r + 1) % print_every == 0 or r == 0):
+                _log(f"  [repeat {r+1}/{repeat}]")
+            for name in scheduler_names:
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    _log(f"    - {name}: running …")
+                per_pair_results, total_cost, per_pair_details = run_scheduler(
+                    node_path_list=node_path_list,
+                    importance_list=importance_list,
+                    scheduler_name=name,
+                    bounces=list(bounces),
+                    C_total=int(C_total),
+                    network_generator=network_generator,
+                    return_details=True,
+                )
+                value = 0.0
+                for d, details in enumerate(per_pair_details):
+                    alloc = details.get("alloc_by_path", {})
+                    est = details.get("est_fid_by_path", {})
+                    inner = sum(float(est.get(l, 0.0)) * int(b) for l, b in alloc.items())
+                    value += float(importance_list[d]) * inner
+
+                results[name]["values"][k].append(float(value))
+                results[name]["costs"][k].append(int(total_cost))
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    _log(f"      -> used={total_cost}, value={value:.2f}")
+
+        if verbose:
+            tot, _ = _tick(timer)
+            _log(f"=== done Budget={C_total} | elapsed {tot:.1f}s ===")
+
+    # --- Plot (x = 目標予算) ---
+    plt.rc("axes", prop_cycle=default_cycler)
+    fig, ax = plt.subplots()
+    x = list(budget_list)
+    for name, data in results.items():
+        ys = [float(np.mean(v)) if v else 0.0 for v in data["values"]]
+        ax.plot(x, ys, linewidth=2.0, marker="o", label=name)
+
+    ax.set_xlabel("Budget (target)")
+    ax.set_ylabel("Total Value (Σ I_d Σ f̂_{d,l}·B_{d,l})")
+    ax.grid(True)
+    ax.legend(title="Scheduler")
+    plt.tight_layout()
+    pdf_name = f"{file_name}.pdf"
+    plt.savefig(pdf_name)
+    if shutil.which("pdfcrop"):
+        os.system(f"pdfcrop {pdf_name} {pdf_name}")
+    _log(f"Saved: {pdf_name}")
+
+
+# =========================
+# CI width helpers and plots
+# =========================
+def _ci_radius_hoeffding(n: int, delta: float = 0.1) -> float:
+    if n <= 0:
+        return 1.0
+    return math.sqrt(0.5 * math.log(2.0 / delta) / n)
+
+
+# =========================
+# Width-sum metrics (new)
+# =========================
+
+def _sum_widths_all_links(per_pair_details, delta: float = 0.1) -> float:
+    """
+    すべてのペア・すべてのリンクについて、(UB - LB) を合計。
+    est が無いリンクはスキップ(=寄与0)。測定していないリンクは数えません。
+    """
+    total = 0.0
+    for det in per_pair_details:
+        alloc = det.get("alloc_by_path", {})  # n = 測定回数
+        est   = det.get("est_fid_by_path", {})  # 標本平均
+        for pid, m in est.items():
+            n = int(alloc.get(pid, 0))
+            rad = _ci_radius_hoeffding(n, delta)
+            lb = max(0.0, float(m) - rad)
+            ub = min(1.0, float(m) + rad)
+            total += (ub - lb)
+    return float(total)
+
+
+def _sum_min_widths_per_pair(per_pair_details, delta: float = 0.1) -> float:
+    """
+    ペアごとにリンクの (UB - LB) を算出し、その「最小値」を取り、全ペアで合計。
+    est が空のペアは保守的に 1.0 を加算(“全く分からない”幅として扱う)。
+    """
+    s = 0.0
+    for det in per_pair_details:
+        alloc = det.get("alloc_by_path", {})
+        est   = det.get("est_fid_by_path", {})
+        if not est:
+            s += 1.0
+            continue
+        widths = []
+        for pid, m in est.items():
+            n = int(alloc.get(pid, 0))
+            rad = _ci_radius_hoeffding(n, delta)
+            lb = max(0.0, float(m) - rad)
+            ub = min(1.0, float(m) + rad)
+            widths.append(ub - lb)
+        s += (min(widths) if widths else 1.0)
+    return float(s)
+
+
+def plot_widthsum_alllinks_vs_budget(
+    budget_list,
+    scheduler_names,
+    noise_model,
+    node_path_list,
+    importance_list,
+    bounces=(1, 2, 3, 4),
+    repeat=10,
+    delta=0.1,
+    verbose=True,
+    print_every=1,
+):
+    """
+    y = 全リンク(UB-LB)総和 の平均 ±95%CI を、x = 目標予算 で描画。
+    生データは outputs/plot_widthsum_alllinks_vs_budget_*.pickle に保存。
+    """
+    file_name = f"plot_widthsum_alllinks_vs_budget_{noise_model}"
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    output_dir = os.path.join(root_dir, "outputs")
+    os.makedirs(output_dir, exist_ok=True)
+
+    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+
+    for k, C_total in enumerate(budget_list):
+        if verbose:
+            print(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===", flush=True)
+
+        # 1リピート=1トポロジ(全スケジューラ共有)
+        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
+
+        def network_generator(path_num, pair_idx):
+            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
+
+        for r in range(repeat):
+            if verbose and ((r + 1) % print_every == 0 or r == 0):
+                print(f"  [repeat {r+1}/{repeat}]", flush=True)
+
+            for name in scheduler_names:
+                per_pair_results, total_cost, per_pair_details = run_scheduler(
+                    node_path_list=node_path_list,
+                    importance_list=importance_list,
+                    scheduler_name=name,
+                    bounces=list(bounces),
+                    C_total=int(C_total),
+                    network_generator=network_generator,
+                    return_details=True,
+                )
+                v = _sum_widths_all_links(per_pair_details, delta=delta)
+                results[name]["sums"][k].append(v)
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    print(f"    - {name}: sum_alllinks={v:.4f} (used={total_cost})", flush=True)
+
+    # --- Save raw data (.pickle) ---
+    file_path = os.path.join(output_dir, f"{file_name}.pickle")
+    with open(file_path, "wb") as f:
+        pickle.dump({"budget_list": list(budget_list), "results": results}, f)
+    print(f"Saved pickle: {file_path}")
+
+    # --- Plot mean ± 95% CI across repeats ---
+    plt.rc("axes", prop_cycle=default_cycler)
+    fig, ax = plt.subplots()
+    x = list(budget_list)
+    for name, data in results.items():
+        means, halfs = [], []
+        for vals in data["sums"]:
+            m, h = mean_ci95(vals)
+            means.append(m); halfs.append(h)
+        means = np.asarray(means); halfs = np.asarray(halfs)
+        ax.plot(x, means, linewidth=2.0, marker="o", label=name)
+        ax.fill_between(x, means - halfs, means + halfs, alpha=0.25)
+
+    ax.set_xlabel("Budget (target)")
+    ax.set_ylabel("Sum of (UB - LB) over all links")
+    ax.grid(True)
+    ax.legend(title="Scheduler")
+    plt.tight_layout()
+    pdf_name = f"{file_name}.pdf"
+    plt.savefig(pdf_name)
+    if shutil.which("pdfcrop"):
+        os.system(f"pdfcrop {pdf_name} {pdf_name}")
+    print(f"Saved: {pdf_name}")
+
+
+def plot_minwidthsum_perpair_vs_budget(
+    budget_list,
+    scheduler_names,
+    noise_model,
+    node_path_list,
+    importance_list,
+    bounces=(1, 2, 3, 4),
+    repeat=10,
+    delta=0.1,
+    verbose=True,
+    print_every=1,
+):
+    """
+    y = ペアごとの (UB-LB) 最小値の合計 の平均 ±95%CI、x = 目標予算。
+    生データは outputs/plot_minwidthsum_perpair_vs_budget_*.pickle に保存。
+    """
+    file_name = f"plot_minwidthsum_perpair_vs_budget_{noise_model}"
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    output_dir = os.path.join(root_dir, "outputs")
+    os.makedirs(output_dir, exist_ok=True)
+
+    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+
+    for k, C_total in enumerate(budget_list):
+        if verbose:
+            print(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===", flush=True)
+
+        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
+
+        def network_generator(path_num, pair_idx):
+            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
+
+        for r in range(repeat):
+            if verbose and ((r + 1) % print_every == 0 or r == 0):
+                print(f"  [repeat {r+1}/{repeat}]", flush=True)
+
+            for name in scheduler_names:
+                per_pair_results, total_cost, per_pair_details = run_scheduler(
+                    node_path_list=node_path_list,
+                    importance_list=importance_list,
+                    scheduler_name=name,
+                    bounces=list(bounces),
+                    C_total=int(C_total),
+                    network_generator=network_generator,
+                    return_details=True,
+                )
+                v = _sum_min_widths_per_pair(per_pair_details, delta=delta)
+                results[name]["sums"][k].append(v)
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    print(f"    - {name}: sum_min_perpair={v:.4f} (used={total_cost})", flush=True)
+
+    # --- Save raw data (.pickle) ---
+    file_path = os.path.join(output_dir, f"{file_name}.pickle")
+    with open(file_path, "wb") as f:
+        pickle.dump({"budget_list": list(budget_list), "results": results}, f)
+    print(f"Saved pickle: {file_path}")
+
+    # --- Plot mean ± 95% CI across repeats ---
+    plt.rc("axes", prop_cycle=default_cycler)
+    fig, ax = plt.subplots()
+    x = list(budget_list)
+    for name, data in results.items():
+        means, halfs = [], []
+        for vals in data["sums"]:
+            m, h = mean_ci95(vals)
+            means.append(m); halfs.append(h)
+        means = np.asarray(means); halfs = np.asarray(halfs)
+        ax.plot(x, means, linewidth=2.0, marker="o", label=name)
+        ax.fill_between(x, means - halfs, means + halfs, alpha=0.25)
+
+    ax.set_xlabel("Budget (target)")
+    ax.set_ylabel("Sum over pairs of min (UB - LB)")
+    ax.grid(True)
+    ax.legend(title="Scheduler")
+    plt.tight_layout()
+    pdf_name = f"{file_name}.pdf"
+    plt.savefig(pdf_name)
+    if shutil.which("pdfcrop"):
+        os.system(f"pdfcrop {pdf_name} {pdf_name}")
+    print(f"Saved: {pdf_name}")
+
+
+
+# =========================
+# Weighted width-sum metrics (add-on)
+# =========================
+
+def _sum_weighted_widths_all_links(per_pair_details, importance_list, delta: float = 0.1) -> float:
+    """
+    すべてのペア・すべてのリンクの (UB-LB) に、ペア重要度 I_d を掛けて合計。
+    importance_list[d] が無ければ I_d=1.0 として扱う。
+    """
+    total = 0.0
+    for d, det in enumerate(per_pair_details):
+        I = float(importance_list[d]) if d < len(importance_list) else 1.0
+        alloc = det.get("alloc_by_path", {})
+        est   = det.get("est_fid_by_path", {})
+        for pid, m in est.items():
+            n = int(alloc.get(pid, 0))
+            rad = _ci_radius_hoeffding(n, delta)
+            lb = max(0.0, float(m) - rad)
+            ub = min(1.0, float(m) + rad)
+            total += I * (ub - lb)
+    return float(total)
+
+
+def _sum_weighted_min_widths_per_pair(per_pair_details, importance_list, delta: float = 0.1) -> float:
+    """
+    ペア d ごとに min_l (UB-LB) を計算し、I_d を掛けて全ペアで合計。
+    est が空のペアは保守的に幅=1.0 として I_d*1.0 を加算。
+    """
+    s = 0.0
+    for d, det in enumerate(per_pair_details):
+        I = float(importance_list[d]) if d < len(importance_list) else 1.0
+        alloc = det.get("alloc_by_path", {})
+        est   = det.get("est_fid_by_path", {})
+        if not est:
+            s += I * 1.0
+            continue
+        widths = []
+        for pid, m in est.items():
+            n = int(alloc.get(pid, 0))
+            rad = _ci_radius_hoeffding(n, delta)
+            lb = max(0.0, float(m) - rad)
+            ub = min(1.0, float(m) + rad)
+            widths.append(ub - lb)
+        s += I * (min(widths) if widths else 1.0)
+    return float(s)
+
+
+def plot_widthsum_alllinks_weighted_vs_budget(
+    budget_list,
+    scheduler_names,
+    noise_model,
+    node_path_list,
+    importance_list,
+    bounces=(1, 2, 3, 4),
+    repeat=10,
+    delta=0.1,
+    verbose=True,
+    print_every=1,
+):
+    """
+    y = Σ_d Σ_l I_d·(UB-LB) の平均 ±95%CI、x = 目標予算。
+    生データは outputs/plot_widthsum_alllinks_weighted_vs_budget_*.pickle に保存。
+    """
+    file_name = f"plot_widthsum_alllinks_weighted_vs_budget_{noise_model}"
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    output_dir = os.path.join(root_dir, "outputs")
+    os.makedirs(output_dir, exist_ok=True)
+
+    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+
+    for k, C_total in enumerate(budget_list):
+        if verbose:
+            print(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===", flush=True)
+
+        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
+
+        def network_generator(path_num, pair_idx):
+            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
+
+        for r in range(repeat):
+            if verbose and ((r + 1) % print_every == 0 or r == 0):
+                print(f"  [repeat {r+1}/{repeat}]", flush=True)
+
+            for name in scheduler_names:
+                per_pair_results, total_cost, per_pair_details = run_scheduler(
+                    node_path_list=node_path_list,
+                    importance_list=importance_list,
+                    scheduler_name=name,
+                    bounces=list(bounces),
+                    C_total=int(C_total),
+                    network_generator=network_generator,
+                    return_details=True,
+                )
+                v = _sum_weighted_widths_all_links(per_pair_details, importance_list, delta=delta)
+                results[name]["sums"][k].append(v)
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    print(f"    - {name}: wsum_alllinks={v:.4f} (used={total_cost})", flush=True)
+
+    # --- Save raw data (.pickle) ---
+    file_path = os.path.join(output_dir, f"{file_name}.pickle")
+    with open(file_path, "wb") as f:
+        pickle.dump({"budget_list": list(budget_list), "results": results}, f)
+    print(f"Saved pickle: {file_path}")
+
+    # --- Plot mean ± 95% CI ---
+    plt.rc("axes", prop_cycle=default_cycler)
+    fig, ax = plt.subplots()
+    x = list(budget_list)
+    for name, data in results.items():
+        means, halfs = [], []
+        for vals in data["sums"]:
+            m, h = mean_ci95(vals)
+            means.append(m); halfs.append(h)
+        means = np.asarray(means); halfs = np.asarray(halfs)
+        ax.plot(x, means, linewidth=2.0, marker="o", label=name)
+        ax.fill_between(x, means - halfs, means + halfs, alpha=0.25)
+
+    ax.set_xlabel("Budget (target)")
+    ax.set_ylabel("Weighted sum of (UB - LB) over all links (× I_d)")
+    ax.grid(True); ax.legend(title="Scheduler")
+    plt.tight_layout()
+    pdf_name = f"{file_name}.pdf"
+    plt.savefig(pdf_name)
+    if shutil.which("pdfcrop"):
+        os.system(f"pdfcrop {pdf_name} {pdf_name}")
+    print(f"Saved: {pdf_name}")
+
+
+def plot_minwidthsum_perpair_weighted_vs_budget(
+    budget_list,
+    scheduler_names,
+    noise_model,
+    node_path_list,
+    importance_list,
+    bounces=(1, 2, 3, 4),
+    repeat=10,
+    delta=0.1,
+    verbose=True,
+    print_every=1,
+):
+    """
+    y = Σ_d I_d·min_l(UB-LB) の平均 ±95%CI、x = 目標予算。
+    生データは outputs/plot_minwidthsum_perpair_weighted_vs_budget_*.pickle に保存。
+    """
+    file_name = f"plot_minwidthsum_perpair_weighted_vs_budget_{noise_model}"
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    output_dir = os.path.join(root_dir, "outputs")
+    os.makedirs(output_dir, exist_ok=True)
+
+    results = {name: {"sums": [[] for _ in budget_list]} for name in scheduler_names}
+
+    for k, C_total in enumerate(budget_list):
+        if verbose:
+            print(f"\n=== [{noise_model}] Budget={C_total} ({k+1}/{len(budget_list)}) ===", flush=True)
+
+        fidelity_bank = [generate_fidelity_list_random(n) for n in node_path_list]
+
+        def network_generator(path_num, pair_idx):
+            return QuantumNetwork(path_num, fidelity_bank[pair_idx], noise_model)
+
+        for r in range(repeat):
+            if verbose and ((r + 1) % print_every == 0 or r == 0):
+                print(f"  [repeat {r+1}/{repeat}]", flush=True)
+
+            for name in scheduler_names:
+                per_pair_results, total_cost, per_pair_details = run_scheduler(
+                    node_path_list=node_path_list,
+                    importance_list=importance_list,
+                    scheduler_name=name,
+                    bounces=list(bounces),
+                    C_total=int(C_total),
+                    network_generator=network_generator,
+                    return_details=True,
+                )
+                v = _sum_weighted_min_widths_per_pair(per_pair_details, importance_list, delta=delta)
+                results[name]["sums"][k].append(v)
+                if verbose and ((r + 1) % print_every == 0 or r == 0):
+                    print(f"    - {name}: wsum_min_perpair={v:.4f} (used={total_cost})", flush=True)
+
+    # --- Save raw data (.pickle) ---
+    file_path = os.path.join(output_dir, f"{file_name}.pickle")
+    with open(file_path, "wb") as f:
+        pickle.dump({"budget_list": list(budget_list), "results": results}, f)
+    print(f"Saved pickle: {file_path}")
+
+    # --- Plot mean ± 95% CI ---
+    plt.rc("axes", prop_cycle=default_cycler)
+    fig, ax = plt.subplots()
+    x = list(budget_list)
+    for name, data in results.items():
+        means, halfs = [], []
+        for vals in data["sums"]:
+            m, h = mean_ci95(vals)
+            means.append(m); halfs.append(h)
+        means = np.asarray(means); halfs = np.asarray(halfs)
+        ax.plot(x, means, linewidth=2.0, marker="o", label=name)
+        ax.fill_between(x, means - halfs, means + halfs, alpha=0.25)
+
+    ax.set_xlabel("Budget (target)")
+    ax.set_ylabel("Weighted sum over pairs of min (UB - LB) (× I_d)")
+    ax.grid(True); ax.legend(title="Scheduler")
+    plt.tight_layout()
+    pdf_name = f"{file_name}.pdf"
+    plt.savefig(pdf_name)
+    if shutil.which("pdfcrop"):
+        os.system(f"pdfcrop {pdf_name} {pdf_name}")
+    print(f"Saved: {pdf_name}")
+
+
+# =========================
+# 95%CI helpers (repeats 可変対応)
+# =========================
+# 小 n 用の簡易表(両側95%、df = n-1)
+_T95 = {
+    1: 12.706,
+    2: 4.303,
+    3: 3.182,
+    4: 2.776,
+    5: 2.571,
+    6: 2.447,
+    7: 2.365,
+    8: 2.306,
+    9: 2.262,
+    10: 2.228,
+    11: 2.201,
+    12: 2.179,
+    13: 2.160,
+    14: 2.145,
+    15: 2.131,
+    16: 2.120,
+    17: 2.110,
+    18: 2.101,
+    19: 2.093,
+    20: 2.086,
+    21: 2.080,
+    22: 2.074,
+    23: 2.069,
+    24: 2.064,
+    25: 2.060,
+    26: 2.056,
+    27: 2.052,
+    28: 2.048,
+    29: 2.045,
+}
+
+
+def tcrit_95(n: int) -> float:
+    """repeats=n に対する両側95% t臨界値 (df=n-1)。n<2 は 0 を返す。"""
+    if n <= 1:
+        return 0.0
+    df = n - 1
+    if df in _T95:
+        return _T95[df]
+    if df >= 30:
+        return 1.96  # 正規近似
+    return 2.13  # 小 n 保守値
+
+
+def mean_ci95(vals):
+    """同一 budget 上の値列 vals(可変 n)に対して (mean, halfwidth) を返す。"""
+    arr = np.asarray(vals, dtype=float)
+    n = len(arr)
+    if n == 0:
+        return 0.0, 0.0
+    if n == 1:
+        return float(arr[0]), 0.0
+    mean = float(arr.mean())
+    s = float(arr.std(ddof=1))
+    half = tcrit_95(n) * (s / math.sqrt(n))
+    return mean, half
+
+
+def _plot_with_ci_band(ax, xs, mean, half, *, label, line_kwargs=None, band_kwargs=None):
+    line_kwargs = {} if line_kwargs is None else dict(line_kwargs)
+    band_kwargs = {"alpha": 0.25} | ({} if band_kwargs is None else dict(band_kwargs))
+    ax.plot(xs, mean, label=label, **line_kwargs)
+    ax.fill_between(xs, mean - half, mean + half, **band_kwargs)

+ 24 - 0
add_linkselfie/fidelity.py

@@ -0,0 +1,24 @@
+
+# fidelity.py
+# Keep fidelity/importance generators small and explicit.
+import random
+
+def generate_fidelity_list_random(path_num: int, alpha: float = 0.95, beta: float = 0.85, variance: float = 0.04):
+    """
+    Generate `path_num` fidelities.
+    One "good" link around alpha, the rest around beta, clipped to [0.5, 1.0].
+    """
+    vals = []
+    for i in range(path_num):
+        mu = alpha if i == 0 else beta
+        # simple Gaussian around mu, but clipped
+        v = random.gauss(mu, variance**0.5)
+        v = max(0.5, min(1.0, v))
+        vals.append(v)
+    # shuffle so the "good" link is not always index 0
+    random.shuffle(vals)
+    return vals
+
+def generate_importance_list_random(n: int, low: float = 0.5, high: float = 2.0):
+    """Return a list of n importances I_n ~ Uniform[low, high]."""
+    return [random.uniform(low, high) for _ in range(n)]

+ 10 - 50
add_linkselfie/main.py

@@ -2,26 +2,12 @@
 # -*- coding: utf-8 -*-
 """
 main.py — evaluation.py の各種プロットを一括実行
-  Unweighted:
-    1) plot_accuracy_vs_budget
-    2) plot_value_vs_used
-    3) plot_value_vs_budget_target
-    4) plot_widthsum_alllinks_vs_budget
-    5) plot_minwidthsum_perpair_vs_budget
-  Weighted(重要度 I_d を幅に掛ける版):
-    6) plot_widthsum_alllinks_weighted_vs_budget
-    7) plot_minwidthsum_perpair_weighted_vs_budget
-
-出力:
-  - 生データ pickle -> ./outputs/
-  - 図 PDF -> カレントディレクトリ
 """
 
 from multiprocessing.pool import Pool
 import os
 import random
 
-# 任意:あなたの環境のユーティリティ。無ければフォールバック。
 try:
     from utils import set_random_seed
 except Exception:
@@ -33,35 +19,27 @@ except Exception:
         except Exception:
             pass
 
-# evaluation 側のプロット関数
 from evaluation import (
-    # Accuracy / Value 系
     plot_accuracy_vs_budget,
     plot_value_vs_used,
     plot_value_vs_budget_target,
-
-    # 幅(UB-LB)系 - Unweighted
     plot_widthsum_alllinks_vs_budget,
     plot_minwidthsum_perpair_vs_budget,
-
-    # 幅(UB-LB)系 - Weighted (× I_d)
     plot_widthsum_alllinks_weighted_vs_budget,
     plot_minwidthsum_perpair_weighted_vs_budget,
 )
 
-
 def main():
-    # ===== 実験パラメータ =====
     set_random_seed(12)
     num_workers      = max(1, (os.cpu_count() or 4) // 2)
-    noise_model_list = ["Depolar"]              # 例: ["Depolar", "Dephase"]
-    scheduler_names  = ["LNaive", "Greedy"]     # 実装済みスケジューラ名に合わせて
-    node_path_list   = [5, 5, 5]                # ペアごとのリンク本数
-    importance_list  = [0.3, 0.6, 0.9]          # value系&weighted幅系で使用
-    budget_list      = [3000, 6000, 9000]
-    bounces          = (1, 2, 3, 4)             # 測定深さ候補(あなたの定義に従う)
-    repeat           = 10                       # 反復回数(精度と時間のトレードオフ)
-    delta            = 0.1                      # 幅用の信頼度パラメータ(Hoeffding)
+    noise_model_list = ["Depolar"]
+    scheduler_names  = ["LNaive", "Greedy"]
+    node_path_list   = [5, 5, 5]
+    importance_list  = [0.3, 0.6, 0.9]
+    budget_list      = [1000,2000,3000,4000,5000,6000,7000,8000,9000,10000]
+    bounces          = (1, 2, 3, 4)
+    repeat           = 10
+    delta            = 0.1
 
     print("=== Config ===")
     print(f"workers={num_workers}, noise_models={noise_model_list}")
@@ -70,60 +48,46 @@ def main():
     print(f"budgets={budget_list}, bounces={bounces}, repeat={repeat}, delta={delta}")
     print("================\n")
 
-    # ===== 実行キュー =====
     p = Pool(processes=num_workers)
     jobs = []
 
     for noise_model in noise_model_list:
-        # --- Accuracy ---
         jobs.append(p.apply_async(
             plot_accuracy_vs_budget,
             args=(budget_list, scheduler_names, noise_model,
                   node_path_list, importance_list, bounces, repeat),
             kwds={"verbose": True}
         ))
-
-        # --- Value: x=used(実コスト平均) ---
         jobs.append(p.apply_async(
             plot_value_vs_used,
             args=(budget_list, scheduler_names, noise_model,
                   node_path_list, importance_list, bounces, repeat),
             kwds={"verbose": True}
         ))
-
-        # --- Value: x=target(指定予算) ---
         jobs.append(p.apply_async(
             plot_value_vs_budget_target,
             args=(budget_list, scheduler_names, noise_model,
                   node_path_list, importance_list, bounces, repeat),
             kwds={"verbose": True}
         ))
-
-        # --- Width (UB-LB) Unweighted: 全リンク総和 ---
         jobs.append(p.apply_async(
             plot_widthsum_alllinks_vs_budget,
             args=(budget_list, scheduler_names, noise_model,
                   node_path_list, importance_list, bounces, repeat),
             kwds={"delta": delta, "verbose": True}
         ))
-
-        # --- Width (UB-LB) Unweighted: ペア最小幅の総和 ---
         jobs.append(p.apply_async(
             plot_minwidthsum_perpair_vs_budget,
             args=(budget_list, scheduler_names, noise_model,
                   node_path_list, importance_list, bounces, repeat),
             kwds={"delta": delta, "verbose": True}
         ))
-
-        # --- Width (UB-LB) Weighted: 全リンク I_d·幅 総和 ---
         jobs.append(p.apply_async(
             plot_widthsum_alllinks_weighted_vs_budget,
             args=(budget_list, scheduler_names, noise_model,
                   node_path_list, importance_list, bounces, repeat),
             kwds={"delta": delta, "verbose": True}
         ))
-
-        # --- Width (UB-LB) Weighted: ペアごとの I_d·最小幅 総和 ---
         jobs.append(p.apply_async(
             plot_minwidthsum_perpair_weighted_vs_budget,
             args=(budget_list, scheduler_names, noise_model,
@@ -131,15 +95,11 @@ def main():
             kwds={"delta": delta, "verbose": True}
         ))
 
-    # ===== 実行 & 同期 =====
-    p.close()
-    p.join()
-    for j in jobs:
-        j.get()
+    p.close(); p.join()
+    for j in jobs: j.get()
 
     print("\nAll jobs finished.")
     print("Pickles -> ./outputs/,  PDF -> カレントディレクトリ に保存されます。")
 
-
 if __name__ == "__main__":
     main()

+ 145 - 0
add_linkselfie/mainold.py

@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+main.py — evaluation.py の各種プロットを一括実行
+  Unweighted:
+    1) plot_accuracy_vs_budget
+    2) plot_value_vs_used
+    3) plot_value_vs_budget_target
+    4) plot_widthsum_alllinks_vs_budget
+    5) plot_minwidthsum_perpair_vs_budget
+  Weighted(重要度 I_d を幅に掛ける版):
+    6) plot_widthsum_alllinks_weighted_vs_budget
+    7) plot_minwidthsum_perpair_weighted_vs_budget
+
+出力:
+  - 生データ pickle -> ./outputs/
+  - 図 PDF -> カレントディレクトリ
+"""
+
+from multiprocessing.pool import Pool
+import os
+import random
+
+# 任意:あなたの環境のユーティリティ。無ければフォールバック。
+try:
+    from utils import set_random_seed
+except Exception:
+    def set_random_seed(seed: int = 12):
+        random.seed(seed)
+        try:
+            import numpy as np
+            np.random.seed(seed)
+        except Exception:
+            pass
+
+# evaluation 側のプロット関数
+from evaluation import (
+    # Accuracy / Value 系
+    plot_accuracy_vs_budget,
+    plot_value_vs_used,
+    plot_value_vs_budget_target,
+
+    # 幅(UB-LB)系 - Unweighted
+    plot_widthsum_alllinks_vs_budget,
+    plot_minwidthsum_perpair_vs_budget,
+
+    # 幅(UB-LB)系 - Weighted (× I_d)
+    plot_widthsum_alllinks_weighted_vs_budget,
+    plot_minwidthsum_perpair_weighted_vs_budget,
+)
+
+
+def main():
+    # ===== 実験パラメータ =====
+    set_random_seed(12)
+    num_workers      = max(1, (os.cpu_count() or 4) // 2)
+    noise_model_list = ["Depolar"]              # 例: ["Depolar", "Dephase"]
+    scheduler_names  = ["LNaive", "Greedy"]     # 実装済みスケジューラ名に合わせて
+    node_path_list   = [5, 5, 5]                # ペアごとのリンク本数
+    importance_list  = [0.3, 0.6, 0.9]          # value系&weighted幅系で使用
+    budget_list      = [3000, 6000, 9000, 12000, 15000, 18000]
+    bounces          = (1, 2, 3, 4)             # 測定深さ候補(あなたの定義に従う)
+    repeat           = 10                       # 反復回数(精度と時間のトレードオフ)
+    delta            = 0.1                      # 幅用の信頼度パラメータ(Hoeffding)
+
+    print("=== Config ===")
+    print(f"workers={num_workers}, noise_models={noise_model_list}")
+    print(f"schedulers={scheduler_names}")
+    print(f"node_path_list={node_path_list}, importance_list={importance_list}")
+    print(f"budgets={budget_list}, bounces={bounces}, repeat={repeat}, delta={delta}")
+    print("================\n")
+
+    # ===== 実行キュー =====
+    p = Pool(processes=num_workers)
+    jobs = []
+
+    for noise_model in noise_model_list:
+        # --- Accuracy ---
+        jobs.append(p.apply_async(
+            plot_accuracy_vs_budget,
+            args=(budget_list, scheduler_names, noise_model,
+                  node_path_list, importance_list, bounces, repeat),
+            kwds={"verbose": True}
+        ))
+
+        # --- Value: x=used(実コスト平均) ---
+        jobs.append(p.apply_async(
+            plot_value_vs_used,
+            args=(budget_list, scheduler_names, noise_model,
+                  node_path_list, importance_list, bounces, repeat),
+            kwds={"verbose": True}
+        ))
+
+        # --- Value: x=target(指定予算) ---
+        jobs.append(p.apply_async(
+            plot_value_vs_budget_target,
+            args=(budget_list, scheduler_names, noise_model,
+                  node_path_list, importance_list, bounces, repeat),
+            kwds={"verbose": True}
+        ))
+
+        # --- Width (UB-LB) Unweighted: 全リンク総和 ---
+        jobs.append(p.apply_async(
+            plot_widthsum_alllinks_vs_budget,
+            args=(budget_list, scheduler_names, noise_model,
+                  node_path_list, importance_list, bounces, repeat),
+            kwds={"delta": delta, "verbose": True}
+        ))
+
+        # --- Width (UB-LB) Unweighted: ペア最小幅の総和 ---
+        jobs.append(p.apply_async(
+            plot_minwidthsum_perpair_vs_budget,
+            args=(budget_list, scheduler_names, noise_model,
+                  node_path_list, importance_list, bounces, repeat),
+            kwds={"delta": delta, "verbose": True}
+        ))
+
+        # --- Width (UB-LB) Weighted: 全リンク I_d·幅 総和 ---
+        jobs.append(p.apply_async(
+            plot_widthsum_alllinks_weighted_vs_budget,
+            args=(budget_list, scheduler_names, noise_model,
+                  node_path_list, importance_list, bounces, repeat),
+            kwds={"delta": delta, "verbose": True}
+        ))
+
+        # --- Width (UB-LB) Weighted: ペアごとの I_d·最小幅 総和 ---
+        jobs.append(p.apply_async(
+            plot_minwidthsum_perpair_weighted_vs_budget,
+            args=(budget_list, scheduler_names, noise_model,
+                  node_path_list, importance_list, bounces, repeat),
+            kwds={"delta": delta, "verbose": True}
+        ))
+
+    # ===== 実行 & 同期 =====
+    p.close()
+    p.join()
+    for j in jobs:
+        j.get()
+
+    print("\nAll jobs finished.")
+    print("Pickles -> ./outputs/,  PDF -> カレントディレクトリ に保存されます。")
+
+
+if __name__ == "__main__":
+    main()

+ 154 - 0
add_linkselfie/memo.org

@@ -0,0 +1,154 @@
+** ミーティング
+*** 対話の引用部分
+ノードS〜ノードD 間のリンク集合 L だけでなく、
+ノードS〜ノードD_n間のリンク集合 L_n (1 <= n <= N, N はノードSの隣接ノード数)がそれぞれ入力される。
+ノードS〜ノードD_n間の重要度 I_n (0〜1 の値) が入力として与えられる。
+総バウンスコスト C が入力として与えられる。
+この時、ノードS〜ノードD_n のノードペアの中で、
+K 個のノードペア (S, D_s_1), (S, D_s_2), ... (S, D_s_K) における
+忠実度が最大のリンクをそれぞれ発見する。
+ただし、発見するリンクのノードペア数 K は、
+重要度と忠実度の積の総和、つまり
+I = \sum_{k = 1}^K I_s_k * F_s_k
+が最大となるように定める。
+ここで F_n は (S, D_n) 間において忠実度の最大のリンクの忠実度である。
+
+A. この拡張問題は、複数のノードペア (S, Dₙ) に対して、限られたリソース(バウンスコスト)の中で、重要なノードペアの
+高忠実度リンクを選定する最適化問題です。以下に、この問題の形式的な定義
+を記述します。
+
+*** 考えたこと
+- 先生のストーリー案に関するchatGPTとの対話をもう1度読みなおした
+- 現状の評価指標では、 **発見するリンクのノードペア数 K** は、重要度と忠
+  実度の積の総和、つまりI =\sum_{k = 1}^K I_s_k * F_s_kが最大となるよ
+  うに定める。という部分の **発見するリンク** の定義をしていない。
+- societyではリンク価値を定義し、それとバウンスの積をとることでその価
+  値スコアを最大化する問題として解いていた
+  - しかしこの方法では発見するリンクを動的に決めることはできない
+  - また、評価指標を最大化するような分配方法が最も価値が高いリンクに全
+    測定予算を配ることであり、これは測定精度と資源削減のトレードオフを
+    考えていると言えない
+
+- そこで、これらの問題を解決する評価指標として信頼区間幅UB,LBを用いた
+  評価指標を導入する案を考えた。(UB,LBは推定忠実度の上限、下限)
+- 将来的には信頼区間幅がある閾値 x よりも小さいリンクを何本発見できた
+  かというような指標を考えている
+  
+解くべき問題
+ストーリー
+指標
+
+指標に関しては3章を書いてみせる
+
+
+
+同じような問題を解いている人がいればその手法を使う
+多腕バンディッド、それ以外でも
+
+まずストーリーをアップデートする
+
+オーバーヘッドどうするか
+これに関しては言わないといけない
+忠実度のゆらぎがどの程度でおこるかを考える必要がある
+
+- IEEE ICC 2026 の論文提出締切は 2025年9月29日。
+  - 論文の質はおいといて先に6ページの論文を書く
+  - 何点でもいいから期限には間にあわせる
+
+- 今ある結果、ストーリーで6枚書く
+
+
+
+
+計画
+23くらいまでにストーリーをきめる?
+
+
+逆順で
+
+
+
+29に6ページ(それ以降に校閲依頼)
+先生にはストーリーの校閲依頼
+
+足りないもの
+
+
+ミーティングをこまかく
+週1ではたりない。こまかく短く
+*** 9/26までに絶対必要なものを埋める
+アルゴリズム すぐおわる shun
+アルゴリズムの前提条件 制約条件など、どういう環境を仮定しているか
+-> 時間かかりそう shun
+
+
+関連研究(引用した論文) shun
+-> 時間かかる
+オーバーヘッドの話(必要不必要関係なく) 証拠集めに時間かかる
+-> 証拠集めなしで一旦書く linkselfieの何倍くらいの時間がかかるか
+shun
+
+# 関連研究からやっていく
+# 過去の論文をみて参考にする
+# 15-25こ。1ページ前後
+1章で軽く触れる程度
+
+
+linkselfieに何書いているか読む
+linkselfieの関連研究を読む->まねできるところを探す
+GPTを1から10まで使わない
+論文を読む
+その後みずに書く
+重要度の研究があればみる
+
+追加で必要なところを考える
+
+
+yutoさん
+アブストラクト すぐおわる
+量子ネットワークの説明 すぐおわる
+IEEEiccのフォーマットにする すぐおわる
+関連研究(引用した論文) 整形
+アルゴリズムの前提条件 制約条件など、どういう環境を仮定しているか
+-> 時間かかりそう 整形
+アルゴリズム すぐおわる 整形
+まとめと今後の課題 すぐおわる
+
+
+*** 9/29までに絶対に6ページにする
+問題設定の図、実験に使用するトポロジの図
+アルゴリズムの表
+リサーチクエスチョン
+シンボルの定義(あってもなくても)
+
+*** それ以降
+評価指標を新しくした実験
+ストーリーのアップデート
+
+
+
+何をやるべきか
+1 今の論文に書かれていないものを埋める(足りないもの)
+2 6ページにするためにはどうするべきか考える
+.
+.
+.
+3 ストーリー、それに合わせた実験結果をより良いものに変える
+
+
+
+章構成
+1 はじめに
+2 関連研究
+3 アルゴリズムの前提条件
+4 アルゴリズム
+5 実験
+6 まとめと考察
+
+何を伝えたいか
+今回だとストーリー、量子ネットワーク、問題設定、linkselfieとの違い
+アルゴリズムの中身
+
+linkselfieと2,3本他の論文を読む
+
+これらを文量で区切る

+ 37 - 0
add_linkselfie/memo.org~

@@ -0,0 +1,37 @@
+** ミーティング
+*** 対話の引用部分
+ノードS〜ノードD 間のリンク集合 L だけでなく、
+ノードS〜ノードD_n間のリンク集合 L_n (1 <= n <= N, N はノードSの隣接ノード数)がそれぞれ入力される。
+ノードS〜ノードD_n間の重要度 I_n (0〜1 の値) が入力として与えられる。
+総バウンスコスト C が入力として与えられる。
+この時、ノードS〜ノードD_n のノードペアの中で、
+K 個のノードペア (S, D_s_1), (S, D_s_2), ... (S, D_s_K) における
+忠実度が最大のリンクをそれぞれ発見する。
+ただし、発見するリンクのノードペア数 K は、
+重要度と忠実度の積の総和、つまり
+I = \sum_{k = 1}^K I_s_k * F_s_k
+が最大となるように定める。
+ここで F_n は (S, D_n) 間において忠実度の最大のリンクの忠実度である。
+
+A. この拡張問題は、複数のノードペア (S, Dₙ) に対して、限られたリソース(バウンスコスト)の中で、重要なノードペアの
+高忠実度リンクを選定する最適化問題です。以下に、この問題の形式的な定義
+を記述します。
+
+*** 考えたこと
+- 先生のストーリー案に関するchatGPTとの対話をもう1度読みなおした
+- 現状の評価指標では、 **発見するリンクのノードペア数 K** は、重要度と忠
+  実度の積の総和、つまりI =\sum_{k = 1}^K I_s_k * F_s_kが最大となるよ
+  うに定める。という部分の **発見するリンク** の定義をしていない。
+- societyではリンク価値を定義し、それとバウンスの積をとることでその価
+  値スコアを最大化する問題として解いていた
+  - しかしこの方法では発見するリンクを動的に決めることはできない
+  - また、評価指標を最大化するような分配方法が最も価値が高いリンクに全
+    測定予算を配ることであり、これは測定精度と資源削減のトレードオフを
+    考えていると言えない
+
+- そこで、これらの問題を解決する評価指標として信頼区間幅UB,LBを用いた
+  評価指標を導入する案を考えた。(UB,LBは推定忠実度の上限、下限)
+- 将来的には信頼区間幅がある閾値 x よりも小さいリンクを何本発見できた
+  かというような指標を考えている
+  
+  

+ 27 - 0
add_linkselfie/memo.txt

@@ -129,3 +129,30 @@ coverageの閾値を考慮しないver
 選ばれるリンクはペア間で最大の忠実度のリンクのみ
 
 この3つに言えることだが、ベースラインはどのような手法を想定している?
+
+
+** ミーティング
+ノードS〜ノードD 間のリンク集合 L だけでなく、
+ノードS〜ノードD_n間のリンク集合 L_n (1 <= n <= N, N はノードSの隣接ノード数)がそれぞれ入力される。
+ノードS〜ノードD_n間の重要度 I_n (0〜1 の値) が入力として与えられる。
+総バウンスコスト C が入力として与えられる。
+この時、ノードS〜ノードD_n のノードペアの中で、
+K 個のノードペア (S, D_s_1), (S, D_s_2), ... (S, D_s_K) における
+忠実度が最大のリンクをそれぞれ発見する。
+ただし、発見するリンクのノードペア数 K は、
+重要度と忠実度の積の総和、つまり
+I = \sum_{k = 1}^K I_s_k * F_s_k
+が最大となるように定める。
+ここで F_n は (S, D_n) 間において忠実度の最大のリンクの忠実度である。
+
+A. この拡張問題は、複数のノードペア (S, Dₙ) に対して、限られたリソース(バウンスコスト)の中で、重要なノードペアの
+高忠実度リンクを選定する最適化問題です。以下に、この問題の形式的な定義
+を記述します。
+
+
+
+- 先生のストーリー案に関するchatGPTとの対話をもう1度読みなおした
+- 現状の評価指標では
+発見するリンクのノードペア数 K は、重要度と忠実度の積の総和、つまりI =
+\sum_{k = 1}^K I_s_k * F_s_kが最大となるように定める。
+ん

+ 57 - 0
add_linkselfie/metrics/widths.py

@@ -0,0 +1,57 @@
+# metrics/widths.py — width metrics (Py3.8 safe typing)
+
+import math
+from typing import List, Dict, Any
+
+def ci_radius_hoeffding(n: int, delta: float = 0.1) -> float:
+    if n <= 0:
+        return 1.0
+    return math.sqrt(0.5 * math.log(2.0 / delta) / n)
+
+def _widths_for_details_single_pair(det: Dict[str, Any], delta: float) -> List[float]:
+    alloc = det.get("alloc_by_path", {}) or {}
+    est   = det.get("est_fid_by_path", {}) or {}
+    widths: List[float] = []
+    for pid, m in est.items():
+        n = int(alloc.get(pid, 0))
+        r = ci_radius_hoeffding(n, delta)
+        lb = max(0.0, float(m) - r)
+        ub = min(1.0, float(m) + r)
+        widths.append(ub - lb)
+    return widths
+
+def sum_widths_all_links(per_pair_details: List[Dict[str, Any]], delta: float = 0.1) -> float:
+    s = 0.0
+    for det in per_pair_details:
+        for w in _widths_for_details_single_pair(det, delta):
+            s += float(w)
+    return s
+
+def sum_minwidths_perpair(per_pair_details: List[Dict[str, Any]], delta: float = 0.1) -> float:
+    s = 0.0
+    for det in per_pair_details:
+        widths = _widths_for_details_single_pair(det, delta)
+        if widths:
+            s += float(min(widths))
+        else:
+            s += 1.0  # 未推定ペアは1.0を加算(保守的)
+    return s
+
+def sum_weighted_widths_all_links(per_pair_details: List[Dict[str, Any]], importance_list: List[float], delta: float = 0.1) -> float:
+    s = 0.0
+    for d, det in enumerate(per_pair_details):
+        I = float(importance_list[d]) if d < len(importance_list) else 1.0
+        for w in _widths_for_details_single_pair(det, delta):
+            s += I * float(w)
+    return s
+
+def sum_weighted_min_widths_perpair(per_pair_details: List[Dict[str, Any]], importance_list: List[float], delta: float = 0.1) -> float:
+    s = 0.0
+    for d, det in enumerate(per_pair_details):
+        I = float(importance_list[d]) if d < len(importance_list) else 1.0
+        widths = _widths_for_details_single_pair(det, delta)
+        if widths:
+            s += I * float(min(widths))
+        else:
+            s += I * 1.0
+    return s

+ 57 - 0
add_linkselfie/metrics/widths.py~

@@ -0,0 +1,57 @@
+
+from __future__ import annotations
+import math
+from typing import List, Dict, Any
+
+def ci_radius_hoeffding(n: int, delta: float = 0.1) -> float:
+    if n <= 0:
+        return 1.0
+    return math.sqrt(0.5 * math.log(2.0 / delta) / n)
+
+def _widths_for_details_single_pair(det: Dict[str, Any], delta: float) -> list[float]:
+    alloc = det.get("alloc_by_path", {}) or {}
+    est   = det.get("est_fid_by_path", {}) or {}
+    widths = []
+    for pid, m in est.items():
+        n = int(alloc.get(pid, 0))
+        r = ci_radius_hoeffding(n, delta)
+        lb = max(0.0, float(m) - r)
+        ub = min(1.0, float(m) + r)
+        widths.append(ub - lb)
+    return widths
+
+def sum_widths_all_links(per_pair_details: List[Dict[str, Any]], delta: float = 0.1) -> float:
+    s = 0.0
+    for det in per_pair_details:
+        for w in _widths_for_details_single_pair(det, delta):
+            s += float(w)
+    return s
+
+def sum_minwidths_perpair(per_pair_details: List[Dict[str, Any]], delta: float = 0.1) -> float:
+    s = 0.0
+    for det in per_pair_details:
+        widths = _widths_for_details_single_pair(det, delta)
+        if widths:
+            s += float(min(widths))
+        else:
+            s += 1.0
+    return s
+
+def sum_weighted_widths_all_links(per_pair_details: List[Dict[str, Any]], importance_list: list[float], delta: float = 0.1) -> float:
+    s = 0.0
+    for d, det in enumerate(per_pair_details):
+        I = float(importance_list[d]) if d < len(importance_list) else 1.0
+        for w in _widths_for_details_single_pair(det, delta):
+            s += I * float(w)
+    return s
+
+def sum_weighted_min_widths_perpair(per_pair_details: List[Dict[str, Any]], importance_list: list[float], delta: float = 0.1) -> float:
+    s = 0.0
+    for d, det in enumerate(per_pair_details):
+        I = float(importance_list[d]) if d < len(importance_list) else 1.0
+        widths = _widths_for_details_single_pair(det, delta)
+        if widths:
+            s += I * float(min(widths))
+        else:
+            s += I * 1.0
+    return s

+ 74 - 0
add_linkselfie/piclecsv.py

@@ -0,0 +1,74 @@
+import os, pickle, csv, sys, re
+
+# ここに変換したいファイルを列挙(相対パスOK)
+files = [
+    "outputs/plot_minwidthsum_perpair_vs_budget_Depolar.pickle",
+    "outputs/plot_minwidthsum_perpair_weighted_vs_budget_Depolar.pickle",
+    "outputs/plot_widthsum_alllinks_vs_budget_Depolar.pickle",
+    "outputs/plot_widthsum_alllinks_weighted_vs_budget_Depolar.pickle",
+]
+
+# 指標候補(見つかった順に採用)
+PREFERRED_KEYS = [
+    "minwidthsum_weighted",
+    "minwidthsum",
+    "widthsum_alllinks_weighted",
+    "widthsum_alllinks",
+    "accuracy",  # 念のため
+    "value",     # 念のため
+    "metric"     # 念のため
+]
+
+def pick_metric_key(results):
+    """results は {budget: {...}}。どのキーでCSV化するか自動推定"""
+    for b, r in results.items():
+        if isinstance(r, dict) and r:
+            keys = set(r.keys())
+            # ノイズになりがちなキーを除外
+            keys -= {"per_pair_details", "details", "meta"}
+            # 優先候補から探す
+            for k in PREFERRED_KEYS:
+                if k in keys:
+                    return k
+            # それでも見つからなければ、数値っぽい最初のキー
+            for k in keys:
+                v = r.get(k)
+                if isinstance(v, (int, float)) or (v is not None and not isinstance(v, (dict, list, tuple, set))):
+                    return k
+    return None
+
+for path in files:
+    if not os.path.exists(path):
+        print(f"[WARN] not found: {path}")
+        continue
+
+    with open(path, "rb") as f:
+        try:
+            obj = pickle.load(f)
+        except Exception as e:
+            print(f"[ERROR] pickle.load failed for {path}: {e}")
+            continue
+
+    budgets = obj.get("budget_list", [])
+    results = obj.get("results", {})
+
+    if not budgets or not isinstance(results, dict) or not results:
+        print(f"[WARN] {path}: budgets or results is empty(サイズが小さい594Bケースかも)")
+        continue
+
+    metric_key = pick_metric_key(results)
+    if not metric_key:
+        print(f"[WARN] {path}: 指標キーが見つからないためスキップ(resultsの中身を要確認)")
+        continue
+
+    out_csv = os.path.splitext(path)[0] + ".csv"  # 同名で .csv を outputs/ に出力
+    os.makedirs(os.path.dirname(out_csv), exist_ok=True)
+
+    with open(out_csv, "w", newline="") as fcsv:
+        w = csv.writer(fcsv)
+        w.writerow(["budget", metric_key])
+        for b in budgets:
+            v = results.get(b, {}).get(metric_key)
+            w.writerow([b, v])
+
+    print(f"[OK] {out_csv} (列: budget,{metric_key})")

+ 29 - 0
add_linkselfie/piclecsv.py~

@@ -0,0 +1,29 @@
+cd ~/quantum/add_linkselfie/outputs
+python - <<'PY'
+import pickle, pprint
+path = "plot_minwidthsum_perpair_weighted_vs_budget_Depolar.pickle"
+with open(path,"rb") as f: obj = pickle.load(f)
+
+budgets = obj.get("budget_list", [])
+results = obj.get("results", {})
+print("budgets:", budgets[:10], "... (len =", len(budgets),")")
+print("results keys sample:", list(results.keys())[:5])
+
+for b in (min(budgets), budgets[len(budgets)//2], max(budgets)):
+    r = results.get(b)
+    if r is None: continue
+    print(f"\n=== Budget {b} ===")
+    for k in ("minwidthsum_weighted","widthsum_alllinks","per_pair_details"):
+        if k in r:
+            v = r[k]
+            print(f"{k}: type={type(v)}",
+                  ("len="+str(len(v)) if hasattr(v, '__len__') else ""))
+    det = r.get("per_pair_details", [])
+    if det:
+        d0 = det[0]
+        print("per_pair_details[0] keys:", list(d0.keys()))
+        if "min_width_per_pair" in d0:
+            print("min_width_per_pair:", d0["min_width_per_pair"])
+        if "alloc_by_path" in d0:
+            print("alloc_by_path (sample):", list(d0["alloc_by_path"].items())[:3])
+PY

BIN
add_linkselfie/plot_accuracy_vs_budget_Depolar.pdf


BIN
add_linkselfie/plot_minwidthsum_perpair_vs_budget_Depolar.pdf


BIN
add_linkselfie/plot_minwidthsum_perpair_weighted_vs_budget_Depolar.pdf


BIN
add_linkselfie/plot_value_vs_budget_target_Depolar.pdf


BIN
add_linkselfie/plot_value_vs_used_Depolar.pdf


BIN
add_linkselfie/plot_widthsum_alllinks_vs_budget_Depolar.pdf


BIN
add_linkselfie/plot_widthsum_alllinks_weighted_vs_budget_Depolar.pdf


+ 210 - 0
add_linkselfie/simulation.py

@@ -0,0 +1,210 @@
+# simulation.py
+# -*- coding: utf-8 -*-
+from __future__ import annotations
+from dataclasses import dataclass
+from typing import Dict, Any, List
+import csv, os, math, random
+
+# ===== 既存ネットワークAPIに合わせたアダプタ =====
+class Adapter:
+    """
+    あなたの network.py / nb_protocol.py を変更せずに使うための薄いラッパ。
+    - QuantumNetwork(path_num, fidelity_list, noise_model) を自前で構築
+    - 単一ペア 'Alice-Bob' に path_id=1..path_num を割当
+    - スケジューラが期待する nb_protocol 互換API(sample_path / true_fidelity)を Shim で提供
+    """
+    def __init__(self, noise_model: str, path_num: int, fidelity_list: List[float], seed: int | None = None):
+        if seed is not None:
+            random.seed(seed)
+        import network as qnet
+        # QuantumNetwork を直接構築(network.py を変更しない)
+        self.net = qnet.QuantumNetwork(path_num=path_num, fidelity_list=fidelity_list, noise_model=noise_model)
+        self.pairs = ["Alice-Bob"]
+        self.paths_map = {"Alice-Bob": list(range(1, path_num + 1))}
+        # nb_protocol 互換 Shim
+        self.nbp = _NBPShim(self.net)
+
+    # ---- ヘルパ ----
+    def true_fidelity(self, path_id: Any) -> float:
+        return self.nbp.true_fidelity(self.net, path_id)
+
+    def list_pairs(self) -> List[Any]:
+        return list(self.pairs)
+
+    def list_paths_of(self, pair_id: Any) -> List[Any]:
+        return list(self.paths_map.get(pair_id, []))
+
+    # ---- スケジューラ呼び出し ----
+    def run_scheduler(self, scheduler_name: str, budget_target: int,
+                      importance: Dict[Any, float]) -> Dict[str, Any]:
+        """
+        スケジューラに共通IFで実行要求する。
+        返り値の想定(辞書):
+          {
+            'used_cost_total': int,
+            'per_pair_details': [
+               {
+                 'pair_id': pair_id,
+                 'alloc_by_path': {path_id: sample_count, ...},
+                 'est_fid_by_path': {path_id: mean_estimate, ...},
+                 'best_pred_path': path_id,
+               }, ...
+            ]
+          }
+        """
+        if scheduler_name == "greedy":
+            from schedulers.greedy_scheduler import run as greedy_run
+            return greedy_run(self.net, self.pairs, self.paths_map, budget_target, importance, self.nbp)
+        elif scheduler_name == "naive":
+            from schedulers.lnaive_scheduler import run as naive_run
+            return naive_run(self.net, self.pairs, self.paths_map, budget_target, importance, self.nbp)
+        elif scheduler_name == "online_nb":
+            from schedulers.lonline_nb import run as onb_run
+            return onb_run(self.net, self.pairs, self.paths_map, budget_target, importance, self.nbp)
+        else:
+            raise ValueError(f"unknown scheduler: {scheduler_name}")
+
+# ===== 便利関数 =====
+def hoeffding_radius(n: int, delta: float = 0.05) -> float:
+    if n <= 0:
+        return 1.0
+    return math.sqrt(0.5 * math.log(2.0 / delta) / n)
+
+def clamp01(x: float) -> float:
+    return 0.0 if x < 0.0 else (1.0 if x > 1.0 else x)
+
+# ===== CSV I/O =====
+CSV_HEADER = [
+    "run_id", "noise", "scheduler", "budget_target",
+    "used_cost_total",
+    "pair_id", "path_id",
+    "importance",               # I_d
+    "samples",                  # B_{d,l}
+    "est_mean", "lb", "ub", "width",
+    "is_best_true", "is_best_pred"
+]
+
+def open_csv(path: str):
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    exists = os.path.exists(path)
+    f = open(path, "a", newline="")
+    w = csv.writer(f)
+    if not exists:
+        w.writerow(CSV_HEADER)
+    return f, w
+
+# ===== メインシミュレーション =====
+@dataclass
+class ExperimentConfig:
+    noise_model: str
+    budgets: List[int]
+    schedulers: List[str]            # ["greedy", "naive", "online_nb", ...]
+    repeats: int
+    importance_mode: str = "both"    # "both" / "weighted_only" / "unweighted_only"
+    delta_ci: float = 0.05           # 95%CI相当
+    out_csv: str = "outputs/raw_simulation_data.csv"
+    seed: int | None = None
+    # QuantumNetwork 構築用
+    path_num: int = 5
+    fidelity_list: List[float] | None = None
+
+def _importance_for_pairs(pairs: List[Any], mode: str) -> Dict[str, Dict[Any, float]]:
+    res: Dict[str, Dict[Any, float]] = {}
+    if mode in ("both", "unweighted_only"):
+        res["unweighted"] = {p: 1.0 for p in pairs}
+    if mode in ("both", "weighted_only"):
+        # 重要度は例として一様乱数(必要なら差替え)
+        res["weighted"] = {p: 0.5 + random.random() for p in pairs}
+    return res
+
+def run_and_append_csv(cfg: ExperimentConfig) -> str:
+    fid = cfg.fidelity_list or _default_fidelities(cfg.path_num)
+    adp = Adapter(cfg.noise_model, cfg.path_num, fid, seed=cfg.seed)
+    pairs = adp.list_pairs()
+    importance_sets = _importance_for_pairs(pairs, cfg.importance_mode)
+
+    f, w = open_csv(cfg.out_csv)
+    try:
+        run_id = 0
+        for _ in range(cfg.repeats):
+            run_id += 1
+            for budget in cfg.budgets:
+                for sched in cfg.schedulers:
+                    for imp_tag, I in importance_sets.items():
+                        # スケジューラ実行
+                        result = adp.run_scheduler(sched, budget, I)
+
+                        used_cost_total = int(result.get("used_cost_total", budget))
+                        per_pair_details: List[Dict[str, Any]] = result.get("per_pair_details", [])
+
+                        # 真の最良パス(正答率判定用)
+                        true_best_by_pair = {}
+                        for pair in pairs:
+                            paths = adp.list_paths_of(pair)
+                            best = None
+                            bestv = -1.0
+                            for pid in paths:
+                                tf = adp.true_fidelity(pid)
+                                if tf > bestv:
+                                    bestv, best = tf, pid
+                            true_best_by_pair[pair] = best
+
+                        # CSV行を形成
+                        for det in per_pair_details:
+                            pair_id = det["pair_id"]
+                            alloc = det.get("alloc_by_path", {}) or {}
+                            est   = det.get("est_fid_by_path", {}) or {}
+                            pred  = det.get("best_pred_path")
+
+                            for path_id, samples in alloc.items():
+                                m = float(est.get(path_id, 0.5))
+                                r = hoeffding_radius(int(samples), cfg.delta_ci)
+                                lb = clamp01(m - r)
+                                ub = clamp01(m + r)
+                                width = ub - lb
+
+                                is_true_best = (true_best_by_pair.get(pair_id) == path_id)
+                                is_best_pred = (pred == path_id)
+
+                                w.writerow([
+                                    f"{run_id}-{imp_tag}",
+                                    cfg.noise_model,
+                                    sched,
+                                    budget,
+                                    used_cost_total,
+                                    pair_id,
+                                    path_id,
+                                    I.get(pair_id, 1.0),
+                                    int(samples),
+                                    m, lb, ub, width,
+                                    int(is_true_best), int(is_best_pred),
+                                ])
+    finally:
+        f.close()
+    return cfg.out_csv
+
+# ===== nb_protocol 互換 Shim =====
+class _NBPShim:
+    """
+    スケジューラが期待する nb_protocol 風のAPIを提供:
+      - sample_path(net, path_id, n): QuantumNetwork.benchmark_path を呼ぶ
+      - true_fidelity(net, path_id): 量子チャネルの ground truth を返す
+    """
+    def __init__(self, net):
+        self.net = net
+
+    def sample_path(self, net, path_id: int, n: int) -> float:
+        # 1-bounce を n 回の測定にマップ(nb_protocol.NBProtocolAlice の設計に整合)
+        p, _cost = self.net.benchmark_path(path_id, bounces=[1], sample_times={1: int(n)})
+        return float(p)
+
+    def true_fidelity(self, net, path_id: int) -> float:
+        return float(self.net.quantum_channels[path_id - 1].fidelity)
+
+# ===== デフォルト忠実度の簡易生成(必要なら差替え) =====
+def _default_fidelities(path_num: int) -> List[float]:
+    alpha, beta, var = 0.93, 0.85, 0.02
+    res = [max(0.8, min(0.999, random.gauss(beta, var))) for _ in range(path_num)]
+    best_idx = random.randrange(path_num)
+    res[best_idx] = max(0.85, min(0.999, random.gauss(alpha, var)))
+    return res

+ 124 - 0
add_linkselfie/simulation.py~

@@ -0,0 +1,124 @@
+
+# simulation.py
+# Produces ONE CSV of raw data with a "scheduler" and "used" (actual spent cost) columns.
+
+from dataclasses import dataclass
+from typing import List, Dict, Tuple
+import math
+import csv
+import random
+
+from fidelity import generate_fidelity_list_random, generate_importance_list_random
+
+@dataclass
+class SimConfig:
+    n_pairs: int = 3
+    links_per_pair: int = 5
+    budgets: List[int] = None
+    trials: int = 10
+    seed: int = 42
+    init_samples_per_link: int = 4
+    delta: float = 0.1
+    cost_per_sample: int = 1
+    schedulers: List[str] = None  # names for series
+
+    def __post_init__(self):
+        if self.budgets is None:
+            self.budgets = [500, 1000, 2000, 5000]
+        if self.schedulers is None:
+            self.schedulers = ["GreedySimple"]
+
+def _radius(n: int, delta: float) -> float:
+    if n <= 0:
+        return float("inf")
+    return math.sqrt(0.5 * math.log(2.0 / max(1e-12, delta)) / n)
+
+def _argmax(d: Dict[int, float]) -> int:
+    best_k = None
+    best_v = -1e9
+    for k, v in d.items():
+        if v > best_v or (v == best_v and (best_k is None or k < best_k)):
+            best_k, best_v = k, v
+    return best_k if best_k is not None else -1
+
+def _run_scheduler_greedy_simple(n_pairs, links_per_pair, budgets_sorted, delta, init_samples_per_link, cost_per_sample, true_fids_per_pair, importances, writer, trial, scheduler_name):
+    # Per-budget deterministic re-run for snapshots
+    rng_state0 = random.getstate()
+    for b in budgets_sorted:
+        random.setstate(rng_state0)
+        est: Dict[Tuple[int,int], float] = {}
+        ns: Dict[Tuple[int,int], int] = {}
+        for p in range(n_pairs):
+            for l in range(links_per_pair):
+                est[(p,l)] = 0.0
+                ns[(p,l)] = 0
+        used = 0
+        # phase-1: uniform
+        stop = False
+        for p in range(n_pairs):
+            for l in range(links_per_pair):
+                for _ in range(init_samples_per_link):
+                    x = 1 if random.random() < true_fids_per_pair[p][l] else 0
+                    ns[(p,l)] += 1
+                    est[(p,l)] += (x - est[(p,l)]) / ns[(p,l)]
+                    used += cost_per_sample
+                    if used >= b:
+                        stop = True
+                        break
+                if stop: break
+            if stop: break
+        # phase-2: greedy
+        while used < b:
+            for p in range(n_pairs):
+                cur = {l: est[(p,l)] for l in range(links_per_pair)}
+                best_l = max(cur.keys(), key=lambda kk: cur[kk])
+                x = 1 if random.random() < true_fids_per_pair[p][best_l] else 0
+                ns[(p,best_l)] += 1
+                est[(p,best_l)] += (x - est[(p,best_l)]) / ns[(p,best_l)]
+                used += cost_per_sample
+                if used >= b:
+                    break
+        # emit rows (same 'used' for all rows in this (trial,budget,scheduler))
+        for p in range(n_pairs):
+            tb = max(range(links_per_pair), key=lambda l: true_fids_per_pair[p][l])
+            cur_map = {l: est[(p,l)] for l in range(links_per_pair)}
+            ca = max(range(links_per_pair), key=lambda l: cur_map[l])
+            for l in range(links_per_pair):
+                k = (p,l)
+                r = _radius(ns[k], delta)
+                lb = max(0.0, est[k] - r if ns[k] > 0 else 0.0)
+                ub = min(1.0, est[k] + r if ns[k] > 0 else 1.0)
+                writer.writerow([
+                    scheduler_name, trial, b, used, p, l, importances[p], true_fids_per_pair[p][l],
+                    est[k], ns[k], lb, ub,
+                    1 if l == tb else 0,
+                    1 if l == ca else 0,
+                ])
+    random.setstate(rng_state0)
+
+def run_simulation(csv_path: str, cfg: SimConfig) -> None:
+    random.seed(cfg.seed)
+    budgets_sorted = sorted(set(cfg.budgets))
+
+    with open(csv_path, "w", newline="") as f:
+        w = csv.writer(f)
+        w.writerow([
+            "scheduler","trial","budget_target","used","pair_id","link_id","importance","true_fid",
+            "est_mean","n_samples","lb","ub","is_true_best","is_pair_current_argmax"
+        ])
+
+        for trial in range(cfg.trials):
+            # Ground truth per pair
+            true_fids_per_pair: Dict[int, Dict[int, float]] = {}
+            importances: Dict[int, float] = {}
+            for p in range(cfg.n_pairs):
+                true_list = generate_fidelity_list_random(cfg.links_per_pair)
+                true_fids_per_pair[p] = {i: true_list[i] for i in range(cfg.links_per_pair)}
+                importances[p] = generate_importance_list_random(1)[0]
+
+            for sched_name in cfg.schedulers:
+                _run_scheduler_greedy_simple(
+                    cfg.n_pairs, cfg.links_per_pair, budgets_sorted, cfg.delta,
+                    cfg.init_samples_per_link, cfg.cost_per_sample,
+                    true_fids_per_pair, importances, w, trial, sched_name
+                )

+ 47 - 0
add_linkselfie/viz/plots.py

@@ -0,0 +1,47 @@
+# viz/plots.py — Python 3.8 & older Matplotlib compatible
+
+from __future__ import annotations
+import math
+import numpy as np
+import matplotlib.pyplot as plt
+from cycler import cycler
+
+# ---- Safe color cycle (hex codes; no 'C0' refs) ----
+COLORS = [
+    "#1f77b4",  # blue
+    "#ff7f0e",  # orange
+    "#2ca02c",  # green
+    "#d62728",  # red
+    "#9467bd",  # purple
+    "#8c564b",  # brown
+]
+plt.rc("axes", prop_cycle=cycler("color", COLORS))
+
+def tcrit_95(n: int) -> float:
+    if n <= 1:
+        return float("inf")
+    if n < 30:
+        return 2.262
+    return 1.96
+
+def mean_ci95(vals):
+    arr = np.array(list(vals), dtype=float)
+    n = len(arr)
+    if n == 0:
+        return 0.0, 0.0
+    if n == 1:
+        return float(arr[0]), 0.0
+    m = float(arr.mean())
+    s = float(arr.std(ddof=1))
+    half = tcrit_95(n) * (s / math.sqrt(n))
+    return m, half
+
+def plot_with_ci_band(ax, xs, mean, half, *, label, line_kwargs=None, band_kwargs=None):
+    """Plot mean line and shaded CI band (Py3.8 safe, old-mpl safe)."""
+    line_kwargs = {} if line_kwargs is None else dict(line_kwargs)
+    band = {"alpha": 0.25}
+    if band_kwargs is not None:
+        band.update(dict(band_kwargs))
+    line, = ax.plot(xs, mean, label=label, **line_kwargs)
+    ax.fill_between(xs, mean - half, mean + half, **band)
+    return line

+ 49 - 0
add_linkselfie/viz/plots.py~

@@ -0,0 +1,49 @@
+
+# viz/plots.py — Python 3.8 compatible plotting utilities
+
+from __future__ import annotations
+import math
+import numpy as np
+import matplotlib.pyplot as plt
+from cycler import cycler
+
+# ----- Unified style (kept lightweight) -----
+default_cycler = (
+    cycler(color=["C0", "C1", "C2", "C3", "C4", "C5"])
+    + cycler(marker=["s", "D", "^", "v", "o", "x"])
+    + cycler(linestyle=[":", "--", "-", "-.", "--", ":"])
+)
+plt.rc("axes", prop_cycle=default_cycler)
+
+# ----- 95% CI (t critical) -----
+def tcrit_95(n: int) -> float:
+    """Return ~95% t critical value for sample size n (simple, conservative)."""
+    if n <= 1:
+        return float("inf")
+    if n < 30:
+        # Conservative constant (close to df=9..29 range)
+        return 2.262
+    return 1.96
+
+def mean_ci95(vals):
+    """Return (mean, half_width) for 95% CI."""
+    arr = np.array(list(vals), dtype=float)
+    n = len(arr)
+    if n == 0:
+        return 0.0, 0.0
+    if n == 1:
+        return float(arr[0]), 0.0
+    m = float(arr.mean())
+    s = float(arr.std(ddof=1))
+    half = tcrit_95(n) * (s / math.sqrt(n))
+    return m, half
+
+def plot_with_ci_band(ax, xs, mean, half, *, label, line_kwargs=None, band_kwargs=None):
+    """Plot mean line and shaded CI band (Python 3.8 safe)."""
+    line_kwargs = {} if line_kwargs is None else dict(line_kwargs)
+    band = {"alpha": 0.25}
+    if band_kwargs is not None:
+        band.update(dict(band_kwargs))
+    line, = ax.plot(xs, mean, label=label, **line_kwargs)
+    ax.fill_between(xs, mean - half, mean + half, **band)
+    return line