1bnjmn3 commited on
Commit
c5f84b2
·
verified ·
1 Parent(s): 4b1aebf

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. Phase 1/01_benchmark_qgan +249 -0
  2. Phase 1/Bridge-island_visual.py +79 -0
  3. Phase 1/V2.1_QGAN_HailMary.py +231 -0
  4. Phase 1/V2_benchmark_cgan +187 -0
  5. Phase 1/V2_benchmark_qgan +232 -0
  6. Phase 1/complex_tokamak_data.csv +0 -0
  7. Phase 1/main_fusion_qgan.py +367 -0
  8. Phase 1/physics_data +110 -0
  9. Phase 1/plots_benchmark/QGAN_Trial_7_ROC.png +0 -0
  10. Phase 1/plots_v2_cgan/roc_trial_1.png +0 -0
  11. Phase 1/plots_v2_cgan/roc_trial_10.png +0 -0
  12. Phase 1/plots_v2_cgan/roc_trial_11.png +0 -0
  13. Phase 1/plots_v2_cgan/roc_trial_12.png +0 -0
  14. Phase 1/plots_v2_cgan/roc_trial_13.png +0 -0
  15. Phase 1/plots_v2_cgan/roc_trial_14.png +0 -0
  16. Phase 1/plots_v2_cgan/roc_trial_15.png +0 -0
  17. Phase 1/plots_v2_cgan/roc_trial_16.png +0 -0
  18. Phase 1/plots_v2_cgan/roc_trial_17.png +0 -0
  19. Phase 1/plots_v2_cgan/roc_trial_18.png +0 -0
  20. Phase 1/plots_v2_cgan/roc_trial_19.png +0 -0
  21. Phase 1/plots_v2_cgan/roc_trial_2.png +0 -0
  22. Phase 1/plots_v2_cgan/roc_trial_20.png +0 -0
  23. Phase 1/plots_v2_cgan/roc_trial_3.png +0 -0
  24. Phase 1/plots_v2_cgan/roc_trial_4.png +0 -0
  25. Phase 1/plots_v2_cgan/roc_trial_5.png +0 -0
  26. Phase 1/plots_v2_cgan/roc_trial_6.png +0 -0
  27. Phase 1/plots_v2_cgan/roc_trial_7.png +0 -0
  28. Phase 1/plots_v2_cgan/roc_trial_8.png +0 -0
  29. Phase 1/plots_v2_cgan/roc_trial_9.png +0 -0
  30. Phase 1/plots_v2_qgan/roc_trial_1.png +0 -0
  31. Phase 1/plots_v2_qgan/roc_trial_10.png +0 -0
  32. Phase 1/plots_v2_qgan/roc_trial_11.png +0 -0
  33. Phase 1/plots_v2_qgan/roc_trial_12.png +0 -0
  34. Phase 1/plots_v2_qgan/roc_trial_13.png +0 -0
  35. Phase 1/plots_v2_qgan/roc_trial_14.png +0 -0
  36. Phase 1/plots_v2_qgan/roc_trial_15.png +0 -0
  37. Phase 1/plots_v2_qgan/roc_trial_16.png +0 -0
  38. Phase 1/plots_v2_qgan/roc_trial_17.png +0 -0
  39. Phase 1/plots_v2_qgan/roc_trial_18.png +0 -0
  40. Phase 1/plots_v2_qgan/roc_trial_19.png +0 -0
  41. Phase 1/plots_v2_qgan/roc_trial_2.png +0 -0
  42. Phase 1/plots_v2_qgan/roc_trial_20.png +0 -0
  43. Phase 1/plots_v2_qgan/roc_trial_3.png +0 -0
  44. Phase 1/plots_v2_qgan/roc_trial_4.png +0 -0
  45. Phase 1/plots_v2_qgan/roc_trial_5.png +0 -0
  46. Phase 1/plots_v2_qgan/roc_trial_6.png +0 -0
  47. Phase 1/plots_v2_qgan/roc_trial_7.png +0 -0
  48. Phase 1/plots_v2_qgan/roc_trial_8.png +0 -0
  49. Phase 1/plots_v2_qgan/roc_trial_9.png +0 -0
  50. plasma_sim_v2.py +64 -0
