Skip to content

feat: add software autofocus support to MDAWidget#553

Draft
gcharvin wants to merge 5 commits into
pymmcore-plus:mainfrom
gcharvin:charvin/feature-software-autofocus
Draft

feat: add software autofocus support to MDAWidget#553
gcharvin wants to merge 5 commits into
pymmcore-plus:mainfrom
gcharvin:charvin/feature-software-autofocus

Conversation

@gcharvin
Copy link
Copy Markdown
Contributor

@gcharvin gcharvin commented Apr 7, 2026

Summary

Draft feature PR for design feedback. This adds software autofocus support to the MDA widget path.

It includes:

  • autofocus mode selection in the MDA widget
  • software autofocus settings stored in MDA metadata
  • a software autofocus dialog and MDA engine integration
  • focus channel selection
  • basic failure policy handling

Background

This was developed and field-tested during TiEclipse timelapse work where hardware PFS-like behavior was needed from software autofocus inside MDA execution. I do not have a screenshot or formal bug report saved, so this PR is documented from commit history and the behavior implemented here.

Notes for reviewers

This is intentionally a draft. I would like feedback on API shape, metadata schema, and whether the engine integration belongs here or should be refactored before merge. Hardware coverage is limited to the TiEclipse workflow so far.

Validation

  • python -m ruff check src/pymmcore_widgets/mda/_autofocus.py src/pymmcore_widgets/mda/_core_mda.py src/pymmcore_widgets/useq_widgets/_autofocus.py src/pymmcore_widgets/useq_widgets/_mda_sequence.py
  • python -m compileall -q src/pymmcore_widgets/mda/_autofocus.py src/pymmcore_widgets/mda/_core_mda.py src/pymmcore_widgets/useq_widgets/_autofocus.py src/pymmcore_widgets/useq_widgets/_mda_sequence.py

@gcharvin gcharvin changed the title Draft: Add software autofocus support to MDAWidget feat: add software autofocus support to MDAWidget Apr 7, 2026
@tlambert03
Copy link
Copy Markdown
Member

thanks for opening this @gcharvin! Great feature, definitely needed. (I think there's an issue discussing it somewhere in one of the repos, but not immediately finding it).

One high level thought I'm having here is that this feels similar in spirit to pymmcore-plus/pymmcore-plus#439 (pixel calibration routine to find the camera affine transform): basically, they are both "extended routines" that operate on a core. They both take in some config, and produce a result. So, I've been thinking that we need some sort of a routines module that would be a grab bag of purely-programmatic, GUI-free things that take a core + some config, execute some functionality, and return some data.

class SoftwareAutofocus:
    """Scan Z, score focus, return best position."""

    class Events(SignalGroup):
        step_scored = Signal(float, float)
        attempt_started = Signal(int, int)
        finished = Signal(SoftwareAutofocusResult)
        failed = Signal(str)

    events = Events()

    def run(
        self,
        core: CMMCorePlus,
        config: SoftwareAutofocusConfig,  # some dataclass with config
    ) -> SoftwareAutofocusResult:  # some dataclass with result
        ...

I've struggled to decide where these sorts of things belong.

  1. Part of me thinks we should create a new pymmcore_plus.routines module... then at the GUI layer, we would build widgets to gather the config, create a routine, optionally connect events to gui feedback, and run it.

    This pattern would be the cleanest separation of concerns, and would encourage people to contribute/use these routines even if they don't particularly care about our opinionated GUIs.

  2. The other part of me says "ugh... but the multi-repo dance that would ensue anytime you want to change something...". That is certainly annoying, and it does bite us particularly as we work on the highest level app pymmcore-gui these days. So that part of me wants to put routines at the highest level, in pymmcore-gui (maybe widgets, but even that could be annoying once it starts to want broader core coordination).


Does anyone have thoughts A) on the general concept of a "routines" pattern? B) on where it should live (low programmatic level with separation of concerns, or don't try... just put it in a higher level lib)

@marktsuchida, @gcharvin, @fdrgsp?

@gcharvin
Copy link
Copy Markdown
Contributor Author

gcharvin commented Apr 8, 2026

Thanks Talley, I agree that this feels more like a “routine” than something that should live directly in the widget/MDA layer.

I’d be happy to refactor the software autofocus in that direction: move the core logic into a GUI-free SoftwareAutofocus object, with a config object, a result object, and maybe a few progress events like step scored / finished / failed. Then the widget would just collect the config and display progress, and the MDA engine would just call the same routine.

I also agree that pymmcore_plus.routines is probably the cleanest home conceptually. Maybe a reasonable first step would be to prototype it as a GUI-free module in pymmcore-widgets, and then move it down to pymmcore-plus once the shape feels stable?

@tlambert03
Copy link
Copy Markdown
Member

tlambert03 commented Apr 8, 2026

I also agree that pymmcore_plus.routines is probably the cleanest home conceptually. Maybe a reasonable first step would be to prototype it as a GUI-free module in pymmcore-widgets, and then move it down to pymmcore-plus once the shape feels stable?

that sounds great to me! want to use this PR to play with the pattern?


A couple additional thoughts on the implementation here:

My main thought is about the general pattern of subclassing MDAEngine. I can see why you did it, since otherwise you basically have to reimplement a zstack... but in this case I think that's how I would have designed it. In either case: the biggest issue I see here currently is self._mmc.mda.set_engine(self._autofocus_engine). Regardless of whether you use MDAEngine here or not (and I think we probably shouldn't), I'm afraid of unintended side effects of changing the global MDARunner's engine with core.mda.set_engine(). (side note: if needed you actually could create an additional dedicated MDARunner()/MDAEngine() pair that drive the same core object if you really want to use MDAEngine here). For the autofocus estimation, let's just go to lower level core methods if possible (and if you find yourself re-implementing what feels to be way too much logic, let's discuss)

But, those are just side thoughts: the key next step should be architectural. Let's try to break up the programmatic and GUI portions into two clear modules, where the routine has a run() method, config, data, and events as described in #553 (comment).

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