Feat/widget development #35
@@ -14,6 +14,7 @@ logger = bec_logger.logger
|
||||
|
||||
_Widgets = {
|
||||
"DataViewer": "DataViewer",
|
||||
"DigitalTwin": "DigitalTwin",
|
||||
}
|
||||
|
||||
|
||||
@@ -39,3 +40,27 @@ class DataViewer(RPCBase):
|
||||
"""
|
||||
Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget.
|
||||
"""
|
||||
|
||||
|
||||
class DigitalTwin(RPCBase):
|
||||
"""Main widget of Digital Twin"""
|
||||
|
||||
_IMPORT_MODULE = "superxas_bec.bec_widgets.widgets.digital_twin.digital_twin"
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def attach(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def detach(self):
|
||||
"""
|
||||
Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget.
|
||||
"""
|
||||
|
||||
@@ -6,8 +6,7 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from typing import Literal, Optional, cast
|
||||
from typing import Literal
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
@@ -15,27 +14,12 @@ from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme, get_accent_colors
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtGui import QFont
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QDoubleSpinBox,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLayout,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from superxas_bec.bec_widgets.widgets.data_viewer.qt_widgets import TaggedListWidget
|
||||
from superxas_bec.bec_widgets.widgets.data_viewer.viewer import HDF5Viewer
|
||||
from .panels.input_panel import InputPanel
|
||||
from .panels.scan_view import ScanViewer
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -54,20 +38,15 @@ class DataViewer(BECWidget, QWidget):
|
||||
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
logger.info(f"Type of self.client: {type(self.client)}")
|
||||
|
||||
central = QWidget()
|
||||
self.root_layout = QVBoxLayout(central)
|
||||
|
||||
self.input = InputPanel()
|
||||
self.viewer = HDF5Viewer()
|
||||
|
||||
self.viewer = ScanViewer()
|
||||
self.root_layout.addWidget(self.input, 0)
|
||||
self.root_layout.addWidget(self.viewer, 1)
|
||||
|
||||
self.setLayout(self.root_layout)
|
||||
self.setWindowTitle("Data Viewer")
|
||||
# self.resize(1800, 800)
|
||||
|
||||
self.history = []
|
||||
self.bec_dispatcher.connect_slot(self.on_history_update, MessageEndpoints.scan_history())
|
||||
@@ -75,14 +54,9 @@ class DataViewer(BECWidget, QWidget):
|
||||
|
||||
self.current_row = 0
|
||||
|
||||
logger.info(self.client.acl)
|
||||
logger.info(self.client.active_account)
|
||||
logger.info(self.client.proc)
|
||||
logger.info(self.client.username)
|
||||
|
||||
self.input.scan_sel.currentItemChanged_connect(self.scan_sel_changed)
|
||||
self.input.load_button.clicked_connect(self.load_dataset)
|
||||
self.input.unload_button.clicked_connect(self.unload_all_datasets)
|
||||
self.input.load_button.clicked_connect(self.load_scan)
|
||||
self.input.unload_button.clicked_connect(self.unload_all_scans)
|
||||
self.input.open_fm_button.clicked_connect(self.open_in_file_manager)
|
||||
|
||||
def apply_theme(self, theme: Literal["dark", "light"]):
|
||||
@@ -97,10 +71,12 @@ class DataViewer(BECWidget, QWidget):
|
||||
|
||||
@SafeSlot()
|
||||
def scan_sel_changed(self, *_, **kwargs):
|
||||
"""Updates the current row value of the scan selection list"""
|
||||
self.current_row = kwargs["value"]().row()
|
||||
|
||||
@SafeSlot()
|
||||
def open_in_file_manager(self, *_):
|
||||
"""Open the scan folder in the systems default file manager"""
|
||||
if len(self.history) > 0:
|
||||
scan = self.history[self.current_row]
|
||||
filepath = scan["file_components"][0].decode().rsplit("/", 1)[0]
|
||||
@@ -113,58 +89,100 @@ class DataViewer(BECWidget, QWidget):
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
def load_dataset(
|
||||
self, *_
|
||||
): # TODO: Check scan file components for combined xas/xrd scans. Is the Pilatus file in there as well?
|
||||
def load_scan(self, *_):
|
||||
"""
|
||||
Loads a scan. Find all files within the scan folder, sort them and
|
||||
then load the files in the scan view
|
||||
"""
|
||||
if len(self.history) > 0:
|
||||
scan = self.history[self.current_row]
|
||||
file = scan["file_components"][0] + b"_master." + scan["file_components"][1]
|
||||
logger.info(file.decode())
|
||||
self.viewer.load_files([file.decode()])
|
||||
base_filepath = scan["file_components"][0].decode().rsplit("/", 1)[0]
|
||||
filenames = [
|
||||
f
|
||||
for f in os.listdir(base_filepath)
|
||||
if os.path.isfile(os.path.join(base_filepath, f))
|
||||
]
|
||||
|
||||
def sort_priority(name):
|
||||
if "master" in name:
|
||||
return 0
|
||||
if name.endswith(".h5"):
|
||||
return 1
|
||||
return 2
|
||||
|
||||
sorted_files = [
|
||||
f"{base_filepath}/{name}" for name in sorted(filenames, key=sort_priority)
|
||||
]
|
||||
self.viewer.load_files(sorted_files)
|
||||
|
||||
@SafeSlot()
|
||||
def unload_all_datasets(self, *_):
|
||||
def unload_all_scans(self, *_):
|
||||
"""Removes all scans from the scan view"""
|
||||
self.viewer.clear_files()
|
||||
|
||||
def duration_string(self, start: str, end: str) -> str:
|
||||
def duration_formatted(self, start: str, end: str) -> str:
|
||||
"""
|
||||
Calculates the duration of a scan based on start end end time and
|
||||
formats it as an easy readable string.
|
||||
|
||||
Args:
|
||||
start(str): start time in iso-format
|
||||
end(str): end time in iso-format
|
||||
|
||||
Returns:
|
||||
str: Formatted duration, e.g. '1min 10s' or '1h 13min'
|
||||
"""
|
||||
start_dt = datetime.fromisoformat(start)
|
||||
end_dt = datetime.fromisoformat(end)
|
||||
|
||||
seconds = abs(int((end_dt - start_dt).total_seconds()))
|
||||
|
||||
days, remainder = divmod(seconds, 86400)
|
||||
hours, remainder = divmod(remainder, 3600)
|
||||
minutes, _ = divmod(remainder, 60)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
parts = []
|
||||
|
||||
if days:
|
||||
parts.append(f"{days}d")
|
||||
if hours:
|
||||
parts.append(f"{hours}h")
|
||||
if minutes:
|
||||
parts.append(f"{minutes}min")
|
||||
|
||||
if not days and not hours and minutes < 10:
|
||||
parts.append(f"{seconds}s")
|
||||
return " ".join(parts) if parts else "<1min"
|
||||
|
||||
def time_formatted(self, iso_time: str) -> str:
|
||||
"""
|
||||
Formates a time as an easy readable string.
|
||||
|
||||
Args:
|
||||
iso_time(str): Time in iso-format
|
||||
|
||||
Returns:
|
||||
str: Time formatted with format '%d.%m.%Y %H:%M', e.g. '14.01.1995 08:12'
|
||||
"""
|
||||
dt = datetime.fromisoformat(iso_time)
|
||||
return dt.strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
@SafeSlot()
|
||||
def on_history_update(self, *_):
|
||||
"""Updates the scan list based on the bec scan history."""
|
||||
self.history = []
|
||||
self.input.scan_sel.clear()
|
||||
# Get the length of the scan history, which is 0 when the bec server was started
|
||||
# and no scan has finished yet. Limit the history to the latest 20 scans.
|
||||
if self.client.history is None:
|
||||
return
|
||||
max_scans = min(len(self.client.history), MAX_HIST_LEN)
|
||||
for n in range(1, max_scans): # last scans, limited by MAX_HIST_LEN
|
||||
# logger.info(self.client.history[-n].metadata["bec"]["status"])
|
||||
start_time = self.client.history[-n].metadata["start_time"]
|
||||
end_time = self.client.history[-n].metadata["end_time"]
|
||||
# logger.info(type(start_time))
|
||||
scan_data = self.client.history[-n].metadata["bec"]
|
||||
# logger.info(scan_data)
|
||||
start_time = self.client.history[-n].metadata["start_time"] # type: ignore
|
||||
end_time = self.client.history[-n].metadata["end_time"] # type: ignore
|
||||
scan_data = self.client.history[-n].metadata["bec"] # type: ignore
|
||||
scan_number = scan_data["scan_number"]
|
||||
scan_name = scan_data["scan_name"]
|
||||
comment = scan_data["metadata"]["user_metadata"]["comment"]
|
||||
sample_name = scan_data["metadata"]["user_metadata"]["sample_name"]
|
||||
if "metadata" in scan_data:
|
||||
comment = scan_data["metadata"]["user_metadata"]["comment"]
|
||||
sample_name = scan_data["metadata"]["user_metadata"]["sample_name"]
|
||||
else:
|
||||
comment, sample_name = "", ""
|
||||
status = scan_data["status"]
|
||||
self.history.append(
|
||||
{
|
||||
@@ -186,142 +204,13 @@ class DataViewer(BECWidget, QWidget):
|
||||
tags.append((comment, get_accent_colors().warning.name()))
|
||||
if status == "closed":
|
||||
tags.append((status, get_accent_colors().success.name()))
|
||||
elif status == "halted":
|
||||
elif status == "halted" or status == "aborted":
|
||||
tags.append((status, get_accent_colors().emergency.name()))
|
||||
else:
|
||||
tags.append((status, "#656365"))
|
||||
tags.append((self.duration_string(start_time, end_time), "#656365"))
|
||||
tags.append((self.duration_formatted(start_time, end_time), "#656365"))
|
||||
tags.append((self.time_formatted(start_time), "#656365"))
|
||||
self.input.scan_sel.addTaggedItem(label=str(scan_number), tags=tags)
|
||||
# logger.info(f"Scan history: {self.history}")
|
||||
|
||||
|
||||
class InputPanel(QWidget):
|
||||
"""Panel for scan selection of the data viewer widget"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QHBoxLayout(self)
|
||||
# self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
|
||||
|
||||
# Scan selection
|
||||
self.scan_sel = ListWidget("scan_sel", "Scan", ["Si", "Rh", "Pt"])
|
||||
self.load_button = Button(label_button="Load Dataset", enabled=True)
|
||||
self.unload_button = Button(label_button="Unload all", enabled=True)
|
||||
self.open_fm_button = Button(label_button="Open in File Manager", enabled=True)
|
||||
|
||||
self._button_layout = QVBoxLayout()
|
||||
self._button_layout.addWidget(self.load_button)
|
||||
self._button_layout.addWidget(self.unload_button)
|
||||
self._button_layout.addWidget(self.open_fm_button)
|
||||
self._button_layout.addStretch()
|
||||
|
||||
# Assemble complete scan selection group
|
||||
self.input_group = Group(
|
||||
"Scan selection", [self._button_layout, self.scan_sel], orientation="horizontal"
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.input_group)
|
||||
# self._layout.addStretch()
|
||||
|
||||
|
||||
class Group(QGroupBox):
|
||||
def __init__(self, label, objs, orientation="vertical"):
|
||||
super().__init__(label)
|
||||
if orientation == "vertical":
|
||||
self._layout = QVBoxLayout(self) # type: ignore
|
||||
elif orientation == "horizontal": # assume horizontal
|
||||
self._layout = QHBoxLayout(self) # type: ignore
|
||||
else:
|
||||
raise ValueError(f"Orientation {orientation} is not supported!")
|
||||
for obj in objs:
|
||||
if isinstance(obj, QWidget):
|
||||
self._layout.addWidget(obj) # type: ignore
|
||||
elif isinstance(obj, QLayout):
|
||||
self._layout.addLayout(obj)
|
||||
|
||||
|
||||
class ListWidget(QWidget):
|
||||
def __init__(self, identifier="", label="", enums=[]):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.identifier = identifier
|
||||
# self.label = QLabel(label)
|
||||
# self.label.setFixedWidth(140)
|
||||
# self.label.setContentsMargins(0, 0, 10, 0)
|
||||
# self.label.setWordWrap(True)
|
||||
# layout.addWidget(self.label)
|
||||
self.value = TaggedListWidget()
|
||||
# self.value.setFixedWidth(400)
|
||||
# for entry in enums:
|
||||
# self.value.addItem(entry)
|
||||
layout.addWidget(self.value)
|
||||
|
||||
def clear(self):
|
||||
self.value.clear()
|
||||
|
||||
def addTaggedItem(self, label, tags):
|
||||
self.value.addTaggedItem(label, tags)
|
||||
|
||||
def setCurrentIndex(self, text):
|
||||
self.value.setCurrentIndex(text)
|
||||
|
||||
# def currentIndex(self) -> int:
|
||||
# return self.value.currentIndex()
|
||||
|
||||
# def has_focus(self) -> bool:
|
||||
# return QApplication.focusWidget() is self.value.view()
|
||||
|
||||
def currentItemChanged_connect(self, func):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.value.currentItemChanged.connect(
|
||||
partial(
|
||||
func,
|
||||
identifier=self.identifier,
|
||||
value_obj=self.value,
|
||||
value=lambda: self.value.currentIndex(),
|
||||
)
|
||||
)
|
||||
|
||||
def setDisabled(self, disable):
|
||||
self.value.setDisabled(disable)
|
||||
|
||||
|
||||
class Button(QWidget):
|
||||
def __init__(self, label=None, label_button: str = "", enabled=False):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
if label is not None:
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(140)
|
||||
layout.addWidget(self.label)
|
||||
self.button = QPushButton(label_button)
|
||||
if label is not None:
|
||||
self.button.setFixedWidth(160)
|
||||
self.enable_button(enabled)
|
||||
layout.addWidget(self.button)
|
||||
|
||||
def clicked_connect(self, func):
|
||||
"""Connect a function to the button press."""
|
||||
self.button.clicked.connect(func)
|
||||
|
||||
def enable_button(self, enable: bool = False):
|
||||
if enable:
|
||||
self.button.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}"
|
||||
)
|
||||
self.button.setEnabled(True)
|
||||
else: # disabled
|
||||
self.button.setStyleSheet(
|
||||
"QPushButton {{background-color: rgb(120, 120, 120); color: white;}}"
|
||||
)
|
||||
self.button.setDisabled(True)
|
||||
|
||||
def setText(self, text):
|
||||
self.button.setText(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -5,7 +5,7 @@ from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from superxas_bec.bec_widgets.widgets.data_viewer.data_viewer import DataViewer
|
||||
from .data_viewer import DataViewer
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
File-format loader (HDF5, images).
|
||||
"""
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Iterator, Literal, Optional
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
|
||||
|
||||
class NodeInfo:
|
||||
"""Describes a single node (group or dataset) inside a loaded file."""
|
||||
|
||||
__slots__ = ("name", "path", "kind", "dtype", "shape")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
path: str,
|
||||
kind: Literal["group", "dataset"],
|
||||
dtype: str = "",
|
||||
shape: tuple[int, ...] = (),
|
||||
):
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.kind = kind
|
||||
self.dtype = dtype # e.g. "float", "int", "str", …
|
||||
self.shape = shape # empty tuple for scalars / groups
|
||||
|
||||
|
||||
class BaseFileLoader(ABC):
|
||||
"""
|
||||
Abstract base class for file-format loaders.
|
||||
|
||||
Subclass this to add support for a new format. Three things are required:
|
||||
|
||||
1. ``EXTENSIONS`` — tuple of lowercase extensions this loader handles,
|
||||
e.g. ``(".h5", ".hdf5")``.
|
||||
|
||||
2. ``open(filepath)`` — open the file and keep any handles alive.
|
||||
|
||||
3. ``iter_nodes(path)`` — yield ``NodeInfo`` objects for the direct
|
||||
children of *path* (depth-1 walk; the tree widget calls this
|
||||
recursively as the user expands nodes).
|
||||
|
||||
4. ``read_dataset(path)`` — return the dataset at *path* as a
|
||||
``numpy.ndarray``.
|
||||
|
||||
5. ``close()`` — release any open file handles.
|
||||
|
||||
6. ``child_count(path)`` — return the number of direct children of a
|
||||
group node (used for the status label; override if cheap to compute).
|
||||
"""
|
||||
|
||||
EXTENSIONS: tuple[str, ...] = ()
|
||||
|
||||
@abstractmethod
|
||||
def open(self, filepath: str) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def iter_nodes(self, path: str) -> Iterator[NodeInfo]: ...
|
||||
|
||||
@abstractmethod
|
||||
def read_dataset(self, path: str) -> np.ndarray: ...
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None: ...
|
||||
|
||||
def child_count(self, path: str) -> int:
|
||||
return sum(1 for _ in self.iter_nodes(path))
|
||||
|
||||
|
||||
class HDF5Loader(BaseFileLoader):
|
||||
"""Loader for HDF5 / NeXus files (.h5, .hdf5, .nxs, .nx)."""
|
||||
|
||||
EXTENSIONS = (".h5", ".hdf5", ".hdf", ".nxs", ".nx")
|
||||
|
||||
def __init__(self):
|
||||
self._file: Optional[h5py.File] = None
|
||||
|
||||
def open(self, filepath: str) -> None:
|
||||
self._file = h5py.File(filepath, "r")
|
||||
|
||||
def close(self) -> None:
|
||||
if self._file is not None:
|
||||
self._file.close()
|
||||
self._file = None
|
||||
|
||||
def iter_nodes(self, path: str) -> Iterator[NodeInfo]:
|
||||
assert self._file is not None, "File not open"
|
||||
obj = self._file[path] if path != "/" else self._file
|
||||
|
||||
if not isinstance(obj, h5py.Group):
|
||||
return
|
||||
|
||||
for key in obj.keys():
|
||||
try:
|
||||
child = obj[key]
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
child_path = child.name # h5py always gives the absolute path
|
||||
|
||||
if isinstance(child, h5py.Group):
|
||||
yield NodeInfo(name=key, path=child_path, kind="group")
|
||||
|
||||
elif isinstance(child, h5py.Dataset):
|
||||
shape_tuple = child.shape
|
||||
d = child.dtype
|
||||
dtype = "unknown"
|
||||
if np.issubdtype(d, np.integer):
|
||||
dtype = "int"
|
||||
if np.issubdtype(d, np.floating):
|
||||
dtype = "float"
|
||||
if np.issubdtype(d, np.complexfloating):
|
||||
dtype = "complex"
|
||||
if d.kind in ("S", "U", "O"):
|
||||
dtype = "str"
|
||||
|
||||
yield NodeInfo(
|
||||
name=key, path=child_path, kind="dataset", dtype=dtype, shape=shape_tuple
|
||||
)
|
||||
|
||||
def read_dataset(self, path: str) -> np.ndarray:
|
||||
assert self._file is not None, "File not open"
|
||||
return self._file[path][()]
|
||||
|
||||
def child_count(self, path: str) -> int:
|
||||
assert self._file is not None, "File not open"
|
||||
obj = self._file[path] if path != "/" else self._file
|
||||
return len(obj) if isinstance(obj, h5py.Group) else 0
|
||||
|
||||
|
||||
class ImageLoader(BaseFileLoader):
|
||||
"""
|
||||
Loader for raster image files.
|
||||
|
||||
The file is treated as a single, flat dataset. ``iter_nodes`` yields one
|
||||
leaf node.
|
||||
|
||||
Requires: Pillow (``pip install Pillow``)
|
||||
"""
|
||||
|
||||
EXTENSIONS = (
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".tiff",
|
||||
".tif",
|
||||
".bmp",
|
||||
".webp",
|
||||
".ico",
|
||||
".ppm",
|
||||
".pgm",
|
||||
".pbm",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self._filepath: Optional[str] = None
|
||||
|
||||
def open(self, filepath: str) -> None:
|
||||
# Validate that Pillow can open it; keep only the path.
|
||||
try:
|
||||
from PIL import Image as _PILImage # noqa: F401 — existence check
|
||||
|
||||
_PILImage.open(filepath).verify()
|
||||
except Exception as exc:
|
||||
raise OSError(f"Cannot open image {filepath!r}: {exc}") from exc
|
||||
self._filepath = filepath
|
||||
|
||||
def close(self) -> None:
|
||||
self._filepath = None
|
||||
|
||||
def iter_nodes(self, path: str) -> Iterator[NodeInfo]:
|
||||
"""Images have no internal hierarchy — yield a single leaf node."""
|
||||
if path != "/" or self._filepath is None:
|
||||
return
|
||||
from PIL import Image as _PILImage
|
||||
|
||||
with _PILImage.open(self._filepath) as img:
|
||||
w, h = img.size
|
||||
mode = img.mode # e.g. "RGB", "RGBA", "L", …
|
||||
|
||||
name = os.path.basename(self._filepath)
|
||||
yield NodeInfo(name=name, path="/image", kind="dataset", dtype=mode, shape=(h, w))
|
||||
|
||||
def read_dataset(self, path: str) -> np.ndarray:
|
||||
"""Return the image as a uint8 numpy array (H x W x C or H x W)."""
|
||||
from PIL import Image as _PILImage
|
||||
|
||||
with _PILImage.open(self._filepath) as img: # type: ignore[arg-type]
|
||||
# Animated GIF → first frame only
|
||||
if hasattr(img, "n_frames") and img.n_frames > 1:
|
||||
img.seek(0)
|
||||
return np.asarray(img)
|
||||
|
||||
def child_count(self, path: str) -> int:
|
||||
return 1 if path == "/" else 0
|
||||
|
||||
|
||||
class LoaderRegistry:
|
||||
"""Maps file extensions to loader classes."""
|
||||
|
||||
def __init__(self):
|
||||
self._registry: dict[str, type[BaseFileLoader]] = {}
|
||||
|
||||
def register(self, loader_cls: type[BaseFileLoader]) -> None:
|
||||
"""Register a loader class for all extensions it declares."""
|
||||
for ext in loader_cls.EXTENSIONS:
|
||||
self._registry[ext.lower()] = loader_cls
|
||||
|
||||
def get_loader(self, filepath: str) -> Optional[BaseFileLoader]:
|
||||
"""Return a fresh loader instance for *filepath*, or None if unsupported."""
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
cls = self._registry.get(ext)
|
||||
return cls() if cls is not None else None
|
||||
|
||||
@property
|
||||
def supported_extensions(self) -> list[str]:
|
||||
return sorted(self._registry)
|
||||
|
||||
|
||||
# Default global registry — pre-populated with built-in loaders.
|
||||
registry = LoaderRegistry()
|
||||
registry.register(HDF5Loader)
|
||||
registry.register(ImageLoader)
|
||||
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
Data viewer displaying the data
|
||||
"""
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
from bec_widgets.utils.colors import Colors
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtGui import QFont, QPixmap
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QRadioButton,
|
||||
QSizePolicy,
|
||||
QSlider,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MAX_ROWS = 2000
|
||||
MAX_COLS = 500
|
||||
|
||||
|
||||
class DataView(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._layout = QVBoxLayout(self)
|
||||
|
||||
header = QHBoxLayout()
|
||||
self.path_label = QLabel("")
|
||||
self.path_label.setObjectName("path_label")
|
||||
self.info_label = QLabel("")
|
||||
self.info_label.setObjectName("info_label")
|
||||
header.addWidget(self.path_label, 1)
|
||||
header.addWidget(self.info_label)
|
||||
self._layout.addLayout(header)
|
||||
|
||||
mode_box = QGroupBox("View mode")
|
||||
mode_layout = QHBoxLayout(mode_box)
|
||||
mode_layout.setContentsMargins(8, 4, 8, 4)
|
||||
self.rb_auto = QRadioButton("Auto")
|
||||
self.rb_plot = QRadioButton("Plot")
|
||||
self.rb_image = QRadioButton("Image")
|
||||
self.rb_table = QRadioButton("Table")
|
||||
self.rb_auto.setChecked(True)
|
||||
for rb in (self.rb_auto, self.rb_image, self.rb_plot, self.rb_table):
|
||||
mode_layout.addWidget(rb)
|
||||
rb.toggled.connect(self._on_mode_change)
|
||||
mode_layout.addStretch()
|
||||
self._layout.addWidget(mode_box)
|
||||
|
||||
self.content = QWidget()
|
||||
self.content_layout = QVBoxLayout(self.content)
|
||||
self.content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.addWidget(self.content, 1)
|
||||
|
||||
self.plot_widget = None
|
||||
self.image_widget = None
|
||||
self._current_data = None
|
||||
self.show_empty()
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
bg_color = pg.getConfigOption("background")
|
||||
fg_color = pg.getConfigOption("foreground")
|
||||
if self.plot_widget is not None:
|
||||
n_curves = len(self.plot_widget.listDataItems())
|
||||
colors = Colors.golden_angle_color(
|
||||
colormap="plasma", num=max(10, n_curves + 1), format="HEX"
|
||||
)
|
||||
for idx, curve in enumerate(self.plot_widget.listDataItems()):
|
||||
curve.setPen(pg.mkPen(color=colors[idx]))
|
||||
# Background
|
||||
self.plot_widget.setBackground(bg_color)
|
||||
# Axes (tick marks, tick labels, axis line)
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
ax = self.plot_widget.getAxis(axis)
|
||||
ax.setPen(pg.mkPen(color=fg_color))
|
||||
ax.setTextPen(pg.mkPen(color=fg_color))
|
||||
|
||||
if self.image_widget is not None:
|
||||
self.image_widget.getView().setBackgroundColor(bg_color)
|
||||
self.image_widget.ui.histogram.setBackground(bg_color)
|
||||
|
||||
def _clear_stack(self):
|
||||
while self.content_layout.count():
|
||||
item = self.content_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
self.plot_widget = None
|
||||
self.image_widget = None
|
||||
|
||||
def show_empty(self):
|
||||
"""Empties the content area."""
|
||||
self._clear_stack()
|
||||
empty_label = QLabel("No data selected")
|
||||
empty_label.setObjectName("info_label")
|
||||
empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.path_label.setText("")
|
||||
self.info_label.setText("")
|
||||
self.content_layout.addWidget(empty_label)
|
||||
|
||||
def show_unsupported(self, path: str = "") -> None:
|
||||
"""Display a friendly 'not implemented' message for unknown file types."""
|
||||
self._clear_stack()
|
||||
self._current_data = None
|
||||
self.path_label.setText(path)
|
||||
self.info_label.setText("")
|
||||
lbl = QLabel("File type not supported")
|
||||
lbl.setObjectName("info_label")
|
||||
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.content_layout.addWidget(lbl)
|
||||
|
||||
def _on_mode_change(self):
|
||||
if self._current_data is not None:
|
||||
self.display(self._current_data, self.path_label.text())
|
||||
|
||||
def _active_mode(self):
|
||||
if self.rb_plot.isChecked():
|
||||
return "plot"
|
||||
if self.rb_image.isChecked():
|
||||
return "image"
|
||||
if self.rb_table.isChecked():
|
||||
return "table"
|
||||
return "auto"
|
||||
|
||||
def display(self, data, path: str = "") -> None:
|
||||
"""
|
||||
Render *data* in the panel.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data:
|
||||
A ``numpy.ndarray`` (or anything convertible to one).
|
||||
path:
|
||||
Human-readable label shown in the header.
|
||||
"""
|
||||
self._current_data = data
|
||||
self.path_label.setText(path)
|
||||
|
||||
if not isinstance(data, np.ndarray):
|
||||
data = np.array(data)
|
||||
|
||||
self.info_label.setText(f"shape {data.shape}, dtype {data.dtype}, {data.size} elements")
|
||||
|
||||
mode = self._active_mode()
|
||||
if mode == "auto":
|
||||
if data.ndim <= 1 and data.size > 1:
|
||||
mode = "plot"
|
||||
elif data.ndim <= 4 and min(data.shape, default=0) > 1:
|
||||
mode = "image"
|
||||
else:
|
||||
mode = "table"
|
||||
|
||||
if mode == "plot":
|
||||
self._show_plot_1d(data)
|
||||
elif mode == "image":
|
||||
self._show_image_2d(data)
|
||||
else:
|
||||
self._show_table(data)
|
||||
|
||||
def _show_plot_1d(self, data):
|
||||
self._clear_stack()
|
||||
|
||||
is_2d = data.ndim == 2
|
||||
|
||||
if is_2d:
|
||||
n_rows, _ = data.shape
|
||||
row_data = data[0].astype(np.float32)
|
||||
else:
|
||||
row_data = data.reshape(-1).astype(np.float32)
|
||||
|
||||
x = np.arange(row_data.size, dtype=np.float32)
|
||||
|
||||
self.plot_widget = pg.PlotWidget()
|
||||
plot_item = self.plot_widget.getPlotItem()
|
||||
assert plot_item is not None, "PlotWidget has no PlotItem"
|
||||
plot_item.showGrid(x=True, y=True, alpha=0.25)
|
||||
self.plot_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
plot_item.setAutoVisible(y=False) # type: ignore[attr-defined]
|
||||
|
||||
curve = pg.PlotDataItem(
|
||||
x, row_data, pen=pg.mkPen(color="#2980b9", width=1.6), antialias=False
|
||||
)
|
||||
self.plot_widget.addItem(curve)
|
||||
|
||||
curve.setDownsampling(auto=True, method="peak")
|
||||
curve.setClipToView(True)
|
||||
curve.setSkipFiniteCheck(True)
|
||||
|
||||
plot_item.enableAutoRange() # type: ignore[attr-defined]
|
||||
|
||||
if is_2d:
|
||||
slider = QSlider(Qt.Orientation.Vertical)
|
||||
slider.setMinimum(0)
|
||||
slider.setMaximum(n_rows - 1)
|
||||
slider.setValue(0)
|
||||
slider.setFixedWidth(32)
|
||||
slider.setPageStep(1)
|
||||
|
||||
current_row_label = QLabel("0")
|
||||
current_row_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
current_row_label.setFixedWidth(32)
|
||||
|
||||
max_row_label = QLabel(f"0:{n_rows-1}")
|
||||
max_row_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
max_row_label.setFixedWidth(32)
|
||||
|
||||
def on_row_changed(row):
|
||||
current_row_label.setText(str(row))
|
||||
new_data = data[row].astype(np.float32)
|
||||
new_x = np.arange(new_data.size, dtype=np.float32)
|
||||
curve.setData(new_x, new_data)
|
||||
plot_item.enableAutoRange() # type: ignore[attr-defined]
|
||||
|
||||
slider.valueChanged.connect(on_row_changed)
|
||||
|
||||
slider_col = QWidget()
|
||||
slider_col.setFixedWidth(36)
|
||||
col_layout = QVBoxLayout(slider_col)
|
||||
col_layout.setContentsMargins(0, 0, 0, 0)
|
||||
col_layout.setSpacing(2)
|
||||
col_layout.addWidget(max_row_label)
|
||||
col_layout.addWidget(slider)
|
||||
col_layout.addWidget(current_row_label)
|
||||
|
||||
container = QWidget()
|
||||
h_layout = QHBoxLayout(container)
|
||||
h_layout.setContentsMargins(0, 0, 0, 0)
|
||||
h_layout.setSpacing(4)
|
||||
h_layout.addWidget(slider_col)
|
||||
h_layout.addWidget(self.plot_widget)
|
||||
|
||||
self.content_layout.addWidget(container)
|
||||
else:
|
||||
self.content_layout.addWidget(self.plot_widget)
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def _show_image_2d(self, data):
|
||||
self._clear_stack()
|
||||
|
||||
stacked = False
|
||||
n_images = 0
|
||||
rgb = False
|
||||
img = data
|
||||
if data.ndim == 3:
|
||||
if data.shape[-1] in (3, 4):
|
||||
rgb = True
|
||||
else:
|
||||
stacked = True
|
||||
n_images = data.shape[0]
|
||||
img = data[0, :]
|
||||
elif data.ndim == 4:
|
||||
if data.shape[-1] in (3, 4):
|
||||
rgb = True
|
||||
stacked = True
|
||||
n_images = data.shape[0]
|
||||
img = data[0, :]
|
||||
|
||||
self.image_widget = pg.ImageView()
|
||||
|
||||
self.image_widget.ui.roiBtn.hide()
|
||||
self.image_widget.ui.menuBtn.hide()
|
||||
|
||||
if not rgb:
|
||||
self.image_widget.setColorMap(pg.colormap.get("inferno", source="matplotlib"))
|
||||
|
||||
def set_image(img, autoLevels=True, autoHistogramRange=True):
|
||||
if rgb:
|
||||
self.image_widget.imageItem.setOpts(axisOrder="row-major")
|
||||
self.image_widget.setImage(img)
|
||||
else:
|
||||
self.image_widget.setImage(
|
||||
img.T, autoLevels=autoLevels, autoHistogramRange=autoHistogramRange
|
||||
)
|
||||
|
||||
set_image(img)
|
||||
self.image_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
if stacked:
|
||||
slider = QSlider(Qt.Orientation.Vertical)
|
||||
slider.setMinimum(0)
|
||||
slider.setMaximum(n_images - 1)
|
||||
slider.setValue(0)
|
||||
slider.setFixedWidth(32)
|
||||
slider.setPageStep(1)
|
||||
|
||||
current_image_label = QLabel("0")
|
||||
current_image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
current_image_label.setFixedWidth(32)
|
||||
|
||||
max_image_label = QLabel(f"0:{n_images-1}")
|
||||
max_image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
max_image_label.setFixedWidth(32)
|
||||
|
||||
def on_image_changed(row):
|
||||
current_image_label.setText(str(row))
|
||||
set_image(data[row, :], autoLevels=False, autoHistogramRange=False)
|
||||
|
||||
slider.valueChanged.connect(on_image_changed)
|
||||
|
||||
slider_col = QWidget()
|
||||
slider_col.setFixedWidth(36)
|
||||
col_layout = QVBoxLayout(slider_col)
|
||||
col_layout.setContentsMargins(0, 0, 0, 0)
|
||||
col_layout.setSpacing(2)
|
||||
col_layout.addWidget(max_image_label)
|
||||
col_layout.addWidget(slider)
|
||||
col_layout.addWidget(current_image_label)
|
||||
|
||||
container = QWidget()
|
||||
h_layout = QHBoxLayout(container)
|
||||
h_layout.setContentsMargins(0, 0, 0, 0)
|
||||
h_layout.setSpacing(4)
|
||||
h_layout.addWidget(slider_col)
|
||||
h_layout.addWidget(self.image_widget)
|
||||
|
||||
self.content_layout.addWidget(container)
|
||||
else:
|
||||
self.content_layout.addWidget(self.image_widget)
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def _show_table(self, data):
|
||||
self._clear_stack()
|
||||
|
||||
if data.ndim == 0:
|
||||
flat = data.reshape(1, 1)
|
||||
elif data.ndim == 1:
|
||||
flat = data.reshape(-1, 1)
|
||||
elif data.ndim == 2:
|
||||
flat = data
|
||||
else:
|
||||
flat = data.reshape(-1, data.shape[-1])
|
||||
|
||||
rows, cols = flat.shape
|
||||
show_rows = min(rows, MAX_ROWS)
|
||||
show_cols = min(cols, MAX_COLS)
|
||||
|
||||
if rows > MAX_ROWS or cols > MAX_COLS:
|
||||
note = QLabel(f"⚠ Showing {show_rows}/{rows} rows x {show_cols}/{cols} columns")
|
||||
note.setObjectName("info_label")
|
||||
note.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.content_layout.addWidget(note)
|
||||
|
||||
table = QTableWidget(show_rows, show_cols)
|
||||
table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
table.setSelectionMode(QAbstractItemView.SelectionMode.ContiguousSelection)
|
||||
table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
|
||||
is_float = np.issubdtype(flat.dtype, np.floating)
|
||||
is_complex = np.iscomplexobj(flat)
|
||||
is_bytes = flat.dtype.kind == "S"
|
||||
|
||||
for r in range(show_rows):
|
||||
for c in range(show_cols):
|
||||
val = flat[r, c]
|
||||
txt = (
|
||||
f"{val:.6g}"
|
||||
if is_float
|
||||
else f"{val:.4g}" if is_complex else str(val.decode()) if is_bytes else str(val)
|
||||
)
|
||||
cell = QTableWidgetItem(txt)
|
||||
cell.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
table.setItem(r, c, cell)
|
||||
|
||||
self.content_layout.addWidget(table)
|
||||
@@ -0,0 +1,32 @@
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
|
||||
|
||||
# pylint: disable=E0402
|
||||
from ..widgets.qt_widgets import Button, Group, ListWidget
|
||||
|
||||
|
||||
class InputPanel(QWidget):
|
||||
"""Panel for scan selection of the data viewer widget"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QHBoxLayout(self)
|
||||
|
||||
# Scan selection
|
||||
self.scan_sel = ListWidget("scan_sel")
|
||||
self.load_button = Button(label_button="Load Dataset", enabled=True)
|
||||
self.unload_button = Button(label_button="Unload all", enabled=True)
|
||||
self.open_fm_button = Button(label_button="Open in File Manager", enabled=True)
|
||||
|
||||
self._button_layout = QVBoxLayout()
|
||||
self._button_layout.addWidget(self.load_button)
|
||||
self._button_layout.addWidget(self.unload_button)
|
||||
self._button_layout.addWidget(self.open_fm_button)
|
||||
self._button_layout.addStretch()
|
||||
|
||||
# Assemble complete scan selection group
|
||||
self.input_group = Group(
|
||||
"Scan selection", [self._button_layout, self.scan_sel], orientation="horizontal"
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.input_group)
|
||||
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
Scan viewer. Displays files of one or more scans in a tree view
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Literal, Optional
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtGui import QBrush, QColor
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QMainWindow,
|
||||
QSplitter,
|
||||
QTreeWidget,
|
||||
QTreeWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
# pylint: disable=E0402
|
||||
from ..loaders import BaseFileLoader, registry
|
||||
from ..widgets.qt_widgets import Group
|
||||
from .data_view import DataView
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
ICON_SIZE = 20
|
||||
|
||||
|
||||
class ScanViewer(QMainWindow):
|
||||
"""
|
||||
Generic scan viewer. Supports any format registered in *registry*.
|
||||
|
||||
Args:
|
||||
filepath(str): Optional path to open on startup.
|
||||
"""
|
||||
|
||||
def __init__(self, filepath: Optional[str] = None):
|
||||
super().__init__()
|
||||
self.registry = registry
|
||||
|
||||
self._open_files: dict[str, tuple[BaseFileLoader, str]] = {}
|
||||
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
root_layout = QHBoxLayout(central)
|
||||
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
splitter.setChildrenCollapsible(False)
|
||||
|
||||
left_pane = QWidget()
|
||||
left_layout = QVBoxLayout(left_pane)
|
||||
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setMinimumWidth(250)
|
||||
self.tree.setHeaderLabels(["Name", "Type", "Shape"])
|
||||
self.tree.header().setStretchLastSection(False)
|
||||
self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
self.tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.tree.header().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.tree.itemClicked.connect(self._on_item_clicked)
|
||||
|
||||
left_layout.addWidget(self.tree, 1)
|
||||
|
||||
self.data_panel = DataView()
|
||||
|
||||
splitter.addWidget(left_pane)
|
||||
splitter.addWidget(self.data_panel)
|
||||
|
||||
splitter.setStretchFactor(0, 1)
|
||||
splitter.setStretchFactor(1, 3)
|
||||
splitter.setSizes([300, 900])
|
||||
splitter.setHandleWidth(6)
|
||||
splitter.setChildrenCollapsible(False)
|
||||
|
||||
self.scan_view_group = Group("Scan view", [splitter])
|
||||
|
||||
root_layout.addWidget(self.scan_view_group)
|
||||
|
||||
if filepath:
|
||||
self.load_files([filepath])
|
||||
|
||||
def apply_theme(self, theme: Literal["dark", "light"]):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (str): Theme, either "dark" or "light"
|
||||
"""
|
||||
self.data_panel.apply_theme(theme)
|
||||
|
||||
def load_files(self, filepaths: list[str]) -> None:
|
||||
"""Open one or more files and add each as a top-level tree node."""
|
||||
for fp in filepaths:
|
||||
if fp in self._open_files:
|
||||
continue # already loaded
|
||||
|
||||
loader = self.registry.get_loader(fp)
|
||||
if loader is None:
|
||||
supported = ", ".join(self.registry.supported_extensions)
|
||||
logger.warning("No loader found for %r. Supported extensions: %s", fp, supported)
|
||||
continue
|
||||
|
||||
try:
|
||||
loader.open(fp)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to open %r: %s", fp, exc)
|
||||
continue
|
||||
|
||||
display_name = os.path.basename(fp)
|
||||
self._open_files[fp] = (loader, display_name)
|
||||
self._add_file_to_tree(fp, loader, display_name)
|
||||
|
||||
def clear_files(self) -> None:
|
||||
"""Close all open files and reset the tree."""
|
||||
for loader, _ in self._open_files.values():
|
||||
loader.close()
|
||||
self._open_files.clear()
|
||||
self.tree.clear()
|
||||
self.data_panel.show_empty()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Close all"""
|
||||
for loader, _ in self._open_files.values():
|
||||
loader.close()
|
||||
self._open_files.clear()
|
||||
super().closeEvent(event)
|
||||
|
||||
def _add_file_to_tree(self, filepath: str, loader: BaseFileLoader, display_name: str) -> None:
|
||||
"""Add a single file as a new top-level node in the tree."""
|
||||
dataset_icon = material_icon(
|
||||
"dataset", size=(ICON_SIZE, ICON_SIZE), color=get_accent_colors().default.name()
|
||||
)
|
||||
root_item = QTreeWidgetItem(self.tree, [display_name, "Group", ""])
|
||||
root_item.setIcon(0, dataset_icon)
|
||||
root_item.setData(0, Qt.ItemDataRole.UserRole, "/") # path
|
||||
root_item.setData(0, Qt.ItemDataRole.UserRole + 1, "group") # kind
|
||||
root_item.setData(0, Qt.ItemDataRole.UserRole + 2, filepath) # file key
|
||||
|
||||
self._populate_tree(root_item, loader, "/")
|
||||
self.tree.addTopLevelItem(root_item)
|
||||
|
||||
# Expand first 2 levels by default
|
||||
self.tree.expandItem(root_item)
|
||||
for i in range(root_item.childCount()):
|
||||
self.tree.expandItem(root_item.child(i))
|
||||
|
||||
self.tree.setCurrentItem(root_item)
|
||||
|
||||
def _populate_tree(
|
||||
self, parent_item: QTreeWidgetItem, loader: BaseFileLoader, path: str
|
||||
) -> None:
|
||||
folder_icon = material_icon("folder", size=(ICON_SIZE, ICON_SIZE), color="#2980b9")
|
||||
vector_icon = material_icon("show_chart", size=(ICON_SIZE, ICON_SIZE), color="#2980b9")
|
||||
array_icon = material_icon(
|
||||
"stacked_line_chart", size=(ICON_SIZE, ICON_SIZE), color="#2980b9"
|
||||
)
|
||||
scalar_icon = material_icon("point_scan", size=(ICON_SIZE, ICON_SIZE), color="#2980b9")
|
||||
str_icon = material_icon("text_snippet", size=(ICON_SIZE, ICON_SIZE), color="#2980b9")
|
||||
|
||||
for node in loader.iter_nodes(path):
|
||||
shape_str = "x".join(str(s) for s in node.shape) if node.shape else "scalar"
|
||||
|
||||
if node.kind == "group":
|
||||
item = QTreeWidgetItem(parent_item, [node.name, "Group", ""])
|
||||
item.setIcon(0, folder_icon)
|
||||
item.setData(0, Qt.ItemDataRole.UserRole, node.path)
|
||||
item.setData(0, Qt.ItemDataRole.UserRole + 1, "group")
|
||||
self._populate_tree(item, loader, node.path)
|
||||
|
||||
else: # dataset
|
||||
if shape_str == "scalar":
|
||||
icon = str_icon if node.dtype == "str" else scalar_icon
|
||||
elif "x" in shape_str:
|
||||
icon = array_icon
|
||||
else:
|
||||
icon = vector_icon
|
||||
|
||||
item = QTreeWidgetItem(parent_item, [node.name, node.dtype, shape_str])
|
||||
item.setIcon(0, icon)
|
||||
item.setData(0, Qt.ItemDataRole.UserRole, node.path)
|
||||
item.setData(0, Qt.ItemDataRole.UserRole + 1, "dataset")
|
||||
item.setForeground(1, QBrush(QColor(get_accent_colors().success.name())))
|
||||
item.setForeground(2, QBrush(QColor("#656365")))
|
||||
|
||||
def _get_filepath_for_item(self, item: QTreeWidgetItem) -> str:
|
||||
"""Walk up the tree to find the filepath stored on the root node."""
|
||||
node = item
|
||||
while node.parent():
|
||||
node = node.parent()
|
||||
return node.data(0, Qt.ItemDataRole.UserRole + 2)
|
||||
|
||||
def _on_item_clicked(self, item: QTreeWidgetItem, _col: int) -> None:
|
||||
path = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
kind = item.data(0, Qt.ItemDataRole.UserRole + 1)
|
||||
filepath = self._get_filepath_for_item(item)
|
||||
|
||||
if not path or not filepath:
|
||||
return
|
||||
|
||||
loader, _ = self._open_files[filepath]
|
||||
|
||||
if kind == "dataset":
|
||||
data = loader.read_dataset(path)
|
||||
self.data_panel.display(data, path)
|
||||
@@ -6,7 +6,7 @@ def main(): # pragma: no cover
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from superxas_bec.bec_widgets.widgets.data_viewer.data_viewer_plugin import DataViewerPlugin
|
||||
from .data_viewer_plugin import DataViewerPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(DataViewerPlugin())
|
||||
|
||||
|
||||
@@ -1,514 +0,0 @@
|
||||
"""
|
||||
HDF5 Viewer — qtpy + pyqtgraph + h5py
|
||||
"""
|
||||
|
||||
from typing import Literal, Optional, cast
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from bec_widgets.utils.colors import Colors, get_accent_colors
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtGui import QBrush, QColor, QFont
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QRadioButton,
|
||||
QSizePolicy,
|
||||
QSlider,
|
||||
QSplitter,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QTreeWidget,
|
||||
QTreeWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
ICON_SIZE = 20
|
||||
|
||||
|
||||
# ── Data / Plot panel ──────────────────────────────────────────────────────
|
||||
class DataPanel(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._layout = QVBoxLayout(self)
|
||||
|
||||
# Header
|
||||
hdr = QHBoxLayout()
|
||||
self.path_label = QLabel("Select a dataset from the tree")
|
||||
self.path_label.setObjectName("path_label")
|
||||
self.info_label = QLabel("")
|
||||
self.info_label.setObjectName("info_label")
|
||||
hdr.addWidget(self.path_label, 1)
|
||||
hdr.addWidget(self.info_label)
|
||||
self._layout.addLayout(hdr)
|
||||
|
||||
# View-mode selector
|
||||
mode_box = QGroupBox("View mode")
|
||||
mode_layout = QHBoxLayout(mode_box)
|
||||
mode_layout.setContentsMargins(8, 4, 8, 4)
|
||||
self.rb_auto = QRadioButton("Auto")
|
||||
self.rb_plot = QRadioButton("Plot")
|
||||
self.rb_image = QRadioButton("Image")
|
||||
self.rb_table = QRadioButton("Table")
|
||||
self.rb_auto.setChecked(True)
|
||||
for rb in (self.rb_auto, self.rb_plot, self.rb_image, self.rb_table):
|
||||
mode_layout.addWidget(rb)
|
||||
rb.toggled.connect(self._on_mode_change)
|
||||
mode_layout.addStretch()
|
||||
self._layout.addWidget(mode_box)
|
||||
|
||||
# Content stack
|
||||
self.stack = QWidget()
|
||||
self.stack_layout = QVBoxLayout(self.stack)
|
||||
self.stack_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.addWidget(self.stack, 1)
|
||||
|
||||
self.plot_widget = None
|
||||
self.image_widget = None
|
||||
|
||||
self._current_data = None
|
||||
self.show_empty()
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────
|
||||
def _clear_stack(self):
|
||||
while self.stack_layout.count():
|
||||
item = self.stack_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
self.plot_widget = None
|
||||
self.image_widget = None
|
||||
|
||||
def show_empty(self):
|
||||
self._clear_stack()
|
||||
lbl = QLabel("No data selected")
|
||||
lbl.setObjectName("info_label")
|
||||
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.stack_layout.addWidget(lbl)
|
||||
|
||||
def _on_mode_change(self):
|
||||
if self._current_data is not None:
|
||||
self.display(self._current_data, self.path_label.text())
|
||||
|
||||
def _active_mode(self):
|
||||
if self.rb_plot.isChecked():
|
||||
return "plot"
|
||||
if self.rb_image.isChecked():
|
||||
return "image"
|
||||
if self.rb_table.isChecked():
|
||||
return "table"
|
||||
return "auto"
|
||||
|
||||
# ── public ────────────────────────────────────────────────────────────
|
||||
def display(self, data, path=""):
|
||||
self._current_data = data
|
||||
self.path_label.setText(path)
|
||||
|
||||
if not isinstance(data, np.ndarray):
|
||||
data = np.array(data)
|
||||
|
||||
self.info_label.setText(
|
||||
f"shape {data.shape} · dtype {data.dtype} · {data.size:,} elements"
|
||||
)
|
||||
|
||||
mode = self._active_mode()
|
||||
if mode == "auto":
|
||||
if data.ndim <= 1 and data.size > 1:
|
||||
mode = "plot"
|
||||
elif data.ndim == 2 and min(data.shape) > 1:
|
||||
mode = "image"
|
||||
else:
|
||||
mode = "table"
|
||||
|
||||
if mode == "plot":
|
||||
self._show_plot_1d(data)
|
||||
elif mode == "image":
|
||||
self._show_image_2d(data)
|
||||
else:
|
||||
self._show_table(data)
|
||||
|
||||
# ── 1-D line plot ──────────────────────────────────────────────────────
|
||||
def _show_plot_1d(self, data):
|
||||
self._clear_stack()
|
||||
|
||||
is_2d = data.ndim == 2
|
||||
|
||||
if is_2d:
|
||||
n_rows, n_cols = data.shape
|
||||
row_data = data[0].astype(np.float32)
|
||||
else:
|
||||
row_data = data.reshape(-1).astype(np.float32)
|
||||
|
||||
x = np.arange(row_data.size, dtype=np.float32)
|
||||
|
||||
self.plot_widget = pg.PlotWidget()
|
||||
plot_item = self.plot_widget.getPlotItem()
|
||||
assert plot_item is not None, "PlotWidget has no PlotItem"
|
||||
plot_item.showGrid(x=True, y=True, alpha=0.25)
|
||||
self.plot_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
plot_item.setAutoVisible(y=False) # type: ignore[attr-defined]
|
||||
|
||||
curve = pg.PlotDataItem(
|
||||
x, row_data, pen=pg.mkPen(color="#2980b9", width=1.6), antialias=False
|
||||
)
|
||||
self.plot_widget.addItem(curve)
|
||||
|
||||
curve.setDownsampling(auto=True, method="peak")
|
||||
curve.setClipToView(True)
|
||||
curve.setSkipFiniteCheck(True)
|
||||
|
||||
plot_item.enableAutoRange() # type: ignore[attr-defined]
|
||||
|
||||
if is_2d:
|
||||
# --- Slider ---
|
||||
slider = QSlider(Qt.Orientation.Vertical)
|
||||
slider.setMinimum(0)
|
||||
slider.setMaximum(n_rows - 1)
|
||||
slider.setValue(0)
|
||||
slider.setFixedWidth(32)
|
||||
slider.setPageStep(1)
|
||||
|
||||
row_label = QLabel("0")
|
||||
row_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
row_label.setFixedWidth(32)
|
||||
|
||||
def on_row_changed(row):
|
||||
row_label.setText(str(row))
|
||||
new_data = data[row].astype(np.float32)
|
||||
new_x = np.arange(new_data.size, dtype=np.float32)
|
||||
curve.setData(new_x, new_data)
|
||||
plot_item.enableAutoRange() # type: ignore[attr-defined]
|
||||
|
||||
slider.valueChanged.connect(on_row_changed)
|
||||
|
||||
slider_col = QWidget()
|
||||
slider_col.setFixedWidth(36)
|
||||
col_layout = QVBoxLayout(slider_col)
|
||||
col_layout.setContentsMargins(0, 0, 0, 0)
|
||||
col_layout.setSpacing(2)
|
||||
col_layout.addWidget(row_label)
|
||||
col_layout.addWidget(slider)
|
||||
|
||||
container = QWidget()
|
||||
h_layout = QHBoxLayout(container)
|
||||
h_layout.setContentsMargins(0, 0, 0, 0)
|
||||
h_layout.setSpacing(4)
|
||||
h_layout.addWidget(slider_col)
|
||||
h_layout.addWidget(self.plot_widget)
|
||||
|
||||
self.stack_layout.addWidget(container)
|
||||
else:
|
||||
self.stack_layout.addWidget(self.plot_widget)
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
bg_color = pg.getConfigOption("background")
|
||||
fg_color = pg.getConfigOption("foreground")
|
||||
if self.plot_widget is not None:
|
||||
n_curves = len(self.plot_widget.listDataItems())
|
||||
colors = Colors.golden_angle_color(
|
||||
colormap="plasma", num=max(10, n_curves + 1), format="HEX"
|
||||
)
|
||||
for idx, curve in enumerate(self.plot_widget.listDataItems()):
|
||||
curve.setPen(pg.mkPen(color=colors[idx]))
|
||||
# Background
|
||||
self.plot_widget.setBackground(bg_color)
|
||||
# Axes (tick marks, tick labels, axis line)
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
ax = self.plot_widget.getAxis(axis)
|
||||
ax.setPen(pg.mkPen(color=fg_color))
|
||||
ax.setTextPen(pg.mkPen(color=fg_color))
|
||||
|
||||
if self.image_widget is not None:
|
||||
self.image_widget.getView().setBackgroundColor(bg_color)
|
||||
self.image_widget.ui.histogram.setBackground(bg_color)
|
||||
|
||||
# ── 2-D image ──────────────────────────────────────────────────────────
|
||||
def _show_image_2d(self, data):
|
||||
self._clear_stack()
|
||||
|
||||
squeezed = np.squeeze(data)
|
||||
if squeezed.ndim > 2:
|
||||
squeezed = squeezed.reshape(-1, squeezed.shape[-1])
|
||||
|
||||
# complex → magnitude
|
||||
img_data = np.abs(squeezed) if np.iscomplexobj(squeezed) else squeezed.astype(float)
|
||||
|
||||
# ImageView gives us colorbar + histogram + zoom for free
|
||||
self.image_widget = pg.ImageView()
|
||||
self.image_widget.ui.roiBtn.hide()
|
||||
self.image_widget.ui.menuBtn.hide()
|
||||
|
||||
# Use 'inferno'-like LUT
|
||||
self.image_widget.setColorMap(pg.colormap.get("inferno", source="matplotlib"))
|
||||
|
||||
# pyqtgraph ImageView expects (cols, rows) — transpose so row 0 is at top
|
||||
self.image_widget.setImage(img_data.T, autoLevels=True, autoHistogramRange=True)
|
||||
self.image_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.stack_layout.addWidget(self.image_widget)
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
# ── Table ──────────────────────────────────────────────────────────────
|
||||
def _show_table(self, data):
|
||||
self._clear_stack()
|
||||
|
||||
MAX_ROWS, MAX_COLS = 2000, 500
|
||||
|
||||
if data.ndim == 0:
|
||||
flat = data.reshape(1, 1)
|
||||
elif data.ndim == 1:
|
||||
flat = data.reshape(-1, 1)
|
||||
elif data.ndim == 2:
|
||||
flat = data
|
||||
else:
|
||||
flat = data.reshape(-1, data.shape[-1])
|
||||
|
||||
rows, cols = flat.shape
|
||||
show_rows = min(rows, MAX_ROWS)
|
||||
show_cols = min(cols, MAX_COLS)
|
||||
|
||||
if rows > MAX_ROWS or cols > MAX_COLS:
|
||||
note = QLabel(f"⚠ Showing {show_rows}/{rows} rows × {show_cols}/{cols} cols")
|
||||
note.setObjectName("info_label")
|
||||
note.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.stack_layout.addWidget(note)
|
||||
|
||||
table = QTableWidget(show_rows, show_cols)
|
||||
table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
table.setSelectionMode(QAbstractItemView.SelectionMode.ContiguousSelection)
|
||||
table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
table.horizontalHeader().setDefaultSectionSize(90)
|
||||
table.verticalHeader().setDefaultSectionSize(22)
|
||||
table.setFont(QFont("JetBrains Mono, Consolas, monospace", 10))
|
||||
|
||||
is_float = np.issubdtype(flat.dtype, np.floating)
|
||||
is_complex = np.iscomplexobj(flat)
|
||||
is_bytes = flat.dtype.kind == "S"
|
||||
|
||||
for r in range(show_rows):
|
||||
for c in range(show_cols):
|
||||
val = flat[r, c]
|
||||
txt = (
|
||||
f"{val:.6g}"
|
||||
if is_float
|
||||
else f"{val:.4g}" if is_complex else str(val.decode()) if is_bytes else str(val)
|
||||
)
|
||||
cell = QTableWidgetItem(txt)
|
||||
cell.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
table.setItem(r, c, cell)
|
||||
|
||||
self.stack_layout.addWidget(table)
|
||||
|
||||
|
||||
# ── Main window ────────────────────────────────────────────────────────────
|
||||
class HDF5Viewer(QMainWindow):
|
||||
def __init__(self, filepath=None):
|
||||
super().__init__()
|
||||
self.h5files = {} # filepath -> h5py.File
|
||||
self._build_ui()
|
||||
if filepath:
|
||||
self.load_files([filepath])
|
||||
|
||||
def apply_theme(self, theme: Literal["dark", "light"]):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (str): Theme, either "dark" or "light"
|
||||
"""
|
||||
self.data_panel.apply_theme(theme)
|
||||
|
||||
def load_files(self, filepaths: list[str]):
|
||||
"""Open one or more HDF5 files and add each as a top-level tree node."""
|
||||
for f in filepaths:
|
||||
if f in self.h5files:
|
||||
continue # already loaded
|
||||
self.h5files[f] = h5py.File(f, "r")
|
||||
self._add_file_to_tree(f)
|
||||
|
||||
def _build_ui(self):
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
|
||||
root_layout = QHBoxLayout(central)
|
||||
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
splitter.setChildrenCollapsible(False)
|
||||
|
||||
# ── Left pane ──
|
||||
left_pane = QWidget()
|
||||
left_layout = QVBoxLayout(left_pane)
|
||||
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setHeaderLabels(["Name", "Type", "Shape"])
|
||||
self.tree.header().setStretchLastSection(False)
|
||||
self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
self.tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.tree.header().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.tree.itemClicked.connect(self._on_item_clicked)
|
||||
|
||||
left_layout.addWidget(self.tree, 1)
|
||||
|
||||
# ── Right pane ──
|
||||
self.data_panel = DataPanel()
|
||||
|
||||
splitter.addWidget(left_pane)
|
||||
splitter.addWidget(self.data_panel)
|
||||
|
||||
splitter.setStretchFactor(0, 1)
|
||||
splitter.setStretchFactor(1, 3)
|
||||
splitter.setSizes([300, 900])
|
||||
splitter.setHandleWidth(6)
|
||||
splitter.setChildrenCollapsible(False)
|
||||
|
||||
root_layout.addWidget(splitter)
|
||||
|
||||
def _add_file_to_tree(self, filepath: str):
|
||||
"""Add a single file as a new top-level node in the tree."""
|
||||
h5file = self.h5files[filepath]
|
||||
filename = filepath.split("/")[-1]
|
||||
|
||||
dataset_icon = material_icon(
|
||||
"dataset", size=(ICON_SIZE, ICON_SIZE), color=get_accent_colors().default.name()
|
||||
)
|
||||
root_item = QTreeWidgetItem(self.tree, [filename, "Group", ""])
|
||||
root_item.setIcon(0, dataset_icon)
|
||||
root_item.setData(0, Qt.ItemDataRole.UserRole, "/")
|
||||
root_item.setData(0, Qt.ItemDataRole.UserRole + 1, "group")
|
||||
root_item.setData(0, Qt.ItemDataRole.UserRole + 2, filepath) # so clicks know which file
|
||||
|
||||
self.populate_tree(root_item, h5file)
|
||||
self.tree.addTopLevelItem(root_item)
|
||||
|
||||
# Expand first 2 levels
|
||||
self.tree.expandItem(root_item)
|
||||
for i in range(root_item.childCount()):
|
||||
self.tree.expandItem(root_item.child(i))
|
||||
|
||||
self.tree.setCurrentItem(root_item)
|
||||
|
||||
def populate_tree(self, parent_item, h5_obj):
|
||||
folder_icon = material_icon("folder", size=(ICON_SIZE, ICON_SIZE), color="#2980b9")
|
||||
vector_icon = material_icon("show_chart", size=(ICON_SIZE, ICON_SIZE), color="#2980b9")
|
||||
array_icon = material_icon(
|
||||
"stacked_line_chart", size=(ICON_SIZE, ICON_SIZE), color="#2980b9"
|
||||
)
|
||||
scalar_icon = material_icon("point_scan", size=(ICON_SIZE, ICON_SIZE), color="#2980b9")
|
||||
str_icon = material_icon("text_snippet", size=(ICON_SIZE, ICON_SIZE), color="#2980b9")
|
||||
|
||||
if not isinstance(h5_obj, h5py.Group):
|
||||
return
|
||||
|
||||
for key in h5_obj.keys():
|
||||
try:
|
||||
child = h5_obj[key]
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if isinstance(child, h5py.Group):
|
||||
item = QTreeWidgetItem(parent_item, [key, "Group", ""])
|
||||
item.setIcon(0, folder_icon)
|
||||
|
||||
item.setData(0, Qt.ItemDataRole.UserRole, child.name)
|
||||
item.setData(0, Qt.ItemDataRole.UserRole + 1, "group")
|
||||
|
||||
self.populate_tree(item, child)
|
||||
|
||||
elif isinstance(child, h5py.Dataset):
|
||||
shape_str = "x".join(str(s) for s in child.shape) or "scalar"
|
||||
|
||||
d = child.dtype
|
||||
dtype = "unknown"
|
||||
if np.issubdtype(d, np.integer):
|
||||
dtype = "int"
|
||||
if np.issubdtype(d, np.floating):
|
||||
dtype = "float"
|
||||
if np.issubdtype(d, np.complexfloating):
|
||||
dtype = "complex"
|
||||
if d.kind in ("S", "U", "O"):
|
||||
dtype = "str"
|
||||
|
||||
if shape_str == "scalar":
|
||||
if dtype == "str":
|
||||
icon = str_icon
|
||||
else:
|
||||
icon = scalar_icon
|
||||
elif "x" in shape_str:
|
||||
icon = array_icon
|
||||
else:
|
||||
icon = vector_icon
|
||||
|
||||
item = QTreeWidgetItem(parent_item, [key, dtype, shape_str])
|
||||
item.setIcon(0, icon)
|
||||
|
||||
item.setData(0, Qt.ItemDataRole.UserRole, child.name)
|
||||
item.setData(0, Qt.ItemDataRole.UserRole + 1, "dataset")
|
||||
|
||||
item.setForeground(1, QBrush(QColor(get_accent_colors().success.name())))
|
||||
item.setForeground(2, QBrush(QColor("#656365")))
|
||||
|
||||
def _get_filepath_for_item(self, item: QTreeWidgetItem) -> str:
|
||||
"""Walk up the tree to find the filepath stored on the root node."""
|
||||
node = item
|
||||
while node.parent():
|
||||
node = node.parent()
|
||||
return node.data(0, Qt.ItemDataRole.UserRole + 2)
|
||||
|
||||
def _on_item_clicked(self, item, _col):
|
||||
path = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
kind = item.data(0, Qt.ItemDataRole.UserRole + 1)
|
||||
filepath = self._get_filepath_for_item(item)
|
||||
|
||||
if not path or not filepath:
|
||||
return
|
||||
|
||||
h5file = self.h5files[filepath]
|
||||
obj = h5file[path] if path != "/" else h5file
|
||||
|
||||
if kind == "dataset":
|
||||
data = h5file[path][()]
|
||||
self.data_panel.display(data, path)
|
||||
else:
|
||||
n = len(obj) if isinstance(obj, h5py.Group) else 0
|
||||
|
||||
def clear_files(self):
|
||||
"""Close all open HDF5 files and reset the tree."""
|
||||
for h5file in self.h5files.values():
|
||||
h5file.close()
|
||||
self.h5files.clear()
|
||||
self.tree.clear()
|
||||
self.data_panel.show_empty()
|
||||
|
||||
def closeEvent(self, event):
|
||||
for h5file in self.h5files.values():
|
||||
h5file.close()
|
||||
self.h5files.clear()
|
||||
super().closeEvent(event)
|
||||
+235
-151
@@ -1,151 +1,235 @@
|
||||
"""
|
||||
TaggedListWidget — a QListWidget where each item shows a label + styled tag pills.
|
||||
Selection works natively; no popup, no paintEvent hacks.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import QPoint, QRect, QSize, Qt
|
||||
from qtpy.QtGui import QColor, QFont, QFontMetrics, QPainter, QPen
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QLabel,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QStyle,
|
||||
QStyledItemDelegate,
|
||||
QStyleOptionViewItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
# ── Design tokens ──────────────────────────────────────────────────────────────
|
||||
ITEM_HEIGHT = 30
|
||||
H_PAD = 12
|
||||
TAG_H_PAD = 7
|
||||
TAG_V_PAD = 3
|
||||
TAG_GAP = 5
|
||||
LABEL_TAG_GAP = 12
|
||||
CORNER_RADIUS = 4
|
||||
TAG_TEXT_COLOR = "#FFFFFF"
|
||||
|
||||
# ── Enum compat (PySide6 nested vs PyQt5 flat) ────────────────────────────────
|
||||
try:
|
||||
_DEMIBOLD = QFont.Weight.DemiBold
|
||||
_MEDIUM = QFont.Weight.Medium
|
||||
except AttributeError:
|
||||
_DEMIBOLD = QFont.DemiBold
|
||||
_MEDIUM = QFont.Medium
|
||||
|
||||
_AlignVCenter = Qt.AlignmentFlag.AlignVCenter if hasattr(Qt, "AlignmentFlag") else Qt.AlignVCenter
|
||||
_AlignCenter = Qt.AlignmentFlag.AlignCenter if hasattr(Qt, "AlignmentFlag") else Qt.AlignCenter
|
||||
_UserRole = Qt.ItemDataRole.UserRole if hasattr(Qt, "ItemDataRole") else Qt.UserRole
|
||||
_NoPen = Qt.PenStyle.NoPen if hasattr(Qt, "PenStyle") else Qt.NoPen
|
||||
_AA = QPainter.RenderHint.Antialiasing if hasattr(QPainter, "RenderHint") else QPainter.Antialiasing
|
||||
|
||||
try:
|
||||
_State_Selected = QStyle.StateFlag.State_Selected
|
||||
_State_MouseOver = QStyle.StateFlag.State_MouseOver
|
||||
except AttributeError:
|
||||
_State_Selected = QStyle.State_Selected
|
||||
_State_MouseOver = QStyle.State_MouseOver
|
||||
|
||||
|
||||
# ── Delegate ──────────────────────────────────────────────────────────────────
|
||||
class TaggedDelegate(QStyledItemDelegate):
|
||||
|
||||
def sizeHint(self, option: QStyleOptionViewItem, index) -> QSize:
|
||||
return QSize(option.rect.width() or 300, ITEM_HEIGHT)
|
||||
|
||||
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index) -> None:
|
||||
painter.save()
|
||||
|
||||
is_selected = bool(option.state & _State_Selected)
|
||||
is_hover = bool(option.state & _State_MouseOver)
|
||||
|
||||
# Background
|
||||
if is_selected:
|
||||
painter.fillRect(option.rect, option.palette.highlight())
|
||||
elif is_hover:
|
||||
painter.fillRect(option.rect, QColor("#F1F5F9"))
|
||||
else:
|
||||
painter.fillRect(option.rect, option.palette.base())
|
||||
|
||||
label_text = (
|
||||
index.data(
|
||||
Qt.ItemDataRole.DisplayRole if hasattr(Qt, "ItemDataRole") else Qt.DisplayRole
|
||||
)
|
||||
or ""
|
||||
)
|
||||
tags: list = index.data(_UserRole) or []
|
||||
|
||||
# Label
|
||||
label_font = painter.font() # inherit default font
|
||||
label_font.setWeight(_DEMIBOLD)
|
||||
painter.setFont(label_font)
|
||||
label_color = (
|
||||
option.palette.highlightedText().color()
|
||||
if is_selected
|
||||
else option.palette.text().color()
|
||||
)
|
||||
painter.setPen(QPen(label_color))
|
||||
|
||||
fm = QFontMetrics(label_font)
|
||||
label_w = fm.horizontalAdvance(label_text)
|
||||
label_y = option.rect.top() + (ITEM_HEIGHT - fm.height()) // 2 + fm.ascent()
|
||||
painter.drawText(QPoint(option.rect.left() + H_PAD, label_y), label_text)
|
||||
|
||||
# Tag pills
|
||||
tag_font = painter.font() # inherit default font
|
||||
tag_font.setWeight(_MEDIUM)
|
||||
fm_tag = QFontMetrics(tag_font)
|
||||
|
||||
x = option.rect.left() + H_PAD + label_w + LABEL_TAG_GAP
|
||||
for tag_text, hex_color in tags:
|
||||
tag_text = str(tag_text)
|
||||
tw = fm_tag.horizontalAdvance(tag_text) + 2 * TAG_H_PAD
|
||||
th = fm_tag.height() + 2 * TAG_V_PAD
|
||||
ty = option.rect.top() + (ITEM_HEIGHT - th) // 2
|
||||
pill = QRect(x, ty, tw, th)
|
||||
|
||||
fill = QColor(hex_color)
|
||||
if is_selected:
|
||||
fill = fill.lighter(140)
|
||||
|
||||
painter.setRenderHint(_AA)
|
||||
painter.setPen(_NoPen)
|
||||
painter.setBrush(fill)
|
||||
painter.drawRoundedRect(pill, CORNER_RADIUS, CORNER_RADIUS)
|
||||
|
||||
painter.setFont(tag_font)
|
||||
painter.setPen(QPen(QColor(TAG_TEXT_COLOR)))
|
||||
painter.drawText(pill, _AlignCenter, tag_text)
|
||||
|
||||
x += tw + TAG_GAP
|
||||
|
||||
painter.restore()
|
||||
|
||||
|
||||
# ── Widget ────────────────────────────────────────────────────────────────────
|
||||
class TaggedListWidget(QListWidget):
|
||||
"""QListWidget with label + coloured tag pills per row."""
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setItemDelegate(TaggedDelegate(self))
|
||||
self.setMouseTracking(True) # enables hover highlight
|
||||
|
||||
def addTaggedItem(self, label: str, tags: list | None = None) -> QListWidgetItem:
|
||||
"""
|
||||
Add a row. tags is a list of (text, hex_color) pairs, e.g.
|
||||
[("v1.26", "#2563EB"), ("stable", "#16A34A")]
|
||||
"""
|
||||
item = QListWidgetItem(str(label))
|
||||
if tags:
|
||||
item.setData(_UserRole, list(tags))
|
||||
self.addItem(item)
|
||||
return item
|
||||
|
||||
def currentTags(self) -> list:
|
||||
item = self.currentItem()
|
||||
return item.data(_UserRole) or [] if item else []
|
||||
"""
|
||||
Universal Qt widgets
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import QPoint, QRect, QSize, Qt
|
||||
from qtpy.QtGui import QColor, QFont, QFontMetrics, QPainter, QPen
|
||||
from qtpy.QtWidgets import (
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLayout,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QPushButton,
|
||||
QStyle,
|
||||
QStyledItemDelegate,
|
||||
QStyleOptionViewItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class Group(QGroupBox):
|
||||
def __init__(self, label, objs, orientation="vertical"):
|
||||
super().__init__(label)
|
||||
if orientation == "vertical":
|
||||
self._layout = QVBoxLayout(self)
|
||||
elif orientation == "horizontal":
|
||||
self._layout = QHBoxLayout(self)
|
||||
else:
|
||||
raise ValueError(f"Orientation {orientation} is not supported!")
|
||||
for obj in objs:
|
||||
if isinstance(obj, QWidget):
|
||||
self._layout.addWidget(obj)
|
||||
elif isinstance(obj, QLayout):
|
||||
self._layout.addLayout(obj)
|
||||
|
||||
|
||||
class Button(QWidget):
|
||||
def __init__(self, label=None, label_button: str = "", enabled=False):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
if label is not None:
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(140)
|
||||
layout.addWidget(self.label)
|
||||
self.button = QPushButton(label_button)
|
||||
if label is not None:
|
||||
self.button.setFixedWidth(160)
|
||||
self.enable_button(enabled)
|
||||
layout.addWidget(self.button)
|
||||
|
||||
def clicked_connect(self, func):
|
||||
"""Connect a function to the button press."""
|
||||
self.button.clicked.connect(func)
|
||||
|
||||
def enable_button(self, enable: bool = False):
|
||||
if enable:
|
||||
self.button.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}"
|
||||
)
|
||||
self.button.setEnabled(True)
|
||||
else: # disabled
|
||||
self.button.setStyleSheet(
|
||||
"QPushButton {{background-color: rgb(120, 120, 120); color: white;}}"
|
||||
)
|
||||
self.button.setDisabled(True)
|
||||
|
||||
def setText(self, text):
|
||||
self.button.setText(text)
|
||||
|
||||
|
||||
class ListWidget(QWidget):
|
||||
def __init__(self, identifier=""):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self.identifier = identifier
|
||||
self.value = TaggedListWidget()
|
||||
layout.addWidget(self.value)
|
||||
|
||||
def clear(self):
|
||||
self.value.clear()
|
||||
|
||||
def addTaggedItem(self, label, tags):
|
||||
self.value.addTaggedItem(label, tags)
|
||||
|
||||
def setCurrentIndex(self, text):
|
||||
self.value.setCurrentIndex(text)
|
||||
|
||||
def currentItemChanged_connect(self, func):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.value.currentItemChanged.connect(
|
||||
partial(
|
||||
func,
|
||||
identifier=self.identifier,
|
||||
value_obj=self.value,
|
||||
value=lambda: self.value.currentIndex(),
|
||||
)
|
||||
)
|
||||
|
||||
def setDisabled(self, disable):
|
||||
self.value.setDisabled(disable)
|
||||
|
||||
|
||||
class TaggedListWidget(QListWidget):
|
||||
"""QListWidget with label + coloured tag pills per row."""
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setItemDelegate(TaggedDelegate(self))
|
||||
self.setMouseTracking(True) # enables hover highlight
|
||||
|
||||
def addTaggedItem(self, label: str, tags: list | None = None) -> QListWidgetItem:
|
||||
"""
|
||||
Add a row. tags is a list of (text, hex_color) pairs, e.g.
|
||||
[("v1.26", "#2563EB"), ("stable", "#16A34A")]
|
||||
"""
|
||||
item = QListWidgetItem(str(label))
|
||||
if tags:
|
||||
item.setData(_UserRole, list(tags))
|
||||
self.addItem(item)
|
||||
return item
|
||||
|
||||
def currentTags(self) -> list:
|
||||
item = self.currentItem()
|
||||
return item.data(_UserRole) or [] if item else []
|
||||
|
||||
|
||||
ITEM_HEIGHT = 30
|
||||
H_PAD = 12
|
||||
TAG_H_PAD = 7
|
||||
TAG_V_PAD = 3
|
||||
TAG_GAP = 5
|
||||
LABEL_TAG_GAP = 12
|
||||
CORNER_RADIUS = 4
|
||||
TAG_TEXT_COLOR = "#FFFFFF"
|
||||
_DEMIBOLD = QFont.Weight.DemiBold
|
||||
_MEDIUM = QFont.Weight.Medium
|
||||
_AlignCenter = (
|
||||
Qt.AlignmentFlag.AlignCenter if hasattr(Qt, "AlignmentFlag") else Qt.AlignmentFlag.AlignCenter
|
||||
)
|
||||
_UserRole = Qt.ItemDataRole.UserRole if hasattr(Qt, "ItemDataRole") else Qt.ItemDataRole.UserRole
|
||||
_NoPen = Qt.PenStyle.NoPen if hasattr(Qt, "PenStyle") else Qt.PenStyle.NoPen
|
||||
_AA = (
|
||||
QPainter.RenderHint.Antialiasing
|
||||
if hasattr(QPainter, "RenderHint")
|
||||
else QPainter.RenderHint.Antialiasing
|
||||
)
|
||||
_State_Selected = QStyle.StateFlag.State_Selected
|
||||
_State_MouseOver = QStyle.StateFlag.State_MouseOver
|
||||
|
||||
|
||||
class TaggedDelegate(QStyledItemDelegate):
|
||||
|
||||
def sizeHint(self, option: QStyleOptionViewItem, index) -> QSize:
|
||||
return QSize(option.rect.width() or 300, ITEM_HEIGHT)
|
||||
|
||||
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index) -> None:
|
||||
painter.save()
|
||||
|
||||
is_selected = bool(option.state & _State_Selected)
|
||||
is_hover = bool(option.state & _State_MouseOver)
|
||||
|
||||
# Background
|
||||
if is_selected:
|
||||
painter.fillRect(option.rect, option.palette.highlight())
|
||||
elif is_hover:
|
||||
painter.fillRect(option.rect, QColor("#F1F5F9"))
|
||||
else:
|
||||
painter.fillRect(option.rect, option.palette.base())
|
||||
|
||||
label_text = (
|
||||
index.data(
|
||||
Qt.ItemDataRole.DisplayRole
|
||||
if hasattr(Qt, "ItemDataRole")
|
||||
else Qt.ItemDataRole.DisplayRole
|
||||
)
|
||||
or ""
|
||||
)
|
||||
tags: list = index.data(_UserRole) or []
|
||||
|
||||
# Label
|
||||
label_font = painter.font() # inherit default font
|
||||
label_font.setWeight(_DEMIBOLD)
|
||||
painter.setFont(label_font)
|
||||
label_color = (
|
||||
option.palette.highlightedText().color()
|
||||
if is_selected
|
||||
else option.palette.text().color()
|
||||
)
|
||||
painter.setPen(QPen(label_color))
|
||||
|
||||
fm = QFontMetrics(label_font)
|
||||
label_w = fm.horizontalAdvance(label_text)
|
||||
label_y = option.rect.top() + (ITEM_HEIGHT - fm.height()) // 2 + fm.ascent()
|
||||
painter.drawText(QPoint(option.rect.left() + H_PAD, label_y), label_text)
|
||||
|
||||
# Tag pills
|
||||
tag_font = painter.font() # inherit default font
|
||||
tag_font.setWeight(_MEDIUM)
|
||||
fm_tag = QFontMetrics(tag_font)
|
||||
|
||||
x = option.rect.left() + H_PAD + label_w + LABEL_TAG_GAP
|
||||
for tag_text, hex_color in tags:
|
||||
tag_text = str(tag_text)
|
||||
tw = fm_tag.horizontalAdvance(tag_text) + 2 * TAG_H_PAD
|
||||
th = fm_tag.height() + 2 * TAG_V_PAD
|
||||
ty = option.rect.top() + (ITEM_HEIGHT - th) // 2
|
||||
pill = QRect(x, ty, tw, th)
|
||||
|
||||
fill = QColor(hex_color)
|
||||
if is_selected:
|
||||
fill = fill.lighter(140)
|
||||
|
||||
painter.setRenderHint(_AA)
|
||||
painter.setPen(_NoPen)
|
||||
painter.setBrush(fill)
|
||||
painter.drawRoundedRect(pill, CORNER_RADIUS, CORNER_RADIUS)
|
||||
|
||||
painter.setFont(tag_font)
|
||||
painter.setPen(QPen(QColor(TAG_TEXT_COLOR)))
|
||||
painter.drawText(pill, _AlignCenter, tag_text)
|
||||
|
||||
x += tw + TAG_GAP
|
||||
|
||||
painter.restore()
|
||||
@@ -6,8 +6,10 @@ from __future__ import annotations
|
||||
|
||||
designer_plugins = {
|
||||
"DataViewer": ("superxas_bec.bec_widgets.widgets.data_viewer.data_viewer", "DataViewer"),
|
||||
"DigitalTwin": ("superxas_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"),
|
||||
}
|
||||
|
||||
widget_icons = {
|
||||
"DataViewer": "find_in_page",
|
||||
"DigitalTwin": "lightbulb",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .beamline import get_parameters
|
||||
|
||||
parameters = get_parameters()
|
||||
@@ -0,0 +1,51 @@
|
||||
import socket
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
from .types import BeamlineId
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def get_beamline_id() -> BeamlineId:
|
||||
"""
|
||||
Based on the bec servers hostname, tries to extract the beamline
|
||||
identifier (e.g. x01da, x10da, etc).
|
||||
|
||||
Raises:
|
||||
ValueError if beamline cannot be extracted from hostname or beamline not implemented.
|
||||
"""
|
||||
bec_hostname = socket.gethostname()
|
||||
start = bec_hostname.find("x")
|
||||
if start != -1:
|
||||
beamline = bec_hostname[start : start + 5]
|
||||
match beamline:
|
||||
case "x01da":
|
||||
return BeamlineId.X01DA
|
||||
case "x10da":
|
||||
return BeamlineId.X10DA
|
||||
case _:
|
||||
raise ValueError(f"Not implemented beamline {beamline}")
|
||||
else:
|
||||
logger.warning(f"Failed to extract beamline from bec server hostname {bec_hostname}")
|
||||
choice = input("Do you want to manually select a beamline? (yes/no): ").strip().lower()
|
||||
if choice in ["yes", "y"]:
|
||||
bl = input(f"Choose from: {[bl.value for bl in BeamlineId]}")
|
||||
if bl in BeamlineId:
|
||||
logger.info(f"Manually selected beamline {bl}")
|
||||
return BeamlineId(bl)
|
||||
else:
|
||||
raise ValueError(f"Wrong selection {bl}")
|
||||
else:
|
||||
raise ValueError("Cannot open digital twin without a beamline")
|
||||
|
||||
|
||||
def get_parameters():
|
||||
beamline = get_beamline_id()
|
||||
if beamline == "x01da":
|
||||
from . import x01da_parameters as parameters
|
||||
elif beamline == "x10da":
|
||||
from . import x10da_parameters as parameters
|
||||
else:
|
||||
raise ValueError(f"Unknown beamline: {beamline}")
|
||||
return parameters
|
||||
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Calculates the positions of axes based on a beamline config
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
|
||||
from .. import parameters as bl
|
||||
from ..types import BeamlineId, ConfigDict
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def calc_positions(beamline: BeamlineId, cfg: ConfigDict) -> dict[str, dict[str, float]]:
|
||||
"""
|
||||
Calculates the positions of axes based on a beamline config.
|
||||
|
||||
Args:
|
||||
cfg(ConfigDict): Dictionary with beamline config
|
||||
|
||||
Returns:
|
||||
dict[str, dict[str, float]]: Dictionary mapping device names to dictionaries
|
||||
containing a "value" key with the corresponding float value (position).
|
||||
"""
|
||||
|
||||
pos = {}
|
||||
|
||||
## FE slits
|
||||
trxr = -np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1]
|
||||
trxw = (
|
||||
(np.arctan(cfg["h_acc"]) * bl.feSlits.center1[1])
|
||||
/ bl.feSlits.center1[1]
|
||||
* bl.feSlits.center2[1]
|
||||
)
|
||||
tryb = -np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1]
|
||||
tryt = (
|
||||
(np.arctan(cfg["v_acc"]) * bl.feSlits.center1[1])
|
||||
/ bl.feSlits.center1[1]
|
||||
* bl.feSlits.center2[1]
|
||||
)
|
||||
|
||||
xgap = trxw - trxr
|
||||
ygap = tryt - tryb
|
||||
|
||||
pos["sldi_gapx"] = {"value": xgap}
|
||||
pos["sldi_gapy"] = {"value": ygap}
|
||||
|
||||
## Collimating Mirror
|
||||
obj_dist = bl.cm.center[1] # object distance
|
||||
beam_vs = 2 * obj_dist * np.tan(cfg["v_acc"]) # vertical size of beam after CM
|
||||
|
||||
# TRX
|
||||
if cfg["cm_stripe"] in bl.cm.surface:
|
||||
index = bl.cm.surface.index(cfg["cm_stripe"])
|
||||
else:
|
||||
raise ValueError(f"Requested stripe {cfg['cm_stripe']} not found in parameters!")
|
||||
cm_trx = -(bl.cm.limOptX[0][index] + bl.cm.limOptX[1][index]) / 2
|
||||
pos["cm_trx"] = {"value": cm_trx}
|
||||
|
||||
# TRY
|
||||
height = obj_dist * np.tan(cfg["v_acc"]) ** 2 * 1 / np.tan(cfg["cm_pitch"])
|
||||
pos["cm_try"] = {"value": height}
|
||||
|
||||
# Pitch
|
||||
pos["cm_rotx"] = {
|
||||
"value": -cfg["cm_pitch"] * 1e3
|
||||
} # invert and convert to mrad (same as EGU of rotx axis)
|
||||
|
||||
# Bending Radius
|
||||
radius = (
|
||||
2.0 * obj_dist / np.sin(cfg["cm_pitch"])
|
||||
) # Elements of modern X-ray Physics, page 108 ff.
|
||||
pos["cm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km
|
||||
|
||||
## Monochromator
|
||||
if cfg["mo1_mode"] == "Monochromatic":
|
||||
# Add 2x CM pitch to the bragg angle
|
||||
bragg = cfg["mo1_bragg"]
|
||||
elif cfg["mo1_mode"] == "Pinkbeam":
|
||||
# Align xtal surfaces parallel to beam
|
||||
bragg = 0
|
||||
else:
|
||||
raise ValueError("Monochromator mode not supported")
|
||||
pos["mo1_bragg_angle"] = {"value": bragg / np.pi * 180} # Bragg angle in deg
|
||||
|
||||
# TRY, Height
|
||||
l = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"])
|
||||
yhor = l * np.cos(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"]))
|
||||
yver = yhor * np.tan(2.0 * cfg["cm_pitch"])
|
||||
|
||||
if cfg["mo1_mode"] == "Monochromatic":
|
||||
beam_offset_mo1 = (
|
||||
l * np.sin(2.0 * (cfg["mo1_bragg"] + cfg["cm_pitch"])) - yver
|
||||
) # Resultat ist korrekt!
|
||||
elif cfg["mo1_mode"] == "Pinkbeam":
|
||||
beam_offset_mo1 = 0
|
||||
else:
|
||||
raise ValueError("Monochromator mode not supported")
|
||||
|
||||
def csc(a):
|
||||
return 1 / np.sin(a)
|
||||
|
||||
def cot(a):
|
||||
return 1 / np.tan(a)
|
||||
|
||||
# calculate height of center of first crystal surface
|
||||
f = bl.mo1.rotOffset # rotation offset, mm
|
||||
d = bl.mo1.heightOffset # xtal height offset, mm
|
||||
c = d * csc(cfg["mo1_bragg"]) - f * cot(cfg["mo1_bragg"])
|
||||
|
||||
# Calculate height of center of rotation
|
||||
b = np.sqrt(
|
||||
d**2 * csc(cfg["mo1_bragg"]) ** 2
|
||||
- 2 * d * f * cot(cfg["mo1_bragg"]) * csc(cfg["mo1_bragg"])
|
||||
+ f**2 * cot(cfg["mo1_bragg"]) ** 2
|
||||
+ f**2
|
||||
)
|
||||
h = np.cos(np.pi / 2 - np.arctan(f / c) - cfg["mo1_bragg"] - 2 * cfg["cm_pitch"]) * b
|
||||
h2 = ((bl.mo1.center[1] - bl.cm.center[1]) - np.sqrt(b**2 - h**2)) * np.tan(2 * cfg["cm_pitch"])
|
||||
height_mo1_real = (
|
||||
h + h2
|
||||
) # per design, the height should not change if the pitch of the CM is not changed!
|
||||
if cfg["mo1_mode"] == "Monochromatic":
|
||||
pass
|
||||
elif cfg["mo1_mode"] == "Pinkbeam":
|
||||
height_mo1_real = (
|
||||
height_mo1_real - 13
|
||||
) # Move down to let beam pass between both crystal without touching copper cooler
|
||||
else:
|
||||
raise ValueError("Monochromator mode not supported")
|
||||
pos["mo1_try"] = {"value": height_mo1_real}
|
||||
|
||||
# TRX, Crystal selection
|
||||
if cfg["mo1_mode"] == "Monochromatic":
|
||||
xtal = cfg["mo1_xtal"].translate(
|
||||
str.maketrans("", "", "()")
|
||||
) # Remove brackets from xtal name to conform with parameters
|
||||
if xtal in bl.mo1.xtal:
|
||||
index = bl.mo1.xtal.index(xtal)
|
||||
else:
|
||||
raise ValueError(f"Requested xtal {xtal} not found in parameters!")
|
||||
pos["mo1_trx"] = {"value": bl.mo1.xtalOffsetX[index]}
|
||||
else:
|
||||
pos["mo1_trx"] = {"value": 0}
|
||||
|
||||
diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono
|
||||
dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
|
||||
|
||||
## Slits 1
|
||||
d = bl.opSlits1.center[1] - bl.cm.center[1] - dz
|
||||
sl1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1
|
||||
pos["sl1_centery"] = {"value": sl1_beam_height}
|
||||
pos["sl1_gapy"] = {"value": beam_vs}
|
||||
|
||||
## Beam Monitor 1
|
||||
d = bl.opBM1.center[1] - bl.cm.center[1] - dz
|
||||
bm1_beam_height = d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1
|
||||
pos["bm1_try"] = {"value": bm1_beam_height}
|
||||
|
||||
## Focusing Mirror
|
||||
p = bl.fm.center[1]
|
||||
q = cfg["smpl"] - bl.fm.center[1]
|
||||
f = (p * q) / (p + q) # focal length
|
||||
|
||||
# Bender radius
|
||||
if cfg["fm_qy"] is None:
|
||||
radius = 2 * q / np.sin(cfg["fm_rotx"]) # ideal bending radius for focused beam
|
||||
else:
|
||||
radius = (
|
||||
2 * cfg["fm_qy"] / np.sin(cfg["fm_rotx"])
|
||||
) # ideal bending radius for unfocused beam
|
||||
pos["fm_bnd_radius"] = {"value": radius * 1e-6} # Convert to km
|
||||
|
||||
# Pitch
|
||||
d = bl.fm.center[1] - bl.cm.center[1] - dz
|
||||
fm_rotx = (
|
||||
2 * cfg["cm_pitch"] - cfg["fm_rotx"]
|
||||
) # calculate pitch in absolute values (according to horizontal plane)
|
||||
pos["fm_rotx"] = {
|
||||
"value": -fm_rotx * 1e3
|
||||
} # invert and convert to mrad (same as EGU of rotx axis)
|
||||
|
||||
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
|
||||
|
||||
# TRY
|
||||
if cfg["fm_stripe"] == "Rh (toroid)":
|
||||
r = bl.fm.r[0]
|
||||
h_cyl = bl.fm.hToroid[0]
|
||||
else: # PT toroid
|
||||
r = bl.fm.r[1]
|
||||
h_cyl = bl.fm.hToroid[1]
|
||||
width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"] * 1e-3)
|
||||
alpha = np.arccos(1 - width_beam**2 / (2 * r**2))
|
||||
h = r - (r * np.cos(alpha / 2))
|
||||
fm_beam_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"]
|
||||
fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1 - h_cyl + h / 2) * cfg[
|
||||
"fm_gain_height"
|
||||
]
|
||||
pos["fm_try"] = {"value": fm_height}
|
||||
|
||||
# TRX
|
||||
if cfg["fm_stripe"] == "Rh (toroid)":
|
||||
x_cyl = -bl.fm.xToroid[0]
|
||||
else:
|
||||
x_cyl = -bl.fm.xToroid[1]
|
||||
pos["fm_trx"] = {"value": x_cyl}
|
||||
|
||||
elif cfg["fm_stripe"] in ("Rh (flat)", "Pt (flat)"):
|
||||
|
||||
# TRY
|
||||
fm_height = (d * np.tan(2 * cfg["cm_pitch"]) + beam_offset_mo1) * cfg["fm_gain_height"]
|
||||
fm_beam_height = fm_height
|
||||
pos["fm_try"] = {"value": fm_height}
|
||||
|
||||
# TRX
|
||||
if cfg["fm_stripe"] == "Rh (flat)":
|
||||
x_flat = -bl.fm.xFlat[0]
|
||||
else:
|
||||
x_flat = -bl.fm.xFlat[1]
|
||||
pos["fm_trx"] = {"value": x_flat}
|
||||
|
||||
else:
|
||||
raise ValueError("FM Stripe selection not valid")
|
||||
|
||||
pos["fm_roty"] = {"value": 0}
|
||||
pos["fm_rotz"] = {"value": 0}
|
||||
|
||||
## Slits 2
|
||||
if hasattr(bl, "opSlits2"):
|
||||
d = bl.opSlits2.center[1] - bl.fm.center[1]
|
||||
sl2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
|
||||
pos["sl2_centery"] = {"value": sl2_beam_height}
|
||||
pos["sl2_gapy"] = {"value": beam_vs}
|
||||
|
||||
## Beam Monitor 2
|
||||
d = bl.opBM2.center[1] - bl.fm.center[1]
|
||||
bm2_beam_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
|
||||
pos["bm2_try"] = {"value": bm2_beam_height}
|
||||
|
||||
## Optical Table
|
||||
|
||||
if beamline == "x01da":
|
||||
# TRY
|
||||
d = bl.ehWindow.center[1] - bl.fm.center[1]
|
||||
ot_height = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
|
||||
pos["ot_try"] = {"value": ot_height}
|
||||
|
||||
# Pitch
|
||||
ot_pitch = -(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])
|
||||
pos["ot_rotx"] = {"value": ot_pitch * 1e3}
|
||||
|
||||
# TRZ ES1
|
||||
ot_es1_trz = cfg["smpl"]
|
||||
pos["ot_es1_trz"] = {"value": ot_es1_trz}
|
||||
|
||||
# ES0 exit window
|
||||
pos["es0wi_try"] = {
|
||||
"value": 5
|
||||
} # At 5mm, the middle of the window is 500 mm from the table (neutral position)
|
||||
else:
|
||||
# Exit window height
|
||||
d = bl.ehWindow.center[1] - bl.fm.center[1]
|
||||
es0wi_try = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
|
||||
pos["es0wi_try"] = {"value": es0wi_try}
|
||||
|
||||
# ES1 table height
|
||||
d = bl.es1.center[1] - bl.fm.center[1]
|
||||
es1_try = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
|
||||
pos["es1_try"] = {"value": es1_try}
|
||||
|
||||
# IC0 height
|
||||
d = bl.es1ic0.center[1] - bl.fm.center[1]
|
||||
es1ic0_try = (
|
||||
fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) - es1_try
|
||||
)
|
||||
pos["es1ic0_try"] = {"value": es1ic0_try}
|
||||
|
||||
# IC1 height
|
||||
d = bl.es1ic1.center[1] - bl.fm.center[1]
|
||||
es1ic1_try = (
|
||||
fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) - es1_try
|
||||
)
|
||||
pos["es1ic1_try"] = {"value": es1ic1_try}
|
||||
|
||||
# IC2 height
|
||||
d = bl.es1ic2.center[1] - bl.fm.center[1]
|
||||
es1ic2_try = (
|
||||
fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"])) - es1_try
|
||||
)
|
||||
pos["es1ic2_try"] = {"value": es1ic2_try}
|
||||
|
||||
# ES2 table height
|
||||
d = bl.es2.center[1] - bl.fm.center[1]
|
||||
es2_try = fm_beam_height - d * np.tan(-(2 * cfg["cm_pitch"] - 2 * cfg["fm_rotx"]))
|
||||
pos["es2_try"] = {"value": es2_try}
|
||||
|
||||
return pos
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Calculates the sideview coordinates based on a beamline config.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .. import parameters as bl
|
||||
from ..types import ConfigDict, DataDict
|
||||
|
||||
|
||||
def calc_sideview(cfg: ConfigDict) -> DataDict:
|
||||
"""
|
||||
Calculates the sideview coordinates based on a beamline config.
|
||||
|
||||
Args:
|
||||
cfg(ConfigDict): Dictionary with beamline config
|
||||
|
||||
Returns:
|
||||
DataDict: Sideview data
|
||||
"""
|
||||
|
||||
beam: DataDict = {"x": [], "y": []}
|
||||
|
||||
beam["x"] = []
|
||||
beam["y"] = []
|
||||
beam["x"].append(0) # Source
|
||||
beam["y"].append(bl.sourceHeight)
|
||||
beam["x"].append(bl.cm.center[1]) # CM
|
||||
beam["y"].append(bl.sourceHeight)
|
||||
if cfg["mo1_mode"] == "Monochromatic":
|
||||
diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono
|
||||
dy = diag * np.sin(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
|
||||
dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
|
||||
beam["x"].append(bl.mo1.center[1] - dz / 2) # Mono 1.1
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1])
|
||||
)
|
||||
beam["x"].append(bl.mo1.center[1] + dz / 2) # Mono 1.2
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.mo1.center[1] - dz / 2 - bl.cm.center[1])
|
||||
+ dy
|
||||
)
|
||||
beam["x"].append(bl.fm.center[1]) # FM
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz)
|
||||
+ dy
|
||||
)
|
||||
beam["x"].append(cfg["smpl"]) # Experiment
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz)
|
||||
+ dy
|
||||
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
|
||||
)
|
||||
elif cfg["mo1_mode"] == "Pinkbeam":
|
||||
beam["x"].append(bl.fm.center[1]) # FM
|
||||
beam["y"].append(
|
||||
bl.sourceHeight + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1])
|
||||
)
|
||||
beam["x"].append(cfg["smpl"]) # Experiment
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1])
|
||||
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
|
||||
)
|
||||
|
||||
return beam
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Calculates the surface coordinates based on a beamline config.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
|
||||
from .. import parameters as bl
|
||||
from ..types import ConfigDict, SurfaceDict
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def calc_surfaces(cfg: ConfigDict) -> SurfaceDict:
|
||||
"""
|
||||
Calculates the surface coordinates based on a beamline config.
|
||||
|
||||
Args:
|
||||
cfg(ConfigDict): Dictionary with beamline config
|
||||
|
||||
Returns:
|
||||
SurfaceDict: Surface data
|
||||
"""
|
||||
|
||||
out: SurfaceDict = {
|
||||
"cm": {"x": [], "y": []},
|
||||
"mo1_1": {"x": [], "y": []},
|
||||
"mo1_2": {"x": [], "y": []},
|
||||
"fm": {"x": [], "y": []},
|
||||
}
|
||||
|
||||
# Collimating mirror
|
||||
l = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) / np.sin(cfg["cm_pitch"])
|
||||
|
||||
w1 = 2 * (bl.cm.center[1] - l / 2) * np.tan(cfg["h_acc"])
|
||||
w2 = 2 * (bl.cm.center[1] + l / 2) * np.tan(cfg["h_acc"])
|
||||
|
||||
# index = bl.cm.surface.index(cfg["cm_stripe"])
|
||||
|
||||
cen = -cfg["cm_trx"]
|
||||
|
||||
out["cm"]["x"] = [cen - w1 / 2, cen - w2 / 2, cen + w2 / 2, cen + w1 / 2]
|
||||
out["cm"]["y"] = [-l / 2, l / 2, l / 2, -l / 2]
|
||||
|
||||
# Monochromator
|
||||
# calculate height of center of first crystal surface
|
||||
c = bl.mo1.heightOffset * 1 / np.sin(cfg["mo1_bragg"]) - bl.mo1.rotOffset * 1 / np.tan(
|
||||
cfg["mo1_bragg"]
|
||||
)
|
||||
e = bl.mo1.xtalGap[0] / np.tan(cfg["mo1_bragg"]) - c
|
||||
|
||||
xtal = cfg["mo1_xtal"].translate(
|
||||
str.maketrans("", "", "()")
|
||||
) # Remove brackets from xtal name to conform with parameters
|
||||
index = bl.mo1.xtal.index(xtal)
|
||||
|
||||
xtal_pos = bl.mo1.xtalOffsetX[index]
|
||||
xtal_length_1 = bl.mo1.xtalLength1[index]
|
||||
xtal_length_2 = bl.mo1.xtalLength2[index]
|
||||
|
||||
width_beam = 2 * bl.mo1.center[1] * np.tan(cfg["h_acc"])
|
||||
|
||||
height_beam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"])
|
||||
w = height_beam / np.sin(cfg["mo1_bragg"])
|
||||
|
||||
if cfg["mo1_mode"] == "Monochromatic":
|
||||
out["mo1_1"]["x"] = [
|
||||
xtal_pos - width_beam / 2,
|
||||
xtal_pos + width_beam / 2,
|
||||
xtal_pos + width_beam / 2,
|
||||
xtal_pos - width_beam / 2,
|
||||
]
|
||||
out["mo1_1"]["y"] = [
|
||||
xtal_length_1 / 2 - c - w / 2,
|
||||
xtal_length_1 / 2 - c - w / 2,
|
||||
xtal_length_1 / 2 - c + w / 2,
|
||||
xtal_length_1 / 2 - c + w / 2,
|
||||
]
|
||||
out["mo1_2"]["x"] = [
|
||||
xtal_pos - width_beam / 2,
|
||||
xtal_pos + width_beam / 2,
|
||||
xtal_pos + width_beam / 2,
|
||||
xtal_pos - width_beam / 2,
|
||||
]
|
||||
out["mo1_2"]["y"] = [
|
||||
-xtal_length_2 / 2 + e - w / 2,
|
||||
-xtal_length_2 / 2 + e - w / 2,
|
||||
-xtal_length_2 / 2 + e + w / 2,
|
||||
-xtal_length_2 / 2 + e + w / 2,
|
||||
]
|
||||
else: # Pinkbeam
|
||||
out["mo1_1"]["x"] = []
|
||||
out["mo1_1"]["y"] = []
|
||||
out["mo1_2"]["x"] = []
|
||||
out["mo1_2"]["y"] = []
|
||||
|
||||
if cfg["fm_stripe"] is None:
|
||||
return out
|
||||
# Focusing mirror
|
||||
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
|
||||
surface = bl.fm.surfaceToroid
|
||||
stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip()
|
||||
index = surface.index(stripe)
|
||||
r = bl.fm.r[index]
|
||||
else:
|
||||
surface = bl.fm.surfaceFlat
|
||||
stripe = re.sub(r"\s*\(.*?\)", "", cfg["fm_stripe"]).strip()
|
||||
index = surface.index(stripe)
|
||||
r = bl.fm.r[index]
|
||||
off = -cfg["fm_trx"]
|
||||
|
||||
width_beam = 2 * bl.fm.center[1] * np.tan(cfg["h_acc"])
|
||||
|
||||
if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"):
|
||||
|
||||
l = height_beam / np.sin(cfg["fm_rotx"])
|
||||
alpha = np.arccos(1 - width_beam**2 / (2 * r**2))
|
||||
h = r - (r * np.cos(alpha / 2))
|
||||
z = h / np.tan(cfg["fm_rotx"])
|
||||
|
||||
x = [off - width_beam / 2, off - width_beam / 2]
|
||||
y = [l / 2 - z / 2, -l / 2 - z / 2]
|
||||
|
||||
res = 20
|
||||
x_elipse = np.linspace(0, np.pi, res)
|
||||
y_elipse = np.linspace(0, np.pi, res)
|
||||
x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse]
|
||||
y_elipse = [width_beam * np.sin(i) * z / width_beam - l / 2 - z / 2 for i in y_elipse]
|
||||
|
||||
x.extend(x_elipse)
|
||||
y.extend(y_elipse)
|
||||
|
||||
x.extend([off + width_beam / 2, off + width_beam / 2])
|
||||
y.extend([-l / 2 - z / 2, l / 2 - z / 2])
|
||||
|
||||
res = 50
|
||||
x_elipse = np.linspace(np.pi, 0, res)
|
||||
y_elipse = np.linspace(np.pi, 0, res)
|
||||
x_elipse = [-width_beam / 2 * np.cos(i) + off for i in x_elipse]
|
||||
y_elipse = [width_beam * np.sin(i) * z / width_beam + l / 2 - z / 2 for i in y_elipse]
|
||||
|
||||
x.extend(x_elipse)
|
||||
y.extend(y_elipse)
|
||||
|
||||
out["fm"]["x"] = x
|
||||
out["fm"]["y"] = y
|
||||
|
||||
else: # flat surface, no toroid
|
||||
l = height_beam / np.sin(cfg["fm_rotx"])
|
||||
|
||||
w1 = 2 * (bl.fm.center[1] - l / 2) * np.tan(cfg["h_acc"])
|
||||
w2 = 2 * (bl.fm.center[1] + l / 2) * np.tan(cfg["h_acc"])
|
||||
|
||||
out["fm"]["x"] = [off - w1 / 2, off + w1 / 2, off + w2 / 2, off - w2 / 2]
|
||||
out["fm"]["y"] = [-l / 2, -l / 2, l / 2, l / 2]
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,519 @@
|
||||
"""
|
||||
Various calculations for the digital twin
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Literal, cast
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from scipy.interpolate import UnivariateSpline
|
||||
from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm
|
||||
|
||||
from .. import parameters as bl
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
H = 6.62606957e-34
|
||||
E = 1.602176634e-19
|
||||
C = 299792458
|
||||
RE = 2.8179e-15
|
||||
|
||||
|
||||
def sldi_gap_to_acc(sldi_gapx: float, sldi_gapy: float) -> tuple[float, float]:
|
||||
"""
|
||||
Calculate the slits acceptance based on the gap values
|
||||
|
||||
Args:
|
||||
sldi_gapx(float): GAPX value of the slits in mm
|
||||
sldi_gapy(float): GAPY value of the slits in mm
|
||||
|
||||
Returns:
|
||||
tuple[float, float]: Horizontal and vertical acceptance in rad
|
||||
"""
|
||||
d1 = bl.feSlits.center1[1]
|
||||
d2 = bl.feSlits.center2[1]
|
||||
h_acc = np.tan(sldi_gapx / (d2 + d1))
|
||||
v_acc = np.tan(sldi_gapy / (d2 + d1))
|
||||
return h_acc, v_acc
|
||||
|
||||
|
||||
def cm_trx_to_stripe(cm_trx: float) -> str | None:
|
||||
"""
|
||||
Based on the trx value of the collimating mirror, return
|
||||
the correct stripe
|
||||
|
||||
Args:
|
||||
cm_trx(float): Collimating mirror trx value
|
||||
|
||||
Returns
|
||||
str | None: Stripe of the mirror, None if not found
|
||||
"""
|
||||
cm_stripe = None
|
||||
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
|
||||
if low <= cm_trx <= high:
|
||||
cm_stripe = name
|
||||
return cm_stripe
|
||||
|
||||
|
||||
def cm_stripe_to_trx(cm_stripe: str) -> float | None:
|
||||
"""
|
||||
Based on the stripe of the collimating mirror, return
|
||||
the trx value
|
||||
|
||||
Args:
|
||||
cm_stripe(str): Stripe of the collimating mirror
|
||||
|
||||
Returns:
|
||||
float | None: TRX value of the stripe. None if not found
|
||||
"""
|
||||
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
|
||||
if cm_stripe == name:
|
||||
return -(low + high) / 2
|
||||
return None
|
||||
|
||||
|
||||
def fm_trx_to_stripe(fm_trx: float) -> str | None:
|
||||
"""
|
||||
Based on the trx value of the focusing mirror, return
|
||||
the correct stripe
|
||||
|
||||
Args:
|
||||
fm_trx(float): focusing mirror trx value
|
||||
|
||||
Returns
|
||||
str | None: Stripe of the mirror, None if not found
|
||||
"""
|
||||
fm_stripe = None
|
||||
if hasattr(bl.fm, "surfaceFlat"):
|
||||
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
|
||||
if low <= fm_trx <= high:
|
||||
fm_stripe = name + " (flat)"
|
||||
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
|
||||
if low <= fm_trx <= high:
|
||||
fm_stripe = name + " (toroid)"
|
||||
return fm_stripe
|
||||
|
||||
|
||||
def fm_stripe_to_trx(fm_stripe: str) -> float | None:
|
||||
"""
|
||||
Based on the stripe of the focusing mirror, return
|
||||
the trx value
|
||||
|
||||
Args:
|
||||
fm_stripe(str): Stripe of the focusing mirror
|
||||
|
||||
Returns:
|
||||
float | None: TRX value of the stripe. None if not found
|
||||
"""
|
||||
if hasattr(bl.fm, "surfaceFlat"):
|
||||
for name, low, high in zip(bl.fm.surfaceFlat, bl.fm.limOptXFlat[1], bl.fm.limOptXFlat[0]):
|
||||
if fm_stripe == name + " (flat)":
|
||||
return (low + high) / 2
|
||||
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
|
||||
if fm_stripe == name + " (toroid)":
|
||||
return -(low + high) / 2
|
||||
return None
|
||||
|
||||
|
||||
def mo1_energy_resolution(xtal: Literal["Si111", "Si311"], energy: float) -> float:
|
||||
"""
|
||||
Calculate the energy resolution of the monochromator
|
||||
|
||||
Args:
|
||||
xtal(str): Xtal name. "Si111" or "Si311"
|
||||
energy(float): Energy in eV
|
||||
|
||||
Returns:
|
||||
float: Energy resolution in eV
|
||||
"""
|
||||
index = bl.mo1.xtal.index(xtal)
|
||||
crystal = bl.mo1.material1[index]
|
||||
|
||||
dtheta = np.linspace(-30, 90, 601)
|
||||
theta = crystal.get_Bragg_angle(energy) + dtheta * 1e-6
|
||||
refl = np.abs(crystal.get_amplitude(energy, np.sin(theta))[0]) ** 2 # single crystal
|
||||
|
||||
refl2 = refl**2 # DCM with parallel crystals
|
||||
|
||||
# FWHM of the DCM curve
|
||||
spline = UnivariateSpline(dtheta, refl2 - refl2.max() / 2, s=0)
|
||||
roots = cast(np.ndarray, spline.roots())
|
||||
r1, r2 = float(roots[0]), float(roots[1])
|
||||
fwhm_rad = (r2 - r1) * 1e-6 # µrad → rad
|
||||
|
||||
# Energy resolution
|
||||
theta_b = crystal.get_Bragg_angle(energy)
|
||||
de_over_e = fwhm_rad / np.tan(theta_b)
|
||||
de = de_over_e * energy
|
||||
|
||||
# logger.info(f"DCM FWHM : {r2-r1:.2f} µrad")
|
||||
# logger.info(f"ΔE/E : {dE_over_E:.2e}")
|
||||
# logger.info(f"ΔE : {dE:.3f} eV at {E} eV")
|
||||
|
||||
return de
|
||||
|
||||
|
||||
def cm_reflectivity(cm_stripe: str, cm_pitch: float, energy: float) -> float:
|
||||
"""
|
||||
Calculate the reflectivity of the mirror stripe based
|
||||
on the pitch and energy.
|
||||
|
||||
Args:
|
||||
cm_stripe(str): Mirror stripe
|
||||
cm_pitch(float): Pitch of the mirror (beam incidence angle)
|
||||
energy(float): Energy of the beam in eV
|
||||
|
||||
Returns:
|
||||
float: Reflectivity [0-1]
|
||||
"""
|
||||
if cm_stripe is None:
|
||||
return np.nan
|
||||
index = bl.cm.surface.index(cm_stripe)
|
||||
rs, _ = bl.cm.material[index].get_amplitude(energy, np.sin(cm_pitch))[0:2]
|
||||
refl = abs(rs) ** 2
|
||||
return refl
|
||||
|
||||
|
||||
def fm_reflectivity(fm_stripe: str, fm_pitch: float, energy: float) -> float:
|
||||
"""
|
||||
Calculate the reflectivity of the mirror stripe based
|
||||
on the pitch and energy.
|
||||
|
||||
Args:
|
||||
cm_stripe(str): Mirror stripe
|
||||
cm_pitch(float): Pitch of the mirror (beam incidence angle)
|
||||
energy(float): Energy of the beam in eV
|
||||
|
||||
Returns:
|
||||
float: Reflectivity [0-1]
|
||||
"""
|
||||
if fm_stripe is None:
|
||||
return np.nan
|
||||
if fm_stripe in ("Rh (toroid)", "Pt (toroid)"):
|
||||
surface = bl.fm.surfaceToroid
|
||||
material = bl.fm.materialToroid
|
||||
stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip()
|
||||
index = surface.index(stripe)
|
||||
else:
|
||||
surface = bl.fm.surfaceFlat
|
||||
material = bl.fm.materialFlat
|
||||
stripe = re.sub(r"\s*\(.*?\)", "", fm_stripe).strip()
|
||||
index = surface.index(stripe)
|
||||
rs, _ = material[index].get_amplitude(energy, np.sin(fm_pitch))[0:2]
|
||||
refl = abs(rs) ** 2
|
||||
return refl
|
||||
|
||||
|
||||
def mo1_bragg_angle(
|
||||
mo_mode: Literal["Monochromatic", "Pinkbeam"], d_spacing: float, energy: float, cm_pitch: float
|
||||
) -> tuple[float, float]:
|
||||
"""
|
||||
Calculate the bragg angle of the monochromator.
|
||||
Corrects for the collimating mirror pitch.
|
||||
|
||||
Args:
|
||||
mo_mode(str): Monochromator mode. "Monochromatic" or "Pinkbeam"
|
||||
d_spacing(float): D-spacing of the crystal in Angstrom
|
||||
energy(float): Energy of the beam in eV
|
||||
cm_pitch(float): Pitch of collimating mirror in rad
|
||||
|
||||
Returns:
|
||||
tuple[float, float]: Bragg angle and corrected bragg angle
|
||||
"""
|
||||
wl = C * H / (E * energy)
|
||||
val = wl / (2 * d_spacing * 1e-10)
|
||||
bragg_angle = 0
|
||||
if val > -1 and val < 1:
|
||||
bragg_angle = np.asin(val)
|
||||
if mo_mode == "Monochromatic":
|
||||
# Add 2x CM pitch to the bragg angle
|
||||
bragg_angle_cor = (2 * cm_pitch) + bragg_angle
|
||||
else:
|
||||
# Align xtal surfaces parallel to beam
|
||||
bragg_angle_cor = 2 * cm_pitch
|
||||
return bragg_angle, bragg_angle_cor
|
||||
|
||||
|
||||
def fm_ideal_pitch(
|
||||
fm_focus: Literal["Defocused", "Focused", "Manual"],
|
||||
fm_stripe: str,
|
||||
smpl: float,
|
||||
sldi_hacc: float | None = None,
|
||||
sldi_vacc: float | None = None,
|
||||
fm_focx: float | None = None,
|
||||
fm_focy: float | None = None,
|
||||
) -> tuple[float, float | None]:
|
||||
"""
|
||||
Calculates the ideal pitch for the focusing mirror depending on the
|
||||
focusing strategy.
|
||||
If "Defocused" is chosed, sldi_hacc, sldi_vacc, fm_focx and fm_focy
|
||||
must be provided.
|
||||
|
||||
Args:
|
||||
fm_focus(str): Focus strategy. "Defocused", "Focused" or "Manual
|
||||
fm_stripe(str): Mirror stripe
|
||||
smpl(float): Sample position in mm from source
|
||||
sldi_hacc(float): Horizontal acceptance of frontend slits. Defaults to None
|
||||
sldi_vacc(float): Vertical acceptance of frontend slits. Defaults to None
|
||||
fm_focx(float): Requested horizontal spot size in mm. Defaults to None
|
||||
fm_focy(float): Requested vertical spot size in mm. Defaults to None
|
||||
|
||||
Returns:
|
||||
tuple[float, float | None]: Pitch of mirror in rad, qy in mm
|
||||
"""
|
||||
|
||||
# logger.info("Calculate pitch and qy now...")
|
||||
# logger.info(f"sldi_hacc: {sldi_hacc}")
|
||||
# logger.info(f"sldi_vacc: {sldi_vacc}")
|
||||
# logger.info(f"fm_stripe: {fm_stripe}")
|
||||
# logger.info(f"smpl: {smpl}")
|
||||
p_cm = bl.cm.center[1] # posCM
|
||||
p = bl.fm.center[1] # posFM
|
||||
q = smpl - bl.fm.center[1] # dist posFM to posEX
|
||||
if fm_focus == "Defocused":
|
||||
assert sldi_hacc is not None, "sldi_hacc must be provided for Defocused mode"
|
||||
assert sldi_vacc is not None, "sldi_vacc must be provided for Defocused mode"
|
||||
assert fm_focx is not None, "fm_focx must be provided for Defocused mode"
|
||||
assert fm_focy is not None, "fm_focy must be provided for Defocused mode"
|
||||
a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror
|
||||
# logger.info(f"a: {a}")
|
||||
# logger.info(f"sldi_hacc: {sldi_hacc}")
|
||||
# logger.info(f"bl.fm.center[1]: {bl.fm.center[1]}")
|
||||
# logger.info(f"p: {p}")
|
||||
# logger.info(f"q: {q}")
|
||||
b = (
|
||||
2 * np.tan(sldi_vacc) * bl.cm.center[1]
|
||||
) # Beam height at focusing mirror (collimated beam)
|
||||
x = fm_focx
|
||||
# logger.info(f"x: {x}")
|
||||
x = 0.098821 * x**2 + 0.512344 * x # polynom to correct for spot size
|
||||
# logger.info(f"x (corrected): {x}")
|
||||
y = fm_focy
|
||||
y = 3.183562 * y**2 + 1.258364 * y # polynom to correct for spot size
|
||||
qx = q + x * p / a
|
||||
qy = q + y * p_cm / b
|
||||
f = (p * qx) / (p + qx) # focal length
|
||||
# logger.info(f"qx: {qx}")
|
||||
# logger.info(f"f: {f}")
|
||||
else: # Calculate for focused beam on sample in "manual" and "focused" mode
|
||||
qy = None
|
||||
f = (p * q) / (p + q) # focal length
|
||||
pitch = 0
|
||||
if "Rh" in fm_stripe:
|
||||
pitch = np.arcsin(bl.fm.r[0] / (2 * f)) # ideal pitch for FM
|
||||
if "Pt" in fm_stripe:
|
||||
pitch = np.arcsin(bl.fm.r[1] / (2 * f)) # ideal pitch for FM
|
||||
# logger.info(f"fm_pitch: {pitch}")
|
||||
# logger.info(f"qy: {qy}")
|
||||
return pitch, qy
|
||||
|
||||
|
||||
def calc_beamsize(
|
||||
sldi_hacc: float,
|
||||
sldi_vacc: float,
|
||||
fm_stripe: str,
|
||||
fm_pitch: float,
|
||||
fm_radius: float,
|
||||
smpl: float,
|
||||
) -> tuple[float, float | None]:
|
||||
"""
|
||||
Calculate the resulting beamsize according to the input parameters
|
||||
|
||||
Args:
|
||||
sldi_hacc(float): Horizontal acceptance of frontend slits
|
||||
sldi_vacc(float): Vertical acceptance of frontend slits
|
||||
fm_stripe(str): Mirror stripe
|
||||
fm_pitch(float): Focusing mirror pitch in rad
|
||||
fm_radius(float): Focusing mirror bender radius in m
|
||||
smpl(float): Sample position in mm from source
|
||||
|
||||
Returns:
|
||||
tuple[float, float | None]: horizontal spot size, vertical spot size, both in mm
|
||||
"""
|
||||
|
||||
# logger.info("Calculate beamsize now...")
|
||||
# logger.info(f"sldi_hacc: {sldi_hacc}")
|
||||
# logger.info(f"sldi_vacc: {sldi_vacc}")
|
||||
# logger.info(f"fm_stripe: {fm_stripe}")
|
||||
# logger.info(f"fm_pitch: {fm_pitch}")
|
||||
# logger.info(f"fm_radius: {fm_radius}")
|
||||
# logger.info(f"smpl: {smpl}")
|
||||
p_cm = bl.cm.center[1] # posCM
|
||||
p = bl.fm.center[1] # posFM
|
||||
q = smpl - bl.fm.center[1] # dist posFM to posEX
|
||||
qy = fm_radius * np.sin(fm_pitch) / 2
|
||||
a = 2 * np.tan(sldi_hacc) * bl.fm.center[1] # Beam width at focusing mirror
|
||||
b = 2 * np.tan(sldi_vacc) * bl.cm.center[1] # Beam height at focusing mirror (collimated beam)
|
||||
f = 0
|
||||
if "Rh" in fm_stripe:
|
||||
f = bl.fm.r[0] / (2 * np.sin(fm_pitch))
|
||||
if "Pt" in fm_stripe:
|
||||
f = bl.fm.r[1] / (2 * np.sin(fm_pitch))
|
||||
qx = p * f / (p - f)
|
||||
x = a * (qx - q) / p
|
||||
y = b * (qy - q) / p_cm
|
||||
# Change this | to a plus if calculation is not correct
|
||||
fm_focx = -4 * (64043 - 125000 * np.sqrt(0.26249637 + 0.395284 * x)) / 98821
|
||||
# Change this | to a plus if calculation is not correct
|
||||
fm_focy = -1 * (314591 - 250000 * np.sqrt(1.58347995 + 12.734248 * y)) / 1591781
|
||||
# logger.info(f"f: {f}")
|
||||
# logger.info(f"qx: {qx}")
|
||||
# logger.info(f"qy: {qy}")
|
||||
# logger.info(f"fm_focx: {fm_focx}")
|
||||
# logger.info(f"fm_focy: {fm_focy}")
|
||||
return fm_focx, fm_focy
|
||||
|
||||
|
||||
def cm_critical_angle(cm_stripe: Literal["Si", "Pt", "Rh"], energy) -> float:
|
||||
"""
|
||||
Calculate the critical angle of the mirror stripe
|
||||
|
||||
Args:
|
||||
cm_stripe(str): Mirror stripe. "Si", "Pt" or "Rh"
|
||||
energy(float): Energy in eV
|
||||
|
||||
Returns:
|
||||
float: Critical angle in rad
|
||||
"""
|
||||
if cm_stripe == "Si":
|
||||
stripe = bl.stripeSi
|
||||
elif cm_stripe == "Pt":
|
||||
stripe = bl.stripePt
|
||||
else:
|
||||
stripe = bl.stripeRh
|
||||
w = CHeVcm / 100 / energy # convert energy [eV] to wavelength [m]
|
||||
f1 = stripe.elements[0].Z + np.real(stripe.elements[0].get_f1f2(energy))
|
||||
number_density = stripe.rho * 1e3 * AVOGADRO / (stripe.elements[0].mass / 1e3)
|
||||
critical_angle = np.sqrt(number_density * RE * w**2 * f1 / np.pi)
|
||||
return critical_angle
|
||||
|
||||
|
||||
def mirror_surface_geometries(
|
||||
mirror: Literal["cm", "fm_toroid", "fm_flat"],
|
||||
) -> dict[str, tuple[float, float, float, float]]:
|
||||
"""
|
||||
Return the mirror stripe geometries
|
||||
|
||||
Args:
|
||||
mirror(str): Mirror. "cm", "fm_toroid" or "fm_flat"
|
||||
|
||||
Returns:
|
||||
dict[str, tuple[float, float, float, float]]: Dictionary mapping surface
|
||||
names to tuples of (x, y, width, height).
|
||||
"""
|
||||
if mirror == "cm":
|
||||
surface = bl.cm.surface
|
||||
lim_opt_x = bl.cm.limOptX
|
||||
lim_opt_y = bl.cm.limOptY
|
||||
elif mirror == "fm_toroid":
|
||||
surface = bl.fm.surfaceToroid
|
||||
lim_opt_x = bl.fm.limOptXToroid
|
||||
lim_opt_y = bl.fm.limOptYToroid
|
||||
elif mirror == "fm_flat":
|
||||
surface = bl.fm.surfaceFlat
|
||||
lim_opt_x = bl.fm.limOptXFlat
|
||||
lim_opt_y = bl.fm.limOptYFlat
|
||||
else:
|
||||
raise ValueError(f"Requested mirror {mirror} not available!")
|
||||
geom = {}
|
||||
for sf, lx, hx, ly, hy in zip(surface, lim_opt_x[0], lim_opt_x[1], lim_opt_y[0], lim_opt_y[1]):
|
||||
geom[sf] = (lx, ly, hx - lx, hy - ly)
|
||||
return geom
|
||||
|
||||
|
||||
def mo_surface_geometries(
|
||||
mo: Literal["mo1"], plane: Literal[0, 1]
|
||||
) -> dict[str, tuple[float, float, float, float]]:
|
||||
"""
|
||||
Return the monochromator xtal geometries
|
||||
|
||||
Args:
|
||||
mo(str): Monochromator. Only "mo1" implemented
|
||||
plane(int): Surface of xtal. 0 and 1 (First and second)
|
||||
|
||||
Returns:
|
||||
dict[str, tuple[float, float, float, float]]: Dictionary mapping surface
|
||||
names to tuples of (x, y, width, height).
|
||||
"""
|
||||
if mo == "mo1":
|
||||
xtal = bl.mo1.xtal
|
||||
xtal_width = bl.mo1.xtalWidth
|
||||
xtal_offset_x = bl.mo1.xtalOffsetX
|
||||
if plane == 0:
|
||||
xtal_length = bl.mo1.xtalLength1
|
||||
else:
|
||||
xtal_length = bl.mo1.xtalLength2
|
||||
else:
|
||||
return {}
|
||||
geom = {}
|
||||
for sf, w, offx, length in zip(xtal, xtal_width, xtal_offset_x, xtal_length):
|
||||
geom[sf] = (offx - w / 2, -length / 2, w, length)
|
||||
return geom
|
||||
|
||||
|
||||
def wall_geometries() -> list[list[float]]:
|
||||
"""
|
||||
Return the wall geometries
|
||||
|
||||
Returns:
|
||||
list[list[float]]: List of [x, y, width, height] geometry values for each wall.
|
||||
"""
|
||||
geom = []
|
||||
if not hasattr(bl, "walls"):
|
||||
return geom
|
||||
for i, _ in enumerate(bl.walls.start):
|
||||
geom.append(
|
||||
[
|
||||
bl.walls.start[i],
|
||||
bl.walls.height[i][0],
|
||||
bl.walls.end[i] - bl.walls.start[i],
|
||||
bl.walls.height[i][1] - bl.walls.height[i][0],
|
||||
]
|
||||
)
|
||||
return geom
|
||||
|
||||
|
||||
def pipe_geometries() -> list[dict[str, np.ndarray]]:
|
||||
"""
|
||||
Return the wall geometries
|
||||
|
||||
Returns:
|
||||
list[dict[str, np.ndarray]]: List of dictionaries with keys "x" and "y",
|
||||
each containing a numpy array of two float values representing
|
||||
the start and end coordinates of the pipe top and bottom edges.
|
||||
"""
|
||||
pipes = []
|
||||
if not hasattr(bl, "vacuum_pipes"):
|
||||
return pipes
|
||||
for i, _ in enumerate(bl.vacuum_pipes.center):
|
||||
top = bl.vacuum_pipes.center[i] + bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight
|
||||
bottom = bl.vacuum_pipes.center[i] - bl.vacuum_pipes.diameter[i] / 2 + bl.sourceHeight
|
||||
pipes.append(
|
||||
{
|
||||
"x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
|
||||
"y": np.array([top, top]),
|
||||
}
|
||||
)
|
||||
pipes.append(
|
||||
{
|
||||
"x": np.array([bl.vacuum_pipes.start[i], bl.vacuum_pipes.end[i]]),
|
||||
"y": np.array([bottom, bottom]),
|
||||
}
|
||||
)
|
||||
return pipes
|
||||
|
||||
|
||||
def table_to_smpl_pos(table: str) -> float:
|
||||
"""
|
||||
Return the sample position based on the table name.
|
||||
|
||||
Args:
|
||||
table (str): Table name, e.g. ES1 or ES2
|
||||
"""
|
||||
|
||||
if table == bl.es1.name:
|
||||
return bl.es1.center[1]
|
||||
if table == bl.es2.name:
|
||||
return bl.es2.center[1]
|
||||
raise ValueError(f"Table {table} not found in beamline parameter file")
|
||||
@@ -0,0 +1,985 @@
|
||||
"""
|
||||
Digital Twin: Custom BEC widget to support the beamline alignment.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Literal, cast
|
||||
|
||||
import numpy as np
|
||||
import yaml
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme, get_accent_colors
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import Qt, QTimer
|
||||
from qtpy.QtGui import QFont
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QStyle,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .beamline import get_beamline_id
|
||||
from .calculations.calc_positions import calc_positions
|
||||
from .calculations.calc_sideview import calc_sideview
|
||||
from .calculations.calc_surfaces import calc_surfaces
|
||||
from .calculations.calc_varia import (
|
||||
calc_beamsize,
|
||||
cm_critical_angle,
|
||||
cm_reflectivity,
|
||||
cm_stripe_to_trx,
|
||||
cm_trx_to_stripe,
|
||||
fm_ideal_pitch,
|
||||
fm_reflectivity,
|
||||
fm_stripe_to_trx,
|
||||
fm_trx_to_stripe,
|
||||
mo1_bragg_angle,
|
||||
mo1_energy_resolution,
|
||||
sldi_gap_to_acc,
|
||||
table_to_smpl_pos,
|
||||
)
|
||||
from .panels.input_panel import InputPanel
|
||||
from .panels.mover_panel import MoverPanel
|
||||
from .panels.plots import SideviewPlot, SurfacePlots
|
||||
from .panels.settings_panel import SettingsPanel
|
||||
from .types import ConfigDict
|
||||
from .widgets.qt_widgets import ComboBox, InputNumberField
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
OFFSET_FILE_X01DA = Path(__file__).with_name("x01da_offsets.yaml")
|
||||
OFFSET_FILE_X10DA = Path(__file__).with_name("x10da_offsets.yaml")
|
||||
|
||||
|
||||
class DigitalTwin(BECWidget, QWidget):
|
||||
"""
|
||||
Main widget of Digital Twin
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "lightbulb"
|
||||
|
||||
def __init__(self, *arg, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
self.beamline = get_beamline_id()
|
||||
# Debugging, override beamline!
|
||||
# self.beamline = BeamlineId.X10DA
|
||||
|
||||
self.offset_file = Path()
|
||||
match self.beamline:
|
||||
case "x01da":
|
||||
self.offset_file = OFFSET_FILE_X01DA
|
||||
case "x10da":
|
||||
self.offset_file = OFFSET_FILE_X10DA
|
||||
|
||||
# Check if devices are all in config
|
||||
self.check_bec_config()
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.check_bec_config, MessageEndpoints.device_config_update()
|
||||
)
|
||||
|
||||
logger.info(f"Start Digital Twin with beamline {self.beamline} and all devices available")
|
||||
|
||||
self.content_widget = QWidget(self)
|
||||
self.root_layout = QHBoxLayout(self.content_widget)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.root_layout.setSpacing(6)
|
||||
|
||||
self.input_widget = QWidget()
|
||||
self.input_layout = QVBoxLayout(self.input_widget)
|
||||
self.input_layout.setContentsMargins(4, 4, 4, 4)
|
||||
self.input_layout.setSpacing(6)
|
||||
self.input = InputPanel(self.beamline)
|
||||
self.settings = SettingsPanel()
|
||||
self.input_layout.addWidget(self.input)
|
||||
self.input_layout.addWidget(self.settings)
|
||||
self.input_layout.addStretch()
|
||||
|
||||
self.plot_widget = QWidget()
|
||||
self.plot_layout = QVBoxLayout(self.plot_widget)
|
||||
self.plot_layout.setContentsMargins(4, 4, 4, 4)
|
||||
self.plot_layout.setSpacing(6)
|
||||
self.sideview_plot = SideviewPlot()
|
||||
self.surface_plots = SurfacePlots(self.beamline)
|
||||
self.plot_layout.addWidget(self.sideview_plot, stretch=1)
|
||||
self.plot_layout.addWidget(self.surface_plots, stretch=1)
|
||||
self.plot_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.mover = MoverPanel(self.beamline, self.dev)
|
||||
|
||||
self.input_scroll = self._scroll_area(self.input_widget, min_width=320, max_width=360)
|
||||
self.mover_scroll = self._scroll_area(self.mover, min_width=380, max_width=460)
|
||||
|
||||
self.root_layout.addWidget(self.input_scroll)
|
||||
self.root_layout.addWidget(self.plot_widget, stretch=1)
|
||||
self.root_layout.addWidget(self.mover_scroll)
|
||||
widget_layout = self.layout()
|
||||
if widget_layout is None:
|
||||
widget_layout = QVBoxLayout(self)
|
||||
widget_layout.setContentsMargins(0, 0, 0, 0)
|
||||
widget_layout.setSpacing(0)
|
||||
widget_layout.addWidget(self.content_widget)
|
||||
self.setWindowTitle("Digital Twin")
|
||||
self.resize(1450, 950)
|
||||
|
||||
self.input.energy.value_changed_connect(self.calc_assistant)
|
||||
self.input.sldi_hacc.value_changed_connect(self.calc_assistant)
|
||||
self.input.sldi_vacc.value_changed_connect(self.calc_assistant)
|
||||
self.input.cm_stripe.activated_connect(self.calc_assistant)
|
||||
self.input.cm_pitch.value_changed_connect(self.calc_assistant)
|
||||
self.input.mo1_mode.activated_connect(self.calc_assistant)
|
||||
self.input.mo1_xtal.activated_connect(self.calc_assistant)
|
||||
self.input.fm_stripe.activated_connect(self.calc_assistant)
|
||||
self.input.fm_focus.activated_connect(self.calc_assistant)
|
||||
self.input.fm_rotx.value_changed_connect(self.calc_assistant)
|
||||
self.input.fm_focx.value_changed_connect(self.calc_assistant)
|
||||
self.input.fm_focy.value_changed_connect(self.calc_assistant)
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
self.input.smpl.value_changed_connect(self.calc_assistant)
|
||||
case ComboBox():
|
||||
self.input.smpl.activated_connect(self.calc_assistant)
|
||||
|
||||
self.input.adapt_reality.clicked_connect(self.adapt_reality)
|
||||
self.settings.load_offsets.clicked_connect(self.load_offsets)
|
||||
self.settings.show_offsets.clicked_connect(self.show_offsets)
|
||||
|
||||
self.bragg_angle = 0.0
|
||||
self.qy = 0.0
|
||||
self.offsets = {}
|
||||
|
||||
# Initialize all values
|
||||
self.load_offsets(recalculate=False)
|
||||
self.calc_assistant(identifier="init")
|
||||
self.adapt_reality()
|
||||
|
||||
# Timer: update reality plots every 1 second
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setInterval(1000)
|
||||
self._timer.timeout.connect(self.calc_reality)
|
||||
self._timer.start()
|
||||
|
||||
@staticmethod
|
||||
def _scroll_area(widget: QWidget, min_width: int, max_width: int) -> QScrollArea:
|
||||
"""Wrap a side panel in a compact vertical scroll area."""
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding)
|
||||
scroll.setWidget(widget)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
scroll.setMinimumWidth(min_width)
|
||||
scroll.setMaximumWidth(max_width)
|
||||
scroll.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
||||
return scroll
|
||||
|
||||
def apply_theme(self, theme: Literal["dark", "light"]):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (str): Theme, either "dark" or "light"
|
||||
"""
|
||||
self.sideview_plot.apply_theme(theme)
|
||||
self.surface_plots.apply_theme(theme)
|
||||
self.mover.apply_theme(theme)
|
||||
|
||||
@SafeSlot()
|
||||
def check_bec_config(self, *args):
|
||||
"""
|
||||
Checks the BEC config and opens a window if not all necessary
|
||||
devices are loaded in the config. If called from a slot from
|
||||
BEC dispatcher whenever there is a config update, stop the timer
|
||||
that updates the plot in the background.
|
||||
"""
|
||||
reload_config = (args[0] if args else {}).get("action") == "reload"
|
||||
if reload_config:
|
||||
self._timer.stop()
|
||||
devices = [
|
||||
"abs",
|
||||
"sldi_gapx",
|
||||
"sldi_gapy",
|
||||
"cm_trx",
|
||||
"cm_try",
|
||||
"cm_bnd",
|
||||
"cm_rotx",
|
||||
"mo1_bragg",
|
||||
"mo1_trx",
|
||||
"mo1_try",
|
||||
"sl1_centery",
|
||||
"sl1_gapy",
|
||||
"bm1_try",
|
||||
"fm_trx",
|
||||
"fm_try",
|
||||
"fm_bnd",
|
||||
"fm_rotx",
|
||||
"fm_roty",
|
||||
"fm_rotz",
|
||||
"bm2_try",
|
||||
"es0wi_try",
|
||||
]
|
||||
if self.beamline == "x01da": # X01DA specific devices
|
||||
devices.extend(
|
||||
[
|
||||
"cm_bnd_radius",
|
||||
"fm_bnd_radius",
|
||||
"sl2_centery",
|
||||
"sl2_gapy",
|
||||
"ot_try",
|
||||
"ot_es1_trz",
|
||||
]
|
||||
)
|
||||
if self.beamline == "x10da": # X10DA specific devices
|
||||
devices.extend(["mo1_rotx", "es1_try", "es1ic0_try", "es1ic1_try", "es1ic2_try"])
|
||||
|
||||
while True:
|
||||
missing = [d for d in devices if d not in self.dev]
|
||||
if not missing:
|
||||
break
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Digital Twin - Config Check")
|
||||
dialog.setFixedWidth(400)
|
||||
layout = QVBoxLayout()
|
||||
|
||||
top = QHBoxLayout()
|
||||
icon = QLabel()
|
||||
icon_pixmap = (
|
||||
QApplication.style()
|
||||
.standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning)
|
||||
.pixmap(48, 48)
|
||||
)
|
||||
icon.setPixmap(icon_pixmap)
|
||||
icon.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
top.addWidget(icon)
|
||||
|
||||
text = QLabel(
|
||||
"The current config does not include all required devices to run Digital Twin."
|
||||
+ "Reload the config with the correct devices."
|
||||
)
|
||||
text.setWordWrap(True)
|
||||
text.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
top.addWidget(text, stretch=1)
|
||||
layout.addLayout(top)
|
||||
|
||||
info = QLabel("Missing devices:\n" + ", ".join(missing))
|
||||
info.setWordWrap(True)
|
||||
info.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
layout.addWidget(info)
|
||||
layout.addStretch()
|
||||
|
||||
buttons = QHBoxLayout()
|
||||
check_again = QPushButton("Check Again")
|
||||
close_app = QPushButton("Close Application")
|
||||
check_again.clicked.connect(dialog.accept)
|
||||
close_app.clicked.connect(dialog.reject)
|
||||
buttons.addWidget(check_again)
|
||||
buttons.addWidget(close_app)
|
||||
layout.addLayout(buttons)
|
||||
|
||||
dialog.setLayout(layout)
|
||||
dialog.show()
|
||||
info.setMinimumHeight(info.heightForWidth(info.width()))
|
||||
if dialog.exec_() == QDialog.DialogCode.Rejected:
|
||||
running_app = QApplication.instance()
|
||||
if running_app is not None:
|
||||
running_app.exit(0)
|
||||
if reload_config:
|
||||
self._timer.start()
|
||||
|
||||
@SafeSlot()
|
||||
def calc_assistant(self, *_, **kwargs):
|
||||
"""
|
||||
Calculates various values for the assistant.
|
||||
If called from a qt slot, the identifier represents
|
||||
the button pressed / value changed. Based on the identifier,
|
||||
calculate different values.
|
||||
Note: identifier=init calculates all values
|
||||
"""
|
||||
identifier = kwargs["identifier"]
|
||||
match identifier:
|
||||
case "init":
|
||||
self.update_mo1_mode()
|
||||
self.calc_mo1_bragg_angle()
|
||||
self.calc_cm_crit_pitch()
|
||||
self.calc_cm_reflectivity()
|
||||
self.update_fm_mode()
|
||||
self.calc_fm_reflectivity()
|
||||
self.calc_cm_fm_harm_suppr()
|
||||
self.calc_fm_ideal_pitch()
|
||||
self.calc_mo1_energy_resolution()
|
||||
case "energy":
|
||||
self.calc_mo1_bragg_angle()
|
||||
self.calc_cm_crit_pitch()
|
||||
self.calc_cm_reflectivity()
|
||||
self.calc_fm_reflectivity()
|
||||
self.calc_cm_fm_harm_suppr()
|
||||
self.calc_mo1_energy_resolution()
|
||||
case "hacc":
|
||||
self.calc_fm_ideal_pitch()
|
||||
case "vacc":
|
||||
self.calc_fm_ideal_pitch()
|
||||
case "cm_stripe":
|
||||
self.calc_cm_crit_pitch()
|
||||
self.calc_cm_reflectivity()
|
||||
self.calc_cm_fm_harm_suppr()
|
||||
case "cm_pitch":
|
||||
self.calc_cm_reflectivity()
|
||||
self.calc_cm_fm_harm_suppr()
|
||||
case "mo1_mode":
|
||||
self.update_mo1_mode()
|
||||
case "mo1_xtal":
|
||||
self.calc_mo1_bragg_angle()
|
||||
self.calc_mo1_energy_resolution()
|
||||
case "fm_focus":
|
||||
self.update_fm_mode()
|
||||
self.calc_fm_ideal_pitch()
|
||||
case "fm_focx":
|
||||
self.calc_fm_ideal_pitch()
|
||||
case "fm_focy":
|
||||
self.calc_fm_ideal_pitch()
|
||||
case "fm_rotx":
|
||||
self.calc_fm_reflectivity()
|
||||
self.calc_cm_fm_harm_suppr()
|
||||
case "fm_stripe":
|
||||
self.calc_fm_reflectivity()
|
||||
self.calc_cm_fm_harm_suppr()
|
||||
self.calc_fm_ideal_pitch()
|
||||
case "smpl":
|
||||
self.calc_fm_ideal_pitch()
|
||||
self.calc_positions()
|
||||
self.calc_assistant_sideview()
|
||||
self.calc_assistant_surfaces()
|
||||
|
||||
def get_assistant_config(self, apply_offset: bool = False) -> ConfigDict:
|
||||
"""
|
||||
Assembles the digital twin config from the assistants input.
|
||||
|
||||
Args:
|
||||
apply_offset(bool): Applies the offset values to the config.
|
||||
Defaults to False
|
||||
|
||||
Returns:
|
||||
ConfigDict: config of the assistant
|
||||
"""
|
||||
fm_focus = self.input.fm_focus.currentText()
|
||||
if fm_focus == "Manual":
|
||||
fm_rotx = self.input.fm_rotx.value()
|
||||
fm_qy = None
|
||||
elif fm_focus == "Focused":
|
||||
fm_rotx = self.input.fm_rotx_ideal.value()
|
||||
fm_qy = None
|
||||
else: # Focused
|
||||
fm_rotx = self.input.fm_rotx_ideal.value()
|
||||
fm_qy = self.qy
|
||||
|
||||
cm_stripe = self.input.cm_stripe.currentText()
|
||||
cm_trx = cm_stripe_to_trx(cm_stripe)
|
||||
fm_stripe = self.input.fm_stripe.currentText()
|
||||
fm_trx = fm_stripe_to_trx(fm_stripe)
|
||||
|
||||
assert cm_trx is not None, f"No cm_trx found for given stripe {cm_stripe}!"
|
||||
assert fm_trx is not None, f"No fm_trx found for given stripe {fm_stripe}!"
|
||||
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
smpl = self.input.smpl.value()
|
||||
case ComboBox():
|
||||
table = self.input.smpl.currentText()
|
||||
smpl = table_to_smpl_pos(table)
|
||||
|
||||
config: ConfigDict = {
|
||||
"energy": self.input.energy.value(),
|
||||
"h_acc": self.input.sldi_hacc.value(),
|
||||
"v_acc": self.input.sldi_vacc.value(),
|
||||
"cm_pitch": -self.input.cm_pitch.value(),
|
||||
"cm_stripe": cm_stripe,
|
||||
"cm_trx": cm_trx,
|
||||
"mo1_mode": self.input.mo1_mode.currentText(),
|
||||
"mo1_xtal": self.input.mo1_xtal.currentText(),
|
||||
"mo1_bragg": self.bragg_angle,
|
||||
"fm_rotx": -fm_rotx,
|
||||
"fm_stripe": fm_stripe,
|
||||
"fm_trx": fm_trx,
|
||||
"fm_qy": fm_qy,
|
||||
"fm_gain_height": 1,
|
||||
"smpl": smpl,
|
||||
}
|
||||
|
||||
# Apply offsets
|
||||
if apply_offset:
|
||||
for axis, _ in config.items():
|
||||
if axis in self.offsets:
|
||||
axis_offsets = self.offsets[axis]
|
||||
if "modifier" in axis_offsets and "offset" in axis_offsets:
|
||||
for idx, rng in enumerate(axis_offsets["modifier"]["range"]):
|
||||
if rng[0] < config[axis_offsets["modifier"]["axis"]] < rng[1]:
|
||||
config[axis] += axis_offsets["offset"][idx]
|
||||
break
|
||||
elif "offset" in axis_offsets:
|
||||
config[axis] += axis_offsets["offset"]
|
||||
|
||||
# Convert to SI units!
|
||||
config["h_acc"] *= 1e-3
|
||||
config["v_acc"] *= 1e-3
|
||||
config["cm_pitch"] *= 1e-3
|
||||
config["fm_rotx"] *= 1e-3
|
||||
|
||||
# logger.info(f'Config created: {config}')
|
||||
return config
|
||||
|
||||
def get_reality_config(self) -> ConfigDict:
|
||||
"""
|
||||
Assembles the digital twin config based on the real axis positions.
|
||||
|
||||
Returns:
|
||||
ConfigDict: config of the reality
|
||||
"""
|
||||
mo1_trx = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"]
|
||||
if abs(mo1_trx) > 5:
|
||||
mo1_mode = "Monochromatic"
|
||||
else:
|
||||
mo1_mode = "Pinkbeam"
|
||||
mo1_bragg = self.dev.mo1_bragg.read(cached=True)
|
||||
sldi_gapx = self.dev.sldi_gapx.read(cached=True)["sldi_gapx"]["value"]
|
||||
sldi_gapy = self.dev.sldi_gapy.read(cached=True)["sldi_gapy"]["value"]
|
||||
h_acc, v_acc = sldi_gap_to_acc(sldi_gapx, sldi_gapy)
|
||||
cm_trx = self.dev.cm_trx.read(cached=True)["cm_trx"]["value"]
|
||||
cm_stripe = cm_trx_to_stripe(-cm_trx)
|
||||
cm_pitch = self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"]
|
||||
fm_trx = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"]
|
||||
fm_stripe = fm_trx_to_stripe(-fm_trx)
|
||||
fm_rotx = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
|
||||
fm_rotx_real = 2 * cm_pitch - fm_rotx
|
||||
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
smpl = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
|
||||
case ComboBox():
|
||||
table = self.input.smpl.currentText()
|
||||
smpl = table_to_smpl_pos(table)
|
||||
|
||||
raw = { # Config in SI units!
|
||||
"energy": mo1_bragg["mo1_bragg"]["value"],
|
||||
"h_acc": h_acc,
|
||||
"v_acc": v_acc,
|
||||
"cm_pitch": -cm_pitch * 1e-3,
|
||||
"cm_stripe": cm_stripe,
|
||||
"cm_trx": cm_trx,
|
||||
"mo1_mode": mo1_mode,
|
||||
"mo1_xtal": mo1_bragg["mo1_bragg_crystal_current_xtal_string"]["value"],
|
||||
"mo1_bragg": mo1_bragg["mo1_bragg_angle"]["value"] / 180 * np.pi,
|
||||
"fm_rotx": -fm_rotx_real * 1e-3,
|
||||
"fm_stripe": fm_stripe,
|
||||
"fm_trx": fm_trx,
|
||||
"fm_qy": None,
|
||||
"fm_gain_height": 1,
|
||||
"smpl": smpl,
|
||||
}
|
||||
config = cast(ConfigDict, raw)
|
||||
# logger.info(f'Config created: {config}')
|
||||
|
||||
abs_open = self.dev.abs.read(cached=True)["abs_status_string"]["value"] == "OPEN"
|
||||
if not abs_open:
|
||||
ready = True
|
||||
for mover in self.mover.mover_widgets:
|
||||
if mover.status in ("moving", "error"):
|
||||
ready = False
|
||||
if ready:
|
||||
self.mover.abs.enable_open(True) # Enable open button
|
||||
else:
|
||||
self.mover.abs.enable_open(False) # Disable open button
|
||||
else:
|
||||
self.mover.abs.enable_open(False) # Disable open button
|
||||
|
||||
self.mover.sldi_gapx.set_feedback(sldi_gapx)
|
||||
self.mover.sldi_gapy.set_feedback(sldi_gapy)
|
||||
self.mover.cm_trx.set_feedback(cm_trx)
|
||||
self.mover.cm_try.set_feedback(self.dev.cm_try.read(cached=True)["cm_try"]["value"])
|
||||
self.mover.cm_bnd.set_feedback(
|
||||
self.dev.cm_bnd_radius.read(cached=True)["cm_bnd_radius"]["value"]
|
||||
)
|
||||
self.mover.cm_rotx.set_feedback(cm_pitch)
|
||||
self.mover.mo1_bragg_angle.set_feedback(mo1_bragg["mo1_bragg_angle"]["value"])
|
||||
self.mover.mo1_trx.set_feedback(mo1_trx)
|
||||
self.mover.mo1_try.set_feedback(self.dev.mo1_try.read(cached=True)["mo1_try"]["value"])
|
||||
self.mover.sl1_centery.set_feedback(
|
||||
self.dev.sl1_centery.read(cached=True)["sl1_centery"]["value"]
|
||||
)
|
||||
self.mover.sl1_gapy.set_feedback(self.dev.sl1_gapy.read(cached=True)["sl1_gapy"]["value"])
|
||||
self.mover.bm1_try.set_feedback(self.dev.bm1_try.read(cached=True)["bm1_try"]["value"])
|
||||
self.mover.fm_trx.set_feedback(fm_trx)
|
||||
self.mover.fm_try.set_feedback(self.dev.fm_try.read(cached=True)["fm_try"]["value"])
|
||||
self.mover.fm_bnd.set_feedback(
|
||||
self.dev.fm_bnd_radius.read(cached=True)["fm_bnd_radius"]["value"]
|
||||
)
|
||||
self.mover.fm_rotx.set_feedback(fm_rotx)
|
||||
self.mover.fm_roty.set_feedback(self.dev.fm_roty.read(cached=True)["fm_roty"]["value"])
|
||||
self.mover.fm_rotz.set_feedback(self.dev.fm_rotz.read(cached=True)["fm_rotz"]["value"])
|
||||
if self.beamline == "x01da":
|
||||
self.mover.sl2_centery.set_feedback(
|
||||
self.dev.sl2_centery.read(cached=True)["sl2_centery"]["value"]
|
||||
)
|
||||
self.mover.sl2_gapy.set_feedback(
|
||||
self.dev.sl2_gapy.read(cached=True)["sl2_gapy"]["value"]
|
||||
)
|
||||
self.mover.bm2_try.set_feedback(self.dev.bm2_try.read(cached=True)["bm2_try"]["value"])
|
||||
if self.beamline == "x01da":
|
||||
self.mover.ot_try.set_feedback(self.dev.ot_try.read(cached=True)["ot_try"]["value"])
|
||||
self.mover.ot_rotx.set_feedback(self.dev.ot_rotx.read(cached=True)["ot_rotx"]["value"])
|
||||
self.mover.ot_es1_trz.set_feedback(smpl)
|
||||
self.mover.es0wi_try.set_feedback(
|
||||
self.dev.es0wi_try.read(cached=True)["es0wi_try"]["value"]
|
||||
)
|
||||
if self.beamline == "x10da":
|
||||
self.mover.es1_try.set_feedback(self.dev.es1_try.read(cached=True)["es1_try"]["value"])
|
||||
self.mover.es1ic0_try.set_feedback(
|
||||
self.dev.es1ic0_try.read(cached=True)["es1ic0_try"]["value"]
|
||||
)
|
||||
self.mover.es1ic1_try.set_feedback(
|
||||
self.dev.es1ic1_try.read(cached=True)["es1ic1_try"]["value"]
|
||||
)
|
||||
self.mover.es1ic2_try.set_feedback(
|
||||
self.dev.es1ic2_try.read(cached=True)["es1ic2_try"]["value"]
|
||||
)
|
||||
self.mover.es2_try.set_feedback(self.dev.es2_try.read(cached=True)["es2_try"]["value"])
|
||||
|
||||
self.mover.abs.set_feedback(abs_open)
|
||||
return config
|
||||
|
||||
@SafeSlot()
|
||||
def adapt_reality(self, *_):
|
||||
"""
|
||||
Based on the real axis positions, adjust the assistant to reflect
|
||||
the reality.
|
||||
"""
|
||||
pos = {}
|
||||
pos["sldi_gapx"] = self.dev.sldi_gapx.read(cached=True)["sldi_gapx"]["value"]
|
||||
pos["sldi_gapy"] = self.dev.sldi_gapy.read(cached=True)["sldi_gapy"]["value"]
|
||||
pos["cm_trx"] = self.dev.cm_trx.read(cached=True)["cm_trx"]["value"]
|
||||
pos["cm_rotx"] = self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"]
|
||||
pos["mo1_trx"] = self.dev.mo1_trx.read(cached=True)["mo1_trx"]["value"]
|
||||
pos["fm_trx"] = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"]
|
||||
pos["fm_rotx"] = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
|
||||
pos["fm_bnd_radius"] = self.dev.fm_bnd_radius.read(cached=True)["fm_bnd_radius"]["value"]
|
||||
if self.beamline == "x01da":
|
||||
pos["ot_es1_trz"] = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
|
||||
|
||||
# Removing offsets
|
||||
for axis, _ in pos.items():
|
||||
if axis in self.offsets:
|
||||
axis_offsets = self.offsets[axis]
|
||||
if "modifier" in axis_offsets and "offset" in axis_offsets:
|
||||
for idx, rng in enumerate(axis_offsets["modifier"]["range"]):
|
||||
if rng[0] < pos[axis_offsets["modifier"]["axis"]] < rng[1]:
|
||||
pos[axis] -= axis_offsets["offset"][idx]
|
||||
break
|
||||
elif "offset" in axis_offsets:
|
||||
pos[axis] -= axis_offsets["offset"]
|
||||
|
||||
self.input.energy.set_number(self.dev.mo1_bragg.read(cached=True)["mo1_bragg"]["value"])
|
||||
h_acc, v_acc = sldi_gap_to_acc(pos["sldi_gapx"], pos["sldi_gapy"])
|
||||
self.input.sldi_hacc.set_number(h_acc * 1e3)
|
||||
self.input.sldi_vacc.set_number(v_acc * 1e3)
|
||||
self.input.cm_stripe.set_current_text(cm_trx_to_stripe(-pos["cm_trx"]))
|
||||
self.input.cm_pitch.set_number(pos["cm_rotx"])
|
||||
if abs(pos["mo1_trx"]) > 5:
|
||||
mo1_mode = "Monochromatic"
|
||||
else:
|
||||
mo1_mode = "Pinkbeam"
|
||||
self.input.mo1_mode.set_current_text(mo1_mode)
|
||||
self.input.mo1_xtal.set_current_text(
|
||||
self.dev.mo1_bragg.read(cached=True)["mo1_bragg_crystal_current_xtal_string"]["value"]
|
||||
)
|
||||
fm_stripe = fm_trx_to_stripe(-pos["fm_trx"])
|
||||
self.input.fm_stripe.set_current_text(fm_stripe)
|
||||
fm_rotx_real = 2 * pos["cm_rotx"] - pos["fm_rotx"]
|
||||
self.input.fm_rotx.set_number(fm_rotx_real)
|
||||
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
smpl = pos["ot_es1_trz"]
|
||||
self.input.smpl.set_number(pos["ot_es1_trz"])
|
||||
case ComboBox():
|
||||
table = self.ask_table_selection(self.input.smpl.currentText())
|
||||
smpl = table_to_smpl_pos(table)
|
||||
self.input.smpl.set_current_text(table)
|
||||
|
||||
fm_focx, fm_focy = calc_beamsize(
|
||||
h_acc, v_acc, fm_stripe, -fm_rotx_real * 1e-3, pos["fm_bnd_radius"] * 1e6, smpl
|
||||
)
|
||||
if fm_focx < 0.08 and fm_focy < 0.08:
|
||||
self.input.fm_focus.set_current_text("Focused")
|
||||
else:
|
||||
self.input.fm_focus.set_current_text("Defocused")
|
||||
self.input.fm_focx.set_number(fm_focx)
|
||||
self.input.fm_focy.set_number(fm_focy)
|
||||
|
||||
self.calc_assistant(identifier="init")
|
||||
|
||||
def ask_table_selection(self, preset=None) -> str | None:
|
||||
"""
|
||||
Opens a dialog asking the user to select a table (ES1 or ES2).
|
||||
|
||||
Args:
|
||||
preset (str): Preset text for the table, either 'ES1' or 'ES2'.
|
||||
|
||||
Returns:
|
||||
The selected table ('ES1' or 'ES2'), or None if the user cancelled.
|
||||
"""
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Select Table")
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
text = QLabel("Select the current table in use.")
|
||||
|
||||
combo = QComboBox()
|
||||
choice = ["ES1", "ES2"]
|
||||
combo.addItems(choice)
|
||||
if preset is not None:
|
||||
if preset in choice:
|
||||
combo.setCurrentText(preset)
|
||||
|
||||
buttons = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
buttons.accepted.connect(dialog.accept)
|
||||
buttons.rejected.connect(dialog.reject)
|
||||
|
||||
layout.addWidget(text)
|
||||
layout.addWidget(combo)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
return combo.currentText()
|
||||
return None
|
||||
|
||||
@SafeSlot()
|
||||
def load_offsets(self, *_, recalculate: bool = True):
|
||||
"""
|
||||
Loads or unloads the offsets from the file
|
||||
|
||||
Args:
|
||||
recalculate(bool): Recalculates the assistant values after loading.
|
||||
Defaults to True
|
||||
"""
|
||||
|
||||
if self.offsets == {}:
|
||||
# Load offsets
|
||||
if not self.offset_file.exists():
|
||||
raise FileNotFoundError(f"Offset file not found: {self.offset_file}")
|
||||
|
||||
with self.offset_file.open("r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Expected a YAML mapping, got {type(data).__name__}")
|
||||
|
||||
self.offsets = data
|
||||
|
||||
if recalculate:
|
||||
self.calc_assistant(identifier="init")
|
||||
|
||||
self.settings.load_offsets.setText("Unload")
|
||||
self.settings.offsets_status.setText("Loaded and applied")
|
||||
self.settings.offsets_status.setColor(get_accent_colors().success.name())
|
||||
self.settings.show_offsets.enable_button(True)
|
||||
else:
|
||||
# Unload offsets
|
||||
self.offsets = {}
|
||||
self.calc_assistant(identifier="init")
|
||||
|
||||
self.settings.load_offsets.setText("Load")
|
||||
self.settings.offsets_status.setText("No offsets")
|
||||
self.settings.offsets_status.setColor(get_accent_colors().default.name())
|
||||
self.settings.show_offsets.enable_button(False)
|
||||
|
||||
@SafeSlot()
|
||||
def show_offsets(self, *_):
|
||||
"""
|
||||
Shows the offsets in a popup window
|
||||
"""
|
||||
dialog = QDialog()
|
||||
dialog.setWindowTitle("Digital Twin - Offsets")
|
||||
dialog.setFixedWidth(500)
|
||||
layout = QVBoxLayout(dialog)
|
||||
layout.setSpacing(12)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
intro_label = QLabel("The offsets are saved in the digital twin BEC widget folder:")
|
||||
intro_label.setWordWrap(True)
|
||||
layout.addWidget(intro_label)
|
||||
|
||||
file = QLabel(str(self.offset_file))
|
||||
file.setWordWrap(True)
|
||||
font = QFont()
|
||||
font.setItalic(True)
|
||||
file.setFont(font)
|
||||
layout.addWidget(file)
|
||||
|
||||
text_edit = QPlainTextEdit()
|
||||
text_edit.setReadOnly(True)
|
||||
text_edit.setFont(QFont("Consolas", 9))
|
||||
|
||||
class InlineListDumper(yaml.Dumper):
|
||||
"""YAML dumper that renders all sequences on a single line."""
|
||||
|
||||
def represent_sequence(self, tag, sequence, *_):
|
||||
return super().represent_sequence(tag, sequence, flow_style=True)
|
||||
|
||||
text_edit.setPlainText(yaml.dump(self.offsets, Dumper=InlineListDumper, sort_keys=False))
|
||||
layout.addWidget(text_edit)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||
buttons.rejected.connect(dialog.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
dialog.exec()
|
||||
|
||||
def update_fm_mode(self):
|
||||
"""
|
||||
Updates the focusing mirror input group based on the
|
||||
selection of the focus strategy.
|
||||
"""
|
||||
fm_focus = self.input.fm_focus.currentText()
|
||||
if fm_focus == "Manual":
|
||||
self.input.fm_rotx.setVisible(True)
|
||||
self.input.fm_rotx_ideal.setVisible(True)
|
||||
self.input.fm_focx.setVisible(False)
|
||||
self.input.fm_focy.setVisible(False)
|
||||
self.input.fm_rotx_ideal.setLabel("Incidence Angle for focused beam")
|
||||
elif fm_focus == "Focused":
|
||||
self.input.fm_rotx.setVisible(False)
|
||||
self.input.fm_rotx_ideal.setVisible(True)
|
||||
self.input.fm_focx.setVisible(False)
|
||||
self.input.fm_focy.setVisible(False)
|
||||
self.input.fm_rotx_ideal.setLabel("Incidence Angle for focused beam")
|
||||
else: # Defocused
|
||||
self.input.fm_rotx.setVisible(False)
|
||||
self.input.fm_rotx_ideal.setVisible(True)
|
||||
self.input.fm_focx.setVisible(True)
|
||||
self.input.fm_focy.setVisible(True)
|
||||
self.input.fm_rotx_ideal.setLabel("Incidence Angle for defocused beam")
|
||||
|
||||
@SafeSlot()
|
||||
def calc_reality(self):
|
||||
"""
|
||||
Updates the plots for the reality scene
|
||||
"""
|
||||
config = self.get_reality_config()
|
||||
data = calc_sideview(config)
|
||||
self.sideview_plot.update_curves("reality", data=data)
|
||||
surfaces = calc_surfaces(config)
|
||||
self.surface_plots.update_surfaces(scene="reality", data=surfaces)
|
||||
|
||||
def calc_mo1_energy_resolution(self):
|
||||
"""
|
||||
Calculates the energy resolution of the monochromator
|
||||
"""
|
||||
xtal = self.input.mo1_xtal.currentText().translate(
|
||||
str.maketrans("", "", "()")
|
||||
) # Remove brackets from xtal name to conform with parameters
|
||||
xtal = cast(Literal["Si111", "Si311"], xtal)
|
||||
energy = self.input.energy.value()
|
||||
self.input.mo1_eres.setValue(mo1_energy_resolution(xtal, energy))
|
||||
|
||||
def calc_cm_reflectivity(self):
|
||||
"""
|
||||
Calculates the collimating mirror reflectivity
|
||||
"""
|
||||
cm_stripe = self.input.cm_stripe.currentText()
|
||||
cm_pitch = -self.input.cm_pitch.value() * 1e-3
|
||||
energy = self.input.energy.value()
|
||||
self.input.cm_refl.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, energy))
|
||||
self.input.cm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV")
|
||||
self.input.cm_refl_harm.setValue(100 * cm_reflectivity(cm_stripe, cm_pitch, 3 * energy))
|
||||
self.input.cm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV")
|
||||
|
||||
def calc_fm_reflectivity(self):
|
||||
"""
|
||||
Calculates the focusing mirror reflectivity
|
||||
"""
|
||||
fm_stripe = self.input.fm_stripe.currentText()
|
||||
fm_focus = self.input.fm_focus.currentText()
|
||||
if fm_focus == "Manual":
|
||||
fm_rotx = -self.input.fm_rotx.value() * 1e-3
|
||||
else:
|
||||
fm_rotx = -self.input.fm_rotx_ideal.value() * 1e-3
|
||||
energy = self.input.energy.value()
|
||||
self.input.fm_refl.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, energy))
|
||||
self.input.fm_refl.setLabel(f"Reflectivity at \n{energy:.0f} eV")
|
||||
self.input.fm_refl_harm.setValue(100 * fm_reflectivity(fm_stripe, fm_rotx, 3 * energy))
|
||||
self.input.fm_refl_harm.setLabel(f"Reflectivity at \n{3*energy:.0f} eV")
|
||||
|
||||
def calc_cm_fm_harm_suppr(self):
|
||||
"""
|
||||
Calculates the combined harmonics suppression of both mirrors
|
||||
"""
|
||||
harm_suppr = (self.input.cm_refl.value() * self.input.fm_refl.value()) / (
|
||||
self.input.cm_refl_harm.value() * self.input.fm_refl_harm.value()
|
||||
)
|
||||
self.input.cm_fm_harm_suppr.setValue(harm_suppr)
|
||||
self.input.cm_fm_harm_suppr.setLabel(
|
||||
f"Total Suppression Factor at {3 * self.input.energy.value():.0f} eV"
|
||||
)
|
||||
|
||||
def calc_assistant_sideview(self):
|
||||
"""
|
||||
Updates the sideview plot based on the assistant values
|
||||
"""
|
||||
config = self.get_assistant_config(apply_offset=True)
|
||||
data = calc_sideview(config)
|
||||
self.sideview_plot.update_curves("assistant", data)
|
||||
|
||||
def calc_assistant_surfaces(self):
|
||||
"""
|
||||
Updates the surface plot based on the assistant values
|
||||
"""
|
||||
surfaces = calc_surfaces(self.get_assistant_config())
|
||||
self.surface_plots.update_surfaces(scene="assistant", data=surfaces)
|
||||
|
||||
def calc_positions(self):
|
||||
"""
|
||||
Calculates the positions for the axes based on the assistant values
|
||||
"""
|
||||
out = calc_positions(self.beamline, self.get_assistant_config())
|
||||
|
||||
# Apply offsets
|
||||
for axis, axis_data in out.items():
|
||||
if axis in self.offsets:
|
||||
axis_offsets = self.offsets[axis]
|
||||
if "modifier" in axis_offsets and "offset" in axis_offsets:
|
||||
for idx, rng in enumerate(axis_offsets["modifier"]["range"]):
|
||||
if rng[0] < out[axis_offsets["modifier"]["axis"]]["value"] < rng[1]:
|
||||
axis_data["value"] += axis_offsets["offset"][idx]
|
||||
break
|
||||
elif "offset" in axis_offsets:
|
||||
axis_data["value"] += axis_offsets["offset"]
|
||||
|
||||
self.mover.sldi_gapx.set_target(out["sldi_gapx"]["value"])
|
||||
self.mover.sldi_gapy.set_target(out["sldi_gapy"]["value"])
|
||||
self.mover.cm_trx.set_target(out["cm_trx"]["value"])
|
||||
self.mover.cm_try.set_target(out["cm_try"]["value"])
|
||||
self.mover.cm_bnd.set_target(out["cm_bnd_radius"]["value"])
|
||||
self.mover.cm_rotx.set_target(out["cm_rotx"]["value"])
|
||||
self.mover.mo1_bragg_angle.set_target(out["mo1_bragg_angle"]["value"])
|
||||
self.mover.mo1_trx.set_target(out["mo1_trx"]["value"])
|
||||
self.mover.mo1_try.set_target(out["mo1_try"]["value"])
|
||||
self.mover.sl1_centery.set_target(out["sl1_centery"]["value"])
|
||||
self.mover.sl1_gapy.set_target(out["sl1_gapy"]["value"])
|
||||
self.mover.bm1_try.set_target(out["bm1_try"]["value"])
|
||||
self.mover.fm_trx.set_target(out["fm_trx"]["value"])
|
||||
self.mover.fm_try.set_target(out["fm_try"]["value"])
|
||||
self.mover.fm_bnd.set_target(out["fm_bnd_radius"]["value"])
|
||||
self.mover.fm_rotx.set_target(out["fm_rotx"]["value"])
|
||||
self.mover.fm_roty.set_target(out["fm_roty"]["value"])
|
||||
self.mover.fm_rotz.set_target(out["fm_rotz"]["value"])
|
||||
if self.beamline == "x01da":
|
||||
self.mover.sl2_centery.set_target(out["sl2_centery"]["value"])
|
||||
self.mover.sl2_gapy.set_target(out["sl2_gapy"]["value"])
|
||||
self.mover.bm2_try.set_target(out["bm2_try"]["value"])
|
||||
if self.beamline == "x01da":
|
||||
self.mover.ot_try.set_target(out["ot_try"]["value"])
|
||||
self.mover.ot_rotx.set_target(out["ot_rotx"]["value"])
|
||||
self.mover.ot_es1_trz.set_target(out["ot_es1_trz"]["value"])
|
||||
self.mover.es0wi_try.set_target(out["es0wi_try"]["value"])
|
||||
if self.beamline == "x10da":
|
||||
self.mover.es1_try.set_target(out["es1_try"]["value"])
|
||||
self.mover.es1ic0_try.set_target(out["es1ic0_try"]["value"])
|
||||
self.mover.es1ic1_try.set_target(out["es1ic1_try"]["value"])
|
||||
self.mover.es1ic2_try.set_target(out["es1ic2_try"]["value"])
|
||||
self.mover.es2_try.set_target(out["es2_try"]["value"])
|
||||
|
||||
def calc_mo1_bragg_angle(self):
|
||||
"""
|
||||
Calculates bragg angle in rad
|
||||
"""
|
||||
xtal = self.input.mo1_xtal.currentText()
|
||||
if xtal == "Si(111)":
|
||||
d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si111.read(cached=True)[
|
||||
"mo1_bragg_crystal_d_spacing_si111"
|
||||
]["value"]
|
||||
elif xtal == "Si(311)":
|
||||
d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si311.read(cached=True)[
|
||||
"mo1_bragg_crystal_d_spacing_si311"
|
||||
]["value"]
|
||||
else:
|
||||
raise ValueError(f"Invalid xtal selection: {xtal}")
|
||||
cm_pitch = -self.dev.cm_rotx.read(cached=True)["cm_rotx"]["value"] * 1e-3
|
||||
mo1_mode = cast(Literal["Monochromatic", "Pinkbeam"], self.input.mo1_mode.currentText())
|
||||
energy = self.input.energy.value()
|
||||
theta, _ = mo1_bragg_angle(mo1_mode, d_spacing, energy, cm_pitch)
|
||||
self.bragg_angle = theta
|
||||
self.input.mo1_bragg_angle.setValue(theta / np.pi * 180)
|
||||
|
||||
def update_mo1_mode(self):
|
||||
"""
|
||||
Updates the monochromator input group based on the
|
||||
selection of the mode.
|
||||
"""
|
||||
if self.input.mo1_mode.currentText() == "Monochromatic":
|
||||
self.input.mo1_xtal.setVisible(True)
|
||||
self.input.mo1_bragg_angle.setVisible(True)
|
||||
self.input.mo1_eres.setVisible(True)
|
||||
else:
|
||||
self.input.mo1_xtal.setVisible(False)
|
||||
self.input.mo1_bragg_angle.setVisible(False)
|
||||
self.input.mo1_eres.setVisible(False)
|
||||
|
||||
def calc_fm_ideal_pitch(self):
|
||||
"""
|
||||
Calculate the ideal pitch for the focusing mirror.
|
||||
"""
|
||||
fm_focus = cast(
|
||||
Literal["Defocused", "Focused", "Manual"], self.input.fm_focus.currentText()
|
||||
)
|
||||
fm_stripe = self.input.fm_stripe.currentText()
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
smpl = self.input.smpl.value()
|
||||
case ComboBox():
|
||||
table = self.input.smpl.currentText()
|
||||
smpl = table_to_smpl_pos(table)
|
||||
sldi_hacc = self.input.sldi_hacc.value() * 1e-3
|
||||
sldi_vacc = self.input.sldi_vacc.value() * 1e-3
|
||||
fm_focx = self.input.fm_focx.value()
|
||||
fm_focy = self.input.fm_focy.value()
|
||||
fm_rotx, qy = fm_ideal_pitch(
|
||||
fm_focus, fm_stripe, smpl, sldi_hacc, sldi_vacc, fm_focx, fm_focy
|
||||
)
|
||||
self.qy = qy
|
||||
self.input.fm_rotx_ideal.setValue(-fm_rotx * 1e3)
|
||||
|
||||
def calc_cm_crit_pitch(self):
|
||||
"""
|
||||
Calculate the critical pitch for the collimating mirror
|
||||
"""
|
||||
cm_stripe = cast(Literal["Si", "Pt", "Rh"], self.input.cm_stripe.currentText())
|
||||
energy = self.input.energy.value()
|
||||
self.input.cm_pitch_critical.setValue(-cm_critical_angle(cm_stripe, energy) * 1e3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("light")
|
||||
dispatcher = BECDispatcher(gui_id="digital_twin")
|
||||
win = DigitalTwin()
|
||||
win.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['digital_twin.py']}
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from .digital_twin import DigitalTwin
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='DigitalTwin' name='digital_twin'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class DigitalTwinPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = DigitalTwin(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(DigitalTwin.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "digital_twin"
|
||||
|
||||
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 "DigitalTwin"
|
||||
|
||||
def toolTip(self):
|
||||
return "DigitalTwin"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Panel for user inputs of the digital twin widget
|
||||
"""
|
||||
|
||||
from typing import Union
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from ..types import BeamlineId
|
||||
from ..widgets.qt_widgets import Button, ComboBox, Group, InputNumberField, NumberIndicator
|
||||
|
||||
|
||||
class InputPanel(QWidget):
|
||||
"""
|
||||
Panel for user inputs of the digital twin widget
|
||||
|
||||
Args:
|
||||
beamline (BeamlineId): Beamline id type
|
||||
"""
|
||||
|
||||
def __init__(self, beamline: BeamlineId, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setContentsMargins(4, 4, 4, 4)
|
||||
self._layout.setSpacing(4)
|
||||
|
||||
# Adapt to reality
|
||||
self.adapt_reality = Button(label_button="Adapt to reality", enabled=True)
|
||||
|
||||
# Energy
|
||||
self.energy = InputNumberField(
|
||||
"energy", "Energy", unit="eV", init=8979, decimals=0, single_step=100, ll=4000, hl=65000
|
||||
)
|
||||
|
||||
# FE Slits Acceptance
|
||||
self.sldi_hacc = InputNumberField(
|
||||
"h_acc",
|
||||
"Horizontal",
|
||||
unit="mrad",
|
||||
prefix="±",
|
||||
init=0.25,
|
||||
decimals=3,
|
||||
single_step=0.01,
|
||||
ll=-0.1,
|
||||
hl=0.9,
|
||||
)
|
||||
self.sldi_vacc = InputNumberField(
|
||||
"v_acc",
|
||||
"Vertical",
|
||||
unit="mrad",
|
||||
prefix="±",
|
||||
init=0.1,
|
||||
decimals=3,
|
||||
single_step=0.01,
|
||||
ll=-0.1,
|
||||
hl=0.5,
|
||||
)
|
||||
self.sldi_ass_group = Group("FE Slits Acceptance", [self.sldi_hacc, self.sldi_vacc])
|
||||
|
||||
# Collimating mirror
|
||||
self.cm_stripe = ComboBox("cm_stripe", "Stripe", ["Si", "Rh", "Pt"])
|
||||
self.cm_pitch = InputNumberField(
|
||||
"cm_pitch",
|
||||
"Pitch",
|
||||
unit="mrad",
|
||||
init=-2.391,
|
||||
decimals=3,
|
||||
single_step=0.01,
|
||||
ll=-4.6,
|
||||
hl=-1.2,
|
||||
)
|
||||
self.cm_pitch_critical = NumberIndicator("Critical Pitch", "mrad", decimals=3)
|
||||
self.cm_refl = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
|
||||
self.cm_refl_harm = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
|
||||
self.cm_ass_group = Group(
|
||||
"Collimating Mirror",
|
||||
[
|
||||
self.cm_stripe,
|
||||
self.cm_pitch,
|
||||
self.cm_pitch_critical,
|
||||
self.cm_refl,
|
||||
self.cm_refl_harm,
|
||||
],
|
||||
)
|
||||
|
||||
# Monochromator
|
||||
self.mo1_mode = ComboBox("mo1_mode", "Mode", ["Monochromatic", "Pinkbeam"])
|
||||
self.mo1_xtal = ComboBox("mo1_xtal", "Crystal", ["Si(111)", "Si(311)"])
|
||||
self.mo1_bragg_angle = NumberIndicator("Bragg Angle", "deg", decimals=1)
|
||||
self.mo1_eres = NumberIndicator("Energy Resolution", "eV", decimals=2)
|
||||
self.mo1_ass_group = Group(
|
||||
"Monochromator", [self.mo1_mode, self.mo1_xtal, self.mo1_bragg_angle, self.mo1_eres]
|
||||
)
|
||||
|
||||
# Focusing Mirror
|
||||
stripes: dict[BeamlineId, list[str]] = {
|
||||
BeamlineId.X01DA: ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"],
|
||||
BeamlineId.X10DA: ["Rh (toroid)", "Pt (toroid)"],
|
||||
}
|
||||
self.fm_stripe = ComboBox("fm_stripe", "Stripe", stripes[beamline])
|
||||
self.fm_focus = ComboBox("fm_focus", "Focus Type", ["Manual", "Focused", "Defocused"])
|
||||
self.fm_rotx = InputNumberField(
|
||||
"fm_rotx",
|
||||
"Incidence Angle",
|
||||
unit="mrad",
|
||||
init=-2.391,
|
||||
decimals=3,
|
||||
single_step=0.01,
|
||||
ll=-10,
|
||||
hl=2,
|
||||
)
|
||||
self.fm_focx = InputNumberField(
|
||||
"fm_focx",
|
||||
"Beam Size Horizontal",
|
||||
unit="mm",
|
||||
init=1,
|
||||
decimals=1,
|
||||
single_step=0.1,
|
||||
ll=0,
|
||||
hl=30,
|
||||
)
|
||||
self.fm_focy = InputNumberField(
|
||||
"fm_focy",
|
||||
"Beam Size Vertical",
|
||||
unit="mm",
|
||||
init=1,
|
||||
decimals=1,
|
||||
single_step=0.1,
|
||||
ll=0,
|
||||
hl=10,
|
||||
)
|
||||
self.fm_rotx_ideal = NumberIndicator("Incidence Angle for focused beam", "mrad", decimals=3)
|
||||
self.fm_refl = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
|
||||
self.fm_refl_harm = NumberIndicator("Reflectivity at x eV", "%", decimals=0)
|
||||
self.fm_ass_group = Group(
|
||||
"Focusing Mirror",
|
||||
[
|
||||
self.fm_stripe,
|
||||
self.fm_focus,
|
||||
self.fm_rotx,
|
||||
self.fm_focx,
|
||||
self.fm_focy,
|
||||
self.fm_rotx_ideal,
|
||||
self.fm_refl,
|
||||
self.fm_refl_harm,
|
||||
],
|
||||
)
|
||||
|
||||
# Sample
|
||||
self.cm_fm_harm_suppr = NumberIndicator("Total Suppression Factor at x eV", "", decimals=0)
|
||||
self.smpl = self._create_smpl(beamline)
|
||||
|
||||
# Assemble complete assistant group
|
||||
self.input_group = Group(
|
||||
"User Input",
|
||||
[
|
||||
self.adapt_reality,
|
||||
self.energy,
|
||||
self.sldi_ass_group,
|
||||
self.cm_ass_group,
|
||||
self.mo1_ass_group,
|
||||
self.fm_ass_group,
|
||||
self.cm_fm_harm_suppr,
|
||||
self.smpl,
|
||||
],
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.input_group)
|
||||
self._layout.addStretch()
|
||||
|
||||
def _create_smpl(self, beamline: BeamlineId) -> Union[InputNumberField, ComboBox]:
|
||||
match beamline:
|
||||
case BeamlineId.X01DA:
|
||||
return InputNumberField(
|
||||
"smpl",
|
||||
"Sample Position",
|
||||
unit="mm",
|
||||
init=23511,
|
||||
decimals=0,
|
||||
single_step=100,
|
||||
ll=23000,
|
||||
hl=30000,
|
||||
)
|
||||
case BeamlineId.X10DA:
|
||||
return ComboBox("smpl", "Sample Position", ["ES1", "ES2"])
|
||||
@@ -0,0 +1,462 @@
|
||||
"""
|
||||
Panel to move an axis to a certain position
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from ..types import BeamlineId
|
||||
from ..widgets.move_widget import AbsorberWidget, MoveWidget
|
||||
from ..widgets.qt_widgets import Group
|
||||
|
||||
|
||||
class MoverPanel(QWidget):
|
||||
""" "Panel to move an axis to a certain position"""
|
||||
|
||||
def __init__(self, beamline: BeamlineId, dev, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setContentsMargins(4, 4, 4, 4)
|
||||
self._layout.setSpacing(4)
|
||||
|
||||
self.mover_widgets = []
|
||||
|
||||
# FE Slits
|
||||
self.sldi_gapx = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="sldi_gapx",
|
||||
label="GAPX",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.sldi_gapx)
|
||||
|
||||
self.sldi_gapy = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="sldi_gapy",
|
||||
label="GAPY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.sldi_gapy)
|
||||
|
||||
self.sldi_mov_group = Group("FE Slits", [self.sldi_gapx, self.sldi_gapy])
|
||||
|
||||
# Absorber
|
||||
self.abs = AbsorberWidget(absorber=dev.abs, label="")
|
||||
|
||||
self.abs_group = Group("Absorber", [self.abs])
|
||||
|
||||
# Collimating mirror
|
||||
self.cm_trx = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="cm_trx",
|
||||
label="TRX",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.cm_trx)
|
||||
|
||||
self.cm_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="cm_try",
|
||||
label="TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.cm_try)
|
||||
|
||||
self.cm_bnd = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="cm_bnd",
|
||||
label="BENDER",
|
||||
unit="km",
|
||||
decimals=2,
|
||||
deadband=0.2,
|
||||
)
|
||||
self.mover_widgets.append(self.cm_bnd)
|
||||
|
||||
self.cm_rotx = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="cm_rotx",
|
||||
label="PITCH",
|
||||
unit="mrad",
|
||||
decimals=3,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.cm_rotx)
|
||||
|
||||
self.cm_mov_group = Group(
|
||||
"Collimating Mirror", [self.cm_trx, self.cm_try, self.cm_bnd, self.cm_rotx]
|
||||
)
|
||||
|
||||
# Monochromator
|
||||
self.mo1_bragg_angle = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="mo1_bragg_angle",
|
||||
label="Bragg Angle",
|
||||
unit="deg",
|
||||
decimals=3,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.mo1_bragg_angle)
|
||||
|
||||
self.mo1_trx = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="mo1_trx",
|
||||
label="TRX",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.mo1_trx)
|
||||
|
||||
self.mo1_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="mo1_try",
|
||||
label="TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.mo1_try)
|
||||
|
||||
self.mo1_mov_group = Group(
|
||||
"Monochromator", [self.mo1_bragg_angle, self.mo1_trx, self.mo1_try]
|
||||
)
|
||||
|
||||
# OP Slits 1
|
||||
self.sl1_centery = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="sl1_centery",
|
||||
label="CENTERY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.sl1_centery)
|
||||
|
||||
self.sl1_gapy = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="sl1_gapy",
|
||||
label="GAPY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.sl1_gapy)
|
||||
|
||||
self.sl1_mov_group = Group("OP Slits 1", [self.sl1_centery, self.sl1_gapy])
|
||||
|
||||
# OP Beam Monitor 1
|
||||
self.bm1_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="bm1_try",
|
||||
label="TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.bm1_try)
|
||||
|
||||
self.bm1_mov_group = Group("OP Beam Monitor 1", [self.bm1_try])
|
||||
|
||||
# Focusing Mirror
|
||||
self.fm_trx = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="fm_trx",
|
||||
label="TRX",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.fm_trx)
|
||||
|
||||
self.fm_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="fm_try",
|
||||
label="TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.fm_try)
|
||||
|
||||
self.fm_bnd = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="fm_bnd",
|
||||
label="BENDER",
|
||||
unit="km",
|
||||
decimals=2,
|
||||
deadband=0.2,
|
||||
)
|
||||
self.mover_widgets.append(self.fm_bnd)
|
||||
|
||||
self.fm_rotx = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="fm_rotx",
|
||||
label="PITCH",
|
||||
unit="mrad",
|
||||
decimals=3,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.fm_rotx)
|
||||
|
||||
self.fm_roty = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="fm_roty",
|
||||
label="YAW",
|
||||
unit="mrad",
|
||||
decimals=3,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.fm_roty)
|
||||
|
||||
self.fm_rotz = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="fm_rotz",
|
||||
label="ROLL",
|
||||
unit="mrad",
|
||||
decimals=3,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.fm_rotz)
|
||||
|
||||
self.fm_mov_group = Group(
|
||||
"Focusing Mirror",
|
||||
[self.fm_trx, self.fm_try, self.fm_bnd, self.fm_rotx, self.fm_roty, self.fm_rotz],
|
||||
)
|
||||
|
||||
if beamline == "x01da":
|
||||
# OP Slits 2
|
||||
self.sl2_centery = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="sl2_centery",
|
||||
label="CENTERY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.sl2_centery)
|
||||
|
||||
self.sl2_gapy = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="sl2_gapy",
|
||||
label="GAPY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.sl2_gapy)
|
||||
|
||||
self.sl2_mov_group = Group("OP Slits 2", [self.sl2_centery, self.sl2_gapy])
|
||||
|
||||
# OP Beam Monitor 2
|
||||
self.bm2_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="bm2_try",
|
||||
label="TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.bm2_try)
|
||||
|
||||
self.bm2_mov_group = Group("OP Beam Monitor 2", [self.bm2_try])
|
||||
|
||||
if beamline == "x01da":
|
||||
# Optical Table
|
||||
self.ot_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="ot_try",
|
||||
label="TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.2,
|
||||
)
|
||||
self.mover_widgets.append(self.ot_try)
|
||||
|
||||
self.ot_rotx = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="ot_rotx",
|
||||
label="ROTX",
|
||||
unit="mrad",
|
||||
decimals=3,
|
||||
deadband=0.05,
|
||||
)
|
||||
self.mover_widgets.append(self.ot_rotx)
|
||||
|
||||
self.ot_mov_group = Group("Optical Table", [self.ot_try, self.ot_rotx])
|
||||
|
||||
# Experimental Station 0
|
||||
self.es0wi_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="es0wi_try",
|
||||
label="ES0 WI",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.es0wi_try)
|
||||
|
||||
self.es0_mov_group = Group("Experimental Station 0", [self.es0wi_try])
|
||||
|
||||
# Experimental Station 1
|
||||
if beamline == "x01da":
|
||||
self.ot_es1_trz = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="ot_es1_trz",
|
||||
label="ES1 TRZ",
|
||||
unit="mm",
|
||||
decimals=0,
|
||||
deadband=5,
|
||||
)
|
||||
self.mover_widgets.append(self.ot_es1_trz)
|
||||
|
||||
if beamline == "x10da":
|
||||
self.es1_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="es1_try",
|
||||
label="ES1 TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.es1_try)
|
||||
|
||||
self.es1ic0_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="es1ic0_try",
|
||||
label="IC0 TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.es1ic0_try)
|
||||
|
||||
self.es1ic1_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="es1ic1_try",
|
||||
label="IC1 TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.es1ic1_try)
|
||||
|
||||
self.es1ic2_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="es1ic2_try",
|
||||
label="IC2 TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.es1ic2_try)
|
||||
|
||||
if beamline == "x01da":
|
||||
self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz])
|
||||
else:
|
||||
self.es1_mov_group = Group(
|
||||
"Experimental Station 1", [self.es1_try, self.es1ic1_try, self.es1ic2_try]
|
||||
)
|
||||
|
||||
# Experimental Station 2
|
||||
if beamline == "x10da":
|
||||
self.es2_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="es2_try",
|
||||
label="ES2 TRY",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.01,
|
||||
)
|
||||
self.mover_widgets.append(self.es2_try)
|
||||
|
||||
self.es2_mov_group = Group("Experimental Station 2", [self.es2_try])
|
||||
|
||||
# Assemble complete mover group
|
||||
if beamline == "x01da":
|
||||
self.mover_group = Group(
|
||||
"Mover",
|
||||
[
|
||||
self.sldi_mov_group,
|
||||
self.abs_group,
|
||||
self.cm_mov_group,
|
||||
self.mo1_mov_group,
|
||||
self.sl1_mov_group,
|
||||
self.bm1_mov_group,
|
||||
self.fm_mov_group,
|
||||
self.sl2_mov_group,
|
||||
self.bm2_mov_group,
|
||||
self.ot_mov_group,
|
||||
self.es0_mov_group,
|
||||
self.es1_mov_group,
|
||||
],
|
||||
)
|
||||
else:
|
||||
self.mover_group = Group(
|
||||
"Mover",
|
||||
[
|
||||
self.sldi_mov_group,
|
||||
self.abs_group,
|
||||
self.cm_mov_group,
|
||||
self.mo1_mov_group,
|
||||
self.sl1_mov_group,
|
||||
self.bm1_mov_group,
|
||||
self.fm_mov_group,
|
||||
self.bm2_mov_group,
|
||||
self.es0_mov_group,
|
||||
self.es1_mov_group,
|
||||
self.es2_mov_group,
|
||||
],
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.mover_group)
|
||||
self._layout.addStretch()
|
||||
|
||||
def apply_theme(self, theme: Literal["dark", "light"]):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (str): Theme, either "dark" or "light"
|
||||
"""
|
||||
for widget in self.mover_widgets:
|
||||
widget.apply_theme(theme)
|
||||
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
Two plot classes to plot side-view and surface-view
|
||||
"""
|
||||
|
||||
from typing import Literal, Optional, cast
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QBrush, QColor
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QApplication, QGraphicsRectItem, QHBoxLayout, QVBoxLayout, QWidget
|
||||
|
||||
from ..calculations.calc_varia import (
|
||||
mirror_surface_geometries,
|
||||
mo_surface_geometries,
|
||||
pipe_geometries,
|
||||
wall_geometries,
|
||||
)
|
||||
from ..types import BeamlineId, DataDict, SurfaceDict
|
||||
from ..widgets.qt_widgets import Group
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SurfacePlots(QWidget):
|
||||
"""Plot widget with two curves and legend."""
|
||||
|
||||
def __init__(self, beamline: BeamlineId, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.beamline = beamline
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._layout.setContentsMargins(4, 4, 4, 4)
|
||||
self._layout.setSpacing(6)
|
||||
|
||||
self.surfaces: dict[str, SurfaceDict] = {
|
||||
"assistant": {
|
||||
"cm": {"x": [], "y": []},
|
||||
"mo1_1": {"x": [], "y": []},
|
||||
"mo1_2": {"x": [], "y": []},
|
||||
"fm": {"x": [], "y": []},
|
||||
},
|
||||
"reality": {
|
||||
"cm": {"x": [], "y": []},
|
||||
"mo1_1": {"x": [], "y": []},
|
||||
"mo1_2": {"x": [], "y": []},
|
||||
"fm": {"x": [], "y": []},
|
||||
},
|
||||
}
|
||||
|
||||
self.plots = {"fm": {}, "mo1_2": {}, "mo1_1": {}, "cm": {}}
|
||||
|
||||
self.color_impenetrable = (0, 0, 0)
|
||||
self.colors = [(255, 255, 0), (255, 0, 255)]
|
||||
self.text_color = (255, 255, 255)
|
||||
|
||||
# Create plot widgets
|
||||
for name, widget in self.plots.items():
|
||||
plot_widget = pg.PlotWidget()
|
||||
plot_widget.getAxis("bottom").enableAutoSIPrefix(False)
|
||||
|
||||
plot_group = Group("Surface " + name, [plot_widget])
|
||||
|
||||
plot_widget.setLabel("left", "Z [mm]")
|
||||
plot_widget.setLabel("bottom", "X [mm]")
|
||||
plot_widget.setMouseEnabled(x=False, y=False)
|
||||
plot_widget.setMenuEnabled(False)
|
||||
plot_widget.hideButtons()
|
||||
|
||||
widget["widget"] = plot_widget
|
||||
self._layout.addWidget(plot_group)
|
||||
|
||||
# Create surfaces
|
||||
for idx, scene in enumerate(self.surfaces):
|
||||
for name, _ in self.surfaces[scene].items():
|
||||
if scene == "assistant":
|
||||
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
|
||||
pen = pg.mkPen(
|
||||
QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine
|
||||
)
|
||||
z_value = 2
|
||||
else:
|
||||
brush = QBrush(QColor(*self.colors[idx], 255))
|
||||
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=1)
|
||||
z_value = 1
|
||||
widget = self.plots[name]
|
||||
self.plots[name][scene] = widget["widget"].plot(
|
||||
[], [], pen=pen, name=scene, brush=brush, fillLevel=0
|
||||
)
|
||||
self.plots[name][scene].setZValue(z_value)
|
||||
|
||||
self.walls = []
|
||||
self.texts = []
|
||||
|
||||
self.plot_walls()
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
bg_color = pg.getConfigOption("background")
|
||||
fg_color = pg.getConfigOption("foreground")
|
||||
for _, plot in self.plots.items():
|
||||
# Background
|
||||
plot["widget"].setBackground(bg_color)
|
||||
# Axes (tick marks, tick labels, axis line)
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
ax = plot["widget"].getAxis(axis)
|
||||
ax.setPen(pg.mkPen(color=fg_color))
|
||||
ax.setTextPen(pg.mkPen(color=fg_color))
|
||||
|
||||
if theme == "light":
|
||||
self.color_impenetrable = (30, 30, 30)
|
||||
self.colors = [(79, 163, 224), (240, 128, 60)]
|
||||
self.text_color = (255, 255, 255)
|
||||
else: # dark theme
|
||||
self.color_impenetrable = (180, 180, 180)
|
||||
self.colors = [(26, 111, 173), (212, 83, 10)]
|
||||
self.text_color = (0, 0, 0)
|
||||
|
||||
for idx, scene in enumerate(self.surfaces):
|
||||
for name, _ in self.surfaces[scene].items():
|
||||
if scene == "assistant":
|
||||
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
|
||||
pen = pg.mkPen(
|
||||
QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine
|
||||
)
|
||||
else:
|
||||
brush = QBrush(QColor(*self.colors[idx], 255))
|
||||
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=0)
|
||||
self.plots[name][scene].setPen(pen)
|
||||
self.plots[name][scene].setBrush(brush)
|
||||
|
||||
for wall in self.walls:
|
||||
wall.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
|
||||
wall.setBrush(QBrush(QColor(*self.color_impenetrable)))
|
||||
|
||||
for text in self.texts:
|
||||
text.setColor(self.text_color)
|
||||
|
||||
def plot_walls(self):
|
||||
"""Plot walls"""
|
||||
|
||||
def plot_surface(widget, surfaces):
|
||||
for name, surface in surfaces.items():
|
||||
rect = QGraphicsRectItem(*surface)
|
||||
rect.setBrush(QBrush(QColor(*self.color_impenetrable)))
|
||||
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
|
||||
widget.addItem(rect)
|
||||
text = pg.TextItem(name, color=self.text_color, anchor=(0.5, 0.5))
|
||||
widget.addItem(text)
|
||||
text.setPos(surface[0] + surface[2] / 2, surface[1] + surface[3] / 2)
|
||||
text.setZValue(10)
|
||||
self.walls.append(rect)
|
||||
self.texts.append(text)
|
||||
|
||||
for name, plot in self.plots.items():
|
||||
if name == "cm":
|
||||
plot_surface(plot["widget"], mirror_surface_geometries("cm"))
|
||||
elif name == "mo1_1":
|
||||
plot_surface(plot["widget"], mo_surface_geometries("mo1", 0))
|
||||
elif name == "mo1_2":
|
||||
plot_surface(plot["widget"], mo_surface_geometries("mo1", 1))
|
||||
elif name == "fm":
|
||||
if self.beamline == "x01da":
|
||||
plot_surface(plot["widget"], mirror_surface_geometries("fm_flat"))
|
||||
plot_surface(plot["widget"], mirror_surface_geometries("fm_toroid"))
|
||||
else:
|
||||
raise ValueError(f"Plot {name} not found!")
|
||||
for name, plot in self.plots.items():
|
||||
plot["widget"].disableAutoRange()
|
||||
|
||||
def update_surfaces(self, scene: Literal["assistant", "reality"], data: SurfaceDict):
|
||||
"""Update the curves of the plot
|
||||
|
||||
Args:
|
||||
scene (str): The scene to update, either "assistant" or "reality".
|
||||
data (DataDict): The new data to plot, with keys "x" and "y",
|
||||
each containing a list of values.
|
||||
"""
|
||||
self.surfaces[scene] = data
|
||||
for name, device in self.surfaces[scene].items():
|
||||
device = cast(DataDict, device)
|
||||
plot = self.plots[name][scene]
|
||||
x = np.array(device["x"] + [device["x"][0]]) if len(device["x"]) != 0 else np.array([])
|
||||
y = np.array(device["y"] + [device["y"][0]]) if len(device["y"]) != 0 else np.array([])
|
||||
plot.setData(x=x, y=y)
|
||||
|
||||
|
||||
class SideviewPlot(QWidget):
|
||||
"""Plot widget with two curves and legend."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setContentsMargins(4, 4, 4, 4)
|
||||
self._layout.setSpacing(0)
|
||||
|
||||
self.plot_widget = pg.PlotWidget()
|
||||
self.plot_widget.getAxis("bottom").enableAutoSIPrefix(False)
|
||||
self.plot_widget.invertX(True)
|
||||
self.plot_widget.addLegend()
|
||||
|
||||
self.color_impenetrable = (0, 0, 0)
|
||||
self.colors = [(255, 255, 0), (255, 0, 255)]
|
||||
|
||||
self.data: dict[str, DataDict] = {
|
||||
"assistant": {"x": [0, 1000, 2000], "y": [0, 20, 30]},
|
||||
"reality": {"x": [0, 1000, 2000], "y": [0, 15, 50]},
|
||||
}
|
||||
|
||||
self.plots = {}
|
||||
|
||||
self.pipes = []
|
||||
self.walls = []
|
||||
|
||||
for idx, scene in enumerate(self.data.keys()):
|
||||
if scene == "assistant":
|
||||
pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.PenStyle.DotLine)
|
||||
z_value = 2
|
||||
else:
|
||||
pen = pg.mkPen(color=self.colors[idx], width=2)
|
||||
z_value = 1
|
||||
self.plots[scene] = self.plot_widget.plot([], [], pen=pen, name=scene)
|
||||
self.plots[scene].setZValue(z_value)
|
||||
|
||||
self.plot_group = Group("Side View", [self.plot_widget])
|
||||
|
||||
self.plot_widget.setLabel("left", "Height [mm]")
|
||||
self.plot_widget.setLabel("bottom", "Distance [mm]")
|
||||
self.plot_widget.setMouseEnabled(x=False, y=False)
|
||||
self.plot_widget.setXRange(0, 25000, 0.1) # pylint: disable=E1121 # type: ignore
|
||||
self.plot_widget.setYRange(-20, 120, 0.1) # pylint: disable=E1121 # type: ignore
|
||||
self.plot_widget.setMenuEnabled(False)
|
||||
self.plot_widget.hideButtons()
|
||||
|
||||
self._layout.addWidget(self.plot_group)
|
||||
|
||||
self.plot_vacuum_pipes()
|
||||
self.plot_walls()
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
bg_color = pg.getConfigOption("background")
|
||||
fg_color = pg.getConfigOption("foreground")
|
||||
# Background
|
||||
self.plot_widget.setBackground(bg_color)
|
||||
# Axes (tick marks, tick labels, axis line)
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
ax = self.plot_widget.getAxis(axis)
|
||||
ax.setPen(pg.mkPen(color=fg_color))
|
||||
ax.setTextPen(pg.mkPen(color=fg_color))
|
||||
|
||||
if theme == "light":
|
||||
self.color_impenetrable = (30, 30, 30)
|
||||
self.colors = [(79, 163, 224), (240, 128, 60)]
|
||||
self.text_color = (255, 255, 255)
|
||||
else: # dark theme
|
||||
self.color_impenetrable = (180, 180, 180)
|
||||
self.colors = [(26, 111, 173), (212, 83, 10)]
|
||||
self.text_color = (0, 0, 0)
|
||||
|
||||
for idx, scene in enumerate(self.data):
|
||||
if scene == "assistant":
|
||||
brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern)
|
||||
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3, style=Qt.PenStyle.DashLine)
|
||||
else:
|
||||
brush = QBrush(QColor(*self.colors[idx], 255))
|
||||
pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3)
|
||||
self.plots[scene].setPen(pen)
|
||||
self.plots[scene].setBrush(brush)
|
||||
|
||||
for wall in self.walls:
|
||||
wall.setPen(pg.mkPen(color=self.color_impenetrable, width=3))
|
||||
wall.setBrush(QBrush(QColor(*self.color_impenetrable)))
|
||||
|
||||
for pipe in self.pipes:
|
||||
pipe.setPen(pg.mkPen(color=self.color_impenetrable, width=3))
|
||||
|
||||
def plot_vacuum_pipes(self):
|
||||
"""Plot vacuum pipes"""
|
||||
pipes = pipe_geometries()
|
||||
for pipe in pipes:
|
||||
self.pipes.append(
|
||||
self.plot_widget.plot(
|
||||
x=pipe["x"], y=pipe["y"], pen=pg.mkPen(color=self.color_impenetrable, width=2)
|
||||
)
|
||||
)
|
||||
|
||||
def plot_walls(self):
|
||||
"""Plot walls"""
|
||||
walls = wall_geometries()
|
||||
for wall in walls:
|
||||
rect = QGraphicsRectItem(wall[0], wall[1], wall[2], wall[3])
|
||||
rect.setBrush(QBrush(QColor(*self.color_impenetrable)))
|
||||
rect.setPen(pg.mkPen(color=self.color_impenetrable, width=2))
|
||||
self.plot_widget.addItem(rect)
|
||||
self.walls.append(rect)
|
||||
|
||||
def update_curves(self, scene: Literal["assistant", "reality"], data: DataDict):
|
||||
"""Update the curves of the plot
|
||||
|
||||
Args:
|
||||
scene (str): The scene to update, either "assistant" or "reality".
|
||||
data (DataDict): The new data to plot, with keys "x" and "y",
|
||||
each containing a list of values.
|
||||
"""
|
||||
self.data[scene] = data
|
||||
plot = self.plots[scene]
|
||||
plot.setData(x=self.data[scene]["x"], y=self.data[scene]["y"])
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Settings panel for the digital twin widget
|
||||
"""
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from ..widgets.qt_widgets import Button, Group, TextIndicator
|
||||
|
||||
|
||||
class SettingsPanel(QWidget):
|
||||
"""Settings panel for the digital twin widget"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setContentsMargins(4, 4, 4, 4)
|
||||
self._layout.setSpacing(4)
|
||||
|
||||
# Reload offsets
|
||||
self.load_offsets = Button(label="Load Offsets", label_button="Load", enabled=True)
|
||||
self.offsets_status = TextIndicator(label="Offsets")
|
||||
self.show_offsets = Button(label="Show Offsets", label_button="Show", enabled=True)
|
||||
|
||||
# Assemble complete offset group
|
||||
self.offset_group = Group(
|
||||
"Axes Offsets", [self.load_offsets, self.offsets_status, self.show_offsets]
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.offset_group)
|
||||
self._layout.addStretch()
|
||||
@@ -0,0 +1,15 @@
|
||||
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 .digital_twin_plugin import DigitalTwinPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(DigitalTwinPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Types used for the beamline config and for plotting data"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class BeamlineId(str, Enum):
|
||||
"""
|
||||
Identifier for supported beamlines.
|
||||
"""
|
||||
|
||||
X01DA = "x01da"
|
||||
X10DA = "x10da"
|
||||
|
||||
|
||||
class ConfigDict(TypedDict):
|
||||
"""
|
||||
Typed dictionary representing the beamline configuration.
|
||||
|
||||
Attributes:
|
||||
energy (float): Beam energy.
|
||||
h_acc (float): Horizontal acceptance.
|
||||
v_acc (float): Vertical acceptance.
|
||||
cm_pitch (float): CM pitch angle.
|
||||
cm_stripe (str): CM stripe name.
|
||||
cm_trx (float): CM translation x.
|
||||
mo1_mode (str): MO1 mode.
|
||||
mo1_xtal (str): MO1 crystal.
|
||||
mo1_bragg (float): MO1 Bragg angle.
|
||||
fm_rotx (float): FM rotation x.
|
||||
fm_stripe (str): FM stripe name.
|
||||
fm_trx (float): FM translation x.
|
||||
fm_qy (float): FM qy value.
|
||||
fm_gain_height (int): FM gain height.
|
||||
smpl (float): Sample value.
|
||||
"""
|
||||
|
||||
energy: float
|
||||
h_acc: float
|
||||
v_acc: float
|
||||
cm_pitch: float
|
||||
cm_stripe: str
|
||||
cm_trx: float
|
||||
mo1_mode: str
|
||||
mo1_xtal: str
|
||||
mo1_bragg: float
|
||||
fm_rotx: float
|
||||
fm_stripe: str
|
||||
fm_trx: float
|
||||
fm_qy: None | float
|
||||
fm_gain_height: int
|
||||
smpl: float
|
||||
|
||||
|
||||
class DataDict(TypedDict):
|
||||
"""
|
||||
Typed dictionary representing plot data.
|
||||
|
||||
Attributes:
|
||||
x (list[float]): List of x-axis values.
|
||||
y (list[float]): List of y-axis values.
|
||||
"""
|
||||
|
||||
x: list
|
||||
y: list
|
||||
|
||||
|
||||
class SurfaceDict(TypedDict):
|
||||
"""
|
||||
Typed dictionary representing the surfaces of a scene,
|
||||
grouping plot data by surface type.
|
||||
|
||||
Attributes:
|
||||
cm (DataDict): Data for the cm surface.
|
||||
mo1_1 (DataDict): Data for the mo1_1 surface.
|
||||
mo1_2 (DataDict): Data for the mo1_2 surface.
|
||||
fm (DataDict): Data for the fm surface.
|
||||
"""
|
||||
|
||||
cm: DataDict
|
||||
mo1_1: DataDict
|
||||
mo1_2: DataDict
|
||||
fm: DataDict
|
||||
@@ -0,0 +1,645 @@
|
||||
"""Move widget to display an axis and also move it through BEC"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Literal, Optional
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import Property # type: ignore[attr-defined]
|
||||
from qtpy.QtCore import Signal # type: ignore[attr-defined]
|
||||
from qtpy.QtCore import QObject, QPropertyAnimation, Qt, QThread
|
||||
from qtpy.QtGui import QTransform
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QWidget
|
||||
|
||||
# pylint: disable=E0402
|
||||
from .....devices.absorber import STATUS as ABS_STATUS
|
||||
from ..types import BeamlineId
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class Status:
|
||||
"""Status class for the axis"""
|
||||
|
||||
IN_POSITION = "in_position" # green mdi.check-circle
|
||||
NOT_IN_POSITION = "not_in_position" # orange mdi.close-circle
|
||||
MOVING = "moving" # blue mdi.loading (spinning)
|
||||
ERROR = "error" # red mdi.alert-circle
|
||||
|
||||
|
||||
class StatusIcon(QWidget):
|
||||
"""
|
||||
Displays a status icon using bec_qthemes Material Design Icons.
|
||||
Handles its own spin animation for the MOVING state via QPropertyAnimation.
|
||||
"""
|
||||
|
||||
ICON_SIZE = 20
|
||||
|
||||
_ICON_MAP = {
|
||||
Status.IN_POSITION: ("check_circle", "#27ae60"),
|
||||
Status.NOT_IN_POSITION: ("cancel", "#e6d922"),
|
||||
Status.ERROR: ("warning", "#e74c3c"),
|
||||
Status.MOVING: ("cycle", "#2980b9"),
|
||||
}
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._status = None
|
||||
self._rotation = 0.0
|
||||
|
||||
self._label = QLabel(self)
|
||||
self._label.setFixedSize(self.ICON_SIZE, self.ICON_SIZE)
|
||||
self._label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.setFixedSize(self.ICON_SIZE, self.ICON_SIZE)
|
||||
|
||||
self._spin_anim = QPropertyAnimation(self, b"rotation") # type: ignore[call-arg]
|
||||
self._spin_anim.setStartValue(0)
|
||||
self._spin_anim.setEndValue(360)
|
||||
self._spin_anim.setDuration(1000)
|
||||
self._spin_anim.setLoopCount(-1) # Loop indefinitely
|
||||
|
||||
self.set_status(Status.NOT_IN_POSITION)
|
||||
|
||||
def get_rotation(self) -> float:
|
||||
"""
|
||||
Return the current rotation angle in degrees.
|
||||
|
||||
Returns:
|
||||
float: Rotation angle in deg
|
||||
"""
|
||||
return self._rotation
|
||||
|
||||
def set_rotation(self, angle: float):
|
||||
"""
|
||||
Set the rotation angle and update the displayed pixmap.
|
||||
|
||||
Rotates the current base pixmap around its center point using a smooth
|
||||
transformation. Has no effect on the display if no base pixmap is set.
|
||||
|
||||
Args:
|
||||
angle (float): Rotation angle in degrees, clockwise.
|
||||
"""
|
||||
self._rotation = angle
|
||||
if self._current_pixmap_base is not None:
|
||||
cx = self._current_pixmap_base.width() / 2
|
||||
cy = self._current_pixmap_base.height() / 2
|
||||
t = QTransform().translate(cx, cy).rotate(angle).translate(-cx, -cy)
|
||||
self._label.setPixmap(
|
||||
self._current_pixmap_base.transformed(t, Qt.TransformationMode.SmoothTransformation)
|
||||
)
|
||||
|
||||
rotation = Property(float, get_rotation, set_rotation) # type: ignore[call-arg]
|
||||
|
||||
def set_status(self, status: str):
|
||||
"""
|
||||
Update the widget's status and refresh the displayed icon accordingly.
|
||||
|
||||
Looks up the icon name and color associated with the given status from
|
||||
``_ICON_MAP``, renders a new pixmap, and starts or stops the spin
|
||||
animation depending on whether the status is ``Status.MOVING``. Returns
|
||||
early without any updates if the status has not changed.
|
||||
|
||||
Args:
|
||||
status (str): The new status value. Must be a key in ``_ICON_MAP``.
|
||||
"""
|
||||
if status == self._status:
|
||||
return
|
||||
self._status = status
|
||||
|
||||
icon_name, color = self._ICON_MAP[status]
|
||||
icon = material_icon(
|
||||
icon_name, size=(self.ICON_SIZE, self.ICON_SIZE), color=color, convert_to_pixmap=True
|
||||
)
|
||||
self._current_pixmap_base = icon
|
||||
|
||||
if status == Status.MOVING:
|
||||
self._spin_anim.start()
|
||||
else:
|
||||
self._spin_anim.stop()
|
||||
self._label.setPixmap(icon)
|
||||
|
||||
|
||||
class MotionWorker(QObject):
|
||||
"""
|
||||
Executes motion on the specified motor and includes some safety during
|
||||
motion for certain motors.
|
||||
"""
|
||||
|
||||
position_changed = Signal(float)
|
||||
error = Signal()
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, beamline: BeamlineId, dev, motor, target_pos: float):
|
||||
super().__init__()
|
||||
self.beamline = beamline
|
||||
self.dev = dev
|
||||
self.motor = motor
|
||||
self._target = target_pos
|
||||
self._stop_flag = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
"""Sets the stop flag"""
|
||||
self._stop_flag.set()
|
||||
|
||||
def run(self):
|
||||
"""Prepares the movement based on the axis (motor)"""
|
||||
match self.motor:
|
||||
case "sldi_gapx" | "sldi_gapy" | "sldi_centerx" | "sldi_centery":
|
||||
self.motion()
|
||||
case "cm_trx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[{"device": self.dev["cm_roty"], "abs_tol": 0.05}],
|
||||
)
|
||||
case "cm_roty":
|
||||
self.motion(
|
||||
abs_closed=True, surveyed_axes=[{"device": self.dev["cm_trx"], "abs_tol": 0.05}]
|
||||
)
|
||||
case "cm_try":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_rotx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_rotz":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_bnd":
|
||||
if self.beamline == "x01da":
|
||||
p1 = (
|
||||
1 / (self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] * 1e3) + 0.0284
|
||||
) / 2e-6
|
||||
p2 = (1 / (self._target * 1e3) + 0.0284) / 2e-6
|
||||
else:
|
||||
p1 = 541900 / self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] - 32570
|
||||
p2 = 541900 / self._target - 32570
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb={"device": self.dev["cm_bnd_radius"]})
|
||||
case "mo1_try" | "mo1_trx" | "mo1_roty":
|
||||
self.motion(abs_closed=True)
|
||||
case "mo1_bragg_angle":
|
||||
if self.beamline == "x01da":
|
||||
self.motion()
|
||||
else: # x10da needs to move goniometer
|
||||
self.motion(alias="mo1_rotx")
|
||||
case "sl1_centery" | "sl1_gapy" | "bm1_try":
|
||||
self.motion()
|
||||
case "fm_trx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[{"device": self.dev["fm_roty"], "abs_tol": 0.05}],
|
||||
)
|
||||
case "fm_roty":
|
||||
self.motion(
|
||||
abs_closed=True, surveyed_axes=[{"device": self.dev["fm_trx"], "abs_tol": 0.05}]
|
||||
)
|
||||
case "fm_try":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_rotx"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotz"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_rotx":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_try"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotz"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_rotz":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_try"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotx"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_bnd":
|
||||
if self.beamline == "x01da":
|
||||
p1 = (
|
||||
1 / (self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"] * 1e3)
|
||||
+ 4.28e-5
|
||||
) / 1.84e-9
|
||||
p2 = (1 / (self._target * 1e3) + 4.28e-5) / 1.84e-9
|
||||
else:
|
||||
p1 = (
|
||||
593088.7 / self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"]
|
||||
+ 26124.41
|
||||
)
|
||||
p2 = 593088.7 / self._target + 26124.41
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb={"device": self.dev["fm_bnd_radius"]})
|
||||
case "sl2_centery" | "sl2_gapy" | "bm2_try":
|
||||
self.motion()
|
||||
case "ot_try" | "ot_rotx" | "ot_es1_trz":
|
||||
self.motion()
|
||||
case "es0wi_try":
|
||||
self.motion()
|
||||
case "es1_try" | "es2_try":
|
||||
self.motion()
|
||||
case "es1ic0_try" | "es1ic1_try" | "es1ic2_try":
|
||||
self.motion()
|
||||
case _:
|
||||
logger.warning(f"Motor {self.motor} not integrated in digital twin!")
|
||||
|
||||
def motion(
|
||||
self,
|
||||
abs_closed: bool = False,
|
||||
relative: bool = False,
|
||||
rb=None,
|
||||
surveyed_axes=None,
|
||||
alias=None,
|
||||
):
|
||||
"""
|
||||
Moves an axis while surverying a set of axes (if set).
|
||||
Example surveyed_axes:
|
||||
[{'device': bec_device_object, 'abs_tol': 0.1},]
|
||||
|
||||
Args:
|
||||
surveyed_axes (list): List of dictionaries of devices
|
||||
"""
|
||||
try:
|
||||
if alias:
|
||||
self.motor = alias
|
||||
if abs_closed:
|
||||
if self.dev.abs.status.get() == ABS_STATUS.OPEN:
|
||||
status = self.dev.abs.close()
|
||||
# TODO Set timeout to 0.001 and check if it actually raises
|
||||
# (it should not start motion).
|
||||
# Check of behavior of digital twin afterwards.
|
||||
status.wait(timeout=5)
|
||||
if surveyed_axes is not None:
|
||||
for surv_ax in surveyed_axes:
|
||||
surv_ax["name"] = surv_ax["device"].dotted_name
|
||||
surv_ax["old_value"] = surv_ax["device"].read(cached=True)[surv_ax["name"]][
|
||||
"value"
|
||||
]
|
||||
if rb is not None:
|
||||
rb["name"] = rb["device"].dotted_name
|
||||
status = self.dev[self.motor].move(self._target, relative=relative)
|
||||
last_check = time.time()
|
||||
update_interval = 0.1
|
||||
while status.status == "RUNNING":
|
||||
now = time.time()
|
||||
if time.time() - last_check < update_interval:
|
||||
time.sleep(0.01)
|
||||
last_check = now
|
||||
if self._stop_flag.is_set():
|
||||
self.dev[self.motor].stop()
|
||||
self._stop_flag.clear()
|
||||
if rb is not None:
|
||||
self.position_changed.emit(rb["device"].read(cached=True)[rb["name"]]["value"])
|
||||
else:
|
||||
self.position_changed.emit(
|
||||
self.dev[self.motor].read(cached=True)[self.motor]["value"]
|
||||
)
|
||||
if surveyed_axes is not None:
|
||||
for surv_ax in surveyed_axes:
|
||||
fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
|
||||
if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]:
|
||||
self.dev[self.motor].stop()
|
||||
self.error.emit()
|
||||
self.finished.emit()
|
||||
break
|
||||
self.finished.emit()
|
||||
except:
|
||||
self.error.emit()
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class MoveWidget(QWidget):
|
||||
"""
|
||||
One motor stage control group containing:
|
||||
- Target label (target position)
|
||||
- Feedback label (current position)
|
||||
- Status icon (bec_qthemes)
|
||||
- Start / Stop button
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, beamline: BeamlineId, dev, motor, label: str = "", unit=None, decimals=3, deadband=0.0
|
||||
):
|
||||
super().__init__()
|
||||
self.fb = 0.0
|
||||
self.target = 0
|
||||
self.beamline = beamline
|
||||
self.dev = dev
|
||||
self.motor = motor
|
||||
self.deadband = deadband
|
||||
self.status = Status.IN_POSITION
|
||||
self._thread: QThread | None = None
|
||||
self._worker: MotionWorker | None = None
|
||||
|
||||
self.text_color = (0, 0, 0)
|
||||
|
||||
self.unit = unit
|
||||
self.decimals = decimals
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(4, 0, 4, 0)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Name
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(76)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
|
||||
# Target
|
||||
self.target_label = QLabel("-")
|
||||
self.target_label.setFixedWidth(84)
|
||||
layout.addWidget(self.target_label)
|
||||
|
||||
# Feedback
|
||||
self.fb_label = QLabel("-")
|
||||
self.fb_label.setFixedWidth(84)
|
||||
layout.addWidget(self.fb_label)
|
||||
|
||||
# Status icon
|
||||
self.status_icon = StatusIcon()
|
||||
self.status_icon.setFixedWidth(24)
|
||||
layout.addWidget(self.status_icon)
|
||||
|
||||
# Start / Stop button
|
||||
self.btn_action = QPushButton("Move")
|
||||
self.btn_action.setFixedWidth(64)
|
||||
self.btn_action.setFixedHeight(20)
|
||||
self.btn_action.clicked.connect(self._on_button_clicked)
|
||||
layout.addWidget(self.btn_action)
|
||||
self.btn_mode = "start"
|
||||
|
||||
self._apply_button_style("start")
|
||||
|
||||
self.apply_theme()
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
if theme == "light":
|
||||
self.text_color = {"target": (79, 163, 224), "fb": (240, 128, 60)}
|
||||
else: # dark theme
|
||||
self.text_color = {"target": (26, 111, 173), "fb": (212, 83, 10)}
|
||||
r, g, b = self.text_color["target"]
|
||||
self.target_label.setStyleSheet(f"QLabel {{color: rgb({r}, {g}, {b})}}")
|
||||
r, g, b = self.text_color["fb"]
|
||||
self.fb_label.setStyleSheet(f"QLabel {{color: rgb({r}, {g}, {b})}}")
|
||||
|
||||
if self.btn_mode == "start":
|
||||
self.btn_action.setStyleSheet(
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
)
|
||||
else:
|
||||
self.btn_action.setStyleSheet(
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
)
|
||||
|
||||
def set_target(self, target):
|
||||
"""Change the target value in the ui"""
|
||||
self.target = target
|
||||
text = f"{target:.{int(self.decimals)}f}"
|
||||
if self.unit is not None:
|
||||
text = text + " " + self.unit
|
||||
self.target_label.setText(text)
|
||||
self._on_target_or_fb_changed()
|
||||
|
||||
def set_feedback(self, fb):
|
||||
"""Change the feedback value in the ui"""
|
||||
if self.status != Status.MOVING:
|
||||
self.fb = fb
|
||||
text = f"{fb:.{int(self.decimals)}f}"
|
||||
if self.unit is not None:
|
||||
text = text + " " + self.unit
|
||||
self.fb_label.setText(text)
|
||||
self._on_target_or_fb_changed()
|
||||
|
||||
def _apply_button_style(self, mode: str):
|
||||
"""Apply a button style depending on if the button shows start or stop"""
|
||||
self.btn_mode = mode
|
||||
if mode == "start":
|
||||
self.btn_action.setText("Move")
|
||||
self.btn_action.setStyleSheet(
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
)
|
||||
else: # stop
|
||||
self.btn_action.setText("Stop")
|
||||
self.btn_action.setStyleSheet(
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
)
|
||||
|
||||
def _set_status(self, status: str):
|
||||
"""Set the current status icon in the ui"""
|
||||
self.status = status
|
||||
self.status_icon.set_status(status)
|
||||
|
||||
def _on_target_or_fb_changed(self):
|
||||
"""Re-evaluate in-position status whenever the target value changes."""
|
||||
if self.status in (Status.ERROR, Status.MOVING):
|
||||
return
|
||||
if abs(self.fb - self.target) <= self.deadband:
|
||||
self._set_status(Status.IN_POSITION)
|
||||
else:
|
||||
self._set_status(Status.NOT_IN_POSITION)
|
||||
|
||||
def _on_button_clicked(self):
|
||||
"""Starts or stops motion depending on current situation"""
|
||||
if self._thread and self._thread.isRunning():
|
||||
self._stop_motion()
|
||||
else:
|
||||
self._start_motion()
|
||||
|
||||
def _start_motion(self):
|
||||
"""Start a motion"""
|
||||
target = self.target
|
||||
if abs(target - self.fb) <= self.deadband:
|
||||
self._set_status(Status.IN_POSITION)
|
||||
return
|
||||
|
||||
self._set_status(Status.MOVING)
|
||||
self._apply_button_style("stop")
|
||||
|
||||
self._worker = MotionWorker(self.beamline, self.dev, self.motor, target)
|
||||
self._thread = QThread()
|
||||
self._worker.moveToThread(self._thread)
|
||||
|
||||
self._thread.started.connect(self._worker.run)
|
||||
self._worker.position_changed.connect(self._on_position_changed)
|
||||
self._worker.error.connect(self._on_error)
|
||||
self._worker.error.connect(self._thread.quit)
|
||||
self._worker.finished.connect(self._on_motion_finished)
|
||||
self._worker.finished.connect(self._thread.quit)
|
||||
self._thread.finished.connect(self._cleanup_thread)
|
||||
|
||||
self._thread.start()
|
||||
|
||||
def _on_error(self):
|
||||
"""Called when an error occurs"""
|
||||
self._set_status(Status.ERROR)
|
||||
self._apply_button_style("start")
|
||||
|
||||
def _stop_motion(self):
|
||||
"""Attempts to stop the motion"""
|
||||
if self._worker:
|
||||
self._worker.stop()
|
||||
|
||||
def _on_position_changed(self, pos: float):
|
||||
"""Change the feedback value in the ui"""
|
||||
self.fb = pos
|
||||
text = f"{pos:.{int(self.decimals)}f}"
|
||||
if self.unit is not None:
|
||||
text = text + " " + self.unit
|
||||
self.fb_label.setText(text)
|
||||
|
||||
def _on_motion_finished(self):
|
||||
"""Finished a movement"""
|
||||
target = self.target
|
||||
if self.status != Status.ERROR:
|
||||
if abs(self.fb - target) <= self.deadband:
|
||||
self._set_status(Status.IN_POSITION)
|
||||
else:
|
||||
self._set_status(Status.NOT_IN_POSITION)
|
||||
self._apply_button_style("start")
|
||||
|
||||
def _cleanup_thread(self):
|
||||
"""Cleaning up of the mover thread"""
|
||||
if self._thread:
|
||||
self._thread.deleteLater()
|
||||
self._thread = None
|
||||
if self._worker:
|
||||
self._worker.deleteLater()
|
||||
self._worker = None
|
||||
|
||||
def shutdown(self):
|
||||
"""Cleaning up of the mover when shutting down the application"""
|
||||
if self._worker:
|
||||
self._worker.stop()
|
||||
if self._thread:
|
||||
self._thread.quit()
|
||||
self._thread.wait(2000) # max 2 s grace period
|
||||
|
||||
|
||||
class AbsorberWidget(QWidget):
|
||||
"""
|
||||
Control of the frontend absorber (only open)
|
||||
"""
|
||||
|
||||
def __init__(self, absorber, label: str = "Absorber"):
|
||||
super().__init__()
|
||||
self.absorber = absorber
|
||||
self.fb = False
|
||||
self.text_color = (0, 0, 0)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(4, 0, 4, 0)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Name
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(76)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
|
||||
# Blank
|
||||
self.blank_label = QLabel("")
|
||||
self.blank_label.setFixedWidth(84)
|
||||
layout.addWidget(self.blank_label)
|
||||
|
||||
# Feedback
|
||||
self.fb_label = QLabel("-")
|
||||
self.fb_label.setFixedWidth(84)
|
||||
layout.addWidget(self.fb_label)
|
||||
|
||||
# Blank icon
|
||||
self.blank_icon = QLabel("")
|
||||
self.blank_icon.setFixedWidth(24)
|
||||
layout.addWidget(self.blank_icon)
|
||||
|
||||
# Open
|
||||
self.btn_action = QPushButton("Open")
|
||||
self.btn_action.setFixedWidth(64)
|
||||
self.btn_action.setFixedHeight(20)
|
||||
self.btn_action.clicked.connect(self._on_button_clicked)
|
||||
layout.addWidget(self.btn_action)
|
||||
|
||||
def set_feedback(self, fb: bool):
|
||||
"""
|
||||
Displays the status of the absober in the ui
|
||||
|
||||
Args:
|
||||
fb (bool): True will set the button to Open, False to Closed
|
||||
"""
|
||||
self.fb = fb
|
||||
if fb:
|
||||
self.fb_label.setText("Open")
|
||||
self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().success.name()}}}")
|
||||
else:
|
||||
self.fb_label.setText("Closed")
|
||||
self.fb_label.setStyleSheet(f"QLabel {{color: {get_accent_colors().emergency.name()}}}")
|
||||
|
||||
def enable_open(self, enable: bool = False):
|
||||
"""
|
||||
Enable or disable the open/close button
|
||||
|
||||
Args:
|
||||
enable (bool): Enables and disables the button
|
||||
"""
|
||||
if enable:
|
||||
self.btn_action.setStyleSheet(
|
||||
"QPushButton "
|
||||
+ f"{{background-color: {get_accent_colors().success.name()}; color: white;}}"
|
||||
)
|
||||
self.btn_action.setEnabled(True)
|
||||
else: # disabled
|
||||
self.btn_action.setStyleSheet(
|
||||
"QPushButton {{background-color: rgb(120, 120, 120); color: white;}}"
|
||||
)
|
||||
self.btn_action.setDisabled(True)
|
||||
|
||||
def _on_button_clicked(self):
|
||||
"""Open absorber"""
|
||||
self.absorber.open()
|
||||
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Universal Qt widgets
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtGui import QFont
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QDoubleSpinBox,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
LABEL_WIDTH = 118
|
||||
ROW_MARGINS = (4, 0, 4, 0)
|
||||
ROW_SPACING = 6
|
||||
|
||||
|
||||
class Group(QGroupBox):
|
||||
def __init__(self, label, widgets):
|
||||
super().__init__(label)
|
||||
self.layout = QVBoxLayout(self) # type: ignore
|
||||
self.layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.layout.setSpacing(4)
|
||||
for widget in widgets:
|
||||
self.layout.addWidget(widget) # type: ignore
|
||||
|
||||
|
||||
class NumberIndicator(QWidget):
|
||||
def __init__(self, label="", unit=None, highlight=False, decimals=3):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(*ROW_MARGINS)
|
||||
layout.setSpacing(ROW_SPACING)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(LABEL_WIDTH)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
self.val = QLabel("-")
|
||||
self.val.setAlignment(Qt.AlignTop) # type: ignore
|
||||
layout.addWidget(self.val)
|
||||
self.unit = unit
|
||||
self.highlight = highlight
|
||||
self.decimals = decimals
|
||||
self.number = 0
|
||||
if highlight:
|
||||
font = QFont()
|
||||
font.setBold(True)
|
||||
font.setPointSize(14)
|
||||
self.label.setFont(font)
|
||||
self.val.setFont(font)
|
||||
|
||||
def value(self) -> float:
|
||||
return self.number
|
||||
|
||||
def setLabel(self, label) -> None:
|
||||
self.label.setText(label)
|
||||
|
||||
def setValue(self, number):
|
||||
self.number = number
|
||||
text = f"{number:.{int(self.decimals)}f}"
|
||||
if self.unit is not None:
|
||||
text = text + " " + self.unit
|
||||
self.val.setText(text)
|
||||
|
||||
|
||||
class InputNumberField(QWidget):
|
||||
def __init__(
|
||||
self,
|
||||
identifier="",
|
||||
label="",
|
||||
unit=None,
|
||||
prefix=None,
|
||||
init=0.0,
|
||||
decimals=1,
|
||||
single_step=0.1,
|
||||
ll=-1e6,
|
||||
hl=1e6,
|
||||
):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(*ROW_MARGINS)
|
||||
layout.setSpacing(ROW_SPACING)
|
||||
self.identifier = identifier
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(LABEL_WIDTH)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
self.val = QDoubleSpinBox()
|
||||
self.val.setRange(ll, hl)
|
||||
self.val.setDecimals(decimals)
|
||||
self.val.setSingleStep(single_step)
|
||||
self.val.setValue(init)
|
||||
if unit is not None:
|
||||
self.val.setSuffix(" " + unit)
|
||||
if prefix is not None:
|
||||
self.val.setPrefix(prefix + " ")
|
||||
layout.addWidget(self.val)
|
||||
|
||||
def set_number(self, number):
|
||||
self.val.setValue(number)
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
return self.val.hasFocus()
|
||||
|
||||
def value(self) -> float:
|
||||
return self.val.value()
|
||||
|
||||
def value_changed_connect(self, func):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.val.valueChanged.connect(
|
||||
partial(
|
||||
func, identifier=self.identifier, value_obj=self.val, value=lambda: self.val.value()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ComboBox(QWidget):
|
||||
def __init__(self, identifier="", label="", enums=None):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(*ROW_MARGINS)
|
||||
layout.setSpacing(ROW_SPACING)
|
||||
self.identifier = identifier
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(LABEL_WIDTH)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
self.value = QComboBox()
|
||||
for entry in enums or []:
|
||||
self.value.addItem(entry)
|
||||
layout.addWidget(self.value)
|
||||
|
||||
def set_current_text(self, text):
|
||||
self.value.setCurrentText(text)
|
||||
|
||||
def currentText(self) -> str:
|
||||
return self.value.currentText()
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
return QApplication.focusWidget() is self.value.view()
|
||||
|
||||
def activated_connect(self, func):
|
||||
"""Connect a function to the Enter/Return key press."""
|
||||
self.value.activated.connect(
|
||||
partial(
|
||||
func,
|
||||
identifier=self.identifier,
|
||||
value_obj=self.value,
|
||||
value=lambda: self.value.currentText(),
|
||||
)
|
||||
)
|
||||
|
||||
def setDisabled(self, disable):
|
||||
self.value.setDisabled(disable)
|
||||
|
||||
|
||||
class Button(QWidget):
|
||||
def __init__(self, label=None, label_button: str = "", enabled=False):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(*ROW_MARGINS)
|
||||
layout.setSpacing(ROW_SPACING)
|
||||
if label is not None:
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(LABEL_WIDTH)
|
||||
layout.addWidget(self.label)
|
||||
self.button = QPushButton(label_button)
|
||||
self.enable_button(enabled)
|
||||
layout.addWidget(self.button)
|
||||
|
||||
def clicked_connect(self, func):
|
||||
"""Connect a function to the button press."""
|
||||
self.button.clicked.connect(func)
|
||||
|
||||
def enable_button(self, enable: bool = False):
|
||||
if enable:
|
||||
self.button.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}"
|
||||
)
|
||||
self.button.setEnabled(True)
|
||||
else: # disabled
|
||||
self.button.setStyleSheet(
|
||||
"QPushButton {{background-color: rgb(120, 120, 120); color: white;}}"
|
||||
)
|
||||
self.button.setDisabled(True)
|
||||
|
||||
def setText(self, text):
|
||||
self.button.setText(text)
|
||||
|
||||
|
||||
class TextIndicator(QWidget):
|
||||
def __init__(self, label):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(*ROW_MARGINS)
|
||||
layout.setSpacing(ROW_SPACING)
|
||||
self.label = QLabel(label)
|
||||
self.label.setFixedWidth(LABEL_WIDTH)
|
||||
self.label.setWordWrap(True)
|
||||
layout.addWidget(self.label)
|
||||
self.text = QLabel("-")
|
||||
self.text.setAlignment(Qt.AlignTop) # type: ignore
|
||||
layout.addWidget(self.text)
|
||||
|
||||
def setLabel(self, label) -> None:
|
||||
self.label.setText(label)
|
||||
|
||||
def setText(self, text):
|
||||
self.text.setText(text)
|
||||
|
||||
def setColor(self, color: str):
|
||||
self.text.setStyleSheet(f"QLabel {{color:{color}}}")
|
||||
@@ -0,0 +1,50 @@
|
||||
cm_try:
|
||||
offset: 0.15
|
||||
|
||||
mo1_trx:
|
||||
modifier:
|
||||
axis: mo1_trx
|
||||
range: [[-30, -0.1], [0.1, 30]]
|
||||
offset: [-2.3, 1.31]
|
||||
|
||||
mo1_try:
|
||||
modifier:
|
||||
axis: mo1_trx
|
||||
range: [[-30, -0.1], [0.1, 30]]
|
||||
offset: [-1.78, -1.78]
|
||||
|
||||
sl1_centery:
|
||||
offset: -1.2
|
||||
|
||||
fm_trx:
|
||||
modifier:
|
||||
axis: fm_trx
|
||||
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
|
||||
offset: [-0.61, 0, 0, -0.16]
|
||||
|
||||
fm_try:
|
||||
modifier:
|
||||
axis: fm_trx
|
||||
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
|
||||
offset: [0.028, 0, 0, -0.45]
|
||||
|
||||
fm_rotx:
|
||||
modifier:
|
||||
axis: fm_trx
|
||||
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
|
||||
offset: [0.027, 0, 0, 0.045]
|
||||
|
||||
fm_roty:
|
||||
modifier:
|
||||
axis: fm_trx
|
||||
range: [[-66, -31], [-24, 7], [11, 31], [38, 66]]
|
||||
offset: [-0.038, 0, 0, -0.053]
|
||||
|
||||
sl2_centery:
|
||||
offset: -0.7
|
||||
|
||||
ot_try:
|
||||
offset: -0.49
|
||||
|
||||
ot_rotx:
|
||||
offset: 0
|
||||
@@ -0,0 +1,323 @@
|
||||
"""
|
||||
X01DA / Debye Beamline Parameters.
|
||||
This file describes the parameter of each component of the Debye beamline
|
||||
to be used for raytracing and geometrical calculations.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
import numpy as np
|
||||
import xrt.backends.raycing.materials as rm
|
||||
|
||||
# XRT definitions
|
||||
filterBeryl = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
|
||||
stripeSi = rm.Material("Si", rho=2.33) # pyright: ignore[reportArgumentType]
|
||||
stripePt = rm.Material("Pt", rho=21.45) # pyright: ignore[reportArgumentType]
|
||||
stripeRh = rm.Material("Rh", rho=12.41) # pyright: ignore[reportArgumentType]
|
||||
stripeCr = rm.Material("Cr", rho=7.14) # pyright: ignore[reportArgumentType]
|
||||
stripePyrex = rm.Material(
|
||||
"Si", rho=2.20
|
||||
) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType]
|
||||
|
||||
si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface
|
||||
si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface
|
||||
si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface
|
||||
si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface
|
||||
si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface
|
||||
si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface
|
||||
si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface
|
||||
si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface
|
||||
|
||||
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterBe = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterSi3N4 = rm.Material(
|
||||
["Si", "N"], quantities=[3, 4], rho=3.44, kind="plate"
|
||||
) # pyright: ignore[reportArgumentType]
|
||||
filterAl = rm.Material("Al", rho=2.69, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
|
||||
# General parameters
|
||||
sourceHeight = 0
|
||||
|
||||
# Synchrotron
|
||||
synchrotron = namedtuple(
|
||||
"synchrotron", ["eE", "eI", "eEspread", "eEpsilonX", "eEpsilonZ", "betaX", "betaZ"]
|
||||
)
|
||||
|
||||
sls1 = synchrotron(
|
||||
eE=2.4, eI=0.4, eEspread=0.878e-3, eEpsilonX=5.63, eEpsilonZ=0.007, betaX=0.45, betaZ=14.4
|
||||
)
|
||||
|
||||
sls2 = synchrotron(
|
||||
eE=2.7, eI=0.4, eEspread=1.147e-3, eEpsilonX=0.156, eEpsilonZ=0.01, betaX=0.18, betaZ=4.6
|
||||
)
|
||||
|
||||
# Source
|
||||
bendingMagnet = namedtuple("bendingMagnet", ["name", "center", "sync", "B0"])
|
||||
|
||||
sls1_14t = bendingMagnet(name="FE-BM-SLS1-1.4T", center=(0, 0, 0), sync=sls1, B0=1.4)
|
||||
|
||||
sls2_21t = bendingMagnet(name="FE-BM-SLS2-2.1T", center=(0, 0, 0), sync=sls2, B0=2.1)
|
||||
|
||||
sls2_35t = bendingMagnet(name="FE-BM-SLS2-3.5T", center=(0, 0, 0), sync=sls2, B0=3.5)
|
||||
|
||||
sls2_50t = bendingMagnet(name="FE-BM-SLS2-5.0T", center=(0, 0, 0), sync=sls2, B0=5.0)
|
||||
|
||||
# FE slits
|
||||
fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"])
|
||||
|
||||
feSlits = fe_slits(
|
||||
name="FE-SLITS",
|
||||
center=(0, 6117, sourceHeight),
|
||||
center1=(0, 5045, sourceHeight),
|
||||
center2=(0, 5289.5, sourceHeight),
|
||||
maxDivH=1.8e-3,
|
||||
maxDivV=0.8e-3,
|
||||
)
|
||||
|
||||
# FE Window
|
||||
filt = namedtuple(
|
||||
"filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"]
|
||||
)
|
||||
|
||||
feWindow = filt(
|
||||
name="FE-WINDOW",
|
||||
center=(0.0, 7020, sourceHeight),
|
||||
pitch=np.pi / 2,
|
||||
limPhysX=(-6, 6),
|
||||
limPhysY=(-3.0, 3.0),
|
||||
surface="None",
|
||||
material=filterDiamond,
|
||||
thickness=0.1,
|
||||
)
|
||||
feWindow = feWindow._replace(surface=f"CVD Diamond window {feWindow.thickness*1e3:0.0f} $\\mu$m")
|
||||
|
||||
# Collimating mirror
|
||||
collimatingMirror = namedtuple(
|
||||
"collimatingMirror",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"surface",
|
||||
"material",
|
||||
"limPhysX",
|
||||
"limPhysY",
|
||||
"limOptX",
|
||||
"limOptY",
|
||||
"R",
|
||||
"pitch",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx1",
|
||||
"tx2",
|
||||
],
|
||||
)
|
||||
|
||||
cm = collimatingMirror(
|
||||
name="FE-CM",
|
||||
center=[0, 6890, sourceHeight],
|
||||
surface=("Si", "Pt", "Rh"),
|
||||
material=(stripeSi, stripePt, stripeRh),
|
||||
limPhysX=(-34, 34),
|
||||
limPhysY=(-600, 600),
|
||||
limOptX=((-21, -7, 14), (-11, 11, 23)),
|
||||
limOptY=((-500, -500, -500), (500, 500, 500)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[-5.0e-3, -0.0e-3],
|
||||
jack1=[0.0, 7210.0, 0.0], # Tripod X, Y, Z (global)
|
||||
jack2=[-210.0, 8310.0, 0.0],
|
||||
jack3=[210.0, 8310.0, 0.0],
|
||||
tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local)
|
||||
tx2=[0.0, 575],
|
||||
) # X-Stage 2
|
||||
|
||||
apertures = namedtuple("apertures", ["name", "center", "opening"])
|
||||
|
||||
fePS = apertures(
|
||||
name="FE-PS", center=[0, 8815, sourceHeight], opening=[-20.0, 20.0, -20.0 + 12.5, 20.0 + 12.5]
|
||||
) # left, right, bottom, top
|
||||
|
||||
opWbBsBlock = apertures(
|
||||
name="OP-WB-BS-BLOCK", center=[0.0, 13860, sourceHeight], opening=[-18.0, 18.0, 25, 85.5]
|
||||
) # left, right, bottom, top
|
||||
# opening=[-18., 18., 42, 76], # X10DA
|
||||
|
||||
# Monochromator
|
||||
monochromator = namedtuple(
|
||||
"monochromator",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"xtal",
|
||||
"material1",
|
||||
"material2",
|
||||
"xtalWidth",
|
||||
"xtalOffsetX",
|
||||
"xtalLength1",
|
||||
"xtalLength2",
|
||||
"xtalGap",
|
||||
"rotOffset",
|
||||
"heightOffset",
|
||||
"braggLim",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx",
|
||||
],
|
||||
)
|
||||
|
||||
mo1 = monochromator(
|
||||
name="OP-MO1",
|
||||
center=[0.0, 11750, sourceHeight],
|
||||
xtal=("Si311", "Si111"),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth=(24, 24),
|
||||
xtalOffsetX=(-21.2, 21.2),
|
||||
xtalLength1=(55, 55),
|
||||
xtalLength2=(105, 105),
|
||||
xtalGap=(8, 8),
|
||||
rotOffset=6,
|
||||
heightOffset=8.5,
|
||||
braggLim=[3.6, 33],
|
||||
jack1=[0.0, 11350.0, 0.0], # Tripod maybe not available!
|
||||
jack2=[-400.0, 12350.0, 0.0],
|
||||
jack3=[400.0, 12350.0, 0.0],
|
||||
tx=0.0,
|
||||
) # X-Stage [x]
|
||||
|
||||
mo2 = monochromator(
|
||||
name="OP-CCM2",
|
||||
center=[0.0, 13250, sourceHeight],
|
||||
xtal=("Si311", "Si111"),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth=(24, 24),
|
||||
xtalOffsetX=(-21, 21),
|
||||
xtalLength1=(55, 55),
|
||||
xtalLength2=(105, 105),
|
||||
xtalGap=(8, 8),
|
||||
rotOffset=6,
|
||||
heightOffset=8.5,
|
||||
braggLim=[3.6, 33],
|
||||
jack1=[0.0, 13350.0, 0.0], # Tripod maybe not available!
|
||||
jack2=[-400.0, 14350.0, 0.0],
|
||||
jack3=[400.0, 14350.0, 0.0],
|
||||
tx=0.0,
|
||||
) # X-Stage [x]
|
||||
|
||||
# OP Slits
|
||||
op_slits = namedtuple("op_slits", ["name", "center"])
|
||||
|
||||
opSlits1 = op_slits(name="OP-SLITS 1", center=(0, 14349.6, sourceHeight))
|
||||
|
||||
opSlits2 = op_slits(name="OP-SLITS 2", center=(0, 18134.8, sourceHeight))
|
||||
|
||||
# OP Beam Monitors
|
||||
op_bm = namedtuple("op_bm", ["name", "center"])
|
||||
|
||||
opBM1 = op_bm(name="OP Beam Monitor 1", center=(0, 14599.6, sourceHeight))
|
||||
|
||||
opBM2 = op_bm(name="OP Beam Monitor 2", center=(0, 18384.8, sourceHeight))
|
||||
|
||||
# Focusing mirror
|
||||
focusingMirror = namedtuple(
|
||||
"focusingMirror",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"surfaceToroid",
|
||||
"materialToroid",
|
||||
"surfaceFlat",
|
||||
"materialFlat",
|
||||
"limPhysXToroid",
|
||||
"limPhysYToroid",
|
||||
"limPhysXFlat",
|
||||
"limPhysYFlat",
|
||||
"limOptXToroid",
|
||||
"limOptYToroid",
|
||||
"limOptXFlat",
|
||||
"limOptYFlat",
|
||||
"R",
|
||||
"pitch",
|
||||
"r",
|
||||
"xToroid",
|
||||
"xFlat",
|
||||
"hToroid",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx1",
|
||||
"tx2",
|
||||
],
|
||||
)
|
||||
|
||||
fm = focusingMirror(
|
||||
name="OP-FM",
|
||||
center=[0.0, 15670, sourceHeight], # nominal height 58 mm above ring, SLS1!
|
||||
surfaceToroid=("Rh", "Pt"),
|
||||
materialToroid=(stripeRh, stripePt),
|
||||
surfaceFlat=("Rh", "Pt"),
|
||||
materialFlat=(stripeRh, stripePt),
|
||||
limPhysXToroid=(-79.0, 79.0),
|
||||
limPhysYToroid=(-575.0, 575.0),
|
||||
limPhysXFlat=(-79.0, 79.0),
|
||||
limPhysYFlat=(-575.0, 575.0),
|
||||
limOptXToroid=((-38, 66), (-66, 31)),
|
||||
limOptYToroid=((-500.0, -500.0), (500.0, 500.0)),
|
||||
limOptXFlat=((-11.45, 23.55), (-30.45, -6.45)),
|
||||
limOptYFlat=((-500.0, -500.0), (500.0, 500.0)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[-5.0e-3, 0e-3],
|
||||
r=[35.510, 24.986],
|
||||
xToroid=[-52, 48.5], # offset in local x
|
||||
xFlat=[-20.95, 8.55],
|
||||
hToroid=[2.88, 7.15], # depth of the cylinder at x = xCylinder1 and x = xCylinder2.
|
||||
jack1=[-130.0, 15535 - 538.0, 0.0],
|
||||
jack2=[130.0, 15535 + 538.0, 0.0],
|
||||
jack3=[0.0, 15535 + 538.0, 0.0],
|
||||
tx1=[0.0, -575.0], # X-Stage 1 [x, y]
|
||||
tx2=[0.0, 575.0],
|
||||
) # X-Stage 2 [x, y]
|
||||
|
||||
# EH Window
|
||||
ehWindow = filt(
|
||||
name="EH-WINDOW",
|
||||
center=(0.0, 19998.3, sourceHeight),
|
||||
pitch=np.pi / 2,
|
||||
limPhysX=(-20.0, 20.0),
|
||||
limPhysY=(-4, 4),
|
||||
surface="None",
|
||||
material=filterSi3N4,
|
||||
thickness=0.002,
|
||||
)
|
||||
ehWindow = ehWindow._replace(surface=f"Beryllium window {ehWindow.thickness*1e3:0.0f} $\\mu$m")
|
||||
|
||||
# Sample
|
||||
sample = namedtuple("sample", ["name", "center"])
|
||||
|
||||
smpl = sample(name="EH-SMPL", center=[0, 23365, sourceHeight])
|
||||
|
||||
smpl2 = sample(name="EH-SMPL2", center=[0, 27500, sourceHeight])
|
||||
|
||||
tables = {}
|
||||
|
||||
# Vacuum pipes
|
||||
# DN40CF ID = 35 mm oder 37 mm
|
||||
# DN50CF ID = 47.5 mm
|
||||
# DN63CF ID = 60.2 mm oder 66 mm
|
||||
# DN100CF ID = 97.4 mm oder 104 mm
|
||||
pipe = namedtuple("pipes", ["center", "diameter", "start", "end"])
|
||||
vacuum_pipes = pipe(
|
||||
center=[27.5, (37.5 + 27.5) / 2, 37.5, 62.5, 72.5],
|
||||
diameter=[97.4, 97.4, 97.4, 97.4, 97.4],
|
||||
start=[10952.88, 11750 + 250, mo2.center[1] + 250, 14000, fm.center[1]],
|
||||
end=[11750 - 250, mo2.center[1] - 250, 14000, fm.center[1], ehWindow.center[1]],
|
||||
)
|
||||
|
||||
Walls = namedtuple("walls", ["start", "end", "height"])
|
||||
walls = Walls(start=[13999.30], end=[13999 + 75.5 + 30], height=[[-20, 25]])
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
cm_try:
|
||||
offset: -0.7
|
||||
|
||||
mo1_try:
|
||||
offset: -31.42
|
||||
|
||||
mo1_trx:
|
||||
modifier:
|
||||
axis: mo1_trx
|
||||
range: [[-30, -0.1], [0.1, 30]]
|
||||
offset: [-4.3, 0]
|
||||
|
||||
sl1_centery:
|
||||
offset: -55.54
|
||||
|
||||
bm1_try:
|
||||
offset: 52.22
|
||||
|
||||
fm_trx:
|
||||
modifier:
|
||||
axis: fm_trx
|
||||
range: [[-100, -48], [-47, 0]]
|
||||
offset: [-0.3, 0.52]
|
||||
|
||||
fm_try:
|
||||
modifier:
|
||||
axis: fm_trx
|
||||
range: [[-100, -48], [-47, 0]]
|
||||
offset: [-42.56, -41.49]
|
||||
|
||||
# pitch
|
||||
fm_rotx:
|
||||
modifier:
|
||||
axis: fm_trx
|
||||
range: [[-100, -48], [-47, 0]]
|
||||
offset: [1.30, 1.049]
|
||||
|
||||
# yaw
|
||||
fm_roty:
|
||||
modifier:
|
||||
axis: fm_trx
|
||||
range: [[-100, -48], [-47, 0]]
|
||||
offset: [1.754, 1.924]
|
||||
|
||||
bm2_try:
|
||||
offset: -19
|
||||
|
||||
es0wi_try:
|
||||
offset: -71.98
|
||||
|
||||
es1_try:
|
||||
offset: -113.26
|
||||
|
||||
es1ic1_try:
|
||||
offset: 10.39
|
||||
|
||||
es1ic2_try:
|
||||
offset: 3.55
|
||||
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
X10DA / SuperXAS Beamline Parameters.
|
||||
This file describes the parameter of each component of the SuperXAS beamline
|
||||
to be used for raytracing and geometrical calculations.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
import numpy as np
|
||||
import xrt.backends.raycing.materials as rm
|
||||
|
||||
# XRT definitions
|
||||
filterBeryl = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
|
||||
stripeSi = rm.Material("Si", rho=2.33) # pyright: ignore[reportArgumentType]
|
||||
stripePt = rm.Material("Pt", rho=21.45) # pyright: ignore[reportArgumentType]
|
||||
stripeRh = rm.Material("Rh", rho=12.41) # pyright: ignore[reportArgumentType]
|
||||
stripeCr = rm.Material("Cr", rho=7.14) # pyright: ignore[reportArgumentType]
|
||||
stripePyrex = rm.Material(
|
||||
"Si", rho=2.20
|
||||
) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType]
|
||||
|
||||
si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface
|
||||
si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface
|
||||
si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface
|
||||
si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface
|
||||
si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface
|
||||
si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface
|
||||
si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface
|
||||
si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface
|
||||
|
||||
filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterBe = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterSi3N4 = rm.Material(
|
||||
["Si", "N"], quantities=[3, 4], rho=3.44, kind="plate"
|
||||
) # pyright: ignore[reportArgumentType]
|
||||
filterAl = rm.Material("Al", rho=2.69, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType]
|
||||
|
||||
# General parameters
|
||||
sourceHeight = 0
|
||||
|
||||
# Synchrotron
|
||||
synchrotron = namedtuple(
|
||||
"synchrotron", ["eE", "eI", "eEspread", "eEpsilonX", "eEpsilonZ", "betaX", "betaZ"]
|
||||
)
|
||||
|
||||
sls1 = synchrotron(
|
||||
eE=2.4, eI=0.4, eEspread=0.878e-3, eEpsilonX=5.63, eEpsilonZ=0.007, betaX=0.45, betaZ=14.4
|
||||
)
|
||||
|
||||
sls2 = synchrotron(
|
||||
eE=2.7, eI=0.4, eEspread=1.147e-3, eEpsilonX=0.156, eEpsilonZ=0.01, betaX=0.18, betaZ=4.6
|
||||
)
|
||||
|
||||
# Source
|
||||
bendingMagnet = namedtuple("bendingMagnet", ["name", "center", "sync", "B0"])
|
||||
|
||||
sls1_14t = bendingMagnet(name="FE-BM-SLS1-1.4T", center=(0, 0, 0), sync=sls1, B0=1.4)
|
||||
|
||||
sls2_21t = bendingMagnet(name="FE-BM-SLS2-2.1T", center=(0, 0, 0), sync=sls2, B0=2.1)
|
||||
|
||||
sls2_35t = bendingMagnet(name="FE-BM-SLS2-3.5T", center=(0, 0, 0), sync=sls2, B0=3.5)
|
||||
|
||||
sls2_50t = bendingMagnet(name="FE-BM-SLS2-5.0T", center=(0, 0, 0), sync=sls2, B0=5.0)
|
||||
|
||||
# FE slits
|
||||
fe_slits = namedtuple("slits", ["name", "center", "center1", "center2", "maxDivH", "maxDivV"])
|
||||
|
||||
feSlits = fe_slits(
|
||||
name="FE-SLITS",
|
||||
center=(0, 6117, sourceHeight),
|
||||
center1=(0, 5038.4, sourceHeight),
|
||||
center2=(0, 5282.9, sourceHeight),
|
||||
maxDivH=1.8e-3,
|
||||
maxDivV=0.8e-3,
|
||||
)
|
||||
|
||||
# Filters
|
||||
filt = namedtuple(
|
||||
"filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"]
|
||||
)
|
||||
|
||||
feWindow = filt(
|
||||
name="FE-WINDOW",
|
||||
center=(0.0, 6158, sourceHeight),
|
||||
pitch=np.pi / 2,
|
||||
limPhysX=(-6, 6),
|
||||
limPhysY=(-3.0, 3.0),
|
||||
surface="None",
|
||||
material=filterDiamond,
|
||||
thickness=0.1,
|
||||
)
|
||||
feWindow = feWindow._replace(
|
||||
surface="CVD Diamond window {0:0.0f} $\\mu$m".format(feWindow.thickness * 1e3)
|
||||
)
|
||||
|
||||
feFilt = filt(
|
||||
name="FE-FI",
|
||||
center=(0.0, 6590, sourceHeight),
|
||||
pitch=np.pi / 2,
|
||||
limPhysX=(-15, 15),
|
||||
limPhysY=(-10, 10),
|
||||
surface="None",
|
||||
material=filterGraphite,
|
||||
thickness=0.25,
|
||||
)
|
||||
feFilt = feFilt._replace(surface="Graphite filter {0:0.0f} $\\mu$m".format(feFilt.thickness * 1e3))
|
||||
|
||||
# Collimating mirror
|
||||
collimatingMirror = namedtuple(
|
||||
"collimatingMirror",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"surface",
|
||||
"material",
|
||||
"limPhysX",
|
||||
"limPhysY",
|
||||
"limOptX",
|
||||
"limOptY",
|
||||
"R",
|
||||
"pitch",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx1",
|
||||
"tx2",
|
||||
],
|
||||
)
|
||||
|
||||
cm = collimatingMirror(
|
||||
name="FE-CM",
|
||||
center=[0, 7560.8, sourceHeight],
|
||||
surface=("Pt", "Si", "Rh"),
|
||||
material=(stripePt, stripeSi, stripeRh),
|
||||
limPhysX=(-30, 30),
|
||||
limPhysY=(-600, 600),
|
||||
limOptX=((-21, -0.5, 11), (-4, 9.5, 23)),
|
||||
limOptY=((-500, -500, -500), (500, 500, 500)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[1.4e-3, 4.5e-3],
|
||||
jack1=[0.0, 7210.0, 0.0], # Tripod X, Y, Z (global)
|
||||
jack2=[-210.0, 8310.0, 0.0],
|
||||
jack3=[210.0, 8310.0, 0.0],
|
||||
tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local)
|
||||
tx2=[0.0, 575],
|
||||
) # X-Stage 2
|
||||
|
||||
apertures = namedtuple("apertures", ["name", "center", "opening"])
|
||||
|
||||
fePS = apertures(
|
||||
name="FE-PS", center=[0, 8760, sourceHeight], opening=[-39 / 2, 39 / 2, -10, 29]
|
||||
) # left, right, bottom, top
|
||||
|
||||
opWbBsBlock = apertures(
|
||||
name="OP-WB-BS-BLOCK", center=[0.0, 13606 - 135, sourceHeight], opening=[-18.0, 18.0, 42, 76]
|
||||
) # left, right, bottom, top
|
||||
|
||||
opSlits1 = apertures(
|
||||
name="OP-SLITS 1", center=[0, 14145 - 135, sourceHeight], opening=[-35 / 2, 35 / 2, 47.5, 82.5]
|
||||
)
|
||||
|
||||
# OP Beam Monitors
|
||||
op_bm = namedtuple("op_bm", ["name", "center"])
|
||||
|
||||
opBM1 = op_bm(name="OP Beam Monitor 1", center=(0, 14525 - 135, sourceHeight))
|
||||
|
||||
opBM2 = op_bm(name="OP Beam Monitor 2", center=(0, 17161.6 - 135, sourceHeight))
|
||||
|
||||
# Monochromator
|
||||
monochromator = namedtuple(
|
||||
"monochromator",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"xtal",
|
||||
"material1",
|
||||
"material2",
|
||||
"xtalWidth",
|
||||
"xtalOffsetX",
|
||||
"xtalLength1",
|
||||
"xtalLength2",
|
||||
"xtalGap",
|
||||
"rotOffset",
|
||||
"heightOffset",
|
||||
"braggLim",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx",
|
||||
],
|
||||
)
|
||||
|
||||
mo1 = monochromator(
|
||||
name="OP-CCM1",
|
||||
center=[0.0, 11670 - 135, sourceHeight],
|
||||
xtal=("Si311", "Si111"),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth=(20, 20),
|
||||
xtalOffsetX=(19.2, -19.2),
|
||||
xtalLength1=(60, 60),
|
||||
xtalLength2=(60, 60),
|
||||
xtalGap=(8, 8),
|
||||
rotOffset=6, # not sure what it is
|
||||
heightOffset=8.5, # not sure what it is
|
||||
braggLim=[4, 35],
|
||||
jack1=[0.0, 11350.0, 0.0], # Tripod not available!
|
||||
jack2=[-400.0, 12350.0, 0.0],
|
||||
jack3=[400.0, 12350.0, 0.0],
|
||||
tx=0.0,
|
||||
) # X-Stage [x]
|
||||
|
||||
# Focusing mirror
|
||||
focusingMirror = namedtuple(
|
||||
"focusingMirror",
|
||||
[
|
||||
"name",
|
||||
"center",
|
||||
"surfaceToroid",
|
||||
"materialToroid",
|
||||
"limPhysXToroid",
|
||||
"limPhysYToroid",
|
||||
"limOptXToroid",
|
||||
"limOptYToroid",
|
||||
"R",
|
||||
"pitch",
|
||||
"r",
|
||||
"xToroid",
|
||||
"hToroid",
|
||||
"jack1",
|
||||
"jack2",
|
||||
"jack3",
|
||||
"tx1",
|
||||
"tx2",
|
||||
],
|
||||
)
|
||||
|
||||
OFFSET_TRX = 46.8735
|
||||
|
||||
fm = focusingMirror(
|
||||
name="OP-FM",
|
||||
center=[0.0, 15580 - 135, sourceHeight],
|
||||
surfaceToroid=("Rh", "Pt"),
|
||||
materialToroid=(stripeRh, stripePt),
|
||||
limPhysXToroid=(-54.0, 54.0),
|
||||
limPhysYToroid=(-565.0, 565.0),
|
||||
limOptXToroid=(
|
||||
(43.388 + OFFSET_TRX, -4.865 + OFFSET_TRX),
|
||||
(4.865 + OFFSET_TRX, -40.882 + OFFSET_TRX),
|
||||
),
|
||||
limOptYToroid=((-500.0, -500.0), (500.0, 500.0)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[1.4e-3, 4.5e-3],
|
||||
r=[30, 20],
|
||||
xToroid=[24.126 + OFFSET_TRX, -22 + OFFSET_TRX], # offset in local x
|
||||
hToroid=[7.0, 11.3], # depth of the cylinder at x = xCylinder1 and x = xCylinder2.
|
||||
jack1=[0.0, 14980.0, 0.0],
|
||||
jack2=[-75.0, 16180.0, 0.0],
|
||||
jack3=[75.0, 16180.0, 0.0],
|
||||
tx1=[0.0, -575.0], # X-Stage 1 [x, y]
|
||||
tx2=[0.0, 575.0],
|
||||
) # X-Stage 2 [x, y]
|
||||
|
||||
# Entry wall experimental hutch: 21593 mm from source (SLS2)
|
||||
|
||||
# Exit window
|
||||
ehWindow = filt(
|
||||
name="EH-WINDOW",
|
||||
center=(0.0, 22063, sourceHeight),
|
||||
pitch=np.pi / 2,
|
||||
limPhysX=(-10.0, 10.0),
|
||||
limPhysY=(17.5, 92.5),
|
||||
surface="None",
|
||||
material=filterBe,
|
||||
thickness=0.25,
|
||||
)
|
||||
ehWindow = ehWindow._replace(
|
||||
surface="Beryllium window {0:0.0f} $\\mu$m".format(ehWindow.thickness * 1e3)
|
||||
)
|
||||
|
||||
# Sample
|
||||
sample = namedtuple("sample", ["name", "center"])
|
||||
|
||||
es1 = sample(name="ES1", center=[0, 23823, sourceHeight])
|
||||
es2 = sample(name="ES2", center=[0, 25843, sourceHeight])
|
||||
|
||||
# Ionization chambers
|
||||
ic = namedtuple("sample", ["name", "center"])
|
||||
|
||||
es1ic0 = ic(name="ES1 IC0", center=[0, 23633, sourceHeight])
|
||||
es1ic1 = ic(name="ES1 IC1", center=[0, 24383, sourceHeight])
|
||||
es1ic2 = ic(name="ES1 IC2", center=[0, 24723, sourceHeight])
|
||||
@@ -1,239 +0,0 @@
|
||||
"""
|
||||
X10DA / SuperXAS Beamline Parameters.
|
||||
This file describes the parameter of each component of the SuperXAS beamline
|
||||
to be used for raytracing and geometrical calculations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
from collections import namedtuple
|
||||
|
||||
import xrt.backends.raycing.materials as rm
|
||||
|
||||
# if os.environ.get("USE_XRT", "True").lower() in ("1", "true", "yes"):
|
||||
# import xrt.backends.raycing.materials as rm # type: ignore
|
||||
# else:
|
||||
# class _DummyClass:
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# pass
|
||||
# class _DummyMaterials:
|
||||
# Material = _DummyClass
|
||||
# CrystalSi = _DummyClass
|
||||
# rm = _DummyMaterials()
|
||||
|
||||
# XRT definitions
|
||||
filterBeryl = rm.Material('Be', rho=1.85, kind='plate')
|
||||
filterDiamond = rm.Material('C', rho=3.52, kind='plate')
|
||||
filterGraphite = rm.Material('C', rho=2.266, kind='plate')
|
||||
|
||||
stripeSi = rm.Material('Si', rho=2.33)
|
||||
stripePt = rm.Material('Pt', rho=21.45)
|
||||
stripeRh = rm.Material('Rh', rho=12.41)
|
||||
stripeCr = rm.Material('Cr', rho=7.14)
|
||||
stripePyrex = rm.Material('Si', rho=2.20) # Use Si as bare element and the density of SiO2
|
||||
|
||||
si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface
|
||||
si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface
|
||||
si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface
|
||||
si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface
|
||||
si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface
|
||||
si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface
|
||||
si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface
|
||||
si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface
|
||||
|
||||
filterDiamond = rm.Material('C', rho=3.52, kind='plate')
|
||||
filterBe = rm.Material('Be', rho=1.85, kind='plate')
|
||||
filterSi3N4 = rm.Material(['Si', 'N'], quantities=[3, 4], rho=3.44, kind='plate')
|
||||
filterAl = rm.Material('Al', rho=2.69, kind='plate')
|
||||
filterGraphite = rm.Material('C', rho=2.266, kind='plate')
|
||||
|
||||
sourceHeight = 0
|
||||
|
||||
#Synchrotron
|
||||
synchrotron = namedtuple('synchrotron', ['eE', 'eI', 'eEspread',
|
||||
'eEpsilonX', 'eEpsilonZ', 'betaX', 'betaZ'])
|
||||
|
||||
sls1 = synchrotron(
|
||||
eE = 2.4,
|
||||
eI = 0.4,
|
||||
eEspread=0.878e-3,
|
||||
eEpsilonX=5.63,
|
||||
eEpsilonZ=0.007,
|
||||
betaX=0.45,
|
||||
betaZ=14.4,
|
||||
)
|
||||
|
||||
sls2 = synchrotron(
|
||||
eE=2.7,
|
||||
eI=0.4,
|
||||
eEspread=1.147e-3,
|
||||
eEpsilonX=0.156,
|
||||
eEpsilonZ=0.01,
|
||||
betaX=0.18,
|
||||
betaZ=4.6,
|
||||
)
|
||||
|
||||
# Source
|
||||
bendingMagnet = namedtuple('bendingMagnet', ['name', 'center', 'sync', 'B0'])
|
||||
|
||||
sls1_29t = bendingMagnet(
|
||||
name='FE-BM-SLS1-2.9T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls1,
|
||||
B0=2.9,)
|
||||
|
||||
sls2_21t = bendingMagnet(
|
||||
name='FE-BM-SLS2-2.1T',
|
||||
center=(0, 0, 0),
|
||||
sync=sls1,
|
||||
B0=2.1,)
|
||||
|
||||
# FE slits
|
||||
slits = namedtuple('slits', ['name', 'center', 'maxDivH', 'maxDivV'])
|
||||
|
||||
feSlits = slits(
|
||||
name='FE-SLITS',
|
||||
center=(0, 5290, sourceHeight),
|
||||
maxDivH=1.8e-3,
|
||||
maxDivV=0.8e-3,)
|
||||
|
||||
# Filters
|
||||
filt = namedtuple('filt', ['name', 'center', 'pitch', 'limPhysX', 'limPhysY', 'surface', 'material', 'thickness'])
|
||||
|
||||
feWindow = filt(
|
||||
name='FE-WINDOW',
|
||||
center=(0., 6158, sourceHeight),
|
||||
pitch=np.pi/2,
|
||||
limPhysX=(-6, 6),
|
||||
limPhysY=(-3., 3.),
|
||||
surface='None',
|
||||
material=filterDiamond,
|
||||
thickness=0.1,)
|
||||
feWindow = feWindow._replace(surface='CVD Diamond window {0:0.0f} $\mu$m'.format(feWindow.thickness*1e3))
|
||||
|
||||
feFilt = filt(
|
||||
name='FE-FI',
|
||||
center=(0., 6590, sourceHeight),
|
||||
pitch=np.pi/2,
|
||||
limPhysX=(-15, 15),
|
||||
limPhysY=(-10, 10),
|
||||
surface='None',
|
||||
material=filterGraphite,
|
||||
thickness=0.25,)
|
||||
feFilt = feFilt._replace(surface='Graphite filter {0:0.0f} $\mu$m'.format(feFilt.thickness*1e3))
|
||||
|
||||
# Collimating mirror
|
||||
collimatingMirror = namedtuple('collimatingMirror', ['name',
|
||||
'center', 'surface', 'material', 'limPhysX', 'limPhysY',
|
||||
'limOptX', 'limOptY', 'R', 'pitch', 'jack1', 'jack2', 'jack3',
|
||||
'tx1', 'tx2'])
|
||||
|
||||
cm = collimatingMirror(
|
||||
name='FE-CM',
|
||||
center=[0, 7618, sourceHeight],
|
||||
surface=('Rh','Si','Pt'),
|
||||
material=(stripeRh, stripeSi, stripePt),
|
||||
limPhysX=(-30, 30),
|
||||
limPhysY=(-600, 600),
|
||||
limOptX=((11, -2, -21), (21, 8, -5)),
|
||||
limOptY=((-500, -500, -500), (500, 500, 500)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[1.4e-3, 4.5e-3],
|
||||
jack1=[0., 7210., 0.], #Tripod X, Y, Z (global)
|
||||
jack2=[-210., 8310., 0.],
|
||||
jack3=[210., 8310., 0.],
|
||||
tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local)
|
||||
tx2=[0.0, 575],) # X-Stage 2
|
||||
|
||||
apertures = namedtuple('apertures', ['name', 'center', 'opening'])
|
||||
|
||||
fePS = apertures(
|
||||
name='FE-PS',
|
||||
center=[0, 8760, sourceHeight],
|
||||
opening=[-39/2, 39/2, -10, 29]) # left, right, bottom, top
|
||||
|
||||
opWbBsBlock = apertures(
|
||||
name='OP-WB-BS-BLOCK',
|
||||
center=[0., 13606-135, sourceHeight],
|
||||
opening=[-18., 18., 42, 76]) # left, right, bottom, top
|
||||
|
||||
opSlits = apertures(
|
||||
name='OP-SLITS',
|
||||
center=[0, 14145-135, sourceHeight],
|
||||
opening=[-35/2, 35/2, 47.5, 82.5])
|
||||
|
||||
# Monochromator
|
||||
monochromator = namedtuple('monochromator', ['name', 'center',
|
||||
'xtal', 'material1', 'material2', 'xtalWidth', 'xtalOffsetX',
|
||||
'xtalLength1', 'xtalLength2', 'xtalGap', 'rotOffset',
|
||||
'heightOffset', 'braggLim', 'jack1', 'jack2', 'jack3', 'tx'])
|
||||
|
||||
mo1 = monochromator(
|
||||
name='OP-CCM1',
|
||||
center=[0., 11670-135, sourceHeight],
|
||||
xtal=('Si311','Si111'),
|
||||
material1=(si311_1, si111_1),
|
||||
material2=(si311_2, si111_2),
|
||||
xtalWidth = (20, 20),
|
||||
xtalOffsetX=(-19.2, 19.2),
|
||||
xtalLength1 = (60, 60),
|
||||
xtalLength2 = (60, 60),
|
||||
xtalGap = (8, 8),
|
||||
rotOffset = 6, # not sure what it is
|
||||
heightOffset = 8.5, # not sure what it is
|
||||
braggLim = [4, 35],
|
||||
jack1=[0., 11350., 0.], #Tripod not available!
|
||||
jack2=[-400., 12350., 0.],
|
||||
jack3=[400., 12350., 0.],
|
||||
tx=0.0,) # X-Stage [x]
|
||||
|
||||
# Focusing mirror
|
||||
focusingMirror = namedtuple('focusingMirror', ['name', 'center',
|
||||
'surfaceToroid', 'materialToroid',
|
||||
'limPhysXToroid', 'limPhysYToroid',
|
||||
'limOptXToroid', 'limOptYToroid',
|
||||
'R', 'pitch', 'r', 'xToroid', 'hToroid', 'jack1', 'jack2', 'jack3',
|
||||
'tx1', 'tx2'])
|
||||
|
||||
fm = focusingMirror(
|
||||
name='OP-FM',
|
||||
center=[0., 15580-135, sourceHeight],
|
||||
surfaceToroid=('Rh', 'Pt'),
|
||||
materialToroid=(stripeRh, stripePt),
|
||||
limPhysXToroid=(-54., 54.),
|
||||
limPhysYToroid=(-565., 565.),
|
||||
limOptXToroid=((90.25, 41.75), (51.75, 5.75)), # With old VME axis, no absolute value!
|
||||
limOptYToroid=((-500., -500.), (500., 500.)),
|
||||
R=[3e6, 15e6],
|
||||
pitch=[1.4e-3, 4.5e-3],
|
||||
r=[30, 20],
|
||||
xToroid=[24.126, -22,874], # offset in local x
|
||||
hToroid=[7., 11.3], # depth of the cylinder at x = xCylinder1 and x = xCylinder2.
|
||||
jack1=[0., 14980., 0.],
|
||||
jack2=[-75., 16180., 0.],
|
||||
jack3=[75., 16180., 0.],
|
||||
tx1=[0., -575.], # X-Stage 1 [x, y]
|
||||
tx2=[0., 575.],) # X-Stage 2 [x, y]
|
||||
|
||||
ehWindow = filt(
|
||||
name='EH-WINDOW',
|
||||
center=(0., 22225-135, sourceHeight),
|
||||
pitch=np.pi/2,
|
||||
limPhysX=(-10., 10.),
|
||||
limPhysY=(17.5, 92.5),
|
||||
surface='None',
|
||||
material=filterBe,
|
||||
thickness=0.25,)
|
||||
ehWindow = ehWindow._replace(surface='Beryllium window {0:0.0f} $\mu$m'.format(ehWindow.thickness*1e3))
|
||||
|
||||
# Sample
|
||||
sample = namedtuple('sample', ['name', 'center'])
|
||||
|
||||
smpl = sample(
|
||||
name='OP-SMPL',
|
||||
center=[0, 24000-135, sourceHeight],)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Optical Table ES1 ##
|
||||
###################################
|
||||
|
||||
es1ot_trx:
|
||||
es1_trx:
|
||||
readoutPriority: baseline
|
||||
description: ES1 Table X Translation
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
@@ -12,7 +12,7 @@ es1ot_trx:
|
||||
enabled: true
|
||||
softwareTrigger: false
|
||||
|
||||
es1ot_try:
|
||||
es1_try:
|
||||
readoutPriority: baseline
|
||||
description: ES1 Table Y Translation
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
@@ -26,7 +26,7 @@ es1ot_try:
|
||||
## Exit Window ##
|
||||
###################################
|
||||
|
||||
eswi_try:
|
||||
es0wi_try:
|
||||
readoutPriority: baseline
|
||||
description: End Station 0 Exit Window Y-translation
|
||||
deviceClass: ophyd_devices.EpicsMotor
|
||||
@@ -112,4 +112,28 @@ es1man_roty:
|
||||
prefix: X10DA-ES1-MAN:ROTY
|
||||
enabled: true
|
||||
onFailure: retry
|
||||
softwareTrigger: false
|
||||
|
||||
###################################
|
||||
## Optical Table ES2 ##
|
||||
###################################
|
||||
|
||||
es2_trx:
|
||||
readoutPriority: baseline
|
||||
description: ES2 Table X Translation
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X10DA-ES2-ET2:TRX
|
||||
onFailure: retry
|
||||
enabled: true
|
||||
softwareTrigger: false
|
||||
|
||||
es2_try:
|
||||
readoutPriority: baseline
|
||||
description: ES2 Table Y Translation
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X10DA-ES2-ET2:TRY
|
||||
onFailure: retry
|
||||
enabled: true
|
||||
softwareTrigger: false
|
||||
@@ -13,9 +13,11 @@ from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
|
||||
|
||||
class AbsorberError(Exception):
|
||||
"""Absorber specific exception"""
|
||||
|
||||
|
||||
class STATUS(int, enum.Enum):
|
||||
"""Absorber States"""
|
||||
|
||||
@@ -35,14 +37,24 @@ class STATUS(int, enum.Enum):
|
||||
MAN_OPEN = 13
|
||||
UNDEFINED = 14
|
||||
|
||||
|
||||
class Absorber(PSIDeviceBase):
|
||||
"""Class for the Frontend Absorber"""
|
||||
|
||||
USER_ACCESS = ["open", "close"]
|
||||
|
||||
request = Cpt(EpicsSignal, suffix="REQUEST", kind="config", doc="Open/Close Absorber")
|
||||
status = Cpt(EpicsSignalRO, suffix="STATUS", kind="config", doc="Absorber Status")
|
||||
status_string = Cpt(EpicsSignalRO, suffix="STATUS", kind="config", string=True, doc="Absorber Status")
|
||||
status = Cpt(
|
||||
EpicsSignalRO, suffix="STATUS", kind="normal", auto_monitor=True, doc="Absorber Status"
|
||||
)
|
||||
status_string = Cpt(
|
||||
EpicsSignalRO,
|
||||
suffix="STATUS",
|
||||
kind="normal",
|
||||
auto_monitor=True,
|
||||
string=True,
|
||||
doc="Absorber Status",
|
||||
)
|
||||
|
||||
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
|
||||
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
|
||||
|
||||
@@ -236,6 +236,8 @@ class Mo1BraggPositioner(Device, PositionerBase):
|
||||
high_lim = Cpt(EpicsSignalRO, suffix="hi_lim_pos_energy_RBV", kind="config", auto_monitor=True)
|
||||
velocity = Cpt(EpicsSignalWithRBV, suffix="move_velocity", kind="config", auto_monitor=True)
|
||||
|
||||
angle = Cpt(EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True)
|
||||
|
||||
########## Move Command PVs ##########
|
||||
|
||||
move_abs = Cpt(EpicsSignal, suffix="move_abs", kind="config", put_complete=True)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from bec_server.file_writer.default_writer import DefaultFormat
|
||||
|
||||
import superxas_bec.bec_widgets.widgets.x10da_parameters as bl
|
||||
import superxas_bec.bec_widgets.widgets.digital_twin.x10da_parameters as bl
|
||||
|
||||
|
||||
class SuperXASNexusStructure(DefaultFormat):
|
||||
"""Nexus Structure for SuperXAS"""
|
||||
@@ -31,8 +32,7 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
|
||||
if "curr" in self.device_manager.devices:
|
||||
ring_current = source.create_soft_link(
|
||||
name="ring_current",
|
||||
target="/entry/collection/devices/curr/curr/value",
|
||||
name="ring_current", target="/entry/collection/devices/curr/curr/value"
|
||||
)
|
||||
ring_current.attrs["NX_class"] = "NX_FLOAT"
|
||||
ring_current.attrs["units"] = "mA"
|
||||
@@ -57,12 +57,12 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
name="reflection",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_xtal_string/value",
|
||||
)
|
||||
reflection.attrs["NX_class"] = "NX_CHAR"
|
||||
reflection.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
# Create a softlink
|
||||
d_spacing = crystal.create_soft_link(
|
||||
name="d_spacing",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value",
|
||||
name="d_spacing",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_d_spacing/value",
|
||||
)
|
||||
d_spacing.attrs["NX_class"] = "NX_FLOAT"
|
||||
d_spacing.attrs["units"] = "angstrom"
|
||||
@@ -78,23 +78,22 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
name="phi_offset",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_phi_off/value",
|
||||
)
|
||||
phi_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
phi_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
phi_offset.attrs["units"] = "degree"
|
||||
|
||||
azm_offset = crystal.create_soft_link(
|
||||
name="azm_offset",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_azm_off/value",
|
||||
)
|
||||
azm_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
azm_offset.attrs["NX_class"] = "NX_FLOAT"
|
||||
azm_offset.attrs["units"] = "degree"
|
||||
|
||||
miscut = crystal.create_soft_link(
|
||||
name="miscut",
|
||||
target="/entry/collection/devices/mo1_bragg/mo1_bragg_crystal_current_miscut/value",
|
||||
)
|
||||
miscut.attrs["NX_class"] = "NX_FLOAT"
|
||||
miscut.attrs["units"] = "degree"
|
||||
|
||||
miscut.attrs["NX_class"] = "NX_FLOAT"
|
||||
miscut.attrs["units"] = "degree"
|
||||
|
||||
###################
|
||||
### cm mirror specific information
|
||||
@@ -108,7 +107,7 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
)
|
||||
cm_substrate_material.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
#previous error due to space in name field
|
||||
# previous error due to space in name field
|
||||
|
||||
if "cm_bnd" in self.device_manager.devices:
|
||||
cm_bending = collimating_mirror.create_soft_link(
|
||||
@@ -139,15 +138,15 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
cm_roll_angle.attrs["NX_class"] = "NX_FLOAT"
|
||||
cm_roll_angle.attrs["units"] = "mrad"
|
||||
|
||||
if 'cm_trx' in self.device_manager.devices:
|
||||
cm_trx = - self.device_manager.devices.cm_trx.read(cached=True).get('cm_trx').get('value')
|
||||
stripe = 'Unknown'
|
||||
if "cm_trx" in self.device_manager.devices:
|
||||
cm_trx = (
|
||||
-self.device_manager.devices.cm_trx.read(cached=True).get("cm_trx").get("value")
|
||||
)
|
||||
stripe = "Unknown"
|
||||
for name, low, high in zip(bl.cm.surface, bl.cm.limOptX[0], bl.cm.limOptX[1]):
|
||||
if low <= cm_trx <= high:
|
||||
stripe = name
|
||||
cm_stripe = collimating_mirror.create_dataset(
|
||||
name="stripe", data=stripe
|
||||
)
|
||||
cm_stripe = collimating_mirror.create_dataset(name="stripe", data=stripe)
|
||||
cm_stripe.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
###################
|
||||
@@ -157,9 +156,7 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
focusing_mirror = instrument.create_group(name="focusing_mirror")
|
||||
focusing_mirror.attrs["NX_class"] = "NXmirror"
|
||||
|
||||
fm_substrate_material = focusing_mirror.create_dataset(
|
||||
name="substrate_material", data="Si"
|
||||
)
|
||||
fm_substrate_material = focusing_mirror.create_dataset(name="substrate_material", data="Si")
|
||||
fm_substrate_material.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if "fm_bnd" in self.device_manager.devices:
|
||||
@@ -191,15 +188,17 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
fm_roll_angle.attrs["NX_class"] = "NX_FLOAT"
|
||||
fm_roll_angle.attrs["units"] = "mrad"
|
||||
|
||||
if 'fm_trx' in self.device_manager.devices:
|
||||
fm_trx = - self.device_manager.devices.fm_trx.read(cached=True).get('fm_trx').get('value')
|
||||
stripe = 'Unknown'
|
||||
for name, low, high in zip(bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]):
|
||||
if low <= fm_trx <= high:
|
||||
stripe = name + ' (toroid)'
|
||||
fm_stripe = focusing_mirror.create_dataset(
|
||||
name="stripe", data=stripe
|
||||
if "fm_trx" in self.device_manager.devices:
|
||||
fm_trx = (
|
||||
-self.device_manager.devices.fm_trx.read(cached=True).get("fm_trx").get("value")
|
||||
)
|
||||
stripe = "Unknown"
|
||||
for name, low, high in zip(
|
||||
bl.fm.surfaceToroid, bl.fm.limOptXToroid[1], bl.fm.limOptXToroid[0]
|
||||
):
|
||||
if low <= fm_trx <= high:
|
||||
stripe = name + " (toroid)"
|
||||
fm_stripe = focusing_mirror.create_dataset(name="stripe", data=stripe)
|
||||
fm_stripe.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
###################
|
||||
@@ -207,46 +206,73 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
###################
|
||||
|
||||
## Logic if device exist
|
||||
if "nidaq" in self.device_manager.devices:
|
||||
|
||||
#ai_chans_bits = self.device_manager.devices.nidaq.ai_chans.read(cached=True).get("nidaq_ai_chans").get("value")
|
||||
ai_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_ai_chans", {}).get("value")
|
||||
ci_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_ci_chans", {}).get("value")
|
||||
#add_chans_bits = self.device_manager.devices.nidaq.add_chans.read(cached=True).get("nidaq_add_chans").get("value")
|
||||
add_chans_bits = self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value")
|
||||
if "nidaq" in self.device_manager.devices:
|
||||
|
||||
# ai_chans_bits = self.device_manager.devices.nidaq.ai_chans.read(cached=True).get("nidaq_ai_chans").get("value")
|
||||
ai_chans_bits = (
|
||||
self.configuration.get("nidaq", {}).get("nidaq_ai_chans", {}).get("value")
|
||||
)
|
||||
ci_chans_bits = (
|
||||
self.configuration.get("nidaq", {}).get("nidaq_ci_chans", {}).get("value")
|
||||
)
|
||||
# add_chans_bits = self.device_manager.devices.nidaq.add_chans.read(cached=True).get("nidaq_add_chans").get("value")
|
||||
add_chans_bits = (
|
||||
self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value")
|
||||
)
|
||||
|
||||
rle = (
|
||||
self.configuration.get("nidaq", {}).get("nidaq_enable_compression", {}).get("value")
|
||||
)
|
||||
|
||||
measurement_mode = entry.create_group(name="mode")
|
||||
measurement_mode.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if (int(ci_chans_bits) & 0x7F) != 0:
|
||||
# Create a dataset
|
||||
rayspec_sdd_active = measurement_mode.create_group(name="Multi_Element_Partial_Fluorescence_Yield")
|
||||
me_sdd = rayspec_sdd_active.create_dataset(name="Detector", data="Rayspec 7 element Silicon Drift Detector")
|
||||
me_sdd.attrs["NX_class"] = "NX_CHAR"
|
||||
if ci_chans_bits is not None:
|
||||
if (int(ci_chans_bits) & 0x7F) != 0:
|
||||
# Create a dataset
|
||||
rayspec_sdd_active = measurement_mode.create_group(
|
||||
name="Multi_Element_Partial_Fluorescence_Yield"
|
||||
)
|
||||
me_sdd = rayspec_sdd_active.create_dataset(
|
||||
name="Detector", data="Rayspec 7 element Silicon Drift Detector"
|
||||
)
|
||||
me_sdd.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if (int(ci_chans_bits) & (1<<8)) != 0:
|
||||
# Create a dataset
|
||||
ketek_sdd_active = measurement_mode.create_group(name="Single_Element_Partial_Fluorescence_Yield")
|
||||
se_sdd = ketek_sdd_active.create_dataset(name="Detector", data="Ketex mini single element Silicon Drift Detector")
|
||||
se_sdd.attrs["NX_class"] = "NX_CHAR"
|
||||
if (int(ci_chans_bits) & (1 << 8)) != 0:
|
||||
# Create a dataset
|
||||
ketek_sdd_active = measurement_mode.create_group(
|
||||
name="Single_Element_Partial_Fluorescence_Yield"
|
||||
)
|
||||
se_sdd = ketek_sdd_active.create_dataset(
|
||||
name="Detector", data="Ketex mini single element Silicon Drift Detector"
|
||||
)
|
||||
se_sdd.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if ((int(ai_chans_bits) & (1<<6)) != 0):
|
||||
# Create a dataset
|
||||
pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield")
|
||||
tfy = pips_active.create_dataset(name="Detector", data="Mirion Technologies Partially Depeleted PIPS Detector")
|
||||
tfy.attrs["NX_class"] = "NX_CHAR"
|
||||
if ai_chans_bits is not None:
|
||||
if (int(ai_chans_bits) & (1 << 6)) != 0:
|
||||
# Create a dataset
|
||||
pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield")
|
||||
tfy = pips_active.create_dataset(
|
||||
name="Detector",
|
||||
data="Mirion Technologies Partially Depeleted PIPS Detector",
|
||||
)
|
||||
tfy.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if ((int(ai_chans_bits) & (1<<0)) != 0) & ((int(ai_chans_bits) & (1<<2)) != 0):
|
||||
# Create a dataset
|
||||
ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission")
|
||||
sam_trans = ai0ai2_active.create_dataset(name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers")
|
||||
sam_trans.attrs["NX_class"] = "NX_CHAR"
|
||||
if ((int(ai_chans_bits) & (1 << 0)) != 0) & ((int(ai_chans_bits) & (1 << 2)) != 0):
|
||||
# Create a dataset
|
||||
ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission")
|
||||
sam_trans = ai0ai2_active.create_dataset(
|
||||
name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers"
|
||||
)
|
||||
sam_trans.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
if ((int(ai_chans_bits) & (1<<2)) != 0) & ((int(ai_chans_bits) & (1<<4)) != 0):
|
||||
# Create a dataset
|
||||
ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission")
|
||||
ref_trans = ai2ai4_active.create_dataset(name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers")
|
||||
ref_trans.attrs["NX_class"] = "NX_CHAR"
|
||||
if ((int(ai_chans_bits) & (1 << 2)) != 0) & ((int(ai_chans_bits) & (1 << 4)) != 0):
|
||||
# Create a dataset
|
||||
ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission")
|
||||
ref_trans = ai2ai4_active.create_dataset(
|
||||
name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers"
|
||||
)
|
||||
ref_trans.attrs["NX_class"] = "NX_CHAR"
|
||||
|
||||
main_data = entry.create_group(name="data")
|
||||
main_data.attrs["NX_class"] = "NXdata"
|
||||
@@ -254,45 +280,60 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
##################
|
||||
## energy, test whether the signal exists. how to check from config?
|
||||
###################
|
||||
|
||||
|
||||
energy = main_data.create_group(name="energy")
|
||||
energy.attrs["NX_class"] = "NXdata"
|
||||
energy.attrs["units"] = "eV"
|
||||
|
||||
main_data.create_soft_link(name="energy", target="/entry/collection/readout_groups/async/nidaq/nidaq_energy/value")
|
||||
|
||||
|
||||
main_data.create_soft_link(
|
||||
name="energy",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_energy/value",
|
||||
)
|
||||
|
||||
##################
|
||||
## i0
|
||||
###################
|
||||
|
||||
if (int(ai_chans_bits) & (1<<0)) !=0:
|
||||
|
||||
if (int(ai_chans_bits) & (1 << 0)) != 0:
|
||||
i0 = main_data.create_group(name="i0")
|
||||
i0.attrs["NX_class"] = "NXdata"
|
||||
i0.attrs["units"] = "V"
|
||||
|
||||
main_data.create_soft_link(name="i0", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value")
|
||||
|
||||
if rle:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value"
|
||||
else:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai0/value"
|
||||
main_data.create_soft_link(name="i0", target=target)
|
||||
|
||||
##################
|
||||
## i1
|
||||
###################
|
||||
|
||||
if (int(ai_chans_bits) & (1<<2)) !=0:
|
||||
|
||||
if (int(ai_chans_bits) & (1 << 2)) != 0:
|
||||
i1 = main_data.create_group(name="i1")
|
||||
i1.attrs["NX_class"] = "NXdata"
|
||||
i1.attrs["units"] = "V"
|
||||
|
||||
main_data.create_soft_link(name="i1", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value")
|
||||
if rle:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value"
|
||||
else:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai2/value"
|
||||
main_data.create_soft_link(name="i1", target=target)
|
||||
|
||||
##################
|
||||
## i2
|
||||
###################
|
||||
|
||||
if (int(ai_chans_bits) & (1<<4)) !=0:
|
||||
if (int(ai_chans_bits) & (1 << 4)) != 0:
|
||||
i2 = main_data.create_group(name="i2")
|
||||
i2.attrs["NX_class"] = "NXdata"
|
||||
i2.attrs["units"] = "V"
|
||||
|
||||
main_data.create_soft_link(name="i2", target="/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value")
|
||||
if rle:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value"
|
||||
else:
|
||||
target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai4/value"
|
||||
main_data.create_soft_link(name="i2", target=target)
|
||||
|
||||
##################
|
||||
## ci sum
|
||||
@@ -303,38 +344,46 @@ class SuperXASNexusStructure(DefaultFormat):
|
||||
ci_sum.attrs["NX_class"] = "NXdata"
|
||||
ci_sum.attrs["units"] = "counts"
|
||||
|
||||
main_data.create_soft_link(name="Fluorescence_Sum", target="/entry/collection/readout_groups/async/nidaq/nidaq_cisum/value")
|
||||
main_data.create_soft_link(
|
||||
name="Fluorescence_Sum",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_cisum/value",
|
||||
)
|
||||
|
||||
##################
|
||||
## mu sample, test whether the signal exists. how to check from config?
|
||||
###################
|
||||
|
||||
if (int(add_chans_bits) & (1<<0)) !=0:
|
||||
if (int(add_chans_bits) & (1 << 0)) != 0:
|
||||
mu_sample = main_data.create_group(name="mu_sample")
|
||||
mu_sample.attrs["NX_class"] = "NXdata"
|
||||
|
||||
main_data.create_soft_link(name="mu_sample", target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_abs/value")
|
||||
|
||||
main_data.create_soft_link(
|
||||
name="mu_sample",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_abs/value",
|
||||
)
|
||||
|
||||
##################
|
||||
## fluo sample, test whether the signal exists. how to check from config?
|
||||
###################
|
||||
|
||||
if (int(add_chans_bits) & (1<<1)) !=0:
|
||||
if (int(add_chans_bits) & (1 << 1)) != 0:
|
||||
mu_sample = main_data.create_group(name="fluo_sample")
|
||||
mu_sample.attrs["NX_class"] = "NXdata"
|
||||
|
||||
main_data.create_soft_link(name="fluo_sample", target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_fluo/value")
|
||||
|
||||
main_data.create_soft_link(
|
||||
name="fluo_sample",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_smpl_fluo/value",
|
||||
)
|
||||
|
||||
##################
|
||||
## mu reference, test whether the signal exists. how to check from config?
|
||||
###################
|
||||
|
||||
if (int(add_chans_bits) & (1<<2)) !=0:
|
||||
|
||||
if (int(add_chans_bits) & (1 << 2)) != 0:
|
||||
mu_reference = main_data.create_group(name="mu_reference")
|
||||
mu_reference.attrs["NX_class"] = "NXdata"
|
||||
|
||||
main_data.create_soft_link(name="mu_reference", target="/entry/collection/readout_groups/async/nidaq/nidaq_ref_abs/value")
|
||||
|
||||
|
||||
|
||||
|
||||
main_data.create_soft_link(
|
||||
name="mu_reference",
|
||||
target="/entry/collection/readout_groups/async/nidaq/nidaq_ref_abs/value",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user