Add files using upload-large-folder tool
Browse files- Phase 1/01_benchmark_qgan +249 -0
- Phase 1/Bridge-island_visual.py +79 -0
- Phase 1/V2.1_QGAN_HailMary.py +231 -0
- Phase 1/V2_benchmark_cgan +187 -0
- Phase 1/V2_benchmark_qgan +232 -0
- Phase 1/complex_tokamak_data.csv +0 -0
- Phase 1/main_fusion_qgan.py +367 -0
- Phase 1/physics_data +110 -0
- Phase 1/plots_benchmark/QGAN_Trial_7_ROC.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_1.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_10.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_11.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_12.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_13.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_14.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_15.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_16.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_17.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_18.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_19.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_2.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_20.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_3.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_4.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_5.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_6.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_7.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_8.png +0 -0
- Phase 1/plots_v2_cgan/roc_trial_9.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_1.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_10.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_11.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_12.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_13.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_14.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_15.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_16.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_17.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_18.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_19.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_2.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_20.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_3.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_4.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_5.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_6.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_7.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_8.png +0 -0
- Phase 1/plots_v2_qgan/roc_trial_9.png +0 -0
- 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()
|