mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 05:48:20 +00:00
merge lab_resource,并修改transfer liquid
This commit is contained in:
@@ -1177,6 +1177,11 @@ class QueueProcessor:
|
|||||||
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
||||||
|
|
||||||
for job_info in queued_jobs:
|
for job_info in queued_jobs:
|
||||||
|
# 快照可能已过期:在遍历过程中 end_job() 可能已将此 job 移至 READY,
|
||||||
|
# 此时不应再发送 busy/need_more,否则会覆盖已发出的 free=True 通知
|
||||||
|
if job_info.status != JobStatus.QUEUE:
|
||||||
|
continue
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
"action": "report_action_state",
|
"action": "report_action_state",
|
||||||
"data": {
|
"data": {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from pylabrobot.resources import (
|
|||||||
ResourceHolder,
|
ResourceHolder,
|
||||||
Lid,
|
Lid,
|
||||||
Trash,
|
Trash,
|
||||||
Tip,
|
Tip, TubeRack,
|
||||||
)
|
)
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
@@ -208,7 +208,8 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
):
|
):
|
||||||
|
if spread == "":
|
||||||
|
spread = "wide"
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.aspirate(
|
return await self._simulate_handler.aspirate(
|
||||||
resources,
|
resources,
|
||||||
@@ -221,17 +222,33 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread,
|
spread,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
)
|
)
|
||||||
await super().aspirate(
|
try:
|
||||||
resources,
|
await super().aspirate(
|
||||||
vols,
|
resources,
|
||||||
use_channels,
|
vols,
|
||||||
flow_rates,
|
use_channels,
|
||||||
offsets,
|
flow_rates,
|
||||||
liquid_height,
|
offsets,
|
||||||
blow_out_air_volume,
|
liquid_height,
|
||||||
spread,
|
blow_out_air_volume,
|
||||||
**backend_kwargs,
|
spread,
|
||||||
)
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
||||||
|
await super().aspirate(
|
||||||
|
resources,
|
||||||
|
vols,
|
||||||
|
use_channels,
|
||||||
|
flow_rates,
|
||||||
|
offsets,
|
||||||
|
liquid_height,
|
||||||
|
blow_out_air_volume,
|
||||||
|
spread="custom",
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
res_samples = []
|
res_samples = []
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
@@ -243,7 +260,8 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
channels_to_use = use_channels
|
channels_to_use = use_channels
|
||||||
|
|
||||||
for resource, volume, channel in zip(resources, vols, channels_to_use):
|
for resource, volume, channel in zip(resources, vols, channels_to_use):
|
||||||
res_samples.append({"name": resource.name, "sample_uuid": resource.unilabos_extra.get("sample_uuid", None)})
|
sample_uuid_value = getattr(resource, "unilabos_extra", {}).get(EXTRA_SAMPLE_UUID, None)
|
||||||
|
res_samples.append({"name": resource.name, "sample_uuid": sample_uuid_value})
|
||||||
res_volumes.append(volume)
|
res_volumes.append(volume)
|
||||||
self.pending_liquids_dict[channel] = {
|
self.pending_liquids_dict[channel] = {
|
||||||
EXTRA_SAMPLE_UUID: sample_uuid_value,
|
EXTRA_SAMPLE_UUID: sample_uuid_value,
|
||||||
@@ -263,6 +281,8 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
) -> SimpleReturn:
|
) -> SimpleReturn:
|
||||||
|
if spread == "":
|
||||||
|
spread = "wide"
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.dispense(
|
return await self._simulate_handler.dispense(
|
||||||
resources,
|
resources,
|
||||||
@@ -275,16 +295,33 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread,
|
spread,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
)
|
)
|
||||||
await super().dispense(
|
try:
|
||||||
resources,
|
await super().dispense(
|
||||||
vols,
|
resources,
|
||||||
use_channels,
|
vols,
|
||||||
flow_rates,
|
use_channels,
|
||||||
offsets,
|
flow_rates,
|
||||||
liquid_height,
|
offsets,
|
||||||
blow_out_air_volume,
|
liquid_height,
|
||||||
**backend_kwargs,
|
blow_out_air_volume,
|
||||||
)
|
spread,
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
||||||
|
await super().dispense(
|
||||||
|
resources,
|
||||||
|
vols,
|
||||||
|
use_channels,
|
||||||
|
flow_rates,
|
||||||
|
offsets,
|
||||||
|
liquid_height,
|
||||||
|
blow_out_air_volume,
|
||||||
|
"custom",
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
res_samples = []
|
res_samples = []
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
for resource, volume, channel in zip(resources, vols, use_channels):
|
for resource, volume, channel in zip(resources, vols, use_channels):
|
||||||
@@ -702,10 +739,13 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
|
|
||||||
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
|
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
|
||||||
"""
|
"""
|
||||||
assert issubclass(plate.__class__, Plate), "plate must be a Plate"
|
assert issubclass(plate.__class__, Plate) or issubclass(plate.__class__, TubeRack) , f"plate must be a Plate, now: {type(plate)}"
|
||||||
plate: Plate = cast(Plate, cast(Resource, plate))
|
plate: Union[Plate, TubeRack]
|
||||||
# 根据 well_names 获取对应的 Well 对象
|
# 根据 well_names 获取对应的 Well 对象
|
||||||
wells = [plate.get_well(name) for name in well_names]
|
if issubclass(plate.__class__, Plate):
|
||||||
|
wells = [plate.get_well(name) for name in well_names]
|
||||||
|
elif issubclass(plate.__class__, TubeRack):
|
||||||
|
wells = [plate.get_tube(name) for name in well_names]
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
|
|
||||||
# 如果 liquid_names 和 volumes 都为空,直接返回
|
# 如果 liquid_names 和 volumes 都为空,直接返回
|
||||||
@@ -851,7 +891,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(sources)):
|
for _ in range(len(sources)):
|
||||||
tip = []
|
tip = []
|
||||||
for __ in range(len(use_channels)):
|
for __ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
resources=[sources[_]],
|
resources=[sources[_]],
|
||||||
@@ -891,7 +931,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(sources), 8):
|
for i in range(0, len(sources), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = waste_liquid[i : i + 8]
|
current_targets = waste_liquid[i : i + 8]
|
||||||
current_reagent_sources = sources[i : i + 8]
|
current_reagent_sources = sources[i : i + 8]
|
||||||
@@ -985,7 +1025,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(targets)):
|
for _ in range(len(targets)):
|
||||||
tip = []
|
tip = []
|
||||||
for x in range(len(use_channels)):
|
for x in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1037,7 +1077,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
current_reagent_sources = reagent_sources[i : i + 8]
|
current_reagent_sources = reagent_sources[i : i + 8]
|
||||||
@@ -1162,11 +1202,19 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
Number of mix cycles. If *None* (default) no mixing occurs regardless of
|
Number of mix cycles. If *None* (default) no mixing occurs regardless of
|
||||||
mix_stage.
|
mix_stage.
|
||||||
"""
|
"""
|
||||||
|
num_sources = len(sources)
|
||||||
|
num_targets = len(targets)
|
||||||
|
len_asp_vols = len(asp_vols)
|
||||||
|
len_dis_vols = len(dis_vols)
|
||||||
# 确保 use_channels 有默认值
|
# 确保 use_channels 有默认值
|
||||||
if use_channels is None:
|
if use_channels is None:
|
||||||
# 默认使用设备所有通道(例如 8 通道移液站默认就是 0-7)
|
# 默认使用设备所有通道(例如 8 通道移液站默认就是 0-7)
|
||||||
use_channels = list(range(self.channel_num)) if self.channel_num > 0 else [0]
|
use_channels = list(range(self.channel_num)) if self.channel_num == 8 else [0]
|
||||||
|
elif len(use_channels) == 8:
|
||||||
|
if self.channel_num != 8:
|
||||||
|
raise ValueError(f"if channel_num is 8, use_channels length must be 8, but got {len(use_channels)}")
|
||||||
|
if num_sources%8 != 0 or num_targets%8 != 0 or len_asp_vols%8 != 0 or len_dis_vols%8 != 0:
|
||||||
|
raise ValueError(f"if channel_num is 8, sources, targets, asp_vols, and dis_vols length must be divisible by 8, but got {num_sources}, {num_targets}, {len_asp_vols}, and {len_dis_vols}")
|
||||||
|
|
||||||
if is_96_well:
|
if is_96_well:
|
||||||
pass # This mode is not verified.
|
pass # This mode is not verified.
|
||||||
@@ -1200,87 +1248,228 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
# 识别传输模式(mix_times 为 None 也应该能正常移液,只是不做 mix)
|
# 识别传输模式(mix_times 为 None 也应该能正常移液,只是不做 mix)
|
||||||
num_sources = len(sources)
|
num_sources = len(sources)
|
||||||
num_targets = len(targets)
|
num_targets = len(targets)
|
||||||
|
len_asp_vols = len(asp_vols)
|
||||||
|
len_dis_vols = len(dis_vols)
|
||||||
|
|
||||||
if num_sources == 1 and num_targets > 1:
|
# if num_targets != 1 and num_sources != 1:
|
||||||
# 模式1: 一对多 (1 source -> N targets)
|
# if len_asp_vols != num_sources and len_asp_vols != num_targets:
|
||||||
await self._transfer_one_to_many(
|
# raise ValueError(f"asp_vols length must be equal to sources or targets length, but got {len_asp_vols} and {num_sources} and {num_targets}")
|
||||||
sources[0],
|
# if len_dis_vols != num_sources and len_dis_vols != num_targets:
|
||||||
targets,
|
# raise ValueError(f"dis_vols length must be equal to sources or targets length, but got {len_dis_vols} and {num_sources} and {num_targets}")
|
||||||
tip_racks,
|
|
||||||
use_channels,
|
if len(use_channels) != 8:
|
||||||
asp_vols,
|
max_len = max(num_sources, num_targets)
|
||||||
dis_vols,
|
for i in range(max_len):
|
||||||
asp_flow_rates,
|
|
||||||
dis_flow_rates,
|
# 辅助函数:安全地从列表中获取元素,如果列表为空则返回None
|
||||||
offsets,
|
def safe_get(lst, idx, default=None):
|
||||||
touch_tip,
|
return [lst[idx]] if lst else default
|
||||||
liquid_height,
|
|
||||||
blow_out_air_volume,
|
# 动态构建参数字典,只传递实际提供的参数
|
||||||
spread,
|
kwargs = {
|
||||||
mix_stage,
|
'sources': [sources[i%num_sources]],
|
||||||
mix_times,
|
'targets': [targets[i%num_targets]],
|
||||||
mix_vol,
|
'tip_racks': tip_racks,
|
||||||
mix_rate,
|
'use_channels': use_channels,
|
||||||
mix_liquid_height,
|
'asp_vols': [asp_vols[i%len_asp_vols]],
|
||||||
delays,
|
'dis_vols': [dis_vols[i%len_dis_vols]],
|
||||||
)
|
}
|
||||||
elif num_sources > 1 and num_targets == 1:
|
|
||||||
# 模式2: 多对一 (N sources -> 1 target)
|
# 条件性添加可选参数
|
||||||
await self._transfer_many_to_one(
|
if asp_flow_rates is not None:
|
||||||
sources,
|
kwargs['asp_flow_rates'] = [asp_flow_rates[i%len_asp_vols]]
|
||||||
targets[0],
|
if dis_flow_rates is not None:
|
||||||
tip_racks,
|
kwargs['dis_flow_rates'] = [dis_flow_rates[i%len_dis_vols]]
|
||||||
use_channels,
|
if offsets is not None:
|
||||||
asp_vols,
|
kwargs['offsets'] = safe_get(offsets, i)
|
||||||
dis_vols,
|
if touch_tip is not None:
|
||||||
asp_flow_rates,
|
kwargs['touch_tip'] = touch_tip if touch_tip else False
|
||||||
dis_flow_rates,
|
if liquid_height is not None:
|
||||||
offsets,
|
kwargs['liquid_height'] = safe_get(liquid_height, i)
|
||||||
touch_tip,
|
if blow_out_air_volume is not None:
|
||||||
liquid_height,
|
kwargs['blow_out_air_volume'] = safe_get(blow_out_air_volume, i)
|
||||||
blow_out_air_volume,
|
if spread is not None:
|
||||||
spread,
|
kwargs['spread'] = spread
|
||||||
mix_stage,
|
if mix_stage is not None:
|
||||||
mix_times,
|
kwargs['mix_stage'] = safe_get(mix_stage, i)
|
||||||
mix_vol,
|
if mix_times is not None:
|
||||||
mix_rate,
|
kwargs['mix_times'] = safe_get(mix_times, i)
|
||||||
mix_liquid_height,
|
if mix_vol is not None:
|
||||||
delays,
|
kwargs['mix_vol'] = safe_get(mix_vol, i)
|
||||||
)
|
if mix_rate is not None:
|
||||||
elif num_sources == num_targets:
|
kwargs['mix_rate'] = safe_get(mix_rate, i)
|
||||||
# 模式3: 一对一 (N sources -> N targets)
|
if mix_liquid_height is not None:
|
||||||
await self._transfer_one_to_one(
|
kwargs['mix_liquid_height'] = safe_get(mix_liquid_height, i)
|
||||||
sources,
|
if delays is not None:
|
||||||
targets,
|
kwargs['delays'] = safe_get(delays, i)
|
||||||
tip_racks,
|
|
||||||
use_channels,
|
await self._transfer_base_method(**kwargs)
|
||||||
asp_vols,
|
|
||||||
dis_vols,
|
|
||||||
asp_flow_rates,
|
|
||||||
dis_flow_rates,
|
# if num_sources == 1 and num_targets > 1:
|
||||||
offsets,
|
# # 模式1: 一对多 (1 source -> N targets)
|
||||||
touch_tip,
|
# await self._transfer_one_to_many(
|
||||||
liquid_height,
|
# sources,
|
||||||
blow_out_air_volume,
|
# targets,
|
||||||
spread,
|
# tip_racks,
|
||||||
mix_stage,
|
# use_channels,
|
||||||
mix_times,
|
# asp_vols,
|
||||||
mix_vol,
|
# dis_vols,
|
||||||
mix_rate,
|
# asp_flow_rates,
|
||||||
mix_liquid_height,
|
# dis_flow_rates,
|
||||||
delays,
|
# offsets,
|
||||||
)
|
# touch_tip,
|
||||||
else:
|
# liquid_height,
|
||||||
raise ValueError(
|
# blow_out_air_volume,
|
||||||
f"Unsupported transfer mode: {num_sources} sources -> {num_targets} targets. "
|
# spread,
|
||||||
"Supported modes: 1->N, N->1, or N->N."
|
# mix_stage,
|
||||||
)
|
# mix_times,
|
||||||
|
# mix_vol,
|
||||||
|
# mix_rate,
|
||||||
|
# mix_liquid_height,
|
||||||
|
# delays,
|
||||||
|
# )
|
||||||
|
# elif num_sources > 1 and num_targets == 1:
|
||||||
|
# # 模式2: 多对一 (N sources -> 1 target)
|
||||||
|
# await self._transfer_many_to_one(
|
||||||
|
# sources,
|
||||||
|
# targets[0],
|
||||||
|
# tip_racks,
|
||||||
|
# use_channels,
|
||||||
|
# asp_vols,
|
||||||
|
# dis_vols,
|
||||||
|
# asp_flow_rates,
|
||||||
|
# dis_flow_rates,
|
||||||
|
# offsets,
|
||||||
|
# touch_tip,
|
||||||
|
# liquid_height,
|
||||||
|
# blow_out_air_volume,
|
||||||
|
# spread,
|
||||||
|
# mix_stage,
|
||||||
|
# mix_times,
|
||||||
|
# mix_vol,
|
||||||
|
# mix_rate,
|
||||||
|
# mix_liquid_height,
|
||||||
|
# delays,
|
||||||
|
# )
|
||||||
|
# elif num_sources == num_targets:
|
||||||
|
# # 模式3: 一对一 (N sources -> N targets)
|
||||||
|
# await self._transfer_one_to_one(
|
||||||
|
# sources,
|
||||||
|
# targets,
|
||||||
|
# tip_racks,
|
||||||
|
# use_channels,
|
||||||
|
# asp_vols,
|
||||||
|
# dis_vols,
|
||||||
|
# asp_flow_rates,
|
||||||
|
# dis_flow_rates,
|
||||||
|
# offsets,
|
||||||
|
# touch_tip,
|
||||||
|
# liquid_height,
|
||||||
|
# blow_out_air_volume,
|
||||||
|
# spread,
|
||||||
|
# mix_stage,
|
||||||
|
# mix_times,
|
||||||
|
# mix_vol,
|
||||||
|
# mix_rate,
|
||||||
|
# mix_liquid_height,
|
||||||
|
# delays,
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# raise ValueError(
|
||||||
|
# f"Unsupported transfer mode: {num_sources} sources -> {num_targets} targets. "
|
||||||
|
# "Supported modes: 1->N, N->1, or N->N."
|
||||||
|
# )
|
||||||
|
|
||||||
return TransferLiquidReturn(
|
return TransferLiquidReturn(
|
||||||
sources=ResourceTreeSet.from_plr_resources(list(sources), known_newly_created=False).dump(), # type: ignore
|
sources=ResourceTreeSet.from_plr_resources(list(sources), known_newly_created=False).dump(), # type: ignore
|
||||||
targets=ResourceTreeSet.from_plr_resources(list(targets), known_newly_created=False).dump(), # type: ignore
|
targets=ResourceTreeSet.from_plr_resources(list(targets), known_newly_created=False).dump(), # type: ignore
|
||||||
)
|
)
|
||||||
|
async def _transfer_base_method(
|
||||||
|
self,
|
||||||
|
sources: Sequence[Container],
|
||||||
|
targets: Sequence[Container],
|
||||||
|
tip_racks: Sequence[TipRack],
|
||||||
|
use_channels: List[int],
|
||||||
|
asp_vols: List[float],
|
||||||
|
dis_vols: List[float],
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
|
||||||
|
# 从kwargs中提取参数,提供默认值
|
||||||
|
asp_flow_rates = kwargs.get('asp_flow_rates')
|
||||||
|
dis_flow_rates = kwargs.get('dis_flow_rates')
|
||||||
|
offsets = kwargs.get('offsets')
|
||||||
|
touch_tip = kwargs.get('touch_tip', False)
|
||||||
|
liquid_height = kwargs.get('liquid_height')
|
||||||
|
blow_out_air_volume = kwargs.get('blow_out_air_volume')
|
||||||
|
spread = kwargs.get('spread', 'wide')
|
||||||
|
mix_stage = kwargs.get('mix_stage')
|
||||||
|
mix_times = kwargs.get('mix_times')
|
||||||
|
mix_vol = kwargs.get('mix_vol')
|
||||||
|
mix_rate = kwargs.get('mix_rate')
|
||||||
|
mix_liquid_height = kwargs.get('mix_liquid_height')
|
||||||
|
delays = kwargs.get('delays')
|
||||||
|
|
||||||
|
tip = []
|
||||||
|
tip.extend(self._get_next_tip())
|
||||||
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
|
await self.mix(
|
||||||
|
targets=[targets[0]],
|
||||||
|
mix_time=mix_times,
|
||||||
|
mix_vol=mix_vol,
|
||||||
|
offsets=offsets if offsets else None,
|
||||||
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
|
use_channels=use_channels,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.aspirate(
|
||||||
|
resources=[sources[0]],
|
||||||
|
vols=[asp_vols[0]],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[asp_flow_rates[0]] if asp_flow_rates and len(asp_flow_rates) > 0 else None,
|
||||||
|
offsets=[offsets[0]] if offsets and len(offsets) > 0 else None,
|
||||||
|
liquid_height=[liquid_height[0]] if liquid_height and len(liquid_height) > 0 else None,
|
||||||
|
blow_out_air_volume=(
|
||||||
|
[blow_out_air_volume[0]] if blow_out_air_volume and len(blow_out_air_volume) > 0 else None
|
||||||
|
),
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
if delays is not None:
|
||||||
|
await self.custom_delay(seconds=delays[0])
|
||||||
|
await self.dispense(
|
||||||
|
resources=[targets[0]],
|
||||||
|
vols=[dis_vols[0]],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[dis_flow_rates[0]] if dis_flow_rates and len(dis_flow_rates) > 0 else None,
|
||||||
|
offsets=[offsets[0]] if offsets and len(offsets) > 0 else None,
|
||||||
|
blow_out_air_volume=(
|
||||||
|
[blow_out_air_volume[0]] if blow_out_air_volume and len(blow_out_air_volume) > 0 else None
|
||||||
|
),
|
||||||
|
liquid_height=[liquid_height[0]] if liquid_height and len(liquid_height) > 0 else None,
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
if delays is not None and len(delays) > 1:
|
||||||
|
await self.custom_delay(seconds=delays[1])
|
||||||
|
if mix_stage in ["after", "both"] and mix_times is not None and mix_times > 0:
|
||||||
|
await self.mix(
|
||||||
|
targets=[targets[0]],
|
||||||
|
mix_time=mix_times,
|
||||||
|
mix_vol=mix_vol,
|
||||||
|
offsets=offsets if offsets else None,
|
||||||
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
|
use_channels=use_channels,
|
||||||
|
)
|
||||||
|
if delays is not None and len(delays) > 1:
|
||||||
|
await self.custom_delay(seconds=delays[0])
|
||||||
|
await self.touch_tip(targets[0])
|
||||||
|
await self.discard_tips(use_channels=use_channels)
|
||||||
|
|
||||||
async def _transfer_one_to_one(
|
async def _transfer_one_to_one(
|
||||||
self,
|
self,
|
||||||
sources: Sequence[Container],
|
sources: Sequence[Container],
|
||||||
@@ -1322,7 +1511,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(targets)):
|
for _ in range(len(targets)):
|
||||||
tip = []
|
tip = []
|
||||||
for ___ in range(len(use_channels)):
|
for ___ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
@@ -1386,7 +1575,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
current_reagent_sources = sources[i : i + 8]
|
current_reagent_sources = sources[i : i + 8]
|
||||||
@@ -1491,7 +1680,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
# 单通道模式:一次吸液,多次分液
|
# 单通道模式:一次吸液,多次分液
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
@@ -1563,7 +1752,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
@@ -1702,7 +1891,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.mix(
|
await self.mix(
|
||||||
@@ -1721,7 +1910,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for idx, source in enumerate(sources):
|
for idx, source in enumerate(sources):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1804,7 +1993,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.mix(
|
await self.mix(
|
||||||
@@ -1822,7 +2011,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(sources), 8):
|
for i in range(0, len(sources), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
current_sources = sources[i : i + 8]
|
current_sources = sources[i : i + 8]
|
||||||
@@ -2022,7 +2211,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for rack in tip_racks:
|
for rack in tip_racks:
|
||||||
for tip in rack:
|
for tip in rack:
|
||||||
yield tip
|
yield tip
|
||||||
raise RuntimeError("Out of tips!")
|
# raise RuntimeError("Out of tips!")
|
||||||
|
|
||||||
|
def _get_next_tip(self):
|
||||||
|
"""从 current_tip 迭代器获取下一个 tip,耗尽时抛出明确错误而非 StopIteration"""
|
||||||
|
try:
|
||||||
|
return next(self.current_tip)
|
||||||
|
except StopIteration as e:
|
||||||
|
raise RuntimeError("Tip rack exhausted: no more tips available for transfer") 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."""
|
||||||
|
|||||||
@@ -928,57 +928,35 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
) -> TransferLiquidReturn:
|
) -> TransferLiquidReturn:
|
||||||
if self.step_mode:
|
if self.step_mode:
|
||||||
self._unilabos_backend.create_protocol(f"step_mode_protocol_{time.time()}")
|
await self.create_protocol(f"step_mode_protocol_{time.time()}")
|
||||||
|
|
||||||
res =await super().transfer_liquid(
|
res =await super().transfer_liquid(
|
||||||
sources,
|
sources,
|
||||||
targets,
|
targets,
|
||||||
tip_racks,
|
tip_racks,
|
||||||
use_channels=use_channels,
|
use_channels=use_channels,
|
||||||
asp_vols=asp_vols,
|
asp_vols=asp_vols,
|
||||||
dis_vols=dis_vols,
|
dis_vols=dis_vols,
|
||||||
asp_flow_rates=asp_flow_rates,
|
asp_flow_rates=asp_flow_rates,
|
||||||
dis_flow_rates=dis_flow_rates,
|
dis_flow_rates=dis_flow_rates,
|
||||||
offsets=offsets,
|
offsets=offsets,
|
||||||
touch_tip=touch_tip,
|
touch_tip=touch_tip,
|
||||||
liquid_height=liquid_height,
|
liquid_height=liquid_height,
|
||||||
blow_out_air_volume=blow_out_air_volume,
|
blow_out_air_volume=blow_out_air_volume,
|
||||||
spread=spread,
|
spread=spread,
|
||||||
is_96_well=is_96_well,
|
is_96_well=is_96_well,
|
||||||
mix_stage=mix_stage,
|
mix_stage=mix_stage,
|
||||||
mix_times=mix_times,
|
mix_times=mix_times,
|
||||||
mix_vol=mix_vol,
|
mix_vol=mix_vol,
|
||||||
mix_rate=mix_rate,
|
mix_rate=mix_rate,
|
||||||
mix_liquid_height=mix_liquid_height,
|
mix_liquid_height=mix_liquid_height,
|
||||||
delays=delays,
|
delays=delays,
|
||||||
none_keys=none_keys,
|
none_keys=none_keys,
|
||||||
)
|
)
|
||||||
self._unilabos_backend.run_protocol()
|
if self.step_mode:
|
||||||
return res
|
await self.run_protocol()
|
||||||
else:
|
|
||||||
return await super().transfer_liquid(
|
return res
|
||||||
sources,
|
|
||||||
targets,
|
|
||||||
tip_racks,
|
|
||||||
use_channels=use_channels,
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
asp_flow_rates=asp_flow_rates,
|
|
||||||
dis_flow_rates=dis_flow_rates,
|
|
||||||
offsets=offsets,
|
|
||||||
touch_tip=touch_tip,
|
|
||||||
liquid_height=liquid_height,
|
|
||||||
blow_out_air_volume=blow_out_air_volume,
|
|
||||||
spread=spread,
|
|
||||||
is_96_well=is_96_well,
|
|
||||||
mix_stage=mix_stage,
|
|
||||||
mix_times=mix_times,
|
|
||||||
mix_vol=mix_vol,
|
|
||||||
mix_rate=mix_rate,
|
|
||||||
mix_liquid_height=mix_liquid_height,
|
|
||||||
delays=delays,
|
|
||||||
none_keys=none_keys,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def custom_delay(self, seconds=0, msg=None):
|
async def custom_delay(self, seconds=0, msg=None):
|
||||||
return await super().custom_delay(seconds, msg)
|
return await super().custom_delay(seconds, msg)
|
||||||
@@ -1028,18 +1006,32 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
):
|
):
|
||||||
|
try:
|
||||||
return await super().aspirate(
|
return await super().aspirate(
|
||||||
resources,
|
resources,
|
||||||
vols,
|
vols,
|
||||||
use_channels,
|
use_channels,
|
||||||
flow_rates,
|
flow_rates,
|
||||||
offsets,
|
offsets,
|
||||||
liquid_height,
|
liquid_height,
|
||||||
blow_out_air_volume,
|
blow_out_air_volume,
|
||||||
spread,
|
spread,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
)
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
||||||
|
return await super().aspirate(
|
||||||
|
resources,
|
||||||
|
vols,
|
||||||
|
use_channels,
|
||||||
|
flow_rates,
|
||||||
|
offsets,
|
||||||
|
liquid_height,
|
||||||
|
blow_out_air_volume,
|
||||||
|
spread="custom",
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
async def drop_tips(
|
async def drop_tips(
|
||||||
self,
|
self,
|
||||||
@@ -1063,17 +1055,33 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
):
|
):
|
||||||
return await super().dispense(
|
try:
|
||||||
resources,
|
return await super().dispense(
|
||||||
vols,
|
resources,
|
||||||
use_channels,
|
vols,
|
||||||
flow_rates,
|
use_channels,
|
||||||
offsets,
|
flow_rates,
|
||||||
liquid_height,
|
offsets,
|
||||||
blow_out_air_volume,
|
liquid_height,
|
||||||
spread,
|
blow_out_air_volume,
|
||||||
**backend_kwargs,
|
spread,
|
||||||
)
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
||||||
|
# 目标资源过小无法分布多通道时,退化为 custom(所有通道对准中心)
|
||||||
|
return await super().dispense(
|
||||||
|
resources,
|
||||||
|
vols,
|
||||||
|
use_channels,
|
||||||
|
flow_rates,
|
||||||
|
offsets,
|
||||||
|
liquid_height,
|
||||||
|
blow_out_air_volume,
|
||||||
|
"custom",
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
async def discard_tips(
|
async def discard_tips(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -4134,19 +4134,8 @@ liquid_handler:
|
|||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources
|
handler_key: sources
|
||||||
label: sources
|
label: 待移动液体
|
||||||
- data_key: targets
|
- data_key: targets
|
||||||
data_source: handle
|
|
||||||
data_type: resource
|
|
||||||
handler_key: targets
|
|
||||||
label: targets
|
|
||||||
- data_key: tip_racks
|
|
||||||
data_source: handle
|
|
||||||
data_type: resource
|
|
||||||
handler_key: tip_racks
|
|
||||||
label: tip_racks
|
|
||||||
output:
|
|
||||||
- data_key: sources
|
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets
|
handler_key: targets
|
||||||
@@ -4161,9 +4150,9 @@ liquid_handler:
|
|||||||
data_source: executor
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources_out
|
handler_key: sources_out
|
||||||
label: sources
|
label: 移液后源孔
|
||||||
- data_key: targets
|
- data_key: targets.@flatten
|
||||||
data_source: handle
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_out
|
handler_key: targets_out
|
||||||
label: 移液后目标孔
|
label: 移液后目标孔
|
||||||
@@ -4812,13 +4801,13 @@ liquid_handler.biomek:
|
|||||||
targets: ''
|
targets: ''
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: sources
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources
|
handler_key: sources
|
||||||
label: sources
|
label: sources
|
||||||
output:
|
output:
|
||||||
- data_key: targets
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets
|
handler_key: targets
|
||||||
@@ -4971,21 +4960,21 @@ liquid_handler.biomek:
|
|||||||
volume: 0.0
|
volume: 0.0
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: sources
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources
|
handler_key: sources
|
||||||
label: sources
|
label: sources
|
||||||
- data_key: targets
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets
|
handler_key: targets
|
||||||
label: targets
|
label: targets
|
||||||
- data_key: tip_racks
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: tip_racks
|
handler_key: tip_rack
|
||||||
label: tip_racks
|
label: tip_rack
|
||||||
output:
|
output:
|
||||||
- data_key: sources
|
- data_key: sources
|
||||||
data_source: handle
|
data_source: handle
|
||||||
@@ -5166,28 +5155,30 @@ liquid_handler.biomek:
|
|||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources
|
handler_key: sources
|
||||||
label: sources
|
io_type: target
|
||||||
|
label: 待移动液体
|
||||||
- data_key: targets
|
- data_key: targets
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets
|
handler_key: targets
|
||||||
label: targets
|
label: 转移目标
|
||||||
- data_key: tip_racks
|
- data_key: tip_racks
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: tip_racks
|
handler_key: tip_rack
|
||||||
label: tip_racks
|
label: 枪头盒
|
||||||
output:
|
output:
|
||||||
- data_key: sources
|
- data_key: sources.@flatten
|
||||||
data_source: handle
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources_out
|
handler_key: sources_out
|
||||||
label: sources
|
io_type: source
|
||||||
- data_key: targets
|
label: 移液后源孔
|
||||||
data_source: handle
|
- data_key: targets.@flatten
|
||||||
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_out
|
handler_key: targets_out
|
||||||
label: targets
|
label: 移液后目标孔
|
||||||
placeholder_keys:
|
placeholder_keys:
|
||||||
sources: unilabos_resources
|
sources: unilabos_resources
|
||||||
targets: unilabos_resources
|
targets: unilabos_resources
|
||||||
@@ -7735,31 +7726,6 @@ liquid_handler.prcxi:
|
|||||||
title: move_to参数
|
title: move_to参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommandAsync
|
type: UniLabJsonCommandAsync
|
||||||
auto-plr_pos_to_prcxi:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
resource: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
resource:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- resource
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: plr_pos_to_prcxi参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-post_init:
|
auto-post_init:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -8655,19 +8621,7 @@ liquid_handler.prcxi:
|
|||||||
z: 0.0
|
z: 0.0
|
||||||
sample_id: ''
|
sample_id: ''
|
||||||
type: ''
|
type: ''
|
||||||
handles:
|
handles: {}
|
||||||
input:
|
|
||||||
- data_key: plate
|
|
||||||
data_source: handle
|
|
||||||
data_type: resource
|
|
||||||
handler_key: plate
|
|
||||||
label: plate
|
|
||||||
output:
|
|
||||||
- data_key: plate
|
|
||||||
data_source: handle
|
|
||||||
data_type: resource
|
|
||||||
handler_key: plate
|
|
||||||
label: plate
|
|
||||||
placeholder_keys:
|
placeholder_keys:
|
||||||
plate: unilabos_resources
|
plate: unilabos_resources
|
||||||
to: unilabos_resources
|
to: unilabos_resources
|
||||||
@@ -10276,26 +10230,26 @@ liquid_handler.prcxi:
|
|||||||
- data_key: sources
|
- data_key: sources
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources
|
handler_key: sources_identifier
|
||||||
label: sources
|
label: 待移动液体
|
||||||
- data_key: targets
|
- data_key: targets
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets
|
handler_key: targets_identifier
|
||||||
label: targets
|
label: 转移目标
|
||||||
- data_key: tip_racks
|
- data_key: tip_racks
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: tip_racks
|
handler_key: tip_rack_identifier
|
||||||
label: tip_racks
|
label: 枪头盒
|
||||||
output:
|
output:
|
||||||
- data_key: sources
|
- data_key: sources.@flatten
|
||||||
data_source: handle
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources_out
|
handler_key: sources_out
|
||||||
label: sources
|
label: 移液后源孔
|
||||||
- data_key: targets
|
- data_key: targets.@flatten
|
||||||
data_source: handle
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_out
|
handler_key: targets_out
|
||||||
label: 移液后目标孔
|
label: 移液后目标孔
|
||||||
@@ -10679,12 +10633,6 @@ liquid_handler.prcxi:
|
|||||||
type: string
|
type: string
|
||||||
deck:
|
deck:
|
||||||
type: object
|
type: object
|
||||||
deck_y:
|
|
||||||
default: 400
|
|
||||||
type: string
|
|
||||||
deck_z:
|
|
||||||
default: 300
|
|
||||||
type: string
|
|
||||||
host:
|
host:
|
||||||
type: string
|
type: string
|
||||||
is_9320:
|
is_9320:
|
||||||
@@ -10695,44 +10643,17 @@ liquid_handler.prcxi:
|
|||||||
type: string
|
type: string
|
||||||
port:
|
port:
|
||||||
type: integer
|
type: integer
|
||||||
rail_interval:
|
|
||||||
default: 0
|
|
||||||
type: string
|
|
||||||
rail_nums:
|
|
||||||
default: 4
|
|
||||||
type: string
|
|
||||||
rail_width:
|
|
||||||
default: 27.5
|
|
||||||
type: string
|
|
||||||
setup:
|
setup:
|
||||||
default: true
|
default: true
|
||||||
type: string
|
type: string
|
||||||
simulator:
|
simulator:
|
||||||
default: false
|
default: false
|
||||||
type: string
|
type: string
|
||||||
start_rail:
|
|
||||||
default: 2
|
|
||||||
type: string
|
|
||||||
step_mode:
|
step_mode:
|
||||||
default: false
|
default: false
|
||||||
type: string
|
type: string
|
||||||
timeout:
|
timeout:
|
||||||
type: number
|
type: number
|
||||||
x_increase:
|
|
||||||
default: -0.003636
|
|
||||||
type: string
|
|
||||||
x_offset:
|
|
||||||
default: -0.8
|
|
||||||
type: string
|
|
||||||
xy_coupling:
|
|
||||||
default: -0.0045
|
|
||||||
type: string
|
|
||||||
y_increase:
|
|
||||||
default: -0.003636
|
|
||||||
type: string
|
|
||||||
y_offset:
|
|
||||||
default: -37.98
|
|
||||||
type: string
|
|
||||||
required:
|
required:
|
||||||
- deck
|
- deck
|
||||||
- host
|
- host
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class Registry:
|
|||||||
test_resource_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_resource", {})
|
test_resource_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_resource", {})
|
||||||
test_resource_schema = self._generate_unilab_json_command_schema(
|
test_resource_schema = self._generate_unilab_json_command_schema(
|
||||||
test_resource_method_info.get("args", []),
|
test_resource_method_info.get("args", []),
|
||||||
"auto-test_resource",
|
"test_resource",
|
||||||
test_resource_method_info.get("return_annotation"),
|
test_resource_method_info.get("return_annotation"),
|
||||||
)
|
)
|
||||||
test_resource_schema["description"] = "用于测试物料、设备和样本。"
|
test_resource_schema["description"] = "用于测试物料、设备和样本。"
|
||||||
|
|||||||
6884
unilabos/registry/resources/opentrons/lab.yaml
Normal file
6884
unilabos/registry/resources/opentrons/lab.yaml
Normal file
File diff suppressed because it is too large
Load Diff
3681
unilabos/resources/lab_resources.py
Normal file
3681
unilabos/resources/lab_resources.py
Normal file
File diff suppressed because it is too large
Load Diff
1
unilabos/resources/opentrons_custom_labware_defs.json
Normal file
1
unilabos/resources/opentrons_custom_labware_defs.json
Normal file
File diff suppressed because one or more lines are too long
@@ -38,24 +38,52 @@ class LabSample(TypedDict):
|
|||||||
extra: Dict[str, Any]
|
extra: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionSizeType(TypedDict):
|
||||||
|
depth: float
|
||||||
|
width: float
|
||||||
|
height: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionSize(BaseModel):
|
class ResourceDictPositionSize(BaseModel):
|
||||||
depth: float = Field(description="Depth", default=0.0) # z
|
depth: float = Field(description="Depth", default=0.0) # z
|
||||||
width: float = Field(description="Width", default=0.0) # x
|
width: float = Field(description="Width", default=0.0) # x
|
||||||
height: float = Field(description="Height", default=0.0) # y
|
height: float = Field(description="Height", default=0.0) # y
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionScaleType(TypedDict):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionScale(BaseModel):
|
class ResourceDictPositionScale(BaseModel):
|
||||||
x: float = Field(description="x scale", default=0.0)
|
x: float = Field(description="x scale", default=0.0)
|
||||||
y: float = Field(description="y scale", default=0.0)
|
y: float = Field(description="y scale", default=0.0)
|
||||||
z: float = Field(description="z scale", default=0.0)
|
z: float = Field(description="z scale", default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionObjectType(TypedDict):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionObject(BaseModel):
|
class ResourceDictPositionObject(BaseModel):
|
||||||
x: float = Field(description="X coordinate", default=0.0)
|
x: float = Field(description="X coordinate", default=0.0)
|
||||||
y: float = Field(description="Y coordinate", default=0.0)
|
y: float = Field(description="Y coordinate", default=0.0)
|
||||||
z: float = Field(description="Z coordinate", default=0.0)
|
z: float = Field(description="Z coordinate", default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionType(TypedDict):
|
||||||
|
size: ResourceDictPositionSizeType
|
||||||
|
scale: ResourceDictPositionScaleType
|
||||||
|
layout: Literal["2d", "x-y", "z-y", "x-z"]
|
||||||
|
position: ResourceDictPositionObjectType
|
||||||
|
position3d: ResourceDictPositionObjectType
|
||||||
|
rotation: ResourceDictPositionObjectType
|
||||||
|
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"]
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPosition(BaseModel):
|
class ResourceDictPosition(BaseModel):
|
||||||
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
|
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
|
||||||
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
|
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
|
||||||
@@ -74,6 +102,24 @@ class ResourceDictPosition(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictType(TypedDict):
|
||||||
|
id: str
|
||||||
|
uuid: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
resource_schema: Dict[str, Any]
|
||||||
|
model: Dict[str, Any]
|
||||||
|
icon: str
|
||||||
|
parent_uuid: Optional[str]
|
||||||
|
parent: Optional["ResourceDictType"]
|
||||||
|
type: Union[Literal["device"], str]
|
||||||
|
klass: str
|
||||||
|
pose: ResourceDictPositionType
|
||||||
|
config: Dict[str, Any]
|
||||||
|
data: Dict[str, Any]
|
||||||
|
extra: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
||||||
class ResourceDict(BaseModel):
|
class ResourceDict(BaseModel):
|
||||||
id: str = Field(description="Resource ID")
|
id: str = Field(description="Resource ID")
|
||||||
@@ -468,10 +514,17 @@ class ResourceTreeSet(object):
|
|||||||
trees.append(tree_instance)
|
trees.append(tree_instance)
|
||||||
return cls(trees)
|
return cls(trees)
|
||||||
|
|
||||||
def to_plr_resources(self, skip_devices=True) -> List["PLRResource"]:
|
def to_plr_resources(
|
||||||
|
self, skip_devices: bool = True, requested_uuids: Optional[List[str]] = None
|
||||||
|
) -> List["PLRResource"]:
|
||||||
"""
|
"""
|
||||||
将 ResourceTreeSet 转换为 PLR 资源列表
|
将 ResourceTreeSet 转换为 PLR 资源列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skip_devices: 是否跳过 device 类型节点
|
||||||
|
requested_uuids: 若指定,则按此 UUID 顺序返回对应资源(用于批量查询时一一对应),
|
||||||
|
否则返回各树的根节点列表
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[PLRResource]: PLR 资源实例列表
|
List[PLRResource]: PLR 资源实例列表
|
||||||
"""
|
"""
|
||||||
@@ -526,6 +579,71 @@ class ResourceTreeSet(object):
|
|||||||
d["model"] = res.config.get("model", None)
|
d["model"] = res.config.get("model", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
# deserialize 会单独处理的元数据 key,不传给构造函数
|
||||||
|
_META_KEYS = {"type", "parent_name", "location", "children", "rotation", "barcode"}
|
||||||
|
# deserialize 自定义逻辑使用的 key(如 TipSpot 用 prototype_tip 构建 make_tip),需保留
|
||||||
|
_DESERIALIZE_PRESERVED_KEYS = {"prototype_tip"}
|
||||||
|
|
||||||
|
def remove_incompatible_params(plr_d: dict) -> None:
|
||||||
|
"""递归移除 PLR 类不接受的参数,避免 deserialize 报错。
|
||||||
|
- 移除构造函数不接受的参数(如 compute_height_from_volume、ordering、category)
|
||||||
|
- 对 TubeRack:将 ordering 转为 ordered_items
|
||||||
|
- 保留 deserialize 自定义逻辑需要的 key(如 prototype_tip)
|
||||||
|
"""
|
||||||
|
if "type" in plr_d:
|
||||||
|
sub_cls = find_subclass(plr_d["type"], PLRResource)
|
||||||
|
if sub_cls is not None:
|
||||||
|
spec = inspect.signature(sub_cls)
|
||||||
|
valid_params = set(spec.parameters.keys())
|
||||||
|
# TubeRack 特殊处理:先转换 ordering,再参与后续过滤
|
||||||
|
if "ordering" not in valid_params and "ordering" in plr_d:
|
||||||
|
ordering = plr_d.pop("ordering", None)
|
||||||
|
if sub_cls.__name__ == "TubeRack":
|
||||||
|
plr_d["ordered_items"] = (
|
||||||
|
_ordering_to_ordered_items(plr_d, ordering)
|
||||||
|
if ordering
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
# 移除构造函数不接受的参数(保留 META 和 deserialize 自定义逻辑需要的 key)
|
||||||
|
for key in list(plr_d.keys()):
|
||||||
|
if (
|
||||||
|
key not in _META_KEYS
|
||||||
|
and key not in _DESERIALIZE_PRESERVED_KEYS
|
||||||
|
and key not in valid_params
|
||||||
|
):
|
||||||
|
plr_d.pop(key, None)
|
||||||
|
for child in plr_d.get("children", []):
|
||||||
|
remove_incompatible_params(child)
|
||||||
|
|
||||||
|
def _ordering_to_ordered_items(plr_d: dict, ordering: dict) -> dict:
|
||||||
|
"""将 ordering 转为 ordered_items,从 children 构建 Tube 对象"""
|
||||||
|
from pylabrobot.resources import Tube, Coordinate
|
||||||
|
from pylabrobot.serializer import deserialize as plr_deserialize
|
||||||
|
|
||||||
|
children = plr_d.get("children", [])
|
||||||
|
ordered_items = {}
|
||||||
|
for idx, (ident, child_name) in enumerate(ordering.items()):
|
||||||
|
child_data = children[idx] if idx < len(children) else None
|
||||||
|
if child_data is None:
|
||||||
|
continue
|
||||||
|
loc_data = child_data.get("location")
|
||||||
|
loc = (
|
||||||
|
plr_deserialize(loc_data)
|
||||||
|
if loc_data
|
||||||
|
else Coordinate(0, 0, 0)
|
||||||
|
)
|
||||||
|
tube = Tube(
|
||||||
|
name=child_data.get("name", child_name or ident),
|
||||||
|
size_x=child_data.get("size_x", 10),
|
||||||
|
size_y=child_data.get("size_y", 10),
|
||||||
|
size_z=child_data.get("size_z", 50),
|
||||||
|
max_volume=child_data.get("max_volume", 1000),
|
||||||
|
)
|
||||||
|
tube.location = loc
|
||||||
|
ordered_items[ident] = tube
|
||||||
|
plr_d["children"] = [] # 已并入 ordered_items,避免重复反序列化
|
||||||
|
return ordered_items
|
||||||
|
|
||||||
plr_resources = []
|
plr_resources = []
|
||||||
tracker = DeviceNodeResourceTracker()
|
tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
@@ -545,9 +663,7 @@ class ResourceTreeSet(object):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
||||||
)
|
)
|
||||||
spec = inspect.signature(sub_cls)
|
remove_incompatible_params(plr_dict)
|
||||||
if "category" not in spec.parameters:
|
|
||||||
plr_dict.pop("category", None)
|
|
||||||
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
||||||
from pylabrobot.resources import Coordinate
|
from pylabrobot.resources import Coordinate
|
||||||
from pylabrobot.serializer import deserialize
|
from pylabrobot.serializer import deserialize
|
||||||
@@ -567,6 +683,18 @@ class ResourceTreeSet(object):
|
|||||||
logger.error(f"堆栈: {traceback.format_exc()}")
|
logger.error(f"堆栈: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
if requested_uuids:
|
||||||
|
# 按请求的 UUID 顺序返回对应资源(从整棵树中按 uuid 提取)
|
||||||
|
result = []
|
||||||
|
for uid in requested_uuids:
|
||||||
|
if uid in tracker.uuid_to_resources:
|
||||||
|
result.append(tracker.uuid_to_resources[uid])
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"请求的 UUID {uid} 在资源树中未找到。"
|
||||||
|
f"可用 UUID 数量: {len(tracker.uuid_to_resources)}"
|
||||||
|
)
|
||||||
|
return result
|
||||||
return plr_resources
|
return plr_resources
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
}
|
}
|
||||||
res.response = json.dumps(final_response)
|
res.response = json.dumps(final_response)
|
||||||
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
||||||
if hasattr(self.driver_instance, "create_resource"):
|
if hasattr(self.driver_instance, "create_resource") and self.node_name != "host_node":
|
||||||
create_resource_func = getattr(self.driver_instance, "create_resource")
|
create_resource_func = getattr(self.driver_instance, "create_resource")
|
||||||
try:
|
try:
|
||||||
ret = create_resource_func(
|
ret = create_resource_func(
|
||||||
@@ -1389,9 +1389,13 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
if uuid_indices:
|
if uuid_indices:
|
||||||
uuids = [item[1] for item in uuid_indices]
|
uuids = [item[1] for item in uuid_indices]
|
||||||
resource_tree = await self.get_resource(uuids)
|
resource_tree = await self.get_resource(uuids)
|
||||||
plr_resources = resource_tree.to_plr_resources()
|
plr_resources = resource_tree.to_plr_resources(requested_uuids=uuids)
|
||||||
for i, (idx, _, resource_data) in enumerate(uuid_indices):
|
for i, (idx, _, resource_data) in enumerate(uuid_indices):
|
||||||
plr_resource = plr_resources[i]
|
try:
|
||||||
|
plr_resource = plr_resources[i]
|
||||||
|
except Exception as e:
|
||||||
|
self.lab_logger().error(f"资源查询结果: 共 {len(queried_resources)} 个资源,但查询结果只有 {len(plr_resources)} 个资源,索引为 {i} 的资源不存在")
|
||||||
|
raise e
|
||||||
if "sample_id" in resource_data:
|
if "sample_id" in resource_data:
|
||||||
plr_resource.unilabos_extra[EXTRA_SAMPLE_UUID] = resource_data["sample_id"]
|
plr_resource.unilabos_extra[EXTRA_SAMPLE_UUID] = resource_data["sample_id"]
|
||||||
queried_resources[idx] = plr_resource
|
queried_resources[idx] = plr_resource
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from unilabos.resources.resource_tracker import (
|
|||||||
ResourceTreeInstance,
|
ResourceTreeInstance,
|
||||||
RETURN_UNILABOS_SAMPLES,
|
RETURN_UNILABOS_SAMPLES,
|
||||||
JSON_UNILABOS_PARAM,
|
JSON_UNILABOS_PARAM,
|
||||||
PARAM_SAMPLE_UUIDS,
|
PARAM_SAMPLE_UUIDS, SampleUUIDsType, LabSample,
|
||||||
)
|
)
|
||||||
from unilabos.ros.initialize_device import initialize_device_from_dict
|
from unilabos.ros.initialize_device import initialize_device_from_dict
|
||||||
from unilabos.ros.msgs.message_converter import (
|
from unilabos.ros.msgs.message_converter import (
|
||||||
@@ -65,6 +65,7 @@ class DeviceActionStatus:
|
|||||||
class TestResourceReturn(TypedDict):
|
class TestResourceReturn(TypedDict):
|
||||||
resources: List[List[ResourceDict]]
|
resources: List[List[ResourceDict]]
|
||||||
devices: List[Dict[str, Any]]
|
devices: List[Dict[str, Any]]
|
||||||
|
unilabos_samples: List[LabSample]
|
||||||
|
|
||||||
|
|
||||||
class TestLatencyReturn(TypedDict):
|
class TestLatencyReturn(TypedDict):
|
||||||
@@ -1582,6 +1583,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
def test_resource(
|
def test_resource(
|
||||||
self,
|
self,
|
||||||
|
sample_uuids: SampleUUIDsType,
|
||||||
resource: ResourceSlot = None,
|
resource: ResourceSlot = None,
|
||||||
resources: List[ResourceSlot] = None,
|
resources: List[ResourceSlot] = None,
|
||||||
device: DeviceSlot = None,
|
device: DeviceSlot = None,
|
||||||
@@ -1596,6 +1598,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
return {
|
return {
|
||||||
"resources": ResourceTreeSet.from_plr_resources([resource, *resources], known_newly_created=True).dump(),
|
"resources": ResourceTreeSet.from_plr_resources([resource, *resources], known_newly_created=True).dump(),
|
||||||
"devices": [device, *devices],
|
"devices": [device, *devices],
|
||||||
|
"unilabos_samples": [LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for sample_uuid, content in sample_uuids.items()]
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_pong_response(self, pong_data: dict):
|
def handle_pong_response(self, pong_data: dict):
|
||||||
|
|||||||
@@ -21,12 +21,12 @@
|
|||||||
},
|
},
|
||||||
"host": "10.20.30.184",
|
"host": "10.20.30.184",
|
||||||
"port": 9999,
|
"port": 9999,
|
||||||
"debug": true,
|
"debug": false,
|
||||||
"setup": true,
|
"setup": false,
|
||||||
"is_9320": true,
|
"is_9320": true,
|
||||||
"timeout": 10,
|
"timeout": 10,
|
||||||
"matrix_id": "5de524d0-3f95-406c-86dd-f83626ebc7cb",
|
"matrix_id": "5de524d0-3f95-406c-86dd-f83626ebc7cb",
|
||||||
"simulator": true,
|
"simulator": false,
|
||||||
"channel_num": 2
|
"channel_num": 2
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
@@ -789,6 +789,47 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trash",
|
||||||
|
"name": "trash",
|
||||||
|
|
||||||
|
"children": [],
|
||||||
|
"parent": "T16",
|
||||||
|
"type": "trash",
|
||||||
|
"class": "",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "PRCXI9300Trash",
|
||||||
|
"size_x": 127.5,
|
||||||
|
"size_y": 86,
|
||||||
|
"size_z": 10,
|
||||||
|
"rotation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"type": "Rotation"
|
||||||
|
},
|
||||||
|
"category": "trash",
|
||||||
|
"model": null,
|
||||||
|
"barcode": null,
|
||||||
|
"max_volume": "Infinity",
|
||||||
|
"material_z_thickness": 0,
|
||||||
|
"compute_volume_from_height": null,
|
||||||
|
"compute_height_from_volume": null
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquids": [],
|
||||||
|
"pending_liquids": [],
|
||||||
|
"liquid_history": [],
|
||||||
|
"Material": {
|
||||||
|
"uuid": "730067cf07ae43849ddf4034299030e9"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"edges": []
|
"edges": []
|
||||||
|
|||||||
@@ -119,11 +119,14 @@ DEVICE_NAME_DEFAULT = "PRCXI" # transfer_liquid, set_liquid_from_plate 等动
|
|||||||
# 节点类型
|
# 节点类型
|
||||||
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
||||||
|
|
||||||
|
CLASS_NAMES_MAPPING = {
|
||||||
|
"plate": "PRCXI_BioER_96_wellplate",
|
||||||
|
"tip_rack": "PRCXI_300ul_Tips",
|
||||||
|
}
|
||||||
# create_resource 节点默认参数
|
# create_resource 节点默认参数
|
||||||
CREATE_RESOURCE_DEFAULTS = {
|
CREATE_RESOURCE_DEFAULTS = {
|
||||||
"device_id": "/PRCXI",
|
"device_id": "/PRCXI",
|
||||||
"parent_template": "/PRCXI/PRCXI_Deck/T{slot}", # {slot} 会被替换为实际的 slot 值
|
"parent_template": "/PRCXI/PRCXI_Deck/T{slot}", # {slot} 会被替换为实际的 slot 值
|
||||||
"class_name": "PRCXI_BioER_96_wellplate",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 默认液体体积 (uL)
|
# 默认液体体积 (uL)
|
||||||
@@ -367,11 +370,10 @@ def build_protocol_graph(
|
|||||||
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
labware_info: reagent 信息字典,格式为 {name: {slot, well}, ...},用于 set_liquid 和 well 查找
|
labware_info: labware 信息字典,格式为 {name: {slot, well, labware, ...}, ...}
|
||||||
protocol_steps: 协议步骤列表
|
protocol_steps: 协议步骤列表
|
||||||
workstation_name: 工作站名称
|
workstation_name: 工作站名称
|
||||||
action_resource_mapping: action 到 resource_name 的映射字典,可选
|
action_resource_mapping: action 到 resource_name 的映射字典,可选
|
||||||
labware_defs: labware 定义列表,格式为 [{"name": "...", "slot": "1", "type": "lab_xxx"}, ...]
|
|
||||||
"""
|
"""
|
||||||
G = WorkflowGraph()
|
G = WorkflowGraph()
|
||||||
resource_last_writer = {} # reagent_name -> "node_id:port"
|
resource_last_writer = {} # reagent_name -> "node_id:port"
|
||||||
@@ -379,7 +381,19 @@ def build_protocol_graph(
|
|||||||
|
|
||||||
protocol_steps = refactor_data(protocol_steps, action_resource_mapping)
|
protocol_steps = refactor_data(protocol_steps, action_resource_mapping)
|
||||||
|
|
||||||
# ==================== 第一步:按 slot 创建 create_resource 节点 ====================
|
# ==================== 第一步:按 slot 去重创建 create_resource 节点 ====================
|
||||||
|
# 收集所有唯一的 slot
|
||||||
|
slots_info = {} # slot -> {labware, res_id}
|
||||||
|
for labware_id, item in labware_info.items():
|
||||||
|
slot = str(item.get("slot", ""))
|
||||||
|
if slot and slot not in slots_info:
|
||||||
|
res_id = f"plate_slot_{slot}"
|
||||||
|
slots_info[slot] = {
|
||||||
|
"labware": item.get("labware", ""),
|
||||||
|
"res_id": res_id,
|
||||||
|
"labware_id": labware_id,
|
||||||
|
}
|
||||||
|
|
||||||
# 创建 Group 节点,包含所有 create_resource 节点
|
# 创建 Group 节点,包含所有 create_resource 节点
|
||||||
group_node_id = str(uuid.uuid4())
|
group_node_id = str(uuid.uuid4())
|
||||||
G.add_node(
|
G.add_node(
|
||||||
@@ -395,41 +409,38 @@ def build_protocol_graph(
|
|||||||
param=None,
|
param=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 直接使用 JSON 中的 labware 定义,每个 slot 一条记录,type 即 class_name
|
# 为每个唯一的 slot 创建 create_resource 节点
|
||||||
res_index = 0
|
for slot, info in slots_info.items():
|
||||||
for lw in (labware_defs or []):
|
|
||||||
slot = str(lw.get("slot", ""))
|
|
||||||
if not slot or slot in slot_to_create_resource:
|
|
||||||
continue # 跳过空 slot 或已处理的 slot
|
|
||||||
|
|
||||||
lw_name = lw.get("name", f"slot {slot}")
|
|
||||||
lw_type = lw.get("type", CREATE_RESOURCE_DEFAULTS["class_name"])
|
|
||||||
res_id = f"plate_slot_{slot}"
|
|
||||||
|
|
||||||
res_index += 1
|
|
||||||
node_id = str(uuid.uuid4())
|
node_id = str(uuid.uuid4())
|
||||||
|
res_id = info["res_id"]
|
||||||
|
res_type_name = info["labware"].lower().replace(".", "point")
|
||||||
|
res_type_name = f"lab_{res_type_name}"
|
||||||
|
|
||||||
G.add_node(
|
G.add_node(
|
||||||
node_id,
|
node_id,
|
||||||
template_name="create_resource",
|
template_name="create_resource",
|
||||||
resource_name="host_node",
|
resource_name="host_node",
|
||||||
name=lw_name,
|
name=f"{res_type_name}_slot{slot}",
|
||||||
description=f"Create {lw_name}",
|
description=f"Create plate on slot {slot}",
|
||||||
lab_node_type="Labware",
|
lab_node_type="Labware",
|
||||||
footer="create_resource-host_node",
|
footer="create_resource-host_node",
|
||||||
device_name=DEVICE_NAME_HOST,
|
device_name=DEVICE_NAME_HOST,
|
||||||
type=NODE_TYPE_DEFAULT,
|
type=NODE_TYPE_DEFAULT,
|
||||||
parent_uuid=group_node_id,
|
parent_uuid=group_node_id, # 指向 Group 节点
|
||||||
minimized=True,
|
minimized=True, # 折叠显示
|
||||||
param={
|
param={
|
||||||
"res_id": res_id,
|
"res_id": res_id,
|
||||||
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
||||||
"class_name": lw_type,
|
"class_name": res_type_name,
|
||||||
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot),
|
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot),
|
||||||
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||||
"slot_on_deck": slot,
|
"slot_on_deck": slot,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
slot_to_create_resource[slot] = node_id
|
slot_to_create_resource[slot] = node_id
|
||||||
|
if "tip" in res_type_name and "rack" in res_type_name:
|
||||||
|
resource_last_writer[info["labware_id"]] = f"{node_id}:labware"
|
||||||
|
# create_resource 之间不需要 ready 连接
|
||||||
|
|
||||||
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
|
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
|
||||||
# 创建 Group 节点,包含所有 set_liquid_from_plate 节点
|
# 创建 Group 节点,包含所有 set_liquid_from_plate 节点
|
||||||
@@ -511,6 +522,7 @@ def build_protocol_graph(
|
|||||||
"reagent": "reagent",
|
"reagent": "reagent",
|
||||||
"solvent": "solvent",
|
"solvent": "solvent",
|
||||||
"compound": "compound",
|
"compound": "compound",
|
||||||
|
"tip_racks": "tip_rack_identifier",
|
||||||
}
|
}
|
||||||
|
|
||||||
OUTPUT_PORT_MAPPING = {
|
OUTPUT_PORT_MAPPING = {
|
||||||
|
|||||||
Reference in New Issue
Block a user