Phase 1/01_benchmark_qgan ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- STEP 0: IMPORTS & "HEADLESS" MODE ---
2
+ import matplotlib
3
+ matplotlib.use('Agg')
4
+ import numpy as np
5
+ import pandas as pd
6
+ import matplotlib.pyplot as plt
7
+ import os
8
+ from sklearn.preprocessing import StandardScaler
9
+ from sklearn.decomposition import PCA
10
+ from sklearn.metrics import roc_curve, auc
11
+ import torch
12
+ import torch.nn as nn
13
+ from torch.optim import Adam
14
+ from qiskit import QuantumCircuit
15
+ from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap
16
+ from qiskit.primitives import Sampler
17
+ from qiskit_machine_learning.neural_networks import SamplerQNN
18
+ from qiskit_machine_learning.connectors import TorchConnector
19
+ import warnings
20
+ warnings.filterwarnings("ignore")
21
+
22
+ # Critical for reproducibility and stability
23
+ torch.manual_seed(42)
24
+ np.random.seed(42)
25
+ print("All libraries imported. Running in headless mode.")
26
+
27
+ # --- Create plots directory ---
28
+ if not os.path.exists('plots_benchmark'):
29
+ os.makedirs('plots_benchmark')
30
+ print("Plots will be saved to 'plots_benchmark/' directory.")
31
+
32
+ # --- MOST REALISTIC SYNTHETIC TOKAMAK DATA (2025 physics-faithful) ---
33
+ np.random.seed(42)
34
+ n_features = 50
35
+ n_healthy = 8000
36
+ n_disruptive = 600
37
+
38
+ mean_healthy = np.zeros(n_features)
39
+ cov_healthy = 0.12 * np.eye(n_features)
40
+ cov_healthy += 0.04 * np.ones((n_features, n_features))
41
+ X_healthy = np.random.multivariate_normal(mean_healthy, cov_healthy, n_healthy)
42
+
43
+ X_disrupt = []
44
+ n_per_mode = 100
45
+ base = X_healthy[:n_per_mode].copy()
46
+
47
+ mode1 = base.copy(); mode1[:, 0:6] += np.random.uniform(2.8, 4.5, (n_per_mode, 6)); mode1[:, 20:28] += np.random.uniform(2.0, 3.8, (n_per_mode, 8))
48
+ mode2 = base.copy(); mode2[:, 8:18] += np.random.normal(3.5, 1.2, (n_per_mode, 10))
49
+ mode3 = base.copy(); mode3[:, 30:36] -= np.random.uniform(3.0, 5.5, (n_per_mode, 6))
50
+ mode4 = base.copy(); mode4[:, 15:25] += 3.2; mode4[:, 35:45] *= 0.25
51
+ mode5 = base.copy(); mode5[:, 6:12] += np.random.uniform(-4.0, 4.0, (n_per_mode, 6))
52
+ mode6 = base.copy(); mode6[:, 25:35] += np.random.uniform(3.0, 5.0, (n_per_mode, 10))
53
+
54
+ X_disrupt = np.vstack([mode1, mode2, mode3, mode4, mode5, mode6])
55
+ X_disrupt += np.random.normal(0, 0.25, X_disrupt.shape)
56
+
57
+ healthy_df = pd.DataFrame(X_healthy, columns=[f'sensor_{i}' for i in range(n_features)])
58
+ healthy_df['is_disruptive'] = 0
59
+ disrupt_df = pd.DataFrame(X_disrupt, columns=[f'sensor_{i}' for i in range(n_features)])
60
+ disrupt_df['is_disruptive'] = 1
61
+
62
+ simulated_data = pd.concat([healthy_df, disrupt_df], ignore_index=True)
63
+ simulated_data = simulated_data.sample(frac=1, random_state=42).reset_index(drop=True)
64
+ print(f"Generated ultra-realistic synthetic tokamak data: {n_healthy} healthy + {n_disruptive} disruptive")
65
+
66
+ # --- DATA PIPELINE ---
67
+ X_train_healthy = simulated_data[simulated_data['is_disruptive'] == 0].drop('is_disruptive', axis=1)
68
+ X_test_anomalous = simulated_data[simulated_data['is_disruptive'] == 1].drop('is_disruptive', axis=1)
69
+ features_to_use = X_train_healthy.columns[:20]
70
+ X_train_healthy = X_train_healthy[features_to_use]
71
+ X_test_anomalous = X_test_anomalous[features_to_use]
72
+
73
+ scaler = StandardScaler()
74
+ X_scaled = scaler.fit_transform(X_train_healthy)
75
+ pca = PCA(n_components=2)
76
+ X_pca = pca.fit_transform(X_scaled)
77
+
78
+ bins_per_dim = 4
79
+ real_distribution_hist, x_edges, y_edges = np.histogram2d(X_pca[:, 0], X_pca[:, 1], bins=bins_per_dim, density=True)
80
+ real_distribution = real_distribution_hist.flatten()
81
+ real_distribution = real_distribution / (np.sum(real_distribution) + 1e-12)
82
+ x_centers = (x_edges[:-1] + x_edges[1:]) / 2
83
+ y_centers = (y_edges[:-1] + y_edges[1:]) / 2
84
+ grid_samples = np.array(np.meshgrid(x_centers, y_centers)).T.reshape(-1, 2)
85
+ print("Data pipeline complete.")
86
+
87
+ # --- TENSORS ---
88
+ real_data_dist = torch.tensor(real_distribution, dtype=torch.float32).reshape(-1, 1)
89
+ data_samples = torch.tensor(grid_samples, dtype=torch.float32)
90
+ valid = torch.ones(real_data_dist.shape, dtype=torch.float32)
91
+ fake = torch.zeros(real_data_dist.shape, dtype=torch.float32)
92
+
93
+ # --- LOSS ---
94
+ def adversarial_loss(input_scores, target_labels, weights):
95
+ bce = target_labels * torch.log(input_scores + 1e-12) + (1 - target_labels) * torch.log(1 - input_scores + 1e-12)
96
+ return -torch.sum(weights * bce)
97
+
98
+ # --- ANOMALY SCORER ---
99
+ def detect_anomaly(reading, scaler, pca, model):
100
+ scaled = scaler.transform(reading.reshape(1, -1))
101
+ pcaed = pca.transform(scaled)
102
+ tensor = torch.tensor(pcaed, dtype=torch.float32)
103
+ model.eval()
104
+ with torch.no_grad():
105
+ return model(tensor).item()
106
+
107
+ # --- MAIN TRIAL LOOP ---
108
+ N_TRIALS = 20
109
+ num_epochs = 800
110
+ qgan_auc_scores = []
111
+
112
+ for trial in range(N_TRIALS):
113
+ print(f"\n--- QGAN TRIAL {trial+1}/{N_TRIALS} (Upgraded 2025 NISQ Best Practices) ---")
114
+
115
+ # === UPGRADED QUANTUM GENERATOR (This is the winner) ===
116
+ num_qubits = 4
117
+ feature_map = ZZFeatureMap(feature_dimension=2, reps=2, entanglement='linear')
118
+ ansatz = RealAmplitudes(num_qubits, reps=10, entanglement='full')
119
+
120
+ qc = QuantumCircuit(num_qubits)
121
+ qc.compose(feature_map, inplace=True)
122
+ qc.compose(ansatz, inplace=True)
123
+
124
+ sampler = Sampler()
125
+ qnn = SamplerQNN(
126
+ circuit=qc,
127
+ sampler=sampler,
128
+ input_params=feature_map.parameters,
129
+ weight_params=ansatz.parameters,
130
+ interpret=lambda x: x % 16,
131
+ output_shape=16
132
+ )
133
+
134
+ # Small initial weights = stable training
135
+ initial_weights = 0.05 * (2 * torch.rand(qnn.num_weights, requires_grad=True) - 1)
136
+ generator = TorchConnector(qnn, initial_weights=initial_weights)
137
+
138
+ # === STRONGER, MORE STABLE DISCRIMINATOR ===
139
+ class Discriminator(nn.Module):
140
+ def __init__(self, input_size=2):
141
+ super().__init__()
142
+ self.net = nn.Sequential(
143
+ nn.Linear(input_size, 64),
144
+ nn.LayerNorm(64),
145
+ nn.LeakyReLU(0.2),
146
+ nn.Dropout(0.3),
147
+ nn.Linear(64, 32),
148
+ nn.LayerNorm(32),
149
+ nn.LeakyReLU(0.2),
150
+ nn.Linear(32, 1),
151
+ nn.Sigmoid()
152
+ )
153
+ def forward(self, x):
154
+ return self.net(x)
155
+ discriminator = Discriminator()
156
+
157
+ # === OPTIMIZED LEARNING RATES (No collapse) ===
158
+ generator_optimizer = Adam(generator.parameters(), lr=0.004, betas=(0.5, 0.9))
159
+ discriminator_optimizer = Adam(discriminator.parameters(), lr=0.006, betas=(0.5, 0.9))
160
+
161
+ # === FINAL STABLE CONTINUOUS-DENSITY QGAN (NO MORE COLLAPSE) ===
162
+ from sklearn.neighbors import KernelDensity
163
+
164
+ # Fit a smooth continuous density on real healthy data — this is our true target
165
+ kde = KernelDensity(bandwidth=0.35, kernel='gaussian', rtol=1e-4)
166
+ kde.fit(X_pca) # use ALL healthy points for best density estimate
167
+
168
+ def get_density(samples):
169
+ return np.exp(kde.score_samples(samples))
170
+
171
+ # Pre-compute true density on the 16 evaluation points
172
+ true_density = torch.tensor(get_density(grid_samples), dtype=torch.float32).reshape(-1, 1)
173
+ true_density = true_density / (true_density.sum() + 1e-12)
174
+
175
+ print(f"Starting STABLE continuous-density training ({num_epochs} epochs)...")
176
+ for epoch in range(num_epochs):
177
+ # --- Discriminator training
178
+ discriminator_optimizer.zero_grad()
179
+ real_scores = discriminator(data_samples) # (16,1)
180
+ fake_data = generator(data_samples) # (16,16) → probabilities
181
+ fake_scores = discriminator(data_samples)
182
+
183
+ # Standard non-saturating GAN loss + small density bonus
184
+ d_loss_real = -torch.log(real_scores + 1e-12).mean()
185
+ d_loss_fake = -torch.log(1 - fake_scores + 1e-12).mean()
186
+ d_loss = d_loss_real + d_loss_fake
187
+ d_loss.backward()
188
+ discriminator_optimizer.step()
189
+
190
+ # Generator training — wants fake data in high-density regions
191
+ generator_optimizer.zero_grad()
192
+ fake_data = generator(data_samples)
193
+ fake_scores = discriminator(data_samples)
194
+ # Main GAN loss + tiny density reward to prevent collapse
195
+ g_gan_loss = -fake_scores.mean()
196
+ density_bonus = (fake_data * true_density).sum(dim=1).mean() * 0.05
197
+ g_loss = g_gan_loss - density_bonus
198
+ g_loss.backward()
199
+ generator_optimizer.step()
200
+
201
+ if (epoch + 1) % 200 == 0:
202
+ print(f" Epoch {epoch+1:3d} | D: {d_loss.item():.3f} | G: {g_loss.item():.3f}")
203
+
204
+ # === FINAL VALIDATION (continuous density + discriminator) ===
205
+ healthy_scores = []
206
+ for i in range(1000):
207
+ raw = X_train_healthy.iloc[i].values.reshape(1, -1)
208
+ pca_pt = pca.transform(scaler.transform(raw))
209
+ disc_score = detect_anomaly(X_train_healthy.iloc[i].values, scaler, pca, discriminator)
210
+ density = get_density(pca_pt)[0]
211
+ combined = disc_score * (density ** 0.15) # soft density weighting
212
+ healthy_scores.append(combined)
213
+
214
+ anomalous_scores = []
215
+ for i in range(len(X_test_anomalous)):
216
+ raw = X_test_anomalous.iloc[i].values.reshape(1, -1)
217
+ pca_pt = pca.transform(scaler.transform(raw))
218
+ disc_score = detect_anomaly(X_test_anomalous.iloc[i].values, scaler, pca, discriminator)
219
+ density = get_density(pca_pt)[0]
220
+ combined = disc_score * (density ** 0.15)
221
+ anomalous_scores.append(combined)
222
+
223
+ # ROC
224
+ y_true = np.concatenate([np.zeros(1000), np.ones(len(anomalous_scores))])
225
+ y_pred = np.concatenate([healthy_scores, anomalous_scores])
226
+ fpr, tpr, _ = roc_curve(y_true, y_pred)
227
+ roc_auc = auc(fpr, tpr)
228
+ qgan_auc_scores.append(roc_auc)
229
+ print(f"Trial {trial+1}/20 finished — AUC = {roc_auc:.4f}")
230
+
231
+ # Save ROC
232
+ plt.figure(figsize=(7,6))
233
+ plt.plot(fpr, tpr, label=f'AUC = {roc_auc:.4f}')
234
+ plt.plot([0,1],[0,1],'k--')
235
+ plt.title(f'QGAN Stable ROC - Trial {trial+1}')
236
+ plt.legend(); plt.grid(alpha=0.3)
237
+ plt.savefig(f'plots_benchmark/QGAN_STABLE_Trial_{trial+1}_ROC.png', dpi=150)
238
+ plt.close()
239
+ qgan_auc_scores.append(roc_auc)
240
+ print(f"Trial {trial+1} AUC: {roc_auc:.4f}")
241
+
242
+
243
+
244
+ # === FINAL RESULTS ===
245
+ print("\n" + "="*50)
246
+ print("FINAL UPGRADED QGAN RESULTS (2025 Best Practices)")
247
+ print(f"Average AUC: {np.mean(qgan_auc_scores):.4f} ± {np.std(qgan_auc_scores):.4f}")
248
+ print(f"Best AUC: {np.max(qgan_auc_scores):.4f}")
249
+ print("="*50)
Phase 1/Bridge-island_visual.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from matplotlib.patches import Ellipse, FancyBboxPatch
4
+
5
+ # --- CONFIGURATION ---
6
+ np.random.seed(42)
7
+ n_points = 300
8
+
9
+ # --- GENERATE "ISLAND" DATA (Schematic) ---
10
+ # Mode 1: Ramp Up (Bottom Left)
11
+ x1 = np.random.normal(-2, 0.4, n_points)
12
+ y1 = np.random.normal(-2, 0.4, n_points)
13
+
14
+ # Mode 2: Flat Top (Center)
15
+ x2 = np.random.normal(0, 0.3, n_points)
16
+ y2 = np.random.normal(0, 0.3, n_points)
17
+
18
+ # Mode 3: Ramp Down (Top Right)
19
+ x3 = np.random.normal(2, 0.4, n_points)
20
+ y3 = np.random.normal(2, 0.4, n_points)
21
+
22
+ # Anomalies (The Trap - In the gaps)
23
+ xa = np.random.normal(-1, 0.15, 50)
24
+ ya = np.random.normal(-1, 0.15, 50)
25
+
26
+ xb = np.random.normal(1.2, 0.15, 50)
27
+ yb = np.random.normal(1.2, 0.15, 50)
28
+
29
+ # --- PLOTTING ---
30
+ fig, ax = plt.subplots(figsize=(12, 7))
31
+
32
+ # 1. Plot the "Healthy" Islands
33
+ ax.scatter(x1, y1, c='#1f77b4', alpha=0.6, s=20, label='Healthy Plasma (3 Modes)')
34
+ ax.scatter(x2, y2, c='#1f77b4', alpha=0.6, s=20)
35
+ ax.scatter(x3, y3, c='#1f77b4', alpha=0.6, s=20)
36
+
37
+ # 2. Plot the "Trap" Anomalies
38
+ ax.scatter(xa, ya, c='red', marker='x', s=60, linewidth=2, label='Anomalies (Hidden in Gaps)')
39
+ ax.scatter(xb, yb, c='red', marker='x', s=60, linewidth=2)
40
+
41
+ # 3. DRAW THE "CLASSICAL BRIDGE" (The Failure)
42
+ # A large, smooth ellipse that covers everything, including the traps
43
+ classical_boundary = Ellipse((0, 0), width=7, height=7, angle=45,
44
+ edgecolor='red', linestyle='--', linewidth=3,
45
+ facecolor='red', alpha=0.1, label='Classical AI "Bridge" (Lazy)')
46
+ ax.add_patch(classical_boundary)
47
+
48
+ # 4. DRAW THE "QUANTUM TOPOLOGY" (The Success)
49
+ # Tight circles around the modes
50
+ q1 = Ellipse((-2, -2), width=2.5, height=2.5, angle=0, edgecolor='green', linewidth=3, facecolor='none')
51
+ q2 = Ellipse((0, 0), width=2.0, height=2.0, angle=0, edgecolor='green', linewidth=3, facecolor='none')
52
+ q3 = Ellipse((2, 2), width=2.5, height=2.5, angle=0, edgecolor='green', linewidth=3, facecolor='none')
53
+
54
+ ax.add_patch(q1)
55
+ ax.add_patch(q2)
56
+ ax.add_patch(q3)
57
+ # Ghost patch for legend
58
+ ax.plot([], [], color='green', linewidth=3, label='Quantum AI "Topology" (Correct)')
59
+
60
+ # --- ANNOTATIONS ---
61
+ ax.text(-1.1, -0.8, "THE TRAP", color='red', fontsize=12, fontweight='bold')
62
+ ax.text(1.3, 1.4, "THE TRAP", color='red', fontsize=12, fontweight='bold')
63
+ ax.text(-3.5, 1.5, "Classical AI bridges the gap\nand calls anomalies 'Safe'",
64
+ color='red', fontsize=11, bbox=dict(facecolor='white', alpha=0.8))
65
+ ax.text(1.5, -2.5, "Quantum AI respects\nthe separation",
66
+ color='green', fontsize=11, bbox=dict(facecolor='white', alpha=0.8))
67
+
68
+ # Style
69
+ ax.set_title("Why Classical AI Fails Fusion: The 'Bridge' Problem", fontsize=16, fontweight='bold')
70
+ ax.legend(loc='upper left', fontsize=10)
71
+ ax.set_xticks([])
72
+ ax.set_yticks([])
73
+ ax.grid(True, alpha=0.2)
74
+
75
+ # Save
76
+ plt.tight_layout()
77
+ plt.savefig('slide_visual_the_trap.png', dpi=300)
78
+ plt.close()
79
+ print("Visual generated: slide_visual_the_trap.png")
Phase 1/V2.1_QGAN_HailMary.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """benchmark_qgan_v2.py
3
+
4
+ The "Hail Mary" Configuration (V2.1)
5
+ - Deep Quantum Circuit (reps=6)
6
+ - High Generator LR (0.05) / Low Discriminator LR (0.001)
7
+ - 700 Epochs
8
+ """
9
+
10
+ # --- STEP 0: IMPORTS & HEADLESS MODE ---
11
+ import matplotlib
12
+ matplotlib.use('Agg') # No pop-ups
13
+ import numpy as np
14
+ import pandas as pd
15
+ import matplotlib.pyplot as plt
16
+ import os
17
+ from sklearn.preprocessing import StandardScaler
18
+ from sklearn.decomposition import PCA
19
+ from sklearn.metrics import roc_curve, auc
20
+ import torch
21
+ import torch.nn as nn
22
+ from torch.optim import Adam
23
+
24
+ # Qiskit Imports
25
+ from qiskit import QuantumCircuit
26
+ from qiskit.circuit.library import EfficientSU2
27
+ from qiskit.primitives import Sampler
28
+ from qiskit_machine_learning.neural_networks import SamplerQNN
29
+ from qiskit_machine_learning.connectors import TorchConnector
30
+
31
+ print("All libraries imported. Running 'Hail Mary' QGAN V2.1")
32
+
33
+ # --- SETUP DIRECTORIES ---
34
+ if not os.path.exists('plots_v2_qgan'):
35
+ os.makedirs('plots_v2_qgan')
36
+ print("Plots will be saved to 'plots_v2_qgan/' directory.")
37
+
38
+ # --- STEP 1: LOAD THE V2 PHYSICS DATA ---
39
+ print("\nLoading 'complex_tokamak_data.csv'...")
40
+ try:
41
+ df_total = pd.read_csv('complex_tokamak_data.csv')
42
+ except FileNotFoundError:
43
+ print("ERROR: 'complex_tokamak_data.csv' not found. Run physics_data.py first!")
44
+ exit()
45
+
46
+ print(f"Data Loaded. Shape: {df_total.shape}")
47
+
48
+ # --- STEP 2: PRE-PROCESSING (THE PIPELINE) ---
49
+ print("Running Data Pipeline...")
50
+
51
+ # Split Healthy vs Anomalous
52
+ X_train_healthy = df_total[df_total['label'] == 0].drop('label', axis=1)
53
+ X_test_anomalous = df_total[df_total['label'] == 1].drop('label', axis=1)
54
+
55
+ # 1. Scale (Standardize)
56
+ scaler = StandardScaler()
57
+ X_scaled = scaler.fit_transform(X_train_healthy)
58
+
59
+ # 2. PCA (Compress to 2 Latent Dimensions)
60
+ N_DIM = 2
61
+ pca = PCA(n_components=N_DIM)
62
+ X_pca = pca.fit_transform(X_scaled)
63
+
64
+ # 3. Discretize (Map to 4x4 Grid = 16 bins)
65
+ bins_per_dim = 4
66
+ num_qubits = 4 # 2^4 = 16
67
+
68
+ real_distribution_hist, x_edges, y_edges = np.histogram2d(
69
+ X_pca[:, 0],
70
+ X_pca[:, 1],
71
+ bins=bins_per_dim,
72
+ density=True
73
+ )
74
+
75
+ # Flatten grid to probability vector
76
+ real_distribution = real_distribution_hist.flatten()
77
+ real_distribution = real_distribution / np.sum(real_distribution) # Normalize
78
+
79
+ # Get grid centers for the Discriminator
80
+ x_centers = (x_edges[:-1] + x_edges[1:]) / 2
81
+ y_centers = (y_edges[:-1] + y_edges[1:]) / 2
82
+ grid_samples = np.array(np.meshgrid(x_centers, y_centers)).T.reshape(-1, N_DIM)
83
+
84
+ print("Target distribution created.")
85
+
86
+ # Convert to PyTorch Tensors
87
+ real_data_dist = torch.tensor(real_distribution, dtype=torch.float32).reshape(-1, 1)
88
+ data_samples = torch.tensor(grid_samples, dtype=torch.float32)
89
+ valid = torch.ones(real_data_dist.shape, dtype=torch.float32)
90
+ fake = torch.zeros(real_data_dist.shape, dtype=torch.float32)
91
+
92
+ # --- HELPER FUNCTIONS ---
93
+ def adversarial_loss(input, target, w):
94
+ bce_loss = target * torch.log(input) + (1 - target) * torch.log(1 - input)
95
+ weighted_loss = w * bce_loss
96
+ total_loss = -torch.sum(weighted_loss)
97
+ return total_loss
98
+
99
+ def detect_anomaly(new_sensor_reading, scaler_obj, pca_obj, discriminator_model):
100
+ x_scaled = scaler_obj.transform(new_sensor_reading.reshape(1, -1))
101
+ x_pca = pca_obj.transform(x_scaled)
102
+ x_tensor = torch.tensor(x_pca, dtype=torch.float32)
103
+ discriminator_model.eval()
104
+ with torch.no_grad():
105
+ score = discriminator_model(x_tensor)
106
+ return score.item()
107
+
108
+ # --- STEP 3: THE BENCHMARK LOOP (HAIL MARY SETTINGS) ---
109
+ N_TRIALS = 20
110
+ num_epochs = 700 # INCREASED: Give it more time to converge
111
+ qgan_auc_scores = []
112
+
113
+ print(f"\nStarting {N_TRIALS} Trials of QGAN-AD (Hail Mary Config)...")
114
+
115
+ for trial in range(N_TRIALS):
116
+ print(f"\n--- TRIAL {trial+1}/{N_TRIALS} ---")
117
+
118
+ # A. BUILD GENERATOR (Quantum)
119
+ qc = QuantumCircuit(num_qubits)
120
+ qc.h(qc.qubits)
121
+
122
+ # HAIL MARY TWEAK 1: Deeper Circuit
123
+ # reps=6 provides more parameters to fit the 3 separate islands
124
+ ansatz = EfficientSU2(num_qubits, reps=6)
125
+ qc.compose(ansatz, inplace=True)
126
+
127
+ sampler = Sampler()
128
+ qnn = SamplerQNN(
129
+ circuit=qc,
130
+ sampler=sampler,
131
+ input_params=[],
132
+ weight_params=qc.parameters
133
+ )
134
+
135
+ initial_weights = 0.1 * (2 * torch.rand(qnn.num_weights) - 1)
136
+ generator = TorchConnector(qnn, initial_weights=initial_weights)
137
+
138
+ # B. BUILD DISCRIMINATOR (Classical)
139
+ class Discriminator(nn.Module):
140
+ def __init__(self, input_size):
141
+ super(Discriminator, self).__init__()
142
+ self.model = nn.Sequential(
143
+ nn.Linear(input_size, 32),
144
+ nn.LeakyReLU(0.2),
145
+ nn.Linear(32, 16),
146
+ nn.LeakyReLU(0.2),
147
+ nn.Linear(16, 1),
148
+ nn.Sigmoid()
149
+ )
150
+ def forward(self, x):
151
+ return self.model(x)
152
+
153
+ discriminator = Discriminator(input_size=N_DIM)
154
+
155
+ # C. OPTIMIZERS (HAIL MARY TWEAK 2)
156
+ # Generator: 0.05 (Very Fast - Needs to learn complex shapes quickly)
157
+ # Discriminator: 0.001 (Sedated - Prevents it from killing G too early)
158
+ lr_g = 0.05
159
+ lr_d = 0.001
160
+
161
+ # Added betas to help momentum
162
+ opt_g = Adam(generator.parameters(), lr=lr_g, betas=(0.7, 0.999))
163
+ opt_d = Adam(discriminator.parameters(), lr=lr_d, betas=(0.7, 0.999))
164
+
165
+ # D. TRAINING
166
+ g_losses = []
167
+ d_losses = []
168
+
169
+ for epoch in range(num_epochs):
170
+ # Train D
171
+ opt_d.zero_grad()
172
+ disc_real = discriminator(data_samples)
173
+ gen_dist = generator(torch.tensor([])).reshape(-1, 1)
174
+ loss_d_real = adversarial_loss(disc_real, valid, real_data_dist)
175
+ loss_d_fake = adversarial_loss(disc_real, fake, gen_dist.detach())
176
+ loss_d = (loss_d_real + loss_d_fake) / 2
177
+ loss_d.backward()
178
+ opt_d.step()
179
+
180
+ # Train G
181
+ opt_g.zero_grad()
182
+ gen_dist = generator(torch.tensor([])).reshape(-1, 1)
183
+ disc_fake = discriminator(data_samples)
184
+ loss_g = adversarial_loss(disc_fake, valid, gen_dist)
185
+ loss_g.backward()
186
+ opt_g.step()
187
+
188
+ if epoch % 100 == 0:
189
+ g_losses.append(loss_g.item())
190
+ d_losses.append(loss_d.item())
191
+
192
+ # E. VALIDATION
193
+ healthy_subset = X_train_healthy.sample(n=500)
194
+ y_true = []
195
+ y_scores = []
196
+
197
+ for i in range(len(healthy_subset)):
198
+ sample = healthy_subset.iloc[i].values
199
+ score = detect_anomaly(sample, scaler, pca, discriminator)
200
+ y_true.append(0)
201
+ y_scores.append(score)
202
+
203
+ for i in range(len(X_test_anomalous)):
204
+ sample = X_test_anomalous.iloc[i].values
205
+ score = detect_anomaly(sample, scaler, pca, discriminator)
206
+ y_true.append(1)
207
+ y_scores.append(score)
208
+
209
+ # F. METRICS & PLOTS
210
+ fpr, tpr, _ = roc_curve(y_true, 1 - np.array(y_scores))
211
+ roc_auc = auc(fpr, tpr)
212
+ qgan_auc_scores.append(roc_auc)
213
+
214
+ print(f"Trial {trial+1} Complete. AUC: {roc_auc:.4f}")
215
+
216
+ # Save ROC Plot
217
+ plt.figure(figsize=(6,5))
218
+ plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'AUC={roc_auc:.2f}')
219
+ plt.plot([0, 1], [0, 1], color='navy', linestyle='--')
220
+ plt.title(f"QGAN V2.1 (Hail Mary) Trial {trial+1}")
221
+ plt.legend(loc="lower right")
222
+ plt.savefig(f'plots_v2_qgan/roc_trial_{trial+1}.png')
223
+ plt.close()
224
+
225
+ # --- FINAL SUMMARY ---
226
+ print("\n" + "="*30)
227
+ print("--- QGAN V2.1 (HAIL MARY) RESULTS ---")
228
+ print(f"Mean AUC: {np.mean(qgan_auc_scores):.4f}")
229
+ print(f"Max AUC: {np.max(qgan_auc_scores):.4f}")
230
+ print(f"Std Dev: {np.std(qgan_auc_scores):.4f}")
231
+ print("="*30)
Phase 1/V2_benchmark_cgan ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """benchmark_cgan_v2.py
3
+ The CLASSICAL Control Experiment.
4
+ """
5
+
6
+ import matplotlib
7
+ matplotlib.use('Agg') # Headless mode
8
+ import numpy as np
9
+ import pandas as pd
10
+ import matplotlib.pyplot as plt
11
+ import os
12
+ from sklearn.preprocessing import StandardScaler
13
+ from sklearn.decomposition import PCA
14
+ from sklearn.metrics import roc_curve, auc
15
+ import torch
16
+ import torch.nn as nn
17
+ from torch.optim import Adam
18
+
19
+ print("Running CLASSICAL Benchmark V2 (Headless).")
20
+
21
+ # --- SETUP DIRECTORIES ---
22
+ if not os.path.exists('plots_v2_cgan'):
23
+ os.makedirs('plots_v2_cgan')
24
+ print("Plots will be saved to 'plots_v2_cgan/' directory.")
25
+
26
+ # --- STEP 1: LOAD THE SAME V2 DATA ---
27
+ # Crucial: We must use the exact same CSV
28
+ try:
29
+ df_total = pd.read_csv('complex_tokamak_data.csv')
30
+ except FileNotFoundError:
31
+ print("ERROR: 'complex_tokamak_data.csv' not found.")
32
+ exit()
33
+
34
+ # --- STEP 2: EXACT SAME PIPELINE ---
35
+ X_train_healthy = df_total[df_total['label'] == 0].drop('label', axis=1)
36
+ X_test_anomalous = df_total[df_total['label'] == 1].drop('label', axis=1)
37
+
38
+ scaler = StandardScaler()
39
+ X_scaled = scaler.fit_transform(X_train_healthy)
40
+
41
+ N_DIM = 2
42
+ pca = PCA(n_components=N_DIM)
43
+ X_pca = pca.fit_transform(X_scaled)
44
+
45
+ bins_per_dim = 4
46
+ num_qubits = 4
47
+
48
+ # Create Target Distribution
49
+ real_distribution_hist, x_edges, y_edges = np.histogram2d(
50
+ X_pca[:, 0], X_pca[:, 1], bins=bins_per_dim, density=True
51
+ )
52
+ real_distribution = real_distribution_hist.flatten()
53
+ real_distribution = real_distribution / np.sum(real_distribution)
54
+
55
+ # Grid Centers
56
+ x_centers = (x_edges[:-1] + x_edges[1:]) / 2
57
+ y_centers = (y_edges[:-1] + y_edges[1:]) / 2
58
+ grid_samples = np.array(np.meshgrid(x_centers, y_centers)).T.reshape(-1, N_DIM)
59
+
60
+ # Tensors
61
+ real_data_dist = torch.tensor(real_distribution, dtype=torch.float32).reshape(-1, 1)
62
+ data_samples = torch.tensor(grid_samples, dtype=torch.float32)
63
+ valid = torch.ones(real_data_dist.shape, dtype=torch.float32)
64
+ fake = torch.zeros(real_data_dist.shape, dtype=torch.float32)
65
+
66
+ # --- HELPER FUNCTIONS ---
67
+ def adversarial_loss(input, target, w):
68
+ bce_loss = target * torch.log(input) + (1 - target) * torch.log(1 - input)
69
+ weighted_loss = w * bce_loss
70
+ total_loss = -torch.sum(weighted_loss)
71
+ return total_loss
72
+
73
+ def detect_anomaly(new_sensor_reading, scaler_obj, pca_obj, discriminator_model):
74
+ x_scaled = scaler_obj.transform(new_sensor_reading.reshape(1, -1))
75
+ x_pca = pca_obj.transform(x_scaled)
76
+ x_tensor = torch.tensor(x_pca, dtype=torch.float32)
77
+ discriminator_model.eval()
78
+ with torch.no_grad():
79
+ score = discriminator_model(x_tensor)
80
+ return score.item()
81
+
82
+ # --- STEP 3: THE BENCHMARK LOOP ---
83
+ N_TRIALS = 20
84
+ num_epochs = 400
85
+ cgan_auc_scores = []
86
+
87
+ print(f"\nStarting {N_TRIALS} Trials of C-GAN...")
88
+
89
+ for trial in range(N_TRIALS):
90
+ print(f"\n--- TRIAL {trial+1}/{N_TRIALS} ---")
91
+
92
+ # A. CLASSICAL GENERATOR
93
+ # This mimics the QGAN architecture (learning a distribution)
94
+ # but uses standard weights instead of quantum rotation angles.
95
+ class ClassicalGenerator(nn.Module):
96
+ def __init__(self, output_size=16):
97
+ super(ClassicalGenerator, self).__init__()
98
+ # 16 raw numbers (logits) that we learn
99
+ self.logits = nn.Parameter(torch.randn(1, output_size))
100
+
101
+ def forward(self):
102
+ # Softmax converts logits to a probability distribution (0.0 to 1.0)
103
+ return torch.softmax(self.logits, dim=1)
104
+
105
+ generator = ClassicalGenerator(output_size=16)
106
+
107
+ # B. DISCRIMINATOR (Identical to QGAN)
108
+ class Discriminator(nn.Module):
109
+ def __init__(self, input_size):
110
+ super(Discriminator, self).__init__()
111
+ self.model = nn.Sequential(
112
+ nn.Linear(input_size, 32),
113
+ nn.LeakyReLU(0.2),
114
+ nn.Linear(32, 16),
115
+ nn.LeakyReLU(0.2),
116
+ nn.Linear(16, 1),
117
+ nn.Sigmoid()
118
+ )
119
+ def forward(self, x):
120
+ return self.model(x)
121
+
122
+ discriminator = Discriminator(input_size=N_DIM)
123
+
124
+ # C. OPTIMIZERS
125
+ # Classical usually needs lower LR to avoid "overshooting" the solution
126
+ opt_g = Adam(generator.parameters(), lr=0.01)
127
+ opt_d = Adam(discriminator.parameters(), lr=0.01)
128
+
129
+ # D. TRAINING
130
+ for epoch in range(num_epochs):
131
+ # Train D
132
+ opt_d.zero_grad()
133
+ disc_real = discriminator(data_samples)
134
+ gen_dist = generator().reshape(-1, 1)
135
+ loss_d_real = adversarial_loss(disc_real, valid, real_data_dist)
136
+ loss_d_fake = adversarial_loss(disc_real, fake, gen_dist.detach())
137
+ loss_d = (loss_d_real + loss_d_fake) / 2
138
+ loss_d.backward()
139
+ opt_d.step()
140
+
141
+ # Train G
142
+ opt_g.zero_grad()
143
+ gen_dist = generator().reshape(-1, 1)
144
+ disc_fake = discriminator(data_samples)
145
+ loss_g = adversarial_loss(disc_fake, valid, gen_dist)
146
+ loss_g.backward()
147
+ opt_g.step()
148
+
149
+ # E. VALIDATION
150
+ healthy_subset = X_train_healthy.sample(n=500)
151
+ y_true = []
152
+ y_scores = []
153
+
154
+ for i in range(len(healthy_subset)):
155
+ sample = healthy_subset.iloc[i].values
156
+ score = detect_anomaly(sample, scaler, pca, discriminator)
157
+ y_true.append(0)
158
+ y_scores.append(score)
159
+
160
+ for i in range(len(X_test_anomalous)):
161
+ sample = X_test_anomalous.iloc[i].values
162
+ score = detect_anomaly(sample, scaler, pca, discriminator)
163
+ y_true.append(1)
164
+ y_scores.append(score)
165
+
166
+ # F. METRICS
167
+ fpr, tpr, _ = roc_curve(y_true, 1 - np.array(y_scores))
168
+ roc_auc = auc(fpr, tpr)
169
+ cgan_auc_scores.append(roc_auc)
170
+ print(f"Trial {trial+1} Complete. AUC: {roc_auc:.4f}")
171
+
172
+ # Save Plot
173
+ plt.figure(figsize=(6,5))
174
+ plt.plot(fpr, tpr, color='green', lw=2, label=f'AUC={roc_auc:.2f}')
175
+ plt.plot([0, 1], [0, 1], color='navy', linestyle='--')
176
+ plt.title(f"C-GAN V2 Trial {trial+1}")
177
+ plt.legend(loc="lower right")
178
+ plt.savefig(f'plots_v2_cgan/roc_trial_{trial+1}.png')
179
+ plt.close()
180
+
181
+ # --- FINAL SUMMARY ---
182
+ print("\n" + "="*30)
183
+ print("--- C-GAN V2 RESULTS ---")
184
+ print(f"Mean AUC: {np.mean(cgan_auc_scores):.4f}")
185
+ print(f"Max AUC: {np.max(cgan_auc_scores):.4f}")
186
+ print(f"Std Dev: {np.std(cgan_auc_scores):.4f}")
187
+ print("="*30)
Phase 1/V2_benchmark_qgan ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """benchmark_qgan_v2.py
3
+
4
+ The "gap test" benchmark for QGAN on physics-informed tokamak data.
5
+ """
6
+
7
+ # --- STEP 0: IMPORTS & HEADLESS MODE ---
8
+ import matplotlib
9
+ matplotlib.use('Agg') # No pop-ups
10
+ import numpy as np
11
+ import pandas as pd
12
+ import matplotlib.pyplot as plt
13
+ import os
14
+ from sklearn.preprocessing import StandardScaler
15
+ from sklearn.decomposition import PCA
16
+ from sklearn.metrics import roc_curve, auc
17
+ import torch
18
+ import torch.nn as nn
19
+ from torch.optim import Adam
20
+
21
+ # Qiskit Imports
22
+ from qiskit import QuantumCircuit
23
+ from qiskit.circuit.library import EfficientSU2
24
+ from qiskit.primitives import Sampler
25
+ from qiskit_machine_learning.neural_networks import SamplerQNN
26
+ from qiskit_machine_learning.connectors import TorchConnector
27
+
28
+ print("All libraries imported. Running in Headless Mode.")
29
+
30
+ # --- SETUP DIRECTORIES ---
31
+ if not os.path.exists('plots_v2_qgan'):
32
+ os.makedirs('plots_v2_qgan')
33
+ print("Plots will be saved to 'plots_v2_qgan/' directory.")
34
+
35
+ # --- STEP 1: LOAD THE V2 PHYSICS DATA ---
36
+ print("\nLoading 'complex_tokamak_data.csv'...")
37
+ try:
38
+ df_total = pd.read_csv('complex_tokamak_data.csv')
39
+ except FileNotFoundError:
40
+ print("ERROR: 'complex_tokamak_data.csv' not found. Run physics_data.py first!")
41
+ exit()
42
+
43
+ print(f"Data Loaded. Shape: {df_total.shape}")
44
+
45
+ # --- STEP 2: PRE-PROCESSING (THE PIPELINE) ---
46
+ # We process the data ONCE before the loop to save time
47
+ print("Running Data Pipeline...")
48
+
49
+ # Split Healthy vs Anomalous
50
+ X_train_healthy = df_total[df_total['label'] == 0].drop('label', axis=1)
51
+ X_test_anomalous = df_total[df_total['label'] == 1].drop('label', axis=1)
52
+
53
+ # 1. Scale (Standardize)
54
+ scaler = StandardScaler()
55
+ X_scaled = scaler.fit_transform(X_train_healthy)
56
+
57
+ # 2. PCA (Compress to 2 Latent Dimensions)
58
+ N_DIM = 2
59
+ pca = PCA(n_components=N_DIM)
60
+ X_pca = pca.fit_transform(X_scaled)
61
+
62
+ # 3. Discretize (Map to 4x4 Grid = 16 bins)
63
+ bins_per_dim = 4
64
+ num_qubits = 4 # 2^4 = 16
65
+
66
+ real_distribution_hist, x_edges, y_edges = np.histogram2d(
67
+ X_pca[:, 0],
68
+ X_pca[:, 1],
69
+ bins=bins_per_dim,
70
+ density=True
71
+ )
72
+
73
+ # Flatten grid to probability vector
74
+ real_distribution = real_distribution_hist.flatten()
75
+ real_distribution = real_distribution / np.sum(real_distribution) # Normalize
76
+
77
+ # Get grid centers for the Discriminator
78
+ x_centers = (x_edges[:-1] + x_edges[1:]) / 2
79
+ y_centers = (y_edges[:-1] + y_edges[1:]) / 2
80
+ grid_samples = np.array(np.meshgrid(x_centers, y_centers)).T.reshape(-1, N_DIM)
81
+
82
+ print("Target distribution created (The 'Islands' map).")
83
+
84
+ # Convert to PyTorch Tensors
85
+ real_data_dist = torch.tensor(real_distribution, dtype=torch.float32).reshape(-1, 1)
86
+ data_samples = torch.tensor(grid_samples, dtype=torch.float32)
87
+ valid = torch.ones(real_data_dist.shape, dtype=torch.float32)
88
+ fake = torch.zeros(real_data_dist.shape, dtype=torch.float32)
89
+
90
+ # --- HELPER FUNCTIONS ---
91
+ def adversarial_loss(input, target, w):
92
+ # Weighted BCE Loss
93
+ bce_loss = target * torch.log(input) + (1 - target) * torch.log(1 - input)
94
+ weighted_loss = w * bce_loss
95
+ total_loss = -torch.sum(weighted_loss)
96
+ return total_loss
97
+
98
+ def detect_anomaly(new_sensor_reading, scaler_obj, pca_obj, discriminator_model):
99
+ # Pipeline for a single sample
100
+ x_scaled = scaler_obj.transform(new_sensor_reading.reshape(1, -1))
101
+ x_pca = pca_obj.transform(x_scaled)
102
+ x_tensor = torch.tensor(x_pca, dtype=torch.float32)
103
+ discriminator_model.eval()
104
+ with torch.no_grad():
105
+ score = discriminator_model(x_tensor)
106
+ return score.item()
107
+
108
+ # --- STEP 3: THE BENCHMARK LOOP ---
109
+ N_TRIALS = 20
110
+ num_epochs = 400 # Slightly reduced for speed, usually enough for convergence
111
+ qgan_auc_scores = []
112
+
113
+ print(f"\nStarting {N_TRIALS} Trials of QGAN-AD...")
114
+
115
+ for trial in range(N_TRIALS):
116
+ print(f"\n--- TRIAL {trial+1}/{N_TRIALS} ---")
117
+
118
+ # A. BUILD GENERATOR (Quantum)
119
+ # EfficientSU2 is good for "expressivity" (handling the islands)
120
+ qc = QuantumCircuit(num_qubits)
121
+ qc.h(qc.qubits)
122
+ ansatz = EfficientSU2(num_qubits, reps=4) # reps=4 is a good balance
123
+ qc.compose(ansatz, inplace=True)
124
+
125
+ sampler = Sampler()
126
+ qnn = SamplerQNN(
127
+ circuit=qc,
128
+ sampler=sampler,
129
+ input_params=[],
130
+ weight_params=qc.parameters
131
+ )
132
+
133
+ # Connect to PyTorch
134
+ initial_weights = 0.1 * (2 * torch.rand(qnn.num_weights) - 1)
135
+ generator = TorchConnector(qnn, initial_weights=initial_weights)
136
+
137
+ # B. BUILD DISCRIMINATOR (Classical)
138
+ class Discriminator(nn.Module):
139
+ def __init__(self, input_size):
140
+ super(Discriminator, self).__init__()
141
+ self.model = nn.Sequential(
142
+ nn.Linear(input_size, 32), # Slightly wider for complex data
143
+ nn.LeakyReLU(0.2),
144
+ nn.Linear(32, 16),
145
+ nn.LeakyReLU(0.2),
146
+ nn.Linear(16, 1),
147
+ nn.Sigmoid()
148
+ )
149
+ def forward(self, x):
150
+ return self.model(x)
151
+
152
+ discriminator = Discriminator(input_size=N_DIM)
153
+
154
+ # C. OPTIMIZERS
155
+ # Generator needs to be faster to learn the complex map
156
+ lr_g = 0.04
157
+ lr_d = 0.01
158
+ opt_g = Adam(generator.parameters(), lr=lr_g)
159
+ opt_d = Adam(discriminator.parameters(), lr=lr_d)
160
+
161
+ # D. TRAINING
162
+ g_losses = []
163
+ d_losses = []
164
+
165
+ for epoch in range(num_epochs):
166
+ # Train D
167
+ opt_d.zero_grad()
168
+ disc_real = discriminator(data_samples)
169
+ gen_dist = generator(torch.tensor([])).reshape(-1, 1)
170
+ loss_d_real = adversarial_loss(disc_real, valid, real_data_dist)
171
+ loss_d_fake = adversarial_loss(disc_real, fake, gen_dist.detach())
172
+ loss_d = (loss_d_real + loss_d_fake) / 2
173
+ loss_d.backward()
174
+ opt_d.step()
175
+
176
+ # Train G
177
+ opt_g.zero_grad()
178
+ gen_dist = generator(torch.tensor([])).reshape(-1, 1)
179
+ disc_fake = discriminator(data_samples)
180
+ loss_g = adversarial_loss(disc_fake, valid, gen_dist)
181
+ loss_g.backward()
182
+ opt_g.step()
183
+
184
+ if epoch % 100 == 0:
185
+ g_losses.append(loss_g.item())
186
+ d_losses.append(loss_d.item())
187
+
188
+ # E. VALIDATION (THE GAP TEST)
189
+ # We test on a subset of healthy and ALL anomalous
190
+ healthy_subset = X_train_healthy.sample(n=500)
191
+
192
+ y_true = []
193
+ y_scores = []
194
+
195
+ # Test Healthy
196
+ for i in range(len(healthy_subset)):
197
+ sample = healthy_subset.iloc[i].values
198
+ score = detect_anomaly(sample, scaler, pca, discriminator)
199
+ y_true.append(0) # 0 = Healthy
200
+ y_scores.append(score)
201
+
202
+ # Test Anomalous (The Trap)
203
+ for i in range(len(X_test_anomalous)):
204
+ sample = X_test_anomalous.iloc[i].values
205
+ score = detect_anomaly(sample, scaler, pca, discriminator)
206
+ y_true.append(1) # 1 = Anomalous
207
+ y_scores.append(score)
208
+
209
+ # F. METRICS & PLOTS
210
+ # Calculate AUC (Flip scores because 0.0 is anomalous)
211
+ fpr, tpr, _ = roc_curve(y_true, 1 - np.array(y_scores))
212
+ roc_auc = auc(fpr, tpr)
213
+ qgan_auc_scores.append(roc_auc)
214
+
215
+ print(f"Trial {trial+1} Complete. AUC: {roc_auc:.4f}")
216
+
217
+ # Save ROC Plot
218
+ plt.figure(figsize=(6,5))
219
+ plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'AUC={roc_auc:.2f}')
220
+ plt.plot([0, 1], [0, 1], color='navy', linestyle='--')
221
+ plt.title(f"QGAN V2 Trial {trial+1}")
222
+ plt.legend(loc="lower right")
223
+ plt.savefig(f'plots_v2_qgan/roc_trial_{trial+1}.png')
224
+ plt.close()
225
+
226
+ # --- FINAL SUMMARY ---
227
+ print("\n" + "="*30)
228
+ print("--- QGAN V2 BENCHMARK RESULTS ---")
229
+ print(f"Mean AUC: {np.mean(qgan_auc_scores):.4f}")
230
+ print(f"Max AUC: {np.max(qgan_auc_scores):.4f}")
231
+ print(f"Std Dev: {np.std(qgan_auc_scores):.4f}")
232
+ print("="*30)
Phase 1/complex_tokamak_data.csv ADDED
The diff for this file is too large to render. See raw diff
 
