修改workflow上传逻辑,在trash初始化后再开始移液,修改枪头pick和drop的判断

This commit is contained in:
q434343
2026-03-19 02:35:25 +08:00
parent 1a267729e4
commit cdbca70222
2 changed files with 96 additions and 14 deletions

View File

@@ -1469,6 +1469,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
if len(use_channels) != 8: if len(use_channels) != 8:
max_len = max(num_sources, num_targets) max_len = max(num_sources, num_targets)
prev_dropped = True # 循环开始前通道上无 tip
for i in range(max_len): for i in range(max_len):
# 辅助函数: # 辅助函数:
@@ -1528,6 +1529,29 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
if delays is not None: if delays is not None:
kwargs['delays'] = safe_get(delays, i) kwargs['delays'] = safe_get(delays, i)
cur_source = sources[i % num_sources]
cur_target = targets[i % num_targets]
# drop: 仅当下一轮的 source 和 target 都相同时才保留 tip下一轮可以复用
drop_tip = True
if i < max_len - 1:
next_source = sources[(i + 1) % num_sources]
next_target = targets[(i + 1) % num_targets]
if cur_target is next_target and cur_source is next_source:
drop_tip = False
# pick_up: 仅当上一轮保留了 tip未 drop且 source 相同时才复用
pick_up_tip = True
if i > 0 and not prev_dropped:
prev_source = sources[(i - 1) % num_sources]
if cur_source is prev_source:
pick_up_tip = False
prev_dropped = drop_tip
kwargs['pick_up'] = pick_up_tip
kwargs['drop'] = drop_tip
await self._transfer_base_method(**kwargs) await self._transfer_base_method(**kwargs)
@@ -1543,6 +1567,8 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
use_channels: List[int], use_channels: List[int],
asp_vols: List[float], asp_vols: List[float],
dis_vols: List[float], dis_vols: List[float],
pick_up: bool = True,
drop: bool = True,
**kwargs **kwargs
): ):
@@ -1563,8 +1589,9 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
delays = kwargs.get('delays') delays = kwargs.get('delays')
tip = [] tip = []
tip.append(self._get_next_tip()) if pick_up:
await self.pick_up_tips(tip) tip.append(self._get_next_tip())
await self.pick_up_tips(tip)
blow_out_air_volume_before_vol = 0.0 blow_out_air_volume_before_vol = 0.0
if blow_out_air_volume_before is not None and len(blow_out_air_volume_before) > 0: if blow_out_air_volume_before is not None and len(blow_out_air_volume_before) > 0:
blow_out_air_volume_before_vol = float(blow_out_air_volume_before[0] or 0.0) blow_out_air_volume_before_vol = float(blow_out_air_volume_before[0] or 0.0)
@@ -1646,7 +1673,8 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
if delays is not None and len(delays) > 1: if delays is not None and len(delays) > 1:
await self.custom_delay(seconds=delays[0]) await self.custom_delay(seconds=delays[0])
await self.touch_tip(targets[0]) await self.touch_tip(targets[0])
await self.discard_tips(use_channels=use_channels) if drop:
await self.discard_tips(use_channels=use_channels)
# except Exception as e: # except Exception as e:
# traceback.print_exc() # traceback.print_exc()
@@ -1768,25 +1796,75 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
for rack in tip_racks: for rack in tip_racks:
if isinstance(rack, TipSpot): if isinstance(rack, TipSpot):
yield rack yield rack
elif hasattr(rack, "get_all_items"): elif isinstance(rack, TipRack):
yield from rack.get_all_items() for item in rack:
else: if isinstance(item, list):
for tip in rack: yield from item
yield tip else:
yield item
def _get_next_tip(self): def _get_next_tip(self):
"""从 current_tip 迭代器获取下一个 tip耗尽时抛出明确错误而非 StopIteration""" """从 current_tip 迭代器获取下一个 tip耗尽时抛出明确错误而非 StopIteration"""
try: try:
return next(self.current_tip) return next(self.current_tip)
except StopIteration as e: except StopIteration as e:
raise RuntimeError("Tip rack exhausted: no more tips available for transfer") from e diag_parts = []
tip_racks = getattr(self, 'tip_racks', None)
if tip_racks is not None:
for idx, rack in enumerate(tip_racks):
r_name = getattr(rack, 'name', '?')
r_type = type(rack).__name__
is_tr = isinstance(rack, TipRack)
is_ts = isinstance(rack, TipSpot)
n_children = len(getattr(rack, 'children', []))
diag_parts.append(
f"rack[{idx}] name={r_name}, type={r_type}, "
f"is_TipRack={is_tr}, is_TipSpot={is_ts}, children={n_children}"
)
else:
diag_parts.append("tip_racks=None")
by_type = getattr(self, '_tip_racks_by_type', {})
diag_parts.append(f"_tip_racks_by_type keys={list(by_type.keys())}")
raise RuntimeError(
f"Tip rack exhausted: no more tips available for transfer. "
f"Diagnostics: {'; '.join(diag_parts)}"
) from e
def set_tiprack(self, tip_racks: Sequence[TipRack]): def set_tiprack(self, tip_racks: Sequence[TipRack]):
"""Set the tip racks for the liquid handler.""" """Set the tip racks for the liquid handler.
Groups tip racks by type name (``type(rack).__name__``).
- Only actual TipRack / TipSpot instances are registered.
- If a rack has already been registered (by ``name``), it is skipped.
- If a rack is new and its type already exists, it is appended to that type's list.
- If the type is new, a new key-value pair is created.
If the current ``tip_racks`` contain no valid TipRack/TipSpot (e.g. a
Plate was passed by mistake), the iterator falls back to all previously
registered racks.
"""
if not hasattr(self, '_tip_racks_by_type'):
self._tip_racks_by_type: Dict[str, List[TipRack]] = {}
self._seen_rack_names: Set[str] = set()
for rack in tip_racks:
if not isinstance(rack, (TipRack, TipSpot)):
continue
rack_name = rack.name if hasattr(rack, 'name') else str(id(rack))
if rack_name in self._seen_rack_names:
continue
self._seen_rack_names.add(rack_name)
type_key = type(rack).__name__
if type_key not in self._tip_racks_by_type:
self._tip_racks_by_type[type_key] = []
self._tip_racks_by_type[type_key].append(rack)
valid_racks = [r for r in tip_racks if isinstance(r, (TipRack, TipSpot))]
if not valid_racks:
valid_racks = [r for racks in self._tip_racks_by_type.values() for r in racks]
self.tip_racks = tip_racks self.tip_racks = tip_racks
tip_iter = self.iter_tips(tip_racks) self.current_tip = self.iter_tips(valid_racks)
self.current_tip = tip_iter
async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0): async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0):
""" """

