Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a150689
Add a crude implementation to control beamshift with RMB (for FEI cRED)
Baharis Nov 10, 2025
24a369a
Add a crude implementation to control stage opsition with LMB
Baharis Nov 11, 2025
94fd7fb
Allow rotation speed control even if goniotool is not available
Baharis Nov 11, 2025
5f5fac3
Add more details, otherwise more difficult to reach, to pets input
Baharis Nov 20, 2025
0ffc01f
Add some more examples to the pets prefix
Baharis Nov 20, 2025
a73ed8f
Videoframe instance needs no app kwarg anymore since it is class attr…
Baharis Nov 21, 2025
0b94d82
Raname `HasQMixin` to `ModuleFrameMixin` to generalize it better
Baharis Nov 21, 2025
621d954
`Any` is a significantly better type hint that `None`
Baharis Nov 21, 2025
8eeda49
Do not change default `ENABLE_FOOTFREE_OPTION`
Baharis Nov 21, 2025
190d632
Improve lmb/rmb responsiveness, messages, GUI feedback
Baharis Nov 21, 2025
2f1d726
After all, why would you ever want to disable the footfree option???
Baharis Nov 21, 2025
9bfe76c
Change "Rotation Speed" to "Rotation speed" to align with other settings
Baharis Nov 21, 2025
d672d28
Update src/instamatic/gui/ctrl_frame.py
Baharis Nov 24, 2025
08048e2
Update src/instamatic/gui/ctrl_frame.py
Baharis Nov 24, 2025
dc96cd0
Make local config path reference more readable
Baharis Nov 24, 2025
31c28ea
Implement `toggle_lmb_stage` fallback using `move_in_projection`
Baharis Nov 24, 2025
f639dab
Implement fixes necessary to handle remote camera via server
Baharis Nov 24, 2025
560b318
Merge branch 'main' into remote_camera
Baharis Nov 24, 2025
c593bf4
Allow setting non-integer rotation speeds
Baharis Nov 24, 2025
99853eb
Merge branch 'remote_camera' into ctrl_tem_with_mouse
Baharis Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/instamatic/gui/autocred_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions src/instamatic/gui/base_module.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from queue import Queue
from typing import Any


class BaseModule:
Expand Down Expand Up @@ -35,7 +36,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: Any
2 changes: 2 additions & 0 deletions src/instamatic/gui/click_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/cred_fei_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
6 changes: 3 additions & 3 deletions src/instamatic/gui/cred_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = False
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):
Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/cred_tvips_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
93 changes: 81 additions & 12 deletions src/instamatic/gui/ctrl_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

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
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

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."""

Expand Down Expand Up @@ -74,21 +78,31 @@ 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, 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')
e_stage_y = Spinbox(frame, textvariable=self.var_stage_y, **stage)
e_stage_y.grid(row=6, column=2, sticky='EW')
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
)
Expand Down Expand Up @@ -212,6 +226,8 @@ def init_vars(self):
self.var_diff_defocus_on = BooleanVar(value=False)

self.var_stage_wait = BooleanVar(value=True)
self.var_lmb_stage = BooleanVar(value=False)
self.var_rmb_beam = BooleanVar(value=False)

def set_mode(self, event=None):
self.ctrl.mode.set(self.var_mode.get())
Expand Down Expand Up @@ -257,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
Expand Down Expand Up @@ -301,6 +322,54 @@ 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."""

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])
Comment thread
Baharis marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can refactored to make it available as a method on stage, something like:

(not entirely sure about the design here)

Suggested change
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])
dimensions = self.ctrl.cam.get_camera_dimensions()
self.ctrl.stage.move_to_pixel(click.x, click.y, matrix=stage_matrix, dimensions=dimensions)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly wrote an implementation of a move method, because yeah, it would have its place in the codebase:

    def move(
        self,
        delta_x: Optional[int_nm] = None,
        delta_y: Optional[int_nm] = None,
        delta_z: Optional[int_nm] = None,
        delta_a: Optional[float_deg] = None,
        delta_b: Optional[float_deg] = None,
        wait: bool = True,
        speed: Optional[float] = None,
    ) -> None:
        """Move the stage by given values relative to its current position."""
        x, y, z, a, b = self.get()
        kwargs = {
            'x': None if delta_x is None else x + delta_x,
            'y': None if delta_y is None else y + delta_y,
            'z': None if delta_z is None else z + delta_z,
            'a': None if delta_a is None else a + delta_a,
            'b': None if delta_b is None else b + delta_b,
            'wait': wait
        }
        if speed is not None:
            kwargs['speed'] = speed
        self.set(**kwargs)

...but then I realized that what would be actually much more intuitive in real conditions would actually be not set nor move but move_in_projection, and it is already implemented in stage... and it does make code shorter.

As for self.ctrl.stage.move_to_pixel... I am not sure. I am not certain it is necessary, and with the current local implementation taking only 4 lines... Maybe it is? It may get rid of some repetition? I do not have enough conviction to run a deep dive investigation right now, maybe it will resurface in the future.


d.add_listener('lmb_stage', _callback, active=True)
else:
d.listeners.pop('lmb_stage', None)
Comment thread
Baharis marked this conversation as resolved.
Outdated

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'
Comment thread
Baharis marked this conversation as resolved.
Outdated
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)
Comment thread
Baharis marked this conversation as resolved.
Outdated

def stage_stop(self):
self.q.put(('ctrl', {'task': 'stage.stop'}))

Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/debug_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand All @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/fast_adt_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 4 additions & 2 deletions src/instamatic/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -136,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)
Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/machine_learning_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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."""

Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/red_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/sed_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -50,7 +50,7 @@
Press <OK> to start"""


class ExperimentalSED(LabelFrame, HasQMixin):
class ExperimentalSED(LabelFrame, ModuleFrameMixin):
"""GUI panel to start a SerialED experiment."""

def __init__(self, parent):
Expand Down
7 changes: 3 additions & 4 deletions src/instamatic/gui/videostream_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,23 @@

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."""

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

Expand Down