HPInc / HP-Digital-Microfluidics

HP Digital Microfluidics Software Platform and Libraries
MIT License
3 stars 1 forks source link

Add "splash zones" around extraction points (and possibly wells) #57

Closed EvanKirshenbaum closed 9 months ago

EvanKirshenbaum commented 9 months ago

Adding and removing fluid using a pipettor will create a flow within a flooded chip. The current thinking is that while the transfer is happening, nearby drops will need to be held in place and prevented from moving.

Viktor's current thought is that "nearby" for Opentrons probably means about 2 pads in any direction.

My current thought on this is for pads within a "splash zone" around an extraction port will need to have a reference to the port and look at it to see whether there's motion in progress or planned before reserving next pads. Mixes will probably need to do this over their whole space at each step.

If there's motion in progress, the transfer needs to be held off, and if there's a transfer in progress, motion needs to be held off, so this will necessarily be a multi-step process. I'm not thrilled about this, but the alternative would seem to be turning off the clock, which will freeze everybody and screw up wall-clock actions like thermocycling.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Nov 17, 2021 at 10:12 AM PST. Closed on Jun 10, 2022 at 3:56 PM PDT.
EvanKirshenbaum commented 9 months ago

This issue was referenced by the following commits before migration:

EvanKirshenbaum commented 9 months ago

What would be a good starting point in to code for this issue?

Migrated from internal repository. Originally created by Rares Vernica on Mar 17, 2022 at 12:30 PM PDT.
EvanKirshenbaum commented 9 months ago

Probably the best place to start to drill into what goes on is paths.Path, or possibly backing further out to pcr.CombSynth.pipeline_mixes, which uses it a lot. There, you'll see code like

                    paths.append(Path.teleport_into(ep, reagent=frag)
                                    .to_pad(pad)
                                    .start(mix.as_process(n_shuttles=n_shuttles, result=result))
                                    .to_pad(ep.pad)
                                    .reach(passed_by)
                                    .to_row(16)
                                    .then_process(update_drop(c1))
                                    .extended(self.mix_to_tcycle(0)))

and later (out in CombSynth.run()),

Path.run_paths(paths, system=system)

In particular, if you can wrap your head around how

Path.run_paths([Path.teleport_into(ep, reagent=frag).to_pad(pad)], system=system)

obtains a drop of reagent frag via extraction port ep and walks it to pad pad, you'll probably have everything you need to understand what sorts of modifications will be needed to keep other nearby drops that might happen to be walking nearby from being affected.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Mar 17, 2022 at 4:22 PM PDT.
EvanKirshenbaum commented 9 months ago

ExtractionPoint has a prepare_for_add method in which it reserves the destination pad. This might be a good starting point to reserve or hold off the drops in the splash zone:

    def prepare_for_add(self) -> None:
        expect_drop = self.pad.drop is not None
        self.reserve_pad(expect_drop=expect_drop).wait()
        self.pad.schedule(Pad.TurnOn).wait()
Migrated from internal repository. Originally created by Rares Vernica on Apr 25, 2022 at 12:44 PM PDT.
EvanKirshenbaum commented 9 months ago

Right, but you have to be a bit careful, because drops in the zone may have already reserved a pad for their next step (which will lead to them turning their current pad off), and that pad may be outside the zone. (I don't actually know if that will be a problem, but let's assume it will.)

Basically,

  1. You have to reserve all pads around any drop in the zone. If you can't, it means that somebody else has, and you need to wait a tick to try again. (You don't have to release the reservations you have, so this shouldn't be a race.)
  2. You have to reserve all pads on the boundary of the zone, so that no drops can walk in. Same caveats apply.

The simplest approach is probably to just reserve

  1. All pads in the zone
  2. All pads on all_neighbors() of pads in the zone that have drops in them.
  3. Keep track of a set of pads that you've already reserved, so that you don't block yourself.
  4. If there are some for which reserve() fails, keep doing as much as you can, but loop around in a tick to finish.

This can probably all be done by generalizing what happens in reserve_pad().

And in pipettor_added(), when last is True, you need to unreserve all the pads you reserved.

And, of course, the same sort of thing needs to happen in prepare_for_remove() and pipettor_removed(),