View File

@@ -412,6 +412,8 @@ def build_protocol_graph(
param=None, param=None,
) )
trash_create_node_id = None # 记录 trash 的 create_resource 节点
# 为每个唯一的 slot 创建 create_resource 节点 # 为每个唯一的 slot 创建 create_resource 节点
for slot, info in slots_info.items(): for slot, info in slots_info.items():
node_id = str(uuid.uuid4()) node_id = str(uuid.uuid4())
@@ -445,6 +447,8 @@ def build_protocol_graph(
slot_to_create_resource[slot] = node_id slot_to_create_resource[slot] = node_id
if object_type == "tiprack": if object_type == "tiprack":
resource_last_writer[info["labware_id"]] = f"{node_id}:labware" resource_last_writer[info["labware_id"]] = f"{node_id}:labware"
if object_type == "trash":
trash_create_node_id = node_id
# create_resource 之间不需要 ready 连接 # create_resource 之间不需要 ready 连接
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ==================== # ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
@@ -516,8 +520,8 @@ def build_protocol_graph(
# set_liquid_from_plate 的输出 output_wells 用于连接 transfer_liquid # set_liquid_from_plate 的输出 output_wells 用于连接 transfer_liquid
resource_last_writer[labware_id] = f"{node_id}:output_wells" resource_last_writer[labware_id] = f"{node_id}:output_wells"
# transfer_liquid 之间通过 ready 串联,从 None 开始 # transfer_liquid 之间通过 ready 串联;若存在 trash 节点,第一个 transfer_liquid 从 trash 的 ready 开始
last_control_node_id = None last_control_node_id = trash_create_node_id
# 端口名称映射JSON 字段名 -> 实际 handle key # 端口名称映射JSON 字段名 -> 实际 handle key
INPUT_PORT_MAPPING = { INPUT_PORT_MAPPING = {