Closed EvanKirshenbaum closed 9 months ago
This issue was referenced by the following commits before migration:
What would be a good starting point in to code for this issue?
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.
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()
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,
The simplest approach is probably to just reserve
all_neighbors()
of pads in the zone that have drops in them. 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.)
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
That looks like it's on the right track. A couple of comments:
reserved_pads
doesn't need to be a member. It can just be local to reserve_pad()
, but outside of reserve_condition()
.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.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.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.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.
Assuming that wells won't be included in the splash zones
Add to Joey a splash zone radius command line parameter
@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.
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?
Took care or all the comments except the the command line argument on splash radius
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:
joey.Board.__init__()
. Probably optional, defaulting to zero.
Board.__init__()
and stashing it as a Board
property.ExtractionPoint.__init__()
.
None
, with the notion that it asks its board
.Moreover, we would have to pass the command line arguments to
make_board
inExerciser.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 ExtractionPoint
s, 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.
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.
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.