From 1f1340e6565480d0d69fd6c414b8ca68d48e50ef Mon Sep 17 00:00:00 2001 From: TBThomas56 Date: Wed, 13 Sep 2023 10:41:06 +0000 Subject: [PATCH 01/11] Update API to better match real detector --- src/tickit_devices/eiger/eiger_schema.py | 10 ++- src/tickit_devices/eiger/eiger_settings.py | 66 ++++++++++++++++++- src/tickit_devices/eiger/eiger_status.py | 23 +++++-- .../eiger/monitor/monitor_config.py | 13 +++- .../eiger/monitor/monitor_status.py | 14 +++- .../eiger/stream/stream_config.py | 12 +++- .../eiger/stream/stream_status.py | 10 ++- 7 files changed, 128 insertions(+), 20 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_schema.py b/src/tickit_devices/eiger/eiger_schema.py index e2d2e4c6..9aa12393 100644 --- a/src/tickit_devices/eiger/eiger_schema.py +++ b/src/tickit_devices/eiger/eiger_schema.py @@ -82,6 +82,9 @@ class ValueType(Enum): rw_bool: partial = partial( field_config, value_type=ValueType.BOOL, access_mode=AccessMode.READ_WRITE ) +ro_bool: partial = partial( + field_config, value_type=ValueType.BOOL, access_mode=AccessMode.READ_ONLY +) rw_float_grid: partial = partial( field_config, value_type=ValueType.FLOAT_GRID, @@ -95,8 +98,11 @@ class ValueType(Enum): ro_date: partial = partial( field_config, value_type=ValueType.DATE, access_mode=AccessMode.READ_ONLY ) -rw_datetime: partial = partial( - field_config, value_type=ValueType.DATETIME, access_mode=AccessMode.READ_WRITE +ro_datetime: partial = partial( + field_config, value_type=ValueType.DATETIME, access_mode=AccessMode.READ_ONLY +) +ro_state: partial = partial( + field_config, value_type=ValueType.STATE, access_mode=AccessMode.READ_ONLY ) rw_state: partial = partial( field_config, value_type=ValueType.STATE, access_mode=AccessMode.READ_WRITE diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index 52d2f1a2..0017104a 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -6,7 +6,9 @@ from .eiger_schema import ( ro_float, + ro_int, ro_str, + ro_str_list, rw_bool, rw_float, rw_float_grid, @@ -22,6 +24,58 @@ FRAME_HEIGHT: int = 4362 +def config_keys() -> list[str]: + return [ + "auto_summation", + "beam_center_x", + "beam_center_y", + "bit_depth_image", + "bit_depth_readout", + "chi_increment", + "chi_start", + "compression", + "count_time", + "counting_mode", + "countrate_correction_applied", + "countrate_correction_count_cutoff", + "data_collection_date", + "description", + "detector_distance", + "detector_number", + "detector_readout_time", + "eiger_fw_version", + "element", + "flatfield_correction_applied", + "frame_count_time", + "frame_time", + "kappa_increment", + "kappa_start", + "nimages", + "ntrigger", + "number_of_excluded_pixels", + "omega_increment", + "omega_start", + "phi_increment", + "phi_start", + "photon_energy", + "pixel_mask_applied", + "roi_mode", + "sensor_material", + "sensor_thickness", + "software_version", + "threshold_energy", + "trigger_mode", + "two_theta_increment", + "two_theta_start", + "virtual_pixel_correction_applied", + "wavelength", + "x_pixel_size", + "x_pixels_in_detector", + "y_pixel_size", + "y_pixels_in_detector", + ] + + class KA_Energy(Enum): """Possible element K-alpha energies for samples.""" @@ -70,6 +124,9 @@ class EigerSettings: default="bslz4", metadata=rw_str(allowed_values=["bslz4", "lz4"]) ) count_time: float = field(default=0.1, metadata=rw_float()) + counting_mode: str = field( + default="normal", metadata=rw_str(allowed_values=["normal", "retrigger"]) + ) countrate_correction_applied: bool = field(default=True, metadata=rw_bool()) countrate_correction_count_cutoff: int = field(default=1000, metadata=rw_int()) data_collection_date: str = field( @@ -81,6 +138,7 @@ class EigerSettings: detector_distance: float = field(default=2.0, metadata=rw_float()) detector_number: str = field(default="EIGERSIM001", metadata=ro_str()) detector_readout_time: float = field(default=0.01, metadata=rw_float()) + eiger_fw_version: str = field(default="1.8.0", metadata=ro_str()) element: str = field( default="Co", metadata=rw_str(allowed_values=["", *(e.name for e in KA_Energy)]) ) @@ -88,6 +146,7 @@ class EigerSettings: default_factory=lambda: [[]], metadata=rw_float_grid() ) flatfield_correction_applied: bool = field(default=True, metadata=rw_bool()) + frame_count_time: float = field(default=0.01, metadata=ro_float()) frame_time: float = field(default=0.12, metadata=rw_float()) kappa_increment: float = field(default=0.0, metadata=rw_float()) kappa_start: float = field(default=0.0, metadata=rw_float()) @@ -115,11 +174,14 @@ class EigerSettings: ) two_theta_increment: float = field(default=0.0, metadata=rw_float()) two_theta_start: float = field(default=0.0, metadata=rw_float()) + virtual_pixel_correction_applied: bool = field(default=True, metadata=rw_bool()) wavelength: float = field(default=1.0, metadata=rw_float()) x_pixel_size: float = field(default=0.01, metadata=ro_float()) - x_pixels_in_detector: int = field(default=FRAME_WIDTH, metadata=rw_int()) + x_pixels_in_detector: int = field(default=FRAME_WIDTH, metadata=ro_int()) y_pixel_size: float = field(default=0.01, metadata=ro_float()) - y_pixels_in_detector: int = field(default=FRAME_HEIGHT, metadata=rw_int()) + y_pixels_in_detector: int = field(default=FRAME_HEIGHT, metadata=ro_int()) + + keys: list[str] = field(default_factory=config_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} diff --git a/src/tickit_devices/eiger/eiger_status.py b/src/tickit_devices/eiger/eiger_status.py index bfcb701a..bc979c7c 100644 --- a/src/tickit_devices/eiger/eiger_status.py +++ b/src/tickit_devices/eiger/eiger_status.py @@ -1,9 +1,11 @@ +"""Eiger_status temporary docstring - to be changed.""" + from dataclasses import dataclass, field, fields from datetime import datetime from enum import Enum from typing import Any -from .eiger_schema import ro_str_list, rw_datetime, rw_float, rw_state +from .eiger_schema import ro_datetime, ro_float, ro_state, ro_str_list class State(Enum): @@ -19,19 +21,26 @@ class State(Enum): ERROR = "error" +def status_keys() -> list[str]: + # TO DO: The real detector does not have errors + return ["humidity", "state", "temperature", "time", "error"] + + @dataclass class EigerStatus: """Stores the status parameters of the Eiger detector.""" state: State = field( default=State.NA, - metadata=rw_state(allowed_values=[state.value for state in State]), + metadata=ro_state(allowed_values=[state.value for state in State]), ) - errors: list[str] = field(default_factory=list, metadata=ro_str_list()) - th0_temp: float = field(default=24.5, metadata=rw_float()) - th0_humidity: float = field(default=0.2, metadata=rw_float()) - time: datetime = field(default=datetime.now(), metadata=rw_datetime()) - dcu_buffer_free: float = field(default=0.5, metadata=rw_float()) + error: list[str] = field(default_factory=list, metadata=ro_str_list()) + temperature: float = field(default=24.5, metadata=ro_float()) + humidity: float = field(default=0.2, metadata=ro_float()) + time: datetime = field(default=datetime.now(), metadata=ro_datetime()) + dcu_buffer_free: float = field(default=0.5, metadata=ro_float()) + + keys: list[str] = field(default_factory=status_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} diff --git a/src/tickit_devices/eiger/monitor/monitor_config.py b/src/tickit_devices/eiger/monitor/monitor_config.py index 3b4bd0ed..5ef22f89 100644 --- a/src/tickit_devices/eiger/monitor/monitor_config.py +++ b/src/tickit_devices/eiger/monitor/monitor_config.py @@ -1,7 +1,11 @@ from dataclasses import dataclass, field, fields -from typing import Any +from typing import Any, List -from tickit_devices.eiger.eiger_schema import rw_int, rw_str +from tickit_devices.eiger.eiger_schema import ro_str_list, rw_bool, rw_int, rw_state + + +def monitor_config_keys() -> list[str]: + return ["buffer_size", "discard_new", "mode"] @dataclass @@ -9,9 +13,12 @@ class MonitorConfig: """Eiger monitor configuration taken from the API spec.""" mode: str = field( - default="enabled", metadata=rw_str(allowed_values=["enabled", "disabled"]) + default="enabled", metadata=rw_state(allowed_values=["enabled", "disabled"]) ) buffer_size: int = field(default=512, metadata=rw_int()) + discard_new: bool = field(default=False, metadata=rw_bool()) + + keys: List[str] = field(default_factory=monitor_config_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} diff --git a/src/tickit_devices/eiger/monitor/monitor_status.py b/src/tickit_devices/eiger/monitor/monitor_status.py index 93cdbed5..fe5455c8 100644 --- a/src/tickit_devices/eiger/monitor/monitor_status.py +++ b/src/tickit_devices/eiger/monitor/monitor_status.py @@ -1,7 +1,12 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import ro_str_list +from tickit_devices.eiger.eiger_schema import ro_bool, ro_int, ro_state, ro_str_list + + +def monitor_status_keys() -> list[str]: + # TO DO: The real detector does not have errors + return ["buffer_fill_level", "buffer_free", "dropped", "error", "state"] @dataclass @@ -9,6 +14,13 @@ class MonitorStatus: """Eiger monitor status taken from the API spec.""" error: list[str] = field(default_factory=lambda: [], metadata=ro_str_list()) + buffer_fill_level: int = field(default=2, metadata=ro_int()) + buffer_free: bool = field(default_factory=bool, metadata=ro_bool()) + dropped: int = field(default=0, metadata=ro_int()) + state: str = field( + default="normal", metadata=ro_state(allowed_values=["normal", "overflow"]) + ) + keys: list[str] = field(default_factory=monitor_status_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} diff --git a/src/tickit_devices/eiger/stream/stream_config.py b/src/tickit_devices/eiger/stream/stream_config.py index 0581f871..0de5f379 100644 --- a/src/tickit_devices/eiger/stream/stream_config.py +++ b/src/tickit_devices/eiger/stream/stream_config.py @@ -1,7 +1,11 @@ from dataclasses import dataclass, field, fields -from typing import Any +from typing import Any, List -from tickit_devices.eiger.eiger_schema import rw_str +from tickit_devices.eiger.eiger_schema import ro_str_list, rw_state, rw_str + + +def stream_config_keys() -> list[str]: + return ["header_appendix", "header_detail", "image_appendix", "mode"] @dataclass @@ -9,7 +13,7 @@ class StreamConfig: """Eiger stream configuration taken from the API spec.""" mode: str = field( - default="enabled", metadata=rw_str(allowed_values=["disabled", "enabled"]) + default="enabled", metadata=rw_state(allowed_values=["disabled", "enabled"]) ) header_detail: str = field( default="basic", metadata=rw_str(allowed_values=["none", "basic", "all"]) @@ -17,6 +21,8 @@ class StreamConfig: header_appendix: str = field(default="", metadata=rw_str()) image_appendix: str = field(default="", metadata=rw_str()) + keys: List[str] = field(default_factory=stream_config_keys, metadata=ro_str_list()) + def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} for field_ in fields(self): diff --git a/src/tickit_devices/eiger/stream/stream_status.py b/src/tickit_devices/eiger/stream/stream_status.py index 6de8fe3b..5d56eb57 100644 --- a/src/tickit_devices/eiger/stream/stream_status.py +++ b/src/tickit_devices/eiger/stream/stream_status.py @@ -1,17 +1,23 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import ro_int, ro_str, ro_str_list +from tickit_devices.eiger.eiger_schema import ro_int, ro_state, ro_str_list + + +def stream_status_keys() -> list[str]: + return ["error", "dropped", "state"] @dataclass class StreamStatus: """Eiger stream status taken from the API spec.""" - state: str = field(default="ready", metadata=ro_str()) + state: str = field(default="ready", metadata=ro_state()) error: list[str] = field(default_factory=lambda: [], metadata=ro_str_list()) dropped: int = field(default=0, metadata=ro_int()) + keys: list[str] = field(default_factory=stream_status_keys, metadata=ro_str_list()) + def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} for field_ in fields(self): From 220bcec6ee54c166b711c35a8637fb3ab6c08288 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Wed, 4 Oct 2023 12:20:43 +0000 Subject: [PATCH 02/11] Stop aiohttp logging of every request --- src/tickit_devices/eiger/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tickit_devices/eiger/__init__.py b/src/tickit_devices/eiger/__init__.py index e904ca02..cbb21ba1 100644 --- a/src/tickit_devices/eiger/__init__.py +++ b/src/tickit_devices/eiger/__init__.py @@ -1,3 +1,5 @@ +import logging + import pydantic.v1.dataclasses from tickit.adapters.io import HttpIo, ZeroMqPushIo from tickit.core.adapter import AdapterContainer @@ -18,6 +20,7 @@ class Eiger(ComponentConfig): zmq_port: int = 9999 def __call__(self) -> Component: # noqa: D102 + logging.getLogger("aiohttp.access").setLevel(logging.WARNING) device = EigerDevice() adapters = [ AdapterContainer( From bf95d71e6119123749823892778da6b50527bce6 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Wed, 13 Sep 2023 12:34:05 +0000 Subject: [PATCH 03/11] Handle bad requests with 404 --- src/tickit_devices/eiger/eiger_adapters.py | 65 ++++++++++-------- tests/eiger/test_eiger_adapters.py | 55 ++++++++++++++- tests/eiger/test_eiger_system.py | 78 ++++++++++------------ 3 files changed, 126 insertions(+), 72 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 7f7f6a3c..7df01094 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -7,7 +7,7 @@ from tickit.adapters.zmq import ZeroMqPushAdapter from tickit_devices.eiger.eiger import EigerDevice -from tickit_devices.eiger.eiger_schema import SequenceComplete, Value, construct_value +from tickit_devices.eiger.eiger_schema import SequenceComplete, construct_value from tickit_devices.eiger.eiger_status import State API_VERSION = "1.8.0" @@ -41,12 +41,9 @@ async def get_config(self, request: web.Request) -> web.Response: param = request.match_info["parameter_name"] if hasattr(self.device.settings, param): - data = construct_value(self.device.settings, param) - + return web.json_response(construct_value(self.device.settings, param)) else: - data = serialize(Value("None", "string", access_mode="None")) - - return web.json_response(data) + return web.json_response(status=404) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/config/{parameter_name}") async def put_config(self, request: web.Request) -> web.Response: @@ -81,7 +78,7 @@ async def put_config(self, request: web.Request) -> web.Response: return web.json_response(serialize([param])) else: LOGGER.debug("Eiger has no config variable: " + str(param)) - return web.json_response(serialize([])) + return web.json_response(status=404) @HttpEndpoint.get(f"/{DETECTOR_API}" + "/status/{status_param}") async def get_status(self, request: web.Request) -> web.Response: @@ -100,7 +97,7 @@ async def get_status(self, request: web.Request) -> web.Response: data = construct_value(self.device.status, param) else: - data = serialize(Value("None", "string", access_mode="None")) + return web.json_response(status=404) return web.json_response(data) @@ -242,9 +239,10 @@ async def get_stream_status(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - data = construct_value(self.device.stream.status, param) - - return web.json_response(data) + if hasattr(self.device.stream.status, param): + return web.json_response(construct_value(self.device.stream.status, param)) + else: + return web.json_response(status=404) @HttpEndpoint.get(f"/{STREAM_API}" + "/config/{param}") async def get_stream_config(self, request: web.Request) -> web.Response: @@ -259,9 +257,10 @@ async def get_stream_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - data = construct_value(self.device.stream.config, param) - - return web.json_response(data) + if hasattr(self.device.stream.config, param): + return web.json_response(construct_value(self.device.stream.config, param)) + else: + return web.json_response(status=404) @HttpEndpoint.put(f"/{STREAM_API}" + "/config/{param}") async def put_stream_config(self, request: web.Request) -> web.Response: @@ -290,7 +289,7 @@ async def put_stream_config(self, request: web.Request) -> web.Response: return web.json_response(serialize([param])) else: LOGGER.debug("Eiger has no config variable: " + str(param)) - return web.json_response(serialize([])) + return web.json_response(status=404) @HttpEndpoint.get(f"/{MONITOR_API}" + "/config/{param}") async def get_monitor_config(self, request: web.Request) -> web.Response: @@ -305,9 +304,10 @@ async def get_monitor_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - data = construct_value(self.device.monitor_config, param) - - return web.json_response(data) + if hasattr(self.device.monitor_config, param): + return web.json_response(construct_value(self.device.monitor_config, param)) + else: + return web.json_response(status=404) @HttpEndpoint.put(f"/{MONITOR_API}" + "/config/{param}") async def put_monitor_config(self, request: web.Request) -> web.Response: @@ -336,7 +336,7 @@ async def put_monitor_config(self, request: web.Request) -> web.Response: return web.json_response(serialize([param])) else: LOGGER.debug("Eiger has no config variable: " + str(param)) - return web.json_response(serialize([])) + return web.json_response(status=404) @HttpEndpoint.get(f"/{MONITOR_API}" + "/status/{param}") async def get_monitor_status(self, request: web.Request) -> web.Response: @@ -351,9 +351,10 @@ async def get_monitor_status(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - data = construct_value(self.device.monitor_status, param) - - return web.json_response(data) + if hasattr(self.device.monitor_status, param): + return web.json_response(construct_value(self.device.monitor_status, param)) + else: + return web.json_response(status=404) @HttpEndpoint.get(f"/{FILEWRITER_API}" + "/config/{param}") async def get_filewriter_config(self, request: web.Request) -> web.Response: @@ -368,9 +369,12 @@ async def get_filewriter_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - data = construct_value(self.device.filewriter_config, param) - - return web.json_response(data) + if hasattr(self.device.filewriter_config, param): + return web.json_response( + construct_value(self.device.filewriter_config, param) + ) + else: + return web.json_response(status=404) @HttpEndpoint.put(f"/{FILEWRITER_API}" + "/config/{param}") async def put_filewriter_config(self, request: web.Request) -> web.Response: @@ -399,7 +403,7 @@ async def put_filewriter_config(self, request: web.Request) -> web.Response: return web.json_response(serialize([param])) else: LOGGER.debug("Eiger has no config variable: " + str(param)) - return web.json_response(serialize([])) + return web.json_response(status=404) @HttpEndpoint.get(f"/{FILEWRITER_API}" + "/status/{param}") async def get_filewriter_status(self, request: web.Request) -> web.Response: @@ -414,9 +418,12 @@ async def get_filewriter_status(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - data = construct_value(self.device.filewriter_status, param) - - return web.json_response(data) + if hasattr(self.device.filewriter_status, param): + return web.json_response( + construct_value(self.device.filewriter_status, param) + ) + else: + return web.json_response(status=404) class EigerZMQAdapter(ZeroMqPushAdapter): diff --git a/tests/eiger/test_eiger_adapters.py b/tests/eiger/test_eiger_adapters.py index 67958f80..205c26f7 100644 --- a/tests/eiger/test_eiger_adapters.py +++ b/tests/eiger/test_eiger_adapters.py @@ -1,6 +1,8 @@ +import pytest from pytest_mock import MockerFixture -from tickit_devices.eiger.eiger_adapters import EigerZMQAdapter +from tickit_devices.eiger.eiger import EigerDevice +from tickit_devices.eiger.eiger_adapters import EigerRESTAdapter, EigerZMQAdapter def test_after_update(mocker: MockerFixture) -> None: @@ -19,3 +21,54 @@ def test_after_update(mocker: MockerFixture) -> None: add_mock.reset_mock() zmq_adapter.after_update() add_mock.assert_not_called() + + +@pytest.mark.asyncio +async def test_rest_adapter_404(mocker: MockerFixture): + eiger_adapter = EigerRESTAdapter(EigerDevice()) + + request = mocker.MagicMock() + request.json = mocker.AsyncMock() + + request.match_info = {"parameter_name": "doesnt_exist"} + assert (await eiger_adapter.get_config(request)).status == 404 + assert (await eiger_adapter.get_board_000_status(request)).status == 404 + + request.json.return_value = {} + assert (await eiger_adapter.put_config(request)).status == 404 + + request.match_info = {"parameter_name": "doesnt_exist", "threshold": "1"} + assert (await eiger_adapter.get_threshold_config(request)).status == 404 + assert (await eiger_adapter.put_threshold_config(request)).status == 404 + + request.match_info = {"param": "doesnt_exist"} + assert (await eiger_adapter.get_monitor_status(request)).status == 404 + assert (await eiger_adapter.get_monitor_config(request)).status == 404 + assert (await eiger_adapter.put_monitor_config(request)).status == 404 + assert (await eiger_adapter.get_filewriter_status(request)).status == 404 + assert (await eiger_adapter.get_filewriter_config(request)).status == 404 + assert (await eiger_adapter.put_filewriter_config(request)).status == 404 + assert (await eiger_adapter.get_stream_status(request)).status == 404 + assert (await eiger_adapter.get_stream_config(request)).status == 404 + assert (await eiger_adapter.put_stream_config(request)).status == 404 + + request.match_info = {"status_param": "doesnt_exist"} + assert (await eiger_adapter.get_status(request)).status == 404 + assert (await eiger_adapter.get_builder_status(request)).status == 404 + + +@pytest.mark.asyncio +async def test_rest_adapter_command_404(mocker: MockerFixture): + eiger_adapter = EigerRESTAdapter(EigerDevice()) + + request = mocker.MagicMock() + request.text = mocker.AsyncMock() + request.json = mocker.AsyncMock() + request.match_info = {"key": "value"} + + assert (await eiger_adapter.initialize_eiger(request)).status == 404 + assert (await eiger_adapter.arm_eiger(request)).status == 404 + assert (await eiger_adapter.disarm_eiger(request)).status == 404 + assert (await eiger_adapter.trigger_eiger(request)).status == 404 + assert (await eiger_adapter.cancel_eiger(request)).status == 404 + assert (await eiger_adapter.abort_eiger(request)).status == 404 diff --git a/tests/eiger/test_eiger_system.py b/tests/eiger/test_eiger_system.py index eb7ffdfb..864ddcb3 100644 --- a/tests/eiger/test_eiger_system.py +++ b/tests/eiger/test_eiger_system.py @@ -32,15 +32,6 @@ async def get_status(status, expected): async with aiohttp.ClientSession() as session: await get_status(status="state", expected="na") - # Test setting config var before Eiger set up - async with session.put( - DETECTOR_URL + "config/element", - headers=headers, - json={"value": "test"}, - timeout=REQUEST_TIMEOUT, - ) as response: - assert (await response.json()) == [] - # Test each command for key, value in commands.items(): async with session.put( @@ -50,30 +41,20 @@ async def get_status(status, expected): assert value == (await response.json()) # Check status - await get_status(status="doesnt_exist", expected="None") await get_status(status="board_000/th0_temp", expected=24.5) - await get_status(status="board_000/doesnt_exist", expected="None") + await get_status(status="board_000/th0_humidity", expected=0.2) await get_status(status="builder/dcu_buffer_free", expected=0.5) - await get_status(status="builder/doesnt_exist", expected="None") # Test Eiger in IDLE state await get_status(status="state", expected="idle") - # Test settings/getting config async with session.get( - DETECTOR_URL + "config/doesnt_exist", - timeout=REQUEST_TIMEOUT, - ) as response: - assert (await response.json())["value"] == "None" - - async with session.put( - DETECTOR_URL + "config/doesnt_exist", - headers=headers, - json={"value": "test"}, + STREAM_URL + "status/keys", timeout=REQUEST_TIMEOUT, ) as response: - assert (await response.json()) == [] + assert (await response.json()) == ["dropped", "state"] + # Test settings/getting config async with session.get( DETECTOR_URL + "config/element", timeout=REQUEST_TIMEOUT, @@ -108,14 +89,6 @@ async def get_status(status, expected): ) as response: assert ["mode"] == (await response.json()) - async with session.put( - FILE_WRITER_URL + "config/test", - headers=headers, - json={"value": "test"}, - timeout=REQUEST_TIMEOUT, - ) as response: - assert [] == (await response.json()) - # Test filewriter, monitor and stream endpoints async with session.get( FILE_WRITER_URL + "status/state", @@ -137,14 +110,6 @@ async def get_status(status, expected): ) as response: assert ["mode"] == (await response.json()) - async with session.put( - MONITOR_URL + "config/test", - headers=headers, - json={"value": "test"}, - timeout=REQUEST_TIMEOUT, - ) as response: - assert [] == (await response.json()) - async with session.get( MONITOR_URL + "status/error", timeout=REQUEST_TIMEOUT, @@ -166,12 +131,34 @@ async def get_status(status, expected): assert ["mode"] == (await response.json()) async with session.put( - STREAM_URL + "config/test", + DETECTOR_URL + "config/threshold/1/energy", headers=headers, - json={"value": "test"}, + json={"value": 6829}, timeout=REQUEST_TIMEOUT, ) as response: - assert [] == (await response.json()) + assert ["energy"] == (await response.json()) + + async with session.get( + DETECTOR_URL + "config/threshold/1/energy", + headers=headers, + timeout=REQUEST_TIMEOUT, + ) as response: + assert 6829 == (await response.json())["value"] + + async with session.put( + DETECTOR_URL + "config/threshold/difference/mode", + headers=headers, + json={"value": "enabled"}, + timeout=REQUEST_TIMEOUT, + ) as response: + assert ["mode"] == (await response.json()) + + async with session.get( + DETECTOR_URL + "config/threshold/difference/mode", + headers=headers, + timeout=REQUEST_TIMEOUT, + ) as response: + assert "enabled" == (await response.json())["value"] # Test acquisition in ints mode async with session.put( @@ -203,3 +190,10 @@ async def get_status(status, expected): timeout=REQUEST_TIMEOUT, ) as response: assert {"sequence id": 4} == (await response.json()) + + # Test we get a 404 for a non-existent URI + async with session.get( + DETECTOR_URL + "status/doesnt_exist", + timeout=REQUEST_TIMEOUT, + ) as response: + assert response.status == 404 From 51bdfbbfd8618576c821039286ac17bb08fe0159 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Mon, 6 Nov 2023 15:03:40 +0000 Subject: [PATCH 04/11] Update API to match latest firmware Except for allowed values of element, which seem to be completely different and I don't have the energies to input for each element. Note some of these changes apply to the old firmware. --- src/tickit_devices/eiger/eiger_adapters.py | 65 ++++++++-- src/tickit_devices/eiger/eiger_schema.py | 6 +- src/tickit_devices/eiger/eiger_settings.py | 120 +++++++++++++++--- src/tickit_devices/eiger/eiger_status.py | 33 +++-- .../eiger/monitor/monitor_config.py | 10 +- .../eiger/monitor/monitor_status.py | 13 +- .../eiger/stream/stream_config.py | 10 +- .../eiger/stream/stream_status.py | 14 +- tests/eiger/test_eiger_filewriter_config.py | 7 +- tests/eiger/test_eiger_filewriter_status.py | 3 + tests/eiger/test_eiger_monitor_config.py | 7 +- tests/eiger/test_eiger_monitor_status.py | 3 + tests/eiger/test_eiger_settings.py | 25 +++- tests/eiger/test_eiger_status.py | 3 + tests/eiger/test_eiger_stream_config.py | 5 + tests/eiger/test_eiger_stream_status.py | 3 + 16 files changed, 265 insertions(+), 62 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 7df01094..e22be3e7 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -8,7 +8,6 @@ from tickit_devices.eiger.eiger import EigerDevice from tickit_devices.eiger.eiger_schema import SequenceComplete, construct_value -from tickit_devices.eiger.eiger_status import State API_VERSION = "1.8.0" DETECTOR_API = f"detector/api/{API_VERSION}" @@ -61,13 +60,7 @@ async def put_config(self, request: web.Request) -> web.Response: response = await request.json() - if self.device.get_state() is not State.IDLE: - LOGGER.warning("Eiger not initialized or is currently running.") - return web.json_response(serialize([])) - elif ( - hasattr(self.device.settings, param) - and self.device.get_state() is State.IDLE - ): + if hasattr(self.device.settings, param): attr = response["value"] LOGGER.debug(f"Changing to {str(attr)} for {str(param)}") @@ -80,6 +73,62 @@ async def put_config(self, request: web.Request) -> web.Response: LOGGER.debug("Eiger has no config variable: " + str(param)) return web.json_response(status=404) + @HttpEndpoint.get( + f"/{DETECTOR_API}" + "/config/threshold/{threshold}/{parameter_name}" + ) + async def get_threshold_config(self, request: web.Request) -> web.Response: + """A HTTP Endpoint for requesting threshold configuration from the Eiger. + + Args: + request (web.Request): The request object that takes the given parameter. + + Returns: + web.Response: The response object returned given the result of the HTTP + request. + """ + threshold = request.match_info["threshold"] + param = request.match_info["parameter_name"] + + config = self.device.settings.threshold_config + if threshold in config and hasattr(config[threshold], param): + return web.json_response(construct_value(config[threshold], param)) + else: + return web.json_response(status=404) + + @HttpEndpoint.put( + f"/{DETECTOR_API}" + "/config/threshold/{threshold}/{parameter_name}" + ) + async def put_threshold_config(self, request: web.Request) -> web.Response: + """A HTTP Endpoint for requesting threshold configuration from the Eiger. + + Args: + request (web.Request): The request object that takes the given parameter. + + Returns: + web.Response: The response object returned given the result of the HTTP + request. + """ + threshold = request.match_info["threshold"] + param = request.match_info["parameter_name"] + + response = await request.json() + + config = self.device.settings.threshold_config + if threshold in config and hasattr(config[threshold], param): + attr = response["value"] + + LOGGER.debug( + f"Changing to {str(attr)} for threshold/{threshold}{str(param)}" + ) + + config[threshold][param] = attr + + LOGGER.debug(f"Set threshold/{threshold}{str(param)} to {str(attr)}") + return web.json_response(serialize([param])) + else: + LOGGER.debug("Eiger has no config variable: " + str(param)) + return web.json_response(status=404) + @HttpEndpoint.get(f"/{DETECTOR_API}" + "/status/{status_param}") async def get_status(self, request: web.Request) -> web.Response: """A HTTP Endpoint for requesting the status of the Eiger. diff --git a/src/tickit_devices/eiger/eiger_schema.py b/src/tickit_devices/eiger/eiger_schema.py index 9aa12393..aae582c0 100644 --- a/src/tickit_devices/eiger/eiger_schema.py +++ b/src/tickit_devices/eiger/eiger_schema.py @@ -5,7 +5,7 @@ from functools import partial from typing import Any, Generic, TypeVar -from apischema import serialized +from apischema import order, serialized from apischema.fields import with_fields_set from apischema.metadata import skip from apischema.serialization import serialize @@ -73,6 +73,9 @@ class ValueType(Enum): rw_uint: partial = partial( field_config, value_type=ValueType.UINT, access_mode=AccessMode.READ_WRITE ) +ro_uint: partial = partial( + field_config, value_type=ValueType.UINT, access_mode=AccessMode.READ_ONLY +) rw_str: partial = partial( field_config, value_type=ValueType.STRING, access_mode=AccessMode.READ_WRITE ) @@ -112,6 +115,7 @@ class ValueType(Enum): ) +@order(["access_mode", "allowed_values", "max", "min", "unit", "value", "value_type"]) @with_fields_set @dataclass class Value(Generic[T]): diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index 0017104a..744e4926 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -6,14 +6,14 @@ from .eiger_schema import ( ro_float, - ro_int, ro_str, ro_str_list, + ro_uint, rw_bool, rw_float, rw_float_grid, - rw_int, rw_str, + rw_uint, rw_uint_grid, ) @@ -45,13 +45,21 @@ def config_keys() -> list[str]: "detector_readout_time", "eiger_fw_version", "element", + "extg_mode", + "fast_arm", "flatfield_correction_applied", "frame_count_time", "frame_time", + "incident_energy", + "incident_particle_type", + "instrument_name", "kappa_increment", "kappa_start", + "mask_to_zero", + "nexpi", "nimages", "ntrigger", + "ntriggers_skipped", "number_of_excluded_pixels", "omega_increment", "omega_start", @@ -60,15 +68,27 @@ def config_keys() -> list[str]: "photon_energy", "pixel_mask_applied", "roi_mode", + "sample_name", "sensor_material", "sensor_thickness", "software_version", + "source_name", + "threshold/1/energy", + "threshold/1/mode", + "threshold/1/number_of_excluded_pixels", + "threshold/2/energy", + "threshold/2/mode", + "threshold/2/number_of_excluded_pixels", + "threshold/difference/lower_threshold", + "threshold/difference/mode", + "threshold/difference/upper_threshold", "threshold_energy", + "total_flux", "trigger_mode", "two_theta_increment", "two_theta_start", "virtual_pixel_correction_applied", - "wavelength", + # "wavelength", # Eiger does not report wavelength as a key "x_pixel_size", "x_pixels_in_detector", "y_pixel_size", @@ -109,6 +129,47 @@ class KA_Energy(Enum): Zn: float = 8638.86 +@dataclass +class Threshold: + """Data container for a single threshold configuration.""" + + energy: float = field(default=6729, metadata=rw_float()) + mode: str = field( + default="enabled", metadata=rw_str(allowed_values=["enabled", "disabled"]) + ) + number_of_excluded_pixels: int = field(default=0, metadata=ro_uint()) + + def __getitem__(self, key: str) -> Any: # noqa: D105 + for field_ in fields(self): + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") + + def __setitem__(self, key: str, value: Any) -> None: # noqa: D105 + self.__dict__[key] = value + + +@dataclass +class ThresholdDifference: + """Configuration for the threshold difference.""" + + lower_threshold: int = field(default=1, metadata=ro_uint()) + mode: str = field( + default="disabled", metadata=rw_str(allowed_values=["enabled", "disabled"]) + ) + upper_threshold: int = field(default=2, metadata=ro_uint()) + number_of_excluded_pixels: int = field(default=0, metadata=ro_uint()) + + def __getitem__(self, key: str) -> Any: # noqa: D105 + for field_ in fields(self): + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") + + def __setitem__(self, key: str, value: Any) -> None: # noqa: D105 + self.__dict__[key] = value + + @dataclass class EigerSettings: """A data container for Eiger device configuration.""" @@ -116,19 +177,19 @@ class EigerSettings: auto_summation: bool = field(default=True, metadata=rw_bool()) beam_center_x: float = field(default=0.0, metadata=rw_float()) beam_center_y: float = field(default=0.0, metadata=rw_float()) - bit_depth_image: int = field(default=16, metadata=rw_int()) - bit_depth_readout: int = field(default=16, metadata=rw_int()) + bit_depth_image: int = field(default=16, metadata=ro_uint()) + bit_depth_readout: int = field(default=16, metadata=ro_uint()) chi_increment: float = field(default=0.0, metadata=rw_float()) chi_start: float = field(default=0.0, metadata=rw_float()) compression: str = field( - default="bslz4", metadata=rw_str(allowed_values=["bslz4", "lz4"]) + default="bslz4", metadata=rw_str(allowed_values=["lz4", "bslz4", "none"]) ) count_time: float = field(default=0.1, metadata=rw_float()) counting_mode: str = field( default="normal", metadata=rw_str(allowed_values=["normal", "retrigger"]) ) countrate_correction_applied: bool = field(default=True, metadata=rw_bool()) - countrate_correction_count_cutoff: int = field(default=1000, metadata=rw_int()) + countrate_correction_count_cutoff: int = field(default=1000, metadata=ro_uint()) data_collection_date: str = field( default="2021-30-09T16:30:00.000-01:00", metadata=ro_str() ) @@ -137,22 +198,32 @@ class EigerSettings: ) detector_distance: float = field(default=2.0, metadata=rw_float()) detector_number: str = field(default="EIGERSIM001", metadata=ro_str()) - detector_readout_time: float = field(default=0.01, metadata=rw_float()) + detector_readout_time: float = field(default=0.01, metadata=ro_float()) eiger_fw_version: str = field(default="1.8.0", metadata=ro_str()) element: str = field( - default="Co", metadata=rw_str(allowed_values=["", *(e.name for e in KA_Energy)]) + default="Co", metadata=rw_str(allowed_values=[*(e.name for e in KA_Energy)]) + ) + extg_mode: str = field( + default="double", metadata=rw_str(allowed_values=["single", "double"]) ) + fast_arm: bool = field(default=False, metadata=rw_bool()) flatfield: list[list[float]] = field( default_factory=lambda: [[]], metadata=rw_float_grid() ) flatfield_correction_applied: bool = field(default=True, metadata=rw_bool()) frame_count_time: float = field(default=0.01, metadata=ro_float()) frame_time: float = field(default=0.12, metadata=rw_float()) + incident_energy: float = field(default=13458, metadata=rw_float()) + incident_particle_type: str = field(default="photons", metadata=ro_str()) + instrument_name: str = field(default="", metadata=rw_str()) kappa_increment: float = field(default=0.0, metadata=rw_float()) kappa_start: float = field(default=0.0, metadata=rw_float()) - nimages: int = field(default=1, metadata=rw_int()) - ntrigger: int = field(default=1, metadata=rw_int()) - number_of_excuded_pixels: int = field(default=0, metadata=rw_int()) + mask_to_zero: bool = field(default=False, metadata=rw_bool()) + nexpi: int = field(default=1, metadata=rw_uint()) + nimages: int = field(default=1, metadata=rw_uint()) + ntrigger: int = field(default=1, metadata=rw_uint()) + ntriggers_skipped: int = field(default=0, metadata=rw_uint()) + number_of_excluded_pixels: int = field(default=0, metadata=ro_uint()) omega_increment: float = field(default=0.0, metadata=rw_float()) omega_start: float = field(default=0.0, metadata=rw_float()) phi_increment: float = field(default=0.0, metadata=rw_float()) @@ -163,26 +234,43 @@ class EigerSettings: ) pixel_mask_applied: bool = field(default=False, metadata=rw_bool()) roi_mode: str = field( - default="disabled", metadata=rw_str(allowed_values=["disabled", "4M"]) + default="disabled", metadata=rw_str(allowed_values=["disabled", "4M-L", "4M-R"]) ) + sample_name: str = field(default="", metadata=rw_str()) sensor_material: str = field(default="Silicon", metadata=ro_str()) sensor_thickness: float = field(default=0.01, metadata=ro_float()) software_version: str = field(default="0.1.0", metadata=ro_str()) + source_name: str = field(default="", metadata=rw_str()) threshold_energy: float = field(default=4020.5, metadata=rw_float()) + total_flux: float = field(default=0.0, metadata=rw_float()) trigger_mode: str = field( - default="exts", metadata=rw_str(allowed_values=["exts", "ints", "exte", "inte"]) + default="exts", + metadata=rw_str( + allowed_values=["eies", "exte", "extg", "exts", "inte", "ints"] + ), ) two_theta_increment: float = field(default=0.0, metadata=rw_float()) two_theta_start: float = field(default=0.0, metadata=rw_float()) virtual_pixel_correction_applied: bool = field(default=True, metadata=rw_bool()) wavelength: float = field(default=1.0, metadata=rw_float()) x_pixel_size: float = field(default=0.01, metadata=ro_float()) - x_pixels_in_detector: int = field(default=FRAME_WIDTH, metadata=ro_int()) + x_pixels_in_detector: int = field(default=FRAME_WIDTH, metadata=ro_uint()) y_pixel_size: float = field(default=0.01, metadata=ro_float()) - y_pixels_in_detector: int = field(default=FRAME_HEIGHT, metadata=ro_int()) + y_pixels_in_detector: int = field(default=FRAME_HEIGHT, metadata=ro_uint()) keys: list[str] = field(default_factory=config_keys, metadata=ro_str_list()) + def __post_init__(self): + self._threshold_config = { + "1": Threshold(), + "2": Threshold(energy=18841), + "difference": ThresholdDifference(), + } + + @property + def threshold_config(self): + return self._threshold_config + def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} for field_ in fields(self): diff --git a/src/tickit_devices/eiger/eiger_status.py b/src/tickit_devices/eiger/eiger_status.py index bc979c7c..475e473f 100644 --- a/src/tickit_devices/eiger/eiger_status.py +++ b/src/tickit_devices/eiger/eiger_status.py @@ -5,25 +5,33 @@ from enum import Enum from typing import Any -from .eiger_schema import ro_datetime, ro_float, ro_state, ro_str_list +from .eiger_schema import ro_float, ro_str, ro_str_list class State(Enum): """Possible states of the Eiger detector.""" NA = "na" + IDLE = "idle" READY = "ready" - INITIALIZE = "initialize" - CONFIGURE = "configure" ACQUIRE = "acquire" - IDLE = "idle" - TEST = "test" + CONFIGURE = "configure" + INITIALIZE = "initialize" ERROR = "error" + # TEST = "test" def status_keys() -> list[str]: - # TO DO: The real detector does not have errors - return ["humidity", "state", "temperature", "time", "error"] + return [ + # "error", # Eiger does not report error as a key + "humidity", + "link_0", + "link_1", + "series_unique_id", + "state", + "temperature", + "time", + ] @dataclass @@ -32,13 +40,18 @@ class EigerStatus: state: State = field( default=State.NA, - metadata=ro_state(allowed_values=[state.value for state in State]), + metadata=ro_str(allowed_values=[state.value for state in State]), ) - error: list[str] = field(default_factory=list, metadata=ro_str_list()) + error: list[str] = field(default_factory=list, metadata=ro_str()) temperature: float = field(default=24.5, metadata=ro_float()) humidity: float = field(default=0.2, metadata=ro_float()) - time: datetime = field(default=datetime.now(), metadata=ro_datetime()) + time: datetime = field(default=datetime.now(), metadata=ro_str()) dcu_buffer_free: float = field(default=0.5, metadata=ro_float()) + link_0: str = field(default="up", metadata=ro_str(allowed_values=["up", "down"])) + link_1: str = field(default="up", metadata=ro_str(allowed_values=["up", "down"])) + series_unique_id: str = field( + default="01HBV3JPF9T4ZDPADX6EMK6XMZ", metadata=ro_str() + ) keys: list[str] = field(default_factory=status_keys, metadata=ro_str_list()) diff --git a/src/tickit_devices/eiger/monitor/monitor_config.py b/src/tickit_devices/eiger/monitor/monitor_config.py index 5ef22f89..001c3656 100644 --- a/src/tickit_devices/eiger/monitor/monitor_config.py +++ b/src/tickit_devices/eiger/monitor/monitor_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields -from typing import Any, List +from typing import Any -from tickit_devices.eiger.eiger_schema import ro_str_list, rw_bool, rw_int, rw_state +from tickit_devices.eiger.eiger_schema import ro_str_list, rw_bool, rw_str, rw_uint def monitor_config_keys() -> list[str]: @@ -13,12 +13,12 @@ class MonitorConfig: """Eiger monitor configuration taken from the API spec.""" mode: str = field( - default="enabled", metadata=rw_state(allowed_values=["enabled", "disabled"]) + default="enabled", metadata=rw_str(allowed_values=["enabled", "disabled"]) ) - buffer_size: int = field(default=512, metadata=rw_int()) + buffer_size: int = field(default=512, metadata=rw_uint()) discard_new: bool = field(default=False, metadata=rw_bool()) - keys: List[str] = field(default_factory=monitor_config_keys, metadata=ro_str_list()) + keys: list[str] = field(default_factory=monitor_config_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} diff --git a/src/tickit_devices/eiger/monitor/monitor_status.py b/src/tickit_devices/eiger/monitor/monitor_status.py index fe5455c8..cc38c367 100644 --- a/src/tickit_devices/eiger/monitor/monitor_status.py +++ b/src/tickit_devices/eiger/monitor/monitor_status.py @@ -1,24 +1,23 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import ro_bool, ro_int, ro_state, ro_str_list +from tickit_devices.eiger.eiger_schema import ro_int, ro_str, ro_str_list, ro_uint def monitor_status_keys() -> list[str]: - # TO DO: The real detector does not have errors - return ["buffer_fill_level", "buffer_free", "dropped", "error", "state"] + return ["buffer_free", "dropped", "error", "state"] @dataclass class MonitorStatus: """Eiger monitor status taken from the API spec.""" - error: list[str] = field(default_factory=lambda: [], metadata=ro_str_list()) + error: list[str] = field(default_factory=lambda: [], metadata=ro_str()) buffer_fill_level: int = field(default=2, metadata=ro_int()) - buffer_free: bool = field(default_factory=bool, metadata=ro_bool()) - dropped: int = field(default=0, metadata=ro_int()) + buffer_free: int = field(default_factory=int, metadata=ro_uint()) + dropped: int = field(default=0, metadata=ro_uint()) state: str = field( - default="normal", metadata=ro_state(allowed_values=["normal", "overflow"]) + default="normal", metadata=ro_str(allowed_values=["normal", "overflow"]) ) keys: list[str] = field(default_factory=monitor_status_keys, metadata=ro_str_list()) diff --git a/src/tickit_devices/eiger/stream/stream_config.py b/src/tickit_devices/eiger/stream/stream_config.py index 0de5f379..ae23aa96 100644 --- a/src/tickit_devices/eiger/stream/stream_config.py +++ b/src/tickit_devices/eiger/stream/stream_config.py @@ -1,11 +1,11 @@ from dataclasses import dataclass, field, fields -from typing import Any, List +from typing import Any -from tickit_devices.eiger.eiger_schema import ro_str_list, rw_state, rw_str +from tickit_devices.eiger.eiger_schema import ro_str_list, rw_str def stream_config_keys() -> list[str]: - return ["header_appendix", "header_detail", "image_appendix", "mode"] + return ["format", "header_appendix", "header_detail", "image_appendix", "mode"] @dataclass @@ -13,7 +13,7 @@ class StreamConfig: """Eiger stream configuration taken from the API spec.""" mode: str = field( - default="enabled", metadata=rw_state(allowed_values=["disabled", "enabled"]) + default="enabled", metadata=rw_str(allowed_values=["enabled", "disabled"]) ) header_detail: str = field( default="basic", metadata=rw_str(allowed_values=["none", "basic", "all"]) @@ -21,7 +21,7 @@ class StreamConfig: header_appendix: str = field(default="", metadata=rw_str()) image_appendix: str = field(default="", metadata=rw_str()) - keys: List[str] = field(default_factory=stream_config_keys, metadata=ro_str_list()) + keys: list[str] = field(default_factory=stream_config_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 f = {} diff --git a/src/tickit_devices/eiger/stream/stream_status.py b/src/tickit_devices/eiger/stream/stream_status.py index 5d56eb57..6ff6f785 100644 --- a/src/tickit_devices/eiger/stream/stream_status.py +++ b/src/tickit_devices/eiger/stream/stream_status.py @@ -1,20 +1,24 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import ro_int, ro_state, ro_str_list +from tickit_devices.eiger.eiger_schema import ro_str, ro_str_list, ro_uint def stream_status_keys() -> list[str]: - return ["error", "dropped", "state"] + # Eiger does not report error as a key + return ["dropped", "state"] @dataclass class StreamStatus: """Eiger stream status taken from the API spec.""" - state: str = field(default="ready", metadata=ro_state()) - error: list[str] = field(default_factory=lambda: [], metadata=ro_str_list()) - dropped: int = field(default=0, metadata=ro_int()) + state: str = field( + default="ready", + metadata=ro_str(allowed_values=["disabled", "ready", "acquire", "error"]), + ) + error: list[str] = field(default_factory=lambda: [], metadata=ro_str()) + dropped: int = field(default=0, metadata=ro_uint()) keys: list[str] = field(default_factory=stream_status_keys, metadata=ro_str_list()) diff --git a/tests/eiger/test_eiger_filewriter_config.py b/tests/eiger/test_eiger_filewriter_config.py index 13e12ca8..896a75c7 100644 --- a/tests/eiger/test_eiger_filewriter_config.py +++ b/tests/eiger/test_eiger_filewriter_config.py @@ -14,5 +14,10 @@ def test_eiger_filewriter_config_constructor(): FileWriterConfig() -def test_eiger_filewriter_config_getitem(filewriter_config): +def test_eiger_filewriter_config_get_set(filewriter_config): assert "enabled" == filewriter_config["mode"]["value"] + filewriter_config["mode"] = "disabled" + assert "disabled" == filewriter_config["mode"]["value"] + + with pytest.raises(ValueError): + filewriter_config["doesnt_exist"] diff --git a/tests/eiger/test_eiger_filewriter_status.py b/tests/eiger/test_eiger_filewriter_status.py index 9650ea1b..c4b3c22d 100644 --- a/tests/eiger/test_eiger_filewriter_status.py +++ b/tests/eiger/test_eiger_filewriter_status.py @@ -16,3 +16,6 @@ def test_eiger_filewriter_status_constructor(): def test_eiger_status_getitem(filewriter_status): assert "ready" == filewriter_status["state"]["value"] + + with pytest.raises(ValueError): + filewriter_status["doesnt_exist"] diff --git a/tests/eiger/test_eiger_monitor_config.py b/tests/eiger/test_eiger_monitor_config.py index f4149fbe..c27150e5 100644 --- a/tests/eiger/test_eiger_monitor_config.py +++ b/tests/eiger/test_eiger_monitor_config.py @@ -14,5 +14,10 @@ def test_eiger_monitor_config_constructor(): MonitorConfig() -def test_eiger_monitor_config_getitem(monitor_config): +def test_eiger_monitor_config_get_set(monitor_config): assert "enabled" == monitor_config["mode"]["value"] + monitor_config["mode"] = "disabled" + assert "disabled" == monitor_config["mode"]["value"] + + with pytest.raises(ValueError): + monitor_config["doesnt_exist"] diff --git a/tests/eiger/test_eiger_monitor_status.py b/tests/eiger/test_eiger_monitor_status.py index 573f854c..ff099085 100644 --- a/tests/eiger/test_eiger_monitor_status.py +++ b/tests/eiger/test_eiger_monitor_status.py @@ -16,3 +16,6 @@ def test_eiger_monitor_status_constructor(): def test_eiger_monitor_status_getitem(monitor_status): assert [] == monitor_status["error"]["value"] + + with pytest.raises(ValueError): + monitor_status["doesnt_exist"] diff --git a/tests/eiger/test_eiger_settings.py b/tests/eiger/test_eiger_settings.py index ad6fcfa1..8460f8e2 100644 --- a/tests/eiger/test_eiger_settings.py +++ b/tests/eiger/test_eiger_settings.py @@ -14,10 +14,13 @@ def test_eiger_settings_constructor(): EigerSettings() -def test_eiger_settings_getitem(eiger_settings): - value = eiger_settings["count_time"]["value"] +def test_eiger_settings_get_set(eiger_settings): + assert eiger_settings["count_time"]["value"] == 0.1 + eiger_settings["count_time"] = 0.2 + assert eiger_settings["count_time"]["value"] == 0.2 - assert 0.1 == value + with pytest.raises(ValueError): + eiger_settings["doesnt_exist"] def test_eiger_settings_get_element(eiger_settings): @@ -59,3 +62,19 @@ def test_eiger_settings_set_count_time(eiger_settings): eiger_settings.count_time + eiger_settings.detector_readout_time == eiger_settings.frame_time ) + + +def test_eiger_settings_threshold_config(eiger_settings): + assert eiger_settings.threshold_config["1"]["energy"]["value"] == 6729 + eiger_settings.threshold_config["1"]["energy"] = 6829 + assert eiger_settings.threshold_config["1"]["energy"]["value"] == 6829 + + with pytest.raises(ValueError): + eiger_settings.threshold_config["1"]["doesnt_exist"] + + assert eiger_settings.threshold_config["difference"]["mode"]["value"] == "disabled" + eiger_settings.threshold_config["difference"]["mode"] = "enabled" + assert eiger_settings.threshold_config["difference"]["mode"]["value"] == "enabled" + + with pytest.raises(ValueError): + eiger_settings.threshold_config["difference"]["doesnt_exist"] diff --git a/tests/eiger/test_eiger_status.py b/tests/eiger/test_eiger_status.py index 956b94f7..03270215 100644 --- a/tests/eiger/test_eiger_status.py +++ b/tests/eiger/test_eiger_status.py @@ -16,3 +16,6 @@ def test_eiger_status_constructor(): def test_eiger_status_getitem(eiger_status): assert 24.5 == eiger_status["th0_temp"]["value"] + + with pytest.raises(ValueError): + eiger_status["doesnt_exist"] diff --git a/tests/eiger/test_eiger_stream_config.py b/tests/eiger/test_eiger_stream_config.py index 868ccea4..1a56ec37 100644 --- a/tests/eiger/test_eiger_stream_config.py +++ b/tests/eiger/test_eiger_stream_config.py @@ -16,3 +16,8 @@ def test_eiger_stream_config_constructor(): def test_eiger_stream_config_getitem(stream_config): assert "enabled" == stream_config["mode"]["value"] + stream_config["mode"] = "disabled" + assert "disabled" == stream_config["mode"]["value"] + + with pytest.raises(ValueError): + stream_config["doesnt_exist"] diff --git a/tests/eiger/test_eiger_stream_status.py b/tests/eiger/test_eiger_stream_status.py index 347d5bc5..49fbe0e6 100644 --- a/tests/eiger/test_eiger_stream_status.py +++ b/tests/eiger/test_eiger_stream_status.py @@ -16,3 +16,6 @@ def test_eiger_stream_status_constructor(): def test_eiger_status_getitem(stream_status): assert "ready" == stream_status["state"]["value"] + + with pytest.raises(ValueError): + stream_status["doesnt_exist"] From 8385a85c44ce44e0d8dc7ec793b69627184a2be4 Mon Sep 17 00:00:00 2001 From: James Souter Date: Wed, 6 Mar 2024 11:19:45 +0000 Subject: [PATCH 05/11] Refactor getitem for eiger dataclasses --- src/tickit_devices/eiger/eiger_settings.py | 9 +++------ src/tickit_devices/eiger/eiger_status.py | 9 +++------ src/tickit_devices/eiger/filewriter/filewriter_config.py | 9 +++------ src/tickit_devices/eiger/filewriter/filewriter_status.py | 9 +++------ src/tickit_devices/eiger/monitor/monitor_config.py | 9 +++------ src/tickit_devices/eiger/monitor/monitor_status.py | 9 +++------ src/tickit_devices/eiger/stream/stream_config.py | 9 +++------ src/tickit_devices/eiger/stream/stream_status.py | 9 +++------ 8 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index 744e4926..93555373 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -272,13 +272,10 @@ def threshold_config(self): return self._threshold_config def __getitem__(self, key: str) -> Any: # noqa: D105 - f = {} for field_ in fields(self): - f[field_.name] = { - "value": vars(self)[field_.name], - "metadata": field_.metadata, - } - return f[key] + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") def __setitem__(self, key: str, value: Any) -> None: # noqa: D105 self.__dict__[key] = value diff --git a/src/tickit_devices/eiger/eiger_status.py b/src/tickit_devices/eiger/eiger_status.py index 475e473f..39db5355 100644 --- a/src/tickit_devices/eiger/eiger_status.py +++ b/src/tickit_devices/eiger/eiger_status.py @@ -56,10 +56,7 @@ class EigerStatus: keys: list[str] = field(default_factory=status_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 - f = {} for field_ in fields(self): - f[field_.name] = { - "value": vars(self)[field_.name], - "metadata": field_.metadata, - } - return f[key] + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") diff --git a/src/tickit_devices/eiger/filewriter/filewriter_config.py b/src/tickit_devices/eiger/filewriter/filewriter_config.py index b4b47bfb..4d3a934f 100644 --- a/src/tickit_devices/eiger/filewriter/filewriter_config.py +++ b/src/tickit_devices/eiger/filewriter/filewriter_config.py @@ -17,13 +17,10 @@ class FileWriterConfig: compression_enabled: bool = field(default=False, metadata=rw_bool()) def __getitem__(self, key: str) -> Any: # noqa: D105 - f = {} for field_ in fields(self): - f[field_.name] = { - "value": vars(self)[field_.name], - "metadata": field_.metadata, - } - return f[key] + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") def __setitem__(self, key: str, value: Any) -> None: # noqa: D105 self.__dict__[key] = value diff --git a/src/tickit_devices/eiger/filewriter/filewriter_status.py b/src/tickit_devices/eiger/filewriter/filewriter_status.py index c783f43b..8b3e9239 100644 --- a/src/tickit_devices/eiger/filewriter/filewriter_status.py +++ b/src/tickit_devices/eiger/filewriter/filewriter_status.py @@ -13,10 +13,7 @@ class FileWriterStatus: files: list[str] = field(default_factory=lambda: [], metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 - f = {} for field_ in fields(self): - f[field_.name] = { - "value": vars(self)[field_.name], - "metadata": field_.metadata, - } - return f[key] + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") diff --git a/src/tickit_devices/eiger/monitor/monitor_config.py b/src/tickit_devices/eiger/monitor/monitor_config.py index 001c3656..46761805 100644 --- a/src/tickit_devices/eiger/monitor/monitor_config.py +++ b/src/tickit_devices/eiger/monitor/monitor_config.py @@ -21,13 +21,10 @@ class MonitorConfig: keys: list[str] = field(default_factory=monitor_config_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 - f = {} for field_ in fields(self): - f[field_.name] = { - "value": vars(self)[field_.name], - "metadata": field_.metadata, - } - return f[key] + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") def __setitem__(self, key: str, value: Any) -> None: # noqa: D105 self.__dict__[key] = value diff --git a/src/tickit_devices/eiger/monitor/monitor_status.py b/src/tickit_devices/eiger/monitor/monitor_status.py index cc38c367..4649ff9a 100644 --- a/src/tickit_devices/eiger/monitor/monitor_status.py +++ b/src/tickit_devices/eiger/monitor/monitor_status.py @@ -22,10 +22,7 @@ class MonitorStatus: keys: list[str] = field(default_factory=monitor_status_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 - f = {} for field_ in fields(self): - f[field_.name] = { - "value": vars(self)[field_.name], - "metadata": field_.metadata, - } - return f[key] + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") diff --git a/src/tickit_devices/eiger/stream/stream_config.py b/src/tickit_devices/eiger/stream/stream_config.py index ae23aa96..c990aa55 100644 --- a/src/tickit_devices/eiger/stream/stream_config.py +++ b/src/tickit_devices/eiger/stream/stream_config.py @@ -24,13 +24,10 @@ class StreamConfig: keys: list[str] = field(default_factory=stream_config_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 - f = {} for field_ in fields(self): - f[field_.name] = { - "value": vars(self)[field_.name], - "metadata": field_.metadata, - } - return f[key] + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") def __setitem__(self, key: str, value: Any) -> None: # noqa: D105 self.__dict__[key] = value diff --git a/src/tickit_devices/eiger/stream/stream_status.py b/src/tickit_devices/eiger/stream/stream_status.py index 6ff6f785..2ff97a7a 100644 --- a/src/tickit_devices/eiger/stream/stream_status.py +++ b/src/tickit_devices/eiger/stream/stream_status.py @@ -23,10 +23,7 @@ class StreamStatus: keys: list[str] = field(default_factory=stream_status_keys, metadata=ro_str_list()) def __getitem__(self, key: str) -> Any: # noqa: D105 - f = {} for field_ in fields(self): - f[field_.name] = { - "value": vars(self)[field_.name], - "metadata": field_.metadata, - } - return f[key] + if field_.name == key: + return {"value": vars(self)[field_.name], "metadata": field_.metadata} + raise ValueError(f"No field with name {key}") From c46e24a18b42f744510cee825ebcc2563f270d4d Mon Sep 17 00:00:00 2001 From: James Souter Date: Thu, 7 Mar 2024 10:04:29 +0000 Subject: [PATCH 06/11] Fix board_000 gets for eiger --- src/tickit_devices/eiger/data/schema.py | 2 +- src/tickit_devices/eiger/eiger_adapters.py | 8 +++++++- tests/eiger/test_eiger_status.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/tickit_devices/eiger/data/schema.py b/src/tickit_devices/eiger/data/schema.py index 30232642..e7ab5ce3 100644 --- a/src/tickit_devices/eiger/data/schema.py +++ b/src/tickit_devices/eiger/data/schema.py @@ -65,7 +65,7 @@ class ImageCharacteristicsHeader(BaseModel): class ImageConfigHeader(BaseModel): """Sent before a detector image blob. - Describes the metrics on the image acquisition. + Describes the metrics on the image acquisition. Time values are ints given in ns. """ real_time: int diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index e22be3e7..7b985615 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -161,7 +161,13 @@ async def get_board_000_status(self, request: web.Request) -> web.Response: web.Response: The response object returned given the result of the HTTP request. """ - return await self.get_status(request) + if "th0_temp" in request.message.path: + data = construct_value(self.device.status, "temperature") + return web.json_response(data) + elif "th0_humidity" in request.message.path: + data = construct_value(self.device.status, "humidity") + return web.json_response(data) + return web.json_response(status=404) @HttpEndpoint.get(f"/{DETECTOR_API}" + "/status/builder/{status_param}") async def get_builder_status(self, request: web.Request) -> web.Response: diff --git a/tests/eiger/test_eiger_status.py b/tests/eiger/test_eiger_status.py index 03270215..6219ac15 100644 --- a/tests/eiger/test_eiger_status.py +++ b/tests/eiger/test_eiger_status.py @@ -15,7 +15,7 @@ def test_eiger_status_constructor(): def test_eiger_status_getitem(eiger_status): - assert 24.5 == eiger_status["th0_temp"]["value"] + assert 24.5 == eiger_status["temperature"]["value"] with pytest.raises(ValueError): eiger_status["doesnt_exist"] From dca264b4790a93c8a97dc163e56997535bc584a8 Mon Sep 17 00:00:00 2001 From: James Souter Date: Fri, 5 Apr 2024 11:10:54 +0100 Subject: [PATCH 07/11] send keys as list without metadata fields to match real detector behaviour --- src/tickit_devices/eiger/eiger_schema.py | 6 +++--- src/tickit_devices/eiger/eiger_settings.py | 3 +-- src/tickit_devices/eiger/eiger_status.py | 4 ++-- src/tickit_devices/eiger/monitor/monitor_config.py | 4 ++-- src/tickit_devices/eiger/monitor/monitor_status.py | 4 ++-- src/tickit_devices/eiger/stream/stream_config.py | 4 ++-- src/tickit_devices/eiger/stream/stream_status.py | 4 ++-- 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_schema.py b/src/tickit_devices/eiger/eiger_schema.py index aae582c0..8a1cb50e 100644 --- a/src/tickit_devices/eiger/eiger_schema.py +++ b/src/tickit_devices/eiger/eiger_schema.py @@ -133,8 +133,9 @@ class Value(Generic[T]): def construct_value(obj, param): # noqa: D103 value = obj[param]["value"] meta = obj[param]["metadata"] - - if "allowed_values" in meta: + if param == "keys": + data = serialize(value) + elif "allowed_values" in meta: data = serialize( Value( value, @@ -143,7 +144,6 @@ def construct_value(obj, param): # noqa: D103 allowed_values=meta["allowed_values"], ) ) - else: data = serialize( Value( diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index 93555373..5d920680 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -7,7 +7,6 @@ from .eiger_schema import ( ro_float, ro_str, - ro_str_list, ro_uint, rw_bool, rw_float, @@ -258,7 +257,7 @@ class EigerSettings: y_pixel_size: float = field(default=0.01, metadata=ro_float()) y_pixels_in_detector: int = field(default=FRAME_HEIGHT, metadata=ro_uint()) - keys: list[str] = field(default_factory=config_keys, metadata=ro_str_list()) + keys: list[str] = field(default_factory=config_keys) def __post_init__(self): self._threshold_config = { diff --git a/src/tickit_devices/eiger/eiger_status.py b/src/tickit_devices/eiger/eiger_status.py index 39db5355..a50a705d 100644 --- a/src/tickit_devices/eiger/eiger_status.py +++ b/src/tickit_devices/eiger/eiger_status.py @@ -5,7 +5,7 @@ from enum import Enum from typing import Any -from .eiger_schema import ro_float, ro_str, ro_str_list +from .eiger_schema import ro_float, ro_str class State(Enum): @@ -53,7 +53,7 @@ class EigerStatus: default="01HBV3JPF9T4ZDPADX6EMK6XMZ", metadata=ro_str() ) - keys: list[str] = field(default_factory=status_keys, metadata=ro_str_list()) + keys: list[str] = field(default_factory=status_keys) def __getitem__(self, key: str) -> Any: # noqa: D105 for field_ in fields(self): diff --git a/src/tickit_devices/eiger/monitor/monitor_config.py b/src/tickit_devices/eiger/monitor/monitor_config.py index 46761805..f8025e89 100644 --- a/src/tickit_devices/eiger/monitor/monitor_config.py +++ b/src/tickit_devices/eiger/monitor/monitor_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import ro_str_list, rw_bool, rw_str, rw_uint +from tickit_devices.eiger.eiger_schema import rw_bool, rw_str, rw_uint def monitor_config_keys() -> list[str]: @@ -18,7 +18,7 @@ class MonitorConfig: buffer_size: int = field(default=512, metadata=rw_uint()) discard_new: bool = field(default=False, metadata=rw_bool()) - keys: list[str] = field(default_factory=monitor_config_keys, metadata=ro_str_list()) + keys: list[str] = field(default_factory=monitor_config_keys) def __getitem__(self, key: str) -> Any: # noqa: D105 for field_ in fields(self): diff --git a/src/tickit_devices/eiger/monitor/monitor_status.py b/src/tickit_devices/eiger/monitor/monitor_status.py index 4649ff9a..c1d3220c 100644 --- a/src/tickit_devices/eiger/monitor/monitor_status.py +++ b/src/tickit_devices/eiger/monitor/monitor_status.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import ro_int, ro_str, ro_str_list, ro_uint +from tickit_devices.eiger.eiger_schema import ro_int, ro_str, ro_uint def monitor_status_keys() -> list[str]: @@ -19,7 +19,7 @@ class MonitorStatus: state: str = field( default="normal", metadata=ro_str(allowed_values=["normal", "overflow"]) ) - keys: list[str] = field(default_factory=monitor_status_keys, metadata=ro_str_list()) + keys: list[str] = field(default_factory=monitor_status_keys) def __getitem__(self, key: str) -> Any: # noqa: D105 for field_ in fields(self): diff --git a/src/tickit_devices/eiger/stream/stream_config.py b/src/tickit_devices/eiger/stream/stream_config.py index c990aa55..ebdc93f4 100644 --- a/src/tickit_devices/eiger/stream/stream_config.py +++ b/src/tickit_devices/eiger/stream/stream_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import ro_str_list, rw_str +from tickit_devices.eiger.eiger_schema import rw_str def stream_config_keys() -> list[str]: @@ -21,7 +21,7 @@ class StreamConfig: header_appendix: str = field(default="", metadata=rw_str()) image_appendix: str = field(default="", metadata=rw_str()) - keys: list[str] = field(default_factory=stream_config_keys, metadata=ro_str_list()) + keys: list[str] = field(default_factory=stream_config_keys) def __getitem__(self, key: str) -> Any: # noqa: D105 for field_ in fields(self): diff --git a/src/tickit_devices/eiger/stream/stream_status.py b/src/tickit_devices/eiger/stream/stream_status.py index 2ff97a7a..80dab580 100644 --- a/src/tickit_devices/eiger/stream/stream_status.py +++ b/src/tickit_devices/eiger/stream/stream_status.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import ro_str, ro_str_list, ro_uint +from tickit_devices.eiger.eiger_schema import ro_str, ro_uint def stream_status_keys() -> list[str]: @@ -20,7 +20,7 @@ class StreamStatus: error: list[str] = field(default_factory=lambda: [], metadata=ro_str()) dropped: int = field(default=0, metadata=ro_uint()) - keys: list[str] = field(default_factory=stream_status_keys, metadata=ro_str_list()) + keys: list[str] = field(default_factory=stream_status_keys) def __getitem__(self, key: str) -> Any: # noqa: D105 for field_ in fields(self): From 75302e06fc1b30b43d027c4df763f48cc46dfa7b Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Wed, 10 Apr 2024 13:19:30 +0000 Subject: [PATCH 08/11] Implement multi-trigger acquisitions Currently the sim does not implement ntrigger correctly and always only does one trigger. This updates the logic to produce a series with nimage images, repeating ntrigger times, as the real detector does. --- src/tickit_devices/eiger/eiger.py | 33 ++++++++++++++-------- src/tickit_devices/eiger/eiger_adapters.py | 2 +- tests/eiger/test_eiger_system.py | 18 ++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/tickit_devices/eiger/eiger.py b/src/tickit_devices/eiger/eiger.py index 9d66a319..fa02b826 100644 --- a/src/tickit_devices/eiger/eiger.py +++ b/src/tickit_devices/eiger/eiger.py @@ -72,22 +72,23 @@ def __init__( self.monitor_callback_period = SimTime(int(1e9)) self._num_frames_left: int = 0 + self._num_triggers_left: int = 0 self._total_frames: int = 0 self._data_queue: Queue = Queue() self._series_id: int = 0 - self._finished_aquisition: asyncio.Event | None = None + self._finished_trigger: asyncio.Event | None = None @property - def finished_aquisition(self) -> asyncio.Event: + def finished_trigger(self) -> asyncio.Event: """Event that is set when an acqusition series is complete. Property ensures the event is created. """ - if self._finished_aquisition is None: - self._finished_aquisition = asyncio.Event() + if self._finished_trigger is None: + self._finished_trigger = asyncio.Event() - return self._finished_aquisition + return self._finished_trigger async def initialize(self) -> None: """Initialize the detector. @@ -104,6 +105,7 @@ async def arm(self) -> None: self._series_id += 1 self.stream.begin_series(self.settings, self._series_id) self._num_frames_left = self.settings.nimages + self._num_triggers_left = self.settings.ntrigger self._set_state(State.READY) async def disarm(self) -> None: @@ -169,11 +171,16 @@ def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: self.Outputs(), SimTime(time + int(self.settings.frame_time * 1e9)) ) else: - self.finished_aquisition.set() + self.finished_trigger.set() + + if self._num_triggers_left > 0: + self._set_state(State.READY) + self._num_frames_left = self.settings.nimages + else: + LOGGER.debug("Ending Series...") + self._set_state(State.IDLE) + self.stream.end_series(self._series_id) - LOGGER.debug("Ending Series...") - self._set_state(State.IDLE) - self.stream.end_series(self._series_id) if inputs.get("trigger", False): self._begin_acqusition_mode() # Should have another update immediately to begin acquisition @@ -182,12 +189,15 @@ def update(self, time: SimTime, inputs: Inputs) -> DeviceUpdate[Outputs]: return DeviceUpdate(self.Outputs(), None) def _begin_acqusition_mode(self) -> None: + self._num_triggers_left -= 1 self._set_state(State.ACQUIRE) LOGGER.info("Now in acquiring mode") - self.finished_aquisition.clear() + self.finished_trigger.clear() def _acquire_frame(self) -> None: - frame_id = self.settings.nimages - self._num_frames_left + frame_id = ( + (self.settings.ntrigger - self._num_triggers_left) * self.settings.nimages + ) - self._num_frames_left LOGGER.debug(f"Frame id {frame_id}") shape = ( @@ -198,6 +208,7 @@ def _acquire_frame(self) -> None: self.stream.insert_image(image, self._series_id) self._num_frames_left -= 1 LOGGER.debug(f"Frames left: {self._num_frames_left}") + LOGGER.debug(f"Triggers left: {self._num_triggers_left}") def get_state(self) -> State: """Get the eiger's current state diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 7b985615..6c2c0cf3 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -245,7 +245,7 @@ async def trigger_eiger(self, request: web.Request) -> web.Response: await self.device.trigger() await self.interrupt() - await self.device.finished_aquisition.wait() + await self.device.finished_trigger.wait() return web.json_response(serialize(SequenceComplete(4))) diff --git a/tests/eiger/test_eiger_system.py b/tests/eiger/test_eiger_system.py index 864ddcb3..d77a13ff 100644 --- a/tests/eiger/test_eiger_system.py +++ b/tests/eiger/test_eiger_system.py @@ -177,6 +177,14 @@ async def get_status(status, expected): await get_status(status="state", expected="idle") + async with session.put( + DETECTOR_URL + "config/ntrigger", + headers=headers, + json={"value": 2}, + timeout=REQUEST_TIMEOUT, + ) as response: + assert ["ntrigger"] == (await response.json()) + async with session.put( DETECTOR_URL + "command/arm", timeout=REQUEST_TIMEOUT, @@ -191,6 +199,16 @@ async def get_status(status, expected): ) as response: assert {"sequence id": 4} == (await response.json()) + await get_status(status="state", expected="ready") + + async with session.put( + DETECTOR_URL + "command/trigger", + timeout=REQUEST_TIMEOUT, + ) as response: + assert {"sequence id": 4} == (await response.json()) + + await get_status(status="state", expected="idle") + # Test we get a 404 for a non-existent URI async with session.get( DETECTOR_URL + "status/doesnt_exist", From aa0cf9fa9a73cc7e153fe7806ebffcd7fc7ae5c5 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Wed, 24 Jul 2024 15:41:59 +0000 Subject: [PATCH 09/11] Respond with 404 when commands given unexpected parameter Arguably this is a bug, but it is what the real detector does. --- src/tickit_devices/eiger/eiger_adapters.py | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 6c2c0cf3..bac24b5e 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -15,6 +15,11 @@ MONITOR_API = "monitor/api/1.8.0" FILEWRITER_API = "filewriter/api/1.8.0" + +def command_404(key: str) -> str: + return f'error during request: path error: unknown path: "{key}"' + + LOGGER = logging.getLogger("EigerAdapter") @@ -193,6 +198,9 @@ async def initialize_eiger(self, request: web.Request) -> web.Response: web.Response: The response object returned given the result of the HTTP request. """ + if await request.text() and await request.json(): + return web.json_response(status=404, text=command_404("initialize")) + await self.device.initialize() LOGGER.debug("Initializing Eiger...") @@ -209,6 +217,9 @@ async def arm_eiger(self, request: web.Request) -> web.Response: web.Response: The response object returned given the result of the HTTP request. """ + if await request.text() and await request.json(): + return web.json_response(status=404, text=command_404("arm")) + await self.device.arm() LOGGER.debug("Arming Eiger...") @@ -225,6 +236,9 @@ async def disarm_eiger(self, request: web.Request) -> web.Response: web.Response: The response object returned given the result of the HTTP request. """ + if await request.text() and await request.json(): + return web.json_response(status=404, text=command_404("disarm")) + await self.device.disarm() LOGGER.debug("Disarming Eiger...") @@ -241,6 +255,11 @@ async def trigger_eiger(self, request: web.Request) -> web.Response: web.Response: The response object returned given the result of the HTTP request. """ + if await request.text() and await request.json(): + # Only expect a parameter in "inte" mode + if self.device.settings.trigger_mode != "inte": + return web.json_response(status=404, text=command_404("initialize")) + LOGGER.debug("Triggering Eiger") await self.device.trigger() @@ -260,6 +279,9 @@ async def cancel_eiger(self, request: web.Request) -> web.Response: web.Response: The response object returned given the result of the HTTP request. """ + if await request.text() and await request.json(): + return web.json_response(status=404, text=command_404("cancel")) + await self.device.cancel() LOGGER.debug("Cancelling Eiger...") @@ -276,6 +298,9 @@ async def abort_eiger(self, request: web.Request) -> web.Response: web.Response: The response object returned given the result of the HTTP request. """ + if await request.text() and await request.json(): + return web.json_response(status=404, text=command_404("abort")) + await self.device.abort() LOGGER.debug("Aborting Eiger...") From 92d114cc2e132d0d33cbee4d1b5972487ad8cb47 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Wed, 24 Jul 2024 15:43:24 +0000 Subject: [PATCH 10/11] Produce flatfield, mask and countrate arrays --- .../eiger/stream/eiger_stream.py | 12 +++++------- tests/eiger/test_eiger_stream.py | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/tickit_devices/eiger/stream/eiger_stream.py b/src/tickit_devices/eiger/stream/eiger_stream.py index 92295c73..6fac5782 100644 --- a/src/tickit_devices/eiger/stream/eiger_stream.py +++ b/src/tickit_devices/eiger/stream/eiger_stream.py @@ -3,6 +3,7 @@ from queue import Queue from typing import Any, TypedDict +import numpy as np from pydantic.v1 import BaseModel from tickit.core.typedefs import SimTime @@ -77,8 +78,7 @@ def begin_series(self, settings: EigerSettings, series_id: int) -> None: type="float32", ) self._buffer(flatfield_header) - flatfield_data_blob = {"blob": "blob"} - self._buffer(flatfield_data_blob) + self._buffer(np.zeros(shape=(y, x), dtype="float32").tobytes()) pixel_mask_header = AcquisitionDetailsHeader( htype="dpixelmask-1.0", @@ -86,17 +86,15 @@ def begin_series(self, settings: EigerSettings, series_id: int) -> None: type="uint32", ) self._buffer(pixel_mask_header) - pixel_mask_data_blob = {"blob": "blob"} - self._buffer(pixel_mask_data_blob) + self._buffer(np.zeros(shape=(y, x), dtype="uint32").tobytes()) countrate_table_header = AcquisitionDetailsHeader( htype="dcountrate_table-1.0", - shape=(x, y), + shape=(2, 1000), type="float32", ) self._buffer(countrate_table_header) - countrate_table_data_blob = {"blob": "blob"} - self._buffer(countrate_table_data_blob) + self._buffer(np.zeros(shape=(1000, 2), dtype="float32").tobytes()) def insert_image(self, image: Image, series_id: int) -> None: """Send headers and an data blob for a single image. diff --git a/tests/eiger/test_eiger_stream.py b/tests/eiger/test_eiger_stream.py index 3ead2a59..ad83c9d9 100644 --- a/tests/eiger/test_eiger_stream.py +++ b/tests/eiger/test_eiger_stream.py @@ -1,5 +1,6 @@ from collections.abc import Mapping from typing import Any +from unittest.mock import ANY import pytest from pydantic.v1 import BaseModel @@ -56,19 +57,19 @@ def stream() -> EigerStream: shape=(X_SIZE, Y_SIZE), type="float32", ), - {"blob": "blob"}, + ANY, AcquisitionDetailsHeader( htype="dpixelmask-1.0", shape=(X_SIZE, Y_SIZE), type="uint32", ), - {"blob": "blob"}, + ANY, AcquisitionDetailsHeader( htype="dcountrate_table-1.0", - shape=(X_SIZE, Y_SIZE), + shape=(2, 1000), type="float32", ), - {"blob": "blob"}, + ANY, ] @@ -92,7 +93,9 @@ def test_begin_series_produces_correct_headers( stream.config.header_detail = header_detail stream.begin_series(settings, TEST_SERIES_ID) blobs = list(stream.consume_data()) - assert blobs == expected_headers + + for a, b in zip(expected_headers, blobs, strict=True): + assert a == b @pytest.mark.parametrize("number_of_times", [1, 2]) @@ -122,7 +125,10 @@ def test_data_buffered(stream: EigerStream) -> None: stream.insert_image(image, TEST_SERIES_ID) stream.end_series(TEST_SERIES_ID) blobs = list(stream.consume_data()) - assert blobs == ALL_HEADERS + expected_image_blobs(image) + END_SERIES_FOOTER + + expected_blobs = ALL_HEADERS + expected_image_blobs(image) + END_SERIES_FOOTER + for a, b in zip(expected_blobs, blobs, strict=True): + assert a == b def expected_image_blobs(image: Image) -> list[bytes | BaseModel]: From 76c6b76bd2421cac9d8dc0833ca351c3ec718379 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 23 Aug 2024 09:13:15 +0000 Subject: [PATCH 11/11] Add stream header missing fields --- src/tickit_devices/eiger/eiger_settings.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index 5d920680..c47bca41 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -169,6 +169,10 @@ def __setitem__(self, key: str, value: Any) -> None: # noqa: D105 self.__dict__[key] = value +def detector_translation() -> list[float]: + return [0.0, 0.0, 0.0] + + @dataclass class EigerSettings: """A data container for Eiger device configuration.""" @@ -198,6 +202,9 @@ class EigerSettings: detector_distance: float = field(default=2.0, metadata=rw_float()) detector_number: str = field(default="EIGERSIM001", metadata=ro_str()) detector_readout_time: float = field(default=0.01, metadata=ro_float()) + detector_translation: list[float] = field( + default_factory=detector_translation, metadata=ro_float() + ) eiger_fw_version: str = field(default="1.8.0", metadata=ro_str()) element: str = field( default="Co", metadata=rw_str(allowed_values=[*(e.name for e in KA_Energy)]) @@ -212,6 +219,7 @@ class EigerSettings: flatfield_correction_applied: bool = field(default=True, metadata=rw_bool()) frame_count_time: float = field(default=0.01, metadata=ro_float()) frame_time: float = field(default=0.12, metadata=rw_float()) + frame_period: float = field(default=0.12, metadata=rw_float()) incident_energy: float = field(default=13458, metadata=rw_float()) incident_particle_type: str = field(default="photons", metadata=ro_str()) instrument_name: str = field(default="", metadata=rw_str())