From 864c803f86ddf1c16549c4f6d3edf30d76c74a1e Mon Sep 17 00:00:00 2001 From: David Hammer <dhammer@mailbox.org> Date: Wed, 2 Mar 2022 18:32:56 +0100 Subject: [PATCH] Cleanup, only mdl geometries, update scenes --- setup.py | 11 +- src/calng/AgipdCorrection.py | 27 +- src/calng/CalibrationManager.py | 2 +- src/calng/DsscCorrection.py | 26 +- src/calng/JungfrauCorrection.py | 29 +- src/calng/__init__.py | 0 src/calng/base_correction.py | 2 +- src/calng/base_geometry.py | 640 +++++++++++++++----------------- src/calng/mdl_geometry_base.py | 369 ------------------ src/calng/scenes.py | 213 ++++++++++- 10 files changed, 512 insertions(+), 807 deletions(-) delete mode 100644 src/calng/__init__.py delete mode 100644 src/calng/mdl_geometry_base.py diff --git a/setup.py b/setup.py index c9555f58..51f02cb4 100644 --- a/setup.py +++ b/setup.py @@ -25,11 +25,8 @@ setup(name='calng', entry_points={ 'karabo.bound_device': [ 'AgipdCorrection = calng.AgipdCorrection:AgipdCorrection', - 'ManualAgipdGeometry = calng.AgipdCorrection:ManualAgipdGeometry', 'DsscCorrection = calng.DsscCorrection:DsscCorrection', - 'ManualDsscGeometry = calng.DsscCorrection:ManualDsscGeometry', 'JungfrauCorrection = calng.JungfrauCorrection:JungfrauCorrection', - 'ManualJungfrauGeometry = calng.JungfrauCorrection:ManualJungfrauGeometry', 'LpdCorrection = calng.LpdCorrection:LpdCorrection', 'ShmemToZMQ = calng.ShmemToZMQ:ShmemToZMQ', 'ShmemTrainMatcher = calng.ShmemTrainMatcher:ShmemTrainMatcher', @@ -38,10 +35,10 @@ setup(name='calng', 'karabo.middlelayer_device': [ 'CalibrationManager = calng.CalibrationManager:CalibrationManager', - 'MdlAgipd1MGeometry = calng.mdl_geometry_base:MdlAgipd1MGeometry', - 'MdlDssc1MGeometry = calng.mdl_geometry_base:MdlDssc1MGeometry', - 'MdlLpd1MGeometry = calng.mdl_geometry_base:MdlLpd1MGeometry', - 'MdlJungfrauGeometry = calng.mdl_geometry_base:MdlJungfrauGeometry', + 'Agipd1MGeometry = calng.geometries.Agipd1MGeometry:Agipd1MGeometry', + 'Dssc1MGeometry = calng.geometries:Dssc1MGeometry.Dssc1MGeometry', + 'Lpd1MGeometry = calng.geometries:Lpd1MGeometry.Lpd1MGeometry', + 'JungfrauGeometry = calng.geometries:JungfrauGeometry.JungfrauGeometry', ], }, package_data={'': ['kernels/*']}, diff --git a/src/calng/AgipdCorrection.py b/src/calng/AgipdCorrection.py index caa7812e..d86b953e 100644 --- a/src/calng/AgipdCorrection.py +++ b/src/calng/AgipdCorrection.py @@ -14,7 +14,7 @@ from karabo.bound import ( VECTOR_STRING_ELEMENT, ) -from . import base_calcat, base_geometry, base_gpu, utils +from . import base_calcat, base_gpu, utils from ._version import version as deviceVersion from .base_correction import BaseCorrection, add_correction_step_schema, preview_schema @@ -36,6 +36,7 @@ class AgipdGainMode(enum.IntEnum): FIXED_MEDIUM_GAIN = 2 FIXED_LOW_GAIN = 3 + class CorrectionFlags(enum.IntFlag): NONE = 0 THRESHOLD = 1 @@ -803,27 +804,3 @@ class AgipdCorrection(BaseCorrection): self.kernel_runner.override_bad_pixel_flags_to_use( self._override_bad_pixel_flags ) - - -@KARABO_CLASSINFO("ManualAgipdGeometry", deviceVersion) -class ManualAgipdGeometry(base_geometry.ManualQuadrantsGeometryBase): - def __init__(self, *args, **kwargs): - import extra_geom - self.geometry_class = extra_geom.AGIPD_1MGeometry - super().__init__(*args, **kwargs) - - @staticmethod - def expectedParameters(expected): - super(ManualAgipdGeometry, ManualAgipdGeometry).expectedParameters(expected) - - expected.setDefaultValue("quadrantCorners.Q1.x", -525) - expected.setDefaultValue("quadrantCorners.Q1.y", 625) - - expected.setDefaultValue("quadrantCorners.Q2.x", -550) - expected.setDefaultValue("quadrantCorners.Q2.y", -10) - - expected.setDefaultValue("quadrantCorners.Q3.x", 520) - expected.setDefaultValue("quadrantCorners.Q3.y", -160) - - expected.setDefaultValue("quadrantCorners.Q4.x", 542.5) - expected.setDefaultValue("quadrantCorners.Q4.y", 475) diff --git a/src/calng/CalibrationManager.py b/src/calng/CalibrationManager.py index cf46b4c5..a8367b12 100644 --- a/src/calng/CalibrationManager.py +++ b/src/calng/CalibrationManager.py @@ -262,7 +262,7 @@ class CalibrationManager(DeviceClientBase, Device): name = params.get('name', default='overview') if name == 'overview': # Assumes there are correction devices known to manager - scene_data = scenes.manager_device_overview_scene( + scene_data = scenes.manager_device_overview( self.deviceId, self.getDeviceSchema(), self._correction_device_schema, diff --git a/src/calng/DsscCorrection.py b/src/calng/DsscCorrection.py index 7bf97300..81d38e16 100644 --- a/src/calng/DsscCorrection.py +++ b/src/calng/DsscCorrection.py @@ -9,7 +9,7 @@ from karabo.bound import ( VECTOR_STRING_ELEMENT, ) -from . import base_calcat, base_geometry, base_gpu, utils +from . import base_calcat, base_gpu, utils from ._version import version as deviceVersion from .base_correction import BaseCorrection, add_correction_step_schema @@ -287,27 +287,3 @@ class DsscCorrection(BaseCorrection): self._update_correction_flags() self.log_status_info(f"Done loading {constant.name} to GPU") - - -@KARABO_CLASSINFO("ManualDsscGeometry", deviceVersion) -class ManualDsscGeometry(base_geometry.ManualQuadrantsGeometryBase): - def __init__(self, *args, **kwargs): - import extra_geom - self.geometry_class = extra_geom.DSSC_1MGeometry - super().__init__(*args, **kwargs) - - @staticmethod - def expectedParameters(expected): - super(ManualDsscGeometry, ManualDsscGeometry).expectedParameters(expected) - - expected.setDefaultValue("quadrantCorners.Q1.x", -130) - expected.setDefaultValue("quadrantCorners.Q1.y", 5) - - expected.setDefaultValue("quadrantCorners.Q2.x", -130) - expected.setDefaultValue("quadrantCorners.Q2.y", -125) - - expected.setDefaultValue("quadrantCorners.Q3.x", 5) - expected.setDefaultValue("quadrantCorners.Q3.y", -125) - - expected.setDefaultValue("quadrantCorners.Q4.x", 5) - expected.setDefaultValue("quadrantCorners.Q4.y", 5) diff --git a/src/calng/JungfrauCorrection.py b/src/calng/JungfrauCorrection.py index 836da284..7be0e149 100644 --- a/src/calng/JungfrauCorrection.py +++ b/src/calng/JungfrauCorrection.py @@ -9,10 +9,9 @@ from karabo.bound import ( OVERWRITE_ELEMENT, STRING_ELEMENT, VECTOR_STRING_ELEMENT, - Hash, ) -from . import base_calcat, base_geometry, base_gpu, utils +from . import base_calcat, base_gpu, utils from ._version import version as deviceVersion from .base_correction import BaseCorrection, add_correction_step_schema, preview_schema @@ -415,29 +414,3 @@ class JungfrauCorrection(BaseCorrection): self._update_correction_flags() self.log_status_info(f"Done loading {constant.name} to GPU") - - -@KARABO_CLASSINFO("ManualJungfrauGeometry", deviceVersion) -class ManualJungfrauGeometry(base_geometry.ManualModulesGeometryBase): - def __init__(self, *args, **kwargs): - import extra_geom - self.geometry_class = extra_geom.JUNGFRAUGeometry - super().__init__(*args, **kwargs) - - @staticmethod - def expectedParameters(expected): - # TODO: come up with some sweet defaults (this is two modules from docs 4M) - ( - OVERWRITE_ELEMENT(expected) - .key("modules") - .setNewDefaultValue( - [ - Hash( - "posX", 95, "posY", 564, "orientationX", -1, "orientationY", -1 - ), - Hash( - "posX", 95, "posY", 17, "orientationX", -1, "orientationY", -1 - ), - ] - ) - ) diff --git a/src/calng/__init__.py b/src/calng/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py index 02cf87f2..12a10b24 100644 --- a/src/calng/base_correction.py +++ b/src/calng/base_correction.py @@ -760,7 +760,7 @@ class BaseCorrection(PythonDevice): payload["name"] = name payload["success"] = True if name == "overview": - payload["data"] = scenes.correction_device_overview_scene( + payload["data"] = scenes.correction_device_overview( device_id=self.getInstanceId(), schema=self.getFullSchema(), ) diff --git a/src/calng/base_geometry.py b/src/calng/base_geometry.py index 1e5f12d2..6ae36b79 100644 --- a/src/calng/base_geometry.py +++ b/src/calng/base_geometry.py @@ -1,252 +1,200 @@ import contextlib +import logging import pickle import matplotlib.pyplot as plt import numpy as np -from karabo.bound import ( - BOOL_ELEMENT, - DOUBLE_ELEMENT, - IMAGEDATA_ELEMENT, - INT32_ELEMENT, - KARABO_CLASSINFO, - NODE_ELEMENT, - OUTPUT_CHANNEL, - OVERWRITE_ELEMENT, - SLOT_ELEMENT, - STRING_ELEMENT, - TABLE_ELEMENT, - VECTOR_CHAR_ELEMENT, - VECTOR_STRING_ELEMENT, - Encoding, +from karabo.middlelayer import ( + AccessLevel, + AccessMode, + Assignment, + Bool, + Configurable, + DaqPolicy, + Device, + Double, + EncodingType, Hash, + Image, ImageData, - PythonDevice, - Schema, + Int32, + Node, + OutputChannel, + Slot, State, + String, + Unit, + VectorChar, + VectorString, + slot, ) -from karabo.common.api import KARABO_SCHEMA_DISPLAY_TYPE_SCENES as DT_SCENES from matplotlib.backends.backend_agg import FigureCanvasAgg -from . import scenes from ._version import version as deviceVersion - -geometry_schema = Schema() -( - VECTOR_CHAR_ELEMENT(geometry_schema) - .key("pickledGeometry") - .displayedName("Pickled geometry") - .assignmentOptional() - .defaultValue([]) - .commit() -) - -preview_schema = Schema() -(IMAGEDATA_ELEMENT(preview_schema).key("overview").commit()) - -ModuleColumn = Schema() -( - DOUBLE_ELEMENT(ModuleColumn) - .key("posX") - .assignmentOptional() - .defaultValue(95) - .reconfigurable() - .commit(), - - DOUBLE_ELEMENT(ModuleColumn) - .key("posY") - .assignmentOptional() - .defaultValue(564) - .reconfigurable() - .commit(), - - INT32_ELEMENT(ModuleColumn) - .key("orientationX") - .assignmentOptional() - .defaultValue(-1) - .reconfigurable() - .commit(), - - INT32_ELEMENT(ModuleColumn) - .key("orientationY") - .assignmentOptional() - .defaultValue(-1) - .reconfigurable() - .commit(), -) +from . import scenes -@KARABO_CLASSINFO("ManualGeometryBase", deviceVersion) -class ManualGeometryBase(PythonDevice): - geometry_class = None # concrete device subclass must set this +class GeometrySchema(Configurable): + pickledGeometry = VectorChar( + displayedName="Pickled geometry", + assignment=Assignment.OPTIONAL, + defaultValue=[], + ) - @staticmethod - def expectedParameters(expected): - # Karabo things - ( - OVERWRITE_ELEMENT(expected) - .key("state") - .setNewDefaultValue(State.INIT) - .commit(), - OVERWRITE_ELEMENT(expected) - .key("doNotCompressEvents") - .setNewDefaultValue(True) - .commit(), +def makeXYCoordinateNode( + default_x, default_y, x_args=None, y_args=None, node_args=None +): + class XYCoordinate(Configurable): + x = Double( + defaultValue=default_x, + accessMode=AccessMode.RECONFIGURABLE, + assignment=Assignment.OPTIONAL, + **({} if x_args is None else x_args), ) - - # "mandatory" for geometry serving device - ( - OUTPUT_CHANNEL(expected) - .key("geometryOutput") - .dataSchema(geometry_schema) - .commit(), - - SLOT_ELEMENT(expected) - .key("sendGeometry") - .displayedName("Send geometry") - .description( - "Send current geometry on output channel. This output channel is " - "typically used by assembler devices waiting for new geometries. " - "Updating the geometry will usually automatically cause update, but " - "hit this slot in case geometry is missing somewhere." - ) - .allowedStates(State.ON) - .commit(), - - SLOT_ELEMENT(expected) - .key("updatePreview") - .displayedName("Update preview") - .allowedStates(State.ON) - .commit(), - - OUTPUT_CHANNEL(expected) - .key("previewOutput") - .dataSchema(preview_schema) - .commit(), - - IMAGEDATA_ELEMENT(expected).key("layoutPreview").commit(), + y = Double( + defaultValue=default_y, + accessMode=AccessMode.RECONFIGURABLE, + assignment=Assignment.OPTIONAL, + **({} if y_args is None else y_args), ) - # options to load from file - # future plan: pick option from manual / file / encoder or other devices - ( - NODE_ELEMENT(expected) - .key("geometryFile") - .displayedName("Geometry file") - .description("Allows loading geometry from CrystFEL geometry file") - .commit(), - - STRING_ELEMENT(expected) - .key("geometryFile.filePath") - .displayedName("File path") - .description( - "Full path (including filename and suffix) to the desired geometry " - "file. Keep in mind that the default directory is $KARABO/var/data on " - "device server node, so it's probably wise to give absolute path." - ) - .assignmentOptional() - .defaultValue("") - .reconfigurable() - .commit(), - - SLOT_ELEMENT(expected) - .key("geometryFile.load") - .displayedName("Load file") - .description( - "Trigger loading from file. Automatically called after starting device " - "if geometryFile.filePath is set. Should be manually called after " - "changing file path. In case loading the file fails, manual settings " - "will be used." - ) - .allowedStates(State.ON) - .commit(), - - BOOL_ELEMENT(expected) - .key("geometryFile.updateManualOnLoad") - .displayedName("Update manual settings") - .description( - "If this flag is on, the manual settings on this device will be " - "updated according to the loaded geometry file. This is useful when " - "you want to load a file and then tweak the geometry a bit." - ) - .assignmentOptional() - .defaultValue(True) - .commit(), + return Node(XYCoordinate, **({} if node_args is None else node_args)) + + +def makeXYOffsetNode(): + return makeXYCoordinateNode( + 0, + 0, + x_args={"unitSymbol": Unit.METER}, + y_args={"unitSymbol": Unit.METER}, + node_args={ + "displayedName": "Offset", + "description": "See EXtra-geom documentation for details. This offset is " + "applied to entire detector after initial geometry is created from manual " + "parameters. Example: To move entire geometry up by 2 mm relative to beam, " + "set offset.y to 2e-3.", + }, + ) + + +class GeometryFileNode(Configurable): + filePath = String( + defaultValue="", + displayedName="File path", + description="Full path (including filename and suffix) to the desired geometry " + "file. Keep in mind that the default directory is $KARABO/var/data on device " + "server node, so it's probably wise to give absolute path.", + assignment=Assignment.OPTIONAL, + accessMode=AccessMode.RECONFIGURABLE, + ) + offset = makeXYOffsetNode() + updateManualOnLoad = Bool( + defaultValue=True, + displayedName="Update manual settings", + description="If this flag is on, the manual settings on this device will be " + "updated according to the loaded geometry file. This is useful when you want " + "to load a file and then tweak the geometry a bit. This will zero current " + "offset.", + assignment=Assignment.OPTIONAL, + accessMode=AccessMode.RECONFIGURABLE, + ) + + +class ManualGeometryBase(Device): + __version__ = deviceVersion + geometry_class = None # subclass must set + # subclass must add slot setManual + + availableScenes = VectorString( + displayedName="Available scenes", + displayType="Scenes", + requiredAccessLevel=AccessLevel.OBSERVER, + accessMode=AccessMode.READONLY, + defaultValue=[ + "overview", + ], + daqPolicy=DaqPolicy.OMIT, + ) + + geometryPreview = Image( + ImageData(np.empty(0, dtype=np.uint32)), + displayedName="Geometry preview", + encoding=EncodingType.RGBA, + ) + + geometryOutput = OutputChannel(GeometrySchema) + + geometryFile = Node( + GeometryFileNode, + displayedName="Geometry file", + description="Allows loading geometry from CrystFEL geometry file", + ) + + @Slot( + displayedName="Send geometry", + description="Send current geometry on output channel. This output channel is " + "typically used by assembler devices waiting for new geometries. Updating the " + "geometry will usually automatically cause update, but hit this slot in case " + "geometry is missing somewhere.", + allowedStates=[State.ACTIVE], + ) + async def sendGeometry(self): + self.geometryOutput.schema.pickledGeometry = self.pickled_geometry + await self.geometryOutput.writeData() + + @Slot( + displayedName="Update preview", + allowedStates=[State.ACTIVE], + ) + async def updatePreview(self): + axis = self.geometry.inspect() + axis.figure.tight_layout(pad=0) + axis.figure.set_facecolor("none") + # axis.figure.set_size_inches(6, 6) + # axis.figure.set_dpi(300) + canvas = FigureCanvasAgg(axis.figure) + canvas.draw() + image_buffer = np.frombuffer(canvas.buffer_rgba(), dtype=np.uint8).reshape( + canvas.get_width_height()[::-1] + (4,) ) - - # scenes are fun - ( - VECTOR_STRING_ELEMENT(expected) - .key("availableScenes") - .setSpecialDisplayType(DT_SCENES) - .readOnly() - .initialValue(["overview"]) - .commit(), + self.geometryPreview = ImageData( + image_buffer, encoding=EncodingType.RGBA, bitsPerPixel=3 * 8 ) + self._set_status("Preview updated") - def __init__(self, config): - super().__init__(config) - - # slots go in __init__ - self.KARABO_SLOT(self.sendGeometry) - self.KARABO_SLOT(self.updatePreview) - self.KARABO_SLOT(self.requestScene) - self.KARABO_SLOT(self.load_geometry_from_file, slotName="geometryFile_load") - - plt.switch_backend("agg") # plotting backend which works for preview hack - - # these will be set by load_geometry_from_file or get_manual_geometry - self.geometry = None - self.pickled_geometry = None - self.registerInitialFunction(self._initialization) - - def _initialization(self): - if self.get("geometryFile.filePath"): - self.log_status_info("geometryFile.filePath set, will try to load geometry") - self.load_geometry_from_file() - if self.geometry is None: - # no file path or loading failed - self.get_manual_geometry() - - self.updateState(State.ON) - - def _set_geometry(self, geometry, update_preview=True, send=True): - self.geometry = geometry - self.pickled_geometry = pickle.dumps(self.geometry) - if update_preview: - self.updatePreview() - if send: - self.sendGeometry() - - def get_manual_geometry(self): - # subclass must implement this - raise NotImplementedError() - - def _update_manual_from_current(self): - # subclass should implement this - # TODO: figure out for JUNGFRAUGeometry - raise NotImplementedError() - - def load_geometry_from_file(self): + @Slot( + displayedName="Load from file", + allowedStates=[State.ACTIVE], + ) + async def loadFromFile(self): with self.push_state(State.CHANGING): geometry = None - file_path = self.get("geometryFile.filePath") - self.log_status_info(f"Loading geometry from {file_path}...") + file_path = self.geometryFile.filePath.value + self._set_status(f"Loading geometry from {file_path}...") try: geometry = self.geometry_class.from_crystfel_geom(file_path) except FileNotFoundError: - self.log_status_warn("Geometry file not found") + self._set_status("Geometry file not found", level=logging.WARN) except RuntimeError as e: - self.log_status_warn(f"Failed to load geometry file: {e}") + self._set_status( + f"Failed to load geometry file: {e}", level=logging.WARN + ) except Exception as e: - self.log_status_warn(f"Misc. exception when loading geometry file: {e}") + self._set_status( + f"Misc. exception when loading geometry file: {e}", + level=logging.WARN, + ) else: - self._set_geometry(geometry) - self.log_status_info("Successfully loaded geometry from file") - if self.get("geometryFile.updateManualOnLoad"): - self.log_status_info( + geometry = geometry.offset( + (self.geometryFile.offset.x, self.geometryFile.offset.y) + ) + await self._set_geometry(geometry) + self._set_status("Successfully loaded geometry from file") + if self.geometryFile.updateManualOnLoad.value: + self._set_status( "Updating manual settings on device to reflect loaded geometry" ) self._update_manual_from_current() @@ -254,160 +202,154 @@ class ManualGeometryBase(PythonDevice): return False - def requestScene(self, params): - payload = Hash() - scene_name = params.get("name", default="") - payload["name"] = scene_name - payload["success"] = True - if scene_name == "overview": - payload["data"] = scenes.manual_geometry_overview( - device_id=self.getInstanceId() - ) - else: - payload["success"] = False - response = Hash() - response["type"] = "deviceScene" - response["origin"] = self.getInstanceId() - response["payload"] = payload - self.reply(response) - - def sendGeometry(self): - self.writeChannel( - "geometryOutput", Hash("pickledGeometry", self.pickled_geometry) - ) - - def updatePreview(self): - axis = self.geometry.inspect() - axis.figure.tight_layout(pad=0) - axis.figure.set_facecolor("none") - # axis.figure.set_size_inches(6, 6) - # axis.figure.set_dpi(300) - canvas = FigureCanvasAgg(axis.figure) - canvas.draw() - image_buffer = np.frombuffer(canvas.buffer_rgba(), dtype=np.uint8).reshape( - canvas.get_width_height()[::-1] + (4,) - ) - self.set( - "layoutPreview", - ImageData(image_buffer, encoding=Encoding.RGBA, bitsPerPixel=3 * 8), - ) - self.log_status_info("Preview updated") - - def preReconfigure(self, config): - self._prereconfigure_update_hash = config + def _update_manual_from_current(self): + # subclass should implement (neat when loading from CrystFEL geom) + raise NotImplementedError() - def postReconfigure(self): - del self._prereconfigure_update_hash + async def _set_geometry(self, geometry, update_preview=True, send=True): + self.geometry = geometry + self.pickled_geometry = pickle.dumps(self.geometry) + if update_preview: + await self.updatePreview() + if send: + await self.sendGeometry() - def log_status_info(self, msg): - self.log.INFO(msg) - self.set("status", msg) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + plt.switch_backend("agg") # plotting backend which works for preview hack - def log_status_warn(self, msg): - self.log.WARN(msg) - self.set("status", msg) + async def onInitialization(self): + self.state = State.INIT + # TODO: try to load file if set + await self.setManual() + self.state = State.ACTIVE @contextlib.contextmanager def push_state(self, state): - previous_state = self.get("state") - self.updateState(state) + previous_state = self.state + self.state = state try: yield finally: - self.updateState(previous_state) + self.state = previous_state + + def _set_status(self, text, level=logging.INFO): + """Add and log a status message. + + Suppresses throttling from the gui server. + """ + + self.status = text + self.logger.log(level, text) + + +def makeQuadrantCornersNode(default_values): + assert len(default_values) == 4 + assert all(len(x) == 2 for x in default_values) + + class QuadrantCornersNode(Configurable): + Q1 = makeXYCoordinateNode(*default_values[0]) + Q2 = makeXYCoordinateNode(*default_values[1]) + Q3 = makeXYCoordinateNode(*default_values[2]) + Q4 = makeXYCoordinateNode(*default_values[3]) + offset = makeXYOffsetNode() + + return Node(QuadrantCornersNode) -@KARABO_CLASSINFO("ManualQuadrantsGeometryBase", deviceVersion) class ManualQuadrantsGeometryBase(ManualGeometryBase): - @staticmethod - def expectedParameters(expected): - # note: subclasses should set better defaults - (NODE_ELEMENT(expected).key("quadrantCorners").commit(),) - for q in range(1, 5): - ( - NODE_ELEMENT(expected).key(f"quadrantCorners.Q{q}").commit(), - DOUBLE_ELEMENT(expected) - .key(f"quadrantCorners.Q{q}.x") - .assignmentOptional() - .defaultValue(0) - .reconfigurable() - .commit(), - - DOUBLE_ELEMENT(expected) - .key(f"quadrantCorners.Q{q}.y") - .assignmentOptional() - .defaultValue(0) - .reconfigurable() - .commit(), + quadrantCorners = None # subclass must define (with nice defaults) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._quadrant_corners = [ + self.quadrantCorners.Q1, + self.quadrantCorners.Q2, + self.quadrantCorners.Q3, + self.quadrantCorners.Q4, + ] + + @slot + def requestScene(self, params): + name = params.get("name", default="overview") + if name == "overview": + # Assumes there are correction devices known to manager + scene_data = scenes.quadrant_geometry_overview( + self.deviceId, ) + payload = Hash("success", True, "name", name, "data", scene_data) - def postReconfigure(self): - if any( - path.startswith("quadrantCorners") - for path in self._prereconfigure_update_hash.getPaths() - ): - self.get_manual_geometry() + return Hash("type", "deviceScene", "origin", self.deviceId, "payload", payload) - super().postReconfigure() - - def get_manual_geometry(self): - self.log_status_info("Updating geometry from manual configuration") + @Slot( + displayedName="Set from device", + allowedStates=[State.ACTIVE], + ) + async def setManual(self): + self._set_status("Updating geometry from manual configuration") with self.push_state(State.CHANGING): - self.quadrant_corners = tuple( + geometry = self.geometry_class.from_quad_positions( + [(Q.x.value, Q.y.value) for Q in self._quadrant_corners] + ).offset( ( - self.get(f"quadrantCorners.Q{q}.x"), - self.get(f"quadrantCorners.Q{q}.y"), + self.quadrantCorners.offset.x.value, + self.quadrantCorners.offset.y.value, ) - for q in range(1, 5) ) - geometry = self.geometry_class.from_quad_positions(self.quadrant_corners) - self._set_geometry(geometry) + await self._set_geometry(geometry) def _update_manual_from_current(self): - update = Hash() - for (x, y), quadrant in zip(self.geometry.quad_positions(), range(1, 5)): - update.set(f"quadrantCorners.Q{quadrant}.x", x) - update.set(f"quadrantCorners.Q{quadrant}.y", y) - self.set(update) - - -@KARABO_CLASSINFO("ManualModulesGeometryBase", deviceVersion) -class ManualModulesGeometryBase(ManualGeometryBase): - @staticmethod - def expectedParameters(expected): - ( - TABLE_ELEMENT(expected) - .key("modules") - .setColumns(ModuleColumn) - .assignmentOptional() - .defaultValue([]) - .reconfigurable() - .commit(), - - OVERWRITE_ELEMENT(expected) - .key("geometryFile.updateManualOnLoad") - .setNewDefaultValue(False) - .commit(), - ) - - def postReconfigure(self): - if self._prereconfigure_update_hash.has("modules"): - self.get_manual_geometry() + # TODO: consider what to do about offset + for corner, (x, y) in zip( + self._quadrant_corners, self.geometry.quad_positions() + ): + corner.x = x + corner.y = y + self.quadrantCorners.offset.x = 0 + self.quadrantCorners.offset.y = 0 + + +class ModuleListItem(Configurable): + posX = Double( + assignment=Assignment.OPTIONAL, + defaultValue=0, + ) + posY = Double( + assignment=Assignment.OPTIONAL, + defaultValue=0, + ) + orientX = Int32(assignment=Assignment.OPTIONAL, defaultValue=1) + orientY = Int32(assignment=Assignment.OPTIONAL, defaultValue=1) + + +class ManualModuleListGeometryBase(ManualGeometryBase): + moduleList = None # subclass must define (with nice defaults) + offset = makeXYOffsetNode() + + @slot + def requestScene(self, params): + name = params.get("name", default="overview") + if name == "overview": + # Assumes there are correction devices known to manager + scene_data = scenes.modules_geometry_overview( + self.deviceId, + ) + payload = Hash("success", True, "name", name, "data", scene_data) - super().postReconfigure() + return Hash("type", "deviceScene", "origin", self.deviceId, "payload", payload) - def get_manual_geometry(self): - self.log_status_info("Updating geometry from manual configuration") + @Slot( + displayedName="Set from device", + allowedStates=[State.ACTIVE], + ) + async def setManual(self): + self._set_status("Updating geometry from manual configuration") with self.push_state(State.CHANGING): - modules = self.get("modules") - module_pos = [ - (module.get("posX"), module.get("posY")) for module in modules - ] - orientations = [ - (module.get("orientationX"), module.get("orientationY")) - for module in modules - ] geometry = self.geometry_class.from_module_positions( - module_pos, orientations=orientations - ) - self._set_geometry(geometry) + [(x, y) for (x, y, _, _) in self.moduleList.value], + [ + (orient_x, orient_y) + for (_, _, orient_x, orient_y) in self.moduleList.value + ], + ).offset((self.offset.x.value, self.offset.y.value)) + await self._set_geometry(geometry) diff --git a/src/calng/mdl_geometry_base.py b/src/calng/mdl_geometry_base.py deleted file mode 100644 index 4b4dad87..00000000 --- a/src/calng/mdl_geometry_base.py +++ /dev/null @@ -1,369 +0,0 @@ -import contextlib -import logging -import pickle - -import extra_geom -from karabo.middlelayer import ( - AccessMode, - Assignment, - Bool, - Configurable, - Device, - Double, - EncodingType, - Hash, - Image, - ImageData, - Int32, - Node, - OutputChannel, - Slot, - State, - String, - Unit, - VectorChar, - VectorHash, -) -import matplotlib.pyplot as plt -from matplotlib.backends.backend_agg import FigureCanvasAgg -import numpy as np - -from ._version import version as deviceVersion - - -class GeometrySchema(Configurable): - pickledGeometry = VectorChar( - displayedName="Pickled geometry", - assignment=Assignment.OPTIONAL, - defaultValue=[], - ) - - -class GeometryFileNode(Configurable): - filePath = String( - defaultValue="", - displayedName="File path", - description="Full path (including filename and suffix) to the desired geometry " - "file. Keep in mind that the default directory is $KARABO/var/data on device " - "server node, so it's probably wise to give absolute path.", - assignment=Assignment.OPTIONAL, - accessMode=AccessMode.RECONFIGURABLE, - ) - updateManualOnLoad = Bool( - defaultValue=True, - displayedName="Update manual settings", - description="If this flag is on, the manual settings on this device will be " - "updated according to the loaded geometry file. This is useful when you want " - "to load a file and then tweak the geometry a bit. This will zero current " - "offset.", - assignment=Assignment.OPTIONAL, - accessMode=AccessMode.RECONFIGURABLE, - ) - - -class ManualGeometryBase(Device): - __version__ = deviceVersion - geometry_class = None # subclass must set - # subclass must add slot setManual - - geometryPreview = Image( - ImageData(np.empty(0, dtype=np.uint32)), - displayedName="Geometry preview", - encoding=EncodingType.RGBA, - ) - - geometryOutput = OutputChannel(GeometrySchema) - - geometryFile = Node( - GeometryFileNode, - displayedName="Geometry file", - description="Allows loading geometry from CrystFEL geometry file", - ) - - @Slot( - displayedName="Send geometry", - description="Send current geometry on output channel. This output channel is " - "typically used by assembler devices waiting for new geometries. Updating the " - "geometry will usually automatically cause update, but hit this slot in case " - "geometry is missing somewhere.", - allowedStates=[State.ACTIVE], - ) - async def sendGeometry(self): - self.geometryOutput.schema.pickledGeometry = self.pickled_geometry - await self.geometryOutput.writeData() - - @Slot( - displayedName="Update preview", - allowedStates=[State.ACTIVE], - ) - async def updatePreview(self): - axis = self.geometry.inspect() - axis.figure.tight_layout(pad=0) - axis.figure.set_facecolor("none") - # axis.figure.set_size_inches(6, 6) - # axis.figure.set_dpi(300) - canvas = FigureCanvasAgg(axis.figure) - canvas.draw() - image_buffer = np.frombuffer(canvas.buffer_rgba(), dtype=np.uint8).reshape( - canvas.get_width_height()[::-1] + (4,) - ) - self.geometryPreview = ImageData( - image_buffer, encoding=EncodingType.RGBA, bitsPerPixel=3 * 8 - ) - self._set_status("Preview updated") - - @Slot( - displayedName="Load from file", - allowedStates=[State.ACTIVE], - ) - async def loadFromFile(self): - with self.push_state(State.CHANGING): - geometry = None - - file_path = self.geometryFile.filePath.value - self._set_status(f"Loading geometry from {file_path}...") - - try: - geometry = self.geometry_class.from_crystfel_geom(file_path) - except FileNotFoundError: - self._set_status("Geometry file not found", level=logging.WARN) - except RuntimeError as e: - self._set_status( - f"Failed to load geometry file: {e}", level=logging.WARN - ) - except Exception as e: - self._set_status( - f"Misc. exception when loading geometry file: {e}", - level=logging.WARN, - ) - else: - await self._set_geometry(geometry) - self._set_status("Successfully loaded geometry from file") - if self.geometryFile.updateManualOnLoad.value: - self._set_status( - "Updating manual settings on device to reflect loaded geometry" - ) - self._update_manual_from_current() - return True - - return False - - def _update_manual_from_current(self): - # subclass should implement (neat when loading from CrystFEL geom) - raise NotImplementedError() - - async def _set_geometry(self, geometry, update_preview=True, send=True): - self.geometry = geometry - self.pickled_geometry = pickle.dumps(self.geometry) - if update_preview: - await self.updatePreview() - if send: - await self.sendGeometry() - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - plt.switch_backend("agg") # plotting backend which works for preview hack - - async def onInitialization(self): - self.state = State.INIT - # TODO: try to load file if set - await self.setManual() - self.state = State.ACTIVE - - @contextlib.contextmanager - def push_state(self, state): - previous_state = self.state - self.state = state - try: - yield - finally: - self.state = previous_state - - def _set_status(self, text, level=logging.INFO): - """Add and log a status message. - - Suppresses throttling from the gui server. - """ - - self.status = text - self.logger.log(level, text) - - -def makeXYCoordinateNode( - default_x, default_y, x_args=None, y_args=None, node_args=None -): - class XYCoordinate(Configurable): - x = Double( - defaultValue=default_x, - accessMode=AccessMode.RECONFIGURABLE, - assignment=Assignment.OPTIONAL, - **({} if x_args is None else x_args), - ) - y = Double( - defaultValue=default_y, - accessMode=AccessMode.RECONFIGURABLE, - assignment=Assignment.OPTIONAL, - **({} if y_args is None else y_args), - ) - - return Node(XYCoordinate, **({} if node_args is None else node_args)) - - -def makeXYOffsetNode(): - return makeXYCoordinateNode( - 0, - 0, - x_args={"unitSymbol": Unit.METER}, - y_args={"unitSymbol": Unit.METER}, - node_args={ - "displayedName": "Offset", - "description": "See EXtra-geom documentation for details. This offset is " - "applied to entire detector after initial geometry is created from manual " - "parameters. Example: To move entire geometry up by 2 mm relative to beam, " - "set offset.y to 2e-3.", - }, - ) - - -def makeQuadrantCornersNode(default_values): - assert len(default_values) == 4 - assert all(len(x) == 2 for x in default_values) - - class QuadrantCornersNode(Configurable): - Q1 = makeXYCoordinateNode(*default_values[0]) - Q2 = makeXYCoordinateNode(*default_values[1]) - Q3 = makeXYCoordinateNode(*default_values[2]) - Q4 = makeXYCoordinateNode(*default_values[3]) - offset = makeXYOffsetNode() - - return Node(QuadrantCornersNode) - - -class ManualQuadrantsGeometryBase(ManualGeometryBase): - quadrantCorners = None # subclass must define (with nice defaults) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._quadrant_corners = [ - self.quadrantCorners.Q1, - self.quadrantCorners.Q2, - self.quadrantCorners.Q3, - self.quadrantCorners.Q4, - ] - - @Slot( - displayedName="Set from device", - allowedStates=[State.ACTIVE], - ) - async def setManual(self): - self._set_status("Updating geometry from manual configuration") - with self.push_state(State.CHANGING): - geometry = self.geometry_class.from_quad_positions( - [(Q.x.value, Q.y.value) for Q in self._quadrant_corners] - ).offset( - ( - self.quadrantCorners.offset.x.value, - self.quadrantCorners.offset.y.value, - ) - ) - await self._set_geometry(geometry) - - def _update_manual_from_current(self): - # TODO: consider what to do about offset - for corner, (x, y) in zip( - self._quadrant_corners, self.geometry.quad_positions() - ): - corner.x = x - corner.y = y - self.quadrantCorners.offset.x = 0 - self.quadrantCorners.offset.y = 0 - - -class MdlAgipd1MGeometry(ManualQuadrantsGeometryBase): - geometry_class = extra_geom.AGIPD_1MGeometry - quadrantCorners = makeQuadrantCornersNode( - [ - (-525, 625), - (-550, -10), - (520, -160), - (542.5, 475), - ] - ) - - -class MdlDssc1MGeometry(ManualQuadrantsGeometryBase): - geometry_class = extra_geom.DSSC_1MGeometry - quadrantCorners = makeQuadrantCornersNode( - [ - (-130, 5), - (-130, -125), - (5, -125), - (5, 5), - ] - ) - - -class MdlLpd1MGeometry(ManualQuadrantsGeometryBase): - geometry_class = extra_geom.LPD_1MGeometry - quadrantCorners = makeQuadrantCornersNode( - [ - (11.4, 299), - (-11.5, 8), - (254.5, -16), - (278.5, 275), - ] - ) - - -class ModuleListItem(Configurable): - posX = Double() - posY = Double() - orientX = Int32() - orientY = Int32() - - -class ManualModuleListGeometryBase(ManualGeometryBase): - moduleList = None # subclass must define (with nice defaults) - offset = makeXYOffsetNode() - - @Slot( - displayedName="Set from device", - allowedStates=[State.ACTIVE], - ) - async def setManual(self): - self._set_status("Updating geometry from manual configuration") - with self.push_state(State.CHANGING): - self._geometry = self.geometry_class.from_module_positions( - [(x, y) for (x, y, _, _) in self.moduleList.value], - [ - (orient_x, orient_y) - for (_, _, orient_x, orient_y) in self.moduleList.value - ], - ).offset((self.offset.x.value, self.offset.y.value)) - self._pickled_geometry = pickle.dumps(self._geometry) - - def _update_manual_from_current(self): - self._set_status("Not yet able to update manual config from file") - - -class MdlJungfrauGeometry(ManualModuleListGeometryBase): - geometry_class = extra_geom.JUNGFRAUGeometry - moduleList = VectorHash( - displayedName="Modules", - rows=ModuleListItem, - defaultValue=[ - Hash("posX", x, "posY", y, "orientX", ox, "orientY", oy) - for (x, y, ox, oy) in [ - (95, 564, -1, -1), - (95, 17, -1, -1), - (95, -530, -1, -1), - (95, -1077, -1, -1), - (-1125, -1078, 1, 1), - (-1125, -531, 1, 1), - (-1125, 16, 1, 1), - (-1125, 563, 1, 1), - ] - ], - accessMode=AccessMode.RECONFIGURABLE, - assignment=Assignment.OPTIONAL, - ) diff --git a/src/calng/scenes.py b/src/calng/scenes.py index fb00b680..5db57128 100644 --- a/src/calng/scenes.py +++ b/src/calng/scenes.py @@ -20,7 +20,9 @@ from karabo.common.scenemodel.api import ( RectangleModel, SceneModel, SceneTargetWindow, + TableElementModel, TrendGraphModel, + WebCamGraphModel, write_scene, ) @@ -596,6 +598,191 @@ class AssemblerDeviceStatus(VerticalLayout): ) +@titled("Manual geometry settings") +@boxed +class ManualQuadrantGeometrySettings(VerticalLayout): + def __init__(self, device_id): + super().__init__(padding=0) + self.children.append( + HorizontalLayout( + Space(width=3 * BASE_INC, height=BASE_INC), + LabelModel(text="x", width=4 * BASE_INC, height=BASE_INC), + LabelModel(text="y", width=4 * BASE_INC, height=BASE_INC), + ) + ) + self.children.extend( + [ + HorizontalLayout( + LabelModel(text=f"{thing}", width=3 * BASE_INC, height=BASE_INC), + DoubleLineEditModel( + keys=[f"{device_id}.quadrantCorners.{thing}.x"], + width=4 * BASE_INC, + height=BASE_INC, + ), + DoubleLineEditModel( + keys=[f"{device_id}.quadrantCorners.{thing}.y"], + width=4 * BASE_INC, + height=BASE_INC, + ), + ) + for thing in ("Q1", "Q2", "Q3", "Q4", "offset") + ] + ) + self.children.append( + DisplayCommandModel( + keys=[f"{device_id}.setManual"], + width=6 * BASE_INC, + height=BASE_INC, + ), + ) + + +@titled("Manual geometry settings") +@boxed +class ManualModulesGeometrySettings(VerticalLayout): + def __init__(self, device_id): + super().__init__(padding=0) + self.children.append( + TableElementModel( + keys=[f"{device_id}.moduleList"], + klass="EditableTableElement", + width=14 * BASE_INC, + height=10 * BASE_INC, + ) + ) + self.children.append( + HorizontalLayout( + LabelModel(text="Offset", width=3 * BASE_INC, height=BASE_INC), + DoubleLineEditModel( + keys=[f"{device_id}.offset.x"], + width=4 * BASE_INC, + height=BASE_INC, + ), + DoubleLineEditModel( + keys=[f"{device_id}.offset.y"], + width=4 * BASE_INC, + height=BASE_INC, + ), + ) + ) + self.children.append( + DisplayCommandModel( + keys=[f"{device_id}.setManual"], + width=6 * BASE_INC, + height=BASE_INC, + ), + ) + + +@titled("Tweak current geometry") +@boxed +class TweakCurrentGeometry(VerticalLayout): + def __init__(self, device_id): + super().__init__(padding=0) + self.children.append( + HorizontalLayout( + LabelModel(text="Offset", width=3 * BASE_INC, height=BASE_INC), + DoubleLineEditModel( + keys=[f"{device_id}.tweakGeometry.offset.x"], + width=4 * BASE_INC, + height=BASE_INC, + ), + DoubleLineEditModel( + keys=[f"{device_id}.tweakGeometry.offset.y"], + width=4 * BASE_INC, + height=BASE_INC, + ), + ) + ) + + +@titled("Geometry preview") +@boxed +class GeometryPreview(VerticalLayout): + def __init__(self, device_id): + super().__init__(padding=0) + self.children.append( + HorizontalLayout( + DisplayCommandModel( + keys=[f"{device_id}.updatePreview"], + width=6 * BASE_INC, + height=BASE_INC, + ), + DisplayCommandModel( + keys=[f"{device_id}.sendGeometry"], + width=6 * BASE_INC, + height=BASE_INC, + ), + ) + ) + self.children.append( + WebCamGraphModel( + keys=[f"{device_id}.geometryPreview"], + width=30 * BASE_INC, + height=30 * BASE_INC, + x=PADDING, + y=PADDING, + ) + ) + + +@titled("Geometry from file") +@boxed +class GeometryFromFileSettings(VerticalLayout): + def __init__(self, device_id): + super().__init__(padding=0) + self.children.append( + LabelModel( + text="File path:", + width=4 * BASE_INC, + height=BASE_INC, + ) + ) + self.children.append( + LineEditModel( + keys=[f"{device_id}.geometryFile.filePath"], + klass="EditableLineEdit", + width=8 * BASE_INC, + height=BASE_INC, + ) + ) + self.children.append( + HorizontalLayout( + LabelModel(text="Offset", width=3 * BASE_INC, height=BASE_INC), + DoubleLineEditModel( + keys=[f"{device_id}.geometryFile.offset.x"], + width=4 * BASE_INC, + height=BASE_INC, + ), + DoubleLineEditModel( + keys=[f"{device_id}.geometryFile.offset.y"], + width=4 * BASE_INC, + height=BASE_INC, + ), + ) + ) + self.children.append( + HorizontalLayout( + LabelModel( + text="Update manual settings", width=6 * BASE_INC, height=BASE_INC + ), + CheckBoxModel( + keys=[f"{device_id}.geometryFile.updateManualOnLoad"], + klass="EditableCheckBox", + width=2 * BASE_INC, + height=BASE_INC, + ), + ) + ) + self.children.append( + DisplayCommandModel( + keys=[f"{device_id}.loadFromFile"], + width=6 * BASE_INC, + height=BASE_INC, + ), + ) + + # section: generating actual scenes @@ -624,7 +811,7 @@ def scene_generator(fun): @scene_generator -def correction_device_overview_scene(device_id, schema): +def correction_device_overview(device_id, schema): schema_hash = schema_to_hash(schema) return HorizontalLayout( @@ -652,7 +839,7 @@ def correction_device_overview_scene(device_id, schema): @scene_generator -def manager_device_overview_scene( +def manager_device_overview( manager_device_id, manager_device_schema, correction_device_schema, @@ -835,6 +1022,28 @@ def detector_assembler_overview(device_id, geometry_device_id): ) +@scene_generator +def quadrant_geometry_overview(device_id): + return VerticalLayout( + HorizontalLayout( + ManualQuadrantGeometrySettings(device_id), + GeometryFromFileSettings(device_id), + ), + GeometryPreview(device_id), + ) + + +@scene_generator +def modules_geometry_overview(device_id): + return VerticalLayout( + HorizontalLayout( + ManualModulesGeometrySettings(device_id), + GeometryFromFileSettings(device_id), + ), + GeometryPreview(device_id), + ) + + # section: here be monsters -- GitLab