0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

feat(motor_map): new MotorMap widget based on PlotBase

This commit is contained in:
2025-03-14 17:24:30 +01:00
parent d9de75bbfa
commit 7a9a1dc220
13 changed files with 1919 additions and 0 deletions

View File

@ -0,0 +1,322 @@
import numpy as np
import pyqtgraph as pg
from bec_widgets.widgets.plots_next_gen.motor_map.motor_map import MotorMap
from tests.unit_tests.client_mocks import mocked_client
from .conftest import create_widget
def test_motor_map_initialization(qtbot, mocked_client):
"""Test the initialization of the MotorMap widget."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
# test default values
assert mm.config.widget_class == "MotorMap"
assert mm.config.scatter_size == 5
assert mm.config.max_points == 5000
assert mm.config.num_dim_points == 100
assert mm.x_grid is True
assert mm.y_grid is True
def test_motor_map_select_motor(qtbot, mocked_client):
"""Test selecting motors for the motor map."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy", validate_bec=True)
assert mm.config.x_motor.name == "samx"
assert mm.config.y_motor.name == "samy"
assert mm.config.x_motor.limits == [-10, 10]
assert mm.config.y_motor.limits == [-5, 5]
assert mm.config.scatter_size == 5
assert mm.config.max_points == 5000
assert mm.config.num_dim_points == 100
assert mm.x_grid is True
assert mm.y_grid is True
def test_motor_map_properties(qtbot, mocked_client):
"""Test setting and getting properties of MotorMap."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
# Test color property
mm.color = (100, 150, 200, 255)
assert mm.color == (100, 150, 200, 255)
mm.color = "#FF5500" # Test hex color
assert mm.color[0] == 255
assert mm.color[1] == 85
assert mm.color[2] == 0
# Test scatter_size property
mm.scatter_size = 10
qtbot.wait(200)
assert mm.scatter_size == 10
assert mm.config.scatter_size == 10
assert mm._trace.opts["size"] == 10
# Test max_points property
mm.max_points = 2000
assert mm.max_points == 2000
assert mm.config.max_points == 2000
# Test precision property
mm.precision = 3
assert mm.precision == 3
assert mm.config.precision == 3
# Test num_dim_points property
mm.num_dim_points = 50
assert mm.num_dim_points == 50
assert mm.config.num_dim_points == 50
# Test background_value property
mm.background_value = 40
qtbot.wait(200)
assert mm.background_value == 40
assert mm.config.background_value == 40
np.all(mm._limit_map.image == 40.0)
def test_motor_map_get_limits(qtbot, mocked_client):
"""Test getting motor limits."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
expected_limits = {"samx": [-10, 10], "samy": [-5, 5]}
for motor_name, expected_limit in expected_limits.items():
actual_limit = mm._get_motor_limit(motor_name)
assert actual_limit == expected_limit
def test_motor_map_get_init_position(qtbot, mocked_client):
"""Test getting the initial position of motors."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map("samx", "samy")
mm.precision = 2
motor_map_dev = mm.client.device_manager.devices
expected_positions = {
("samx", "samx"): motor_map_dev["samx"].read()["samx"]["value"],
("samy", "samy"): motor_map_dev["samy"].read()["samy"]["value"],
}
for (motor_name, entry), expected_position in expected_positions.items():
actual_position = mm._get_motor_init_position(motor_name, 2)
assert actual_position == expected_position
def test_motor_map_reset_history(qtbot, mocked_client):
"""Test resetting the history of motor positions."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(motor_x="samx", motor_y="samy")
# Simulate some motor movement history
mm._buffer = {"x": [1.0, 2.0, 3.0, 4.0], "y": [5.0, 6.0, 7.0, 8.0]}
# Reset history
mm.reset_history()
# Should keep only the last point
assert len(mm._buffer["x"]) == 1
assert len(mm._buffer["y"]) == 1
assert mm._buffer["x"][0] == 4.0
assert mm._buffer["y"][0] == 8.0
def test_motor_map_on_device_readback(qtbot, mocked_client):
"""Test the motor map updates when receiving device readback."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
# Clear the buffer and add initial position
mm._buffer = {"x": [1.0], "y": [2.0]}
# Simulate device readback for x motor
msg_x = {"signals": {"samx": {"value": 3.0}}}
mm.on_device_readback(msg_x, {})
qtbot.wait(200) # Allow time for the update to process
assert len(mm._buffer["x"]) == 2
assert len(mm._buffer["y"]) == 2
assert mm._buffer["x"][1] == 3.0
assert mm._buffer["y"][1] == 2.0 # Y should remain the same
# Simulate device readback for y motor
msg_y = {"signals": {"samy": {"value": 4.0}}}
mm.on_device_readback(msg_y, {})
assert len(mm._buffer["x"]) == 3
assert len(mm._buffer["y"]) == 3
assert mm._buffer["x"][2] == 3.0 # X should remain the same
assert mm._buffer["y"][2] == 4.0
def test_motor_map_max_points_limit(qtbot, mocked_client):
"""Test that the buffer doesn't exceed max_points."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
# Add more points than max_points
mm._buffer = {"x": [1.0, 2.0, 3.0, 4.0], "y": [5.0, 6.0, 7.0, 8.0]}
mm.config.max_points = 3
# Trigger update that should trim buffer
mm._update_plot()
# Check that buffer was trimmed to max_points
assert len(mm._buffer["x"]) == 3
assert len(mm._buffer["y"]) == 3
# Should keep the most recent points
assert mm._buffer["x"] == [2.0, 3.0, 4.0]
assert mm._buffer["y"] == [6.0, 7.0, 8.0]
def test_motor_map_crosshair_creation(qtbot, mocked_client):
"""Test the creation of the coordinate crosshair."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
# The Initial state should be None
assert mm.v_line is None
assert mm.h_line is None
assert mm.coord_label is None
# Create the crosshair
mm._add_coordinates_crosshair(3.0, 4.0)
# Check if crosshair elements were created
assert mm.v_line is not None
assert mm.h_line is not None
assert mm.coord_label is not None
# Test position
assert mm.v_line.value() == 3.0
assert mm.h_line.value() == 4.0
def test_motor_map_limit_map(qtbot, mocked_client):
"""Test the creation of the limit map."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
# Create a limit map
limit_map = mm._make_limit_map([0, 10], [0, 5])
# Check that the limit map was created with the right type
assert isinstance(limit_map, pg.ImageItem)
# Check the dimensions of the image data
image_data = limit_map.image
assert image_data.shape[0] == 11 # 0 to 10 inclusive
assert image_data.shape[1] == 6 # 0 to 5 inclusive
def test_motor_map_change_limits(qtbot, mocked_client):
mm = create_widget(qtbot, MotorMap, client=mocked_client)
mm.map(x_name="samx", y_name="samy")
# Original mocked limits are
# samx: [-10, 10]
# samy: [-5, 5]
# Original Limits Map
assert mm._limit_map.image.shape[0] == 21 # -10 to 10 inclusive
assert mm._limit_map.image.shape[1] == 11 # -5 to 5 inclusive
assert mm.config.x_motor.limits == [-10, 10]
assert mm.config.y_motor.limits == [-5, 5]
# Change the limits of the samx motor
mm.dev["samx"].limits = [-20, 20]
msg = {"signals": {"high": {"value": 20}, "low": {"value": -20}}}
mm.on_device_limits(msg, {})
qtbot.wait(200) # Allow time for the update to process
# Check that the limits map was updated
assert mm.config.x_motor.limits == [-20, 20]
assert mm.config.y_motor.limits == [-5, 5]
assert mm._limit_map.image.shape[0] == 41 # -20 to 20 inclusive
assert mm._limit_map.image.shape[1] == 11 # -5 to 5 inclusive -> same as before
# Change back the limits
mm.dev["samx"].limits = [-10, 10]
def test_motor_map_get_data(qtbot, mocked_client):
"""Test getting data from the motor map."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
# Set up some test data
mm._buffer = {"x": [1.0, 2.0, 3.0], "y": [4.0, 5.0, 6.0]}
# Get data
data = mm.get_data()
# Check that the data is correct
assert "x" in data
assert "y" in data
assert data["x"] == [1.0, 2.0, 3.0]
assert data["y"] == [4.0, 5.0, 6.0]
def test_motor_map_toolbar_selection(qtbot, mocked_client):
"""Test motor selection via the toolbar bundle."""
mm = create_widget(qtbot, MotorMap, client=mocked_client)
# Verify toolbar bundle was created during initialization
assert hasattr(mm, "motor_selection_bundle")
assert mm.motor_selection_bundle is not None
mm.motor_selection_bundle.motor_x.setCurrentText("samx")
mm.motor_selection_bundle.motor_y.setCurrentText("samy")
assert mm.config.x_motor.name == "samx"
assert mm.config.y_motor.name == "samy"
mm.motor_selection_bundle.motor_y.setCurrentText("samz")
assert mm.config.x_motor.name == "samx"
assert mm.config.y_motor.name == "samz"
def test_motor_map_settings_dialog(qtbot, mocked_client):
"""Test the settings dialog for the motor map."""
mm = create_widget(qtbot, MotorMap, client=mocked_client, popups=True)
assert "popup_bundle" in mm.toolbar.bundles
for action_id in mm.toolbar.bundles["popup_bundle"]:
assert mm.toolbar.widgets[action_id].action.isVisible() is True
# set properties to be fetched by dialog
mm.map(x_name="samx", y_name="samy")
mm.precision = 2
mm.max_points = 1000
mm.scatter_size = 10
mm.background_value = 50
mm.num_dim_points = 20
mm.color = (255, 0, 0, 255)
mm.show_motor_map_settings()
qtbot.wait(200)
assert mm.motor_map_settings is not None
assert mm.motor_map_settings.isVisible() is True
# Check that the settings dialog has the correct values
assert mm.motor_map_settings.widget.ui.precision.value() == 2
assert mm.motor_map_settings.widget.ui.max_points.value() == 1000
assert mm.motor_map_settings.widget.ui.scatter_size.value() == 10
assert mm.motor_map_settings.widget.ui.background_value.value() == 50
assert mm.motor_map_settings.widget.ui.num_dim_points.value() == 20
assert mm.motor_map_settings.widget.ui.color_scatter.get_color(format="RGBA") == (
255,
0,
0,
255,
)
mm.motor_map_settings.close()
qtbot.wait(200)
assert mm.motor_map_settings is None