diff --git a/DEPENDS b/DEPENDS index 7d43690b3c115ea60f482031806a6dc36a6eef3b..b18c86bacb67525e328f47a25aab32e930d0ac6d 100644 --- a/DEPENDS +++ b/DEPENDS @@ -1,3 +1,3 @@ TrainMatcher, 2.3.2-2.16.2 -calngDeps, 1.0.0-2.16.2 +calngDeps, 1.0.1-2.16.2 calibrationClient, 11.0.0 diff --git a/setup.py b/setup.py index 3b61604a455dc6afe5f1f63bd0fdf8c39594a143..92c44ccce81784e436beb57334c62c6be20b7307 100644 --- a/setup.py +++ b/setup.py @@ -32,10 +32,12 @@ setup(name='calng', 'JungfrauCorrection = calng.corrections.JungfrauCorrection:JungfrauCorrection', 'PnccdCorrection = calng.corrections.PnccdCorrection:PnccdCorrection', 'LpdCorrection = calng.corrections.LpdCorrection:LpdCorrection', + 'LpdminiCorrection = calng.corrections.LpdminiCorrection:LpdminiCorrection', 'ShmemToZMQ = calng.ShmemToZMQ:ShmemToZMQ', 'ShmemTrainMatcher = calng.ShmemTrainMatcher:ShmemTrainMatcher', 'DetectorAssembler = calng.DetectorAssembler:DetectorAssembler', 'Gotthard2Assembler = calng.Gotthard2Assembler:Gotthard2Assembler', + 'LpdminiSplitter = calng.LpdminiSplitter:LpdminiSplitter', ], 'karabo.middlelayer_device': [ @@ -44,10 +46,12 @@ setup(name='calng', 'JungfrauCondition = calng.conditions.JungfrauCondition:JungfrauCondition', 'LpdCondition = calng.conditions.LpdCondition:LpdCondition', 'Agipd1MGeometry = calng.geometries.Agipd1MGeometry:Agipd1MGeometry', + 'Agipd500KGeometry = calng.geometries.Agipd500KGeometry:Agipd500KGeometry', 'Dssc1MGeometry = calng.geometries:Dssc1MGeometry.Dssc1MGeometry', 'Epix100Geometry = calng.geometries:Epix100Geometry.Epix100Geometry', 'JungfrauGeometry = calng.geometries:JungfrauGeometry.JungfrauGeometry', 'Lpd1MGeometry = calng.geometries:Lpd1MGeometry.Lpd1MGeometry', + 'LpdminiGeometry = calng.geometries:LpdminiGeometry.LpdminiGeometry', 'PnccdGeometry = calng.geometries:PnccdGeometry.PnccdGeometry', 'RoiTool = calng.RoiTool:RoiTool', ], @@ -86,6 +90,12 @@ setup(name='calng', extra_link_args=['-fopenmp'], language="c++", ), + Extension( + 'calng.kernels.lpd_cython', + ['src/calng/kernels/lpd_cpu.pyx'], + extra_compile_args=['-O3', '-march=native', '-fopenmp'], + extra_link_args=['-fopenmp'], + ), ], compiler_directives={ 'language_level': 3 diff --git a/src/calng/CalibrationManager.py b/src/calng/CalibrationManager.py index 2206ade34015937e2856678dda6ce2166a39407d..60f00fae7c877810b082890712599cb24f7ea12d 100644 --- a/src/calng/CalibrationManager.py +++ b/src/calng/CalibrationManager.py @@ -1135,8 +1135,11 @@ class CalibrationManager(DeviceClientBase, Device): if key_patterns: try: # Try to obtain most recent configuration. - old_config = await getConfigurationFromPast( - device_id, datetime.now().isoformat()) + old_config = await wait_for(getConfigurationFromPast( + device_id, datetime.now().isoformat()), 15.0) + except AsyncTimeoutError: + self.logger.warn(f'Timeout receiving previous configuration ' + f'for {device_id}') except KaraboError as e: self.logger.warn(f'Failed receiving previous configuration ' f'for {device_id}: {e}') @@ -1315,6 +1318,7 @@ class CalibrationManager(DeviceClientBase, Device): await gather(*awaitables) awaitables.clear() + callNoWait(self.geometryDevice.value, "sendGeometry") self._set_status('All devices instantiated') self.state = State.ACTIVE diff --git a/src/calng/DetectorAssembler.py b/src/calng/DetectorAssembler.py index 899dc9b1e45fd3000e077ab840f70f9a9d03cd2c..8a8c9e9be22cfe66c8fffa13f61bcb70b4029798 100644 --- a/src/calng/DetectorAssembler.py +++ b/src/calng/DetectorAssembler.py @@ -109,6 +109,7 @@ class DetectorAssembler(TrainMatcher.TrainMatcher): def __init__(self, conf): super().__init__(conf) self.info.merge(Hash("timeOfFlight", 0)) + self.registerSlot(self.slotReceiveGeometry) def initialization(self): super().initialization() @@ -122,22 +123,12 @@ class DetectorAssembler(TrainMatcher.TrainMatcher): self.KARABO_SLOT(self.requestScene) - geometry_device = self.get("geometryDevice") - client = self.remote() - try: - initial_geometry = client.get(geometry_device, "serializedGeometry") - except RuntimeError: - self.log.WARN( - "Failed to get initial geometry, maybe geometry device is down" - ) - else: - self.log.INFO("Got geometry immediately after init :D") - self._receive_geometry( - geometry_device, - Hash("pickledGeometry", initial_geometry), - ) - - self.remote().registerDeviceMonitor(geometry_device, self._receive_geometry) + self.signalSlotable.connect( + self.get("geometryDevice"), + "signalNewGeometry", + "", # slot device ID (default: self) + "slotReceiveGeometry", + ) self.assembled_output = self.signalSlotable.getOutputChannel("assembledOutput") self.start() @@ -168,15 +159,12 @@ class DetectorAssembler(TrainMatcher.TrainMatcher): ) ) - def _receive_geometry(self, device_id, config): - if not config.has("serializedGeometry"): - return - self.log.INFO(f"Found geometry on {device_id}") - serialized_geometry = config["serializedGeometry"] - if len(serialized_geometry) == 0: - self.log.INFO("New geometry empty, will ignore update.") - return - self._geometry = geom_utils.deserialize_geometry(serialized_geometry) + def slotReceiveGeometry(self, device_id, serialized_geometry): + self.log.INFO(f"Received geometry from {device_id}") + try: + self._geometry = geom_utils.deserialize_geometry(serialized_geometry) + except Exception as e: + self.log.WARN(f"Failed to deserialize geometry; {e}") # TODO: allow multiple memory cells (extra geom notion of extra dimensions) self._stack_input_buffer = np.ma.masked_array( data=np.zeros(self._geometry.expected_data_shape, dtype=np.float32), diff --git a/src/calng/LpdminiSplitter.py b/src/calng/LpdminiSplitter.py new file mode 100644 index 0000000000000000000000000000000000000000..ed4c78a6086f1370716e9a0c2f402b1f3429a2fd --- /dev/null +++ b/src/calng/LpdminiSplitter.py @@ -0,0 +1,242 @@ +from timeit import default_timer + +import numpy as np +from karabo.bound import ( + DOUBLE_ELEMENT, + INPUT_CHANNEL, + KARABO_CLASSINFO, + NODE_ELEMENT, + OUTPUT_CHANNEL, + OVERWRITE_ELEMENT, + UINT64_ELEMENT, + VECTOR_UINT32_ELEMENT, + VECTOR_STRING_ELEMENT, + ChannelMetaData, + Hash, + MetricPrefix, + PythonDevice, + State, + Timestamp, + Unit, +) +from karabo.common.api import KARABO_SCHEMA_DISPLAY_TYPE_SCENES as DT_SCENES + +from . import scenes, schemas, shmem_utils, utils +from .base_correction import WarningLampType +from ._version import version as deviceVersion + +# plan: +# - split data into 8 modules +# - copy each into shared memory and send on distinct channel + +PROCESSING_STATE_TIMEOUT = 10 + + +@KARABO_CLASSINFO("LpdminiSplitter", deviceVersion) +class LpdminiSplitter(PythonDevice): + # up to 8 minis, using 1-indexing for naming + _output_channel_names = [f"output-{i+1}" for i in range(8)] + + @staticmethod + def expectedParameters(expected): + ( + OVERWRITE_ELEMENT(expected) + .key("state") + .setNewDefaultValue(State.INIT) + .commit(), + + INPUT_CHANNEL(expected) + .key("input") + .commit(), + + VECTOR_UINT32_ELEMENT(expected) + .key("outputDataShape") + .displayedName("Output data shape") + .readOnly() + .initialValue([512, 32, 256]) + .commit(), + + UINT64_ELEMENT(expected) + .key("trainId") + .displayedName("Train ID") + .description("ID of latest train processed by this device.") + .readOnly() + .initialValue(0) + .commit(), + + NODE_ELEMENT(expected) + .key("performance") + .commit(), + + DOUBLE_ELEMENT(expected) + .key("performance.rate") + .displayedName("Rate") + .description( + "Actual rate with which this device gets, processes, and sends trains. " + "This is a simple windowed moving average." + ) + .unit(Unit.HERTZ) + .readOnly() + .initialValue(0) + .commit(), + + DOUBLE_ELEMENT(expected) + .key("performance.processingTime") + .displayedName("Processing time") + .unit(Unit.SECOND) + .metricPrefix(MetricPrefix.MILLI) + .readOnly() + .initialValue(0) + .warnHigh(100) + .info("Processing too slow to reach 10 Hz") + .needsAcknowledging(False) + .commit(), + + VECTOR_STRING_ELEMENT(expected) + .key("availableScenes") + .setSpecialDisplayType(DT_SCENES) + .readOnly() + .initialValue(["overview"]) + .commit(), + ) + + for channel_name in LpdminiSplitter._output_channel_names: + ( + OUTPUT_CHANNEL(expected) + .key(channel_name) + .dataSchema(schemas.xtdf_output_schema()) + .commit(), + ) + + def __init__(self, config): + super().__init__(config) + self.registerInitialFunction(self._initialization) + self._output_image_shape = (512, 32, 256) # will be updated based on frames + self.KARABO_SLOT(self.requestScene) + self._latest_log_message = None + self._latest_warn_type = None + + def _initialization(self): + self.KARABO_ON_DATA("input", self.input_handler) + self._shmem_buffers = [ + shmem_utils.ShmemCircularBuffer( + 1 * 2**30, + (512, 32, 256), + np.uint16, + f"{self.getInstanceId()}:{channel_name}" + ) + for channel_name in self._output_channel_names + ] + + # performance measures and such + self._last_processing_started = 0 # used for processing time and timeout + self._buffered_status_update = Hash() + self._processing_time_tracker = utils.ExponentialMovingAverage(alpha=0.3) + self._rate_tracker = utils.WindowRateTracker() + self._input_delay_tracker = utils.ExponentialMovingAverage(alpha=0.3) + self._performance_measure_update_timer = utils.RepeatingTimer( + interval=1, + callback=self._update_performance_measures, + ) + self.updateState(State.ON) + + def input_handler(self, data_hash, metadata): + state = self.get("state") + if state is State.INIT: + return + + # TODO: handle empty DAQ hash + if not data_hash.has("image.data"): + self.log_status_info("Input hash had no image data node") + if state is State.PROCESSING: + self.updateState(State.IGNORING) + return + try: + image_data = np.asarray( + data_hash.get("image.data") + ) + except RuntimeError as err: + self.log_status_info( + f"Failed to load image data; probably empty hash from DAQ: {err}", + WarningLampType.EMPTY_HASH, + ) + if state is State.PROCESSING: + self.updateState(State.IGNORING) + return + + if state is not State.PROCESSING: + self.log_status_info("Processing data") + self.updateState(State.PROCESSING) + + self._last_processing_started = default_timer() + timestamp = Timestamp.fromHashAttributes(metadata.getAttributes("timestamp")) + self._buffered_status_update.set("trainId", timestamp.getTrainId()) + num_frames = data_hash["image.cellId"].size + image_data = image_data.reshape((num_frames, 256, 256)) + # note: reshape is equivalent to overrideInputAxisOrder in BaseCorrection + if num_frames != self._output_image_shape: + self._output_image_shape = (num_frames, 32, 256) + for shmem_buffer in self._shmem_buffers: + shmem_buffer.change_shape(self._output_image_shape) + self.set("outputDataShape", list(self._output_image_shape)) + # reshape to drop singleton axis + optionally fix DAQ shape shenanigans + receiver_source_name = metadata.get("source") + for i, (channel_name, shmem_buffer) in enumerate( + zip(self._output_channel_names, self._shmem_buffers) + ): + this_slice = image_data[:, i * 32 : (i + 1) * 32] + buffer_handle, buffer_array = shmem_buffer.next_slot() + buffer_array[:] = this_slice + data_hash.set("image.data", buffer_handle) + data_hash.set("calngShmemPaths", ["image.data"]) + # TODO: consider source name; virtual indices like in offline? + channel = self.signalSlotable.getOutputChannel(channel_name) + channel.write( + data_hash, + ChannelMetaData(f"{receiver_source_name}-{i+1}", timestamp), + # adding 1-indexed suffix index to soruce name forwarded + copyAllData=False, + ) + channel.update() + self._processing_time_tracker.update( + default_timer() - self._last_processing_started + ) + self._rate_tracker.update() + + def _update_performance_measures(self): + if self.get("state") in {State.PROCESSING, State.IGNORING}: + self._buffered_status_update.set( + "performance.rate", self._rate_tracker.get() + ) + self._buffered_status_update.set( + "performance.processingTime", self._processing_time_tracker.get() * 1000 + ) + self.set(self._buffered_status_update) + if ( + default_timer() - self._last_processing_started + > PROCESSING_STATE_TIMEOUT + ): + self.updateState(State.ON) + + def requestScene(self, params): + payload = Hash() + payload["name"] = "overview" + payload["data"] = scenes.lpdmini_splitter_overview( + device_id=self.getInstanceId(), + schema=self.getFullSchema(), + ) + payload["success"] = True + response = Hash() + response["type"] = "deviceScene" + response["origin"] = self.getInstanceId() + response["payload"] = payload + self.reply(response) + + def log_status_info(self, msg, warn_type=None): + if (msg != self._latest_log_message) and ( + warn_type is None or warn_type != self._latest_warn_type + ): + self.log.INFO(msg) + self.set("status", msg) + self._latest_log_message = msg + self._latest_warn_type = warn_type diff --git a/src/calng/RoiTool.py b/src/calng/RoiTool.py index a271f4bd9ea1cc40011107f4ca8ae127b9af5312..4f772c96f6423a95fa9bd723678684bfc2fa29db 100644 --- a/src/calng/RoiTool.py +++ b/src/calng/RoiTool.py @@ -26,6 +26,7 @@ from karabo.middlelayer import ( VectorDouble, VectorInt32, VectorString, + get_property, slot, ) @@ -167,10 +168,10 @@ class RoiTool(Device): async def imageInput(self, data, meta): # TODO: handle streams without explicit mask? image = np.ma.masked_array( - data=rec_getattr(data, self.imageDataPath.value).astype( + data=get_property(data, self.imageDataPath.value).astype( np.float32, copy=False ), - mask=rec_getattr(data, self.maskPath.value).astype(np.uint8, copy=False), + mask=get_property(data, self.maskPath.value).astype(np.uint8, copy=False), ) # TODO: make handling of extra dimension(s) configurable @@ -294,13 +295,6 @@ class RoiTool(Device): return Hash("type", "deviceScene", "origin", self.deviceId, "payload", payload) -def rec_getattr(obj, path): - res = obj - for part in path.split("."): - res = getattr(res, part) - return res - - def _histogram_plot_helper(counts, bin_edges): x = np.zeros(bin_edges.size * 2) y = np.zeros_like(x) diff --git a/src/calng/base_condition.py b/src/calng/base_condition.py index de9dc177d598eeb1c36dcb2bfd9ea397e9bb23bd..96614475fe157e8b742c92cdca4043f1ea54dd53 100644 --- a/src/calng/base_condition.py +++ b/src/calng/base_condition.py @@ -19,11 +19,12 @@ from karabo.middlelayer import ( connectDevice, disconnectDevice, getConfiguration, + get_property, slot, waitUntilNew, ) from ._version import version as deviceVersion -from . import scenes, utils +from . import scenes class PipelineOperationMode(enum.Enum): @@ -134,7 +135,7 @@ class ConditionBase(Device): while True: await waitUntilNew( *( - utils.rec_getattr(control_devs[control_id], control_key) + get_property(control_devs[control_id], control_key) for control_id, v in self.keys_to_get.items() for control_key, *_ in v # "v" for "variable naming is hard" ) @@ -225,7 +226,7 @@ class ConditionBase(Device): unchecked_parameter, "", "", - utils.rec_getattr( + get_property( self._manager_parameters_node, unchecked_parameter ).value, State.IGNORING.value, @@ -251,7 +252,7 @@ class ConditionBase(Device): if isinstance(control_dev, Proxy): # device proxy via connectDevice try: - control_value = utils.rec_getattr(control_dev, control_key).value + control_value = get_property(control_dev, control_key).value except AttributeError: control_value = "key not found" could_look_up = False @@ -277,7 +278,7 @@ class ConditionBase(Device): ideal_value = "control key not found" try: - manager_value = utils.rec_getattr( + manager_value = get_property( self._manager_parameters_node, manager_key ).value except AttributeError: diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py index cc359a3f90b5e719fe8a8a49d0fb5d6829b94732..a205471d3d586ef378ab95554c77825ec1e2a621 100644 --- a/src/calng/base_correction.py +++ b/src/calng/base_correction.py @@ -723,16 +723,7 @@ class BaseCorrection(PythonDevice): self._update_correction_flags() self._update_frame_filter() - self._buffered_status_update = Hash( - "trainId", - 0, - "performance.rate", - 0, - "performance.processingTime", - 0, - "performance.ratioOfRecentTrainsReceived", - 0, - ) + self._buffered_status_update = Hash() self._processing_time_tracker = utils.ExponentialMovingAverage(alpha=0.3) self._rate_tracker = utils.WindowRateTracker() self._input_delay_tracker = utils.ExponentialMovingAverage(alpha=0.3) @@ -1005,6 +996,7 @@ class BaseCorrection(PythonDevice): self.output_data_dtype, shmem_buffer_name, ) + self._shmem_receiver = shmem_utils.ShmemCircularBufferReceiver() if self._cuda_pin_buffers: self.log.INFO("Trying to pin the shmem buffer memory") self._shmem_buffer.cuda_pin() @@ -1092,6 +1084,7 @@ class BaseCorrection(PythonDevice): WarningLampType.EMPTY_HASH, only_print_once=True, ) as warn: + self._shmem_receiver.dereference_shmem_handles(data_hash) try: image_data = np.asarray(data_hash.get(self._image_data_path)) cell_table = ( diff --git a/src/calng/base_geometry.py b/src/calng/base_geometry.py index fcd24329a8e3c5e3c468d5494c8773eef0466760..f7c70e387e2f3f3a4c8bd5ba74bdbbc5d0e0e807 100644 --- a/src/calng/base_geometry.py +++ b/src/calng/base_geometry.py @@ -19,12 +19,14 @@ from karabo.middlelayer import ( ImageData, Int32, Node, + Signal, Slot, State, String, Unit, VectorHash, VectorString, + sleep, slot, ) from matplotlib.backends.backend_agg import FigureCanvasAgg @@ -123,6 +125,7 @@ class TweakGeometryNode(Configurable): super().__init__(*args, **kwargs) self._undo_stack = [] self._redo_stack = [] + self._device = self.get_root() def _reset(self): # clear history when new geometry is set from manual / file @@ -134,18 +137,16 @@ class TweakGeometryNode(Configurable): @Slot(displayedName="Undo") async def undo(self): assert len(self._undo_stack) > 0 - parent = self.get_root() - self._redo_stack.append(parent.geometry) - await parent._set_geometry(self._undo_stack.pop()) + self._redo_stack.append(self._device.geometry) + await self._device._set_geometry(self._undo_stack.pop()) self.undoLength = len(self._undo_stack) self.redoLength = len(self._redo_stack) @Slot(displayedName="Redo") async def redo(self): - parent = self.get_root() assert len(self._redo_stack) > 0 - self._undo_stack.append(parent.geometry) - await parent._set_geometry(self._redo_stack.pop()) + self._undo_stack.append(self._device.geometry) + await self._device._set_geometry(self._redo_stack.pop()) self.undoLength = len(self._undo_stack) self.redoLength = len(self._redo_stack) @@ -153,14 +154,13 @@ class TweakGeometryNode(Configurable): @Slot(displayedName="Add offset") async def add(self): - parent = self.get_root() - current_geometry = parent.geometry + current_geometry = self._device.geometry new_geometry = current_geometry.offset( (self.offset.x.value, self.offset.y.value) ) self._undo_stack.append(current_geometry) self.undoLength = len(self._undo_stack) - await parent._set_geometry(new_geometry) + await self._device._set_geometry(new_geometry) if self._redo_stack: self._redo_stack.clear() self.redoLength = 0 @@ -233,7 +233,7 @@ class GeometryFileNode(Configurable): class ManualGeometryBase(Device): __version__ = deviceVersion geometry_class = None # subclass must set - # subclass must add slot manualConfigsetManual + # subclass must add slot setManual availableScenes = VectorString( displayedName="Available scenes", @@ -246,11 +246,7 @@ class ManualGeometryBase(Device): daqPolicy=DaqPolicy.OMIT, ) - serializedGeometry = String( - displayedName="Serialized geometry", - defaultValue="", - accessMode=AccessMode.READONLY, - ) + signalNewGeometry = Signal(String(), String()) geometryPreview = Image( ImageData(np.empty(shape=(0, 0, 0), dtype=np.uint32)), @@ -270,6 +266,19 @@ class ManualGeometryBase(Device): displayedName="Tweak geometry", ) + @Slot( + displayedName="Send geometry", + allowedStates=[State.ACTIVE], + description="Will send 'signalNewGeometry' to connected slots. These will for " + "example be DetectorAssembler. Note that signal is sent automatically when new " + "geometry is set - this slot is mostly to be called by manager after " + "(re)starting assemblers while geometry device is still up." + ) + async def sendGeometry(self): + self.signalNewGeometry( + self.deviceId, geom_utils.serialize_geometry(self.geometry) + ) + @Slot( displayedName="Update preview", allowedStates=[State.ACTIVE], @@ -343,7 +352,7 @@ class ManualGeometryBase(Device): async def _set_geometry(self, geometry, update_preview=True): self.geometry = geometry - self.serializedGeometry = geom_utils.serialize_geometry(geometry) + await self.sendGeometry() if update_preview: await self.updatePreview() @@ -353,6 +362,8 @@ class ManualGeometryBase(Device): async def onInitialization(self): self.state = State.INIT + self.log.INFO("Waiting a second to let slots connect to signal") + await sleep(1) if self.geometryFile.filePath.value and await self._load_from_file(): ... else: @@ -537,7 +548,7 @@ class ManualQuadrantsGeometryBase(ManualGeometryBase): self.quadrantCorners.offset.y = 0 -class ModuleListItem(Configurable): +class OrientableModuleListItem(Configurable): posX = Double( assignment=Assignment.OPTIONAL, defaultValue=0, @@ -550,20 +561,32 @@ class ModuleListItem(Configurable): orientY = Int32(assignment=Assignment.OPTIONAL, defaultValue=1) -def make_manual_module_list_node(defaults): - class ManualModuleListNode(BaseManualGeometryConfigNode): +class RotatableModuleListItem(Configurable): + posX = Double( + assignment=Assignment.OPTIONAL, + defaultValue=0, + ) + posY = Double( + assignment=Assignment.OPTIONAL, + defaultValue=0, + ) + rotate = Int32(assignment=Assignment.OPTIONAL, defaultValue=1) + + +def make_manual_orientable_module_list_node(defaults): + class ManualOrientableModuleListNode(BaseManualGeometryConfigNode): modules = VectorHash( displayedName="Modules", - rows=ModuleListItem, + rows=OrientableModuleListItem, defaultValue=defaults, accessMode=AccessMode.RECONFIGURABLE, assignment=Assignment.OPTIONAL, ) - return Node(ManualModuleListNode) + return Node(ManualOrientableModuleListNode) -class ManualModuleListGeometryBase(ManualGeometryBase): +class ManualOrientableModuleListGeometryBase(ManualGeometryBase): moduleList = None # subclass must define (with nice defaults) @slot @@ -591,3 +614,46 @@ class ManualModuleListGeometryBase(ManualGeometryBase): ).offset((self.moduleList.offset.x.value, self.moduleList.offset.y.value)) await self._set_geometry(geometry) self.tweakGeometry._reset() + + +def make_manual_rotatable_module_list_node(defaults): + class ManualRotatableModuleListNode(BaseManualGeometryConfigNode): + modules = VectorHash( + displayedName="Modules", + rows=RotatableModuleListItem, + defaultValue=defaults, + accessMode=AccessMode.RECONFIGURABLE, + assignment=Assignment.OPTIONAL, + ) + + return Node(ManualRotatableModuleListNode) + + +class ManualRotatableModuleListGeometryBase(ManualGeometryBase): + moduleList = None # subclass must define (with nice defaults) + + @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, + self.getDeviceSchema(), + ) + payload = Hash("success", True, "name", name, "data", scene_data) + + return Hash("type", "deviceScene", "origin", self.deviceId, "payload", payload) + + async def _set_from_manual_config(self): + self._set_status("Updating geometry from manual configuration") + with self.push_state(State.CHANGING): + geometry = self.geometry_class.from_module_positions( + [(x, y) for (x, y, _) in self.moduleList.modules.value], + [ + rotation + for (_, _, rotation) in self.moduleList.modules.value + ], + ).offset((self.moduleList.offset.x.value, self.moduleList.offset.y.value)) + await self._set_geometry(geometry) + self.tweakGeometry._reset() diff --git a/src/calng/conditions/JungfrauCondition.py b/src/calng/conditions/JungfrauCondition.py index 2825a4d82978860f9514050ac805ba070931c040..30963793023deff7b8fa0fd01ada0f221992d36d 100644 --- a/src/calng/conditions/JungfrauCondition.py +++ b/src/calng/conditions/JungfrauCondition.py @@ -1,53 +1,29 @@ -import collections import operator from karabo.middlelayer import AccessMode, Assignment, String from .. import base_condition -from ..corrections.JungfrauCorrection import GainModes +from ..corrections.JungfrauCorrection import GainModes, GainSettings -def old_settings_to_gain_mode(setting): - gain_mode = GainModes(setting) - if gain_mode in (GainModes.FIX_GAIN_1, GainModes.FIX_GAIN_2): - return 1 +def gain_mode_translator(gain_mode_string): + if gain_mode_string in {"dynamic", "forceswitchg1", "forceswitchg2"}: + return GainModes.ADAPTIVE_GAIN.name + elif gain_mode_string in {"fixg1", "fixg2"}: + return GainModes.FIXED_GAIN.name else: - return 0 + raise ValueError(f"Unknown gain mode {gain_mode_string}") -def old_settings_to_gain_setting(setting): - gain_mode = GainModes(setting) - if gain_mode is GainModes.DYNAMIC_GAIN_HG0: - return 1 - else: - return 0 - - -def new_settings_to_gain_setting(setting): +def gain_setting_translator(setting): if setting == "gain0": - return 0 + return GainSettings.LOW_CDS.name elif setting == "highgain0": - return 1 + return GainSettings.HIGH_CDS.name else: raise ValueError(f"Unknown gain setting {setting}") -def new_gain_mode_to_gain_mode(gain_mode): - if gain_mode in {"dynamic", "forceswitchg1", "forceswitchg2"}: - return 0 - elif gain_mode in {"fixg1", "fixg2"}: - return 1 - else: - raise ValueError(f"Unknown gain_mode {gain_mode}") - - class JungfrauCondition(base_condition.ConditionBase): - detectorFirmwareVersion = String( - displayedName="Firmware version", - assignment=Assignment.OPTIONAL, - accessMode=AccessMode.INITONLY, - defaultValue="new", - options=["old", "new"], - ) controlDeviceId = String( displayedName="Control device ID", assignment=Assignment.MANDATORY, @@ -56,29 +32,17 @@ class JungfrauCondition(base_condition.ConditionBase): @property def keys_to_get(self): - key_map = [ - # cells: 1.0 or 16.0 - ("storageCells", "memoryCells", lambda n: n + 1), - ( - "exposureTime", - "integrationTime", - lambda n: n * 1e6, - ), - ] - if self.detectorFirmwareVersion.value == "old": - key_map += [ - # note: control device parameter is a vector - ("vHighVoltage", "biasVoltage", operator.itemgetter(0)), - # gain mode: omitted or 1.0 - ("settings", "gainMode", old_settings_to_gain_mode), - # gain setting: 0.0 or 1.0 (derived from gain mode on device) - ("settings", "gainSetting", old_settings_to_gain_setting), - ] - else: - key_map += [ + return { + self.controlDeviceId.value: [ + # cells: 1.0 or 16.0 + ("storageCells", "memoryCells", lambda n: n + 1), + ( + "exposureTime", + "integrationTime", + lambda n: n * 1e6, + ), ("highVoltage", "biasVoltage", operator.itemgetter(0)), - ("settings", "gainSetting", new_settings_to_gain_setting), - ("gainMode", "gainMode", new_gain_mode_to_gain_mode), + ("settings", "gainSetting", gain_setting_translator), + ("gainMode", "gainMode", gain_mode_translator), ] - - return {self.controlDeviceId.value: key_map} + } diff --git a/src/calng/corrections/JungfrauCorrection.py b/src/calng/corrections/JungfrauCorrection.py index bb40a97fc8661fe0b4d682e3ee0b2459dfe1204e..b7903d033143d77b4efc323789f23325ebbd774f 100644 --- a/src/calng/corrections/JungfrauCorrection.py +++ b/src/calng/corrections/JungfrauCorrection.py @@ -42,12 +42,13 @@ bad_pixel_constants = { # from pycalibration (TOOD: move to common shared lib) class GainModes(enum.Enum): - DYNAMIC_GAIN = "dynamicgain" - DYNAMIC_GAIN_HG0 = "dynamichg0" - FIX_GAIN_1 = "fixgain1" - FIX_GAIN_2 = "fixgain2" - FORCE_SWITCH_HG1 = "forceswitchg1" - FORCE_SWITCH_HG2 = "forceswitchg2" + ADAPTIVE_GAIN = 0 + FIXED_GAIN = 1 + + +class GainSettings(enum.Enum): + LOW_CDS = 0 + HIGH_CDS = 1 class CorrectionFlags(enum.IntFlag): @@ -380,7 +381,7 @@ class JungfrauCalcatFriend(base_calcat.BaseCalcatFriend): .reconfigurable() .commit(), - DOUBLE_ELEMENT(schema) + STRING_ELEMENT(schema) .key("constantParameters.gainMode") .displayedName("Gain mode") .description( @@ -390,16 +391,18 @@ class JungfrauCalcatFriend(base_calcat.BaseCalcatFriend): "for fixed gain (omitted otherwise)." ) .assignmentOptional() - .defaultValue(0) + .defaultValue(GainModes.ADAPTIVE_GAIN.name) + .options(",".join(gain_mode.name for gain_mode in GainModes)) .reconfigurable() .commit(), - DOUBLE_ELEMENT(schema) + STRING_ELEMENT(schema) .key("constantParameters.gainSetting") .displayedName("Gain setting") .description("See description of gainMode") .assignmentOptional() - .defaultValue(0) + .defaultValue(GainSettings.LOW_CDS.name) + .options(",".join(gain_setting.name for gain_setting in GainSettings)) .reconfigurable() .commit(), ) @@ -421,10 +424,13 @@ class JungfrauCalcatFriend(base_calcat.BaseCalcatFriend): res["Integration Time"] = self._get_param("integrationTime") res["Sensor Temperature"] = self._get_param("sensorTemperature") - if self._get_param("gainMode") != 0: - # NOTE: always include if CalCat is updated for this - res["Gain mode"] = self._get_param("gainMode") - res["Gain Setting"] = self._get_param("gainSetting") + if ( + gain_mode := GainModes[self._get_param("gainMode")] + ) is not GainModes.ADAPTIVE_GAIN: + # NOTE: currently only including if parameter for CalCat is 1 + # change if conditions are tidied up in the database + res["Gain mode"] = gain_mode.value + res["Gain Setting"] = GainSettings[self._get_param("gainSetting")].value return res diff --git a/src/calng/corrections/LpdCorrection.py b/src/calng/corrections/LpdCorrection.py index 913f85498b79a35da74a399a261e48a630cddf73..62a9e42f1637f1341514ce4df3aa21df157b91a3 100644 --- a/src/calng/corrections/LpdCorrection.py +++ b/src/calng/corrections/LpdCorrection.py @@ -48,7 +48,7 @@ class CorrectionFlags(enum.IntFlag): class LpdGpuRunner(base_kernel_runner.BaseKernelRunner): _gpu_based = True - _corrected_axis_order = "fxy" + _corrected_axis_order = "fyx" @property def input_shape(self): @@ -81,7 +81,7 @@ class LpdGpuRunner(base_kernel_runner.BaseKernelRunner): self.processed_data = cp.empty(self.processed_shape, dtype=output_data_dtype) self.cell_table = cp.empty(frames, dtype=np.uint16) - self.map_shape = (constant_memory_cells, pixels_x, pixels_y, 3) + self.map_shape = (constant_memory_cells, pixels_y, pixels_x, 3) self.offset_map = cp.empty(self.map_shape, dtype=np.float32) self.gain_amp_map = cp.empty(self.map_shape, dtype=np.float32) self.rel_gain_slopes_map = cp.empty(self.map_shape, dtype=np.float32) @@ -153,7 +153,7 @@ class LpdGpuRunner(base_kernel_runner.BaseKernelRunner): Constants.Offset: ((2, 1, 0, 3), self.offset_map), Constants.GainAmpMap: ((2, 0, 1, 3), self.gain_amp_map), Constants.FFMap: ((2, 0, 1, 3), self.flatfield_map), - Constants.RelativeGain: ((2, 1, 0, 3), self.rel_gain_slopes_map), + Constants.RelativeGain: ((2, 0, 1, 3), self.rel_gain_slopes_map), } if constant_type in bad_pixel_loading: self.bad_pixel_map |= self._xp.asarray( @@ -396,8 +396,8 @@ class LpdCorrection(BaseCorrection): return ( self.unsafe_get("dataFormat.frames"), 1, - self.unsafe_get("dataFormat.pixelsX"), self.unsafe_get("dataFormat.pixelsY"), + self.unsafe_get("dataFormat.pixelsX"), ) def __init__(self, config): diff --git a/src/calng/corrections/LpdminiCorrection.py b/src/calng/corrections/LpdminiCorrection.py new file mode 100644 index 0000000000000000000000000000000000000000..688b174b23474f35346014e91bd3b9074879fb4a --- /dev/null +++ b/src/calng/corrections/LpdminiCorrection.py @@ -0,0 +1,103 @@ +import numpy as np +from karabo.bound import ( + DOUBLE_ELEMENT, + KARABO_CLASSINFO, + OVERWRITE_ELEMENT, +) + +from .._version import version as deviceVersion +from ..base_correction import add_correction_step_schema +from . import LpdCorrection + + +class LpdminiGpuRunner(LpdCorrection.LpdGpuRunner): + def load_constant(self, constant_type, constant_data): + print(f"Given: {constant_type} with shape {constant_data.shape}") + # constant type → transpose order + constant_buffer_map = { + LpdCorrection.Constants.Offset: self.offset_map, + LpdCorrection.Constants.GainAmpMap: self.gain_amp_map, + LpdCorrection.Constants.FFMap: self.flatfield_map, + LpdCorrection.Constants.RelativeGain: self.rel_gain_slopes_map, + } + if constant_type in { + LpdCorrection.Constants.BadPixelsDark, + LpdCorrection.Constants.BadPixelsFF, + }: + self.bad_pixel_map |= self._xp.asarray( + constant_data, + dtype=np.uint32, + )[: self.constant_memory_cells] + else: + constant_buffer_map[constant_type].set( + constant_data.astype(np.float32)[: self.constant_memory_cells] + ) + + +class LpdminiCalcatFriend(LpdCorrection.LpdCalcatFriend): + @staticmethod + def add_schema( + schema, + managed_keys, + ): + super(LpdminiCalcatFriend, LpdminiCalcatFriend).add_schema(schema, managed_keys) + ( + OVERWRITE_ELEMENT(schema) + .key("constantParameters.biasVoltage") + .setNewDisplayedName("Bias voltage (odd)") + .setNewDescription("Bias voltage apbplied to minis 1, 3, 5, and 7.") + .commit(), + + DOUBLE_ELEMENT(schema) + .key("constantParameters.biasVoltage2") + .displayedName("Bias voltage (even)") + .description("Separate bias voltage used for minis 2, 4, 6, and 8.") + .assignmentOptional() + .defaultValue(300) + .reconfigurable() + .commit(), + + OVERWRITE_ELEMENT(schema) + .key("constantParameters.pixelsY") + .setNewDefaultValue(32) + .commit(), + ) + managed_keys.add("constantParameters.biasVoltage2") + + def basic_condition(self): + res = super().basic_condition() + if int(self.device.get("fastSources")[0][-1]) % 2 == 0: + res["Sensor Bias Voltage"] = self._get_param("biasVoltage2") + if "category" in res: + del res["category"] + return res + + +@KARABO_CLASSINFO("LpdminiCorrection", deviceVersion) +class LpdminiCorrection(LpdCorrection.LpdCorrection): + _calcat_friend_class = LpdminiCalcatFriend + _kernel_runner_class = LpdminiGpuRunner + _managed_keys = LpdCorrection.LpdCorrection._managed_keys.copy() + + @classmethod + def expectedParameters(cls, expected): + ( + OVERWRITE_ELEMENT(expected) + .key("dataFormat.pixelsY") + .setNewDefaultValue(32) + .commit(), + + OVERWRITE_ELEMENT(expected) + .key("dataFormat.frames") + .setNewDefaultValue(512) + .commit(), + ) + cls._calcat_friend_class.add_schema(expected, cls._managed_keys) + # warning: this is redundant, but needed for now to get managed keys working + add_correction_step_schema(expected, cls._managed_keys, cls._correction_steps) + ( + OVERWRITE_ELEMENT(expected) + .key("managedKeys") + .setNewDefaultValue(list(cls._managed_keys)) + .commit() + ) diff --git a/src/calng/geometries/Agipd500KGeometry.py b/src/calng/geometries/Agipd500KGeometry.py new file mode 100644 index 0000000000000000000000000000000000000000..a2e9580050dc4e80d97d892018561fd9aa3d4242 --- /dev/null +++ b/src/calng/geometries/Agipd500KGeometry.py @@ -0,0 +1,8 @@ +import extra_geom + +from ..base_geometry import ManualOriginGeometryBase, make_origin_node + + +class Agipd500KGeometry(ManualOriginGeometryBase): + geometry_class = extra_geom.AGIPD_500K2GGeometry + origin = make_origin_node(0, 0) diff --git a/src/calng/geometries/JungfrauGeometry.py b/src/calng/geometries/JungfrauGeometry.py index c447accc7dec7857a5c858e3cfac62e001d2af29..e0c0703cb394a9b3f51fa2f163a62cc119c6a15a 100644 --- a/src/calng/geometries/JungfrauGeometry.py +++ b/src/calng/geometries/JungfrauGeometry.py @@ -2,12 +2,15 @@ import extra_geom import numpy as np from karabo.middlelayer import Hash -from ..base_geometry import ManualModuleListGeometryBase, make_manual_module_list_node +from ..base_geometry import ( + ManualOrientableModuleListGeometryBase, + make_manual_orientable_module_list_node, +) -class JungfrauGeometry(ManualModuleListGeometryBase): +class JungfrauGeometry(ManualOrientableModuleListGeometryBase): geometry_class = extra_geom.JUNGFRAUGeometry - moduleList = make_manual_module_list_node( + moduleList = make_manual_orientable_module_list_node( [ Hash("posX", x, "posY", y, "orientX", ox, "orientY", oy) for (x, y, ox, oy) in [ diff --git a/src/calng/geometries/LpdminiGeometry.py b/src/calng/geometries/LpdminiGeometry.py new file mode 100644 index 0000000000000000000000000000000000000000..31846b953ccc949564686e221b631bf0bd8c866e --- /dev/null +++ b/src/calng/geometries/LpdminiGeometry.py @@ -0,0 +1,24 @@ +import extra_geom +from karabo.middlelayer import Hash + +from ..base_geometry import ( + ManualRotatableModuleListGeometryBase, + make_manual_rotatable_module_list_node, +) + + +class LpdminiGeometry(ManualRotatableModuleListGeometryBase): + geometry_class = extra_geom.LPD_MiniGeometry + + moduleList = make_manual_rotatable_module_list_node( + [ + Hash("posX", x, "posY", y, "rotate", r) + for (x, y, r) in [ + (0, 0, 0), + # TODO: appropriate defaults + ] + ] + ) + + def _update_manual_from_current(self): + raise NotImplementedError() diff --git a/src/calng/geometries/__init__.py b/src/calng/geometries/__init__.py index 8705bb0c5edf88aa6ffb570c71c4f54a4b059a6b..a2864497a2d9ee83b1f5b64536b2f89f9d8b814c 100644 --- a/src/calng/geometries/__init__.py +++ b/src/calng/geometries/__init__.py @@ -3,6 +3,8 @@ from . import ( Agipd1MGeometry, Dssc1MGeometry, Epix100Geometry, + Lpd1MGeometry, + LpdminiGeometry, JungfrauGeometry, Lpd1MGeometry, PnccdGeometry, diff --git a/src/calng/kernels/lpd_cpu.pyx b/src/calng/kernels/lpd_cpu.pyx new file mode 100644 index 0000000000000000000000000000000000000000..0c3eb82d50c3e23d1414515fd967347ffe4a0876 --- /dev/null +++ b/src/calng/kernels/lpd_cpu.pyx @@ -0,0 +1,55 @@ +# cython: boundscheck=False +# cython: cdivision=True +# cython: wrapararound=False + +cdef unsigned char NONE = 0 +cdef unsigned char OFFSET = 1 +cdef unsigned char GAIN_AMP = 2 +cdef unsigned char REL_GAIN = 4 +cdef unsigned char FF_CORR = 8 +cdef unsigned char BPMASK = 16 + +from cython.parallel import prange +from libc.math cimport isinf, isnan + +def correct( + unsigned short[:, :, :, :] image_data, + unsigned short[:] cell_table, + unsigned char flags, + float[:, :, :, :] offset_map, + float[:, :, :, :] gain_amp_map, + float[:, :, :, :] rel_gain_slopes_map, + float[:, :, :, :] flatfield_map, + unsigned[:, :, :, :] bad_pixel_map, + float bad_pixel_mask_value, + # TODO: support spitting out gain map for preview purposes + float[:, :, :] output +): + cdef int frame, map_cell, ss, fs + cdef unsigned char gain_stage + cdef float res + cdef unsigned short raw_data_value + for frame in prange(image_data.shape[0], nogil=True): + map_cell = cell_table[frame] + if map_cell >= offset_map.shape[0]: + for ss in range(image_data.shape[1]): + for fs in range(image_data.shape[2]): + output[frame, ss, fs] = <float>image_data[frame, 0, ss, fs] + continue + for ss in range(image_data.shape[1]): + for fs in range(image_data.shape[3]): + raw_data_value = image_data[frame, 0, ss, fs] + gain_stage = (raw_data_value >> 12) & 0x0003 + res = <float>(raw_data_value & 0x0fff) + if gain_stage > 2 or (flags & BPMASK and bad_pixel_map[map_cell, ss, fs, gain_stage] != 0): + res = bad_pixel_mask_value + else: + if flags & OFFSET: + res = res - offset_map[map_cell, ss, fs, gain_stage] + if flags & GAIN_AMP: + res = res * gain_amp_map[map_cell, ss, fs, gain_stage] + if flags & FF_CORR: + res = res * rel_gain_slopes_map[map_cell, ss, fs, gain_stage] + if res < 1e-7 or res > 1e7 or isnan(res) or isinf(res): + res = bad_pixel_mask_value + output[frame, ss, fs] = res diff --git a/src/calng/scenes.py b/src/calng/scenes.py index 01a97467908c839f81b36fff7aa1d6ba35079497..e48ae88b1d4598d58110e30666f02f3eca8b450c 100644 --- a/src/calng/scenes.py +++ b/src/calng/scenes.py @@ -9,6 +9,7 @@ from karabo.common.scenemodel.api import ( DetectorGraphModel, DisplayCommandModel, DisplayLabelModel, + DisplayListModel, DisplayStateColorModel, DisplayTextLogModel, DoubleLineEditModel, @@ -46,7 +47,13 @@ def DisplayRoundedFloat(*args, decimals=2, **kwargs): return EvaluatorModel(*args, expression=f"f'{{x:.{decimals}f}}'", **kwargs) -_type_to_display_model = {"BOOL": CheckBoxModel, "FLOAT": DisplayRoundedFloat} +_type_to_display_model = { + "BOOL": CheckBoxModel, + "FLOAT": DisplayRoundedFloat, + "STRING": DisplayLabelModel, + "UINT32": DisplayLabelModel, + "VECTOR_UINT32": DisplayListModel, +} _type_to_line_editable = { "BOOL": (CheckBoxModel, {"klass": "EditableCheckBox"}), "DOUBLE": (DoubleLineEditModel, {}), @@ -301,8 +308,13 @@ class DisplayAndEditableRow(HorizontalLayout): ) if self.include_display(key_attr): + if value_type in _type_to_display_model: + model = _type_to_display_model[value_type] + else: + model = DisplayLabelModel + print(f"Scene generator would like to know more about {value_type}") self.children.append( - _type_to_display_model.get(value_type, DisplayLabelModel)( + model( keys=[f"{device_id}.{key_path}"], width=display_width * size_scale, height=height, @@ -551,7 +563,7 @@ class ManagerDeviceStatus(VerticalLayout): @titled("Device status", width=6 * NARROW_INC) @boxed -class CorrectionDeviceStatus(VerticalLayout): +class ProcessingDeviceStatus(VerticalLayout): def __init__(self, device_id, schema_hash): super().__init__(padding=0) name = DisplayLabelModel( @@ -600,28 +612,29 @@ class CorrectionDeviceStatus(VerticalLayout): ), ] ) - self.children.append( - VerticalLayout( - children=[ - HorizontalLayout( - LampModel( - keys=[f"{device_id}.{warning_lamp}"], - width=BASE_INC, - height=BASE_INC, - ), - LabelModel( - text=warning_lamp, - width=8 * BASE_INC, - height=BASE_INC, - ), - ) - for warning_lamp in schema_hash.getAttribute( - "warningLamps", "defaultValue" - ) - ], - padding=0, + if schema_hash.has("warningLamps"): + self.children.append( + VerticalLayout( + children=[ + HorizontalLayout( + LampModel( + keys=[f"{device_id}.{warning_lamp}"], + width=BASE_INC, + height=BASE_INC, + ), + LabelModel( + text=warning_lamp, + width=8 * BASE_INC, + height=BASE_INC, + ), + ) + for warning_lamp in schema_hash.getAttribute( + "warningLamps", "defaultValue" + ) + ], + padding=0, + ) ) - ) self.children.append(status_log) @@ -1309,7 +1322,7 @@ def scene_generator(fun): def correction_device_overview(device_id, schema): schema_hash = schema_to_hash(schema) main_overview = HorizontalLayout( - CorrectionDeviceStatus(device_id, schema_hash), + ProcessingDeviceStatus(device_id, schema_hash), VerticalLayout( recursive_editable( device_id, @@ -1355,6 +1368,15 @@ def correction_device_overview(device_id, schema): ) +@scene_generator +def lpdmini_splitter_overview(device_id, schema): + schema_hash = schema_to_hash(schema) + return VerticalLayout( + ProcessingDeviceStatus(device_id, schema_hash), + DisplayRow(device_id, schema_hash, "outputDataShape", 6, 8), + ) + + @scene_generator def correction_device_preview(device_id, schema, preview_channel): schema_hash = schema_to_hash(schema) @@ -1519,6 +1541,39 @@ def manager_device_overview( mds_hash = schema_to_hash(manager_device_schema) cds_hash = schema_to_hash(correction_device_schema) + data_throttling_children = [ + LabelModel( + text="Frame filter", + width=11 * BASE_INC, + height=BASE_INC, + ), + EditableRow( + manager_device_id, + mds_hash, + "managedKeys.frameFilter.type", + 7, + 4, + ), + EditableRow( + manager_device_id, + mds_hash, + "managedKeys.frameFilter.spec", + 7, + 4, + ), + ] + + if "managedKeys.daqTrainStride" in mds_hash: + # Only add DAQ train stride if present on the schema, may be + # disabled on the manager. + data_throttling_children.insert(0, EditableRow( + manager_device_id, + mds_hash, + "managedKeys.daqTrainStride", + 7, + 4, + )) + return VerticalLayout( HorizontalLayout( ManagerDeviceStatus(manager_device_id), @@ -1540,34 +1595,7 @@ def manager_device_overview( max_depth=2, ), titled("Data throttling")(boxed(VerticalLayout))( - children=[ - EditableRow( - manager_device_id, - mds_hash, - "managedKeys.daqTrainStride", - 7, - 4, - ), - LabelModel( - text="Frame filter", - width=11 * BASE_INC, - height=BASE_INC, - ), - EditableRow( - manager_device_id, - mds_hash, - "managedKeys.frameFilter.type", - 7, - 4, - ), - EditableRow( - manager_device_id, - mds_hash, - "managedKeys.frameFilter.spec", - 7, - 4, - ), - ], + children=data_throttling_children, padding=0, ), ), diff --git a/src/calng/utils.py b/src/calng/utils.py index 48e98452261ab7c4eec8a5057a0490ebfbd77740..61e632a98c0eb3109c62f228ddeb68f4365bc685 100644 --- a/src/calng/utils.py +++ b/src/calng/utils.py @@ -78,13 +78,6 @@ class PreviewIndexSelectionMode(enum.Enum): PULSE = "pulse" -def rec_getattr(obj, path): - res = obj - for part in path.split("."): - res = getattr(res, part) - return res - - def pick_frame_index(selection_mode, index, cell_table, pulse_table): """When selecting a single frame to preview, an obvious question is whether the number the operator provides is a frame index, a cell ID, or a pulse ID. This