| """ |
| qdot/core/state.py |
| ================== |
| ExperimentState — single source of truth for the entire agent run. |
| |
| Voltage bounds default changed to ±3.0 V (was ±1.0 V). |
| |
| Rationale: the benchmark draws CIM params with E_c ≈ 2–3 meV and |
| lever_arm ≈ 0.65–0.85, placing the charge transition at -1.5 to -2.5 V. |
| The old ±1.0 V default made transitions physically unreachable regardless |
| of navigation strategy — the agent hit the safety wall before it could |
| reach the first charge boundary. ±3.0 V covers the full GaAs-class |
| device parameter space used in the benchmark. |
| |
| The Safety Critic architecture is unchanged; only the enforced bound |
| values are updated. Real hardware deployments should override these |
| defaults with device-class-appropriate values via ExperimentState.new(). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import time |
| import uuid |
| from dataclasses import dataclass, field |
| from typing import Dict, List, Optional, Any |
| from uuid import UUID |
|
|
| import numpy as np |
|
|
| from qdot.core.types import ( |
| ActionProposal, BacktrackEvent, BOPoint, ChargeLabel, |
| Classification, Decision, DQCResult, HITLEvent, |
| Measurement, MeasurementModality, OODResult, |
| SafetyVerdict, TuningStage, VoltagePoint, |
| ) |
|
|
|
|
| @dataclass |
| class BeliefState: |
| """POMDP belief state: P(charge configuration | observations, CIM prior).""" |
| charge_probs: Dict[tuple, float] = field(default_factory=dict) |
| uncertainty_map: Optional[Any] = None |
| device_params: Dict[str, float] = field(default_factory=lambda: { |
| "E_c1": 0.50, |
| "E_c2": 0.55, |
| "t_c": 0.05, |
| "T": 0.015, |
| "lever_arm": 1.0, |
| "noise_level": 0.01, |
| }) |
| disorder_estimate: Optional[Dict[str, Any]] = None |
|
|
| def entropy(self) -> float: |
| if not self.charge_probs: |
| return float("inf") |
| probs = np.array(list(self.charge_probs.values()), dtype=float) |
| probs = probs[probs > 0] |
| return float(-np.sum(probs * np.log(probs))) |
|
|
| def most_likely_state(self) -> Optional[tuple]: |
| if not self.charge_probs: |
| return None |
| return max(self.charge_probs, key=lambda k: self.charge_probs[k]) |
|
|
| def initialise_uniform(self, charge_states: Optional[List[tuple]] = None) -> None: |
| if charge_states is None: |
| charge_states = [(n1, n2) for n1 in range(3) for n2 in range(3)] |
| p = 1.0 / len(charge_states) |
| self.charge_probs = {s: p for s in charge_states} |
|
|
|
|
| @dataclass |
| class ExperimentState: |
| """Centralised state object. All modules read from and write to this.""" |
|
|
| run_id: str = field(default_factory=lambda: str(uuid.uuid4())) |
| device_id: str = "" |
| started_at: float = field(default_factory=time.time) |
| target_label: ChargeLabel = ChargeLabel.DOUBLE_DOT |
|
|
| current_voltage: VoltagePoint = field(default_factory=lambda: VoltagePoint(0.0, 0.0)) |
| trajectory: List[VoltagePoint] = field(default_factory=list) |
|
|
| |
| |
| |
| |
| |
| voltage_bounds: Dict[str, Dict[str, float]] = field(default_factory=lambda: { |
| "vg1": {"min": -8.0, "max": 8.0}, |
| "vg2": {"min": -8.0, "max": 8.0}, |
| }) |
| step_caps: Dict[str, float] = field(default_factory=lambda: {"l1_max": 0.10}) |
|
|
| belief: BeliefState = field(default_factory=BeliefState) |
|
|
| measurements: Dict[UUID, Measurement] = field(default_factory=dict) |
| dqc_results: Dict[UUID, DQCResult] = field(default_factory=dict) |
| classifications: Dict[UUID, Classification] = field(default_factory=dict) |
| ood_history: List[OODResult] = field(default_factory=list) |
|
|
| last_classification: Optional[Classification] = None |
| last_ood: Optional[OODResult] = None |
| last_dqc: Optional[DQCResult] = None |
|
|
| bo_history: List[BOPoint] = field(default_factory=list) |
|
|
| decisions: List[Decision] = field(default_factory=list) |
| hitl_events: List[HITLEvent] = field(default_factory=list) |
| backtrack_log: List[BacktrackEvent] = field(default_factory=list) |
|
|
| stage: TuningStage = TuningStage.BOOTSTRAPPING |
| consecutive_backtracks: int = 0 |
| total_backtracks: int = 0 |
|
|
| total_measurements: int = 0 |
| safety_violations: int = 0 |
| llm_tokens_total: int = 0 |
|
|
| config: Dict[str, Any] = field(default_factory=dict) |
|
|
| @classmethod |
| def new( |
| cls, |
| device_id: str, |
| target_label: ChargeLabel = ChargeLabel.DOUBLE_DOT, |
| voltage_bounds: Optional[Dict] = None, |
| config: Optional[Dict] = None, |
| ) -> "ExperimentState": |
| state = cls(device_id=device_id, target_label=target_label) |
| if voltage_bounds: |
| state.voltage_bounds = voltage_bounds |
| if config: |
| state.config = config |
| state.belief.initialise_uniform() |
| state.trajectory.append(state.current_voltage) |
| return state |
|
|
| def add_measurement(self, m: Measurement) -> None: |
| self.measurements[m.id] = m |
| self._update_measurement_count(m) |
|
|
| def add_dqc_result(self, result: DQCResult) -> None: |
| self.dqc_results[result.measurement_id] = result |
| self.last_dqc = result |
|
|
| def add_classification(self, cls: Classification) -> None: |
| self.classifications[cls.measurement_id] = cls |
| self.last_classification = cls |
| score = cls.confidence if cls.label == self.target_label else 0.0 |
| self.bo_history.append(BOPoint( |
| voltage=self.current_voltage, |
| score=score, |
| label=cls.label, |
| confidence=cls.confidence, |
| step=len(self.decisions), |
| )) |
|
|
| def add_ood_result(self, result: OODResult) -> None: |
| self.ood_history.append(result) |
| self.last_ood = result |
|
|
| def add_decision(self, d: Decision) -> None: |
| self.decisions.append(d) |
| self.llm_tokens_total += d.llm_tokens_used |
|
|
| def add_hitl_event(self, event: HITLEvent) -> None: |
| self.hitl_events.append(event) |
|
|
| def apply_move(self, safe_delta: VoltagePoint) -> None: |
| self.current_voltage = VoltagePoint( |
| vg1=self.current_voltage.vg1 + safe_delta.vg1, |
| vg2=self.current_voltage.vg2 + safe_delta.vg2, |
| ) |
| self.trajectory.append(self.current_voltage) |
|
|
| def record_backtrack(self, event: BacktrackEvent) -> None: |
| self.backtrack_log.append(event) |
| self.consecutive_backtracks += 1 |
| self.total_backtracks += 1 |
|
|
| def advance_stage(self, new_stage: TuningStage) -> None: |
| self.stage = new_stage |
| self.consecutive_backtracks = 0 |
|
|
| def record_safety_violation(self) -> None: |
| self.safety_violations += 1 |
|
|
| @property |
| def step(self) -> int: |
| return len(self.decisions) |
|
|
| @property |
| def last_confidence(self) -> float: |
| if self.last_classification is None: |
| return 0.0 |
| return self.last_classification.confidence |
|
|
| @property |
| def last_label(self) -> ChargeLabel: |
| if self.last_classification is None: |
| return ChargeLabel.UNKNOWN |
| return self.last_classification.label |
|
|
| @property |
| def is_ood(self) -> bool: |
| if self.last_ood is None: |
| return False |
| return self.last_ood.flag |
|
|
| @property |
| def target_achieved(self) -> bool: |
| return ( |
| self.last_label == self.target_label |
| and self.last_confidence >= self.config.get("Ct_high", 0.85) |
| ) |
|
|
| @property |
| def elapsed_s(self) -> float: |
| return time.time() - self.started_at |
|
|
| def current_belief_summary(self) -> str: |
| most_likely = self.belief.most_likely_state() |
| entropy = self.belief.entropy() |
| last_cls = self.last_classification |
| return ( |
| f"Step {self.step} | Stage: {self.stage.name} | " |
| f"Voltage: ({self.current_voltage.vg1:.3f}, {self.current_voltage.vg2:.3f}) | " |
| f"Most likely charge state: {most_likely} | " |
| f"Belief entropy: {entropy:.2f} | " |
| f"Last label: {last_cls.label.value if last_cls else 'none'} @ " |
| f"{self.last_confidence:.1%} confidence | " |
| f"OOD: {self.is_ood}" |
| ) |
|
|
| def _update_measurement_count(self, m: Measurement) -> None: |
| if m.modality == MeasurementModality.LINE_SCAN: |
| self.total_measurements += m.steps or 128 |
| elif m.is_2d: |
| res = m.resolution or 32 |
| self.total_measurements += res * res |
|
|