From 05421bf944ac44bdfd08bd8d63bc77b49488a859 Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 24 Jun 2026 23:05:28 +0200 Subject: [PATCH] new_gui: data-collection bookmarks (mark / go / collect-from) Mark smargon positions during data collection (numbered markers on the camera, projected via smargon_to_picture and tracked through rotation). Per-bookmark: Go (move_smargon + set_omega) and Collect (rotation scan with start = bookmark, i.e. launch data collection from that point). Cleared on unmount. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/aare/gui/new_gui/README.md | 4 + src/aare/gui/new_gui/main_window.py | 7 ++ src/aare/gui/new_gui/manual_view.py | 73 ++++++++++++++ src/aare/gui/new_gui/requests.py | 5 +- src/aare/gui/new_gui/widgets/bookmarks.py | 95 +++++++++++++++++++ src/aare/gui/new_gui/widgets/camera.py | 24 +++++ .../gui/new_gui/widgets/pipeline_panel.py | 6 ++ 7 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/aare/gui/new_gui/widgets/bookmarks.py diff --git a/src/aare/gui/new_gui/README.md b/src/aare/gui/new_gui/README.md index 91901d14..c65192fb 100644 --- a/src/aare/gui/new_gui/README.md +++ b/src/aare/gui/new_gui/README.md @@ -78,6 +78,10 @@ Backend contract is documented in the `new-gui-backend-paths` memory. - **Preconditions**: data-collection actions are gated by a ring/shutter/hutch warning (1-hour snooze); mounting is hard-blocked by the hutch PSS; `sample_missing` shows a modal (suppressed during automation). +- **Bookmarks** (mounted): + Bookmark captures the current smargon position + (shown as numbered markers on the camera, projected via `smargon_to_picture`); + **Go** returns there (`move_smargon` + `set_omega`); **Collect** launches a + rotation scan with `start` = the bookmark (data collection *from* that point). - **Mount / Unmount** → `mount(SampleShortInfo)` / `unmount()`. - **Motors** → `set_omega_rel`, `move_smargon`, `zoom`, `front_light`, `back_light`, and a staff-only **Energy** control → `change_energy` (keV). diff --git a/src/aare/gui/new_gui/main_window.py b/src/aare/gui/new_gui/main_window.py index 3f672290..875acff4 100644 --- a/src/aare/gui/new_gui/main_window.py +++ b/src/aare/gui/new_gui/main_window.py @@ -201,6 +201,7 @@ class MainWindow(QWidget): self.manual.smart_params_requested.connect(self._on_smart_params) self.manual.xrf_scan_requested.connect(self._do_xrf) self.manual.raster_goto_requested.connect(self.daq.move_smargon) + self.manual.bookmark_goto_requested.connect(self._on_bookmark_goto) self.manual.status_message.connect(self._note) if hasattr(self.daq, "run_number_incremented"): self.daq.run_number_incremented.connect( @@ -620,6 +621,12 @@ class MainWindow(QWidget): return self.daq.move_smargon(SmargonCoordinate(sh_mm=sh)) + def _on_bookmark_goto(self, coord, omega_deg: float) -> None: + """Return to a bookmarked position (move smargon, then omega).""" + self.daq.move_smargon(coord) + if hasattr(self.daq, "set_omega"): + self.daq.set_omega(float(omega_deg)) + def _on_smargon_angles(self, chi_deg: float, phi_deg: float) -> None: """Move to absolute chi/phi, preserving the current sample-holder position.""" from aare.common.coordinate import SmargonCoordinate diff --git a/src/aare/gui/new_gui/manual_view.py b/src/aare/gui/new_gui/manual_view.py index 1c8a1cfa..588c95f6 100644 --- a/src/aare/gui/new_gui/manual_view.py +++ b/src/aare/gui/new_gui/manual_view.py @@ -37,6 +37,7 @@ class ManualView(QWidget): smart_params_requested = Signal(object) # SimpleScanParameters (automate mode) xrf_scan_requested = Signal(object) # FluorescenceSpectrumParameterModel raster_goto_requested = Signal(object) # SmargonCoordinate (goto a grid) + bookmark_goto_requested = Signal(object, float) # SmargonCoordinate, omega_deg status_message = Signal(str) # transient note to surface in UI def __init__(self, state: AppState, palette: Palette, defaults: dict, parent=None): @@ -48,6 +49,7 @@ class ManualView(QWidget): self._grid_rect_img: tuple[float, float, float, float] | None = None self._raster_grids: list = [] self._grids_dialog = None + self._bookmarks: list = [] # [{coord: SmargonCoordinate, omega: float}] root = QHBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) @@ -159,6 +161,13 @@ class ManualView(QWidget): t.end.connect(s.end) t.unmount.connect(s.unmount) + # bookmarks + bm = self.pipeline.bookmarks + bm.bookmark_add.connect(self._on_bookmark_add) + bm.bookmark_goto.connect(self._on_bookmark_goto) + bm.bookmark_collect.connect(self._on_bookmark_collect) + bm.bookmark_remove.connect(self._on_bookmark_remove) + # ------------------------------------------------------- run handlers def _on_run_center(self, method: str) -> None: # Only the automatic loop-centering method remains a button; click-to- @@ -297,6 +306,65 @@ class ManualView(QWidget): else: self.status_message.emit("Grid has no stored position to go to.") + # ----------------------------------------------------- bookmarks + def _on_bookmark_add(self) -> None: + from copy import deepcopy + geom = getattr(self._last_status, "geom", None) + smg = getattr(geom, "smargon", None) if geom else None + if smg is None or getattr(smg, "sh_mm", None) is None: + self.status_message.emit("No smargon position yet; cannot bookmark.") + return + omega = getattr(geom, "omega_deg", 0.0) or 0.0 + self._bookmarks.append({"coord": deepcopy(smg), "omega": float(omega)}) + self.pipeline.bookmarks.set_bookmarks(len(self._bookmarks)) + self._project_bookmarks() + + def _on_bookmark_goto(self, index: int) -> None: + if 0 <= index < len(self._bookmarks): + bm = self._bookmarks[index] + self.bookmark_goto_requested.emit(bm["coord"], bm["omega"]) + + def _on_bookmark_collect(self, index: int) -> None: + if not (0 <= index < len(self._bookmarks)): + return + bm = self._bookmarks[index] + cs = self.pipeline.settings + req = requests.build_rotation_request( + collect_params=cs.collect_params(), + status=self._last_status, + exp_time_s=float(self._defaults.get("rotation_exp_time_s", + self._defaults.get("exp_time_s", 0.02))), + start_omega_deg=bm["omega"], + file_prefix=self.pipeline.filename.file_prefix("data"), + start=bm["coord"], + ) + self._state.run_now("collect") + self.rotation_scan_requested.emit(req) + + def _on_bookmark_remove(self, index: int) -> None: + if 0 <= index < len(self._bookmarks): + del self._bookmarks[index] + self.pipeline.bookmarks.set_bookmarks(len(self._bookmarks)) + self._project_bookmarks() + + def _project_bookmarks(self) -> None: + from aare.gui.new_gui.widgets.bookmarks import BOOKMARK_COLORS + geom = getattr(self._last_status, "geom", None) + if geom is None or not hasattr(geom, "smargon_to_picture"): + self.camera.set_bookmarks([]) + return + items = [] + for i, bm in enumerate(self._bookmarks): + sh = getattr(bm["coord"], "sh_mm", None) + if sh is None: + continue + try: + px = geom.smargon_to_picture(sh) + except Exception: + continue + items.append((px.x, px.y, i + 1, BOOKMARK_COLORS[i % len(BOOKMARK_COLORS)])) + self.camera.set_bookmarks(items) + # ------------------------------------------------------- refreshers def _refresh_mount(self) -> None: phase = self._state.mount_phase @@ -316,6 +384,9 @@ class ManualView(QWidget): if phase != "mounted": self.camera.clear_grid() self._grid_rect_img = None + self._bookmarks = [] # bookmarks are per-sample + self.pipeline.bookmarks.set_bookmarks(0) + self.camera.set_bookmarks([]) self.pipeline.update_from_state(self._state) def _refresh_pipe(self) -> None: @@ -346,6 +417,8 @@ class ManualView(QWidget): self._last_status = s self.motors.update_daq_status(s) self.camera_controls.update_daq_status(s) + if self._bookmarks: + self._project_bookmarks() bl = getattr(s, "bl", None) if bl is not None and getattr(bl, "zoom", None) is not None: self.camera.set_zoom(bl.zoom) diff --git a/src/aare/gui/new_gui/requests.py b/src/aare/gui/new_gui/requests.py index aa25c6c6..e581914b 100644 --- a/src/aare/gui/new_gui/requests.py +++ b/src/aare/gui/new_gui/requests.py @@ -117,7 +117,10 @@ def build_rotation_request( exp_time_s: float, start_omega_deg: float, file_prefix: str | None = None, + start=None, ) -> RotationScanRequest: + """``start`` (SmargonCoordinate | None) lets a bookmark launch collection from + that position; None starts where the goniometer currently is.""" return RotationScanRequest( exp_time_s=exp_time_s, start_omega_deg=float(start_omega_deg), @@ -125,7 +128,7 @@ def build_rotation_request( steps=int(collect_params["steps"]), dtz=collect_params.get("dtz"), transmission=collect_params.get("transmission"), - start=None, + start=start, end=None, screening=False, file_prefix=file_prefix, diff --git a/src/aare/gui/new_gui/widgets/bookmarks.py b/src/aare/gui/new_gui/widgets/bookmarks.py new file mode 100644 index 00000000..8fa22d3a --- /dev/null +++ b/src/aare/gui/new_gui/widgets/bookmarks.py @@ -0,0 +1,95 @@ +"""Data-collection bookmarks: mark smargon positions, return to them or collect. + +A compact bar: "+ Bookmark" captures the current position; each saved bookmark +is a chip with Go (move there), Collect (launch data collection from there) and +remove. Markers are drawn on the camera by the Manual view. +""" + +from __future__ import annotations + +from PySide6.QtCore import Qt, Signal +from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QWidget + +# Bookmark colours (match the old SmargonBookmark palette). +BOOKMARK_COLORS = ("#cf222e", "#1a7f37", "#4d8bff", "#6d4bd1", "#9bcc3f") + + +class BookmarksBar(QWidget): + bookmark_add = Signal() + bookmark_goto = Signal(int) + bookmark_collect = Signal(int) + bookmark_remove = Signal(int) + + def __init__(self, palette, parent=None): + super().__init__(parent) + self._p = palette + self._lay = QHBoxLayout(self) + self._lay.setContentsMargins(0, 0, 0, 0) + self._lay.setSpacing(8) + + from aare.gui.new_gui.widgets.common import micro_label + self._lay.addWidget(micro_label("BOOKMARKS")) + add = QPushButton("+ Bookmark") + add.setCursor(Qt.PointingHandCursor) + add.setStyleSheet( + f"QPushButton {{ background:{palette.surface};" + f" border:1px solid {palette.border_control}; border-radius:8px;" + f" padding:6px 12px; font-size:12px; color:{palette.text_secondary}; }}") + add.clicked.connect(self.bookmark_add) + self._lay.addWidget(add) + + self._chips_from = self._lay.count() # where chips start + self._lay.addStretch(1) + + def set_bookmarks(self, count: int) -> None: + # remove existing chips (keep label + add button + trailing stretch) + while self._lay.count() > self._chips_from + 1: + item = self._lay.takeAt(self._chips_from) + w = item.widget() + if w: + w.setParent(None) + w.deleteLater() + for i in range(count): + self._lay.insertWidget(self._lay.count() - 1, self._make_chip(i)) + + def _make_chip(self, i: int) -> QWidget: + p = self._p + color = BOOKMARK_COLORS[i % len(BOOKMARK_COLORS)] + chip = QWidget() + chip.setStyleSheet( + f"background:{p.surface}; border:1px solid {p.border_control};" + f" border-radius:9px;") + cl = QHBoxLayout(chip) + cl.setContentsMargins(8, 3, 6, 3) + cl.setSpacing(6) + dot = QLabel(str(i + 1)) + dot.setFixedSize(16, 16) + dot.setAlignment(Qt.AlignCenter) + dot.setStyleSheet( + f"background:{color}; color:#fff; border-radius:8px; font-size:10px;" + f" font-weight:700;") + cl.addWidget(dot) + go = self._mini("Go", p) + go.clicked.connect(lambda _=False, ix=i: self.bookmark_goto.emit(ix)) + col = self._mini("Collect", p, accent=True) + col.clicked.connect(lambda _=False, ix=i: self.bookmark_collect.emit(ix)) + rm = self._mini("✕", p, danger=True) + rm.clicked.connect(lambda _=False, ix=i: self.bookmark_remove.emit(ix)) + cl.addWidget(go) + cl.addWidget(col) + cl.addWidget(rm) + return chip + + def _mini(self, text: str, p, accent: bool = False, danger: bool = False) -> QPushButton: + b = QPushButton(text) + b.setCursor(Qt.PointingHandCursor) + if accent: + css = f"background:{p.accent}; color:#fff; border:none;" + elif danger: + css = f"background:transparent; color:{p.danger}; border:none;" + else: + css = (f"background:{p.surface}; border:1px solid {p.border_control};" + f" color:{p.text_secondary};") + b.setStyleSheet( + f"QPushButton {{ {css} border-radius:6px; padding:3px 8px; font-size:11px; }}") + return b diff --git a/src/aare/gui/new_gui/widgets/camera.py b/src/aare/gui/new_gui/widgets/camera.py index 70e2f76d..f26234b9 100644 --- a/src/aare/gui/new_gui/widgets/camera.py +++ b/src/aare/gui/new_gui/widgets/camera.py @@ -61,6 +61,7 @@ class CameraViewport(QWidget): self._detections: list = [] # raw boxes from prediction self._det_shape = None # [h, w] of the inference image self._show_detections = True + self._bookmarks: list = [] # [(x_px, y_px, label, color)] # Raster grid-draw state self._grid_draw_enabled = False @@ -254,6 +255,11 @@ class CameraViewport(QWidget): self._beam_busy = bool(busy) self.update() + def set_bookmarks(self, items: list) -> None: + """items: list of (x_px, y_px, label, color_hex) in image coords.""" + self._bookmarks = items or [] + self.update() + def update_predictions(self, payload: dict) -> None: if not isinstance(payload, dict): return @@ -387,8 +393,26 @@ class CameraViewport(QWidget): if self._show_detections and self._mode == "mounted": self._paint_detections(painter) + self._paint_bookmarks(painter) self._paint_grid(painter) + def _paint_bookmarks(self, painter: QPainter) -> None: + if not self._bookmarks or self._pixmap is None or self._pixmap.isNull(): + return + f = QFont() + f.setPixelSize(10) + f.setBold(True) + painter.setFont(f) + for x_px, y_px, label, color in self._bookmarks: + c = self._image_to_widget(QPoint(int(x_px), int(y_px))) + col = QColor(color) + painter.setBrush(col) + painter.setPen(QPen(QColor("#ffffff"), 1)) + painter.drawEllipse(c, 7, 7) + painter.setPen(QColor("#ffffff")) + painter.drawText(QRect(c.x() - 7, c.y() - 6, 14, 13), + Qt.AlignCenter, str(label)) + def _paint_detections(self, painter: QPainter) -> None: if not self._detections or self._pixmap is None or self._pixmap.isNull(): return diff --git a/src/aare/gui/new_gui/widgets/pipeline_panel.py b/src/aare/gui/new_gui/widgets/pipeline_panel.py index 8b0b95b5..ca57e4f4 100644 --- a/src/aare/gui/new_gui/widgets/pipeline_panel.py +++ b/src/aare/gui/new_gui/widgets/pipeline_panel.py @@ -662,11 +662,17 @@ class PipelinePanel(QWidget): self.transport = TransportRow(palette) lay.addWidget(self.transport) + # data-collection bookmarks + from aare.gui.new_gui.widgets.bookmarks import BookmarksBar + self.bookmarks = BookmarksBar(palette) + lay.addWidget(self.bookmarks) + self.set_mounted(False) def set_mounted(self, mounted: bool) -> None: self._mounted = mounted self._badge.setVisible(mounted) + self.bookmarks.setVisible(mounted) self.transport.setVisible(mounted) self.settings.set_run_enabled(mounted) self.tracker.set_preview(not mounted)