Skip to content

Conversation

@Baharis
Copy link
Member

@Baharis Baharis commented Nov 21, 2025

The cRED protocol introduced in 2017 was the first to allow tracking crystals during an experiment. This is done manually, by collecting a de-focused image every N frames and allowing the user to shift the beam accordingly. On JEOL machines, this can be done with a trackball; however, two sources of input can cause communication issues on FEI. Consequently, this PR introduces two additional switches in Instamatic "control" tab: when toggled, 'Move stage with LMB' and 'Move beam with RMB' bind left and right mouse buttons to stage and beamshift controls, allowing control over these motors/deflectors via the stream window:

image

To allow "control" module to move the beam, I needed it to have access to the beamshift calibration, which required it to access the "io" module associated with the frame at the top of the window that handles paths. This is because all experiment types save beamshift calibration in the current experiment directory. With the current design, this is impossible: the only part of instamatic with access to all modules is the central program controller (via the AppLoader) responsible for running "tasks".

In general, I noticed that often I want module X to have access to some data in module Y. It felt strange to me that there is no mechanism to do it. However, in #147, I modified the AppLoader to give all modules access to the task q. Here, I decided to do the same for app: by changing 2-lines, self.app now becomes a class-level attribute for all loaded modules:

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)

This way, every module can call self.app.get_module to access another module. This is immensely powerful as it enables modules to communicate and complement. For example, in this PR, "control" module uses self.app.get_module('stream').click_dispatcher to move stage/beam via clicks and self.app.get_module('io').get_experiment_directory() to find beamshift calibration.

In addition to changes listed above, this PR suggests a few smaller related modifications:

  • Set cred_frame ENABLE_FOOTFREE_OPTION by default to True – otherwise cRED just does not work unless you have a foot pedal, which it certainly not the norm.
  • Reveals rotation speed setting in "control" frame even if "not using goniotool" – after all, if you are using FEI Tecnai/Titan, you indeed do not have a goniotool, but you still might have full control over the speed settings!
  • Rename HasQMixin to ModuleFrameMixin to reflect its extended role – in future I would like to rework the module/job system in general, as preparing this PR made me realize it has an immense potential to allow external modules while lowering boilerplate and code complexity, plus current instamatic.gui folder is frankly quite a mess right now.

Edit: since these seemed related and few for a separate PR, I also added the following changes:

  • Synchronize calls to CamClient._eval_dict: fixes errors that may occur when running experiments using a streamable camera via server (which by the way is now completely possible and decently fast);
  • Changed ExperimentalCtrl.var_goniotool_tx from IntVar to DoubleVar: necessary to set rotation speeds on a non-Jeol machine (for Jeol, added a server-side convertion of speed back to int);
  • Fixed instamatic/server/cam_server.py imports, doc-strings: can be run as a script again.

@Baharis Baharis requested a review from stefsmeets November 21, 2025 17:47
@Baharis Baharis self-assigned this Nov 21, 2025
Copy link
Member

@stefsmeets stefsmeets left a comment

Choose a reason for hiding this comment

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

Looks good, I like the ModuleFrameMixin better than HasQMixin. I intended to have some modules be more 'generic' like 'io', so they were available everywhere. Other experimental modules were more specific, so they did not need to interact with each other. It's cool to see this system could be re-used for your purpose.

I left some small comments in the PR.

Comment on lines 340 to 344
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])
Copy link
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
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.

Baharis and others added 4 commits November 24, 2025 11:45
@Baharis
Copy link
Member Author

Baharis commented Nov 24, 2025

I admit I have not fully tested the code in vivo yet, once I do I will 🚀 this very message and likely merge around Wednesday.

@Baharis Baharis changed the title Fix cRED @ FEI by revealing speed settings and mouse stage/beam control Speed setting, mouse stage/beam control, and remote cameras Nov 24, 2025
@Baharis
Copy link
Member Author

Baharis commented Nov 24, 2025

I tested this branch today alongside another one. In the process I realized, that the second branch is very small and complementary. Therefore, I added a few tiny changes here, as documented in "EDIT" above:

  • Synchronize calls to CamClient._eval_dict: fixes errors that may occur when running experiments using a streamable camera via server (which by the way is now completely possible and decently fast);
  • Changed ExperimentalCtrl.var_goniotool_tx from IntVar to DoubleVar: necessary to set rotation speeds on a non-Jeol machine (for Jeol, added a server-side conversion of speed back to int);
  • Fixed instamatic/server/cam_server.py imports, doc-strings: can be run as a script again.

Controlling the stage and beam using the mouse have ultimately proved to be non-essential for "fixing" cRED for FEI machines, but all in all I believe they are valuable addition to the control panel. Consequently, I have generalized the name of this thread. I am happy to report that with all modifications introduced here:

  • The control panel can be properly used to set FEI gonio rotation speed;
  • Mouse left/right button can be used to control stage/beam position without issues;
  • Remote cameras accessed via a server can be streamed and used for experiments as normal;
  • cRED works on an FEI TEM (n.b. moving beam using trackball while rotating stage works!).

Intending to merge on Wednesday.

@Baharis Baharis merged commit 8335778 into instamatic-dev:main Nov 27, 2025
6 checks passed
@Baharis Baharis deleted the ctrl_tem_with_mouse branch November 27, 2025 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants