mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-15 05:00:55 +02:00
Compare commits
49 Commits
v0.45.0
...
feature/vs
| Author | SHA1 | Date | |
|---|---|---|---|
| de7eaf7826 | |||
| 1694215c06 | |||
|
|
e55daee756 | ||
| 1111610f32 | |||
| 81484e8160 | |||
|
|
2e349bd705 | ||
| a156803389 | |||
| 2955b5ec02 | |||
| ff52100e23 | |||
| 026c0792be | |||
| b632ed1095 | |||
|
|
98beea37e6 | ||
| 4bcae0f921 | |||
| 22fb5a5656 | |||
| 4da625e439 | |||
| 05e268d466 | |||
| 42a9a0ca15 | |||
| b6feb9adb3 | |||
| 1bc18a201c | |||
| c12f2cee80 | |||
| c2c583fce6 | |||
| 5600624c57 | |||
| 66c0649d7e | |||
| 2446c401d9 | |||
| 4d0df364d3 | |||
| ecdf0f122b | |||
| df5234aa52 | |||
| 62080e6b40 | |||
| 2e3f46ea36 | |||
| be9847e9d2 | |||
| 2f7317b328 | |||
| bd3b1ba043 | |||
|
|
59e82dfd00 | ||
| 0b86a0009d | |||
| 49327a8dbd | |||
| 301bb916da | |||
| 285bf0164b | |||
| 90907e0a9c | |||
| 9def3734af | |||
|
|
3a241e897b | ||
| ee617b73a2 | |||
| 92cea90971 | |||
|
|
452ba20216 | ||
| cf29035e28 | |||
|
|
3fc09dd2aa | ||
| fe101f9328 | |||
| 754d81edf3 | |||
| 3d399ba1f5 | |||
| 6dc1000de5 |
@@ -1,12 +1,22 @@
|
||||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official language image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/python/tags/
|
||||
image: $CI_DOCKER_REGISTRY/python:3.10
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
|
||||
#commands to run in the Docker container before starting each job.
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
BEC_CORE_BRANCH: "master"
|
||||
OPHYD_DEVICES_BRANCH: "master"
|
||||
BEC_CORE_BRANCH: "main"
|
||||
OPHYD_DEVICES_BRANCH: "main"
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
- if: $CI_PIPELINE_SOURCE == "pipeline"
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
include:
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
@@ -17,6 +27,7 @@ stages:
|
||||
- Formatter
|
||||
- test
|
||||
- AdditionalTests
|
||||
- End2End
|
||||
- Deploy
|
||||
|
||||
formatter:
|
||||
@@ -87,7 +98,7 @@ tests:
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev]
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
|
||||
- coverage report
|
||||
- coverage xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
@@ -98,28 +109,64 @@ tests:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
#tests-3.10-pyqt5: #todo enable when we decide what qt distributions we want to support
|
||||
# extends: "tests"
|
||||
# stage: AdditionalTests
|
||||
# image: $CI_DOCKER_REGISTRY/python:3.10
|
||||
# script:
|
||||
# - apt-get update
|
||||
# - apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
# - pip install .[dev,pyqt5]
|
||||
# - pytest -v --random-order ./tests
|
||||
|
||||
tests-3.11:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DOCKER_REGISTRY/python:3.11
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
|
||||
allow_failure: true
|
||||
|
||||
tests-3.12:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DOCKER_REGISTRY/python:3.12
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
|
||||
allow_failure: true
|
||||
|
||||
end-2-end-conda:
|
||||
stage: End2End
|
||||
needs: []
|
||||
image: continuumio/miniconda3
|
||||
allow_failure: false
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- conda config --prepend channels conda-forge
|
||||
- conda config --set channel_priority strict
|
||||
- conda config --set always_yes yes --set changeps1 no
|
||||
- conda create -q -n test-environment python=3.10
|
||||
- conda init bash
|
||||
- source ~/.bashrc
|
||||
- conda activate test-environment
|
||||
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
|
||||
- cd ./bec
|
||||
- source ./bin/install_bec_dev.sh -t
|
||||
|
||||
- pip install -e ./bec_lib[dev]
|
||||
- pip install -e ./bec_ipython_client[dev]
|
||||
- cd ../
|
||||
- pip install -e .[dev]
|
||||
- cd ./tests/end-2-end
|
||||
- pytest --start-servers --flush-redis --random-order
|
||||
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- ./logs/*.log
|
||||
expire_in: 1 week
|
||||
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "web"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "pipeline"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "production"'
|
||||
|
||||
|
||||
semver:
|
||||
stage: Deploy
|
||||
@@ -144,10 +191,11 @@ semver:
|
||||
semantic-release publish -v DEBUG
|
||||
-D version_variable=./setup.py:__version__
|
||||
-D hvcs=gitlab
|
||||
-D branch=main
|
||||
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME == "master"'
|
||||
- if: '$CI_COMMIT_REF_NAME == "main"'
|
||||
|
||||
pages:
|
||||
stage: Deploy
|
||||
@@ -158,6 +206,6 @@ pages:
|
||||
- if: '$CI_COMMIT_TAG != null'
|
||||
variables:
|
||||
TARGET_BRANCH: $CI_COMMIT_TAG
|
||||
- if: '$CI_COMMIT_REF_NAME == "master"'
|
||||
- if: '$CI_COMMIT_REF_NAME == "main"'
|
||||
script:
|
||||
- curl -X POST -d "branches=$CI_COMMIT_REF_NAME" -d "token=$RTD_TOKEN" https://readthedocs.org/api/v2/webhook/bec-widgets/253243/
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -2,6 +2,55 @@
|
||||
|
||||
<!--next-version-placeholder-->
|
||||
|
||||
## v0.46.6 (2024-04-19)
|
||||
|
||||
### Fix
|
||||
|
||||
* **cli:** Fixed support for devices as cli input ([`1111610`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/1111610f3206c5c46db6b4bd1e8827f1a4cd9e3f))
|
||||
|
||||
## v0.46.5 (2024-04-19)
|
||||
|
||||
### Fix
|
||||
|
||||
* **widgets/figure:** Individual cleanup disabled, making stuck rpc ([`ff52100`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/ff52100e234debdfb5ccc0869352cfafde52ac93))
|
||||
* **plots/waveform:** Colormap is correctly passed from BECFigure ([`026c079`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/026c0792bee25723013fffe57ccff10d9b652913))
|
||||
|
||||
## v0.46.4 (2024-04-16)
|
||||
|
||||
### Fix
|
||||
|
||||
* Renaming of bec_client to bec_ipython_client ([`4da625e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/4da625e4398bdd937c2b788592f15f7530148292))
|
||||
* **plots/motor_map:** User can get data as dict from BECMotorMap ([`c12f2ce`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/c12f2cee80b13137a2b70e2d121a079e20d124e2))
|
||||
* **plots/image:** User can get data as np.ndarray from BECImageItem ([`c2c583f`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/c2c583fce6f28981990c504dd065705124e40e44))
|
||||
* **rpc/server:** Server can accept client or dispatcher ([`ecdf0f1`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/ecdf0f122b628ee378b80793d498cedafe50fbf8))
|
||||
|
||||
## v0.46.3 (2024-04-11)
|
||||
|
||||
### Fix
|
||||
|
||||
* **test_fake_redis:** TestMessage fixed to pydantic BaseModel ([`0b86a00`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/0b86a0009d9366b710294a3ab55cb9f4894472c0))
|
||||
* **plots/motor_map:** Removed single callback flag for connecting device_readback motors ([`49327a8`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/49327a8dbde270c67bc0ce7c757fd4a3eae118b4))
|
||||
* **cli/client_utils:** Print_log is buffered; add output processing thread ([`285bf01`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/285bf0164b6deb91678f03ab2a190680b6d83a02))
|
||||
* Producer->connector ([`9def373`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/9def3734afb361ac2d5cc933661766cdc440e09d))
|
||||
|
||||
## v0.46.2 (2024-04-10)
|
||||
|
||||
### Fix
|
||||
|
||||
* **widget/plots:** Added "get_config" to all children of `BECConnector` to USER_ACCESS ([`ee617b7`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/ee617b73a2fcad8194394182fcecb0dd4f583a8e))
|
||||
|
||||
## v0.46.1 (2024-04-10)
|
||||
|
||||
### Fix
|
||||
|
||||
* **rpc/client:** Correct name for RPC class BECWaveform (instead of BECWaveform1D) ([`cf29035`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/cf29035e283e55efa547cbac88e8b622190dfc4d))
|
||||
|
||||
## v0.46.0 (2024-04-09)
|
||||
|
||||
### Feature
|
||||
|
||||
* **plot/waveform1d:** BECWaveform1D can show z data of scatter coded to different detector like BECMonitor2DScatter; BECWaveform1D name changed to BECWaveform ([`3d399ba`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/3d399ba1f5d85bc67964febcf8921355f9f1c285))
|
||||
|
||||
## v0.45.0 (2024-03-26)
|
||||
|
||||
### Feature
|
||||
|
||||
@@ -6,14 +6,13 @@ from bec_widgets.cli.client_utils import BECFigureClientMixin, RPCBase, rpc_call
|
||||
|
||||
|
||||
class BECPlotBase(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
|
||||
def config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Args:
|
||||
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
|
||||
Returns:
|
||||
dict: The configuration of the plot widget.
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -136,15 +135,27 @@ class BECPlotBase(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class BECWaveform1D(RPCBase):
|
||||
class BECWaveform(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_curve_scan(
|
||||
self,
|
||||
x_name: "str",
|
||||
y_name: "str",
|
||||
z_name: "Optional[str]" = None,
|
||||
x_entry: "Optional[str]" = None,
|
||||
y_entry: "Optional[str]" = None,
|
||||
z_entry: "Optional[str]" = None,
|
||||
color: "Optional[str]" = None,
|
||||
color_map_z: "Optional[str]" = "plasma",
|
||||
label: "Optional[str]" = None,
|
||||
validate_bec: "bool" = True,
|
||||
**kwargs
|
||||
@@ -156,7 +167,10 @@ class BECWaveform1D(RPCBase):
|
||||
x_entry(str): Entry of the x signal.
|
||||
y_name(str): Name of the y signal.
|
||||
y_entry(str): Entry of the y signal.
|
||||
z_name(str): Name of the z signal.
|
||||
z_entry(str): Entry of the z signal.
|
||||
color(str, optional): Color of the curve. Defaults to None.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str, optional): Label of the curve. Defaults to None.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
@@ -252,16 +266,6 @@ class BECWaveform1D(RPCBase):
|
||||
dict | pd.DataFrame: Data of all curves in the specified format.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Args:
|
||||
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
|
||||
Returns:
|
||||
dict: The configuration of the plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs) -> "None":
|
||||
"""
|
||||
@@ -383,6 +387,15 @@ class BECWaveform1D(RPCBase):
|
||||
|
||||
|
||||
class BECFigure(RPCBase, BECFigureClientMixin):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def axes(self) -> "list[BECPlotBase]":
|
||||
@@ -404,18 +417,21 @@ class BECFigure(RPCBase, BECFigureClientMixin):
|
||||
self,
|
||||
x_name: "str" = None,
|
||||
y_name: "str" = None,
|
||||
z_name: "str" = None,
|
||||
x_entry: "str" = None,
|
||||
y_entry: "str" = None,
|
||||
z_entry: "str" = None,
|
||||
x: "list | np.ndarray" = None,
|
||||
y: "list | np.ndarray" = None,
|
||||
color: "Optional[str]" = None,
|
||||
color_map_z: "Optional[str]" = "plasma",
|
||||
label: "Optional[str]" = None,
|
||||
validate: "bool" = True,
|
||||
row: "int" = None,
|
||||
col: "int" = None,
|
||||
config=None,
|
||||
**axis_kwargs
|
||||
) -> "BECWaveform1D":
|
||||
) -> "BECWaveform":
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
Args:
|
||||
@@ -484,31 +500,37 @@ class BECFigure(RPCBase, BECFigureClientMixin):
|
||||
self,
|
||||
x_name: "str" = None,
|
||||
y_name: "str" = None,
|
||||
z_name: "str" = None,
|
||||
x_entry: "str" = None,
|
||||
y_entry: "str" = None,
|
||||
z_entry: "str" = None,
|
||||
x: "list | np.ndarray" = None,
|
||||
y: "list | np.ndarray" = None,
|
||||
color: "Optional[str]" = None,
|
||||
color_map_z: "Optional[str]" = "plasma",
|
||||
label: "Optional[str]" = None,
|
||||
validate: "bool" = True,
|
||||
**axis_kwargs
|
||||
) -> "BECWaveform1D":
|
||||
) -> "BECWaveform":
|
||||
"""
|
||||
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
|
||||
Args:
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
y_entry(str): The name of the entry for the y-axis.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECWaveform1D: The waveform plot widget.
|
||||
BECWaveform: The waveform plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -592,18 +614,17 @@ class BECFigure(RPCBase, BECFigureClientMixin):
|
||||
Clear all widgets from the figure and reset to default state
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Args:
|
||||
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
|
||||
Returns:
|
||||
dict: The configuration of the plot widget.
|
||||
"""
|
||||
|
||||
|
||||
class BECCurve(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
@@ -634,6 +655,14 @@ class BECCurve(RPCBase):
|
||||
symbol_color(str, optional): Color of the symbol. Defaults to None.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_colormap(self, colormap: "str"):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol(self, symbol: "str"):
|
||||
"""
|
||||
@@ -684,6 +713,15 @@ class BECCurve(RPCBase):
|
||||
|
||||
|
||||
class BECImageShow(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_image_by_config(self, config: "ImageItemConfig | dict") -> "BECImageItem":
|
||||
"""
|
||||
@@ -707,14 +745,6 @@ class BECImageShow(RPCBase):
|
||||
ImageItemConfig|dict: The configuration of the image.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_image_list(self) -> "list[BECImageItem]":
|
||||
"""
|
||||
Get the list of images.
|
||||
Returns:
|
||||
list[BECImageItem]: The list of images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_image_dict(self) -> "dict[str, dict[str, BECImageItem]]":
|
||||
"""
|
||||
@@ -876,16 +906,6 @@ class BECImageShow(RPCBase):
|
||||
use_threading(bool): Whether to use threading.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Args:
|
||||
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
|
||||
Returns:
|
||||
dict: The configuration of the plot widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs) -> "None":
|
||||
"""
|
||||
@@ -1005,20 +1025,37 @@ class BECImageShow(RPCBase):
|
||||
Remove the plot widget from the figure.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def images(self) -> "list[BECImageItem]":
|
||||
"""
|
||||
Get the list of images.
|
||||
Returns:
|
||||
list[BECImageItem]: The list of images.
|
||||
"""
|
||||
|
||||
|
||||
class BECConnector(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
|
||||
def config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Args:
|
||||
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
|
||||
Returns:
|
||||
dict: The configuration of the plot widget.
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
|
||||
class BECImageItem(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
@@ -1121,17 +1158,24 @@ class BECImageItem(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
|
||||
def get_data(self) -> "np.ndarray":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Args:
|
||||
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
|
||||
Get the data of the image.
|
||||
Returns:
|
||||
dict: The configuration of the plot widget.
|
||||
np.ndarray: The data of the image.
|
||||
"""
|
||||
|
||||
|
||||
class BECMotorMap(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def change_motors(
|
||||
self,
|
||||
@@ -1190,3 +1234,11 @@ class BECMotorMap(RPCBase):
|
||||
Args:
|
||||
scatter_size(int): Size of the scatter points.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_data(self) -> "dict":
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from functools import wraps
|
||||
@@ -33,6 +36,17 @@ def rpc_call(func):
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# we could rely on a strict type check here, but this is more flexible
|
||||
# moreover, it would anyway crash for objects...
|
||||
out = []
|
||||
for arg in args:
|
||||
if hasattr(arg, "name"):
|
||||
arg = arg.name
|
||||
out.append(arg)
|
||||
args = tuple(out)
|
||||
for key, val in kwargs.items():
|
||||
if hasattr(val, "name"):
|
||||
kwargs[key] = val.name
|
||||
if not self.gui_is_alive():
|
||||
raise RuntimeError("GUI is not alive")
|
||||
return self._run_rpc(func.__name__, *args, **kwargs)
|
||||
@@ -79,8 +93,9 @@ def update_script(figure: BECFigure, msg):
|
||||
print(f"Scan {scan_number} is running")
|
||||
dev_x = scan_report_devices[0]
|
||||
dev_y = scan_report_devices[1]
|
||||
dev_z = get_selected_device(monitored_devices, figure.selected_device)
|
||||
figure.clear_all()
|
||||
plt = figure.plot(dev_x, dev_y, label=f"Scan {scan_number}")
|
||||
plt = figure.plot(dev_x, dev_y, dev_z, label=f"Scan {scan_number}")
|
||||
plt.set(title=f"Scan {scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
elif scan_report_devices:
|
||||
dev_x = scan_report_devices[0]
|
||||
@@ -99,6 +114,7 @@ class BECFigureClientMixin:
|
||||
self.update_script = update_script
|
||||
self._target_endpoint = MessageEndpoints.scan_status()
|
||||
self._selected_device = None
|
||||
self.stderr_output = []
|
||||
|
||||
@property
|
||||
def selected_device(self):
|
||||
@@ -151,8 +167,10 @@ class BECFigureClientMixin:
|
||||
if self._process is None:
|
||||
return
|
||||
self._run_rpc("close", (), wait_for_rpc_response=False)
|
||||
self._process.kill()
|
||||
self._process.terminate()
|
||||
self._process_output_processing_thread.join()
|
||||
self._process = None
|
||||
self._client.shutdown()
|
||||
|
||||
def _start_plot_process(self) -> None:
|
||||
"""
|
||||
@@ -163,10 +181,12 @@ class BECFigureClientMixin:
|
||||
monitor_module = importlib.import_module("bec_widgets.cli.server")
|
||||
monitor_path = monitor_module.__file__
|
||||
|
||||
command = f"python {monitor_path} --id {self._gui_id}"
|
||||
command = [sys.executable, "-u", monitor_path, "--id", self._gui_id]
|
||||
self._process = subprocess.Popen(
|
||||
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
self._process_output_processing_thread = threading.Thread(target=self._get_output)
|
||||
self._process_output_processing_thread.start()
|
||||
|
||||
def print_log(self) -> None:
|
||||
"""
|
||||
@@ -174,19 +194,22 @@ class BECFigureClientMixin:
|
||||
"""
|
||||
if self._process is None:
|
||||
return
|
||||
print(self._get_stderr_output())
|
||||
print("".join(self.stderr_output))
|
||||
# Flush list
|
||||
self.stderr_output.clear()
|
||||
|
||||
def _get_stderr_output(self) -> str:
|
||||
stderr_output = []
|
||||
while self._process.poll() is not None:
|
||||
readylist, _, _ = select.select([self._process.stderr], [], [], 0.1)
|
||||
if not readylist:
|
||||
break
|
||||
line = self._process.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
stderr_output.append(line.decode("utf-8"))
|
||||
return "".join(stderr_output)
|
||||
def _get_output(self) -> str:
|
||||
os.set_blocking(self._process.stdout.fileno(), False)
|
||||
os.set_blocking(self._process.stderr.fileno(), False)
|
||||
while self._process.poll() is None:
|
||||
readylist, _, _ = select.select([self._process.stdout, self._process.stderr], [], [], 1)
|
||||
if self._process.stdout in readylist:
|
||||
# print("*"*10, self._process.stdout.read(1024), flush=True, end="")
|
||||
self._process.stdout.read(1024)
|
||||
if self._process.stderr in readylist:
|
||||
# print("!"*10, self._process.stderr.read(1024), flush=True, end="", file=sys.stderr)
|
||||
print(self._process.stderr.read(1024), flush=True, end="", file=sys.stderr)
|
||||
self.stderr_output.append(self._process.stderr.read(1024))
|
||||
|
||||
|
||||
class RPCBase:
|
||||
|
||||
@@ -109,15 +109,15 @@ if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.plots import BECImageShow, BECMotorMap, BECPlotBase, BECWaveform1D
|
||||
from bec_widgets.widgets.plots import BECImageShow, BECMotorMap, BECPlotBase, BECWaveform
|
||||
from bec_widgets.widgets.plots.image import BECImageItem
|
||||
from bec_widgets.widgets.plots.waveform1d import BECCurve
|
||||
from bec_widgets.widgets.plots.waveform import BECCurve
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
client_path = os.path.join(current_path, "client.py")
|
||||
clss = [
|
||||
BECPlotBase,
|
||||
BECWaveform1D,
|
||||
BECWaveform,
|
||||
BECFigure,
|
||||
BECCurve,
|
||||
BECImageShow,
|
||||
|
||||
@@ -6,15 +6,15 @@ from qtpy.QtCore import QTimer
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.plots import BECCurve, BECImageShow, BECWaveform1D
|
||||
from bec_widgets.widgets.plots import BECCurve, BECImageShow, BECWaveform
|
||||
|
||||
|
||||
class BECWidgetsCLIServer:
|
||||
WIDGETS = [BECWaveform1D, BECFigure, BECCurve, BECImageShow]
|
||||
WIDGETS = [BECWaveform, BECFigure, BECCurve, BECImageShow]
|
||||
|
||||
def __init__(self, gui_id: str = None, dispatcher: BECDispatcher = None) -> None:
|
||||
def __init__(self, gui_id: str = None, dispatcher: BECDispatcher = None, client=None) -> None:
|
||||
self.dispatcher = BECDispatcher() if dispatcher is None else dispatcher
|
||||
self.client = self.dispatcher.client
|
||||
self.client = self.dispatcher.client if client is None else client
|
||||
self.client.start()
|
||||
self.gui_id = gui_id
|
||||
self.fig = BECFigure(gui_id=self.gui_id)
|
||||
|
||||
102
bec_widgets/examples/jupyter_console/jupyter_console_window.py
Normal file
102
bec_widgets/examples/jupyter_console/jupyter_console_window.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import uic
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.widgets import BECFigure
|
||||
|
||||
|
||||
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.kernel_manager = QtInProcessKernelManager()
|
||||
self.kernel_manager.start_kernel(show_banner=False)
|
||||
self.kernel_client = self.kernel_manager.client()
|
||||
self.kernel_client.start_channels()
|
||||
|
||||
self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
|
||||
# self.set_console_font_size(70)
|
||||
|
||||
def shutdown_kernel(self):
|
||||
self.kernel_client.stop_channels()
|
||||
self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"""A widget that contains a Jupyter console linked to BEC Widgets with full API access (contains Qt and pyqtgraph API)."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
uic.loadUi(os.path.join(current_path, "jupyter_console_window.ui"), self)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
self.splitter.setSizes([200, 100])
|
||||
self.safe_close = False
|
||||
# self.figure.clean_signal.connect(self.confirm_close)
|
||||
|
||||
# console push
|
||||
self.console.kernel_manager.kernel.shell.push(
|
||||
{
|
||||
"fig": self.figure,
|
||||
"w1": self.w1,
|
||||
"w2": self.w2,
|
||||
"w3": self.w3,
|
||||
"bec": self.figure.client,
|
||||
"scans": self.figure.client.scans,
|
||||
"dev": self.figure.client.device_manager.devices,
|
||||
}
|
||||
)
|
||||
|
||||
def _init_ui(self):
|
||||
# Plotting window
|
||||
self.glw_1_layout = QVBoxLayout(self.glw) # Create a new QVBoxLayout
|
||||
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
|
||||
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
|
||||
|
||||
# add stuff to figure
|
||||
self._init_figure()
|
||||
|
||||
self.console_layout = QVBoxLayout(self.widget_console)
|
||||
self.console = JupyterConsoleWidget()
|
||||
self.console_layout.addWidget(self.console)
|
||||
self.console.set_default_style("linux")
|
||||
|
||||
def _init_figure(self):
|
||||
self.figure.plot("samx", "bpm4d")
|
||||
self.figure.motor_map("samx", "samy")
|
||||
self.figure.image("eiger", color_map="viridis", vrange=(0, 100))
|
||||
|
||||
self.figure.change_layout(2, 2)
|
||||
|
||||
self.w1 = self.figure[0, 0]
|
||||
self.w2 = self.figure[0, 1]
|
||||
self.w3 = self.figure[1, 0]
|
||||
|
||||
# curves for w1
|
||||
self.w1.add_curve_scan("samx", "samy", "bpm4i", pen_style="dash")
|
||||
self.w1.add_curve_scan("samx", "samy", "bpm3a", pen_style="dash")
|
||||
self.c1 = self.w1.get_config()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Jupyter Console")
|
||||
win = JupyterConsoleWindow()
|
||||
win.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@@ -3,7 +3,6 @@ import time
|
||||
from bec_lib import MessageEndpoints, RedisConnector, messages
|
||||
|
||||
connector = RedisConnector("localhost:6379")
|
||||
producer = connector.producer()
|
||||
metadata = {}
|
||||
|
||||
scan_id = "ScanID1"
|
||||
@@ -15,9 +14,7 @@ for ii in range(20):
|
||||
data = {"mca1": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "mca2": [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]}
|
||||
msg = messages.DeviceMessage(signals=data, metadata=metadata).dumps()
|
||||
|
||||
# producer.send(topic=MessageEndpoints.device_status(device="mca"), msg=msg)
|
||||
|
||||
producer.xadd(
|
||||
connector.xadd(
|
||||
topic=MessageEndpoints.device_async_readback(
|
||||
scan_id=scan_id, device="mca"
|
||||
), # scan_id will be different for each scan
|
||||
|
||||
@@ -41,7 +41,7 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
uic.loadUi(os.path.join(current_path, "line_plot.ui"), self)
|
||||
|
||||
self._idle_time = 100
|
||||
self.producer = RedisConnector(["localhost:6379"]).producer()
|
||||
self.connector = RedisConnector(["localhost:6379"])
|
||||
|
||||
self.y_value_list = y_value_list
|
||||
self.previous_y_value_list = None
|
||||
@@ -215,7 +215,7 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
]
|
||||
}
|
||||
msg = messages.DeviceMessage(signals=return_dict).dumps()
|
||||
self.producer.set_and_publish("px_stream/gui_event", msg=msg)
|
||||
self.connector.set_and_publish("px_stream/gui_event", msg=msg)
|
||||
self.roi_signal.emit(region)
|
||||
|
||||
def init_table(self):
|
||||
@@ -271,7 +271,7 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
endpoint = f"px_stream/projection_{self._current_proj}/data"
|
||||
msgs = self.client.producer.lrange(topic=endpoint, start=-1, end=-1)
|
||||
msgs = self.client.connector.lrange(topic=endpoint, start=-1, end=-1)
|
||||
data = msgs
|
||||
if not data:
|
||||
continue
|
||||
@@ -296,7 +296,7 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
def new_proj(self, content: dict, _metadata: dict):
|
||||
proj_nr = content["signals"]["proj_nr"]
|
||||
endpoint = f"px_stream/projection_{proj_nr}/metadata"
|
||||
msg_raw = self.client.producer.get(topic=endpoint)
|
||||
msg_raw = self.client.connector.get(topic=endpoint)
|
||||
msg = messages.DeviceMessage.loads(msg_raw)
|
||||
self._current_q = msg.content["signals"]["q"]
|
||||
self._current_norm = msg.content["signals"]["norm_sum"]
|
||||
|
||||
@@ -31,11 +31,11 @@ class ConnectionConfig(BaseModel):
|
||||
class BECConnector:
|
||||
"""Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
|
||||
|
||||
USER_ACCESS = ["get_config"]
|
||||
USER_ACCESS = ["config_dict"]
|
||||
|
||||
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
|
||||
# BEC related connections
|
||||
self.bec_dispatcher = BECDispatcher()
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
|
||||
if config:
|
||||
@@ -54,6 +54,24 @@ class BECConnector:
|
||||
else:
|
||||
self.gui_id = self.config.gui_id
|
||||
|
||||
@property
|
||||
def config_dict(self) -> dict:
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
return self.config.model_dump()
|
||||
|
||||
@config_dict.setter
|
||||
def config_dict(self, config: BaseModel) -> None:
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_gui_id(self, gui_id: str) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,54 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import os
|
||||
import collections
|
||||
from collections.abc import Callable
|
||||
from typing import Union
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import redis
|
||||
from bec_lib import BECClient, ServiceConfig
|
||||
from bec_lib.endpoints import EndpointInfo
|
||||
from bec_lib import BECClient
|
||||
from bec_lib.redis_connector import MessageObject, RedisConnector
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
# Adding a new pyqt signal requires a class factory, as they must be part of the class definition
|
||||
# and cannot be dynamically added as class attributes after the class has been defined.
|
||||
_signal_class_factory = (
|
||||
type(f"Signal{i}", (QObject,), dict(signal=pyqtSignal(dict, dict))) for i in itertools.count()
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.endpoints import EndpointInfo
|
||||
|
||||
|
||||
class _Connection:
|
||||
class QtThreadSafeCallback(QObject):
|
||||
cb_signal = pyqtSignal(dict, dict)
|
||||
|
||||
def __init__(self, cb):
|
||||
super().__init__()
|
||||
|
||||
self.cb = cb
|
||||
self.cb_signal.connect(self.cb)
|
||||
|
||||
def __hash__(self):
|
||||
# make 2 differents QtThreadSafeCallback to look
|
||||
# identical when used as dictionary keys, if the
|
||||
# callback is the same
|
||||
return id(self.cb)
|
||||
|
||||
def __call__(self, msg_content, metadata):
|
||||
self.cb_signal.emit(msg_content, metadata)
|
||||
|
||||
|
||||
class QtRedisConnector(RedisConnector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _execute_callback(self, cb, msg, kwargs):
|
||||
if not isinstance(cb, QtThreadSafeCallback):
|
||||
return super()._execute_callback(cb, msg, kwargs)
|
||||
# if msg.msg_type == "bundle_message":
|
||||
# # big warning: how to handle bundle messages?
|
||||
# # message with messages inside ; which slot to call?
|
||||
# # bundle_msg = msg
|
||||
# # for msg in bundle_msg:
|
||||
# # ...
|
||||
# # for now, only consider the 1st message
|
||||
# msg = msg[0]
|
||||
# raise RuntimeError(f"
|
||||
if isinstance(msg, MessageObject):
|
||||
if isinstance(msg.value, list):
|
||||
msg = msg.value[0]
|
||||
else:
|
||||
msg = msg.value
|
||||
|
||||
# we can notice kwargs are lost when passed to Qt slot
|
||||
metadata = msg.metadata
|
||||
cb(msg.content, metadata)
|
||||
else:
|
||||
# from stream
|
||||
msg = msg["data"]
|
||||
cb(msg.content, msg.metadata)
|
||||
|
||||
|
||||
class BECDispatcher:
|
||||
"""Utility class to keep track of slots connected to a particular redis connector"""
|
||||
|
||||
def __init__(self, callback) -> None:
|
||||
self.callback = callback
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
self.slots = set()
|
||||
# keep a reference to a new signal class, so it is not gc'ed
|
||||
self._signal_container = next(_signal_class_factory)()
|
||||
self.signal: pyqtSignal = self._signal_container.signal
|
||||
|
||||
|
||||
class _BECDispatcher(QObject):
|
||||
"""Utility class to keep track of slots connected to a particular redis connector"""
|
||||
def __new__(cls, client=None, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(BECDispatcher, cls).__new__(cls)
|
||||
cls._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, client=None):
|
||||
super().__init__()
|
||||
self.client = BECClient() if client is None else client
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._slots = collections.defaultdict(set)
|
||||
self.client = client
|
||||
|
||||
if self.client is None:
|
||||
self.client = BECClient(connector_cls=QtRedisConnector, forced=True)
|
||||
else:
|
||||
if self.client.started:
|
||||
# have to reinitialize client to use proper connector
|
||||
self.client.shutdown()
|
||||
self.client._BECClient__init_params["connector_cls"] = QtRedisConnector
|
||||
|
||||
try:
|
||||
self.client.start()
|
||||
except redis.exceptions.ConnectionError:
|
||||
print("Could not connect to Redis, skipping start of BECClient.")
|
||||
|
||||
self._connections = {}
|
||||
self._initialized = True
|
||||
|
||||
@classmethod
|
||||
def reset_singleton(cls):
|
||||
cls._instance = None
|
||||
cls._initialized = False
|
||||
|
||||
def connect_slot(
|
||||
self,
|
||||
slot: Callable,
|
||||
topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
|
||||
single_callback_for_all_topics=False,
|
||||
) -> None:
|
||||
"""Connect widget's pyqt slot, so that it is called on new pub/sub topic message.
|
||||
|
||||
@@ -56,132 +115,27 @@ class _BECDispatcher(QObject):
|
||||
slot (Callable): A slot method/function that accepts two inputs: content and metadata of
|
||||
the corresponding pub/sub message
|
||||
topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
|
||||
single_callback_for_all_topics (bool): If True, use the same callback for all topics, otherwise use
|
||||
separate callbacks.
|
||||
"""
|
||||
# Normalise the topics input
|
||||
if isinstance(topics, (str, EndpointInfo)):
|
||||
topics = [topics]
|
||||
slot = QtThreadSafeCallback(slot)
|
||||
self.client.connector.register(topics, cb=slot)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._slots[slot].update(set(topics_str))
|
||||
|
||||
endpoint_to_consumer_type = {
|
||||
(topic.endpoint if isinstance(topic, EndpointInfo) else topic): (
|
||||
topic.message_op.name if isinstance(topic, EndpointInfo) else "SEND"
|
||||
)
|
||||
for topic in topics
|
||||
}
|
||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
|
||||
self.client.connector.unregister(topics, cb=slot)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._slots[slot].difference_update(set(topics_str))
|
||||
if not self._slots[slot]:
|
||||
del self._slots[slot]
|
||||
|
||||
# Group topics by consumer type
|
||||
consumer_type_to_endpoints = {}
|
||||
for endpoint, consumer_type in endpoint_to_consumer_type.items():
|
||||
if consumer_type not in consumer_type_to_endpoints:
|
||||
consumer_type_to_endpoints[consumer_type] = []
|
||||
consumer_type_to_endpoints[consumer_type].append(endpoint)
|
||||
def disconnect_topics(self, topics: Union[str, list]):
|
||||
self.client.connector.unregister(topics)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
for slot in list(self._slots.keys()):
|
||||
slot_topics = self._slots[slot]
|
||||
slot_topics.difference_update(set(topics_str))
|
||||
if not slot_topics:
|
||||
del self._slots[slot]
|
||||
|
||||
for consumer_type, endpoints in consumer_type_to_endpoints.items():
|
||||
topics_key = (
|
||||
tuple(sorted(endpoints)) if single_callback_for_all_topics else tuple(endpoints)
|
||||
)
|
||||
|
||||
if topics_key not in self._connections:
|
||||
self._connections[topics_key] = self._create_connection(endpoints, consumer_type)
|
||||
connection = self._connections[topics_key]
|
||||
|
||||
if slot not in connection.slots:
|
||||
connection.signal.connect(slot)
|
||||
connection.slots.add(slot)
|
||||
|
||||
def _create_connection(self, topics: list, consumer_type: str) -> _Connection:
|
||||
"""Creates a new connection for given topics."""
|
||||
|
||||
def cb(msg):
|
||||
if isinstance(msg, dict):
|
||||
msg = msg["data"]
|
||||
else:
|
||||
msg = msg.value
|
||||
for connection_key, connection in self._connections.items():
|
||||
if set(topics).intersection(connection_key):
|
||||
if isinstance(msg, list):
|
||||
msg = msg[0]
|
||||
connection.signal.emit(msg.content, msg.metadata)
|
||||
|
||||
try:
|
||||
if consumer_type == "STREAM":
|
||||
self.client.connector.register_stream(topics=topics, cb=cb, newest_only=True)
|
||||
else:
|
||||
self.client.connector.register(topics=topics, cb=cb)
|
||||
except redis.exceptions.ConnectionError:
|
||||
print("Could not connect to Redis, skipping registration of topics.")
|
||||
|
||||
return _Connection(cb)
|
||||
|
||||
def _do_disconnect_slot(self, topic, slot):
|
||||
print(f"Disconnecting {slot} from {topic}")
|
||||
connection = self._connections[topic]
|
||||
try:
|
||||
connection.signal.disconnect(slot)
|
||||
except TypeError:
|
||||
print(f"Could not disconnect slot:'{slot}' from topic:'{topic}'")
|
||||
print("Continue to remove slot:'{slot}' from 'connection.slots'.")
|
||||
connection.slots.remove(slot)
|
||||
if not connection.slots:
|
||||
del self._connections[topic]
|
||||
|
||||
def _disconnect_slot_from_topic(self, slot: Callable, topic: str) -> None:
|
||||
"""A helper method to disconnect a slot from a specific topic.
|
||||
|
||||
Args:
|
||||
slot (Callable): A slot to be disconnected
|
||||
topic (str): A corresponding topic that can typically be acquired via
|
||||
bec_lib.MessageEndpoints
|
||||
"""
|
||||
connection = self._connections.get(topic)
|
||||
if connection and slot in connection.slots:
|
||||
self._do_disconnect_slot(topic, slot)
|
||||
|
||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]) -> None:
|
||||
"""Disconnect widget's pyqt slot from pub/sub updates on a topic.
|
||||
|
||||
Args:
|
||||
slot (Callable): A slot to be disconnected
|
||||
topics (str | list): A corresponding topic or list of topics that can typically be acquired via
|
||||
bec_lib.MessageEndpoints
|
||||
"""
|
||||
# Normalise the topics input
|
||||
if isinstance(topics, (str, EndpointInfo)):
|
||||
topics = [topics]
|
||||
|
||||
endpoints = [
|
||||
topic.endpoint if isinstance(topic, EndpointInfo) else topic for topic in topics
|
||||
]
|
||||
|
||||
for key, connection in list(self._connections.items()):
|
||||
if slot in connection.slots:
|
||||
common_topics = set(endpoints).intersection(key)
|
||||
if common_topics:
|
||||
remaining_topics = set(key) - set(endpoints)
|
||||
# Disconnect slot from common topics
|
||||
self._do_disconnect_slot(key, slot)
|
||||
# Reconnect slot to remaining topics if any
|
||||
if remaining_topics:
|
||||
self.connect_slot(slot, list(remaining_topics), True)
|
||||
|
||||
def disconnect_all(self):
|
||||
"""Disconnect all slots from all topics."""
|
||||
for key, connection in list(self._connections.items()):
|
||||
for slot in list(connection.slots):
|
||||
self._disconnect_slot_from_topic(slot, key)
|
||||
|
||||
|
||||
# variable holding the Singleton instance of BECDispatcher
|
||||
_bec_dispatcher = None
|
||||
|
||||
|
||||
def BECDispatcher():
|
||||
global _bec_dispatcher
|
||||
if _bec_dispatcher is None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bec-client", default=None)
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
_bec_dispatcher = _BECDispatcher(args.bec_client)
|
||||
return _bec_dispatcher
|
||||
def disconnect_all(self, *args, **kwargs):
|
||||
self.disconnect_topics(self.client.connector._topics_cb)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from .editor import BECEditor
|
||||
from .figure import BECFigure, FigureConfig
|
||||
from .monitor import BECMonitor
|
||||
from .monitor_scatter_2D import BECMonitor2DScatter
|
||||
from .motor_control import (
|
||||
MotorControlAbsolute,
|
||||
MotorControlRelative,
|
||||
@@ -10,5 +9,5 @@ from .motor_control import (
|
||||
MotorThread,
|
||||
)
|
||||
from .motor_map import MotorMap
|
||||
from .plots import BECCurve, BECMotorMap, BECWaveform1D
|
||||
from .plots import BECCurve, BECMotorMap, BECWaveform
|
||||
from .scan_control import ScanControl
|
||||
|
||||
32
bec_widgets/widgets/editor/vscode.py
Normal file
32
bec_widgets/widgets/editor/vscode.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from qtpy.QtCore import QUrl
|
||||
from qtpy.QtWebEngineWidgets import QWebEngineView
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class WebsiteWidget(QWidget):
|
||||
def __init__(self, url):
|
||||
super().__init__()
|
||||
self.editor = QWebEngineView(self)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.editor)
|
||||
self.setLayout(layout)
|
||||
self.editor.setUrl(QUrl(url))
|
||||
|
||||
|
||||
class VSCodeEditor(WebsiteWidget):
|
||||
token = "bec"
|
||||
host = "localhost"
|
||||
port = 7000
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(f"http://{self.host}:{self.port}?tkn={self.token}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
mainWin = WebsiteWidget("https://scilog.psi.ch")
|
||||
mainWin.show()
|
||||
sys.exit(app.exec())
|
||||
59
bec_widgets/widgets/editor/vscode_server.py
Normal file
59
bec_widgets/widgets/editor/vscode_server.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Module to handle the vscode server
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
|
||||
|
||||
class VSCodeServer:
|
||||
"""
|
||||
Class to handle the vscode server
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __init__(self, port=7000, token="bec"):
|
||||
self.started = False
|
||||
self._server = None
|
||||
self.port = port
|
||||
self.token = token
|
||||
|
||||
def __new__(cls, *args, forced=False, **kwargs):
|
||||
if cls._instance is None or forced:
|
||||
cls._instance = super(VSCodeServer, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
Start the vscode server in a subprocess
|
||||
"""
|
||||
if self.started:
|
||||
return
|
||||
self._server = subprocess.Popen(
|
||||
f"code serve-web --port {self.port} --connection-token={self.token} --accept-server-license-terms",
|
||||
shell=True,
|
||||
)
|
||||
self.started = True
|
||||
|
||||
def wait(self):
|
||||
"""
|
||||
Wait for the server to finish
|
||||
"""
|
||||
if not self.started:
|
||||
return
|
||||
if not self._server:
|
||||
return
|
||||
self._server.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Start the vscode server")
|
||||
parser.add_argument("--port", type=int, default=7000, help="Port to start the server")
|
||||
parser.add_argument("--token", type=str, default="bec", help="Token to start the server")
|
||||
args = parser.parse_args()
|
||||
|
||||
server = VSCodeServer(port=args.port, token=args.token)
|
||||
server.start_server()
|
||||
server.wait()
|
||||
@@ -19,7 +19,7 @@ from bec_widgets.widgets.plots import (
|
||||
BECImageShow,
|
||||
BECMotorMap,
|
||||
BECPlotBase,
|
||||
BECWaveform1D,
|
||||
BECWaveform,
|
||||
Waveform1DConfig,
|
||||
WidgetConfig,
|
||||
)
|
||||
@@ -44,7 +44,7 @@ class WidgetHandler:
|
||||
def __init__(self):
|
||||
self.widget_factory = {
|
||||
"PlotBase": (BECPlotBase, WidgetConfig),
|
||||
"Waveform1D": (BECWaveform1D, Waveform1DConfig),
|
||||
"Waveform1D": (BECWaveform, Waveform1DConfig),
|
||||
"ImShow": (BECImageShow, ImageConfig),
|
||||
"MotorMap": (BECMotorMap, MotorMapConfig),
|
||||
}
|
||||
@@ -97,6 +97,7 @@ class WidgetHandler:
|
||||
|
||||
class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"axes",
|
||||
"widgets",
|
||||
"add_plot",
|
||||
@@ -109,7 +110,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
"change_layout",
|
||||
"change_theme",
|
||||
"clear_all",
|
||||
"get_config",
|
||||
]
|
||||
|
||||
clean_signal = pyqtSignal()
|
||||
@@ -164,18 +164,21 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
self,
|
||||
x_name: str = None,
|
||||
y_name: str = None,
|
||||
z_name: str = None,
|
||||
x_entry: str = None,
|
||||
y_entry: str = None,
|
||||
z_entry: str = None,
|
||||
x: list | np.ndarray = None,
|
||||
y: list | np.ndarray = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform1D:
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
Args:
|
||||
@@ -197,8 +200,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
|
||||
# TODO remove repetition from .plot method
|
||||
|
||||
# User wants to add scan curve
|
||||
if x_name is not None and y_name is not None and x is None and y is None:
|
||||
# User wants to add scan curve -> 1D Waveform
|
||||
if x_name is not None and y_name is not None and z_name is None and x is None and y is None:
|
||||
waveform.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
@@ -208,7 +211,27 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
color=color,
|
||||
label=label,
|
||||
)
|
||||
# User wants to add custom curve
|
||||
# User wants to add scan curve -> 2D Waveform Scatter
|
||||
if (
|
||||
x_name is not None
|
||||
and y_name is not None
|
||||
and z_name is not None
|
||||
and x is None
|
||||
and y is None
|
||||
):
|
||||
waveform.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
# User wants to add custom curve
|
||||
elif x is not None and y is not None and x_name is None and y_name is None:
|
||||
waveform.add_curve_custom(
|
||||
x=x,
|
||||
@@ -223,52 +246,81 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
self,
|
||||
x_name: str = None,
|
||||
y_name: str = None,
|
||||
z_name: str = None,
|
||||
x_entry: str = None,
|
||||
y_entry: str = None,
|
||||
z_entry: str = None,
|
||||
x: list | np.ndarray = None,
|
||||
y: list | np.ndarray = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform1D:
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
|
||||
Args:
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
y_entry(str): The name of the entry for the y-axis.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECWaveform1D: The waveform plot widget.
|
||||
BECWaveform: The waveform plot widget.
|
||||
"""
|
||||
waveform = self._find_first_widget_by_class(BECWaveform1D, can_fail=True)
|
||||
waveform = self._find_first_widget_by_class(BECWaveform, can_fail=True)
|
||||
if waveform is not None:
|
||||
if axis_kwargs:
|
||||
waveform.set(**axis_kwargs)
|
||||
else:
|
||||
waveform = self.add_plot(**axis_kwargs)
|
||||
|
||||
# User wants to add scan curve
|
||||
if x_name is not None and y_name is not None and x is None and y is None:
|
||||
# User wants to add scan curve -> 1D Waveform
|
||||
if x_name is not None and y_name is not None and z_name is None and x is None and y is None:
|
||||
waveform.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
validate=validate,
|
||||
color=color,
|
||||
color_map_z="plasma",
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
# User wants to add scan curve -> 2D Waveform Scatter
|
||||
elif (
|
||||
x_name is not None
|
||||
and y_name is not None
|
||||
and z_name is not None
|
||||
and x is None
|
||||
and y is None
|
||||
):
|
||||
waveform.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
# User wants to add custom curve
|
||||
elif x is not None and y is not None and x_name is None and y_name is None:
|
||||
elif (
|
||||
x is not None and y is not None and x_name is None and y_name is None and z_name is None
|
||||
):
|
||||
waveform.add_curve_custom(
|
||||
x=x,
|
||||
y=y,
|
||||
@@ -735,8 +787,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
|
||||
def clear_all(self):
|
||||
"""Clear all widgets from the figure and reset to default state"""
|
||||
for widget in self._widgets.values():
|
||||
widget.cleanup()
|
||||
# for widget in self._widgets.values():
|
||||
# widget.cleanup()
|
||||
self.clear()
|
||||
self._widgets = defaultdict(dict)
|
||||
self.grid = []
|
||||
@@ -744,155 +796,3 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
self.config = FigureConfig(
|
||||
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
|
||||
)
|
||||
|
||||
|
||||
##################################################
|
||||
##################################################
|
||||
# Debug window
|
||||
##################################################
|
||||
##################################################
|
||||
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
|
||||
|
||||
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.kernel_manager = QtInProcessKernelManager()
|
||||
self.kernel_manager.start_kernel(show_banner=False)
|
||||
self.kernel_client = self.kernel_manager.client()
|
||||
self.kernel_client.start_channels()
|
||||
|
||||
self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
|
||||
# self.set_console_font_size(70)
|
||||
|
||||
def shutdown_kernel(self):
|
||||
self.kernel_client.stop_channels()
|
||||
self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
class DebugWindow(QWidget): # pragma: no cover:
|
||||
"""Debug window for BEC widgets"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
uic.loadUi(os.path.join(current_path, "figure_debug_minimal.ui"), self)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
self.splitter.setSizes([200, 100])
|
||||
self.safe_close = False
|
||||
# self.figure.clean_signal.connect(self.confirm_close)
|
||||
|
||||
# console push
|
||||
self.console.kernel_manager.kernel.shell.push(
|
||||
{
|
||||
"fig": self.figure,
|
||||
"w1": self.w1,
|
||||
"w2": self.w2,
|
||||
"w3": self.w3,
|
||||
"w4": self.w4,
|
||||
"bec": self.figure.client,
|
||||
"scans": self.figure.client.scans,
|
||||
"dev": self.figure.client.device_manager.devices,
|
||||
}
|
||||
)
|
||||
|
||||
def _init_ui(self):
|
||||
# Plotting window
|
||||
self.glw_1_layout = QVBoxLayout(self.glw) # Create a new QVBoxLayout
|
||||
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
|
||||
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
|
||||
|
||||
# add stuff to figure
|
||||
self._init_figure()
|
||||
|
||||
self.console_layout = QVBoxLayout(self.widget_console)
|
||||
self.console = JupyterConsoleWidget()
|
||||
self.console_layout.addWidget(self.console)
|
||||
self.console.set_default_style("linux")
|
||||
|
||||
def _init_figure(self):
|
||||
self.figure.add_widget(widget_type="Waveform1D", row=0, col=0, title="Widget 1")
|
||||
self.figure.add_widget(widget_type="Waveform1D", row=0, col=1, title="Widget 2")
|
||||
self.figure.add_image(
|
||||
title="Image", row=1, col=0, color_map="viridis", color_bar="simple", vrange=(0, 100)
|
||||
)
|
||||
self.figure.add_image(title="Image", row=1, col=1, vrange=(0, 100))
|
||||
|
||||
self.w1 = self.figure[0, 0]
|
||||
self.w2 = self.figure[0, 1]
|
||||
self.w3 = self.figure[1, 0]
|
||||
self.w4 = self.figure[1, 1]
|
||||
|
||||
# curves for w1
|
||||
self.w1.add_curve_scan("samx", "bpm4i", pen_style="dash")
|
||||
self.w1.add_curve_custom(
|
||||
x=[1, 2, 3, 4, 5],
|
||||
y=[1, 2, 3, 4, 5],
|
||||
label="curve-custom",
|
||||
color="blue",
|
||||
pen_style="dashdot",
|
||||
)
|
||||
self.c1 = self.w1.get_config()
|
||||
|
||||
# curves for w2
|
||||
self.w2.add_curve_scan("samx", "bpm3a", pen_style="solid")
|
||||
self.w2.add_curve_scan("samx", "bpm4d", pen_style="dot")
|
||||
self.w2.add_curve_custom(
|
||||
x=[1, 2, 3, 4, 5], y=[5, 4, 3, 2, 1], color="red", pen_style="dashdot"
|
||||
)
|
||||
|
||||
# curves for w3
|
||||
# self.w3.add_curve_scan("samx", "bpm4i", pen_style="dash")
|
||||
# self.w3.add_curve_custom(
|
||||
# x=[1, 2, 3, 4, 5],
|
||||
# y=[1, 2, 3, 4, 5],
|
||||
# label="curve-custom",
|
||||
# color="blue",
|
||||
# pen_style="dashdot",
|
||||
# )
|
||||
|
||||
# curves for w4
|
||||
# self.w4.add_curve_scan("samx", "bpm4i", pen_style="dash")
|
||||
# self.w4.add_curve_custom(
|
||||
# x=[1, 2, 3, 4, 5],
|
||||
# y=[1, 2, 3, 4, 5],
|
||||
# label="curve-custom",
|
||||
# color="blue",
|
||||
# pen_style="dashdot",
|
||||
# )
|
||||
|
||||
# Image setting for w3
|
||||
|
||||
self.w3.add_monitor_image("eiger", vrange=(0, 100), color_bar="full")
|
||||
|
||||
# Image setting for w4
|
||||
self.w4.add_monitor_image("eiger", vrange=(0, 100), color_map="viridis")
|
||||
|
||||
# def confirm_close(self):
|
||||
# self.safe_close = True
|
||||
#
|
||||
# def closeEvent(self, event):
|
||||
# self.figure.cleanup()
|
||||
# if self.safe_close == True:
|
||||
# print("Safe close")
|
||||
# event.accept()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
win = DebugWindow()
|
||||
win.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .monitor_scatter_2D import BECMonitor2DScatter
|
||||
@@ -1,374 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import MessageEndpoints
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import yaml_dialog
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
CONFIG_DEFAULT = {
|
||||
"plot_settings": {"colormap": "CET-L4", "num_columns": 1},
|
||||
"waveform2D": [
|
||||
{
|
||||
"plot_name": "Waveform 2D Scatter (1)",
|
||||
"x_label": "Sam X",
|
||||
"y_label": "Sam Y",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "samy", "entry": "samy"}],
|
||||
"z": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"plot_name": "Waveform 2D Scatter (2)",
|
||||
"x_label": "Sam Y",
|
||||
"y_label": "Sam X",
|
||||
"signals": {
|
||||
"x": [{"name": "samy", "entry": "samy"}],
|
||||
"y": [{"name": "samx", "entry": "samx"}],
|
||||
"z": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class BECMonitor2DScatter(QWidget):
|
||||
update_signal = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
client=None,
|
||||
config: dict = None,
|
||||
enable_crosshair: bool = True,
|
||||
gui_id=None,
|
||||
skip_validation: bool = True,
|
||||
toolbar_enabled=True,
|
||||
):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
# Client and device manager from BEC
|
||||
self.plot_data = None
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.queue = self.client.queue
|
||||
|
||||
self.validator = None # TODO implement validator when ready
|
||||
self.gui_id = gui_id
|
||||
|
||||
if self.gui_id is None:
|
||||
self.gui_id = self.__class__.__name__ + str(time.time())
|
||||
|
||||
# Connect dispatcher slots #TODO connect endpoints related to CLI
|
||||
bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
|
||||
# Config related variables
|
||||
self.plot_data = None
|
||||
self.plot_settings = None
|
||||
self.num_columns = None
|
||||
self.database = {}
|
||||
self.plots = {}
|
||||
self.grid_coordinates = []
|
||||
|
||||
self.curves_data = {}
|
||||
# Current configuration
|
||||
self.config = config
|
||||
self.skip_validation = skip_validation
|
||||
|
||||
# Enable crosshair
|
||||
self.enable_crosshair = enable_crosshair
|
||||
|
||||
# Displayed Data
|
||||
self.database = {}
|
||||
|
||||
self.crosshairs = None
|
||||
self.plots = None
|
||||
self.curves_data = None
|
||||
self.grid_coordinates = None
|
||||
self.scan_id = None
|
||||
|
||||
# Connect the update signal to the update plot method
|
||||
self.proxy_update_plot = pg.SignalProxy(
|
||||
self.update_signal, rateLimit=10, slot=self.update_plot
|
||||
)
|
||||
|
||||
# Init UI
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.setLayout(self.layout)
|
||||
if toolbar_enabled: # TODO implement toolbar when ready
|
||||
self._init_toolbar()
|
||||
|
||||
self.glw = pg.GraphicsLayoutWidget()
|
||||
self.layout.addWidget(self.glw)
|
||||
|
||||
if self.config is None:
|
||||
print("No initial config found for BECDeviceMonitor")
|
||||
else:
|
||||
self.on_config_update(self.config)
|
||||
|
||||
def _init_toolbar(self):
|
||||
"""Initialize the toolbar."""
|
||||
# TODO implement toolbar when ready
|
||||
# from bec_widgets.widgets import ModularToolBar
|
||||
#
|
||||
# # Create and configure the toolbar
|
||||
# self.toolbar = ModularToolBar(self)
|
||||
#
|
||||
# # Add the toolbar to the layout
|
||||
# self.layout.addWidget(self.toolbar)
|
||||
|
||||
def _init_config(self):
|
||||
"""Initialize the configuration."""
|
||||
# Global widget settings
|
||||
self._get_global_settings()
|
||||
|
||||
# Plot data
|
||||
self.plot_data = self.config.get("waveform2D", [])
|
||||
|
||||
# Initiate database
|
||||
self.database = self._init_database()
|
||||
|
||||
# Initialize the plot UI
|
||||
self._init_ui()
|
||||
|
||||
def _get_global_settings(self):
|
||||
"""Get the global widget settings."""
|
||||
|
||||
self.plot_settings = self.config.get("plot_settings", {})
|
||||
|
||||
self.num_columns = self.plot_settings.get("num_columns", 1)
|
||||
self.colormap = self.plot_settings.get("colormap", "viridis")
|
||||
|
||||
def _init_database(self) -> dict:
|
||||
"""
|
||||
Initialize the database to store the data for each plot.
|
||||
Returns:
|
||||
dict: The database.
|
||||
"""
|
||||
|
||||
database = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
||||
|
||||
return database
|
||||
|
||||
def _init_ui(self, num_columns: int = 3) -> None:
|
||||
"""
|
||||
Initialize the UI components, create plots and store their grid positions.
|
||||
|
||||
Args:
|
||||
num_columns (int): Number of columns to wrap the layout.
|
||||
|
||||
This method initializes a dictionary `self.plots` to store the plot objects
|
||||
along with their corresponding x and y signal names. It dynamically arranges
|
||||
the plots in a grid layout based on the given number of columns and dynamically
|
||||
stretches the last plots to fit the remaining space.
|
||||
"""
|
||||
self.glw.clear()
|
||||
self.plots = {}
|
||||
self.imageItems = {}
|
||||
self.grid_coordinates = []
|
||||
self.scatterPlots = {}
|
||||
self.colorBars = {}
|
||||
|
||||
num_plots = len(self.plot_data)
|
||||
# Check if num_columns exceeds the number of plots
|
||||
if num_columns >= num_plots:
|
||||
num_columns = num_plots
|
||||
self.plot_settings["num_columns"] = num_columns # Update the settings
|
||||
print(
|
||||
"Warning: num_columns in the YAML file was greater than the number of plots."
|
||||
f" Resetting num_columns to number of plots:{num_columns}."
|
||||
)
|
||||
else:
|
||||
self.plot_settings["num_columns"] = num_columns # Update the settings
|
||||
|
||||
num_rows = num_plots // num_columns
|
||||
last_row_cols = num_plots % num_columns
|
||||
remaining_space = num_columns - last_row_cols
|
||||
|
||||
for i, plot_config in enumerate(self.plot_data):
|
||||
row, col = i // num_columns, i % num_columns
|
||||
colspan = 1
|
||||
|
||||
if row == num_rows and remaining_space > 0:
|
||||
if last_row_cols == 1:
|
||||
colspan = num_columns
|
||||
else:
|
||||
colspan = remaining_space // last_row_cols + 1
|
||||
remaining_space -= colspan - 1
|
||||
last_row_cols -= 1
|
||||
|
||||
plot_name = plot_config.get("plot_name", "")
|
||||
|
||||
x_label = plot_config.get("x_label", "")
|
||||
y_label = plot_config.get("y_label", "")
|
||||
|
||||
plot = self.glw.addPlot(row=row, col=col, colspan=colspan, title=plot_name)
|
||||
plot.setLabel("bottom", x_label)
|
||||
plot.setLabel("left", y_label)
|
||||
plot.addLegend()
|
||||
|
||||
self.plots[plot_name] = plot
|
||||
|
||||
self.grid_coordinates.append((row, col))
|
||||
|
||||
self._init_curves()
|
||||
|
||||
def _init_curves(self):
|
||||
"""Init scatter plot pg containers"""
|
||||
self.scatterPlots = {}
|
||||
for i, plot_config in enumerate(self.plot_data):
|
||||
plot_name = plot_config.get("plot_name", "")
|
||||
plot = self.plots[plot_name]
|
||||
plot.clear()
|
||||
|
||||
# Create ScatterPlotItem for each plot
|
||||
scatterPlot = pg.ScatterPlotItem(size=10)
|
||||
plot.addItem(scatterPlot)
|
||||
self.scatterPlots[plot_name] = scatterPlot
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict):
|
||||
"""
|
||||
Validate and update the configuration settings.
|
||||
Args:
|
||||
config(dict): Configuration settings
|
||||
"""
|
||||
# TODO implement BEC CLI commands similar to BECPlotter
|
||||
# convert config from BEC CLI to correct formatting
|
||||
config_tag = config.get("config", None)
|
||||
if config_tag is not None:
|
||||
config = config["config"]
|
||||
|
||||
if self.skip_validation is True:
|
||||
self.config = config
|
||||
self._init_config()
|
||||
|
||||
else: # TODO implement validator
|
||||
print("Do validation")
|
||||
|
||||
def flush(self):
|
||||
"""Reset current plot"""
|
||||
|
||||
self.database = self._init_database()
|
||||
self._init_curves()
|
||||
|
||||
@pyqtSlot(dict, dict)
|
||||
def on_scan_segment(self, msg, metadata):
|
||||
"""
|
||||
Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
|
||||
|
||||
Args:
|
||||
msg (dict): Message received with scan data.
|
||||
metadata (dict): Metadata of the scan.
|
||||
"""
|
||||
|
||||
# TODO check if this is correct
|
||||
current_scan_id = msg.get("scan_id", None)
|
||||
if current_scan_id is None:
|
||||
return
|
||||
|
||||
if current_scan_id != self.scan_id:
|
||||
self.scan_id = current_scan_id
|
||||
self.scan_data = self.queue.scan_storage.find_scan_by_ID(self.scan_id)
|
||||
if not self.scan_data:
|
||||
print(f"No data found for scan_id: {self.scan_id}") # TODO better error
|
||||
return
|
||||
self.flush()
|
||||
|
||||
# Update the database with new data
|
||||
self.update_database_with_scan_data(msg)
|
||||
|
||||
# Emit signal to update plot #TODO could be moved to update_database_with_scan_data just for coresponding plot name
|
||||
self.update_signal.emit()
|
||||
|
||||
def update_database_with_scan_data(self, msg):
|
||||
"""
|
||||
Update the database with data from the new scan segment.
|
||||
|
||||
Args:
|
||||
msg (dict): Message containing the new scan data.
|
||||
"""
|
||||
data = msg.get("data", {})
|
||||
for plot_config in self.plot_data: # Iterate over the list
|
||||
plot_name = plot_config["plot_name"]
|
||||
x_signal = plot_config["signals"]["x"][0]["name"]
|
||||
y_signal = plot_config["signals"]["y"][0]["name"]
|
||||
z_signal = plot_config["signals"]["z"][0]["name"]
|
||||
|
||||
if x_signal in data and y_signal in data and z_signal in data:
|
||||
x_value = data[x_signal][x_signal]["value"]
|
||||
y_value = data[y_signal][y_signal]["value"]
|
||||
z_value = data[z_signal][z_signal]["value"]
|
||||
|
||||
# Update database for the corresponding plot
|
||||
self.database[plot_name]["x"][x_signal].append(x_value)
|
||||
self.database[plot_name]["y"][y_signal].append(y_value)
|
||||
self.database[plot_name]["z"][z_signal].append(z_value)
|
||||
|
||||
def update_plot(self):
|
||||
"""
|
||||
Update the plots with the latest data from the database.
|
||||
"""
|
||||
for plot_name, scatterPlot in self.scatterPlots.items():
|
||||
x_data = self.database[plot_name]["x"]
|
||||
y_data = self.database[plot_name]["y"]
|
||||
z_data = self.database[plot_name]["z"]
|
||||
|
||||
if x_data and y_data and z_data:
|
||||
# Extract values for each axis
|
||||
x_values = next(iter(x_data.values()), [])
|
||||
y_values = next(iter(y_data.values()), [])
|
||||
z_values = next(iter(z_data.values()), [])
|
||||
|
||||
# Check if the data lists are not empty
|
||||
if x_values and y_values and z_values:
|
||||
# Normalize z_values for color mapping
|
||||
z_min, z_max = np.min(z_values), np.max(z_values)
|
||||
if z_max != z_min: # Ensure that there is a range in the z values
|
||||
z_values_norm = (z_values - z_min) / (z_max - z_min)
|
||||
colormap = pg.colormap.get(
|
||||
self.colormap
|
||||
) # using colormap from global settings
|
||||
colors = [colormap.map(z) for z in z_values_norm]
|
||||
|
||||
# Update scatter plot data with colors
|
||||
scatterPlot.setData(x=x_values, y=y_values, brush=colors)
|
||||
else:
|
||||
# Handle case where all z values are the same (e.g., avoid division by zero)
|
||||
scatterPlot.setData(x=x_values, y=y_values) # Default brush can be used
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--config_file", help="Path to the config file.")
|
||||
parser.add_argument("--config", help="Path to the config file.")
|
||||
parser.add_argument("--id", help="GUI ID.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.config is not None:
|
||||
# Load config from file
|
||||
config = json.loads(args.config)
|
||||
elif args.config_file is not None:
|
||||
# Load config from file
|
||||
config = yaml_dialog.load_yaml(args.config_file)
|
||||
else:
|
||||
config = CONFIG_DEFAULT
|
||||
|
||||
client = BECDispatcher().client
|
||||
client.start()
|
||||
app = QApplication(sys.argv)
|
||||
monitor = BECMonitor2DScatter(config=config, gui_id=args.id, skip_validation=True)
|
||||
monitor.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -217,7 +217,6 @@ class MotorMap(pg.GraphicsLayoutWidget):
|
||||
bec_dispatcher.connect_slot(
|
||||
self.on_device_readback,
|
||||
endpoints,
|
||||
single_callback_for_all_topics=True,
|
||||
)
|
||||
|
||||
def _add_limits_to_plot_data(self):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .image import BECImageItem, BECImageShow, ImageItemConfig
|
||||
from .motor_map import BECMotorMap, MotorMapConfig
|
||||
from .plot_base import AxisConfig, BECPlotBase, WidgetConfig
|
||||
from .waveform1d import BECCurve, BECWaveform1D, Waveform1DConfig
|
||||
from .waveform import BECCurve, BECWaveform, Waveform1DConfig
|
||||
|
||||
@@ -59,6 +59,7 @@ class ImageConfig(WidgetConfig):
|
||||
|
||||
class BECImageItem(BECConnector, pg.ImageItem):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"set",
|
||||
"set_fft",
|
||||
"set_log",
|
||||
@@ -70,7 +71,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
"set_auto_downsample",
|
||||
"set_monitor",
|
||||
"set_vrange",
|
||||
"get_config",
|
||||
"get_data",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -243,6 +244,14 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
self.color_bar.setLevels(min=vmin, max=vmax)
|
||||
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
||||
|
||||
def get_data(self) -> np.ndarray:
|
||||
"""
|
||||
Get the data of the image.
|
||||
Returns:
|
||||
np.ndarray: The data of the image.
|
||||
"""
|
||||
return self.image
|
||||
|
||||
def _add_color_bar(
|
||||
self, color_bar_style: str = "simple", vrange: Optional[tuple[int, int]] = None
|
||||
):
|
||||
@@ -281,9 +290,9 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
|
||||
class BECImageShow(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"add_image_by_config",
|
||||
"get_image_config",
|
||||
"get_image_list",
|
||||
"get_image_dict",
|
||||
"add_monitor_image",
|
||||
"add_custom_image",
|
||||
@@ -298,7 +307,6 @@ class BECImageShow(BECPlotBase):
|
||||
"set_rotation",
|
||||
"set_transpose",
|
||||
"toggle_threading",
|
||||
"get_config",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
@@ -311,6 +319,7 @@ class BECImageShow(BECPlotBase):
|
||||
"lock_aspect_ratio",
|
||||
"plot",
|
||||
"remove",
|
||||
"images",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -448,7 +457,8 @@ class BECImageShow(BECPlotBase):
|
||||
else:
|
||||
return image.config # TODO check if this works
|
||||
|
||||
def get_image_list(self) -> list[BECImageItem]:
|
||||
@property
|
||||
def images(self) -> list[BECImageItem]:
|
||||
"""
|
||||
Get the list of images.
|
||||
Returns:
|
||||
@@ -460,6 +470,16 @@ class BECImageShow(BECPlotBase):
|
||||
images.append(image)
|
||||
return images
|
||||
|
||||
@images.setter
|
||||
def images(self, value: dict[str, dict[str, BECImageItem]]):
|
||||
"""
|
||||
Set the images from a dictionary.
|
||||
|
||||
Args:
|
||||
value (dict[str, dict[str, BECImageItem]]): The images to set, organized by source and id.
|
||||
"""
|
||||
self._images = value
|
||||
|
||||
def get_image_dict(self) -> dict[str, dict[str, BECImageItem]]:
|
||||
"""
|
||||
Get all images.
|
||||
|
||||
@@ -14,7 +14,7 @@ from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.widgets.plots.plot_base import BECPlotBase, WidgetConfig
|
||||
from bec_widgets.widgets.plots.waveform1d import Signal, SignalData
|
||||
from bec_widgets.widgets.plots.waveform import Signal, SignalData
|
||||
|
||||
|
||||
class MotorMapConfig(WidgetConfig):
|
||||
@@ -36,12 +36,14 @@ class MotorMapConfig(WidgetConfig):
|
||||
|
||||
class BECMotorMap(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"change_motors",
|
||||
"set_max_points",
|
||||
"set_precision",
|
||||
"set_num_dim_points",
|
||||
"set_background_value",
|
||||
"set_scatter_size",
|
||||
"get_data",
|
||||
]
|
||||
|
||||
# QT Signals
|
||||
@@ -127,6 +129,18 @@ class BECMotorMap(BECPlotBase):
|
||||
# Redraw the motor map
|
||||
self._make_motor_map()
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
data = {
|
||||
"x": self.database_buffer["x"],
|
||||
"y": self.database_buffer["y"],
|
||||
}
|
||||
return data
|
||||
|
||||
# TODO setup all visual properties
|
||||
def set_max_points(self, max_points: int) -> None:
|
||||
"""
|
||||
@@ -185,9 +199,7 @@ class BECMotorMap(BECPlotBase):
|
||||
MessageEndpoints.device_readback(self.motor_y),
|
||||
]
|
||||
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_device_readback, endpoints, single_callback_for_all_topics=True
|
||||
)
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, endpoints)
|
||||
|
||||
def _make_motor_map(self):
|
||||
"""
|
||||
|
||||
@@ -37,7 +37,7 @@ class WidgetConfig(ConnectionConfig):
|
||||
|
||||
class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
USER_ACCESS = [
|
||||
"get_config",
|
||||
"config_dict",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
|
||||
@@ -34,6 +34,7 @@ class Signal(BaseModel):
|
||||
source: str
|
||||
x: SignalData # TODO maybe add metadata for config gui later
|
||||
y: SignalData
|
||||
z: Optional[SignalData] = None
|
||||
|
||||
|
||||
class CurveConfig(ConnectionConfig):
|
||||
@@ -49,12 +50,13 @@ class CurveConfig(ConnectionConfig):
|
||||
)
|
||||
source: Optional[str] = Field(None, description="The source of the curve.")
|
||||
signals: Optional[Signal] = Field(None, description="The signal of the curve.")
|
||||
colormap: Optional[str] = Field("plasma", description="The colormap of the curves z gradient.")
|
||||
|
||||
|
||||
class Waveform1DConfig(WidgetConfig):
|
||||
color_palette: Literal["plasma", "viridis", "inferno", "magma"] = Field(
|
||||
"plasma", description="The color palette of the figure widget."
|
||||
)
|
||||
) # TODO can be extended to all colormaps from current pyqtgraph session
|
||||
curves: dict[str, CurveConfig] = Field(
|
||||
{}, description="The list of curves to be added to the 1D waveform widget."
|
||||
)
|
||||
@@ -62,9 +64,11 @@ class Waveform1DConfig(WidgetConfig):
|
||||
|
||||
class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"set",
|
||||
"set_data",
|
||||
"set_color",
|
||||
"set_colormap",
|
||||
"set_symbol",
|
||||
"set_symbol_color",
|
||||
"set_symbol_size",
|
||||
@@ -135,6 +139,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"color": self.set_color,
|
||||
"colormap": self.set_colormap,
|
||||
"symbol": self.set_symbol,
|
||||
"symbol_color": self.set_symbol_color,
|
||||
"symbol_size": self.set_symbol_size,
|
||||
@@ -203,6 +208,14 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
self.config.pen_style = pen_style
|
||||
self.apply_config()
|
||||
|
||||
def set_colormap(self, colormap: str):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
self.config.colormap = colormap
|
||||
|
||||
def get_data(self) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Get the data of the curve.
|
||||
@@ -213,8 +226,9 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
return x_data, y_data
|
||||
|
||||
|
||||
class BECWaveform1D(BECPlotBase):
|
||||
class BECWaveform(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"add_curve_scan",
|
||||
"add_curve_custom",
|
||||
"remove_curve",
|
||||
@@ -224,7 +238,6 @@ class BECWaveform1D(BECPlotBase):
|
||||
"get_curve_config",
|
||||
"apply_config",
|
||||
"get_all_data",
|
||||
"get_config",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
@@ -467,9 +480,12 @@ class BECWaveform1D(BECPlotBase):
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
z_name: Optional[str] = None,
|
||||
x_entry: Optional[str] = None,
|
||||
y_entry: Optional[str] = None,
|
||||
z_entry: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate_bec: bool = True,
|
||||
**kwargs,
|
||||
@@ -481,7 +497,10 @@ class BECWaveform1D(BECPlotBase):
|
||||
x_entry(str): Entry of the x signal.
|
||||
y_name(str): Name of the y signal.
|
||||
y_entry(str): Entry of the y signal.
|
||||
z_name(str): Name of the z signal.
|
||||
z_entry(str): Entry of the z signal.
|
||||
color(str, optional): Color of the curve. Defaults to None.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str, optional): Label of the curve. Defaults to None.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
@@ -492,11 +511,14 @@ class BECWaveform1D(BECPlotBase):
|
||||
curve_source = "scan_segment"
|
||||
|
||||
# Get entry if not provided and validate
|
||||
x_entry, y_entry = self._validate_signal_entries(
|
||||
x_name, y_name, x_entry, y_entry, validate_bec
|
||||
x_entry, y_entry, z_entry = self._validate_signal_entries(
|
||||
x_name, y_name, z_name, x_entry, y_entry, z_entry, validate_bec
|
||||
)
|
||||
|
||||
label = label or f"{y_name}-{y_entry}"
|
||||
if z_name is not None and z_entry is not None:
|
||||
label = label or f"{z_name}-{z_entry}"
|
||||
else:
|
||||
label = label or f"{y_name}-{y_entry}"
|
||||
|
||||
curve_exits = self._check_curve_id(label, self._curves_data)
|
||||
if curve_exits:
|
||||
@@ -515,11 +537,13 @@ class BECWaveform1D(BECPlotBase):
|
||||
parent_id=self.gui_id,
|
||||
label=label,
|
||||
color=color,
|
||||
color_map=color_map_z,
|
||||
source=curve_source,
|
||||
signals=Signal(
|
||||
source=curve_source,
|
||||
x=SignalData(name=x_name, entry=x_entry),
|
||||
y=SignalData(name=y_name, entry=y_entry),
|
||||
z=SignalData(name=z_name, entry=z_entry) if z_name else None,
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
@@ -530,28 +554,35 @@ class BECWaveform1D(BECPlotBase):
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
z_name: str | None,
|
||||
x_entry: str | None,
|
||||
y_entry: str | None,
|
||||
z_entry: str | None,
|
||||
validate_bec: bool = True,
|
||||
) -> tuple[str, str]:
|
||||
) -> tuple[str, str, str | None]:
|
||||
"""
|
||||
Validate the signal name and entry.
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
y_name(str): Name of the y signal.
|
||||
z_name(str): Name of the z signal.
|
||||
x_entry(str|None): Entry of the x signal.
|
||||
y_entry(str|None): Entry of the y signal.
|
||||
z_entry(str|None): Entry of the z signal.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
Returns:
|
||||
tuple[str,str]: Validated x and y entries.
|
||||
tuple[str,str,str|None]: Validated x, y, z entries.
|
||||
"""
|
||||
if validate_bec:
|
||||
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
|
||||
y_entry = self.entry_validator.validate_signal(y_name, y_entry)
|
||||
if z_name:
|
||||
z_entry = self.entry_validator.validate_signal(z_name, z_entry)
|
||||
else:
|
||||
x_entry = x_name if x_entry is None else x_entry
|
||||
y_entry = y_name if y_entry is None else y_entry
|
||||
return x_entry, y_entry
|
||||
z_entry = z_name if z_entry is None else z_entry
|
||||
return x_entry, y_entry, z_entry
|
||||
|
||||
def _check_curve_id(self, val: Any, dict_to_check: dict) -> bool:
|
||||
"""
|
||||
@@ -654,19 +685,54 @@ class BECWaveform1D(BECPlotBase):
|
||||
Args:
|
||||
data(ScanData): Data from the scan segment.
|
||||
"""
|
||||
data_x = None
|
||||
data_y = None
|
||||
data_z = None
|
||||
for curve_id, curve in self._curves_data["scan_segment"].items():
|
||||
x_name = curve.config.signals.x.name
|
||||
x_entry = curve.config.signals.x.entry
|
||||
y_name = curve.config.signals.y.name
|
||||
y_entry = curve.config.signals.y.entry
|
||||
if curve.config.signals.z:
|
||||
z_name = curve.config.signals.z.name
|
||||
z_entry = curve.config.signals.z.entry
|
||||
|
||||
try:
|
||||
data_x = data[x_name][x_entry].val
|
||||
data_y = data[y_name][y_entry].val
|
||||
if curve.config.signals.z:
|
||||
data_z = data[z_name][z_entry].val
|
||||
color_z = self._make_z_gradient(
|
||||
data_z, curve.config.colormap
|
||||
) # TODO decide how to implement custom gradient
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
curve.setData(data_x, data_y)
|
||||
if data_z is not None and color_z is not None:
|
||||
curve.setData(x=data_x, y=data_y, symbolBrush=color_z)
|
||||
else:
|
||||
curve.setData(data_x, data_y)
|
||||
|
||||
def _make_z_gradient(self, data_z: list | np.ndarray, colormap: str) -> list | None:
|
||||
"""
|
||||
Make a gradient color for the z values.
|
||||
Args:
|
||||
data_z(list|np.ndarray): Z values.
|
||||
colormap(str): Colormap for the gradient color.
|
||||
|
||||
Returns:
|
||||
list: List of colors for the z values.
|
||||
"""
|
||||
# Normalize z_values for color mapping
|
||||
z_min, z_max = np.min(data_z), np.max(data_z)
|
||||
|
||||
if z_max != z_min: # Ensure that there is a range in the z values
|
||||
z_values_norm = (data_z - z_min) / (z_max - z_min)
|
||||
colormap = pg.colormap.get(colormap) # using colormap from global settings
|
||||
colors = [colormap.map(z, mode="qcolor") for z in z_values_norm]
|
||||
return colors
|
||||
else:
|
||||
return None
|
||||
|
||||
def scan_history(self, scan_index: int = None, scan_id: str = None):
|
||||
"""
|
||||
2
setup.py
2
setup.py
@@ -1,7 +1,7 @@
|
||||
# pylint: disable= missing-module-docstring
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
__version__ = "0.45.0"
|
||||
__version__ = "0.46.6"
|
||||
|
||||
# Default to PyQt6 if no other Qt binding is installed
|
||||
QT_DEPENDENCY = "PyQt6>=6.0"
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
from bec_lib.bec_service import BECService
|
||||
|
||||
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def threads_check():
|
||||
current_threads = set(
|
||||
th
|
||||
for th in threading.enumerate()
|
||||
if "loguru" not in th.name and th is not threading.main_thread()
|
||||
)
|
||||
yield
|
||||
threads_after = set(
|
||||
th
|
||||
for th in threading.enumerate()
|
||||
if "loguru" not in th.name and th is not threading.main_thread()
|
||||
)
|
||||
additional_threads = threads_after - current_threads
|
||||
assert (
|
||||
len(additional_threads) == 0
|
||||
), f"Test creates {len(additional_threads)} threads that are not cleaned: {additional_threads}"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def bec_dispatcher(threads_check):
|
||||
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
|
||||
yield bec_dispatcher
|
||||
bec_dispatcher.disconnect_all()
|
||||
# clean BEC client
|
||||
bec_dispatcher.client.shutdown()
|
||||
# reinitialize singleton for next test
|
||||
bec_dispatcher_module._bec_dispatcher = None
|
||||
181
tests/end-2-end/test_bec_figure_rpc.py
Normal file
181
tests/end-2-end/test_bec_figure_rpc.py
Normal file
@@ -0,0 +1,181 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_lib import MessageEndpoints
|
||||
|
||||
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||
from bec_widgets.cli.server import BECWidgetsCLIServer
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_server(qtbot, bec_client_lib, threads_check):
|
||||
dispatcher = BECDispatcher(client=bec_client_lib) # Has to init singleton with fixture client
|
||||
server = BECWidgetsCLIServer(gui_id="id_test")
|
||||
qtbot.addWidget(server.fig)
|
||||
qtbot.waitExposed(server.fig)
|
||||
qtbot.wait(1000) # 1s long to wait until gui is ready
|
||||
yield server
|
||||
dispatcher.disconnect_all()
|
||||
server.client.shutdown()
|
||||
server.shutdown()
|
||||
dispatcher.reset_singleton()
|
||||
|
||||
|
||||
def test_rpc_waveform1d_custom_curve(rpc_server, qtbot):
|
||||
fig = BECFigure(rpc_server.gui_id)
|
||||
fig_server = rpc_server.fig
|
||||
|
||||
ax = fig.add_plot()
|
||||
curve = ax.add_curve_custom([1, 2, 3], [1, 2, 3])
|
||||
curve.set_color("red")
|
||||
curve = ax.curves[0]
|
||||
curve.set_color("blue")
|
||||
|
||||
assert len(fig_server.widgets) == 1
|
||||
assert len(fig_server.widgets["widget_1"].curves) == 1
|
||||
|
||||
|
||||
def test_rpc_plotting_shortcuts_init_configs(rpc_server, qtbot):
|
||||
fig = BECFigure(rpc_server.gui_id)
|
||||
fig_server = rpc_server.fig
|
||||
|
||||
plt = fig.plot("samx", "bpm4i")
|
||||
im = fig.image("eiger")
|
||||
motor_map = fig.motor_map("samx", "samy")
|
||||
plt_z = fig.add_plot("samx", "samy", "bpm4i")
|
||||
|
||||
# Checking if classes are correctly initialised
|
||||
assert len(fig_server.widgets) == 4
|
||||
assert plt.__class__.__name__ == "BECWaveform"
|
||||
assert plt.__class__ == BECWaveform
|
||||
assert im.__class__.__name__ == "BECImageShow"
|
||||
assert im.__class__ == BECImageShow
|
||||
assert motor_map.__class__.__name__ == "BECMotorMap"
|
||||
assert motor_map.__class__ == BECMotorMap
|
||||
|
||||
# check if the correct devices are set
|
||||
# plot
|
||||
assert plt.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
||||
"z": None,
|
||||
}
|
||||
# image
|
||||
assert im.config_dict["images"]["eiger"]["monitor"] == "eiger"
|
||||
# motor map
|
||||
assert motor_map.config_dict["signals"] == {
|
||||
"source": "device_readback",
|
||||
"x": {
|
||||
"name": "samx",
|
||||
"entry": "samx",
|
||||
"unit": None,
|
||||
"modifier": None,
|
||||
"limits": [-50.0, 50.0],
|
||||
},
|
||||
"y": {
|
||||
"name": "samy",
|
||||
"entry": "samy",
|
||||
"unit": None,
|
||||
"modifier": None,
|
||||
"limits": [-50.0, 50.0],
|
||||
},
|
||||
"z": None,
|
||||
}
|
||||
# plot with z scatter
|
||||
assert plt_z.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
"y": {"name": "samy", "entry": "samy", "unit": None, "modifier": None, "limits": None},
|
||||
"z": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
||||
}
|
||||
|
||||
|
||||
def test_rpc_waveform_scan(rpc_server, qtbot):
|
||||
fig = BECFigure(rpc_server.gui_id)
|
||||
|
||||
# add 3 different curves to track
|
||||
plt = fig.plot("samx", "bpm4i")
|
||||
fig.plot("samx", "bpm3a")
|
||||
fig.plot("samx", "bpm4d")
|
||||
|
||||
client = rpc_server.client
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
queue = client.queue
|
||||
|
||||
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
|
||||
# wait for scan to finish
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
|
||||
last_scan_data = queue.scan_storage.storage[-1].data
|
||||
|
||||
# get data from curves
|
||||
plt_data = plt.get_all_data()
|
||||
|
||||
# check plotted data
|
||||
assert plt_data["bpm4i-bpm4i"]["x"] == last_scan_data["samx"]["samx"].val
|
||||
assert plt_data["bpm4i-bpm4i"]["y"] == last_scan_data["bpm4i"]["bpm4i"].val
|
||||
assert plt_data["bpm3a-bpm3a"]["x"] == last_scan_data["samx"]["samx"].val
|
||||
assert plt_data["bpm3a-bpm3a"]["y"] == last_scan_data["bpm3a"]["bpm3a"].val
|
||||
assert plt_data["bpm4d-bpm4d"]["x"] == last_scan_data["samx"]["samx"].val
|
||||
assert plt_data["bpm4d-bpm4d"]["y"] == last_scan_data["bpm4d"]["bpm4d"].val
|
||||
|
||||
|
||||
def test_rpc_image(rpc_server, qtbot):
|
||||
fig = BECFigure(rpc_server.gui_id)
|
||||
|
||||
im = fig.image("eiger")
|
||||
|
||||
client = rpc_server.client
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
|
||||
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
|
||||
# wait for scan to finish
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
|
||||
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
|
||||
"data"
|
||||
].data
|
||||
qtbot.wait(500)
|
||||
last_image_plot = im.images[0].get_data()
|
||||
|
||||
# check plotted data
|
||||
np.testing.assert_equal(last_image_device, last_image_plot)
|
||||
|
||||
|
||||
def test_rpc_motor_map(rpc_server, qtbot):
|
||||
fig = BECFigure(rpc_server.gui_id)
|
||||
fig_server = rpc_server.fig
|
||||
|
||||
motor_map = fig.motor_map("samx", "samy")
|
||||
|
||||
client = rpc_server.client
|
||||
dev = client.device_manager.devices
|
||||
scans = client.scans
|
||||
|
||||
initial_pos_x = dev.samx.read()["samx"]["value"]
|
||||
initial_pos_y = dev.samy.read()["samy"]["value"]
|
||||
|
||||
status = scans.mv(dev.samx, 1, dev.samy, 2, relative=True)
|
||||
|
||||
# wait for scan to finish
|
||||
while not status.status == "COMPLETED":
|
||||
qtbot.wait(200)
|
||||
final_pos_x = dev.samx.read()["samx"]["value"]
|
||||
final_pos_y = dev.samy.read()["samy"]["value"]
|
||||
|
||||
# check plotted data
|
||||
motor_map_data = motor_map.get_data()
|
||||
|
||||
np.testing.assert_equal(
|
||||
[motor_map_data["x"][0], motor_map_data["y"][0]], [initial_pos_x, initial_pos_y]
|
||||
)
|
||||
np.testing.assert_equal(
|
||||
[motor_map_data["x"][-1], motor_map_data["y"][-1]], [final_pos_x, final_pos_y]
|
||||
)
|
||||
@@ -1,239 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from bec_lib.connector import MessageObject
|
||||
from bec_lib.messages import ScanMessage
|
||||
|
||||
msg = MessageObject(topic="", value=ScanMessage(point_id=0, scan_id=0, data={}))
|
||||
|
||||
|
||||
@pytest.fixture(name="consumer")
|
||||
def _consumer(bec_dispatcher):
|
||||
bec_dispatcher.client.connector = Mock()
|
||||
yield bec_dispatcher.client.connector
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Failed to connect to redis.")
|
||||
def test_connect_one_slot(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
consumer.register.assert_called_once()
|
||||
# trigger consumer callback as if a message was published
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
slot1.assert_called_once()
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
|
||||
|
||||
def test_connect_identical(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
consumer.register.assert_called_once()
|
||||
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
slot1.assert_called_once()
|
||||
|
||||
|
||||
def test_connect_many_slots_one_topic(bec_dispatcher, consumer):
|
||||
slot1, slot2 = Mock(), Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
consumer.register.assert_called_once()
|
||||
bec_dispatcher.connect_slot(slot=slot2, topics="topic0")
|
||||
consumer.register.assert_called_once()
|
||||
# trigger consumer callback as if a message was published
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
slot1.assert_called_once()
|
||||
slot2.assert_called_once()
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
assert slot2.call_count == 2
|
||||
|
||||
|
||||
def test_connect_one_slot_many_topics(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
assert consumer.register.call_count == 1
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic1")
|
||||
assert consumer.register.call_count == 2
|
||||
# trigger consumer callback as if a message was published
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
slot1.assert_called_once()
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
|
||||
|
||||
def test_disconnect_one_slot_one_topic(bec_dispatcher, consumer):
|
||||
slot1, slot2 = Mock(), Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
|
||||
# disconnect using a different topic
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic1")
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
|
||||
# disconnect using a different slot
|
||||
bec_dispatcher.disconnect_slot(slot=slot2, topics="topic0")
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
|
||||
# disconnect using the right slot and topics
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic0")
|
||||
# reset count to for slot
|
||||
slot1.reset_mock()
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 0
|
||||
|
||||
|
||||
def test_disconnect_identical(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
# Try to connect slot twice
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
|
||||
# Test to call the slot once (slot should be not connected twice)
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
|
||||
# Disconnect the slot
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic0")
|
||||
|
||||
# Test to call the slot once (slot should be not connected anymore), count remains 1
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
|
||||
|
||||
def test_disconnect_many_slots_one_topic(bec_dispatcher, consumer):
|
||||
slot1, slot2, slot3 = Mock(), Mock(), Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
bec_dispatcher.connect_slot(slot=slot2, topics="topic0")
|
||||
|
||||
# disconnect using a different slot
|
||||
bec_dispatcher.disconnect_slot(slot3, topics="topic0")
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
assert slot2.call_count == 1
|
||||
|
||||
# disconnect using a different topics
|
||||
bec_dispatcher.disconnect_slot(slot1, topics="topic1")
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
assert slot2.call_count == 2
|
||||
|
||||
# disconnect using the right slot and topics
|
||||
bec_dispatcher.disconnect_slot(slot1, topics="topic0")
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
assert slot2.call_count == 3
|
||||
|
||||
|
||||
def test_disconnect_one_slot_many_topics(bec_dispatcher, consumer):
|
||||
slot1, slot2 = Mock(), Mock()
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic1")
|
||||
|
||||
# disconnect using a different slot
|
||||
bec_dispatcher.disconnect_slot(slot=slot2, topics="topic0")
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 1
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 2
|
||||
|
||||
# disconnect using a different topics
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic3")
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 3
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 4
|
||||
|
||||
# disconnect using the right slot and topics
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic0")
|
||||
# Calling disconnected topic0 should not call slot1
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 4
|
||||
# Calling topic1 should still call slot1
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 5
|
||||
|
||||
# disconnect remaining topic1 from slot1, calling any topic should not increase count
|
||||
bec_dispatcher.disconnect_slot(slot=slot1, topics="topic1")
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
assert slot1.call_count == 5
|
||||
|
||||
|
||||
def test_disconnect_all(bec_dispatcher, consumer):
|
||||
# Mock slots to connect
|
||||
slot1, slot2, slot3 = Mock(), Mock(), Mock()
|
||||
|
||||
# Connect slots to different topics
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics="topic0")
|
||||
bec_dispatcher.connect_slot(slot=slot2, topics="topic1")
|
||||
bec_dispatcher.connect_slot(slot=slot3, topics="topic2")
|
||||
|
||||
# Call disconnect_all method
|
||||
bec_dispatcher.disconnect_all()
|
||||
|
||||
# Simulate messages and verify that none of the slots are called
|
||||
consumer.register.call_args_list[0].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[1].kwargs["cb"](msg)
|
||||
consumer.register.call_args_list[2].kwargs["cb"](msg)
|
||||
|
||||
# Ensure that the slots have not been called
|
||||
assert slot1.call_count == 0
|
||||
assert slot2.call_count == 0
|
||||
assert slot3.call_count == 0
|
||||
|
||||
# Also, check that the consumer for each topic is shutdown
|
||||
assert "topic0" not in bec_dispatcher._connections
|
||||
assert "topic1" not in bec_dispatcher._connections
|
||||
assert "topic2" not in bec_dispatcher._connections
|
||||
|
||||
|
||||
def test_connect_one_slot_multiple_topics_single_callback(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
|
||||
# Connect the slot to multiple topics using a single callback
|
||||
topics = ["topic1", "topic2"]
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics=topics, single_callback_for_all_topics=True)
|
||||
|
||||
# Verify the initial state
|
||||
assert len(bec_dispatcher._connections) == 1 # One connection for all topics
|
||||
assert len(bec_dispatcher._connections[tuple(sorted(topics))].slots) == 1 # One slot connected
|
||||
|
||||
# Simulate messages being published on each topic
|
||||
for topic in topics:
|
||||
msg_with_topic = MessageObject(
|
||||
topic=topic, value=ScanMessage(point_id=0, scan_id=0, data={})
|
||||
)
|
||||
consumer.register.call_args.kwargs["cb"](msg_with_topic)
|
||||
|
||||
# Verify that the slot is called once for each topic
|
||||
assert slot1.call_count == len(topics)
|
||||
|
||||
# Verify that a single consumer is created for all topics
|
||||
consumer.register.assert_called_once()
|
||||
|
||||
|
||||
def test_disconnect_all_with_single_callback_for_multiple_topics(bec_dispatcher, consumer):
|
||||
slot1 = Mock()
|
||||
|
||||
# Connect the slot to multiple topics using a single callback
|
||||
topics = ["topic1", "topic2"]
|
||||
bec_dispatcher.connect_slot(slot=slot1, topics=topics, single_callback_for_all_topics=True)
|
||||
|
||||
# Verify the initial state
|
||||
assert len(bec_dispatcher._connections) == 1 # One connection for all topics
|
||||
assert len(bec_dispatcher._connections[tuple(sorted(topics))].slots) == 1 # One slot connected
|
||||
|
||||
# Call disconnect_all method
|
||||
bec_dispatcher.disconnect_all()
|
||||
|
||||
# Verify that the slot is disconnected
|
||||
assert len(bec_dispatcher._connections) == 0 # All connections are removed
|
||||
assert slot1.call_count == 0 # Slot has not been called
|
||||
|
||||
# Simulate messages and verify that the slot is not called
|
||||
consumer.register.call_args.kwargs["cb"](msg)
|
||||
assert slot1.call_count == 0 # Slot has not been called
|
||||
@@ -1,162 +0,0 @@
|
||||
# pylint: disable=missing-module-docstring, missing-function-docstring
|
||||
from collections import defaultdict
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from qtpy import QtGui
|
||||
|
||||
from bec_widgets.widgets import BECMonitor2DScatter
|
||||
|
||||
CONFIG_DEFAULT = {
|
||||
"plot_settings": {"colormap": "CET-L4", "num_columns": 1},
|
||||
"waveform2D": [
|
||||
{
|
||||
"plot_name": "Waveform 2D Scatter (1)",
|
||||
"x_label": "Sam X",
|
||||
"y_label": "Sam Y",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "samy", "entry": "samy"}],
|
||||
"z": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"plot_name": "Waveform 2D Scatter (2)",
|
||||
"x_label": "Sam X",
|
||||
"y_label": "Sam Y",
|
||||
"signals": {
|
||||
"x": [{"name": "samy", "entry": "samy"}],
|
||||
"y": [{"name": "samx", "entry": "samx"}],
|
||||
"z": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
CONFIG_ONE_PLOT = {
|
||||
"plot_settings": {"colormap": "CET-L4", "num_columns": 1},
|
||||
"waveform2D": [
|
||||
{
|
||||
"plot_name": "Waveform 2D Scatter (1)",
|
||||
"x_label": "Sam X",
|
||||
"y_label": "Sam Y",
|
||||
"signals": {
|
||||
"x": [{"name": "aptrx", "entry": "aptrx"}],
|
||||
"y": [{"name": "aptry", "entry": "aptry"}],
|
||||
"z": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def monitor_2Dscatter(qtbot):
|
||||
client = MagicMock()
|
||||
widget = BECMonitor2DScatter(client=client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config, number_of_plots", [(CONFIG_DEFAULT, 2), (CONFIG_ONE_PLOT, 1)])
|
||||
def test_initialization(monitor_2Dscatter, config, number_of_plots):
|
||||
config_load = config
|
||||
monitor_2Dscatter.on_config_update(config_load)
|
||||
assert isinstance(monitor_2Dscatter, BECMonitor2DScatter)
|
||||
assert monitor_2Dscatter.client is not None
|
||||
assert monitor_2Dscatter.config == config_load
|
||||
assert len(monitor_2Dscatter.plot_data) == number_of_plots
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config ", [(CONFIG_DEFAULT), (CONFIG_ONE_PLOT)])
|
||||
def test_database_initialization(monitor_2Dscatter, config):
|
||||
monitor_2Dscatter.on_config_update(config)
|
||||
# Check if the database is a defaultdict
|
||||
assert isinstance(monitor_2Dscatter.database, defaultdict)
|
||||
for axis_dict in monitor_2Dscatter.database.values():
|
||||
assert isinstance(axis_dict, defaultdict)
|
||||
for signal_list in axis_dict.values():
|
||||
assert isinstance(signal_list, defaultdict)
|
||||
|
||||
# Access the elements
|
||||
for plot_config in config["waveform2D"]:
|
||||
plot_name = plot_config["plot_name"]
|
||||
|
||||
for axis in ["x", "y", "z"]:
|
||||
for signal in plot_config["signals"][axis]:
|
||||
signal_name = signal["name"]
|
||||
assert not monitor_2Dscatter.database[plot_name][axis][signal_name]
|
||||
assert isinstance(monitor_2Dscatter.database[plot_name][axis][signal_name], list)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config ", [(CONFIG_DEFAULT), (CONFIG_ONE_PLOT)])
|
||||
def test_ui_initialization(monitor_2Dscatter, config):
|
||||
monitor_2Dscatter.on_config_update(config)
|
||||
assert len(monitor_2Dscatter.plots) == len(config["waveform2D"])
|
||||
for plot_config in config["waveform2D"]:
|
||||
plot_name = plot_config["plot_name"]
|
||||
assert plot_name in monitor_2Dscatter.plots
|
||||
plot = monitor_2Dscatter.plots[plot_name]
|
||||
assert plot.titleLabel.text == plot_name
|
||||
|
||||
|
||||
def simulate_scan_data(monitor, x_value, y_value, z_value):
|
||||
"""Helper function to simulate scan data input with three devices."""
|
||||
msg = {
|
||||
"data": {
|
||||
"samx": {"samx": {"value": x_value}},
|
||||
"samy": {"samy": {"value": y_value}},
|
||||
"gauss_bpm": {"gauss_bpm": {"value": z_value}},
|
||||
},
|
||||
"scan_id": 1,
|
||||
}
|
||||
monitor.on_scan_segment(msg, {})
|
||||
|
||||
|
||||
def test_data_update_and_plotting(monitor_2Dscatter, qtbot):
|
||||
monitor_2Dscatter.on_config_update(CONFIG_DEFAULT)
|
||||
data_sets = [(1, 4, 7), (2, 5, 8), (3, 6, 9)] # (x, y, z) tuples
|
||||
plot_name = "Waveform 2D Scatter (1)"
|
||||
|
||||
for x, y, z in data_sets:
|
||||
simulate_scan_data(monitor_2Dscatter, x, y, z)
|
||||
qtbot.wait(100) # Wait for the plot to update
|
||||
|
||||
# Retrieve the plot and check if the number of data points matches
|
||||
scatterPlot = monitor_2Dscatter.scatterPlots[plot_name]
|
||||
assert len(scatterPlot.data) == len(data_sets)
|
||||
|
||||
# Check if the data in the database matches the sent data
|
||||
x_data = [
|
||||
point
|
||||
for points_list in monitor_2Dscatter.database[plot_name]["x"].values()
|
||||
for point in points_list
|
||||
]
|
||||
y_data = [
|
||||
point
|
||||
for points_list in monitor_2Dscatter.database[plot_name]["y"].values()
|
||||
for point in points_list
|
||||
]
|
||||
z_data = [
|
||||
point
|
||||
for points_list in monitor_2Dscatter.database[plot_name]["z"].values()
|
||||
for point in points_list
|
||||
]
|
||||
|
||||
assert x_data == [x for x, _, _ in data_sets]
|
||||
assert y_data == [y for _, y, _ in data_sets]
|
||||
assert z_data == [z for _, _, z in data_sets]
|
||||
|
||||
|
||||
def test_color_mapping(monitor_2Dscatter, qtbot):
|
||||
monitor_2Dscatter.on_config_update(CONFIG_DEFAULT)
|
||||
data_sets = [(1, 4, 7), (2, 5, 8), (3, 6, 9)] # (x, y, z) tuples
|
||||
for x, y, z in data_sets:
|
||||
simulate_scan_data(monitor_2Dscatter, x, y, z)
|
||||
qtbot.wait(100) # Wait for the plot to update
|
||||
|
||||
scatterPlot = monitor_2Dscatter.scatterPlots["Waveform 2D Scatter (1)"]
|
||||
|
||||
# Check if colors are applied
|
||||
assert all(isinstance(point.brush().color(), QtGui.QColor) for point in scatterPlot.points())
|
||||
0
tests/unit_tests/__init__.py
Normal file
0
tests/unit_tests/__init__.py
Normal file
@@ -1,7 +1,9 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import fakeredis
|
||||
import pytest
|
||||
from bec_lib import BECClient, RedisConnector
|
||||
from bec_lib.device import Positioner
|
||||
from bec_lib.devicemanager import DeviceContainer
|
||||
|
||||
@@ -92,12 +94,24 @@ DEVICES = [
|
||||
]
|
||||
|
||||
|
||||
def fake_redis_server(host, port):
|
||||
redis = fakeredis.FakeRedis()
|
||||
return redis
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_client():
|
||||
def mocked_client(bec_dispatcher):
|
||||
connector = RedisConnector("localhost:1", redis_cls=fake_redis_server)
|
||||
# Create a MagicMock object
|
||||
client = MagicMock()
|
||||
client = MagicMock() # TODO change to real BECClient
|
||||
|
||||
# Shutdown the original client
|
||||
bec_dispatcher.client.shutdown()
|
||||
# Mock the connector attribute
|
||||
bec_dispatcher.client = client
|
||||
|
||||
# Mock the device_manager.devices attribute
|
||||
client.connector = connector
|
||||
client.device_manager = DMMock()
|
||||
client.device_manager.add_devives(DEVICES)
|
||||
|
||||
@@ -121,3 +135,4 @@ def mocked_client():
|
||||
|
||||
with patch("builtins.isinstance", new=isinstance_mock):
|
||||
yield client
|
||||
connector.shutdown() # TODO change to real BECClient
|
||||
14
tests/unit_tests/conftest.py
Normal file
14
tests/unit_tests/conftest.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def bec_dispatcher(threads_check):
|
||||
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
|
||||
yield bec_dispatcher
|
||||
bec_dispatcher.disconnect_all()
|
||||
# clean BEC client
|
||||
bec_dispatcher.client.shutdown()
|
||||
# reinitialize singleton for next test
|
||||
bec_dispatcher_module.BECDispatcher.reset_singleton()
|
||||
59
tests/unit_tests/test_bec_dispatcher.py
Normal file
59
tests/unit_tests/test_bec_dispatcher.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import time
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import redis
|
||||
from bec_lib.connector import MessageObject
|
||||
from bec_lib.messages import ScanMessage
|
||||
from bec_lib.redis_connector import RedisConnector
|
||||
from bec_lib.serialization import MsgpackSerialization
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import QtRedisConnector
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bec_dispatcher_w_connector(bec_dispatcher, topics_msg_list):
|
||||
def pubsub_msg_generator():
|
||||
for topic, msg in topics_msg_list:
|
||||
yield {"channel": topic.encode(), "pattern": None, "data": msg}
|
||||
while True:
|
||||
time.sleep(0.2)
|
||||
yield StopIteration
|
||||
|
||||
with mock.patch("redis.Redis"):
|
||||
pubsub = redis.Redis().pubsub()
|
||||
messages = pubsub_msg_generator()
|
||||
pubsub.get_message.side_effect = lambda timeout: next(messages)
|
||||
connector = QtRedisConnector("localhost:1")
|
||||
bec_dispatcher.client.connector = connector
|
||||
yield bec_dispatcher
|
||||
|
||||
|
||||
dummy_msg = MsgpackSerialization.dumps(ScanMessage(point_id=0, scan_id="0", data={}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"topics_msg_list",
|
||||
[
|
||||
(
|
||||
("topic1", dummy_msg),
|
||||
("topic2", dummy_msg),
|
||||
("topic3", dummy_msg),
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_dispatcher_disconnect_all(bec_dispatcher_w_connector, qtbot):
|
||||
bec_dispatcher = bec_dispatcher_w_connector
|
||||
cb1 = mock.Mock(spec=[])
|
||||
cb2 = mock.Mock(spec=[])
|
||||
|
||||
bec_dispatcher.connect_slot(cb1, "topic1")
|
||||
bec_dispatcher.connect_slot(cb1, "topic2")
|
||||
bec_dispatcher.connect_slot(cb2, "topic2")
|
||||
bec_dispatcher.connect_slot(cb2, "topic3")
|
||||
assert len(bec_dispatcher.client.connector._topics_cb) == 3
|
||||
|
||||
bec_dispatcher.disconnect_all()
|
||||
|
||||
assert len(bec_dispatcher.client.connector._topics_cb) == 0
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets import BECFigure, BECMotorMap, BECWaveform1D
|
||||
from bec_widgets.widgets import BECFigure, BECMotorMap, BECWaveform
|
||||
from bec_widgets.widgets.plots import BECImageShow
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
@@ -16,7 +16,8 @@ def bec_figure(qtbot, mocked_client):
|
||||
widget = BECFigure(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def test_bec_figure_init(bec_figure):
|
||||
@@ -50,8 +51,8 @@ def test_bec_figure_add_remove_plot(bec_figure):
|
||||
assert "widget_1" in bec_figure._widgets
|
||||
assert "widget_2" in bec_figure._widgets
|
||||
assert "widget_3" in bec_figure._widgets
|
||||
assert bec_figure._widgets["widget_1"].config.widget_class == "BECWaveform1D"
|
||||
assert bec_figure._widgets["widget_2"].config.widget_class == "BECWaveform1D"
|
||||
assert bec_figure._widgets["widget_1"].config.widget_class == "BECWaveform"
|
||||
assert bec_figure._widgets["widget_2"].config.widget_class == "BECWaveform"
|
||||
assert bec_figure._widgets["widget_3"].config.widget_class == "BECPlotBase"
|
||||
|
||||
# Check accessing positions by the grid in figure
|
||||
@@ -64,7 +65,7 @@ def test_bec_figure_add_remove_plot(bec_figure):
|
||||
assert len(bec_figure._widgets) == initial_count + 2
|
||||
assert "widget_1" not in bec_figure._widgets
|
||||
assert "widget_3" in bec_figure._widgets
|
||||
assert bec_figure._widgets["widget_2"].config.widget_class == "BECWaveform1D"
|
||||
assert bec_figure._widgets["widget_2"].config.widget_class == "BECWaveform"
|
||||
|
||||
|
||||
def test_add_different_types_of_widgets(bec_figure):
|
||||
@@ -72,7 +73,7 @@ def test_add_different_types_of_widgets(bec_figure):
|
||||
im = bec_figure.image("eiger")
|
||||
motor_map = bec_figure.motor_map("samx", "samy")
|
||||
|
||||
assert plt.__class__ == BECWaveform1D
|
||||
assert plt.__class__ == BECWaveform
|
||||
assert im.__class__ == BECImageShow
|
||||
assert motor_map.__class__ == BECMotorMap
|
||||
|
||||
@@ -228,3 +229,16 @@ def test_clear_all(bec_figure):
|
||||
|
||||
assert len(bec_figure._widgets) == 0
|
||||
assert np.shape(bec_figure.grid) == (0,)
|
||||
|
||||
|
||||
def test_shortcuts(bec_figure):
|
||||
plt = bec_figure.plot("samx", "bpm4i")
|
||||
im = bec_figure.image("eiger")
|
||||
motor_map = bec_figure.motor_map("samx", "samy")
|
||||
|
||||
assert plt.config.widget_class == "BECWaveform"
|
||||
assert plt.__class__ == BECWaveform
|
||||
assert im.config.widget_class == "BECImageShow"
|
||||
assert im.__class__ == BECImageShow
|
||||
assert motor_map.config.widget_class == "BECMotorMap"
|
||||
assert motor_map.__class__ == BECMotorMap
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from bec_widgets.widgets import BECMotorMap
|
||||
from bec_widgets.widgets.plots.motor_map import MotorMapConfig
|
||||
from bec_widgets.widgets.plots.waveform1d import Signal, SignalData
|
||||
from bec_widgets.widgets.plots.waveform import Signal, SignalData
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
29
tests/unit_tests/test_client_utils.py
Normal file
29
tests/unit_tests/test_client_utils.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.cli.client import BECFigure
|
||||
|
||||
from .client_mocks import FakeDevice
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cli_figure():
|
||||
fig = BECFigure(gui_id="test")
|
||||
with mock.patch.object(fig, "_run_rpc") as mock_rpc_call:
|
||||
with mock.patch.object(fig, "gui_is_alive", return_value=True):
|
||||
yield fig, mock_rpc_call
|
||||
|
||||
|
||||
def test_rpc_call_plot(cli_figure):
|
||||
fig, mock_rpc_call = cli_figure
|
||||
fig.plot("samx", "bpm4i")
|
||||
mock_rpc_call.assert_called_with("plot", "samx", "bpm4i")
|
||||
|
||||
|
||||
def test_rpc_call_accepts_device_as_input(cli_figure):
|
||||
dev1 = FakeDevice("samx")
|
||||
dev2 = FakeDevice("bpm4i")
|
||||
fig, mock_rpc_call = cli_figure
|
||||
fig.plot(dev1, dev2)
|
||||
mock_rpc_call.assert_called_with("plot", "samx", "bpm4i")
|
||||
0
tests/unit_tests/test_msgs/__init__.py
Normal file
0
tests/unit_tests/test_msgs/__init__.py
Normal file
@@ -1,6 +1,4 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
import os
|
||||
import pickle
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -8,8 +6,7 @@ from qtpy.QtWidgets import QLineEdit
|
||||
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
from bec_widgets.widgets import ScanControl
|
||||
|
||||
from .test_msgs.available_scans_message import available_scans_message
|
||||
from tests.unit_tests.test_msgs.available_scans_message import available_scans_message
|
||||
|
||||
|
||||
class FakePositioner:
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.plots.waveform1d import CurveConfig, Signal, SignalData
|
||||
from bec_widgets.widgets.plots.waveform import CurveConfig, Signal, SignalData
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .test_bec_figure import bec_figure
|
||||
@@ -49,7 +49,7 @@ def test_adding_curve_with_same_id(bec_figure):
|
||||
|
||||
def test_create_waveform1D_by_config(bec_figure):
|
||||
w1_config_input = {
|
||||
"widget_class": "BECWaveform1D",
|
||||
"widget_class": "BECWaveform",
|
||||
"gui_id": "widget_1",
|
||||
"parent_id": "BECFigure_1708689320.788527",
|
||||
"row": 0,
|
||||
@@ -73,6 +73,7 @@ def test_create_waveform1D_by_config(bec_figure):
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"colormap": "plasma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
@@ -95,6 +96,7 @@ def test_create_waveform1D_by_config(bec_figure):
|
||||
"modifier": None,
|
||||
"limits": None,
|
||||
},
|
||||
"z": None,
|
||||
},
|
||||
},
|
||||
"curve-custom": {
|
||||
@@ -103,6 +105,7 @@ def test_create_waveform1D_by_config(bec_figure):
|
||||
"parent_id": "widget_1",
|
||||
"label": "curve-custom",
|
||||
"color": "blue",
|
||||
"colormap": "plasma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
@@ -232,6 +235,7 @@ def test_change_curve_appearance_methods(bec_figure, qtbot):
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
||||
"z": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -260,6 +264,7 @@ def test_change_curve_appearance_args(bec_figure):
|
||||
"source": "scan_segment",
|
||||
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
|
||||
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
|
||||
"z": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -343,6 +348,7 @@ def test_curve_add_by_config(bec_figure):
|
||||
"parent_id": "widget_1",
|
||||
"label": "bpm4i-bpm4i",
|
||||
"color": "#cc4778",
|
||||
"colormap": "plasma",
|
||||
"symbol": "o",
|
||||
"symbol_color": None,
|
||||
"symbol_size": 5,
|
||||
@@ -359,6 +365,7 @@ def test_curve_add_by_config(bec_figure):
|
||||
"modifier": None,
|
||||
"limits": None,
|
||||
},
|
||||
"z": None,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -428,3 +435,43 @@ def test_scan_history_with_val_access(bec_figure, qtbot):
|
||||
|
||||
assert np.array_equal(x_data, [1, 2, 3])
|
||||
assert np.array_equal(y_data, [4, 5, 6])
|
||||
|
||||
|
||||
def test_scatter_2d_update(bec_figure, qtbot):
|
||||
w1 = bec_figure.add_plot()
|
||||
|
||||
c1 = w1.add_curve_scan(x_name="samx", y_name="samx", z_name="bpm4i")
|
||||
|
||||
msg = {
|
||||
"data": {
|
||||
"samx": {"samx": {"value": [1, 2, 3]}},
|
||||
"samy": {"samy": {"value": [4, 5, 6]}},
|
||||
"bpm4i": {"bpm4i": {"value": [1, 3, 2]}},
|
||||
},
|
||||
"scan_id": 1,
|
||||
}
|
||||
msg_metadata = {"scan_name": "line_scan"}
|
||||
|
||||
mock_scan_data = MagicMock()
|
||||
mock_scan_data.data = {
|
||||
device_name: {
|
||||
entry: MagicMock(val=msg["data"][device_name][entry]["value"])
|
||||
for entry in msg["data"][device_name]
|
||||
}
|
||||
for device_name in msg["data"]
|
||||
}
|
||||
|
||||
w1.queue.scan_storage.find_scan_by_ID.return_value = mock_scan_data
|
||||
|
||||
w1.on_scan_segment(msg, msg_metadata)
|
||||
qtbot.wait(500)
|
||||
|
||||
data = c1.get_data()
|
||||
expected_x_y_data = ([1, 2, 3], [1, 2, 3])
|
||||
expected_z_colors = w1._make_z_gradient([1, 3, 2], "plasma")
|
||||
|
||||
scatter_points = c1.scatter.points()
|
||||
colors = [point.brush().color() for point in scatter_points]
|
||||
|
||||
assert np.array_equal(data, expected_x_y_data)
|
||||
assert colors == expected_z_colors
|
||||
Reference in New Issue
Block a user