import time from datetime import datetime import numpy as np import networkx as nx import matplotlib.pyplot as plt import seaborn as sns from itertools import permutations import openjij as oj import os # ============ 輸出文件夾設置 (含時間戳) ============ TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S") OUTPUT_DIR = f"./plots_output/plots_output_{TIMESTAMP}" if not os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR) print(f"✓ 已建立輸出文件夾: {OUTPUT_DIR}") # ================================================== # ==================== 可調整參數區 ==================== # 問題規模設定 CITIES = 50 # 初始核心問題規模 (測試建議先用 12-15) CITIES_BOUND = 10 # 問題規模的上下範圍 (單一數字控制:以 CITIES 為中心,例如 15±5,即 10~20) RANDOM = False RANDOM_SEED = 42 COORD_RANGE = (0.0, 10.0) # 【核心創新】:環境變異數控制與複雜度壓力測試參數 N_LIST = list(range(CITIES - CITIES_BOUND, CITIES + CITIES_BOUND + 1, 4)) # 自動產生測試的問題規模範圍 COORD_STD = 15.0 # 空間分佈變異數 (越大代表城市分佈越不均勻、越崎嶇) RISK_STD = 10.0 # 擾動風險變異數 (越大代表某些路段特別危險,H-infinity 衝突極大) # 演算法執行設定 NUM_RUNS = 10 # 正式比較的執行次數 BETA = 1 # 🌟 稍微調高 (原為 10.0),讓低溫結冰得更紮實 SWEEPS_MAIN_TEST = 1000 # 主要比較測試的退火步數 SWEEPS_COMPLEXITY_TEST = 200 # 複雜度擴展測試的退火步數 SWEEPS_HEATMAP_TEST = 200 # 熱力圖測試的退火步數 HEATMAP_RUNS = 5 # 熱力圖測試每種情況的平均次數 # QUBO 參數 PENALTY = 1000.0 # 🌟 從 3000 大幅降回 500 (解開高爾夫球場效應) BIG_PENALTY = 2000.0 # 🌟 起點約束 # TSP 求解參數 EXACT_LIMIT = 8 # 小於等於這個數量的城市會使用暴力搜尋求解最佳路徑 (確保結果正確性),超過則使用 nearest neighbor heuristic # 魯棒優化參數 (H-infinity Robust QUBO) USE_ROBUST = True # 強制開啟,這是本論文的核心 GAMMA = 0.5 SIGMA = 1.0 ALPHA = 10.0 # 故障訊號參數 (Fault Signal) ENABLE_FAULT_SIGNAL = True # 是否加入故障訊號測試 FAULT_LAMBDA = 50000.0 # 故障懲罰權重 (lambda),設高一點演算法才會怕 FAULT_PROBABILITY = 0.0 # 兩城市間發生故障/禁飛的機率 (0.1 代表 10% 的路徑斷線) # 執行區塊控制 RUN_MAIN_TEST = False # 是否執行主要演算法(SA vs SQA)比較測試 RUN_TSP_TEST = True # 🌟 是否執行單機 TSP (Traveling Salesman Problem) 測試 COMPARE_H_INFINITY = False # 是否比較有無 H-infinity 避障算法的效果 (將輸出 2x3 或 3x2 圖表) RUN_COMPLEXITY_TEST = False # 是否執行複雜度擴展(N_LIST)測試 SHOW_TERRAIN_PLOTS = False # 是否顯示能量地形圖與驗證報告 ENABLE_DISTURBANCE_MATRIX = True # 是否生成擾動矩陣 (F 矩陣) ENABLE_QUANTUM_MINEFIELD = True # 是否產生極端能量障礙與欺騙陷阱 (若為 False,則產生一般隨機地圖) PLOT_ENV_DIS_FAULT = True # 是否繪製擾動矩陣與故障矩陣對比圖 # ===================================================== def generate_quantum_minefield(N, random_seed=42): """ 生成具有「極大能量障礙」、「欺騙陷阱」與「峽谷路障」的極端測試矩陣。 目標:逼迫 SA 掉入陷阱或卡死在路障前,而 SQA 能穿透勢壘找到繞道。 """ np.random.seed(random_seed) # 1. 築起能量高牆 (The Wall) D = np.random.uniform(30.0, 40.0, (N, N)) F = np.random.uniform(20.0, 30.0, (N, N)) np.fill_diagonal(D, 0) np.fill_diagonal(F, 0) # 2. 鑿出「黃金隧道 (Global Minimum)」 golden_path = list(range(N)) for i in range(N): u = golden_path[i] v = golden_path[(i + 1) % N] D[u, v] = 2.0 F[u, v] = 0.0 # 3. 佈置「致命陷阱 (Deceptive Traps)」 for i in range(N): trap_node = (i + int(N/2)) % N if trap_node != golden_path[(i + 1) % N] and trap_node != i: # 致命誘惑:眼前只要 0.1,但進去後就是 30.0 以上的高牆 D[i, trap_node] = 0.1 F[i, trap_node] = 0.1 # ========================================== # 4. [新增] 在峽谷中砸下路障,並建立「穿隧繞道」 # ========================================== # 挑選黃金隧道中的幾個節點作為路障發生地 (例如 1/3 和 2/3 處) obstacle_indices = [N // 3, 2 * N // 3] for idx in obstacle_indices: u = golden_path[idx] v = golden_path[(idx + 1) % N] # [砸下巨石] 切斷原本順暢的黃金路線 D[u, v] = 50.0 F[u, v] = 50.0 # 模擬 I_fault 突發故障 # [建立繞道] 徵用一個原本是陷阱的點作為安全繞道 (Bypass) bypass_node = (u + int(N/2) + 1) % N # 第一段:高能勢壘 (阻擋 SA) # SA 在 u 點時看到:往陷阱只要 0.1,往繞道要 12.0。SA 必定會選陷阱然後死掉。 D[u, bypass_node] = 12.0 F[u, bypass_node] = 0.0 # 第二段:柳暗花明 (專屬 SQA 的出路) # SQA 會看破全局,穿透 12.0 的障礙,走這一步接回黃金大道 D[bypass_node, v] = 1.0 F[bypass_node, v] = 0.0 # 確保 Bypass 節點不要保留原本的陷阱死路設定 # 關閉 Bypass 到其他隨機點的低成本誘惑 for j in range(N): if j != v and j != bypass_node: D[bypass_node, j] = np.random.uniform(30.0, 40.0) F[bypass_node, j] = np.random.uniform(20.0, 30.0) print(f"⚠️ [極限測試] 已成功生成 N={N} 的量子雷區地貌(含動態路障)!") return D, F def verify_minefield_stats(D, F): """ 數值驗證:計算「黃金捷徑」與「一般隨機路徑」的成本差異, 證明地圖中確實存在極端的高牆與深谷。 """ N = len(D) # 1. 計算黃金捷徑 (0 -> 1 -> 2 -> ... -> N-1 -> 0) 的總成本 golden_path = list(range(N)) golden_cost = 0 for i in range(N): u = golden_path[i] v = golden_path[(i + 1) % N] golden_cost += (D[u, v] + F[u, v]) # 2. 計算一條「不小心踩到高牆」的隨機路徑成本 np.random.seed(99) random_path = np.random.permutation(N).tolist() random_cost = 0 for i in range(N): u = random_path[i] v = random_path[(i + 1) % N] random_cost += (D[u, v] + F[u, v]) print("\n🔍 [雷區地貌驗證報告]") print(f"✅ 黃金隧道總成本: {golden_cost:.2f} (演算法的終極目標)") print(f"❌ 隨機踩雷總成本: {random_cost:.2f} (一般 SA 容易落入的下場)") print(f"⚖️ 難度倍率: 隨機路徑的成本是黃金捷徑的 {random_cost/golden_cost:.1f} 倍!\n") def plot_energy_landscape_heatmap(D, F): """ 視覺化驗證:繪製 D+F 總成本矩陣的熱力圖。 """ Total_Cost_Matrix = D + F plt.figure(figsize=(8, 6)) # 使用 'hot' 顏色地圖,顏色越亮(黃/白)代表成本越高(高牆),越暗(黑/紅)代表成本越低(深谷) plt.imshow(Total_Cost_Matrix, cmap='hot', interpolation='nearest') plt.colorbar(label='Total Energy Cost (D + F)') plt.title('Quantum Minefield Energy Landscape') plt.xlabel('To City') plt.ylabel('From City') # 標示出黃金路線 (次對角線) plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, "energy_landscape_heatmap.png"), dpi=300) print(f"🎨 能量地形圖已儲存: {os.path.join(OUTPUT_DIR, 'energy_landscape_heatmap.png')}") # plt.show() 將在最後統一呼叫 def plot_energy_landscape_3d(D, F): """ 視覺化驗證二:繪製 D+F 總成本矩陣的 3D 能量地形圖。 """ Total_Cost_Matrix = D + F N = len(D) X, Y = np.meshgrid(range(N), range(N)) fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection='3d') # 繪製 3D 表面 surf = ax.plot_surface(X, Y, Total_Cost_Matrix, cmap='hot', edgecolor='none', alpha=0.9) fig.colorbar(surf, label='Total Energy Cost (D + F)', shrink=0.5, aspect=5) ax.set_title('3D Quantum Minefield Energy Landscape') ax.set_xlabel('To City') ax.set_ylabel('From City') ax.set_zlabel('Cost') plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, "energy_landscape_3d.png"), dpi=300) print(f"🎨 3D 能量地形圖已儲存: {os.path.join(OUTPUT_DIR, 'energy_landscape_3d.png')}") # plt.show() 將在最後統一呼叫 # ----------------- 基礎輔助函數 ----------------- def generate_distance_matrix(num_cities, random=True, seed=None, coord_range=None): if seed is None: seed = RANDOM_SEED if coord_range is None: coord_range = COORD_RANGE if not random: np.random.seed(seed) else: np.random.seed(None) low, high = coord_range coords = np.random.uniform(low, high, size=(num_cities, 2)) D = np.zeros((num_cities, num_cities), dtype=float) for i in range(num_cities): for j in range(i + 1, num_cities): dist = np.linalg.norm(coords[i] - coords[j]) D[i, j] = dist D[j, i] = dist return np.round(D, 2), coords def generate_disturbance_matrix(num_cities, seed=None): if seed is None: seed = RANDOM_SEED np.random.seed(seed) F = np.random.rand(num_cities, num_cities) F = (F + F.T) / 2 np.fill_diagonal(F, 0.0) return F def generate_fault_matrix(num_cities, prob=0.1, seed=None): """產生 0 或 1 的故障矩陣 I_fault(i,j)""" if seed is not None: np.random.seed(seed) # 產生 0 或 1 的矩陣 Fault_Mat = np.random.choice([0, 1], size=(num_cities, num_cities), p=[1-prob, prob]) # 確保矩陣對稱 (i 到 j 故障,j 到 i 也故障) Fault_Mat = np.maximum(Fault_Mat, Fault_Mat.T) np.fill_diagonal(Fault_Mat, 0.0) return Fault_Mat def idx_mtsp(k, i, p, N): return k * (N * N) + i * N + p def build_robust_qubo(D, F, Fault_Mat=None, gamma=GAMMA, sigma=SIGMA, alpha=ALPHA, penalty=PENALTY, big_penalty=BIG_PENALTY, lambda_val=FAULT_LAMBDA): N = D.shape[0] Q = {} def addQ(u, v, w): if w == 0: return if u > v: u, v = v, u Q[(u, v)] = Q.get((u, v), 0.0) + w for k in range(2): for p in range(N): q = (p + 1) % N for i in range(N): for j in range(N): dij, fij = D[i, j], F[i, j] if dij == 0: continue # 這裡就是 H-infinity 的核心風險項 risk_term = (sigma / (gamma**2)) * (fij**2) total_weight = dij + (alpha * risk_term) # 🌟 新增:加入故障訊號懲罰 if ENABLE_FAULT_SIGNAL and Fault_Mat is not None: total_weight += lambda_val * Fault_Mat[i, j] u, v = idx_mtsp(k, i, p, N), idx_mtsp(k, j, q, N) addQ(u, v, total_weight) for k in range(2): for p in range(N): vars_pos = [idx_mtsp(k, i, p, N) for i in range(N)] for u in vars_pos: addQ(u, u, -penalty) for a in range(N): for b in range(a+1, N): addQ(vars_pos[a], vars_pos[b], 2*penalty) for i in range(1, N): vars_city = [idx_mtsp(k, i, p, N) for k in range(2) for p in range(N)] for u in vars_city: addQ(u, u, -penalty) L = len(vars_city) for a in range(L): for b in range(a+1, L): addQ(vars_city[a], vars_city[b], 2*penalty) for k in range(2): for i in range(1, N): addQ(idx_mtsp(k, i, 0, N), idx_mtsp(k, i, 0, N), big_penalty) addQ(idx_mtsp(k, 0, 0, N), idx_mtsp(k, 0, 0, N), -big_penalty) return Q, N # --------------------------------------------------------- # 🌟 [新增] 貪婪演算法解決 Robust mTSP # --------------------------------------------------------- def solve_greedy_mtsp(D, F, Fault_Mat=None, gamma=GAMMA, sigma=SIGMA, alpha=ALPHA, lambda_val=FAULT_LAMBDA): """ 貪婪演算法 (Nearest Neighbor) 解決 Robust mTSP 邏輯:每次挑選目前累積成本較低的 UAV,讓它飛往剩餘未拜訪城市中「當下綜合成本最低」的那個。 Makespan 只計算距離(D),不包括風險和故障成本。 """ N = D.shape[0] unvisited = set(range(1, N)) p1, p2 = [], [] curr1, curr2 = 0, 0 cost1, cost2 = 0.0, 0.0 # 綜合成本(用於決策) dist_cost1, dist_cost2 = 0.0, 0.0 # 純距離成本(用於 Makespan) # 計算兩點之間的綜合成本 (與 QUBO 的權重公式一致) def edge_cost(u, v): """綜合成本:用於演算法決策""" if u == v: return float('inf') w = D[u, v] + alpha * (sigma / (gamma**2)) * (F[u, v]**2) if ENABLE_FAULT_SIGNAL and Fault_Mat is not None: w += lambda_val * Fault_Mat[u, v] return w while unvisited: # 決定換哪台無人機飛 (選目前綜合成本較低的,以平衡 Makespan) if cost1 <= cost2: # 找對 UAV 1 來說最便宜的下一個城市 next_node = min(unvisited, key=lambda v: edge_cost(curr1, v)) cost1 += edge_cost(curr1, next_node) dist_cost1 += D[curr1, next_node] # 只加距離 curr1 = next_node p1.append(next_node) unvisited.remove(next_node) else: # 找對 UAV 2 來說最便宜的下一個城市 next_node = min(unvisited, key=lambda v: edge_cost(curr2, v)) cost2 += edge_cost(curr2, next_node) dist_cost2 += D[curr2, next_node] # 只加距離 curr2 = next_node p2.append(next_node) unvisited.remove(next_node) # 最後兩台都要飛回起點 (Depot 0) - 只計算距離 dist_cost1 += D[curr1, 0] dist_cost2 += D[curr2, 0] # Makespan 只用距離成本計算 makespan = max(dist_cost1, dist_cost2) return p1, p2, makespan # --------------------------------------------------------- # 🌟 [新增] 產生平滑的 SQA Schedule # --------------------------------------------------------- def get_smooth_sqa_schedule(beta, total_sweeps, num_steps=100): """產生標準 SQA 的平滑退火排程 (s 從 0.0 遞增到 1.0)""" schedule = [] sweeps_per_step = max(1, int(total_sweeps / num_steps)) for s in np.linspace(0.0, 1.0, num_steps): schedule.append([float(s), float(beta), int(sweeps_per_step)]) return schedule def decode_slots(sample, N): slots = [] for k in range(2): row = [] for p in range(N): chosen = 0 for i in range(N): if sample.get(idx_mtsp(k, i, p, N), 0) == 1: chosen = i break row.append(chosen) slots.append(row) return slots[0], slots[1] def repair_routes_from_slots(u1_slots, u2_slots, N, D): count1, count2 = [0]*N, [0]*N for c in u1_slots: if 0 <= c < N: count1[c] += 1 for c in u2_slots: if 0 <= c < N: count2[c] += 1 assign1, assign2 = [], [] for city in range(1, N): if count1[city] > count2[city]: assign1.append(city) elif count2[city] > count1[city]: assign2.append(city) else: # 平票:計算該城市到兩個 UAV 分配城市群的平均距離 if assign1: avg_dist1 = np.mean([D[city, c] for c in assign1]) else: avg_dist1 = float('inf') if assign2: avg_dist2 = np.mean([D[city, c] for c in assign2]) else: avg_dist2 = float('inf') # 選擇距離較近的 UAV if avg_dist1 <= avg_dist2: assign1.append(city) else: assign2.append(city) return sorted(assign1), sorted(assign2) def uav_cost(path, D): if not path or len(path) == 0: return 0.0 cost = D[0, path[0]] for i in range(len(path)-1): cost += D[path[i], path[i+1]] cost += D[path[-1], 0] return float(cost) def uav_disturbance_energy(path, F, gamma=GAMMA, sigma=SIGMA, alpha=ALPHA): if not path or len(path) == 0: return 0.0 risk = alpha * (sigma / (gamma**2)) * (F[0, path[0]]**2) for i in range(len(path)-1): risk += alpha * (sigma / (gamma**2)) * (F[path[i], path[i+1]]**2) risk += alpha * (sigma / (gamma**2)) * (F[path[-1], 0]**2) return float(risk) def best_order_for_cities(cities, D, exact_limit=EXACT_LIMIT): cities = list(cities) if len(cities) <= 1: return cities, uav_cost(cities, D) if len(cities) <= exact_limit: best_perm, best_cost = None, float('inf') for perm in permutations(cities): c = uav_cost(list(perm), D) if c < best_cost: best_cost, best_perm = c, list(perm) return best_perm, best_cost else: # 簡單的 nearest neighbor heuristic 當作 fallback rem = set(cities) curr = min(rem, key=lambda x: D[0, x]) rem.remove(curr) route = [curr] while rem: nxt = min(rem, key=lambda x: D[curr, x]) route.append(nxt) rem.remove(nxt) curr = nxt return route, uav_cost(route, D) def get_makespan_and_risk(sample, N, D, F): u1, u2 = decode_slots(sample, N) a1, a2 = repair_routes_from_slots(u1, u2, N, D) p1, c1 = best_order_for_cities(a1, D) p2, c2 = best_order_for_cities(a2, D) makespan = max(c1, c2) dist_energy = uav_disturbance_energy(p1, F) + uav_disturbance_energy(p2, F) # 確保回傳路徑,以便印出 return makespan, dist_energy, p1, p2 # ==================================================================== # 🌟 [新增] 單機 TSP (Traveling Salesman Problem) 版本函數群 # ==================================================================== # 這些函數提供單機TSP的完整解決方案,用於與mTSP版本對比 # ==================================================================== def solve_greedy_tsp(CITIES, D, F, Fault_Mat, alpha, gamma, sigma, ENABLE_FAULT_SIGNAL, lambda_val): """ 貪婪演算法 (Nearest Neighbor) 解決單機 TSP 邏輯:從起點 0 開始,每次貪心地選擇「綜合成本最低」的未拜訪城市,最後回到起點。 特點:這種短視的貪心策略容易被「誘騙陷阱」欺騙,是 Greedy 的典型弱點。 """ start_time = time.time() unvisited = set(range(1, CITIES)) # 1 到 CITIES-1 path = [0] # 起點為 0 total_cost = 0.0 # 綜合評估函數(與 QUBO 的權重公式一致) def edge_cost(u, v): if u == v: return float('inf') w = D[u, v] + alpha * (sigma / (gamma**2)) * (F[u, v]**2) if ENABLE_FAULT_SIGNAL and Fault_Mat is not None: w += lambda_val * Fault_Mat[u, v] return w current_node = 0 while unvisited: # 找下一個綜合成本最小的城市(局部最佳化) next_node = min(unvisited, key=lambda city: edge_cost(current_node, city)) total_cost += edge_cost(current_node, next_node) path.append(next_node) unvisited.remove(next_node) current_node = next_node # 最後一哩路:被迫從最後一個點飛回起點(這就是 Greedy 的致命傷!) total_cost += edge_cost(current_node, 0) path.append(0) exec_time = time.time() - start_time return path, total_cost, exec_time def build_robust_qubo_tsp(CITIES, D, F, Fault_Mat, alpha, gamma, sigma, ENABLE_FAULT_SIGNAL, lambda_val, A): """ 為單機 TSP 構建 Robust QUBO 模型 變數索引化:x[v,i] 代表「城市 v 在第 i 個時間步被拜訪」 約束: 1. 每個城市恰好被拜訪一次 2. 每個時間步恰好有一個城市被拜訪 目標函數:最小化總路徑成本(包括距離與 H-infinity 風險) """ Q = {} N_visit = CITIES - 1 # 需要拜訪的城市數量(不含起點 0) # 變數 index 映射:將 (city, step) 轉為 1D index def get_idx(city, step): # city 是 1~CITIES-1, step 是 0~N_visit-1 return (city - 1) * N_visit + step # ===== 約束條件 A:每個城市只能在一個時間點被拜訪 (去且只去一次) ===== for v in range(1, CITIES): for i in range(N_visit): idx = get_idx(v, i) Q[(idx, idx)] = Q.get((idx, idx), 0) - A for j in range(i + 1, N_visit): idx2 = get_idx(v, j) Q[(idx, idx2)] = Q.get((idx, idx2), 0) + 2 * A # ===== 約束條件 B:每個時間點只能排一個城市 ===== for i in range(N_visit): for v in range(1, CITIES): idx = get_idx(v, i) Q[(idx, idx)] = Q.get((idx, idx), 0) - A for u in range(v + 1, CITIES): idx2 = get_idx(u, i) Q[(idx, idx2)] = Q.get((idx, idx2), 0) + 2 * A # ===== 綜合評估函數 ===== def edge_cost(u, v): w = D[u, v] + alpha * (sigma / (gamma**2)) * (F[u, v]**2) if ENABLE_FAULT_SIGNAL and Fault_Mat is not None: w += lambda_val * Fault_Mat[u, v] return w # ===== 目標函數:計算路徑總成本 ===== # (a) 從起點 0 出發的第一步 for v in range(1, CITIES): idx = get_idx(v, 0) Q[(idx, idx)] = Q.get((idx, idx), 0) + edge_cost(0, v) # (b) 中間的連續路徑 (step i 到 step i+1) for i in range(N_visit - 1): for u in range(1, CITIES): for v in range(1, CITIES): if u != v: idx_u = get_idx(u, i) idx_v = get_idx(v, i + 1) if idx_u < idx_v: Q[(idx_u, idx_v)] = Q.get((idx_u, idx_v), 0) + edge_cost(u, v) else: Q[(idx_v, idx_u)] = Q.get((idx_v, idx_u), 0) + edge_cost(u, v) # (c) 最後一步:從最後一個城市回到起點 0 for u in range(1, CITIES): idx = get_idx(u, N_visit - 1) Q[(idx, idx)] = Q.get((idx, idx), 0) + edge_cost(u, 0) return Q def decode_solution(sample, CITIES): """ 將 QUBO/SA/SQA 的量子採樣結果解碼成實際的城市訪問順序 返回值: - list: [0, city1, city2, ..., cityN, 0] 的完整路徑 - None: 如果結果違反 TSP 約束(重複、漏掉城市等) """ N_visit = CITIES - 1 path = [-1] * N_visit # 讀取量子/模擬退火的計算結果 for key, val in sample.items(): if val == 1: city = (key // N_visit) + 1 step = key % N_visit if step < N_visit: path[step] = city # 防呆:如果演算法違反 TSP 規則(沒走完或重複走),標記為無效解 if -1 in path or len(set(path)) != N_visit: return None return [0] + path + [0] # 加上起點與終點 0 def calculate_cost(path, D, F, Fault_Mat, alpha, gamma, sigma, ENABLE_FAULT_SIGNAL, lambda_val): """ 計算給定路徑的總成本 成本公式:Σ (distance + H-infinity risk + fault penalty) 對於無效路徑(None),返回無限大。 """ if path is None: return float('inf') # 不合法路徑給予無限大成本 total_cost = 0.0 for i in range(len(path) - 1): u, v = path[i], path[i+1] w = D[u, v] + alpha * (sigma / (gamma**2)) * (F[u, v]**2) if ENABLE_FAULT_SIGNAL and Fault_Mat is not None: w += lambda_val * Fault_Mat[u, v] total_cost += w return total_cost # ============================ # 複雜度與環境控制功能 (新增) # ============================ def run_comparative_evaluations(Q, N, D, F, s_star): print(f"\n⚔️ [階段三] 正式對決 (SA vs SQA, {NUM_RUNS} runs)") results = { 'sa': {'mk': [], 'risk': [], 'energy': [], 'faults': [], 'dist': [], 'best_mk': float('inf'), 'best_p1': None, 'best_p2': None}, 'sqa': {'mk': [], 'risk': [], 'energy': [], 'faults': [], 'dist': [], 'best_mk': float('inf'), 'best_p1': None, 'best_p2': None} } sqa_sampler = oj.SQASampler() sa_sampler = oj.SASampler() # 🌟 核心修改:建立 Sweeps 的退火排程 TOTAL_SWEEPS = SWEEPS_MAIN_TEST smooth_sched = get_smooth_sqa_schedule(beta=BETA, total_sweeps=TOTAL_SWEEPS, num_steps=100) for r in range(NUM_RUNS): # SA res_sa = sa_sampler.sample_qubo(Q, num_reads=10, num_sweeps=SWEEPS_MAIN_TEST) mk, risk, p1, p2 = get_makespan_and_risk(res_sa.first.sample, N, D, F) fault_count = count_faults_hit(p1, Fault_Mat) + count_faults_hit(p2, Fault_Mat) if ENABLE_FAULT_SIGNAL else 0 # 🌟 新增:計算raw distance raw_dist = 0 for path in [p1, p2]: if not path: continue full = [0] + path + [0] for i in range(len(full)-1): raw_dist += D[full[i], full[i+1]] results['sa']['mk'].append(mk) results['sa']['risk'].append(risk) results['sa']['energy'].append(res_sa.first.energy) results['sa']['faults'].append(fault_count) results['sa']['dist'].append(raw_dist) if mk < results['sa']['best_mk']: results['sa']['best_mk'] = mk results['sa']['best_p1'] = p1[:] # 🌟 使用 [:] 深度複製,確保快照 results['sa']['best_p2'] = p2[:] # 🌟 使用 [:] 深度複製,確保快照 print(f" [SA 執行 {r+1}/{NUM_RUNS}] 路徑 1: [0, {', '.join(map(str, p1))}, 0], 路徑 2: [0, {', '.join(map(str, p2))}, 0] | 成本: {mk:.2f} | 故障數: {fault_count}") # Standard SQA t0 = time.time() res_std = sqa_sampler.sample_qubo(Q, schedule=smooth_sched, num_reads=10) mk, risk, p1, p2 = get_makespan_and_risk(res_std.first.sample, N, D, F) fault_count = count_faults_hit(p1, Fault_Mat) + count_faults_hit(p2, Fault_Mat) if ENABLE_FAULT_SIGNAL else 0 # 🌟 新增:計算raw distance raw_dist = 0 for path in [p1, p2]: if not path: continue full = [0] + path + [0] for i in range(len(full)-1): raw_dist += D[full[i], full[i+1]] results['sqa']['mk'].append(mk) results['sqa']['risk'].append(risk) results['sqa']['energy'].append(res_std.first.energy) results['sqa']['faults'].append(fault_count) results['sqa']['dist'].append(raw_dist) if mk < results['sqa']['best_mk']: results['sqa']['best_mk'] = mk results['sqa']['best_p1'] = p1[:] # 🌟 使用 [:] 深度複製,確保快照 results['sqa']['best_p2'] = p2[:] # 🌟 使用 [:] 深度複製,確保快照 print(f" [Std-SQA 執行 {r+1}/{NUM_RUNS}] 耗時: {time.time()-t0:.2f}s | 路徑 1: [0, {', '.join(map(str, p1))}, 0], 路徑 2: [0, {', '.join(map(str, p2))}, 0] | 成本: {mk:.2f} | 故障數: {fault_count}") return results def create_basic_distribution_charts(results, greedy_mk=None, greedy_risk=None, greedy_energy=None, greedy_faults=None, greedy_dist=None): # 🌟 修改:在 COMPARE_H_INFINITY 模式下忽略 greedy 数据(因为会导致显示过于复杂) if COMPARE_H_INFINITY: greedy_mk = greedy_risk = greedy_energy = greedy_faults = greedy_dist = None if COMPARE_H_INFINITY: # 根据是否有greedy数据调整列数 (3列: Risk, Faults, Distance) num_cols = 3 fig, axes = plt.subplots(2, num_cols, figsize=(15, 10)) title_suffix = " (+ Greedy)" if greedy_mk is not None else "" fig.suptitle(r"Performance Distribution: Without vs With $H_\infty$ Obstacle Avoidance" + title_suffix, fontsize=16, fontweight='bold', y=1.05) row_labels = ['Without Obstacle Avoidance', r'With $H_\infty$ Obstacle Avoidance'] res_keys = ['baseline', 'robust'] for row, r_key in enumerate(res_keys): res = results[r_key] if greedy_mk is not None: labels = ['Greedy', 'SA', 'SQA'] colors = ['#2ca02c', 'gray', 'steelblue'] risk_data = [greedy_risk if greedy_risk is not None else [0.0], res['sa']['risk'], res['sqa']['risk']] fault_data = [greedy_faults if greedy_faults is not None else [0], res['sa']['faults'], res['sqa']['faults']] dist_data = [greedy_dist if greedy_dist is not None else [0.0], res['sa']['dist'], res['sqa']['dist']] else: labels = ['SA', 'SQA'] colors = ['gray', 'steelblue'] risk_data = [res['sa']['risk'], res['sqa']['risk']] fault_data = [res['sa']['faults'], res['sqa']['faults']] dist_data = [res['sa']['dist'], res['sqa']['dist']] parts1 = axes[row, 0].violinplot(risk_data, showmeans=True) axes[row, 0].set_title(f"[{row_labels[row]}] Risk Distribution") axes[row, 0].set_ylabel("Risk Penalty") parts2 = axes[row, 1].violinplot(fault_data, showmeans=True) axes[row, 1].set_title(f"[{row_labels[row]}] Faults Hit Distribution") axes[row, 1].set_ylabel("Number of Faults") parts3 = axes[row, 2].violinplot(dist_data, showmeans=True) axes[row, 2].set_title(f"[{row_labels[row]}] Raw Distance") axes[row, 2].set_ylabel("Distance Units") parts_list = [parts1, parts2, parts3] for col, parts in enumerate(parts_list): ax = axes[row, col] ax.set_xticks(np.arange(1, len(labels) + 1)) ax.set_xticklabels(labels, fontsize=11) ax.grid(alpha=0.3, axis='y') for pc, color in zip(parts['bodies'], colors): pc.set_facecolor(color) pc.set_alpha(0.7) else: # 单行模式:3个subplot (Risk, Faults, Raw Distance) - Makespan和Energy已单独分離 fig, axes = plt.subplots(1, 3, figsize=(15, 5)) title_suffix = " (+ Greedy)" if greedy_mk is not None else "" fig.suptitle("Performance Distribution Comparison" + title_suffix, fontsize=16, fontweight='bold', y=1.05) if greedy_mk is not None: labels = ['Greedy', 'SA', 'SQA'] colors = ['#2ca02c', 'gray', 'steelblue'] risk_data = [greedy_risk if greedy_risk is not None else [0.0], results['sa']['risk'], results['sqa']['risk']] fault_data = [greedy_faults if greedy_faults is not None else [0], results['sa']['faults'], results['sqa']['faults']] dist_data = [greedy_dist if greedy_dist is not None else [0.0], results['sa']['dist'], results['sqa']['dist']] else: labels = ['SA', 'SQA'] colors = ['gray', 'steelblue'] risk_data = [results['sa']['risk'], results['sqa']['risk']] fault_data = [results['sa']['faults'], results['sqa']['faults']] dist_data = [results['sa']['dist'], results['sqa']['dist']] # 1. Risk 分布 parts1 = axes[0].violinplot(risk_data, showmeans=True) axes[0].set_title(r"$H_\infty$ Disturbance Risk Distribution") axes[0].set_ylabel("Risk Penalty") # 2. Faults 分布 parts2 = axes[1].violinplot(fault_data, showmeans=True) axes[1].set_title("Faults Hit Distribution") axes[1].set_ylabel("Number of Faults") # 3. Raw Distance 分布 parts3 = axes[2].violinplot(dist_data, showmeans=True) axes[2].set_title("Raw Total Distance (D)") axes[2].set_ylabel("Distance Units") # 設定提琴圖外觀 parts_list = [parts1, parts2, parts3] for i, ax in enumerate(axes): ax.set_xticks(np.arange(1, len(labels) + 1)) ax.set_xticklabels(labels, fontsize=11) ax.grid(alpha=0.3, axis='y') parts = parts_list[i] for pc, color in zip(parts['bodies'], colors): pc.set_facecolor(color) pc.set_alpha(0.7) plt.tight_layout(rect=[0, 0.03, 1, 0.95]) plt.savefig(os.path.join(OUTPUT_DIR, "distribution_comparison.png"), dpi=300) print(f"\n🎨 圖表已繪製: {os.path.join(OUTPUT_DIR, 'distribution_comparison.png')}") # plt.show() (Moved to the end) def create_makespan_energy_charts(results, greedy_mk=None, greedy_energy=None): """繪製第二分布圖:Makespan 與 Energy 分布對比 (2個子圖) 注:Energy 分布不包含 Greedy 數據 """ if COMPARE_H_INFINITY: greedy_mk = greedy_energy = None if COMPARE_H_INFINITY: # 2x2 模式用於 H-infinity 對比 fig, axes = plt.subplots(2, 2, figsize=(14, 10)) fig.suptitle(r"Makespan & Energy Distribution: Without vs With $H_\infty$ Obstacle Avoidance", fontsize=16, fontweight='bold', y=1.05) row_labels = ['Without Obstacle Avoidance', r'With $H_\infty$ Obstacle Avoidance'] res_keys = ['baseline', 'robust'] for row, r_key in enumerate(res_keys): res = results[r_key] if greedy_mk is not None: mk_labels = ['Greedy', 'SA', 'SQA'] mk_colors = ['#2ca02c', 'gray', 'steelblue'] mk_data = [greedy_mk, res['sa']['mk'], res['sqa']['mk']] # Energy 不包含 Greedy energy_labels = ['SA', 'SQA'] energy_colors = ['gray', 'steelblue'] energy_data = [res['sa']['energy'], res['sqa']['energy']] else: mk_labels = ['SA', 'SQA'] mk_colors = ['gray', 'steelblue'] mk_data = [res['sa']['mk'], res['sqa']['mk']] energy_labels = ['SA', 'SQA'] energy_colors = ['gray', 'steelblue'] energy_data = [res['sa']['energy'], res['sqa']['energy']] # Makespan 圖 parts1 = axes[row, 0].violinplot(mk_data, showmeans=True) axes[row, 0].set_title(f"[{row_labels[row]}] Makespan Distribution") axes[row, 0].set_ylabel("Distance Cost") axes[row, 0].set_xticks(np.arange(1, len(mk_labels) + 1)) axes[row, 0].set_xticklabels(mk_labels, fontsize=11) axes[row, 0].grid(alpha=0.3, axis='y') for pc, color in zip(parts1['bodies'], mk_colors): pc.set_facecolor(color) pc.set_alpha(0.7) # Energy 圖 (不包含 Greedy) parts2 = axes[row, 1].violinplot(energy_data, showmeans=True) axes[row, 1].set_title(f"[{row_labels[row]}] Energy Distribution") axes[row, 1].set_ylabel("QUBO Energy") axes[row, 1].set_xticks(np.arange(1, len(energy_labels) + 1)) axes[row, 1].set_xticklabels(energy_labels, fontsize=11) axes[row, 1].grid(alpha=0.3, axis='y') for pc, color in zip(parts2['bodies'], energy_colors): pc.set_facecolor(color) pc.set_alpha(0.7) else: # 單行模式:2個子圖 (Makespan, Energy) fig, axes = plt.subplots(1, 2, figsize=(14, 5)) title_suffix = " (Makespan incl. Greedy, Energy excl. Greedy)" if greedy_mk is not None else "" fig.suptitle("Makespan & Energy Distribution Comparison" + title_suffix, fontsize=16, fontweight='bold', y=1.05) if greedy_mk is not None: # Makespan 包含 Greedy mk_labels = ['Greedy', 'SA', 'SQA'] mk_colors = ['#2ca02c', 'gray', 'steelblue'] mk_data = [greedy_mk, results['sa']['mk'], results['sqa']['mk']] # Energy 不包含 Greedy energy_labels = ['SA', 'SQA'] energy_colors = ['gray', 'steelblue'] energy_data = [results['sa']['energy'], results['sqa']['energy']] else: mk_labels = ['SA', 'SQA'] mk_colors = ['gray', 'steelblue'] mk_data = [results['sa']['mk'], results['sqa']['mk']] energy_labels = ['SA', 'SQA'] energy_colors = ['gray', 'steelblue'] energy_data = [results['sa']['energy'], results['sqa']['energy']] # 1. Makespan 分布 (包含 Greedy) parts1 = axes[0].violinplot(mk_data, showmeans=True) axes[0].set_title("Makespan (Distance Cost) Distribution") axes[0].set_ylabel("Distance Cost") axes[0].set_xticks(np.arange(1, len(mk_labels) + 1)) axes[0].set_xticklabels(mk_labels, fontsize=11) axes[0].grid(alpha=0.3, axis='y') for pc, color in zip(parts1['bodies'], mk_colors): pc.set_facecolor(color) pc.set_alpha(0.7) # 2. Energy 分布 (不包含 Greedy) parts2 = axes[1].violinplot(energy_data, showmeans=True) axes[1].set_title("QUBO System Raw Energy") axes[1].set_ylabel("System Energy") axes[1].set_xticks(np.arange(1, len(energy_labels) + 1)) axes[1].set_xticklabels(energy_labels, fontsize=11) axes[1].grid(alpha=0.3, axis='y') for pc, color in zip(parts2['bodies'], energy_colors): pc.set_facecolor(color) pc.set_alpha(0.7) plt.tight_layout(rect=[0, 0.03, 1, 0.95]) plt.savefig(os.path.join(OUTPUT_DIR, "makespan_energy_comparison.png"), dpi=300) print(f"🎨 Makespan & Energy 分布圖已繪製: {os.path.join(OUTPUT_DIR, 'makespan_energy_comparison.png')}") # plt.show() (Moved to the end) def plot_real_route_comparison(N, coords, p1_greedy, p2_greedy, p1_sa, p2_sa, p1_sqa, p2_sqa): """ 視覺化 Greedy 與 SA 與 SQA 的實體飛行路線對比 (使用真實座標) """ fig, axes = plt.subplots(1, 3, figsize=(20, 7)) fig.suptitle("UAV Actual Physical Trajectory: Greedy vs SA vs SQA", fontsize=16, fontweight='bold') G = nx.Graph() G.add_nodes_from(range(N)) # 🌟 關鍵修改:使用真實的 (x, y) 座標作為節點位置 🌟 pos = {i: (coords[i][0], coords[i][1]) for i in range(N)} titles = ["Greedy Algorithm", "Classical SA", "SQA"] routes_list = [(p1_greedy, p2_greedy), (p1_sa, p2_sa), (p1_sqa, p2_sqa)] for ax, title, (p1, p2) in zip(axes, titles, routes_list): ax.set_title(title, fontsize=14) # 畫節點 (依照真實地理位置散佈) nx.draw_networkx_nodes(G, pos, nodelist=[0], node_color='red', node_shape='s', node_size=300, ax=ax, label='Depot') nx.draw_networkx_nodes(G, pos, nodelist=range(1, N), node_color='skyblue', node_size=150, ax=ax) nx.draw_networkx_labels(G, pos, font_size=8, ax=ax) # 畫路線 def add_edges(path, color, style): if not path: return edges = [(0, path[0])] + [(path[i], path[i+1]) for i in range(len(path)-1)] + [(path[-1], 0)] nx.draw_networkx_edges(G, pos, edgelist=edges, edge_color=color, style=style, width=2.0, arrows=True, ax=ax) add_edges(p1, 'blue', 'solid') add_edges(p2, 'darkorange', 'dashed') plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, "route_trajectory_comparison.png"), dpi=300) print(f"🎨 飛行軌跡對比圖已繪製: {os.path.join(OUTPUT_DIR, 'route_trajectory_comparison.png')}") # ============================ # 複雜度與環境控制功能 (新增) # ============================= def inject_hidden_shortcut(D, F): """ 注入隱藏的黃金捷徑 (Hidden Shortcut): 創造一條極度狹窄且成本極低的路線 (例如 4 -> 5 -> 6 -> 7)。 如果沒照順序走,成本會極高;一旦走對,總成本會大幅下降。 """ N = D.shape[0] if N < 8: return D, F # 城市太少無法做長捷徑 # 定義黃金路線的節點 (假設是 4, 5, 6, 7) golden_nodes = [4, 5, 6, 7] # 1. 築起高牆:先把這幾個城市之間的所有連線,都變成極高成本 (阻止 SA 亂湊) for i in golden_nodes: for j in golden_nodes: if i != j: D[i, j] = 8.0 F[i, j] = 0.8 # 高風險 # 2. 挖出深谷:只開通 4->5, 5->6, 6->7 這條唯一且完美的捷徑 for i in range(len(golden_nodes) - 1): n1 = golden_nodes[i] n2 = golden_nodes[i + 1] # 距離極短,風險為 0 D[n1, n2] = 0.05 D[n2, n1] = 0.05 F[n1, n2] = 0.0 F[n2, n1] = 0.0 print("\n✨ [隱藏捷徑已佈署] 演算法將挑戰尋找極狹窄的黃金路線 (4->5->6->7)!") return D, F def inject_deceptive_trap(D, F, alpha=10.0, gamma=0.5, sigma=1.0): """ 注入欺騙性陷阱 (Deceptive Trap): 改造矩陣,創造一條「看似完美的捷徑」,測試演算法是否會陷入局部陷阱。 """ N = D.shape[0] if N < 4: return D, F # 城市太少無法做陷阱 # 定義陷阱節點 trap_start = 1 trap_end = 2 safe_detour = 3 # ========================================== # 陷阱 1:致命捷徑 (距離極度誘人,但風險爆表) # ========================================== D[trap_start, trap_end] = 0.1 D[trap_end, trap_start] = 0.1 F[trap_start, trap_end] = 0.99 F[trap_end, trap_start] = 0.99 # ========================================== # 陷阱 2:安全繞路 (距離較遠,但完全無風險) # ========================================== # 路線: trap_start -> safe_detour -> trap_end D[trap_start, safe_detour] = 4.0 D[safe_detour, trap_start] = 4.0 D[safe_detour, trap_end] = 4.0 D[trap_end, safe_detour] = 4.0 F[trap_start, safe_detour] = 0.01 F[safe_detour, trap_start] = 0.01 F[safe_detour, trap_end] = 0.01 F[trap_end, safe_detour] = 0.01 # 計算並印出真實成本,讓您確認陷阱是否成立 # 真實成本 = 距離 + alpha * (sigma / gamma^2) * F^2 risk_multiplier = alpha * (sigma / (gamma**2)) shortcut_cost = 0.1 + risk_multiplier * (0.99**2) detour_cost = (4.0 + risk_multiplier * (0.01**2)) + (4.0 + risk_multiplier * (0.01**2)) print(f" ➤ 致命捷徑 (1->2) 表觀距離: 0.1 | 隱藏真實成本: {shortcut_cost:.2f}") print(f" ➤ 安全繞路 (1->3->2) 表觀距離: 8.0 | 隱藏真實成本: {detour_cost:.2f}") return D, F def generate_controlled_matrices(num_cities, coord_std, risk_std, seed=42): """根據指定的標準差(變異數)生成 D 與 F 矩陣,精準控制問題複雜度""" np.random.seed(seed) # 1. 生成距離矩陣 D (使用常態分佈控制空間聚集度) coords = np.random.normal(loc=0.0, scale=coord_std, size=(num_cities, 2)) D = np.zeros((num_cities, num_cities), dtype=float) for i in range(num_cities): for j in range(i + 1, num_cities): dist = np.linalg.norm(coords[i] - coords[j]) D[i, j] = D[j, i] = dist # 2. 生成擾動矩陣 F (使用常態分佈控制風險極端值) F = np.abs(np.random.normal(loc=1.0, scale=risk_std, size=(num_cities, num_cities))) F = (F + F.T) / 2 # 確保對稱 np.fill_diagonal(F, 0.0) return np.round(D, 2), np.round(F, 2) def decode_and_eval(sample, N, D): # 快速解碼與貪婪/暴力求成本 u1, u2 = [], [] for p in range(N): for i in range(N): if sample.get(idx_mtsp(0, i, p, N), 0) == 1: u1.append(i) if sample.get(idx_mtsp(1, i, p, N), 0) == 1: u2.append(i) # 簡單修復 (去除重複,補齊缺失) c1, c2 = set(u1), set(u2) a1, a2 = [], [] for city in range(1, N): if city in c1 and city not in c2: a1.append(city) elif city in c2 and city not in c1: a2.append(city) else: # 平票:計算該城市到兩個 UAV 分配城市群的平均距離 if a1: avg_dist1 = np.mean([D[city, c] for c in a1]) else: avg_dist1 = float('inf') if a2: avg_dist2 = np.mean([D[city, c] for c in a2]) else: avg_dist2 = float('inf') # 選擇距離較近的 UAV if avg_dist1 <= avg_dist2: a1.append(city) else: a2.append(city) # 評估成本 def eval_cost(cities): if not cities: return 0 if len(cities) <= EXACT_LIMIT: bc = float('inf') for p in permutations(cities): c = D[0, p[0]] + sum(D[p[i], p[i+1]] for i in range(len(p)-1)) + D[p[-1], 0] bc = min(bc, c) return bc # 貪婪 rem, path, curr = set(cities), [], 0 while rem: nxt = min(rem, key=lambda x: D[curr, x]) path.append(nxt) rem.remove(nxt) curr = nxt return D[0, path[0]] + sum(D[path[i], path[i+1]] for i in range(len(path)-1)) + D[path[-1], 0] return max(eval_cost(a1), eval_cost(a2)) def run_complexity_scaling(): print(f"\n🚀 開始複雜度壓力測試...") print(f"⚙️ 空間變異數(COORD_STD)={COORD_STD}, 風險變異數(RISK_STD)={RISK_STD}") std_means, std_errs = [], [] sampler = oj.SQASampler() for N in N_LIST: print(f"\n📊 測試規模 N={N} ...") D, F = generate_controlled_matrices(N, COORD_STD, RISK_STD) # 🌟 新增:生成故障矩陣 Fault_Mat = generate_fault_matrix(N, prob=FAULT_PROBABILITY) if ENABLE_FAULT_SIGNAL else None # 動態計算懲罰值 max_d = np.max(D) max_f = np.max(F) if F is not None else 0 max_possible_cost = max_d + max_f dyn_penalty = max_possible_cost * 12.0 dyn_big_penalty = dyn_penalty * 5.0 Q, _ = build_robust_qubo(D, F, Fault_Mat=Fault_Mat, penalty=dyn_penalty, big_penalty=dyn_big_penalty) std_results = [] # 總步數統一為 SWEEPS_COMPLEXITY_TEST 步 std_sched = get_smooth_sqa_schedule(BETA, total_sweeps=SWEEPS_COMPLEXITY_TEST, num_steps=20) # 這裡設定每次 N 跑的次數,與原本 NUM_RUNS 獨立,建議 10 次 runs_for_scale = 10 for r in range(runs_for_scale): # Standard SQA res_std = sampler.sample_qubo(Q, schedule=std_sched, num_reads=5) std_results.append(decode_and_eval(res_std.first.sample, N, D)) mean_std, err_std = np.mean(std_results), np.std(std_results) std_means.append(mean_std) std_errs.append(err_std) print(f" SQA 平均: {mean_std:.2f} ± {err_std:.2f}") return std_means, std_errs def plot_crossover(std_means, std_errs): plt.figure(figsize=(10, 6)) plt.title(f"Complexity Scaling: Algorithm Performance vs Problem Size (N)\n(Coord Std={COORD_STD}, Risk Std={RISK_STD})", fontsize=14, fontweight='bold') # Draw line and error bars plt.errorbar(N_LIST, std_means, yerr=std_errs, fmt='-o', color='steelblue', linewidth=2.5, capsize=5, markersize=8, label='SQA') plt.xlabel("Problem Scale (Number of Cities $N$)", fontsize=12) plt.ylabel("Optimized Makespan (Distance Cost)", fontsize=12) plt.xticks(N_LIST) plt.grid(alpha=0.4, linestyle='--') plt.legend(fontsize=11) plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, "complexity_crossover.png"), dpi=300) print(f"🎨 Complexity scaling analysis saved to: {os.path.join(OUTPUT_DIR, 'complexity_crossover.png')}") # plt.show() (Moved to the end) def count_faults_hit(path, Fault_Mat): """計算路徑中踩到多少個故障點 (故障路段數)""" if Fault_Mat is None or path is None: return 0 fault_count = 0 for i in range(len(path) - 1): city_from = path[i] city_to = path[i + 1] if Fault_Mat[city_from, city_to] > 0.5: # 故障點 fault_count += 1 return fault_count def plot_disturbance_and_fault_matrices(F, Fault_Mat): """將擾動矩陣 (F) 與 故障矩陣 (Fault_Mat) 並排繪製熱力圖""" if Fault_Mat is None: return fig, axes = plt.subplots(1, 2, figsize=(14, 6)) fig.suptitle("Environment Disturbance vs. Fault Map", fontsize=16, fontweight='bold') # 畫擾動矩陣 F (連續值) sns.heatmap(F, ax=axes[0], cmap="YlOrRd", annot=False) axes[0].set_title("Disturbance Matrix (F)", fontsize=14) axes[0].set_xlabel("City index"); axes[0].set_ylabel("City index") # 畫故障矩陣 Fault_Mat (0或1) sns.heatmap(Fault_Mat, ax=axes[1], cmap="Reds", cbar=False, linewidths=0.5, linecolor='lightgray') axes[1].set_title("Fault Matrix (0: Normal, 1: Fault)", fontsize=14) axes[1].set_xlabel("City index"); axes[1].set_ylabel("City index") plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, "disturbance_fault_matrices.png"), dpi=300) print(f"🎨 擾動和故障矩陣圖已儲存: {os.path.join(OUTPUT_DIR, 'disturbance_fault_matrices.png')}") # plt.show() (Moved to the end) def plot_comprehensive_best_comparison(greedy_mks, sa_mks, sqa_mks, greedy_p1, greedy_p2, sa_p1, sa_p2, sqa_p1, sqa_p2, D, F, Fault_Mat): """繪製 2x2 Subplot: 綜合比較最佳解的 Makespan、故障數、純距離、純擾動 (加入 Greedy)""" if Fault_Mat is None: return def calc_raw_metrics(p1, p2): dist_total, disturb_total, fault_hits = 0, 0, 0 for path in [p1, p2]: if not path: continue full = [0] + path + [0] for i in range(len(full)-1): u, v = full[i], full[i+1] dist_total += D[u, v] disturb_total += F[u, v] fault_hits += Fault_Mat[u, v] return dist_total, disturb_total, fault_hits # 計算三者的真實物理指標 greedy_dist, greedy_disturb, greedy_faults = calc_raw_metrics(greedy_p1, greedy_p2) sa_dist, sa_disturb, sa_faults = calc_raw_metrics(sa_p1, sa_p2) sqa_dist, sqa_disturb, sqa_faults = calc_raw_metrics(sqa_p1, sqa_p2) fig, axes = plt.subplots(2, 2, figsize=(15, 11)) fig.suptitle("Comprehensive Performance: Greedy vs SA vs SQA", fontsize=18, fontweight='bold') labels = ['Greedy', 'Classical SA', 'SQA'] colors = ['#2ca02c', '#4C72B0', '#DD8452'] # 綠色(Greedy), 藍色(SA), 橘色(SQA) def plot_bar(ax, vals, title, ylabel, is_int=False): bars = ax.bar(labels, vals, color=colors, edgecolor='black', linewidth=1.2) ax.set_title(title, fontsize=14, fontweight='bold') ax.set_ylabel(ylabel, fontsize=12) if is_int: ax.yaxis.set_major_locator(plt.MaxNLocator(integer=True)) for bar, v in zip(bars, vals): yval = bar.get_height() text_str = f'{int(v)}' if is_int else f'{v:.1f}' ax.text(bar.get_x() + bar.get_width()/2.0, yval + (yval*0.01), text_str, ha='center', va='bottom', fontweight='bold', fontsize=12) plot_bar(axes[0, 0], [greedy_mks, sa_mks, sqa_mks], "1. Best Makespan (Total Objective Cost)", "Cost Score") plot_bar(axes[0, 1], [greedy_faults, sa_faults, sqa_faults], "2. Faults Hit (Safety Penalty)", "Number of Hits", is_int=True) axes[0, 1].set_title("2. Faults Hit (Safety Penalty)", color='darkred', fontweight='bold') plot_bar(axes[1, 0], [greedy_dist, sa_dist, sqa_dist], "3. Total Raw Distance (D)", "Distance Units") plot_bar(axes[1, 1], [greedy_disturb, sa_disturb, sqa_disturb], "4. Total Environmental Disturbance (F)", "Disturbance Level") plt.tight_layout(rect=[0, 0.03, 1, 0.95]) plt.savefig(os.path.join(OUTPUT_DIR, "comprehensive_best_comparison.png"), dpi=300) print(f"🎨 三柱綜合最佳對比圖已儲存: {os.path.join(OUTPUT_DIR, 'comprehensive_best_comparison.png')}") # plt.show() (Moved to the end) # ==================================================================== # 🌟 [新增] 單機 TSP 結果繪圖函數 # ==================================================================== def plot_tsp_results_comparison(tsp_results, city_coords=None): """ 繪製單機 TSP 的 Greedy vs SA 對比圖表 包含: 1. 成本對比柱狀圖 2. 路徑可視化(如果提供城市座標) """ if tsp_results is None: return # ===== 圖表 1: 成本對比 ===== fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) fig.suptitle("Single Machine TSP: Greedy vs Simulated Annealing", fontsize=16, fontweight='bold') # 成本對比 algorithms = ['Greedy\n(Myopic)', 'SA\n(Metaheuristic)'] costs = [ tsp_results['comparison']['greedy_cost'], tsp_results['comparison']['sa_cost'] ] colors = ['#2ca02c', '#4C72B0'] bars = ax1.bar(algorithms, costs, color=colors, edgecolor='black', linewidth=1.5, width=0.5) # 添加數值標籤 for bar, cost in zip(bars, costs): height = bar.get_height() ax1.text(bar.get_x() + bar.get_width()/2., height, f'{cost:.2f}', ha='center', va='bottom', fontweight='bold', fontsize=12) # 添加改進百分比 improvement = tsp_results['comparison']['improvement'] ax1.text(0.5, max(costs) * 0.5, f"改進: {improvement:.1f}%", ha='center', fontsize=14, fontweight='bold', bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7)) ax1.set_ylabel("Total Path Cost", fontsize=12) ax1.set_title("Cost Comparison", fontsize=13, fontweight='bold') ax1.grid(axis='y', alpha=0.3) # ===== 圖表 2: 路徑長度對比 ===== greedy_path = tsp_results['greedy']['path'] sa_path = tsp_results['sa']['path'] ax2.text(0.5, 0.9, "Single Machine TSP Path Summary", ha='center', fontsize=13, fontweight='bold', transform=ax2.transAxes) # Greedy 路徑信息 if greedy_path is not None: ax2.text(0.05, 0.75, f"Greedy Path ({len(greedy_path)-2} cities):", fontsize=11, fontweight='bold', transform=ax2.transAxes) greedy_str = f"[{greedy_path[0]}, {', '.join(map(str, greedy_path[1:-1]))}, {greedy_path[-1]}]" ax2.text(0.05, 0.65, greedy_str, fontsize=9, family='monospace', transform=ax2.transAxes, wrap=True) else: ax2.text(0.05, 0.75, "Greedy Path: Invalid/None", fontsize=11, fontweight='bold', color='red', transform=ax2.transAxes) ax2.text(0.05, 0.55, f"Cost: {tsp_results['comparison']['greedy_cost']:.2f}", fontsize=10, color='#2ca02c', fontweight='bold', transform=ax2.transAxes) # SA 路徑信息 if sa_path is not None: ax2.text(0.05, 0.40, f"SA Path ({len(sa_path)-2} cities):", fontsize=11, fontweight='bold', transform=ax2.transAxes) sa_str = f"[{sa_path[0]}, {', '.join(map(str, sa_path[1:-1]))}, {sa_path[-1]}]" ax2.text(0.05, 0.30, sa_str, fontsize=9, family='monospace', transform=ax2.transAxes, wrap=True) else: ax2.text(0.05, 0.40, "SA Path: Invalid/None", fontsize=11, fontweight='bold', color='red', transform=ax2.transAxes) ax2.text(0.05, 0.20, f"Cost: {tsp_results['comparison']['sa_cost']:.2f}", fontsize=10, color='#4C72B0', fontweight='bold', transform=ax2.transAxes) ax2.axis('off') plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, "tsp_comparison.png"), dpi=300, bbox_inches='tight') print(f"🎨 單機 TSP 對比圖已儲存: {os.path.join(OUTPUT_DIR, 'tsp_comparison.png')}") # ===== 圖表 3: 如果有城市座標,繪製路徑圖 ===== if city_coords is not None and len(city_coords) > 0 and greedy_path is not None and sa_path is not None: fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) fig.suptitle("Single Machine TSP: Path Visualization", fontsize=16, fontweight='bold') # 繪製 Greedy 路徑 ax1.scatter(city_coords[:, 0], city_coords[:, 1], s=100, c='red', zorder=3, label='Cities') greedy_path_coords = city_coords[greedy_path] ax1.plot(greedy_path_coords[:, 0], greedy_path_coords[:, 1], 'g-', linewidth=2, label='Greedy Path') ax1.plot(greedy_path_coords[0, 0], greedy_path_coords[0, 1], 'go', markersize=12, label='Start/End') # 添加城市標籤 for i, (x, y) in enumerate(city_coords): ax1.text(x, y+1, str(i), ha='center', fontsize=9, fontweight='bold') ax1.set_title(f"Greedy Path (Cost: {tsp_results['comparison']['greedy_cost']:.2f})", fontsize=12, fontweight='bold') ax1.set_xlabel("X Coordinate") ax1.set_ylabel("Y Coordinate") ax1.legend(loc='best') ax1.grid(alpha=0.3) ax1.set_aspect('equal') # 繪製 SA 路徑 ax2.scatter(city_coords[:, 0], city_coords[:, 1], s=100, c='red', zorder=3, label='Cities') sa_path_coords = city_coords[sa_path] ax2.plot(sa_path_coords[:, 0], sa_path_coords[:, 1], 'b-', linewidth=2, label='SA Path') ax2.plot(sa_path_coords[0, 0], sa_path_coords[0, 1], 'bo', markersize=12, label='Start/End') # 添加城市標籤 for i, (x, y) in enumerate(city_coords): ax2.text(x, y+1, str(i), ha='center', fontsize=9, fontweight='bold') ax2.set_title(f"SA Path (Cost: {tsp_results['comparison']['sa_cost']:.2f})", fontsize=12, fontweight='bold') ax2.set_xlabel("X Coordinate") ax2.set_ylabel("Y Coordinate") ax2.legend(loc='best') ax2.grid(alpha=0.3) ax2.set_aspect('equal') plt.tight_layout() plt.savefig(os.path.join(OUTPUT_DIR, "tsp_path_visualization.png"), dpi=300, bbox_inches='tight') print(f"🎨 單機 TSP 路徑可視化已儲存: {os.path.join(OUTPUT_DIR, 'tsp_path_visualization.png')}") elif sa_path is None or greedy_path is None: print("⚠️ 警告: SA 路徑無效(違反 TSP 約束),略過路徑可視化圖表") # ============================ # 參數記錄功能 # ============================ def save_parameters_to_file(output_dir): """將所有參數記錄到 .txt 文件""" params_file = os.path.join(output_dir, "parameters_log.txt") with open(params_file, 'w', encoding='utf-8') as f: f.write("=" * 80 + "\n") f.write("實驗參數設定記錄\n") f.write("=" * 80 + "\n\n") f.write(f"生成時間: {TIMESTAMP}\n\n") # 問題規模設定 f.write("[問題規模設定]\n") f.write(f"CITIES = {CITIES} # 初始核心問題規模\n") f.write(f"CITIES_BOUND = {CITIES_BOUND} # 問題規模的上下範圍\n") f.write(f"RANDOM = {RANDOM}\n") f.write(f"RANDOM_SEED = {RANDOM_SEED}\n") f.write(f"COORD_RANGE = {COORD_RANGE}\n") f.write(f"N_LIST = {N_LIST}\n\n") # 環境變異數控制 f.write("[環境變異數控制與複雜度測試參數]\n") f.write(f"COORD_STD = {COORD_STD} # 空間分佈變異數\n") f.write(f"RISK_STD = {RISK_STD} # 擾動風險變異數\n\n") # 演算法執行設定 f.write("[演算法執行設定]\n") f.write(f"NUM_RUNS = {NUM_RUNS} # 正式比較的執行次數\n") f.write(f"BETA = {BETA} # 退火溫度參數\n") f.write(f"SWEEPS_MAIN_TEST = {SWEEPS_MAIN_TEST} # 主要測試的退火步數\n") f.write(f"SWEEPS_COMPLEXITY_TEST = {SWEEPS_COMPLEXITY_TEST} # 複雜度測試的退火步數\n") f.write(f"HEATMAP_RUNS = {HEATMAP_RUNS} # 熱力圖測試每種情況的平均次數\n\n") # QUBO參數 f.write("[QUBO參數]\n") f.write(f"PENALTY = {PENALTY} # 約束違反懲罰權重\n") f.write(f"BIG_PENALTY = {BIG_PENALTY} # 起點約束權重\n") f.write(f"EXACT_LIMIT = {EXACT_LIMIT} # TSP精確求解上限\n\n") # 魯棒優化參數 f.write("[魯棒優化參數 (H-infinity)]\n") f.write(f"USE_ROBUST = {USE_ROBUST} # 是否啟用魯棒優化\n") f.write(f"GAMMA = {GAMMA} # H-infinity 阻尼因子\n") f.write(f"SIGMA = {SIGMA} # H-infinity 擾動標度\n") f.write(f"ALPHA = {ALPHA} # H-infinity 權重係數\n\n") # 故障訊號參數 f.write("[故障訊號參數]\n") f.write(f"ENABLE_FAULT_SIGNAL = {ENABLE_FAULT_SIGNAL} # 是否加入故障訊號\n") f.write(f"FAULT_LAMBDA = {FAULT_LAMBDA} # 故障懲罰權重\n") f.write(f"FAULT_PROBABILITY = {FAULT_PROBABILITY} # 故障發生機率\n\n") # 執行區塊控制 f.write("[執行區塊控制]\n") f.write(f"RUN_MAIN_TEST = {RUN_MAIN_TEST} # 執行主要演算法比較\n") f.write(f"RUN_TSP_TEST = {RUN_TSP_TEST} # 執行單機 TSP 測試\n") f.write(f"COMPARE_H_INFINITY = {COMPARE_H_INFINITY} # 比較H-infinity效果\n") f.write(f"RUN_COMPLEXITY_TEST = {RUN_COMPLEXITY_TEST} # 執行複雜度測試\n") f.write(f"SHOW_TERRAIN_PLOTS = {SHOW_TERRAIN_PLOTS} # 顯示能量地形圖\n") f.write(f"ENABLE_DISTURBANCE_MATRIX = {ENABLE_DISTURBANCE_MATRIX} # 生成擾動矩陣\n") f.write(f"ENABLE_QUANTUM_MINEFIELD = {ENABLE_QUANTUM_MINEFIELD} # 產生量子雷區\n") f.write(f"PLOT_ENV_DIS_FAULT = {PLOT_ENV_DIS_FAULT} # 繪製環境矩陣\n\n") f.write("=" * 80 + "\n") f.write("📝 參數記錄已保存\n") f.write("=" * 80 + "\n") print(f"📝 參數記錄文件已生成: {params_file}") return params_file # ============================ # 主程式執行入口 # =========================== if __name__ == "__main__": # 立即生成參數記錄文件 save_parameters_to_file(OUTPUT_DIR) print("="*50) if ENABLE_QUANTUM_MINEFIELD: print("🚀 生成極限量子雷區測試...") D, F = generate_quantum_minefield(CITIES, random_seed=RANDOM_SEED) # 根據 ENABLE_DISTURBANCE_MATRIX 參數決定是否使用擾動矩陣 if not ENABLE_DISTURBANCE_MATRIX: F = np.zeros((CITIES, CITIES)) print("📍 擾動矩陣已禁用,使用零矩陣") # 為極限雷區建立環狀顯示用實體座標 (符合原有 layout 的精神) city_coords = [] for i in range(CITIES): angle = 2 * np.pi * i / CITIES city_coords.append((50 + 40 * np.cos(angle), 50 + 40 * np.sin(angle))) # 進行數值驗證與畫圖 if SHOW_TERRAIN_PLOTS: verify_minefield_stats(D, F) plot_energy_landscape_heatmap(D, F) plot_energy_landscape_3d(D, F) else: print("🚀 生成一般隨機任務環境...") D, city_coords = generate_distance_matrix(CITIES, random=RANDOM) if ENABLE_DISTURBANCE_MATRIX: F = generate_disturbance_matrix(CITIES) else: F = np.zeros((CITIES, CITIES)) print("📍 擾動矩陣已禁用,使用零矩陣") # 您可選擇是否在此呼叫原有的干擾產生器 # D, F = inject_deceptive_trap(D, F, alpha=ALPHA, gamma=GAMMA, sigma=SIGMA) # D, F = inject_hidden_shortcut(D, F) # 🌟 新增:生成故障矩陣 if ENABLE_FAULT_SIGNAL: print("🚨 故障訊號 (Fault Signal) 已啟用!正在生成隨機禁飛路段...") Fault_Mat = generate_fault_matrix(CITIES, prob=FAULT_PROBABILITY, seed=RANDOM_SEED) else: Fault_Mat = None # 原本的其他環境干擾暫時維持 (也可以選擇註解掉,因為 generate_quantum_minefield 已經很極端) # 為了保持之前的介面一致,這裡先保留動態懲罰值計算 # ======== 🌟 新增:動態自適應懲罰值 ======== # 找出矩陣中最遙遠的距離與最大的擾動 max_d = np.max(D) max_f = np.max(F) if F is not None else 0 max_possible_cost = max_d + max_f # 根據最大可能成本,動態設定懲罰值 (通常設為 10 倍 ~ 15 倍最穩定) PENALTY = max_possible_cost * 12.0 BIG_PENALTY = PENALTY * 5.0 Q = None if RUN_MAIN_TEST: Q, _ = build_robust_qubo(D, F, Fault_Mat=Fault_Mat, penalty=PENALTY, big_penalty=BIG_PENALTY) if RUN_MAIN_TEST: print(f"🔧 [自動調校] 偵測到單趟最大成本: {max_possible_cost:.2f}") print(f"🔧 [自動調校] 動態 PENALTY 設為: {PENALTY:.2f}, BIG_PENALTY 設為: {BIG_PENALTY:.2f}") if COMPARE_H_INFINITY: print("==================================================") print("🚀 [開始 H-infinity 避障算法對比測試]") print("==================================================") # --- 基準測試 (無避障策略,不考慮障礙物風險) --- print("\n--- 基準測試 (無避障策略 - Algorithm Ignores Obstacles) ---") Q_baseline, _ = build_robust_qubo(D, F, Fault_Mat=Fault_Mat, alpha=0.0, penalty=PENALTY, big_penalty=BIG_PENALTY) results_base = run_comparative_evaluations(Q_baseline, CITIES, D, F, 0.5) # --- 穩健測試 (有避障策略,考慮障礙物風險) --- print("\n--- 穩健測試 (有避障策略 - Algorithm Avoids Obstacles) ---") Q_robust, _ = build_robust_qubo(D, F, Fault_Mat=Fault_Mat, alpha=ALPHA, penalty=PENALTY, big_penalty=BIG_PENALTY) results_rob = run_comparative_evaluations(Q_robust, CITIES, D, F, 0.5) # Pack results for plotting results = { 'baseline': results_base, 'robust': results_rob } results_for_academic = results_rob else: print("==================================================") print(f"🚀 Robust mTSP Optimization (mTSP N={CITIES})") print("==================================================") print("\n[初始化] 距離矩陣 D:") print(D) print("\n[初始化] 擾動矩陣 F:") print(F) Q, _ = build_robust_qubo(D, F, Fault_Mat=Fault_Mat, penalty=PENALTY, big_penalty=BIG_PENALTY) # 正式評估 results = run_comparative_evaluations(Q, CITIES, D, F, 0.5) results_for_academic = results # ==================================================================== # 🌟 [新增] 單機 TSP 測試區塊 # ==================================================================== tsp_results = None if RUN_TSP_TEST: print("\n" + "="*60) print("🚀 [單機 TSP 測試] Traveling Salesman Problem (Single UAV)") print("="*60) # --- 方案 1: Greedy 近鄰演算法 --- print("\n[TSP 測試 1/3] 執行 Greedy 近鄰演算法...") tsp_greedy_path, tsp_greedy_cost, tsp_greedy_time = solve_greedy_tsp( CITIES, D, F, Fault_Mat=Fault_Mat if ENABLE_FAULT_SIGNAL else None, alpha=ALPHA, gamma=GAMMA, sigma=SIGMA, ENABLE_FAULT_SIGNAL=ENABLE_FAULT_SIGNAL, lambda_val=FAULT_LAMBDA ) print(f" ✓ Greedy TSP 成本: {tsp_greedy_cost:.2f} | 耗時: {tsp_greedy_time:.4f}s") print(f" 路徑: {tsp_greedy_path}") # --- 方案 2: 構建 QUBO 模型用於 SA 和 SQA --- print("\n[TSP 測試 2/3] 構建單機 TSP 的 QUBO 模型...") Q_tsp = build_robust_qubo_tsp( CITIES, D, F, Fault_Mat=Fault_Mat if ENABLE_FAULT_SIGNAL else None, alpha=ALPHA, gamma=GAMMA, sigma=SIGMA, ENABLE_FAULT_SIGNAL=ENABLE_FAULT_SIGNAL, lambda_val=FAULT_LAMBDA, A=PENALTY ) print(f" ✓ QUBO 矩陣規模: {len(Q_tsp)} 項") # --- 方案 3: 使用 SA 求解 TSP --- print("\n[TSP 測試 3/3] 使用 Simulated Annealing 求解 TSP...") sa_sampler = oj.SASampler() sa_tsp_results = [] for run_idx in range(min(3, NUM_RUNS)): # 只跑 3 次示意 response_sa = sa_sampler.sample_qubo(Q_tsp, num_reads=1, num_sweeps=SWEEPS_MAIN_TEST) best_sample = response_sa.first.sample tsp_path = decode_solution(best_sample, CITIES) # 跳過無效解 if tsp_path is None: print(f" ⚠️ SA TSP Run {run_idx+1}: 無效解(違反 TSP 約束)") continue tsp_cost = calculate_cost(tsp_path, D, F, Fault_Mat=Fault_Mat if ENABLE_FAULT_SIGNAL else None, alpha=ALPHA, gamma=GAMMA, sigma=SIGMA, ENABLE_FAULT_SIGNAL=ENABLE_FAULT_SIGNAL, lambda_val=FAULT_LAMBDA) sa_tsp_results.append({ 'path': tsp_path, 'cost': tsp_cost, 'energy': response_sa.first.energy }) print(f" ✓ SA TSP Run {run_idx+1}: 成本={tsp_cost:.2f}, 能量={response_sa.first.energy:.4f}") # 找最佳 SA 結果 if len(sa_tsp_results) > 0: best_sa_tsp = min(sa_tsp_results, key=lambda x: x['cost']) print(f"\n 🏆 SA TSP 最佳結果: {best_sa_tsp['cost']:.2f}") print(f" 路徑: {best_sa_tsp['path']}") else: print("\n ❌ SA TSP: 未找到有效解,使用 Greedy 作為備選") best_sa_tsp = { 'path': tsp_greedy_path, 'cost': tsp_greedy_cost, 'energy': float('inf') } # --- 彙總結果 --- tsp_results = { 'greedy': { 'path': tsp_greedy_path, 'cost': tsp_greedy_cost, 'time': tsp_greedy_time }, 'sa': best_sa_tsp, 'comparison': { 'greedy_cost': tsp_greedy_cost, 'sa_cost': best_sa_tsp['cost'], 'improvement': ((tsp_greedy_cost - best_sa_tsp['cost']) / tsp_greedy_cost * 100) if tsp_greedy_cost > 0 else 0 } } # 打印對比摘要 print("\n" + "="*60) print("📊 單機 TSP 求解摘要:") print("="*60) print(f"Greedy 隨機演算法: 成本 = {tsp_results['comparison']['greedy_cost']:.2f}") print(f"Simulated Annealing: 成本 = {tsp_results['comparison']['sa_cost']:.2f}") print(f"優化改進: {tsp_results['comparison']['improvement']:.1f}%") print("="*60 + "\n") s_m, s_e = None, None if RUN_COMPLEXITY_TEST: # Stage 4: Complexity stress testing and crossover analysis s_m, s_e = run_complexity_scaling() print("\n🏁 所有運算完成!開始繪製圖表...") # 🌟 新增:繪製 TSP 結果圖表 if RUN_TSP_TEST and tsp_results is not None: print("\n📊 繪製單機 TSP 結果對比圖...") plot_tsp_results_comparison(tsp_results, city_coords=np.array(city_coords)) if RUN_MAIN_TEST: # 🌟 新增:執行 Greedy 演算法 (提前計算以便傳給繪圖函數) t0_greedy = time.time() greedy_p1, greedy_p2, greedy_makespan = solve_greedy_mtsp( D, F, Fault_Mat=Fault_Mat if ENABLE_FAULT_SIGNAL else None ) elapsed_time_greedy = time.time() - t0_greedy # 計算 Greedy 的故障數和raw distance greedy_fault_count = 0 greedy_raw_dist = 0 if ENABLE_FAULT_SIGNAL: greedy_fault_count = count_faults_hit(greedy_p1, Fault_Mat) + count_faults_hit(greedy_p2, Fault_Mat) # 🌟 新增:計算greedy的raw distance for path in [greedy_p1, greedy_p2]: if not path: continue full = [0] + path + [0] for i in range(len(full)-1): greedy_raw_dist += D[full[i], full[i+1]] # 輸出 Greedy 的結果 print(f" [Greedy 演算法] 耗時: {elapsed_time_greedy:.4f}s | 路徑 1: [0, {', '.join(map(str, greedy_p1))}, 0], 路徑 2: [0, {', '.join(map(str, greedy_p2))}, 0] | 成本: {greedy_makespan:.2f} | 故障數: {greedy_fault_count}") # 繪製圖表一:Risk、Faults、Distance 分布對比 create_basic_distribution_charts(results, greedy_mk=[greedy_makespan], greedy_risk=[0.0], greedy_faults=[greedy_fault_count], greedy_dist=[greedy_raw_dist]) # 繪製圖表:Makespan、Energy 分布對比 create_makespan_energy_charts(results, greedy_mk=[greedy_makespan], greedy_energy=[greedy_makespan]) # 繪製圖表:Makespan與 Energy 分布對比 create_makespan_energy_charts(results, greedy_mk=[greedy_makespan], greedy_energy=[greedy_makespan]) # 繪製圖表:路徑對比圖 if COMPARE_H_INFINITY: target_res = results['robust'] else: target_res = results # 🌟 新增:1. 畫出擾動與故障矩陣對比熱力圖 if ENABLE_FAULT_SIGNAL and PLOT_ENV_DIS_FAULT: plot_disturbance_and_fault_matrices(F, Fault_Mat) best_sa_1 = target_res['sa']['best_p1'] best_sa_2 = target_res['sa']['best_p2'] best_qa_1 = target_res['sqa']['best_p1'] best_qa_2 = target_res['sqa']['best_p2'] qa_makespan = target_res['sqa']['best_mk'] sa_makespan = target_res['sa']['best_mk'] # 🌟 修改:畫出「三柱」綜合指標對比圖 (Greedy vs SA vs SQA) if ENABLE_FAULT_SIGNAL: plot_comprehensive_best_comparison( greedy_mks=greedy_makespan, sa_mks=sa_makespan, sqa_mks=qa_makespan, greedy_p1=greedy_p1, greedy_p2=greedy_p2, sa_p1=best_sa_1, sa_p2=best_sa_2, sqa_p1=best_qa_1, sqa_p2=best_qa_2, D=D, F=F, Fault_Mat=Fault_Mat ) plot_real_route_comparison(CITIES, city_coords, greedy_p1, greedy_p2, best_sa_1, best_sa_2, best_qa_1, best_qa_2) if RUN_COMPLEXITY_TEST and s_m is not None: # Plot Chart Three: Complexity Stress Testing and Crossover Analysis plot_crossover(s_m, s_e) print("\n" + "="*60) print("🏁 所有實驗與圖表繪製完成!") print("="*60) print(f"\n📁 所有圖表已儲存在: {os.path.abspath(OUTPUT_DIR)}") print("\n📊 已生成的圖表文件:") if os.path.exists(OUTPUT_DIR): files = sorted(os.listdir(OUTPUT_DIR)) for i, fname in enumerate(files, 1): if fname.endswith('.png'): print(f" {i}. {fname}") print("\n📈 圖表說明:") if RUN_TSP_TEST: print(" • tsp_comparison.png - 單機 TSP: Greedy vs SA 成本對比") print(" • tsp_path_visualization.png - 單機 TSP: 實際路徑可視化") if RUN_MAIN_TEST: print(" • distribution_comparison.png - mTSP: Risk/Faults/Distance 分布") print(" • makespan_energy_comparison.png - mTSP: Makespan & Energy 分布") print(" • comprehensive_best_comparison.png - mTSP: Greedy/SA/SQA 綜合對比") print(" • real_route_comparison.png - mTSP: 最優路徑可視化") if ENABLE_FAULT_SIGNAL and PLOT_ENV_DIS_FAULT: print(" • disturbance_fault_matrices.png - 擾動與故障矩陣熱力圖") print("\n💡 提示: 所有圖表窗口將同時顯示,請在各窗口完成檢視後關閉。") print("="*60 + "\n") # 同時顯示所有圖表窗口 plt.show()