Source code for semi_cr.core.lab.virtualization.gate_virtualization

from __future__ import annotations

from typing import TYPE_CHECKING

import dearpygui.dearpygui as dpg
import numpy as np

from semi_cr.apps.spincontrol.matrix_viz import MatrixEditor

if TYPE_CHECKING:
    from semi_cr.core.lab.gdsfile import GDSFile
    from semi_cr.core.lab.virtualization.virtual_gate_layer import VirtualGateLayer


[docs] class M1Editor(MatrixEditor): """Editor for MAViS M_1 sensor compensation matrix. The M_1 matrix defines sensor compensation - how virtual gates map to physical gates. Rows are physical gates (from GDS file), columns are virtual gates (v-prefixed). Voltage transformation: physical_voltages = M1 @ virtual_voltages Example: For gates [P1, P2, P3], creates a 3x3 identity matrix with: - Row headers: P1, P2, P3 - Column headers: vP1, vP2, vP3 When a VirtualGateLayer is provided via the ``layer`` argument, the editor drives the layer directly: voltage inputs set virtual gate Parameters on the layer, and matrix cell edits call ``layer.set_matrix()``. Physical voltage display is then read back from the layer's downstream parameters. When ``layer`` is None the editor works as a standalone calculator. """ def __init__( self, gds: GDSFile | None = None, gate_names: list[str] | None = None, layer: VirtualGateLayer | None = None, ): if gds is not None: gate_names = gds.get_gate_names() elif gate_names is None: raise ValueError("Must provide either gds or gate_names") self.gate_names = gate_names n = len(gate_names) matrix = np.identity(n) row_names = gate_names col_names = [f"v{name}" for name in gate_names] super().__init__(matrix, row_names=row_names, col_names=col_names) self._layer = layer # Virtual and physical voltage arrays self.virtual_voltages = np.zeros(n) self.physical_voltages = np.zeros(n) # Widget IDs for voltage displays self.virtual_input_ids: list[int] = [] self.physical_text_ids: list[int] = [] # Store base title for mode indicator self._base_title = "Charge Sensor Virtualization"
[docs] def get_title(self) -> str: if self.inverted_mode: return f"{self._base_title} (M\u2071\u207b\u00b9)" return self._base_title
def _on_invert_mode_changed(self): """Update window title when invert mode changes.""" if self.window_id is not None: dpg.set_item_label(self.window_id, self.get_title())
[docs] def draw(self, parent_id: int | None = None): """Draw the matrix editor with virtual/physical voltage controls.""" with self._get_container(parent_id): with dpg.group(horizontal=True): # Left: M1 matrix table in a child window with dpg.child_window( width=1200, autosize_y=True, border=False, no_scrollbar=True, no_scroll_with_mouse=True, ): self._draw_matrix_table() # Right: Voltage calculator in a child window with dpg.child_window( width=300, autosize_y=True, border=False, no_scrollbar=True, no_scroll_with_mouse=True, ): dpg.add_text("Voltage Calculator") with dpg.group(horizontal=True): # Virtual voltages (input) with dpg.group(): dpg.add_text("Virtual") for i, name in enumerate(self.col_names): with dpg.group(horizontal=True): dpg.add_text(f"{name}:") input_id = dpg.add_input_float( default_value=0.0, width=80, step=0, format="%.3f", callback=self._on_virtual_voltage_changed, user_data=i, ) self.virtual_input_ids.append(input_id) dpg.add_spacer(width=20) # Physical voltages (output) with dpg.group(): dpg.add_text("Physical") for i, name in enumerate(self.row_names): with dpg.group(horizontal=True): dpg.add_text(f"{name}:") text_id = dpg.add_text("0.000", color=(150, 255, 150)) self.physical_text_ids.append(text_id) # Create popup for keyboard input (must be top-level, outside container) with dpg.window( show=False, no_title_bar=True, no_move=True, no_resize=True, no_scrollbar=True, no_collapse=True, autosize=True, ) as self.edit_popup_id: self.edit_input_id = dpg.add_input_double( width=120, min_value=self.vmin, max_value=self.vmax, min_clamped=True, max_clamped=True, step=self.step, ) # Match font to matrix window dpg.bind_item_font(self.edit_popup_id, "ttf-font-small")
def _draw_matrix_table(self): """Draw the matrix table (extracted from MatrixEditor.draw).""" with dpg.table( header_row=True, resizable=False, policy=dpg.mvTable_SizingStretchProp, borders_outerH=True, borders_innerV=True, borders_innerH=True, borders_outerV=True, ) as self.table_id: # Header row: empty corner + column labels dpg.add_table_column(label="") for j in range(self.c): dpg.add_table_column(label=self.col_names[j]) # Data rows with row header in first column for i in range(self.r): row_items = [] with dpg.table_row(): dpg.add_text(self.row_names[i]) # Row header for j in range(self.c): cell_id = dpg.add_selectable( label=f"{self.M[i][j]:.02f}", span_columns=False, disable_popup_close=True, ) # Use item handler for click detection with dpg.item_handler_registry() as handler: dpg.add_item_clicked_handler( callback=self._on_cell_clicked, user_data=(i, j), ) dpg.bind_item_handler_registry(cell_id, handler) row_items.append(cell_id) self.cell_items.append(row_items) self._update_all_colors() def _on_virtual_voltage_changed(self, sender, app_data, user_data): """Callback when a virtual voltage input changes.""" idx = user_data if self._layer is not None: gate_name = self.col_names[idx] getattr(self._layer, gate_name).voltage.set(app_data) self.virtual_voltages[idx] = app_data self.physical_voltages = np.array([p.get() for p in self._layer._downstream]) else: self.virtual_voltages[idx] = app_data self._recalculate_physical_voltages() self._update_physical_display() def _recalculate_physical_voltages(self): """Recalculate physical voltages: P = M1 @ V (standalone mode only).""" self.physical_voltages = self.M @ self.virtual_voltages def _update_physical_display(self): """Update the physical voltage text displays.""" for i, text_id in enumerate(self.physical_text_ids): dpg.set_value(text_id, f"{self.physical_voltages[i]:.3f}") def _sync_from_layer(self): """Pull current matrix from the layer into self.M (for external matrix changes).""" if self._layer is not None: self.M = self._layer.matrix def _update_cell(self, i: int, j: int): """Override to also update the layer matrix and recalculate physical voltages.""" super()._update_cell(i, j) if self._layer is not None: self._layer.set_matrix(self.M) self.physical_voltages = np.array([p.get() for p in self._layer._downstream]) else: self._recalculate_physical_voltages() self._update_physical_display()
[docs] def update(self): """Sync matrix from layer before any update.""" self._sync_from_layer() super().update()