Compare commits
65 Commits
fix/remove
...
fix/fixes_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
|||
| 0f072a786e | |||
| 05a1e3d8be | |||
| e9fd9084b8 | |||
| 40ef387134 | |||
|
|
6ed84664f2 | ||
|
|
e5e3343da7 | ||
|
|
c8866faccc | ||
|
|
3b561c251c | ||
|
|
bc187040ad | ||
|
|
efd27a27e8 | ||
|
|
7096ef3323 | ||
| 13378f24dd | |||
|
|
f5b898ea1c
|
||
|
3d62bea04b
|
|||
|
1518845d25
|
|||
|
ff3b6686db
|
@@ -2,7 +2,7 @@
|
|||||||
# It is needed to track the repo template version, and editing may break things.
|
# It is needed to track the repo template version, and editing may break things.
|
||||||
# This file will be overwritten by copier on template updates.
|
# This file will be overwritten by copier on template updates.
|
||||||
|
|
||||||
_commit: v1.2.2
|
_commit: v1.2.8
|
||||||
_src_path: https://github.com/bec-project/plugin_copier_template.git
|
_src_path: https://github.com/bec-project/plugin_copier_template.git
|
||||||
make_commit: false
|
make_commit: false
|
||||||
project_name: csaxs_bec
|
project_name: csaxs_bec
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ on:
|
|||||||
description: "Python version to use"
|
description: "Python version to use"
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: "3.11"
|
default: "3.12"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
@@ -44,7 +44,19 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "${{ inputs.PYTHON_VERSION || '3.11' }}"
|
python-version: "${{ inputs.PYTHON_VERSION || '3.12' }}"
|
||||||
|
|
||||||
|
- name: Checkout BEC Plugin Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: bec/csaxs_bec
|
||||||
|
ref: "${{ inputs.BEC_PLUGIN_REPO_BRANCH || github.head_ref || github.sha }}"
|
||||||
|
path: ./csaxs_bec
|
||||||
|
|
||||||
|
- name: Lint for merge conflicts from template updates
|
||||||
|
shell: bash
|
||||||
|
# Find all Copier conflicts except this line
|
||||||
|
run: '! grep -r "<<<<<<< before updating" | grep -v "grep -r \"<<<<<<< before updating"'
|
||||||
|
|
||||||
- name: Checkout BEC Core
|
- name: Checkout BEC Core
|
||||||
run: git clone --depth 1 --branch "${{ inputs.BEC_CORE_BRANCH || 'main' }}" https://github.com/bec-project/bec.git ./bec
|
run: git clone --depth 1 --branch "${{ inputs.BEC_CORE_BRANCH || 'main' }}" https://github.com/bec-project/bec.git ./bec
|
||||||
@@ -55,13 +67,6 @@ jobs:
|
|||||||
- name: Checkout BEC Widgets
|
- name: Checkout BEC Widgets
|
||||||
run: git clone --depth 1 --branch "${{ inputs.BEC_WIDGETS_BRANCH || 'main' }}" https://github.com/bec-project/bec_widgets.git ./bec_widgets
|
run: git clone --depth 1 --branch "${{ inputs.BEC_WIDGETS_BRANCH || 'main' }}" https://github.com/bec-project/bec_widgets.git ./bec_widgets
|
||||||
|
|
||||||
- name: Checkout BEC Plugin Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: bec/csaxs_bec
|
|
||||||
ref: "${{ inputs.BEC_PLUGIN_REPO_BRANCH || github.head_ref || github.sha }}"
|
|
||||||
path: ./csaxs_bec
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
62
.gitea/workflows/create_update_pr.yml
Normal file
62
.gitea/workflows/create_update_pr.yml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: Create template upgrade PR for csaxs_bec
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create_update_branch_and_pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
pip install copier PySide6
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Perform update
|
||||||
|
run: |
|
||||||
|
git config --global user.email "bec_ci_staging@psi.ch"
|
||||||
|
git config --global user.name "BEC automated CI"
|
||||||
|
|
||||||
|
branch="chore/update-template-$(python -m uuid)"
|
||||||
|
echo "switching to branch $branch"
|
||||||
|
git checkout -b $branch
|
||||||
|
|
||||||
|
echo "Running copier update..."
|
||||||
|
output="$(copier update --trust --defaults --conflict inline 2>&1)"
|
||||||
|
echo "$output"
|
||||||
|
msg="$(printf '%s\n' "$output" | head -n 1)"
|
||||||
|
|
||||||
|
if ! grep -q "make_commit: true" .copier-answers.yml ; then
|
||||||
|
echo "Autocommit not made, committing..."
|
||||||
|
git add -A
|
||||||
|
git commit -a -m "$msg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if diff-index --quiet HEAD ; then
|
||||||
|
echo "No changes detected"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push -u origin $branch
|
||||||
|
curl -X POST "https://gitea.psi.ch/api/v1/repos/${{ gitea.repository }}/pulls" \
|
||||||
|
-H "Authorization: token ${{ secrets.CI_REPO_WRITE }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"title\": \"Template: $(echo $msg)\",
|
||||||
|
\"body\": \"This PR was created by Gitea Actions\",
|
||||||
|
\"head\": \"$(echo $branch)\",
|
||||||
|
\"base\": \"main\"
|
||||||
|
}"
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
include:
|
|
||||||
- project: bec/awi_utils
|
|
||||||
file: /templates/plugin-repo-template.yml
|
|
||||||
inputs:
|
|
||||||
name: "csaxs"
|
|
||||||
target: "csaxs_bec"
|
|
||||||
branch: $CHILD_PIPELINE_BRANCH
|
|
||||||
|
|
||||||
pages:
|
|
||||||
stage: Deploy
|
|
||||||
needs: []
|
|
||||||
variables:
|
|
||||||
TARGET_BRANCH: $CI_COMMIT_REF_NAME
|
|
||||||
rules:
|
|
||||||
- if: "$CI_COMMIT_TAG != null"
|
|
||||||
variables:
|
|
||||||
TARGET_BRANCH: $CI_COMMIT_TAG
|
|
||||||
- if: '$CI_COMMIT_REF_NAME == "main" && $CI_PROJECT_PATH == "bec/csaxs_bec"'
|
|
||||||
script:
|
|
||||||
- curl -X POST -d "branches=$CI_COMMIT_REF_NAME" -d "token=$RTD_TOKEN" https://readthedocs.org/api/v2/webhook/sls-csaxs/270162/
|
|
||||||
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 datetime
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -12,7 +11,12 @@ from bec_lib.alarm_handler import AlarmBase
|
|||||||
from bec_lib.pdf_writer import PDFWriter
|
from bec_lib.pdf_writer import PDFWriter
|
||||||
from typeguard import typechecked
|
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 .alignment import XrayEyeAlign
|
||||||
from .lamni_optics_mixin import LaMNIInitStages, LamNIOpticsMixin
|
from .lamni_optics_mixin import LaMNIInitStages, LamNIOpticsMixin
|
||||||
@@ -23,29 +27,29 @@ if builtins.__dict__.get("bec") is not None:
|
|||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
umv = builtins.__dict__.get("umv")
|
||||||
|
mv = builtins.__dict__.get("mv")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
umvr = builtins.__dict__.get("umvr")
|
||||||
|
|
||||||
|
|
||||||
class LamNI(LamNIOpticsMixin):
|
class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||||
def __init__(self, client):
|
def __init__(self, client):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.client = client
|
self.client = client
|
||||||
self.device_manager = client.device_manager
|
self.device_manager = client.device_manager
|
||||||
self.align = XrayEyeAlign(client, self)
|
self.align = XrayEyeAlign(client, self)
|
||||||
self.init = LaMNIInitStages(client)
|
self.init = LaMNIInitStages(client)
|
||||||
self.check_shutter = True
|
|
||||||
self.check_light_available = True
|
# Extracted collaborators
|
||||||
self.check_fofb = True
|
self.reconstructor = PtychoReconstructor(self.ptycho_reconstruct_foldername)
|
||||||
self._check_msgs = []
|
self.tomo_id_manager = TomoIDManager()
|
||||||
|
self.OMNYTools = OMNYTools(self.client)
|
||||||
|
|
||||||
self.tomo_id = -1
|
self.tomo_id = -1
|
||||||
self.special_angles = []
|
self.special_angles = []
|
||||||
self.special_angle_repeats = 20
|
self.special_angle_repeats = 20
|
||||||
self.special_angle_tolerance = 20
|
self.special_angle_tolerance = 20
|
||||||
self._current_special_angles = []
|
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
|
# Progress tracking
|
||||||
self.progress = {}
|
self.progress = {}
|
||||||
self.progress["tomo_type"] = "Equally spaced sub-tomograms"
|
self.progress["tomo_type"] = "Equally spaced sub-tomograms"
|
||||||
@@ -56,105 +60,6 @@ class LamNI(LamNIOpticsMixin):
|
|||||||
self.progress["total_projections"] = 1
|
self.progress["total_projections"] = 1
|
||||||
self.progress["angle"] = 0
|
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
|
# Special angles
|
||||||
@@ -212,7 +117,6 @@ class LamNI(LamNIOpticsMixin):
|
|||||||
def feedback_status(self):
|
def feedback_status(self):
|
||||||
self.device_manager.devices.rtx.controller.show_feedback_status()
|
self.device_manager.devices.rtx.controller.show_feedback_status()
|
||||||
|
|
||||||
|
|
||||||
def show_interferometer_positions(self):
|
def show_interferometer_positions(self):
|
||||||
self.device_manager.devices.rtx.controller.show_feedback_status()
|
self.device_manager.devices.rtx.controller.show_feedback_status()
|
||||||
|
|
||||||
@@ -222,6 +126,12 @@ class LamNI(LamNIOpticsMixin):
|
|||||||
def show_analog_signals(self):
|
def show_analog_signals(self):
|
||||||
return self.device_manager.devices.rtx.controller.show_analog_signals()
|
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)
|
# Global parameters (backed by BEC global vars)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -371,6 +281,7 @@ class LamNI(LamNIOpticsMixin):
|
|||||||
@ptycho_reconstruct_foldername.setter
|
@ptycho_reconstruct_foldername.setter
|
||||||
def ptycho_reconstruct_foldername(self, val: str):
|
def ptycho_reconstruct_foldername(self, val: str):
|
||||||
self.client.set_global_var("ptycho_reconstruct_foldername", val)
|
self.client.set_global_var("ptycho_reconstruct_foldername", val)
|
||||||
|
self.reconstructor.folder_name = val # keep reconstructor in sync
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tomo_angle_stepsize(self):
|
def tomo_angle_stepsize(self):
|
||||||
@@ -445,18 +356,6 @@ class LamNI(LamNIOpticsMixin):
|
|||||||
# Logging helpers
|
# 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):
|
def write_to_scilog(self, content, tags: list = None):
|
||||||
try:
|
try:
|
||||||
if tags is not None:
|
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(
|
def add_sample_database(
|
||||||
self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user
|
self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user
|
||||||
):
|
):
|
||||||
"""Add a sample to the OMNY sample database and retrieve the tomo id."""
|
"""Add a sample to the OMNY sample database and retrieve the tomo id."""
|
||||||
subprocess.run(
|
return self.tomo_id_manager.register(
|
||||||
"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt"
|
sample_name=samplename,
|
||||||
f" 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}"
|
date=date,
|
||||||
f"&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}"
|
eaccount=eaccount,
|
||||||
f"&additional={sample_additional_info}&user={user}'",
|
scan_number=scan_number,
|
||||||
shell=True,
|
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
|
# 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_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):
|
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)
|
self._current_scan_list.append(bec.queue.next_scan_number)
|
||||||
log_message = (
|
log_message = (
|
||||||
f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle},"
|
f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle},"
|
||||||
f" scan number {bec.queue.next_scan_number}.\n"
|
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
|
corridor_size = self.corridor_size if self.corridor_size > 0 else None
|
||||||
scans.lamni_fermat_scan(
|
scans.lamni_fermat_scan(
|
||||||
fov_size=[self.lamni_piezo_range_x, self.lamni_piezo_range_y],
|
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"):
|
def tomo_reconstruct(self, base_path="~/Data10/specES1"):
|
||||||
"""Write the tomo reconstruct file for the reconstruction queue."""
|
"""Write the tomo reconstruct file for the reconstruction queue."""
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
base_path = os.path.expanduser(base_path)
|
self.reconstructor.write(
|
||||||
ptycho_queue_path = Path(os.path.join(base_path, self.ptycho_reconstruct_foldername))
|
scan_list=self._current_scan_list,
|
||||||
ptycho_queue_path.mkdir(parents=True, exist_ok=True)
|
next_scan_number=bec.queue.next_scan_number,
|
||||||
|
base_path=base_path,
|
||||||
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")
|
|
||||||
)
|
)
|
||||||
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:
|
def _at_each_angle(self, angle: float) -> None:
|
||||||
self.tomo_scan_projection(angle)
|
self.tomo_scan_projection(angle)
|
||||||
@@ -635,7 +524,6 @@ class LamNI(LamNIOpticsMixin):
|
|||||||
print(f"Starting LamNI scan for angle {angle} in subtomo {subtomo_number}")
|
print(f"Starting LamNI scan for angle {angle} in subtomo {subtomo_number}")
|
||||||
self._print_progress()
|
self._print_progress()
|
||||||
while not successful:
|
while not successful:
|
||||||
self._start_beam_check()
|
|
||||||
if not self.special_angles:
|
if not self.special_angles:
|
||||||
self._current_special_angles = []
|
self._current_special_angles = []
|
||||||
if 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):
|
for scan_nr in range(start_scan_number, end_scan_number):
|
||||||
self._write_tomo_scan_number(scan_nr, angle, subtomo_number)
|
self._write_tomo_scan_number(scan_nr, angle, subtomo_number)
|
||||||
|
|
||||||
if self._was_beam_okay() and not error_caught:
|
#todo here bl chk, if ok then successfull true
|
||||||
successful = True
|
successful = True
|
||||||
else:
|
|
||||||
self._wait_for_beamline_checks()
|
|
||||||
|
|
||||||
def _golden(self, ii, howmany_sorted, maxangle=360, reverse=False):
|
def _golden(self, ii, howmany_sorted, maxangle=360, reverse=False):
|
||||||
"""Return the ii-th golden ratio angle within sorted bunches and its subtomo number."""
|
"""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? ")
|
user_input = input("Are these parameters correctly set for your scan? ")
|
||||||
if user_input == "y":
|
if user_input == "y":
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.tomo_countingtime = self._get_val("<ctime> s", self.tomo_countingtime, float)
|
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)
|
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,13 +5,15 @@ from rich import box
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
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
|
from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import OMNYTools
|
||||||
|
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
|
scans = builtins.__dict__.get("scans")
|
||||||
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
class LamNIInitError(Exception):
|
class LamNIInitError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -49,7 +51,7 @@ class LaMNIInitStages:
|
|||||||
self.drive_axis_to_limit(dev.lsamrot, "forward")
|
self.drive_axis_to_limit(dev.lsamrot, "forward")
|
||||||
dev.lsamrot.enabled = False
|
dev.lsamrot.enabled = False
|
||||||
print("Now hard reboot the controller and run the initialization routine again.")
|
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
|
return
|
||||||
|
|
||||||
if self.OMNYTools.yesno(
|
if self.OMNYTools.yesno(
|
||||||
@@ -170,7 +172,7 @@ class LamNIOpticsMixin:
|
|||||||
|
|
||||||
def leye_out(self):
|
def leye_out(self):
|
||||||
self.loptics_in()
|
self.loptics_in()
|
||||||
fshclose()
|
dev.omnyfsh.fshopen()
|
||||||
leyey_out = self._get_user_param_safe("leyey", "out")
|
leyey_out = self._get_user_param_safe("leyey", "out")
|
||||||
umv(dev.leyey, leyey_out)
|
umv(dev.leyey, leyey_out)
|
||||||
|
|
||||||
|
|||||||
442
csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXSDLPCA200.py
Normal file
442
csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXSDLPCA200.py
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
"""
|
||||||
|
csaxs_dlpca200.py
|
||||||
|
=================
|
||||||
|
BEC control script for FEMTO DLPCA-200 Variable Gain Low Noise Current Amplifiers
|
||||||
|
connected to Galil RIO digital outputs.
|
||||||
|
|
||||||
|
DLPCA-200 Remote Control (datasheet page 4)
|
||||||
|
-------------------------------------------
|
||||||
|
Sub-D pin -> function:
|
||||||
|
Pin 10 -> gain LSB (digital out channel, index 0 in bit-tuple)
|
||||||
|
Pin 11 -> gain MID (digital out channel, index 1 in bit-tuple)
|
||||||
|
Pin 12 -> gain MSB (digital out channel, index 2 in bit-tuple)
|
||||||
|
Pin 13 -> coupling LOW = AC, HIGH = DC
|
||||||
|
Pin 14 -> speed mode HIGH = low noise (Pin14=1), LOW = high speed (Pin14=0)
|
||||||
|
|
||||||
|
Gain truth table (MSB, MID, LSB):
|
||||||
|
0,0,0 -> low-noise: 1e3 high-speed: 1e5
|
||||||
|
0,0,1 -> low-noise: 1e4 high-speed: 1e6
|
||||||
|
0,1,0 -> low-noise: 1e5 high-speed: 1e7
|
||||||
|
0,1,1 -> low-noise: 1e6 high-speed: 1e8
|
||||||
|
1,0,0 -> low-noise: 1e7 high-speed: 1e9
|
||||||
|
1,0,1 -> low-noise: 1e8 high-speed: 1e10
|
||||||
|
1,1,0 -> low-noise: 1e9 high-speed: 1e11
|
||||||
|
|
||||||
|
Strategy: prefer low-noise mode (1e3-1e9). For 1e10 and 1e11,
|
||||||
|
automatically fall back to high-speed mode.
|
||||||
|
|
||||||
|
Device wiring example (galilrioesxbox):
|
||||||
|
bpm4: Pin10->ch0, Pin11->ch1, Pin12->ch2, Pin13->ch3, Pin14->ch4
|
||||||
|
bim: Pin10->ch6, Pin11->ch7, Pin12->ch8, Pin13->ch9, Pin14->ch10
|
||||||
|
|
||||||
|
Usage examples
|
||||||
|
--------------
|
||||||
|
csaxs_amp = cSAXSDLPCA200(client)
|
||||||
|
|
||||||
|
csaxs_amp.set_gain("bpm4", 1e7) # low-noise if possible
|
||||||
|
csaxs_amp.set_gain("bim", 1e10) # auto high-speed
|
||||||
|
csaxs_amp.set_coupling("bpm4", "DC")
|
||||||
|
csaxs_amp.set_coupling("bim", "AC")
|
||||||
|
csaxs_amp.info("bpm4") # print current settings
|
||||||
|
csaxs_amp.info_all() # print all configured amplifiers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
from bec_lib import bec_logger
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
bec = builtins.__dict__.get("bec")
|
||||||
|
dev = builtins.__dict__.get("dev")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Amplifier registry
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Each entry describes one DLPCA-200 amplifier connected to a Galil RIO.
|
||||||
|
#
|
||||||
|
# Keys inside "channels":
|
||||||
|
# gain_lsb -> digital output channel number wired to DLPCA-200 Pin 10
|
||||||
|
# gain_mid -> digital output channel number wired to DLPCA-200 Pin 11
|
||||||
|
# gain_msb -> digital output channel number wired to DLPCA-200 Pin 12
|
||||||
|
# coupling -> digital output channel number wired to DLPCA-200 Pin 13
|
||||||
|
# speed_mode -> digital output channel number wired to DLPCA-200 Pin 14
|
||||||
|
#
|
||||||
|
# To add a new amplifier, simply extend this dict.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
DLPCA200_AMPLIFIER_CONFIG: dict[str, dict] = {
|
||||||
|
"bpm4": {
|
||||||
|
"rio_device": "galilrioesxbox",
|
||||||
|
"description": "Beam Position Monitor 4 current amplifier",
|
||||||
|
"channels": {
|
||||||
|
"gain_lsb": 0, # Pin 10 -> Galil ch0
|
||||||
|
"gain_mid": 1, # Pin 11 -> Galil ch1
|
||||||
|
"gain_msb": 2, # Pin 12 -> Galil ch2
|
||||||
|
"coupling": 3, # Pin 13 -> Galil ch3
|
||||||
|
"speed_mode": 4, # Pin 14 -> Galil ch4
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"bim": {
|
||||||
|
"rio_device": "galilrioesxbox",
|
||||||
|
"description": "Beam Intensity Monitor current amplifier",
|
||||||
|
"channels": {
|
||||||
|
"gain_lsb": 6, # Pin 10 -> Galil ch6
|
||||||
|
"gain_mid": 7, # Pin 11 -> Galil ch7
|
||||||
|
"gain_msb": 8, # Pin 12 -> Galil ch8
|
||||||
|
"coupling": 9, # Pin 13 -> Galil ch9
|
||||||
|
"speed_mode": 10, # Pin 14 -> Galil ch10
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DLPCA-200 gain encoding tables
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# (msb, mid, lsb) -> gain in V/A
|
||||||
|
_GAIN_BITS_LOW_NOISE: dict[tuple, int] = {
|
||||||
|
(0, 0, 0): int(1e3),
|
||||||
|
(0, 0, 1): int(1e4),
|
||||||
|
(0, 1, 0): int(1e5),
|
||||||
|
(0, 1, 1): int(1e6),
|
||||||
|
(1, 0, 0): int(1e7),
|
||||||
|
(1, 0, 1): int(1e8),
|
||||||
|
(1, 1, 0): int(1e9),
|
||||||
|
}
|
||||||
|
|
||||||
|
_GAIN_BITS_HIGH_SPEED: dict[tuple, int] = {
|
||||||
|
(0, 0, 0): int(1e5),
|
||||||
|
(0, 0, 1): int(1e6),
|
||||||
|
(0, 1, 0): int(1e7),
|
||||||
|
(0, 1, 1): int(1e8),
|
||||||
|
(1, 0, 0): int(1e9),
|
||||||
|
(1, 0, 1): int(1e10),
|
||||||
|
(1, 1, 0): int(1e11),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inverse maps: gain -> (msb, mid, lsb, low_noise_flag)
|
||||||
|
# low_noise_flag: True = Pin14 HIGH, False = Pin14 LOW
|
||||||
|
_GAIN_TO_BITS: dict[int, tuple] = {}
|
||||||
|
for _bits, _gain in _GAIN_BITS_LOW_NOISE.items():
|
||||||
|
_GAIN_TO_BITS[_gain] = (*_bits, True)
|
||||||
|
for _bits, _gain in _GAIN_BITS_HIGH_SPEED.items():
|
||||||
|
if _gain not in _GAIN_TO_BITS: # low-noise takes priority
|
||||||
|
_GAIN_TO_BITS[_gain] = (*_bits, False)
|
||||||
|
|
||||||
|
VALID_GAINS = sorted(_GAIN_TO_BITS.keys())
|
||||||
|
|
||||||
|
|
||||||
|
class cSAXSDLPCA200Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class cSAXSDLPCA200:
|
||||||
|
"""
|
||||||
|
Control class for FEMTO DLPCA-200 current amplifiers connected via Galil RIO
|
||||||
|
digital outputs in a BEC environment.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Forward control: set_gain(), set_coupling()
|
||||||
|
- Readback reporting: info(), info_all(), read_settings()
|
||||||
|
- Robust error handling and logging following cSAXS conventions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TAG = "[DLPCA200]"
|
||||||
|
|
||||||
|
def __init__(self, client, config: dict | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
client : BEC client object (passed through for future use)
|
||||||
|
config : optional override for DLPCA200_AMPLIFIER_CONFIG.
|
||||||
|
Falls back to the module-level dict if not provided.
|
||||||
|
"""
|
||||||
|
self.client = client
|
||||||
|
self._config: dict[str, dict] = config if config is not None else DLPCA200_AMPLIFIER_CONFIG
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Internal helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _require_dev(self) -> None:
|
||||||
|
if dev is None:
|
||||||
|
raise cSAXSDLPCA200Error(
|
||||||
|
f"{self.TAG} BEC 'dev' namespace is not available in this session."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_cfg(self, amp_name: str) -> dict:
|
||||||
|
"""Return config dict for a named amplifier, raising on unknown names."""
|
||||||
|
if amp_name not in self._config:
|
||||||
|
known = ", ".join(sorted(self._config.keys()))
|
||||||
|
raise cSAXSDLPCA200Error(f"{self.TAG} Unknown amplifier '{amp_name}'. Known: [{known}]")
|
||||||
|
return self._config[amp_name]
|
||||||
|
|
||||||
|
def _get_rio(self, amp_name: str):
|
||||||
|
"""Return the live RIO device object for a given amplifier."""
|
||||||
|
self._require_dev()
|
||||||
|
cfg = self._get_cfg(amp_name)
|
||||||
|
rio_name = cfg["rio_device"]
|
||||||
|
try:
|
||||||
|
rio = getattr(dev, rio_name)
|
||||||
|
except AttributeError:
|
||||||
|
raise cSAXSDLPCA200Error(f"{self.TAG} RIO device '{rio_name}' not found in BEC 'dev'.")
|
||||||
|
return rio
|
||||||
|
|
||||||
|
def _dout_get(self, rio, ch: int) -> int:
|
||||||
|
"""Read one digital output channel (returns 0 or 1)."""
|
||||||
|
attr = getattr(rio.digital_out, f"ch{ch}")
|
||||||
|
val = attr.get()
|
||||||
|
return int(val)
|
||||||
|
|
||||||
|
def _dout_set(self, rio, ch: int, value: bool) -> None:
|
||||||
|
"""Write one digital output channel (True=HIGH=1, False=LOW=0)."""
|
||||||
|
attr = getattr(rio.digital_out, f"ch{ch}")
|
||||||
|
attr.set(value)
|
||||||
|
|
||||||
|
def _read_gain_bits(self, amp_name: str) -> tuple[int, int, int, int]:
|
||||||
|
"""
|
||||||
|
Read current gain bit-state from hardware.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
(msb, mid, lsb, speed_mode)
|
||||||
|
speed_mode: 1 = low-noise (Pin14=HIGH), 0 = high-speed (Pin14=LOW)
|
||||||
|
"""
|
||||||
|
rio = self._get_rio(amp_name)
|
||||||
|
ch = self._get_cfg(amp_name)["channels"]
|
||||||
|
msb = self._dout_get(rio, ch["gain_msb"])
|
||||||
|
mid = self._dout_get(rio, ch["gain_mid"])
|
||||||
|
lsb = self._dout_get(rio, ch["gain_lsb"])
|
||||||
|
speed_mode = self._dout_get(rio, ch["speed_mode"])
|
||||||
|
return msb, mid, lsb, speed_mode
|
||||||
|
|
||||||
|
def _decode_gain(self, msb: int, mid: int, lsb: int, speed_mode: int) -> int | None:
|
||||||
|
"""
|
||||||
|
Decode hardware bit-state into gain value (V/A).
|
||||||
|
|
||||||
|
speed_mode=1 -> low-noise table, speed_mode=0 -> high-speed table.
|
||||||
|
Returns None if the bit combination is not in the table.
|
||||||
|
"""
|
||||||
|
bits = (msb, mid, lsb)
|
||||||
|
if speed_mode:
|
||||||
|
return _GAIN_BITS_LOW_NOISE.get(bits)
|
||||||
|
else:
|
||||||
|
return _GAIN_BITS_HIGH_SPEED.get(bits)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public API - control
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def set_gain(self, amp_name: str, gain: float, force_high_speed: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Set the transimpedance gain of a DLPCA-200 amplifier.
|
||||||
|
|
||||||
|
The method automatically selects low-noise mode (Pin14=HIGH) whenever
|
||||||
|
the requested gain is achievable in low-noise mode (1e3 - 1e9 V/A).
|
||||||
|
For gains of 1e10 and 1e11 V/A, high-speed mode is used automatically.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
amp_name : str
|
||||||
|
Amplifier name as defined in DLPCA200_AMPLIFIER_CONFIG (e.g. "bpm4").
|
||||||
|
gain : float or int
|
||||||
|
Target gain in V/A. Must be one of:
|
||||||
|
1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11.
|
||||||
|
force_high_speed : bool, optional
|
||||||
|
If True, force high-speed (low-noise=False) mode even for gains
|
||||||
|
below 1e10. Default: False (prefer low-noise).
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
csaxs_amp.set_gain("bpm4", 1e7) # low-noise mode (automatic)
|
||||||
|
csaxs_amp.set_gain("bim", 1e10) # high-speed mode (automatic)
|
||||||
|
csaxs_amp.set_gain("bpm4", 1e7, force_high_speed=True) # override to high-speed
|
||||||
|
"""
|
||||||
|
gain_int = int(gain)
|
||||||
|
if gain_int not in _GAIN_TO_BITS:
|
||||||
|
valid_str = ", ".join(
|
||||||
|
f"1e{int(round(__import__('math').log10(g)))}" for g in VALID_GAINS
|
||||||
|
)
|
||||||
|
raise cSAXSDLPCA200Error(
|
||||||
|
f"{self.TAG} Invalid gain {gain:.2e} V/A for '{amp_name}'. "
|
||||||
|
f"Valid values: {valid_str}"
|
||||||
|
)
|
||||||
|
|
||||||
|
msb, mid, lsb, low_noise_preferred = _GAIN_TO_BITS[gain_int]
|
||||||
|
|
||||||
|
# Apply force_high_speed override
|
||||||
|
if force_high_speed and low_noise_preferred:
|
||||||
|
# Check if this gain is achievable in high-speed mode
|
||||||
|
hs_entry = next(
|
||||||
|
(bits for bits, g in _GAIN_BITS_HIGH_SPEED.items() if g == gain_int), None
|
||||||
|
)
|
||||||
|
if hs_entry is None:
|
||||||
|
raise cSAXSDLPCA200Error(
|
||||||
|
f"{self.TAG} Gain {gain:.2e} V/A is not achievable in high-speed mode "
|
||||||
|
f"for '{amp_name}'."
|
||||||
|
)
|
||||||
|
msb, mid, lsb = hs_entry
|
||||||
|
low_noise_preferred = False
|
||||||
|
|
||||||
|
use_low_noise = low_noise_preferred and not force_high_speed
|
||||||
|
|
||||||
|
try:
|
||||||
|
rio = self._get_rio(amp_name)
|
||||||
|
ch = self._get_cfg(amp_name)["channels"]
|
||||||
|
|
||||||
|
self._dout_set(rio, ch["gain_msb"], bool(msb))
|
||||||
|
self._dout_set(rio, ch["gain_mid"], bool(mid))
|
||||||
|
self._dout_set(rio, ch["gain_lsb"], bool(lsb))
|
||||||
|
self._dout_set(rio, ch["speed_mode"], use_low_noise) # True=low-noise
|
||||||
|
|
||||||
|
mode_str = "low-noise" if use_low_noise else "high-speed"
|
||||||
|
logger.info(
|
||||||
|
f"{self.TAG} [{amp_name}] gain set to {gain_int:.2e} V/A "
|
||||||
|
f"({mode_str} mode, bits MSB={msb} MID={mid} LSB={lsb})"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"{amp_name}: gain -> {gain_int:.2e} V/A [{mode_str}] "
|
||||||
|
f"(bits: MSB={msb} MID={mid} LSB={lsb})"
|
||||||
|
)
|
||||||
|
|
||||||
|
except cSAXSDLPCA200Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise cSAXSDLPCA200Error(
|
||||||
|
f"{self.TAG} Failed to set gain on '{amp_name}': {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
def set_coupling(self, amp_name: str, coupling: str) -> None:
|
||||||
|
"""
|
||||||
|
Set AC or DC coupling on a DLPCA-200 amplifier.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
amp_name : str
|
||||||
|
Amplifier name (e.g. "bpm4", "bim").
|
||||||
|
coupling : str
|
||||||
|
"AC" or "DC" (case-insensitive).
|
||||||
|
DC -> Pin13 HIGH, AC -> Pin13 LOW.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
csaxs_amp.set_coupling("bpm4", "DC")
|
||||||
|
csaxs_amp.set_coupling("bim", "AC")
|
||||||
|
"""
|
||||||
|
coupling_upper = coupling.strip().upper()
|
||||||
|
if coupling_upper not in ("AC", "DC"):
|
||||||
|
raise cSAXSDLPCA200Error(
|
||||||
|
f"{self.TAG} Invalid coupling '{coupling}' for '{amp_name}'. " f"Use 'AC' or 'DC'."
|
||||||
|
)
|
||||||
|
|
||||||
|
pin13_high = coupling_upper == "DC"
|
||||||
|
|
||||||
|
try:
|
||||||
|
rio = self._get_rio(amp_name)
|
||||||
|
ch = self._get_cfg(amp_name)["channels"]
|
||||||
|
self._dout_set(rio, ch["coupling"], pin13_high)
|
||||||
|
|
||||||
|
logger.info(f"{self.TAG} [{amp_name}] coupling set to {coupling_upper}")
|
||||||
|
print(f"{amp_name}: coupling -> {coupling_upper}")
|
||||||
|
|
||||||
|
except cSAXSDLPCA200Error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise cSAXSDLPCA200Error(
|
||||||
|
f"{self.TAG} Failed to set coupling on '{amp_name}': {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public API - readback / reporting
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def read_settings(self, amp_name: str) -> dict:
|
||||||
|
"""
|
||||||
|
Read back the current settings from hardware digital outputs.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict with keys:
|
||||||
|
"amp_name" : str
|
||||||
|
"gain" : int or None - gain in V/A (None if unknown bit pattern)
|
||||||
|
"mode" : str - "low-noise" or "high-speed"
|
||||||
|
"coupling" : str - "AC" or "DC"
|
||||||
|
"bits" : dict - raw bit values {msb, mid, lsb, speed_mode, coupling}
|
||||||
|
"""
|
||||||
|
rio = self._get_rio(amp_name)
|
||||||
|
ch = self._get_cfg(amp_name)["channels"]
|
||||||
|
|
||||||
|
msb = self._dout_get(rio, ch["gain_msb"])
|
||||||
|
mid = self._dout_get(rio, ch["gain_mid"])
|
||||||
|
lsb = self._dout_get(rio, ch["gain_lsb"])
|
||||||
|
speed_mode = self._dout_get(rio, ch["speed_mode"])
|
||||||
|
coupling_bit = self._dout_get(rio, ch["coupling"])
|
||||||
|
|
||||||
|
gain = self._decode_gain(msb, mid, lsb, speed_mode)
|
||||||
|
mode = "low-noise" if speed_mode else "high-speed"
|
||||||
|
coupling = "DC" if coupling_bit else "AC"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"amp_name": amp_name,
|
||||||
|
"gain": gain,
|
||||||
|
"mode": mode,
|
||||||
|
"coupling": coupling,
|
||||||
|
"bits": {
|
||||||
|
"msb": msb,
|
||||||
|
"mid": mid,
|
||||||
|
"lsb": lsb,
|
||||||
|
"speed_mode": speed_mode,
|
||||||
|
"coupling": coupling_bit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def info(self, amp_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Print a plain summary of the current settings for one amplifier.
|
||||||
|
|
||||||
|
Example output
|
||||||
|
--------------
|
||||||
|
Amplifier : bpm4
|
||||||
|
Description : Beam Position Monitor 4 current amplifier
|
||||||
|
RIO device : galilrioesxbox
|
||||||
|
Gain : 1.00e+07 V/A
|
||||||
|
Mode : low-noise
|
||||||
|
Coupling : DC
|
||||||
|
Raw bits : MSB=1 MID=0 LSB=0 speed=1 coup=1
|
||||||
|
"""
|
||||||
|
cfg = self._get_cfg(amp_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
s = self.read_settings(amp_name)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"{self.TAG} [{amp_name}] Could not read settings: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
gain_str = (
|
||||||
|
f"{s['gain']:.2e} V/A" if s["gain"] is not None else "UNKNOWN (invalid bit pattern)"
|
||||||
|
)
|
||||||
|
bits = s["bits"]
|
||||||
|
|
||||||
|
print(f" {'Amplifier':<12}: {amp_name}")
|
||||||
|
print(f" {'Description':<12}: {cfg.get('description', '')}")
|
||||||
|
print(f" {'RIO device':<12}: {cfg['rio_device']}")
|
||||||
|
print(f" {'Gain':<12}: {gain_str}")
|
||||||
|
print(f" {'Mode':<12}: {s['mode']}")
|
||||||
|
print(f" {'Coupling':<12}: {s['coupling']}")
|
||||||
|
print(
|
||||||
|
f" {'Raw bits':<12}: MSB={bits['msb']} MID={bits['mid']} LSB={bits['lsb']} speed={bits['speed_mode']} coup={bits['coupling']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def info_all(self) -> None:
|
||||||
|
"""
|
||||||
|
Print a plain summary for ALL configured amplifiers.
|
||||||
|
"""
|
||||||
|
print("\nDLPCA-200 Amplifier Status Report")
|
||||||
|
print("-" * 40)
|
||||||
|
for amp_name in sorted(self._config.keys()):
|
||||||
|
self.info(amp_name)
|
||||||
|
print()
|
||||||
|
|
||||||
|
def list_amplifiers(self) -> list[str]:
|
||||||
|
"""Return sorted list of configured amplifier names."""
|
||||||
|
return sorted(self._config.keys())
|
||||||
@@ -41,8 +41,10 @@ import builtins
|
|||||||
if builtins.__dict__.get("bec") is not None:
|
if builtins.__dict__.get("bec") is not None:
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
class cSAXSFilterTransmission:
|
class cSAXSFilterTransmission:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ from bec_lib import bec_logger
|
|||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
# Pull BEC globals if present
|
# Pull BEC globals if present
|
||||||
bec = builtins.__dict__.get("bec")
|
if builtins.__dict__.get("bec") is not None:
|
||||||
dev = builtins.__dict__.get("dev")
|
bec = builtins.__dict__.get("bec")
|
||||||
umv = builtins.__dict__.get("umv")
|
dev = builtins.__dict__.get("dev")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
scans = builtins.__dict__.get("scans")
|
||||||
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
|
|
||||||
class cSAXSInitSmaractStagesError(Exception):
|
class cSAXSInitSmaractStagesError(Exception):
|
||||||
@@ -34,6 +37,92 @@ class cSAXSInitSmaractStages:
|
|||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Internal helpers (runtime-based)
|
# 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:
|
def _yesno(self, question: str, default: str = "y") -> bool:
|
||||||
"""
|
"""
|
||||||
Use OMNYTools.yesno if available; otherwise default to 'yes' (or fallback to input()).
|
Use OMNYTools.yesno if available; otherwise default to 'yes' (or fallback to input()).
|
||||||
@@ -104,6 +193,7 @@ class cSAXSInitSmaractStages:
|
|||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Public API
|
# Public API
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
|
|
||||||
def smaract_reference_stages(self, force: bool = False, devices_to_reference=None):
|
def smaract_reference_stages(self, force: bool = False, devices_to_reference=None):
|
||||||
"""
|
"""
|
||||||
Reference SmarAct stages using runtime discovery.
|
Reference SmarAct stages using runtime discovery.
|
||||||
@@ -164,6 +254,19 @@ class cSAXSInitSmaractStages:
|
|||||||
devices_to_reference = [devices_to_reference]
|
devices_to_reference = [devices_to_reference]
|
||||||
selection = set(devices_to_reference) if devices_to_reference else None
|
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)
|
# Build axis map for selected devices (or all devices present)
|
||||||
axis_map = self._build_session_axis_map(selection=selection)
|
axis_map = self._build_session_axis_map(selection=selection)
|
||||||
if selection:
|
if selection:
|
||||||
@@ -171,7 +274,6 @@ class cSAXSInitSmaractStages:
|
|||||||
if unknown:
|
if unknown:
|
||||||
print(f"Unknown devices requested or missing 'bl_smar_stage' (ignored): {unknown}")
|
print(f"Unknown devices requested or missing 'bl_smar_stage' (ignored): {unknown}")
|
||||||
|
|
||||||
|
|
||||||
newly_referenced = []
|
newly_referenced = []
|
||||||
already_referenced = []
|
already_referenced = []
|
||||||
failed = []
|
failed = []
|
||||||
@@ -188,6 +290,17 @@ class cSAXSInitSmaractStages:
|
|||||||
failed.append(dev_name)
|
failed.append(dev_name)
|
||||||
continue
|
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:
|
try:
|
||||||
is_ref = d.controller.axis_is_referenced(ch)
|
is_ref = d.controller.axis_is_referenced(ch)
|
||||||
|
|
||||||
@@ -243,7 +356,17 @@ class cSAXSInitSmaractStages:
|
|||||||
def smaract_check_all_referenced(self):
|
def smaract_check_all_referenced(self):
|
||||||
"""
|
"""
|
||||||
Check reference state for all SmarAct devices that define 'bl_smar_stage'.
|
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)
|
axis_map = self._build_session_axis_map(selection=None)
|
||||||
for dev_name in sorted(axis_map.keys()):
|
for dev_name in sorted(axis_map.keys()):
|
||||||
ch = axis_map[dev_name]
|
ch = axis_map[dev_name]
|
||||||
@@ -251,6 +374,16 @@ class cSAXSInitSmaractStages:
|
|||||||
if d is None:
|
if d is None:
|
||||||
print(f"{dev_name}: device not accessible or unsupported.")
|
print(f"{dev_name}: device not accessible or unsupported.")
|
||||||
continue
|
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:
|
try:
|
||||||
if d.controller.axis_is_referenced(ch):
|
if d.controller.axis_is_referenced(ch):
|
||||||
print(f"{dev_name} (axis {ch}) is referenced.")
|
print(f"{dev_name} (axis {ch}) is referenced.")
|
||||||
@@ -259,6 +392,7 @@ class cSAXSInitSmaractStages:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error checking {dev_name} (axis {ch}): {e}")
|
print(f"Error checking {dev_name} (axis {ch}): {e}")
|
||||||
|
|
||||||
|
|
||||||
def smaract_components_to_initial_position(self, devices_to_move=None):
|
def smaract_components_to_initial_position(self, devices_to_move=None):
|
||||||
"""
|
"""
|
||||||
Move selected (or all) SmarAct-based components to their configured init_position.
|
Move selected (or all) SmarAct-based components to their configured init_position.
|
||||||
@@ -383,7 +517,6 @@ class cSAXSInitSmaractStages:
|
|||||||
if not self._yesno("Proceed with the motions listed above?", "y"):
|
if not self._yesno("Proceed with the motions listed above?", "y"):
|
||||||
logger.info("[cSAXS] Motion to initial position aborted by user.")
|
logger.info("[cSAXS] Motion to initial position aborted by user.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- Execution phase (SIMULTANEOUS MOTION) ---
|
# --- Execution phase (SIMULTANEOUS MOTION) ---
|
||||||
if umv is None:
|
if umv is None:
|
||||||
logger.error("[cSAXS] 'umv' is not available in this session.")
|
logger.error("[cSAXS] 'umv' is not available in this session.")
|
||||||
|
|||||||
Binary file not shown.
@@ -15,15 +15,21 @@ from csaxs_bec.bec_ipython_client.plugins.cSAXS import cSAXSBeamlineChecks
|
|||||||
from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_optics_mixin import FlomniOpticsMixin
|
from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_optics_mixin import FlomniOpticsMixin
|
||||||
from csaxs_bec.bec_ipython_client.plugins.flomni.x_ray_eye_align import XrayEyeAlign
|
from csaxs_bec.bec_ipython_client.plugins.flomni.x_ray_eye_align import XrayEyeAlign
|
||||||
from csaxs_bec.bec_ipython_client.plugins.flomni.gui_tools import flomniGuiTools
|
from csaxs_bec.bec_ipython_client.plugins.flomni.gui_tools import flomniGuiTools
|
||||||
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,
|
||||||
|
)
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
if builtins.__dict__.get("bec") is not None:
|
if builtins.__dict__.get("bec") is not None:
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
|
|
||||||
class FlomniToolsError(Exception):
|
class FlomniToolsError(Exception):
|
||||||
@@ -63,9 +69,6 @@ class FlomniError(Exception):
|
|||||||
# print("Please expicitely confirm y or n.")
|
# print("Please expicitely confirm y or n.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FlomniInitStagesMixin:
|
class FlomniInitStagesMixin:
|
||||||
|
|
||||||
def flomni_init_stages(self):
|
def flomni_init_stages(self):
|
||||||
@@ -81,6 +84,15 @@ class FlomniInitStagesMixin:
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
sensor_voltage_target = dev.ftransy.user_parameter.get("sensor_voltage")
|
||||||
|
sensor_voltage = float(dev.ftransy.controller.socket_put_and_receive("MG@AN[1]").strip())
|
||||||
|
|
||||||
|
if not np.isclose(sensor_voltage, sensor_voltage_target, 0.25):
|
||||||
|
print(f"Sensor voltage of the gripper is {sensor_voltage}, while target from config is {sensor_voltage_target}")
|
||||||
|
print("Verify that the value is acceptable and update config file. Reload config and start again.")
|
||||||
|
return
|
||||||
|
|
||||||
print("Starting to drive ftransy to +y limit")
|
print("Starting to drive ftransy to +y limit")
|
||||||
self.drive_axis_to_limit(dev.ftransy, "forward")
|
self.drive_axis_to_limit(dev.ftransy, "forward")
|
||||||
dev.ftransy.limits = [-100, 0]
|
dev.ftransy.limits = [-100, 0]
|
||||||
@@ -108,7 +120,7 @@ class FlomniInitStagesMixin:
|
|||||||
|
|
||||||
|
|
||||||
if self.OMNYTools.yesno("Init of foptz. Can the stage move to the upstream limit without collision?"):
|
if self.OMNYTools.yesno("Init of foptz. Can the stage move to the upstream limit without collision?"):
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -162,7 +174,7 @@ class FlomniInitStagesMixin:
|
|||||||
print("done")
|
print("done")
|
||||||
|
|
||||||
if self.OMNYTools.yesno("Init of tracking stages. Did you remove the outer laser flight tubes?"):
|
if self.OMNYTools.yesno("Init of tracking stages. Did you remove the outer laser flight tubes?"):
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
else:
|
else:
|
||||||
print("Stopping.")
|
print("Stopping.")
|
||||||
return
|
return
|
||||||
@@ -178,7 +190,7 @@ class FlomniInitStagesMixin:
|
|||||||
print("done")
|
print("done")
|
||||||
|
|
||||||
if self.OMNYTools.yesno("Init of sample stage. Is the piezo at about 0 deg?"):
|
if self.OMNYTools.yesno("Init of sample stage. Is the piezo at about 0 deg?"):
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
else:
|
else:
|
||||||
print("Stopping.")
|
print("Stopping.")
|
||||||
return
|
return
|
||||||
@@ -195,7 +207,7 @@ class FlomniInitStagesMixin:
|
|||||||
|
|
||||||
print("Initializing UPR stage.")
|
print("Initializing UPR stage.")
|
||||||
if self.OMNYTools.yesno("To ensure that the end switches work, please check that they are currently not pushed. Is everything okay?"):
|
if self.OMNYTools.yesno("To ensure that the end switches work, please check that they are currently not pushed. Is everything okay?"):
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
else:
|
else:
|
||||||
print("Stopping.")
|
print("Stopping.")
|
||||||
return
|
return
|
||||||
@@ -216,7 +228,7 @@ class FlomniInitStagesMixin:
|
|||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
if self.OMNYTools.yesno("Shall I start the index search?"):
|
if self.OMNYTools.yesno("Shall I start the index search?"):
|
||||||
print("good then. Starting index search.")
|
print("OK. continue.. Starting index search.")
|
||||||
else:
|
else:
|
||||||
print("Stopping.")
|
print("Stopping.")
|
||||||
return
|
return
|
||||||
@@ -235,7 +247,7 @@ class FlomniInitStagesMixin:
|
|||||||
print("done")
|
print("done")
|
||||||
|
|
||||||
if self.OMNYTools.yesno("Init of foptx. Can the stage move to the positive limit without collision? Attention: tracker flight tube!"):
|
if self.OMNYTools.yesno("Init of foptx. Can the stage move to the positive limit without collision? Attention: tracker flight tube!"):
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
else:
|
else:
|
||||||
print("Stopping.")
|
print("Stopping.")
|
||||||
return
|
return
|
||||||
@@ -259,7 +271,7 @@ class FlomniInitStagesMixin:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if self.OMNYTools.yesno("Start limit switch search of fopty?"):
|
if self.OMNYTools.yesno("Start limit switch search of fopty?"):
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
else:
|
else:
|
||||||
print("Stopping.")
|
print("Stopping.")
|
||||||
return
|
return
|
||||||
@@ -411,6 +423,7 @@ class FlomniSampleTransferMixin:
|
|||||||
raise FlomniError("Ftray is not at the 'IN' position. Aborting.")
|
raise FlomniError("Ftray is not at the 'IN' position. Aborting.")
|
||||||
|
|
||||||
def ftransfer_flomni_stage_in(self):
|
def ftransfer_flomni_stage_in(self):
|
||||||
|
time.sleep(1)
|
||||||
sample_in_position = dev.flomni_samples.is_sample_slot_used(0)
|
sample_in_position = dev.flomni_samples.is_sample_slot_used(0)
|
||||||
#bool(float(dev.flomni_samples.sample_placed.sample0.get()))
|
#bool(float(dev.flomni_samples.sample_placed.sample0.get()))
|
||||||
if not sample_in_position:
|
if not sample_in_position:
|
||||||
@@ -425,7 +438,19 @@ class FlomniSampleTransferMixin:
|
|||||||
umv(dev.fsamx, fsamx_in)
|
umv(dev.fsamx, fsamx_in)
|
||||||
dev.fsamx.limits = [fsamx_in - 0.4, fsamx_in + 0.4]
|
dev.fsamx.limits = [fsamx_in - 0.4, fsamx_in + 0.4]
|
||||||
|
|
||||||
self.flomnigui_idle()
|
print("Moving X-ray eye in.")
|
||||||
|
|
||||||
|
if self.OMNYTools.yesno("Please confirm that this is ok with the flight tube. This check is to be removed after commissioning", "n"):
|
||||||
|
print("OK. continue.")
|
||||||
|
else:
|
||||||
|
print("Stopping.")
|
||||||
|
raise FlomniError("Manual abort of x-ray eye in.")
|
||||||
|
|
||||||
|
self.feye_in()
|
||||||
|
print("Moving X-ray optics out.")
|
||||||
|
self.foptics_out()
|
||||||
|
self.xrayeye_update_frame()
|
||||||
|
|
||||||
|
|
||||||
def laser_tracker_show_all(self):
|
def laser_tracker_show_all(self):
|
||||||
dev.rtx.controller.laser_tracker_show_all()
|
dev.rtx.controller.laser_tracker_show_all()
|
||||||
@@ -530,12 +555,6 @@ class FlomniSampleTransferMixin:
|
|||||||
self.flomnigui_show_cameras()
|
self.flomnigui_show_cameras()
|
||||||
|
|
||||||
|
|
||||||
if self.OMNYTools.yesno("Please confirm that there is currently no sample in the gripper. It would be dropped!", "y"):
|
|
||||||
print("good then")
|
|
||||||
else:
|
|
||||||
print("Stopping.")
|
|
||||||
raise FlomniError("The sample transfer was manually aborted.")
|
|
||||||
|
|
||||||
self.ftransfer_gripper_move(position)
|
self.ftransfer_gripper_move(position)
|
||||||
|
|
||||||
self.ftransfer_controller_enable_mount_mode()
|
self.ftransfer_controller_enable_mount_mode()
|
||||||
@@ -583,6 +602,8 @@ class FlomniSampleTransferMixin:
|
|||||||
if sample_in_position:
|
if sample_in_position:
|
||||||
raise FlomniError(f"The planned put position [{position}] already has a sample.")
|
raise FlomniError(f"The planned put position [{position}] already has a sample.")
|
||||||
|
|
||||||
|
self.flomnigui_show_cameras()
|
||||||
|
|
||||||
self.ftransfer_gripper_move(position)
|
self.ftransfer_gripper_move(position)
|
||||||
|
|
||||||
self.ftransfer_controller_enable_mount_mode()
|
self.ftransfer_controller_enable_mount_mode()
|
||||||
@@ -608,7 +629,7 @@ class FlomniSampleTransferMixin:
|
|||||||
self.ftransfer_controller_disable_mount_mode()
|
self.ftransfer_controller_disable_mount_mode()
|
||||||
self.ensure_gripper_up()
|
self.ensure_gripper_up()
|
||||||
|
|
||||||
sample_name = dev.flomni_samples.sample_in_gripper.get()
|
sample_name = dev.flomni_samples.sample_in_gripper_name.get()
|
||||||
self.flomni_modify_storage_non_interactive(100, 0, "-")
|
self.flomni_modify_storage_non_interactive(100, 0, "-")
|
||||||
self.flomni_modify_storage_non_interactive(position, 1, sample_name)
|
self.flomni_modify_storage_non_interactive(position, 1, sample_name)
|
||||||
|
|
||||||
@@ -736,7 +757,7 @@ class FlomniSampleTransferMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.OMNYTools.yesno("All OK? Continue?", "y"):
|
if self.OMNYTools.yesno("All OK? Continue?", "y"):
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
dev.ftransy.controller.socket_put_confirmed("confirm=1")
|
dev.ftransy.controller.socket_put_confirmed("confirm=1")
|
||||||
else:
|
else:
|
||||||
print("Stopping.")
|
print("Stopping.")
|
||||||
@@ -770,7 +791,7 @@ class FlomniSampleTransferMixin:
|
|||||||
if position == 0 and fsamx_pos > -160:
|
if position == 0 and fsamx_pos > -160:
|
||||||
|
|
||||||
if self.OMNYTools.yesno("May the flomni stage be moved out for the sample change? Feedback will be disabled and alignment will be lost!", "y"):
|
if self.OMNYTools.yesno("May the flomni stage be moved out for the sample change? Feedback will be disabled and alignment will be lost!", "y"):
|
||||||
print("good then")
|
print("OK. continue.")
|
||||||
self.ftransfer_flomni_stage_out()
|
self.ftransfer_flomni_stage_out()
|
||||||
else:
|
else:
|
||||||
print("Stopping.")
|
print("Stopping.")
|
||||||
@@ -968,43 +989,63 @@ class FlomniAlignmentMixin:
|
|||||||
|
|
||||||
def read_alignment_offset(
|
def read_alignment_offset(
|
||||||
self,
|
self,
|
||||||
dir_path=os.path.expanduser("~/Data10/specES1/internal/"),
|
dir_path=os.path.expanduser("~/data/raw/logs/"),
|
||||||
setup="flomni",
|
setup="flomni",
|
||||||
use_vertical_default_values=True,
|
use_vertical_default_values=True,
|
||||||
|
get_data_from_gui=False,
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
Read the alignment offset from the given directory and set the global parameter
|
|
||||||
tomo_alignment_fit.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dir_path (str, optional): The directory to read the alignment offset from. Defaults to os.path.expanduser("~/Data10/specES1/internal/").
|
|
||||||
"""
|
|
||||||
tomo_alignment_fit = np.zeros((2, 5))
|
tomo_alignment_fit = np.zeros((2, 5))
|
||||||
with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file:
|
|
||||||
tomo_alignment_fit[0][0] = file.readline()
|
|
||||||
|
|
||||||
with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file:
|
if not get_data_from_gui:
|
||||||
tomo_alignment_fit[0][1] = file.readline()
|
"""
|
||||||
|
Read the alignment parameters from xray eye fit.
|
||||||
|
|
||||||
with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file:
|
"""
|
||||||
tomo_alignment_fit[0][2] = file.readline()
|
with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file:
|
||||||
|
tomo_alignment_fit[0][0] = file.readline()
|
||||||
|
|
||||||
with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file:
|
with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file:
|
||||||
tomo_alignment_fit[1][0] = file.readline()
|
tomo_alignment_fit[0][1] = file.readline()
|
||||||
|
|
||||||
with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file:
|
with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file:
|
||||||
tomo_alignment_fit[1][1] = file.readline()
|
tomo_alignment_fit[0][2] = file.readline()
|
||||||
|
|
||||||
with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file:
|
with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file:
|
||||||
tomo_alignment_fit[1][2] = file.readline()
|
tomo_alignment_fit[1][0] = file.readline()
|
||||||
|
|
||||||
with open(os.path.join(dir_path, "ptychotomoalign_Ay3.txt"), "r") as file:
|
with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file:
|
||||||
tomo_alignment_fit[1][3] = file.readline()
|
tomo_alignment_fit[1][1] = file.readline()
|
||||||
|
|
||||||
|
with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file:
|
||||||
|
tomo_alignment_fit[1][2] = file.readline()
|
||||||
|
|
||||||
|
with open(os.path.join(dir_path, "ptychotomoalign_Ay3.txt"), "r") as file:
|
||||||
|
tomo_alignment_fit[1][3] = file.readline()
|
||||||
|
|
||||||
|
with open(os.path.join(dir_path, "ptychotomoalign_Cy3.txt"), "r") as file:
|
||||||
|
tomo_alignment_fit[1][4] = file.readline()
|
||||||
|
|
||||||
|
print("New alignment parameters loaded from filesystem, meaning Matlab fit:")
|
||||||
|
|
||||||
|
else:
|
||||||
|
params = dev.omny_xray_gui.fit_params_x.get()
|
||||||
|
|
||||||
|
#amplitude
|
||||||
|
tomo_alignment_fit[0][0] = params['SineModel_0_amplitude']
|
||||||
|
#phase
|
||||||
|
tomo_alignment_fit[0][1] = params['SineModel_0_shift']
|
||||||
|
#offset
|
||||||
|
tomo_alignment_fit[0][2] = params['LinearModel_1_intercept']
|
||||||
|
print("applying vertical default values from mirror calibration, not from fit!")
|
||||||
|
tomo_alignment_fit[1][0] = 0
|
||||||
|
tomo_alignment_fit[1][1] = 0
|
||||||
|
tomo_alignment_fit[1][2] = 0
|
||||||
|
tomo_alignment_fit[1][3] = 0
|
||||||
|
tomo_alignment_fit[1][4] = 0
|
||||||
|
print("New alignment parameters loaded based on Xray eye alignment GUI:")
|
||||||
|
|
||||||
with open(os.path.join(dir_path, "ptychotomoalign_Cy3.txt"), "r") as file:
|
|
||||||
tomo_alignment_fit[1][4] = file.readline()
|
|
||||||
|
|
||||||
print("New alignment parameters loaded:")
|
|
||||||
print(
|
print(
|
||||||
f"X Amplitude {tomo_alignment_fit[0][0]}, "
|
f"X Amplitude {tomo_alignment_fit[0][0]}, "
|
||||||
f"X Phase {tomo_alignment_fit[0][1]}, "
|
f"X Phase {tomo_alignment_fit[0][1]}, "
|
||||||
@@ -1157,18 +1198,12 @@ class Flomni(
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.client = client
|
self.client = client
|
||||||
self.device_manager = client.device_manager
|
self.device_manager = client.device_manager
|
||||||
self.check_shutter = False
|
|
||||||
self.check_light_available = False
|
|
||||||
self.check_fofb = False
|
|
||||||
self._check_msgs = []
|
|
||||||
self.tomo_id = -1
|
self.tomo_id = -1
|
||||||
self.special_angles = []
|
self.special_angles = []
|
||||||
self.special_angle_repeats = 20
|
self.special_angle_repeats = 20
|
||||||
self.special_angle_tolerance = 20
|
self.special_angle_tolerance = 20
|
||||||
self._current_special_angles = []
|
self._current_special_angles = []
|
||||||
self._beam_is_okay = True
|
self._beam_is_okay = True
|
||||||
self._stop_beam_check_event = None
|
|
||||||
self.beam_check_thread = None
|
|
||||||
self.corr_pos_y = []
|
self.corr_pos_y = []
|
||||||
self.corr_angle_y = []
|
self.corr_angle_y = []
|
||||||
self.corr_pos_y_2 = []
|
self.corr_pos_y_2 = []
|
||||||
@@ -1182,6 +1217,8 @@ class Flomni(
|
|||||||
self.progress["angle"] = 0
|
self.progress["angle"] = 0
|
||||||
self.progress["tomo_type"] = 0
|
self.progress["tomo_type"] = 0
|
||||||
self.OMNYTools = OMNYTools(self.client)
|
self.OMNYTools = OMNYTools(self.client)
|
||||||
|
self.reconstructor = PtychoReconstructor(self.ptycho_reconstruct_foldername)
|
||||||
|
self.tomo_id_manager = TomoIDManager()
|
||||||
self.align = XrayEyeAlign(self.client, self)
|
self.align = XrayEyeAlign(self.client, self)
|
||||||
self.set_client(client)
|
self.set_client(client)
|
||||||
|
|
||||||
@@ -1213,27 +1250,6 @@ class Flomni(
|
|||||||
def axis_id_to_numeric(self, axis_id) -> int:
|
def axis_id_to_numeric(self, axis_id) -> int:
|
||||||
return ord(axis_id.lower()) - 97
|
return ord(axis_id.lower()) - 97
|
||||||
|
|
||||||
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 set_special_angles(self, angles: list, repeats: int = 20, tolerance: float = 0.5):
|
def set_special_angles(self, angles: list, repeats: int = 20, tolerance: float = 0.5):
|
||||||
"""Set the special angles for a tomo
|
"""Set the special angles for a tomo
|
||||||
|
|
||||||
@@ -1377,6 +1393,7 @@ class Flomni(
|
|||||||
@ptycho_reconstruct_foldername.setter
|
@ptycho_reconstruct_foldername.setter
|
||||||
def ptycho_reconstruct_foldername(self, val: str):
|
def ptycho_reconstruct_foldername(self, val: str):
|
||||||
self.client.set_global_var("ptycho_reconstruct_foldername", val)
|
self.client.set_global_var("ptycho_reconstruct_foldername", val)
|
||||||
|
self.reconstructor.folder_name = val # keep reconstructor in sync
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tomo_angle_stepsize(self):
|
def tomo_angle_stepsize(self):
|
||||||
@@ -1437,18 +1454,6 @@ class Flomni(
|
|||||||
def sample_name(self):
|
def sample_name(self):
|
||||||
return self.sample_get_name(0)
|
return self.sample_get_name(0)
|
||||||
|
|
||||||
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):
|
def write_to_scilog(self, content, tags: list = None):
|
||||||
try:
|
try:
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
@@ -1470,6 +1475,9 @@ class Flomni(
|
|||||||
return
|
return
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
|
|
||||||
|
|
||||||
|
self.feye_out()
|
||||||
tags = ["BEC_alignment_tomo", self.sample_name]
|
tags = ["BEC_alignment_tomo", self.sample_name]
|
||||||
self.write_to_scilog(
|
self.write_to_scilog(
|
||||||
f"Starting alignment scan. First scan number: {bec.queue.next_scan_number}.", tags
|
f"Starting alignment scan. First scan number: {bec.queue.next_scan_number}.", tags
|
||||||
@@ -1484,7 +1492,6 @@ class Flomni(
|
|||||||
if 0 <= angle < 180.05:
|
if 0 <= angle < 180.05:
|
||||||
print(f"Starting flOMNI scan for angle {angle}")
|
print(f"Starting flOMNI scan for angle {angle}")
|
||||||
while not successful:
|
while not successful:
|
||||||
self._start_beam_check()
|
|
||||||
try:
|
try:
|
||||||
start_scan_number = bec.queue.next_scan_number
|
start_scan_number = bec.queue.next_scan_number
|
||||||
self.tomo_scan_projection(angle)
|
self.tomo_scan_projection(angle)
|
||||||
@@ -1497,18 +1504,15 @@ class Flomni(
|
|||||||
error_caught = True
|
error_caught = True
|
||||||
else:
|
else:
|
||||||
raise exc
|
raise exc
|
||||||
|
#todo here was if blchk success, then setting to success true
|
||||||
|
successful = True
|
||||||
|
|
||||||
if self._was_beam_okay() and not error_caught:
|
|
||||||
successful = True
|
|
||||||
else:
|
|
||||||
self._wait_for_beamline_checks()
|
|
||||||
end_scan_number = bec.queue.next_scan_number
|
end_scan_number = bec.queue.next_scan_number
|
||||||
for scan_nr in range(start_scan_number, end_scan_number):
|
for scan_nr in range(start_scan_number, end_scan_number):
|
||||||
self._write_tomo_scan_number(scan_nr, angle, 0)
|
self._write_tomo_scan_number(scan_nr, angle, 0)
|
||||||
|
|
||||||
print("Alignment scan finished. Please run SPEC_ptycho_align and load the new fit.")
|
|
||||||
|
|
||||||
umv(dev.fsamroy, 0)
|
umv(dev.fsamroy, 0)
|
||||||
|
self.OMNYTools.printgreenbold("\n\nAlignment scan finished. Please run SPEC_ptycho_align and load the new fit.")
|
||||||
|
|
||||||
def _write_subtomo_to_scilog(self, subtomo_number):
|
def _write_subtomo_to_scilog(self, subtomo_number):
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
@@ -1542,6 +1546,9 @@ class Flomni(
|
|||||||
|
|
||||||
self._write_subtomo_to_scilog(subtomo_number)
|
self._write_subtomo_to_scilog(subtomo_number)
|
||||||
|
|
||||||
|
if start_angle is not None:
|
||||||
|
print(f"Sub tomo scan with start angle {start_angle} requested.")
|
||||||
|
|
||||||
if start_angle is None:
|
if start_angle is None:
|
||||||
if subtomo_number == 1:
|
if subtomo_number == 1:
|
||||||
start_angle = 0
|
start_angle = 0
|
||||||
@@ -1560,29 +1567,94 @@ class Flomni(
|
|||||||
elif subtomo_number == 8:
|
elif subtomo_number == 8:
|
||||||
start_angle = self.tomo_angle_stepsize / 8.0 * 7
|
start_angle = self.tomo_angle_stepsize / 8.0 * 7
|
||||||
|
|
||||||
|
|
||||||
# _tomo_shift_angles (potential global variable)
|
# _tomo_shift_angles (potential global variable)
|
||||||
_tomo_shift_angles = 0
|
_tomo_shift_angles = 0
|
||||||
angle_end = start_angle + 180
|
# compute number of projections
|
||||||
|
|
||||||
|
start = start_angle + _tomo_shift_angles
|
||||||
|
|
||||||
|
if subtomo_number % 2: # odd = forward
|
||||||
|
max_allowed_angle = 180.05 + self.tomo_angle_stepsize
|
||||||
|
proposed_end = start + 180
|
||||||
|
angle_end = min(proposed_end, max_allowed_angle)
|
||||||
|
span = angle_end - start
|
||||||
|
|
||||||
|
else: # even = reverse
|
||||||
|
min_allowed_angle = 0
|
||||||
|
proposed_end = start - 180
|
||||||
|
angle_end = max(proposed_end, min_allowed_angle)
|
||||||
|
span = start - angle_end
|
||||||
|
|
||||||
|
# number of projections needed to maintain step size
|
||||||
|
N = int(span / self.tomo_angle_stepsize) + 1
|
||||||
|
|
||||||
angles = np.linspace(
|
angles = np.linspace(
|
||||||
start_angle + _tomo_shift_angles,
|
start,
|
||||||
angle_end,
|
angle_end,
|
||||||
num=int(180 / self.tomo_angle_stepsize) + 1,
|
num=N,
|
||||||
endpoint=True,
|
endpoint=True,
|
||||||
)
|
)
|
||||||
# reverse even sub-tomograms
|
|
||||||
if not (subtomo_number % 2):
|
if subtomo_number % 2: # odd subtomos → forward direction
|
||||||
angles = np.flip(angles)
|
# clamp end angle to max allowed
|
||||||
for angle in angles:
|
max_allowed_angle = 180.05 + self.tomo_angle_stepsize
|
||||||
|
proposed_end = start + 180
|
||||||
|
angle_end = min(proposed_end, max_allowed_angle)
|
||||||
|
|
||||||
|
angles = np.linspace(
|
||||||
|
start,
|
||||||
|
angle_end,
|
||||||
|
num=N,
|
||||||
|
endpoint=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # even subtomos → reverse direction
|
||||||
|
# go FROM start_angle down toward 0
|
||||||
|
min_allowed_angle = 0
|
||||||
|
proposed_end = start - 180
|
||||||
|
angle_end = max(proposed_end, min_allowed_angle)
|
||||||
|
|
||||||
|
angles = np.linspace(
|
||||||
|
start,
|
||||||
|
angle_end,
|
||||||
|
num=N,
|
||||||
|
endpoint=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, angle in enumerate(angles):
|
||||||
|
|
||||||
self.progress["subtomo"] = subtomo_number
|
self.progress["subtomo"] = subtomo_number
|
||||||
self.progress["subtomo_projection"] = np.where(angles == angle)[0][0]
|
|
||||||
self.progress["subtomo_total_projections"] = 180 / self.tomo_angle_stepsize
|
# --- NEW LOGIC FOR OFFSET WHEN start_angle IS SPECIFIED ---
|
||||||
self.progress["projection"] = (subtomo_number - 1) * self.progress[
|
if i == 0:
|
||||||
"subtomo_total_projections"
|
step = self.tomo_angle_stepsize
|
||||||
] + self.progress["subtomo_projection"]
|
sa = start_angle
|
||||||
|
|
||||||
|
if start_angle is None:
|
||||||
|
# normal operation: always start at zero
|
||||||
|
self._subtomo_offset = 0
|
||||||
|
|
||||||
|
else:
|
||||||
|
if subtomo_number % 2: # odd = forward direction
|
||||||
|
self._subtomo_offset = round(sa / step)
|
||||||
|
else: # even = reverse direction
|
||||||
|
self._subtomo_offset = round((180 - sa) / step)
|
||||||
|
|
||||||
|
# progress index must always increase
|
||||||
|
self.progress["subtomo_projection"] = self._subtomo_offset + i
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
# existing progress fields
|
||||||
|
self.progress["subtomo_total_projections"] = int(180 / self.tomo_angle_stepsize)
|
||||||
|
self.progress["projection"] = (subtomo_number - 1) * self.progress["subtomo_total_projections"] + self.progress["subtomo_projection"]
|
||||||
self.progress["total_projections"] = 180 / self.tomo_angle_stepsize * 8
|
self.progress["total_projections"] = 180 / self.tomo_angle_stepsize * 8
|
||||||
self.progress["angle"] = angle
|
self.progress["angle"] = angle
|
||||||
|
|
||||||
|
# finally do the scan at this angle
|
||||||
self._tomo_scan_at_angle(angle, subtomo_number)
|
self._tomo_scan_at_angle(angle, subtomo_number)
|
||||||
|
|
||||||
|
|
||||||
def _tomo_scan_at_angle(self, angle, subtomo_number):
|
def _tomo_scan_at_angle(self, angle, subtomo_number):
|
||||||
successful = False
|
successful = False
|
||||||
error_caught = False
|
error_caught = False
|
||||||
@@ -1590,7 +1662,7 @@ class Flomni(
|
|||||||
print(f"Starting flOMNI scan for angle {angle} in subtomo {subtomo_number}")
|
print(f"Starting flOMNI scan for angle {angle} in subtomo {subtomo_number}")
|
||||||
self._print_progress()
|
self._print_progress()
|
||||||
while not successful:
|
while not successful:
|
||||||
self._start_beam_check()
|
#self.bl_chk._bl_chk_start()
|
||||||
if not self.special_angles:
|
if not self.special_angles:
|
||||||
self._current_special_angles = []
|
self._current_special_angles = []
|
||||||
if self._current_special_angles:
|
if self._current_special_angles:
|
||||||
@@ -1613,10 +1685,10 @@ class Flomni(
|
|||||||
else:
|
else:
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
if self._was_beam_okay() and not error_caught:
|
# if self.bl_chk._bl_chk_stop() and not error_caught:
|
||||||
successful = True
|
successful = True
|
||||||
else:
|
# else:
|
||||||
self._wait_for_beamline_checks()
|
# self.bl_chk._bl_chk_wait_until_recovered()
|
||||||
end_scan_number = bec.queue.next_scan_number
|
end_scan_number = bec.queue.next_scan_number
|
||||||
for scan_nr in range(start_scan_number, end_scan_number):
|
for scan_nr in range(start_scan_number, end_scan_number):
|
||||||
self._write_tomo_scan_number(scan_nr, angle, subtomo_number)
|
self._write_tomo_scan_number(scan_nr, angle, subtomo_number)
|
||||||
@@ -1624,6 +1696,14 @@ class Flomni(
|
|||||||
def tomo_scan(self, subtomo_start=1, start_angle=None, projection_number=None):
|
def tomo_scan(self, subtomo_start=1, start_angle=None, projection_number=None):
|
||||||
"""start a tomo scan"""
|
"""start a tomo scan"""
|
||||||
|
|
||||||
|
if not self._check_eye_out_and_optics_in():
|
||||||
|
print("Attention: The setup is not in measurement condition.\nXray eye might be IN or the Xray optics OUT.")
|
||||||
|
if self.OMNYTools.yesno("Shall I continue?", "n"):
|
||||||
|
print("OK")
|
||||||
|
else:
|
||||||
|
print("Stopping.")
|
||||||
|
return
|
||||||
|
|
||||||
self.flomnigui_show_progress()
|
self.flomnigui_show_progress()
|
||||||
|
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
@@ -1637,19 +1717,19 @@ class Flomni(
|
|||||||
):
|
):
|
||||||
|
|
||||||
# pylint: disable=undefined-variable
|
# pylint: disable=undefined-variable
|
||||||
if bec.active_account != "":
|
# if bec.active_account != "":
|
||||||
self.tomo_id = self.add_sample_database(
|
# self.tomo_id = self.add_sample_database(
|
||||||
self.sample_name,
|
# self.sample_name,
|
||||||
str(datetime.date.today()),
|
# str(datetime.date.today()),
|
||||||
bec.active_account.decode(),
|
# bec.active_account,
|
||||||
bec.queue.next_scan_number,
|
# bec.queue.next_scan_number,
|
||||||
"flomni",
|
# "flomni",
|
||||||
"test additional info",
|
# "test additional info",
|
||||||
"BEC",
|
# "BEC",
|
||||||
)
|
# )
|
||||||
self.write_pdf_report()
|
# self.write_pdf_report()
|
||||||
else:
|
# else:
|
||||||
self.tomo_id = 0
|
self.tomo_id = 0
|
||||||
|
|
||||||
with scans.dataset_id_on_hold:
|
with scans.dataset_id_on_hold:
|
||||||
if self.tomo_type == 1:
|
if self.tomo_type == 1:
|
||||||
@@ -1754,6 +1834,11 @@ class Flomni(
|
|||||||
else:
|
else:
|
||||||
raise FlomniError("undefined tomo type")
|
raise FlomniError("undefined tomo type")
|
||||||
|
|
||||||
|
self.progress['projection'] = self.progress['total_projections']
|
||||||
|
self.progress["subtomo_projection"] = self.progress["subtomo_total_projections"]
|
||||||
|
self._print_progress()
|
||||||
|
self.OMNYTools.printgreenbold("Tomoscan finished")
|
||||||
|
|
||||||
def _print_progress(self):
|
def _print_progress(self):
|
||||||
print("\x1b[95mProgress report:")
|
print("\x1b[95mProgress report:")
|
||||||
print(f"Tomo type: ....................... {self.progress['tomo_type']}")
|
print(f"Tomo type: ....................... {self.progress['tomo_type']}")
|
||||||
@@ -1768,13 +1853,15 @@ class Flomni(
|
|||||||
self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user
|
self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user
|
||||||
):
|
):
|
||||||
"""Add a sample to the omny sample database. This also retrieves the tomo id."""
|
"""Add a sample to the omny sample database. This also retrieves the tomo id."""
|
||||||
subprocess.run(
|
return self.tomo_id_manager.register(
|
||||||
f"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'",
|
sample_name=samplename,
|
||||||
shell=True,
|
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
|
|
||||||
|
|
||||||
def _at_each_angle(self, angle: float) -> None:
|
def _at_each_angle(self, angle: float) -> None:
|
||||||
if "flomni_at_each_angle" in builtins.__dict__:
|
if "flomni_at_each_angle" in builtins.__dict__:
|
||||||
@@ -1833,26 +1920,18 @@ class Flomni(
|
|||||||
|
|
||||||
return angle, subtomo_number
|
return angle, subtomo_number
|
||||||
|
|
||||||
def tomo_reconstruct(self, base_path="~/Data10/specES1"):
|
def tomo_reconstruct(self, base_path="~/data/raw/logs/reconstruction_queue"):
|
||||||
"""write the tomo reconstruct file for the reconstruction queue"""
|
"""write the tomo reconstruct file for the reconstruction queue"""
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
base_path = os.path.expanduser(base_path)
|
self.reconstructor.write(
|
||||||
ptycho_queue_path = Path(os.path.join(base_path, self.ptycho_reconstruct_foldername))
|
scan_list=self._current_scan_list,
|
||||||
ptycho_queue_path.mkdir(parents=True, exist_ok=True)
|
next_scan_number=bec.queue.next_scan_number,
|
||||||
|
base_path=base_path,
|
||||||
# pylint: disable=undefined-variable
|
|
||||||
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")
|
|
||||||
)
|
)
|
||||||
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 _write_tomo_scan_number(self, scan_number: int, angle: float, subtomo_number: int) -> None:
|
def _write_tomo_scan_number(self, scan_number: int, angle: float, subtomo_number: int) -> None:
|
||||||
tomo_scan_numbers_file = os.path.expanduser(
|
tomo_scan_numbers_file = os.path.expanduser(
|
||||||
"~/tomography_scannumbers.txt"
|
"~/data/raw/logs/tomography_scannumbers.txt"
|
||||||
)
|
)
|
||||||
with open(tomo_scan_numbers_file, "a+") as out_file:
|
with open(tomo_scan_numbers_file, "a+") as out_file:
|
||||||
# pylint: disable=undefined-variable
|
# pylint: disable=undefined-variable
|
||||||
@@ -1861,6 +1940,10 @@ class Flomni(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def tomo_scan_projection(self, angle: float):
|
def tomo_scan_projection(self, angle: float):
|
||||||
|
|
||||||
|
dev.rtx.controller.laser_tracker_check_signalstrength()
|
||||||
|
|
||||||
|
|
||||||
scans = builtins.__dict__.get("scans")
|
scans = builtins.__dict__.get("scans")
|
||||||
|
|
||||||
# additional_correction = self.align.compute_additional_correction(angle)
|
# additional_correction = self.align.compute_additional_correction(angle)
|
||||||
@@ -1895,7 +1978,6 @@ class Flomni(
|
|||||||
f"{str(datetime.datetime.now())}: flomni scan projection at angle {angle}, scan"
|
f"{str(datetime.datetime.now())}: flomni scan projection at angle {angle}, scan"
|
||||||
f" number {bec.queue.next_scan_number}.\n"
|
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])
|
# self.write_to_scilog(log_message, ["BEC_scans", self.sample_name])
|
||||||
scans.flomni_fermat_scan(
|
scans.flomni_fermat_scan(
|
||||||
fovx=self.fovx,
|
fovx=self.fovx,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import time
|
import time
|
||||||
|
import numpy as np
|
||||||
from rich import box
|
from rich import box
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
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:
|
class FlomniOpticsMixin:
|
||||||
@@ -16,12 +16,18 @@ class FlomniOpticsMixin:
|
|||||||
return param.get(var)
|
return param.get(var)
|
||||||
|
|
||||||
def feye_out(self):
|
def feye_out(self):
|
||||||
fshclose()
|
dev.omnyfsh.fshclose()
|
||||||
self.foptics_in()
|
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")
|
feyex_out = self._get_user_param_safe("feyex", "out")
|
||||||
umv(dev.feyex, feyex_out)
|
umv(dev.feyex, feyex_out)
|
||||||
|
|
||||||
epics_put("XOMNYI-XEYE-ACQ:0", 2)
|
|
||||||
# move rotation stage to zero to avoid problems with wires
|
# move rotation stage to zero to avoid problems with wires
|
||||||
umv(dev.fsamroy, 0)
|
umv(dev.fsamroy, 0)
|
||||||
# umv(dev.fttrx1, 9.2)
|
# umv(dev.fttrx1, 9.2)
|
||||||
@@ -32,16 +38,39 @@ class FlomniOpticsMixin:
|
|||||||
|
|
||||||
feyex_in = self._get_user_param_safe("feyex", "in")
|
feyex_in = self._get_user_param_safe("feyex", "in")
|
||||||
feyey_in = self._get_user_param_safe("feyey", "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)
|
||||||
|
|
||||||
|
self.xrayeye_update_frame()
|
||||||
|
|
||||||
def _ffzp_in(self):
|
def _ffzp_in(self):
|
||||||
foptx_in = self._get_user_param_safe("foptx", "in")
|
foptx_in = self._get_user_param_safe("foptx", "in")
|
||||||
fopty_in = self._get_user_param_safe("fopty", "in")
|
fopty_in = self._get_user_param_safe("fopty", "in")
|
||||||
umv(dev.foptx, foptx_in)
|
|
||||||
umv(
|
current_foptx = dev.foptx.readback.get()
|
||||||
dev.fopty, fopty_in
|
current_fopty = dev.fopty.readback.get()
|
||||||
) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
|
|
||||||
|
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):
|
def ffzp_in(self):
|
||||||
"""
|
"""
|
||||||
@@ -84,19 +113,85 @@ class FlomniOpticsMixin:
|
|||||||
# umv(dev.losax, -1.4850, dev.losay, -0.1930)
|
# umv(dev.losax, -1.4850, dev.losay, -0.1930)
|
||||||
# umv(dev.losaz, 1.0000)
|
# umv(dev.losaz, 1.0000)
|
||||||
# 7.2, 150
|
# 7.2, 150
|
||||||
|
|
||||||
fosax_in = self._get_user_param_safe("fosax", "in")
|
fosax_in = self._get_user_param_safe("fosax", "in")
|
||||||
fosay_in = self._get_user_param_safe("fosay", "in")
|
fosay_in = self._get_user_param_safe("fosay", "in")
|
||||||
fosaz_in = self._get_user_param_safe("fosaz", "in")
|
fosaz_in = self._get_user_param_safe("fosaz", "in")
|
||||||
|
|
||||||
|
# tighten limits
|
||||||
dev.fosax.limits = [fosax_in - 0.1, fosax_in + 0.1]
|
dev.fosax.limits = [fosax_in - 0.1, fosax_in + 0.1]
|
||||||
dev.fosay.limits = [fosay_in - 0.1, fosay_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]
|
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
|
# 11 kev
|
||||||
# umv(dev.losax, -1.161000, dev.losay, -0.196)
|
# umv(dev.losax, -1.161000, dev.losay, -0.196)
|
||||||
# umv(dev.losaz, 1.0000)
|
# 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):
|
def fosa_out(self):
|
||||||
self.ensure_fheater_up()
|
self.ensure_fheater_up()
|
||||||
curtain_is_triggered = dev.foptz.controller.fosaz_light_curtain_is_triggered()
|
curtain_is_triggered = dev.foptz.controller.fosaz_light_curtain_is_triggered()
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import builtins
|
import builtins
|
||||||
|
import time
|
||||||
from bec_widgets.cli.client import BECDockArea
|
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
if builtins.__dict__.get("bec") is not None:
|
if builtins.__dict__.get("bec") is not None:
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
|
|
||||||
class flomniGuiToolsError(Exception):
|
class flomniGuiToolsError(Exception):
|
||||||
@@ -27,9 +29,10 @@ class flomniGuiTools:
|
|||||||
|
|
||||||
def flomnigui_show_gui(self):
|
def flomnigui_show_gui(self):
|
||||||
if "flomni" in self.gui.windows:
|
if "flomni" in self.gui.windows:
|
||||||
self.gui.flomni.show()
|
self.gui.flomni.raise_window()
|
||||||
else:
|
else:
|
||||||
self.gui.new("flomni")
|
self.gui.new("flomni")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
def flomnigui_stop_gui(self):
|
def flomnigui_stop_gui(self):
|
||||||
self.gui.flomni.hide()
|
self.gui.flomni.hide()
|
||||||
@@ -37,39 +40,45 @@ class flomniGuiTools:
|
|||||||
def flomnigui_raise(self):
|
def flomnigui_raise(self):
|
||||||
self.gui.flomni.raise_window()
|
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):
|
def flomnigui_show_xeyealign(self):
|
||||||
self.flomnigui_show_gui()
|
self.flomnigui_show_gui()
|
||||||
if self._flomnigui_check_attribute_not_exists("xeyegui"):
|
if self._flomnigui_check_attribute_not_exists("xeyegui"):
|
||||||
self.flomnigui_remove_all_docks()
|
self.flomnigui_remove_all_docks()
|
||||||
self.xeyegui = self.gui.flomni.new("xeyegui").new("XRayEye")
|
self.xeyegui = self.gui.flomni.new("XRayEye", object_name="xrayeye")
|
||||||
# start live
|
# start live
|
||||||
if not dev.cam_xeye.live_mode:
|
if not dev.cam_xeye.live_mode_enabled.get():
|
||||||
dev.cam_xeye.live_mode = True
|
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_check_attribute_not_exists("xeyegui"):
|
||||||
|
self.flomnigui_remove_all_docks()
|
||||||
|
self.xeyegui = self.gui.flomni.new("XRayEye")
|
||||||
|
self.xeyegui.switch_tab("fit")
|
||||||
|
|
||||||
def _flomnigui_check_attribute_not_exists(self, attribute_name):
|
def _flomnigui_check_attribute_not_exists(self, attribute_name):
|
||||||
if hasattr(self.gui,"flomni"):
|
if hasattr(self.gui, "flomni"):
|
||||||
if hasattr(self.gui.flomni,attribute_name):
|
if attribute_name == "xeyegui":
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
def flomnigui_show_cameras(self):
|
def flomnigui_show_cameras(self):
|
||||||
self.flomnigui_show_gui()
|
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()
|
self.flomnigui_remove_all_docks()
|
||||||
camera_gripper_image = self.gui.flomni.new("camera_gripper").new("Image")
|
camera_gripper_image = self.gui.flomni.new("Image")
|
||||||
if self._flomnicam_check_device_exists(dev.cam_flomni_gripper):
|
if self._flomnicam_check_device_exists(dev.cam_flomni_gripper):
|
||||||
camera_gripper_image.image(("cam_flomni_gripper", "preview"))
|
camera_gripper_image.image(device="cam_flomni_gripper", signal="preview")
|
||||||
camera_gripper_image.lock_aspect_ratio = True
|
camera_gripper_image.lock_aspect_ratio = True
|
||||||
camera_gripper_image.enable_fps_monitor = True
|
camera_gripper_image.enable_fps_monitor = True
|
||||||
camera_gripper_image.enable_toolbar = False
|
camera_gripper_image.enable_toolbar = False
|
||||||
@@ -78,9 +87,9 @@ class flomniGuiTools:
|
|||||||
dev.cam_flomni_gripper.start_live_mode()
|
dev.cam_flomni_gripper.start_live_mode()
|
||||||
else:
|
else:
|
||||||
print("Cannot open camera_gripper. Device does not exist.")
|
print("Cannot open camera_gripper. Device does not exist.")
|
||||||
camera_overview_image = self.gui.flomni.new("camera_overview").new("Image")
|
camera_overview_image = self.gui.flomni.new("Image")
|
||||||
if self._flomnicam_check_device_exists(dev.cam_flomni_overview):
|
if self._flomnicam_check_device_exists(dev.cam_flomni_overview):
|
||||||
camera_overview_image.image(("cam_flomni_overview", "preview"))
|
camera_overview_image.image(device="cam_flomni_overview", signal="preview")
|
||||||
camera_overview_image.lock_aspect_ratio = True
|
camera_overview_image.lock_aspect_ratio = True
|
||||||
camera_overview_image.enable_fps_monitor = True
|
camera_overview_image.enable_fps_monitor = True
|
||||||
camera_overview_image.enable_toolbar = False
|
camera_overview_image.enable_toolbar = False
|
||||||
@@ -91,10 +100,11 @@ class flomniGuiTools:
|
|||||||
print("Cannot open camera_overview. Device does not exist.")
|
print("Cannot open camera_overview. Device does not exist.")
|
||||||
|
|
||||||
def flomnigui_remove_all_docks(self):
|
def flomnigui_remove_all_docks(self):
|
||||||
#dev.cam_flomni_overview.stop_live_mode()
|
# dev.cam_flomni_overview.stop_live_mode()
|
||||||
#dev.cam_flomni_gripper.stop_live_mode()
|
# dev.cam_flomni_gripper.stop_live_mode()
|
||||||
#dev.cam_xeye.live_mode = False
|
# dev.cam_xeye.live_mode = False
|
||||||
self.gui.flomni.delete_all()
|
if hasattr(self.gui, "flomni"):
|
||||||
|
self.gui.flomni.delete_all()
|
||||||
self.progressbar = None
|
self.progressbar = None
|
||||||
self.text_box = None
|
self.text_box = None
|
||||||
|
|
||||||
@@ -102,14 +112,15 @@ class flomniGuiTools:
|
|||||||
self.flomnigui_show_gui()
|
self.flomnigui_show_gui()
|
||||||
if self._flomnigui_check_attribute_not_exists("idle_text_box"):
|
if self._flomnigui_check_attribute_not_exists("idle_text_box"):
|
||||||
self.flomnigui_remove_all_docks()
|
self.flomnigui_remove_all_docks()
|
||||||
idle_text_box = self.gui.flomni.new("idle_textbox").new("TextBox")
|
idle_text_box = self.gui.flomni.new("TextBox")
|
||||||
text = (
|
text = (
|
||||||
"<pre>"
|
"<pre>"
|
||||||
+ " ,---.,--. ,-----. ,--. ,--.,--. ,--.,--. \n"
|
+ "██████╗ ███████╗ ██████╗ ███████╗██╗ ██████╗ ███╗ ███╗███╗ ██╗██╗\n"
|
||||||
+ "/ .-'| |' .-. '| `.' || ,'.| || | \n"
|
+ "██╔══██╗██╔════╝██╔════╝ ██╔════╝██║ ██╔═══██╗████╗ ████║████╗ ██║██║\n"
|
||||||
+ "| `-,| || | | || |'.'| || |' ' || | \n"
|
+ "██████╔╝█████╗ ██║ █████╗ ██║ ██║ ██║██╔████╔██║██╔██╗ ██║██║\n"
|
||||||
+ "| .-'| |' '-' '| | | || | ` || | \n"
|
+ "██╔══██╗██╔══╝ ██║ ██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╗██║██║\n"
|
||||||
+ "`--' `--' `-----' `--' `--'`--' `--'`--' \n"
|
+ "██████╔╝███████╗╚██████╗ ██║ ███████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚████║██║\n"
|
||||||
|
+ "╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝\n"
|
||||||
+ "</pre>"
|
+ "</pre>"
|
||||||
)
|
)
|
||||||
idle_text_box.set_html_text(text)
|
idle_text_box.set_html_text(text)
|
||||||
@@ -118,13 +129,12 @@ class flomniGuiTools:
|
|||||||
import csaxs_bec
|
import csaxs_bec
|
||||||
from pathlib import Path
|
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
|
csaxs_bec_basepath = Path(csaxs_bec.__file__).parent
|
||||||
docs_folder = (
|
docs_folder = csaxs_bec_basepath / "bec_ipython_client" / "plugins" / "flomni" / "docs"
|
||||||
csaxs_bec_basepath /
|
|
||||||
"bec_ipython_client" / "plugins" / "flomni" / "docs"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not docs_folder.is_dir():
|
if not docs_folder.is_dir():
|
||||||
raise NotADirectoryError(f"Docs folder not found: {docs_folder}")
|
raise NotADirectoryError(f"Docs folder not found: {docs_folder}")
|
||||||
@@ -162,10 +172,9 @@ class flomniGuiTools:
|
|||||||
self.pdf_viewer = self.gui.flomni.new(widget="PdfViewerWidget")
|
self.pdf_viewer = self.gui.flomni.new(widget="PdfViewerWidget")
|
||||||
|
|
||||||
# --- Load PDF ---------------------------------------------------------
|
# --- 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")
|
print(f"\nLoaded: {pdf_file.name}\n")
|
||||||
|
|
||||||
|
|
||||||
def _flomnicam_check_device_exists(self, device):
|
def _flomnicam_check_device_exists(self, device):
|
||||||
try:
|
try:
|
||||||
device
|
device
|
||||||
@@ -179,26 +188,18 @@ class flomniGuiTools:
|
|||||||
if self._flomnigui_check_attribute_not_exists("progressbar"):
|
if self._flomnigui_check_attribute_not_exists("progressbar"):
|
||||||
self.flomnigui_remove_all_docks()
|
self.flomnigui_remove_all_docks()
|
||||||
# Add a new dock with a RingProgressBar widget
|
# Add a new dock with a RingProgressBar widget
|
||||||
self.progressbar = self.gui.flomni.new("progressbar").new("RingProgressBar")
|
self.progressbar = self.gui.flomni.new("RingProgressBar")
|
||||||
# Customize the size of the progress ring
|
|
||||||
self.progressbar.set_line_widths(20)
|
# Setting multiple rings with different values
|
||||||
# Disable automatic updates and manually set the self.progressbar value
|
self.progressbar.add_ring().set_update("manual")
|
||||||
self.progressbar.enable_auto_updates(False)
|
self.progressbar.add_ring().set_update("manual")
|
||||||
# Set precision for the self.progressbar display
|
self.progressbar.add_ring().set_update("scan")
|
||||||
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._flomnigui_update_progress()
|
self._flomnigui_update_progress()
|
||||||
|
|
||||||
def _flomnigui_update_progress(self):
|
def _flomnigui_update_progress(self):
|
||||||
|
main_progress_ring = self.progressbar.rings[0]
|
||||||
|
subtomo_progress_ring = self.progressbar.rings[1]
|
||||||
if self.progressbar is not None:
|
if self.progressbar is not None:
|
||||||
progress = self.progress["projection"] / self.progress["total_projections"] * 100
|
progress = self.progress["projection"] / self.progress["total_projections"] * 100
|
||||||
subtomo_progress = (
|
subtomo_progress = (
|
||||||
@@ -206,10 +207,20 @@ class flomniGuiTools:
|
|||||||
/ self.progress["subtomo_total_projections"]
|
/ self.progress["subtomo_total_projections"]
|
||||||
* 100
|
* 100
|
||||||
)
|
)
|
||||||
self.progressbar.set_value([progress, subtomo_progress, 0])
|
main_progress_ring.set_value(progress)
|
||||||
if self.text_box is not None:
|
subtomo_progress_ring.set_value(subtomo_progress)
|
||||||
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)
|
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'])}"
|
||||||
|
)
|
||||||
|
self.progressbar.set_center_label(text)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -220,6 +231,7 @@ if __name__ == "__main__":
|
|||||||
client.start()
|
client.start()
|
||||||
client.gui = BECGuiClient()
|
client.gui = BECGuiClient()
|
||||||
|
|
||||||
flomni_gui = flomniGuiTools(client)
|
flomni_gui = flomniGuiTools()
|
||||||
|
flomni_gui.set_client(client)
|
||||||
flomni_gui.flomnigui_show_gui()
|
flomni_gui.flomnigui_show_gui()
|
||||||
flomni_gui.flomnigui_show_progress()
|
flomni_gui.flomnigui_show_progress()
|
||||||
|
|||||||
@@ -5,16 +5,22 @@ import os
|
|||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from bec_lib import bec_logger
|
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
|
logger = bec_logger.logger
|
||||||
# import builtins to avoid linter errors
|
# import builtins to avoid linter errors
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bec_ipython_client.plugins.flomni import Flomni
|
from bec_ipython_client.plugins.flomni import Flomni
|
||||||
@@ -22,7 +28,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class XrayEyeAlign:
|
class XrayEyeAlign:
|
||||||
# pixel calibration, multiply to get mm
|
# pixel calibration, multiply to get mm
|
||||||
labview=False
|
test_wo_movements = True
|
||||||
PIXEL_CALIBRATION = 0.1 / 113 # .2 with binning
|
PIXEL_CALIBRATION = 0.1 / 113 # .2 with binning
|
||||||
|
|
||||||
def __init__(self, client, flomni: Flomni) -> None:
|
def __init__(self, client, flomni: Flomni) -> None:
|
||||||
@@ -34,244 +40,250 @@ class XrayEyeAlign:
|
|||||||
self.flomni.reset_correction()
|
self.flomni.reset_correction()
|
||||||
self.flomni.reset_tomo_alignment_fit()
|
self.flomni.reset_tomo_alignment_fit()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gui(self):
|
||||||
|
return self.flomni.xeyegui
|
||||||
|
|
||||||
def _reset_init_values(self):
|
def _reset_init_values(self):
|
||||||
self.shift_xy = [0, 0]
|
self.shift_xy = [0, 0]
|
||||||
self._xray_fov_xy = [0, 0]
|
self._xray_fov_xy = [0, 0]
|
||||||
|
|
||||||
def save_frame(self):
|
def update_frame(self, keep_shutter_open=False):
|
||||||
epics_put("XOMNYI-XEYE-SAVFRAME:0", 1)
|
if self.flomni._flomnigui_check_attribute_not_exists("xeyegui"):
|
||||||
|
|
||||||
def update_frame(self,keep_shutter_open=False):
|
|
||||||
if self.labview:
|
|
||||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
|
||||||
|
|
||||||
if not self.labview:
|
|
||||||
self.flomni.flomnigui_show_xeyealign()
|
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 not dev.cam_xeye.live_mode_enabled.get():
|
||||||
if self.labview:
|
dev.cam_xeye.live_mode_enabled.put(True)
|
||||||
# wait for start live
|
|
||||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
|
||||||
time.sleep(0.5)
|
|
||||||
print("waiting for live view to start...")
|
|
||||||
|
|
||||||
fshopen()
|
self.gui.on_live_view_enabled(True)
|
||||||
|
|
||||||
if self.labview:
|
dev.omnyfsh.fshopen()
|
||||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
|
||||||
|
|
||||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
|
||||||
print("waiting for new frame...")
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
# stop live view
|
# stop live view
|
||||||
if not keep_shutter_open:
|
if not keep_shutter_open:
|
||||||
epics_put("XOMNYI-XEYE-ACQ:0", 0)
|
self.gui.on_live_view_enabled(False)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
fshclose()
|
dev.omnyfsh.fshclose()
|
||||||
print("got new frame")
|
print("Received new frame.")
|
||||||
else:
|
else:
|
||||||
print("Staying in live view, shutter is and remains open!")
|
print("Staying in live view, shutter is and remains open!")
|
||||||
|
|
||||||
def tomo_rotate(self, val: float):
|
def tomo_rotate(self, val: float):
|
||||||
# pylint: disable=undefined-variable
|
if not self.test_wo_movements:
|
||||||
umv(self.device_manager.devices.fsamroy, val)
|
umv(self.device_manager.devices.fsamroy, val)
|
||||||
|
|
||||||
def get_tomo_angle(self):
|
def get_tomo_angle(self):
|
||||||
return self.device_manager.devices.fsamroy.readback.get()
|
return self.device_manager.devices.fsamroy.readback.get()
|
||||||
|
|
||||||
def update_fov(self, k: int):
|
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[0] = max(
|
||||||
self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0])
|
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, enablex: bool, enabley: bool):
|
||||||
def movement_buttons_enabled(self):
|
self.gui.on_motors_enable(enablex, enabley)
|
||||||
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 send_message(self, msg: str):
|
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:
|
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...")
|
self.send_message("Getting things ready. Please wait...")
|
||||||
|
|
||||||
#potential unresolved movement requests to zero
|
self.gui.enable_submit_button(False)
|
||||||
epics_put("XOMNYI-XEYE-MVX:0", 0)
|
|
||||||
epics_put("XOMNYI-XEYE-MVY:0", 0)
|
# 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
|
# reset shift xy and fov params
|
||||||
self._reset_init_values()
|
self._reset_init_values()
|
||||||
|
|
||||||
self.flomni.lights_off()
|
self.flomni.lights_off()
|
||||||
|
|
||||||
self.flomni.flomnigui_show_xeyealign()
|
if not self.test_wo_movements:
|
||||||
self.flomni.flomnigui_raise()
|
self.tomo_rotate(0)
|
||||||
|
|
||||||
self.tomo_rotate(0)
|
self.flomni.feye_in()
|
||||||
epics_put("XOMNYI-XEYE-ANGLE:0", 0)
|
|
||||||
|
|
||||||
self.flomni.feye_in()
|
|
||||||
|
|
||||||
self.flomni.laser_tracker_on()
|
self.flomni.laser_tracker_on()
|
||||||
|
|
||||||
self.flomni.feedback_enable_with_reset()
|
self.flomni.feedback_enable_with_reset()
|
||||||
|
|
||||||
# disable movement buttons
|
# disable movement buttons
|
||||||
self.movement_buttons_enabled = False
|
self.movement_buttons_enabled(False, False)
|
||||||
|
|
||||||
sample_name = self.flomni.sample_get_name(0)
|
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
|
# this makes sure we are in a defined state
|
||||||
self.flomni.feedback_disable()
|
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")
|
self.flomni.ffzp_in()
|
||||||
umv(dev.fsamx, fsamx_in - 0.25)
|
|
||||||
|
|
||||||
self.flomni.ffzp_in()
|
|
||||||
self.update_frame(keep_shutter_open)
|
self.update_frame(keep_shutter_open)
|
||||||
|
|
||||||
# enable submit buttons
|
self.gui.enable_submit_button(True)
|
||||||
self.movement_buttons_enabled = False
|
dev.omny_xray_gui.step.set(0).wait()
|
||||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
|
||||||
epics_put("XOMNYI-XEYE-STEP:0", 0)
|
|
||||||
self.send_message("Submit center value of FZP.")
|
self.send_message("Submit center value of FZP.")
|
||||||
|
|
||||||
k = 0
|
k = 0
|
||||||
while True:
|
while True:
|
||||||
if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1:
|
if dev.omny_xray_gui.submit.get() == 1:
|
||||||
val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") / 2 * self.PIXEL_CALIBRATION # in mm
|
|
||||||
self.alignment_values[k] = val_x
|
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]}")
|
print(f"Clicked position {k}: x {self.alignment_values[k]}")
|
||||||
rtx_position = dev.rtx.readback.get() / 1000
|
rtx_position = dev.rtx.readback.get() / 1000
|
||||||
print(f"Current rtx position {rtx_position}")
|
print(f"Current rtx position {rtx_position}")
|
||||||
self.alignment_values[k] -= rtx_position
|
self.alignment_values[k] -= rtx_position
|
||||||
print(f"Corrected position {k}: x {self.alignment_values[k]}")
|
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
|
if k == 0: # received center value of FZP
|
||||||
self.send_message("please wait ...")
|
self.send_message("please wait ...")
|
||||||
self.movement_buttons_enabled = False
|
self.movement_buttons_enabled(False, False)
|
||||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
self.gui.enable_submit_button(False)
|
||||||
|
|
||||||
self.flomni.feedback_disable()
|
self.flomni.feedback_disable()
|
||||||
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
|
if not self.test_wo_movements:
|
||||||
umv(dev.fsamx, fsamx_in)
|
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()
|
time.sleep(0.5)
|
||||||
umv(dev.fsamx, fsamx_in - 0.25)
|
|
||||||
|
|
||||||
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.flomni.feedback_enable_with_reset()
|
||||||
|
|
||||||
self.update_frame(keep_shutter_open)
|
self.update_frame(keep_shutter_open)
|
||||||
self.send_message("Adjust sample height and submit center")
|
self.send_message("Step 1/5: Adjust sample height and submit center")
|
||||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
self.gui.enable_submit_button(True)
|
||||||
self.movement_buttons_enabled = True
|
self.movement_buttons_enabled(True, True)
|
||||||
|
|
||||||
elif 1 <= k < 5: # received sample center value at samroy 0 ... 315
|
elif 1 <= k < 5: # received sample center value at samroy 0 ... 315
|
||||||
self.send_message("please wait ...")
|
self.send_message("please wait ...")
|
||||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
|
self.gui.enable_submit_button(False)
|
||||||
self.movement_buttons_enabled = False
|
self.movement_buttons_enabled(False, False)
|
||||||
|
|
||||||
umv(dev.rtx, 0)
|
umv(dev.rtx, 0)
|
||||||
self.tomo_rotate(k * 45)
|
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.update_frame(keep_shutter_open)
|
||||||
self.send_message("Submit sample center")
|
self.send_message(f"Step {k+1}/5: Submit sample center")
|
||||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
self.gui.enable_submit_button(True)
|
||||||
epics_put("XOMNYI-XEYE-ENAMVX:0", 1)
|
self.movement_buttons_enabled(True, False)
|
||||||
self.update_fov(k)
|
self.update_fov(k)
|
||||||
|
|
||||||
elif k == 5: # received sample center value at samroy 270 and done
|
elif k == 5: # received sample center value at samroy 270 and done
|
||||||
self.send_message("done...")
|
self.send_message("done...")
|
||||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
self.gui.enable_submit_button(False)
|
||||||
self.movement_buttons_enabled = False
|
self.movement_buttons_enabled(False, False)
|
||||||
self.update_fov(k)
|
self.update_fov(k)
|
||||||
break
|
break
|
||||||
|
|
||||||
k += 1
|
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:
|
if _xrayeyalignmvx != 0:
|
||||||
umvr(dev.rtx, _xrayeyalignmvx)
|
umvr(dev.rtx, _xrayeyalignmvx)
|
||||||
print(f"Current rtx position {dev.rtx.readback.get() / 1000}")
|
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)
|
time.sleep(3)
|
||||||
|
dev.omny_xray_gui.mvx.set(0)
|
||||||
self.update_frame(keep_shutter_open)
|
self.update_frame(keep_shutter_open)
|
||||||
|
|
||||||
if k < 2:
|
if k < 2:
|
||||||
# allow movements, store movements to calculate center
|
# allow movements, store movements to calculate center
|
||||||
_xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0")
|
_xrayeyalignmvy = dev.omny_xray_gui.mvy.get()
|
||||||
if _xrayeyalignmvy != 0:
|
if _xrayeyalignmvy != 0:
|
||||||
self.flomni.feedback_disable()
|
self.flomni.feedback_disable()
|
||||||
umvr(dev.fsamy, _xrayeyalignmvy / 1000)
|
if not self.test_wo_movements:
|
||||||
|
umvr(dev.fsamy, _xrayeyalignmvy / 1000)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
epics_put("XOMNYI-XEYE-MVY:0", 0)
|
dev.omny_xray_gui.mvy.set(0)
|
||||||
self.flomni.feedback_enable_with_reset()
|
self.flomni.feedback_enable_with_reset()
|
||||||
self.update_frame(keep_shutter_open)
|
self.update_frame(keep_shutter_open)
|
||||||
time.sleep(0.2)
|
time.sleep(0.1)
|
||||||
|
|
||||||
self.write_output()
|
self.write_output()
|
||||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||||
|
|
||||||
self.tomo_rotate(0)
|
if keep_shutter_open:
|
||||||
|
if self.flomni.OMNYTools.yesno("Close the shutter now?", "y"):
|
||||||
umv(dev.rtx, 0)
|
dev.omnyfsh.fshclose()
|
||||||
|
self.gui.on_live_view_enabled(False)
|
||||||
# free camera
|
print("setting 'XOMNYI-XEYE-ACQ:0'")
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy"
|
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy"
|
||||||
f" = {fovy:.0f} microns"
|
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.")
|
||||||
|
|
||||||
def write_output(self):
|
def write_output(self):
|
||||||
file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues")
|
file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues")
|
||||||
if not os.path.exists(file):
|
if not os.path.exists(file):
|
||||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||||
|
|
||||||
with open(file, "w") as alignment_values_file:
|
with open(file, "w") as alignment_values_file:
|
||||||
alignment_values_file.write("angle\thorizontal\n")
|
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):
|
for k in range(1, 6):
|
||||||
fovx_offset = self.alignment_values[0] - self.alignment_values[k]
|
fovx_offset = self.alignment_values[0] - self.alignment_values[k]
|
||||||
print(f"Writing to file new alignment: number {k}, value x {fovx_offset}")
|
fovx_offsets[k - 1] = fovx_offset # store in array
|
||||||
alignment_values_file.write(f"{(k-1)*45}\t{fovx_offset*1000}\n")
|
|
||||||
|
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()
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import builtins
|
import builtins
|
||||||
|
|
||||||
from bec_widgets.cli.client import BECDockArea
|
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
if builtins.__dict__.get("bec") is not None:
|
if builtins.__dict__.get("bec") is not None:
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
|
|
||||||
class OMNYGuiToolsError(Exception):
|
class OMNYGuiToolsError(Exception):
|
||||||
@@ -19,7 +19,7 @@ class OMNYGuiTools:
|
|||||||
|
|
||||||
def __init__(self, client):
|
def __init__(self, client):
|
||||||
self.gui = getattr(client, "gui", None)
|
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.fig200 = None
|
||||||
self.fig201 = None
|
self.fig201 = None
|
||||||
self.fig202 = None
|
self.fig202 = None
|
||||||
@@ -81,16 +81,12 @@ class OMNYGuiTools:
|
|||||||
pass
|
pass
|
||||||
text = (
|
text = (
|
||||||
"<pre>"
|
"<pre>"
|
||||||
+ " ,o888888o. ,8. ,8. b. 8 `8.`8888. ,8' \n"
|
+ "██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ██╗██╗ ██╗\n"
|
||||||
+ " . 8888 `88. ,888. ,888. 888o. 8 `8.`8888. ,8' \n"
|
+ "██╔══██╗██╔════╝██╔════╝ ██╔═══██╗████╗ ████║████╗ ██║╚██╗ ██╔╝\n"
|
||||||
+ ",8 8888 `8b .`8888. .`8888. Y88888o. 8 `8.`8888. ,8' \n"
|
+ "██████╔╝█████╗ ██║ ██║ ██║██╔████╔██║██╔██╗ ██║ ╚████╔╝ \n"
|
||||||
+ "88 8888 `8b ,8.`8888. ,8.`8888. .`Y888888o. 8 `8.`8888.,8' \n"
|
+ "██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╗██║ ╚██╔╝ \n"
|
||||||
+ "88 8888 88 ,8'8.`8888,8^8.`8888. 8o. `Y888888o. 8 `8.`88888' \n"
|
+ "██████╔╝███████╗╚██████╗ ╚██████╔╝██║ ╚═╝ ██║██║ ╚████║ ██║ \n"
|
||||||
+ "88 8888 88 ,8' `8.`8888' `8.`8888. 8`Y8o. `Y88888o8 `8. 8888 \n"
|
+ "╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ \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"
|
|
||||||
+ "</pre>"
|
+ "</pre>"
|
||||||
)
|
)
|
||||||
self.idle_text_box.set_html_text(text)
|
self.idle_text_box.set_html_text(text)
|
||||||
@@ -137,7 +133,9 @@ class OMNYGuiTools:
|
|||||||
if self.progressbar is None:
|
if self.progressbar is None:
|
||||||
self.omnygui_remove_all_docks()
|
self.omnygui_remove_all_docks()
|
||||||
# Add a new dock with a RingProgressBar widget
|
# 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
|
# Customize the size of the progress ring
|
||||||
self.progressbar.set_line_widths(20)
|
self.progressbar.set_line_widths(20)
|
||||||
# Disable automatic updates and manually set the self.progressbar value
|
# Disable automatic updates and manually set the self.progressbar value
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ logger = bec_logger.logger
|
|||||||
if builtins.__dict__.get("bec") is not None:
|
if builtins.__dict__.get("bec") is not None:
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
class OMNYInitError(Exception):
|
class OMNYInitError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -851,18 +852,6 @@ class OMNY(
|
|||||||
def sample_name(self):
|
def sample_name(self):
|
||||||
return dev.omny_samples.get_sample_name_in_samplestage()
|
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):
|
def write_to_scilog(self, content, tags: list = None):
|
||||||
try:
|
try:
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
@@ -1287,7 +1276,6 @@ class OMNY(
|
|||||||
f"{str(datetime.datetime.now())}: omny scan projection at angle {angle}, scan"
|
f"{str(datetime.datetime.now())}: omny scan projection at angle {angle}, scan"
|
||||||
f" number {bec.queue.next_scan_number}.\n"
|
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])
|
# self.write_to_scilog(log_message, ["BEC_scans", self.sample_name])
|
||||||
scans.omny_fermat_scan(
|
scans.omny_fermat_scan(
|
||||||
fovx=self.fovx,
|
fovx=self.fovx,
|
||||||
|
|||||||
@@ -1,25 +1,36 @@
|
|||||||
import time
|
import builtins
|
||||||
import numpy as np
|
import datetime
|
||||||
import sys
|
|
||||||
import termios
|
|
||||||
import tty
|
|
||||||
import fcntl
|
import fcntl
|
||||||
import os
|
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 import box
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
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:
|
if builtins.__dict__.get("bec") is not None:
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
|
||||||
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
def umvr(*args):
|
||||||
|
return scans.umv(*args, relative=True)
|
||||||
|
|
||||||
class OMNYToolsError(Exception):
|
class OMNYToolsError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -108,24 +119,20 @@ class OMNYTools:
|
|||||||
next1, next2 = sys.stdin.read(2)
|
next1, next2 = sys.stdin.read(2)
|
||||||
if next1 == "[":
|
if next1 == "[":
|
||||||
if next2 == "A":
|
if next2 == "A":
|
||||||
# print("up")
|
|
||||||
if dev2 != "none":
|
if dev2 != "none":
|
||||||
umvr(dev2, step2)
|
umvr(dev2, step2)
|
||||||
if special_command != "none":
|
if special_command != "none":
|
||||||
special_command()
|
special_command()
|
||||||
elif next2 == "B":
|
elif next2 == "B":
|
||||||
# print(" down")
|
|
||||||
if dev2 != "none":
|
if dev2 != "none":
|
||||||
umvr(dev2, -step2)
|
umvr(dev2, -step2)
|
||||||
if special_command != "none":
|
if special_command != "none":
|
||||||
special_command()
|
special_command()
|
||||||
elif next2 == "C":
|
elif next2 == "C":
|
||||||
# print("right")
|
|
||||||
umvr(dev1, step1)
|
umvr(dev1, step1)
|
||||||
if special_command != "none":
|
if special_command != "none":
|
||||||
special_command()
|
special_command()
|
||||||
elif next2 == "D":
|
elif next2 == "D":
|
||||||
# print("left")
|
|
||||||
umvr(dev1, -step1)
|
umvr(dev1, -step1)
|
||||||
if special_command != "none":
|
if special_command != "none":
|
||||||
special_command()
|
special_command()
|
||||||
@@ -141,13 +148,135 @@ class OMNYTools:
|
|||||||
step2 = step2 / 2
|
step2 = step2 / 2
|
||||||
print(f"\rHalf step size. New step size: {step1}, {step2}\r")
|
print(f"\rHalf step size. New step size: {step1}, {step2}\r")
|
||||||
except IOError:
|
except IOError:
|
||||||
# No input available, keep looping
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Sleep for a short period to avoid high CPU usage
|
|
||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Restore the terminal to its original state
|
# Restore the terminal to its original state
|
||||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
|
||||||
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)
|
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")
|
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=10000")
|
||||||
|
|
||||||
def oeye_out(self):
|
def oeye_out(self):
|
||||||
fshclose()
|
dev.omnyfsh.fshclose()
|
||||||
if self.OMNYTools.yesno("Did you move in the optics?"):
|
if self.OMNYTools.yesno("Did you move in the optics?"):
|
||||||
umv(dev.oeyez, -2)
|
umv(dev.oeyez, -2)
|
||||||
self._oeyey_mv(-60.3)
|
self._oeyey_mv(-60.3)
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fsh
|
|||||||
if builtins.__dict__.get("bec") is not None:
|
if builtins.__dict__.get("bec") is not None:
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
|
|
||||||
class OMNYTransferError(Exception):
|
class OMNYTransferError(Exception):
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ logger = bec_logger.logger
|
|||||||
# import builtins to avoid linter errors
|
# import builtins to avoid linter errors
|
||||||
bec = builtins.__dict__.get("bec")
|
bec = builtins.__dict__.get("bec")
|
||||||
dev = builtins.__dict__.get("dev")
|
dev = builtins.__dict__.get("dev")
|
||||||
umv = builtins.__dict__.get("umv")
|
scans = builtins.__dict__.get("scans")
|
||||||
umvr = builtins.__dict__.get("umvr")
|
|
||||||
|
def umv(*args):
|
||||||
|
return scans.umv(*args, relative=False)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bec_ipython_client.plugins.omny import OMNY
|
from bec_ipython_client.plugins.omny import OMNY
|
||||||
|
|||||||
@@ -30,29 +30,74 @@ logger = bec_logger.logger
|
|||||||
|
|
||||||
logger.info("Using the cSAXS startup script.")
|
logger.info("Using the cSAXS startup script.")
|
||||||
|
|
||||||
# pylint: disable=import-error
|
|
||||||
_args = _main_dict["args"]
|
|
||||||
|
|
||||||
_session_name = "cSAXS"
|
|
||||||
if _args.session.lower() == "lamni":
|
|
||||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import *
|
|
||||||
from csaxs_bec.bec_ipython_client.plugins.LamNI import *
|
|
||||||
|
|
||||||
_session_name = "LamNI"
|
|
||||||
lamni = LamNI(bec)
|
|
||||||
logger.success("LamNI session loaded.")
|
|
||||||
|
|
||||||
elif _args.session.lower() == "csaxs":
|
|
||||||
print("Loading cSAXS session")
|
|
||||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import *
|
|
||||||
|
|
||||||
logger.success("cSAXS session loaded.")
|
|
||||||
|
|
||||||
from csaxs_bec.bec_ipython_client.plugins.tool_box.debug_tools import DebugTools
|
from csaxs_bec.bec_ipython_client.plugins.tool_box.debug_tools import DebugTools
|
||||||
|
|
||||||
debug = DebugTools()
|
debug = DebugTools()
|
||||||
logger.success("Debug tools loaded. Use 'debug' to access them.")
|
logger.success("Debug tools loaded. Use 'debug' to access them.")
|
||||||
|
|
||||||
|
# pylint: disable=import-error
|
||||||
|
_args = _main_dict["args"]
|
||||||
|
|
||||||
|
_session_name = "cSAXS"
|
||||||
|
|
||||||
|
print("Loading cSAXS session")
|
||||||
|
from csaxs_bec.bec_ipython_client.plugins.cSAXS.cSAXS import cSAXS
|
||||||
|
csaxs = cSAXS(bec)
|
||||||
|
logger.success("cSAXS session loaded.")
|
||||||
|
|
||||||
|
|
||||||
|
if _args.session.lower() == "lamni":
|
||||||
|
from csaxs_bec.bec_ipython_client.plugins.LamNI import LamNI
|
||||||
|
|
||||||
|
_session_name = "LamNI"
|
||||||
|
lamni = LamNI(bec)
|
||||||
|
logger.success("LamNI session loaded.")
|
||||||
|
print(r"""
|
||||||
|
██████╗ ███████╗ ██████╗ ██╗ █████╗ ███╗ ███╗███╗ ██╗██╗
|
||||||
|
██╔══██╗██╔════╝██╔════╝ ██║ ██╔══██╗████╗ ████║████╗ ██║██║
|
||||||
|
██████╔╝█████╗ ██║ ██║ ███████║██╔████╔██║██╔██╗ ██║██║
|
||||||
|
██╔══██╗██╔══╝ ██║ ██║ ██╔══██║██║╚██╔╝██║██║╚██╗██║██║
|
||||||
|
██████╔╝███████╗╚██████╗ ███████╗██║ ██║██║ ╚═╝ ██║██║ ╚████║██║
|
||||||
|
╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝
|
||||||
|
|
||||||
|
B E C L a m N I
|
||||||
|
""")
|
||||||
|
|
||||||
|
elif _args.session.lower() == "omny":
|
||||||
|
from csaxs_bec.bec_ipython_client.plugins.flomni import OMNY
|
||||||
|
|
||||||
|
_session_name = "OMNY"
|
||||||
|
omny = OMNY(bec)
|
||||||
|
logger.success("OMNY session loaded.")
|
||||||
|
print(r"""
|
||||||
|
██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ██╗██╗ ██╗
|
||||||
|
██╔══██╗██╔════╝██╔════╝ ██╔═══██╗████╗ ████║████╗ ██║╚██╗ ██╔╝
|
||||||
|
██████╔╝█████╗ ██║ ██║ ██║██╔████╔██║██╔██╗ ██║ ╚████╔╝
|
||||||
|
██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╗██║ ╚██╔╝
|
||||||
|
██████╔╝███████╗╚██████╗ ╚██████╔╝██║ ╚═╝ ██║██║ ╚████║ ██║
|
||||||
|
╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝
|
||||||
|
|
||||||
|
B E C O M N Y
|
||||||
|
""")
|
||||||
|
|
||||||
|
elif _args.session.lower() == "flomni":
|
||||||
|
from csaxs_bec.bec_ipython_client.plugins.flomni import Flomni
|
||||||
|
|
||||||
|
_session_name = "flomni"
|
||||||
|
flomni = Flomni(bec)
|
||||||
|
logger.success("flomni session loaded.")
|
||||||
|
print(r"""
|
||||||
|
██████╗ ███████╗ ██████╗ ███████╗██╗ ██████╗ ███╗ ███╗███╗ ██╗██╗
|
||||||
|
██╔══██╗██╔════╝██╔════╝ ██╔════╝██║ ██╔═══██╗████╗ ████║████╗ ██║██║
|
||||||
|
██████╔╝█████╗ ██║ █████╗ ██║ ██║ ██║██╔████╔██║██╔██╗ ██║██║
|
||||||
|
██╔══██╗██╔══╝ ██║ ██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╗██║██║
|
||||||
|
██████╔╝███████╗╚██████╗ ██║ ███████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚████║██║
|
||||||
|
╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝
|
||||||
|
|
||||||
|
B E C f l O M N I
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
# SETUP BEAMLINE INFO
|
# SETUP BEAMLINE INFO
|
||||||
from bec_ipython_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo
|
from bec_ipython_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo
|
||||||
@@ -66,3 +111,34 @@ bec._beamline_mixin._bl_info_register(OperatorInfo)
|
|||||||
# SETUP PROMPTS
|
# SETUP PROMPTS
|
||||||
bec._ip.prompts.session_name = _session_name
|
bec._ip.prompts.session_name = _session_name
|
||||||
bec._ip.prompts.status = 1
|
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 = {
|
_Widgets = {
|
||||||
"OmnyAlignment": "OmnyAlignment",
|
|
||||||
"XRayEye": "XRayEye",
|
"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):
|
class XRayEye(RPCBase):
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def active_roi(self) -> "BaseROI | None":
|
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.
|
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
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def user_message(self):
|
def user_message(self):
|
||||||
@@ -111,6 +38,30 @@ class XRayEye(RPCBase):
|
|||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def on_live_view_enabled(self, enabled: "bool"):
|
||||||
|
"""
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
@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_call
|
||||||
|
def enable_submit_button(self, enable: "bool"):
|
||||||
|
"""
|
||||||
|
Enable/disable submit button.
|
||||||
|
Args:
|
||||||
|
enable(int): -1 disable else enable
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def sample_name(self):
|
def sample_name(self):
|
||||||
@@ -139,6 +90,18 @@ class XRayEye(RPCBase):
|
|||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def switch_tab(self, tab: "str"):
|
||||||
|
"""
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def submit_fit_array(self, fit_array):
|
||||||
|
"""
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class XRayEye2DControl(RPCBase):
|
class XRayEye2DControl(RPCBase):
|
||||||
@rpc_call
|
@rpc_call
|
||||||
@@ -146,3 +109,15 @@ class XRayEye2DControl(RPCBase):
|
|||||||
"""
|
"""
|
||||||
Cleanup the BECConnector
|
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()
|
|
||||||
@@ -5,6 +5,7 @@ from bec_lib.endpoints import MessageEndpoints
|
|||||||
from bec_qthemes import material_icon
|
from bec_qthemes import material_icon
|
||||||
from bec_widgets import BECWidget, SafeProperty, SafeSlot
|
from bec_widgets import BECWidget, SafeProperty, SafeSlot
|
||||||
from bec_widgets.widgets.plots.image.image import Image
|
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.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
||||||
from bec_widgets.widgets.plots.roi.image_roi import BaseROI, CircularROI, RectangularROI
|
from bec_widgets.widgets.plots.roi.image_roi import BaseROI, CircularROI, RectangularROI
|
||||||
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||||
@@ -21,6 +22,8 @@ from qtpy.QtWidgets import (
|
|||||||
QToolButton,
|
QToolButton,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
|
QTextEdit,
|
||||||
|
QTabWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
@@ -41,38 +44,38 @@ class XRayEye2DControl(BECWidget, QWidget):
|
|||||||
""")
|
""")
|
||||||
# Up
|
# Up
|
||||||
self.move_up_button = QToolButton(parent=self)
|
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)
|
self.root_layout.addWidget(self.move_up_button, 0, 2)
|
||||||
# Up tweak button
|
# Up tweak button
|
||||||
self.move_up_tweak_button = QToolButton(parent=self)
|
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)
|
self.root_layout.addWidget(self.move_up_tweak_button, 1, 2)
|
||||||
|
|
||||||
# Left
|
# Left
|
||||||
self.move_left_button = QToolButton(parent=self)
|
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)
|
self.root_layout.addWidget(self.move_left_button, 2, 0)
|
||||||
# Left tweak button
|
# Left tweak button
|
||||||
self.move_left_tweak_button = QToolButton(parent=self)
|
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)
|
self.root_layout.addWidget(self.move_left_tweak_button, 2, 1)
|
||||||
|
|
||||||
# Right
|
# Right
|
||||||
self.move_right_button = QToolButton(parent=self)
|
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)
|
self.root_layout.addWidget(self.move_right_button, 2, 4)
|
||||||
# Right tweak button
|
# Right tweak button
|
||||||
self.move_right_tweak_button = QToolButton(parent=self)
|
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)
|
self.root_layout.addWidget(self.move_right_tweak_button, 2, 3)
|
||||||
|
|
||||||
# Down
|
# Down
|
||||||
self.move_down_button = QToolButton(parent=self)
|
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)
|
self.root_layout.addWidget(self.move_down_button, 4, 2)
|
||||||
# Down tweak button
|
# Down tweak button
|
||||||
self.move_down_tweak_button = QToolButton(parent=self)
|
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)
|
self.root_layout.addWidget(self.move_down_tweak_button, 3, 2)
|
||||||
|
|
||||||
# Connections
|
# Connections
|
||||||
@@ -124,40 +127,69 @@ class XRayEye2DControl(BECWidget, QWidget):
|
|||||||
|
|
||||||
|
|
||||||
class XRayEye(BECWidget, QWidget):
|
class XRayEye(BECWidget, QWidget):
|
||||||
USER_ACCESS = ["active_roi", "enable_live_view", "enable_live_view.setter", "user_message", "user_message.setter",
|
USER_ACCESS = [
|
||||||
"sample_name", "sample_name.setter", "enable_move_buttons", "enable_move_buttons.setter"]
|
"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
|
PLUGIN = True
|
||||||
|
|
||||||
def __init__(self, parent=None, **kwargs):
|
def __init__(self, parent=None, **kwargs):
|
||||||
super().__init__(parent=parent, **kwargs)
|
super().__init__(parent=parent, **kwargs)
|
||||||
|
self._connected_motor = None
|
||||||
self.get_bec_shortcuts()
|
self.get_bec_shortcuts()
|
||||||
|
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
self._make_connections()
|
self._make_connections()
|
||||||
|
|
||||||
# Connection to redis endpoints
|
# 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.connect_motors()
|
||||||
self.resize(800, 600)
|
self.resize(800, 600)
|
||||||
QTimer.singleShot(0, self._init_gui_trigger)
|
QTimer.singleShot(0, self._init_gui_trigger)
|
||||||
|
|
||||||
def _init_ui(self):
|
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.alignment_tab = QWidget(parent=self)
|
||||||
self.image.enable_toolbar = False # Disable default toolbar to not allow to user set anything
|
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.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
|
# 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 = QVBoxLayout(self.control_panel)
|
||||||
self.control_panel_layout.setContentsMargins(0, 0, 0, 0)
|
self.control_panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.control_panel_layout.setSpacing(10)
|
self.control_panel_layout.setSpacing(10)
|
||||||
|
|
||||||
# ROI toolbar + Live toggle (header row)
|
# ROI toolbar + Live toggle (header row)
|
||||||
self.roi_manager = ROIPropertyTree(parent=self, image_widget=self.image, compact=True,
|
self.roi_manager = ROIPropertyTree(
|
||||||
compact_orientation="horizontal")
|
parent=self, image_widget=self.image, compact=True, compact_orientation="horizontal"
|
||||||
|
)
|
||||||
header_row = QHBoxLayout()
|
header_row = QHBoxLayout()
|
||||||
header_row.setContentsMargins(0, 0, 0, 0)
|
header_row.setContentsMargins(0, 0, 0, 0)
|
||||||
header_row.setSpacing(8)
|
header_row.setSpacing(8)
|
||||||
@@ -166,16 +198,36 @@ class XRayEye(BECWidget, QWidget):
|
|||||||
self.live_preview_label = QLabel("Live Preview", parent=self)
|
self.live_preview_label = QLabel("Live Preview", parent=self)
|
||||||
self.live_preview_toggle = ToggleSwitch(parent=self)
|
self.live_preview_toggle = ToggleSwitch(parent=self)
|
||||||
self.live_preview_toggle.checked = False
|
self.live_preview_toggle.checked = False
|
||||||
header_row.addWidget(self.live_preview_label, 0, Qt.AlignVCenter)
|
header_row.addWidget(self.live_preview_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||||
header_row.addWidget(self.live_preview_toggle, 0, Qt.AlignVCenter)
|
header_row.addWidget(self.live_preview_toggle, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||||
self.control_panel_layout.addLayout(header_row)
|
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
|
# separator
|
||||||
self.control_panel_layout.addWidget(self._create_separator())
|
self.control_panel_layout.addWidget(self._create_separator())
|
||||||
|
|
||||||
# 2D Positioner (fixed size)
|
# 2D Positioner (fixed size)
|
||||||
self.motor_control_2d = XRayEye2DControl(parent=self)
|
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
|
# separator
|
||||||
self.control_panel_layout.addWidget(self._create_separator())
|
self.control_panel_layout.addWidget(self._create_separator())
|
||||||
@@ -190,9 +242,8 @@ class XRayEye(BECWidget, QWidget):
|
|||||||
# Submit button
|
# Submit button
|
||||||
self.submit_button = QPushButton("Submit", parent=self)
|
self.submit_button = QPushButton("Submit", parent=self)
|
||||||
# Add to layout form
|
# 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(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)
|
step_size_form.addWidget(self.submit_button, 2, 0, 1, 2)
|
||||||
|
|
||||||
# Add form to control panel
|
# Add form to control panel
|
||||||
@@ -207,7 +258,8 @@ class XRayEye(BECWidget, QWidget):
|
|||||||
self.sample_name_line_edit.setReadOnly(True)
|
self.sample_name_line_edit.setReadOnly(True)
|
||||||
form.addWidget(QLabel("Sample", parent=self), 0, 0)
|
form.addWidget(QLabel("Sample", parent=self), 0, 0)
|
||||||
form.addWidget(self.sample_name_line_edit, 0, 1)
|
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)
|
self.message_line_edit.setReadOnly(True)
|
||||||
form.addWidget(QLabel("Message", parent=self), 1, 0)
|
form.addWidget(QLabel("Message", parent=self), 1, 0)
|
||||||
form.addWidget(self.message_line_edit, 1, 1)
|
form.addWidget(self.message_line_edit, 1, 1)
|
||||||
@@ -217,12 +269,57 @@ class XRayEye(BECWidget, QWidget):
|
|||||||
self.control_panel.adjustSize()
|
self.control_panel.adjustSize()
|
||||||
p_hint = self.control_panel.sizeHint()
|
p_hint = self.control_panel.sizeHint()
|
||||||
self.control_panel.setFixedWidth(p_hint.width())
|
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)
|
# Core Layout: image (expanding) | control panel (fixed)
|
||||||
self.core_layout.addWidget(self.image)
|
self.core_layout.addWidget(self.image)
|
||||||
self.core_layout.addWidget(self.control_panel)
|
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):
|
def _make_connections(self):
|
||||||
# Fetch initial state
|
# Fetch initial state
|
||||||
self.on_live_view_enabled(True)
|
self.on_live_view_enabled(True)
|
||||||
@@ -230,38 +327,44 @@ class XRayEye(BECWidget, QWidget):
|
|||||||
|
|
||||||
# Make connections
|
# Make connections
|
||||||
self.live_preview_toggle.enabled.connect(self.on_live_view_enabled)
|
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)
|
self.submit_button.clicked.connect(self.submit)
|
||||||
|
|
||||||
def _create_separator(self):
|
def _create_separator(self):
|
||||||
sep = QFrame(parent=self)
|
sep = QFrame(parent=self)
|
||||||
sep.setFrameShape(QFrame.HLine)
|
sep.setFrameShape(QFrame.Shape.HLine)
|
||||||
sep.setFrameShadow(QFrame.Sunken)
|
sep.setFrameShadow(QFrame.Shadow.Sunken)
|
||||||
sep.setLineWidth(1)
|
sep.setLineWidth(1)
|
||||||
return sep
|
return sep
|
||||||
|
|
||||||
def _init_gui_trigger(self):
|
def _init_gui_trigger(self):
|
||||||
self.dev.omny_xray_gui.read()
|
self.dev.omny_xray_gui.read()
|
||||||
|
self.dev.omnyfsh.read()
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Device Connection logic
|
# Device Connection logic
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
def connect_motors(self):
|
def connect_motors(self):
|
||||||
""" Checks one of the possible motors for flomni, omny and lamni setup."""
|
"""Checks one of the possible motors for flomni, omny and lamni setup."""
|
||||||
possible_motors = ['osamroy', 'lsamrot', 'fsamroy']
|
possible_motors = ["osamroy", "lsamrot", "fsamroy"]
|
||||||
|
|
||||||
for motor in possible_motors:
|
for motor in possible_motors:
|
||||||
if motor in self.dev:
|
if motor in self.dev:
|
||||||
self.bec_dispatcher.connect_slot(self.on_tomo_angle_readback, MessageEndpoints.device_readback(motor))
|
self.bec_dispatcher.connect_slot(
|
||||||
logger.info(f"Succesfully connected to {motor}")
|
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
|
# Properties ported from the original OmnyAlignment, can be adjusted as needed
|
||||||
################################################################################
|
################################################################################
|
||||||
@SafeProperty(str)
|
@SafeProperty(str)
|
||||||
def user_message(self):
|
def user_message(self):
|
||||||
return self.message_line_edit.text()
|
return self.message_line_edit.toPlainText()
|
||||||
|
|
||||||
@user_message.setter
|
@user_message.setter
|
||||||
def user_message(self, message: str):
|
def user_message(self, message: str):
|
||||||
@@ -291,6 +394,13 @@ class XRayEye(BECWidget, QWidget):
|
|||||||
# Slots ported from the original OmnyAlignment, can be adjusted as needed
|
# Slots ported from the original OmnyAlignment, can be adjusted as needed
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
|
@SafeSlot(str)
|
||||||
|
def switch_tab(self, tab: str):
|
||||||
|
if tab == "fit":
|
||||||
|
self.tab_widget.setCurrentIndex(1)
|
||||||
|
else:
|
||||||
|
self.tab_widget.setCurrentIndex(0)
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def get_roi_coordinates(self) -> dict | None:
|
def get_roi_coordinates(self) -> dict | None:
|
||||||
"""Get the coordinates of the currently active ROI."""
|
"""Get the coordinates of the currently active ROI."""
|
||||||
@@ -307,14 +417,48 @@ class XRayEye(BECWidget, QWidget):
|
|||||||
self.live_preview_toggle.blockSignals(True)
|
self.live_preview_toggle.blockSignals(True)
|
||||||
if enabled:
|
if enabled:
|
||||||
self.live_preview_toggle.checked = 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)
|
self.live_preview_toggle.blockSignals(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.image.disconnect_monitor(CAMERA)
|
self.image.disconnect_monitor(CAMERA[0], CAMERA[1])
|
||||||
self.live_preview_toggle.checked = enabled
|
self.live_preview_toggle.checked = enabled
|
||||||
self.live_preview_toggle.blockSignals(False)
|
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)
|
@SafeSlot(bool, bool)
|
||||||
def on_motors_enable(self, x_enable: bool, y_enable: bool):
|
def on_motors_enable(self, x_enable: bool, y_enable: bool):
|
||||||
"""
|
"""
|
||||||
@@ -327,98 +471,114 @@ class XRayEye(BECWidget, QWidget):
|
|||||||
self.motor_control_2d.enable_controls_hor(x_enable)
|
self.motor_control_2d.enable_controls_hor(x_enable)
|
||||||
self.motor_control_2d.enable_controls_ver(y_enable)
|
self.motor_control_2d.enable_controls_ver(y_enable)
|
||||||
|
|
||||||
@SafeSlot(int)
|
@SafeSlot(bool)
|
||||||
def enable_submit_button(self, enable: int):
|
def enable_submit_button(self, enable: bool):
|
||||||
"""
|
"""
|
||||||
Enable/disable submit button.
|
Enable/disable submit button.
|
||||||
Args:
|
Args:
|
||||||
enable(int): -1 disable else enable
|
enable(int): -1 disable else enable
|
||||||
"""
|
"""
|
||||||
if enable == -1:
|
if enable:
|
||||||
self.submit_button.setEnabled(False)
|
|
||||||
else:
|
|
||||||
self.submit_button.setEnabled(True)
|
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)
|
@SafeSlot(bool, bool)
|
||||||
def on_tomo_angle_readback(self, data: dict, meta: dict):
|
def on_tomo_angle_readback(self, data: dict, meta: dict):
|
||||||
#TODO implement if needed
|
# TODO implement if needed
|
||||||
print(f"data: {data}")
|
print(f"data: {data}")
|
||||||
print(f"meta: {meta}")
|
print(f"meta: {meta}")
|
||||||
|
|
||||||
@SafeSlot(dict, dict)
|
@SafeSlot()
|
||||||
def device_updates(self, data: dict, meta: dict):
|
def submit_fit_array(self, fit_array):
|
||||||
"""
|
self.tab_widget.setCurrentIndex(1)
|
||||||
Slot to handle device updates from omny_xray_gui device.
|
# self.fix_x.title = " got fit array"
|
||||||
|
print(f"got fit array {fit_array}")
|
||||||
Args:
|
self.waveform_x.curves[0].set_data(x=fit_array[0], y=fit_array[1])
|
||||||
data(dict): data from device
|
self.waveform_y.curves[0].set_data(x=fit_array[0], y=fit_array[2])
|
||||||
meta(dict): metadata from device
|
# 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])
|
||||||
|
|
||||||
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()
|
@SafeSlot()
|
||||||
def submit(self):
|
def submit(self):
|
||||||
"""Execute submit action by submit button."""
|
"""Execute submit action by submit button."""
|
||||||
if self.roi_manager.single_active_roi is None:
|
self.submit_button.blockSignals(True)
|
||||||
logger.warning("No active ROI")
|
try:
|
||||||
return
|
if self.roi_manager.single_active_roi is None:
|
||||||
roi_coordinates = self.roi_manager.single_active_roi.get_coordinates()
|
logger.warning("No active ROI")
|
||||||
roi_center_x = roi_coordinates['center_x']
|
return
|
||||||
roi_center_y = roi_coordinates['center_y']
|
roi_coordinates = self.roi_manager.single_active_roi.get_coordinates()
|
||||||
# Case of rectangular ROI
|
roi_center_x = roi_coordinates["center_x"]
|
||||||
if isinstance(self.roi_manager.single_active_roi, RectangularROI):
|
roi_center_y = roi_coordinates["center_y"]
|
||||||
roi_width = roi_coordinates['width']
|
# Case of rectangular ROI
|
||||||
roi_height = roi_coordinates['height']
|
if isinstance(self.roi_manager.single_active_roi, RectangularROI):
|
||||||
elif isinstance(self.roi_manager.single_active_roi, CircularROI):
|
roi_width = roi_coordinates["width"]
|
||||||
roi_width = roi_coordinates['diameter']
|
roi_height = roi_coordinates["height"]
|
||||||
roi_height = roi_coordinates['radius']
|
elif isinstance(self.roi_manager.single_active_roi, CircularROI):
|
||||||
else:
|
roi_width = roi_coordinates["diameter"]
|
||||||
logger.warning("Unsupported ROI type for submit action.")
|
roi_height = roi_coordinates["radius"]
|
||||||
return
|
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
|
||||||
# submit roi coordinates
|
step = int(self.dev.omny_xray_gui.step.read().get("omny_xray_gui_step").get("value"))
|
||||||
step = int(self.dev.omny_xray_gui.step.read().get("omny_xray_gui_step").get('value'))
|
|
||||||
|
|
||||||
xval_x = getattr(self.dev.omny_xray_gui.xval_x, f"xval_x_{step}").set(roi_center_x)
|
getattr(self.dev.omny_xray_gui, 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)
|
getattr(self.dev.omny_xray_gui, 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)
|
getattr(self.dev.omny_xray_gui, f"width_x_{step}").set(roi_width)
|
||||||
width_y = getattr(self.dev.omny_xray_gui.width_y, f"width_y_{step}").set(roi_height)
|
getattr(self.dev.omny_xray_gui, f"width_y_{step}").set(roi_height)
|
||||||
self.dev.omny_xray_gui.submit.set(1)
|
self.dev.omny_xray_gui.submit.set(1)
|
||||||
|
finally:
|
||||||
|
self.submit_button.blockSignals(False)
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Cleanup connections on widget close -> disconnect slots and stop live mode of camera."""
|
"""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"))
|
if self._connected_motor is not None:
|
||||||
getattr(self.dev,CAMERA[0]).live_mode = False
|
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()
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
|
from bec_widgets.utils import BECDispatcher
|
||||||
|
from bec_widgets.utils.colors import apply_theme
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
apply_theme("light")
|
||||||
|
dispatcher = BECDispatcher(gui_id="xray")
|
||||||
win = XRayEye()
|
win = XRayEye()
|
||||||
|
|
||||||
win.resize(1000, 800)
|
win.resize(1000, 800)
|
||||||
|
|||||||
@@ -9,27 +9,27 @@ eiger_1_5:
|
|||||||
readoutPriority: async
|
readoutPriority: async
|
||||||
softwareTrigger: False
|
softwareTrigger: False
|
||||||
|
|
||||||
eiger_9:
|
# eiger_9:
|
||||||
description: Eiger 9M detector
|
# description: Eiger 9M detector
|
||||||
deviceClass: csaxs_bec.devices.jungfraujoch.eiger_9m.Eiger9M
|
# deviceClass: csaxs_bec.devices.jungfraujoch.eiger_9m.Eiger9M
|
||||||
deviceConfig:
|
# deviceConfig:
|
||||||
detector_distance: 100
|
# detector_distance: 100
|
||||||
beam_center: [0, 0]
|
# beam_center: [0, 0]
|
||||||
onFailure: raise
|
# onFailure: raise
|
||||||
enabled: true
|
# enabled: true
|
||||||
readoutPriority: async
|
# readoutPriority: async
|
||||||
softwareTrigger: False
|
# softwareTrigger: False
|
||||||
|
|
||||||
ids_cam:
|
# ids_cam:
|
||||||
description: IDS camera for live image acquisition
|
# description: IDS camera for live image acquisition
|
||||||
deviceClass: csaxs_bec.devices.ids_cameras.IDSCamera
|
# deviceClass: csaxs_bec.devices.ids_cameras.IDSCamera
|
||||||
deviceConfig:
|
# deviceConfig:
|
||||||
camera_id: 201
|
# camera_id: 201
|
||||||
bits_per_pixel: 24
|
# bits_per_pixel: 24
|
||||||
m_n_colormode: 1
|
# m_n_colormode: 1
|
||||||
live_mode: True
|
# live_mode: True
|
||||||
onFailure: raise
|
# onFailure: raise
|
||||||
enabled: true
|
# enabled: true
|
||||||
readoutPriority: async
|
# readoutPriority: async
|
||||||
softwareTrigger: True
|
# softwareTrigger: True
|
||||||
|
|
||||||
|
|||||||
25
csaxs_bec/device_configs/bl_general.yaml
Normal file
25
csaxs_bec/device_configs/bl_general.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
############################################################
|
||||||
|
##################### EPS ##################################
|
||||||
|
############################################################
|
||||||
|
x12saEPS:
|
||||||
|
description: X12SA EPS info and control
|
||||||
|
deviceClass: csaxs_bec.devices.epics.eps.EPS
|
||||||
|
deviceConfig: {}
|
||||||
|
enabled: true
|
||||||
|
onFailure: buffer
|
||||||
|
readOnly: false
|
||||||
|
readoutPriority: baseline
|
||||||
|
############################################################
|
||||||
|
##################### GalilRIO #############################
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
galilrioesxbox:
|
||||||
|
description: Galil RIO for remote gain switching and slow reading ES XBox
|
||||||
|
deviceClass: csaxs_bec.devices.omny.galil.galil_rio.GalilRIO
|
||||||
|
deviceConfig:
|
||||||
|
host: galilrioesxbox.psi.ch
|
||||||
|
enabled: true
|
||||||
|
onFailure: raise
|
||||||
|
readOnly: false
|
||||||
|
readoutPriority: baseline
|
||||||
|
connectionTimeout: 20
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# This is the main configuration file that is
|
# This is the main configuration file that is
|
||||||
# commented or uncommented according to the type of experiment
|
# commented or uncommented according to the type of experiment
|
||||||
|
|
||||||
optics:
|
# optics:
|
||||||
- !include ./bl_optics_hutch.yaml
|
# - !include ./bl_optics_hutch.yaml
|
||||||
|
|
||||||
frontend:
|
# frontend:
|
||||||
- !include ./bl_frontend.yaml
|
# - !include ./bl_frontend.yaml
|
||||||
|
|
||||||
endstation:
|
endstation:
|
||||||
- !include ./bl_endstation.yaml
|
- !include ./bl_endstation.yaml
|
||||||
@@ -16,8 +16,8 @@ detectors:
|
|||||||
#sastt:
|
#sastt:
|
||||||
# - !include ./sastt.yaml
|
# - !include ./sastt.yaml
|
||||||
|
|
||||||
#flomni:
|
flomni:
|
||||||
# - !include ./ptycho_flomni.yaml
|
- !include ./ptycho_flomni.yaml
|
||||||
|
|
||||||
#omny:
|
#omny:
|
||||||
# - !include ./ptycho_omny.yaml
|
# - !include ./ptycho_omny.yaml
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ ftransy:
|
|||||||
readoutPriority: baseline
|
readoutPriority: baseline
|
||||||
connectionTimeout: 20
|
connectionTimeout: 20
|
||||||
userParameter:
|
userParameter:
|
||||||
sensor_voltage: -2.4
|
sensor_voltage: -1.1
|
||||||
ftransz:
|
ftransz:
|
||||||
description: Sample transer Z
|
description: Sample transer Z
|
||||||
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
|
||||||
@@ -344,6 +344,9 @@ rtx:
|
|||||||
description: flomni rt
|
description: flomni rt
|
||||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
||||||
deviceConfig:
|
deviceConfig:
|
||||||
|
limits:
|
||||||
|
- -200
|
||||||
|
- 200
|
||||||
axis_Id: A
|
axis_Id: A
|
||||||
host: mpc2844.psi.ch
|
host: mpc2844.psi.ch
|
||||||
port: 2222
|
port: 2222
|
||||||
@@ -361,6 +364,9 @@ rty:
|
|||||||
description: flomni rt
|
description: flomni rt
|
||||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
||||||
deviceConfig:
|
deviceConfig:
|
||||||
|
limits:
|
||||||
|
- -100
|
||||||
|
- 100
|
||||||
axis_Id: B
|
axis_Id: B
|
||||||
host: mpc2844.psi.ch
|
host: mpc2844.psi.ch
|
||||||
port: 2222
|
port: 2222
|
||||||
@@ -376,6 +382,9 @@ rtz:
|
|||||||
description: flomni rt
|
description: flomni rt
|
||||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
|
||||||
deviceConfig:
|
deviceConfig:
|
||||||
|
limits:
|
||||||
|
- -100
|
||||||
|
- 100
|
||||||
axis_Id: C
|
axis_Id: C
|
||||||
host: mpc2844.psi.ch
|
host: mpc2844.psi.ch
|
||||||
port: 2222
|
port: 2222
|
||||||
@@ -435,9 +444,9 @@ cam_xeye:
|
|||||||
# deviceConfig:
|
# deviceConfig:
|
||||||
# camera_id: 203
|
# camera_id: 203
|
||||||
# bits_per_pixel: 24
|
# bits_per_pixel: 24
|
||||||
# num_rotation_90: 3
|
# num_rotation_90: 2
|
||||||
# transpose: false
|
# transpose: false
|
||||||
# force_monochrome: true
|
# force_monochrome: false
|
||||||
# m_n_colormode: 1
|
# m_n_colormode: 1
|
||||||
# enabled: true
|
# enabled: true
|
||||||
# onFailure: buffer
|
# onFailure: buffer
|
||||||
@@ -471,8 +480,8 @@ omnyfsh:
|
|||||||
#################### GUI Signals ###########################
|
#################### GUI Signals ###########################
|
||||||
############################################################
|
############################################################
|
||||||
omny_xray_gui:
|
omny_xray_gui:
|
||||||
description: Gui Epics signals
|
description: Gui signals
|
||||||
deviceClass: csaxs_bec.devices.omny.xray_epics_gui.OMNYXRayEpicsGUI
|
deviceClass: csaxs_bec.devices.omny.xray_epics_gui.OMNYXRayAlignGUI
|
||||||
deviceConfig: {}
|
deviceConfig: {}
|
||||||
enabled: true
|
enabled: true
|
||||||
onFailure: buffer
|
onFailure: buffer
|
||||||
@@ -487,3 +496,24 @@ calculated_signal:
|
|||||||
enabled: true
|
enabled: true
|
||||||
readOnly: false
|
readOnly: false
|
||||||
readoutPriority: baseline
|
readoutPriority: baseline
|
||||||
|
|
||||||
|
############################################################
|
||||||
|
#################### OMNY Pandabox #########################
|
||||||
|
############################################################
|
||||||
|
omny_panda:
|
||||||
|
readoutPriority: async
|
||||||
|
deviceClass: csaxs_bec.devices.panda_box.panda_box_omny.PandaBoxOMNY
|
||||||
|
deviceConfig:
|
||||||
|
host: omny-panda.psi.ch
|
||||||
|
signal_alias:
|
||||||
|
FMC_IN.VAL1.Min: cap_voltage_fzp_y_min
|
||||||
|
FMC_IN.VAL1.Max: cap_voltage_fzp_y_max
|
||||||
|
FMC_IN.VAL1.Mean: cap_voltage_fzp_y_mean
|
||||||
|
FMC_IN.VAL2.Min: cap_voltage_fzp_x_min
|
||||||
|
FMC_IN.VAL2.Max: cap_voltage_fzp_x_max
|
||||||
|
FMC_IN.VAL2.Mean: cap_voltage_fzp_x_mean
|
||||||
|
deviceTags:
|
||||||
|
- detector
|
||||||
|
enabled: true
|
||||||
|
readOnly: false
|
||||||
|
softwareTrigger: false
|
||||||
|
|||||||
@@ -271,4 +271,20 @@ rty:
|
|||||||
enabled: true
|
enabled: true
|
||||||
readOnly: False
|
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),
|
("B", CHANNELREFERENCE.A),
|
||||||
("C", CHANNELREFERENCE.T0), # T0
|
("C", CHANNELREFERENCE.T0), # T0
|
||||||
("D", CHANNELREFERENCE.C),
|
("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
|
("F", CHANNELREFERENCE.E), # E + 1mu s
|
||||||
("G", CHANNELREFERENCE.T0),
|
("G", CHANNELREFERENCE.T0),
|
||||||
("H", CHANNELREFERENCE.G),
|
("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
|
# 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.
|
# 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)
|
self.burst_delay.put(0)
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
self.burst_count.put(2)
|
||||||
|
time.sleep(0.02)
|
||||||
self.burst_count.put(1)
|
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:
|
def keep_shutter_open_during_scan(self, open: True) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -291,17 +306,24 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
|||||||
# Burst Period DDG1
|
# Burst Period DDG1
|
||||||
# Set burst_period to shutter width
|
# Set burst_period to shutter width
|
||||||
# c/t0 + self._shutter_to_open_delay + exp_time * burst_count
|
# c/t0 + self._shutter_to_open_delay + exp_time * burst_count
|
||||||
shutter_width = (
|
# SHUTTER WIDTH timing consists of the delay for the shutter to open
|
||||||
self._shutter_to_open_delay + exp_time * frames_per_trigger
|
# + the exposure time * frames per trigger
|
||||||
) # Shutter starts closing at end of exposure
|
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:
|
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
|
# Trigger DDG2
|
||||||
# a = t0 + 2ms, b = a + 1us
|
# a = t0 + 2ms, b = a + 1us
|
||||||
# a has reference to t0, b has reference to a
|
# a has reference to t0, b has reference to a
|
||||||
# Add delay of self._shutter_to_open_delay to allow shutter to open
|
# AB is delayed by the shutter opening time, and the falling edge indicates the shutter has
|
||||||
self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=1e-6)
|
# 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
|
# Trigger shutter
|
||||||
# d = c/t0 + self._shutter_to_open_delay + exp_time * burst_count + 1ms
|
# d = c/t0 + self._shutter_to_open_delay + exp_time * burst_count + 1ms
|
||||||
@@ -309,7 +331,11 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
|||||||
# Shutter opens without delay at t0, closes after exp_time * burst_count + 2ms (self._shutter_to_open_delay)
|
# Shutter opens without delay at t0, closes after exp_time * burst_count + 2ms (self._shutter_to_open_delay)
|
||||||
self.set_delay_pairs(channel="cd", delay=0, width=shutter_width)
|
self.set_delay_pairs(channel="cd", delay=0, width=shutter_width)
|
||||||
|
|
||||||
self.set_delay_pairs(channel="gh", delay=self._shutter_to_open_delay, width=(shutter_width-self._shutter_to_open_delay))
|
self.set_delay_pairs(
|
||||||
|
channel="gh",
|
||||||
|
delay=self._shutter_to_open_delay,
|
||||||
|
width=(shutter_width - self._shutter_to_open_delay),
|
||||||
|
)
|
||||||
|
|
||||||
# Trigger extra pulse for MCS OR gate
|
# Trigger extra pulse for MCS OR gate
|
||||||
# f = e + 1us
|
# f = e + 1us
|
||||||
@@ -317,7 +343,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
|||||||
if self.scan_info.msg.scan_type == "fly":
|
if self.scan_info.msg.scan_type == "fly":
|
||||||
self.set_delay_pairs(channel="ef", delay=0, width=0)
|
self.set_delay_pairs(channel="ef", delay=0, width=0)
|
||||||
else:
|
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
|
# 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
|
# This value has been choosen empirically after testing with the HW. It's
|
||||||
@@ -520,7 +546,6 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
|||||||
- Return the status object to BEC which will automatically resolve once the status register has
|
- Return the status object to BEC which will automatically resolve once the status register has
|
||||||
the END_OF_BURST bit set. The callback of the status object will also stop the polling loop.
|
the END_OF_BURST bit set. The callback of the status object will also stop the polling loop.
|
||||||
"""
|
"""
|
||||||
overall_start = time.time()
|
|
||||||
self._stop_polling()
|
self._stop_polling()
|
||||||
|
|
||||||
# NOTE If the trigger source is not SINGLE_SHOT, the DDG is triggered by an external source
|
# NOTE If the trigger source is not SINGLE_SHOT, the DDG is triggered by an external source
|
||||||
@@ -559,7 +584,6 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
|||||||
# Send trigger
|
# Send trigger
|
||||||
self.trigger_shot.put(1, use_complete=True)
|
self.trigger_shot.put(1, use_complete=True)
|
||||||
self.cancel_on_stop(status)
|
self.cancel_on_stop(status)
|
||||||
logger.info(f"Configured ddg in {time.time()-overall_start}")
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def on_stop(self) -> None:
|
def on_stop(self) -> None:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from ophyd_devices import DeviceStatus, StatusBase
|
|||||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||||
|
|
||||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
|
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
|
||||||
|
BURSTCONFIG,
|
||||||
CHANNELREFERENCE,
|
CHANNELREFERENCE,
|
||||||
OUTPUTPOLARITY,
|
OUTPUTPOLARITY,
|
||||||
STATUSBITS,
|
STATUSBITS,
|
||||||
@@ -37,7 +38,6 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
|
|||||||
ChannelConfig,
|
ChannelConfig,
|
||||||
DelayGeneratorCSAXS,
|
DelayGeneratorCSAXS,
|
||||||
LiteralChannels,
|
LiteralChannels,
|
||||||
BURSTCONFIG,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
@@ -138,6 +138,24 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
|
|||||||
# Set burst config
|
# Set burst config
|
||||||
self.burst_config.put(BURSTCONFIG.FIRST_CYCLE.value)
|
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:
|
def on_stage(self) -> DeviceStatus | StatusBase | None:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Callable, Literal
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
|
from ophyd.utils.errors import WaitTimeoutError
|
||||||
from ophyd import Component as Cpt
|
from ophyd import Component as Cpt
|
||||||
from ophyd import EpicsSignalRO, Kind
|
from ophyd import EpicsSignalRO, Kind
|
||||||
from ophyd_devices import (
|
from ophyd_devices import (
|
||||||
@@ -261,7 +262,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
|||||||
**kwargs: Additional keyword arguments from the subscription, including 'obj' (the EpicsSignalRO instance).
|
**kwargs: Additional keyword arguments from the subscription, including 'obj' (the EpicsSignalRO instance).
|
||||||
"""
|
"""
|
||||||
with self._rlock:
|
with self._rlock:
|
||||||
logger.info(f"Received update on mcs card {self.name}")
|
|
||||||
if self._omit_mca_callbacks.is_set():
|
if self._omit_mca_callbacks.is_set():
|
||||||
return # Suppress callbacks when erasing all channels
|
return # Suppress callbacks when erasing all channels
|
||||||
self._mca_counter_index += 1
|
self._mca_counter_index += 1
|
||||||
@@ -293,9 +293,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Once we have received all channels, push data to BEC and reset for next accumulation
|
# Once we have received all channels, push data to BEC and reset for next accumulation
|
||||||
logger.info(
|
|
||||||
f"Received update for {attr_name}, index {self._mca_counter_index}/{self.NUM_MCA_CHANNELS}"
|
|
||||||
)
|
|
||||||
if len(self._current_data) == self.NUM_MCA_CHANNELS:
|
if len(self._current_data) == self.NUM_MCA_CHANNELS:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Current data index {self._current_data_index} complete, pushing to BEC."
|
f"Current data index {self._current_data_index} complete, pushing to BEC."
|
||||||
@@ -398,11 +395,12 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
|||||||
# NOTE Make sure that the signal that omits mca callbacks is cleared
|
# NOTE Make sure that the signal that omits mca callbacks is cleared
|
||||||
self._omit_mca_callbacks.clear()
|
self._omit_mca_callbacks.clear()
|
||||||
|
|
||||||
logger.info(f"MCS Card {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
|
|
||||||
# For a fly scan we need to start the mcs card ourselves
|
# For a fly scan we need to start the mcs card ourselves
|
||||||
if self.scan_info.msg.scan_type == "fly":
|
if self.scan_info.msg.scan_type == "fly":
|
||||||
self.erase_start.put(1)
|
self.erase_start.put(1)
|
||||||
|
|
||||||
|
logger.info(f"MCS Card {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
|
||||||
|
|
||||||
def on_prescan(self) -> None | StatusBase:
|
def on_prescan(self) -> None | StatusBase:
|
||||||
"""
|
"""
|
||||||
This method is called after on_stage and before the scan starts. For the MCS card, we need to make sure
|
This method is called after on_stage and before the scan starts. For the MCS card, we need to make sure
|
||||||
@@ -446,7 +444,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
|||||||
while not self._scan_done_thread_kill_event.is_set():
|
while not self._scan_done_thread_kill_event.is_set():
|
||||||
while self._start_monitor_async_data_emission.wait():
|
while self._start_monitor_async_data_emission.wait():
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Monitoring async data emission for {self.name}...")
|
|
||||||
if (
|
if (
|
||||||
hasattr(self.scan_info.msg, "num_points")
|
hasattr(self.scan_info.msg, "num_points")
|
||||||
and self.scan_info.msg.num_points is not None
|
and self.scan_info.msg.num_points is not None
|
||||||
@@ -456,7 +453,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
|||||||
for callback in self._scan_done_callbacks:
|
for callback in self._scan_done_callbacks:
|
||||||
callback(exception=None)
|
callback(exception=None)
|
||||||
else:
|
else:
|
||||||
logger.info(f"Current data index is {self._current_data_index}")
|
|
||||||
if self._current_data_index >= 1:
|
if self._current_data_index >= 1:
|
||||||
for callback in self._scan_done_callbacks:
|
for callback in self._scan_done_callbacks:
|
||||||
callback(exception=None)
|
callback(exception=None)
|
||||||
@@ -518,7 +514,22 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
|||||||
# that the acquisition finishes on the card and that data is emitted to BEC. If the acquisition
|
# 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
|
# 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.
|
# 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
|
# Prepare and register status callback for the async monitoring loop
|
||||||
status_async_data = StatusBase(obj=self)
|
status_async_data = StatusBase(obj=self)
|
||||||
|
|||||||
@@ -156,7 +156,6 @@ class Camera:
|
|||||||
camera_id (int): The ID of the camera device.
|
camera_id (int): The ID of the camera device.
|
||||||
m_n_colormode (Literal[0, 1, 2, 3]): Color mode for the camera.
|
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.
|
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__(
|
def __init__(
|
||||||
|
|||||||
@@ -3,21 +3,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
from typing import TYPE_CHECKING, Literal
|
||||||
from typing import TYPE_CHECKING, Literal, Tuple, TypedDict
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from ophyd import Component as Cpt, Signal, Kind
|
||||||
|
|
||||||
from bec_lib import messages
|
from bec_lib import messages
|
||||||
from bec_lib.logger import bec_logger
|
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.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||||
from ophyd_devices.utils.bec_signals import AsyncSignal, PreviewSignal
|
from ophyd_devices.utils.bec_signals import AsyncSignal, PreviewSignal
|
||||||
|
|
||||||
from csaxs_bec.devices.ids_cameras.base_integration.camera import Camera
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bec_lib.devicemanager import ScanInfo
|
from bec_lib.devicemanager import ScanInfo
|
||||||
from pydantic import ValidationInfo
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@@ -45,8 +43,15 @@ class IDSCamera(PSIDeviceBase):
|
|||||||
doc="Signal for the region of interest (ROI).",
|
doc="Signal for the region of interest (ROI).",
|
||||||
async_update={"type": "add", "max_shape": [None]},
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -83,15 +88,22 @@ class IDSCamera(PSIDeviceBase):
|
|||||||
bits_per_pixel=bits_per_pixel,
|
bits_per_pixel=bits_per_pixel,
|
||||||
connect=False,
|
connect=False,
|
||||||
)
|
)
|
||||||
self._live_mode = False
|
|
||||||
self._inputs = {"live_mode": live_mode}
|
self._inputs = {"live_mode": live_mode}
|
||||||
self._mask = np.zeros((1, 1), dtype=np.uint8)
|
self._mask = np.zeros((1, 1), dtype=np.uint8)
|
||||||
self.image.num_rotation_90 = num_rotation_90
|
self.image.num_rotation_90 = num_rotation_90
|
||||||
self.image.transpose = transpose
|
self.image.transpose = transpose
|
||||||
self._force_monochrome = force_monochrome
|
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 ##############
|
############## 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
|
@property
|
||||||
def mask(self) -> np.ndarray:
|
def mask(self) -> np.ndarray:
|
||||||
"""Return the current region of interest (ROI) for the camera."""
|
"""Return the current region of interest (ROI) for the camera."""
|
||||||
@@ -114,22 +126,15 @@ class IDSCamera(PSIDeviceBase):
|
|||||||
)
|
)
|
||||||
self._mask = value
|
self._mask = value
|
||||||
|
|
||||||
@property
|
def _on_live_mode_enabled_changed(self, *args, value, **kwargs):
|
||||||
def live_mode(self) -> bool:
|
"""Callback for when live mode is changed."""
|
||||||
"""Return whether the camera is in live mode."""
|
enabled = bool(value)
|
||||||
return self._live_mode
|
if enabled and self.cam._connected is False: # pylint: disable=protected-access
|
||||||
|
self.cam.on_connect()
|
||||||
@live_mode.setter
|
if enabled:
|
||||||
def live_mode(self, value: bool):
|
self._start_live()
|
||||||
"""Set the live mode for the camera."""
|
else:
|
||||||
if value != self._live_mode:
|
self._stop_live()
|
||||||
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 set_rect_roi(self, x: int, y: int, width: int, height: int):
|
def set_rect_roi(self, x: int, y: int, width: int, height: int):
|
||||||
"""Set the rectangular region of interest (ROI) for the camera."""
|
"""Set the rectangular region of interest (ROI) for the camera."""
|
||||||
@@ -196,7 +201,7 @@ class IDSCamera(PSIDeviceBase):
|
|||||||
"""Connect to the camera."""
|
"""Connect to the camera."""
|
||||||
self.cam.force_monochrome = self._force_monochrome
|
self.cam.force_monochrome = self._force_monochrome
|
||||||
self.cam.on_connect()
|
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)
|
self.set_rect_roi(0, 0, self.cam.cam.width.value, self.cam.cam.height.value)
|
||||||
|
|
||||||
def on_destroy(self):
|
def on_destroy(self):
|
||||||
@@ -206,7 +211,7 @@ class IDSCamera(PSIDeviceBase):
|
|||||||
|
|
||||||
def on_trigger(self):
|
def on_trigger(self):
|
||||||
"""Handle the trigger event."""
|
"""Handle the trigger event."""
|
||||||
if not self.live_mode:
|
if not bool(self.live_mode_enabled.get()):
|
||||||
return
|
return
|
||||||
image = self.image.get()
|
image = self.image.get()
|
||||||
if image is not None:
|
if image is not None:
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ class Eiger(PSIDeviceBase):
|
|||||||
if data is None:
|
if data is None:
|
||||||
logger.error(f"Received image message on device {self.name} without data.")
|
logger.error(f"Received image message on device {self.name} without data.")
|
||||||
return
|
return
|
||||||
logger.info(f"Received preview image on device {self.name}")
|
|
||||||
self.preview_image.put(data)
|
self.preview_image.put(data)
|
||||||
|
|
||||||
# pylint: disable=missing-function-docstring
|
# pylint: disable=missing-function-docstring
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from csaxs_bec.devices.omny.galil.galil_ophyd import (
|
|||||||
GalilAxesReferenced,
|
GalilAxesReferenced,
|
||||||
GalilController,
|
GalilController,
|
||||||
GalilMotorIsMoving,
|
GalilMotorIsMoving,
|
||||||
GalilMotorResolution,
|
|
||||||
GalilSetpointSignal,
|
GalilSetpointSignal,
|
||||||
GalilSignalRO,
|
GalilSignalRO,
|
||||||
retry_once,
|
retry_once,
|
||||||
@@ -24,6 +23,19 @@ from csaxs_bec.devices.omny.galil.galil_ophyd import (
|
|||||||
logger = bec_logger.logger
|
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):
|
class LamniGalilController(GalilController):
|
||||||
|
|
||||||
|
|
||||||
@@ -154,10 +166,17 @@ class LamniGalilReadbackSignal(GalilSignalRO):
|
|||||||
Returns:
|
Returns:
|
||||||
float: Readback value after adjusting for sign and motor resolution.
|
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}"))
|
if self.parent.axis_Id_numeric < 6:
|
||||||
current_pos *= self.parent.sign
|
current_pos = float(self.controller.socket_put_and_receive(f"TP{self.parent.axis_Id}"))
|
||||||
step_mm = self.parent.motor_resolution.get()
|
current_pos *= self.parent.sign
|
||||||
return current_pos / step_mm
|
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):
|
def read(self):
|
||||||
self._metadata["timestamp"] = time.time()
|
self._metadata["timestamp"] = time.time()
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ class RtFlomniController(Controller):
|
|||||||
parent._min_scan_buffer_reached = False
|
parent._min_scan_buffer_reached = False
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
for pos_index, pos in enumerate(positions):
|
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:
|
if pos_index > 100:
|
||||||
parent._min_scan_buffer_reached = True
|
parent._min_scan_buffer_reached = True
|
||||||
parent._min_scan_buffer_reached = True
|
parent._min_scan_buffer_reached = True
|
||||||
@@ -174,8 +175,12 @@ class RtFlomniController(Controller):
|
|||||||
self.set_device_read_write("foptx", False)
|
self.set_device_read_write("foptx", False)
|
||||||
self.set_device_read_write("fopty", 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)
|
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:
|
if self.rt_pid_voltage is None:
|
||||||
rtx = self.device_manager.devices.rtx
|
rtx = self.device_manager.devices.rtx
|
||||||
self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage")
|
self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage")
|
||||||
@@ -184,31 +189,41 @@ class RtFlomniController(Controller):
|
|||||||
"rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first."
|
"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}")
|
logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}")
|
||||||
expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100
|
expected_voltage = self.rt_pid_voltage
|
||||||
logger.info(f"Expected PID voltage: {expected_voltage}")
|
# logger.info(f"Expected PID voltage: {expected_voltage}")
|
||||||
logger.info(f"Current PID voltage: {self.get_pid_x()}")
|
logger.info(f"Current PID voltage: {self.get_pid_x()}")
|
||||||
|
|
||||||
wait_on_exit = False
|
wait_on_exit = False
|
||||||
while True:
|
# we allow 2V range from center, this corresponds to 30 microns
|
||||||
if np.abs(self.get_pid_x() - expected_voltage) < 1:
|
if np.abs(self.get_pid_x() - expected_voltage) < 2:
|
||||||
break
|
logger.info("No correction of fsamx needed")
|
||||||
wait_on_exit = True
|
else:
|
||||||
self.socket_put("v0")
|
|
||||||
fsamx = self.device_manager.devices.fsamx
|
fsamx = self.device_manager.devices.fsamx
|
||||||
fsamx.read_only = False
|
|
||||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
|
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
|
while True:
|
||||||
logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}")
|
# when we correct, then to 1 V, within 15 microns
|
||||||
fsamx_in = fsamx.user_parameter.get("in")
|
if np.abs(self.get_pid_x() - expected_voltage) < 1:
|
||||||
fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True)
|
logger.info("No further correction needed")
|
||||||
fsamx.read_only = True
|
break
|
||||||
time.sleep(0.1)
|
wait_on_exit = True
|
||||||
self.laser_tracker_on()
|
# disable FZP piezo feedback
|
||||||
time.sleep(0.01)
|
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:
|
if wait_on_exit:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
# enable fast FZP feedback again
|
||||||
self.socket_put("v1")
|
self.socket_put("v1")
|
||||||
|
|
||||||
@threadlocked
|
@threadlocked
|
||||||
@@ -379,7 +394,7 @@ class RtFlomniController(Controller):
|
|||||||
val = float(self.socket_put_and_receive(f"j{axis_number}").strip())
|
val = float(self.socket_put_and_receive(f"j{axis_number}").strip())
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def laser_tracker_check_signalstrength(self):
|
def laser_tracker_check_signalstrength(self, verbose=True):
|
||||||
if not self.laser_tracker_check_enabled():
|
if not self.laser_tracker_check_enabled():
|
||||||
returnval = "disabled"
|
returnval = "disabled"
|
||||||
else:
|
else:
|
||||||
@@ -390,9 +405,10 @@ class RtFlomniController(Controller):
|
|||||||
rtx = self.device_manager.devices.rtx
|
rtx = self.device_manager.devices.rtx
|
||||||
min_signal = rtx.user_parameter.get("min_signal")
|
min_signal = rtx.user_parameter.get("min_signal")
|
||||||
low_signal = rtx.user_parameter.get("low_signal")
|
low_signal = rtx.user_parameter.get("low_signal")
|
||||||
print(f"low signal: {low_signal}")
|
if verbose:
|
||||||
print(f"min signal: {min_signal}")
|
print(f"low signal: {low_signal}")
|
||||||
print(f"signal: {signal}")
|
print(f"min signal: {min_signal}")
|
||||||
|
print(f"signal: {signal}")
|
||||||
if signal < min_signal:
|
if signal < min_signal:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
if signal < min_signal:
|
if signal < min_signal:
|
||||||
@@ -499,7 +515,7 @@ class RtFlomniController(Controller):
|
|||||||
# while scan is running
|
# while scan is running
|
||||||
while mode > 0:
|
while mode > 0:
|
||||||
|
|
||||||
#TODO here?: scan abortion if no progress in scan *raise error
|
# 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}")
|
# 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()
|
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||||
@@ -606,6 +622,18 @@ class RtFlomniSetpointSignal(RtSetpointSignal):
|
|||||||
"The interferometer feedback is not running. Either it is turned off or and"
|
"The interferometer feedback is not running. Either it is turned off or and"
|
||||||
" interferometer error occured."
|
" 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)
|
self.set_with_feedback_disabled(val)
|
||||||
|
|
||||||
def set_with_feedback_disabled(self, val):
|
def set_with_feedback_disabled(self, val):
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class OMNYFastShutter(PSIDeviceBase, Device):
|
|||||||
def fshopen(self):
|
def fshopen(self):
|
||||||
"""Open the fast shutter."""
|
"""Open the fast shutter."""
|
||||||
if self._check_if_cSAXS_shutter_exists_in_config():
|
if self._check_if_cSAXS_shutter_exists_in_config():
|
||||||
|
self.shutter.put(1)
|
||||||
return self.device_manager.devices["fsh"].fshopen()
|
return self.device_manager.devices["fsh"].fshopen()
|
||||||
else:
|
else:
|
||||||
self.shutter.put(1)
|
self.shutter.put(1)
|
||||||
@@ -55,6 +56,7 @@ class OMNYFastShutter(PSIDeviceBase, Device):
|
|||||||
def fshclose(self):
|
def fshclose(self):
|
||||||
"""Close the fast shutter."""
|
"""Close the fast shutter."""
|
||||||
if self._check_if_cSAXS_shutter_exists_in_config():
|
if self._check_if_cSAXS_shutter_exists_in_config():
|
||||||
|
self.shutter.put(0)
|
||||||
return self.device_manager.devices["fsh"].fshclose()
|
return self.device_manager.devices["fsh"].fshclose()
|
||||||
else:
|
else:
|
||||||
self.shutter.put(0)
|
self.shutter.put(0)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import requests
|
|||||||
import threading
|
import threading
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
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
|
from ophyd_devices import PreviewSignal
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@@ -13,6 +13,13 @@ logger = bec_logger.logger
|
|||||||
class WebcamViewer(Device):
|
class WebcamViewer(Device):
|
||||||
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
|
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
|
||||||
preview = Cpt(PreviewSignal, ndim=2, num_rotation_90=0, transpose=False)
|
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:
|
def __init__(self, url:str, name:str, num_rotation_90=0, transpose=False, **kwargs) -> None:
|
||||||
super().__init__(name=name, **kwargs)
|
super().__init__(name=name, **kwargs)
|
||||||
@@ -21,20 +28,54 @@ class WebcamViewer(Device):
|
|||||||
self._update_thread = None
|
self._update_thread = None
|
||||||
self._buffer = b""
|
self._buffer = b""
|
||||||
self._shutdown_event = threading.Event()
|
self._shutdown_event = threading.Event()
|
||||||
|
self._live_mode_lock = threading.RLock()
|
||||||
self.preview.num_rotation_90 = num_rotation_90
|
self.preview.num_rotation_90 = num_rotation_90
|
||||||
self.preview.transpose = transpose
|
self.preview.transpose = transpose
|
||||||
|
self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False)
|
||||||
|
|
||||||
def start_live_mode(self) -> None:
|
def start_live_mode(self) -> None:
|
||||||
if self._connection is not None:
|
self.live_mode_enabled.put(True)
|
||||||
return
|
|
||||||
self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
def stop_live_mode(self) -> None:
|
||||||
self._update_thread.start()
|
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:
|
def _update_loop(self) -> None:
|
||||||
while not self._shutdown_event.is_set():
|
while not self._shutdown_event.is_set():
|
||||||
try:
|
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):
|
for chunk in self._connection.iter_content(chunk_size=1024):
|
||||||
|
if self._shutdown_event.is_set():
|
||||||
|
break
|
||||||
self._buffer += chunk
|
self._buffer += chunk
|
||||||
start = self._buffer.find(b'\xff\xd8') # JPEG start
|
start = self._buffer.find(b'\xff\xd8') # JPEG start
|
||||||
end = self._buffer.find(b'\xff\xd9') # JPEG end
|
end = self._buffer.find(b'\xff\xd9') # JPEG end
|
||||||
@@ -50,16 +91,3 @@ class WebcamViewer(Device):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
content = traceback.format_exc()
|
content = traceback.format_exc()
|
||||||
logger.error(f"Image update loop failed: {content}")
|
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 @@
|
|||||||
|
import numpy as np
|
||||||
from ophyd import Component as Cpt
|
from ophyd import Component as Cpt, Signal, EpicsSignal
|
||||||
from ophyd import Device
|
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(
|
fit_array = Cpt(Signal, value=np.zeros((3, 10)))
|
||||||
EpicsSignal, name="save_frame", read_pv="XOMNYI-XEYE-SAVFRAME:0",auto_monitor=True
|
fit_params_x = Cpt(Signal, value=np.zeros((2, 3)))
|
||||||
)
|
fit_params_y = Cpt(Signal, value=np.zeros((2, 3)))
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|||||||
103
csaxs_bec/devices/panda_box/panda_box.py
Normal file
103
csaxs_bec/devices/panda_box/panda_box.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"""Module to integrate the PandaBox for cSAXS measurements."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from ophyd_devices import StatusBase
|
||||||
|
from ophyd_devices.devices.panda_box.panda_box import PandaBox, PandaState
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class PandaBoxCSAXS(PandaBox):
|
||||||
|
"""
|
||||||
|
PandaBox integration for cSAXS. This class implements cSAXS specific logic for the PandaBox integration.
|
||||||
|
|
||||||
|
TODO: This logic is not yet mapped to any existing hardware. Adapt Docstring once the hardware is defined and integrated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_init(self):
|
||||||
|
super().on_init()
|
||||||
|
self._acquisition_group = "burst"
|
||||||
|
self._timeout_on_completed = 10
|
||||||
|
|
||||||
|
def on_stage(self):
|
||||||
|
start_time = time.time()
|
||||||
|
super().on_stage()
|
||||||
|
# TODO, adjust as seen fit.
|
||||||
|
# Adjust the acquisition group based on scan parameters if needed
|
||||||
|
if self.scan_info.msg.scan_type == "fly":
|
||||||
|
self._acquisition_group = "fly"
|
||||||
|
elif self.scan_info.msg.scan_type == "step":
|
||||||
|
if self.scan_info.msg.scan_parameters["frames_per_trigger"] == 1:
|
||||||
|
self._acquisition_group = "monitored"
|
||||||
|
else:
|
||||||
|
self._acquisition_group = "burst"
|
||||||
|
|
||||||
|
logger.info(f"PandaBox {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
|
||||||
|
|
||||||
|
def on_complete(self):
|
||||||
|
"""On complete is called after the scan is complete. We need to wait for the capture to complete before we can disarm the PandaBox."""
|
||||||
|
|
||||||
|
def _check_capture_complete():
|
||||||
|
captured = 0
|
||||||
|
start_time = time.monotonic()
|
||||||
|
try:
|
||||||
|
expected_points = int(
|
||||||
|
self.scan_info.msg.num_points
|
||||||
|
* self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||||
|
)
|
||||||
|
while captured < expected_points:
|
||||||
|
ret = self.send_raw("*PCAP.CAPTURED?")
|
||||||
|
captured = int(ret[0].split("=")[-1])
|
||||||
|
time.sleep(0.01)
|
||||||
|
if (time.monotonic() - start_time) > self._timeout_on_completed / 2:
|
||||||
|
logger.info(
|
||||||
|
f"Waiting for capture on device {self.name} to complete: captured {captured}/{expected_points} points."
|
||||||
|
)
|
||||||
|
if (time.monotonic() - start_time) > self._timeout_on_completed:
|
||||||
|
raise TimeoutError(
|
||||||
|
f"Pandabox {self.name} did not complete after {self._timeout_on_completed} with points captured {captured}/{expected_points}"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._disarm()
|
||||||
|
|
||||||
|
status_captured = self.task_handler.submit_task(_check_capture_complete, run=True)
|
||||||
|
self.cancel_on_stop(status_captured)
|
||||||
|
return status_captured
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import time
|
||||||
|
|
||||||
|
panda = PandaBoxCSAXS(
|
||||||
|
name="omny_panda",
|
||||||
|
host="omny-panda.psi.ch",
|
||||||
|
signal_alias={
|
||||||
|
"FMC_IN.VAL2.Value": "alias",
|
||||||
|
"FMC_IN.VAL1.Min": "alias2",
|
||||||
|
"FMC_IN.VAL1.Max": "alias3",
|
||||||
|
"FMC_IN.VAL1.Mean": "alias4",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
panda.on_connected()
|
||||||
|
status = StatusBase(obj=panda)
|
||||||
|
panda.add_status_callback(
|
||||||
|
status=status, success=[PandaState.DISARMED], failure=[PandaState.READY]
|
||||||
|
)
|
||||||
|
panda.stop()
|
||||||
|
status.wait(timeout=2)
|
||||||
|
panda.unstage()
|
||||||
|
logger.info(f"Panda connected")
|
||||||
|
ret = panda.stage()
|
||||||
|
logger.info(f"Panda staged")
|
||||||
|
ret = panda.pre_scan()
|
||||||
|
ret.wait(timeout=5)
|
||||||
|
logger.info(f"Panda pre scan done")
|
||||||
|
time.sleep(5)
|
||||||
|
panda.stop()
|
||||||
|
st = panda.complete()
|
||||||
|
st.wait(timeout=5)
|
||||||
|
logger.info(f"Measurement completed")
|
||||||
|
panda.unstage()
|
||||||
|
logger.info(f"Panda Unstaged")
|
||||||
99
csaxs_bec/devices/panda_box/panda_box_omny.py
Normal file
99
csaxs_bec/devices/panda_box/panda_box_omny.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""Module to integrate the PandaBox for cSAXS measurements."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from ophyd_devices import StatusBase
|
||||||
|
from ophyd_devices.devices.panda_box.panda_box import PandaBox, PandaState
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class PandaBoxOMNY(PandaBox):
|
||||||
|
"""PandaBox integration for OMNY. This class implements OMNY specific logic for the PandaBox integration."""
|
||||||
|
|
||||||
|
def on_init(self):
|
||||||
|
super().on_init()
|
||||||
|
self._acquisition_group = "burst"
|
||||||
|
self._timeout_on_completed = 10
|
||||||
|
|
||||||
|
def on_stage(self):
|
||||||
|
start_time = time.time()
|
||||||
|
super().on_stage()
|
||||||
|
# TODO, adjust as seen fit.
|
||||||
|
# Adjust the acquisition group based on scan parameters if needed
|
||||||
|
if self.scan_info.msg.scan_type == "fly":
|
||||||
|
self._acquisition_group = "fly"
|
||||||
|
elif self.scan_info.msg.scan_type == "step":
|
||||||
|
if self.scan_info.msg.scan_parameters["frames_per_trigger"] == 1:
|
||||||
|
self._acquisition_group = "monitored"
|
||||||
|
else:
|
||||||
|
self._acquisition_group = "burst"
|
||||||
|
|
||||||
|
logger.info(f"PandaBox {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
|
||||||
|
|
||||||
|
def on_complete(self):
|
||||||
|
"""On complete is called after the scan is complete. We need to wait for the capture to complete before we can disarm the PandaBox."""
|
||||||
|
|
||||||
|
def _check_capture_complete():
|
||||||
|
captured = 0
|
||||||
|
start_time = time.monotonic()
|
||||||
|
try:
|
||||||
|
expected_points = int(
|
||||||
|
self.scan_info.msg.num_points
|
||||||
|
* self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||||
|
)
|
||||||
|
while captured < expected_points:
|
||||||
|
ret = self.send_raw("*PCAP.CAPTURED?")
|
||||||
|
captured = int(ret[0].split("=")[-1])
|
||||||
|
time.sleep(0.01)
|
||||||
|
if (time.monotonic() - start_time) > self._timeout_on_completed / 2:
|
||||||
|
logger.info(
|
||||||
|
f"Waiting for capture on device {self.name} to complete: captured {captured}/{expected_points} points."
|
||||||
|
)
|
||||||
|
if (time.monotonic() - start_time) > self._timeout_on_completed:
|
||||||
|
raise TimeoutError(
|
||||||
|
f"Pandabox {self.name} did not complete after {self._timeout_on_completed} with points captured {captured}/{expected_points}"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._disarm()
|
||||||
|
|
||||||
|
status_captured = self.task_handler.submit_task(_check_capture_complete, run=True)
|
||||||
|
self.cancel_on_stop(status_captured)
|
||||||
|
return status_captured
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import time
|
||||||
|
|
||||||
|
panda = PandaBoxOMNY(
|
||||||
|
name="omny_panda",
|
||||||
|
host="omny-panda.psi.ch",
|
||||||
|
signal_alias={
|
||||||
|
"FMC_IN.VAL2.Value": "alias",
|
||||||
|
"FMC_IN.VAL1.Min": "alias2",
|
||||||
|
"FMC_IN.VAL1.Max": "alias3",
|
||||||
|
"FMC_IN.VAL1.Mean": "alias4",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
panda.on_connected()
|
||||||
|
status = StatusBase(obj=panda)
|
||||||
|
panda.add_status_callback(
|
||||||
|
status=status, success=[PandaState.DISARMED], failure=[PandaState.READY]
|
||||||
|
)
|
||||||
|
panda.stop()
|
||||||
|
status.wait(timeout=2)
|
||||||
|
panda.unstage()
|
||||||
|
logger.info(f"Panda connected")
|
||||||
|
ret = panda.stage()
|
||||||
|
logger.info(f"Panda staged")
|
||||||
|
ret = panda.pre_scan()
|
||||||
|
ret.wait(timeout=5)
|
||||||
|
logger.info(f"Panda pre scan done")
|
||||||
|
time.sleep(5)
|
||||||
|
panda.stop()
|
||||||
|
st = panda.complete()
|
||||||
|
st.wait(timeout=5)
|
||||||
|
logger.info(f"Measurement completed")
|
||||||
|
panda.unstage()
|
||||||
|
logger.info(f"Panda Unstaged")
|
||||||
@@ -170,6 +170,9 @@ class LamNIMixin:
|
|||||||
self.device_manager.devices.lsamx.read_only = True
|
self.device_manager.devices.lsamx.read_only = True
|
||||||
self.device_manager.devices.lsamy.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")
|
yield from self.stubs.send_rpc_and_wait("rtx", "controller.feedback_enable_without_reset")
|
||||||
|
|
||||||
|
|
||||||
@@ -210,7 +213,7 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
|
|||||||
arg_input = {}
|
arg_input = {}
|
||||||
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
|
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
|
||||||
|
|
||||||
def __init__(self, *args, parameter: dict = None, **kwargs):
|
def __init__(self, *args, parameter: dict = None, frames_per_trigger:int=1, exp_time:float=0,**kwargs):
|
||||||
"""
|
"""
|
||||||
A LamNI scan following Fermat's spiral.
|
A LamNI scan following Fermat's spiral.
|
||||||
|
|
||||||
@@ -230,10 +233,10 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> scans.lamni_fermat_scan(fov_size=[20], step=0.5, exp_time=0.1)
|
>>> scans.lamni_fermat_scan(fov_size=[20], step=0.5, exp_time=0.1)
|
||||||
>>> scans.lamni_fermat_scan(fov_size=[20, 25], center_x=0.02, center_y=0, shift_x=0, shift_y=0, angle=0, step=0.5, fov_circular=0, exp_time=0.1)
|
>>> scans.lamni_fermat_scan(fov_size=[20, 25], center_x=0.02, center_y=0, shift_x=0, shift_y=0, angle=0, step=0.5, fov_circular=0, exp_time=0.1, frames_per_trigger=1)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(parameter=parameter, **kwargs)
|
super().__init__(parameter=parameter, frames_per_trigger=frames_per_trigger, exp_time=exp_time,**kwargs)
|
||||||
self.axis = []
|
self.axis = []
|
||||||
scan_kwargs = parameter.get("kwargs", {})
|
scan_kwargs = parameter.get("kwargs", {})
|
||||||
self.fov_size = scan_kwargs.get("fov_size")
|
self.fov_size = scan_kwargs.get("fov_size")
|
||||||
@@ -482,6 +485,7 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
|
|||||||
yield from self.open_scan()
|
yield from self.open_scan()
|
||||||
yield from self.stage()
|
yield from self.stage()
|
||||||
yield from self.run_baseline_reading()
|
yield from self.run_baseline_reading()
|
||||||
|
yield from self.pre_scan()
|
||||||
yield from self.scan_core()
|
yield from self.scan_core()
|
||||||
yield from self.finalize()
|
yield from self.finalize()
|
||||||
yield from self.unstage()
|
yield from self.unstage()
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ import time
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bec_lib import bec_logger, messages
|
from bec_lib import bec_logger, messages
|
||||||
|
from bec_lib.alarm_handler import Alarms
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_server.scan_server.errors import ScanAbortion
|
from bec_server.scan_server.errors import ScanAbortion
|
||||||
from bec_server.scan_server.scans import SyncFlyScanBase
|
from bec_server.scan_server.scans import SyncFlyScanBase
|
||||||
|
|
||||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import TRIGGERSOURCE
|
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import TRIGGERSOURCE
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
@@ -52,6 +54,7 @@ class FlomniFermatScan(SyncFlyScanBase):
|
|||||||
angle: float = None,
|
angle: float = None,
|
||||||
corridor_size: float = 3,
|
corridor_size: float = 3,
|
||||||
parameter: dict = None,
|
parameter: dict = None,
|
||||||
|
frames_per_trigger: int = 1,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -62,7 +65,8 @@ class FlomniFermatScan(SyncFlyScanBase):
|
|||||||
fovy(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 100 um
|
fovy(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 100 um
|
||||||
cenx(float) [um]: center position in x.
|
cenx(float) [um]: center position in x.
|
||||||
ceny(float) [um]: center position in y.
|
ceny(float) [um]: center position in y.
|
||||||
exp_time(float) [s]: exposure time
|
exp_time(float) [s]: exposure time per burst frame
|
||||||
|
frames_per_trigger(int) : Number of burst frames per point
|
||||||
step(float) [um]: stepsize
|
step(float) [um]: stepsize
|
||||||
zshift(float) [um]: shift in z
|
zshift(float) [um]: shift in z
|
||||||
angle(float) [deg]: rotation angle (will rotate first)
|
angle(float) [deg]: rotation angle (will rotate first)
|
||||||
@@ -71,10 +75,12 @@ class FlomniFermatScan(SyncFlyScanBase):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01)
|
>>> 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, **kwargs)
|
super().__init__(
|
||||||
|
parameter=parameter, exp_time=exp_time, frames_per_trigger=frames_per_trigger, **kwargs
|
||||||
|
)
|
||||||
self.show_live_table = False
|
self.show_live_table = False
|
||||||
self.axis = []
|
self.axis = []
|
||||||
self.fovx = fovx
|
self.fovx = fovx
|
||||||
@@ -152,12 +158,15 @@ class FlomniFermatScan(SyncFlyScanBase):
|
|||||||
|
|
||||||
def _prepare_setup_part2(self):
|
def _prepare_setup_part2(self):
|
||||||
# Prepare DDG1 to use
|
# Prepare DDG1 to use
|
||||||
yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.EXT_RISING_EDGE.value)
|
yield from self.stubs.send_rpc_and_wait(
|
||||||
|
"ddg1", "set_trigger", TRIGGERSOURCE.EXT_RISING_EDGE.value
|
||||||
|
)
|
||||||
|
|
||||||
if self.flomni_rotation_status:
|
if self.flomni_rotation_status:
|
||||||
self.flomni_rotation_status.wait()
|
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)
|
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")
|
yield from self.stubs.send_rpc_and_wait("rtx", "controller.laser_tracker_on")
|
||||||
@@ -165,22 +174,24 @@ class FlomniFermatScan(SyncFlyScanBase):
|
|||||||
rtx_status.wait()
|
rtx_status.wait()
|
||||||
rtz_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._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(
|
tracker_signal_status = yield from self.stubs.send_rpc_and_wait(
|
||||||
"rtx", "controller.laser_tracker_check_signalstrength"
|
"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":
|
if tracker_signal_status == "low":
|
||||||
self.device_manager.connector.raise_alarm(
|
error_info = messages.ErrorInfo(
|
||||||
severity=0,
|
error_message="Signal strength of the laser tracker is low, but sufficient to continue. Realignment recommended!",
|
||||||
alarm_type="LaserTrackerSignalStrength",
|
compact_error_message="Low signal strength of the laser tracker. Realignment recommended!",
|
||||||
source={"device": "rtx", "reason": "low signal strength", "method": "_prepare_setup_part2"},
|
exception_type="LaserTrackerSignalStrengthLow",
|
||||||
metadata={},
|
device="rtx",
|
||||||
msg="Signal strength of the laser tracker is low, sufficient to continue. Realignment recommended!",
|
|
||||||
)
|
)
|
||||||
|
self.device_manager.connector.raise_alarm(severity=Alarms.WARNING, info=error_info)
|
||||||
elif tracker_signal_status == "toolow":
|
elif tracker_signal_status == "toolow":
|
||||||
raise ScanAbortion(
|
raise ScanAbortion(
|
||||||
"Signal strength of the laser tracker is too low for scanning. Realignment required!"
|
"Signal strength of the laser tracker is too low for scanning. Realignment required!"
|
||||||
@@ -305,13 +316,19 @@ class FlomniFermatScan(SyncFlyScanBase):
|
|||||||
# in flomni, we need to move to the start position of the next scan, which is the end position of the current scan
|
# 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()
|
# 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:
|
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
|
return
|
||||||
|
|
||||||
logger.warning("No positions found to return to start")
|
logger.warning("No positions found to return to start")
|
||||||
|
|
||||||
def cleanup(self):
|
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()
|
yield from super().cleanup()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@@ -323,6 +340,7 @@ class FlomniFermatScan(SyncFlyScanBase):
|
|||||||
yield from self.stage()
|
yield from self.stage()
|
||||||
yield from self.run_baseline_reading()
|
yield from self.run_baseline_reading()
|
||||||
yield from self._prepare_setup_part2()
|
yield from self._prepare_setup_part2()
|
||||||
|
yield from self.pre_scan()
|
||||||
yield from self.scan_core()
|
yield from self.scan_core()
|
||||||
yield from self.finalize()
|
yield from self.finalize()
|
||||||
yield from self.unstage()
|
yield from self.unstage()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" Module with JungfrauJochTestScan class. """
|
"""Module with JungfrauJochTestScan class."""
|
||||||
|
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
from bec_server.scan_server.scans import AsyncFlyScanBase, ScanAbortion
|
from bec_server.scan_server.scans import AsyncFlyScanBase, ScanAbortion
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class OMNYFermatScan(SyncFlyScanBase):
|
|||||||
angle: float = None,
|
angle: float = None,
|
||||||
corridor_size: float = 3,
|
corridor_size: float = 3,
|
||||||
parameter: dict = None,
|
parameter: dict = None,
|
||||||
|
frames_per_trigger:int=1,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -62,6 +63,7 @@ class OMNYFermatScan(SyncFlyScanBase):
|
|||||||
cenx(float) [um]: center position in x.
|
cenx(float) [um]: center position in x.
|
||||||
ceny(float) [um]: center position in y.
|
ceny(float) [um]: center position in y.
|
||||||
exp_time(float) [s]: exposure time
|
exp_time(float) [s]: exposure time
|
||||||
|
frames_per_trigger:int: Number of burst frames per trigger, defaults to 1.
|
||||||
step(float) [um]: stepsize
|
step(float) [um]: stepsize
|
||||||
zshift(float) [um]: shift in z
|
zshift(float) [um]: shift in z
|
||||||
angle(float) [deg]: rotation angle (will rotate first)
|
angle(float) [deg]: rotation angle (will rotate first)
|
||||||
@@ -73,7 +75,7 @@ class OMNYFermatScan(SyncFlyScanBase):
|
|||||||
>>> scans.omny_fermat_scan(fovx=20, fovy=25, cenx=10, ceny=0, zshift=0, angle=0, step=2, exp_time=0.01)
|
>>> scans.omny_fermat_scan(fovx=20, fovy=25, cenx=10, ceny=0, zshift=0, angle=0, step=2, exp_time=0.01)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(parameter=parameter, **kwargs)
|
super().__init__(parameter=parameter, exp_time=exp_time, frames_per_trigger=frames_per_trigger, **kwargs)
|
||||||
self.axis = []
|
self.axis = []
|
||||||
self.fovx = fovx
|
self.fovx = fovx
|
||||||
self.fovy = fovy
|
self.fovy = fovy
|
||||||
@@ -299,6 +301,7 @@ class OMNYFermatScan(SyncFlyScanBase):
|
|||||||
yield from self.stage()
|
yield from self.stage()
|
||||||
yield from self.run_baseline_reading()
|
yield from self.run_baseline_reading()
|
||||||
yield from self._prepare_setup_part2()
|
yield from self._prepare_setup_part2()
|
||||||
|
yield from self.pre_scan()
|
||||||
yield from self.scan_core()
|
yield from self.scan_core()
|
||||||
yield from self.finalize()
|
yield from self.finalize()
|
||||||
yield from self.unstage()
|
yield from self.unstage()
|
||||||
|
|||||||
@@ -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.
|
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:
|
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. `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_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.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.
|
||||||
1. `flomni.read_alignment_offset()` read the generated alignment data.
|
|
||||||
|
|
||||||
#### Fine alignment
|
#### Fine alignment
|
||||||
|
|
||||||
After the xrayeyealign, a fine alignment needs to be performed using ptychography.
|
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.
|
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.
|
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_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. `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…
|
#### If something went wrong…
|
||||||
|
|
||||||
A __single projection__ is to be repeated use
|
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
|
`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__:
|
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.
|
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)
|
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")`
|
`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:
|
If the realtime system is restarted, bec will lose communication. To restart:
|
||||||
`flomni.rt_off()` … then wait a few seconds
|
`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.
|
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`
|
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})`
|
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.
|
`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)`
|
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!
|
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.
|
Following functions exist to move the optics in and out, with self-explaining naming.
|
||||||
@@ -193,14 +201,15 @@ The basic scan function can be called by `scans.flomni_fermat_scan()` and offers
|
|||||||
| fovy (float) | Fov in the piezo plane (i.e. piezo range). Max 100 um |
|
| fovy (float) | Fov in the piezo plane (i.e. piezo range). Max 100 um |
|
||||||
| cenx (float) | center position in x |
|
| cenx (float) | center position in x |
|
||||||
| ceny (float) | center position in y |
|
| ceny (float) | center position in y |
|
||||||
| exp_time (float) | exposure time |
|
| exp_time (float) | exposure time per frame |
|
||||||
|
| frames_per_trigger(int) | Number of burst frames per position |
|
||||||
| step (float) | stepsize |
|
| step (float) | stepsize |
|
||||||
| zshift (float) | shift in z |
|
| zshift (float) | shift in z |
|
||||||
| angle (float) | rotation angle (will rotate first) |
|
| angle (float) | rotation angle (will rotate first) |
|
||||||
| corridor_size (float) | corridor size for the corridor optimization. Default 3 um |
|
| corridor_size (float) | corridor size for the corridor optimization. Default 3 um |
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
`scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01)`
|
`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)`
|
||||||
|
|
||||||
#### Overview of the alignment steps
|
#### Overview of the alignment steps
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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.lsamx.update_user_parameter({'center':8.69})`
|
||||||
`dev.lsamy.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
|
#### 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.
|
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)
|
Loading the LamNI scripts is done by starting bec as
|
||||||
`bec.config.update_session_with_file("/bec/csaxs_bec/csaxs_bec/device_configs/lamni_config.yaml")`
|
`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`
|
`from csaxs_bec.bec_ipython_client.plugins.LamNI import LamNI`
|
||||||
`lamni = LamNI(bec)`
|
`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:
|
If the realtime system is restarted, BEC will lose communication. To restart:
|
||||||
`lamni.rt_off()` … then wait a 10 seconds
|
`lamni.rt_off()` … then wait a 10 seconds
|
||||||
`lamni.rt_on()`
|
`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.
|
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
|
### X-ray optics alignment
|
||||||
|
|
||||||
The positions of the optics stages are stored as stage parameters and are thus linked to the configuration file.
|
The positions of the optics stages are stored as stage parameters and are thus linked to the configuration file.
|
||||||
|
|||||||
@@ -327,14 +327,15 @@ The basic scan function can be called by `scans.omny_fermat_scan()` and offers a
|
|||||||
| fovy (float) | Fov in the piezo plane (i.e. piezo range). Max 100 um |
|
| fovy (float) | Fov in the piezo plane (i.e. piezo range). Max 100 um |
|
||||||
| cenx (float) | center position in x |
|
| cenx (float) | center position in x |
|
||||||
| ceny (float) | center position in y |
|
| ceny (float) | center position in y |
|
||||||
| exp_time (float) | exposure time |
|
| exp_time (float) | exposure time per frame |
|
||||||
|
| frames_per_trigger(int) | Number of burst frames per position |
|
||||||
| step (float) | stepsize |
|
| step (float) | stepsize |
|
||||||
| zshift (float) | shift in z |
|
| zshift (float) | shift in z |
|
||||||
| angle (float) | rotation angle (will rotate first) |
|
| angle (float) | rotation angle (will rotate first) |
|
||||||
| corridor_size (float) | corridor size for the corridor optimization. Default 3 um |
|
| corridor_size (float) | corridor size for the corridor optimization. Default 3 um |
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
`scans.omny_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01)`
|
`scans.omny_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)`
|
||||||
|
|
||||||
#### Overview of the alignment steps
|
#### Overview of the alignment steps
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|||||||
name = "csaxs_bec"
|
name = "csaxs_bec"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "The cSAXS plugin repository for BEC"
|
description = "The cSAXS plugin repository for BEC"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.11"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
|||||||
@@ -287,19 +287,20 @@ def test_ddg1_stage(mock_ddg1: DDG1):
|
|||||||
mock_ddg1.stage()
|
mock_ddg1.stage()
|
||||||
|
|
||||||
shutter_width = mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger
|
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_mode.get(), 1) # burst mode is enabled
|
||||||
assert np.isclose(mock_ddg1.burst_delay.get(), 0)
|
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
|
# Trigger DDG2 through EXT/EN
|
||||||
assert np.isclose(mock_ddg1.ab.delay.get(), 2e-3)
|
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
|
# Shutter channel cd
|
||||||
assert np.isclose(mock_ddg1.cd.delay.get(), 0)
|
assert np.isclose(mock_ddg1.cd.delay.get(), 0)
|
||||||
assert np.isclose(mock_ddg1.cd.width.get(), shutter_width)
|
assert np.isclose(mock_ddg1.cd.width.get(), shutter_width)
|
||||||
# MCS channel ef or gate
|
# 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 np.isclose(mock_ddg1.ef.width.get(), 1e-6)
|
||||||
|
|
||||||
assert mock_ddg1.staged == ophyd.Staged.yes
|
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):
|
def test_on_trigger_roi_signal(ids_camera):
|
||||||
"""Test the on_trigger method to ensure it processes the ROI signal correctly."""
|
"""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_image = np.array([[2, 4], [6, 8]])
|
||||||
test_mask = np.array([[1, 0], [0, 1]], dtype=np.uint8)
|
test_mask = np.array([[1, 0], [0, 1]], dtype=np.uint8)
|
||||||
ids_camera.mask = test_mask
|
ids_camera.mask = test_mask
|
||||||
|
|||||||
190
tests/tests_devices/test_panda.py
Normal file
190
tests/tests_devices/test_panda.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"""Module for testing the PandaBoxCSAXS and PandaBoxOMNY devices."""
|
||||||
|
|
||||||
|
# pylint: skip-file
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from ophyd import Staged
|
||||||
|
|
||||||
|
from csaxs_bec.devices.panda_box.panda_box import PandaBoxCSAXS
|
||||||
|
from csaxs_bec.devices.panda_box.panda_box_omny import PandaBoxOMNY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def panda_omny():
|
||||||
|
dev_name = "panda_omny"
|
||||||
|
dev = PandaBoxOMNY(
|
||||||
|
name=dev_name,
|
||||||
|
host="omny-panda-box.psi.ch",
|
||||||
|
signal_alias={
|
||||||
|
"FMC_IN.VAL1.Min": "cap_voltage_fzp_y_min",
|
||||||
|
"FMC_IN.VAL1.Max": "cap_voltage_fzp_y_max",
|
||||||
|
"FMC_IN.VAL1.Mean": "cap_voltage_fzp_y_mean",
|
||||||
|
"FMC_IN.VAL2.Min": "cap_voltage_fzp_x_min",
|
||||||
|
"FMC_IN.VAL2.Max": "cap_voltage_fzp_x_max",
|
||||||
|
"FMC_IN.VAL2.Mean": "cap_voltage_fzp_x_mean",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
yield dev
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def panda_csaxs():
|
||||||
|
dev_name = "panda_csaxs"
|
||||||
|
dev = PandaBoxCSAXS(name=dev_name, host="csaxs-panda-box.psi.ch")
|
||||||
|
yield dev
|
||||||
|
|
||||||
|
|
||||||
|
def test_panda_omny(panda_omny):
|
||||||
|
assert panda_omny.name == "panda_omny"
|
||||||
|
assert panda_omny.host == "omny-panda-box.psi.ch"
|
||||||
|
|
||||||
|
all_signal_names = [name for name, _ in panda_omny.data.signals]
|
||||||
|
# Check that the signal aliases are correctly set up
|
||||||
|
assert "cap_voltage_fzp_y_min" in all_signal_names
|
||||||
|
assert "cap_voltage_fzp_y_max" in all_signal_names
|
||||||
|
assert "cap_voltage_fzp_y_mean" in all_signal_names
|
||||||
|
assert "cap_voltage_fzp_x_min" in all_signal_names
|
||||||
|
assert "cap_voltage_fzp_x_max" in all_signal_names
|
||||||
|
assert "cap_voltage_fzp_x_mean" in all_signal_names
|
||||||
|
|
||||||
|
# Check that the original signal names are not present
|
||||||
|
assert "FMC_IN.VAL1.Min" not in all_signal_names
|
||||||
|
assert "FMC_IN.VAL1.Max" not in all_signal_names
|
||||||
|
assert "FMC_IN.VAL1.Mean" not in all_signal_names
|
||||||
|
assert "FMC_IN.VAL2.Min" not in all_signal_names
|
||||||
|
assert "FMC_IN.VAL2.Max" not in all_signal_names
|
||||||
|
assert "FMC_IN.VAL2.Mean" not in all_signal_names
|
||||||
|
|
||||||
|
assert panda_omny._acquisition_group == "burst"
|
||||||
|
assert panda_omny._timeout_on_completed == 10
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scan_type, frames_per_trigger, expected_acquisition_group",
|
||||||
|
[
|
||||||
|
("fly", 1, "fly"),
|
||||||
|
("fly", 5, "fly"),
|
||||||
|
("step", 10, "burst"),
|
||||||
|
("step", 1, "monitored"), # Default case
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_panda_omny_stage(panda_omny, scan_type, frames_per_trigger, expected_acquisition_group):
|
||||||
|
# Check that the stage signal is present and has the correct PV
|
||||||
|
assert len(panda_omny._status_callbacks) == 0
|
||||||
|
|
||||||
|
panda_omny.scan_info.msg.scan_type = scan_type
|
||||||
|
panda_omny.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
|
||||||
|
panda_omny.stage()
|
||||||
|
|
||||||
|
assert panda_omny._acquisition_group == expected_acquisition_group
|
||||||
|
assert panda_omny.staged == Staged.yes
|
||||||
|
|
||||||
|
|
||||||
|
def test_panda_omny_complete(panda_omny):
|
||||||
|
"""Test the on_complete method of the PandaBoxCSAXS device."""
|
||||||
|
panda_omny.scan_info.msg.num_points = 1
|
||||||
|
panda_omny.scan_info.msg.scan_parameters["frames_per_trigger"] = 1
|
||||||
|
|
||||||
|
panda_omny._timeout_on_completed = 0.5 # Set a short timeout for testing
|
||||||
|
|
||||||
|
def _mock_return_captured(*args, **kwargs):
|
||||||
|
return ["=0"]
|
||||||
|
|
||||||
|
# Timeout Error on complete
|
||||||
|
with (
|
||||||
|
mock.patch.object(panda_omny, "send_raw", side_effect=_mock_return_captured),
|
||||||
|
mock.patch.object(panda_omny, "_disarm", return_value=None) as mock_disarm,
|
||||||
|
):
|
||||||
|
status = panda_omny.on_complete()
|
||||||
|
assert status.done is False
|
||||||
|
assert status.success is False
|
||||||
|
|
||||||
|
with pytest.raises(TimeoutError):
|
||||||
|
status.wait(timeout=4)
|
||||||
|
mock_disarm.assert_called_once()
|
||||||
|
|
||||||
|
# Successful complete
|
||||||
|
panda_omny._timeout_on_completed = 5
|
||||||
|
with (
|
||||||
|
mock.patch.object(panda_omny, "send_raw", side_effect=[["=0"], ["=0"], ["=1"]]),
|
||||||
|
mock.patch.object(panda_omny, "_disarm", return_value=None) as mock_disarm,
|
||||||
|
):
|
||||||
|
status = panda_omny.on_complete()
|
||||||
|
assert status.done is False
|
||||||
|
assert status.success is False
|
||||||
|
|
||||||
|
status.wait(timeout=4)
|
||||||
|
mock_disarm.assert_called_once()
|
||||||
|
assert status.done is True
|
||||||
|
assert status.success is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_panda_csaxs(panda_csaxs):
|
||||||
|
assert panda_csaxs.name == "panda_csaxs"
|
||||||
|
assert panda_csaxs.host == "csaxs-panda-box.psi.ch"
|
||||||
|
|
||||||
|
assert panda_csaxs._acquisition_group == "burst"
|
||||||
|
assert panda_csaxs._timeout_on_completed == 10
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scan_type, frames_per_trigger, expected_acquisition_group",
|
||||||
|
[
|
||||||
|
("fly", 1, "fly"),
|
||||||
|
("fly", 5, "fly"),
|
||||||
|
("step", 10, "burst"),
|
||||||
|
("step", 1, "monitored"), # Default case
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_panda_csaxs_stage(panda_csaxs, scan_type, frames_per_trigger, expected_acquisition_group):
|
||||||
|
"""Test the on_stage method of the PandaBoxCSAXS device for different scan types and frames per trigger."""
|
||||||
|
assert len(panda_csaxs._status_callbacks) == 0
|
||||||
|
|
||||||
|
panda_csaxs.scan_info.msg.scan_type = scan_type
|
||||||
|
panda_csaxs.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
|
||||||
|
panda_csaxs.stage()
|
||||||
|
|
||||||
|
assert panda_csaxs._acquisition_group == expected_acquisition_group
|
||||||
|
assert panda_csaxs.staged == Staged.yes
|
||||||
|
|
||||||
|
|
||||||
|
def test_panda_csaxs_complete(panda_csaxs):
|
||||||
|
"""Test the on_complete method of the PandaBoxCSAXS device."""
|
||||||
|
panda_csaxs.scan_info.msg.num_points = 1
|
||||||
|
panda_csaxs.scan_info.msg.scan_parameters["frames_per_trigger"] = 1
|
||||||
|
|
||||||
|
panda_csaxs._timeout_on_completed = 0.5 # Set a short timeout for testing
|
||||||
|
|
||||||
|
def _mock_return_captured(*args, **kwargs):
|
||||||
|
return ["=0"]
|
||||||
|
|
||||||
|
# Timeout Error on complete
|
||||||
|
with (
|
||||||
|
mock.patch.object(panda_csaxs, "send_raw", side_effect=_mock_return_captured),
|
||||||
|
mock.patch.object(panda_csaxs, "_disarm", return_value=None) as mock_disarm,
|
||||||
|
):
|
||||||
|
status = panda_csaxs.on_complete()
|
||||||
|
assert status.done is False
|
||||||
|
assert status.success is False
|
||||||
|
|
||||||
|
with pytest.raises(TimeoutError):
|
||||||
|
status.wait(timeout=4)
|
||||||
|
mock_disarm.assert_called_once()
|
||||||
|
|
||||||
|
# Successful complete
|
||||||
|
panda_csaxs._timeout_on_completed = 5
|
||||||
|
with (
|
||||||
|
mock.patch.object(panda_csaxs, "send_raw", side_effect=[["=0"], ["=0"], ["=1"]]),
|
||||||
|
mock.patch.object(panda_csaxs, "_disarm", return_value=None) as mock_disarm,
|
||||||
|
):
|
||||||
|
status = panda_csaxs.on_complete()
|
||||||
|
assert status.done is False
|
||||||
|
assert status.success is False
|
||||||
|
|
||||||
|
status.wait(timeout=4)
|
||||||
|
mock_disarm.assert_called_once()
|
||||||
|
assert status.done is True
|
||||||
|
assert status.success is True
|
||||||
@@ -229,6 +229,22 @@ def device_manager_mock():
|
|||||||
"kwargs": {},
|
"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(
|
messages.DeviceInstructionMessage(
|
||||||
metadata={
|
metadata={
|
||||||
"readout_priority": "monitored",
|
"readout_priority": "monitored",
|
||||||
@@ -302,6 +318,36 @@ def device_manager_mock():
|
|||||||
action="set",
|
action="set",
|
||||||
parameter={"value": 2.1508313829565293},
|
parameter={"value": 2.1508313829565293},
|
||||||
),
|
),
|
||||||
|
messages.DeviceInstructionMessage(
|
||||||
|
metadata={
|
||||||
|
"readout_priority": "monitored",
|
||||||
|
"RID": "1234",
|
||||||
|
"device_instr_id": "diid",
|
||||||
|
},
|
||||||
|
device=["bpm4i", "lsamx", "lsamy", "samx", "samy"],
|
||||||
|
action="pre_scan",
|
||||||
|
parameter={},
|
||||||
|
),
|
||||||
|
messages.DeviceInstructionMessage(
|
||||||
|
metadata={
|
||||||
|
"readout_priority": "monitored",
|
||||||
|
"RID": "1234",
|
||||||
|
"device_instr_id": "diid",
|
||||||
|
},
|
||||||
|
device="rtx",
|
||||||
|
action="set",
|
||||||
|
parameter={"value": 1.3681828686580249},
|
||||||
|
),
|
||||||
|
messages.DeviceInstructionMessage(
|
||||||
|
metadata={
|
||||||
|
"readout_priority": "monitored",
|
||||||
|
"RID": "1234",
|
||||||
|
"device_instr_id": "diid",
|
||||||
|
},
|
||||||
|
device="rty",
|
||||||
|
action="set",
|
||||||
|
parameter={"value": 2.1508313829565293},
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
messages.DeviceInstructionMessage(
|
messages.DeviceInstructionMessage(
|
||||||
metadata={
|
metadata={
|
||||||
|
|||||||
Reference in New Issue
Block a user