Skip to content
Snippets Groups Projects
Commit 4dfa248d authored by David Hammer's avatar David Hammer
Browse files

Geometry device: overhaul + allow loading CrystFEL

parent 2a21de06
No related branches found
No related tags found
1 merge request!12Snapshot: field test deployed version as of end of run 202201
......@@ -243,7 +243,7 @@ class DetectorAssembler(TrainMatcher.TrainMatcher):
continue
geometry_device = geometry_device_list[0].split(":")[0]
self.log.INFO(f"Asking {geometry_device} for a geometry")
self.signalSlotable.call(geometry_device, "pleaseSendYourGeometry")
self.signalSlotable.call(geometry_device, "sendGeometry")
time.sleep(1)
if self._geometry is not None:
......
import contextlib
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,
......@@ -73,8 +77,23 @@ ModuleColumn = Schema()
@KARABO_CLASSINFO("ManualGeometryBase", deviceVersion)
class ManualGeometryBase(PythonDevice):
geometry_class = None # concrete device subclass must set this
@staticmethod
def expectedParameters(expected):
# Karabo things
(
OVERWRITE_ELEMENT(expected)
.key("state")
.setNewDefaultValue(State.INIT)
.commit(),
OVERWRITE_ELEMENT(expected)
.key("doNotCompressEvents")
.setNewDefaultValue(True)
.commit(),
)
# "mandatory" for geometry serving device
(
OUTPUT_CHANNEL(expected)
......@@ -82,7 +101,23 @@ class ManualGeometryBase(PythonDevice):
.dataSchema(geometry_schema)
.commit(),
SLOT_ELEMENT(expected).key("pleaseSendYourGeometry").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")
......@@ -92,6 +127,53 @@ class ManualGeometryBase(PythonDevice):
IMAGEDATA_ELEMENT(expected).key("layoutPreview").commit(),
)
# 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(),
)
# scenes are fun
(
VECTOR_STRING_ELEMENT(expected)
......@@ -102,21 +184,75 @@ class ManualGeometryBase(PythonDevice):
.commit(),
)
def update_geom(self):
raise NotImplementedError()
def __init__(self, config):
super().__init__(config)
self.KARABO_SLOT(self.pleaseSendYourGeometry)
# slots go in __init__
self.KARABO_SLOT(self.sendGeometry)
self.KARABO_SLOT(self.updatePreview)
self.KARABO_SLOT(self.requestScene)
self.update_geom()
plt.switch_backend("agg")
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)
self.pleaseSendYourGeometry()
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):
with self.push_state(State.CHANGING):
geometry = None
file_path = self.get("geometryFile.filePath")
self.log_status_info(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")
except RuntimeError as e:
self.log_status_warn(f"Failed to load geometry file: {e}")
except Exception as e:
self.log_status_warn(f"Misc. exception when loading geometry file: {e}")
else:
self._set_geometry(geometry)
self.log_status_info("Successfully loaded geometry from file")
if self.get("geometryFile.updateManualOnLoad"):
self.log_status_info(
"Updating manual settings on device to reflect loaded geometry"
)
self._update_manual_from_current()
return True
return False
def requestScene(self, params):
payload = Hash()
......@@ -135,10 +271,13 @@ class ManualGeometryBase(PythonDevice):
response["payload"] = payload
self.reply(response)
def pleaseSendYourGeometry(self):
self.update_geom()
self.writeChannel("geometryOutput", Hash("pickledGeometry", self.pickled))
axis = self.geom.inspect()
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)
......@@ -152,6 +291,7 @@ class ManualGeometryBase(PythonDevice):
"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
......@@ -159,6 +299,23 @@ class ManualGeometryBase(PythonDevice):
def postReconfigure(self):
del self._prereconfigure_update_hash
def log_status_info(self, msg):
self.log.INFO(msg)
self.set("status", msg)
def log_status_warn(self, msg):
self.log.WARN(msg)
self.set("status", msg)
@contextlib.contextmanager
def push_state(self, state):
previous_state = self.get("state")
self.updateState(state)
try:
yield
finally:
self.updateState(previous_state)
@KARABO_CLASSINFO("ManualQuadrantsGeometryBase", deviceVersion)
class ManualQuadrantsGeometryBase(ManualGeometryBase):
......@@ -189,19 +346,29 @@ class ManualQuadrantsGeometryBase(ManualGeometryBase):
path.startswith("quadrantCorners")
for path in self._prereconfigure_update_hash.getPaths()
):
self.update_geom()
self.get_manual_geometry()
super().postReconfigure()
def update_geom(self):
self.quadrant_corners = tuple(
(self.get(f"quadrantCorners.Q{q}.x"), self.get(f"quadrantCorners.Q{q}.y"))
for q in range(1, 5)
)
self.geom = self.geometry_class.from_quad_positions(self.quadrant_corners)
self.pickled = pickle.dumps(self.geom)
# TODO: send to anyone who asks? make slot for that? send on connect?
self.writeChannel("geometryOutput", Hash("pickledGeometry", self.pickled))
def get_manual_geometry(self):
self.log_status_info("Updating geometry from manual configuration")
with self.push_state(State.CHANGING):
self.quadrant_corners = tuple(
(
self.get(f"quadrantCorners.Q{q}.x"),
self.get(f"quadrantCorners.Q{q}.y"),
)
for q in range(1, 5)
)
geometry = self.geometry_class.from_quad_positions(self.quadrant_corners)
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)
......@@ -216,24 +383,31 @@ class ManualModulesGeometryBase(ManualGeometryBase):
.defaultValue([])
.reconfigurable()
.commit(),
OVERWRITE_ELEMENT(expected)
.key("geometryFile.updateManualOnLoad")
.setNewDefaultValue(False)
.commit(),
)
def postReconfigure(self):
if self._prereconfigure_update_hash.has("modules"):
self.update_geom()
self.get_manual_geometry()
super().postReconfigure()
def update_geom(self):
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
]
self.geom = self.geometry_class.from_module_positions(
module_pos, orientations=orientations
)
self.pickled = pickle.dumps(self.geom)
# TODO: send to anyone who asks? make slot for that?
self.writeChannel("geometryOutput", Hash("pickledGeometry", self.pickled))
def get_manual_geometry(self):
self.log_status_info("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)
......@@ -810,7 +810,7 @@ def detector_assembler_overview(device_id, geometry_device_id):
height=BASE_INC,
),
DisplayCommandModel(
keys=[f"{geometry_device_id}.pleaseSendYourGeometry"],
keys=[f"{geometry_device_id}.sendGeometry"],
width=14 * BASE_INC,
height=BASE_INC,
),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment