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) <noreply@anthropic.com>
This commit is contained in:
appleb_m
2026-06-24 23:05:28 +02:00
co-authored by Claude Opus 4.8
parent 8a5bdf142a
commit 05421bf944
7 changed files with 213 additions and 1 deletions
+4
View File
@@ -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).
+7
View File
@@ -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
+73
View File
@@ -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)
+4 -1
View File
@@ -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,
+95
View File
@@ -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
+24
View File
@@ -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
@@ -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)