HPInc / HP-Digital-Microfluidics

HP Digital Microfluidics Software Platform and Libraries
MIT License
2 stars 0 forks source link

Hybrid Bilby/Wombat device model #280

Open EvanKirshenbaum opened 5 months ago

EvanKirshenbaum commented 5 months ago

With the addition of thermal states (#279) and ESElog (#264), which can theoretically work without the full Bilby control hardware working, @cumbiem has asked for a device model that can use Wombat/Yaminon (i.e., the Opendrop controller) to control drop movement and Bilby for everything else.

I think (hope) that I can do this simply by overriding bilby.Board but overriding ._pad_state() (and _well_gate/pad_state()) to create a wombat.Electrode rather than a glider_client.Electrode. I'll also have to override update_state() to do the wombat piece as well.

For the PlatformTask, I'll want to create one that has within it a bilby_client.PlatformTask and a wombat.YaminonPlatformTask (I don't think that they need a bare wombat.PlatformTask) and adds args from both. This should probably be within bilby_client (or another parallel module) so that it can do the same trick of doing the import within make_board() to avoid needing to have pyglider.pyd on the path. It's possible that it should derive from bilby_client.PlatformTask. In any case, the munging and adding of the dll directory should be split out into something that can be called from both.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Jun 27, 2023 at 11:10 AM PDT.
EvanKirshenbaum commented 5 months ago

This issue was referenced by the following commits before migration:

EvanKirshenbaum commented 5 months ago

That was relatively straightforward. There's now a bilby_yaminon.Board defined simply as

class Board(bilby.Board):
    _yaminon: Final[wombat.Board]

    def __init__(self) -> None:
        with wombat.Config.is_yaminon >> True:
            self._yaminon = wombat.Board()
        super().__init__()

    def _add_pads(self)->None:
        self.pads.update(self._yaminon.pads)

    def _add_all_wells(self) -> None:
        assert len(self._well_list) == 0
        self._well_list.extend(self._yaminon.wells)

    def update_state(self)->None:
        self._yaminon.update_state()
        super().update_state()

    def finish_update(self)->None:
        # We don't propagate up, because we only want to infer drop motion once.
        self._yaminon.finish_update()

    def join_system(self, system: System)->None:
        super().join_system(system)
        self._yaminon.join_system(system)

That is, it inherits from bilby.Board and contains a wombat.Board. It initializes the Wombat board first and in its Bilby initialization, it overrides _add_pads() and (a newly extracted) _add_all_wells() to make our pads and wells copied over from the Wombat board. Doing this, and forwarding update_state() and finish_update() to the Wombat board means that all pads (including well pads and gates) will have Wombat electrodes and will be updated using the Opendrop controller.

The associated PlatformTask similarly inherits from bilby_task.PlatformTask but contains a YaminonPlatformTask, and arguments from both are added.

The only things that were tricky were:

EvanKirshenbaum commented 5 months ago

While working on #221, which split things so only the hybrid board owned the components, but only the internal Yaminon board owned the pads, I was seeing that heater #1 : off wasn't ever finishing, even though it was having its effect. It turns out that the problem is that the hybrid overrode finish_update() to say

     def finish_update(self)->None:
       # We don't propagate up, because we only want to infer drop motion once.
       self._yaminon.finish_update()

This is wrong. Since the pads only journal to the yaminon board, it's safe to propagate (since there won't be any motion to infer), and by not doing it, the cleanup for heater operations (i.e., posting the result to the future) was just sitting in the _after_update queue.

The code now looks like

     def finish_update(self)->None:
       # It's safe to call both, because inferring drop motion on super()
       # (Bilby) won't do anything (because no pads journal to it).  It's
       # necessary because changes to heater/chiller/magnet state will leave
       # callbacks to post results.
       self._yaminon.finish_update()
       super().finish_update()
Migrated from internal repository. Originally created by @EvanKirshenbaum on Aug 17, 2023 at 10:35 AM PDT.