(Just to get it on record, in case this becomes a problem, the Blob being removed may extend outside of the splash zone. It's possible we will need to extend the zone to cover its boundaries.)

Migrated from internal repository. Originally created by @EvanKirshenbaum on Apr 26, 2022 at 11:10 AM PDT.
EvanKirshenbaum commented 9 months ago

With the current commit, the neighboring pads (1st level) are reserved in reserve_pad and freed in pipettor_added. This is the key commit

Here is a log message sequence for three agents:

    def run(self, board:Board, system:System, args:Namespace)->None:
        ep = board.extraction_points[0]
        Path.run_paths([
            Path.teleport_into(ep, reagent=Reagent('R1')).to_pad((11, 15)),
            Path.teleport_into(ep, reagent=Reagent('R2')).to_pad((11, 13)),
            Path.teleport_into(ep, reagent=Reagent('R3')).to_pad((11, 17))
        ], system=system)
> python tools/joey.py dev --clock-speed=100ms --initial-delay=0s --log-level=debug]
   466|  DEBUG|Timer Thread|engine.py:371:run|Timer Thread started
   781|  DEBUG|Clock Thread|engine.py:517:run|Clock Thread started
   782|   INFO|Monitored dev|pipettor.py:256:not_idle|Pipettor("Dummy") is not idle
   782|  DEBUG|Pipettor("Dummy") Thread|types.py:3552:run|queue len:1|before_task:None|after_task:None
   782|  DEBUG|Pipettor("Dummy") Thread|types.py:3558:run|func:TransferSchedule._schedule.<locals>.run_it
   979|   INFO|MainThread|monitor.py:1123:new_speed|Setting tick to 100.0 ms
  1099|   INFO|Pipettor("Dummy") Thread|dummy_pipettor.py:106:perform|Aspirating 1.0 μl of R1 from reagent block.
  1300|  DEBUG|Pipettor("Dummy") Thread|device.py:3178:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,15)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,16)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,16)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,15)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,14)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,14)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,14)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,15)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,16)
  1301|  DEBUG|Pipettor("Dummy") Thread|device.py:3191:reserve_condition|Pad(13,15)|splash|reserved all:{Pad(12,14), Pad(12,15), Pad(12,16), Pad(14,14), Pad(14,15), Pad(14,16), Pad(13,14), Pad(13,15), Pad(13,16)}
  1386|  DEBUG|Device Communication Thread|engine.py:239:run|Device Communication Thread started
  1397|   INFO|Pipettor("Dummy") Thread|dummy_pipettor.py:116:perform|Dispensing 1.0 μl of R1 to ExtractionPoint[XYCoord(13,15)].
  1398|  DEBUG|Pipettor("Dummy") Thread|device.py:3092:pipettor_added|Pad(13,15)|splash|unreserve:{Pad(12,14), Pad(12,15), Pad(12,16), Pad(14,14), Pad(14,15), Pad(14,16), Pad(13,14), Pad(13,15), Pad(13,16)}
  1701|  DEBUG|Pipettor("Dummy") Thread|types.py:3558:run|func:TransferSchedule._schedule.<locals>.run_it
  2015|   INFO|Pipettor("Dummy") Thread|dummy_pipettor.py:106:perform|Aspirating 1.0 μl of R2 from reagent block.
  2217|  DEBUG|Pipettor("Dummy") Thread|device.py:3178:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,15)
  2217|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,16)
  2217|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,16)
  2217|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,15)
  2217|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,14)
  2218|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,14)
  2218|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,14)
  2218|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,15)
  2218|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,16)
  2218|  DEBUG|Pipettor("Dummy") Thread|device.py:3191:reserve_condition|Pad(13,15)|splash|reserved all:{Pad(12,14), Pad(12,15), Pad(12,16), Pad(14,14), Pad(14,15), Pad(14,16), Pad(13,14), Pad(13,15), Pad(13,16)}
  2302|   INFO|Pipettor("Dummy") Thread|dummy_pipettor.py:116:perform|Dispensing 1.0 μl of R2 to ExtractionPoint[XYCoord(13,15)].
  2303|  DEBUG|Pipettor("Dummy") Thread|device.py:3092:pipettor_added|Pad(13,15)|splash|unreserve:{Pad(12,14), Pad(12,15), Pad(12,16), Pad(14,14), Pad(14,15), Pad(14,16), Pad(13,14), Pad(13,15), Pad(13,16)}
  2607|  DEBUG|Pipettor("Dummy") Thread|types.py:3558:run|func:TransferSchedule._schedule.<locals>.run_it
  2922|   INFO|Pipettor("Dummy") Thread|dummy_pipettor.py:106:perform|Aspirating 1.0 μl of R3 from reagent block.
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3178:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,15)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,16)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,16)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,15)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(14,14)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(13,14)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,14)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,15)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3188:reserve_condition|Pad(13,15)|splash|reserved:Pad(12,16)
  3124|  DEBUG|Pipettor("Dummy") Thread|device.py:3191:reserve_condition|Pad(13,15)|splash|reserved all:{Pad(12,14), Pad(12,15), Pad(12,16), Pad(14,14), Pad(14,15), Pad(14,16), Pad(13,14), Pad(13,15), Pad(13,16)}
  3209|   INFO|Pipettor("Dummy") Thread|dummy_pipettor.py:116:perform|Dispensing 1.0 μl of R3 to ExtractionPoint[XYCoord(13,15)].
  3209|  DEBUG|Pipettor("Dummy") Thread|device.py:3092:pipettor_added|Pad(13,15)|splash|unreserve:{Pad(12,14), Pad(12,15), Pad(12,16), Pad(14,14), Pad(14,15), Pad(14,16), Pad(13,14), Pad(13,15), Pad(13,16)}
  3513|   INFO|Pipettor("Dummy") Thread|pipettor.py:252:idle|Pipettor("Dummy") is idle
