Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- config.py +86 -0
- preprocessor.py +148 -0
config.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Central configuration for Rabi Oscillation Quality Classifier.
|
| 3 |
+
V2: Calibrated to real data distributions.
|
| 4 |
+
"""
|
| 5 |
+
import torch
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# ─── Paths ───────────────────────────────────────────────────────────────────
|
| 9 |
+
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 10 |
+
DATA_DIR = os.path.join(PROJECT_DIR, "data copy")
|
| 11 |
+
UI_TEST_DIR = os.path.join(PROJECT_DIR, "ui_test_samples")
|
| 12 |
+
MODEL_PATH = os.path.join(PROJECT_DIR, "best_model.pt")
|
| 13 |
+
CORRECTIONS_DIR = os.path.join(PROJECT_DIR, "data_corrections")
|
| 14 |
+
FAILED_PRED_DIR = os.path.join(PROJECT_DIR, "failed_predictions")
|
| 15 |
+
|
| 16 |
+
# ─── Device (MPS > CUDA > CPU) ──────────────────────────────────────────────
|
| 17 |
+
if torch.backends.mps.is_available():
|
| 18 |
+
DEVICE = torch.device("mps")
|
| 19 |
+
elif torch.cuda.is_available():
|
| 20 |
+
DEVICE = torch.device("cuda")
|
| 21 |
+
else:
|
| 22 |
+
DEVICE = torch.device("cpu")
|
| 23 |
+
|
| 24 |
+
# ─── Signal Preprocessing ───────────────────────────────────────────────────
|
| 25 |
+
SEQ_LEN = 256 # Increased from 128 for finer resolution
|
| 26 |
+
NUM_FIT_PARAMS = 4 # amplitude, T, phase, offset
|
| 27 |
+
|
| 28 |
+
# ─── Model ───────────────────────────────────────────────────────────────────
|
| 29 |
+
NUM_CLASSES = 3 # Classes per head: 0=Bad, 1=Acceptable, 2=Perfect
|
| 30 |
+
IN_CHANNELS = 3 # raw signal + fit curve + residual (signal - fit)
|
| 31 |
+
RESNET_DIM = 256 # ResNet backbone output dim
|
| 32 |
+
DENSE_DIM = 64 # Dense branch output dim
|
| 33 |
+
FUSED_DIM = RESNET_DIM + DENSE_DIM # 320
|
| 34 |
+
|
| 35 |
+
# ─── Training ────────────────────────────────────────────────────────────────
|
| 36 |
+
BATCH_SIZE = 256
|
| 37 |
+
LR = 3e-4 # Lower LR for better convergence
|
| 38 |
+
WEIGHT_DECAY = 1e-4
|
| 39 |
+
EPOCHS = 80 # More epochs
|
| 40 |
+
TRAIN_SIZE = 200_000 # Doubled for better generalization
|
| 41 |
+
VAL_SIZE = 40_000
|
| 42 |
+
PATIENCE = 15 # More patience
|
| 43 |
+
FOCAL_GAMMA = 2.0
|
| 44 |
+
CLASS_WEIGHTS = [1.2, 0.9, 0.9] # Stronger upweight for class 0
|
| 45 |
+
IGNORE_INDEX = -1
|
| 46 |
+
|
| 47 |
+
# ─── Synthetic Generator — Calibrated to Real Data ──────────────────────────
|
| 48 |
+
# Real data statistics (from analysis of 2736 JSON files):
|
| 49 |
+
# amplitude: mean=0.090, std=0.083, range=[~0, 0.40], p25=0.0002
|
| 50 |
+
# T: mean=0.101 but p75=0.062, most values 0.047-0.110
|
| 51 |
+
# phase: range=[-π, π]
|
| 52 |
+
# offset: mean=0.185, std=0.166, range=[~0, 0.98]
|
| 53 |
+
# x_range: mean=0.106, range=[0.006, 3.58]
|
| 54 |
+
# n_points: mean=145, range=[80, 1000]
|
| 55 |
+
|
| 56 |
+
# Data Quality SNR thresholds (dB)
|
| 57 |
+
SNR_PERFECT_MIN = 15.0 # Loosened from 20 (real data is noisier)
|
| 58 |
+
SNR_ACCEPTABLE_MIN = 5.0 # Loosened from 8
|
| 59 |
+
SNR_BORDERLINE_MIN = 3.0
|
| 60 |
+
|
| 61 |
+
# Fit Quality error thresholds (fraction)
|
| 62 |
+
FIT_PERFECT_MAX_ERR = 0.03
|
| 63 |
+
FIT_ACCEPTABLE_MAX_ERR = 0.20
|
| 64 |
+
FIT_BAD_MIN_ERR = 0.20
|
| 65 |
+
FIT_BORDERLINE_ERR_RANGE = (0.15, 0.30)
|
| 66 |
+
|
| 67 |
+
# Rabi oscillation parameter ranges — CALIBRATED TO REAL DATA
|
| 68 |
+
AMPLITUDE_RANGE = (0.0001, 0.42) # Real: [~0, 0.40]
|
| 69 |
+
PERIOD_RANGE = (0.025, 0.80) # Real: [0.025, 0.78] for good fits
|
| 70 |
+
PHASE_RANGE = (-3.1416, 3.1416) # Real: [-π, π]
|
| 71 |
+
OFFSET_RANGE = (0.0001, 0.98) # Real: [~0, 0.98]
|
| 72 |
+
DECAY_RANGE = (0.0, 30.0) # Reduced max decay
|
| 73 |
+
X_MAX_RANGE = (0.005, 0.50) # Real: [0.006, 3.58] but most < 0.2
|
| 74 |
+
|
| 75 |
+
# Extended ranges for diversity
|
| 76 |
+
X_MAX_EXTENDED = (0.50, 3.60) # For occasional long sweeps
|
| 77 |
+
PERIOD_BAD_RANGE = (0.002, 19.0) # Real bad fits have T up to 19
|
| 78 |
+
|
| 79 |
+
# Per-sample x-axis variation
|
| 80 |
+
N_POINTS_RANGE = (80, 500) # Real: [80, 1000]
|
| 81 |
+
|
| 82 |
+
# Proportion of borderline cases in Class 0
|
| 83 |
+
BORDERLINE_FRACTION = 0.5 # Increased from 0.4
|
| 84 |
+
|
| 85 |
+
# Proportion of near-zero amplitude samples (mimics ~25% of real data)
|
| 86 |
+
NEAR_ZERO_AMP_FRACTION = 0.20
|
preprocessor.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Preprocessor for Rabi oscillation JSON experiment files.
|
| 3 |
+
V3: Outputs 3 channels — signal + fit + residual (signal − fit).
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
from config import SEQ_LEN
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def load_json(path: str) -> dict:
|
| 12 |
+
"""Load a JSON experiment file."""
|
| 13 |
+
with open(path, 'r') as f:
|
| 14 |
+
return json.load(f)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def extract_fit_params(data: dict) -> dict:
|
| 18 |
+
"""Extract the 4 fit parameters from JSON data.
|
| 19 |
+
|
| 20 |
+
Returns dict with keys: amplitude, T, phase, offset.
|
| 21 |
+
Returns zeros if fitted_data is missing or None.
|
| 22 |
+
"""
|
| 23 |
+
defaults = {'amplitude': 0.0, 'T': 0.05, 'phase': 0.0, 'offset': 0.0}
|
| 24 |
+
|
| 25 |
+
fitted = data.get('fitted_data')
|
| 26 |
+
if fitted is None:
|
| 27 |
+
return defaults
|
| 28 |
+
|
| 29 |
+
params_list = fitted.get('parameters')
|
| 30 |
+
if params_list is None:
|
| 31 |
+
return defaults
|
| 32 |
+
|
| 33 |
+
params = dict(defaults)
|
| 34 |
+
for p in params_list:
|
| 35 |
+
if p.get('name') in params:
|
| 36 |
+
params[p['name']] = float(p.get('value', 0.0))
|
| 37 |
+
return params
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def rabi_oscillation(x, amplitude, T, phase, offset):
|
| 41 |
+
"""Compute Rabi oscillation curve (without decay, matching JSON fit)."""
|
| 42 |
+
return amplitude * np.cos((2 * np.pi / T) * x + phase) + offset
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def reconstruct_fit_curve(x: np.ndarray, params: dict) -> np.ndarray:
|
| 46 |
+
"""Reconstruct the fit curve on the original X-axis using JSON parameters."""
|
| 47 |
+
return rabi_oscillation(
|
| 48 |
+
x,
|
| 49 |
+
amplitude=params['amplitude'],
|
| 50 |
+
T=params['T'],
|
| 51 |
+
phase=params['phase'],
|
| 52 |
+
offset=params['offset']
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def preprocess_sample(data: dict):
|
| 57 |
+
"""
|
| 58 |
+
Preprocess a single JSON experiment sample.
|
| 59 |
+
|
| 60 |
+
V3: Returns 3-channel tensor (signal, fit, residual).
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
signal_tensor: (3, SEQ_LEN) — [signal, fit, residual]
|
| 64 |
+
params_tensor: (4,) fit parameters [amplitude, T, phase, offset]
|
| 65 |
+
question_id: str ('q1' or 'q2')
|
| 66 |
+
score: float (0.0 or 1.0)
|
| 67 |
+
"""
|
| 68 |
+
# Extract raw data
|
| 69 |
+
x_raw = np.array(data['measured_data']['x_values'], dtype=np.float64)
|
| 70 |
+
y_raw = np.array(data['measured_data']['y_values'], dtype=np.float64)
|
| 71 |
+
|
| 72 |
+
# Extract fit parameters
|
| 73 |
+
params = extract_fit_params(data)
|
| 74 |
+
|
| 75 |
+
# Reconstruct fit curve on original x-axis
|
| 76 |
+
y_fit = reconstruct_fit_curve(x_raw, params)
|
| 77 |
+
|
| 78 |
+
# Create uniform interpolation grid
|
| 79 |
+
x_uniform = np.linspace(x_raw.min(), x_raw.max(), SEQ_LEN)
|
| 80 |
+
|
| 81 |
+
# Interpolate both curves to fixed length
|
| 82 |
+
y_raw_interp = np.interp(x_uniform, x_raw, y_raw)
|
| 83 |
+
y_fit_interp = np.interp(x_uniform, x_raw, y_fit)
|
| 84 |
+
|
| 85 |
+
# Normalize both with same min-max scaling (from raw signal)
|
| 86 |
+
y_min = y_raw_interp.min()
|
| 87 |
+
y_max = y_raw_interp.max()
|
| 88 |
+
y_range = y_max - y_min
|
| 89 |
+
if y_range < 1e-10:
|
| 90 |
+
y_range = 1.0 # Avoid division by zero for flat signals
|
| 91 |
+
|
| 92 |
+
y_raw_norm = (y_raw_interp - y_min) / y_range
|
| 93 |
+
y_fit_norm = (y_fit_interp - y_min) / y_range
|
| 94 |
+
|
| 95 |
+
# Residual channel: signal − fit, normalized to [-1, 1]
|
| 96 |
+
residual = y_raw_norm - y_fit_norm
|
| 97 |
+
res_absmax = np.abs(residual).max()
|
| 98 |
+
if res_absmax < 1e-10:
|
| 99 |
+
res_absmax = 1.0
|
| 100 |
+
res_norm = residual / res_absmax
|
| 101 |
+
|
| 102 |
+
# Build 3-channel tensor
|
| 103 |
+
signal_tensor = torch.tensor(
|
| 104 |
+
np.stack([y_raw_norm, y_fit_norm, res_norm], axis=0),
|
| 105 |
+
dtype=torch.float32
|
| 106 |
+
)
|
| 107 |
+
params_tensor = torch.tensor(
|
| 108 |
+
[params['amplitude'], params['T'], params['phase'], params['offset']],
|
| 109 |
+
dtype=torch.float32
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# Extract labels
|
| 113 |
+
question_id = data.get('question_id', 'q1')
|
| 114 |
+
score = float(data.get('score', 0.0))
|
| 115 |
+
|
| 116 |
+
return signal_tensor, params_tensor, question_id, score
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def load_test_dataset(data_dir: str) -> list:
|
| 120 |
+
"""
|
| 121 |
+
Load and preprocess all JSON files from the test data directory.
|
| 122 |
+
|
| 123 |
+
Returns list of dicts with keys:
|
| 124 |
+
signal, params, question_id, score, filename
|
| 125 |
+
"""
|
| 126 |
+
import os
|
| 127 |
+
samples = []
|
| 128 |
+
json_files = sorted([f for f in os.listdir(data_dir) if f.endswith('.json')])
|
| 129 |
+
|
| 130 |
+
for fname in json_files:
|
| 131 |
+
path = os.path.join(data_dir, fname)
|
| 132 |
+
try:
|
| 133 |
+
data = load_json(path)
|
| 134 |
+
signal_3ch, params, qid, score = preprocess_sample(data)
|
| 135 |
+
samples.append({
|
| 136 |
+
'signal': signal_3ch, # (3, SEQ_LEN)
|
| 137 |
+
'params': params,
|
| 138 |
+
'question_id': qid,
|
| 139 |
+
'score': score,
|
| 140 |
+
'filename': fname,
|
| 141 |
+
'raw_data': data,
|
| 142 |
+
})
|
| 143 |
+
except Exception as e:
|
| 144 |
+
print(f"Warning: Failed to preprocess {fname}: {e}")
|
| 145 |
+
continue
|
| 146 |
+
|
| 147 |
+
print(f"Loaded {len(samples)} samples from {data_dir}")
|
| 148 |
+
return samples
|