""" qdot/perception/features.py ============================ Physics-aware feature extractors for 2D stability diagrams. Ported from the hackathon and promoted to first-class modules. In the new architecture these functions are a *validator layer* that sits after the CNN, not before it. The CNN is the primary classifier; these features act as physics-consistency checks that can set the `physics_override` flag on a Classification. Three extractors: log_preprocess(array) — log₁₀(|G| + ε) normalised to [0,1] fft_peak_ratio(array) — dominant periodicity ratio; > 3.5 → single-dot signal diagonal_strength(array) — Laplacian + gradient angle; high → charge transition lines All functions accept np.ndarray of any 2D shape. """ from __future__ import annotations import numpy as np # --------------------------------------------------------------------------- # 1. Log-space preprocessing # --------------------------------------------------------------------------- def log_preprocess(array: np.ndarray, eps: float = 1e-9) -> np.ndarray: """ Compress conductance dynamic range via log₁₀ and normalise to [0, 1]. This is always the *first* transformation applied to raw conductance data before any feature extraction or CNN inference. It handles the several-orders-of-magnitude variation in conductance across a stability diagram by compressing it into a perceptually uniform scale. Args: array: Raw conductance array, any shape, values ≥ 0. eps: Floor to prevent log(0). Default 1e-9 is safe for typical normalised conductance (adapter already clips to ≥ 0). Returns: Float32 array of the same shape, values ∈ [0, 1]. """ arr = np.asarray(array, dtype=np.float64) arr = np.clip(arr, 0.0, None) # guard against negative noise log_arr = np.log10(arr + eps) lo, hi = log_arr.min(), log_arr.max() if hi - lo > 1e-12: out = (log_arr - lo) / (hi - lo) else: out = np.full_like(log_arr, 0.5) return out.astype(np.float32) # --------------------------------------------------------------------------- # 2. FFT peak ratio # --------------------------------------------------------------------------- def fft_peak_ratio(array: np.ndarray) -> float: """ Measure the dominance of the strongest periodicity in a 2D scan. A single quantum dot produces regularly-spaced Coulomb peaks along one gate axis — a single dominant frequency. A double dot produces a honeycomb with multiple periodicities. A featureless scan produces a noisy spectrum with no dominant peak. Method: 1. Apply log-preprocessing (if not already done — idempotent). 2. Compute 2D FFT magnitude spectrum, zero-DC. 3. peak_ratio = max_power / mean_power of top-5 frequencies. Returns: peak_ratio ∈ ℝ. Empirically, > 3.5 indicates a single dominant periodicity (single-dot signature). This threshold is used as a physics validator, not a hard classifier. """ arr = log_preprocess(array).astype(np.float64) # 2D FFT, shift DC to centre spectrum = np.abs(np.fft.fftshift(np.fft.fft2(arr))) ** 2 # Zero out the DC component (centre pixel) to avoid it dominating cy, cx = spectrum.shape[0] // 2, spectrum.shape[1] // 2 spectrum[cy, cx] = 0.0 flat = spectrum.flatten() flat.sort() # ascending if flat[-1] < 1e-20: # all-zero spectrum → no signal return 0.0 # Ratio of top peak to mean of top-5 (avoids single outlier artefacts) top5 = flat[-5:] mean_top5 = top5.mean() if top5.mean() > 1e-20 else 1e-20 return float(flat[-1] / mean_top5) # --------------------------------------------------------------------------- # 3. Diagonal edge detector # --------------------------------------------------------------------------- def diagonal_strength(array: np.ndarray) -> float: """ Measure the fraction of image gradient energy aligned with the ±45° diagonals characteristic of charge transition lines in stability diagrams. Charge state boundaries appear as diagonal lines because a change in either gate voltage shifts the electrochemical potential of both dots (capacitive coupling). The slope is set by the ratio of the lever arms — always diagonal for symmetric devices, and close to diagonal for asymmetric ones. Method: 1. Log-preprocess. 2. Compute gradient magnitude + angle (Sobel operators). 3. diagonal_strength = fraction of gradient energy with angle ∈ (±45° ± 22.5°) or (±135° ± 22.5°). Returns: diagonal_strength ∈ [0, 1]. Values > 0.4 indicate strong diagonal structure (consistent with double-dot or single-dot transitions). """ arr = log_preprocess(array).astype(np.float64) # Sobel kernels (3×3) for x and y gradients sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float64) sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float64) # Manual 2D convolution (avoids OpenCV dependency here) from scipy.signal import convolve2d gx = convolve2d(arr, sobel_x, mode="same", boundary="symm") gy = convolve2d(arr, sobel_y, mode="same", boundary="symm") magnitude = np.sqrt(gx ** 2 + gy ** 2) angle_deg = np.degrees(np.arctan2(gy, gx)) # ∈ [-180, 180] total_energy = (magnitude ** 2).sum() if total_energy < 1e-20: return 0.0 # Diagonals are at ±45° and ±135°. Accept ±22.5° tolerance. angle_mod = np.abs(angle_deg) % 180 # fold to [0, 180) is_diagonal = ( ((angle_mod >= 22.5) & (angle_mod < 67.5)) | # ~45° ((angle_mod >= 112.5) & (angle_mod < 157.5)) # ~135° ) diagonal_energy = (magnitude[is_diagonal] ** 2).sum() return float(diagonal_energy / total_energy) # --------------------------------------------------------------------------- # 4. Composite physics feature vector # --------------------------------------------------------------------------- def physics_features(array: np.ndarray) -> dict: """ Compute all physics validator features for a single 2D patch. Returns a dict compatible with Classification.features. Keys: fft_peak_ratio — dominance of single periodicity diagonal_strength — fraction of gradient energy on diagonals mean_conductance — mean of log-preprocessed array (proxy for regime) conductance_std — std of log-preprocessed array (proxy for feature contrast) """ preprocessed = log_preprocess(array) return { "fft_peak_ratio": fft_peak_ratio(array), "diagonal_strength": diagonal_strength(array), "mean_conductance": float(preprocessed.mean()), "conductance_std": float(preprocessed.std()), } # --------------------------------------------------------------------------- # 5. Physics heuristic override rules # --------------------------------------------------------------------------- # Thresholds — these are defaults. InspectionAgent receives device-adaptive # versions derived from CIM parameters (lever_arm, E_c) at runtime. PEAK_RATIO_SD_THRESHOLD = 3.5 # above → single-dot signature DIAGONAL_STRENGTH_MIN = 0.25 # below → no transition lines → likely featureless def physics_override_label( cnn_label: str, features: dict, peak_ratio_threshold: float = PEAK_RATIO_SD_THRESHOLD, diagonal_min: float = DIAGONAL_STRENGTH_MIN, ) -> tuple[str | None, str]: """ Apply physics heuristics to check for inconsistencies with the CNN label. Returns: (override_label, reason) where override_label is None if no override is warranted, or a ChargeLabel string if the heuristics suggest the CNN is wrong. Override conditions (conservative — only flag clear contradictions): - CNN says double-dot but peak_ratio > peak_ratio_threshold AND diagonal_strength < diagonal_min → likely single-dot - CNN says anything but diagonal_strength < 0.10 AND conductance_std < 0.05 → featureless → misc """ pr = features.get("fft_peak_ratio", 0.0) ds = features.get("diagonal_strength", 1.0) std = features.get("conductance_std", 1.0) # Featureless check — catches SC and Barrier if ds < 0.10 and std < 0.05: if cnn_label != "misc": return "misc", f"Featureless (diag={ds:.2f}, std={std:.3f})" # Single-dot signal in a purported double-dot if cnn_label == "double-dot" and pr > peak_ratio_threshold and ds < diagonal_min: return "single-dot", f"Strong single periodicity (peak_ratio={pr:.1f})" return None, ""