From a150689f6cddaa2027a14f364280d55263b847cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Mon, 10 Nov 2025 20:25:42 +0100 Subject: [PATCH 01/18] Add a crude implementation to control beamshift with RMB (for FEI cRED) --- src/instamatic/gui/base_module.py | 1 + src/instamatic/gui/cred_frame.py | 3 ++- src/instamatic/gui/ctrl_frame.py | 36 +++++++++++++++++++++++++++++++ src/instamatic/gui/gui.py | 2 ++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/instamatic/gui/base_module.py b/src/instamatic/gui/base_module.py index 29f97f01..cadbf6e0 100644 --- a/src/instamatic/gui/base_module.py +++ b/src/instamatic/gui/base_module.py @@ -39,3 +39,4 @@ class HasQMixin: """Asserts module.q remains reserved for DataCollectionController.q.""" q: Queue + app: None diff --git a/src/instamatic/gui/cred_frame.py b/src/instamatic/gui/cred_frame.py index 65748ae9..0ed94612 100644 --- a/src/instamatic/gui/cred_frame.py +++ b/src/instamatic/gui/cred_frame.py @@ -8,7 +8,7 @@ from .base_module import BaseModule, HasQMixin -ENABLE_FOOTFREE_OPTION = False +ENABLE_FOOTFREE_OPTION = True class ExperimentalcRED(LabelFrame, HasQMixin): @@ -175,6 +175,7 @@ def init_vars(self): self.var_exposure_time_image = DoubleVar(value=0.01) self.var_footfree_rotate_to = DoubleVar(value=65.0) + self.var_footfree_rotate_speed = DoubleVar(value=0.0) self.var_toggle_footfree = BooleanVar(value=False) self.mode = 'regular' diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index 139fea0d..094f1c85 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -8,6 +8,9 @@ from typing import Dict from instamatic import config +from instamatic.calibrate import CalibBeamShift +from instamatic.calibrate.filenames import CALIB_BEAMSHIFT +from instamatic.gui.click_dispatcher import ClickEvent, MouseButton from instamatic.utils.spinbox import Spinbox from .base_module import BaseModule, HasQMixin @@ -44,6 +47,9 @@ def __init__(self, parent): self.o_mode = OptionMenu(frame, self.var_mode, modes[0], *modes, command=self.set_mode) self.o_mode.grid(row=8, column=1, sticky='EW') + self.rmb_beam = Checkbutton(frame, text='Move beam w/ RMB', variable=self.var_rmb_beam) + self.rmb_beam.grid(row=8, column=3) + frame.pack(side='top', fill='x', padx=10, pady=10) frame = Frame(self) @@ -212,6 +218,8 @@ def init_vars(self): self.var_diff_defocus_on = BooleanVar(value=False) self.var_stage_wait = BooleanVar(value=True) + self.var_rmb_beam = BooleanVar(value=False) + self.var_rmb_beam.trace_add('write', self.toggle_rmb_beam) def set_mode(self, event=None): self.ctrl.mode.set(self.var_mode.get()) @@ -301,6 +309,34 @@ def toggle_alpha_wobbler(self): if self.wobble_stop_event: self.wobble_stop_event.set() + def toggle_rmb_beam(self, _name, _index, _mode) -> None: + """If self.var_rmb_beam, move beam using Right Mouse Button.""" + + try: + if not hasattr(self, 'calib_beamshift'): + path = self.app.get_module('io').get_experiment_directory().parent / 'calib' + print(path / CALIB_BEAMSHIFT) + self.calib_beamshift = CalibBeamShift.from_file(path / CALIB_BEAMSHIFT) + print(self.calib_beamshift) + except OSError: + print('No calibration :(') + # self.var_rmb_beam.set(False) + return + + def _cb(click: ClickEvent) -> None: + if click.button == MouseButton.RIGHT: + bs = self.calib_beamshift.pixelcoord_to_beamshift((click.y, click.x)) + self.ctrl.beamshift.set(*bs) + print(f'Beam shift at {self.ctrl.beamshift.get()}') + + if self.var_rmb_beam.get(): + d = self.app.get_module('stream').click_dispatcher + n = 'rmb_beam' + self.click_listener = c if (c := d.listeners.get(n)) else d.add_listener(n, _cb) + self.click_listener.active = True + else: + self.click_listener.active = False + def stage_stop(self): self.q.put(('ctrl', {'task': 'stage.stop'})) diff --git a/src/instamatic/gui/gui.py b/src/instamatic/gui/gui.py index 3b7a4ccf..c5790504 100644 --- a/src/instamatic/gui/gui.py +++ b/src/instamatic/gui/gui.py @@ -43,6 +43,8 @@ def __init__(self, ctrl=None, stream=None, beam_ctrl=None, app=None, log=None): for module in self.app.modules.values(): if 'q' in get_type_hints(module.__class__): module.q = getattr(module, 'q', self.q) + if 'app' in get_type_hints(module.__class__): + module.app = getattr(module, 'app', self.app) self.exitEvent = threading.Event() atexit.register(self.exitEvent.set) From 24a369a7949f3efbeecafd501ece6a137e316834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Tue, 11 Nov 2025 12:49:05 +0100 Subject: [PATCH 02/18] Add a crude implementation to control stage opsition with LMB --- src/instamatic/gui/ctrl_frame.py | 50 +++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index 094f1c85..7f903e9f 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -2,10 +2,10 @@ import queue import threading -from threading import Event from tkinter import * from tkinter.ttk import * -from typing import Dict + +import numpy as np from instamatic import config from instamatic.calibrate import CalibBeamShift @@ -47,9 +47,6 @@ def __init__(self, parent): self.o_mode = OptionMenu(frame, self.var_mode, modes[0], *modes, command=self.set_mode) self.o_mode.grid(row=8, column=1, sticky='EW') - self.rmb_beam = Checkbutton(frame, text='Move beam w/ RMB', variable=self.var_rmb_beam) - self.rmb_beam.grid(row=8, column=3) - frame.pack(side='top', fill='x', padx=10, pady=10) frame = Frame(self) @@ -80,6 +77,14 @@ def __init__(self, parent): ) b_wobble.grid(row=4, column=2, sticky='W', columnspan=2) + text = 'Move stage with LMB' + self.lmb_stage = Checkbutton(frame, text=text, variable=self.var_lmb_stage) + self.lmb_stage.grid(row=1, column=3) + + text = 'Move beam with RMB' + self.rmb_beam = Checkbutton(frame, text=text, variable=self.var_rmb_beam) + self.rmb_beam.grid(row=2, column=3) + e_stage_x = Spinbox(frame, textvariable=self.var_stage_x, **stage) e_stage_x.grid(row=6, column=1, sticky='EW') e_stage_y = Spinbox(frame, textvariable=self.var_stage_y, **stage) @@ -220,6 +225,8 @@ def init_vars(self): self.var_stage_wait = BooleanVar(value=True) self.var_rmb_beam = BooleanVar(value=False) self.var_rmb_beam.trace_add('write', self.toggle_rmb_beam) + self.var_lmb_stage = BooleanVar(value=False) + self.var_lmb_stage.trace_add('write', self.toggle_lmb_stage) def set_mode(self, event=None): self.ctrl.mode.set(self.var_mode.get()) @@ -309,6 +316,31 @@ def toggle_alpha_wobbler(self): if self.wobble_stop_event: self.wobble_stop_event.set() + def toggle_lmb_stage(self, _name, _index, _mode): + """If self.var_lmb_stage, move stage using Left Mouse Button.""" + + try: + stage_matrix = self.ctrl.get_stagematrix() + except KeyError: + print('No calibration :<') + # self.var_lmb_stage.set(False) + return + + def _cb(click: ClickEvent) -> None: + if click.button == MouseButton.LEFT: + cam_dim_x, cam_dim_y = self.ctrl.cam.get_camera_dimensions() + pixel_delta = np.array([click.y - cam_dim_y / 2, click.x - cam_dim_x / 2]) + stage_shift = np.dot(pixel_delta, stage_matrix) + stage_x, stage_y = self.ctrl.stage.xy + self.ctrl.stage.set(x=stage_x + stage_shift[0], y=stage_y + stage_shift[1]) + print(f'Stage position at {self.ctrl.stage.get()}') + + if checked := self.var_lmb_stage.get(): + d = self.app.get_module('stream').click_dispatcher + n = 'lmb_stage' + self.lmb_stage_cl = c if (c := d.listeners.get(n)) else d.add_listener(n, _cb) + self.lmb_stage_cl.active = checked + def toggle_rmb_beam(self, _name, _index, _mode) -> None: """If self.var_rmb_beam, move beam using Right Mouse Button.""" @@ -329,13 +361,11 @@ def _cb(click: ClickEvent) -> None: self.ctrl.beamshift.set(*bs) print(f'Beam shift at {self.ctrl.beamshift.get()}') - if self.var_rmb_beam.get(): + if checked := self.var_rmb_beam.get(): d = self.app.get_module('stream').click_dispatcher n = 'rmb_beam' - self.click_listener = c if (c := d.listeners.get(n)) else d.add_listener(n, _cb) - self.click_listener.active = True - else: - self.click_listener.active = False + self.rmb_beam_cl = c if (c := d.listeners.get(n)) else d.add_listener(n, _cb) + self.rmb_beam_cl.active = checked def stage_stop(self): self.q.put(('ctrl', {'task': 'stage.stop'})) From 94fd7fbce6debe16d061e9b6a83c5c3578d2baa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Tue, 11 Nov 2025 13:07:24 +0100 Subject: [PATCH 03/18] Allow rotation speed control even if goniotool is not available --- src/instamatic/gui/ctrl_frame.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index 7f903e9f..3f505a59d 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -10,6 +10,7 @@ from instamatic import config from instamatic.calibrate import CalibBeamShift from instamatic.calibrate.filenames import CALIB_BEAMSHIFT +from instamatic.exceptions import TEMCommunicationError from instamatic.gui.click_dispatcher import ClickEvent, MouseButton from instamatic.utils.spinbox import Spinbox @@ -79,11 +80,11 @@ def __init__(self, parent): text = 'Move stage with LMB' self.lmb_stage = Checkbutton(frame, text=text, variable=self.var_lmb_stage) - self.lmb_stage.grid(row=1, column=3) + self.lmb_stage.grid(row=1, column=3, columnspan=3, sticky='W') text = 'Move beam with RMB' self.rmb_beam = Checkbutton(frame, text=text, variable=self.var_rmb_beam) - self.rmb_beam.grid(row=2, column=3) + self.rmb_beam.grid(row=2, column=3, columnspan=3, sticky='W') e_stage_x = Spinbox(frame, textvariable=self.var_stage_x, **stage) e_stage_x.grid(row=6, column=1, sticky='EW') @@ -92,14 +93,14 @@ def __init__(self, parent): e_stage_z = Spinbox(frame, textvariable=self.var_stage_z, **stage) e_stage_z.grid(row=6, column=3, sticky='EW') + Label(frame, text='Rotation Speed', width=20).grid(row=5, column=0, sticky='W') + e_goniotool_tx = Spinbox( + frame, width=10, textvariable=self.var_goniotool_tx, from_=1, to=12, increment=1 + ) + e_goniotool_tx.grid(row=5, column=1, sticky='EW') + b_goniotool_set = Button(frame, text='Set', command=self.set_goniotool_tx) + b_goniotool_set.grid(row=5, column=2, sticky='EW') if config.settings.use_goniotool: - Label(frame, text='Rot. Speed', width=20).grid(row=5, column=0, sticky='W') - e_goniotool_tx = Spinbox( - frame, width=10, textvariable=self.var_goniotool_tx, from_=1, to=12, increment=1 - ) - e_goniotool_tx.grid(row=5, column=1, sticky='EW') - b_goniotool_set = Button(frame, text='Set', command=self.set_goniotool_tx) - b_goniotool_set.grid(row=5, column=2, sticky='W') b_goniotool_default = Button( frame, text='Default', command=self.set_goniotool_tx_default ) @@ -272,7 +273,12 @@ def set_positive_angle(self): def set_goniotool_tx(self, event=None, value=None): if not value: value = self.var_goniotool_tx.get() - self.ctrl.stage.set_rotation_speed(value) + try: + self.ctrl.stage.set_rotation_speed(value) + except AttributeError: + print('This TEM does not implement `setRotationSpeed` method') + except TEMCommunicationError: + print('Could not connect to the stage rotation speed controller') def set_goniotool_tx_default(self, event=None): value = 12 From 5f5fac3e522063b0a9093df7fb79a84b80890f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Thu, 20 Nov 2025 15:47:19 +0100 Subject: [PATCH 04/18] Add more details, otherwise more difficult to reach, to pets input --- docs/config.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 7de6f575..712e6145 100644 --- a/docs/config.md +++ b/docs/config.md @@ -258,7 +258,17 @@ This file holds the specifications of the camera. This file is must be located t **pets_prefix** : Arbitrary information to be added at the beginning of the `.pts` file created after an experiment. The prefix can include any [valid PETS2 input lines](http://pets.fzu.cz/download/pets2_manual.pdf). In the case of duplicate commands, prefix lines take precedence over hard-coded and suffix commands, and prevent the latter ones from being added. Additionally, this field can contain new python-style [replacement fields](https://pyformat.info/) which, if present among the `ImgConversion` instance attributes, will be filled automatically after each experiment (see the `pets_suffix` example). A typical `pets_prefix`, capable of overwriting the default detector specification output can look like this: ```yaml -pets_prefix: "noiseparameters 4.2 0\nreflectionsize 8\ndetector asi" +pets_prefix: | + # Measurement conditions: + # Start angle: {start_angle:6.2f} deg + # Stop angle: {stop_angle:6.2f} deg + # Step size: {osc_angle:6.2f} deg + # Exposure: {acquisition_time:6.3f} s + # Wave length: {wavelength} + # Camera distance: {distance} + noiseparameters 4.2 0 + reflectionsize 8 + detector asi ``` **pets_suffix** From 0ffc01f116c6101a1c702118a22e5c874b2202ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Thu, 20 Nov 2025 16:05:56 +0100 Subject: [PATCH 05/18] Add some more examples to the pets prefix --- docs/config.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/config.md b/docs/config.md index 712e6145..2996b5dd 100644 --- a/docs/config.md +++ b/docs/config.md @@ -256,16 +256,18 @@ This file holds the specifications of the camera. This file is must be located t ``` **pets_prefix** -: Arbitrary information to be added at the beginning of the `.pts` file created after an experiment. The prefix can include any [valid PETS2 input lines](http://pets.fzu.cz/download/pets2_manual.pdf). In the case of duplicate commands, prefix lines take precedence over hard-coded and suffix commands, and prevent the latter ones from being added. Additionally, this field can contain new python-style [replacement fields](https://pyformat.info/) which, if present among the `ImgConversion` instance attributes, will be filled automatically after each experiment (see the `pets_suffix` example). A typical `pets_prefix`, capable of overwriting the default detector specification output can look like this: +: Arbitrary information to be added at the beginning of the `.pts` file created after an experiment. The prefix can include any [valid PETS2 input lines](http://pets.fzu.cz/download/pets2_manual.pdf). In the case of duplicate commands, prefix lines take precedence over hard-coded and suffix commands, and prevent the latter ones from being added. Additionally, this field can contain new python-style [replacement fields](https://pyformat.info/) which, if present among the `ImgConversion` instance attributes, will be filled automatically after each experiment. A typical `pets_prefix`, capable of overwriting the default detector specification output can look like this: ```yaml pets_prefix: | - # Measurement conditions: + + # MEASUREMENT CONDITIONS: # Start angle: {start_angle:6.2f} deg - # Stop angle: {stop_angle:6.2f} deg + # End angle: {end_angle:6.2f} deg # Step size: {osc_angle:6.2f} deg # Exposure: {acquisition_time:6.3f} s - # Wave length: {wavelength} - # Camera distance: {distance} + # Wave length: {wavelength:15.8g} A + # Camera distance: {distance:15.8g} mm + noiseparameters 4.2 0 reflectionsize 8 detector asi From a73ed8fd1390ba5296e95ae5ce4baf016ecb67b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 21 Nov 2025 15:53:44 +0100 Subject: [PATCH 06/18] Videoframe instance needs no app kwarg anymore since it is class attribute now --- src/instamatic/gui/gui.py | 4 ++-- src/instamatic/gui/videostream_frame.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/instamatic/gui/gui.py b/src/instamatic/gui/gui.py index c5790504..40d19489 100644 --- a/src/instamatic/gui/gui.py +++ b/src/instamatic/gui/gui.py @@ -138,11 +138,11 @@ def __init__(self, root, cam, modules: list = []): self.app = AppLoader() # the stream window is a special case, because it needs access - # to the cam module and the AppLoader itself + # to the cam module if cam: from .videostream_frame import module as stream_module - stream_module.set_kwargs(stream=cam, app=self.app) + stream_module.set_kwargs(stream=cam) modules.insert(0, stream_module) self.module_frame = Frame(root) diff --git a/src/instamatic/gui/videostream_frame.py b/src/instamatic/gui/videostream_frame.py index b4b4f45a..6e427225 100644 --- a/src/instamatic/gui/videostream_frame.py +++ b/src/instamatic/gui/videostream_frame.py @@ -26,13 +26,12 @@ class VideoStreamFrame(LabelFrame, HasQMixin): """GUI panel to continuously display the last frame streamed from the camera.""" - def __init__(self, parent, stream, app=None): + def __init__(self, parent, stream): LabelFrame.__init__(self, parent, text='Stream') self.parent = parent self.stream = stream - self.app = app self.panel = None From 0b94d82abd0da3d5dfc8b67dbe6a5265c0eca5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 21 Nov 2025 16:01:51 +0100 Subject: [PATCH 07/18] Raname `HasQMixin` to `ModuleFrameMixin` to generalize it better --- src/instamatic/gui/autocred_frame.py | 4 ++-- src/instamatic/gui/base_module.py | 4 ++-- src/instamatic/gui/cred_fei_frame.py | 4 ++-- src/instamatic/gui/cred_frame.py | 4 ++-- src/instamatic/gui/cred_tvips_frame.py | 4 ++-- src/instamatic/gui/ctrl_frame.py | 4 ++-- src/instamatic/gui/debug_frame.py | 4 ++-- src/instamatic/gui/fast_adt_frame.py | 4 ++-- src/instamatic/gui/machine_learning_frame.py | 4 ++-- src/instamatic/gui/red_frame.py | 4 ++-- src/instamatic/gui/sed_frame.py | 4 ++-- src/instamatic/gui/videostream_frame.py | 4 ++-- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/instamatic/gui/autocred_frame.py b/src/instamatic/gui/autocred_frame.py index fecaa50b..718775a2 100644 --- a/src/instamatic/gui/autocred_frame.py +++ b/src/instamatic/gui/autocred_frame.py @@ -13,10 +13,10 @@ from instamatic.calibrate import CalibBeamShift from instamatic.calibrate.filenames import * -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -class ExperimentalautocRED(LabelFrame, HasQMixin): +class ExperimentalautocRED(LabelFrame, ModuleFrameMixin): """Data collection protocol for SerialRED data collection on a high-speed Timepix camera using automated screening and crystal tracking. diff --git a/src/instamatic/gui/base_module.py b/src/instamatic/gui/base_module.py index cadbf6e0..aaa6a281 100644 --- a/src/instamatic/gui/base_module.py +++ b/src/instamatic/gui/base_module.py @@ -35,8 +35,8 @@ def initialize(self, parent): return frame -class HasQMixin: - """Asserts module.q remains reserved for DataCollectionController.q.""" +class ModuleFrameMixin: + """Asserts some class attributes i.e. module.q, app remain reserved.""" q: Queue app: None diff --git a/src/instamatic/gui/cred_fei_frame.py b/src/instamatic/gui/cred_fei_frame.py index d0fd8e9d..6fef0a23 100644 --- a/src/instamatic/gui/cred_fei_frame.py +++ b/src/instamatic/gui/cred_fei_frame.py @@ -5,10 +5,10 @@ from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -class ExperimentalcRED_FEI(LabelFrame, HasQMixin): +class ExperimentalcRED_FEI(LabelFrame, ModuleFrameMixin): """Simple panel to assist cRED data collection (mainly rotation control) on a FEI microscope.""" diff --git a/src/instamatic/gui/cred_frame.py b/src/instamatic/gui/cred_frame.py index 0ed94612..3438566e 100644 --- a/src/instamatic/gui/cred_frame.py +++ b/src/instamatic/gui/cred_frame.py @@ -6,12 +6,12 @@ from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin ENABLE_FOOTFREE_OPTION = True -class ExperimentalcRED(LabelFrame, HasQMixin): +class ExperimentalcRED(LabelFrame, ModuleFrameMixin): """GUI panel for doing cRED experiments on a Timepix camera.""" def __init__(self, parent): diff --git a/src/instamatic/gui/cred_tvips_frame.py b/src/instamatic/gui/cred_tvips_frame.py index b0aeac9a..dc7e20d0 100644 --- a/src/instamatic/gui/cred_tvips_frame.py +++ b/src/instamatic/gui/cred_tvips_frame.py @@ -9,12 +9,12 @@ from instamatic import config from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin barrier = threading.Barrier(2, timeout=60) -class ExperimentalTVIPS(LabelFrame, HasQMixin): +class ExperimentalTVIPS(LabelFrame, ModuleFrameMixin): """GUI panel for doing cRED / SerialRED experiments on a TVIPS camera.""" def __init__(self, parent): diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index 3f505a59d..cb7ce2ae 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -14,10 +14,10 @@ from instamatic.gui.click_dispatcher import ClickEvent, MouseButton from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -class ExperimentalCtrl(LabelFrame, HasQMixin): +class ExperimentalCtrl(LabelFrame, ModuleFrameMixin): """This panel holds some frequently used functions to control the electron microscope.""" diff --git a/src/instamatic/gui/debug_frame.py b/src/instamatic/gui/debug_frame.py index 304c8197..293bf665 100644 --- a/src/instamatic/gui/debug_frame.py +++ b/src/instamatic/gui/debug_frame.py @@ -8,7 +8,7 @@ from instamatic import config -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin scripts_drc = config.locations['scripts'] @@ -22,7 +22,7 @@ VMPORT = config.settings.VM_server_port -class DebugFrame(LabelFrame, HasQMixin): +class DebugFrame(LabelFrame, ModuleFrameMixin): """GUI panel with advanced / debugging functions.""" def __init__(self, parent): diff --git a/src/instamatic/gui/fast_adt_frame.py b/src/instamatic/gui/fast_adt_frame.py index 46638fbc..bfceb0e4 100644 --- a/src/instamatic/gui/fast_adt_frame.py +++ b/src/instamatic/gui/fast_adt_frame.py @@ -8,7 +8,7 @@ from instamatic import controller from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin pad0 = {'sticky': 'EW', 'padx': 0, 'pady': 1} pad10 = {'sticky': 'EW', 'padx': 10, 'pady': 1} @@ -82,7 +82,7 @@ def as_dict(self): return {n: v.get() for n, v in vars(self).items() if isinstance(v, Variable)} -class ExperimentalFastADT(LabelFrame, HasQMixin): +class ExperimentalFastADT(LabelFrame, ModuleFrameMixin): """GUI panel to perform selected FastADT-style (c)RED & PED experiments.""" def __init__(self, parent): diff --git a/src/instamatic/gui/machine_learning_frame.py b/src/instamatic/gui/machine_learning_frame.py index 59fe9d6d..c7777e85 100644 --- a/src/instamatic/gui/machine_learning_frame.py +++ b/src/instamatic/gui/machine_learning_frame.py @@ -10,7 +10,7 @@ from instamatic.formats import read_image -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin from .mpl_frame import ShowMatplotlibFig @@ -25,7 +25,7 @@ def treeview_sort_column(tv, col, reverse): tv.heading(col, command=lambda: treeview_sort_column(tv, col, not reverse)) -class MachineLearningFrame(LabelFrame, HasQMixin): +class MachineLearningFrame(LabelFrame, ModuleFrameMixin): """GUI Panel to read in the results from the machine learning algorithm to identify good/poor crystals based on their diffraction pattern.""" diff --git a/src/instamatic/gui/red_frame.py b/src/instamatic/gui/red_frame.py index e3c0105f..ca6e64b9 100644 --- a/src/instamatic/gui/red_frame.py +++ b/src/instamatic/gui/red_frame.py @@ -5,10 +5,10 @@ from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -class ExperimentalRED(LabelFrame, HasQMixin): +class ExperimentalRED(LabelFrame, ModuleFrameMixin): """GUI panel to perform a simple RED experiment using discrete rotation steps.""" diff --git a/src/instamatic/gui/sed_frame.py b/src/instamatic/gui/sed_frame.py index 9f20fe6b..9fb9a41b 100644 --- a/src/instamatic/gui/sed_frame.py +++ b/src/instamatic/gui/sed_frame.py @@ -9,7 +9,7 @@ from instamatic.calibrate import CalibDirectBeam from instamatic.calibrate.filenames import CALIB_BEAMSHIFT, CALIB_DIRECTBEAM -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin # import matplotlib # matplotlib.use('TkAgg') @@ -50,7 +50,7 @@ Press to start""" -class ExperimentalSED(LabelFrame, HasQMixin): +class ExperimentalSED(LabelFrame, ModuleFrameMixin): """GUI panel to start a SerialED experiment.""" def __init__(self, parent): diff --git a/src/instamatic/gui/videostream_frame.py b/src/instamatic/gui/videostream_frame.py index 6e427225..0ea39bb8 100644 --- a/src/instamatic/gui/videostream_frame.py +++ b/src/instamatic/gui/videostream_frame.py @@ -15,14 +15,14 @@ from instamatic._typing import AnyPath from instamatic.formats import read_tiff, write_tiff -from instamatic.gui.base_module import BaseModule, HasQMixin +from instamatic.gui.base_module import BaseModule, ModuleFrameMixin from instamatic.gui.click_dispatcher import ClickDispatcher from instamatic.gui.videostream_processor import VideoStreamProcessor from instamatic.processing import apply_flatfield_correction from instamatic.utils.spinbox import Spinbox -class VideoStreamFrame(LabelFrame, HasQMixin): +class VideoStreamFrame(LabelFrame, ModuleFrameMixin): """GUI panel to continuously display the last frame streamed from the camera.""" From 621d954e434d71dddb6f836678554fb9299ff26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 21 Nov 2025 16:05:32 +0100 Subject: [PATCH 08/18] `Any` is a significantly better type hint that `None` --- src/instamatic/gui/base_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/instamatic/gui/base_module.py b/src/instamatic/gui/base_module.py index aaa6a281..971abf94 100644 --- a/src/instamatic/gui/base_module.py +++ b/src/instamatic/gui/base_module.py @@ -1,6 +1,7 @@ from __future__ import annotations from queue import Queue +from typing import Any class BaseModule: @@ -39,4 +40,4 @@ class ModuleFrameMixin: """Asserts some class attributes i.e. module.q, app remain reserved.""" q: Queue - app: None + app: Any From 8eeda49e6663d5feaaabba530900c5c7c4b0a0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 21 Nov 2025 16:07:52 +0100 Subject: [PATCH 09/18] Do not change default `ENABLE_FOOTFREE_OPTION` --- src/instamatic/gui/cred_frame.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/instamatic/gui/cred_frame.py b/src/instamatic/gui/cred_frame.py index 3438566e..802256a0 100644 --- a/src/instamatic/gui/cred_frame.py +++ b/src/instamatic/gui/cred_frame.py @@ -8,7 +8,7 @@ from .base_module import BaseModule, ModuleFrameMixin -ENABLE_FOOTFREE_OPTION = True +ENABLE_FOOTFREE_OPTION = False class ExperimentalcRED(LabelFrame, ModuleFrameMixin): @@ -175,7 +175,6 @@ def init_vars(self): self.var_exposure_time_image = DoubleVar(value=0.01) self.var_footfree_rotate_to = DoubleVar(value=65.0) - self.var_footfree_rotate_speed = DoubleVar(value=0.0) self.var_toggle_footfree = BooleanVar(value=False) self.mode = 'regular' From 190d632c4d4f151e4679fb54c37ca1e5bbec0ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 21 Nov 2025 17:28:11 +0100 Subject: [PATCH 10/18] Improve lmb/rmb responsiveness, messages, GUI feedback --- src/instamatic/gui/click_dispatcher.py | 2 + src/instamatic/gui/ctrl_frame.py | 89 +++++++++++++------------- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/instamatic/gui/click_dispatcher.py b/src/instamatic/gui/click_dispatcher.py index 7b430cb2..b07ec574 100644 --- a/src/instamatic/gui/click_dispatcher.py +++ b/src/instamatic/gui/click_dispatcher.py @@ -82,9 +82,11 @@ def add_listener( self, name: str, callback: Optional[Callable[[ClickEvent], None]] = None, + active: bool = False, ) -> ClickListener: """Convenience method that adds and returns a new `ClickListener`""" listener = ClickListener(name, callback) + listener.active = active self.listeners[name] = listener return listener diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index cb7ce2ae..995262ba 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -81,10 +81,12 @@ def __init__(self, parent): text = 'Move stage with LMB' self.lmb_stage = Checkbutton(frame, text=text, variable=self.var_lmb_stage) self.lmb_stage.grid(row=1, column=3, columnspan=3, sticky='W') + self.var_lmb_stage.trace_add('write', self.toggle_lmb_stage) text = 'Move beam with RMB' self.rmb_beam = Checkbutton(frame, text=text, variable=self.var_rmb_beam) self.rmb_beam.grid(row=2, column=3, columnspan=3, sticky='W') + self.var_rmb_beam.trace_add('write', self.toggle_rmb_beam) e_stage_x = Spinbox(frame, textvariable=self.var_stage_x, **stage) e_stage_x.grid(row=6, column=1, sticky='EW') @@ -224,10 +226,8 @@ def init_vars(self): self.var_diff_defocus_on = BooleanVar(value=False) self.var_stage_wait = BooleanVar(value=True) - self.var_rmb_beam = BooleanVar(value=False) - self.var_rmb_beam.trace_add('write', self.toggle_rmb_beam) self.var_lmb_stage = BooleanVar(value=False) - self.var_lmb_stage.trace_add('write', self.toggle_lmb_stage) + self.var_rmb_beam = BooleanVar(value=False) def set_mode(self, event=None): self.ctrl.mode.set(self.var_mode.get()) @@ -325,53 +325,50 @@ def toggle_alpha_wobbler(self): def toggle_lmb_stage(self, _name, _index, _mode): """If self.var_lmb_stage, move stage using Left Mouse Button.""" - try: - stage_matrix = self.ctrl.get_stagematrix() - except KeyError: - print('No calibration :<') - # self.var_lmb_stage.set(False) - return - - def _cb(click: ClickEvent) -> None: - if click.button == MouseButton.LEFT: - cam_dim_x, cam_dim_y = self.ctrl.cam.get_camera_dimensions() - pixel_delta = np.array([click.y - cam_dim_y / 2, click.x - cam_dim_x / 2]) - stage_shift = np.dot(pixel_delta, stage_matrix) - stage_x, stage_y = self.ctrl.stage.xy - self.ctrl.stage.set(x=stage_x + stage_shift[0], y=stage_y + stage_shift[1]) - print(f'Stage position at {self.ctrl.stage.get()}') - - if checked := self.var_lmb_stage.get(): - d = self.app.get_module('stream').click_dispatcher - n = 'lmb_stage' - self.lmb_stage_cl = c if (c := d.listeners.get(n)) else d.add_listener(n, _cb) - self.lmb_stage_cl.active = checked + d = self.app.get_module('stream').click_dispatcher + if self.var_lmb_stage.get(): + try: + stage_matrix = self.ctrl.get_stagematrix() + except KeyError: + print('No stage matrix for current mode and magnification found.') + print('Run `instamatic.calibrate_stagematrix` to use this feature.') + self.var_lmb_stage.set(False) + return + + def _callback(click: ClickEvent) -> None: + if click.button == MouseButton.LEFT: + cam_dim_x, cam_dim_y = self.ctrl.cam.get_camera_dimensions() + pixel_delta = np.array([click.y - cam_dim_y / 2, click.x - cam_dim_x / 2]) + stage_shift = np.dot(pixel_delta, stage_matrix) + x, y = self.ctrl.stage.xy + self.ctrl.stage.set(x=x + stage_shift[0], y=y + stage_shift[1]) + + d.add_listener('lmb_stage', _callback, active=True) + else: + d.listeners.pop('lmb_stage', None) def toggle_rmb_beam(self, _name, _index, _mode) -> None: """If self.var_rmb_beam, move beam using Right Mouse Button.""" - try: - if not hasattr(self, 'calib_beamshift'): - path = self.app.get_module('io').get_experiment_directory().parent / 'calib' - print(path / CALIB_BEAMSHIFT) - self.calib_beamshift = CalibBeamShift.from_file(path / CALIB_BEAMSHIFT) - print(self.calib_beamshift) - except OSError: - print('No calibration :(') - # self.var_rmb_beam.set(False) - return - - def _cb(click: ClickEvent) -> None: - if click.button == MouseButton.RIGHT: - bs = self.calib_beamshift.pixelcoord_to_beamshift((click.y, click.x)) - self.ctrl.beamshift.set(*bs) - print(f'Beam shift at {self.ctrl.beamshift.get()}') - - if checked := self.var_rmb_beam.get(): - d = self.app.get_module('stream').click_dispatcher - n = 'rmb_beam' - self.rmb_beam_cl = c if (c := d.listeners.get(n)) else d.add_listener(n, _cb) - self.rmb_beam_cl.active = checked + d = self.app.get_module('stream').click_dispatcher + if self.var_rmb_beam.get(): + path = self.app.get_module('io').get_experiment_directory().parent / 'calib' + try: + calib_beamshift = CalibBeamShift.from_file(path / CALIB_BEAMSHIFT) + except OSError: + print(f'No {CALIB_BEAMSHIFT} file in directory {path} found.') + print('Run `instamatic.calibrate_beamshift` there to use this feature.') + self.var_rmb_beam.set(False) + return + + def _callback(click: ClickEvent) -> None: + if click.button == MouseButton.RIGHT: + bs = calib_beamshift.pixelcoord_to_beamshift((click.y, click.x)) + self.ctrl.beamshift.set(*[float(b) for b in bs]) + + d.add_listener('rmb_beam', _callback, active=True) + else: + d.listeners.pop('rmb_beam', None) def stage_stop(self): self.q.put(('ctrl', {'task': 'stage.stop'})) From 2f1d72674f3a5863aca5201cb9e17397f6572621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 21 Nov 2025 17:32:53 +0100 Subject: [PATCH 11/18] After all, why would you ever want to disable the footfree option??? --- src/instamatic/gui/cred_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instamatic/gui/cred_frame.py b/src/instamatic/gui/cred_frame.py index 802256a0..b85b45aa 100644 --- a/src/instamatic/gui/cred_frame.py +++ b/src/instamatic/gui/cred_frame.py @@ -8,7 +8,7 @@ from .base_module import BaseModule, ModuleFrameMixin -ENABLE_FOOTFREE_OPTION = False +ENABLE_FOOTFREE_OPTION = True class ExperimentalcRED(LabelFrame, ModuleFrameMixin): From 9bfe76cbf5b64bd7f20365ed6606997c1f0286c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Fri, 21 Nov 2025 17:34:11 +0100 Subject: [PATCH 12/18] Change "Rotation Speed" to "Rotation speed" to align with other settings --- src/instamatic/gui/ctrl_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index 995262ba..f0fcc1fb 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -95,7 +95,7 @@ def __init__(self, parent): e_stage_z = Spinbox(frame, textvariable=self.var_stage_z, **stage) e_stage_z.grid(row=6, column=3, sticky='EW') - Label(frame, text='Rotation Speed', width=20).grid(row=5, column=0, sticky='W') + Label(frame, text='Rotation speed', width=20).grid(row=5, column=0, sticky='W') e_goniotool_tx = Spinbox( frame, width=10, textvariable=self.var_goniotool_tx, from_=1, to=12, increment=1 ) From d672d283178bf12c89d4c5eb5d6c515abe5bf0f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Mon, 24 Nov 2025 11:45:03 +0100 Subject: [PATCH 13/18] Update src/instamatic/gui/ctrl_frame.py Co-authored-by: Stef Smeets --- src/instamatic/gui/ctrl_frame.py | 39 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index f0fcc1fb..51551066 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -326,26 +326,27 @@ def toggle_lmb_stage(self, _name, _index, _mode): """If self.var_lmb_stage, move stage using Left Mouse Button.""" d = self.app.get_module('stream').click_dispatcher - if self.var_lmb_stage.get(): - try: - stage_matrix = self.ctrl.get_stagematrix() - except KeyError: - print('No stage matrix for current mode and magnification found.') - print('Run `instamatic.calibrate_stagematrix` to use this feature.') - self.var_lmb_stage.set(False) - return - - def _callback(click: ClickEvent) -> None: - if click.button == MouseButton.LEFT: - cam_dim_x, cam_dim_y = self.ctrl.cam.get_camera_dimensions() - pixel_delta = np.array([click.y - cam_dim_y / 2, click.x - cam_dim_x / 2]) - stage_shift = np.dot(pixel_delta, stage_matrix) - x, y = self.ctrl.stage.xy - self.ctrl.stage.set(x=x + stage_shift[0], y=y + stage_shift[1]) - - d.add_listener('lmb_stage', _callback, active=True) - else: + if not self.var_lmb_stage.get(): d.listeners.pop('lmb_stage', None) + return + + try: + stage_matrix = self.ctrl.get_stagematrix() + except KeyError: + print('No stage matrix for current mode and magnification found.') + print('Run `instamatic.calibrate_stagematrix` to use this feature.') + self.var_lmb_stage.set(False) + return + + def _callback(click: ClickEvent) -> None: + if click.button == MouseButton.LEFT: + cam_dim_x, cam_dim_y = self.ctrl.cam.get_camera_dimensions() + pixel_delta = np.array([click.y - cam_dim_y / 2, click.x - cam_dim_x / 2]) + stage_shift = np.dot(pixel_delta, stage_matrix) + x, y = self.ctrl.stage.xy + self.ctrl.stage.set(x=x + stage_shift[0], y=y + stage_shift[1]) + + d.add_listener('lmb_stage', _callback, active=True) def toggle_rmb_beam(self, _name, _index, _mode) -> None: """If self.var_rmb_beam, move beam using Right Mouse Button.""" From 08048e2d6cf132b3b9f956b0ce920ad41b382814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Mon, 24 Nov 2025 11:45:23 +0100 Subject: [PATCH 14/18] Update src/instamatic/gui/ctrl_frame.py Co-authored-by: Stef Smeets --- src/instamatic/gui/ctrl_frame.py | 35 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index 51551066..d92063bb 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -352,24 +352,25 @@ def toggle_rmb_beam(self, _name, _index, _mode) -> None: """If self.var_rmb_beam, move beam using Right Mouse Button.""" d = self.app.get_module('stream').click_dispatcher - if self.var_rmb_beam.get(): - path = self.app.get_module('io').get_experiment_directory().parent / 'calib' - try: - calib_beamshift = CalibBeamShift.from_file(path / CALIB_BEAMSHIFT) - except OSError: - print(f'No {CALIB_BEAMSHIFT} file in directory {path} found.') - print('Run `instamatic.calibrate_beamshift` there to use this feature.') - self.var_rmb_beam.set(False) - return - - def _callback(click: ClickEvent) -> None: - if click.button == MouseButton.RIGHT: - bs = calib_beamshift.pixelcoord_to_beamshift((click.y, click.x)) - self.ctrl.beamshift.set(*[float(b) for b in bs]) - - d.add_listener('rmb_beam', _callback, active=True) - else: + if not self.var_rmb_beam.get(): d.listeners.pop('rmb_beam', None) + return + + path = self.app.get_module('io').get_experiment_directory().parent / 'calib' + try: + calib_beamshift = CalibBeamShift.from_file(path / CALIB_BEAMSHIFT) + except OSError: + print(f'No {CALIB_BEAMSHIFT} file in directory {path} found.') + print('Run `instamatic.calibrate_beamshift` there to use this feature.') + self.var_rmb_beam.set(False) + return + + def _callback(click: ClickEvent) -> None: + if click.button == MouseButton.RIGHT: + bs = calib_beamshift.pixelcoord_to_beamshift((click.y, click.x)) + self.ctrl.beamshift.set(*[float(b) for b in bs]) + + d.add_listener('rmb_beam', _callback, active=True) def stage_stop(self): self.q.put(('ctrl', {'task': 'stage.stop'})) From dc96cd05e363befadf7ae5a40de4e5f6c996bbaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Mon, 24 Nov 2025 12:02:36 +0100 Subject: [PATCH 15/18] Make local config path reference more readable --- src/instamatic/gui/ctrl_frame.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index d92063bb..cfe7625b 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -329,7 +329,7 @@ def toggle_lmb_stage(self, _name, _index, _mode): if not self.var_lmb_stage.get(): d.listeners.pop('lmb_stage', None) return - + try: stage_matrix = self.ctrl.get_stagematrix() except KeyError: @@ -356,7 +356,7 @@ def toggle_rmb_beam(self, _name, _index, _mode) -> None: d.listeners.pop('rmb_beam', None) return - path = self.app.get_module('io').get_experiment_directory().parent / 'calib' + path = self.app.get_module('io').get_working_directory() / 'calib' try: calib_beamshift = CalibBeamShift.from_file(path / CALIB_BEAMSHIFT) except OSError: @@ -370,7 +370,7 @@ def _callback(click: ClickEvent) -> None: bs = calib_beamshift.pixelcoord_to_beamshift((click.y, click.x)) self.ctrl.beamshift.set(*[float(b) for b in bs]) - d.add_listener('rmb_beam', _callback, active=True) + d.add_listener('rmb_beam', _callback, active=True) def stage_stop(self): self.q.put(('ctrl', {'task': 'stage.stop'})) From 31c28ea309cf6b4a2988a23aefc7c428ab1fddb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Mon, 24 Nov 2025 12:43:35 +0100 Subject: [PATCH 16/18] Implement `toggle_lmb_stage` fallback using `move_in_projection` --- src/instamatic/gui/ctrl_frame.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index cfe7625b..66a2e9ab 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -342,9 +342,8 @@ def _callback(click: ClickEvent) -> None: if click.button == MouseButton.LEFT: cam_dim_x, cam_dim_y = self.ctrl.cam.get_camera_dimensions() pixel_delta = np.array([click.y - cam_dim_y / 2, click.x - cam_dim_x / 2]) - stage_shift = np.dot(pixel_delta, stage_matrix) - x, y = self.ctrl.stage.xy - self.ctrl.stage.set(x=x + stage_shift[0], y=y + stage_shift[1]) + stage_delta = np.dot(pixel_delta, stage_matrix) + self.ctrl.stage.move_in_projection(*stage_delta) d.add_listener('lmb_stage', _callback, active=True) From f639daba2b39f484a7c85828f402f2efa1b2235f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Mon, 24 Nov 2025 16:19:35 +0100 Subject: [PATCH 17/18] Implement fixes necessary to handle remote camera via server --- src/instamatic/camera/camera_client.py | 50 +++++++++++++------------- src/instamatic/server/cam_server.py | 8 ++--- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/instamatic/camera/camera_client.py b/src/instamatic/camera/camera_client.py index 6b320657..9f75314d 100644 --- a/src/instamatic/camera/camera_client.py +++ b/src/instamatic/camera/camera_client.py @@ -3,9 +3,9 @@ import atexit import socket import subprocess as sp +import threading import time from functools import wraps -from typing import Dict import numpy as np @@ -55,6 +55,7 @@ def __init__( self.name = name self.interface = interface self._bufsize = BUFSIZE + self._eval_lock = threading.Lock() self.verbose = False try: @@ -79,7 +80,7 @@ def __init__( ) print('Use shared memory:', self.use_shared_memory) - self.buffers: Dict[str, np.ndarray] = {} + self.buffers: dict[str, np.ndarray] = {} self.shms = {} self._attr_dct: dict = {} @@ -122,36 +123,37 @@ def wrapper(*args, **kwargs): def _eval_dct(self, dct): """Takes approximately 0.2-0.3 ms per call if HOST=='localhost'.""" - self.s.send(dumper(dct)) + with self._eval_lock: + self.s.send(dumper(dct)) - acquiring_image = dct['attr_name'] == 'get_image' - acquiring_movie = dct['attr_name'] == 'get_movie' + acquiring_image = dct['attr_name'] == 'get_image' + acquiring_movie = dct['attr_name'] == 'get_movie' - if acquiring_movie: - raise NotImplementedError('Acquiring movies over a socket is not supported.') + if acquiring_movie: + raise NotImplementedError('Acquiring movies over a socket is not supported.') - if acquiring_image and not self.use_shared_memory: - response = self.s.recv(self._imagebufsize) - else: - response = self.s.recv(self._bufsize) + if acquiring_image and not self.use_shared_memory: + response = self.s.recv(self._imagebufsize) + else: + response = self.s.recv(self._bufsize) - if response: - status, data = loader(response) - else: - raise RuntimeError(f'Received empty response when evaluating {dct=}') + if response: + status, data = loader(response) + else: + raise RuntimeError(f'Received empty response when evaluating {dct=}') - if self.use_shared_memory and acquiring_image: - data = self.get_data_from_shared_memory(**data) + if self.use_shared_memory and acquiring_image: + data = self.get_data_from_shared_memory(**data) - if status == 200: - return data + if status == 200: + return data - elif status == 500: - error_code, args = data - raise exception_list.get(error_code, TEMCommunicationError)(*args) + elif status == 500: + error_code, args = data + raise exception_list.get(error_code, TEMCommunicationError)(*args) - else: - raise ConnectionError(f'Unknown status code: {status}') + else: + raise ConnectionError(f'Unknown status code: {status}') def _init_dict(self): """Get list of functions and their doc strings from the uninitialized diff --git a/src/instamatic/server/cam_server.py b/src/instamatic/server/cam_server.py index 2d3c926f..290fbf70 100644 --- a/src/instamatic/server/cam_server.py +++ b/src/instamatic/server/cam_server.py @@ -2,7 +2,6 @@ import datetime import logging -import pickle import queue import socket import threading @@ -12,10 +11,9 @@ from instamatic import config from instamatic.camera import get_camera +from instamatic.server.serializer import dumper, loader from instamatic.utils import high_precision_timers -from .serializer import dumper, loader - high_precision_timers.enable() if config.settings.cam_use_shared_memory: @@ -174,13 +172,13 @@ def main(): The host and port are defined in `config/settings.yaml`. -The data sent over the socket is a pickled dictionary with the following elements: +The data sent over the socket is a serialized dict with the following elements: - `attr_name`: Name of the function to call or attribute to return (str) - `args`: (Optional) List of arguments for the function (list) - `kwargs`: (Optiona) Dictionary of keyword arguments for the function (dict) -The response is returned as a pickle object. +The response is returned as a serialized object. """ parser = argparse.ArgumentParser( From c593bf49ebe0271492076169b176897a6552e753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Mon, 24 Nov 2025 18:05:55 +0100 Subject: [PATCH 18/18] Allow setting non-integer rotation speeds --- src/instamatic/gui/ctrl_frame.py | 2 +- src/instamatic/microscope/interface/jeol_microscope.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index 139fea0d..88befcc0 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -203,7 +203,7 @@ def init_vars(self): self.var_stage_y = IntVar(value=0) self.var_stage_z = IntVar(value=0) - self.var_goniotool_tx = IntVar(value=1) + self.var_goniotool_tx = DoubleVar(value=1) self.var_brightness = IntVar(value=65535) self.var_difffocus = IntVar(value=65535) diff --git a/src/instamatic/microscope/interface/jeol_microscope.py b/src/instamatic/microscope/interface/jeol_microscope.py index 76c9e90f..a5156264 100644 --- a/src/instamatic/microscope/interface/jeol_microscope.py +++ b/src/instamatic/microscope/interface/jeol_microscope.py @@ -384,7 +384,7 @@ def getRotationSpeed(self) -> int: def setRotationSpeed(self, value: int): if self.goniotool_available: - self.goniotool.set_rate(value) + self.goniotool.set_rate(int(value)) else: raise TEMCommunicationError('Goniotool connection is not available.')