Skip to content

Conversation

@Baharis
Copy link
Member

@Baharis Baharis commented Nov 6, 2025

Context

In #136 I introduce a new calibration routine: instamatic.calibrate_stage_rotation. Building on the assumption that the stage speed scales linearly with speed setting, it uses a very simple model for estimating rotation speed: time = (pace * span + windup) / speed + delay. With that I created a FastADT frame capable of rotating with desired rotation speed, speed that is expressed not in arbitrary microscope units but rather in degree / second – or, to be more precise, the pace is expressed in seconds / degree.

I needed to have similar calibration for sliding the stage along X and Y axis. I noticed that the model and general idea would be exactly the same as for the rotation. Therefore, instead of adding new logic, I generalized the CalibRotationSpeed to CalibMotionSpeed and used it to introduce a completely new calibration routine instamatic.calibrate_stage_translation that, when run with -a x, -a y or -a z is used to calibrate the speed of x, y, or z, respectively.

Calibrating translation speed is something that I need specifically for my application in the future. I am aware that many microscopes do not have an option to modify stage speed. instamatic.calibrate_stage_translation respects that: whenever it detects that setting with speed is unavailable, it will simply calibrate what is the current speed of the stage and use a simpler model: time = pace * span + delay. Stages will this model will not be particularly useful for the slow-sliding experiment (and I do plan to implement a generic replacement), however they are supported just as well by the code.

In addition to generalizing the calibration routing that, if needed, could be also used for other motors or even processes in general, this PR fixes some standing issues with calibration, in particular, a bunch of issues with typing and mypy complaints resolving around the calibration. Given that in the code speed can be float (FEI), int (Jeol, rotation) or None (Jeol, translation), I am quite proud to say I lowered the number of unique mypy errors to like ~5.

I also vastly improved plotting post-calibration. Now after it is complete, the plot shows both fitted and experimental points. This is useful especially when there is anything wrong with calibration; for example, when testing on the simulated TEM, this allowed me to easily realize that the speed settings are accepted but do not affect translation speed. Maybe this is desirable because it makes simulator behavior more consistent, but this remains something to be discussed.

Figure_2

Just as before, the calibration is also accompanied by tqdm progress bar, logging and printing messages. The final messages were generalized and slightly modified to work with low values of paces for translation (they are expressed in seconds per nanometer and around 1e-6 on my Tecnai):

Calibrating y-axis translation speed based on 150 points.
100%|██████████| 150/150 [20:49<00:00,  8.33s/it]
CalibStageTranslationY fit of motion model complete:
pace       =  1.07185e-05 +/-  4.32003e-08 s / nm
windup     =    -0.027379 +/-     0.002919 s
delay      =     1.721743 +/-     0.027519 s
model time = (pace * span + windup) / speed + delay
CalibStageTranslationY saved to %instamatic%\config\calibration\calib_stage_translation_y.yaml.
Attempting to plot calibration results.

Minor changes

  • Added and documented a new calibration script instamatic.calibrate_stage_translation;
  • CalibStageMotion: renamed methods: speed_time_to_span > time_speed_to_span, plan_rotation > plan_motion;
  • FloatOptions: rewritten to a new separate and more descriptive NumericDomain, now also frozen for consistency;
  • NumericDomain: moved to new src/instamatic/utils/domains.py;
  • instamatic.utils.native: added a native function to quickly convert np.integer to int, np.floating to float;
  • motion calibration files now must use pace and windup keywords instead of alpha_pace and alpha_windup;
    • tests/config/calibration/calib_stage_rotation.yaml: modified to reflect this change.

Code maintenance

  • calibrate_stage_rotation.py: generalized to calibrate_stage_motion.py base for new calibrate_stage_translation.py;
  • calibrate_stage_motion.py: vastly improved typing and some docstrings;
  • calibrate_stage_motion.py: moved data fitting mechanism inside new CalibStageMotion.from_data;
  • tests/test_utils.py: added tests for src/instamatic/utils/domains.py:NumericDomain and native.py:native;
  • config.md: improve the quality of PETS prefix example in the config documentation.

Baharis and others added 30 commits September 26, 2025 13:19
@Baharis Baharis requested a review from stefsmeets November 6, 2025 20:48
@Baharis Baharis self-assigned this Nov 6, 2025
@Baharis
Copy link
Member Author

Baharis commented Nov 20, 2025

Unless I see critical comments or strong suggestions, I will be merging this branch by the next week.

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.

Could be fun to give the SimuMicroscope speed settings. This should not be too difficult, as an arbitrary speed is already available:

When I programmed this I was interested to have the illusion of something happening over time and not too slow so I have to sit around forever, rather than have an accurate representation of the speed setting.

NativeTestCase(input_value=int(1), output_type=int)


@pytest.mark.parametrize('test_case', NativeTestCase.INSTANCES)
Copy link
Member

@stefsmeets stefsmeets Nov 21, 2025

Choose a reason for hiding this comment

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

Wow, a bit too much magic here for my tastes, this is a cool way of parametrizing tests.

I wonder if you can make it more magic by implementing __iter__() on InstanceAutoTracker to turn it into an iterable (https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable) and get rid of the INSTANCES attribute.

Suggested change
@pytest.mark.parametrize('test_case', NativeTestCase.INSTANCES)
@pytest.mark.parametrize('test_case', NativeTestCase)

Copy link
Member Author

@Baharis Baharis Nov 21, 2025

Choose a reason for hiding this comment

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

class IterableType(type):
    def __iter__(self) -> Iterator:
        yield from self.INSTANCES


class InstanceAutoTracker(metaclass=IterableType):
    """Track cls instances: useful for @pytest.mark.parametrize dataclasses"""

    def __init_subclass__(cls, **kwargs) -> None:
        super().__init_subclass__(**kwargs)
        cls.INSTANCES: list[Self] = []

    def __post_init__(self) -> None:
        self.__class__.INSTANCES.append(self)

Learning metaclasses in Python opens some door that maybe should remain closed 😎 . I definitely do not think skipping one class variable call is worth added complexity and removed transparency, as cool as it is, though.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants