1 Commits

Author SHA1 Message Date
gac-x06da d2b18d367a Found local changes on deployment system 2024-06-20 15:50:57 +02:00
81 changed files with 177 additions and 11548 deletions
-9
View File
@@ -1,9 +0,0 @@
# Do not edit this file!
# It is needed to track the repo template version, and editing may break things.
# This file will be overwritten by copier on template updates.
_commit: v1.4.0
_src_path: https://github.com/bec-project/plugin_copier_template.git
make_commit: false
project_name: pxiii_bec
widget_plugins_input: null
-3
View File
@@ -1,3 +0,0 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
semantic-release changelog -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__
semantic-release version -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__
-3
View File
@@ -1,3 +0,0 @@
black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
isort --line-length=100 --profile=black --multi-line=3 --trailing-comma $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
git add $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
-102
View File
@@ -1,102 +0,0 @@
name: CI for pxiii_bec
on:
push:
pull_request:
workflow_dispatch:
inputs:
BEC_WIDGETS_BRANCH:
description: "Branch of BEC Widgets to install"
required: false
type: string
default: "main"
BEC_CORE_BRANCH:
description: "Branch of BEC Core to install"
required: false
type: string
default: "main"
OPHYD_DEVICES_BRANCH:
description: "Branch of Ophyd Devices to install"
required: false
type: string
default: "main"
BEC_PLUGIN_REPO_BRANCH:
description: "Branch of the BEC Plugin Repository to install"
required: false
type: string
default: "main"
PYTHON_VERSION:
description: "Python version to use"
required: false
type: string
default: "3.12"
permissions:
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
env:
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "${{ inputs.PYTHON_VERSION || '3.12' }}"
- name: Checkout BEC Plugin Repository
uses: actions/checkout@v4
with:
repository: bec/pxiii_bec
ref: "${{ inputs.BEC_PLUGIN_REPO_BRANCH || github.head_ref || github.sha }}"
path: ./pxiii_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
uses: actions/checkout@v4
with:
repository: bec/bec
ref: "${{ inputs.BEC_CORE_BRANCH || 'main' }}"
path: ./bec
- name: Checkout Ophyd Devices
uses: actions/checkout@v4
with:
repository: bec/ophyd_devices
ref: "${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}"
path: ./ophyd_devices
- name: Checkout BEC Widgets
uses: actions/checkout@v4
with:
repository: bec/bec_widgets
ref: "${{ inputs.BEC_WIDGETS_BRANCH || 'main' }}"
path: ./bec_widgets
- name: Install dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
- name: Install Python dependencies
shell: bash
run: |
pip install uv
uv pip install --system -e ./ophyd_devices
uv pip install --system -e ./bec/bec_lib[dev]
uv pip install --system -e ./bec/bec_ipython_client
uv pip install --system -e ./bec/bec_server[dev]
uv pip install --system -e ./bec_widgets[dev,pyside6]
uv pip install --system -e ./pxiii_bec
- name: Run Pytest with Coverage
id: coverage
run: pytest --random-order --cov=./pxiii_bec --cov-config=./pxiii_bec/pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail ./pxiii_bec/tests/ || test $? -eq 5
-70
View File
@@ -1,70 +0,0 @@
name: Create template upgrade PR for pxiii_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: Checkout
uses: actions/checkout@v4
- name: Create virtualenv
run: |
python -m virtualenv .venv
- name: Install tools
run: |
source .venv/bin/activate
pip install copier PySide6 bec_lib
- name: Perform update
run: |
source .venv/bin/activate
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..."
copier update --trust --defaults --conflict inline 2>&1 | tee copier.log
status=${PIPESTATUS[0]}
output="$(cat copier.log)"
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\"
}"
-3
View File
@@ -8,9 +8,6 @@
**/.pytest_cache
**/*.egg*
# recovery_config files
recovery_config_*
# file writer data
**.h5
-29
View File
@@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2025, Paul Scherrer Institute
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-1
View File
@@ -1 +0,0 @@
# Add anything you don't want to check in to git, e.g. very large files
+11
View File
@@ -0,0 +1,11 @@
# This file is used to select the BEC and Ophyd Devices version for the auto deployment process.
# Do not edit this file unless you know what you are doing!
# The version can be a git tag, branch or commit hash.
# BEC version to use
BEC_AUTODEPLOY_VERSION="master"
# ophyd_devices version to use
OPHYD_DEVICES_AUTODEPLOY_VERSION="master"
+18
View File
@@ -0,0 +1,18 @@
redis:
host: localhost
port: 6379
mongodb:
host: localhost
port: 27017
scibec:
host: http://[::1]
port: 3030
beamline: "PXIII"
service_config:
general:
reset_queue_on_cancel: True
enforce_ACLs: False
file_writer:
plugin: default_NeXus_format
base_path: ./
+29
View File
@@ -0,0 +1,29 @@
# deployment script to be translated to Ansible
# can be removed once we have the autodeployment in place
BEAMLINE_REPO=gitlab.psi.ch:bec/pxiii-bec.git
git clone git@$BEAMLINE_REPO
module add psi-python311/2024.02
# start redis
docker run --network=host --name redis-bec -d redis
# alternative:
# conda install -y redis; redis-server &
# get the target versions for ophyd_devices and BEC
source ./pxiii-bec/deployment/autodeploy_versions
git clone -b $OPHYD_DEVICES_AUTODEPLOY_VERSION https://gitlab.psi.ch/bec/ophyd_devices.git
git clone -b $BEC_AUTODEPLOY_VERSION https://gitlab.psi.ch/bec/bec.git
# install BEC
cd bec
source ./bin/install_bec_dev.sh
cd ../
pip install -e ./pxiii-bec
# start the BEC server
bec-server start --config ./pxiii-bec/deployment/bec-server-config.yaml
@@ -0,0 +1,2 @@
def beam_stop_scan():
print("running beam stop scan")
@@ -1,13 +1,9 @@
"""
Pre-startup script for BEC client. This script is executed before the BEC client
is started. It can be used to add additional command line arguments.
is started. It can be used to add additional command line arguments.
"""
import os
from bec_lib.service_config import ServiceConfig
import pxiii_bec
from bec_lib import ServiceConfig
def extend_command_line_args(parser):
@@ -22,11 +18,6 @@ def extend_command_line_args(parser):
def get_config() -> ServiceConfig:
"""
Create and return the ServiceConfig for the plugin repository
Create and return the service configuration.
"""
deployment_path = os.path.dirname(os.path.dirname(os.path.dirname(pxiii_bec.__file__)))
files = os.listdir(deployment_path)
if "bec_config.yaml" in files:
return ServiceConfig(config_path=os.path.join(deployment_path, "bec_config.yaml"))
else:
return ServiceConfig(redis={"host": "localhost", "port": 6379})
return ServiceConfig(redis={"host": "x06da-bec-001", "port": 6379})
@@ -1,135 +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>1801</width>
<height>1459</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1801</width>
<height>1459</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Control Panel</string>
</attribute>
<layout class="QGridLayout" name="gridLayout" rowstretch="3,4" columnstretch="2,5">
<item row="0" column="0">
<widget class="Waveform" name="waveform"/>
</item>
<item row="0" column="1">
<widget class="ScanControl" name="scan_control"/>
</item>
<item row="1" column="0">
<widget class="ScanHistory" name="scan_history"/>
</item>
<item row="1" column="1">
<widget class="BECQueue" name="bec_queue"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Logbook</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>24</pointsize>
</font>
</property>
<property name="text">
<string>Coming soon...</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Take a break</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="Minesweeper" name="minesweeper"/>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1073</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ScanControl</class>
<extends>QWidget</extends>
<header>scan_control</header>
</customwidget>
<customwidget>
<class>Waveform</class>
<extends>QWidget</extends>
<header>waveform</header>
</customwidget>
<customwidget>
<class>BECQueue</class>
<extends>QWidget</extends>
<header>bec_queue</header>
</customwidget>
<customwidget>
<class>Minesweeper</class>
<extends>QWidget</extends>
<header>minesweeper</header>
</customwidget>
<customwidget>
<class>ScanHistory</class>
<extends>QWidget</extends>
<header>scan_history</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+1 -1
View File
@@ -1 +1 @@
from .auto_updates import PlotUpdate
+39
View File
@@ -0,0 +1,39 @@
from bec_widgets.cli import AutoUpdates, ScanInfo
class PlotUpdate(AutoUpdates):
# def simple_line_scan(self, info: ScanInfo) -> None:
# """
# Simple line scan.
# """
# dev_x = info.scan_report_devices[0]
# dev_y = self.get_selected_device(info.monitored_devices, self.figure.selected_device)
# if not dev_y:
# return
# self.figure.clear_all()
# plt = self.figure.plot(dev_x, dev_y)
# plt.set(title=f"PXIII: Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
def px_default_line_scan(self, info:ScanInfo):
dev_x = info.scan_report_devices[0]
dev_y = "bpm4i"
self.figure.clear_all()
plt = self.figure.plot(dev_x, dev_y)
plt.set(title="Custom line scan with samy")
def handler(self, info: ScanInfo) -> None:
# EXAMPLES:
# if info.scan_name == "line_scan" and info.scan_report_devices:
# self.simple_line_scan(info)
# return
# if info.scan_name == "grid_scan" and info.scan_report_devices:
# self.simple_grid_scan(info)
# return
if info.scan_report_devices and info.scan_report_devices[0] == "samy":
self.px_default_line_scan(info)
return
super().handler(info)
@@ -1 +0,0 @@
from .auto_updates import AutoUpdates
@@ -1,71 +0,0 @@
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_lib.messages import ScanStatusMessage
from bec_widgets.cli.rpc.rpc_base import RPCResponseTimeoutError
class PlotUpdate(AutoUpdates):
#######################################################################
################# GUI Callbacks #######################################
#######################################################################
def on_start(self) -> None:
"""
Procedure to run when the auto updates are enabled.
"""
self.start_default_dock()
def on_stop(self) -> None:
"""
Procedure to run when the auto updates are disabled.
"""
def on_scan_open(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan starts.
Args:
msg (ScanStatusMessage): The scan status message.
"""
if msg.scan_name == "line_scan" and msg.scan_report_devices:
return self.simple_line_scan(msg)
if msg.scan_name == "grid_scan" and msg.scan_report_devices:
return self.simple_grid_scan(msg)
dev_x = msg.scan_report_devices[0]
if "kwargs" in msg.request_inputs:
dev_y = msg.request_inputs["kwargs"].get("plot", None)
if dev_y is not None:
# Set the dock to the waveform widget
wf = self.set_dock_to_widget("Waveform")
# Clear the waveform widget and plot the data
wf.clear_all()
wf.plot(
x_name=dev_x,
y_name=dev_y,
label=f"Scan {msg.info.scan_number} - {dev_y}",
title=f"Scan {msg.info.scan_number}",
x_label=dev_x,
y_label=dev_y,
)
elif msg.scan_report_devices:
return self.best_effort(msg)
return None
def on_scan_closed(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan ends.
Args:
msg (ScanStatusMessage): The scan status message.
"""
def on_scan_abort(self, msg: ScanStatusMessage) -> None:
"""
Procedure to run when a scan is aborted.
Args:
msg (ScanStatusMessage): The scan status message.
"""
-42
View File
@@ -1,42 +0,0 @@
# This file was automatically generated by generate_cli.py
# type: ignore
from __future__ import annotations
from bec_lib.logger import bec_logger
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
logger = bec_logger.logger
# pylint: skip-file
_Widgets = {
"ScanHistory": "ScanHistory",
}
class ScanHistory(RPCBase):
_IMPORT_MODULE = "pxiii_bec.bec_widgets.widgets.scan_history.scan_history"
@rpc_call
def select_scan_from_history(self, value: "int") -> "None":
"""
Set scan from CLI.
Args:
value (int) : value from history -1 ...-10000
"""
@rpc_call
def add_scan_from_history(self) -> "None":
"""
Load selected scan from history.
"""
@rpc_call
def clear_plot(self) -> "None":
"""
Delete all curves on the plot.
"""
@@ -1,13 +0,0 @@
# This file was automatically generated by generate_cli.py
# type: ignore
from __future__ import annotations
# pylint: skip-file
designer_plugins = {
"ScanHistory": ("pxiii_bec.bec_widgets.widgets.scan_history.scan_history", "ScanHistory"),
}
widget_icons = {
"ScanHistory": "widgets",
}
@@ -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 pxiii_bec.bec_widgets.widgets.scan_history.scan_history_plugin import ScanHistoryPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(ScanHistoryPlugin())
if __name__ == "__main__": # pragma: no cover
main()
@@ -1,191 +0,0 @@
from __future__ import annotations
import os
from typing import TYPE_CHECKING, TypedDict
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.ui_loader import UILoader
from qtpy.QtWidgets import QVBoxLayout, QWidget
logger = bec_logger.logger
if TYPE_CHECKING:
from qtpy.QtWidgets import QPushButton, QLabel, QSpinBox
from bec_widgets.widgets.plots.waveform.waveform import Waveform
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
from bec_widgets.widgets.editors.text_box.text_box import TextBox
class ScanHistoryUIComponents(TypedDict):
waveform: Waveform
metadata_text_box: TextBox
monitor_label: QLabel
monitor_combobox: DeviceComboBox
history_label: QLabel
history_spin_box: QSpinBox
history_add: QPushButton
history_clear: QPushButton
class ScanHistory(BECWidget, QWidget):
USER_ACCESS = ["select_scan_from_history", "add_scan_from_history", "clear_plot"]
PLUGIN = True
ui_file = "./scan_history.ui"
components: ScanHistoryUIComponents
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)
self.components: ScanHistoryUIComponents = {
"waveform" : self.ui.waveform,
"metadata_text_box" : self.ui.metadata_text_box,
"monitor_label" : self.ui.monitor_label,
"monitor_combobox" : self.ui.monitor_combobox,
"history_label" : self.ui.history_label,
"history_spin_box" : self.ui.history_spin_box,
"history_add" : self.ui.history_add,
"history_clear" : self.ui.history_clear,
}
icon_options = {"size": (16, 16), "convert_to_pixmap": False}
self.components['monitor_combobox'].apply_filter = False
self.components['monitor_combobox'].devices = ['dccm_diode_bottom', 'dccm_diode_top', 'dccm_xbpm', 'ssxbpm', 'xbox_diode']
self.components['history_spin_box'].setMinimum(-10000)
self.components['history_spin_box'].setMaximum(-1)
self.components['history_spin_box'].valueChanged.connect(self._scan_history_selected)
self._scan_history_selected(-1)
self.components['history_spin_box'].setValue(-1)
self.components['history_add'].setText("Load")
self.components['history_add'].setStyleSheet(
"background-color: #129490; color: white; font-weight: bold; font-size: 12px;"
)
self.components['history_clear'].setText("Clear")
self.components['history_clear'].setStyleSheet(
"background-color: #065143; color: white; font-weight: bold; font-size: 12px;"
)
self.components['history_add'].clicked.connect(self._refresh_plot)
self.components['history_clear'].clicked.connect(self.clear_plot)
self.setWindowTitle("Scan History")
self._scan_history_selected(-1)
@SafeSlot()
def add_scan_from_history(self) -> None:
"""Load selected scan from history."""
self.components['history_add'].click()
@SafeSlot()
def clear_plot(self) -> None:
"""Delete all curves on the plot."""
self.components['waveform'].clear_all()
@SafeSlot()
def _refresh_plot(self) -> None:
"""Refresh plot."""
spin_box_value = self.components['history_spin_box'].value()
self._check_scan_in_history(spin_box_value)
# Get the data from the client
data = self.client.history[spin_box_value]
# Check that the plot does not already have a curve with the same data
scan_number = int(data.metadata.bec['scan_number'])
monitor_name = self.components['monitor_combobox'].currentText()
# Get signal hints
signal_name = getattr(self.client.device_manager.devices, monitor_name)._hints
signal_name = signal_name[0] if len(signal_name)>0 else signal_name
curve_label = f"Scan-{scan_number}-{monitor_name}-{signal_name}"
if len([curve for curve in self.components['waveform'].curves if curve.config.label == curve_label]):
return
if not hasattr(data.devices, monitor_name):
raise ValueError(f"Device {monitor_name} not found in data.")
# Get scan motors and check that the plot x_axis motor is the same as the scan motor, if not, clear the plot
scan_motors = [motor.decode() for motor in data.metadata.bec['scan_motors']]
x_motor_name = self.components['waveform'].x_mode
if x_motor_name not in scan_motors:
self.clear_plot()
self.components['waveform'].x_mode = x_motor_name = scan_motors[0]
# fetching the data
monitor_data = getattr(data.devices, monitor_name).read()[signal_name]['value']
motor_data = getattr(data.devices, x_motor_name).read()[x_motor_name]['value']
# Plot custom curve, with custom label
self.components['waveform'].plot(x=motor_data, y=monitor_data, label=curve_label)
x_label = f"{x_motor_name} / [{getattr(self.client.device_manager.devices, x_motor_name).egu()}]"
self.components['waveform'].x_label = x_label
def _check_scan_in_history(self, history_value:int) -> None:
"""
Check if scan is in history.
Args:
history_value (int): Value from history -1...-10000
"""
if len(self.client.history) < abs(history_value):
self.components['metadata_text_box'].set_plain_text(f"Scan history does not have the request scan {history_value} of history with length: {len(self.client.history)}")
return
def select_scan_from_history(self, value:int) -> None:
"""
Set scan from CLI.
Args:
value (int) : value from history -1 ...-10000
"""
if value >=0:
raise ValueError(f"Value must be smaller or equal -1, provided {value}")
self.components['history_spin_box'].setValue(value)
@SafeSlot(int)
def _scan_history_selected(self, spin_box_value:int) -> None:
self._check_scan_in_history(spin_box_value)
data = self.client.history[spin_box_value]
data.metadata.bec['scan_motors'][0].decode()
text = str(data)
scan_motor_text = "\n" + "Scan Motors: "
for motor in data.metadata.bec['scan_motors']:
scan_motor_text += f" {motor.decode()}"
self.components['metadata_text_box'].set_plain_text(text + scan_motor_text)
@SafeSlot(str)
def _set_x_axis(self, device_x:str) -> None:
self.components['waveform'].x_mode = device_x
@SafeSlot(str)
def _plot_new_device(self, device:str) -> None:
# if len(curve for curve in self.components["waveform"].curves if curve.config.label == f"{device}-{device}":
self.components["waveform"].plot(device)
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = ScanHistory()
widget.show()
sys.exit(app.exec_())
@@ -1 +0,0 @@
{'files': ['scan_history.py']}
@@ -1,115 +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>955</width>
<height>796</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="9,3">
<item>
<widget class="Waveform" name="waveform">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="monitor_label">
<property name="font">
<font/>
</property>
<property name="text">
<string>BPM Monitor</string>
</property>
</widget>
</item>
<item>
<widget class="DeviceComboBox" name="monitor_combobox"/>
</item>
<item>
<widget class="QLabel" name="history_label">
<property name="text">
<string>Scan History</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="history_spin_box"/>
</item>
<item>
<widget class="QPushButton" name="history_add">
<property name="text">
<string>Add scan</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="history_clear">
<property name="text">
<string>clear all</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="TextBox" name="metadata_text_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>795</width>
<height>191</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>TextBox</class>
<extends>QWidget</extends>
<header>text_box</header>
</customwidget>
<customwidget>
<class>DeviceComboBox</class>
<extends>QComboBox</extends>
<header>device_combobox</header>
</customwidget>
<customwidget>
<class>Waveform</class>
<extends>QWidget</extends>
<header>waveform</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 pxiii_bec.bec_widgets.widgets.scan_history.scan_history import ScanHistory
DOM_XML = """
<ui language='c++'>
<widget class='ScanHistory' name='scan_history'>
</widget>
</ui>
"""
class ScanHistoryPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = ScanHistory(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return designer_material_icon(ScanHistory.ICON_NAME)
def includeFile(self):
return "scan_history"
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 "ScanHistory"
def toolTip(self):
return "ScanHistory"
def whatsThis(self):
return self.toolTip()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,7 @@ import os
def setup_epics_ca():
os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO"
os.environ["EPICS_CA_ADDR_LIST"] = "129.129.110.255"
os.environ["EPICS_CA_ADDR_LIST"] = "129.129.122.255 sls-x12sa-cagw.psi.ch:5836"
os.environ["PYTHONIOENCODING"] = "latin1"
View File
@@ -1,11 +0,0 @@
import os
def setup_epics_ca():
# os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO"
# os.environ["EPICS_CA_ADDR_LIST"] = "129.129.122.255 sls-x12sa-cagw.psi.ch:5836"
os.environ["PYTHONIOENCODING"] = "latin1"
def run():
setup_epics_ca()
@@ -1,151 +0,0 @@
states:
robot_sample_exchange:
allow_modifiers: true
bl_pos: in
bl_bright: 'off'
bs_pos: in
bs_z: safe
coll_y: out
cryo_pos: in
det_cov: 'close'
diag_y: out
fl_bright: 'off'
gon_x: in
# smargon: not implemented
xrf_pos: out
sample_alignment:
allow_modifiers: true
bl_pos: in
bl_bright: 'on'
bs_pos: in
bs_z: safe
coll_y: out
cryo_pos: in
det_cov: 'close'
diag_y: out
fl_bright: 'on'
gon_x: in
# smargon: not implemented
xrf_pos: out
data_collection:
allow_modifiers: true
bl_pos: out
bl_bright: 'off'
bs_pos: in
bs_z: safe
coll_y: in
cryo_pos: in
det_cov: 'open'
diag_y: out
fl_bright: 'on'
gon_x: in
# smargon: not implemented
xrf_pos: out
DC_XRF:
allow_modifiers: true
# bl_pos: out
bl_bright: 'off'
bs_pos: in
bs_z: safe
coll_y: in
cryo_pos: in
det_cov: 'close'
diag_y: out
fl_bright: 'on'
gon_x: in
# smargon: not implemented
xrf_pos: in
manual_sample_exchange:
allow_modifiers: true
bl_pos: out
bl_bright: 'off'
bs_pos: out
bs_z: safe
coll_y: park
cryo_pos: in
det_cov: 'close'
diag_y: park
fl_bright: 'off'
gon_x: in
# smargon: not implemented
xrf_pos: out
beam_visualisation:
bl_pos: out
bl_bright: 'off'
bs_pos: in
bs_z: safe
coll_y: out
cryo_pos: out
det_cov: 'close'
diag_y: scint
fl_bright: 'off'
gon_x: out
# smargon: not implemented
xrf_pos: out
flux_measurement:
bl_pos: in
bl_bright: 'off'
bs_pos: in
bs_z: safe
coll_y: out
cryo_pos: out
det_cov: 'close'
diag_y: i1
fl_bright: 'off'
gon_x: out
# smargon: not implemented
xrf_pos: out
beamstop_alignment:
bl_pos: out
bl_bright: 'off'
bs_pos: in
bs_z: samp
coll_y: out
cryo_pos: out
det_cov: 'close'
diag_y: out
fl_bright: 'on'
gon_x: out
# smargon: not implemented
xrf_pos: out
maintenance:
allow_modifiers: true
bl_pos: out
bl_bright: 'off'
bs_pos: out
bs_z: safe
coll_y: park
cryo_pos: in
det_cov: 'close'
diag_y: park
fl_bright: 'off'
gon_x: out
# smargon: not implemented
xrf_pos: out
xtal_snapshot:
allow_modifiers: true
bl_pos: in
bl_bright: 'on'
bs_pos: in
bs_z: safe
coll_y: intermediate
cryo_pos: in
det_cov: 'close'
diag_y: out
fl_bright: 'on'
gon_x: in
# smargon: not implemented
xrf_pos: out
-81
View File
@@ -1,81 +0,0 @@
import csv
import json
def str_to_bool(val):
return str(val).strip().lower() in ["yes", "true", "1"]
def create(INPUT_CSV, OUTPUT_YAML):
with open(INPUT_CSV, newline="") as csvfile:
reader = csv.DictReader(csvfile)
with open(OUTPUT_YAML, "w") as yamlfile:
for row in reader:
include = row["include"]
name = row["name"]
desc = row["description"]
device_class = row["deviceClass"]
pv = row["PV"]
readout_priority = row["readoutPriority"]
tag = row["tag"]
read_only = str_to_bool(row["readOnly"])
user_param = row.get("userParameter", "").strip()
if str(include).strip().lower() != "yes":
continue
yamlfile.write(f"{name}:\n")
yamlfile.write(f" description: {desc}\n")
if device_class == "Motor" or device_class == "MotorEC":
yamlfile.write(f" deviceClass: ophyd_devices.Epics{device_class}\n")
yamlfile.write(f" deviceConfig: {{prefix: '{pv}'}}\n")
else:
yamlfile.write(f" deviceClass: ophyd.Epics{device_class}\n")
yamlfile.write(
f" deviceConfig: {{read_pv: '{pv}', auto_monitor: true}}\n"
)
yamlfile.write(" onFailure: buffer\n")
yamlfile.write(" enabled: True\n")
yamlfile.write(f" readoutPriority: {readout_priority}\n")
yamlfile.write(" deviceTags:\n")
yamlfile.write(f" - {tag}\n")
yamlfile.write(f" readOnly: {read_only}\n")
yamlfile.write(" softwareTrigger: false\n")
# Only add userParameter for Motors if present
# if device_class == "Motor" and user_param:
if user_param:
try:
parsed = json.loads(user_param)
yamlfile.write(" userParameter:\n")
for k, v in parsed.items():
yamlfile.write(f" {k}: {v}\n")
except json.JSONDecodeError:
yamlfile.write(f" userParameter: {user_param}\n")
yamlfile.write("\n")
print(f"YAML written to {OUTPUT_YAML}")
def main():
devices = "pxiii-standard-devices"
states = "pxiii-state-devices"
device_files = [
f"{devices}.csv",
f"{devices}.yaml"
]
state_files = [
f"{states}.csv",
f"{states}.yaml"
]
create(device_files[0],device_files[1])
create(state_files[0],state_files[1])
main()
@@ -1,127 +0,0 @@
name,description,deviceClass,PV,readoutPriority,tag,readOnly,include,userParameter,
sls_current,SLS Current,SignalRO,ARS07-DPCT-0100:CURR,monitored,SLS,yes,yes,,
fe_sl_xr,FE Slit X Ring,MotorEC,X06DA-FE-SLDI:TRXR,baseline,fe,no,yes,,
fe_sl_yt,FE Slit Y Top,MotorEC,X06DA-FE-SLDI:TRYT,baseline,fe,no,yes,,
fe_sl_xw,FE Slit X Wall,MotorEC,X06DA-FE-SLDI:TRXW,baseline,fe,no,yes,,
fe_sl_yb,FE Slit Y Bottom,MotorEC,X06DA-FE-SLDI:TRYB,baseline,fe,no,yes,,
fe_sl_xcen,FE Slit X Centre,MotorEC,X06DA-FE-SLDI:CENTERX,baseline,fe,no,yes,,
fe_sl_xsize,FE Slit X Size,MotorEC,X06DA-FE-SLDI:SIZEX,baseline,fe,no,yes,,
fe_sl_ycen,FE Slit Y Centre,MotorEC,X06DA-FE-SLDI:CENTERY,baseline,fe,no,yes,,
fe_sl_ysize,FE Slit Y Size,MotorEC,X06DA-FE-SLDI:SIZEY,baseline,fe,no,yes,,
tm_xu,TorM Upstream X,MotorEC,X06DA-FE-MI1:TRXU,baseline,tm,no,yes,,
tm_xd,TorM Downstream X,MotorEC,X06DA-FE-MI1:TRXD,baseline,tm,no,yes,,
tm_yur,TorM Upstream Ring Y,MotorEC,X06DA-FE-MI1:TRYUR,baseline,tm,no,yes,,
tm_yw,TorM Wall Y,MotorEC,X06DA-FE-MI1:TRYUW,baseline,tm,no,yes,,
tm_yd,TorM Downstream Y,MotorEC,X06DA-FE-MI1:TRYD,baseline,tm,no,yes,,
tm_b1,TorM Bender,MotorEC,X06DA-FE-MI1:BEND1,baseline,tm,no,yes,,
tm_yaw,TorM Virtual Yaw,MotorEC,X06DA-FE-MI1:YAW,baseline,tm,no,yes,,
tm_roll,TorM Virtual Roll,MotorEC,X06DA-FE-MI1:ROLL,baseline,tm,no,yes,,
tm_pitch,TorM Virtual Pitch,MotorEC,X06DA-FE-MI1:PITCH,baseline,tm,no,yes,,
tm_x,TorM Virtual X,MotorEC,X06DA-FE-MI1:TRX,baseline,tm,no,yes,,
tm_y,TorM Virtual Y ,MotorEC,X06DA-FE-MI1:TRY,baseline,tm,no,yes,,
bsf_bpm1,BSF BPM Channel 1,SignalRO,X06DA-OP-BSFBPM:SIGNAL1,monitored,bpm,yes,no,,
bsf_bpm2,BSF BPM Channel 2,SignalRO,X06DA-OP-BSFBPM:SIGNAL2,monitored,bpm,yes,no,,
bsf_bpm3,BSF BPM Channel 3,SignalRO,X06DA-OP-BSFBPM:SIGNAL3,monitored,bpm,yes,no,,
bsf_bpm4,BSF BPM Channel 4,SignalRO,X06DA-OP-BSFBPM:SIGNAL4,monitored,bpm,yes,no,,
bsf_bpmsum,BSF BPM Summed,SignalRO,X06DA-OP-BSFBPM:SUM,monitored,bpm,yes,no,,
bsf_sl_xw,BSF Slit outboard,MotorEC,X06DA-OP-BSFSLH:TRXW,baseline,bsf,no,yes,,
bsf_sl_xr,BSF Slit inboard,MotorEC,X06DA-OP-BSFSLH:TRXR,baseline,bsf,no,yes,,
bsf_sl_xcen,BSF X Centre,MotorEC,X06DA-OP-BSFSLH:CENTER,baseline,bsf,no,yes,,
bsf_sl_xsize,BSF X Size,MotorEC,X06DA-OP-BSFSLH:SIZE,baseline,bsf,no,yes,,
bsf_f1_y,BSF Filter 1 Y,MotorEC,X06DA-OP-BSFFI1:TRY,baseline,bsf,no,yes,,
dccm_theta1,DCCM Theta Xtal1,MotorEC,X06DA-OP-DCCM:ROTX-CR1,baseline,dccm,no,yes,,
dccm_theta2,DCCM Theta Xtal2,MotorEC,X06DA-OP-DCCM:ROTX-CR2,baseline,dccm,no,yes,,
dccm_rotz,DCCM RotZ Xtal 2,MotorEC,X06DA-OP-DCCM:ROTZ-CR2,baseline,dccm,no,yes,,
dccm_xbpm1_y,DCCM BPM1 Y,MotorEC,X06DA-OP-DCCMXBPM1:TRY,baseline,dccm,no,yes,,
dccm_xbpm2_y,DCCM BPM2 Y,MotorEC,X06DA-OP-DCCMXBPM2:TRY,baseline,dccm,no,yes,,
dccm_energy,DCCM Energy,Motor,X06DA-OP-DCCM:ENERGY,baseline,dccm,no,yes,,
dccm_di_top,DCCM Diode Top,SignalRO,X06DA-OP-DCCMXBPM1T:READOUT,monitored,dccm,no,yes,,
dccm_di_bot,DCCM Diode Bottom,SignalRO,X06DA-OP-DCCMXBPM1B:READOUT,monitored,dccm,no,yes,,
dccm_bpm1,DCCM BPM Channel 1,SignalRO,X06DA-OP-DCCMXBPM2:Current1:MeanValue_RBV,monitored,dccm,no,yes,,
dccm_bpm2,DCCM BPM Channel 2,SignalRO,X06DA-OP-DCCMXBPM2:Current2:MeanValue_RBV,monitored,dccm,no,yes,,
dccm_bpm3,DCCM BPM Channel 3,SignalRO,X06DA-OP-DCCMXBPM2:Current3:MeanValue_RBV,monitored,dccm,no,yes,,
dccm_bpm4,DCCM BPM Channel 4,SignalRO,X06DA-OP-DCCMXBPM2:Current4:MeanValue_RBV,monitored,dccm,no,yes,,
dccm_bpmsum,DCCM BPM Summed,SignalRO,X06DA-OP-DCCMXBPM2:SumAll:MeanValue_RBV,monitored,dccm,no,yes,,
ss_bpm1,SS BPM Channel 1,SignalRO,X06DA-ES-SSBPM:Current1:MeanValue_RBV,monitored,bpm,yes,yes,,
ss_bpm2,SS BPM Channel 2,SignalRO,X06DA-ES-SSBPM:Current2:MeanValue_RBV,monitored,bpm,yes,yes,,
ss_bpm3,SS BPM Channel 3,SignalRO,X06DA-ES-SSBPM:Current3:MeanValue_RBV,monitored,bpm,yes,yes,,
ss_bpm4,SS BPM Channel 4,SignalRO,X06DA-ES-SSBPM:Current4:MeanValue_RBV,monitored,bpm,yes,yes,,
ss_bpmsum,SS BPM Summed,SignalRO,X06DA-ES-SSBPM:SumAll:MeanValue_RBV,monitored,bpm,yes,yes,,
ss_bpm_x,SS BPM X,Motor,X06DA-ES-SSBPM:TRX,baseline,ss,no,yes,,
ss_bpm_y,SS BPM Y,Motor,X06DA-ES-SSBPM:TRY,baseline,ss,no,yes,,
ss_sl_xw,SS Slit Wall,Motor,X06DA-ES-SSSLH:TRXW,baseline,ss,no,yes,,
ss_sl_xr,SS Slit Ring,Motor,X06DA-ES-SSSLH:TRXR,baseline,ss,no,yes,,
ss_sl_xcen,SS Slit X Centre,Motor,X06DA-ES-SSSLH:CENTER,baseline,ss,no,yes,,
ss_sl_xsize,SS Slit X Size,Motor,X06DA-ES-SSSLH:SIZE,baseline,ss,no,yes,,
ss_sl_yt,SS Slit Top,Motor,X06DA-ES-SSSLV:TRYT,baseline,ss,no,yes,,
ss_sl_yb,SS Slit Bottom,Motor,X06DA-ES-SSSLV:TRYB,baseline,ss,no,yes,,
ss_sl_ycen,SS Slit Y Centre,Motor,X06DA-ES-SSSLV:CENTER,baseline,ss,no,yes,,
ss_sl_ysize,SS Slit Y Size,Motor,X06DA-ES-SSSLV:SIZE,baseline,ss,no,yes,,
ss_xi_x,SS X-ray Eye X,Motor,X06DA-ES-SSXI:TRX,baseline,ss,no,yes,"{""type"": multi-position,""in"": 7.5, ""out"": -2.1}",
ss_xi_y,SS X-ray Eye Y,Motor,X06DA-ES-SSXI:TRY,baseline,ss,no,yes,,
ss_xicam_x,SS Camera X,SignalRO,X06DA-ES-SSCAM:Stats5:CentroidX_RBV,baseline,ss,yes,yes,,
ss_xicam_y,SS Camera Y,SignalRO,X06DA-ES-SSCAM:Stats5:CentroidY_RBV,baseline,ss,yes,yes,,
ss_xicam_max,SS Cam Max,SignalRO,X06DA-ES-SSCAM:Stats5:MaxValue_RBV,monitored,ss,yes,yes,,
ss_xicam_exp,SS Camera Exposure,Signal,X06DA-ES-SSCAM:cam1:AcquireTime,baseline,ss,no,yes,,
ss_xicam_gain,SS Camera Gain,Signal,X06DA-ES-SSCAM:cam1:Gain,baseline,ss,no,yes,,
ss_xicam_xsig,SS Camera X Sigma,Signal,X06DA-ES-SSCAM:Stats5:SigmaX_RBV,baseline,ss,yes,yes,,
ss_xicam_ysig,SS Camera Y Sigma,Signal,X06DA-ES-SSCAM:Stats5:SigmaY_RBV,baseline,ss,yes,yes,,
vfm_xu,VFM Upstream X,MotorEC,X06DA-ES-VFM:TRXU,baseline,vfm,no,yes,,
vfm_xd,VFM Downstream X,MotorEC,X06DA-ES-VFM:TRXD,baseline,vfm,no,yes,,
vfm_yur,VFM Upstream Ring Y,MotorEC,X06DA-ES-VFM:TRYUR,baseline,vfm,no,yes,,
vfm_yw,VFM Wall Y,MotorEC,X06DA-ES-VFM:TRYW,baseline,vfm,no,yes,,
vfm_ydr,VFM Downstream Ring Y,MotorEC,X06DA-ES-VFM:TRYDR,baseline,vfm,no,yes,,
vfm_bu,VFM Upstream Bender,MotorEC,X06DA-ES-VFM:BNDU,baseline,vfm,no,yes,,
vfm_bd,VFM Downstream Bender,MotorEC,X06DA-ES-VFM:BNDD,baseline,vfm,no,yes,,
vfm_yaw,VFM Virtual Yaw,MotorEC,X06DA-ES-VFM:YAW,baseline,vfm,no,yes,,
vfm_roll,VFM Virtual Roll,MotorEC,X06DA-ES-VFM:ROLL,baseline,vfm,no,yes,,
vfm_pitch,VFM Virtual Pitch,MotorEC,X06DA-ES-VFM:PITCH,baseline,vfm,no,yes,,
vfm_x,VFM Virtual X,MotorEC,X06DA-ES-VFM:TRX,baseline,vfm,no,yes,,
vfm_y,VFM Virtual Y ,MotorEC,X06DA-ES-VFM:TRY,baseline,vfm,no,yes,,
hfm_xu,HFM Upstream X,MotorEC,X06DA-ES-HFM:TRXU,baseline,hfm,no,yes,,
hfm_xd,HFM Downstream X,MotorEC,X06DA-ES-HFM:TRXD,baseline,hfm,no,yes,,
hfm_yuw,HFM Upstream Wall Y,MotorEC,X06DA-ES-HFM:TRYUW,baseline,hfm,no,yes,,
hfm_yr,HFM Ring Y,MotorEC,X06DA-ES-HFM:TRYR,baseline,hfm,no,yes,,
hfm_ydw,HFM Downstream Wall Y,MotorEC,X06DA-ES-HFM:TRYDW,baseline,hfm,no,yes,,
hfm_bu,HFM Upstream Bender,MotorEC,X06DA-ES-HFM:BNDU,baseline,hfm,no,yes,,
hfm_bd,HFM Downstream Bender,MotorEC,X06DA-ES-HFM:BNDD,baseline,hfm,no,yes,,
hfm_yaw,HFM Virtual Yaw,MotorEC,X06DA-ES-HFM:YAW,baseline,hfm,no,yes,,
hfm_roll,HFM Virtual Roll,MotorEC,X06DA-ES-HFM:ROLL,baseline,hfm,no,yes,,
hfm_pitch,HFM Virtual Pitch,MotorEC,X06DA-ES-HFM:PITCH,baseline,hfm,no,yes,,
hfm_x,HFM Virtual X,MotorEC,X06DA-ES-HFM:TRX,baseline,hfm,no,yes,,
hfm_y,HFM Virtual Y ,MotorEC,X06DA-ES-HFM:TRY,baseline,hfm,no,yes,,
bcu_bpm1,BCU BPM Channel 1 ,SignalRO,X06DA-ES-BCBPM:Current1:MeanValue_RBV,monitored,bpm,yes,yes,,
bcu_bpm2,BCU BPM Channel 2,SignalRO,X06DA-ES-BCBPM:Current2:MeanValue_RBV,monitored,bpm,yes,yes,,
bcu_bpm3,BCU BPM Channel 3,SignalRO,X06DA-ES-BCBPM:Current3:MeanValue_RBV,monitored,bpm,yes,yes,,
bcu_bpm4,BCU BPM Channel 4,SignalRO,X06DA-ES-BCBPM:Current4:MeanValue_RBV,monitored,bpm,yes,yes,,
bcu_bpmsum,BCU BPM Summed,SignalRO,X06DA-ES-BCBPM:SumAll:MeanValue_RBV,monitored,bpm,yes,yes,,
bcu_bpm_x,BCU BPM X,Motor,X06DA-ES-BCBPM:TRX,baseline,bcu,no,yes,,
bcu_bpm_y,BCU BPM Y ,Motor,X06DA-ES-BCBPM:TRY,baseline,bcu,no,yes,,
bcu_sl_xw,BCU Slit Wall,Motor,X06DA-ES-BCSLH:TRXW,baseline,bcu,no,yes,,
bcu_sl_xr,BCU Slit Ring,Motor,X06DA-ES-BCSLH:TRXR,baseline,bcu,no,yes,,
bcu_sl_xcen,BCU Slit X Centre,Motor,X06DA-ES-BCSLH:CENTER,baseline,bcu,no,yes,,
bcu_sl_xsize,BCU Slit X Size,Motor,X06DA-ES-BCSLH:SIZE,baseline,bcu,no,yes,,
bcu_sl_yt,BCU Slit top,Motor,X06DA-ES-BCSLV:TRYT,baseline,bcu,no,yes,,
bcu_sl_yb,BCU Slit Bottom,Motor,X06DA-ES-BCSLV:TRYB,baseline,bcu,no,yes,,
bcu_sl_ycen,BCU Slit Y Centre,Motor,X06DA-ES-BCSLV:CENTER,baseline,bcu,no,yes,,
bcu_sl_ysize,BCU Slit Y Size,Motor,X06DA-ES-BCSLV:SIZE,baseline,bcu,no,yes,,
samcam_x,Sample Camera X ,SignalRO,X06DA-ES-MS:Stats5:CentroidX_RBV,baseline,scam,yes,no,,
samcam_xsig,Sample Camera X Sigma,SignalRO,X06DA-ES-MS:Stats5:SigmaX_RBV,monitored,scam,yes,no,,
samcam_y,Sample Camera Y ,SignalRO,X06DA-ES-MS:Stats5:CentroidY_RBV,baseline,scam,yes,no,,
samcam_ysig,Sample Camera Y Sigma,SignalRO,X06DA-ES-MS:Stats5:SigmaY_RBV,monitored,scam,yes,no,,
samcam_max,Sample Camera Max,SignalRO,X06DA-ES-MS:Stats5:MaxValue_RBV,monitored,scam,yes,no,,
samcam_exp,Sample Camera Exposure,Signal,X06DA-ES-MS:cam1:AcquireTime,baseline,scam,no,no,,
samcam_gain,Sample Camera Gain,Signal,X06DA-ES-MS:cam1:Gain,baseline,scam,no,no,,
scam_zoom,Sample Camera Zoom,Motor,X06DA-ES-MS:ZOOM,baseline,scam,no,yes,,
coll_x,Collimator X,Motor,X06DA-ES-COL:TRX,baseline,se,no,no,,
diag_z,Scintillator/diode Z,Motor,X06DA-ES-SCL:TRZ,baseline,se,no,no,,
i1,I1 diode,SignalRO,X06DA-ES-SCLDI:READOUT,monitored,bpm,yes,no,,
bs_x,Beamstop X,Motor,X06DA-ES-BS:TRX,baseline,se,no,no,,
bs_y,Beamstop Y,Motor,X06DA-ES-BS:TRY,baseline,se,no,no,,
gon_y,Goniometer Y,Motor,X06DA-ES-DF1:TRY1,baseline,det,no,no,,
gon_z,Goniometer X,Motor,X06DA-ES-DF1:TRZ1,baseline,det,no,no,,
omega,Omega,Motor,X06DA-ES-DF1:ROTU,baseline,det,no,no,,
cryo_x,Cryo X ,Motor,X06DA-ES-CS:TRX,baseline,se,no,no,,
cryo_temp,Cryo Temperature,SignalRO,X06DA-ES-CS:TEMP_RBV,baseline,se,no,no,,
det_y,Detector Y,MotorEC,X06DA-ES-DET:TRY,baseline,det,no,no,,
det_z,Detector Z,MotorEC,X06DA-ES-DET:TRZ,baseline,det,no,no,,
1 name description deviceClass PV readoutPriority tag readOnly include userParameter
2 sls_current SLS Current SignalRO ARS07-DPCT-0100:CURR monitored SLS yes yes
3 fe_sl_xr FE Slit X Ring MotorEC X06DA-FE-SLDI:TRXR baseline fe no yes
4 fe_sl_yt FE Slit Y Top MotorEC X06DA-FE-SLDI:TRYT baseline fe no yes
5 fe_sl_xw FE Slit X Wall MotorEC X06DA-FE-SLDI:TRXW baseline fe no yes
6 fe_sl_yb FE Slit Y Bottom MotorEC X06DA-FE-SLDI:TRYB baseline fe no yes
7 fe_sl_xcen FE Slit X Centre MotorEC X06DA-FE-SLDI:CENTERX baseline fe no yes
8 fe_sl_xsize FE Slit X Size MotorEC X06DA-FE-SLDI:SIZEX baseline fe no yes
9 fe_sl_ycen FE Slit Y Centre MotorEC X06DA-FE-SLDI:CENTERY baseline fe no yes
10 fe_sl_ysize FE Slit Y Size MotorEC X06DA-FE-SLDI:SIZEY baseline fe no yes
11 tm_xu TorM Upstream X MotorEC X06DA-FE-MI1:TRXU baseline tm no yes
12 tm_xd TorM Downstream X MotorEC X06DA-FE-MI1:TRXD baseline tm no yes
13 tm_yur TorM Upstream Ring Y MotorEC X06DA-FE-MI1:TRYUR baseline tm no yes
14 tm_yw TorM Wall Y MotorEC X06DA-FE-MI1:TRYUW baseline tm no yes
15 tm_yd TorM Downstream Y MotorEC X06DA-FE-MI1:TRYD baseline tm no yes
16 tm_b1 TorM Bender MotorEC X06DA-FE-MI1:BEND1 baseline tm no yes
17 tm_yaw TorM Virtual Yaw MotorEC X06DA-FE-MI1:YAW baseline tm no yes
18 tm_roll TorM Virtual Roll MotorEC X06DA-FE-MI1:ROLL baseline tm no yes
19 tm_pitch TorM Virtual Pitch MotorEC X06DA-FE-MI1:PITCH baseline tm no yes
20 tm_x TorM Virtual X MotorEC X06DA-FE-MI1:TRX baseline tm no yes
21 tm_y TorM Virtual Y MotorEC X06DA-FE-MI1:TRY baseline tm no yes
22 bsf_bpm1 BSF BPM Channel 1 SignalRO X06DA-OP-BSFBPM:SIGNAL1 monitored bpm yes no
23 bsf_bpm2 BSF BPM Channel 2 SignalRO X06DA-OP-BSFBPM:SIGNAL2 monitored bpm yes no
24 bsf_bpm3 BSF BPM Channel 3 SignalRO X06DA-OP-BSFBPM:SIGNAL3 monitored bpm yes no
25 bsf_bpm4 BSF BPM Channel 4 SignalRO X06DA-OP-BSFBPM:SIGNAL4 monitored bpm yes no
26 bsf_bpmsum BSF BPM Summed SignalRO X06DA-OP-BSFBPM:SUM monitored bpm yes no
27 bsf_sl_xw BSF Slit outboard MotorEC X06DA-OP-BSFSLH:TRXW baseline bsf no yes
28 bsf_sl_xr BSF Slit inboard MotorEC X06DA-OP-BSFSLH:TRXR baseline bsf no yes
29 bsf_sl_xcen BSF X Centre MotorEC X06DA-OP-BSFSLH:CENTER baseline bsf no yes
30 bsf_sl_xsize BSF X Size MotorEC X06DA-OP-BSFSLH:SIZE baseline bsf no yes
31 bsf_f1_y BSF Filter 1 Y MotorEC X06DA-OP-BSFFI1:TRY baseline bsf no yes
32 dccm_theta1 DCCM Theta Xtal1 MotorEC X06DA-OP-DCCM:ROTX-CR1 baseline dccm no yes
33 dccm_theta2 DCCM Theta Xtal2 MotorEC X06DA-OP-DCCM:ROTX-CR2 baseline dccm no yes
34 dccm_rotz DCCM RotZ Xtal 2 MotorEC X06DA-OP-DCCM:ROTZ-CR2 baseline dccm no yes
35 dccm_xbpm1_y DCCM BPM1 Y MotorEC X06DA-OP-DCCMXBPM1:TRY baseline dccm no yes
36 dccm_xbpm2_y DCCM BPM2 Y MotorEC X06DA-OP-DCCMXBPM2:TRY baseline dccm no yes
37 dccm_energy DCCM Energy Motor X06DA-OP-DCCM:ENERGY baseline dccm no yes
38 dccm_di_top DCCM Diode Top SignalRO X06DA-OP-DCCMXBPM1T:READOUT monitored dccm no yes
39 dccm_di_bot DCCM Diode Bottom SignalRO X06DA-OP-DCCMXBPM1B:READOUT monitored dccm no yes
40 dccm_bpm1 DCCM BPM Channel 1 SignalRO X06DA-OP-DCCMXBPM2:Current1:MeanValue_RBV monitored dccm no yes
41 dccm_bpm2 DCCM BPM Channel 2 SignalRO X06DA-OP-DCCMXBPM2:Current2:MeanValue_RBV monitored dccm no yes
42 dccm_bpm3 DCCM BPM Channel 3 SignalRO X06DA-OP-DCCMXBPM2:Current3:MeanValue_RBV monitored dccm no yes
43 dccm_bpm4 DCCM BPM Channel 4 SignalRO X06DA-OP-DCCMXBPM2:Current4:MeanValue_RBV monitored dccm no yes
44 dccm_bpmsum DCCM BPM Summed SignalRO X06DA-OP-DCCMXBPM2:SumAll:MeanValue_RBV monitored dccm no yes
45 ss_bpm1 SS BPM Channel 1 SignalRO X06DA-ES-SSBPM:Current1:MeanValue_RBV monitored bpm yes yes
46 ss_bpm2 SS BPM Channel 2 SignalRO X06DA-ES-SSBPM:Current2:MeanValue_RBV monitored bpm yes yes
47 ss_bpm3 SS BPM Channel 3 SignalRO X06DA-ES-SSBPM:Current3:MeanValue_RBV monitored bpm yes yes
48 ss_bpm4 SS BPM Channel 4 SignalRO X06DA-ES-SSBPM:Current4:MeanValue_RBV monitored bpm yes yes
49 ss_bpmsum SS BPM Summed SignalRO X06DA-ES-SSBPM:SumAll:MeanValue_RBV monitored bpm yes yes
50 ss_bpm_x SS BPM X Motor X06DA-ES-SSBPM:TRX baseline ss no yes
51 ss_bpm_y SS BPM Y Motor X06DA-ES-SSBPM:TRY baseline ss no yes
52 ss_sl_xw SS Slit Wall Motor X06DA-ES-SSSLH:TRXW baseline ss no yes
53 ss_sl_xr SS Slit Ring Motor X06DA-ES-SSSLH:TRXR baseline ss no yes
54 ss_sl_xcen SS Slit X Centre Motor X06DA-ES-SSSLH:CENTER baseline ss no yes
55 ss_sl_xsize SS Slit X Size Motor X06DA-ES-SSSLH:SIZE baseline ss no yes
56 ss_sl_yt SS Slit Top Motor X06DA-ES-SSSLV:TRYT baseline ss no yes
57 ss_sl_yb SS Slit Bottom Motor X06DA-ES-SSSLV:TRYB baseline ss no yes
58 ss_sl_ycen SS Slit Y Centre Motor X06DA-ES-SSSLV:CENTER baseline ss no yes
59 ss_sl_ysize SS Slit Y Size Motor X06DA-ES-SSSLV:SIZE baseline ss no yes
60 ss_xi_x SS X-ray Eye X Motor X06DA-ES-SSXI:TRX baseline ss no yes {"type": multi-position,"in": 7.5, "out": -2.1}
61 ss_xi_y SS X-ray Eye Y Motor X06DA-ES-SSXI:TRY baseline ss no yes
62 ss_xicam_x SS Camera X SignalRO X06DA-ES-SSCAM:Stats5:CentroidX_RBV baseline ss yes yes
63 ss_xicam_y SS Camera Y SignalRO X06DA-ES-SSCAM:Stats5:CentroidY_RBV baseline ss yes yes
64 ss_xicam_max SS Cam Max SignalRO X06DA-ES-SSCAM:Stats5:MaxValue_RBV monitored ss yes yes
65 ss_xicam_exp SS Camera Exposure Signal X06DA-ES-SSCAM:cam1:AcquireTime baseline ss no yes
66 ss_xicam_gain SS Camera Gain Signal X06DA-ES-SSCAM:cam1:Gain baseline ss no yes
67 ss_xicam_xsig SS Camera X Sigma Signal X06DA-ES-SSCAM:Stats5:SigmaX_RBV baseline ss yes yes
68 ss_xicam_ysig SS Camera Y Sigma Signal X06DA-ES-SSCAM:Stats5:SigmaY_RBV baseline ss yes yes
69 vfm_xu VFM Upstream X MotorEC X06DA-ES-VFM:TRXU baseline vfm no yes
70 vfm_xd VFM Downstream X MotorEC X06DA-ES-VFM:TRXD baseline vfm no yes
71 vfm_yur VFM Upstream Ring Y MotorEC X06DA-ES-VFM:TRYUR baseline vfm no yes
72 vfm_yw VFM Wall Y MotorEC X06DA-ES-VFM:TRYW baseline vfm no yes
73 vfm_ydr VFM Downstream Ring Y MotorEC X06DA-ES-VFM:TRYDR baseline vfm no yes
74 vfm_bu VFM Upstream Bender MotorEC X06DA-ES-VFM:BNDU baseline vfm no yes
75 vfm_bd VFM Downstream Bender MotorEC X06DA-ES-VFM:BNDD baseline vfm no yes
76 vfm_yaw VFM Virtual Yaw MotorEC X06DA-ES-VFM:YAW baseline vfm no yes
77 vfm_roll VFM Virtual Roll MotorEC X06DA-ES-VFM:ROLL baseline vfm no yes
78 vfm_pitch VFM Virtual Pitch MotorEC X06DA-ES-VFM:PITCH baseline vfm no yes
79 vfm_x VFM Virtual X MotorEC X06DA-ES-VFM:TRX baseline vfm no yes
80 vfm_y VFM Virtual Y MotorEC X06DA-ES-VFM:TRY baseline vfm no yes
81 hfm_xu HFM Upstream X MotorEC X06DA-ES-HFM:TRXU baseline hfm no yes
82 hfm_xd HFM Downstream X MotorEC X06DA-ES-HFM:TRXD baseline hfm no yes
83 hfm_yuw HFM Upstream Wall Y MotorEC X06DA-ES-HFM:TRYUW baseline hfm no yes
84 hfm_yr HFM Ring Y MotorEC X06DA-ES-HFM:TRYR baseline hfm no yes
85 hfm_ydw HFM Downstream Wall Y MotorEC X06DA-ES-HFM:TRYDW baseline hfm no yes
86 hfm_bu HFM Upstream Bender MotorEC X06DA-ES-HFM:BNDU baseline hfm no yes
87 hfm_bd HFM Downstream Bender MotorEC X06DA-ES-HFM:BNDD baseline hfm no yes
88 hfm_yaw HFM Virtual Yaw MotorEC X06DA-ES-HFM:YAW baseline hfm no yes
89 hfm_roll HFM Virtual Roll MotorEC X06DA-ES-HFM:ROLL baseline hfm no yes
90 hfm_pitch HFM Virtual Pitch MotorEC X06DA-ES-HFM:PITCH baseline hfm no yes
91 hfm_x HFM Virtual X MotorEC X06DA-ES-HFM:TRX baseline hfm no yes
92 hfm_y HFM Virtual Y MotorEC X06DA-ES-HFM:TRY baseline hfm no yes
93 bcu_bpm1 BCU BPM Channel 1 SignalRO X06DA-ES-BCBPM:Current1:MeanValue_RBV monitored bpm yes yes
94 bcu_bpm2 BCU BPM Channel 2 SignalRO X06DA-ES-BCBPM:Current2:MeanValue_RBV monitored bpm yes yes
95 bcu_bpm3 BCU BPM Channel 3 SignalRO X06DA-ES-BCBPM:Current3:MeanValue_RBV monitored bpm yes yes
96 bcu_bpm4 BCU BPM Channel 4 SignalRO X06DA-ES-BCBPM:Current4:MeanValue_RBV monitored bpm yes yes
97 bcu_bpmsum BCU BPM Summed SignalRO X06DA-ES-BCBPM:SumAll:MeanValue_RBV monitored bpm yes yes
98 bcu_bpm_x BCU BPM X Motor X06DA-ES-BCBPM:TRX baseline bcu no yes
99 bcu_bpm_y BCU BPM Y Motor X06DA-ES-BCBPM:TRY baseline bcu no yes
100 bcu_sl_xw BCU Slit Wall Motor X06DA-ES-BCSLH:TRXW baseline bcu no yes
101 bcu_sl_xr BCU Slit Ring Motor X06DA-ES-BCSLH:TRXR baseline bcu no yes
102 bcu_sl_xcen BCU Slit X Centre Motor X06DA-ES-BCSLH:CENTER baseline bcu no yes
103 bcu_sl_xsize BCU Slit X Size Motor X06DA-ES-BCSLH:SIZE baseline bcu no yes
104 bcu_sl_yt BCU Slit top Motor X06DA-ES-BCSLV:TRYT baseline bcu no yes
105 bcu_sl_yb BCU Slit Bottom Motor X06DA-ES-BCSLV:TRYB baseline bcu no yes
106 bcu_sl_ycen BCU Slit Y Centre Motor X06DA-ES-BCSLV:CENTER baseline bcu no yes
107 bcu_sl_ysize BCU Slit Y Size Motor X06DA-ES-BCSLV:SIZE baseline bcu no yes
108 samcam_x Sample Camera X SignalRO X06DA-ES-MS:Stats5:CentroidX_RBV baseline scam yes no
109 samcam_xsig Sample Camera X Sigma SignalRO X06DA-ES-MS:Stats5:SigmaX_RBV monitored scam yes no
110 samcam_y Sample Camera Y SignalRO X06DA-ES-MS:Stats5:CentroidY_RBV baseline scam yes no
111 samcam_ysig Sample Camera Y Sigma SignalRO X06DA-ES-MS:Stats5:SigmaY_RBV monitored scam yes no
112 samcam_max Sample Camera Max SignalRO X06DA-ES-MS:Stats5:MaxValue_RBV monitored scam yes no
113 samcam_exp Sample Camera Exposure Signal X06DA-ES-MS:cam1:AcquireTime baseline scam no no
114 samcam_gain Sample Camera Gain Signal X06DA-ES-MS:cam1:Gain baseline scam no no
115 scam_zoom Sample Camera Zoom Motor X06DA-ES-MS:ZOOM baseline scam no yes
116 coll_x Collimator X Motor X06DA-ES-COL:TRX baseline se no no
117 diag_z Scintillator/diode Z Motor X06DA-ES-SCL:TRZ baseline se no no
118 i1 I1 diode SignalRO X06DA-ES-SCLDI:READOUT monitored bpm yes no
119 bs_x Beamstop X Motor X06DA-ES-BS:TRX baseline se no no
120 bs_y Beamstop Y Motor X06DA-ES-BS:TRY baseline se no no
121 gon_y Goniometer Y Motor X06DA-ES-DF1:TRY1 baseline det no no
122 gon_z Goniometer X Motor X06DA-ES-DF1:TRZ1 baseline det no no
123 omega Omega Motor X06DA-ES-DF1:ROTU baseline det no no
124 cryo_x Cryo X Motor X06DA-ES-CS:TRX baseline se no no
125 cryo_temp Cryo Temperature SignalRO X06DA-ES-CS:TEMP_RBV baseline se no no
126 det_y Detector Y MotorEC X06DA-ES-DET:TRY baseline det no no
127 det_z Detector Z MotorEC X06DA-ES-DET:TRZ baseline det no no
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
name,description,deviceClass,PV,readoutPriority,tag,readOnly,include,userParameter
bl_bright,Backlight Brightness,Signal,X06DA-ES-BL:SET,baseline,state,no,yes,
bl_pos,Backlight Positioner,Signal,X06DA-ES-BL:POS-SET,baseline,state,no,no,"{""type"":positioner}"
bs_pos,Beamstop Positioner,Signal,X06DA-ES-BS:POS-SET,baseline,state,no,no,"{""type"":positioner}"
bs_z,Beamstop Z,Motor,X06DA-ES-BS:TRZ,baseline,state,no,yes,"{""type"": guarded, ""min"": 13, ""samp"": 15, ""work_min"": 20, ""safe"": 41, ""max_blin"": 42, ""max_blout"": 70}"
coll_y,Collimator Y,Motor,X06DA-ES-COL:TRY,baseline,state,no,yes,"{""type"": multi-position, ""in"": 40, ""out"": 20.0, ""park"": 0,""tol"":0.05}"
cryo_pos,Cryo positioner,Signal,X06DA-ES-CS:POS-SET,baseline,state,no,no,"{""type"":positioner}"
det_cov,Detector cover,Signal,X06DA-ES-DETCOV:SET,baseline,state,no,no,"{""type"":positioner}"
diag_y,Scintillator/diode Y,Motor,X06DA-ES-SCL:TRY,baseline,state,no,yes,"{""type"": multi-position, ""scint"": 39, ""i1"": 44.0, ""out"": 20.0,""park"": 0,""tol"":0.3}"
fl_bright,Frontlight Brightness,Signal,X06DA-ES-FL:SET,baseline,state,no,yes,
xrf_pos,XRF Positioner,Signal,X06DA-ES-XRF:POS-SET,baseline,state,no,no,"{""type"":positioner}"
1 name description deviceClass PV readoutPriority tag readOnly include userParameter
2 bl_bright Backlight Brightness Signal X06DA-ES-BL:SET baseline state no yes
3 bl_pos Backlight Positioner Signal X06DA-ES-BL:POS-SET baseline state no no {"type":positioner}
4 bs_pos Beamstop Positioner Signal X06DA-ES-BS:POS-SET baseline state no no {"type":positioner}
5 bs_z Beamstop Z Motor X06DA-ES-BS:TRZ baseline state no yes {"type": guarded, "min": 13, "samp": 15, "work_min": 20, "safe": 41, "max_blin": 42, "max_blout": 70}
6 coll_y Collimator Y Motor X06DA-ES-COL:TRY baseline state no yes {"type": multi-position, "in": 40, "out": 20.0, "park": 0,"tol":0.05}
7 cryo_pos Cryo positioner Signal X06DA-ES-CS:POS-SET baseline state no no {"type":positioner}
8 det_cov Detector cover Signal X06DA-ES-DETCOV:SET baseline state no no {"type":positioner}
9 diag_y Scintillator/diode Y Motor X06DA-ES-SCL:TRY baseline state no yes {"type": multi-position, "scint": 39, "i1": 44.0, "out": 20.0,"park": 0,"tol":0.3}
10 fl_bright Frontlight Brightness Signal X06DA-ES-FL:SET baseline state no yes
11 xrf_pos XRF Positioner Signal X06DA-ES-XRF:POS-SET baseline state no no {"type":positioner}
@@ -1,63 +0,0 @@
bl_bright:
description: Backlight Brightness
deviceClass: ophyd.EpicsSignal
deviceConfig: {read_pv: 'X06DA-ES-BL:SET', auto_monitor: true}
onFailure: buffer
enabled: True
readoutPriority: baseline
deviceTags:
- state
readOnly: False
softwareTrigger: false
bs_z:
description: Beamstop Z
deviceClass: ophyd_devices.EpicsMotor
deviceConfig: {prefix: 'X06DA-ES-BS:TRZ'}
onFailure: buffer
enabled: True
readoutPriority: baseline
deviceTags:
- state
readOnly: False
softwareTrigger: false
userParameter: {"type": guarded, "min": 13, "samp": 15, "work_min": 20, "safe": 41, "max_blin": 42, "max_blout": 70}
coll_y:
description: Collimator Y
deviceClass: ophyd_devices.EpicsMotor
deviceConfig: {prefix: 'X06DA-ES-COL:TRY'}
onFailure: buffer
enabled: True
readoutPriority: baseline
deviceTags:
- state
readOnly: False
softwareTrigger: false
userParameter: {"type": multi-position, "in": 40, "out": 20.0, "park": 0,"tol":0.05}
diag_y:
description: Scintillator/diode Y
deviceClass: ophyd_devices.EpicsMotor
deviceConfig: {prefix: 'X06DA-ES-SCL:TRY'}
onFailure: buffer
enabled: True
readoutPriority: baseline
deviceTags:
- state
readOnly: False
softwareTrigger: false
userParameter: {"type": multi-position, "scint": 39, "i1": 44.0, "out": 20.0,"park": 0,"tol":0.3}
fl_bright:
description: Frontlight Brightness
deviceClass: ophyd.EpicsSignal
deviceConfig: {read_pv: 'X06DA-ES-FL:SET', auto_monitor: true}
onFailure: buffer
enabled: True
readoutPriority: baseline
deviceTags:
- state
readOnly: False
softwareTrigger: false
@@ -1,4 +0,0 @@
base_config:
- !include ./pxiii-standard-devices.yaml
states_config:
- !include ./pxiii-state-devices.yaml
-426
View File
@@ -1,426 +0,0 @@
"""
``Aerotech`` --- Aerotech control software
******************************************
This module provides an object to control the Aerotech Abr rotational stage.
Methods in the Abr class
========================
Standard bluesky interface:
AerotechAbrStage.configure(d={...})
AerotechAbrStage.kickoff()
AerotechAbrStage.stop()
Additional bluesky functionality:
Aerotech.is_homed()
Aerotech.do_homing(wait=True)
Aerotech.get_ready(ostart=None, orange=None, etime=None, wait=True)
Aerotech.is_done()
Aerotech.is_ready()
Aerotech.is_busy()
Aerotech.start_exposure()
Aerotech.wait_status(status)
Aerotech.move(angle, wait=False, speed=None)
Aerotech.set_shutter(state)
Returns the axis mode: ``-ES-DF1:AXES-MODE``
Examples
========
abr = AerotechAbrStage(prefix="X06DA-ES-DF1", name="abr")
# move omega to 270.0 degrees
abr.omega = 270.0
# move omega to 180 degrees and wait for movement to finish
abr.move(180, wait=True)
# move omega to 3000 degrees at 360 degrees/s and wait for movement to finish
abr.move(3000, velocity=360, wait=True)
# stop any movement
abr.stop() # this function only returns after the STATUS is back to OK
"""
import time
from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind
from ophyd.status import SubscriptionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
try:
from .A3200enums import AbrCmd, AbrMode
except ImportError:
from A3200enums import AbrCmd, AbrMode
from bec_lib import bec_logger
logger = bec_logger.logger
# pylint: disable=logging-fstring-interpolation
class AerotechAbrStage(PSIDeviceBase, Device):
"""Standard PX stage on A3200 controller
This is the wrapper class for the standard rotation stage layout for the PX
beamlines at SLS. It wraps the main rotation axis OMEGA (Aerotech ABR)and
the associated motion axes GMX, GMY and GMZ. The ophyd class associates to
the general PX measurement procedure, which is that the actual scan script
is running as an AeroBasic program on the controller and we communicate to
it via 10+1 global variables.
"""
USER_ACCESS = ["reset", "kickoff", "complete", "set_axis_mode", "arm", "disarm"]
taskStop = Component(EpicsSignal, "-AERO:TSK-STOP", put_complete=True, kind=Kind.omitted)
status = Component(EpicsSignal, "-AERO:STAT", put_complete=True, kind=Kind.omitted)
clear = Component(EpicsSignal, "-AERO:CTRL-CLFT", put_complete=True, kind=Kind.omitted)
# Enable/disable motor movement via the IOC (i.e. make it task-only)
axisModeLocked = Component(EpicsSignal, "-DF1:LOCK", put_complete=True, kind=Kind.omitted)
axisModeDirect = Component(
EpicsSignal, "-DF1:MODE-DIRECT", put_complete=True, kind=Kind.omitted
)
axisAxesMode = Component(EpicsSignal, "-DF1:AXES-MODE", put_complete=True, kind=Kind.omitted)
# Shutter box is missing readback so the -GET signal is installed on the VME
# _shutter = Component(
# EpicsSignal, "-PH1:GET", write_pv="-PH1:SET", put_complete=True, kind=Kind.config,
# )
# Status flags for all axes
omega_done = Component(EpicsSignalRO, "-DF1:OMEGA-DONE", kind=Kind.normal)
gmx_done = Component(EpicsSignalRO, "-DF1:GMX-DONE", kind=Kind.normal)
gmy_done = Component(EpicsSignalRO, "-DF1:GMY-DONE", kind=Kind.normal)
gmz_done = Component(EpicsSignalRO, "-DF1:GMZ-DONE", kind=Kind.normal)
# For some reason the task interface is called PSO...
scan_command = Component(EpicsSignal, "-PSO:CMD", put_complete=True, kind=Kind.omitted)
start_command = Component(
EpicsSignal, "-PSO:START-TEST.PROC", put_complete=True, kind=Kind.omitted
)
stop_command = Component(
EpicsSignal, "-PSO:STOP-TEST.PROC", put_complete=True, kind=Kind.omitted
)
# Global variables to controll AeroBasic scripts
_var_1 = Component(EpicsSignal, "-PSO:VAR-1", put_complete=True, kind=Kind.omitted)
_var_2 = Component(EpicsSignal, "-PSO:VAR-2", put_complete=True, kind=Kind.omitted)
_var_3 = Component(EpicsSignal, "-PSO:VAR-3", put_complete=True, kind=Kind.omitted)
_var_4 = Component(EpicsSignal, "-PSO:VAR-4", put_complete=True, kind=Kind.omitted)
_var_5 = Component(EpicsSignal, "-PSO:VAR-5", put_complete=True, kind=Kind.omitted)
_var_6 = Component(EpicsSignal, "-PSO:VAR-6", put_complete=True, kind=Kind.omitted)
_var_7 = Component(EpicsSignal, "-PSO:VAR-7", put_complete=True, kind=Kind.omitted)
_var_8 = Component(EpicsSignal, "-PSO:VAR-8", put_complete=True, kind=Kind.omitted)
_var_9 = Component(EpicsSignal, "-PSO:VAR-9", put_complete=True, kind=Kind.omitted)
_var_10 = Component(EpicsSignal, "-PSO:VAR-10", put_complete=True, kind=Kind.omitted)
# Task status PVs (programs always run on task 1)
task1 = Component(EpicsSignalRO, "-AERO:TSK1-DONE", auto_monitor=True)
task2 = Component(EpicsSignalRO, "-AERO:TSK2-DONE", auto_monitor=True)
task3 = Component(EpicsSignalRO, "-AERO:TSK3-DONE", auto_monitor=True)
task4 = Component(EpicsSignalRO, "-AERO:TSK4-DONE", auto_monitor=True)
scan_done = Component(EpicsSignal, "-GRD:SCAN-DONE", kind=Kind.config)
def __init__(
self,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
scan_info=None,
**kwargs,
):
# super() will call the mixin class
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
scan_info=scan_info,
**kwargs,
)
def set_axis_mode(self, mode: str, settle_time=0.1) -> None:
"""Set axis mode to direct/measurement mode.
Measurement ensures that the scrips run undisturbed by blocking axis
motion commands from the IOC (i.e. internal only).
Parameters:
-----------
mode : str
Valid values are 'direct' and 'measuring'.
"""
if mode == "direct":
self.axisModeDirect.set(37, settle_time=settle_time).wait()
if mode == "measuring":
self.axisAxesMode.set(AbrMode.MEASURING, settle_time=settle_time).wait()
def on_stage(self):
"""
NOTE: Zac's request is that stage is essentially ARM, i.e. get ready and don't do anything.
"""
d = {}
# FIXME: I don't care about how we fish out config parameters from scan info
scan_args = {
**self.scan_info.msg.request_inputs["inputs"],
**self.scan_info.msg.request_inputs["kwargs"],
**self.scan_info.msg.scan_parameters,
}
scanname = self.scan_info.msg.scan_name
if scanname in (
"standardscan",
"helicalscan",
"helicalscan1",
"helicalscan2",
"helicalscan3",
):
d["scan_command"] = AbrCmd.MEASURE_STANDARD
d["var_1"] = scan_args["start"]
d["var_2"] = scan_args["range"]
d["var_3"] = scan_args["move_time"]
d["var_4"] = scan_args.get("ready_rate", 500)
d["var_5"] = 0
d["var_6"] = 0
d["var_7"] = 0
# d["var_8"] = 0
# d["var_9"] = 0
if scanname in ("verticallinescan", "vlinescan"):
d["scan_command"] = AbrCmd.VERTICAL_LINE_SCAN
d["var_1"] = scan_args["range"] / scan_args["steps"]
d["var_2"] = scan_args["steps"]
d["var_3"] = scan_args["exp_time"]
d["var_4"] = 0
d["var_5"] = 0
d["var_6"] = 0
d["var_7"] = 0
# d["var_8"] = 0
# d["var_9"] = 0
if scanname in ("screeningscan"):
d["scan_command"] = AbrCmd.SCREENING
d["var_1"] = scan_args["start"]
d["var_2"] = scan_args["oscrange"]
d["var_3"] = scan_args["exp_time"]
d["var_4"] = scan_args["range"] / scan_args["steps"]
d["var_5"] = scan_args["steps"]
d["var_6"] = scan_args.get("delta", 0.5)
d["var_7"] = 0
# d["var_8"] = 0
# d["var_9"] = 0
if scanname in ("rasterscan", "rastersimplescan"):
d["scan_command"] = AbrCmd.RASTER_SCAN_SIMPLE
d["var_1"] = scan_args["exp_time"]
d["var_2"] = scan_args["range_x"] / scan_args["steps_x"]
d["var_3"] = scan_args["range_y"] / scan_args["steps_y"]
d["var_4"] = scan_args["steps_x"]
d["var_5"] = scan_args["steps_y"]
d["var_6"] = 0
d["var_7"] = 0
# d["var_8"] = 0
# d["var_9"] = 0
# Reconfigure if got a valid scan config
if len(d) > 0:
self.configure(d)
# Stage the ABR stage
self.arm()
def on_unstage(self):
"""Unstage the ABR controller"""
self.disarm()
def configure(self, d: dict) -> tuple:
""" " Configure the exposure scripts
Script execution at the PX beamlines is based on scripts that are always
running on the controller that execute commands when commanded by
setting a pre-defined set of global variables. This method performs the
configuration of the exposure scrips by setting the required global
variables.
Parameters in d: dict
---------------------
scan_command: int
The index of the desired AeroBasic program to be executed.
Usually supported values are taken from an Enum.
var_1:
var_2:
var_3:
var_4:
var_5:
var_6:
var_7:
var_8:
var_9:
var_10:
"""
old = self.read_configuration()
# ToDo: Check if idle before reconfiguring
self.scan_command.set(d["scan_command"]).wait()
# Set the corresponding global variables
if "var_1" in d and d["var_1"] is not None:
self._var_1.set(d["var_1"]).wait()
if "var_2" in d and d["var_2"] is not None:
self._var_2.set(d["var_2"]).wait()
if "var_3" in d and d["var_3"] is not None:
self._var_3.set(d["var_3"]).wait()
if "var_4" in d and d["var_4"] is not None:
self._var_4.set(d["var_4"]).wait()
if "var_5" in d and d["var_5"] is not None:
self._var_5.set(d["var_5"]).wait()
if "var_6" in d and d["var_6"] is not None:
self._var_6.set(d["var_6"]).wait()
if "var_7" in d and d["var_7"] is not None:
self._var_7.set(d["var_7"]).wait()
if "var_8" in d and d["var_8"] is not None:
self._var_8.set(d["var_8"]).wait()
if "var_9" in d and d["var_9"] is not None:
self._var_9.set(d["var_9"]).wait()
if "var_10" in d and d["var_10"] is not None:
self._var_10.set(d["var_10"]).wait()
new = self.read_configuration()
return old, new
def arm(self):
"""Bluesky-style stage
Since configuration synchronization is not guaranteed, this does
nothing. The script launched by kickoff().
"""
def on_kickoff(self, timeout=1) -> SubscriptionStatus:
"""Kick off the set program"""
self.start_command.set(1).wait()
# Define wait until the busy flag goes high
def is_busy(*, value, **_):
return bool(value == 0)
# Subscribe and wait for update
status = SubscriptionStatus(self.scan_done, is_busy, timeout=timeout, settle_time=0.1)
status.wait()
# return status
def disarm(self, settle_time=0.1):
"""Stops current script and releases the axes"""
# Disarm commands
self.scan_command.set(AbrCmd.NONE, settle_time=settle_time).wait()
self.stop_command.set(1).wait()
# Go to direct mode
self.set_axis_mode("direct", settle_time=settle_time)
def complete(self, timeout=None) -> SubscriptionStatus:
"""Waits for task execution
NOTE: Original complete was raster scanner complete...
"""
# Define wait until the busy flag goes down (excluding initial update)
def is_idle(*, value, **_):
return bool(value == 1)
# Subscribe and wait for update
# status = SubscriptionStatus(self.task1, is_idle, timeout=timeout, settle_time=0.5)
status = SubscriptionStatus(self.scan_done, is_idle, timeout=timeout, settle_time=0.5)
return status
def reset(self, settle_time=0.1, wait_after_reload=1) -> None:
"""Resets the Aerotech controller state
Attempts to reset the currently running measurement task on the A3200
by stopping current motions, reloading aerobasic programs and going
to DIRECT mode.
This will stop movements in both DIRECT and MEASURING modes. During the
stop the `status` temporarely goes to ERROR but reverts to OK after a
couple of seconds.
"""
# Disarm commands
self.scan_command.set(AbrCmd.NONE, settle_time=settle_time).wait()
self.stop_command.set(1, settle_time=settle_time)
# Reload tasks
self.taskStop.set(1, settle_time=wait_after_reload).wait()
# Go to direct mode
self.set_axis_mode("direct", settle_time=settle_time)
# pylint: disable=arguments-differ
def stop(self, settle_time=1.0) -> None:
"""Stops current motions"""
# Disarm commands
self.scan_command.set(AbrCmd.NONE, settle_time=settle_time).wait()
self.stop_command.set(1).wait()
# Go to direct mode
self.set_axis_mode("direct", settle_time=settle_time)
def is_ioc_ok(self):
"""Checks execution status"""
return 0 == self.status.get()
@property
def axis_mode(self):
"""Read axis mode"""
return self.axisAxesMode.get()
# @property
# def shutter(self):
# return self._shutter.get()
# @shutter.setter
# def shutter(self, value):
# if self.axisAxesMode.get():
# print("ABR is not in direct mode; cannot manipulate shutter")
# return False
# state = str(state).lower()
# if state not in ["1", "0", "closed", "open"]:
# print("unknown shutter state requested")
# return None
# elif state in ["1", "open"]:
# state = 1
# elif state == ["0", "closed"]:
# state = 0
# self._shutter.set(state).wait()
# return state == self._shutter.get()
def wait_for_movements(self, timeout=60.0):
"""Waits for all motor movements"""
t_start = time.time()
t_elapsed = 0
while self.is_moving() and t_elapsed < timeout:
t_elapsed = time.time() - t_start
if timeout is not None and t_elapsed > timeout:
raise TimeoutError("Timeout waiting for all axis to stop moving")
time.sleep(0.5)
def is_moving(self):
"""Chechs if all axes are DONE"""
return not (
self.omega_done.get()
and self.gmx_done.get()
and self.gmy_done.get()
and self.gmz_done.get()
)
if __name__ == "__main__":
abr = AerotechAbrStage(prefix="X06DA-ES", name="abr")
abr.wait_for_connection()
-74
View File
@@ -1,74 +0,0 @@
# -*- coding: utf-8 -*-
"""
Enumerations for the MX specific Aerotech A3200 stage.
@author: mohacsi_i
"""
# pylint: disable=too-few-public-methods
class AbrStatus:
"""ABR measurement task status"""
DONE = 0
READY = 1
BUSY = 2
class AbrGridStatus:
"""ABR grid scan status"""
BUSY = 0
DONE = 1
class AbrMode:
"""ABR mode status"""
DIRECT = 0
MEASURING = 1
class AbrShutterStatus:
"""ABR shutter status"""
CLOSE = 0
OPEN = 1
class AbrCmd:
"""ABR command table"""
NONE = 0
RASTER_SCAN_SIMPLE = 1
MEASURE_STANDARD = 2
VERTICAL_LINE_SCAN = 3
SCREENING = 4
SUPER_FAST_OMEGA = 5
STILL_WEDGE = 6
STILLS = 7
REPEAT_SINGLE_OSCILLATION = 8
SINGLE_OSCILLATION = 9
OLD_FASHIONED = 10
RASTER_SCAN = 11
JET_ROTATION = 12
X_HELICAL = 13
X_RUNSEQ = 14
JUNGFRAU = 15
MSOX = 16
SLIT_SCAN = 17
RASTER_SCAN_STILL = 18
SCAN_SASTT = 19
SCAN_SASTT_V2 = 20
SCAN_SASTT_V3 = 21
class AbrAxis:
"""ABR axis index"""
OMEGA = 1
GMX = 2
GMY = 3
GMZ = 4
STY = 5
STZ = 6
-259
View File
@@ -1,259 +0,0 @@
# -*- coding: utf-8 -*-
"""
Created on Tue Jun 11 11:28:38 2024
@author: mohacsi_i
"""
import types
from collections import OrderedDict
from ophyd import Component, PVPositioner, Signal, EpicsSignal, EpicsSignalRO, Kind, PositionerBase
from ophyd.status import Status, MoveStatus
from bec_lib import bec_logger
from .A3200enums import AbrMode
logger = bec_logger.logger
# ABR_DONE = 0
# ABR_READY = 1
# ABR_BUSY = 2
# GRID_SCAN_BUSY = 0
# GRID_SCAN_DONE = 1
# DIRECT_MODE = 0
# MEASURING_MODE = 1
class A3200Axis(PVPositioner):
"""Positioner wrapper for A3200 axes
Positioner wrapper for motors on the Aerotech A3200 controller. As the IOC
does not provide a motor record, this class simply wraps axes into a
standard Ophyd positioner for the BEC. It also has some additional
functionality for error checking and diagnostics.
Examples
--------
omega = A3200Axis('X06DA-ES-DF1:OMEGA', base_pv='X06DA-ES')
Parameters
----------
prefix : str
Axis PV name root.
base_pv : str (situational)
IOC PV name root, i.e. X06DA-ES if standalone class.
"""
USER_ACCESS = ["omove"]
abr_mode_direct = Component(
EpicsSignal, "-DF1:MODE-DIRECT", put_complete=True, kind=Kind.omitted
)
abr_mode = Component(
EpicsSignal, "-DF1:AXES-MODE", auto_monitor=True, put_complete=True, kind=Kind.omitted
)
# Basic PV positioner interface
done = Component(EpicsSignalRO, "-DONE", auto_monitor=True, kind=Kind.config)
readback = Component(EpicsSignalRO, "-RBV", auto_monitor=True, kind=Kind.hinted)
# Setpoint is one of the two...
setpoint = Component(EpicsSignal, "-SETP", kind=Kind.config)
# setpoint = Component(EpicsSignal, "-VAL", kind=Kind.config)
velocity = Component(EpicsSignal, "-SETV", kind=Kind.config)
status = Component(EpicsSignalRO, "-STAT", auto_monitor=True, kind=Kind.config)
# PV to issue native relative movements on the A3200
relmove = Component(EpicsSignal, "-INCP", put_complete=True, kind=Kind.config)
# PV to home axis
home = Component(EpicsSignal, "-HOME", kind=Kind.config)
ishomed = Component(EpicsSignal, "-AS00", kind=Kind.config)
# HW status words
dshw = Component(EpicsSignalRO, "-DSHW", auto_monitor=True, kind=Kind.normal)
ashw = Component(EpicsSignalRO, "-ASHW", auto_monitor=True, kind=Kind.normal)
fslw = Component(EpicsSignalRO, "-FSLW", auto_monitor=True, kind=Kind.normal)
# Rock movement
_rock = Component(EpicsSignal, "-ROCK", put_complete=True, kind=Kind.config)
_rock_dist = Component(EpicsSignal, "-RINCP", put_complete=True, kind=Kind.config)
_rock_velo = Component(EpicsSignal, "-RSETV", put_complete=True, kind=Kind.config)
_rock_count = Component(EpicsSignal, "-COUNT", put_complete=True, kind=Kind.config)
# _rock_accel = Component(EpicsSignal, "-RRATE", put_complete=True, kind=Kind.config)
hlm = Component(Signal, kind=Kind.config)
llm = Component(Signal, kind=Kind.config)
vmin = Component(Signal, kind=Kind.config)
vmax = Component(Signal, kind=Kind.config)
offset = Component(EpicsSignal, "-OFF", put_complete=True, kind=Kind.config)
# pylint: disable=too-many-arguments
def __init__(
self,
prefix="",
*,
name,
base_pv="",
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
llm=0,
hlm=0,
vmin=0,
vmax=0,
**kwargs,
):
"""__init__ MUST have a full argument list"""
# Patching the parent's PVs into the axis class to check for direct/locked mode
if parent is None:
def maybe_add_prefix(self, _, kw, suffix):
# Patched not to enforce parent prefix when no parent
if kw in self.add_prefix:
return suffix
return suffix
self.__class__.__dict__["abr_mode"].maybe_add_prefix = types.MethodType(
maybe_add_prefix, self.__class__.__dict__["abr_mode"]
)
self.__class__.__dict__["abr_mode_direct"].maybe_add_prefix = types.MethodType(
maybe_add_prefix, self.__class__.__dict__["abr_mode_direct"]
)
logger.info(self.__class__.__dict__["abr_mode"].kwargs)
self.__class__.__dict__["abr_mode"].suffix = base_pv + "-DF1:AXES-MODE"
self.__class__.__dict__["abr_mode_direct"].suffix = base_pv + "-DF1:MODE-DIRECT"
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
self.llm.set(llm).wait()
self.hlm.set(hlm).wait()
self.vmin.set(vmin).wait()
self.vmax.set(vmax).wait()
def omove(
self,
position,
wait=True,
timeout=None,
# moved_cb=None,
velocity=None,
relative=False,
direct=False,
**kwargs,
) -> MoveStatus:
"""Native absolute/relative movement on the A3200"""
# Check if we're in direct movement mode
if self.abr_mode.value not in (AbrMode.DIRECT, "DIRECT"):
if direct:
self.abr_mode_direct.set(1).wait()
else:
raise RuntimeError(f"ABR axis not in direct mode: {self.abr_mode.value}")
# Before moving, ensure we can stop (if a stop_signal is configured).
if self.stop_signal is not None:
self.stop_signal.wait_for_connection()
# Set velocity if provided
if velocity is not None:
self.velocity.set(velocity).wait()
# This is adapted from pv_positioner.py
self.check_value(position)
# Get MoveStatus from parent of parent
status = PositionerBase.move(self, position=position, timeout=timeout, **kwargs)
has_done = self.done is not None
if not has_done:
moving_val = 1 - self.done_value
self._move_changed(value=self.done_value)
self._move_changed(value=moving_val)
try:
if relative:
# Relative movement instead of setpoint
self.relmove.put(position, wait=True)
else:
# Standard absolute movement
self.setpoint.put(position, wait=True)
if wait:
status.wait()
except KeyboardInterrupt:
self.stop()
raise
return status
def describe(self):
"""Workaround to schema expected by the BEC"""
d = super().describe()
d[str(self.name)] = d[f"{self.name}_readback"]
return d
def read(self) -> OrderedDict[str, dict]:
"""Workaround to schema expected by the BEC"""
d = super().read()
d[str(self.name)] = d[f"{self.name}_readback"]
return d
def move(self, position, wait=True, timeout=None, moved_cb=None, **kwargs) -> MoveStatus:
"""Exposes the ophyd move command through BEC abstraction"""
return self.omove(position, wait=wait, timeout=timeout, moved_cb=moved_cb, **kwargs)
def rock(self, distance, counts: int, velocity=None, wait=True) -> Status:
"""Repeated single axis zigzag scan I guess PSO should be configured for this"""
self._rock_dist.put(distance)
self._rock_count.put(counts)
if velocity is not None:
self._rock_velo.put(velocity)
# if acceleration is not None:
# self._rock_accel.put(acceleration)
self._rock.put(1)
status = super().move(position=distance)
if wait:
status.wait()
return status
# def is_omega_ok(self):
# """Checks omega axis status"""
# return 0 == self.self.omega.status.get()
# def is_homed(self):
# """Checks if omega is homed"""
# return 1 == self.omega.is_homed.get()
# def do_homing(self, wait=True):
# """Execute the homing procedure.
# Executes the homing procedure on omega and waits (default) until it is completed.
# TODO: Return a status object to do this wwith futures and monitoring.
# PARAMETERS
# `wait` true / false if the routine is to wait for the homing to finish.
# """
# self.omega.home.set(1, settle_time=1).wait()
# if not wait:
# return
# while not self.omega.is_homed():
# time.sleep(0.2)
# Automatically start an axis if directly invoked
if __name__ == "__main__":
omega = A3200Axis(prefix="X06DA-ES-DF1:OMEGA", base_pv="X06DA-ES", name="omega")
omega.wait_for_connection()
-97
View File
@@ -1,97 +0,0 @@
# -*- coding: utf-8 -*-
"""
``NDArrayPreview`` --- Standalone Preview for ImagePlugin
*********************************************************
This module provides a standalone object to receive images to ophyd from the
AreaDetector's ImagePlugin.
Created on Wed Jan 29 2025
@author: mohacsi_i
"""
import numpy as np
from ophyd import Device, Component, EpicsSignal, EpicsSignalWithRBV, Kind, Staged
from ophyd.areadetector import NDDerivedSignal
from bec_lib import bec_logger
logger = bec_logger.logger
class SilentNDDerivedSignal(NDDerivedSignal):
"""Silent version of NDDerivedSignal, it does not spam the terminal on
every defective frame (shit happens, ok?)."""
def _array_shape_callback(self, **kwargs):
try:
super()._array_shape_callback(**kwargs)
except RuntimeError:
pass
class NDArrayPreview(Device):
"""Wrapper class around AreaDetector's NDStdArray plugins
This is a standalone class to display images from AreaDetector's
ImagePlugin without using a parent device. It also offers BEC exposed
methods to transfer image and change image array Kind-ness.
NOTE: As an explicit request, it can toggle data recording
"""
# Subscriptions for plotting image
USER_ACCESS = ["image", "savemode"]
SUB_MONITOR = "device_monitor_2d"
_default_sub = SUB_MONITOR
# Status attributes
min_callback_time = Component(
EpicsSignalWithRBV, "MinCallbackTime", kind=Kind.config, put_complete=True
)
array_size_x = Component(EpicsSignal, "ArraySize0_RBV", kind=Kind.config)
array_size_y = Component(EpicsSignal, "ArraySize1_RBV", kind=Kind.config)
array_size_z = Component(EpicsSignal, "ArraySize2_RBV", kind=Kind.config)
ndimensions = Component(EpicsSignal, "NDimensions_RBV", kind=Kind.config)
array_data = Component(EpicsSignal, "ArrayData", kind=Kind.omitted)
shaped_image = Component(
SilentNDDerivedSignal,
derived_from="array_data",
shape=("array_size_z", "array_size_y", "array_size_x"),
num_dimensions="ndimensions",
kind=Kind.omitted,
)
def read(self):
"""Stream out data on every read()"""
if self._staged == Staged.yes:
image = self.shaped_image.get()
self._run_subs(sub_type=self.SUB_MONITOR, value=image)
return super().read()
def savemode(self, save=False):
"""Toggle save mode for the shaped image"""
# pylint: disable=protected-access
if save:
self.shaped_image._kind = Kind.normal
else:
self.shaped_image._kind = Kind.omitted
def image(self):
"""Fallback method in case image streaming fills up the BEC"""
array_size = (self.array_size_z.get(), self.array_size_y.get(), self.array_size_x.get())
if array_size == (0, 0, 0):
raise RuntimeError("Invalid image; ensure array_callbacks are on")
if array_size[-1] == 0:
array_size = array_size[:-1]
image = self.array_data.get()
return np.array(image).reshape(array_size)
# Automatically connect to SAMCAM at PXIII if directly invoked
if __name__ == "__main__":
img = NDArrayPreview("X06DA-SAMCAM:image1:", name="samimg")
img.wait_for_connection()
-47
View File
@@ -1,47 +0,0 @@
from ophyd import EpicsSignal
from ophyd.status import SubscriptionStatus
class PneumaticValve(EpicsSignal):
"""Wrapper around EpicsSignal to wait until reaching target. Use the
status returned by set() to wait until movement is finished. Do NOT
use put if you want to wait, that's a low-level PV write op.
NOTE: The SET and GET states do not match exactly
"""
def set(self, value, *, timeout=5, settle_time=0.1):
"""Overloaded setter that waits for target state
NOTE: The SubscriptionStatus callback does not run in put()
"""
# Lazy hardcoded state lookup
target = 1 if value in (1, "Measure") else 2
# Define wait until an end state is reached
def on_target(*, value, **_):
return bool(value == target)
# Subscribe a monitor in advance and wait for update
status = SubscriptionStatus(self, on_target, timeout=timeout, settle_time=0.1)
# Set value to start movement
super().set(value, settle_time=settle_time).wait()
# Return the monitor
return status
def check_value(self, value):
"""Input validation"""
if value not in (0, 1, "Measure", "Park"):
raise ValueError(f"Unsupported pneumatic valve target {value}")
return super().check_value(value)
if __name__ == "__main__":
pneum = PneumaticValve(
read_pv="X06DA-ES-BS:GET-POS",
write_pv="X06DA-ES-BS:SET-POS",
auto_monitor=True,
put_complete=True,
name="bspump",
)
pneum.wait_for_connection()
-52
View File
@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
"""
``SamCam`` --- Sample Camera control software
*********************************************
This module provides an object to control the sample camera at the PX III
beamline. The camera should run continously and stream data via ZMQ for
the GUI and alignment scripts.
Created on Thu Jan 30 2025
@author: mohacsi_i
"""
from ophyd import ADComponent
from ophyd_devices.devices.areadetector.cam import GenICam
# from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
PSIDetectorBase,
CustomDetectorMixin,
)
from bec_lib import bec_logger
logger = bec_logger.logger
class SamCamSetup(CustomDetectorMixin):
"""Simple camera mixin class, the SAMCAM is usually streaming"""
def on_stage(self):
"""Just make sure it's running continously"""
self.parent.cam.acquire.put(1, wait=True)
def on_unstage(self):
"""Should run continously"""
def on_stop(self):
"""Should run continously"""
class SamCamDetector(PSIDetectorBase):
"""Sample camera device
The SAMCAM continously streams images to the GUI and sample alignment
scripts via ZMQ.
"""
custom_prepare_cls = SamCamSetup
cam = ADComponent(GenICam, "cam1:")
# image = ADComponent(ImagePlugin_V35, "image1:")
-284
View File
@@ -1,284 +0,0 @@
"""
``SmarGon`` --- SmarGon control software
******************************************
The module provides an object to control the SmarGon goniometer axes at PX III.
The SmarGon axes are interfaced as positioners.
"""
import time
from threading import Thread, Lock
import requests
from requests.adapters import HTTPAdapter, Retry
from collections import OrderedDict
from ophyd import Component, Kind, Signal, PVPositioner
from ophyd.status import SubscriptionStatus
try:
from bec_lib import bec_logger
logger = bec_logger.logger
except ModuleNotFoundError:
import logging
logger = logging.getLogger("SmarGon")
# SmarGon contoller can't really handle multiple connections
# Use this mutex to ensure one access at a time
mutex = Lock()
class SmarGonSignal(Signal):
"""SmarGonSignal (R/W)
Small helper class to read/write parameters from SmarGon. As there is no
motion status readback from smargopolo, this should be substituted with
setting with 'settle_time'.
"""
def __init__(self, *args, write_addr="targetSCS", low_limit=None, high_limit=None, **kwargs):
super().__init__(*args, **kwargs)
self.write_addr = write_addr
self.addr = self.parent.name
self._limits = (low_limit, high_limit)
# self.get()
def put(self, value, *, timestamp=None, **kwargs):
"""Overriden put to add communication with smargopolo"""
# Validate new value and get timestamp
self.check_value(value)
if timestamp is None:
timestamp = time.time()
# Perform the actual write to SmargoPolo
# pylint: disable=protected-access
r = self.parent._go_n_put(f"{self.write_addr}?{self.addr.upper()}={value}")
# pylint: disable=attribute-defined-outside-init
old_value = self._readback
self._timestamp = timestamp
self._readback = r[self.addr.upper()]
self._value = r[self.addr.upper()]
# Notify subscribers
self._run_subs(
sub_type=self.SUB_VALUE, old_value=old_value, value=value, timestamp=self._timestamp
)
@property
def limits(self):
return self._limits
def check_value(self, value, **kwargs):
"""Check if value falls within limits"""
lol = self.limits[0]
if lol is not None:
if value < lol:
raise ValueError(f"Target {value} outside of limits {self.limits}")
hil = self.limits[1]
if hil is not None:
if value > hil:
raise ValueError(f"Target {value} outside of limits {self.limits}")
def get(self, **kwargs):
# pylint: disable=protected-access
r = self.parent._go_n_get(self.write_addr)
# pylint: disable=attribute-defined-outside-init
self._value = r[self.addr.upper()] if isinstance(r, dict) else r
return super().get(**kwargs)
class SmarGonSignalRO(Signal):
"""Small helper class for read-only parameters PVs from SmarGon.
Reads and optionally monitors a variable on the SmarGon.
"""
def __init__(self, *args, read_addr="readbackSCS", auto_monitor=False, **kwargs):
super().__init__(*args, **kwargs)
self._metadata["write_access"] = False
self.read_addr = read_addr
self.addr = self.parent.name
if auto_monitor:
self._mon = Thread(target=self.poll, daemon=True)
self._mon.start()
def get(self, **kwargs):
# pylint: disable=protected-access
r = self.parent._go_n_get(self.read_addr)
if isinstance(r, dict):
self.put(r[self.addr.upper()], force=True)
else:
self.put(r, force=True)
return self._readback
def poll(self):
"""Fooo"""
time.sleep(2)
while True:
time.sleep(0.25)
try:
self.get()
except requests.ConnectTimeout as ex:
logger.error(f"[{self.name}] {ex}")
class SmarGonAxis(PVPositioner):
"""SmarGon client deice
This class controls the SmarGon goniometer via the REST interface. All
SmarGon axes share a common mutex to manage actual HW access.
"""
USER_ACCESS = ["omove"]
# Status attributes
sg_url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
corr = Component(SmarGonSignalRO, read_addr="corr_type", kind=Kind.config)
mode = Component(SmarGonSignalRO, read_addr="mode", kind=Kind.config)
# Axis parameters
readback = Component(SmarGonSignalRO, kind=Kind.hinted, auto_monitor=True)
setpoint = Component(SmarGonSignal, kind=Kind.normal)
done = Component(Signal, value=1, kind=Kind.normal)
# moving = Component(SmarGonMovingSignalRO, kind=Kind.config)
_tol = 0.001
# pylint: disable=too-many-arguments
def __init__(
self,
prefix="SCS",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
sg_url: str = "http://x06da-smargopolo.psi.ch:3000",
low_limit=None,
high_limit=None,
**kwargs,
) -> None:
self.__class__.__dict__["readback"].kwargs["read_addr"] = f"readback{prefix}"
self.__class__.__dict__["setpoint"].kwargs["write_addr"] = f"target{prefix}"
self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit
self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit
self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url
# Fine-tune HTTP connection behavior
# NOTE: SmarGon has a few failed requests every one in a while
self._s = requests.Session()
retries = Retry(total=5, backoff_factor=0.05, status_forcelist=[500, 502, 503, 504])
self._s.mount("http://", HTTPAdapter(max_retries=retries))
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
def initialize(self):
"""Helper function for initial readings"""
# self.corr.get()
# self.mode.get()
r = self._go_n_get("corr_type")
print(r)
def move(self, position, wait=True, timeout=None, moved_cb=None):
"""Move command that's masked by BEC"""
return self.omove(position, wait, timeout, moved_cb)
def omove(self, position, wait=True, timeout=2.0, moved_cb=None):
"""Original move command without the BEC wrappers"""
status = self.setpoint.set(position, settle_time=0.1)
status.wait()
if not wait:
return status
def on_target(*, value, **_):
distance = abs(value - self.setpoint._value)
print(f"[self.name] Distance: {distance}")
return bool(distance < self._tol)
status = SubscriptionStatus(self.readback, on_target, timeout=timeout, settle_time=0.1)
return status
def describe(self):
"""Workaround to schema expected by the BEC"""
d = super().describe()
d[str(self.name)] = d[f"{self.name}_readback"]
return d
def read(self) -> OrderedDict[str, dict]:
"""Workaround to schema expected by the BEC"""
d = super().read()
d[str(self.name)] = d[f"{self.name}_readback"]
return d
def _pos_changed(self, timestamp=None, value=None, **kwargs):
pass
def _go_n_get(self, address, **kwargs):
"""Helper function to connect to smargopolo"""
cmd = f"{self.sg_url.get()}/{address}"
try:
with mutex:
r = self._s.get(cmd, timeout=1, **kwargs)
except TimeoutError:
try:
time.sleep(0.05)
with mutex:
r = self._s.get(cmd, timeout=0.5, **kwargs)
except TimeoutError:
time.sleep(0.05)
with mutex:
r = self._s.get(cmd, timeout=0.5, **kwargs)
if not r.ok:
raise RuntimeError(
f"[{self.name}] Error getting {address}; reply was {r.status_code} => {r.reason}"
)
return r.json()
def _go_n_put(self, address, **kwargs):
"""Helper function to connect to smargopolo"""
cmd = f"{self.sg_url.get()}/{address}"
try:
with mutex:
r = self._s.put(cmd, timeout=1, **kwargs)
except TimeoutError:
try:
time.sleep(0.05)
with mutex:
r = self._s.put(cmd, timeout=0.5, **kwargs)
except TimeoutError:
time.sleep(0.05)
with mutex:
r = self._s.put(cmd, timeout=0.5, **kwargs)
if not r.ok:
raise RuntimeError(
f"[{self.name}] Error putting {address}; reply was {r.status_code} => {r.reason}"
)
return r.json()
if __name__ == "__main__":
shx = SmarGonAxis(prefix="SCS", name="shx", sg_url="http://x06da-smargopolo.psi.ch:3000")
shy = SmarGonAxis(prefix="SCS", name="shy", sg_url="http://x06da-smargopolo.psi.ch:3000")
shz = SmarGonAxis(
prefix="SCS",
name="shz",
low_limit=10,
high_limit=22,
sg_url="http://x06da-smargopolo.psi.ch:3000",
)
shx.wait_for_connection()
shy.wait_for_connection()
shz.wait_for_connection()
-249
View File
@@ -1,249 +0,0 @@
"""
``SmarGon`` --- SmarGon control software
******************************************
The module provides an object to control the SmarGon goniometer axes at PX III.
The SmarGon axes are interfaced as positioners.
"""
import time
import threading
from collections import OrderedDict
import requests
from requests.adapters import HTTPAdapter, Retry
from ophyd import Component, Kind, Signal, PVPositioner
from ophyd.status import SubscriptionStatus
try:
from bec_lib import bec_logger
logger = bec_logger.logger
except ModuleNotFoundError:
import logging
logger = logging.getLogger("SmarGon")
# SmarGon contoller can't really handle multiple connections
# Use this mutex to ensure one access at a time
mutex = threading.Lock()
class LimitedSmarGonSignal(Signal):
"""SmarGonSignal (R/W)
Small helper class to read/write parameters from SmarGon. As there is no
motion status readback from smargopolo, this should be substituted with
setting with 'settle_time'.
"""
def __init__(self, *args, write_addr="targetSCS", low_limit=None, high_limit=None, **kwargs):
self._limits = (low_limit, high_limit)
super().__init__(*args, **kwargs)
self.write_addr = write_addr
@property
def limits(self):
return self._limits
def check_value(self, value, **kwargs):
"""Check if value falls within limits"""
lol = self.limits[0]
if lol is not None:
if value < lol:
raise ValueError(f"Target {value} outside of limits {self.limits}")
hil = self.limits[1]
if hil is not None:
if value > hil:
raise ValueError(f"Target {value} outside of limits {self.limits}")
def put(self, value, *, timestamp=None, force=False, metadata=None, **kwargs):
"""Overriden put to add communication with smargopolo"""
# Validate new value and get timestamp
if not force:
self.check_value(value)
if timestamp is None:
timestamp = time.time()
# Perform the actual write to SmargoPolo
# pylint: disable=protected-access
r = self.parent._go_n_put(f"{self.write_addr}?{self.parent.name.upper()}={value}")
# pylint: disable=attribute-defined-outside-init
old_value = self._readback
self._timestamp = timestamp
self._readback = r[self.parent.name.upper()]
self._value = r[self.parent.name.upper()]
# Notify subscribers
self._run_subs(
sub_type=self.SUB_VALUE, old_value=old_value, value=value, timestamp=self._timestamp
)
class SmarGonAxis(PVPositioner):
"""SmarGon client deice
This class controls the SmarGon goniometer via the REST interface. All
SmarGon axes share a common mutex to manage actual HW access.
"""
USER_ACCESS = ["omove", "oldmove"]
# Status attributes
sg_url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
# Axis parameters
readback = Component(Signal, kind=Kind.hinted, metadata={"write_access": False})
setpoint = Component(LimitedSmarGonSignal, kind=Kind.normal)
done = Component(Signal, value=1, kind=Kind.normal, metadata={"write_access": False})
_tol = 0.001
# pylint: disable=too-many-arguments
def __init__(
self,
prefix="SCS",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
sg_url: str = "http://x06da-smargopolo.psi.ch:3000",
low_limit=None,
high_limit=None,
**kwargs,
) -> None:
# self.__class__.__dict__["setpoint"].kwargs["write_addr"] = f"target{prefix}"
self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit
self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit
self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url
# Fine-tune HTTP connection behavior
# NOTE: SmarGon has a few failed requests every one in a while
self._s = requests.Session()
retries = Retry(total=5, backoff_factor=0.05, status_forcelist=[500, 502, 503, 504])
self._s.mount("http://", HTTPAdapter(max_retries=retries))
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
def on_target():
"""Monitors the setpoint and readback and calculates the on_target flag"""
time.sleep(2)
while True:
# Read back target and setpoint values
# pylint: disable=protected-access
r = self._go_n_get("readbackSCS")
rb = r[self.name.upper()]
self.readback.set(rb, force=True).wait()
r = self._go_n_get("targetSCS")
sp = r[self.name.upper()]
self.setpoint._value = sp
# print(f"Readback: {rb}\tSetpoint: {sp}")
# Check if they're within tolerance
distance = abs(rb - sp)
done = 1 if distance < self._tol else 0
self.done.put(done, force=True)
time.sleep(0.2)
self._mon = threading.Thread(target=on_target, daemon=True)
self._mon.start()
def omove(self, position, wait=True, timeout=None, moved_cb=None):
"""Move command that's masked by BEC"""
self.done.put(0, force=True)
return self.move(position, wait, timeout, moved_cb)
def oldmove(self, position, wait=True, timeout=2.0, moved_cb=None):
"""Original move command without the BEC wrappers"""
status = self.setpoint.set(position, settle_time=0.1).wait()
if not wait:
return status
def on_target(*, value, **_):
distance = abs(value - self.setpoint._value)
print(f"[self.name] Distance: {distance}")
return bool(distance < self._tol)
status = SubscriptionStatus(self.readback, on_target, timeout=timeout, settle_time=0.1)
return status
def describe(self):
"""Workaround to schema expected by the BEC"""
d = super().describe()
d[str(self.name)] = d[f"{self.name}_readback"]
return d
def read(self) -> OrderedDict[str, dict]:
"""Workaround to schema expected by the BEC"""
d = super().read()
d[str(self.name)] = d[f"{self.name}_readback"]
return d
def _pos_changed(self, timestamp=None, value=None, **kwargs):
"""Remove EPICS dependency"""
pass
def _go_n_get(self, address, **kwargs):
"""Helper function to connect to smargopolo"""
cmd = f"{self.sg_url.get()}/{address}"
try:
with mutex:
r = self._s.get(cmd, timeout=1, **kwargs)
except TimeoutError:
try:
time.sleep(0.05)
with mutex:
r = self._s.get(cmd, timeout=0.5, **kwargs)
except TimeoutError:
time.sleep(0.05)
with mutex:
r = self._s.get(cmd, timeout=0.5, **kwargs)
if not r.ok:
raise RuntimeError(
f"[{self.name}] Error getting {address}; reply was {r.status_code} => {r.reason}"
)
return r.json()
def _go_n_put(self, address, **kwargs):
"""Helper function to connect to smargopolo"""
cmd = f"{self.sg_url.get()}/{address}"
try:
with mutex:
r = self._s.put(cmd, timeout=1, **kwargs)
except TimeoutError:
try:
time.sleep(0.05)
with mutex:
r = self._s.put(cmd, timeout=0.5, **kwargs)
except TimeoutError:
time.sleep(0.05)
with mutex:
r = self._s.put(cmd, timeout=0.5, **kwargs)
if not r.ok:
raise RuntimeError(
f"[{self.name}] Error putting {address}; reply was {r.status_code} => {r.reason}"
)
return r.json()
if __name__ == "__main__":
shx = SmarGonAxis(prefix="SCS", name="shx", sg_url="http://x06da-smargopolo.psi.ch:3000")
shy = SmarGonAxis(prefix="SCS", name="shy", sg_url="http://x06da-smargopolo.psi.ch:3000")
shz = SmarGonAxis(
prefix="SCS",
name="shz",
low_limit=10,
high_limit=22,
sg_url="http://x06da-smargopolo.psi.ch:3000",
)
shx.wait_for_connection()
shy.wait_for_connection()
shz.wait_for_connection()
-393
View File
@@ -1,393 +0,0 @@
# #!/usr/bin/env python3
# from time import sleep, time
# from typing import Tuple
# from requests import get, put
# from beamline import beamline
# from mx_redis import SMARGON
# try:
# from mx_preferences import get_config
# host = get_config(beamline)["smargon"]["host"]
# port = get_config(beamline)["smargon"]["port"]
# except Exception:
# host = "x06da-smargopolo.psi.ch"
# port = 3000
# base = f"http://{host}:{port}"
# def gonget(thing: str, **kwargs) -> dict:
# """issue a GET for some API component on the smargopolo server"""
# cmd = f"{base}/{thing}"
# if kwargs.get("verbose", False):
# print(cmd)
# r = get(cmd)
# if not r.ok:
# raise Exception(f"error getting {thing}; server returned {r.status_code} => {r.reason}")
# return r.json()
# def gonput(thing: str, **kwargs):
# """issue a PUT for some API component on the smargopolo server"""
# cmd = f"{base}/{thing}"
# if kwargs.get("verbose", False):
# print(cmd)
# put(cmd)
# def scsput(**kwargs):
# """
# Issue a new absolute target in the SH coordinate system.
# The key "verbose" may be passed in kwargs with any true
# value for verbose behaviour.
# :param kwargs: a dict containing keys ("shx", "shy", "shz", "chi", "phi")
# :type kwargs: dict
# :return:
# :rtype:
# """
# xyz = {
# k.upper(): v for k, v in kwargs.items() if k.lower() in ("shx", "shy", "shz", "chi", "phi")
# }
# thing = "&".join([f"{k.upper()}={float(v):.5f}" for k, v in xyz.items()])
# cmd = f"{base}/targetSCS?{thing}"
# if kwargs.get("verbose", False):
# print(cmd)
# put(cmd)
# def bcsput(**kwargs):
# """
# Issue a new absolute target in the beamline coordinate system.
# The key "verbose" may be passed in kwargs with any true
# value for verbose behaviour.
# :param kwargs: a dict containing keys ("bx", "by", "bz", "chi", "phi")
# :return:
# :rtype:
# """
# xyz = {k.upper(): v for k, v in kwargs.items() if k.lower() in ("bx", "by", "bz", "chi", "phi")}
# thing = "&".join([f"{k.upper()}={float(v):.5f}" for k, v in xyz.items()])
# cmd = f"{base}/targetBCS?{thing}"
# if kwargs.get("verbose", False):
# print(cmd)
# put(cmd)
# def scsrelput(**kwargs) -> None:
# """
# Issue relative increments to current SH coordinate system.
# The key "verbose" may be passed in kwargs with any true
# value for verbose behaviour.
# :param kwargs: a dict containing keys ("shx", "shy", "shz", "chi", "phi")
# :type kwargs: dict
# :return:
# :rtype:
# """
# xyz = {
# k.upper(): v for k, v in kwargs.items() if k.lower() in ("shx", "shy", "shz", "chi", "phi")
# }
# thing = "&".join([f"{k.upper()}={float(v):.5f}" for k, v in xyz.items()])
# cmd = f"{base}/targetSCS_rel?{thing}"
# if kwargs.get("verbose", False):
# print(cmd)
# put(cmd)
# def bcsrelput(**kwargs):
# """
# Issue relative increments to current beamline coordinate system.
# The key "verbose" may be passed in kwargs with any true
# value for verbose behaviour.
# :param kwargs: a dict containing keys ("bx", "by", "bz")
# :type kwargs: dict
# :return:
# :rtype:
# """
# xyz = {k.upper(): v for k, v in kwargs.items() if k.lower() in ("bx", "by", "bz")}
# thing = "&".join([f"{k.upper()}={float(v):.5f}" for k, v in xyz.items()])
# cmd = f"{base}/targetBCS_rel?{thing}"
# if kwargs.get("verbose", False):
# print(cmd)
# put(cmd)
# # url_redis = f"{beamline}-cons-705.psi.ch"
# # print(f"connecting to redis DB #3 on host: {url_redis}")
# # redis_handle = redis.StrictRedis(host=url_redis, db=3)
# # pubsub = redis_handle.pubsub()
# MODE_UNINITIALIZED = 0
# MODE_INITIALIZING = 1
# MODE_READY = 2
# MODE_ERROR = 99
# class SmarGon(object):
# def __init__(self):
# super(SmarGon, self).__init__()
# self.__dict__.update(target=None)
# self.__dict__.update(bookmarks={})
# self.__dict__.update(_latest_message={})
# # pubsub.psubscribe(**{f"__keyspace@{SMARGON.value}__:*": self._cb_readbackSCS})
# # pubsub.run_in_thread(sleep_time=0.5, daemon=True)
# def __repr__(self):
# BX, BY, BZ, OMEGA, CHI, PHI, a, b, c = self.readback_bcs().values()
# return f"<{self.__class__.__name__} X={BX:.3f}, Y={BY:.3f}, Z={BZ:.3f}, CHI={CHI:.3f}, PHI={PHI:.3f}, OMEGA={OMEGA:.3f}>"
# def _cb_readbackSCS(self, msg):
# if msg["data"] in ["hset"]:
# self._latest_message = msg
# def move_home(self, wait=False) -> None:
# """move to beamline coordinate system X, Y, Z, Chi, Phi = 0 0 0 0 0"""
# self.apply_bookmark_sh({"shx": 0.0, "shy": 0.0, "shz": 18.0, "chi": 0.0, "phi": 0.0})
# if wait:
# self.wait_home()
# def xyz(self, coords: Tuple[float, float, float], wait: bool = True) -> None:
# """
# Move smargon in absolute beamline coordinates
# :param coords: a tuple of floats representing X, Y, Z coordinates
# :type coords:
# :param wait:
# :type wait:
# :return:
# :rtype:
# """
# x, y, z = coords
# # the two steps below are necessary otherwise the control system
# # remembers *a* previous CHI
# bcs = self.bcs
# bcs.update({"BX": x, "BY": y, "BZ": z})
# self.bcs = bcs
# if wait:
# self.wait()
# def wait_home(self, timeout: float = 20.0) -> None:
# """
# wait for the smargon to reach its home position:
# SHX = 0.0
# SHY = 0.0
# SHZ = 18.0
# CHI = 0.0
# PHI = 0.0
# :param timeout: time to wait for positions to be reached raises TimeoutError if timeout reached
# :type timeout: float
# :return:
# :rtype:
# """
# tout = timeout + time()
# in_place = [False, False]
# rbv = -999.0
# while not all(in_place) and time() < tout:
# rbv = self.readback_scs()
# in_place = []
# for k, v in {"SHX": 0.0, "SHY": 0.0, "SHZ": 18.0, "CHI": 0.0, "PHI": 0.0}.items():
# in_place.append(abs(rbv[k] - v) < 0.01)
# if time() > tout:
# raise TimeoutError(f"timeout waiting for smargon to reach home position: {rbv}")
# def push_bookmark(self):
# """
# save current absolute coordinates in FIFO stack
# :return:
# :rtype:
# """
# t = round(time())
# self.bookmarks[t] = self.readback_scs()
# def pop_bookmark(self):
# return self.bookmarks.popitem()[1]
# def apply_bookmark_sh(self, scs):
# scsput(**scs)
# def apply_last_bookmark_sh(self):
# scs = self.pop_bookmark()
# scsput(**scs)
# def readback_mcs(self):
# """current motor positions of the smargon sliders"""
# return gonget("readbackMCS")
# def readback_scs(self):
# """current SH coordinates of the smargon model"""
# return gonget("readbackSCS")
# def readback_bcs(self):
# """current beamline coordinates of the smargon"""
# return gonget("readbackBCS")
# def target_scs(self):
# """currently assigned targets for the smargon control system"""
# return gonget("targetSCS")
# def initialize(self):
# """initialize the smargon"""
# self.set_mode(MODE_UNINITIALIZED)
# sleep(0.1)
# self.set_mode(MODE_INITIALIZING)
# def set_mode(self, mode: int):
# """put smargon control system in a given mode
# MODE_UNINITIALIZED = 0
# MODE_INITIALIZING = 1
# MODE_READY = 2
# MODE_ERROR = 99
# """
# gonput(f"mode?mode={mode}")
# def enable_correction(self):
# """enable calibration based corrections"""
# gonput("corr_type?corr_type=1")
# def disable_correction(self):
# """disable calibration based corrections"""
# gonput("corr_type?corr_type=0")
# def chi(self, val=None, wait=False):
# if val is None:
# return self.readback_scs()["CHI"]
# scsput(CHI=val)
# if wait:
# timeout = 10 + time()
# while time() < timeout:
# if abs(val - self.readback_scs()["CHI"]) < 0.1:
# break
# if time() > timeout:
# raise RuntimeError(f"SmarGon CHI did not reach requested target {val} in time")
# def phi(self, val=None, wait=False):
# if val is None:
# return self.readback_scs()["PHI"]
# scsput(PHI=val)
# if wait:
# timeout = 70 + time()
# while time() < timeout:
# if abs(val - self.readback_scs()["PHI"]) < 0.1:
# break
# if time() > timeout:
# raise RuntimeError(f"SmarGon PHI did not reach requested target {val} in time")
# def wait(self, timeout=60.0):
# """waits up to `timeout` seconds for smargon to reach target"""
# target = {
# k.upper(): v
# for k, v in self.target_scs().items()
# if k.lower() in ("shx", "shy", "shz", "chi", "phi")
# }
# timeout = timeout + time()
# while time() < timeout:
# s = {
# k: (abs(v - target[k]) < 0.01)
# for k, v in self.readback_scs().items()
# if k.upper() in ("SHX", "SHY", "SHZ", "CHI", "PHI")
# }
# if all(list(s.values())):
# break
# if time() > timeout:
# raise TimeoutError("timed out waiting for smargon to reach target")
# def __setattr__(self, key, value):
# key = key.lower()
# if key == "mode":
# self.set_mode(value)
# elif key == "correction":
# assert value in (
# 0,
# 1,
# False,
# True,
# ), "correction is either 1 or True (enabled) or 0 (disabled)"
# gonput(f"corr_type?corr_type?{value}")
# elif key == "scs":
# scsput(**value)
# elif key == "bcs":
# bcsput(**value)
# elif key == "target":
# if not isinstance(value, dict):
# raise Exception(
# f"expected a dict with target axis and values got something else: {value}"
# )
# for k in value.keys():
# if k.lower() not in "shx shy shz chi phi ox oy oz".split():
# raise Exception(f'unknown axis in target "{k}"')
# scsput(**value)
# elif key in "shx shy shz chi phi ox oy oz".split():
# scsput(**{key: value})
# elif key in "bx by bz".split():
# bcs = self.readback_bcs()
# bcs[key] = value
# bcsput(**bcs)
# else:
# self.__dict__[key].update(value)
# def __getattr__(self, key):
# key = key.lower()
# if key == "mode":
# return self.readback_mcs()["mode"]
# elif key == "correction":
# return gonget("corr_type")
# elif key == "bcs":
# return self.readback_bcs()
# elif key == "mcs":
# return self.readback_mcs()
# elif key == "scs":
# return self.readback_scs()
# elif key in "shx shy shz chi phi ox oy oz".split():
# return self.readback_scs()[key.upper()]
# elif key in "bx by bz".split():
# return self.readback_bcs()[key.upper()]
# else:
# return self.__getattribute__(key)
# if __name__ == "__main__":
# import argparse
# parser = argparse.ArgumentParser(description="SmarGon client")
# parser.add_argument("-i", "--initialize", help="initialize smargon", action="store_true")
# args = parser.parse_args()
# smargon = SmarGon()
# if args.initialize:
# print("initializing smargon device")
# import Aerotech
# print("moving aerotech back by 50mm")
# abr = Aerotech.Abr()
# abr.incr_x(-50.0, wait=True, velo=100.0)
# print("issuing init command to smargon")
# smargon.initialize()
# sleep(0.5)
# print("waiting for init routine to complete")
# while MODE_READY != smargon.mode:
# sleep(0.5)
# print("moving smargon to HOME position")
# smargon.move_home()
# print("moving aerotech to its previous position")
# abr.incr_x(50.0, wait=True, velo=100.0)
# exit(0)
-206
View File
@@ -1,206 +0,0 @@
# -*- coding: utf-8 -*-
"""
Standard DAQ preview image stream module
Created on Thu Jun 27 17:28:43 2024
@author: mohacsi_i
"""
import json
from time import sleep, time
from threading import Thread
import zmq
import numpy as np
from ophyd import Device, Signal, Component, Kind
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
from bec_lib import bec_logger
logger = bec_logger.logger
ZMQ_TOPIC_FILTER = b""
class StdDaqPreviewMixin(CustomDetectorMixin):
"""Setup class for the standard DAQ preview stream
Parent class: CustomDetectorMixin
"""
_mon = None
def on_stage(self):
"""Start listening for preview data stream"""
if self._mon is not None:
self.parent.unstage()
sleep(0.5)
logger.info(f"[{self.parent.name}] Attaching monitor to {self.parent.url.get()}")
self.parent.connect()
self._stop_polling = False
self._mon = Thread(target=self.poll, daemon=True)
self._mon.start()
def on_unstage(self):
"""Stop a running preview"""
if self._mon is not None:
self._stop_polling = True
# Might hang on recv_multipart
self._mon.join(timeout=1)
# So also disconnect the socket
try:
# pylint: disable=protected-access
self.parent._socket.disconnect(self.parent.url.get())
except zmq.error.ZMQError:
# Might be already closed
pass
def on_stop(self):
"""Stop a running preview"""
self.on_unstage()
def poll(self):
"""Collect streamed updates"""
try:
t_last = time()
while True:
try:
# Exit loop and finish monitoring
if self._stop_polling:
logger.info(f"[{self.parent.name}]\tDetaching monitor")
break
# pylint: disable=no-member
# pylint: disable=protected-access
r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK)
# Length and throtling checks
if len(r) != 2:
logger.warning(
f"[{self.parent.name}] Received malformed array of length {len(r)}"
)
t_curr = time()
t_elapsed = t_curr - t_last
if t_elapsed < self.parent.throttle.get():
sleep(0.1)
continue
# Unpack the Array V1 reply to metadata and array data
meta, data = r
# Update image and update subscribers
header = json.loads(meta)
image = np.frombuffer(data, dtype=header["type"])
if image.size != np.prod(header["shape"]):
err = f"Unexpected array size of {image.size} for header: {header}"
raise ValueError(err)
image = image.reshape(header["shape"])
# Update image and update subscribers
self.parent.array_counter.put(header["frame"], force=True)
self.parent.ndimensions.put(len(header["shape"]), force=True)
self.parent.array_size.put(header["shape"], force=True)
# self.parent.array_data.put(data, force=True)
self.parent.shaped_image.put(image, force=True)
# pylint: disable=protected-access
self.parent._last_image = image
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image)
t_last = t_curr
logger.info(
f"[{self.parent.name}] Updated frame {header['frame']}\t"
f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}"
)
except ValueError:
# Happens when ZMQ partially delivers the multipart message
pass
except zmq.error.Again:
# Happens when receive queue is empty
sleep(0.1)
except Exception as ex:
logger.info(f"[{self.parent.name}]\t{str(ex)}")
raise
finally:
self._mon = None
logger.info(f"[{self.parent.name}]\tDetaching monitor")
class StdDaqPreviewDetector(PSIDetectorBase):
"""Detector wrapper class around the StdDaq preview image stream.
This was meant to provide live image stream directly from the StdDAQ but
also works with other ARRAY v1 streamers, like the AreaDetector ZMQ plugin.
Note that the preview stream must be already throtled in order to cope with
the incoming data and the python class might throttle it further.
NOTE: As an explicit request, it does not record the image data.
You can add a preview widget to the dock by:
cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')
"""
# Subscriptions for plotting image
USER_ACCESS = ["image", "savemode"]
SUB_MONITOR = "device_monitor_2d"
_default_sub = SUB_MONITOR
custom_prepare_cls = StdDaqPreviewMixin
# Configuration attributes
url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
throttle = Component(Signal, value=0.25, kind=Kind.config)
# Streamed data status
array_counter = Component(Signal, kind=Kind.hinted, metadata={"write_access": False})
ndimensions = Component(Signal, kind=Kind.normal, metadata={"write_access": False})
array_size = Component(Signal, kind=Kind.normal, metadata={"write_access": False})
# array_data = Component(Signal, kind=Kind.omitted, metadata={"write_access": False})
shaped_image = Component(Signal, kind=Kind.omitted, metadata={"write_access": False})
_last_image = None
def __init__(
self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs
) -> None:
super().__init__(*args, parent=parent, **kwargs)
self.url.set(url, force=True).wait()
# Connect to the DAQ
self.connect()
def connect(self):
"""Connect to te StDAQs PUB-SUB streaming interface
StdDAQ may reject connection for a few seconds when it restarts,
so if it fails, wait a bit and try to connect again.
"""
# pylint: disable=no-member
# Socket to talk to server
context = zmq.Context()
self._socket = context.socket(zmq.SUB)
self._socket.setsockopt(zmq.SUBSCRIBE, ZMQ_TOPIC_FILTER)
try:
self._socket.connect(self.url.get())
except ConnectionRefusedError:
sleep(1)
self._socket.connect(self.url.get())
def savemode(self, save=False):
"""Toggle save mode for the shaped image"""
# pylint: disable=protected-access
if save:
self.shaped_image._kind = Kind.normal
else:
self.shaped_image._kind = Kind.omitted
def image(self):
"""
Gets the last image as an attribute in case image must be abandoned
due to some caching on the BEC.
"""
return self._last_image
# Automatically connect to MicroSAXS testbench if directly invoked
if __name__ == "__main__":
daq = StdDaqPreviewDetector(url="tcp://129.129.95.111:20000", name="preview")
daq.wait_for_connection()
-14
View File
@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
"""
Ophyd devices for the PX III beamline, including the MX specific Aerotech A3200 stage.
@author: mohacsi_i
"""
from .A3200 import AerotechAbrStage
from .A3200utils import A3200Axis
from .SmarGonA import SmarGonAxis as SmarGonAxisA
from .SmarGonB import SmarGonAxis as SmarGonAxisB
from .StdDaqPreview import StdDaqPreviewDetector
from .NDArrayPreview import NDArrayPreview
from .SamCamDetector import SamCamDetector
from .PneumaticValve import PneumaticValve
-11
View File
@@ -1,11 +0,0 @@
// This file was autogenerated. Do not edit it manually.
## Device List
### pxiii_bec
| Device | Documentation | Module |
| :----- | :------------- | :------ |
| A3200Axis | Positioner wrapper for A3200 axes<br><br><br> Positioner wrapper for motors on the Aerotech A3200 controller. As the IOC<br> does not provide a motor record, this class simply wraps axes into a<br> standard Ophyd positioner for the BEC. It also has some additional<br> functionality for error checking and diagnostics.<br><br> Examples<br> --------<br> omega = A3200Axis('X06DA-ES-DF1:OMEGA', base_pv='X06DA-ES')<br><br> Parameters<br> ----------<br> prefix : str<br> Axis PV name root.<br> base_pv : str (situational)<br> IOC PV name root, i.e. X06DA-ES if standalone class.<br> | [pxiii_bec.devices.A3200utils](https://gitlab.psi.ch/bec/pxiii_bec/-/blob/main/pxiii_bec/devices/A3200utils.py) |
| AerotechAbrStage | Standard PX stage on A3200 controller<br><br> This is the wrapper class for the standard rotation stage layout for the PX<br> beamlines at SLS. It wraps the main rotation axis OMEGA (Aerotech ABR)and<br> the associated motion axes GMX, GMY and GMZ. The ophyd class associates to<br> the general PX measurement procedure, which is that the actual scan script<br> is running as an AeroBasic program on the controller and we communicate to<br> it via 10+1 global variables.<br> | [pxiii_bec.devices.A3200](https://gitlab.psi.ch/bec/pxiii_bec/-/blob/main/pxiii_bec/devices/A3200.py) |
| NDArrayPreview | Wrapper class around AreaDetector's NDStdArray plugins<br><br> This is a standalone class to display images from AreaDetector's<br> ImagePlugin without using a parent device. It also offers BEC exposed<br> methods to transfer image and change image array Kind-ness.<br><br> NOTE: As an explicit request, it can toggle data recording<br> | [pxiii_bec.devices.NDArrayPreview](https://gitlab.psi.ch/bec/pxiii_bec/-/blob/main/pxiii_bec/devices/NDArrayPreview.py) |
| SamCamDetector | Sample camera device<br><br> The SAMCAM continously streams images to the GUI and sample alignment<br> scripts via ZMQ.<br> | [pxiii_bec.devices.SamCamDetector](https://gitlab.psi.ch/bec/pxiii_bec/-/blob/main/pxiii_bec/devices/SamCamDetector.py) |
| SmarGonAxis | SmarGon client deice<br><br> This class controls the SmarGon goniometer via the REST interface. All<br> SmarGon axes share a common mutex to manage actual HW access.<br> | [pxiii_bec.devices.SmarGonB](https://gitlab.psi.ch/bec/pxiii_bec/-/blob/main/pxiii_bec/devices/SmarGonB.py) |
| StdDaqPreviewDetector | Detector wrapper class around the StdDaq preview image stream.<br><br> This was meant to provide live image stream directly from the StdDAQ but<br> also works with other ARRAY v1 streamers, like the AreaDetector ZMQ plugin.<br> Note that the preview stream must be already throtled in order to cope with<br> the incoming data and the python class might throttle it further.<br><br> NOTE: As an explicit request, it does not record the image data.<br><br> You can add a preview widget to the dock by:<br> cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')<br> | [pxiii_bec.devices.StdDaqPreview](https://gitlab.psi.ch/bec/pxiii_bec/-/blob/main/pxiii_bec/devices/StdDaqPreview.py) |
-6
View File
@@ -1,6 +0,0 @@
# Macros
This directory is intended to store macros which will be loaded automatically when starting BEC.
Macros are small functions to make repetitive tasks easier. Functions defined in python files in this directory will be accessible from the BEC console.
Please do not put any code outside of function definitions here. If you wish for code to be automatically run when starting BEC, see the startup script at pxiii_bec/bec_ipython_client/startup/post_startup.py
For a guide on writing macros, please see: https://bec.readthedocs.io/en/latest/user/command_line_interface.html#how-to-write-a-macro
View File
-357
View File
@@ -1,357 +0,0 @@
"""Utility functions for calculating energy, wavelength, and Bragg angle."""
from dataclasses import dataclass
import numpy as np
# from pxii_parameters import (EnergyDefaults, CamConversion)
@dataclass(frozen=True)
class Constants:
"""Constants used in energy calculations"""
# # Physical Constants from https://physics.nist.gov/cuu/Constants/index.html
ANGSTROM_CONVERSION = 1e10 # Convert meters to angstrom
PLANCK_CONST_EV = 4.135667696e-15 # eV/Hz
SPEED_OF_LIGHT = 299792458 # m/s
# d-spacings
d_spacing = {120: 3.13481, 298: 3.13562}
def speed_of_light_ang():
"""
Calculate the speed of light in angstroms per second.
Returns:
float: The speed of light converted to angstroms per second.
"""
return Constants.SPEED_OF_LIGHT * Constants.ANGSTROM_CONVERSION
def en_wav_factor():
"""
Calculate the energy wavelength factor.
This function computes a constant factor used to calculate energy
values in relation to wavelength by combining Planck's constant,
in eV/Hz, and the speed of light in angstrom.
Returns:
float: The computed energy wavelength factor.
"""
return Constants.PLANCK_CONST_EV * speed_of_light_ang()
# Helper Functions
def convert_to_degrees(angle_mrad: float) -> float:
"""
Convert an angle from milliradians to degrees.
Args:
angle_mrad: The angle value in milliradians.
Returns:
The angle converted into degrees as a float.
"""
return np.rad2deg(angle_mrad / 1000)
def create_conversion_result(
energy_ev: float, wavelength: float, bragg_angle_mrad: float
) -> dict:
"""
Creates a dictionary containing converted values of energy and angles.
This function takes the energy in electron-volts, the wavelength,
and the Bragg angle in milliradians as input. It computes and
returns a dictionary containing the energy in both electron-volts
and kiloelectron-volts, the wavelength, the Bragg angle in milliradians,
and the Bragg angle converted to degrees.
Args:
energy_ev: Energy value in electron-volts.
wavelength: Wavelength value.
bragg_angle_mrad: Bragg angle in milliradians.
Returns:
dict: A dictionary containing the following keys:
- "energy_kev": Energy value in kiloelectron-volts.
- "energy_ev": Energy value in electron-volts.
- "wavelength": Wavelength value.
- "bragg_angle_mrad": Bragg angle in milliradians.
- "bragg_angle_deg": Bragg angle in degrees.
"""
return {
"energy_kev": energy_ev / 1000,
"energy_ev": energy_ev,
"wavelength": wavelength,
"bragg_angle_mrad": float(bragg_angle_mrad),
"bragg_angle_deg": float(convert_to_degrees(bragg_angle_mrad)),
}
def print_conversion_result(result: dict) -> None:
"""
Prints the energy-related conversion results to the console.
"""
line = (
f"energy: {result['energy_ev']:.6g} eV, energy: {result['energy_kev']:.6g} keV, "
f"wavelength: {result['wavelength']:.4g} Å, "
f"bragg angle: {result['bragg_angle_mrad']:.5g} mrad, {result['bragg_angle_deg']:.4g} deg"
)
print(line)
# Conversion Functions
def calculate_wavelength_from_angle(bragg_angle_mrad: float, temp=120) -> float:
"""
calculate_wavelength_from_angle(bragg_angle_mrad: float) -> float
Arguments:
bragg_angle_mrad: The Bragg angle in milliradians, used to compute the
sine value required for the wavelength calculation.
Returns:
The calculated wavelength as a float value.
"""
d = Constants.d_spacing[temp]
return 2 * d * np.sin(bragg_angle_mrad / 1000)
def calculate_energy_from_wavelength(wavelength: float) -> float:
"""
Calculates the energy of a photon based on its wavelength.
Args:
wavelength: The wavelength of the photon in angstrom.
Returns:
The energy of the photon in eV.
"""
return en_wav_factor() / wavelength
def calculate_wavelength_from_energy(energy_ev: float) -> float:
"""
Calculates the wavelength of a photon from its energy.
Arguments:
energy_ev: float
The energy of the photon in electronvolts (eV).
Returns:
float
The calculated wavelength of the photon in angstrom.
"""
return en_wav_factor() / energy_ev
def calculate_bragg_angle_from_wavelength(wavelength: float, temp=120) -> float:
"""
Calculate the Bragg angle in milliradians for a given wavelength.
Args:
wavelength: The wavelength in angstrom.
Returns:
The Bragg angle in milliradians as a float.
"""
d = Constants.d_spacing[temp]
angle_rad = np.arcsin(wavelength / (2 * d))
return angle_rad * 1000
def convert_input_angle_to_mrad(bragg_angle: float) -> float:
"""
Convert input angle into milliradians (mrad).
This function takes an angle as input and determines its likely unit,
converting it to milliradians (mrad) if necessary. If the input value
is less than 1, it is assumed to be in radians and is converted to
mrad. If the input value falls between predefined minimum and
maximum values for mrad, it is assumed to be in degrees and thus
converted to mrad using the degrees-to-radians conversion factor.
For input values that don't match these scenarios, it assumes
that the input is already in mrad and returns it unchanged.
Arguments:
bragg_angle (float): The input Bragg angle, which can be in
radians, degrees, or milliradians.
Returns:
float: The Bragg angle converted into milliradians (mrad).
"""
if bragg_angle < 1: # Likely the input angle is in radians
return bragg_angle * 1000
if 3 < bragg_angle < 25: # Likely input angle is in degrees
return np.deg2rad(bragg_angle) * 1000
return bragg_angle # Already in mrad
# Core Functions
def validate_energy(energy_ev):
"""
Validates the energy value to ensure it falls within the acceptable range. The function
converts the provided energy from keV to eV if the input value is less than 1/1000 of the
maximum energy value. It then checks whether the energy is within the defined bounds.
If the energy value is outside the acceptable range, the function raises a ValueError.
Args:
energy_ev (float): The energy value in eV or keV to be validated. If this value is
smaller than 1/1000 of the maximum allowed energy (in eV), it will be multiplied
by 1000 to convert it from keV to eV.
Returns:
float: The validated energy value in eV that falls within the acceptable range.
Raises:
ValueError: If the energy value is outside the defined range of
[MIN_ENERGY_EV, MAX_ENERGY_EV].
"""
if energy_ev < EnergyDefaults.max_energy_ev / 1000: # Assuming the input is in keV.
energy_ev *= 1000
if not EnergyDefaults.min_energy_ev <= energy_ev <= EnergyDefaults.max_energy_ev:
raise ValueError(
f"Energy of {energy_ev} eV is outside the valid range "
f"({EnergyDefaults.min_energy_ev} eV to {EnergyDefaults.max_energy_ev} eV)"
)
return energy_ev
def convert_from_bragg(
bragg_angle_mrad: float, temp=120, print_result: bool = False
) -> dict:
"""
Convert the Bragg angle to wavelength and energy, returning the result as a dictionary.
This function converts a given Bragg angle (in milliradians) into the corresponding
wavelength and energy values, and returns them in a dictionary format. The function
also supports optional printing of the calculated results.
Args:
bragg_angle_mrad (float): The Bragg angle in milliradians to be converted.
print_result (bool): Whether to print the conversion result. Defaults to False.
Returns:
dict: A dictionary containing the following keys:
- 'energy_ev': Energy in electronvolts.
- 'wavelength': Wavelength corresponding to the input angle.
- 'bragg_angle_mrad': Input Bragg angle in milliradians.
"""
bragg_angle_mrad = convert_input_angle_to_mrad(bragg_angle_mrad)
wavelength = float(calculate_wavelength_from_angle(bragg_angle_mrad, temp=temp))
energy_ev = float(calculate_energy_from_wavelength(wavelength))
result = create_conversion_result(energy_ev, wavelength, bragg_angle_mrad)
if print_result:
print_conversion_result(result)
return result
def convert_from_energy(energy_ev: float, temp=120, print_result: bool = False) -> dict:
"""
Convert energy in electron volts (eV) to wavelength and Bragg angle in milliradians
(mrad). This method validates the given energy, calculates corresponding properties,
and optionally prints the result.
Args:
energy_ev: Energy value in electron volts (float) to be converted.
print_result: Flag indicating whether to print the resulting
conversion details (bool). Defaults to False.
Returns:
A dictionary containing the following key-value pairs:
- "energy_ev" (float): Validated energy in eV.
- "wavelength" (float): Calculated wavelength in meters.
- "bragg_angle_mrad" (float): Calculated Bragg angle in mrad.
"""
energy_ev = validate_energy(energy_ev)
wavelength = calculate_wavelength_from_energy(energy_ev)
bragg_angle_mrad = float(
calculate_bragg_angle_from_wavelength(wavelength, temp=temp)
)
result = create_conversion_result(energy_ev, wavelength, bragg_angle_mrad)
if print_result:
print_conversion_result(result)
return result
def convert_from_wavelength(
wavelength: float,
temp: float = 120,
print_result: bool = False,
) -> dict:
"""
Convert a given wavelength value into corresponding energy, Bragg angle, and
generate a result dictionary.
The function processes a wavelength value, checks its validity against a
permitted range, calculates corresponding energy and Bragg angle, and
formats the results into a dictionary. Optionally, the function can print
the result.
Parameters:
wavelength: float
The input wavelength value in Angstroms to be converted. Should
fall within the permitted wavelength range.
print_result: bool
Optional flag indicating whether to print the conversion result.
Default is False.
Returns:
dict
A dictionary containing the energy (electron-volts), wavelength
(Angstroms), and Bragg angle (milliradians). If the wavelength is
outside of the permitted range, returns None.
"""
energy_ev = calculate_energy_from_wavelength(wavelength)
bragg_angle_mrad = float(
calculate_bragg_angle_from_wavelength(wavelength, temp=temp)
)
result = create_conversion_result(energy_ev, wavelength, bragg_angle_mrad)
if print_result:
print_conversion_result(result)
return result
def calc_perp_position(
energy_ev: float,
print_result: bool = False,
) -> float:
"""
Calculate the perpendicular motor position based on provided energy in electron-volts (eV).
This function computes the perpendicular motor position using the given energy value in
electron-volts. The calculation is based on the Bragg angle derived from the energy. An optional
parameter allows printing the result during execution.
Parameters:
energy_ev (float): The energy value in electron-volts used for the calculation.
print_result (bool): Flag to determine whether to print the computed perpendicular offset.
Default is False.
Returns:
float: The computed perpendicular position.
Raises:
None
"""
result = convert_from_energy(energy_ev, print_result=False)
bragg_angle_rad = result["bragg_angle_mrad"] / 1000
perp_offset = float(EnergyDefaults.beam_offset / (2 * np.cos(bragg_angle_rad))) - 3
if print_result:
print(f"Perp = {perp_offset: .4f}")
return perp_offset
def calc_scam_microns(pixels, zoom = 1000):
return pixels/(0.5208 * np.exp(0.002586 * zoom))
def calc_scam_microns(pixels, zoom=1000):
"""Convert pixels to microns for the sample camera"""
return float(pixels / (CamConversion.a * np.exp(CamConversion.b * zoom)))
def calc_bsccam_microns(pixels):
"""Convert pixels to microns for the BSC camera"""
return pixels*20
-495
View File
@@ -1,495 +0,0 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable
from datetime import datetime
from bec_lib.device import Signal, Positioner
# -------------------------------------------------------------------
# Status Enum
# -------------------------------------------------------------------
class Status(Enum):
OK = 0
WARNING = 1
ERROR = 2
UNKNOWN = 3
@property
def color(self):
return {
Status.OK: "green",
Status.WARNING: "yellow",
Status.ERROR: "red",
Status.UNKNOWN: "blue",
}[self]
@property
def color_scilog(self):
return {
Status.OK: "green",
Status.WARNING: "",
Status.ERROR: "red",
Status.UNKNOWN: "",
}[self]
# -------------------------------------------------------------------
# Health Result Object
# -------------------------------------------------------------------
@dataclass
class HealthCheckResult:
name: str
description: str
status: Status
value: Any = None
message: str = ""
category: str = "general"
def __str__(self):
if self.status == Status.OK:
return f"[{self.status.name}] {self.description}"
return (
f"[{self.status.name}] "
f"{self.description}: {self.message}"
)
def formatted_message(self):
if self.status == Status.OK:
return f"[{self.status.name}] {self.name}"
return (
f"[{self.status.name}] "
f"{self.description}: {self.message}"
)
# -------------------------------------------------------------------
# Send to SciLog
# -------------------------------------------------------------------
def send_to_scilog(results):
counts = {
Status.OK: 0,
Status.WARNING: 0,
Status.ERROR: 0,
Status.UNKNOWN: 0,
}
for result in results:
counts[result.status] += 1
timestamp = datetime.now().strftime("%Y/%m/%d %H:%M")
msg = bec.messaging.scilog.new()
msg.add_text(
f"Beamline Health Summary {timestamp}",
bold = True
)
msg.add_text("\n")
for status, count in counts.items():
msg.add_text(
f"{status.name:<10}: {count}",
bold=True,
)
msg.add_text("\n")
for result in results:
msg.add_text(
result.formatted_message(),
# bold=result.status != Status.OK,
color = result.status.color_scilog
)
msg.add_text("\n")
msg.add_tags(["beamline health check"])
msg.send()
# -------------------------------------------------------------------
# Configuration
# -------------------------------------------------------------------
@dataclass
class BeamlineHealthConfig:
signal_rules: dict[str, Callable] = field(
default_factory=lambda: {
"cam": lambda x: x != 0,
"bpm": lambda x: x != 0,
}
)
motor_tolerances: dict[str, float] = field(
default_factory=lambda: {
# examples
# "mono_theta": 0.001,
# "detector_z": 0.1,
}
)
default_motor_tolerance: float = 0.02
# -------------------------------------------------------------------
# Device Collection
# -------------------------------------------------------------------
def get_devices():
return list(dev.items())
# -------------------------------------------------------------------
# Signal Checks
# -------------------------------------------------------------------
def check_signals(devices, config: BeamlineHealthConfig):
results = []
signal_devices = [
(name, obj)
for name, obj in devices
if isinstance(obj, Signal)
]
for name, obj in signal_devices:
try:
data = obj.read()
actual = data[name]["value"]
description = obj.description
except Exception as e:
results.append(
HealthCheckResult(
name=name,
description=name,
status=Status.UNKNOWN,
message=f"Failed to read signal: {e}",
category="signals",
)
)
continue
matched = False
for keyword, rule in config.signal_rules.items():
if keyword in name:
matched = True
try:
passed = rule(actual)
except Exception as e:
results.append(
HealthCheckResult(
name=name,
description=name,
status=Status.UNKNOWN,
value=actual,
message=f"Rule evaluation failed: {e}",
category="signals",
)
)
break
if passed:
results.append(
HealthCheckResult(
name=name,
description=description,
status=Status.OK,
value=actual,
category="signals",
)
)
else:
results.append(
HealthCheckResult(
name=name,
description=description,
status=Status.ERROR,
value=actual,
message=f"Signal value {actual} failed validation",
category="signals",
)
)
break
if not matched:
continue
return results
# -------------------------------------------------------------------
# Motor Checks
# -------------------------------------------------------------------
def check_motors(devices, config: BeamlineHealthConfig):
results = []
motor_devices = [
(name, obj)
for name, obj in devices
if isinstance(obj, Positioner)
]
for name, obj in motor_devices:
try:
data = obj.read()
description = obj.description
actual = data[name]["value"]
error_code = obj.motor_status.get()
move_state = obj.motor_is_moving.get()
except Exception as e:
results.append(
HealthCheckResult(
name=name,
description=name,
status=Status.UNKNOWN,
message=f"Failed to read motor: {e}",
category="motors",
)
)
continue
# -----------------------------------------------------------
# Error state
# -----------------------------------------------------------
if error_code != 0:
results.append(
HealthCheckResult(
name=name,
description=description,
status=Status.ERROR,
value=error_code,
message=f"motor error code: {error_code}",
category="motors",
)
)
continue
# -----------------------------------------------------------
# Moving state
# -----------------------------------------------------------
if move_state != 0:
results.append(
HealthCheckResult(
name=name,
description=description,
status=Status.WARNING,
value=move_state,
message="motor is currently moving",
category="motors",
)
)
continue
# -----------------------------------------------------------
# Setpoint comparison
# -----------------------------------------------------------
sp_key = f"{name}_user_setpoint"
if sp_key in data:
setpoint = data[sp_key]["value"]
diff = abs(actual - setpoint)
tolerance = config.motor_tolerances.get(
name,
config.default_motor_tolerance,
)
if diff > tolerance:
results.append(
HealthCheckResult(
name=name,
description=description,
status=Status.WARNING,
value=diff,
message=(
f"Setpoint {setpoint:.5g} differs "
f"from readback {actual:.5g} "
f"by {diff:.4g}"
),
category="motors",
)
)
else:
results.append(
HealthCheckResult(
name=name,
description=description,
status=Status.OK,
value=actual,
category="motors",
)
)
else:
results.append(
HealthCheckResult(
name=name,
description=description,
status=Status.UNKNOWN,
message="No setpoint available",
category="motors",
)
)
return results
# -------------------------------------------------------------------
# Main Check Entry Point
# -------------------------------------------------------------------
def check2(config: BeamlineHealthConfig | None = None):
if config is None:
config = BeamlineHealthConfig()
devices = get_devices()
results = []
results.extend(check_signals(devices, config))
results.extend(check_motors(devices, config))
# ---------------------------------------------------------------
# Sort by severity
# ---------------------------------------------------------------
results.sort(
key=lambda r: r.status.value,
)
return results
# -------------------------------------------------------------------
# Summary Printer
# -------------------------------------------------------------------
def summary_text(results):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
n_ok = sum(r.status == Status.OK for r in results)
n_warn = sum(r.status == Status.WARNING for r in results)
n_err = sum(r.status == Status.ERROR for r in results)
n_unknown = sum(r.status == Status.UNKNOWN for r in results)
return (
f"==========================================\n"
f"Beamline Health Check at {timestamp}\n"
f"==========================================\n"
f"OK : {n_ok}\n"
f"WARNING : {n_warn}\n"
f"ERROR : {n_err}\n"
f"UNKNOWN : {n_unknown}\n"
"==========================================\n"
)
# return "\n".join(lines)
# -------------------------------------------------------------------
# Filter results
# -------------------------------------------------------------------
def filter_results(results, statuses = None):
if statuses is None:
return results
return [
r for r in results
if r.status in statuses
]
# -------------------------------------------------------------------
# CLI Entry Point
# -------------------------------------------------------------------
def run_check(show_all= False):
results = check2()
print(summary_text(results))
problem_results = filter_results(
results,
statuses={
Status.WARNING,
Status.ERROR,
Status.UNKNOWN,
}
)
send_to_scilog(results)
if not show_all:
results = filter_results(
results,
statuses={
Status.WARNING,
Status.ERROR,
Status.UNKNOWN
}
)
for result in results:
print(result)
-252
View File
@@ -1,252 +0,0 @@
"""Get data from an h5 file or BEC history and perform fitting."""
import numpy as np
from lmfit.models import (
GaussianModel,
LorentzianModel,
VoigtModel,
ConstantModel,
LinearModel,
)
from scipy.ndimage import gaussian_filter1d
import h5py
import matplotlib.pyplot as plt
def create_fit_parameters(
deriv: bool = False,
model: str = "Voigt",
baseline: str = "Linear",
smoothing: None = None,
):
"""Store the fit parameters in a dictionary."""
# map input model to lmfit model name
model_mappings = {
"Gaussian": GaussianModel,
"Lorentzian": LorentzianModel,
"Voigt": VoigtModel,
"Constant": ConstantModel,
"Linear": LinearModel,
}
return {
"deriv": deriv,
"model": model_mappings[model],
"baseline": model_mappings[baseline],
"smoothing": smoothing,
}
def get_data_from_h5(signal_name: str = "lu_bpmsum"):
"""Get data from an h5 file."""
with h5py.File("scan_676.h5", "r") as f:
entry = f["entry"]["collection"]
y_data = entry["devices"][signal_name][signal_name]["value"][:]
motor_data = entry["metadata"]["bec"]
motor_name = motor_data["scan_motors"][0].decode()
scan_number = motor_data["scan_number"][()]
x_data = entry["devices"][motor_name][motor_name]["value"][:]
return {
"x_data": x_data,
"y_data": y_data,
"signal_name": signal_name,
"motor_name": motor_name,
"scan_number": str(scan_number),
}
def get_data_from_history(
history_index: int,
signal_name: str = "lu_bpmsum",
):
"""Read data from the BEC history and return the X and Y data as arrays."""
scan = bec.history[history_index]
md = scan.metadata["bec"]
motor_name = md["scan_motors"][0].decode()
scan_number = md["scan_number"]
x_data = scan.devices[motor_name][motor_name].read()["value"]
y_data = scan.devices[signal_name][signal_name].read()["value"]
return {
"signal_name": signal_name,
"x_data": x_data,
"y_data": y_data,
"motor_name": motor_name,
"scan_number": scan_number,
}
def process_data(data, fit_params):
"""
Process the signal data for fitting based on derivative or smoothing.
"""
smoothing, deriv = fit_params["smoothing"], fit_params["deriv"]
signal_name = data["signal_name"]
y_data = data["y_data"]
if deriv:
if smoothing:
y_smooth = gaussian_filter1d(y_data, smoothing)
fitting_data = np.gradient(y_smooth)
signal_name = f"Derivative of smoothed {signal_name}"
else:
fitting_data = np.gradient(y_data)
signal_name = f"Derivative of {signal_name}"
elif smoothing and smoothing > 0.01:
fitting_data = gaussian_filter1d(y_data, smoothing)
signal_name = f"Smoothed {signal_name}"
else:
fitting_data = y_data
updated_data = {
"y_to_fit": fitting_data,
"signal_name": signal_name,
}
data.update(updated_data)
return data
def fit(data, fit_params):
"""Fit a signal to a model and return the fitting results."""
# Create the model
peak_model = fit_params["model"](prefix="peak_")
baseline_model = fit_params["baseline"](prefix="base_")
full_model = peak_model + baseline_model
# Prepare data
processed_data = process_data(data, fit_params)
params = full_model.make_params()
y_min = np.min(processed_data["y_to_fit"])
# Configure baseline parameters
if fit_params["baseline"] == ConstantModel:
params["base_c"].set(value=y_min)
elif fit_params["baseline"] == LinearModel:
params["base_intercept"].set(value=y_min)
params["base_slope"].set(value=0)
# Add peak-specific parameters
params.update(
peak_model.guess(processed_data["y_to_fit"], x=processed_data["x_data"])
)
# Perform the fitting
lmfit_result = full_model.fit(
processed_data["y_to_fit"], params, x=processed_data["x_data"]
)
# Find the X that gives the max Y
max_index = np.argmax(processed_data["y_to_fit"])
x_max = processed_data["x_data"][max_index]
# Generate data for a smoothed fit curve
fit_xdata = np.linspace(np.min(data["x_data"]), np.max(data["x_data"]), 500)
fit_ydata = lmfit_result.eval(x=fit_xdata, params=lmfit_result.params)
# Collect results
return {
"model": fit_params["model"].__name__,
"fwhm": lmfit_result.params["peak_fwhm"].value,
"centre": lmfit_result.best_values["peak_center"],
"height": lmfit_result.params["peak_height"].value,
"chi_sq": lmfit_result.chisqr,
"lmfit_result": lmfit_result,
"x_max": x_max,
"fit_xdata": fit_xdata,
"fit_ydata": fit_ydata,
}
def plot_fitted_data(data, fit_result):
"""Plot the original data and the fitted model."""
plt.plot(data["x_data"], data["y_to_fit"], label="Data")
plt.plot(fit_result['fit_xdata'], fit_result['fit_ydata'], label="Fit")
plt.xlabel(data["motor_name"])
plt.ylabel(data["signal_name"])
plt.title(f"Scan {data['scan_number']}, fitted with {fit_result['model']}")
plt.grid(True)
plt.legend()
plt.show()
def select_bec_window(dock_area_name="Fitting"):
"""Check to see if the fitting results dock is already open and re-create it if not"""
open_docks = bec.gui.windows
if open_docks.get(dock_area_name) is None:
dock_area = bec.gui.new(dock_area_name)
# wf = dock_area.new("Plot").new(bec.gui.available_widgets.Waveform)
wf = dock_area.new(widget='Waveform', object_name='Plot')
text_box = dock_area.new(widget='TextBox', object_name="Results", where="bottom")
else:
wf = bec.gui.Fitting.Plot
text_box = bec.gui.Fitting.Results
return wf, text_box
def plot_live_data_bec(
motor_name,
signal_name,
window_name="Fitting"
):
"""
Plotting live data for motor and signal using BEC.
This function plots live data from a specified motor and signal.
It clears the current plot window, sets its title, labels the axes
with the provided motor and signal names, and initializes live plotting
on the given signal against the motor.
Args:
motor_name (str): The name of the motor to be used as the x-axis.
signal_name (str): The name of the signal to be used as the y-axis.
Returns:
None
"""
wf, text_box = select_bec_window(window_name)
text_box.set_plain_text("Plotting live data")
wf.clear_all()
wf.title = "Scan: Live scan"
wf.x_label = motor_name
wf.y_label = signal_name
wf.plot(device_x=motor_name, device_y=signal_name)
def plot_fitted_data_bec(
data,
fit_result,
):
"""
Plot fitted data and display fitting parameters in the specified window.
This function selects a BEC window and plots the original data along with the
fitted function. Additionally, it displays the fitting results in a text
box within the same window for better visualization of the fit results.
Parameters:
data : dict
Dictionary containing the original dataset, where 'x_data' and 'y_to_fit'
hold the independent variable and the dependent variable, respectively,
'scan_number' represents the scan number, 'motor_name' and 'signal_name'
provide axis labels.
fit_result : dict
Dictionary containing the results of the fit, including parameters such
as 'centre', 'fwhm', 'height', and the fitted model stored under
'lmfit_result', with its 'best_fit' attribute representing the fitted data.
"""
wf, text_box = select_bec_window()
fit_text = (
f"Fit parameters: Centre = {fit_result['centre']:.4f}, "
f"FWHM = {fit_result['fwhm']:.3f}, "
f"Height = {fit_result['height']:.4f}\n"
f"Model = {fit_result['model']}\n"
f"Chi sq = {fit_result['chi_sq']:.3g}"
)
text_box.set_plain_text(fit_text)
wf.clear_all()
wf.title = f"Scan: {data['scan_number']}"
wf.x_label = data["motor_name"]
wf.y_label = data["signal_name"]
wf.plot(x=data["x_data"], y=data["y_to_fit"], label="Data")
wf.plot(x=fit_result["fit_xdata"], y=fit_result["fit_ydata"], label="Fit")
# wf.Fit.set(symbol_size = 0)
wf.get_curve('Fit').set(symbol_size=0)
-321
View File
@@ -1,321 +0,0 @@
"""Use the methods in mx_basics to perform:
1) a go_to_peak scan, that scans a motor, finds the peak position and moves to peak
2) fits data from a bec history file
"""
from dataclasses import dataclass
import numpy as np
# from pxiii_parameters import FitDefaults, BPMScans, MirrorConfig
# from mx_basics import (
# create_fit_parameters,
# get_data_from_history,
# fit,
# plot_fitted_data_bec,
# plot_live_data_bec,
# )
# Method functions
def calculate_step_size(start: float, stop: float, steps: int) -> float:
"""
Provides the function to calculate the step size for dividing a specified range
into a given number of steps.
Args:
start: The starting value of the range.
stop: The stopping value of the range.
steps: The number of steps to divide the range into. Must be at least 1.
Raises:
ValueError: If the steps value is less than 1.
Returns:
The calculated step size as a float, rounded to three decimal places.
"""
if steps < 1:
raise ValueError("Number of steps must be at least 1.")
return round((stop - start) / steps, 3)
def move_to_position(motor_device, motor_name: str, position: float, data: dict):
"""
Function to move a specified motor device to a given position.
The function verifies if the requested position is within the scan range of the
motor device provided. If the position is outside the range, the motor is
moved to the center of its scan range, an error message is raised, and the
operation is halted. If the position is valid, the motor is moved to the
specified position.
Parameters:
motor_device: The motor device to be moved.
motor_name: str
The name of the motor as a string
position: float
The desired position to move the motor to. Position should be within
the scan range of the motor determined by the provided data.
data: dict
A dictionary containing "x_data", which is used to determine the
scan range of the motor.
Raises:
ValueError: Raised if the specified position is outside the valid scan
range determined by "x_data" in the data dictionary. The motor will
return to the center of its scan range in this case.
"""
motor_min = np.min(data["x_data"])
motor_max = np.max(data["x_data"])
motor_centre = (motor_max + motor_min) / 2
if not motor_min <= position <= motor_max:
scans.umv(motor_device, motor_centre, relative=False)
msg = (
f"Position {position: .2f} is outside the scan range of "
f"{motor_min: .2f} to {motor_max: .2f}. "
f"Returning to centre of scan range {motor_centre: .3f}."
)
raise ValueError(msg)
motor_position = round(position, 4)
scans.umv(motor_device, motor_position, relative=False)
print(f"\n Moving {motor_name} to position {motor_position: .3f}")
# @dataclass(frozen=True)
# class FitDefaults:
# """Default values for fitting routines"""
# # Constants for default models, baselines, and parameters
# MODEL = "Voigt"
# BASELINE = "Linear"
# SETTLE_TIME = 0.1
# RELATIVE_MODE = True
def go_to_peak(
motor_device,
signal_device,
start: float,
stop: float,
steps: int,
relative: bool = FitDefaults.RELATIVE_MODE,
plot: bool = True,
settle: float = FitDefaults.SETTLE_TIME,
confirm: bool = True,
gomax: bool = False,
):
"""
Go to the peak of a signal by scanning a motor within a specified range and
identifying the optimal position based on signal peak data.
Parameters:
motor_device: The motor device to be scanned.
signal_device: The signal device to monitor during the scan.
start (float): The starting position of the scan. Ignored if `relative` is True.
stop (float): The ending position of the scan. Ignored if `relative` is True.
steps (int): The number of steps to divide the scan range into.
relative (bool, optional): If True, interpret `start` and `stop` as relative to
the current motor position. Defaults to RELATIVE_MODE constant.
plot (bool, optional): If True, plot the scan data and the fitted results.
Defaults to True.
settle (float, optional): The time in seconds to wait after each step for the
signal to stabilize. Defaults to DEFAULT_SETTLE_TIME constant.
confirm (bool, optional): If True, ask for user confirmation before starting
the scan. Defaults to True.
Raises:
Exception: Raises exceptions potentially raised by dependent functions or
operations such as plotting, fitting, or motor movement.
Returns:
None
"""
motor_name = motor_device.name
signal_name = signal_device.name
# wf.plot(x_name=motor_name, y_name=signal_name)
if plot:
plot_live_data_bec(motor_name, signal_name)
# Validate and calculate step size
step_size = calculate_step_size(start, stop, steps)
# Confirm the scan range
# current_motor_position = motor_device.user_readback.get()
current_motor_position = motor_device.read()[motor_name]["value"]
if confirm:
if relative:
scan_start = current_motor_position + start
scan_end = current_motor_position + stop
print(
f"\nScanning from {scan_start: .6g} to {scan_end: .6g} in "
f"{steps} steps of size {step_size}"
)
print(f"Relative mode = {relative}")
else:
print(
f"\nScanning from {start: .5g} to {stop: .5g} in {steps} steps of size {step_size}"
)
print(f"Relative mode = {relative}")
input("Press Enter to continue...")
# Perform the scan
scan_result = scans.line_scan(
motor_device, start, stop, steps=steps, relative=relative, settling_time=settle
)
motor_data = scan_result.scan.live_data[motor_name][motor_name].val
signal_data = scan_result.scan.live_data[signal_name][signal_name].val
scan_number = "Current"
data = {
"x_data": np.array(motor_data),
"y_data": np.array(signal_data),
"motor_name": motor_name,
"signal_name": signal_name,
"motor_device": motor_device,
"scan_number": scan_number,
}
# Define and fit model to scan data
fit_params = create_fit_parameters(False, FitDefaults.MODEL, FitDefaults.BASELINE)
fit_result = fit(data, fit_params)
# Plot the fitted data if plot = True
if plot:
plot_fitted_data_bec(data, fit_result)
# If gomax is set then move to the maximum value, rather than the fit centre
if gomax:
value = fit_result["x_max"]
print(f"Max position is at {value}")
move_to_position(data["motor_device"], data["motor_name"], fit_result["x_max"], data)
else:
# Safely move the motor to the peak position
move_to_position(data["motor_device"], data["motor_name"], fit_result["centre"], data)
def fit_history(
history_index: int,
signal_name: str,
deriv: bool = False,
model: str = FitDefaults.MODEL,
move_to_peak: bool = False,
):
"""
Retrieve and analyze historical data by fitting a model, optionally moving to
a peak position.
Parameters:
history_index (int): Index of the historical data set to retrieve.
signal_name (str): Name of the signal to fit.
deriv (bool, optional): Whether to include the derivative in the fitting
procedure. Defaults to False.
model (str, optional): Name of the model to use for fitting. Defaults to
DEFAULT_MODEL.
move_to_peak (bool, optional): Whether to move the motor to the peak position
after fitting. Defaults to False.
Raises:
KeyError: If required keys are not found in the retrieved data dictionary.
ValueError: If the fitting process fails or produces invalid results.
Returns:
None
"""
# Retrieve historical data
data = get_data_from_history(history_index, signal_name)
# Define fitting parameters
fit_params = create_fit_parameters(deriv, model, FitDefaults.BASELINE)
# Perform fit and plot the data
fit_result = fit(data, fit_params)
plot_fitted_data_bec(data, fit_result)
# Optionally move the motor to the peak position
if move_to_peak:
move_to_position(data["motor_device"], data["motor_name"], fit_result["centre"], data)
def scan_bpm(bpmname):
"""
Runs a grid scan of a BPM in x and y, and plots each channel
as a heatmap.
Parameters:
bpmname: the name of the bpm to be scanned e.g. "fe"
"""
# Open a dock area and set up the heatmaps
dock_area = bec.gui.new("XBPM_Scan")
wf5 = dock_area.new("Sum").new(bec.gui.available_widgets.Heatmap)
wf1 = dock_area.new("Ch1", relative_to="Sum", position="bottom").new(
bec.gui.available_widgets.Heatmap
)
wf3 = dock_area.new("Ch3", relative_to="Ch1", position="right").new(
bec.gui.available_widgets.Heatmap
)
wf4 = dock_area.new("Ch4", relative_to="Ch3", position="bottom").new(
bec.gui.available_widgets.Heatmap
)
wf2 = dock_area.new("Ch2", relative_to="Ch1", position="bottom").new(
bec.gui.available_widgets.Heatmap
)
wfscan = dock_area.new("ScanControl").new(bec.gui.available_widgets.ScanControl)
cfg = getattr(BPMScans, bpmname)
wf1.x_label = cfg["x_name"]
wf1.y_label = cfg["y_name"]
wf1.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z1_name"], color_map="plasma")
wf2.x_label = cfg["x_name"]
wf2.y_label = cfg["y_name"]
wf2.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z2_name"], color_map="plasma")
wf3.x_label = cfg["x_name"]
wf3.y_label = cfg["y_name"]
wf3.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z3_name"], color_map="plasma")
wf4.x_label = cfg["x_name"]
wf4.y_label = cfg["y_name"]
wf4.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z4_name"], color_map="plasma")
wf5.x_label = cfg["x_name"]
wf5.y_label = cfg["y_name"]
wf5.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z5_name"], color_map="plasma")
# Run the scan
x_mot = cfg["x_device"]
y_mot = cfg["y_device"]
# scans.grid_scan(x_mot, -0.5, 0.5, 20, y_mot, -0.5, 0.5, 20,
# exp_time=0.5, relative=False, snaked=True)
def optimise_kb(mirror):
"""
Runs a grid scan of a the upstream and downstream benders,
and plots a heatmap of the sample camera x or y sigma.
Parameters:
mirror: either "hfm" or :vfm"
"""
# Open a dock area and set up the heatmaps
dock_area = bec.gui.new(mirror)
wf1 = dock_area.new("Heatmap").new(bec.gui.available_widgets.Heatmap)
wfscan = dock_area.new("ScanControl").new(bec.gui.available_widgets.ScanControl)
cfg = getattr(MirrorConfig, mirror)
wf1.x_label = cfg["bu_name"]
wf1.y_label = cfg["bd_name"]
wf1.plot(x_name=cfg["bu_name"], y_name=cfg["bd_name"], z_name=cfg["z_name"], color_map="plasma")
# Run the scan
x_mot = cfg["x_device"]
y_mot = cfg["y_device"]
# scans.grid_scan(x_mot, -0.02, 0.02, 11, y_mot, -0.02, 0.02, 11,
# exp_time=0.5, relative=True, snaked=True)
-251
View File
@@ -1,251 +0,0 @@
"""Script to change energy at PXIII by setting DCCM motors and mirror stripe
Moving DCCM motors - implemented for dccm_theta1 and dccm_theta2
Mirrors - change of mirror stripe is not yet implemented
Plotting optional
"""
import pandas as pd
import numpy as np
# from mx_methods import go_to_peak
# from pxiii_parameters import EnergyDefaults
# from calculator import (
# validate_energy,
# convert_from_bragg,
# convert_from_energy,
# )
def get_current_energy():
"""
Returns the energy in eV from the current bragg angle.
"""
# current_bragg_angle = dev.dccm_theta1.user_readback.get()
current_bragg_angle = -EnergyDefaults.energy.user_readback.get()
current_energy = convert_from_bragg(current_bragg_angle, print_result=False)[
"energy_ev"
]
return current_energy
# Functions below are common to all beamlines
def calculate_energy_difference(current_energy, target_energy):
"""
Calculate the absolute difference in energy between the current energy level
and the target energy level.
"""
return abs(target_energy - current_energy)
def interpolate_column(energy_ev, x_values, y_values):
"""
Perform interpolation for a specific column of data.
This function uses numpy's interpolation method to perform linear
interpolation. It calculates the interpolated y-values corresponding
to the given energy in electron-volts (energy_ev), based on specified
x-values and y-values.
Parameters:
energy_ev (array-like): Array of energy values in electron-volts
at which interpolation is needed.
x_values (array-like): Array of x-values corresponding to known
data points.
y_values (array-like): Array of y-values corresponding to known
data points.
Returns:
numpy.ndarray: Interpolated y-values computed for the energy_ev
input.
"""
return np.interp(energy_ev, x_values, y_values)
def get_value_from_lut(energy_ev):
"""
Retrieve interpolated values from a Lookup Table (LUT) based on the provided energy
in electron volts (eV).
This function reads a CSV file containing energy lookup data and processes the LUT
to return interpolated values for specified relevant columns. The interpolation is
performed for the provided energy value using the LUT's energy and data values.
Args:
energy_ev (float): The energy value in electron volts for which the interpolated data is
required.
Returns:
dict: A dictionary where the keys are the relevant column names from the LUT, and the values
are the interpolated values as floats.
"""
energy_lookup_data = pd.read_csv(EnergyDefaults.LUT_table)
column_names = energy_lookup_data.columns.tolist()
lut_values = energy_lookup_data.values.astype(float).T
# Filter relevant columns for interpolation
relevant_columns = {"dccm_pitch"}
int_values = {}
for i, col_name in enumerate(column_names):
if col_name in relevant_columns:
int_values[col_name] = float(
interpolate_column(energy_ev, lut_values[0], lut_values[i])
)
return int_values
def get_mirror_stripe(energy_ev):
"""
Determines the mirror stripe material based on the energy level provided.
This function evaluates the given energy level in electron volts (eV) and
identifies the material type that corresponds to the specified thresholds
for silicon, rhodium, and, if applicable, platinum.
Args:
energy_ev (float): Energy level in electron volts, used to determine
the corresponding material type.
Returns:
str: A string indicating the material type corresponding to the provided
energy level. Possible values are "silicon", "rhodium", or "platinum".
"""
if energy_ev <= EnergyDefaults.stripe_thresholds["silicon"]:
return "silicon"
if (
EnergyDefaults.stripe_thresholds["silicon"]
< energy_ev
<= EnergyDefaults.stripe_thresholds["rhodium"]
):
return "rhodium"
return "platinum"
def set_mirror_stripe(energy_ev):
"""
Selects and sets the appropriate mirror stripe based on the given energy value
in electron volts (eV). Prints the selected mirror stripe.
Args:
energy_ev (float): The energy value in electron volts used to determine
the appropriate mirror stripe.
Returns:
None
"""
selected_stripe = get_mirror_stripe(energy_ev)
print(f"Selected mirror stripe: {selected_stripe}")
def mono_pitch_scan(plot=True):
"""Scan the monochromator pitch and move to the peak."""
if plot:
print("Scanning monochromator pitch and moving to peak, with plotting.")
go_to_peak(
EnergyDefaults.mono_pitch,
EnergyDefaults.signals["sig1"],
-EnergyDefaults.pitch_scan["halfwidth"],
EnergyDefaults.pitch_scan["halfwidth"],
steps=EnergyDefaults.pitch_scan["steps"],
relative=True,
settle=0.01,
plot=True,
confirm=False,
)
else:
print("Scanning monochromator pitch and moving to peak, without plotting.")
go_to_peak(
EnergyDefaults.mono_pitch,
EnergyDefaults.signals["sig1"],
-EnergyDefaults.pitch_scan["halfwidth"],
EnergyDefaults.pitch_scan["halfwidth"],
steps=EnergyDefaults.pitch_scan["steps"],
relative=True,
settle=0.01,
plot=False,
confirm=False,
)
def get_dccm_motors_positions(energy_ev):
"""
Retrieve the positions of DCCM motors based on given energy value.
The function returns a dictionary containing all
calculated motor positions.
Arguments:
energy_ev (float): The energy value in electron volts for which the
DCCM motor positions are to be calculated.
Returns:
dict: A dictionary containing the calculated DCCM motor positions
including values retrieved from the lookup table, if applicable.
"""
# dccm_motor_values = get_value_from_lut(energy_ev)
th1_angle = -convert_from_energy(energy_ev, print_result=False)["bragg_angle_deg"]
th2_angle = convert_from_energy(energy_ev, temp=298, print_result=False)["bragg_angle_deg"]
# dccm_motor_values.update({"theta1_angle": th1_angle, "theta2_angle": th2_angle})
dccm_motor_values = {"theta1_angle": th1_angle, "theta2_angle": th2_angle}
return dccm_motor_values
def move_dccm_motors(energy_ev):
"""
Move the DCCM theta1 and theta2 motors to the required positions
for the given energy in eV.
"""
dccm_pos = get_dccm_motors_positions(energy_ev)
print(
f"Moving DCCM theta1: {dccm_pos['theta1_angle']: .5g} deg, theta2: {dccm_pos['theta2_angle']: .5g} deg, "
# f"DCM pitch: {dcm_pos['dcm_pitch']: .5g} mrad, "
)
umv(EnergyDefaults.energy, dccm_pos["theta1_angle"],
EnergyDefaults.mono_pitch, dccm_pos["theta2_angle"])
def bl_energy(energy_ev, plot=True):
"""
Adjusts the beamline's energy to the specified energy in electron volts (eV).
The function validates the target energy, checks the current energy, and makes
adjustments only if the energy difference is significant enough. It performs
necessary operations including changing DCCM motors, updating
the mirror stripe, and scanning to find the optimal DCCM pitch of 2nd crystal..
Args:
energy_ev: Target energy in electron volts to which the beamline should be adjusted.
plot: Boolean flag indicating whether to plot the DCCM pitch scan for finding the peak.
Returns:
None
"""
energy_ev = validate_energy(energy_ev) # Ensure energy is valid.
# Check current energy to avoid unnecessary adjustments.
current_energy = get_current_energy()
energy_diff = calculate_energy_difference(current_energy, energy_ev)
if energy_diff <= EnergyDefaults.min_energy_change:
print(
f"Energy change of {energy_diff:.2f} eV is too small, not changing energy."
)
return
# Step 1: Move and set the DCCM motors.
move_dccm_motors(energy_ev)
# Step 2: Update the mirror stripe.
set_mirror_stripe(energy_ev)
# Step 3: Perform DCCM pitch scan and move to peak.
if plot:
mono_pitch_scan(plot=True)
else:
mono_pitch_scan(plot=False)
-80
View File
@@ -1,80 +0,0 @@
"""File to store beamline parameters and defaults"""
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class FitDefaults:
"""Default values for fitting routines"""
# Constants for default models, baselines, and parameters
MODEL = "Voigt"
BASELINE = "Linear"
SETTLE_TIME = 0.1
RELATIVE_MODE = True
@dataclass(frozen=True)
class EnergyDefaults:
"""Parameters for PXIII energy changes"""
min_energy_change = 1
min_energy_ev = 4500
max_energy_ev = 15000
signals = {
"sig1": dev.dccm_di_top,
"sig2": dev.dccm_bpmsum,
"sig3": dev.ss_bpmsum,
# "sig4": dev.xbox_xbpm,
}
energy = dev.dccm_theta1
mono_pitch = dev.dccm_theta2
# LUT_table = "luts/energy_lut.csv"
stripe_thresholds = {"silicon": 9000, "rhodium": 40000}
pitch_scan = {"halfwidth": 0.15, "steps": 30}
@dataclass(frozen=True)
class CamConversion:
"""Convert pixels to microns for sam cam"""
a = 0.5208
b = 0.002586
@dataclass(frozen=True)
class BPMScans:
"""Define the names of the motors and bpm channels"""
ss = {
"x_name": dev.ss_bpm_x.name,
"y_name": dev.ss_bpm_y.name,
"z1_name": dev.ss_bpm1.name,
"z2_name": dev.ss_bpm2.name,
"z3_name": dev.ss_bpm3.name,
"z4_name": dev.ss_bpm3.name,
"z5_name": dev.ss_bpmsum.name,
"x_device": dev.ss_bpm_x,
"y_device": dev.ss_bpm_y,
}
# @dataclass(frozen=True)
# class MirrorConfig:
# """Define the names of the mirror channels"""
# hfm = {
# "bu_name": dev.hfm_bu.name,
# "bd_name": dev.hfm_bd.name,
# "z_name": dev.samcam_xsig.name,
# "x_device": dev.hfm_bu,
# "y_device": dev.hfm_bd,
# }
# vfm = {
# "bu_name": dev.vfm_bu.name,
# "bd_name": dev.vfm_bd.name,
# "z_name": dev.samcam_ysig.name,
# "x_device": dev.vfm_bu,
# "y_device": dev.vfm_bd,
# }
-8
View File
@@ -1,8 +0,0 @@
from .mx_measurements import (
MeasureStandardWedge,
MeasureVerticalLine,
MeasureRasterSimple,
MeasureScreening,
MeasureHelical,
MeasureHelical2,
)
@@ -1,12 +0,0 @@
# from .metadata_schema_template import ExampleSchema
METADATA_SCHEMA_REGISTRY = {
# Add models which should be used to validate scan metadata here.
# Make a model according to the template, and import it as above
# Then associate it with a scan like so:
# "example_scan": ExampleSchema
}
# Define a default schema type which should be used as the fallback for everything:
DEFAULT_SCHEMA = None
@@ -1,34 +0,0 @@
# # By inheriting from BasicScanMetadata you can define a schema by which metadata
# # supplied to a scan must be validated.
# # This schema is a Pydantic model: https://docs.pydantic.dev/latest/concepts/models/
# # but by default it will still allow you to add any arbitrary information to it.
# # That is to say, when you run a scan with which such a model has been associated in the
# # metadata_schema_registry, you can supply any python dictionary with strings as keys
# # and built-in python types (strings, integers, floats) as values, and these will be
# # added to the experiment metadata, but it *must* contain the keys and values of the
# # types defined in the schema class.
# #
# #
# # For example, say that you would like to enforce recording information about sample
# # pretreatment, you could define the following:
# #
#
# from bec_lib.metadata_schema import BasicScanMetadata
#
#
# class ExampleSchema(BasicScanMetadata):
# treatment_description: str
# treatment_temperature_k: int
#
#
# # If this was used according to the example in metadata_schema_registry.py,
# # then when calling the scan, the user would need to write something like:
# >>> scans.example_scan(
# >>> motor,
# >>> 1,
# >>> 2,
# >>> 3,
# >>> metadata={"treatment_description": "oven overnight", "treatment_temperature_k": 575},
# >>> )
#
# # And the additional metadata would be saved in the HDF5 file created for the scan.
-473
View File
@@ -1,473 +0,0 @@
"""MX measurements module
Scan primitives for standard BEC scans at the PX beamlines at SLS.
Theese scans define the event model and can be called from higher levels.
"""
import time
import numpy as np
from bec_lib import bec_logger
from bec_server.scan_server.scans import AsyncFlyScanBase
logger = bec_logger.logger
class AbrCmd:
"""Valid Aerotech ABR scan commands from the AeroBasic files"""
NONE = 0
RASTER_SCAN_SIMPLE = 1
MEASURE_STANDARD = 2
VERTICAL_LINE_SCAN = 3
SCREENING = 4
# SUPER_FAST_OMEGA = 5 # Some Japanese measured samples in capilaries at high RPMs
# STILL_WEDGE = 6 # NOTE: Unused Step scan
# STILLS = 7 # NOTE: Unused Just send triggers to detector
# REPEAT_SINGLE_OSCILLATION = 8 # NOTE: Unused
# SINGLE_OSCILLATION = 9
# OLD_FASHIONED = 10 # NOTE: Unused
# RASTER_SCAN = 11
# JET_ROTATION = 12 # NOTE: Unused
# X_HELICAL = 13 # NOTE: Unused
# X_RUNSEQ = 14 # NOTE: Unused
# JUNGFRAU = 15
# MSOX = 16 # NOTE: Unused
# SLIT_SCAN = 17 # NOTE: Unused
# RASTER_SCAN_STILL = 18
# SCAN_SASTT = 19
# SCAN_SASTT_V2 = 20
# SCAN_SASTT_V3 = 21
class AerotechFlyscanBase(AsyncFlyScanBase):
"""Base class for MX flyscans
Low-level base class for standard scans at the PX beamlines at SLS. Theese
scans use the A3200 rotation stage and the actual experiment is performed
using an AeroBasic script controlled via global variables. The base class
has some basic safety features like checking status then sets globals and
fires off the scan. Implementations can choose to set the corresponding
configurations in child classes or pass it as command line parameters.
IMPORTANT: The AeroBasic scripts take care of the PSO configuration.
Parameters:
-----------
abr_complete : bool
Wait for the launched ABR task to complete.
"""
scan_type = "fly"
scan_report_hint = "table"
arg_input = {}
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
# Aerotech stage config
abr_raster_reset = False
abr_complete = False
abr_timeout = None
pointID = 0
num_pos = 0
def __init__(self, *args, parameter: dict = None, **kwargs):
"""Just set num_pos=0 to avoid hanging and override defaults if explicitly set from
parameters.
"""
super().__init__(parameter=parameter, **kwargs)
if "abr_raster_reset" in self.caller_kwargs:
self.abr_raster_reset = self.caller_kwargs.get("abr_raster_reset")
if "abr_complete" in self.caller_kwargs:
self.abr_complete = self.caller_kwargs.get("abr_complete")
if "abr_timeout" in self.caller_kwargs:
self.abr_timeout = self.caller_kwargs.get("abr_timeout")
def pre_scan(self):
"""Mostly just checking if ABR stage is ok..."""
# TODO: Move roughly to start position???
# ABR status checking
stat = yield from self.stubs.send_rpc_and_wait("abr", "status.get")
if stat not in (0, "OK"):
raise RuntimeError("Aerotech ABR seems to be in error state {stat}, please reset")
task = yield from self.stubs.send_rpc_and_wait("abr", "task1.get")
# From what I got values are copied to local vars at the start of scan,
# so only kickoff should be forbidden.
if task not in (1, "OK"):
raise RuntimeError("Aerotech ABR task #1 seems to busy")
# Reset the raster scan engine
if self.abr_raster_reset:
yield from self.stubs.send_rpc_and_wait("abr", "raster_scan_done.set", 0)
# Call super
yield from super().pre_scan()
def scan_core(self):
"""The actual scan logic comes here."""
# Kick off the run
yield from self.stubs.send_rpc_and_wait("abr", "bluekickoff")
logger.info("Measurement launched on the ABR stage...")
# Wait for grid scanner to finish
if self.abr_complete:
if self.abr_timeout is not None:
st = yield from self.stubs.send_rpc_and_wait("abr", "complete", self.abr_timeout)
st.wait()
else:
st = yield from self.stubs.send_rpc_and_wait("abr", "complete")
st.wait()
def cleanup(self):
"""Set scan progress to 1 to finish the scan"""
self.num_pos = 1
return super().cleanup()
class MeasureStandardWedge(AerotechFlyscanBase):
"""Standard wedge scan using the OMEGA motor
Measure an absolute continous line scan from `start` to `start` + `range`
during `move_time` on the Omega axis with PSO output.
The scan itself is executed by the scan service running on the Aerotech
controller. Ophyd just configures, launches it and waits for completion.
Example
-------
>>> scans.standard_wedge(start=42, range=10, move_time=20)
Parameters
----------
start : (float, float)
Scan start position of the axis.
range : (float, float)
Scan range of the axis.
move_time : (float)
Total travel time for the movement [s].
ready_rate : float, optional
No clue what is this... (default=500)
"""
scan_name = "standardscan"
required_kwargs = ["start", "range", "move_time"]
class MeasureVerticalLine(AerotechFlyscanBase):
"""Vertical line scan using the GMY motor
Simple relative continous line scan that records a single vertical line
with PSO output. There's no actual stepping, it's only used for velocity
calculation.
The scan itself is executed by the scan service running on the Aerotech
controller. Ophyd just configures, launches it and waits for completion.
Example
-------
>>> scans.measure_vline(range_y=12, steps_y=40, exp_time=0.1)
Parameters
----------
range : float
Step size [mm].
steps : int
Scan range of the axis.
exp_time : float
Eeffective exposure time per step [s]
"""
scan_name = "vlinescan"
required_kwargs = ["exp_time", "range", "steps"]
class MeasureRasterSimple(AerotechFlyscanBase):
"""Simple raster scan
Measure a simplified relative zigzag raster scan in the X-Y plane.
The scan is relative assumes the goniometer is currently at the CENTER of
the first cell (i.e. TOP-LEFT). Each line is executed as a single continous
movement, i.e. there's no actual stepping in the X direction.
The scan itself is executed by the scan service running on the Aerotech
controller. Ophyd just configures, launches it and waits for completion.
Example
-------
>>> scans.raster_simple(exp_time=0.1, range_x=4, range_y=4, steps_x=80, steps_y=80)
Parameters
----------
exp_time : float
Effective exposure time for each cell along the X axis [s].
range_x : float
Scan step size [mm].
range_y : float
Scan step size [mm].
steps_x : int
Number of scan steps in X (fast). Only used for velocity calculation.
steps_y : int
Number of scan steps in Y (slow).
"""
scan_name = "rasterscan"
required_kwargs = ["exp_time", "range_x", "range_y", "steps_x", "steps_y"]
class MeasureScreening(AerotechFlyscanBase):
"""Sample screening scan
Sample screening scan that scans intervals on the rotation axis taking
1 image/interval. This makes sure that we hit diffraction peaks if there
are any crystals.
The scan itself is executed by the scan service running on the Aerotech
controller. Ophyd just configures, launches it and waits for completion.
Example
-------
>>> scans.measure_screening(start=42, range=180, steps=18, exp_time=0.1, oscrange=2.0)
Parameters
----------
start : float
Absolute scan start position of the omega axis [deg].
range : float
Total screened range of the omega axis relative to 'start' [deg].
steps : int
Number of blurred intervals.
exp_time : float
Exposure time per blurred interval [s].
oscrange : float
Motion blurring of each interval [deg]
delta : float
Safety margin for sub-range movements (default: 0.5) [deg].
"""
scan_name = "screeningscan"
required_kwargs = ["start", "range", "steps", "exp_time", "oscrange"]
class MeasureHelical(AerotechFlyscanBase):
"""Helical scan using the OMEGA motor
Measure an absolute continous line scan from `start` to `start` + `range`
during `move_time` on the Omega axis with PSO output.
The scan itself is executed by the scan service running on the Aerotech
controller. Ophyd just configures, launches it and waits for completion.
Example
-------
>>> scans.standard_wedge(start=42, range=10, move_time=20)
Parameters
----------
start : float
Scan start position of the axis.
range : float
Scan range of the axis.
move_time : float
Total travel time for the movement [s].
ready_rate : float, optional
No clue what is this... (default=500)
sg_start : (float, float, float, float, float)
Complete SmarGon coordinate in tuple form.
sg_end : (float, float, float, float, float)
Complete SmarGon coordinate in tuple form.
sg_steps : int
Number of steps with SmarGon.
"""
scan_name = "helicalscan"
required_kwargs = ["start", "range", "move_time", "sg_start", "sg_end", "sg_steps"]
def pre_scan(self):
"""Mostly just checking if ABR stage is ok..."""
# Smargon has no velocity control
self.smargon_start = np.array(self.caller_kwargs.get("sg_start"))
self.smargon_end = np.array(self.caller_kwargs.get("sg_end"))
self.smargon_steps = self.caller_kwargs.get("sg_steps")
self.smargon_range = self.smargon_end - self.smargon_start
self.smargon_step_size = self.smargon_range / self.smargon_steps
self.smargon_step_time = self.caller_kwargs.get("move_time") / self.smargon_steps
logger.info(f"Start:\t{self.smargon_start}")
logger.info(f"End:\t{self.smargon_end}")
logger.info(f"Steps:\t{self.smargon_steps}")
logger.info(f"Range:\t{self.smargon_range}")
logger.info(f"StepSize:\t{self.smargon_step_size}")
logger.info(f"StepTime:\t{self.smargon_step_time}")
# TODO: Move roughly to start position???
st0 = yield from self.stubs.send_rpc("shx", "omove", self.smargon_start[0])
st1 = yield from self.stubs.send_rpc("shy", "omove", self.smargon_start[1])
st2 = yield from self.stubs.send_rpc("shz", "omove", self.smargon_start[2])
st3 = yield from self.stubs.send_rpc("chi", "omove", self.smargon_start[3])
st4 = yield from self.stubs.send_rpc("phi", "omove", self.smargon_start[4])
st0.wait()
st1.wait()
st2.wait()
st3.wait()
st4.wait()
# Call super
yield from super().pre_scan()
def scan_core(self):
"""The actual scan logic comes here."""
# Kick off the run
yield from self.stubs.send_rpc_and_wait("abr", "kickoff")
logger.info("Measurement launched on the ABR stage...")
logger.info("Performing SmarGon stepping...")
for ss in range(self.smargon_steps):
sg_pos = self.smargon_start + ss * self.smargon_step_size
# Move to position but don't care
st0 = yield from self.stubs.send_rpc("shx", "omove", sg_pos[0])
st1 = yield from self.stubs.send_rpc("shy", "omove", sg_pos[1])
st2 = yield from self.stubs.send_rpc("shz", "omove", sg_pos[2])
st3 = yield from self.stubs.send_rpc("chi", "omove", sg_pos[3])
st4 = yield from self.stubs.send_rpc("phi", "omove", sg_pos[4])
t_start = time.time()
st0.wait()
st1.wait()
st2.wait()
st3.wait()
st4.wait()
t_end = time.time()
t_elapsed = t_end - t_start
time.sleep(max(self.smargon_step_time - t_elapsed, 0))
# Wait for scan task to finish
if self.abr_complete:
if self.abr_timeout is not None:
st = yield from self.stubs.send_rpc_and_wait("abr", "complete", self.abr_timeout)
st.wait()
else:
st = yield from self.stubs.send_rpc_and_wait("abr", "complete")
st.wait()
class MeasureHelical2(AerotechFlyscanBase):
"""Helical scan using the OMEGA motor
Measure an absolute continous line scan from `start` to `start` + `range`
during `move_time` on the Omega axis with PSO output.
The scan itself is executed by the scan service running on the Aerotech
controller. Ophyd just configures, launches it and waits for completion.
Example
-------
>>> scans.standard_wedge(start=42, range=10, move_time=20)
Parameters
----------
start : float
Scan start position of the axis.
range : float
Scan range of the axis.
move_time : float
Total travel time for the movement [s].
ready_rate : float, optional
No clue what is this... (default=500)
sg_start : (float, float, float, float, float)
Complete SmarGon coordinate in tuple form.
sg_end : (float, float, float, float, float)
Complete SmarGon coordinate in tuple form.
sg_steps : int
Number of steps with SmarGon.
"""
scan_name = "helicalscan2"
required_kwargs = ["start", "range", "move_time", "sg_start", "sg_end", "sg_steps"]
point_id = 0
# def __init__(self, *args, parameter: dict = None, **kwargs):
# """Just set num_pos=0 to avoid hanging and override defaults if explicitly set from
# parameters.
# """
# self.num_pos = kwargs["sg_steps"]
# super().__init__(*args, parameter=parameter, **kwargs)
def prepare_positions(self):
# Smargon has no velocity control
self.smargon_start = np.array(self.caller_kwargs.get("sg_start"))
self.smargon_end = np.array(self.caller_kwargs.get("sg_end"))
self.smargon_steps = self.caller_kwargs.get("sg_steps")
self.smargon_range = self.smargon_end - self.smargon_start
self.smargon_step_size = self.smargon_range / self.smargon_steps
self.smargon_step_time = self.caller_kwargs.get("move_time") / self.smargon_steps
logger.info(f"Start:\t{self.smargon_start}")
logger.info(f"End:\t{self.smargon_end}")
logger.info(f"Steps:\t{self.smargon_steps}")
logger.info(f"Range:\t{self.smargon_range}")
logger.info(f"StepSize:\t{self.smargon_step_size}")
logger.info(f"StepTime:\t{self.smargon_step_time}")
self.num_pos = self.smargon_steps
self.positions = np.linspace(self.smargon_start, self.smargon_end, self.smargon_steps)
self.start_pos = self.positions[0, :]
# Call super
yield from super().prepare_positions()
# def update_scan_motors(self):
# """ Update step scan motors"""
# self.scan_motors = ['shx', 'shy', 'shz', 'chi', 'phi']
def pre_scan(self):
"""Mostly just checking if ABR stage is ok..."""
# Move roughly to start position
st0 = yield from self.stubs.send_rpc("shx", "omove", self.start_pos[0])
st1 = yield from self.stubs.send_rpc("shy", "omove", self.start_pos[1])
st2 = yield from self.stubs.send_rpc("shz", "omove", self.start_pos[2])
st3 = yield from self.stubs.send_rpc("chi", "omove", self.start_pos[3])
st4 = yield from self.stubs.send_rpc("phi", "omove", self.start_pos[4])
st0.wait()
st1.wait()
st2.wait()
st3.wait()
st4.wait()
# print(f"\n\n{self.readout_priority}\n\n")
# Call super
yield from super().pre_scan()
def scan_core(self):
"""The actual scan logic comes here."""
# Kick off the run
yield from self.stubs.send_rpc_and_wait("abr", "kickoff")
logger.info("Measurement launched on the ABR stage...")
logger.info("Performing SmarGon stepping...")
for _, sg_pos in enumerate(self.positions):
# sg_pos = self.smargon_start + ss * self.smargon_step_size
# Move to position but don't care
st0 = yield from self.stubs.send_rpc("shx", "omove", sg_pos[0])
st1 = yield from self.stubs.send_rpc("shy", "omove", sg_pos[1])
st2 = yield from self.stubs.send_rpc("shz", "omove", sg_pos[2])
st3 = yield from self.stubs.send_rpc("chi", "omove", sg_pos[3])
st4 = yield from self.stubs.send_rpc("phi", "omove", sg_pos[4])
t_start = time.time()
st0.wait()
st1.wait()
st2.wait()
st3.wait()
st4.wait()
t_end = time.time()
t_elapsed = t_end - t_start
time.sleep(max(self.smargon_step_time - t_elapsed, 0))
yield from self.stubs.read(group="monitored", point_id=self.point_id)
self.point_id += 1
# Wait for scan task to finish
if self.abr_complete:
if self.abr_timeout is not None:
st = yield from self.stubs.send_rpc_and_wait("abr", "complete", self.abr_timeout)
st.wait()
else:
st = yield from self.stubs.send_rpc_and_wait("abr", "complete")
st.wait()
@@ -1,12 +0,0 @@
"""
Scan components for pxiii_bec.
The scan components module allows you to define custom components that can be used in your scans.
These components can be used to encapsulate reusable logic, interact with devices, or perform specific actions during the scan lifecycle.
"""
from bec_server.scan_server.scans.scan_components import ScanComponents
class PxiiiBecScanComponents(ScanComponents):
"""Scan components for pxiii_bec."""
@@ -1,33 +0,0 @@
"""
Scan modifier plugin for pxiii_bec.
The scan modifier allows you to modify the scan lifecycle and run custom actions before or after the scan hook or replace the scan hook entirely.
Note that the scan_modifier module must be registered as a plugin in the pyproject.toml file for it to be recognized by the BEC framework and that
there can only be one scan_modifier plugin registered at a time. If you need to run multiple scan modifiers, you can create a single scan
modifier plugin that runs multiple actions in sequence with conditional logic to determine which actions to run based on the scan context.
"""
from bec_server.scan_server.scans.scan_modifier import ScanModifier, scan_hook_impl
class PxiiiBecScanModifier(ScanModifier):
"""
Scan modifier for pxiii_bec.
By inheriting from the ScanModifier base class, you get access to currently running scan (self.scan), the devices (self.dev), the scan info (self.scan_info),
the scan components (self.components) and the scan actions (self.actions).
"""
def __init__(self, **kwargs):
"""Initialize the scan modifier."""
super().__init__(**kwargs)
# Example of running code before the scan stage for a specific scan
# @scan_hook_impl("stage", "before")
# def before_stage(self):
# """Run before the stage hook."""
# self.actions.send_client_info("Custom stage logic executed by ScanModifier.")
# if self.scan_info.scan_name == "example_scan":
# self.dev.samx.set(20)
-50
View File
@@ -1,50 +0,0 @@
import numpy as np
from scipy.ndimage import gaussian_filter1d
from lmfit.models import GaussianModel
def alignment_fit_and_plot(
history_index: int,
device_name: str,
signal_name: str | None = None,
smoothing_sigma: float = 2.0,
):
"""
Get data for a completed scan from the BEC history, apply smoothing, gaussian fit,
gradient, and plot all the results.
Args:
history_index (int): scan to fetch, e.g. -1 for the most recent scan
device_name (str): the device for which to get the monitoring data
signal_name (str | None): the signal to plot, if different from the device name.
smoothing_sigma (float): the sigma for the Gaussian smoothing filter
"""
# Fetch scan data from the history
# by default, signal = device name, unless otherwise specified
signal = signal_name or device_name
scan = bec.history[history_index]
md = scan.metadata["bec"]
data = scan.devices[device_name][signal].read()["value"]
# motor name is a bytes object in the metadata, so make a string
motor_name = md["scan_motors"][0].decode()
# Create a plot and a text box to display results
dock_area = bec.gui.new()
wf = dock_area.new(bec.gui.available_widgets.Waveform)
wf.title = f"Scan {md['scan_number']}: {md['scan_name']} of {motor_name}"
text = dock_area.new(bec.gui.available_widgets.TextBox, where="right")
# Calculate some processed data and add everything to the plot
wf.plot(data, label="Raw data")
smoothed_data = gaussian_filter1d(data, smoothing_sigma)
wf.plot(smoothed_data, label="Smoothed")
gradient = np.gradient(smoothed_data)
wf.plot(gradient, label="gradient")
# Fit a Gaussian model to the smoothed data and show the fitting parameters in the textbox
x_data = scan.devices[motor_name][motor_name].read()["value"]
model = GaussianModel()
result = model.fit(smoothed_data, x=x_data)
text.set_plain_text(f"Fit parameters: \n{result.params.pretty_repr()}")
return result
-85
View File
@@ -1,85 +0,0 @@
# pylint: disable=undefined-variable
# import bec
# import bec_lib.devicemanager.DeviceContainer as dev
import time
def _device_name(device):
return device.name if hasattr(device, "name") else str(device)
def _get_or_create_scan_window(name="CurrentScan"):
window = bec.gui.windows.get(name)
if window is None:
return bec.gui.new(name)
window.delete_all()
return window
def rock(steps, exp_time, scan_start=None, scan_end=None, datasource=None, visual=True, **kwargs):
"""Demo step scan with plotting
This is a simple user-space demo step scan with the BEC. It be a
standard BEC scan, while still setting up the environment.
Example:
--------
ascan(dev.dccm_energy, 12,13, steps=21, exp_time=0.1, datasource=dev.dccm_xbpm)
"""
# Dummy method to check beamline status
if not bl_check_beam():
raise RuntimeError("Beamline is not in ready state")
motor = dev.dccm_theta2
if scan_start is None:
scan_start = -0.05 / dev.dccm_energy.user_readback.get()
if scan_end is None:
scan_end = 0.05 / dev.dccm_energy.user_readback.get()
if visual:
# Get or create scan specific window.
window = _get_or_create_scan_window("CurrentScan")
motor_name = _device_name(motor)
datasource_name = _device_name(datasource)
# Draw a waveform plot in the window.
plt1 = window.new(
bec.gui.available_widgets.Waveform, object_name=f"ScanDisplay_{motor_name}"
)
plt1.plot(device_x=motor_name, device_y=datasource_name, dap="LinearModel")
plt1.x_label = motor_name
plt1.y_label = datasource_name
window.show()
print("Handing over to 'scans.line_scan'")
s = scans.line_scan(
motor,
scan_start,
scan_end,
steps=steps,
exp_time=exp_time,
datasource=datasource,
relative=True,
**kwargs,
)
if visual:
# If fitting via GUI
firt_par = plt1.get_dap_params()
else:
# Without GUI
firt_par = bec.dap.LinearModel.fit(
s, motor.name, motor.name, datasource.name, datasource.name
)
# TODO: Validate fitted position
# TODO: Move to fitted maximum
return s, firt_par
def monitor(device, steps, t=1):
for _ in range(steps):
print(device.read())
time.sleep(t)
-29
View File
@@ -1,29 +0,0 @@
def scan_theta2(scan_start, scan_end, stepno, exp):
# Save the motor starting position
start_value = dev.dccm_theta2.read()['dccm_theta2']['value']
print(f"Motor position is {start_value}")
# Run the scan
s = scans.line_scan(dev.dccm_theta2, scan_start, scan_end, steps=stepno, exp_time=exp, relative=True)
# data = s.devices[dccm_xbpm][dccm_xbpm].read()["value"]
# Move motor back to starting position and print XBPM reading
umv(dev.dccm_theta2, start_value)
xbpm_reading = dev.dccm_xbpm.read()['dccm_xbpm']['value']
print(f"Moving dccm_theta2 back to start position of where XBPM Reading is {xbpm_reading}")
end_value = dev.dccm_theta2.read()['dccm_theta2']['value']
print(f"Motor was at {start_value} before the scan, now at {end_value}")
# # Create a plot to display the results
dock_area = bec.gui.new()
wf = dock_area.new(bec.gui.available_widgets.Waveform)
wf.title = f"Scan of DCCM_theta2"
wf.plot(device_x="dccm_theta2", device_y="dccm_xbpm", label="dccm_xbpm-dccm_xbpm")
dap_xbpm = wf.add_dap_curve(device_label="dccm_xbpm-dccm_xbpm", dap_name="GaussianModel")
print(dap_xbpm.dap_params)
-77
View File
@@ -1,77 +0,0 @@
# pylint: disable=undefined-variable
def bl_check_beam():
"""Check beamline status before scan"""
return True
def _device_name(device):
return device.name if hasattr(device, "name") else str(device)
def _get_or_create_scan_window(name="CurrentScan"):
window = bec.gui.windows.get(name)
if window is None:
return bec.gui.new(name)
window.delete_all()
return window
def ascan(
motor, scan_start, scan_end, steps, exp_time, plot=None, visual=True, relative=False, **kwargs
):
"""Demo step scan with plotting
This is a simple user-space demo step scan with the BEC. It be a
standard BEC scan, while still setting up the environment.
Example:
--------
ascan(dev.dccm_energy, 12,13, steps=21, exp_time=0.1, datasource=dev.dccm_xbpm)
"""
# Dummy method to check beamline status
if not bl_check_beam():
raise RuntimeError("Beamline is not in ready state")
if visual:
# Get or create scan specific window.
window = _get_or_create_scan_window("CurrentScan")
motor_name = _device_name(motor)
plot_name = _device_name(plot)
# Draw a waveform plot in the window.
plt1 = window.new(
bec.gui.available_widgets.Waveform, object_name=f"ScanDisplay_{motor_name}"
)
plt1.plot(device_x=motor_name, device_y=plot_name, dap="LinearModel")
plt1.x_label = motor_name
plt1.y_label = plot_name
window.show()
print("Handing over to 'scans.line_scan'")
s = scans.line_scan(
motor,
scan_start,
scan_end,
steps=steps,
exp_time=exp_time,
plot=plot,
relative=relative,
**kwargs,
)
if visual:
# Fitting via GUI
firt_par = plt1.get_dap_params()
else:
# Fitting without GUI
firt_par = bec.dap.LinearModel.fit(s, motor.name, motor.name, plot.name, plot.name)
# # Some basic fit
# dkey = datasource.full_name
# NOTE: s.scan.data == bec.history[-1]
# datapoints = bec.history[-1].devices[dkey].read()[dkey]['value']
# positions
return s, firt_par
View File
+5 -20
View File
@@ -5,8 +5,8 @@ build-backend = "hatchling.build"
[project]
name = "pxiii_bec"
version = "0.0.0"
description = "A plugin repository for BEC"
requires-python = ">=3.11"
description = "Custom device implementations based on the ophyd hardware abstraction layer"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
@@ -16,34 +16,28 @@ dependencies = [
"bec_ipython_client",
"bec_lib",
"bec_server",
"bec_widgets",
"ophyd_devices",
"std_daq_client",
"rich",
"pyepics",
"pandas~=2.0",
"matplotlib",
"zmq",
]
[project.optional-dependencies]
dev = [
"black",
"copier",
"isort",
"coverage",
"pylint",
"pytest",
"pytest-random-order",
"ophyd_devices",
"bec_server",
"pytest-redis",
]
[project.entry-points."bec"]
plugin_bec = "pxiii_bec"
[project.entry-points."bec.deployment.device_server"]
plugin_ds_startup = "pxiii_bec.deployments.device_server.startup:run"
plugin_ds_startup = "pxiii_bec.deployment.device_server.startup:run"
[project.entry-points."bec.file_writer"]
plugin_file_writer = "pxiii_bec.file_writer"
@@ -51,21 +45,12 @@ plugin_file_writer = "pxiii_bec.file_writer"
[project.entry-points."bec.scans"]
plugin_scans = "pxiii_bec.scans"
[project.entry-points."bec.scans.scan_modifier"]
plugin_scan_modifier = "pxiii_bec.scans.scan_customization.scan_modifier"
[project.entry-points."bec.scans.metadata_schema"]
plugin_metadata_schema = "pxiii_bec.scans.metadata_schema"
[project.entry-points."bec.ipython_client_startup"]
plugin_ipython_client_pre = "pxiii_bec.bec_ipython_client.startup.pre_startup"
plugin_ipython_client_post = "pxiii_bec.bec_ipython_client.startup"
[project.entry-points."bec.widgets.auto_updates"]
plugin_widgets_update = "pxiii_bec.bec_widgets.auto_updates"
[project.entry-points."bec.widgets.user_widgets"]
plugin_widgets = "pxiii_bec.bec_widgets.widgets"
plugin_widgets_update = "pxiii_bec.bec_widgets.auto_updates:PlotUpdate"
[tool.hatch.build.targets.wheel]
include = ["*"]
+22
View File
@@ -0,0 +1,22 @@
from bec_ipython_client import BECIPythonClient
from bec_lib import bec_logger, ServiceConfig
logger = bec_logger.logger
bec_logger.level = bec_logger.LOGLEVEL.SUCCESS
config = ServiceConfig(redis={"host":"x06da-bec-001", "port":6379})
bec = BECIPythonClient(config=config)
bec.start()
bec.load_high_level_interface("spec_hli")
dev = bec.device_manager.devices
scans = bec.scans
logger.success("Started BECClient")
##########################################
from pxiii_bec.bec_ipython_client.plugins.beam_stop_scan import beam_stop_scan
beam_stop_scan()
+9 -12
View File
@@ -1,34 +1,31 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
It can be install via
``` bash
pip install pytest
```
in your _python environment_.
in your *python environment*.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
To run all tests, navigate to the directory of the plugin from the command line, and run the command
```bash
``` bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
+9 -12
View File
@@ -1,34 +1,31 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
It can be install via
``` bash
pip install pytest
```
in your _python environment_.
in your *python environment*.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
To run all tests, navigate to the directory of the plugin from the command line, and run the command
```bash
``` bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
+9 -12
View File
@@ -1,34 +1,31 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
It can be install via
``` bash
pip install pytest
```
in your _python environment_.
in your *python environment*.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
To run all tests, navigate to the directory of the plugin from the command line, and run the command
```bash
``` bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
+9 -12
View File
@@ -1,34 +1,31 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
It can be install via
``` bash
pip install pytest
```
in your _python environment_.
in your *python environment*.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
To run all tests, navigate to the directory of the plugin from the command line, and run the command
```bash
``` bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
-34
View File
@@ -1,34 +0,0 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
pip install pytest
```
in your _python environment_.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
```bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
+9 -12
View File
@@ -1,34 +1,31 @@
# Getting Started with Testing using pytest
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
It can be installed via
```bash
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
It can be install via
``` bash
pip install pytest
```
in your _python environment_.
in your *python environment*.
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
## Introduction
Tests in this package should be stored in the `tests` directory.
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
It is mandatory for test files to begin with `test_` for pytest to discover them.
To run all tests, navigate to the directory of the plugin from the command line, and run the command
To run all tests, navigate to the directory of the plugin from the command line, and run the command
```bash
``` bash
pytest -v --random-order ./tests
```
Note, the python environment needs to be active.
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
## Test examples
Writing tests can be quite specific for the given function.
Writing tests can be quite specific for the given function.
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).