Compare commits
70 Commits
feat/add_p
...
fix/fixes_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9fc3094b6 | ||
|
|
88df4781ec | ||
|
|
3b474c89c8 | ||
|
|
68cc13e1d3 | ||
|
|
700f3f9bb9 | ||
|
|
15a4d45f68 | ||
|
|
7c7f877d78 | ||
|
|
5d61d756c9 | ||
|
|
b37ae3ef57 | ||
|
|
76ed858e5c | ||
|
|
a0555def4d | ||
|
|
c1ad2fc4c3 | ||
|
|
9eee4ee1f7
|
||
|
c97b00cc8c
|
|||
|
d6a4fd37fc
|
|||
|
6d4c9d90fc
|
|||
| 87163cc3f1 | |||
| 7c17a3ae40 | |||
| 663d22fff4 | |||
| 3ca29dd0dd | |||
| 0ac37f538b | |||
|
|
43ae732e34 | ||
|
|
583b15b772 | ||
|
|
4967474271 | ||
|
|
8d6a2b0f5c | ||
|
|
dcde0e783e | ||
|
|
58cd6bdaf7 | ||
|
|
68320e1944 | ||
|
|
5ff32decc4 | ||
|
|
2c31d79f1b | ||
|
|
3ce6bbc134 | ||
| 7c89086ba2 | |||
| 1eb2961b7f | |||
| 9d58dcfb83 | |||
| 541813a02e | |||
| 4b04e7a35d | |||
| 00c45b2bcf | |||
| 138b2668b3 | |||
| 31eb00bd97 | |||
|
|
53e7593b8e
|
||
|
|
8c7e1cf060 | ||
|
|
97d62f2f0b | ||
|
|
0ebaa3a42f | ||
|
|
0a01dd4e36 | ||
|
|
a28d9f0e20 | ||
|
|
4388ecc3b2 | ||
|
|
349fa09a32 | ||
|
|
8298218913 | ||
|
|
1e510659a9 | ||
|
|
a765373805 | ||
|
|
0cad98da6d | ||
|
|
6384d690e5 | ||
|
|
c042a52730 | ||
|
|
3490aca053 | ||
| 315a32d9de | |||
|
|
deaa469ce1 | ||
|
|
774fc0dc36 | ||
|
|
8b732a5de6 | ||
|
|
240fcba4ef | ||
| 058dbf5e5b | |||
| 7b882653ad | |||
| be9938ddb7 | |||
| 2a7b068cc6 | |||
| 73d91617e9 | |||
| 6873ef8287 | |||
|
|
70fa96bd58
|
||
|
|
5155ba9b77
|
||
| 488156fd87 | |||
| 4721ec404b | |||
|
4d69f8f90f
|
21
.gitea/workflows/rtd_deploy.yml
Normal file
21
.gitea/workflows/rtd_deploy.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Read the Docs Deploy Trigger
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
trigger-rtd-webhook:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Read the Docs webhook
|
||||
env:
|
||||
RTD_TOKEN: ${{ secrets.RTD_TOKEN }}
|
||||
run: |
|
||||
curl --fail --show-error --silent \
|
||||
-X POST \
|
||||
-d "branches=${{ github.ref_name }}" \
|
||||
-d "token=${RTD_TOKEN}" \
|
||||
"https://readthedocs.org/api/v2/webhook/sls-csaxs/270162/"
|
||||
@@ -8,15 +8,14 @@ version: 2
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
python: "3.12"
|
||||
jobs:
|
||||
pre_install:
|
||||
- pip install .
|
||||
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
configuration: docs/conf.py
|
||||
|
||||
# If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||
# formats:
|
||||
@@ -24,6 +23,5 @@ sphinx:
|
||||
|
||||
# Optionally declare the Python requirements required to build your docs
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
|
||||
BIN
csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI.png
Normal file
BIN
csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 562 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
188
csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py
Normal file
188
csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py
Normal file
@@ -0,0 +1,188 @@
|
||||
import builtins
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea
|
||||
|
||||
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
|
||||
if builtins.__dict__.get("bec") is not None:
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
scans = builtins.__dict__.get("scans")
|
||||
|
||||
def umv(*args):
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
|
||||
class LamniGuiToolsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LamniGuiTools:
|
||||
|
||||
def __init__(self):
|
||||
self.text_box = None
|
||||
self.progressbar = None
|
||||
|
||||
def set_client(self, client):
|
||||
self.client = client
|
||||
self.gui = self.client.gui
|
||||
|
||||
def lamnigui_show_gui(self):
|
||||
if "lamni" in self.gui.windows:
|
||||
self.gui.lamni.show()
|
||||
else:
|
||||
self.gui.new("lamni")
|
||||
|
||||
def lamnigui_stop_gui(self):
|
||||
self.gui.lamni.hide()
|
||||
|
||||
def lamnigui_raise(self):
|
||||
self.gui.lamni.raise_window()
|
||||
|
||||
def lamnigui_show_xeyealign(self):
|
||||
self.lamnigui_show_gui()
|
||||
if self._lamnigui_check_attribute_not_exists("xeyegui"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
self.xeyegui = self.gui.lamni.new("xeyegui").new("XRayEye")
|
||||
# start live
|
||||
if not dev.cam_xeye.live_mode:
|
||||
dev.cam_xeye.live_mode = True
|
||||
|
||||
|
||||
def _lamnigui_check_attribute_not_exists(self, attribute_name):
|
||||
if hasattr(self.gui,"lamni"):
|
||||
if hasattr(self.gui.lamni,attribute_name):
|
||||
return False
|
||||
return True
|
||||
|
||||
def lamnigui_remove_all_docks(self):
|
||||
self.gui.lamni.delete_all()
|
||||
self.progressbar = None
|
||||
self.text_box = None
|
||||
|
||||
def lamnigui_idle(self):
|
||||
self.lamnigui_show_gui()
|
||||
if self._lamnigui_check_attribute_not_exists("idle_text_box"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
idle_text_box = self.gui.lamni.new("idle_textbox").new("TextBox")
|
||||
text = (
|
||||
"<pre>"
|
||||
+ "██████╗ ███████╗ ██████╗ ██╗ █████╗ ███╗ ███╗███╗ ██╗██╗\n"
|
||||
+ "██╔══██╗██╔════╝██╔════╝ ██║ ██╔══██╗████╗ ████║████╗ ██║██║\n"
|
||||
+ "██████╔╝█████╗ ██║ ██║ ███████║██╔████╔██║██╔██╗ ██║██║\n"
|
||||
+ "██╔══██╗██╔══╝ ██║ ██║ ██╔══██║██║╚██╔╝██║██║╚██╗██║██║\n"
|
||||
+ "██████╔╝███████╗╚██████╗ ███████╗██║ ██║██║ ╚═╝ ██║██║ ╚████║██║\n"
|
||||
+ "╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝\n"
|
||||
+ "</pre>"
|
||||
)
|
||||
idle_text_box.set_html_text(text)
|
||||
|
||||
def lamnigui_docs(self, filename: str | None = None):
|
||||
import csaxs_bec
|
||||
from pathlib import Path
|
||||
|
||||
print("The general lamni documentation is at \nhttps://sls-csaxs.readthedocs.io/en/latest/user/ptychography/lamni.html#user-ptychography-lamni")
|
||||
|
||||
csaxs_bec_basepath = Path(csaxs_bec.__file__).parent
|
||||
docs_folder = (
|
||||
csaxs_bec_basepath /
|
||||
"bec_ipython_client" / "plugins" / "lamni" / "docs"
|
||||
)
|
||||
|
||||
if not docs_folder.is_dir():
|
||||
raise NotADirectoryError(f"Docs folder not found: {docs_folder}")
|
||||
|
||||
pdfs = sorted(docs_folder.glob("*.pdf"))
|
||||
if not pdfs:
|
||||
raise FileNotFoundError(f"No PDF files found in {docs_folder}")
|
||||
|
||||
# --- Resolve PDF ------------------------------------------------------
|
||||
if filename is not None:
|
||||
pdf_file = docs_folder / filename
|
||||
if not pdf_file.exists():
|
||||
raise FileNotFoundError(f"Requested file not found: {filename}")
|
||||
else:
|
||||
print("\nAvailable lamni documentation PDFs:\n")
|
||||
for i, pdf in enumerate(pdfs, start=1):
|
||||
print(f" {i:2d}) {pdf.name}")
|
||||
print()
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = int(input(f"Select a file (1–{len(pdfs)}): "))
|
||||
if 1 <= choice <= len(pdfs):
|
||||
pdf_file = pdfs[choice - 1]
|
||||
break
|
||||
print(f"Enter a number between 1 and {len(pdfs)}.")
|
||||
except ValueError:
|
||||
print("Invalid input. Please enter a number.")
|
||||
|
||||
# --- GUI handling (active existence check) ----------------------------
|
||||
self.lamnigui_show_gui()
|
||||
|
||||
if self._lamnigui_check_attribute_not_exists("PdfViewerWidget"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
self.pdf_viewer = self.gui.lamni.new(widget="PdfViewerWidget")
|
||||
|
||||
# --- Load PDF ---------------------------------------------------------
|
||||
self.pdf_viewer.PdfViewerWidget.load_pdf(str(pdf_file.resolve()))
|
||||
print(f"\nLoaded: {pdf_file.name}\n")
|
||||
|
||||
|
||||
def _lamnicam_check_device_exists(self, device):
|
||||
try:
|
||||
device
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def lamnigui_show_progress(self):
|
||||
self.lamnigui_show_gui()
|
||||
if self._lamnigui_check_attribute_not_exists("progressbar"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
# Add a new dock with a RingProgressBar widget
|
||||
self.progressbar = self.gui.lamni.new("progressbar").new("RingProgressBar")
|
||||
# Customize the size of the progress ring
|
||||
self.progressbar.set_line_widths(20)
|
||||
# Disable automatic updates and manually set the self.progressbar value
|
||||
self.progressbar.enable_auto_updates(False)
|
||||
# Set precision for the self.progressbar display
|
||||
self.progressbar.set_precision(1) # Display self.progressbar with one decimal places
|
||||
# Setting multiple rigns with different values
|
||||
self.progressbar.set_number_of_bars(3)
|
||||
self.progressbar.rings[0].set_update("manual")
|
||||
self.progressbar.rings[1].set_update("manual")
|
||||
self.progressbar.rings[2].set_update("scan")
|
||||
# Set the values of the rings to 50, 75, and 25 from outer to inner ring
|
||||
# self.progressbar.set_value([50, 75])
|
||||
# Add a new dock with a TextBox widget
|
||||
self.text_box = self.gui.lamni.new(name="progress_text").new("TextBox")
|
||||
|
||||
self._lamnigui_update_progress()
|
||||
|
||||
def _lamnigui_update_progress(self):
|
||||
if self.progressbar is not None:
|
||||
progress = self.progress["projection"] / self.progress["total_projections"] * 100
|
||||
subtomo_progress = (
|
||||
self.progress["subtomo_projection"]
|
||||
/ self.progress["subtomo_total_projections"]
|
||||
* 100
|
||||
)
|
||||
self.progressbar.set_value([progress, subtomo_progress, 0])
|
||||
if self.text_box is not None:
|
||||
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
|
||||
self.text_box.set_plain_text(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from bec_lib.client import BECClient
|
||||
from bec_widgets.cli.client_utils import BECGuiClient
|
||||
|
||||
client = BECClient()
|
||||
client.start()
|
||||
client.gui = BECGuiClient()
|
||||
|
||||
lamni_gui = LamniGuiTools(client)
|
||||
lamni_gui.lamnigui_show_gui()
|
||||
lamni_gui.lamnigui_show_progress()
|
||||
@@ -2,7 +2,6 @@ import builtins
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -12,7 +11,12 @@ from bec_lib.alarm_handler import AlarmBase
|
||||
from bec_lib.pdf_writer import PDFWriter
|
||||
from typeguard import typechecked
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import OMNYTools
|
||||
from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import (
|
||||
OMNYTools,
|
||||
PtychoReconstructor,
|
||||
TomoIDManager,
|
||||
)
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI.gui_tools import LamniGuiTools
|
||||
|
||||
from .alignment import XrayEyeAlign
|
||||
from .lamni_optics_mixin import LaMNIInitStages, LamNIOpticsMixin
|
||||
@@ -23,29 +27,29 @@ if builtins.__dict__.get("bec") is not None:
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
umv = builtins.__dict__.get("umv")
|
||||
mv = builtins.__dict__.get("mv")
|
||||
umvr = builtins.__dict__.get("umvr")
|
||||
|
||||
|
||||
class LamNI(LamNIOpticsMixin):
|
||||
class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
def __init__(self, client):
|
||||
super().__init__()
|
||||
self.client = client
|
||||
self.device_manager = client.device_manager
|
||||
self.align = XrayEyeAlign(client, self)
|
||||
self.init = LaMNIInitStages(client)
|
||||
self.check_shutter = True
|
||||
self.check_light_available = True
|
||||
self.check_fofb = True
|
||||
self._check_msgs = []
|
||||
|
||||
# Extracted collaborators
|
||||
self.reconstructor = PtychoReconstructor(self.ptycho_reconstruct_foldername)
|
||||
self.tomo_id_manager = TomoIDManager()
|
||||
self.OMNYTools = OMNYTools(self.client)
|
||||
|
||||
self.tomo_id = -1
|
||||
self.special_angles = []
|
||||
self.special_angle_repeats = 20
|
||||
self.special_angle_tolerance = 20
|
||||
self._current_special_angles = []
|
||||
self._beam_is_okay = True
|
||||
self._stop_beam_check_event = None
|
||||
self.beam_check_thread = None
|
||||
self.OMNYTools = OMNYTools(self.client)
|
||||
|
||||
# Progress tracking
|
||||
self.progress = {}
|
||||
self.progress["tomo_type"] = "Equally spaced sub-tomograms"
|
||||
@@ -56,105 +60,6 @@ class LamNI(LamNIOpticsMixin):
|
||||
self.progress["total_projections"] = 1
|
||||
self.progress["angle"] = 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Beamline checks
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_beamline_checks_enabled(self):
|
||||
print(
|
||||
f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available:"
|
||||
f" {self.check_light_available}"
|
||||
)
|
||||
|
||||
@property
|
||||
def beamline_checks_enabled(self):
|
||||
return {
|
||||
"shutter": self.check_shutter,
|
||||
"fofb": self.check_fofb,
|
||||
"light available": self.check_light_available,
|
||||
}
|
||||
|
||||
@beamline_checks_enabled.setter
|
||||
def beamline_checks_enabled(self, val: bool):
|
||||
self.check_shutter = val
|
||||
self.check_light_available = val
|
||||
self.check_fofb = val
|
||||
self.get_beamline_checks_enabled()
|
||||
|
||||
def _run_beamline_checks(self):
|
||||
msgs = []
|
||||
dev = builtins.__dict__.get("dev")
|
||||
try:
|
||||
if self.check_shutter:
|
||||
shutter_val = dev.x12sa_es1_shutter_status.read(cached=True)
|
||||
if shutter_val["value"].lower() != "open":
|
||||
self._beam_is_okay = False
|
||||
msgs.append("Check beam failed: Shutter is closed.")
|
||||
if self.check_light_available:
|
||||
machine_status = dev.sls_machine_status.read(cached=True)
|
||||
if machine_status["value"] not in ["Light Available", "Light-Available"]:
|
||||
self._beam_is_okay = False
|
||||
msgs.append("Check beam failed: Light not available.")
|
||||
if self.check_fofb:
|
||||
fast_orbit_feedback = dev.sls_fast_orbit_feedback.read(cached=True)
|
||||
if fast_orbit_feedback["value"] != "running":
|
||||
self._beam_is_okay = False
|
||||
msgs.append("Check beam failed: Fast orbit feedback is not running.")
|
||||
except Exception:
|
||||
logger.warning("Failed to check beam.")
|
||||
return msgs
|
||||
|
||||
def _check_beam(self):
|
||||
while not self._stop_beam_check_event.is_set():
|
||||
self._check_msgs = self._run_beamline_checks()
|
||||
if not self._beam_is_okay:
|
||||
self._stop_beam_check_event.set()
|
||||
time.sleep(1)
|
||||
|
||||
def _start_beam_check(self):
|
||||
self._beam_is_okay = True
|
||||
self._stop_beam_check_event = threading.Event()
|
||||
self.beam_check_thread = threading.Thread(target=self._check_beam, daemon=True)
|
||||
self.beam_check_thread.start()
|
||||
|
||||
def _was_beam_okay(self):
|
||||
self._stop_beam_check_event.set()
|
||||
self.beam_check_thread.join()
|
||||
return self._beam_is_okay
|
||||
|
||||
def _print_beamline_checks(self):
|
||||
for msg in self._check_msgs:
|
||||
logger.warning(msg)
|
||||
|
||||
def _wait_for_beamline_checks(self):
|
||||
self._print_beamline_checks()
|
||||
try:
|
||||
msg = bec.logbook.LogbookMessage()
|
||||
msg.add_text(
|
||||
"<p><mark class='pen-red'><strong>Beamline checks failed at"
|
||||
f" {str(datetime.datetime.now())}: {''.join(self._check_msgs)}</strong></mark></p>"
|
||||
).add_tag(["BEC", "beam_check"])
|
||||
self.client.logbook.send_logbook_message(msg)
|
||||
except Exception:
|
||||
logger.warning("Failed to send update to SciLog.")
|
||||
|
||||
while True:
|
||||
self._beam_is_okay = True
|
||||
self._check_msgs = self._run_beamline_checks()
|
||||
if self._beam_is_okay:
|
||||
break
|
||||
self._print_beamline_checks()
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
msg = bec.logbook.LogbookMessage()
|
||||
msg.add_text(
|
||||
"<p><mark class='pen-red'><strong>Operation resumed at"
|
||||
f" {str(datetime.datetime.now())}.</strong></mark></p>"
|
||||
).add_tag(["BEC", "beam_check"])
|
||||
self.client.logbook.send_logbook_message(msg)
|
||||
except Exception:
|
||||
logger.warning("Failed to send update to SciLog.")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Special angles
|
||||
@@ -212,7 +117,6 @@ class LamNI(LamNIOpticsMixin):
|
||||
def feedback_status(self):
|
||||
self.device_manager.devices.rtx.controller.show_feedback_status()
|
||||
|
||||
|
||||
def show_interferometer_positions(self):
|
||||
self.device_manager.devices.rtx.controller.show_feedback_status()
|
||||
|
||||
@@ -222,6 +126,12 @@ class LamNI(LamNIOpticsMixin):
|
||||
def show_analog_signals(self):
|
||||
return self.device_manager.devices.rtx.controller.show_analog_signals()
|
||||
|
||||
def lights_off(self):
|
||||
self.device_manager.devices.lsamx.controller.lights_off()
|
||||
|
||||
def lights_on(self):
|
||||
self.device_manager.devices.lsamx.controller.lights_on()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Global parameters (backed by BEC global vars)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -371,6 +281,7 @@ class LamNI(LamNIOpticsMixin):
|
||||
@ptycho_reconstruct_foldername.setter
|
||||
def ptycho_reconstruct_foldername(self, val: str):
|
||||
self.client.set_global_var("ptycho_reconstruct_foldername", val)
|
||||
self.reconstructor.folder_name = val # keep reconstructor in sync
|
||||
|
||||
@property
|
||||
def tomo_angle_stepsize(self):
|
||||
@@ -445,18 +356,6 @@ class LamNI(LamNIOpticsMixin):
|
||||
# Logging helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def write_to_spec_log(self, content):
|
||||
try:
|
||||
with open(
|
||||
os.path.expanduser(
|
||||
"~/Data10/specES1/log-files/specES1_started_2022_11_30_1313.log"
|
||||
),
|
||||
"a",
|
||||
) as log_file:
|
||||
log_file.write(content)
|
||||
except Exception:
|
||||
logger.warning("Failed to write to spec log file (omny web page).")
|
||||
|
||||
def write_to_scilog(self, content, tags: list = None):
|
||||
try:
|
||||
if tags is not None:
|
||||
@@ -491,23 +390,22 @@ class LamNI(LamNIOpticsMixin):
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Sample database
|
||||
# Sample database — delegated to TomoIDManager in omny general tools
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def add_sample_database(
|
||||
self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user
|
||||
):
|
||||
"""Add a sample to the OMNY sample database and retrieve the tomo id."""
|
||||
subprocess.run(
|
||||
"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt"
|
||||
f" 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}"
|
||||
f"&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}"
|
||||
f"&additional={sample_additional_info}&user={user}'",
|
||||
shell=True,
|
||||
return self.tomo_id_manager.register(
|
||||
sample_name=samplename,
|
||||
date=date,
|
||||
eaccount=eaccount,
|
||||
scan_number=scan_number,
|
||||
setup=setup,
|
||||
additional_info=sample_additional_info,
|
||||
user=user,
|
||||
)
|
||||
with open("/tmp/currsamplesnr.txt") as tomo_number_file:
|
||||
tomo_number = int(tomo_number_file.read())
|
||||
return tomo_number
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Scan projection
|
||||
@@ -524,13 +422,11 @@ class LamNI(LamNIOpticsMixin):
|
||||
|
||||
for stitch_x in range(-self.lamni_stitch_x, self.lamni_stitch_x + 1):
|
||||
for stitch_y in range(-self.lamni_stitch_y, self.lamni_stitch_y + 1):
|
||||
# pylint: disable=undefined-variable
|
||||
self._current_scan_list.append(bec.queue.next_scan_number)
|
||||
log_message = (
|
||||
f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle},"
|
||||
f" scan number {bec.queue.next_scan_number}.\n"
|
||||
)
|
||||
self.write_to_spec_log(log_message)
|
||||
corridor_size = self.corridor_size if self.corridor_size > 0 else None
|
||||
scans.lamni_fermat_scan(
|
||||
fov_size=[self.lamni_piezo_range_x, self.lamni_piezo_range_y],
|
||||
@@ -562,18 +458,11 @@ class LamNI(LamNIOpticsMixin):
|
||||
def tomo_reconstruct(self, base_path="~/Data10/specES1"):
|
||||
"""Write the tomo reconstruct file for the reconstruction queue."""
|
||||
bec = builtins.__dict__.get("bec")
|
||||
base_path = os.path.expanduser(base_path)
|
||||
ptycho_queue_path = Path(os.path.join(base_path, self.ptycho_reconstruct_foldername))
|
||||
ptycho_queue_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
last_scan_number = bec.queue.next_scan_number - 1
|
||||
ptycho_queue_file = os.path.abspath(
|
||||
os.path.join(ptycho_queue_path, f"scan_{last_scan_number:05d}.dat")
|
||||
self.reconstructor.write(
|
||||
scan_list=self._current_scan_list,
|
||||
next_scan_number=bec.queue.next_scan_number,
|
||||
base_path=base_path,
|
||||
)
|
||||
with open(ptycho_queue_file, "w") as queue_file:
|
||||
scans = " ".join([str(scan) for scan in self._current_scan_list])
|
||||
queue_file.write(f"p.scan_number {scans}\n")
|
||||
queue_file.write("p.check_nextscan_started 1\n")
|
||||
|
||||
def _at_each_angle(self, angle: float) -> None:
|
||||
self.tomo_scan_projection(angle)
|
||||
@@ -635,7 +524,6 @@ class LamNI(LamNIOpticsMixin):
|
||||
print(f"Starting LamNI scan for angle {angle} in subtomo {subtomo_number}")
|
||||
self._print_progress()
|
||||
while not successful:
|
||||
self._start_beam_check()
|
||||
if not self.special_angles:
|
||||
self._current_special_angles = []
|
||||
if self._current_special_angles:
|
||||
@@ -662,10 +550,9 @@ class LamNI(LamNIOpticsMixin):
|
||||
for scan_nr in range(start_scan_number, end_scan_number):
|
||||
self._write_tomo_scan_number(scan_nr, angle, subtomo_number)
|
||||
|
||||
if self._was_beam_okay() and not error_caught:
|
||||
successful = True
|
||||
else:
|
||||
self._wait_for_beamline_checks()
|
||||
#todo here bl chk, if ok then successfull true
|
||||
successful = True
|
||||
|
||||
|
||||
def _golden(self, ii, howmany_sorted, maxangle=360, reverse=False):
|
||||
"""Return the ii-th golden ratio angle within sorted bunches and its subtomo number."""
|
||||
@@ -879,7 +766,7 @@ class LamNI(LamNIOpticsMixin):
|
||||
|
||||
user_input = input("Are these parameters correctly set for your scan? ")
|
||||
if user_input == "y":
|
||||
print("good then")
|
||||
print("OK. continue.")
|
||||
return
|
||||
|
||||
self.tomo_countingtime = self._get_val("<ctime> s", self.tomo_countingtime, float)
|
||||
@@ -1011,3 +898,39 @@ class LamNI(LamNIOpticsMixin):
|
||||
)
|
||||
self.client.logbook.send_logbook_message(msg)
|
||||
|
||||
|
||||
def get_calibration_of_capstops_left_and_right(self):
|
||||
import time
|
||||
print("""
|
||||
Manual on how to center the Piezo stage first.
|
||||
To obtain the center voltages one can move in closed loop to the interferometer
|
||||
vertically and observe the capacitive readback signal. Check the limits of the
|
||||
travel, move to center and obtain the required centering voltage.
|
||||
Example: At 0 deg, accessible rty -60 to 51. So the init was 5 microns off.
|
||||
Then this routine here will provide data for the new capstop left and right.
|
||||
""")
|
||||
|
||||
angle = 0
|
||||
umv(dev.lsamrot,0)
|
||||
print("Capstop right\nAngle, Voltage1, Voltage2")
|
||||
mv(dev.lsamrot,361)
|
||||
while angle <= 360:
|
||||
angle = dev.lsamrot.readback.get()
|
||||
voltage1=float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[1]"))
|
||||
voltage2=float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[2]"))
|
||||
if angle<360:
|
||||
print(f"{angle},{voltage1},{voltage2}")
|
||||
time.sleep(.3)
|
||||
|
||||
time.sleep(10)
|
||||
print("\nCapstop left\nAngle, Voltage1, Voltage2")
|
||||
mv(dev.lsamrot,-1)
|
||||
while angle > 0:
|
||||
angle = dev.lsamrot.readback.get()
|
||||
voltage1=float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[1]"))
|
||||
voltage2=float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[2]"))
|
||||
if angle>0:
|
||||
print(f"{angle},{voltage1},{voltage2}")
|
||||
time.sleep(.3)
|
||||
|
||||
print("Finished")
|
||||
@@ -5,7 +5,7 @@ from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put
|
||||
from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import OMNYTools
|
||||
|
||||
dev = builtins.__dict__.get("dev")
|
||||
@@ -51,7 +51,7 @@ class LaMNIInitStages:
|
||||
self.drive_axis_to_limit(dev.lsamrot, "forward")
|
||||
dev.lsamrot.enabled = False
|
||||
print("Now hard reboot the controller and run the initialization routine again.")
|
||||
print("The controller will be disabled in bec. To enable dev.lsamrot.enabled=True")
|
||||
print("Remark: The controller will be disabled in bec. It will be enabled by running the init route, \nbut in case needed, to enable manually set dev.lsamrot.enabled=True")
|
||||
return
|
||||
|
||||
if self.OMNYTools.yesno(
|
||||
@@ -172,7 +172,7 @@ class LamNIOpticsMixin:
|
||||
|
||||
def leye_out(self):
|
||||
self.loptics_in()
|
||||
fshclose()
|
||||
dev.omnyfsh.fshopen()
|
||||
leyey_out = self._get_user_param_safe("leyey", "out")
|
||||
umv(dev.leyey, leyey_out)
|
||||
|
||||
|
||||
@@ -37,6 +37,92 @@ class cSAXSInitSmaractStages:
|
||||
# ------------------------------
|
||||
# Internal helpers (runtime-based)
|
||||
# ------------------------------
|
||||
|
||||
def _ensure_all_session_devices_enabled(self, selection: set | None = None, try_enable: bool = True):
|
||||
"""
|
||||
Ensure all session devices (or a selection) that define 'bl_smar_stage' are enabled.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
selection : set | None
|
||||
If provided, only devices in this set are considered.
|
||||
try_enable : bool
|
||||
If True, attempt to set device.enabled = True for devices that expose 'enabled' and are False.
|
||||
If False, only report status without changing it.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
{
|
||||
"enabled_now": [device_names enabled by this call],
|
||||
"already_enabled": [device_names already enabled or without 'enabled' attr],
|
||||
"failed": [device_names that could not be enabled],
|
||||
"inaccessible": [device_names not accessible]
|
||||
}
|
||||
"""
|
||||
enabled_now = []
|
||||
already_enabled = []
|
||||
failed = []
|
||||
inaccessible = []
|
||||
|
||||
# Build axis map to restrict to SmarAct-based devices (same logic as other helpers)
|
||||
axis_map = self._build_session_axis_map(selection=selection)
|
||||
|
||||
for dev_name in sorted(axis_map.keys()):
|
||||
try:
|
||||
d = self._get_device_object(dev_name)
|
||||
if d is None:
|
||||
inaccessible.append(dev_name)
|
||||
logger.warning(f"[cSAXS] Device {dev_name} not accessible.")
|
||||
continue
|
||||
|
||||
# If device has no 'enabled' attribute, treat as already enabled/usable
|
||||
if not hasattr(d, "enabled"):
|
||||
already_enabled.append(dev_name)
|
||||
continue
|
||||
|
||||
# If already enabled
|
||||
try:
|
||||
if getattr(d, "enabled"):
|
||||
already_enabled.append(dev_name)
|
||||
continue
|
||||
except Exception:
|
||||
# If reading enabled fails, treat as inaccessible for safety
|
||||
failed.append(dev_name)
|
||||
logger.warning(f"[cSAXS] Could not read 'enabled' for {dev_name}.")
|
||||
continue
|
||||
|
||||
# Device exists and is disabled
|
||||
if try_enable:
|
||||
try:
|
||||
logger.info(f"[cSAXS] Enabling device {dev_name} (was disabled).")
|
||||
setattr(d, "enabled", True)
|
||||
# small delay to let device initialize if needed
|
||||
time.sleep(0.05)
|
||||
if getattr(d, "enabled"):
|
||||
enabled_now.append(dev_name)
|
||||
logger.info(f"[cSAXS] Device {dev_name} enabled.")
|
||||
else:
|
||||
failed.append(dev_name)
|
||||
logger.warning(f"[cSAXS] Device {dev_name} still disabled after enabling attempt.")
|
||||
except Exception as exc:
|
||||
failed.append(dev_name)
|
||||
logger.error(f"[cSAXS] Failed to enable {dev_name}: {exc}")
|
||||
else:
|
||||
# Not trying to enable, just report
|
||||
failed.append(dev_name)
|
||||
except Exception as exc:
|
||||
failed.append(dev_name)
|
||||
logger.error(f"[cSAXS] _ensure_all_session_devices_enabled error for {dev_name}: {exc}")
|
||||
|
||||
return {
|
||||
"enabled_now": enabled_now,
|
||||
"already_enabled": already_enabled,
|
||||
"failed": failed,
|
||||
"inaccessible": inaccessible,
|
||||
}
|
||||
|
||||
|
||||
def _yesno(self, question: str, default: str = "y") -> bool:
|
||||
"""
|
||||
Use OMNYTools.yesno if available; otherwise default to 'yes' (or fallback to input()).
|
||||
@@ -107,6 +193,7 @@ class cSAXSInitSmaractStages:
|
||||
# ------------------------------
|
||||
# Public API
|
||||
# ------------------------------
|
||||
|
||||
def smaract_reference_stages(self, force: bool = False, devices_to_reference=None):
|
||||
"""
|
||||
Reference SmarAct stages using runtime discovery.
|
||||
@@ -167,6 +254,19 @@ class cSAXSInitSmaractStages:
|
||||
devices_to_reference = [devices_to_reference]
|
||||
selection = set(devices_to_reference) if devices_to_reference else None
|
||||
|
||||
# First: ensure all relevant devices are enabled before attempting referencing
|
||||
enable_report = self._ensure_all_session_devices_enabled(selection=selection, try_enable=True)
|
||||
if enable_report["failed"]:
|
||||
logger.warning(
|
||||
"[cSAXS] Some devices could not be enabled before referencing: "
|
||||
+ ", ".join(sorted(enable_report["failed"]))
|
||||
)
|
||||
if enable_report["inaccessible"]:
|
||||
logger.warning(
|
||||
"[cSAXS] Some devices were inaccessible before referencing: "
|
||||
+ ", ".join(sorted(enable_report["inaccessible"]))
|
||||
)
|
||||
|
||||
# Build axis map for selected devices (or all devices present)
|
||||
axis_map = self._build_session_axis_map(selection=selection)
|
||||
if selection:
|
||||
@@ -174,7 +274,6 @@ class cSAXSInitSmaractStages:
|
||||
if unknown:
|
||||
print(f"Unknown devices requested or missing 'bl_smar_stage' (ignored): {unknown}")
|
||||
|
||||
|
||||
newly_referenced = []
|
||||
already_referenced = []
|
||||
failed = []
|
||||
@@ -191,6 +290,17 @@ class cSAXSInitSmaractStages:
|
||||
failed.append(dev_name)
|
||||
continue
|
||||
|
||||
# If device exposes 'enabled' and is False, skip (we already tried enabling above)
|
||||
try:
|
||||
if hasattr(d, "enabled") and not getattr(d, "enabled"):
|
||||
print(f"{dev_name}: device disabled, skipping.")
|
||||
failed.append(dev_name)
|
||||
continue
|
||||
except Exception:
|
||||
print(f"{dev_name}: could not read enabled state, skipping.")
|
||||
failed.append(dev_name)
|
||||
continue
|
||||
|
||||
try:
|
||||
is_ref = d.controller.axis_is_referenced(ch)
|
||||
|
||||
@@ -246,7 +356,17 @@ class cSAXSInitSmaractStages:
|
||||
def smaract_check_all_referenced(self):
|
||||
"""
|
||||
Check reference state for all SmarAct devices that define 'bl_smar_stage'.
|
||||
This now enables all relevant devices first (attempt), then performs the checks.
|
||||
"""
|
||||
# Attempt to enable all relevant devices first (do not force enabling if you prefer)
|
||||
enable_report = self._ensure_all_session_devices_enabled(selection=None, try_enable=True)
|
||||
if enable_report["enabled_now"]:
|
||||
print("Now enabled devices which were disabled before: " + ", ".join(sorted(enable_report["enabled_now"])))
|
||||
if enable_report["failed"]:
|
||||
print("Could not enable: " + ", ".join(sorted(enable_report["failed"])))
|
||||
if enable_report["inaccessible"]:
|
||||
print("Inaccessible: " + ", ".join(sorted(enable_report["inaccessible"])))
|
||||
|
||||
axis_map = self._build_session_axis_map(selection=None)
|
||||
for dev_name in sorted(axis_map.keys()):
|
||||
ch = axis_map[dev_name]
|
||||
@@ -254,6 +374,16 @@ class cSAXSInitSmaractStages:
|
||||
if d is None:
|
||||
print(f"{dev_name}: device not accessible or unsupported.")
|
||||
continue
|
||||
|
||||
# Skip devices that expose 'enabled' and are False
|
||||
try:
|
||||
if hasattr(d, "enabled") and not getattr(d, "enabled"):
|
||||
print(f"{dev_name} (axis {ch}) is disabled; skipping reference check.")
|
||||
continue
|
||||
except Exception:
|
||||
print(f"{dev_name} (axis {ch}) enabled-state unknown; skipping.")
|
||||
continue
|
||||
|
||||
try:
|
||||
if d.controller.axis_is_referenced(ch):
|
||||
print(f"{dev_name} (axis {ch}) is referenced.")
|
||||
@@ -262,6 +392,7 @@ class cSAXSInitSmaractStages:
|
||||
except Exception as e:
|
||||
print(f"Error checking {dev_name} (axis {ch}): {e}")
|
||||
|
||||
|
||||
def smaract_components_to_initial_position(self, devices_to_move=None):
|
||||
"""
|
||||
Move selected (or all) SmarAct-based components to their configured init_position.
|
||||
|
||||
Binary file not shown.
BIN
csaxs_bec/bec_ipython_client/plugins/flomni/flOMNI.png
Normal file
BIN
csaxs_bec/bec_ipython_client/plugins/flomni/flOMNI.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put
|
||||
|
||||
|
||||
class FlomniOpticsMixin:
|
||||
@@ -16,12 +16,18 @@ class FlomniOpticsMixin:
|
||||
return param.get(var)
|
||||
|
||||
def feye_out(self):
|
||||
fshclose()
|
||||
dev.omnyfsh.fshclose()
|
||||
self.foptics_in()
|
||||
self.flomnigui_show_xeyealign()
|
||||
self.xrayeye_update_frame()
|
||||
if self.OMNYTools.yesno("Did the direct beam on the xray eye disappear?"):
|
||||
print("excellent.")
|
||||
else:
|
||||
print("Aborting. With visible parts of the direct beam on the xray eye, it cannot be removed.")
|
||||
return
|
||||
feyex_out = self._get_user_param_safe("feyex", "out")
|
||||
umv(dev.feyex, feyex_out)
|
||||
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 2)
|
||||
# move rotation stage to zero to avoid problems with wires
|
||||
umv(dev.fsamroy, 0)
|
||||
# umv(dev.fttrx1, 9.2)
|
||||
@@ -32,16 +38,37 @@ class FlomniOpticsMixin:
|
||||
|
||||
feyex_in = self._get_user_param_safe("feyex", "in")
|
||||
feyey_in = self._get_user_param_safe("feyey", "in")
|
||||
umv(dev.feyex, feyex_in, dev.feyey, feyey_in)
|
||||
#self.align.update_frame()
|
||||
|
||||
current_feyex = dev.feyex.readback.get()
|
||||
current_feyey = dev.feyey.readback.get()
|
||||
|
||||
# check if both are close enough (within 0.01)
|
||||
if np.isclose(current_feyex, feyex_in, atol=0.01) and np.isclose(current_feyey, feyey_in, atol=0.01):
|
||||
# both already in position → do nothing
|
||||
pass
|
||||
else:
|
||||
# move both axes to the desired "in" positions
|
||||
umv(dev.feyex, feyex_in, dev.feyey, feyey_in)
|
||||
|
||||
def _ffzp_in(self):
|
||||
foptx_in = self._get_user_param_safe("foptx", "in")
|
||||
fopty_in = self._get_user_param_safe("fopty", "in")
|
||||
umv(dev.foptx, foptx_in)
|
||||
umv(
|
||||
dev.fopty, fopty_in
|
||||
) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
|
||||
|
||||
current_foptx = dev.foptx.readback.get()
|
||||
current_fopty = dev.fopty.readback.get()
|
||||
|
||||
tol = 0.003
|
||||
|
||||
# if either axis is outside the tolerance → move both
|
||||
need_move_optics = (
|
||||
not np.isclose(current_foptx, foptx_in, atol=tol) or
|
||||
not np.isclose(current_fopty, fopty_in, atol=tol)
|
||||
)
|
||||
|
||||
if need_move_optics:
|
||||
umv(dev.foptx, foptx_in, dev.fopty, fopty_in) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
|
||||
else:
|
||||
print("FZP is already at the in position.")
|
||||
|
||||
def ffzp_in(self):
|
||||
"""
|
||||
@@ -84,19 +111,85 @@ class FlomniOpticsMixin:
|
||||
# umv(dev.losax, -1.4850, dev.losay, -0.1930)
|
||||
# umv(dev.losaz, 1.0000)
|
||||
# 7.2, 150
|
||||
|
||||
fosax_in = self._get_user_param_safe("fosax", "in")
|
||||
fosay_in = self._get_user_param_safe("fosay", "in")
|
||||
fosaz_in = self._get_user_param_safe("fosaz", "in")
|
||||
|
||||
# tighten limits
|
||||
dev.fosax.limits = [fosax_in - 0.1, fosax_in + 0.1]
|
||||
dev.fosay.limits = [fosay_in - 0.1, fosay_in + 0.1]
|
||||
dev.fosaz.limits = [fosaz_in - 0.1, fosaz_in + 0.1]
|
||||
umv(dev.fosax, fosax_in, dev.fosay, fosay_in)
|
||||
umv(dev.fosaz, fosaz_in)
|
||||
|
||||
current_fosax = dev.fosax.readback.get()
|
||||
current_fosay = dev.fosay.readback.get()
|
||||
current_fosaz = dev.fosaz.readback.get()
|
||||
|
||||
# tolerance
|
||||
tol = 0.003
|
||||
|
||||
need_move_osa = (
|
||||
not np.isclose(current_fosax, fosax_in, atol=tol) or
|
||||
not np.isclose(current_fosay, fosay_in, atol=tol) or
|
||||
not np.isclose(current_fosaz, fosaz_in, atol=tol)
|
||||
)
|
||||
|
||||
if need_move_osa:
|
||||
umv(dev.fosax, fosax_in, dev.fosay, fosay_in)
|
||||
umv(dev.fosaz, fosaz_in)
|
||||
else:
|
||||
print("OSA is already at the IN position.")
|
||||
|
||||
# 11 kev
|
||||
# umv(dev.losax, -1.161000, dev.losay, -0.196)
|
||||
# umv(dev.losaz, 1.0000)
|
||||
|
||||
def _check_eye_out_and_optics_in(self, tol=0.003):
|
||||
# --- expected IN positions ---
|
||||
foptx_in = self._get_user_param_safe("foptx", "in")
|
||||
fopty_in = self._get_user_param_safe("fopty", "in")
|
||||
foptz_in = self._get_user_param_safe("foptz", "in")
|
||||
|
||||
# --- expected OUT condition for the X-ray eye ---
|
||||
# eye is OUT when it is *not within tolerance* of its IN position
|
||||
feyex_out = self._get_user_param_safe("feyex", "out")
|
||||
|
||||
# --- current positions ---
|
||||
cx_feyex = dev.feyex.readback.get()
|
||||
|
||||
cx_foptx = dev.foptx.readback.get()
|
||||
cx_fopty = dev.fopty.readback.get()
|
||||
cx_foptz = dev.foptz.readback.get()
|
||||
|
||||
# --- check eye OUT ---
|
||||
eye_out = (
|
||||
np.isclose(cx_feyex, feyex_out, atol=tol)
|
||||
)
|
||||
|
||||
# --- check optics IN ---
|
||||
optics_in = (
|
||||
np.isclose(cx_foptx, foptx_in, atol=tol) and
|
||||
np.isclose(cx_fopty, fopty_in, atol=tol) and
|
||||
np.isclose(cx_foptz, foptz_in, atol=tol)
|
||||
)
|
||||
|
||||
fosax_in = self._get_user_param_safe("fosax", "in")
|
||||
fosay_in = self._get_user_param_safe("fosay", "in")
|
||||
fosaz_in = self._get_user_param_safe("fosaz", "in")
|
||||
|
||||
cx_fosax = dev.fosax.readback.get()
|
||||
cx_fosay = dev.fosay.readback.get()
|
||||
cx_fosaz = dev.fosaz.readback.get()
|
||||
|
||||
osa_in = (
|
||||
np.isclose(cx_fosax, fosax_in, atol=tol) and
|
||||
np.isclose(cx_fosay, fosay_in, atol=tol) and
|
||||
np.isclose(cx_fosaz, fosaz_in, atol=tol)
|
||||
)
|
||||
|
||||
return eye_out and optics_in and osa_in
|
||||
|
||||
|
||||
def fosa_out(self):
|
||||
self.ensure_fheater_up()
|
||||
curtain_is_triggered = dev.foptz.controller.fosaz_light_curtain_is_triggered()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import builtins
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea
|
||||
import time
|
||||
|
||||
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
|
||||
@@ -9,6 +8,7 @@ if builtins.__dict__.get("bec") is not None:
|
||||
dev = builtins.__dict__.get("dev")
|
||||
scans = builtins.__dict__.get("scans")
|
||||
|
||||
|
||||
def umv(*args):
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
@@ -18,10 +18,17 @@ class flomniGuiToolsError(Exception):
|
||||
|
||||
|
||||
class flomniGuiTools:
|
||||
GUI_RPC_TIMEOUT = 20
|
||||
|
||||
def __init__(self):
|
||||
self.text_box = None
|
||||
self.progressbar = None
|
||||
self.flomni_window = None
|
||||
self.xeyegui = None
|
||||
self.pdf_viewer = None
|
||||
self.idle_text_box = None
|
||||
self.camera_gripper_image = None
|
||||
self.camera_overview_image = None
|
||||
|
||||
def set_client(self, client):
|
||||
self.client = client
|
||||
@@ -29,9 +36,11 @@ class flomniGuiTools:
|
||||
|
||||
def flomnigui_show_gui(self):
|
||||
if "flomni" in self.gui.windows:
|
||||
self.gui.flomni.show()
|
||||
self.flomni_window = self.gui.windows["flomni"]
|
||||
self.gui.flomni.raise_window()
|
||||
else:
|
||||
self.gui.new("flomni")
|
||||
self.flomni_window = self.gui.new("flomni", timeout=self.GUI_RPC_TIMEOUT)
|
||||
time.sleep(1)
|
||||
|
||||
def flomnigui_stop_gui(self):
|
||||
self.gui.flomni.hide()
|
||||
@@ -39,94 +48,118 @@ class flomniGuiTools:
|
||||
def flomnigui_raise(self):
|
||||
self.gui.flomni.raise_window()
|
||||
|
||||
# def flomnigui_show_xeyealign(self):
|
||||
# self.flomnigui_show_gui()
|
||||
# if self.xeyegui is None:
|
||||
# self.flomnigui_remove_all_docks()
|
||||
# self.xeyegui = self.gui.flomni.new("xeyegui").new("XRayEye")
|
||||
# # start live
|
||||
# if not dev.cam_xeye.live_mode:
|
||||
# dev.cam_xeye.live_mode = True
|
||||
|
||||
def flomnigui_show_xeyealign(self):
|
||||
self.flomnigui_show_gui()
|
||||
if self._flomnigui_check_attribute_not_exists("xeyegui"):
|
||||
if self._flomnigui_is_missing("xeyegui"):
|
||||
self.flomnigui_remove_all_docks()
|
||||
self.xeyegui = self.gui.flomni.new("xeyegui").new("XRayEye")
|
||||
# start live
|
||||
if not dev.cam_xeye.live_mode:
|
||||
dev.cam_xeye.live_mode = True
|
||||
self.xeyegui = self.gui.flomni.new(
|
||||
"XRayEye", object_name="xrayeye", timeout=self.GUI_RPC_TIMEOUT
|
||||
)
|
||||
# start live
|
||||
if not dev.cam_xeye.live_mode_enabled.get():
|
||||
dev.cam_xeye.live_mode_enabled.put(True)
|
||||
self.xeyegui.switch_tab("alignment")
|
||||
|
||||
def flomnigui_show_xeyealign_fittab(self):
|
||||
self.flomnigui_show_gui()
|
||||
if self._flomnigui_is_missing("xeyegui"):
|
||||
self.flomnigui_remove_all_docks()
|
||||
self.xeyegui = self.gui.flomni.new(
|
||||
"XRayEye", object_name="xrayeye", timeout=self.GUI_RPC_TIMEOUT
|
||||
)
|
||||
self.xeyegui.switch_tab("fit")
|
||||
|
||||
def _flomnigui_check_attribute_not_exists(self, attribute_name):
|
||||
if hasattr(self.gui,"flomni"):
|
||||
if hasattr(self.gui.flomni,attribute_name):
|
||||
return False
|
||||
if hasattr(self.gui, "flomni"):
|
||||
if attribute_name == "xeyegui":
|
||||
if hasattr(self.gui.flomni, "xrayeye"):
|
||||
return False
|
||||
if attribute_name == "progressbar":
|
||||
if hasattr(self.gui.flomni, "RingProgressBar"):
|
||||
return False
|
||||
if attribute_name == "cam_flomni_gripper" or attribute_name == "cam_flomni_overview":
|
||||
if hasattr(self.gui.flomni, "Image") or hasattr(self.gui.flomni, "Image_0"):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _flomnigui_is_missing(self, attribute_name):
|
||||
widget = getattr(self, attribute_name, None)
|
||||
if widget is None:
|
||||
return True
|
||||
if hasattr(widget, "_is_deleted") and widget._is_deleted():
|
||||
return True
|
||||
return False
|
||||
|
||||
def flomnigui_show_cameras(self):
|
||||
self.flomnigui_show_gui()
|
||||
if self._flomnigui_check_attribute_not_exists("camera_gripper") or self._flomnigui_check_attribute_not_exists("camera_overview"):
|
||||
if self._flomnigui_check_attribute_not_exists(
|
||||
"cam_flomni_gripper"
|
||||
) or self._flomnigui_check_attribute_not_exists("cam_flomni_overview"):
|
||||
self.flomnigui_remove_all_docks()
|
||||
camera_gripper_image = self.gui.flomni.new("camera_gripper").new("Image")
|
||||
self.camera_gripper_image = self.gui.flomni.new("Image")
|
||||
if self._flomnicam_check_device_exists(dev.cam_flomni_gripper):
|
||||
camera_gripper_image.image(("cam_flomni_gripper", "preview"))
|
||||
camera_gripper_image.lock_aspect_ratio = True
|
||||
camera_gripper_image.enable_fps_monitor = True
|
||||
camera_gripper_image.enable_toolbar = False
|
||||
camera_gripper_image.outer_axes = False
|
||||
camera_gripper_image.inner_axes = False
|
||||
self.camera_gripper_image.image(device="cam_flomni_gripper", signal="preview")
|
||||
self.camera_gripper_image.lock_aspect_ratio = True
|
||||
self.camera_gripper_image.enable_fps_monitor = True
|
||||
self.camera_gripper_image.enable_toolbar = False
|
||||
self.camera_gripper_image.outer_axes = False
|
||||
self.camera_gripper_image.inner_axes = False
|
||||
dev.cam_flomni_gripper.start_live_mode()
|
||||
else:
|
||||
print("Cannot open camera_gripper. Device does not exist.")
|
||||
camera_overview_image = self.gui.flomni.new("camera_overview").new("Image")
|
||||
self.camera_overview_image = self.gui.flomni.new("Image")
|
||||
if self._flomnicam_check_device_exists(dev.cam_flomni_overview):
|
||||
camera_overview_image.image(("cam_flomni_overview", "preview"))
|
||||
camera_overview_image.lock_aspect_ratio = True
|
||||
camera_overview_image.enable_fps_monitor = True
|
||||
camera_overview_image.enable_toolbar = False
|
||||
camera_overview_image.outer_axes = False
|
||||
camera_overview_image.inner_axes = False
|
||||
self.camera_overview_image.image(device="cam_flomni_overview", signal="preview")
|
||||
self.camera_overview_image.lock_aspect_ratio = True
|
||||
self.camera_overview_image.enable_fps_monitor = True
|
||||
self.camera_overview_image.enable_toolbar = False
|
||||
self.camera_overview_image.outer_axes = False
|
||||
self.camera_overview_image.inner_axes = False
|
||||
dev.cam_flomni_overview.start_live_mode()
|
||||
else:
|
||||
print("Cannot open camera_overview. Device does not exist.")
|
||||
|
||||
def flomnigui_remove_all_docks(self):
|
||||
#dev.cam_flomni_overview.stop_live_mode()
|
||||
#dev.cam_flomni_gripper.stop_live_mode()
|
||||
#dev.cam_xeye.live_mode = False
|
||||
self.gui.flomni.delete_all()
|
||||
# dev.cam_flomni_overview.stop_live_mode()
|
||||
# dev.cam_flomni_gripper.stop_live_mode()
|
||||
# dev.cam_xeye.live_mode = False
|
||||
if hasattr(self.gui, "flomni"):
|
||||
self.gui.flomni.delete_all(timeout=self.GUI_RPC_TIMEOUT)
|
||||
self.progressbar = None
|
||||
self.text_box = None
|
||||
self.xeyegui = None
|
||||
self.pdf_viewer = None
|
||||
self.idle_text_box = None
|
||||
self.camera_gripper_image = None
|
||||
self.camera_overview_image = None
|
||||
|
||||
def flomnigui_idle(self):
|
||||
self.flomnigui_show_gui()
|
||||
if self._flomnigui_check_attribute_not_exists("idle_text_box"):
|
||||
if self._flomnigui_is_missing("idle_text_box"):
|
||||
self.flomnigui_remove_all_docks()
|
||||
idle_text_box = self.gui.flomni.new("idle_textbox").new("TextBox")
|
||||
self.idle_text_box = self.gui.flomni.new("TextBox")
|
||||
text = (
|
||||
"<pre>"
|
||||
+ " ,---.,--. ,-----. ,--. ,--.,--. ,--.,--. \n"
|
||||
+ "/ .-'| |' .-. '| `.' || ,'.| || | \n"
|
||||
+ "| `-,| || | | || |'.'| || |' ' || | \n"
|
||||
+ "| .-'| |' '-' '| | | || | ` || | \n"
|
||||
+ "`--' `--' `-----' `--' `--'`--' `--'`--' \n"
|
||||
+ "██████╗ ███████╗ ██████╗ ███████╗██╗ ██████╗ ███╗ ███╗███╗ ██╗██╗\n"
|
||||
+ "██╔══██╗██╔════╝██╔════╝ ██╔════╝██║ ██╔═══██╗████╗ ████║████╗ ██║██║\n"
|
||||
+ "██████╔╝█████╗ ██║ █████╗ ██║ ██║ ██║██╔████╔██║██╔██╗ ██║██║\n"
|
||||
+ "██╔══██╗██╔══╝ ██║ ██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╗██║██║\n"
|
||||
+ "██████╔╝███████╗╚██████╗ ██║ ███████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚████║██║\n"
|
||||
+ "╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝\n"
|
||||
+ "</pre>"
|
||||
)
|
||||
idle_text_box.set_html_text(text)
|
||||
self.idle_text_box.set_html_text(text)
|
||||
|
||||
def flomnigui_docs(self, filename: str | None = None):
|
||||
import csaxs_bec
|
||||
from pathlib import Path
|
||||
|
||||
print("The general flOMNI documentation is at \nhttps://sls-csaxs.readthedocs.io/en/latest/user/ptychography/flomni.html#user-ptychography-flomni")
|
||||
print(
|
||||
"The general flOMNI documentation is at \nhttps://sls-csaxs.readthedocs.io/en/latest/user/ptychography/flomni.html#user-ptychography-flomni"
|
||||
)
|
||||
|
||||
csaxs_bec_basepath = Path(csaxs_bec.__file__).parent
|
||||
docs_folder = (
|
||||
csaxs_bec_basepath /
|
||||
"bec_ipython_client" / "plugins" / "flomni" / "docs"
|
||||
)
|
||||
docs_folder = csaxs_bec_basepath / "bec_ipython_client" / "plugins" / "flomni" / "docs"
|
||||
|
||||
if not docs_folder.is_dir():
|
||||
raise NotADirectoryError(f"Docs folder not found: {docs_folder}")
|
||||
@@ -159,15 +192,14 @@ class flomniGuiTools:
|
||||
# --- GUI handling (active existence check) ----------------------------
|
||||
self.flomnigui_show_gui()
|
||||
|
||||
if self._flomnigui_check_attribute_not_exists("PdfViewerWidget"):
|
||||
if self._flomnigui_is_missing("pdf_viewer"):
|
||||
self.flomnigui_remove_all_docks()
|
||||
self.pdf_viewer = self.gui.flomni.new(widget="PdfViewerWidget")
|
||||
|
||||
# --- Load PDF ---------------------------------------------------------
|
||||
self.pdf_viewer.PdfViewerWidget.load_pdf(str(pdf_file.resolve()))
|
||||
self.pdf_viewer.load_pdf(str(pdf_file.resolve()))
|
||||
print(f"\nLoaded: {pdf_file.name}\n")
|
||||
|
||||
|
||||
def _flomnicam_check_device_exists(self, device):
|
||||
try:
|
||||
device
|
||||
@@ -178,29 +210,29 @@ class flomniGuiTools:
|
||||
|
||||
def flomnigui_show_progress(self):
|
||||
self.flomnigui_show_gui()
|
||||
if self._flomnigui_check_attribute_not_exists("progressbar"):
|
||||
if self._flomnigui_is_missing("progressbar"):
|
||||
self.flomnigui_remove_all_docks()
|
||||
# Add a new dock with a RingProgressBar widget
|
||||
self.progressbar = self.gui.flomni.new("progressbar").new("RingProgressBar")
|
||||
# Customize the size of the progress ring
|
||||
self.progressbar.set_line_widths(20)
|
||||
# Disable automatic updates and manually set the self.progressbar value
|
||||
self.progressbar.enable_auto_updates(False)
|
||||
# Set precision for the self.progressbar display
|
||||
self.progressbar.set_precision(1) # Display self.progressbar with one decimal places
|
||||
# Setting multiple rigns with different values
|
||||
self.progressbar.set_number_of_bars(3)
|
||||
self.progressbar.rings[0].set_update("manual")
|
||||
self.progressbar.rings[1].set_update("manual")
|
||||
self.progressbar.rings[2].set_update("scan")
|
||||
# Set the values of the rings to 50, 75, and 25 from outer to inner ring
|
||||
# self.progressbar.set_value([50, 75])
|
||||
# Add a new dock with a TextBox widget
|
||||
self.text_box = self.gui.flomni.new(name="progress_text").new("TextBox")
|
||||
self.progressbar = self.gui.flomni.new("RingProgressBar")
|
||||
|
||||
# Setting multiple rings with different values
|
||||
self.progressbar.add_ring().set_update("manual")
|
||||
self.progressbar.add_ring().set_update("manual")
|
||||
self.progressbar.add_ring().set_update("scan")
|
||||
|
||||
self._flomnigui_update_progress()
|
||||
|
||||
def _flomnigui_update_progress(self):
|
||||
"""Update the progress ring bar and center label from the current progress state.
|
||||
|
||||
``self.progress`` is backed by the BEC global variable ``tomo_progress``
|
||||
(see :class:`_ProgressProxy` in ``flomni.py``), so this method reflects
|
||||
the live state that is also accessible from other BEC client sessions via::
|
||||
|
||||
client.get_global_var("tomo_progress")
|
||||
"""
|
||||
main_progress_ring = self.progressbar.rings[0]
|
||||
subtomo_progress_ring = self.progressbar.rings[1]
|
||||
if self.progressbar is not None:
|
||||
progress = self.progress["projection"] / self.progress["total_projections"] * 100
|
||||
subtomo_progress = (
|
||||
@@ -208,10 +240,47 @@ class flomniGuiTools:
|
||||
/ self.progress["subtomo_total_projections"]
|
||||
* 100
|
||||
)
|
||||
self.progressbar.set_value([progress, subtomo_progress, 0])
|
||||
if self.text_box is not None:
|
||||
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
|
||||
self.text_box.set_plain_text(text)
|
||||
main_progress_ring.set_value(progress)
|
||||
subtomo_progress_ring.set_value(subtomo_progress)
|
||||
|
||||
# --- format start time for display --------------------------------
|
||||
start_str = self.progress.get("tomo_start_time")
|
||||
if start_str is not None:
|
||||
import datetime as _dt
|
||||
start_display = _dt.datetime.fromisoformat(start_str).strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
start_display = "N/A"
|
||||
|
||||
# --- format estimated remaining time ------------------------------
|
||||
remaining_s = self.progress.get("estimated_remaining_time")
|
||||
if remaining_s is not None and remaining_s >= 0:
|
||||
import datetime as _dt
|
||||
remaining_s = int(remaining_s)
|
||||
h, rem = divmod(remaining_s, 3600)
|
||||
m, s = divmod(rem, 60)
|
||||
if h > 0:
|
||||
eta_display = f"{h}h {m:02d}m {s:02d}s"
|
||||
elif m > 0:
|
||||
eta_display = f"{m}m {s:02d}s"
|
||||
else:
|
||||
eta_display = f"{s}s"
|
||||
else:
|
||||
eta_display = "N/A"
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
text = (
|
||||
f"Progress report:\n"
|
||||
f" Tomo type: {self.progress['tomo_type']}\n"
|
||||
f" Projection: {self.progress['projection']:.0f}\n"
|
||||
f" Total projections expected {self.progress['total_projections']:.1f}\n"
|
||||
f" Angle: {self.progress['angle']:.1f}\n"
|
||||
f" Current subtomo: {self.progress['subtomo']}\n"
|
||||
f" Current projection within subtomo: {self.progress['subtomo_projection']}\n"
|
||||
f" Total projections per subtomo: {int(self.progress['subtomo_total_projections'])}\n"
|
||||
f" Scan started: {start_display}\n"
|
||||
f" Est. remaining: {eta_display}"
|
||||
)
|
||||
self.progressbar.set_center_label(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -222,6 +291,7 @@ if __name__ == "__main__":
|
||||
client.start()
|
||||
client.gui = BECGuiClient()
|
||||
|
||||
flomni_gui = flomniGuiTools(client)
|
||||
flomni_gui = flomniGuiTools()
|
||||
flomni_gui.set_client(client)
|
||||
flomni_gui.flomnigui_show_gui()
|
||||
flomni_gui.flomnigui_show_progress()
|
||||
|
||||
@@ -5,9 +5,11 @@ import os
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
|
||||
logger = bec_logger.logger
|
||||
# import builtins to avoid linter errors
|
||||
@@ -15,16 +17,18 @@ bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
scans = builtins.__dict__.get("scans")
|
||||
|
||||
|
||||
def umv(*args):
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_ipython_client.plugins.flomni import Flomni
|
||||
|
||||
|
||||
class XrayEyeAlign:
|
||||
# pixel calibration, multiply to get mm
|
||||
labview=False
|
||||
test_wo_movements = True
|
||||
PIXEL_CALIBRATION = 0.1 / 113 # .2 with binning
|
||||
|
||||
def __init__(self, client, flomni: Flomni) -> None:
|
||||
@@ -36,244 +40,252 @@ class XrayEyeAlign:
|
||||
self.flomni.reset_correction()
|
||||
self.flomni.reset_tomo_alignment_fit()
|
||||
|
||||
@property
|
||||
def gui(self):
|
||||
return self.flomni.xeyegui
|
||||
|
||||
def _reset_init_values(self):
|
||||
self.shift_xy = [0, 0]
|
||||
self._xray_fov_xy = [0, 0]
|
||||
|
||||
def save_frame(self):
|
||||
epics_put("XOMNYI-XEYE-SAVFRAME:0", 1)
|
||||
|
||||
def update_frame(self,keep_shutter_open=False):
|
||||
if self.labview:
|
||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
||||
|
||||
if not self.labview:
|
||||
def update_frame(self, keep_shutter_open=False):
|
||||
if self.flomni._flomnigui_check_attribute_not_exists("xeyegui"):
|
||||
self.flomni.flomnigui_show_xeyealign()
|
||||
if not dev.cam_xeye.live_mode:
|
||||
dev.cam_xeye.live_mode = True
|
||||
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 1)
|
||||
if self.labview:
|
||||
# wait for start live
|
||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
||||
time.sleep(0.5)
|
||||
print("waiting for live view to start...")
|
||||
if not dev.cam_xeye.live_mode_enabled.get():
|
||||
dev.cam_xeye.live_mode_enabled.put(True)
|
||||
|
||||
fshopen()
|
||||
self.gui.on_live_view_enabled(True)
|
||||
|
||||
if self.labview:
|
||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
||||
|
||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
||||
print("waiting for new frame...")
|
||||
time.sleep(0.5)
|
||||
dev.omnyfsh.fshopen()
|
||||
|
||||
time.sleep(0.5)
|
||||
# stop live view
|
||||
if not keep_shutter_open:
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 0)
|
||||
self.gui.on_live_view_enabled(False)
|
||||
time.sleep(0.1)
|
||||
fshclose()
|
||||
print("got new frame")
|
||||
dev.omnyfsh.fshclose()
|
||||
print("Received new frame.")
|
||||
else:
|
||||
print("Staying in live view, shutter is and remains open!")
|
||||
|
||||
def tomo_rotate(self, val: float):
|
||||
# pylint: disable=undefined-variable
|
||||
umv(self.device_manager.devices.fsamroy, val)
|
||||
if not self.test_wo_movements:
|
||||
umv(self.device_manager.devices.fsamroy, val)
|
||||
|
||||
def get_tomo_angle(self):
|
||||
return self.device_manager.devices.fsamroy.readback.get()
|
||||
|
||||
def update_fov(self, k: int):
|
||||
self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0])
|
||||
self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0])
|
||||
self._xray_fov_xy[0] = max(
|
||||
getattr(dev.omny_xray_gui, f"width_x_{k}").get(), self._xray_fov_xy[0]
|
||||
)
|
||||
self._xray_fov_xy[1] = max(
|
||||
getattr(dev.omny_xray_gui, f"width_y_{k}").get(), self._xray_fov_xy[1]
|
||||
)
|
||||
|
||||
@property
|
||||
def movement_buttons_enabled(self):
|
||||
return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")]
|
||||
|
||||
@movement_buttons_enabled.setter
|
||||
def movement_buttons_enabled(self, enabled: bool):
|
||||
enabled = int(enabled)
|
||||
epics_put("XOMNYI-XEYE-ENAMVX:0", enabled)
|
||||
epics_put("XOMNYI-XEYE-ENAMVY:0", enabled)
|
||||
def movement_buttons_enabled(self, enablex: bool, enabley: bool):
|
||||
self.gui.on_motors_enable(enablex, enabley)
|
||||
|
||||
def send_message(self, msg: str):
|
||||
epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg)
|
||||
print(f"In alginment GUI: {msg}")
|
||||
self.gui.user_message = msg
|
||||
|
||||
def align(self,keep_shutter_open=False):
|
||||
def align(self, keep_shutter_open=False):
|
||||
self.flomni.flomnigui_show_xeyealign()
|
||||
if not keep_shutter_open:
|
||||
print("This routine can be called with paramter keep_shutter_open=True to keep the shutter always open")
|
||||
print(
|
||||
"This routine can be called with paramter keep_shutter_open=True to keep the shutter always open"
|
||||
)
|
||||
self.send_message("Getting things ready. Please wait...")
|
||||
|
||||
#potential unresolved movement requests to zero
|
||||
epics_put("XOMNYI-XEYE-MVX:0", 0)
|
||||
epics_put("XOMNYI-XEYE-MVY:0", 0)
|
||||
self.gui.enable_submit_button(False)
|
||||
|
||||
# Initialize xray align device
|
||||
# clear potential pending movement requests
|
||||
dev.omny_xray_gui.mvx.set(0)
|
||||
dev.omny_xray_gui.mvy.set(0)
|
||||
# reset submit channel
|
||||
dev.omny_xray_gui.submit.set(0)
|
||||
|
||||
self.movement_buttons_enabled(False, False)
|
||||
|
||||
# reset shift xy and fov params
|
||||
self._reset_init_values()
|
||||
|
||||
self.flomni.lights_off()
|
||||
|
||||
self.flomni.flomnigui_show_xeyealign()
|
||||
self.flomni.flomnigui_raise()
|
||||
if not self.test_wo_movements:
|
||||
self.tomo_rotate(0)
|
||||
|
||||
self.tomo_rotate(0)
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", 0)
|
||||
|
||||
self.flomni.feye_in()
|
||||
self.flomni.feye_in()
|
||||
|
||||
self.flomni.laser_tracker_on()
|
||||
|
||||
self.flomni.feedback_enable_with_reset()
|
||||
|
||||
# disable movement buttons
|
||||
self.movement_buttons_enabled = False
|
||||
self.movement_buttons_enabled(False, False)
|
||||
|
||||
sample_name = self.flomni.sample_get_name(0)
|
||||
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name)
|
||||
self.gui.sample_name = sample_name
|
||||
|
||||
# this makes sure we are in a defined state
|
||||
self.flomni.feedback_disable()
|
||||
|
||||
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
|
||||
if not self.test_wo_movements:
|
||||
self.flomni.fosa_out()
|
||||
|
||||
self.flomni.fosa_out()
|
||||
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
|
||||
umv(dev.fsamx, fsamx_in - 0.25)
|
||||
|
||||
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
|
||||
umv(dev.fsamx, fsamx_in - 0.25)
|
||||
self.flomni.ffzp_in()
|
||||
|
||||
self.flomni.ffzp_in()
|
||||
self.update_frame(keep_shutter_open)
|
||||
|
||||
# enable submit buttons
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
epics_put("XOMNYI-XEYE-STEP:0", 0)
|
||||
self.gui.enable_submit_button(True)
|
||||
dev.omny_xray_gui.step.set(0).wait()
|
||||
self.send_message("Submit center value of FZP.")
|
||||
|
||||
k = 0
|
||||
while True:
|
||||
if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1:
|
||||
val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") / 2 * self.PIXEL_CALIBRATION # in mm
|
||||
self.alignment_values[k] = val_x
|
||||
if dev.omny_xray_gui.submit.get() == 1:
|
||||
|
||||
self.alignment_values[k] = (
|
||||
getattr(dev.omny_xray_gui, f"xval_x_{k}").get() / 2 * self.PIXEL_CALIBRATION
|
||||
) # in mm
|
||||
print(f"Clicked position {k}: x {self.alignment_values[k]}")
|
||||
rtx_position = dev.rtx.readback.get() / 1000
|
||||
print(f"Current rtx position {rtx_position}")
|
||||
self.alignment_values[k] -= rtx_position
|
||||
print(f"Corrected position {k}: x {self.alignment_values[k]}")
|
||||
|
||||
# reset submit channel
|
||||
dev.omny_xray_gui.submit.set(0)
|
||||
if k == 0: # received center value of FZP
|
||||
self.send_message("please wait ...")
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.gui.enable_submit_button(False)
|
||||
|
||||
self.flomni.feedback_disable()
|
||||
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
|
||||
umv(dev.fsamx, fsamx_in)
|
||||
if not self.test_wo_movements:
|
||||
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
|
||||
umv(dev.fsamx, fsamx_in)
|
||||
|
||||
self.flomni.foptics_out()
|
||||
self.flomni.foptics_out()
|
||||
|
||||
self.flomni.feedback_disable()
|
||||
umv(dev.fsamx, fsamx_in - 0.25)
|
||||
time.sleep(0.5)
|
||||
|
||||
if self.labview:
|
||||
self.update_frame(keep_shutter_open)
|
||||
epics_put("XOMNYI-XEYE-RECBG:0", 1)
|
||||
while epics_get("XOMNYI-XEYE-RECBG:0") == 1:
|
||||
time.sleep(0.5)
|
||||
print("waiting for background frame...")
|
||||
|
||||
umv(dev.fsamx, fsamx_in)
|
||||
time.sleep(0.5)
|
||||
self.flomni.feedback_enable_with_reset()
|
||||
|
||||
self.update_frame(keep_shutter_open)
|
||||
self.send_message("Adjust sample height and submit center")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
self.movement_buttons_enabled = True
|
||||
self.send_message("Step 1/5: Adjust sample height and submit center")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, True)
|
||||
|
||||
elif 1 <= k < 5: # received sample center value at samroy 0 ... 315
|
||||
self.send_message("please wait ...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
|
||||
self.movement_buttons_enabled = False
|
||||
self.gui.enable_submit_button(False)
|
||||
self.movement_buttons_enabled(False, False)
|
||||
|
||||
umv(dev.rtx, 0)
|
||||
self.tomo_rotate(k * 45)
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
|
||||
dev.omny_xray_gui.angle.set(self.get_tomo_angle())
|
||||
self.update_frame(keep_shutter_open)
|
||||
self.send_message("Submit sample center")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
epics_put("XOMNYI-XEYE-ENAMVX:0", 1)
|
||||
self.send_message(f"Step {k+1}/5: Submit sample center")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, False)
|
||||
self.update_fov(k)
|
||||
|
||||
elif k == 5: # received sample center value at samroy 270 and done
|
||||
self.send_message("done...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
||||
self.movement_buttons_enabled = False
|
||||
self.gui.enable_submit_button(False)
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.update_fov(k)
|
||||
break
|
||||
|
||||
k += 1
|
||||
epics_put("XOMNYI-XEYE-STEP:0", k)
|
||||
dev.omny_xray_gui.step.set(k)
|
||||
|
||||
_xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0")
|
||||
_xrayeyalignmvx = dev.omny_xray_gui.mvx.get()
|
||||
if _xrayeyalignmvx != 0:
|
||||
umvr(dev.rtx, _xrayeyalignmvx)
|
||||
print(f"Current rtx position {dev.rtx.readback.get() / 1000}")
|
||||
epics_put("XOMNYI-XEYE-MVX:0", 0)
|
||||
if k > 0:
|
||||
epics_put(f"XOMNYI-XEYE-STAGEPOSX:{k}", dev.rtx.readback.get() / 1000)
|
||||
time.sleep(3)
|
||||
dev.omny_xray_gui.mvx.set(0)
|
||||
self.update_frame(keep_shutter_open)
|
||||
|
||||
if k < 2:
|
||||
# allow movements, store movements to calculate center
|
||||
_xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0")
|
||||
_xrayeyalignmvy = dev.omny_xray_gui.mvy.get()
|
||||
if _xrayeyalignmvy != 0:
|
||||
self.flomni.feedback_disable()
|
||||
umvr(dev.fsamy, _xrayeyalignmvy / 1000)
|
||||
if not self.test_wo_movements:
|
||||
umvr(dev.fsamy, _xrayeyalignmvy / 1000)
|
||||
time.sleep(2)
|
||||
epics_put("XOMNYI-XEYE-MVY:0", 0)
|
||||
dev.omny_xray_gui.mvy.set(0)
|
||||
self.flomni.feedback_enable_with_reset()
|
||||
self.update_frame(keep_shutter_open)
|
||||
time.sleep(0.2)
|
||||
time.sleep(0.1)
|
||||
|
||||
self.write_output()
|
||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
|
||||
self.tomo_rotate(0)
|
||||
|
||||
umv(dev.rtx, 0)
|
||||
|
||||
# free camera
|
||||
if self.labview:
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 2)
|
||||
if keep_shutter_open and not self.labview:
|
||||
if self.flomni.OMNYTools.yesno("Close the shutter now?","y"):
|
||||
fshclose()
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 0)
|
||||
if not self.labview:
|
||||
self.flomni.flomnigui_idle()
|
||||
|
||||
if keep_shutter_open:
|
||||
if self.flomni.OMNYTools.yesno("Close the shutter now?", "y"):
|
||||
dev.omnyfsh.fshclose()
|
||||
self.gui.on_live_view_enabled(False)
|
||||
print("setting 'XOMNYI-XEYE-ACQ:0'")
|
||||
|
||||
print(
|
||||
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy"
|
||||
f" = {fovy:.0f} microns"
|
||||
)
|
||||
print("Use the matlab routine to FIT the current alignment...")
|
||||
print("Check the fit in the GUI...")
|
||||
|
||||
print("Then LOAD ALIGNMENT PARAMETERS by running flomni.read_alignment_offset()\n")
|
||||
time.sleep(5)
|
||||
|
||||
print("Automatically loading new alignment parameters from xray eye alignment.\n")
|
||||
|
||||
self.flomni.read_alignment_offset(get_data_from_gui=True)
|
||||
|
||||
self.tomo_rotate(0)
|
||||
|
||||
umv(dev.rtx, 0)
|
||||
print("You are ready to remove the xray eye and start ptychography scans.")
|
||||
print("Fine alignment: flomni.tomo_parameters() , then flomni.tomo_alignment_scan()")
|
||||
print("After that, run the fit in Matlab and load the new fit flomni.read_alignment_offset()")
|
||||
|
||||
def write_output(self):
|
||||
file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues")
|
||||
if not os.path.exists(file):
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
|
||||
with open(file, "w") as alignment_values_file:
|
||||
alignment_values_file.write("angle\thorizontal\n")
|
||||
|
||||
# Initialize an empty list to store fovx values
|
||||
fovx_list = []
|
||||
fovx_offsets = np.zeros(5) # holds offsets for k = 1..5
|
||||
|
||||
for k in range(1, 6):
|
||||
fovx_offset = self.alignment_values[0] - self.alignment_values[k]
|
||||
print(f"Writing to file new alignment: number {k}, value x {fovx_offset}")
|
||||
alignment_values_file.write(f"{(k-1)*45}\t{fovx_offset*1000}\n")
|
||||
fovx_offsets[k - 1] = fovx_offset # store in array
|
||||
|
||||
fovx_x = (k - 1) * 45
|
||||
fovx_list.append([fovx_x, fovx_offset * 1000]) # Append the data to the list
|
||||
|
||||
print(f"Alignment number {k}, value x {fovx_offset}")
|
||||
alignment_values_file.write(f"{fovx_x}\t{fovx_offset * 1000}\n")
|
||||
|
||||
# Now build final numpy array:
|
||||
data = np.array(
|
||||
[
|
||||
[0, 45, 90, 135, 180], # angles
|
||||
fovx_offsets * 1000, # fovx_offset values
|
||||
[0, 0, 0, 0, 0],
|
||||
]
|
||||
)
|
||||
self.gui.submit_fit_array(data)
|
||||
print(f"fit submited with {data}")
|
||||
# self.flomni.flomnigui_show_xeyealign_fittab()
|
||||
|
||||
BIN
csaxs_bec/bec_ipython_client/plugins/omny/OMNY.png
Normal file
BIN
csaxs_bec/bec_ipython_client/plugins/omny/OMNY.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 359 KiB |
@@ -1,7 +1,5 @@
|
||||
import builtins
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea
|
||||
|
||||
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
|
||||
if builtins.__dict__.get("bec") is not None:
|
||||
@@ -21,7 +19,7 @@ class OMNYGuiTools:
|
||||
|
||||
def __init__(self, client):
|
||||
self.gui = getattr(client, "gui", None)
|
||||
self.gui_window = self.gui.windows['main'].widget
|
||||
self.gui_window = self.gui.windows["main"].widget
|
||||
self.fig200 = None
|
||||
self.fig201 = None
|
||||
self.fig202 = None
|
||||
@@ -83,16 +81,12 @@ class OMNYGuiTools:
|
||||
pass
|
||||
text = (
|
||||
"<pre>"
|
||||
+ " ,o888888o. ,8. ,8. b. 8 `8.`8888. ,8' \n"
|
||||
+ " . 8888 `88. ,888. ,888. 888o. 8 `8.`8888. ,8' \n"
|
||||
+ ",8 8888 `8b .`8888. .`8888. Y88888o. 8 `8.`8888. ,8' \n"
|
||||
+ "88 8888 `8b ,8.`8888. ,8.`8888. .`Y888888o. 8 `8.`8888.,8' \n"
|
||||
+ "88 8888 88 ,8'8.`8888,8^8.`8888. 8o. `Y888888o. 8 `8.`88888' \n"
|
||||
+ "88 8888 88 ,8' `8.`8888' `8.`8888. 8`Y8o. `Y88888o8 `8. 8888 \n"
|
||||
+ "88 8888 ,8P ,8' `8.`88' `8.`8888. 8 `Y8o. `Y8888 `8 8888 \n"
|
||||
+ "`8 8888 ,8P ,8' `8.`' `8.`8888. 8 `Y8o. `Y8 8 8888 \n"
|
||||
+ " ` 8888 ,88' ,8' `8 `8.`8888. 8 `Y8o.` 8 8888 \n"
|
||||
+ " `8888888P' ,8' ` `8.`8888. 8 `Yo 8 8888 \n"
|
||||
+ "██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ██╗██╗ ██╗\n"
|
||||
+ "██╔══██╗██╔════╝██╔════╝ ██╔═══██╗████╗ ████║████╗ ██║╚██╗ ██╔╝\n"
|
||||
+ "██████╔╝█████╗ ██║ ██║ ██║██╔████╔██║██╔██╗ ██║ ╚████╔╝ \n"
|
||||
+ "██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╗██║ ╚██╔╝ \n"
|
||||
+ "██████╔╝███████╗╚██████╗ ╚██████╔╝██║ ╚═╝ ██║██║ ╚████║ ██║ \n"
|
||||
+ "╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ \n"
|
||||
+ "</pre>"
|
||||
)
|
||||
self.idle_text_box.set_html_text(text)
|
||||
@@ -139,7 +133,9 @@ class OMNYGuiTools:
|
||||
if self.progressbar is None:
|
||||
self.omnygui_remove_all_docks()
|
||||
# Add a new dock with a RingProgressBar widget
|
||||
self.progressbar = self.gui_window.add_dock(name="progress").add_widget("RingProgressBar")
|
||||
self.progressbar = self.gui_window.add_dock(name="progress").add_widget(
|
||||
"RingProgressBar"
|
||||
)
|
||||
# Customize the size of the progress ring
|
||||
self.progressbar.set_line_widths(20)
|
||||
# Disable automatic updates and manually set the self.progressbar value
|
||||
|
||||
@@ -852,18 +852,6 @@ class OMNY(
|
||||
def sample_name(self):
|
||||
return dev.omny_samples.get_sample_name_in_samplestage()
|
||||
|
||||
def write_to_spec_log(self, content):
|
||||
try:
|
||||
with open(
|
||||
os.path.expanduser(
|
||||
"~/Data10/specES1/log-files/specES1_started_2022_11_30_1313.log"
|
||||
),
|
||||
"a",
|
||||
) as log_file:
|
||||
log_file.write(content)
|
||||
except Exception:
|
||||
logger.warning("Failed to write to spec log file (omny web page).")
|
||||
|
||||
def write_to_scilog(self, content, tags: list = None):
|
||||
try:
|
||||
if tags is not None:
|
||||
@@ -1288,7 +1276,6 @@ class OMNY(
|
||||
f"{str(datetime.datetime.now())}: omny scan projection at angle {angle}, scan"
|
||||
f" number {bec.queue.next_scan_number}.\n"
|
||||
)
|
||||
self.write_to_spec_log(log_message)
|
||||
# self.write_to_scilog(log_message, ["BEC_scans", self.sample_name])
|
||||
scans.omny_fermat_scan(
|
||||
fovx=self.fovx,
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
import builtins
|
||||
import datetime
|
||||
import fcntl
|
||||
import os
|
||||
import builtins
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import termios
|
||||
import threading
|
||||
import time
|
||||
import tty
|
||||
from pathlib import Path
|
||||
|
||||
import epics
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
logger = bec_logger.logger
|
||||
|
||||
if builtins.__dict__.get("bec") is not None:
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
scans = builtins.__dict__.get("scans")
|
||||
|
||||
|
||||
def umv(*args):
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
def umvr(*args):
|
||||
return scans.umv(*args, relative=True)
|
||||
|
||||
class OMNYToolsError(Exception):
|
||||
pass
|
||||
@@ -110,24 +119,20 @@ class OMNYTools:
|
||||
next1, next2 = sys.stdin.read(2)
|
||||
if next1 == "[":
|
||||
if next2 == "A":
|
||||
# print("up")
|
||||
if dev2 != "none":
|
||||
umvr(dev2, step2)
|
||||
if special_command != "none":
|
||||
special_command()
|
||||
elif next2 == "B":
|
||||
# print(" down")
|
||||
if dev2 != "none":
|
||||
umvr(dev2, -step2)
|
||||
if special_command != "none":
|
||||
special_command()
|
||||
elif next2 == "C":
|
||||
# print("right")
|
||||
umvr(dev1, step1)
|
||||
if special_command != "none":
|
||||
special_command()
|
||||
elif next2 == "D":
|
||||
# print("left")
|
||||
umvr(dev1, -step1)
|
||||
if special_command != "none":
|
||||
special_command()
|
||||
@@ -143,13 +148,135 @@ class OMNYTools:
|
||||
step2 = step2 / 2
|
||||
print(f"\rHalf step size. New step size: {step1}, {step2}\r")
|
||||
except IOError:
|
||||
# No input available, keep looping
|
||||
pass
|
||||
|
||||
# Sleep for a short period to avoid high CPU usage
|
||||
time.sleep(0.02)
|
||||
|
||||
finally:
|
||||
# Restore the terminal to its original state
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)
|
||||
|
||||
import socket
|
||||
|
||||
class PtychoReconstructor:
|
||||
"""Writes ptychography reconstruction queue files after each scan projection.
|
||||
|
||||
An external reconstruction engine monitors the queue folder and picks
|
||||
up .dat files as they are written.
|
||||
|
||||
Usage:
|
||||
reconstructor = PtychoReconstructor(folder_name="reconstruction_queue")
|
||||
reconstructor.write(
|
||||
scan_list=[1023, 1024],
|
||||
next_scan_number=1025,
|
||||
base_path="~/data/raw",
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(self, folder_name: str = "reconstruction_queue"):
|
||||
self.folder_name = folder_name
|
||||
|
||||
def _accounts_match(self) -> bool:
|
||||
"""Check if bec.active_account matches the current system user (p vs e prefix)."""
|
||||
try:
|
||||
bec = builtins.__dict__.get("bec")
|
||||
active = bec.active_account # e.g. "p23092"
|
||||
system_user = os.getenv("USER") or os.getlogin() # e.g. "e23092"
|
||||
print(f"Active server account {active}, BEC client account {system_user}.")
|
||||
return active[1:] == system_user[1:]
|
||||
except Exception:
|
||||
logger.warning("Failed to compare active account to system user.")
|
||||
return False
|
||||
|
||||
def write(self, scan_list: list, next_scan_number: int, base_path: str = "~/data/raw/analysis/"):
|
||||
"""Write a reconstruction queue file for the given scan list.
|
||||
|
||||
Args:
|
||||
scan_list (list): Scan numbers belonging to this projection
|
||||
(may contain multiple entries when stitching).
|
||||
next_scan_number (int): The current next scan number, used to
|
||||
name the queue file.
|
||||
base_path (str): Root path under which the queue folder lives.
|
||||
"""
|
||||
if not self._accounts_match():
|
||||
logger.warning("Active BEC account does not match system user — skipping queue file write.")
|
||||
return
|
||||
|
||||
base_path = os.path.expanduser(base_path)
|
||||
queue_path = Path(os.path.join(base_path, self.folder_name))
|
||||
queue_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
last_scan_number = next_scan_number - 1
|
||||
queue_file = os.path.abspath(
|
||||
os.path.join(queue_path, f"scan_{last_scan_number:05d}.dat")
|
||||
)
|
||||
with open(queue_file, "w") as f:
|
||||
scans = " ".join(str(s) for s in scan_list)
|
||||
f.write(f"p.scan_number {scans}\n")
|
||||
f.write("p.check_nextscan_started 1\n")
|
||||
|
||||
|
||||
class TomoIDManager:
|
||||
"""Registers a tomography measurement in the OMNY sample database
|
||||
and returns its assigned tomo ID.
|
||||
|
||||
Usage:
|
||||
id_manager = TomoIDManager()
|
||||
tomo_id = id_manager.register(
|
||||
sample_name="my_sample",
|
||||
date="2024-03-08",
|
||||
eaccount="e12345",
|
||||
scan_number=1001,
|
||||
setup="lamni",
|
||||
additional_info="test info",
|
||||
user="BEC",
|
||||
)
|
||||
"""
|
||||
|
||||
OMNY_URL = "https://omny.web.psi.ch/samples/newmeasurement.php"
|
||||
OMNY_USER = "omny"
|
||||
OMNY_PASSWORD = "samples"
|
||||
TMP_FILE = "/tmp/currsamplesnr.txt"
|
||||
|
||||
def register(
|
||||
self,
|
||||
sample_name: str,
|
||||
date: str,
|
||||
eaccount: str,
|
||||
scan_number: int,
|
||||
setup: str,
|
||||
additional_info: str,
|
||||
user: str,
|
||||
) -> int:
|
||||
"""Register a new measurement and return the assigned tomo ID.
|
||||
|
||||
Args:
|
||||
sample_name (str): Name of the sample.
|
||||
date (str): Date string (e.g. "2024-03-08").
|
||||
eaccount (str): E-account identifier.
|
||||
scan_number (int): First scan number of the measurement.
|
||||
setup (str): Setup name (e.g. "lamni").
|
||||
additional_info (str): Any additional sample information.
|
||||
user (str): User name.
|
||||
|
||||
Returns:
|
||||
int: The tomo ID assigned by the OMNY database.
|
||||
"""
|
||||
url = (
|
||||
f"{self.OMNY_URL}"
|
||||
f"?sample={sample_name}"
|
||||
f"&date={date}"
|
||||
f"&eaccount={eaccount}"
|
||||
f"&scannr={scan_number}"
|
||||
f"&setup={setup}"
|
||||
f"&additional={additional_info}"
|
||||
f"&user={user}"
|
||||
)
|
||||
subprocess.run(
|
||||
f"wget --user={self.OMNY_USER} --password={self.OMNY_PASSWORD}"
|
||||
f" -q -O {self.TMP_FILE} '{url}'",
|
||||
shell=True,
|
||||
)
|
||||
with open(self.TMP_FILE) as f:
|
||||
return int(f.read())
|
||||
@@ -48,7 +48,7 @@ class OMNYOpticsMixin:
|
||||
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=10000")
|
||||
|
||||
def oeye_out(self):
|
||||
fshclose()
|
||||
dev.omnyfsh.fshclose()
|
||||
if self.OMNYTools.yesno("Did you move in the optics?"):
|
||||
umv(dev.oeyez, -2)
|
||||
self._oeyey_mv(-60.3)
|
||||
|
||||
@@ -111,3 +111,34 @@ bec._beamline_mixin._bl_info_register(OperatorInfo)
|
||||
# SETUP PROMPTS
|
||||
bec._ip.prompts.session_name = _session_name
|
||||
bec._ip.prompts.status = 1
|
||||
|
||||
# ACCOUNT MISMATCH CHECK
|
||||
import os
|
||||
|
||||
def _check_account_mismatch():
|
||||
try:
|
||||
active = bec.active_account # e.g. "p23092"
|
||||
system_user = os.getenv("USER") or os.getlogin() # e.g. "e23092"
|
||||
if active[1:] != system_user[1:]:
|
||||
print(f"""
|
||||
\033[91m\033[1m
|
||||
██╗ ██╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗
|
||||
██║ ██║██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║██╔════╝
|
||||
██║ █╗ ██║███████║██████╔╝██╔██╗ ██║██║██╔██╗ ██║██║ ███╗
|
||||
██║███╗██║██╔══██║██╔══██╗██║╚██╗██║██║██║╚██╗██║██║ ██║
|
||||
╚███╔███╔╝██║ ██║██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝
|
||||
╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
ACCOUNT MISMATCH DETECTED!
|
||||
BEC active account : {active}
|
||||
System user : {system_user}
|
||||
|
||||
Data read and written by the BEC client does not match the data account!
|
||||
Please verify you are logged in with the correct account.
|
||||
\033[0m""")
|
||||
except Exception:
|
||||
logger.warning("Failed to verify account match.")
|
||||
|
||||
|
||||
if _args.session.lower() == "lamni" or _args.session.lower() == "flomni" or _args.session.lower() == "omny":
|
||||
_check_account_mismatch()
|
||||
@@ -13,69 +13,10 @@ logger = bec_logger.logger
|
||||
|
||||
|
||||
_Widgets = {
|
||||
"OmnyAlignment": "OmnyAlignment",
|
||||
"XRayEye": "XRayEye",
|
||||
}
|
||||
|
||||
|
||||
class OmnyAlignment(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_live_view(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@enable_live_view.setter
|
||||
@rpc_call
|
||||
def enable_live_view(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def user_message(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@user_message.setter
|
||||
@rpc_call
|
||||
def user_message(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def sample_name(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@sample_name.setter
|
||||
@rpc_call
|
||||
def sample_name(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_move_buttons(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@enable_move_buttons.setter
|
||||
@rpc_call
|
||||
def enable_move_buttons(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
class XRayEye(RPCBase):
|
||||
@rpc_call
|
||||
def active_roi(self) -> "BaseROI | None":
|
||||
@@ -83,20 +24,6 @@ class XRayEye(RPCBase):
|
||||
Return the currently active ROI, or None if no ROI is active.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_live_view(self):
|
||||
"""
|
||||
Get or set the live view enabled state.
|
||||
"""
|
||||
|
||||
@enable_live_view.setter
|
||||
@rpc_call
|
||||
def enable_live_view(self):
|
||||
"""
|
||||
Get or set the live view enabled state.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def user_message(self):
|
||||
@@ -111,6 +38,33 @@ class XRayEye(RPCBase):
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def on_live_view_enabled(self, enabled: "bool"):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def on_motors_enable(self, x_enable: "bool", y_enable: "bool"):
|
||||
"""
|
||||
Enable/Disable motor controls
|
||||
|
||||
Args:
|
||||
x_enable(bool): enable x motor controls
|
||||
y_enable(bool): enable y motor controls
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def enable_submit_button(self, enable: "bool"):
|
||||
"""
|
||||
Enable/disable submit button.
|
||||
Args:
|
||||
enable(int): -1 disable else enable
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def sample_name(self):
|
||||
@@ -139,6 +93,20 @@ class XRayEye(RPCBase):
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def switch_tab(self, tab: "str"):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_timeout(20)
|
||||
@rpc_call
|
||||
def submit_fit_array(self, fit_array):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
class XRayEye2DControl(RPCBase):
|
||||
@rpc_call
|
||||
@@ -146,3 +114,15 @@ class XRayEye2DControl(RPCBase):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def attach(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def detach(self):
|
||||
"""
|
||||
Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget.
|
||||
"""
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
|
||||
|
||||
from typing import TypedDict
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
import os
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.ui_loader import UILoader
|
||||
from qtpy.QtWidgets import QWidget, QPushButton, QLineEdit, QLabel, QVBoxLayout
|
||||
from bec_qthemes import material_icon
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
# class OmnyAlignmentUIComponents(TypedDict):
|
||||
# moveRightButton: QPushButton
|
||||
# moveLeftButton: QPushButton
|
||||
# moveUpButton: QPushButton
|
||||
# moveDownButton: QPushButton
|
||||
# image: Image
|
||||
|
||||
|
||||
class OmnyAlignment(BECWidget, QWidget):
|
||||
USER_ACCESS = ["enable_live_view", "enable_live_view.setter", "user_message", "user_message.setter","sample_name", "sample_name.setter", "enable_move_buttons", "enable_move_buttons.setter"]
|
||||
PLUGIN = True
|
||||
ui_file = "./omny_alignment.ui"
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self._load_ui()
|
||||
|
||||
def _load_ui(self):
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.ui)
|
||||
self.setLayout(layout)
|
||||
|
||||
icon_options = {"size": (16, 16), "convert_to_pixmap": False}
|
||||
self.ui.moveRightButton.setText("")
|
||||
self.ui.moveRightButton.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_right", **icon_options)
|
||||
)
|
||||
|
||||
self.ui.moveLeftButton.setText("")
|
||||
self.ui.moveLeftButton.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_left", **icon_options)
|
||||
)
|
||||
|
||||
self.ui.moveUpButton.setText("")
|
||||
self.ui.moveUpButton.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_up", **icon_options)
|
||||
)
|
||||
|
||||
self.ui.moveDownButton.setText("")
|
||||
self.ui.moveDownButton.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_down", **icon_options)
|
||||
)
|
||||
|
||||
self.ui.confirmButton.setText("OK")
|
||||
|
||||
|
||||
self.ui.liveViewSwitch.enabled.connect(self.on_live_view_enabled)
|
||||
|
||||
# self.ui.moveUpButton.clicked.connect(self.on_move_up)
|
||||
|
||||
|
||||
@property
|
||||
def enable_live_view(self):
|
||||
return self.ui.liveViewSwitch.checked
|
||||
|
||||
@enable_live_view.setter
|
||||
def enable_live_view(self, enable:bool):
|
||||
self.ui.liveViewSwitch.checked = enable
|
||||
|
||||
|
||||
@property
|
||||
def user_message(self):
|
||||
return self.ui.messageLineEdit.text()
|
||||
|
||||
@user_message.setter
|
||||
def user_message(self, message:str):
|
||||
self.ui.messageLineEdit.setText(message)
|
||||
|
||||
@property
|
||||
def sample_name(self):
|
||||
return self.ui.sampleLineEdit.text()
|
||||
|
||||
@sample_name.setter
|
||||
def sample_name(self, message:str):
|
||||
self.ui.sampleLineEdit.setText(message)
|
||||
|
||||
|
||||
@SafeSlot(bool)
|
||||
def on_live_view_enabled(self, enabled:bool):
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
logger.info(f"Live view is enabled: {enabled}")
|
||||
image: Image = self.ui.image
|
||||
if enabled:
|
||||
image.image("cam_xeye")
|
||||
return
|
||||
|
||||
image.disconnect_monitor("cam_xeye")
|
||||
|
||||
|
||||
@property
|
||||
def enable_move_buttons(self):
|
||||
move_up:QPushButton = self.ui.moveUpButton
|
||||
move_down:QPushButton = self.ui.moveDownButton
|
||||
move_left:QPushButton = self.ui.moveLeftButton
|
||||
move_right:QPushButton = self.ui.moveRightButton
|
||||
return move_up.isEnabled() and move_down.isEnabled() and move_left.isEnabled() and move_right.isEnabled()
|
||||
|
||||
@enable_move_buttons.setter
|
||||
def enable_move_buttons(self, enabled:bool):
|
||||
move_up:QPushButton = self.ui.moveUpButton
|
||||
move_down:QPushButton = self.ui.moveDownButton
|
||||
move_left:QPushButton = self.ui.moveLeftButton
|
||||
move_right:QPushButton = self.ui.moveRightButton
|
||||
|
||||
move_up.setEnabled(enabled)
|
||||
move_down.setEnabled(enabled)
|
||||
move_left.setEnabled(enabled)
|
||||
move_right.setEnabled(enabled)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = OmnyAlignment()
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['omny_alignment.py']}
|
||||
@@ -1,125 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>988</width>
|
||||
<height>821</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="2" column="2">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="moveRightButton">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="moveLeftButton">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="moveUpButton">
|
||||
<property name="text">
|
||||
<string>Up</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="moveDownButton">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="confirmButton">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="sampleLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="messageLineEdit"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Sample</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Message</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="3">
|
||||
<widget class="Image" name="image">
|
||||
<property name="enable_toolbar" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="inner_axes" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="monitor" stdset="0">
|
||||
<string>cam_xeye</string>
|
||||
</property>
|
||||
<property name="rotation" stdset="0">
|
||||
<number>3</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="ToggleSwitch" name="liveViewSwitch"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Live View</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>Image</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>image</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,54 +0,0 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from csaxs_bec.bec_widgets.widgets.omny_alignment.omny_alignment import OmnyAlignment
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='OmnyAlignment' name='omny_alignment'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class OmnyAlignmentPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = OmnyAlignment(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(OmnyAlignment.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "omny_alignment"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "OmnyAlignment"
|
||||
|
||||
def toolTip(self):
|
||||
return "OmnyAlignment"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -1,15 +0,0 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from csaxs_bec.bec_widgets.widgets.omny_alignment.omny_alignment_plugin import OmnyAlignmentPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(OmnyAlignmentPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -4,7 +4,9 @@ from bec_lib import bec_logger
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_qthemes import material_icon
|
||||
from bec_widgets import BECWidget, SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.rpc_decorator import rpc_timeout
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
||||
from bec_widgets.widgets.plots.roi.image_roi import BaseROI, CircularROI, RectangularROI
|
||||
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||
@@ -21,6 +23,8 @@ from qtpy.QtWidgets import (
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QTextEdit,
|
||||
QTabWidget,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -41,38 +45,38 @@ class XRayEye2DControl(BECWidget, QWidget):
|
||||
""")
|
||||
# Up
|
||||
self.move_up_button = QToolButton(parent=self)
|
||||
self.move_up_button.setIcon(material_icon('keyboard_double_arrow_up'))
|
||||
self.move_up_button.setIcon(material_icon("keyboard_double_arrow_up"))
|
||||
self.root_layout.addWidget(self.move_up_button, 0, 2)
|
||||
# Up tweak button
|
||||
self.move_up_tweak_button = QToolButton(parent=self)
|
||||
self.move_up_tweak_button.setIcon(material_icon('keyboard_arrow_up'))
|
||||
self.move_up_tweak_button.setIcon(material_icon("keyboard_arrow_up"))
|
||||
self.root_layout.addWidget(self.move_up_tweak_button, 1, 2)
|
||||
|
||||
# Left
|
||||
self.move_left_button = QToolButton(parent=self)
|
||||
self.move_left_button.setIcon(material_icon('keyboard_double_arrow_left'))
|
||||
self.move_left_button.setIcon(material_icon("keyboard_double_arrow_left"))
|
||||
self.root_layout.addWidget(self.move_left_button, 2, 0)
|
||||
# Left tweak button
|
||||
self.move_left_tweak_button = QToolButton(parent=self)
|
||||
self.move_left_tweak_button.setIcon(material_icon('keyboard_arrow_left'))
|
||||
self.move_left_tweak_button.setIcon(material_icon("keyboard_arrow_left"))
|
||||
self.root_layout.addWidget(self.move_left_tweak_button, 2, 1)
|
||||
|
||||
# Right
|
||||
self.move_right_button = QToolButton(parent=self)
|
||||
self.move_right_button.setIcon(material_icon('keyboard_double_arrow_right'))
|
||||
self.move_right_button.setIcon(material_icon("keyboard_double_arrow_right"))
|
||||
self.root_layout.addWidget(self.move_right_button, 2, 4)
|
||||
# Right tweak button
|
||||
self.move_right_tweak_button = QToolButton(parent=self)
|
||||
self.move_right_tweak_button.setIcon(material_icon('keyboard_arrow_right'))
|
||||
self.move_right_tweak_button.setIcon(material_icon("keyboard_arrow_right"))
|
||||
self.root_layout.addWidget(self.move_right_tweak_button, 2, 3)
|
||||
|
||||
# Down
|
||||
self.move_down_button = QToolButton(parent=self)
|
||||
self.move_down_button.setIcon(material_icon('keyboard_double_arrow_down'))
|
||||
self.move_down_button.setIcon(material_icon("keyboard_double_arrow_down"))
|
||||
self.root_layout.addWidget(self.move_down_button, 4, 2)
|
||||
# Down tweak button
|
||||
self.move_down_tweak_button = QToolButton(parent=self)
|
||||
self.move_down_tweak_button.setIcon(material_icon('keyboard_arrow_down'))
|
||||
self.move_down_tweak_button.setIcon(material_icon("keyboard_arrow_down"))
|
||||
self.root_layout.addWidget(self.move_down_tweak_button, 3, 2)
|
||||
|
||||
# Connections
|
||||
@@ -124,40 +128,69 @@ class XRayEye2DControl(BECWidget, QWidget):
|
||||
|
||||
|
||||
class XRayEye(BECWidget, QWidget):
|
||||
USER_ACCESS = ["active_roi", "enable_live_view", "enable_live_view.setter", "user_message", "user_message.setter",
|
||||
"sample_name", "sample_name.setter", "enable_move_buttons", "enable_move_buttons.setter"]
|
||||
USER_ACCESS = [
|
||||
"active_roi",
|
||||
"user_message",
|
||||
"user_message.setter",
|
||||
"on_live_view_enabled",
|
||||
"on_motors_enable",
|
||||
"enable_submit_button",
|
||||
"sample_name",
|
||||
"sample_name.setter",
|
||||
"enable_move_buttons",
|
||||
"enable_move_buttons.setter",
|
||||
"switch_tab",
|
||||
"submit_fit_array",
|
||||
]
|
||||
PLUGIN = True
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self._connected_motor = None
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
self._init_ui()
|
||||
self._make_connections()
|
||||
|
||||
# Connection to redis endpoints
|
||||
self.bec_dispatcher.connect_slot(self.device_updates, MessageEndpoints.device_readback("omny_xray_gui"))
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.getting_shutter_status, MessageEndpoints.device_readback("omnyfsh")
|
||||
)
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.getting_camera_status, MessageEndpoints.device_read_configuration(CAMERA[0])
|
||||
)
|
||||
|
||||
self.connect_motors()
|
||||
self.resize(800, 600)
|
||||
QTimer.singleShot(0, self._init_gui_trigger)
|
||||
|
||||
def _init_ui(self):
|
||||
self.core_layout = QHBoxLayout(self)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.tab_widget = QTabWidget(parent=self)
|
||||
self.root_layout.addWidget(self.tab_widget)
|
||||
|
||||
self.image = Image(parent=self)
|
||||
self.image.enable_toolbar = False # Disable default toolbar to not allow to user set anything
|
||||
self.alignment_tab = QWidget(parent=self)
|
||||
self.core_layout = QHBoxLayout(self.alignment_tab)
|
||||
|
||||
self.image = Image(parent=self.alignment_tab)
|
||||
self.image.color_map = "CET-L2"
|
||||
self.image.enable_toolbar = (
|
||||
False # Disable default toolbar to not allow to user set anything
|
||||
)
|
||||
self.image.inner_axes = False # Disable inner axes to maximize image area
|
||||
self.image.plot_item.vb.invertY(True) # #TODO Invert y axis to match logic of LabView GUI
|
||||
self.image.enable_full_colorbar = True
|
||||
self.image.invert_y = True # Invert y axis to match image coordinates
|
||||
|
||||
# Control panel on the right: vertical layout inside a fixed-width widget
|
||||
self.control_panel = QWidget(parent=self)
|
||||
self.control_panel = QWidget(parent=self.alignment_tab)
|
||||
self.control_panel_layout = QVBoxLayout(self.control_panel)
|
||||
self.control_panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.control_panel_layout.setSpacing(10)
|
||||
|
||||
# ROI toolbar + Live toggle (header row)
|
||||
self.roi_manager = ROIPropertyTree(parent=self, image_widget=self.image, compact=True,
|
||||
compact_orientation="horizontal")
|
||||
self.roi_manager = ROIPropertyTree(
|
||||
parent=self, image_widget=self.image, compact=True, compact_orientation="horizontal"
|
||||
)
|
||||
header_row = QHBoxLayout()
|
||||
header_row.setContentsMargins(0, 0, 0, 0)
|
||||
header_row.setSpacing(8)
|
||||
@@ -166,16 +199,36 @@ class XRayEye(BECWidget, QWidget):
|
||||
self.live_preview_label = QLabel("Live Preview", parent=self)
|
||||
self.live_preview_toggle = ToggleSwitch(parent=self)
|
||||
self.live_preview_toggle.checked = False
|
||||
header_row.addWidget(self.live_preview_label, 0, Qt.AlignVCenter)
|
||||
header_row.addWidget(self.live_preview_toggle, 0, Qt.AlignVCenter)
|
||||
header_row.addWidget(self.live_preview_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
header_row.addWidget(self.live_preview_toggle, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
self.control_panel_layout.addLayout(header_row)
|
||||
|
||||
switch_row = QHBoxLayout()
|
||||
switch_row.setContentsMargins(0, 0, 0, 0)
|
||||
switch_row.setSpacing(8)
|
||||
switch_row.addStretch()
|
||||
self.camera_running_label = QLabel("Camera running", parent=self)
|
||||
self.camera_running_toggle = ToggleSwitch(parent=self)
|
||||
# self.camera_running_toggle.checked = False
|
||||
self.camera_running_toggle.enabled.connect(self.camera_running_enabled)
|
||||
self.shutter_label = QLabel("Shutter open", parent=self)
|
||||
self.shutter_toggle = ToggleSwitch(parent=self)
|
||||
# self.shutter_toggle.checked = False
|
||||
self.shutter_toggle.enabled.connect(self.opening_shutter)
|
||||
switch_row.addWidget(self.shutter_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
switch_row.addWidget(self.shutter_toggle, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
switch_row.addWidget(self.camera_running_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
switch_row.addWidget(self.camera_running_toggle, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
self.control_panel_layout.addLayout(switch_row)
|
||||
|
||||
# separator
|
||||
self.control_panel_layout.addWidget(self._create_separator())
|
||||
|
||||
# 2D Positioner (fixed size)
|
||||
self.motor_control_2d = XRayEye2DControl(parent=self)
|
||||
self.control_panel_layout.addWidget(self.motor_control_2d, 0, Qt.AlignTop | Qt.AlignCenter)
|
||||
self.control_panel_layout.addWidget(
|
||||
self.motor_control_2d, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignCenter
|
||||
)
|
||||
|
||||
# separator
|
||||
self.control_panel_layout.addWidget(self._create_separator())
|
||||
@@ -190,9 +243,8 @@ class XRayEye(BECWidget, QWidget):
|
||||
# Submit button
|
||||
self.submit_button = QPushButton("Submit", parent=self)
|
||||
# Add to layout form
|
||||
step_size_form.addWidget(QLabel("Horizontal", parent=self), 0, 0)
|
||||
step_size_form.addWidget(QLabel("Step Size", parent=self), 0, 0)
|
||||
step_size_form.addWidget(self.step_size, 0, 1)
|
||||
step_size_form.addWidget(QLabel("Vertical", parent=self), 1, 0)
|
||||
step_size_form.addWidget(self.submit_button, 2, 0, 1, 2)
|
||||
|
||||
# Add form to control panel
|
||||
@@ -207,7 +259,8 @@ class XRayEye(BECWidget, QWidget):
|
||||
self.sample_name_line_edit.setReadOnly(True)
|
||||
form.addWidget(QLabel("Sample", parent=self), 0, 0)
|
||||
form.addWidget(self.sample_name_line_edit, 0, 1)
|
||||
self.message_line_edit = QLineEdit(parent=self)
|
||||
self.message_line_edit = QTextEdit(parent=self)
|
||||
self.message_line_edit.setFixedHeight(60)
|
||||
self.message_line_edit.setReadOnly(True)
|
||||
form.addWidget(QLabel("Message", parent=self), 1, 0)
|
||||
form.addWidget(self.message_line_edit, 1, 1)
|
||||
@@ -217,12 +270,57 @@ class XRayEye(BECWidget, QWidget):
|
||||
self.control_panel.adjustSize()
|
||||
p_hint = self.control_panel.sizeHint()
|
||||
self.control_panel.setFixedWidth(p_hint.width())
|
||||
self.control_panel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||
self.control_panel.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
||||
|
||||
# Core Layout: image (expanding) | control panel (fixed)
|
||||
self.core_layout.addWidget(self.image)
|
||||
self.core_layout.addWidget(self.control_panel)
|
||||
|
||||
self.tab_widget.addTab(self.alignment_tab, "Alignment")
|
||||
|
||||
self.fit_tab = QWidget(parent=self)
|
||||
self.fit_layout = QVBoxLayout(self.fit_tab)
|
||||
self.waveform_x = Waveform(parent=self.fit_tab)
|
||||
self.waveform_y = Waveform(parent=self.fit_tab)
|
||||
|
||||
self.waveform_x.plot(
|
||||
x=[0],
|
||||
y=[1],
|
||||
label="fit-x",
|
||||
dap=["SineModel", "LinearModel"],
|
||||
dap_parameters=[
|
||||
{"frequency": {"value": 0.0174533, "vary": False, "min": 0.01, "max": 0.02}},
|
||||
{"slope": {"value": 0, "vary": False, "min": 0.0, "max": 0.02}},
|
||||
],
|
||||
dap_oversample=5,
|
||||
)
|
||||
self.waveform_y.plot(
|
||||
x=[0],
|
||||
y=[2],
|
||||
label="fit-y",
|
||||
dap=["SineModel", "LinearModel"],
|
||||
dap_parameters=[
|
||||
{"frequency": {"value": 0.0174533, "vary": False, "min": 0.01, "max": 0.02}},
|
||||
{"slope": {"value": 0, "vary": False, "min": 0.0, "max": 0.02}},
|
||||
],
|
||||
dap_oversample=5,
|
||||
)
|
||||
self.fit_x = self.waveform_x.curves[0]
|
||||
self.fit_y = self.waveform_y.curves[0]
|
||||
|
||||
self.waveform_x.dap_params_update.connect(self.on_dap_params)
|
||||
self.waveform_y.dap_params_update.connect(self.on_dap_params)
|
||||
|
||||
for wave in (self.waveform_x, self.waveform_y):
|
||||
wave.x_label = "Angle (deg)"
|
||||
wave.x_grid = True
|
||||
wave.y_grid = True
|
||||
wave.enable_toolbar = True
|
||||
|
||||
self.fit_layout.addWidget(self.waveform_x)
|
||||
self.fit_layout.addWidget(self.waveform_y)
|
||||
self.tab_widget.addTab(self.fit_tab, "Fit")
|
||||
|
||||
def _make_connections(self):
|
||||
# Fetch initial state
|
||||
self.on_live_view_enabled(True)
|
||||
@@ -230,40 +328,47 @@ class XRayEye(BECWidget, QWidget):
|
||||
|
||||
# Make connections
|
||||
self.live_preview_toggle.enabled.connect(self.on_live_view_enabled)
|
||||
self.step_size.valueChanged.connect(lambda x: self.motor_control_2d.setProperty("step_size", x))
|
||||
self.step_size.valueChanged.connect(
|
||||
lambda x: self.motor_control_2d.setProperty("step_size", x)
|
||||
)
|
||||
self.submit_button.clicked.connect(self.submit)
|
||||
|
||||
def _create_separator(self):
|
||||
sep = QFrame(parent=self)
|
||||
sep.setFrameShape(QFrame.HLine)
|
||||
sep.setFrameShadow(QFrame.Sunken)
|
||||
sep.setFrameShape(QFrame.Shape.HLine)
|
||||
sep.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
sep.setLineWidth(1)
|
||||
return sep
|
||||
|
||||
def _init_gui_trigger(self):
|
||||
self.dev.omny_xray_gui.read()
|
||||
self.dev.omnyfsh.read()
|
||||
|
||||
################################################################################
|
||||
# Device Connection logic
|
||||
################################################################################
|
||||
|
||||
def connect_motors(self):
|
||||
""" Checks one of the possible motors for flomni, omny and lamni setup."""
|
||||
possible_motors = ['osamroy', 'lsamrot', 'fsamroy']
|
||||
"""Checks one of the possible motors for flomni, omny and lamni setup."""
|
||||
possible_motors = ["osamroy", "lsamrot", "fsamroy"]
|
||||
|
||||
for motor in possible_motors:
|
||||
if motor in self.dev:
|
||||
self.bec_dispatcher.connect_slot(self.on_tomo_angle_readback, MessageEndpoints.device_readback(motor))
|
||||
logger.info(f"Succesfully connected to {motor}")
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_tomo_angle_readback, MessageEndpoints.device_readback(motor)
|
||||
)
|
||||
logger.info(f"Successfully connected to {motor}")
|
||||
self._connected_motor = motor
|
||||
|
||||
################################################################################
|
||||
# Properties ported from the original OmnyAlignment, can be adjusted as needed
|
||||
################################################################################
|
||||
@SafeProperty(str)
|
||||
def user_message(self):
|
||||
return self.message_line_edit.text()
|
||||
return self.message_line_edit.toPlainText()
|
||||
|
||||
@user_message.setter
|
||||
@rpc_timeout(20)
|
||||
def user_message(self, message: str):
|
||||
self.message_line_edit.setText(message)
|
||||
|
||||
@@ -272,6 +377,7 @@ class XRayEye(BECWidget, QWidget):
|
||||
return self.sample_name_line_edit.text()
|
||||
|
||||
@sample_name.setter
|
||||
@rpc_timeout(20)
|
||||
def sample_name(self, message: str):
|
||||
self.sample_name_line_edit.setText(message)
|
||||
|
||||
@@ -291,6 +397,14 @@ class XRayEye(BECWidget, QWidget):
|
||||
# Slots ported from the original OmnyAlignment, can be adjusted as needed
|
||||
################################################################################
|
||||
|
||||
@SafeSlot(str)
|
||||
@rpc_timeout(20)
|
||||
def switch_tab(self, tab: str):
|
||||
if tab == "fit":
|
||||
self.tab_widget.setCurrentIndex(1)
|
||||
else:
|
||||
self.tab_widget.setCurrentIndex(0)
|
||||
|
||||
@SafeSlot()
|
||||
def get_roi_coordinates(self) -> dict | None:
|
||||
"""Get the coordinates of the currently active ROI."""
|
||||
@@ -302,20 +416,56 @@ class XRayEye(BECWidget, QWidget):
|
||||
return roi.get_coordinates()
|
||||
|
||||
@SafeSlot(bool)
|
||||
@rpc_timeout(20)
|
||||
def on_live_view_enabled(self, enabled: bool):
|
||||
logger.info(f"Live view is enabled: {enabled}")
|
||||
self.live_preview_toggle.blockSignals(True)
|
||||
if enabled:
|
||||
self.live_preview_toggle.checked = enabled
|
||||
self.image.image(CAMERA)
|
||||
self.image.image(device=CAMERA[0], signal=CAMERA[1])
|
||||
self.live_preview_toggle.blockSignals(False)
|
||||
return
|
||||
|
||||
self.image.disconnect_monitor(CAMERA)
|
||||
self.image.disconnect_monitor(CAMERA[0], CAMERA[1])
|
||||
self.live_preview_toggle.checked = enabled
|
||||
self.live_preview_toggle.blockSignals(False)
|
||||
|
||||
@SafeSlot(bool)
|
||||
def camera_running_enabled(self, enabled: bool):
|
||||
logger.info(f"Camera running: {enabled}")
|
||||
self.camera_running_toggle.blockSignals(True)
|
||||
self.dev.get(CAMERA[0]).live_mode_enabled.put(enabled)
|
||||
self.camera_running_toggle.checked = enabled
|
||||
self.camera_running_toggle.blockSignals(False)
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def getting_camera_status(self, data, meta):
|
||||
print(f"msg:{data}")
|
||||
live_mode_enabled = data.get("signals").get(f"{CAMERA[0]}_live_mode_enabled").get("value")
|
||||
self.camera_running_toggle.blockSignals(True)
|
||||
self.camera_running_toggle.checked = live_mode_enabled
|
||||
self.camera_running_toggle.blockSignals(False)
|
||||
|
||||
@SafeSlot(bool)
|
||||
def opening_shutter(self, enabled: bool):
|
||||
logger.info(f"Shutter changed from GUI to: {enabled}")
|
||||
self.shutter_toggle.blockSignals(True)
|
||||
if enabled:
|
||||
self.dev.omnyfsh.fshopen()
|
||||
else:
|
||||
self.dev.omnyfsh.fshclose()
|
||||
# self.shutter_toggle.checked = enabled
|
||||
self.shutter_toggle.blockSignals(False)
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def getting_shutter_status(self, data, meta):
|
||||
shutter_open = bool(data.get("signals").get("omnyfsh_shutter").get("value"))
|
||||
self.shutter_toggle.blockSignals(True)
|
||||
self.shutter_toggle.checked = shutter_open
|
||||
self.shutter_toggle.blockSignals(False)
|
||||
|
||||
@SafeSlot(bool, bool)
|
||||
@rpc_timeout(20)
|
||||
def on_motors_enable(self, x_enable: bool, y_enable: bool):
|
||||
"""
|
||||
Enable/Disable motor controls
|
||||
@@ -327,98 +477,116 @@ class XRayEye(BECWidget, QWidget):
|
||||
self.motor_control_2d.enable_controls_hor(x_enable)
|
||||
self.motor_control_2d.enable_controls_ver(y_enable)
|
||||
|
||||
@SafeSlot(int)
|
||||
def enable_submit_button(self, enable: int):
|
||||
@SafeSlot(bool)
|
||||
@rpc_timeout(20)
|
||||
def enable_submit_button(self, enable: bool):
|
||||
"""
|
||||
Enable/disable submit button.
|
||||
Args:
|
||||
enable(int): -1 disable else enable
|
||||
"""
|
||||
if enable == -1:
|
||||
self.submit_button.setEnabled(False)
|
||||
else:
|
||||
if enable:
|
||||
self.submit_button.setEnabled(True)
|
||||
else:
|
||||
self.submit_button.setEnabled(False)
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_dap_params(self, data, meta):
|
||||
print("#######################################")
|
||||
print("getting dap parameters")
|
||||
print(f"data: {data}")
|
||||
print(f"meta: {meta}")
|
||||
self.waveform_x.auto_range(True)
|
||||
self.waveform_y.auto_range(True)
|
||||
# self.bec_dispatcher.disconnect_slot(self.device_updates, MessageEndpoints.device_readback("omny_xray_gui"))
|
||||
curve_id = meta.get("curve_id")
|
||||
|
||||
if curve_id == "fit-x-SineModel+LinearModel":
|
||||
self.dev.omny_xray_gui.fit_params_x.set(data).wait()
|
||||
print(f"setting x data to {data}")
|
||||
else:
|
||||
self.dev.omny_xray_gui.fit_params_y.set(data).wait()
|
||||
print(f"setting y data to {data}")
|
||||
# self.bec_dispatcher.connect_slot(self.device_updates, MessageEndpoints.device_readback("omny_xray_gui"))
|
||||
|
||||
@SafeSlot(bool, bool)
|
||||
def on_tomo_angle_readback(self, data: dict, meta: dict):
|
||||
#TODO implement if needed
|
||||
# TODO implement if needed
|
||||
print(f"data: {data}")
|
||||
print(f"meta: {meta}")
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def device_updates(self, data: dict, meta: dict):
|
||||
"""
|
||||
Slot to handle device updates from omny_xray_gui device.
|
||||
|
||||
Args:
|
||||
data(dict): data from device
|
||||
meta(dict): metadata from device
|
||||
"""
|
||||
|
||||
signals = data.get('signals')
|
||||
enable_live_preview = signals.get("omny_xray_gui_update_frame_acq").get('value')
|
||||
enable_x_motor = signals.get("omny_xray_gui_enable_mv_x").get('value')
|
||||
enable_y_motor = signals.get("omny_xray_gui_enable_mv_y").get('value')
|
||||
self.on_live_view_enabled(bool(enable_live_preview))
|
||||
self.on_motors_enable(bool(enable_x_motor), bool(enable_y_motor))
|
||||
|
||||
# Signals from epics gui device
|
||||
# send message
|
||||
user_message = signals.get("omny_xray_gui_send_message").get('value')
|
||||
self.user_message = user_message
|
||||
# sample name
|
||||
sample_message = signals.get("omny_xray_gui_sample_name").get('value')
|
||||
self.sample_name = sample_message
|
||||
# enable frame acquisition
|
||||
update_frame_acq = signals.get("omny_xray_gui_update_frame_acq").get('value')
|
||||
self.on_live_view_enabled(bool(update_frame_acq))
|
||||
# enable submit button
|
||||
enable_submit_button = signals.get("omny_xray_gui_submit").get('value')
|
||||
self.enable_submit_button(enable_submit_button)
|
||||
@SafeSlot()
|
||||
@rpc_timeout(20)
|
||||
def submit_fit_array(self, fit_array):
|
||||
self.tab_widget.setCurrentIndex(1)
|
||||
# self.fix_x.title = " got fit array"
|
||||
print(f"got fit array {fit_array}")
|
||||
self.waveform_x.curves[0].set_data(x=fit_array[0], y=fit_array[1])
|
||||
self.waveform_y.curves[0].set_data(x=fit_array[0], y=fit_array[2])
|
||||
# self.fit_x.set_data(x=fit_array[0],y=fit_array[1])
|
||||
# self.fit_y.set_data(x=fit_array[0],y=fit_array[2])
|
||||
|
||||
@SafeSlot()
|
||||
def submit(self):
|
||||
"""Execute submit action by submit button."""
|
||||
if self.roi_manager.single_active_roi is None:
|
||||
logger.warning("No active ROI")
|
||||
return
|
||||
roi_coordinates = self.roi_manager.single_active_roi.get_coordinates()
|
||||
roi_center_x = roi_coordinates['center_x']
|
||||
roi_center_y = roi_coordinates['center_y']
|
||||
# Case of rectangular ROI
|
||||
if isinstance(self.roi_manager.single_active_roi, RectangularROI):
|
||||
roi_width = roi_coordinates['width']
|
||||
roi_height = roi_coordinates['height']
|
||||
elif isinstance(self.roi_manager.single_active_roi, CircularROI):
|
||||
roi_width = roi_coordinates['diameter']
|
||||
roi_height = roi_coordinates['radius']
|
||||
else:
|
||||
logger.warning("Unsupported ROI type for submit action.")
|
||||
return
|
||||
self.submit_button.blockSignals(True)
|
||||
try:
|
||||
if self.roi_manager.single_active_roi is None:
|
||||
logger.warning("No active ROI")
|
||||
return
|
||||
roi_coordinates = self.roi_manager.single_active_roi.get_coordinates()
|
||||
roi_center_x = roi_coordinates["center_x"]
|
||||
roi_center_y = roi_coordinates["center_y"]
|
||||
# Case of rectangular ROI
|
||||
if isinstance(self.roi_manager.single_active_roi, RectangularROI):
|
||||
roi_width = roi_coordinates["width"]
|
||||
roi_height = roi_coordinates["height"]
|
||||
elif isinstance(self.roi_manager.single_active_roi, CircularROI):
|
||||
roi_width = roi_coordinates["diameter"]
|
||||
roi_height = roi_coordinates["radius"]
|
||||
else:
|
||||
logger.warning("Unsupported ROI type for submit action.")
|
||||
return
|
||||
|
||||
print(f"current roi: x:{roi_center_x}, y:{roi_center_y}, w:{roi_width},h:{roi_height}") #TODO remove when will be not needed for debugging
|
||||
# submit roi coordinates
|
||||
step = int(self.dev.omny_xray_gui.step.read().get("omny_xray_gui_step").get('value'))
|
||||
# submit roi coordinates
|
||||
step = int(self.dev.omny_xray_gui.step.read().get("omny_xray_gui_step").get("value"))
|
||||
|
||||
getattr(self.dev.omny_xray_gui, f"xval_x_{step}").set(roi_center_x)
|
||||
getattr(self.dev.omny_xray_gui, f"yval_y_{step}").set(roi_center_y)
|
||||
getattr(self.dev.omny_xray_gui, f"width_x_{step}").set(roi_width)
|
||||
getattr(self.dev.omny_xray_gui, f"width_y_{step}").set(roi_height)
|
||||
self.dev.omny_xray_gui.submit.set(1)
|
||||
finally:
|
||||
self.submit_button.blockSignals(False)
|
||||
|
||||
xval_x = getattr(self.dev.omny_xray_gui.xval_x, f"xval_x_{step}").set(roi_center_x)
|
||||
xval_y = getattr(self.dev.omny_xray_gui.yval_y, f"yval_y_{step}").set(roi_center_y)
|
||||
width_x = getattr(self.dev.omny_xray_gui.width_x, f"width_x_{step}").set(roi_width)
|
||||
width_y = getattr(self.dev.omny_xray_gui.width_y, f"width_y_{step}").set(roi_height)
|
||||
self.dev.omny_xray_gui.submit.set(1)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup connections on widget close -> disconnect slots and stop live mode of camera."""
|
||||
self.bec_dispatcher.disconnect_slot(self.device_updates, MessageEndpoints.device_readback("omny_xray_gui"))
|
||||
getattr(self.dev,CAMERA[0]).live_mode = False
|
||||
if self._connected_motor is not None:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_tomo_angle_readback, MessageEndpoints.device_readback(self._connected_motor)
|
||||
)
|
||||
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.getting_shutter_status, MessageEndpoints.device_readback("omnyfsh")
|
||||
)
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.getting_camera_status, MessageEndpoints.device_read_configuration(CAMERA[0])
|
||||
)
|
||||
|
||||
getattr(self.dev, CAMERA[0]).stop_live_mode()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
apply_theme("light")
|
||||
dispatcher = BECDispatcher(gui_id="xray")
|
||||
win = XRayEye()
|
||||
|
||||
win.resize(1000, 800)
|
||||
|
||||
@@ -227,7 +227,7 @@ ftransy:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
sensor_voltage: -2.4
|
||||
sensor_voltage: -1.1
|
||||
ftransz:
|
||||
description: Sample transer Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
@@ -344,6 +344,9 @@ rtx:
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
@@ -361,6 +364,9 @@ rty:
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
limits:
|
||||
- -100
|
||||
- 100
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
@@ -376,6 +382,9 @@ rtz:
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
limits:
|
||||
- -100
|
||||
- 100
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
@@ -386,6 +395,16 @@ rtz:
|
||||
readoutPriority: on_request
|
||||
connectionTimeout: 20
|
||||
|
||||
rt_flyer:
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniFlyer
|
||||
deviceConfig:
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
readoutPriority: async
|
||||
connectionTimeout: 20
|
||||
enabled: true
|
||||
readOnly: False
|
||||
|
||||
############################################################
|
||||
####################### Cameras ############################
|
||||
############################################################
|
||||
@@ -435,9 +454,9 @@ cam_xeye:
|
||||
# deviceConfig:
|
||||
# camera_id: 203
|
||||
# bits_per_pixel: 24
|
||||
# num_rotation_90: 3
|
||||
# num_rotation_90: 2
|
||||
# transpose: false
|
||||
# force_monochrome: true
|
||||
# force_monochrome: false
|
||||
# m_n_colormode: 1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
@@ -472,7 +491,7 @@ omnyfsh:
|
||||
############################################################
|
||||
omny_xray_gui:
|
||||
description: Gui signals
|
||||
deviceClass: csaxs_bec.devices.omny.xray_epics_gui.OMNYXRayEpicsGUI
|
||||
deviceClass: csaxs_bec.devices.omny.xray_epics_gui.OMNYXRayAlignGUI
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
|
||||
@@ -271,4 +271,20 @@ rty:
|
||||
enabled: true
|
||||
readOnly: False
|
||||
|
||||
|
||||
############################################################
|
||||
######################### Cameras ##########################
|
||||
############################################################
|
||||
cam_xeye:
|
||||
description: Camera LamNI Xray eye ID15
|
||||
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
|
||||
deviceConfig:
|
||||
camera_id: 15
|
||||
bits_per_pixel: 24
|
||||
num_rotation_90: 3
|
||||
transpose: false
|
||||
force_monochrome: true
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
161
csaxs_bec/devices/epics/allied_vision_camera.py
Normal file
161
csaxs_bec/devices/epics/allied_vision_camera.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Module for the EPICS integration of the AlliedVision Camera via Vimba SDK."""
|
||||
|
||||
import threading
|
||||
import traceback
|
||||
from enum import IntEnum
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt, Kind, Signal
|
||||
from ophyd.areadetector import ADComponent as ADCpt
|
||||
from ophyd.areadetector import DetectorBase
|
||||
from ophyd_devices import PreviewSignal
|
||||
from ophyd_devices.devices.areadetector.cam import VimbaDetectorCam
|
||||
from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35 as ImagePlugin
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from typeguard import typechecked
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ACQUIRE_MODES(IntEnum):
|
||||
"""Acquiring enums for Allied Vision Camera"""
|
||||
|
||||
ACQUIRING = 1
|
||||
DONE = 0
|
||||
|
||||
|
||||
class AlliedVisionCamera(PSIDeviceBase, DetectorBase):
|
||||
"""
|
||||
Epics Area Detector interface for the Allied Vision Alvium G1-507m camera via Vimba SDK.
|
||||
The IOC runs with under the prefix: 'X12SA-GIGECAM-AV1:'.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
|
||||
|
||||
cam = ADCpt(VimbaDetectorCam, "cam1:")
|
||||
image = ADCpt(ImagePlugin, "image1:")
|
||||
|
||||
preview = Cpt(
|
||||
PreviewSignal,
|
||||
name="preview",
|
||||
ndim=2,
|
||||
num_rotation_90=0,
|
||||
transpose=False,
|
||||
doc="Preview signal of the AlliedVision camera.",
|
||||
)
|
||||
|
||||
live_mode_enabled = Cpt(
|
||||
Signal,
|
||||
name="live_mode_enabled",
|
||||
value=False,
|
||||
doc="Enable or disable live mode.",
|
||||
kind=Kind.config,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
prefix: str,
|
||||
poll_rate: int = 5,
|
||||
num_rotation_90: int = 0,
|
||||
transpose: bool = False,
|
||||
scan_info=None,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self._poll_thread = threading.Thread(
|
||||
target=self._poll_array_data, daemon=True, name=f"{self.name}_poll_thread"
|
||||
)
|
||||
self._poll_thread_kill_event = threading.Event()
|
||||
self._poll_start_event = threading.Event()
|
||||
if poll_rate <= 0:
|
||||
logger.warning(
|
||||
f"Poll rate must be positive for Camera {self.name} and non-zero, setting to 1 Hz."
|
||||
)
|
||||
poll_rate = 1
|
||||
self.stop_live_mode()
|
||||
elif poll_rate > 10:
|
||||
logger.warning(f"Poll rate too high for Camera {self.name}, setting to 10 Hz max.")
|
||||
poll_rate = 10
|
||||
self._poll_rate = poll_rate
|
||||
self._unique_array_id = 0
|
||||
self._pv_timeout = 2.0
|
||||
self.image: ImagePlugin
|
||||
self.preview.num_rotation_90 = num_rotation_90
|
||||
self.preview.transpose = transpose
|
||||
self._live_mode_lock = threading.RLock()
|
||||
self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False)
|
||||
self.cam.acquire.subscribe(self._on_live_mode_enabled_changed, run=False)
|
||||
|
||||
def start_live_mode(self) -> None:
|
||||
"""Start live mode."""
|
||||
self.live_mode_enabled.put(True)
|
||||
|
||||
def stop_live_mode(self) -> None:
|
||||
"""Stop live mode."""
|
||||
self.live_mode_enabled.put(False)
|
||||
|
||||
def _on_live_mode_enabled_changed(self, *args, value, **kwargs) -> None:
|
||||
self._apply_live_mode(bool(value))
|
||||
|
||||
def _apply_live_mode(self, enabled: bool) -> None:
|
||||
with self._live_mode_lock:
|
||||
if enabled:
|
||||
if not self._poll_start_event.is_set():
|
||||
self._poll_start_event.set()
|
||||
self.cam.acquire.put(ACQUIRE_MODES.ACQUIRING.value) # Start acquisition
|
||||
else:
|
||||
logger.info(f"Live mode already started for {self.name}.")
|
||||
return
|
||||
|
||||
if self._poll_start_event.is_set():
|
||||
self._poll_start_event.clear()
|
||||
self.cam.acquire.put(ACQUIRE_MODES.DONE.value) # Stop acquisition
|
||||
else:
|
||||
logger.info(f"Live mode already stopped for {self.name}.")
|
||||
|
||||
def on_connected(self):
|
||||
"""Reset the unique array ID on connection."""
|
||||
self.cam.array_counter.set(0).wait(timeout=self._pv_timeout)
|
||||
self.cam.array_callbacks.set(1).wait(timeout=self._pv_timeout)
|
||||
self._poll_thread.start()
|
||||
|
||||
def _poll_array_data(self):
|
||||
"""Poll the array data for preview updates."""
|
||||
while self._poll_start_event.wait():
|
||||
while not self._poll_thread_kill_event.wait(1 / self._poll_rate):
|
||||
try:
|
||||
# First check if there is a new image
|
||||
if self.image.unique_id.get() != self._unique_array_id:
|
||||
self._unique_array_id = self.image.unique_id.get()
|
||||
else:
|
||||
continue # No new image, skip update
|
||||
# Get new image data
|
||||
value = self.image.array_data.get()
|
||||
if value is None:
|
||||
logger.info(f"No image data available for preview of {self.name}")
|
||||
continue
|
||||
|
||||
array_size = self.image.array_size.get()
|
||||
if array_size[0] == 0: # 2D image, not color image
|
||||
array_size = array_size[1:]
|
||||
# Geometry correction for the image
|
||||
data = np.reshape(value, array_size)
|
||||
self.preview.put(data)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
content = traceback.format_exc()
|
||||
logger.error(
|
||||
f"Error while polling array data for preview of {self.name}: {content}"
|
||||
)
|
||||
|
||||
def on_destroy(self):
|
||||
"""Stop the polling thread on destruction."""
|
||||
self._poll_start_event.set()
|
||||
self._poll_thread_kill_event.set()
|
||||
if self._poll_thread.is_alive():
|
||||
self._poll_thread.join(timeout=2)
|
||||
@@ -104,7 +104,7 @@ DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
|
||||
("B", CHANNELREFERENCE.A),
|
||||
("C", CHANNELREFERENCE.T0), # T0
|
||||
("D", CHANNELREFERENCE.C),
|
||||
("E", CHANNELREFERENCE.D), # D One extra pulse once shutter closes for MCS
|
||||
("E", CHANNELREFERENCE.B), # B One extra pulse once shutter closes for MCS
|
||||
("F", CHANNELREFERENCE.E), # E + 1mu s
|
||||
("G", CHANNELREFERENCE.T0),
|
||||
("H", CHANNELREFERENCE.G),
|
||||
@@ -213,8 +213,23 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
|
||||
# NOTE Burst delay should be set to 0, don't remove as this will not be checked
|
||||
# Also set the burst count to 1 to only have a single pulse for DDG1.
|
||||
# As the IOC may be out of sync with the HW, we make sure that we set the default parameters
|
||||
# in the IOC to the expected values. In the past, we've experienced that IOC and HW can go out
|
||||
# of sync.
|
||||
self.burst_delay.put(1)
|
||||
time.sleep(0.02) # Give HW time to process
|
||||
self.burst_delay.put(0)
|
||||
time.sleep(0.02)
|
||||
|
||||
self.burst_count.put(2)
|
||||
time.sleep(0.02)
|
||||
self.burst_count.put(1)
|
||||
time.sleep(0.02)
|
||||
|
||||
self.burst_mode.put(1)
|
||||
time.sleep(0.02)
|
||||
self.burst_mode.put(0)
|
||||
time.sleep(0.02)
|
||||
|
||||
def keep_shutter_open_during_scan(self, open: True) -> None:
|
||||
"""
|
||||
@@ -291,17 +306,24 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
# Burst Period DDG1
|
||||
# Set burst_period to shutter width
|
||||
# c/t0 + self._shutter_to_open_delay + exp_time * burst_count
|
||||
shutter_width = (
|
||||
self._shutter_to_open_delay + exp_time * frames_per_trigger
|
||||
) # Shutter starts closing at end of exposure
|
||||
# SHUTTER WIDTH timing consists of the delay for the shutter to open
|
||||
# + the exposure time * frames per trigger
|
||||
shutter_width = self._shutter_to_open_delay + exp_time * frames_per_trigger
|
||||
# TOTAL EXPOSURE accounts for the shutter to open AND close. In addition, we add
|
||||
# a short additional delay of 3e-6 to allow for the extra trigger through 'ef'
|
||||
# (delay of 1e-6, width of 1e-6)
|
||||
total_exposure_time = 2 * self._shutter_to_open_delay + exp_time * frames_per_trigger + 3e-6
|
||||
if self.burst_period.get() != shutter_width:
|
||||
self.burst_period.put(shutter_width)
|
||||
# The burst_period has to be slightly longer
|
||||
self.burst_period.put(total_exposure_time)
|
||||
|
||||
# Trigger DDG2
|
||||
# a = t0 + 2ms, b = a + 1us
|
||||
# a has reference to t0, b has reference to a
|
||||
# Add delay of self._shutter_to_open_delay to allow shutter to open
|
||||
self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=1e-6)
|
||||
# AB is delayed by the shutter opening time, and the falling edge indicates the shutter has
|
||||
# fully closed, it has to be considered as the blocking signal for the next acquisition to start.
|
||||
# PS: + 3e-6
|
||||
self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=shutter_width)
|
||||
|
||||
# Trigger shutter
|
||||
# d = c/t0 + self._shutter_to_open_delay + exp_time * burst_count + 1ms
|
||||
@@ -321,7 +343,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
if self.scan_info.msg.scan_type == "fly":
|
||||
self.set_delay_pairs(channel="ef", delay=0, width=0)
|
||||
else:
|
||||
self.set_delay_pairs(channel="ef", delay=0, width=1e-6)
|
||||
self.set_delay_pairs(channel="ef", delay=1e-6, width=1e-6)
|
||||
|
||||
# NOTE Add additional sleep to make sure that the IOC and DDG HW process the values properly
|
||||
# This value has been choosen empirically after testing with the HW. It's
|
||||
|
||||
@@ -29,6 +29,7 @@ from ophyd_devices import DeviceStatus, StatusBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
|
||||
BURSTCONFIG,
|
||||
CHANNELREFERENCE,
|
||||
OUTPUTPOLARITY,
|
||||
STATUSBITS,
|
||||
@@ -37,7 +38,6 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
|
||||
ChannelConfig,
|
||||
DelayGeneratorCSAXS,
|
||||
LiteralChannels,
|
||||
BURSTCONFIG,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -138,6 +138,24 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
# Set burst config
|
||||
self.burst_config.put(BURSTCONFIG.FIRST_CYCLE.value)
|
||||
|
||||
# TODO As the IOC may be out of sync with the HW, we make sure that we set the default parameters
|
||||
# in the IOC to the expected values. In the past, we've experienced that IOC and HW can go out
|
||||
# of sync.
|
||||
self.burst_delay.put(1)
|
||||
time.sleep(0.02) # Give HW time to process
|
||||
self.burst_delay.put(0)
|
||||
time.sleep(0.02)
|
||||
|
||||
self.burst_count.put(2)
|
||||
time.sleep(0.02)
|
||||
self.burst_count.put(1)
|
||||
time.sleep(0.02)
|
||||
|
||||
self.burst_mode.put(1)
|
||||
time.sleep(0.02)
|
||||
self.burst_mode.put(0)
|
||||
time.sleep(0.02)
|
||||
|
||||
def on_stage(self) -> DeviceStatus | StatusBase | None:
|
||||
"""
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Callable, Literal
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd.utils.errors import WaitTimeoutError
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import EpicsSignalRO, Kind
|
||||
from ophyd_devices import (
|
||||
@@ -316,8 +317,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
try:
|
||||
scan_done = bool(value == self._num_total_triggers)
|
||||
self.progress.put(value=value, max_value=self._num_total_triggers, done=scan_done)
|
||||
if scan_done:
|
||||
self._scan_done_event.set()
|
||||
except Exception:
|
||||
content = traceback.format_exc()
|
||||
logger.info(f"Device {self.name} error: {content}")
|
||||
@@ -392,6 +391,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
self._current_data_index = 0
|
||||
|
||||
# NOTE Make sure that the signal that omits mca callbacks is cleared
|
||||
# DO NOT REMOVE!!
|
||||
self._omit_mca_callbacks.clear()
|
||||
|
||||
# For a fly scan we need to start the mcs card ourselves
|
||||
@@ -443,7 +443,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
while not self._scan_done_thread_kill_event.is_set():
|
||||
while self._start_monitor_async_data_emission.wait():
|
||||
try:
|
||||
logger.debug(f"Monitoring async data emission for {self.name}...")
|
||||
if (
|
||||
hasattr(self.scan_info.msg, "num_points")
|
||||
and self.scan_info.msg.num_points is not None
|
||||
@@ -453,7 +452,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
for callback in self._scan_done_callbacks:
|
||||
callback(exception=None)
|
||||
else:
|
||||
logger.info(f"Current data index is {self._current_data_index}")
|
||||
if self._current_data_index >= 1:
|
||||
for callback in self._scan_done_callbacks:
|
||||
callback(exception=None)
|
||||
@@ -515,7 +513,22 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
# that the acquisition finishes on the card and that data is emitted to BEC. If the acquisition
|
||||
# was already finished (i.e. normal step scan sends 1 extra pulse per burst cycle), this will
|
||||
# not have any effect as the card will already be in DONE state and signal.
|
||||
self.software_channel_advance.put(1)
|
||||
if self.scan_info.msg.scan_type == "fly":
|
||||
expected_points = int(
|
||||
self.scan_info.msg.num_points
|
||||
* self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
)
|
||||
|
||||
status = CompareStatus(self.current_channel, expected_points-1, operation_success=">=")
|
||||
try:
|
||||
status.wait(timeout=5)
|
||||
except WaitTimeoutError:
|
||||
text = f"Device {self.name} received num points {self.current_channel.get()} / {expected_points}. Device timed out after 5s."
|
||||
logger.error(text)
|
||||
raise TimeoutError(text)
|
||||
|
||||
# Manually set the last advance
|
||||
self.software_channel_advance.put(1)
|
||||
|
||||
# Prepare and register status callback for the async monitoring loop
|
||||
status_async_data = StatusBase(obj=self)
|
||||
@@ -549,8 +562,9 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Hook called when the device is stopped. In addition, any status that is registered through cancel_on_stop will be cancelled here."""
|
||||
self.stop_all.put(1)
|
||||
self.erase_all.put(1)
|
||||
with suppress_mca_callbacks(self):
|
||||
self.stop_all.put(1)
|
||||
self.erase_all.put(1)
|
||||
|
||||
def mcs_recovery(self, timeout: int = 1) -> None:
|
||||
"""
|
||||
|
||||
@@ -156,7 +156,6 @@ class Camera:
|
||||
camera_id (int): The ID of the camera device.
|
||||
m_n_colormode (Literal[0, 1, 2, 3]): Color mode for the camera.
|
||||
bits_per_pixel (Literal[8, 24]): Number of bits per pixel for the camera.
|
||||
live_mode (bool): Whether to enable live mode for the camera.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -3,21 +3,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Literal, Tuple, TypedDict
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import numpy as np
|
||||
from ophyd import Component as Cpt, Signal, Kind
|
||||
|
||||
from bec_lib import messages
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from csaxs_bec.devices.ids_cameras.base_integration.camera import Camera
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from ophyd_devices.utils.bec_signals import AsyncSignal, PreviewSignal
|
||||
|
||||
from csaxs_bec.devices.ids_cameras.base_integration.camera import Camera
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
from pydantic import ValidationInfo
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -45,8 +43,15 @@ class IDSCamera(PSIDeviceBase):
|
||||
doc="Signal for the region of interest (ROI).",
|
||||
async_update={"type": "add", "max_shape": [None]},
|
||||
)
|
||||
live_mode_enabled = Cpt(
|
||||
Signal,
|
||||
name="live_mode_enabled",
|
||||
value=False,
|
||||
doc="Enable or disable live mode.",
|
||||
kind=Kind.config,
|
||||
)
|
||||
|
||||
USER_ACCESS = ["live_mode", "mask", "set_rect_roi", "get_last_image"]
|
||||
USER_ACCESS = ["start_live_mode", "stop_live_mode", "mask", "set_rect_roi", "get_last_image"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -83,15 +88,22 @@ class IDSCamera(PSIDeviceBase):
|
||||
bits_per_pixel=bits_per_pixel,
|
||||
connect=False,
|
||||
)
|
||||
self._live_mode = False
|
||||
self._inputs = {"live_mode": live_mode}
|
||||
self._mask = np.zeros((1, 1), dtype=np.uint8)
|
||||
self.image.num_rotation_90 = num_rotation_90
|
||||
self.image.transpose = transpose
|
||||
self._force_monochrome = force_monochrome
|
||||
self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False)
|
||||
self.live_mode_enabled.put(bool(live_mode))
|
||||
|
||||
############## Live Mode Methods ##############
|
||||
|
||||
def start_live_mode(self) -> None:
|
||||
self.live_mode_enabled.put(True)
|
||||
|
||||
def stop_live_mode(self) -> None:
|
||||
self.live_mode_enabled.put(False)
|
||||
|
||||
@property
|
||||
def mask(self) -> np.ndarray:
|
||||
"""Return the current region of interest (ROI) for the camera."""
|
||||
@@ -114,22 +126,15 @@ class IDSCamera(PSIDeviceBase):
|
||||
)
|
||||
self._mask = value
|
||||
|
||||
@property
|
||||
def live_mode(self) -> bool:
|
||||
"""Return whether the camera is in live mode."""
|
||||
return self._live_mode
|
||||
|
||||
@live_mode.setter
|
||||
def live_mode(self, value: bool):
|
||||
"""Set the live mode for the camera."""
|
||||
if value != self._live_mode:
|
||||
if self.cam._connected is False: # $ pylint: disable=protected-access
|
||||
self.cam.on_connect()
|
||||
self._live_mode = value
|
||||
if value:
|
||||
self._start_live()
|
||||
else:
|
||||
self._stop_live()
|
||||
def _on_live_mode_enabled_changed(self, *args, value, **kwargs):
|
||||
"""Callback for when live mode is changed."""
|
||||
enabled = bool(value)
|
||||
if enabled and self.cam._connected is False: # pylint: disable=protected-access
|
||||
self.cam.on_connect()
|
||||
if enabled:
|
||||
self._start_live()
|
||||
else:
|
||||
self._stop_live()
|
||||
|
||||
def set_rect_roi(self, x: int, y: int, width: int, height: int):
|
||||
"""Set the rectangular region of interest (ROI) for the camera."""
|
||||
@@ -196,7 +201,7 @@ class IDSCamera(PSIDeviceBase):
|
||||
"""Connect to the camera."""
|
||||
self.cam.force_monochrome = self._force_monochrome
|
||||
self.cam.on_connect()
|
||||
self.live_mode = self._inputs.get("live_mode", False)
|
||||
self.live_mode_enabled.put(bool(self._inputs.get("live_mode", False)))
|
||||
self.set_rect_roi(0, 0, self.cam.cam.width.value, self.cam.cam.height.value)
|
||||
|
||||
def on_destroy(self):
|
||||
@@ -206,7 +211,7 @@ class IDSCamera(PSIDeviceBase):
|
||||
|
||||
def on_trigger(self):
|
||||
"""Handle the trigger event."""
|
||||
if not self.live_mode:
|
||||
if not bool(self.live_mode_enabled.get()):
|
||||
return
|
||||
image = self.image.get()
|
||||
if image is not None:
|
||||
|
||||
@@ -15,7 +15,6 @@ from csaxs_bec.devices.omny.galil.galil_ophyd import (
|
||||
GalilAxesReferenced,
|
||||
GalilController,
|
||||
GalilMotorIsMoving,
|
||||
GalilMotorResolution,
|
||||
GalilSetpointSignal,
|
||||
GalilSignalRO,
|
||||
retry_once,
|
||||
@@ -24,6 +23,19 @@ from csaxs_bec.devices.omny.galil.galil_ophyd import (
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class GalilMotorResolution(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
if self.parent.axis_Id_numeric < 6:
|
||||
return float(
|
||||
self.controller.socket_put_and_receive(f"MG encpermm[{self.parent.axis_Id_numeric}]")
|
||||
)
|
||||
else:
|
||||
return float(
|
||||
self.controller.socket_put_and_receive(f"MG stppermm[{self.parent.axis_Id_numeric}]")
|
||||
)
|
||||
|
||||
class LamniGalilController(GalilController):
|
||||
|
||||
|
||||
@@ -154,11 +166,18 @@ class LamniGalilReadbackSignal(GalilSignalRO):
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
step_mm = self.parent.motor_resolution.get()
|
||||
return current_pos / step_mm
|
||||
|
||||
if self.parent.axis_Id_numeric < 6:
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TP{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
encoder_resolution = self.parent.motor_resolution.get()
|
||||
logger.info(f"Read galil encoder position of axis {self.parent.axis_Id_numeric} to be TP {current_pos} with resolution {encoder_resolution}")
|
||||
return current_pos / encoder_resolution
|
||||
else:
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
step_mm = self.parent.motor_resolution.get()
|
||||
return current_pos / step_mm
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import threading
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError
|
||||
from ophyd_devices import AsyncMultiSignal, DeviceStatus, ProgressSignal
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from csaxs_bec.devices.omny.rt.rt_ophyd import (
|
||||
BECConfigError,
|
||||
RtCommunicationError,
|
||||
RtError,
|
||||
RtReadbackSignal,
|
||||
@@ -92,7 +90,8 @@ class RtFlomniController(Controller):
|
||||
parent._min_scan_buffer_reached = False
|
||||
start_time = time.time()
|
||||
for pos_index, pos in enumerate(positions):
|
||||
parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}")
|
||||
cmd = f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}"
|
||||
parent.socket_put_and_receive(cmd)
|
||||
if pos_index > 100:
|
||||
parent._min_scan_buffer_reached = True
|
||||
parent._min_scan_buffer_reached = True
|
||||
@@ -174,8 +173,12 @@ class RtFlomniController(Controller):
|
||||
self.set_device_read_write("foptx", False)
|
||||
self.set_device_read_write("fopty", False)
|
||||
|
||||
def move_samx_to_scan_region(self, fovx: float, cenx: float):
|
||||
def move_samx_to_scan_region(self, cenx: float, move_in_this_routine: bool = False):
|
||||
# attention. a movement will clear all positions in the rt trajectory generator!
|
||||
if move_in_this_routine == True:
|
||||
self.device_manager.devices.rtx.obj.move(cenx, wait=True)
|
||||
time.sleep(0.05)
|
||||
# at cenx we expect the PID to be close to zero for a good fsamx position
|
||||
if self.rt_pid_voltage is None:
|
||||
rtx = self.device_manager.devices.rtx
|
||||
self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage")
|
||||
@@ -184,31 +187,41 @@ class RtFlomniController(Controller):
|
||||
"rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first."
|
||||
)
|
||||
logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}")
|
||||
expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100
|
||||
logger.info(f"Expected PID voltage: {expected_voltage}")
|
||||
expected_voltage = self.rt_pid_voltage
|
||||
# logger.info(f"Expected PID voltage: {expected_voltage}")
|
||||
logger.info(f"Current PID voltage: {self.get_pid_x()}")
|
||||
|
||||
wait_on_exit = False
|
||||
while True:
|
||||
if np.abs(self.get_pid_x() - expected_voltage) < 1:
|
||||
break
|
||||
wait_on_exit = True
|
||||
self.socket_put("v0")
|
||||
# we allow 2V range from center, this corresponds to 30 microns
|
||||
if np.abs(self.get_pid_x() - expected_voltage) < 2:
|
||||
logger.info("No correction of fsamx needed")
|
||||
else:
|
||||
fsamx = self.device_manager.devices.fsamx
|
||||
fsamx.read_only = False
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
|
||||
fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007
|
||||
logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}")
|
||||
fsamx_in = fsamx.user_parameter.get("in")
|
||||
fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True)
|
||||
fsamx.read_only = True
|
||||
time.sleep(0.1)
|
||||
self.laser_tracker_on()
|
||||
time.sleep(0.01)
|
||||
while True:
|
||||
# when we correct, then to 1 V, within 15 microns
|
||||
if np.abs(self.get_pid_x() - expected_voltage) < 1:
|
||||
logger.info("No further correction needed")
|
||||
break
|
||||
wait_on_exit = True
|
||||
# disable FZP piezo feedback
|
||||
self.socket_put("v0")
|
||||
fsamx.read_only = False
|
||||
logger.info(f"Current PID voltage: {self.get_pid_x()}")
|
||||
# here we accumulate the correction
|
||||
fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.006
|
||||
fsamx_in = fsamx.user_parameter.get("in")
|
||||
logger.info(
|
||||
f"Moving fsamx to {cenx / 1000 * 0.7 + fsamx.obj.pid_x_correction}, PID portion of that {fsamx.obj.pid_x_correction}"
|
||||
)
|
||||
fsamx.obj.move(fsamx_in + cenx / 1000 * 0.7 + fsamx.obj.pid_x_correction, wait=True)
|
||||
fsamx.read_only = True
|
||||
time.sleep(0.1)
|
||||
self.laser_tracker_on()
|
||||
time.sleep(0.01)
|
||||
|
||||
if wait_on_exit:
|
||||
time.sleep(1)
|
||||
|
||||
# enable fast FZP feedback again
|
||||
self.socket_put("v1")
|
||||
|
||||
@threadlocked
|
||||
@@ -379,7 +392,7 @@ class RtFlomniController(Controller):
|
||||
val = float(self.socket_put_and_receive(f"j{axis_number}").strip())
|
||||
return val
|
||||
|
||||
def laser_tracker_check_signalstrength(self):
|
||||
def laser_tracker_check_signalstrength(self, verbose=True):
|
||||
if not self.laser_tracker_check_enabled():
|
||||
returnval = "disabled"
|
||||
else:
|
||||
@@ -390,9 +403,10 @@ class RtFlomniController(Controller):
|
||||
rtx = self.device_manager.devices.rtx
|
||||
min_signal = rtx.user_parameter.get("min_signal")
|
||||
low_signal = rtx.user_parameter.get("low_signal")
|
||||
print(f"low signal: {low_signal}")
|
||||
print(f"min signal: {min_signal}")
|
||||
print(f"signal: {signal}")
|
||||
if verbose:
|
||||
print(f"low signal: {low_signal}")
|
||||
print(f"min signal: {min_signal}")
|
||||
print(f"signal: {signal}")
|
||||
if signal < min_signal:
|
||||
time.sleep(1)
|
||||
if signal < min_signal:
|
||||
@@ -416,27 +430,6 @@ class RtFlomniController(Controller):
|
||||
t.add_row([i, self.read_ssi_interferometer(i)])
|
||||
print(t)
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[4])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[7])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[2])},
|
||||
"average_x_st_fzp": {"value": float(return_table[3])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[4])},
|
||||
"target_y": {"value": float(return_table[5])},
|
||||
"average_y_st_fzp": {"value": float(return_table[6])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[7])},
|
||||
"average_rotz": {"value": float(return_table[8])},
|
||||
"stdev_rotz": {"value": float(return_table[9])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
}
|
||||
return signals
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
if not self.feedback_is_running():
|
||||
@@ -476,91 +469,6 @@ class RtFlomniController(Controller):
|
||||
current_position_in_scan = int(float(return_table[2]))
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
def read_positions_from_sampler(self):
|
||||
# this was for reading after the scan completed
|
||||
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
|
||||
|
||||
read_counter = 0
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
# while scan is running
|
||||
while mode > 0:
|
||||
|
||||
#TODO here?: scan abortion if no progress in scan *raise error
|
||||
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
time.sleep(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
# logger.info(f"{return_table}")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
# logger.info(f"{return_table}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Flomni statistics: Average of all standard deviations: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}"
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.device_manager.connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_flomni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
),
|
||||
)
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
|
||||
class RtFlomniReadbackSignal(RtReadbackSignal):
|
||||
@retry_once
|
||||
@@ -606,6 +514,18 @@ class RtFlomniSetpointSignal(RtSetpointSignal):
|
||||
"The interferometer feedback is not running. Either it is turned off or and"
|
||||
" interferometer error occured."
|
||||
)
|
||||
|
||||
tracker_status = self.parent.controller.laser_tracker_check_signalstrength()
|
||||
|
||||
if tracker_status == "toolow":
|
||||
print(
|
||||
"The interferometer signal is too low for movements. Realignment required."
|
||||
)
|
||||
raise RtError(
|
||||
"The interferometer signal is too low for movements. Realignment required."
|
||||
)
|
||||
|
||||
|
||||
self.set_with_feedback_disabled(val)
|
||||
|
||||
def set_with_feedback_disabled(self, val):
|
||||
@@ -816,6 +736,185 @@ class RtFlomniMotor(Device, PositionerBase):
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
class RtFlomniFlyer(Device):
|
||||
USER_ACCESS = ["controller"]
|
||||
data = Cpt(
|
||||
AsyncMultiSignal,
|
||||
name="data",
|
||||
signals=[
|
||||
"target_x",
|
||||
"average_x_st_fzp",
|
||||
"stdev_x_st_fzp",
|
||||
"target_y",
|
||||
"average_y_st_fzp",
|
||||
"stdev_y_st_fzp",
|
||||
"average_rotz",
|
||||
"stdev_rotz",
|
||||
"average_stdeviations_x_st_fzp",
|
||||
"average_stdeviations_y_st_fzp",
|
||||
],
|
||||
ndim=1,
|
||||
async_update={"type": "add", "max_shape": [None]},
|
||||
max_size=1000,
|
||||
)
|
||||
progress = Cpt(
|
||||
ProgressSignal, doc="ProgressSignal indicating the progress of the device during a scan."
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2844.psi.ch",
|
||||
port=2222,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(prefix=prefix, name=name, parent=parent, **kwargs)
|
||||
self.shutdown_event = threading.Event()
|
||||
self.controller = RtFlomniController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
|
||||
)
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
self.readout_thread = None
|
||||
self.scan_done_event = threading.Event()
|
||||
self.scan_done_event.set()
|
||||
|
||||
def read_positions_from_sampler(self, status: DeviceStatus):
|
||||
"""
|
||||
Read the positions from the sampler and update the data signal.
|
||||
This function runs in a separate thread and continuously checks the
|
||||
scan status.
|
||||
|
||||
Args:
|
||||
status (DeviceStatus): The status object to update when the readout is complete.
|
||||
"""
|
||||
read_counter = 0
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = (
|
||||
self.controller.get_scan_status()
|
||||
)
|
||||
|
||||
# while scan is running
|
||||
while mode > 0 and not self.shutdown_event.wait(0.01):
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = (
|
||||
self.controller.get_scan_status()
|
||||
)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (
|
||||
self.controller.socket_put_and_receive(f"r{read_counter}")
|
||||
).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
self.progress.put(
|
||||
value=read_counter, max_value=number_of_positions_planned, done=False
|
||||
)
|
||||
read_counter = read_counter + 1
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.data.set(signals)
|
||||
|
||||
if self.shutdown_event.wait(0.05):
|
||||
logger.info("Shutdown event set, stopping readout.")
|
||||
# if we are here, the shutdown_event is set. We can exit the readout loop.
|
||||
status.set_finished()
|
||||
return
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter and not self.shutdown_event.is_set():
|
||||
return_table = (self.controller.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
self.progress.put(value=read_counter, max_value=number_of_positions_planned, done=False)
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.data.set(signals)
|
||||
|
||||
# NOTE: No need to set the status to failed if the shutdown_event is set.
|
||||
# The stop() method will take care of that.
|
||||
status.set_finished()
|
||||
self.progress.put(value=read_counter, max_value=number_of_positions_planned, done=True)
|
||||
|
||||
logger.info(
|
||||
"Flomni statistics: Average of all standard deviations: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}"
|
||||
)
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[4])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[7])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[2])},
|
||||
"average_x_st_fzp": {"value": float(return_table[3])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[4])},
|
||||
"target_y": {"value": float(return_table[5])},
|
||||
"average_y_st_fzp": {"value": float(return_table[6])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[7])},
|
||||
"average_rotz": {"value": float(return_table[8])},
|
||||
"stdev_rotz": {"value": float(return_table[9])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
}
|
||||
return signals
|
||||
|
||||
def stage(self):
|
||||
self.shutdown_event.clear()
|
||||
self.scan_done_event.set()
|
||||
return super().stage()
|
||||
|
||||
def start_readout(self, status: DeviceStatus):
|
||||
self.readout_thread = threading.Thread(
|
||||
target=self.read_positions_from_sampler, args=(status,)
|
||||
)
|
||||
self.readout_thread.start()
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
self.shutdown_event.clear()
|
||||
self.scan_done_event.clear()
|
||||
while not self.controller._min_scan_buffer_reached and not self.shutdown_event.wait(0.001):
|
||||
...
|
||||
self.controller.start_scan()
|
||||
self.shutdown_event.wait(0.1)
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
return status
|
||||
|
||||
def complete(self) -> DeviceStatus:
|
||||
"""Wait until the flyer is done."""
|
||||
if self.scan_done_event.is_set():
|
||||
# if the scan_done_event is already set, we can return a finished status immediately
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
return status
|
||||
status = DeviceStatus(self)
|
||||
self.start_readout(status)
|
||||
status.add_callback(lambda *args, **kwargs: self.scan_done_event.set())
|
||||
return status
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.shutdown_event.set()
|
||||
self.scan_done_event.set()
|
||||
if self.readout_thread is not None:
|
||||
self.readout_thread.join()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rtcontroller = RtFlomniController(
|
||||
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222, device_manager=None
|
||||
|
||||
@@ -48,6 +48,7 @@ class OMNYFastShutter(PSIDeviceBase, Device):
|
||||
def fshopen(self):
|
||||
"""Open the fast shutter."""
|
||||
if self._check_if_cSAXS_shutter_exists_in_config():
|
||||
self.shutter.put(1)
|
||||
return self.device_manager.devices["fsh"].fshopen()
|
||||
else:
|
||||
self.shutter.put(1)
|
||||
@@ -55,6 +56,7 @@ class OMNYFastShutter(PSIDeviceBase, Device):
|
||||
def fshclose(self):
|
||||
"""Close the fast shutter."""
|
||||
if self._check_if_cSAXS_shutter_exists_in_config():
|
||||
self.shutter.put(0)
|
||||
return self.device_manager.devices["fsh"].fshclose()
|
||||
else:
|
||||
self.shutter.put(0)
|
||||
|
||||
@@ -2,7 +2,7 @@ import requests
|
||||
import threading
|
||||
import cv2
|
||||
import numpy as np
|
||||
from ophyd import Device, Component as Cpt
|
||||
from ophyd import Device, Component as Cpt, Kind, Signal
|
||||
from ophyd_devices import PreviewSignal
|
||||
import traceback
|
||||
|
||||
@@ -13,6 +13,13 @@ logger = bec_logger.logger
|
||||
class WebcamViewer(Device):
|
||||
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
|
||||
preview = Cpt(PreviewSignal, ndim=2, num_rotation_90=0, transpose=False)
|
||||
live_mode_enabled = Cpt(
|
||||
Signal,
|
||||
name="live_mode_enabled",
|
||||
value=False,
|
||||
doc="Enable or disable live mode.",
|
||||
kind=Kind.config,
|
||||
)
|
||||
|
||||
def __init__(self, url:str, name:str, num_rotation_90=0, transpose=False, **kwargs) -> None:
|
||||
super().__init__(name=name, **kwargs)
|
||||
@@ -21,20 +28,54 @@ class WebcamViewer(Device):
|
||||
self._update_thread = None
|
||||
self._buffer = b""
|
||||
self._shutdown_event = threading.Event()
|
||||
self._live_mode_lock = threading.RLock()
|
||||
self.preview.num_rotation_90 = num_rotation_90
|
||||
self.preview.transpose = transpose
|
||||
self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False)
|
||||
|
||||
def start_live_mode(self) -> None:
|
||||
if self._connection is not None:
|
||||
return
|
||||
self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||
self._update_thread.start()
|
||||
self.live_mode_enabled.put(True)
|
||||
|
||||
def stop_live_mode(self) -> None:
|
||||
self.live_mode_enabled.put(False)
|
||||
|
||||
def _on_live_mode_enabled_changed(self, *args, value, **kwargs) -> None:
|
||||
self._apply_live_mode(bool(value))
|
||||
|
||||
def _apply_live_mode(self, enabled: bool) -> None:
|
||||
with self._live_mode_lock:
|
||||
if enabled:
|
||||
if self._update_thread is not None and self._update_thread.is_alive():
|
||||
return
|
||||
self._shutdown_event.clear()
|
||||
self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||
self._update_thread.start()
|
||||
return
|
||||
|
||||
if self._update_thread is None:
|
||||
return
|
||||
self._shutdown_event.set()
|
||||
if self._connection is not None:
|
||||
try:
|
||||
self._connection.close()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
self._connection = None
|
||||
self._update_thread.join(timeout=2)
|
||||
if self._update_thread.is_alive():
|
||||
logger.warning("Webcam live mode thread did not stop within timeout.")
|
||||
return
|
||||
self._update_thread = None
|
||||
self._buffer = b""
|
||||
self._shutdown_event.clear()
|
||||
|
||||
def _update_loop(self) -> None:
|
||||
while not self._shutdown_event.is_set():
|
||||
try:
|
||||
self._connection = requests.get(self.url, stream=True)
|
||||
self._connection = requests.get(self.url, stream=True, timeout=5)
|
||||
for chunk in self._connection.iter_content(chunk_size=1024):
|
||||
if self._shutdown_event.is_set():
|
||||
break
|
||||
self._buffer += chunk
|
||||
start = self._buffer.find(b'\xff\xd8') # JPEG start
|
||||
end = self._buffer.find(b'\xff\xd9') # JPEG end
|
||||
@@ -50,16 +91,3 @@ class WebcamViewer(Device):
|
||||
except Exception as exc:
|
||||
content = traceback.format_exc()
|
||||
logger.error(f"Image update loop failed: {content}")
|
||||
|
||||
def stop_live_mode(self) -> None:
|
||||
if self._connection is None:
|
||||
return
|
||||
self._shutdown_event.set()
|
||||
if self._connection is not None:
|
||||
self._connection.close()
|
||||
self._connection = None
|
||||
if self._update_thread is not None:
|
||||
self._update_thread.join()
|
||||
self._update_thread = None
|
||||
|
||||
self._shutdown_event.clear()
|
||||
@@ -1,74 +1,44 @@
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
import numpy as np
|
||||
from ophyd import Component as Cpt, Signal, EpicsSignal
|
||||
from ophyd import Device
|
||||
from ophyd import DynamicDeviceComponent as Dcpt
|
||||
from ophyd import EpicsSignal
|
||||
|
||||
|
||||
class OMNYXRayAlignGUI(Device):
|
||||
|
||||
class OMNYXRayEpicsGUI(Device):
|
||||
update_frame_acqdone = Cpt(Signal, value=0)
|
||||
update_frame_acq = Cpt(Signal, value=0)
|
||||
enable_mv_x = Cpt(Signal, value=0)
|
||||
enable_mv_y = Cpt(Signal, value=0)
|
||||
send_message = Cpt(Signal, value=0)
|
||||
sample_name = Cpt(Signal, value=0)
|
||||
angle = Cpt(Signal, value=0)
|
||||
pixel_size = Cpt(Signal, value=0)
|
||||
submit = Cpt(EpicsSignal, name="submit", read_pv="XOMNYI-XEYE-SUBMIT:0", auto_monitor=True)
|
||||
step = Cpt(Signal, value=0)
|
||||
recbg = Cpt(Signal, value=0)
|
||||
mvx = Cpt(Signal, value=0)
|
||||
mvy = Cpt(Signal, value=0)
|
||||
|
||||
save_frame = Cpt(
|
||||
EpicsSignal, name="save_frame", read_pv="XOMNYI-XEYE-SAVFRAME:0",auto_monitor=True
|
||||
)
|
||||
update_frame_acqdone = Cpt(
|
||||
EpicsSignal, name="update_frame_acqdone", read_pv="XOMNYI-XEYE-ACQDONE:0",auto_monitor=True
|
||||
)
|
||||
update_frame_acq = Cpt(
|
||||
EpicsSignal, name="update_frame_acq", read_pv="XOMNYI-XEYE-ACQ:0",auto_monitor=True
|
||||
)
|
||||
width_y_dynamic = {
|
||||
f"width_y_{i}": (EpicsSignal, f"XOMNYI-XEYE-YWIDTH_Y:{i}", {"auto_monitor": True}) for i in range(0, 11)
|
||||
}
|
||||
width_y = Dcpt(width_y_dynamic)
|
||||
width_x_dynamic = {
|
||||
f"width_x_{i}": (EpicsSignal, f"XOMNYI-XEYE-XWIDTH_X:{i}", {"auto_monitor": True}) for i in range(0, 11)
|
||||
}
|
||||
width_x = Dcpt(width_x_dynamic)
|
||||
enable_mv_x = Cpt(
|
||||
EpicsSignal, name="enable_mv_x", read_pv="XOMNYI-XEYE-ENAMVX:0",auto_monitor=True
|
||||
)
|
||||
enable_mv_y = Cpt(
|
||||
EpicsSignal, name="enable_mv_y", read_pv="XOMNYI-XEYE-ENAMVY:0",auto_monitor=True
|
||||
)
|
||||
send_message = Cpt(
|
||||
EpicsSignal, name="send_message", read_pv="XOMNYI-XEYE-MESSAGE:0.DESC",auto_monitor=True
|
||||
)
|
||||
sample_name = Cpt(
|
||||
EpicsSignal, name="sample_name", read_pv="XOMNYI-XEYE-SAMPLENAME:0.DESC",auto_monitor=True
|
||||
)
|
||||
angle = Cpt(
|
||||
EpicsSignal, name="angle", read_pv="XOMNYI-XEYE-ANGLE:0",auto_monitor=True
|
||||
)
|
||||
pixel_size = Cpt(
|
||||
EpicsSignal, name="pixel_size", read_pv="XOMNYI-XEYE-PIXELSIZE:0",auto_monitor=True
|
||||
)
|
||||
submit = Cpt(
|
||||
EpicsSignal, name="submit", read_pv="XOMNYI-XEYE-SUBMIT:0",auto_monitor=True
|
||||
)
|
||||
step = Cpt(
|
||||
EpicsSignal, name="step", read_pv="XOMNYI-XEYE-STEP:0",auto_monitor=True
|
||||
)
|
||||
xval_x_dynamic = {
|
||||
f"xval_x_{i}": (EpicsSignal, f"XOMNYI-XEYE-XVAL_X:{i}", {"auto_monitor": True}) for i in range(0, 11)
|
||||
}
|
||||
xval_x = Dcpt(xval_x_dynamic)
|
||||
yval_y_dynamic = {
|
||||
f"yval_y_{i}": (EpicsSignal, f"XOMNYI-XEYE-YVAL_Y:{i}", {"auto_monitor": True}) for i in range(0, 11)
|
||||
}
|
||||
yval_y = Dcpt(yval_y_dynamic)
|
||||
recbg = Cpt(
|
||||
EpicsSignal, name="recbg", read_pv="XOMNYI-XEYE-RECBG:0",auto_monitor=True
|
||||
)
|
||||
stage_pos_x_dynamic = {
|
||||
f"stage_pos_x_{i}": (EpicsSignal, f"XOMNYI-XEYE-STAGEPOSX:{i}", {"auto_monitor": True}) for i in range(1, 6)
|
||||
}
|
||||
stage_pos_x = Dcpt(stage_pos_x_dynamic)
|
||||
mvx = Cpt(
|
||||
EpicsSignal, name="mvx", read_pv="XOMNYI-XEYE-MVX:0",auto_monitor=True
|
||||
)
|
||||
mvy = Cpt(
|
||||
EpicsSignal, name="mvy", read_pv="XOMNYI-XEYE-MVY:0",auto_monitor=True
|
||||
)
|
||||
fit_array = Cpt(Signal, value=np.zeros((3, 10)))
|
||||
fit_params_x = Cpt(Signal, value=np.zeros((2, 3)))
|
||||
fit_params_y = Cpt(Signal, value=np.zeros((2, 3)))
|
||||
|
||||
# Generate width_y_0 to width_y_10
|
||||
for i in range(11):
|
||||
locals()[f"width_y_{i}"] = Cpt(Signal, value=0)
|
||||
|
||||
# Generate width_x_0 to width_x_10
|
||||
for i in range(11):
|
||||
locals()[f"width_x_{i}"] = Cpt(Signal, value=0)
|
||||
|
||||
# Generate xval_x_0 to xval_x_10
|
||||
for i in range(11):
|
||||
locals()[f"xval_x_{i}"] = Cpt(Signal, value=0)
|
||||
|
||||
# Generate yval_y_0 to yval_y_10
|
||||
for i in range(11):
|
||||
locals()[f"yval_y_{i}"] = Cpt(Signal, value=0)
|
||||
|
||||
# Generate stage_pos_x_1 to stage_pos_x_5
|
||||
for i in range(1, 6):
|
||||
locals()[f"stage_pos_x_{i}"] = Cpt(Signal, value=0)
|
||||
|
||||
@@ -169,6 +169,9 @@ class LamNIMixin:
|
||||
|
||||
self.device_manager.devices.lsamx.read_only = True
|
||||
self.device_manager.devices.lsamy.read_only = True
|
||||
|
||||
#update angle readback before start of the scan
|
||||
yield from self.stubs.send_rpc_and_wait("lsamrot", "readback.get")
|
||||
|
||||
yield from self.stubs.send_rpc_and_wait("rtx", "controller.feedback_enable_without_reset")
|
||||
|
||||
|
||||
@@ -24,21 +24,22 @@ import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.alarm_handler import Alarms
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_server.scan_server.errors import ScanAbortion
|
||||
from bec_server.scan_server.scans import SyncFlyScanBase
|
||||
from bec_server.scan_server.scans import AsyncFlyScanBase
|
||||
|
||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import TRIGGERSOURCE
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FlomniFermatScan(SyncFlyScanBase):
|
||||
class FlomniFermatScan(AsyncFlyScanBase):
|
||||
scan_name = "flomni_fermat_scan"
|
||||
scan_type = "fly"
|
||||
required_kwargs = ["fovx", "fovy", "exp_time", "step", "angle"]
|
||||
arg_input = {}
|
||||
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
|
||||
use_scan_progress_report = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -52,7 +53,7 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
angle: float = None,
|
||||
corridor_size: float = 3,
|
||||
parameter: dict = None,
|
||||
frames_per_trigger:int=1,
|
||||
frames_per_trigger: int = 1,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -76,7 +77,9 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
>>> scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01, frames_per_trigger=1)
|
||||
"""
|
||||
|
||||
super().__init__(parameter=parameter, exp_time=exp_time, frames_per_trigger=frames_per_trigger, **kwargs)
|
||||
super().__init__(
|
||||
parameter=parameter, exp_time=exp_time, frames_per_trigger=frames_per_trigger, **kwargs
|
||||
)
|
||||
self.show_live_table = False
|
||||
self.axis = []
|
||||
self.fovx = fovx
|
||||
@@ -100,6 +103,14 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
self.zshift = -100
|
||||
self.flomni_rotation_status = None
|
||||
|
||||
def scan_report_instructions(self):
|
||||
"""Scan report instructions for the progress bar"""
|
||||
yield from self.stubs.scan_report_instruction({"device_progress": ["rt_flyer"]})
|
||||
|
||||
@property
|
||||
def monitor_sync(self) -> str:
|
||||
return "rt_flyer"
|
||||
|
||||
def initialize(self):
|
||||
self.scan_motors = []
|
||||
self.update_readout_priority()
|
||||
@@ -109,10 +120,6 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
self.positions, corridor_size=self.optim_trajectory_corridor
|
||||
)
|
||||
|
||||
@property
|
||||
def monitor_sync(self):
|
||||
return "rt_flomni"
|
||||
|
||||
def reverse_trajectory(self):
|
||||
"""
|
||||
Reverse the trajectory. Every other scan should be reversed to
|
||||
@@ -153,13 +160,16 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1])
|
||||
|
||||
def _prepare_setup_part2(self):
|
||||
# Prepare DDG1 to use
|
||||
yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.EXT_RISING_EDGE.value)
|
||||
|
||||
# Prepare DDG1 to use
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"ddg1", "set_trigger", TRIGGERSOURCE.EXT_RISING_EDGE.value
|
||||
)
|
||||
|
||||
if self.flomni_rotation_status:
|
||||
self.flomni_rotation_status.wait()
|
||||
|
||||
rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False)
|
||||
# rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False)
|
||||
rtx_status = yield from self.stubs.set(device="rtx", value=self.cenx, wait=False)
|
||||
rtz_status = yield from self.stubs.set(device="rtz", value=self.positions[0][2], wait=False)
|
||||
|
||||
yield from self.stubs.send_rpc_and_wait("rtx", "controller.laser_tracker_on")
|
||||
@@ -167,22 +177,24 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
rtx_status.wait()
|
||||
rtz_status.wait()
|
||||
|
||||
# status = yield from self.stubs.send_rpc("rtx", "move", self.cenx)
|
||||
# status.wait()
|
||||
yield from self._transfer_positions_to_flomni()
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"rtx", "controller.move_samx_to_scan_region", self.fovx, self.cenx
|
||||
)
|
||||
tracker_signal_status = yield from self.stubs.send_rpc_and_wait(
|
||||
"rtx", "controller.laser_tracker_check_signalstrength"
|
||||
)
|
||||
#self.device_manager.connector.send_client_info(tracker_signal_status)
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"rtx", "controller.move_samx_to_scan_region", self.cenx
|
||||
)
|
||||
# self.device_manager.connector.send_client_info(tracker_signal_status)
|
||||
if tracker_signal_status == "low":
|
||||
self.device_manager.connector.raise_alarm(
|
||||
severity=0,
|
||||
alarm_type="LaserTrackerSignalStrength",
|
||||
source={"device": "rtx", "reason": "low signal strength", "method": "_prepare_setup_part2"},
|
||||
metadata={},
|
||||
msg="Signal strength of the laser tracker is low, sufficient to continue. Realignment recommended!",
|
||||
error_info = messages.ErrorInfo(
|
||||
error_message="Signal strength of the laser tracker is low, but sufficient to continue. Realignment recommended!",
|
||||
compact_error_message="Low signal strength of the laser tracker. Realignment recommended!",
|
||||
exception_type="LaserTrackerSignalStrengthLow",
|
||||
device="rtx",
|
||||
)
|
||||
self.device_manager.connector.raise_alarm(severity=Alarms.WARNING, info=error_info)
|
||||
elif tracker_signal_status == "toolow":
|
||||
raise ScanAbortion(
|
||||
"Signal strength of the laser tracker is too low for scanning. Realignment required!"
|
||||
@@ -281,39 +293,37 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
return np.array(positions)
|
||||
|
||||
def scan_core(self):
|
||||
# use a device message to receive the scan number and
|
||||
# scan ID before sending the message to the device server
|
||||
yield from self.stubs.kickoff(device="rtx")
|
||||
while True:
|
||||
yield from self.stubs.read(group="monitored")
|
||||
status = self.connector.get(MessageEndpoints.device_status("rt_scan"))
|
||||
if status:
|
||||
status_id = status.content.get("status", 1)
|
||||
request_id = status.metadata.get("RID")
|
||||
if status_id == 0 and self.metadata.get("RID") == request_id:
|
||||
break
|
||||
if status_id == 2 and self.metadata.get("RID") == request_id:
|
||||
raise ScanAbortion(
|
||||
"An error occured during the flomni readout:"
|
||||
f" {status.metadata.get('error')}"
|
||||
)
|
||||
# send off the flyer
|
||||
yield from self.stubs.kickoff(device="rt_flyer")
|
||||
|
||||
# start the readout loop of the flyer
|
||||
status = yield from self.stubs.complete(device="rt_flyer", wait=False)
|
||||
|
||||
# read the monitors until the flyer is done
|
||||
while not status.done:
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id)
|
||||
self.point_id += 1
|
||||
time.sleep(1)
|
||||
logger.debug("reading monitors")
|
||||
# yield from self.device_rpc("rtx", "controller.kickoff")
|
||||
|
||||
def move_to_start(self):
|
||||
"""return to the start position"""
|
||||
# in flomni, we need to move to the start position of the next scan, which is the end position of the current scan
|
||||
# this method is called in finalize and overwrites the default move_to_start()
|
||||
if isinstance(self.positions, np.ndarray) and len(self.positions[-1]) == 3:
|
||||
yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1])
|
||||
# yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1])
|
||||
# in x we move to cenx, then we avoid jumps in centering routine
|
||||
value = self.positions[-1]
|
||||
value[0] = self.cenx
|
||||
yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=value)
|
||||
return
|
||||
|
||||
logger.warning("No positions found to return to start")
|
||||
|
||||
def cleanup(self):
|
||||
yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.SINGLE_SHOT.value)
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"ddg1", "set_trigger", TRIGGERSOURCE.SINGLE_SHOT.value
|
||||
)
|
||||
yield from super().cleanup()
|
||||
|
||||
def run(self):
|
||||
@@ -321,6 +331,7 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
yield from self.read_scan_motors()
|
||||
self.prepare_positions()
|
||||
yield from self._prepare_setup()
|
||||
yield from self.scan_report_instructions()
|
||||
yield from self.open_scan()
|
||||
yield from self.stage()
|
||||
yield from self.run_baseline_reading()
|
||||
|
||||
@@ -50,16 +50,15 @@ Manually move the gripper to a transfer position
|
||||
After the sample transfer the sample stage moved to the measurement position with your new sample. The Xray eye will automatically move in and the shutter will open. You may already see the sample in the omny xeye interface running on the windows computer.
|
||||
If you see your sample already at the approximately correct height, you can skip steps 1 to 3. Otherwise adjust the height:
|
||||
|
||||
1. `flomni.rt_feedback_disable()` disable the closed loop operation to allow movement of coarse stages
|
||||
1. `flomni.feedback_disable()` disable the closed loop operation to allow movement of coarse stages
|
||||
1. `umvr(dev.fsamy, 0.01)`, attention: unit <mm>, move the sample stage relative up (positive) or down (negative) until the sample is approximately vertically centered in xray eye screen
|
||||
1. `flomni.xrayeye_update_frame()` will update the current image on the xray eye screen
|
||||
1. `flomni.xrayeye_alignment_start()` start the coarse alignment of the sample by measuring (clicking in the X-ray eye software) the sample position at 0, 45, 90, 135, 180 degrees. Then use the matlab routine `SPEC_ptycho_align.m` to fit this data.
|
||||
1. `flomni.read_alignment_offset()` read the generated alignment data.
|
||||
1. `flomni.xrayeye_alignment_start()` start the coarse alignment of the sample by measuring (clicking in the X-ray eye software) the sample position at 0, 45, 90, 135, 180 degrees. The GUI will present a fit of this data, which is automatically loaded to BEC for aligning the sample.
|
||||
|
||||
#### Fine alignment
|
||||
|
||||
After the xrayeyealign, a fine alignment needs to be performed using ptychography.
|
||||
_To bypass the fine alignment: `feye_out`_
|
||||
_To bypass the fine alignment: `flomni.feye_out`_
|
||||
|
||||
|
||||
1. `flomni.tomo_parameters()` Adjust the ptychographic scan parameters for performing an alignment scan. Typically FOVX = FOVX(Xrayeye)+20 mu, shell step = beamsize/2.5, number of projections and tomo mode are ignored in the alignment scans.
|
||||
@@ -71,13 +70,13 @@ _To bypass the fine alignment: `feye_out`_
|
||||
Now that the sample is aligned, the tomographic measurement can be performed.
|
||||
1. `flomni.tomo_parameters()` adjust the scan parameters for the tomographic scan. This includes the parameters for ptychographic scans of projections plus the strategy for angular sampling. The vertical shift adjusts the field of view, up (positive) or down (negative). After adjusting the numbers, type again `flomni.tomo_parameters()` and verify that they are correct.
|
||||
1. `flomni.tomo_scan_projection(angle)` perform a ptychographic scan at the rotation angle <angle>. Launch the tomographic measurement by `flomni.tomo_scan()`.
|
||||
1. Before changing sample, verify that all subtomograms were completely acquired using the `tomo_recons matlab` script.
|
||||
1. Before changing sample, verify that all subtomograms were completely acquired using the tomo_reconstruction matlab script.
|
||||
|
||||
#### If something went wrong…
|
||||
|
||||
A __single projection__ is to be repeated use
|
||||
`flomni.tomo_scan_projection(<angle>)`. The target angle of scans can be found in the second column of the file in
|
||||
`~/Data10/specES1/dat-files/omni_scannumbers.txt`
|
||||
`~/data/raw/logs/tomography_scannumbers.txt`
|
||||
|
||||
To continue an __interrupted tomography scan__:
|
||||
|
||||
@@ -108,12 +107,17 @@ The nano-positioning is controlled by a feedback loop running on a real-time lin
|
||||
|
||||
Once the loop has started, it is possible to start bec with the flOMNI configuration file.
|
||||
|
||||
Starting bec with session will load the scripts
|
||||
`bec --session flomni`
|
||||
|
||||
The flOMNI scripts can be loaded manually by
|
||||
`from csaxs_bec.bec_ipython_client.plugins.flomni import Flomni`
|
||||
`flomni = Flomni(bec)`
|
||||
|
||||
Loading the flOMNI configuration (this command will load the OMNY configuration only - isolated from the beamline)
|
||||
`bec.config.update_session_with_file("/bec/csaxs_bec/csaxs_bec/device_configs/flomni_config.yaml")`
|
||||
|
||||
Loading the flOMNI scripts
|
||||
`from csaxs_bec.bec_ipython_client.plugins.flomni import Flomni`
|
||||
`flomni = Flomni(bec)`
|
||||
|
||||
|
||||
If the realtime system is restarted, bec will lose communication. To restart:
|
||||
`flomni.rt_off()` … then wait a few seconds
|
||||
@@ -138,10 +142,14 @@ This script will first verify that the stages are not in an initialized state, a
|
||||
The positions of the optics stages are stored as stage parameters and are thus linked to the configuration file.
|
||||
Example: The OSAx “in” position can be reviewed by `dev.fosax.user_parameter`
|
||||
Update the value by (example "fosax", "in") by `dev.fosax.update_user_parameter({"in":value})`
|
||||
Important note: if these values are changed, they are not automatically stored to the config file and will only be available in the current session.
|
||||
|
||||
`flomni.ffzp_info()` shows info about the available FZPs at the current energy of the beamline. Optional parameter is the photon _energy_ in keV.
|
||||
Example: `flomni.ffzp_info(6.2)`
|
||||
|
||||
Documents about availabe optics can be accessed by
|
||||
`flomni.flomnigui_docs`
|
||||
|
||||
The [laser feedback](user.ptychography.flomni.laser_feedback) will be disabled and fine alignment lost if foptx/y are moved!
|
||||
|
||||
Following functions exist to move the optics in and out, with self-explaining naming.
|
||||
|
||||
@@ -26,7 +26,7 @@ The effective position of the axis of rotation shifts with sample thickness or m
|
||||
1. `dev.lsamx` and `dev.lsamy` will print current position and the center value. Update the center value by
|
||||
`dev.lsamx.update_user_parameter({'center':8.69})`
|
||||
`dev.lsamy.update_user_parameter({'center':8.69})`
|
||||
1. close the shutter: `fshclose()`
|
||||
1. close the shutter: `dev.omnyfsh.fshclose()`
|
||||
|
||||
#### X-ray eye alignment
|
||||
|
||||
@@ -102,13 +102,16 @@ The nano-positioning is controlled by a feedback loop running on a real-time lin
|
||||
|
||||
Once the loop has started, it is possible to start bec with the LamNI configuration file.
|
||||
|
||||
Loading the LamNI configuration (this command will load the LamNI configuration only - isolated from the beamline)
|
||||
`bec.config.update_session_with_file("/bec/csaxs_bec/csaxs_bec/device_configs/lamni_config.yaml")`
|
||||
Loading the LamNI scripts is done by starting bec as
|
||||
`bec --session lamni`
|
||||
|
||||
Loading the LamNI scripts
|
||||
The scripts can alternatively manually be loaded by
|
||||
`from csaxs_bec.bec_ipython_client.plugins.LamNI import LamNI`
|
||||
`lamni = LamNI(bec)`
|
||||
|
||||
Loading the LamNI configuration (this command will load the LamNI configuration only - isolated from the beamline)
|
||||
`bec.config.update_session_with_file("/bec/csaxs_bec/csaxs_bec/device_configs/lamni_config.yaml")`
|
||||
|
||||
If the realtime system is restarted, BEC will lose communication. To restart:
|
||||
`lamni.rt_off()` … then wait a 10 seconds
|
||||
`lamni.rt_on()`
|
||||
@@ -152,6 +155,12 @@ The underlying scan function can be called as
|
||||
|
||||
Use `scans.lamni_fermat_scan?`for detailed information. A prerequisite for scanning is a running feedback system.
|
||||
|
||||
### GUI tools
|
||||
|
||||
During operation the BEC GUI will show the relevant cameras or progress information. To manually switch view TAB completion on 'lamni.lamnigui_' will show all options to control the GUI. Most useful
|
||||
'lamni.lamnigui_show_progress()' will show the measurement progress GUI
|
||||
'lamnigui_show_xeyealign()' will show the XrayEye alignment GUI
|
||||
|
||||
### X-ray optics alignment
|
||||
|
||||
The positions of the optics stages are stored as stage parameters and are thus linked to the configuration file.
|
||||
|
||||
@@ -287,19 +287,20 @@ def test_ddg1_stage(mock_ddg1: DDG1):
|
||||
mock_ddg1.stage()
|
||||
|
||||
shutter_width = mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger
|
||||
total_exposure = 2 * mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger + 3e-6
|
||||
|
||||
assert np.isclose(mock_ddg1.burst_mode.get(), 1) # burst mode is enabled
|
||||
assert np.isclose(mock_ddg1.burst_delay.get(), 0)
|
||||
assert np.isclose(mock_ddg1.burst_period.get(), shutter_width)
|
||||
assert np.isclose(mock_ddg1.burst_period.get(), total_exposure)
|
||||
|
||||
# Trigger DDG2 through EXT/EN
|
||||
assert np.isclose(mock_ddg1.ab.delay.get(), 2e-3)
|
||||
assert np.isclose(mock_ddg1.ab.width.get(), 1e-6)
|
||||
assert np.isclose(mock_ddg1.ab.width.get(), shutter_width)
|
||||
# Shutter channel cd
|
||||
assert np.isclose(mock_ddg1.cd.delay.get(), 0)
|
||||
assert np.isclose(mock_ddg1.cd.width.get(), shutter_width)
|
||||
# MCS channel ef or gate
|
||||
assert np.isclose(mock_ddg1.ef.delay.get(), 0)
|
||||
assert np.isclose(mock_ddg1.ef.delay.get(), 1e-6)
|
||||
assert np.isclose(mock_ddg1.ef.width.get(), 1e-6)
|
||||
|
||||
assert mock_ddg1.staged == ophyd.Staged.yes
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_on_connected_sets_mask_and_live_mode(ids_camera):
|
||||
|
||||
def test_on_trigger_roi_signal(ids_camera):
|
||||
"""Test the on_trigger method to ensure it processes the ROI signal correctly."""
|
||||
ids_camera.live_mode = True
|
||||
ids_camera.start_live_mode()
|
||||
test_image = np.array([[2, 4], [6, 8]])
|
||||
test_mask = np.array([[1, 0], [0, 1]], dtype=np.uint8)
|
||||
ids_camera.mask = test_mask
|
||||
|
||||
@@ -217,6 +217,16 @@ def test_mcs_card_csaxs_complete_and_stop(mock_mcs_csaxs: MCSCardCSAXS):
|
||||
assert not mcs._start_monitor_async_data_emission.is_set()
|
||||
|
||||
|
||||
def test_mcs_on_stop(mock_mcs_csaxs: MCSCardCSAXS):
|
||||
"""Test that on stop sets the omit_mca_callbacks flag. Also test that on stage clears the omit_mca_callbacks flag."""
|
||||
mcs = mock_mcs_csaxs
|
||||
assert mcs._omit_mca_callbacks.is_set() is False
|
||||
mcs.stop()
|
||||
assert mcs._omit_mca_callbacks.is_set() is True
|
||||
mcs.stage()
|
||||
assert mcs._omit_mca_callbacks.is_set() is False
|
||||
|
||||
|
||||
def test_mcs_recovery(mock_mcs_csaxs: MCSCardCSAXS):
|
||||
mcs = mock_mcs_csaxs
|
||||
# Simulate ongoing acquisition
|
||||
|
||||
@@ -229,6 +229,22 @@ def device_manager_mock():
|
||||
"kwargs": {},
|
||||
},
|
||||
),
|
||||
messages.DeviceInstructionMessage(
|
||||
metadata={
|
||||
"readout_priority": "monitored",
|
||||
"RID": "1234",
|
||||
"device_instr_id": "diid",
|
||||
},
|
||||
device="lsamrot",
|
||||
action="rpc",
|
||||
parameter={
|
||||
"device": "lsamrot",
|
||||
"func": "readback.get",
|
||||
"rpc_id": "rpc_id",
|
||||
"args": (),
|
||||
"kwargs": {},
|
||||
},
|
||||
),
|
||||
messages.DeviceInstructionMessage(
|
||||
metadata={
|
||||
"readout_priority": "monitored",
|
||||
|
||||
Reference in New Issue
Block a user