Migrated from internal repository. Originally created by Rares Vernica on May 02, 2022 at 10:03 PM PDT.
EvanKirshenbaum commented 9 months ago

That looks like it's on the right track. A couple of comments:

  1. reserved_pads doesn't need to be a member. It can just be local to reserve_pad(), but outside of reserve_condition().
  2. You're returning False at the first pad reservation failure. I'd recommend doing as much as you can each time and returning True or False based on whether you're complete.
  3. You have essentially the same code for pad reservation in two places. I'd recommend starting by computing a to_reserve set (in reserve_pad() outside of reserve_condition()) with the ExtractionPoint's pad and everything in the splash zone (if there is one), and having reserve_condition() run through this set and moving pads to reserved_pads when they're successfully reserved. Then, you can just return len(to_reserve) == 0 as the value. This would also obviate the membership check.
  4. Different boards will have different sized splash zones, so the splash_zone parameter should probably be a numeric radius rather than a boolean. And it should probably be optional, defaulting to a property of the extraction point, set at construction (itself probably optional, defaulting to zero.) Then you can have a compute_splash_zone(radius) method that returns a set of pads.
  5. The code doesn't take into account the neighborhood of any drops inside the splash zone. You might want a secondary set for neighbors of drops found in to_reserve that aren't themselves in to_reserve or reserved_pads. If you don't do this, the splash might push a drop in the zone that turns off its electrode to move out of the zone.
    Migrated from internal repository. Originally created by @EvanKirshenbaum on May 03, 2022 at 11:02 AM PDT.
EvanKirshenbaum commented 9 months ago

Assuming that wells won't be included in the splash zones

Migrated from internal repository. Originally created by Rares Vernica on May 03, 2022 at 12:20 PM PDT.
EvanKirshenbaum commented 9 months ago

Add to Joey a splash zone radius command line parameter

Migrated from internal repository. Originally created by Rares Vernica on May 03, 2022 at 1:09 PM PDT.
EvanKirshenbaum commented 9 months ago

@EvanKirshenbaum we discussed having a property getter and setter for Pad.reserved. I'm not sure this is best as we are relying on the return of the current Pad.reserve() function to see if the pad was successfully reserved or not. I'm not sure how we can capture the success of the operation with a property setter.

Migrated from internal repository. Originally created by Rares Vernica on May 09, 2022 at 12:46 PM PDT.
EvanKirshenbaum commented 9 months ago

Regarding having a command line parameter for the splash zone radius, I'm not sure how this would fit in. When constructing a joey board, currently only the pipette can be specified. The rest is hard coded. Do we want to modify joey.Board to take a splash zone argument that gets passed to the ExtractionPoints? Moreover, we would have to pass the command line arguments to make_board in Exerciser.parse_args_and_run.

Or do we want to dynamically modify the ExtractionPoints splash zone radius in Exerciser.parse_args_and_run after they have been created based on the command line arguments?

Also, is this command line argument just for the joey boards?

Migrated from internal repository. Originally created by Rares Vernica on May 09, 2022 at 7:05 PM PDT.
EvanKirshenbaum commented 9 months ago

Took care or all the comments except the the command line argument on splash radius

Migrated from internal repository. Originally created by Rares Vernica on May 09, 2022 at 7:27 PM PDT.
EvanKirshenbaum commented 9 months ago

Regarding having a command line parameter for the splash zone radius, I'm not sure how this would fit in. When constructing a joey board, currently only the pipette can be specified. The rest is hard coded. Do we want to modify joey.Board to take a splash zone argument that gets passed to the ExtractionPoints?

My recommendations would be:

Moreover, we would have to pass the command line arguments to make_board in Exerciser.parse_args_and_run.

They're already there:

    @abstractmethod
    def make_board(self, args: Namespace) -> Board: ...  # @UnusedVariable

Or do we want to dynamically modify the ExtractionPoints splash zone radius in Exerciser.parse_args_and_run after they have been created based on the command line arguments?

I'd say that for now, let's make it a Final property on the ExtractionPoints, set at construction.

Also, is this command line argument just for the joey boards?

If you make it a property of Board, I'd do it up in Exeerciser.add_common_args_to(). Otherwise, put it in JoeyExerciser for now.

Migrated from internal repository. Originally created by @EvanKirshenbaum on May 10, 2022 at 10:45 AM PDT.
EvanKirshenbaum commented 9 months ago

Okay, I think we can call this done. While testing it, I did notice a potentially undesirable interaction with mixing processes, and I opened up a new issue for it (#166). It would only show up with relatively large splash zones (or mixes very close to extraction points), and it's not exactly wrong, just less efficient than it could be.

I also added an issue (#168) for figuring out a way to make the splash zones round. This probably doesn't need to be done any time soon.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Jun 10, 2022 at 3:56 PM PDT.