Phase 1/main_fusion_qgan.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- STEP 1: IMPORT ALL LIBRARIES ---
2
+ # [cite: 66-83]
3
+ import matplotlib
4
+ matplotlib.use('Agg') # <-- ADD THIS: Use "headless" backend
5
+ import numpy as np
6
+ import pandas as pd
7
+ import matplotlib.pyplot as plt
8
+ import os # <-- ADD THIS: To create plot directory
9
+ from sklearn.preprocessing import StandardScaler
10
+ from sklearn.decomposition import PCA
11
+ from sklearn.metrics import roc_curve, auc
12
+ import torch
13
+ import torch.nn as nn
14
+ from torch.optim import Adam
15
+ from qiskit import QuantumCircuit
16
+ from qiskit.circuit.library import EfficientSU2
17
+ from qiskit.primitives import Sampler
18
+ from qiskit_machine_learning.neural_networks import SamplerQNN
19
+ from qiskit_machine_learning.connectors import TorchConnector
20
+
21
+ print("All libraries imported successfully.")
22
+ # --- NEW: Check for plots directory ---
23
+ if not os.path.exists('plots'):
24
+ os.makedirs('plots')
25
+ print("Created 'plots/' directory.")
26
+ else:
27
+ print("Plots will be saved to 'plots/' directory.")
28
+
29
+
30
+
31
+ # --- STEP 2: CREATE SIMULATED TOKAMAK DATA ---
32
+ # [cite: 88-115]
33
+ def get_simulated_tokamak_data(n_features=50, healthy_samples=5000, anomalous_samples=500):
34
+ """
35
+ Generates a fake dataset of 'healthy' and 'anomalous' plasma.
36
+ """
37
+ print(f"Generating {healthy_samples} healthy and {anomalous_samples} anomalous data points...")
38
+
39
+ # 1. Create the 'healthy' data
40
+ healthy_mean = np.zeros(n_features)
41
+ healthy_cov = np.diag(np.ones(n_features))
42
+ X_healthy = np.random.multivariate_normal(healthy_mean, healthy_cov, healthy_samples)
43
+
44
+ # 2. Create the 'anomalous' (disruptive) data
45
+ anomalous_mean = np.ones(n_features) * 2.5
46
+ anomalous_cov = np.diag(np.ones(n_features) * 0.5) # Make it a tighter cluster
47
+ X_anomalous = np.random.multivariate_normal(anomalous_mean, anomalous_cov, anomalous_samples)
48
+
49
+ # 3. Combine them into a single DataFrame
50
+ df_healthy = pd.DataFrame(X_healthy, columns=[f'sensor_{i}' for i in range(n_features)])
51
+ df_healthy['label'] = 0 # 0 = Healthy
52
+
53
+ df_anomalous = pd.DataFrame(X_anomalous, columns=[f'sensor_{i}' for i in range(n_features)])
54
+ df_anomalous['label'] = 1 # 1 = Anomalous
55
+
56
+ df_total = pd.concat([df_healthy, df_anomalous], ignore_index=True)
57
+
58
+ print("Simulated data generation complete.")
59
+ return df_total
60
+
61
+ # --- Run the function to get our data ---
62
+ simulated_data = get_simulated_tokamak_data()
63
+
64
+ # --- Peek at the data ---
65
+ print("First 5 rows of simulated data:")
66
+ print(simulated_data.head())
67
+
68
+
69
+ # --- STEP 3: THE DATA PIPELINE ---
70
+ # [cite: 119-158]
71
+ print("\nStarting data pipeline...")
72
+
73
+ # 3a. CRITICAL DATA FILTERING
74
+ X_train_healthy = simulated_data[simulated_data['label'] == 0].drop('label', axis=1)
75
+ X_test_anomalous = simulated_data[simulated_data['label'] == 1].drop('label', axis=1)
76
+ print(f"Isolated {len(X_train_healthy)} 'healthy' samples for training.")
77
+ print(f"Isolated {len(X_test_anomalous)} 'anomalous' samples for final testing.")
78
+
79
+ # 3b. NORMALIZE THE DATA
80
+ scaler = StandardScaler()
81
+ X_scaled = scaler.fit_transform(X_train_healthy)
82
+ print("Data normalized.")
83
+
84
+ # 3c. DIMENSIONALITY REDUCTION (PCA)
85
+ N_DIM = 2 # 2 Dimensions
86
+ pca = PCA(n_components=N_DIM)
87
+ X_pca = pca.fit_transform(X_scaled)
88
+ print(f"Data compressed from 50 features to {N_DIM} features using PCA.")
89
+
90
+ # 3d. DISCRETIZE THE "HEALTHY" DISTRIBUTION
91
+ bins_per_dim = 4
92
+ num_qubits = 4
93
+
94
+ real_distribution_hist, x_edges, y_edges = np.histogram2d(
95
+ X_pca[:, 0],
96
+ X_pca[:, 1],
97
+ bins=bins_per_dim,
98
+ density=True
99
+ )
100
+
101
+ real_distribution = real_distribution_hist.flatten()
102
+ real_distribution = real_distribution / np.sum(real_distribution)
103
+
104
+ x_centers = (x_edges[:-1] + x_edges[1:]) / 2
105
+ y_centers = (y_edges[:-1] + y_edges[1:]) / 2
106
+ grid_samples = np.array(np.meshgrid(x_centers, y_centers)).T.reshape(-1, N_DIM)
107
+
108
+ print("Data pipeline complete. Target distribution (16 bins) created.")
109
+ print("Shape of target distribution:", real_distribution.shape)
110
+ print("Shape of grid samples (bin-centers):", grid_samples.shape)
111
+
112
+ # 5a. Custom Adversarial Loss Function
113
+ def adversarial_loss(input, target, w):
114
+ bce_loss = target * torch.log(input) + (1 - target) * torch.log(1 - input)
115
+ weighted_loss = w * bce_loss
116
+ total_loss = -torch.sum(weighted_loss)
117
+ return total_loss
118
+ # 5c. Convert Data to Tensors
119
+ real_data_dist = torch.tensor(real_distribution, dtype=torch.float32).reshape(-1, 1)
120
+ data_samples = torch.tensor(grid_samples, dtype=torch.float32)
121
+
122
+ valid = torch.ones(real_data_dist.shape, dtype=torch.float32)
123
+ fake = torch.zeros(real_data_dist.shape, dtype=torch.float32)
124
+
125
+ # 7a. Define the Anomaly Detector Function
126
+ def detect_anomaly(new_sensor_reading, scaler_obj, pca_obj, discriminator_model):
127
+ """
128
+ Takes a single, high-dimensional sensor reading and returns an
129
+ anomaly score. Score ~1.0 = Healthy. Score ~0.0 = Anomalous.
130
+ """
131
+ # 1. Reshape and Scale
132
+ x_scaled = scaler_obj.transform(new_sensor_reading.reshape(1, -1))
133
+ # 2. Compress with PCA
134
+ x_pca = pca_obj.transform(x_scaled) # Output is (1, 2)
135
+ # 3. Convert to PyTorch Tensor
136
+ x_tensor = torch.tensor(x_pca, dtype=torch.float32)
137
+ # 4. Feed to the *trained discriminator*
138
+ discriminator_model.eval() # Set to evaluation mode
139
+ with torch.no_grad():
140
+ anomaly_score = discriminator_model(x_tensor)
141
+ return anomaly_score.item()
142
+
143
+ print("Anomaly Detector function defined.")
144
+
145
+ N_TRIALS = 50
146
+ qgan_auc_scores = []
147
+
148
+
149
+ # --- START OF 50-TRIAL LOOP ---
150
+ for i in range(N_TRIALS):
151
+ print(f"\n--- QGAN TRIAL {i+1}/{N_TRIALS} ---")
152
+
153
+
154
+ # --- STEP 4: ARCHITECT THE QGAN ---
155
+ # [cite: 165-207]
156
+ print("\nBuilding QGAN components...")
157
+
158
+ # 4a. The Quantum Generator (G)
159
+ qc_ansatz = EfficientSU2(num_qubits, reps=6)
160
+ qc = QuantumCircuit(num_qubits)
161
+ qc.h(qc.qubits) # Start in a superposition
162
+ qc.compose(qc_ansatz, inplace=True)
163
+
164
+ # 4b. The SamplerQNN
165
+ sampler = Sampler()
166
+ qnn_generator = SamplerQNN(
167
+ circuit=qc,
168
+ sampler=sampler,
169
+ input_params=[], # The circuit takes NO input
170
+ weight_params=qc.parameters, # All parameters are trainable weights
171
+ )
172
+
173
+ # 4c. The TorchConnector
174
+ initial_weights = (2 * torch.rand(qnn_generator.num_weights) - 1)
175
+ generator = TorchConnector(qnn_generator, initial_weights=initial_weights)
176
+
177
+ # 4d. The Classical Discriminator (D)
178
+ class Discriminator(nn.Module):
179
+ def __init__(self, input_size):
180
+ super(Discriminator, self).__init__()
181
+ self.linear_input = nn.Linear(input_size, 20)
182
+ self.leaky_relu = nn.LeakyReLU(0.2)
183
+ self.linear20 = nn.Linear(20, 1)
184
+ self.sigmoid = nn.Sigmoid()
185
+
186
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
187
+ x = self.linear_input(input)
188
+ x = self.leaky_relu(x)
189
+ x = self.linear20(x)
190
+ x = self.sigmoid(x)
191
+ return x
192
+
193
+ discriminator = Discriminator(input_size=N_DIM)
194
+ print("Generator and Discriminator built.")
195
+
196
+
197
+ # --- STEP 5: DEFINE TRAINING COMPONENTS ---
198
+ # [cite: 213-241]
199
+ print("\nDefining training loop components...")
200
+
201
+
202
+
203
+ # 5b. Optimizers
204
+ # NEW, TUNED LEARNING RATES
205
+ lr_g = 0.02 # Generator learns FASTER
206
+ lr_d = 0.005 # Discriminator learns SLOWER
207
+ b1 = 0.7
208
+ b2 = 0.999
209
+
210
+ print(f"Using tuned learning rates: G={lr_g}, D={lr_d}")
211
+
212
+ generator_optimizer = Adam(generator.parameters(), lr=lr_g, betas=(b1, b2))
213
+ discriminator_optimizer = Adam(discriminator.parameters(), lr=lr_d, betas=(b1, b2))
214
+
215
+
216
+
217
+ g_losses = []
218
+ d_losses = []
219
+ rel_ents = []
220
+ print("Training setup complete.")
221
+
222
+
223
+ # --- STEP 6: RUN THE TRAINING LOOP ---
224
+ # [cite: 244-297]
225
+ num_epochs = 500
226
+ print(f"Starting training for {num_epochs} epochs...")
227
+
228
+ for epoch in range(num_epochs):
229
+ # --- Train Discriminator (The "Guard") ---
230
+ discriminator_optimizer.zero_grad()
231
+ disc_scores = discriminator(data_samples)
232
+ gen_dist = generator(torch.tensor([])).reshape(-1, 1)
233
+ real_loss = adversarial_loss(disc_scores, valid, real_data_dist)
234
+ fake_loss = adversarial_loss(disc_scores, fake, gen_dist.detach())
235
+ discriminator_loss = (real_loss + fake_loss) / 2
236
+ discriminator_loss.backward()
237
+ discriminator_optimizer.step()
238
+
239
+ # --- Train Generator (The "Sparring Partner") ---
240
+ generator_optimizer.zero_grad()
241
+ gen_dist = generator(torch.tensor([])).reshape(-1, 1)
242
+ disc_scores = discriminator(data_samples)
243
+ generator_loss = adversarial_loss(disc_scores, valid, gen_dist)
244
+ generator_loss.backward()
245
+ generator_optimizer.step()
246
+
247
+ # --- Log Progress ---
248
+ g_losses.append(generator_loss.item())
249
+ d_losses.append(discriminator_loss.item())
250
+
251
+ gen_dist_np = gen_dist.detach().numpy().flatten()
252
+ real_dist_np = real_distribution.flatten()
253
+ rel_ent = np.sum(real_dist_np * np.log((real_dist_np + 1e-9) / (gen_dist_np + 1e-9)))
254
+ rel_ents.append(rel_ent)
255
+
256
+ if (epoch + 1) % 50 == 0:
257
+ print(f"Epoch [{epoch+1}/{num_epochs}] G Loss: {generator_loss.item():.4f} | D Loss: {discriminator_loss.item():.4f} | Rel Ent: {rel_ent:.4f}")
258
+
259
+ print("Training complete.")
260
+
261
+ # --- Plot Training Progress ---
262
+ # [cite: 299-317]
263
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
264
+ fig.suptitle("QGAN-AD Training Progress")
265
+
266
+ ax1.plot(g_losses, label='Generator Loss')
267
+ ax1.plot(d_losses, label='Discriminator Loss')
268
+ ax1.set_title("Adversarial Losses")
269
+ ax1.set_xlabel("Epoch")
270
+ ax1.set_ylabel("Loss")
271
+ ax1.legend()
272
+
273
+ ax2.plot(rel_ents, label='Relative Entropy', color='green')
274
+ ax2.set_title("Relative Entropy (G vs. Real)")
275
+ ax2.set_xlabel("Epoch")
276
+ ax2.set_ylabel("Entropy (Lower is better)")
277
+ ax2.legend()
278
+
279
+ print("Displaying training graphs. Close the window to continue to the final test.")
280
+ plt.savefig(f'plots/QGAN_Trial_{i+1}_TrainingProgress.png')
281
+ plt.close(fig) # This closes the plot in memory, fixing the "beach ball"
282
+
283
+
284
+ # --- STEP 7: DEPLOYMENT AND VALIDATION---
285
+ # [cite: 323-345]
286
+ print("\n--- Phase IV: Deployment & Validation ---")
287
+
288
+
289
+
290
+ # 7b. Test 1: Run the detector on "Healthy" data
291
+ # [cite: 347-356]
292
+ print("Testing detector on HEALTHY data (expect scores near 1.0)...")
293
+ healthy_scores = []
294
+ # We'll just test the first 1000 healthy samples for speed
295
+ for i in range(min(1000, len(X_train_healthy))):
296
+ sample = X_train_healthy.iloc[i].values
297
+ score = detect_anomaly(sample, scaler, pca, discriminator)
298
+ healthy_scores.append(score)
299
+
300
+ # 7c. Test 2: Run the detector on "Anomalous" data
301
+ # [cite: 358-366]
302
+ print("Testing detector on ANOMALOUS data (expect scores near 0.0)...")
303
+ anomalous_scores = []
304
+ for i in range(len(X_test_anomalous)):
305
+ sample = X_test_anomalous.iloc[i].values
306
+ score = detect_anomaly(sample, scaler, pca, discriminator)
307
+ anomalous_scores.append(score)
308
+
309
+ print("Validation complete.")
310
+
311
+ # 7d. Visualize the Results: The Histogram
312
+ # [cite: 368-380]
313
+ plt.figure(figsize=(10, 6))
314
+ plt.hist(healthy_scores, bins=50, alpha=0.7, label='Healthy Scores (Trained On)', density=True, color='blue')
315
+ plt.hist(anomalous_scores, bins=50, alpha=0.7, label='Anomalous Scores (NEVER Seen Before)', density=True, color='red')
316
+ plt.title("QGAN-AD: Anomaly Score Distributions")
317
+ plt.xlabel("Anomaly Score (1.0 = Real/Healthy, 0.0 = Fake/Anomalous)")
318
+ plt.ylabel("Probability Density")
319
+ plt.legend()
320
+ plt.savefig(f'plots/QGAN_Trial_{i+1}_Histogram.png')
321
+ plt.close() # Closes the current figure
322
+
323
+ # 7e. The "Money Plot": The ROC Curve
324
+ # [cite: 382-406]
325
+ print("Generating final ROC Curve...")
326
+
327
+ y_true_healthy = np.zeros(len(healthy_scores))
328
+ y_true_anomalous = np.ones(len(anomalous_scores))
329
+ y_true = np.concatenate([y_true_healthy, y_true_anomalous])
330
+ y_scores = np.concatenate([healthy_scores, anomalous_scores])
331
+
332
+ # We use (1 - y_scores) because our 'anomalous' class (1) has a score near 0.0
333
+ fpr, tpr, _ = roc_curve(y_true, 1 - np.array(y_scores))
334
+ roc_auc = auc(fpr, tpr)
335
+ qgan_auc_scores.append(roc_auc)
336
+
337
+ plt.figure(figsize=(10, 8))
338
+ plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.4f})')
339
+ plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
340
+ plt.xlim([0.0, 1.0])
341
+ plt.ylim([0.0, 1.05])
342
+ plt.xlabel('False Positive Rate (Fooled by "Healthy")')
343
+ plt.ylabel('True Positive Rate (Correctly caught "Anomalous")')
344
+ plt.title('Receiver Operating Characteristic (ROC) Curve for QGAN-AD')
345
+ plt.legend(loc="lower right")
346
+
347
+ print(f"\nFINAL RESULT: Area Under Curve (AUC) = {roc_auc:.4f}")
348
+ print("(An AUC of 1.0 is a perfect detector. 0.5 is random guessing.)")
349
+ plt.savefig(f'plots/QGAN_Trial_{i+1}_ROC.png')
350
+ plt.close() # Closes the current figure
351
+
352
+ # ... (This is the end of the `for` loop) ...
353
+
354
+ # --- FINAL RESULTS ---
355
+ # (Make sure this block is UN-INDENTED, so it's OUTSIDE the loop)
356
+ print("\n" + "="*30)
357
+ print("--- FINAL QGAN RESULTS ---")
358
+ print(f"Scores over {N_TRIALS} trials:")
359
+ print([round(score, 4) for score in qgan_auc_scores])
360
+ print(f"\nAverage QGAN AUC: {np.mean(qgan_auc_scores):.4f}")
361
+ print(f"Standard Deviation: {np.std(qgan_auc_scores):.4f}")
362
+ print(f"Max AUC (Best Run): {np.max(qgan_auc_scores):.4f}")
363
+ print(f"Min AUC (Worst Run): {np.min(qgan_auc_scores):.4f}")
364
+ print(f"\nAll {N_TRIALS*3} plots saved to 'plots/' directory.")
365
+ print("="*30)
366
+
367
+ print("\n--- ACTION PLAN COMPLETE ---")
Phase 1/physics_data ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Save this as physics_data.py
2
+ import numpy as np
3
+ import pandas as pd
4
+ import matplotlib.pyplot as plt
5
+ from sklearn.decomposition import PCA
6
+
7
+ def generate_complex_tokamak_data(n_samples=5000, n_features=50):
8
+ """
9
+ Simulates complex, multi-modal Tokamak data.
10
+ Modes:
11
+ 1. Ramp-up (High variation, 'The Takeoff')
12
+ 2. Flat-top (Stable, tight cluster, 'The Cruise')
13
+ 3. Ramp-down (Lower energy, 'The Landing')
14
+ """
15
+ print(f"--- GENERATING PHYSICS-INFORMED DATA ({n_samples} samples) ---")
16
+
17
+ np.random.seed(42)
18
+
19
+ # --- 1. HEALTHY DATA (The "Three Phases") ---
20
+ # This mimics the distinct operational phases of a tokamak discharge.
21
+
22
+ # Mode A: Ramp-Up (25% of data) - Noisy, rising current
23
+ n_a = int(0.25 * n_samples)
24
+ # Centered at (-2, -2) in latent space roughly
25
+ mean_a = np.zeros(n_features) - 2.0
26
+ cov_a = np.eye(n_features) * 0.6
27
+ data_a = np.random.multivariate_normal(mean_a, cov_a, n_a)
28
+
29
+ # Mode B: Flat-Top (50% of data) - The Stable Core
30
+ # This is the "safe" fusion state.
31
+ n_b = int(0.50 * n_samples)
32
+ mean_b = np.zeros(n_features)
33
+ cov_b = np.eye(n_features) * 0.3 # Very tight, organized
34
+ data_b = np.random.multivariate_normal(mean_b, cov_b, n_b)
35
+
36
+ # Mode C: Ramp-Down (25% of data) - Cooling down
37
+ n_c = n_samples - n_a - n_b
38
+ mean_c = np.zeros(n_features) + 2.0
39
+ cov_c = np.eye(n_features) * 0.6
40
+ data_c = np.random.multivariate_normal(mean_c, cov_c, n_c)
41
+
42
+ # Stack them to create the "Healthy Manifold"
43
+ X_healthy = np.vstack([data_a, data_b, data_c])
44
+
45
+ # --- 2. ANOMALOUS DATA (The "Trap") ---
46
+ n_anom = int(n_samples * 0.1) # 10% anomaly rate
47
+
48
+ # Type 1: The "Bridge Gap" (Points hidden BETWEEN modes)
49
+ # These mimic "failed transitions" or "locked modes"
50
+ # If the discriminator is lazy, it will think these gaps are safe bridges.
51
+ n_anom_1 = int(n_anom * 0.6)
52
+ mean_bad_1 = np.zeros(n_features) - 1.0 # Stuck between Ramp and Flat-top
53
+ cov_bad_1 = np.eye(n_features) * 0.15 # Tight cluster in the gap
54
+ data_bad_1 = np.random.multivariate_normal(mean_bad_1, cov_bad_1, n_anom_1)
55
+
56
+ # Type 2: The "Hard Disruption" (Energy Spike / Greenwald Limit)
57
+ n_anom_2 = n_anom - n_anom_1
58
+ mean_bad_2 = np.ones(n_features) * 3.5 # Way outside normal bounds
59
+ cov_bad_2 = np.eye(n_features) * 0.5
60
+ data_bad_2 = np.random.multivariate_normal(mean_bad_2, cov_bad_2, n_anom_2)
61
+
62
+ X_anomalous = np.vstack([data_bad_1, data_bad_2])
63
+
64
+ # --- 3. DATAFRAME CREATION ---
65
+ df_healthy = pd.DataFrame(X_healthy, columns=[f'sensor_{i}' for i in range(n_features)])
66
+ df_healthy['label'] = 0 # 0 = Healthy
67
+
68
+ df_anomalous = pd.DataFrame(X_anomalous, columns=[f'sensor_{i}' for i in range(n_features)])
69
+ df_anomalous['label'] = 1 # 1 = Anomalous
70
+
71
+ df_total = pd.concat([df_healthy, df_anomalous], ignore_index=True)
72
+
73
+ # Shuffle the deck
74
+ df_total = df_total.sample(frac=1).reset_index(drop=True)
75
+
76
+ print(f"Generated {len(df_healthy)} healthy and {len(df_anomalous)} anomalous samples.")
77
+ return df_total
78
+
79
+ if __name__ == "__main__":
80
+ # Generate and Save
81
+ df = generate_complex_tokamak_data()
82
+ df.to_csv('complex_tokamak_data.csv', index=False)
83
+ print("Saved to 'complex_tokamak_data.csv'")
84
+
85
+ # --- VISUAL PROOF ---
86
+ # We use PCA to project the 50D data to 2D so you can SEE the islands
87
+ print("Generating preview plot...")
88
+ pca = PCA(n_components=2)
89
+ X = df.drop('label', axis=1)
90
+ y = df['label']
91
+ X_pca = pca.fit_transform(X)
92
+
93
+ plt.figure(figsize=(10, 8))
94
+
95
+ # Plot Healthy (Blue)
96
+ plt.scatter(X_pca[y==0, 0], X_pca[y==0, 1],
97
+ c='blue', alpha=0.2, s=10, label='Healthy (3 Modes)')
98
+
99
+ # Plot Anomalous (Red)
100
+ plt.scatter(X_pca[y==1, 0], X_pca[y==1, 1],
101
+ c='red', alpha=0.6, s=10, label='Anomalies (The Trap)')
102
+
103
+ plt.title("V2 Benchmark Data: The 'Three Islands' Topology")
104
+ plt.xlabel("Principal Component 1")
105
+ plt.ylabel("Principal Component 2")
106
+ plt.legend()
107
+ plt.grid(True, alpha=0.3)
108
+
109
+ plt.savefig('v2_data_topology.png')
110
+ print("Plot saved to 'v2_data_topology.png'. Open it to see the 'Islands'.")
Phase 1/plots_benchmark/QGAN_Trial_7_ROC.png ADDED
Phase 1/plots_v2_cgan/roc_trial_1.png ADDED
Phase 1/plots_v2_cgan/roc_trial_10.png ADDED
Phase 1/plots_v2_cgan/roc_trial_11.png ADDED
Phase 1/plots_v2_cgan/roc_trial_12.png ADDED
Phase 1/plots_v2_cgan/roc_trial_13.png ADDED
Phase 1/plots_v2_cgan/roc_trial_14.png ADDED
Phase 1/plots_v2_cgan/roc_trial_15.png ADDED
Phase 1/plots_v2_cgan/roc_trial_16.png ADDED
Phase 1/plots_v2_cgan/roc_trial_17.png ADDED
Phase 1/plots_v2_cgan/roc_trial_18.png ADDED
Phase 1/plots_v2_cgan/roc_trial_19.png ADDED
Phase 1/plots_v2_cgan/roc_trial_2.png ADDED
Phase 1/plots_v2_cgan/roc_trial_20.png ADDED
Phase 1/plots_v2_cgan/roc_trial_3.png ADDED
Phase 1/plots_v2_cgan/roc_trial_4.png ADDED
Phase 1/plots_v2_cgan/roc_trial_5.png ADDED
Phase 1/plots_v2_cgan/roc_trial_6.png ADDED
Phase 1/plots_v2_cgan/roc_trial_7.png ADDED
Phase 1/plots_v2_cgan/roc_trial_8.png ADDED
Phase 1/plots_v2_cgan/roc_trial_9.png ADDED
Phase 1/plots_v2_qgan/roc_trial_1.png ADDED
Phase 1/plots_v2_qgan/roc_trial_10.png ADDED
Phase 1/plots_v2_qgan/roc_trial_11.png ADDED
Phase 1/plots_v2_qgan/roc_trial_12.png ADDED
Phase 1/plots_v2_qgan/roc_trial_13.png ADDED
Phase 1/plots_v2_qgan/roc_trial_14.png ADDED
Phase 1/plots_v2_qgan/roc_trial_15.png ADDED
Phase 1/plots_v2_qgan/roc_trial_16.png ADDED
Phase 1/plots_v2_qgan/roc_trial_17.png ADDED
Phase 1/plots_v2_qgan/roc_trial_18.png ADDED
Phase 1/plots_v2_qgan/roc_trial_19.png ADDED
Phase 1/plots_v2_qgan/roc_trial_2.png ADDED
Phase 1/plots_v2_qgan/roc_trial_20.png ADDED
Phase 1/plots_v2_qgan/roc_trial_3.png ADDED
Phase 1/plots_v2_qgan/roc_trial_4.png ADDED
Phase 1/plots_v2_qgan/roc_trial_5.png ADDED
Phase 1/plots_v2_qgan/roc_trial_6.png ADDED
Phase 1/plots_v2_qgan/roc_trial_7.png ADDED
Phase 1/plots_v2_qgan/roc_trial_8.png ADDED
Phase 1/plots_v2_qgan/roc_trial_9.png ADDED
plasma_sim_v2.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+ from scipy.integrate import odeint
4
+
5
+ def lorenz_system(current_state, t, sigma=10, rho=28, beta=8/3):
6
+ """
7
+ The Lorenz Attractor equations.
8
+ x = Proxy for Plasma Radial Position
9
+ y = Proxy for Poloidal Magnetic Field
10
+ z = Proxy for Toroidal Magnetic Field
11
+ """
12
+ x, y, z = current_state
13
+ dx_dt = sigma * (y - x)
14
+ dy_dt = x * (rho - z) - y
15
+ dz_dt = x * y - beta * z
16
+ return [dx_dt, dy_dt, dz_dt]
17
+
18
+ def generate_plasma_data(n_samples=5000):
19
+ """
20
+ Generates synthetic fusion data based on chaotic turbulence.
21
+ """
22
+ print(f"Generating {n_samples} physics-informed plasma samples...")
23
+
24
+ # --- 1. Generate HEALTHY Data (The Attractor) ---
25
+ # We simulate a stable plasma orbit on the 'strange attractor'
26
+ dt = 0.01
27
+ t_span = np.arange(0, n_samples * dt, dt)
28
+ initial_state = [1.0, 1.0, 1.0]
29
+
30
+ # Solve the differential equations
31
+ healthy_data = odeint(lorenz_system, initial_state, t_span)
32
+
33
+ # Add 47 dummy 'noise' sensors to mimic a 50-sensor array
34
+ # Real sensors are just correlated versions of the main physics variables
35
+ noise_sensors = np.random.normal(0, 0.5, (n_samples, 47))
36
+ X_healthy = np.hstack([healthy_data, noise_sensors])
37
+
38
+ # --- 2. Generate ANOMALOUS Data (The Disruption) ---
39
+ # We simulate a 'Locked Mode' by changing the physics parameter 'rho'
40
+ # This causes the plasma to drift off the stable manifold
41
+ t_span_anom = np.arange(0, (n_samples // 10) * dt, dt)
42
+
43
+ # rho=15 changes the topology of the attractor (Disruption Precursor)
44
+ anomalous_data = odeint(lorenz_system, initial_state, t_span_anom, args=(10, 15, 8/3))
45
+
46
+ noise_sensors_anom = np.random.normal(0, 0.5, (len(anomalous_data), 47))
47
+ X_anomalous = np.hstack([anomalous_data, noise_sensors_anom])
48
+
49
+ # --- 3. Save to CSV ---
50
+ df_healthy = pd.DataFrame(X_healthy, columns=[f'sensor_{i}' for i in range(50)])
51
+ df_healthy['label'] = 0 # 0 = Healthy
52
+
53
+ df_anomalous = pd.DataFrame(X_anomalous, columns=[f'sensor_{i}' for i in range(50)])
54
+ df_anomalous['label'] = 1 # 1 = Disruption
55
+
56
+ df_final = pd.concat([df_healthy, df_anomalous], ignore_index=True)
57
+
58
+ output_file = "physics_informed_data.csv"
59
+ df_final.to_csv(output_file, index=False)
60
+ print(f"Success! Saved physics-informed data to {output_file}")
61
+ print("Shape:", df_final.shape)
62
+
63
+ if __name__ == "__main__":
64
+ generate_plasma_data()