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()