| """ |
| qdot/perception/inspector.py |
| ============================ |
| InspectionAgent — Layer 3 perception pipeline orchestrator. |
| |
| Takes a quality-gated Measurement and produces a typed Classification |
| and OODResult for the Executive Agent. This is the single entry point |
| for all perception work in Phase 1. |
| |
| Pipeline (blueprint §5.3): |
| 1. DQC check — already done by Gatekeeper; verdict passed in |
| 2. Log-preprocess — always first transformation |
| 3. CNN classify — EnsembleCNN, 3-class softmax |
| 4. Physics validate— FFT + diagonal features; can set override_flag |
| 5. OOD detect — Mahalanobis distance on penultimate features |
| 6. NL report — structured JSON summary for Executive Agent |
| |
| What this module does NOT do: |
| - Process line scans (those go directly to Executive Agent) |
| - Make voltage decisions (that is the POMDP planner's job) |
| - Trigger HITL (the Executive Agent does this based on the report) |
| |
| The NL report is a structured JSON dict in Phase 1 (no LLM call). |
| LLM calls are reserved for stage transitions and HITL triggers (§5.1). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import time |
| from dataclasses import asdict |
| from typing import Optional, Tuple |
| from uuid import UUID |
|
|
| import numpy as np |
|
|
| from qdot.core.types import ( |
| ChargeLabel, |
| Classification, |
| DQCQuality, |
| DQCResult, |
| Measurement, |
| OODResult, |
| ) |
| from qdot.perception.dqc import DQCGatekeeper |
| from qdot.perception.features import physics_features, physics_override_label |
| from qdot.perception.classifier import EnsembleCNN |
| from qdot.perception.ood import MahalanobisOOD |
|
|
|
|
| |
| INT_TO_LABEL = { |
| 0: ChargeLabel.DOUBLE_DOT, |
| 1: ChargeLabel.SINGLE_DOT, |
| 2: ChargeLabel.MISC, |
| } |
| LABEL_TO_INT = {v: k for k, v in INT_TO_LABEL.items()} |
|
|
|
|
| class InspectionAgent: |
| """ |
| Perception pipeline for 2D charge stability diagrams. |
| |
| Accepts a raw Measurement (normalised conductance array), runs the |
| full classification pipeline, and returns typed result objects that |
| the Executive Agent consumes. |
| |
| Important: Only accepts 2D measurements. Line scans are excluded by |
| design (blueprint §5.3): "The Inspection Agent classifies 2D stability |
| diagrams only. Line scans are used for navigation by the Executive |
| Agent directly." |
| |
| Usage: |
| agent = InspectionAgent(ensemble=ensemble, ood_detector=ood) |
| classification, ood_result = agent.inspect(measurement, dqc_result) |
| |
| # Access structured NL report |
| print(classification.nl_summary) # JSON string |
| """ |
|
|
| |
| |
| MIN_DQC_FOR_CNN = DQCQuality.MODERATE |
|
|
| def __init__( |
| self, |
| ensemble: Optional[EnsembleCNN] = None, |
| ood_detector: Optional[MahalanobisOOD] = None, |
| gatekeeper: Optional[DQCGatekeeper] = None, |
| |
| peak_ratio_threshold: float = 3.5, |
| diagonal_strength_min: float = 0.25, |
| |
| ood_threshold: Optional[float] = None, |
| ) -> None: |
| """ |
| Args: |
| ensemble: Trained EnsembleCNN. If None, uses untrained model |
| (for testing only — predictions will be random). |
| ood_detector: Fitted MahalanobisOOD. If None, OOD detection is |
| skipped and flag is always False. |
| gatekeeper: DQCGatekeeper. If None, a default one is created. |
| peak_ratio_threshold: FFT peak ratio above which SD signature is flagged. |
| diagonal_strength_min: diagonal_strength below which transition lines absent. |
| ood_threshold: Override the detector's calibrated threshold. |
| """ |
| self.ensemble = ensemble or EnsembleCNN() |
| self.ood_detector = ood_detector |
| self.gatekeeper = gatekeeper or DQCGatekeeper() |
| self.peak_ratio_threshold = peak_ratio_threshold |
| self.diagonal_strength_min = diagonal_strength_min |
| self.ood_threshold_override = ood_threshold |
|
|
| |
| |
| |
|
|
| def inspect( |
| self, |
| measurement: Measurement, |
| dqc_result: Optional[DQCResult] = None, |
| ) -> Tuple[Classification, OODResult]: |
| """ |
| Run the full perception pipeline on a 2D measurement. |
| |
| Args: |
| measurement: A Measurement from the Device Adapter. |
| Must be a 2D modality (COARSE_2D, LOCAL_PATCH, FINE_2D). |
| dqc_result: DQCResult from the Gatekeeper. If None, assessment is run here. |
| A LOW quality result raises RuntimeError to prevent CNN pollution. |
| |
| Returns: |
| (Classification, OODResult) |
| |
| Raises: |
| ValueError: if the measurement is not 2D. |
| RuntimeError: if DQC quality is LOW (should have been stopped upstream). |
| """ |
| |
| if not measurement.is_2d: |
| raise ValueError( |
| f"InspectionAgent received a non-2D measurement " |
| f"(modality={measurement.modality}). " |
| "Line scans must be processed directly by the Executive Agent." |
| ) |
|
|
| |
| if dqc_result is None: |
| dqc_result = self.gatekeeper.assess(measurement) |
|
|
| |
| if dqc_result.quality == DQCQuality.LOW: |
| raise RuntimeError( |
| f"InspectionAgent received LOW-quality data " |
| f"(measurement_id={measurement.id}). " |
| "This should have been stopped at the DQC Gatekeeper." |
| ) |
|
|
| arr = np.asarray(measurement.array, dtype=np.float32) |
|
|
| |
| label_idx, confidence, disagreement = self.ensemble.classify(arr) |
| cnn_label = INT_TO_LABEL[label_idx] |
|
|
| |
| phys_feats = physics_features(arr) |
|
|
| |
| override_str, override_reason = physics_override_label( |
| cnn_label=cnn_label.value, |
| features=phys_feats, |
| peak_ratio_threshold=self.peak_ratio_threshold, |
| diagonal_min=self.diagonal_strength_min, |
| ) |
| physics_override = override_str is not None |
| final_label = ( |
| ChargeLabel(override_str) if physics_override else cnn_label |
| ) |
| if physics_override: |
| |
| confidence = min(confidence, 0.65) |
|
|
| |
| features_vec = self.ensemble.extract_features(arr) |
| ood_result = self._run_ood(measurement.id, features_vec) |
|
|
| |
| all_features = {**phys_feats, "ensemble_disagreement": disagreement} |
|
|
| classification = Classification( |
| measurement_id=measurement.id, |
| label=final_label, |
| confidence=confidence, |
| ensemble_disagreement=disagreement, |
| features=all_features, |
| physics_override=physics_override, |
| nl_summary=self._generate_nl_report( |
| measurement=measurement, |
| cnn_label=cnn_label, |
| final_label=final_label, |
| confidence=confidence, |
| disagreement=disagreement, |
| ood_result=ood_result, |
| dqc_result=dqc_result, |
| phys_feats=phys_feats, |
| override_reason=override_reason, |
| ), |
| ) |
|
|
| return classification, ood_result |
|
|
| |
| |
| |
|
|
| def inspect_array(self, array: np.ndarray) -> Tuple[ChargeLabel, float, float]: |
| """ |
| Quick classification of a raw array. |
| |
| Args: |
| array: 2D normalised conductance array (H, W). |
| |
| Returns: |
| (label, confidence, ensemble_disagreement) |
| |
| Does NOT compute OOD or physics override — use inspect() for the |
| full pipeline with a Measurement object. |
| """ |
| arr = np.asarray(array, dtype=np.float32) |
| label_idx, confidence, disagreement = self.ensemble.classify(arr) |
| return INT_TO_LABEL[label_idx], confidence, disagreement |
|
|
| |
| |
| |
|
|
| def _run_ood( |
| self, measurement_id: UUID, features: np.ndarray |
| ) -> OODResult: |
| """Run OOD detection if detector is fitted, else return safe default.""" |
| if self.ood_detector is None or not self.ood_detector._fitted: |
| |
| threshold = self.ood_threshold_override or 24.0 |
| return OODResult( |
| measurement_id=measurement_id, |
| score=0.0, |
| threshold=threshold, |
| flag=False, |
| ) |
|
|
| result = self.ood_detector.score(measurement_id, features) |
|
|
| |
| if self.ood_threshold_override is not None: |
| result = OODResult( |
| measurement_id=measurement_id, |
| score=result.score, |
| threshold=self.ood_threshold_override, |
| flag=result.score > self.ood_threshold_override, |
| ) |
|
|
| return result |
|
|
| def _generate_nl_report( |
| self, |
| measurement: Measurement, |
| cnn_label: ChargeLabel, |
| final_label: ChargeLabel, |
| confidence: float, |
| disagreement: float, |
| ood_result: OODResult, |
| dqc_result: DQCResult, |
| phys_feats: dict, |
| override_reason: str, |
| ) -> str: |
| """ |
| Generate a structured JSON NL report for the Executive Agent. |
| |
| This is a templated report in Phase 1 (no LLM call). |
| The Executive Agent uses this to update its belief state |
| and generate rationale text when triggered. |
| |
| Format matches blueprint §5.1 rationale format: |
| {intent, observation_summary, physics_reasoning, |
| proposed_action, expected_outcome} |
| """ |
| |
| if confidence >= 0.85: |
| conf_desc = "high" |
| elif confidence >= 0.60: |
| conf_desc = "moderate" |
| else: |
| conf_desc = "low" |
|
|
| |
| if disagreement > 0.30: |
| unc_desc = "HIGH — ensemble disagreement above HITL threshold" |
| elif disagreement > 0.15: |
| unc_desc = "moderate" |
| else: |
| unc_desc = "low" |
|
|
| report = { |
| "timestamp": time.time(), |
| "measurement_id": str(measurement.id), |
| "intent": "classify_stability_diagram", |
| "observation_summary": { |
| "modality": measurement.modality.value, |
| "resolution": measurement.resolution, |
| "device_id": measurement.device_id, |
| "dqc_quality": dqc_result.quality.value, |
| "dqc_snr_db": round(dqc_result.snr_db, 2), |
| }, |
| "classification": { |
| "cnn_label": cnn_label.value, |
| "final_label": final_label.value, |
| "confidence": round(confidence, 4), |
| "confidence_tier": conf_desc, |
| "physics_override": override_reason if override_reason else None, |
| }, |
| "uncertainty": { |
| "ensemble_disagreement": round(disagreement, 4), |
| "uncertainty_tier": unc_desc, |
| "hitl_warranted": disagreement > 0.30, |
| }, |
| "physics_reasoning": { |
| "fft_peak_ratio": round(phys_feats.get("fft_peak_ratio", 0.0), 3), |
| "diagonal_strength": round(phys_feats.get("diagonal_strength", 0.0), 3), |
| "mean_conductance": round(phys_feats.get("mean_conductance", 0.0), 3), |
| "conductance_std": round(phys_feats.get("conductance_std", 0.0), 3), |
| "interpretation": self._physics_interpretation(phys_feats, final_label), |
| }, |
| "ood": { |
| "score": round(ood_result.score, 4), |
| "threshold": round(ood_result.threshold, 4), |
| "flag": ood_result.flag, |
| "margin": round(ood_result.margin, 4), |
| "action": ( |
| "trigger_disorder_learner" |
| if ood_result.flag |
| else "continue_normal_loop" |
| ), |
| }, |
| "recommended_executive_action": self._recommend_action( |
| final_label, confidence, disagreement, ood_result |
| ), |
| } |
| return json.dumps(report, indent=2) |
|
|
| def _physics_interpretation( |
| self, features: dict, label: ChargeLabel |
| ) -> str: |
| """Human-readable interpretation of physics features.""" |
| pr = features.get("fft_peak_ratio", 0.0) |
| ds = features.get("diagonal_strength", 0.0) |
|
|
| if label == ChargeLabel.DOUBLE_DOT: |
| return ( |
| f"Diagonal structure present (strength={ds:.2f}), " |
| f"no single-dominant periodicity (peak_ratio={pr:.1f}). " |
| "Consistent with honeycomb topology." |
| ) |
| elif label == ChargeLabel.SINGLE_DOT: |
| return ( |
| f"Dominant periodicity detected (peak_ratio={pr:.1f}). " |
| f"Diagonal structure: {ds:.2f}. " |
| "Consistent with Coulomb diamond pattern from single dot." |
| ) |
| else: |
| return ( |
| f"Featureless or ambiguous scan. " |
| f"Diagonal strength={ds:.2f}, peak_ratio={pr:.1f}. " |
| "Likely SC or pinch-off regime." |
| ) |
|
|
| def _recommend_action( |
| self, |
| label: ChargeLabel, |
| confidence: float, |
| disagreement: float, |
| ood_result: OODResult, |
| ) -> str: |
| """ |
| Recommend the next Executive Agent action based on this classification. |
| This is advisory only — the POMDP planner makes the actual decision. |
| """ |
| if ood_result.flag: |
| return "trigger_disorder_learner" |
| if disagreement > 0.30: |
| return "request_hitl_classification_ambiguous" |
| if label == ChargeLabel.DOUBLE_DOT and confidence >= 0.85: |
| return "proceed_to_navigation_stage" |
| if label == ChargeLabel.DOUBLE_DOT and confidence >= 0.60: |
| return "refine_scan_local_patch" |
| if label == ChargeLabel.SINGLE_DOT: |
| return "adjust_barrier_voltages_increase_coupling" |
| if label == ChargeLabel.MISC and confidence >= 0.70: |
| return "backtrack_coarse_survey" |
| return "take_coarse_2d_scan_for_more_information" |
|
|