mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 13:37:14 +00:00
更新prcxi的版面更新与工作流上传方法
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -251,3 +251,4 @@ test_config.py
|
||||
|
||||
|
||||
/.claude
|
||||
/.cursor
|
||||
|
||||
@@ -793,8 +793,8 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
rail_interval=0,
|
||||
x_increase = -0.003636,
|
||||
y_increase = -0.003636,
|
||||
x_offset = -0.8,
|
||||
y_offset = -37.98,
|
||||
x_offset = 9.2,
|
||||
y_offset = -27.98,
|
||||
deck_z = 300,
|
||||
deck_y = 400,
|
||||
rail_width=27.5,
|
||||
@@ -809,28 +809,10 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
self.x_offset = x_offset
|
||||
self.y_offset = y_offset
|
||||
self.xy_coupling = xy_coupling
|
||||
|
||||
self.left_2_claw = Coordinate(-130.2, 34, -134)
|
||||
self.right_2_left = Coordinate(22,-1, 8)
|
||||
tablets_info = {}
|
||||
plate_positions = []
|
||||
for child in deck.children:
|
||||
number = int(child.name.replace("T", ""))
|
||||
|
||||
if child.children:
|
||||
if "Material" in child.children[0]._unilabos_state:
|
||||
tablets_info[number] = child.children[0]._unilabos_state["Material"].get("uuid", "730067cf07ae43849ddf4034299030e9")
|
||||
else:
|
||||
tablets_info[number] = "730067cf07ae43849ddf4034299030e9"
|
||||
else:
|
||||
tablets_info[number] = "730067cf07ae43849ddf4034299030e9"
|
||||
pos = self.plr_pos_to_prcxi(child)
|
||||
plate_positions.append(
|
||||
{
|
||||
"Number": number,
|
||||
"XPos": pos.x,
|
||||
"YPos": pos.y,
|
||||
"ZPos": pos.z
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if is_9320:
|
||||
@@ -850,8 +832,180 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
)
|
||||
|
||||
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
|
||||
|
||||
def plr_pos_to_prcxi(self, resource: Resource):
|
||||
self._first_transfer_done = False
|
||||
|
||||
@staticmethod
|
||||
def _get_slot_number(resource) -> Optional[int]:
|
||||
"""从 resource 的 unilabos_extra["update_resource_site"](如 "T13")或位置反算槽位号。"""
|
||||
extra = getattr(resource, "unilabos_extra", {}) or {}
|
||||
site = extra.get("update_resource_site", "")
|
||||
if site:
|
||||
digits = "".join(c for c in str(site) if c.isdigit())
|
||||
return int(digits) if digits else None
|
||||
# 使用 resource.location.x 和 resource.location.y 反算槽位号
|
||||
# 参考 _DEFAULT_SITE_POSITIONS: x = (i%4)*137.5+5, y = (int(i/4))*96+13
|
||||
loc = getattr(resource, "location", None)
|
||||
if loc is not None and loc.x is not None and loc.y is not None:
|
||||
col = round((loc.x - 5) / 137.5) # 0-3
|
||||
row = round(3-(loc.y - 13) / 96) # 0-3
|
||||
idx = row * 4 + col # 0-15
|
||||
if 0 <= idx < 16:
|
||||
return idx + 1 # 槽位号从 1 开始
|
||||
|
||||
return None
|
||||
|
||||
def _match_and_create_matrix(self):
|
||||
"""首次 transfer_liquid 时,根据 deck 上的 resource 自动匹配耗材并创建 WorkTabletMatrix。"""
|
||||
backend = self._unilabos_backend
|
||||
api = backend.api_client
|
||||
|
||||
if backend.matrix_id:
|
||||
return
|
||||
|
||||
material_list = api.get_all_materials()
|
||||
if not material_list:
|
||||
return
|
||||
|
||||
# 按 materialEnum 分组: {enum_value: [material, ...]}
|
||||
material_dict = {}
|
||||
material_uuid_map = {}
|
||||
for m in material_list:
|
||||
enum_key = m.get("materialEnum")
|
||||
material_dict.setdefault(enum_key, []).append(m)
|
||||
if "uuid" in m:
|
||||
material_uuid_map[m["uuid"]] = m
|
||||
|
||||
work_tablets = []
|
||||
slot_none = [i for i in range(1, 17)]
|
||||
|
||||
for child in self.deck.children:
|
||||
|
||||
resource = child
|
||||
number = self._get_slot_number(resource)
|
||||
if number is None:
|
||||
continue
|
||||
|
||||
# 如果 resource 已有 Material UUID,直接使用
|
||||
if hasattr(resource, "_unilabos_state") and "Material" in getattr(resource, "_unilabos_state", {}):
|
||||
mat_uuid = resource._unilabos_state["Material"].get("uuid")
|
||||
if mat_uuid and mat_uuid in material_uuid_map:
|
||||
work_tablets.append({"Number": number, "Material": material_uuid_map[mat_uuid]})
|
||||
continue
|
||||
|
||||
# 根据 resource 类型推断 materialEnum
|
||||
# MaterialEnum: Other=0, Tips=1, DeepWellPlate=2, PCRPlate=3, ELISAPlate=4, Reservoir=5, WasteBox=6
|
||||
expected_enum = None
|
||||
if isinstance(resource, PRCXI9300TipRack) or isinstance(resource, TipRack):
|
||||
expected_enum = 1 # Tips
|
||||
elif isinstance(resource, PRCXI9300Trash) or isinstance(resource, Trash):
|
||||
expected_enum = 6 # WasteBox
|
||||
elif isinstance(resource, (PRCXI9300Plate, Plate)):
|
||||
expected_enum = None # Plate 可能是 DeepWellPlate/PCRPlate/ELISAPlate,不限定
|
||||
|
||||
|
||||
# 根据 expected_enum 筛选候选耗材列表
|
||||
if expected_enum is not None:
|
||||
candidates = material_dict.get(expected_enum, [])
|
||||
else:
|
||||
# expected_enum 未确定时,搜索所有耗材
|
||||
candidates = material_list
|
||||
|
||||
# 根据 children 个数和容量匹配最相似的耗材
|
||||
num_children = len(resource.children)
|
||||
child_max_volume = None
|
||||
if resource.children:
|
||||
first_child = resource.children[0]
|
||||
if hasattr(first_child, "max_volume") and first_child.max_volume is not None:
|
||||
child_max_volume = first_child.max_volume
|
||||
|
||||
best_material = None
|
||||
best_score = float("inf")
|
||||
|
||||
for material in candidates:
|
||||
hole_count = (material.get("HoleRow", 0) or 0) * (material.get("HoleColum", 0) or 0)
|
||||
material_volume = material.get("Volume", 0) or 0
|
||||
|
||||
# 孔数差异(高权重优先匹配孔数)
|
||||
hole_diff = abs(num_children - hole_count)
|
||||
# 容量差异(归一化)
|
||||
if child_max_volume is not None and material_volume > 0:
|
||||
vol_diff = abs(child_max_volume - material_volume) / material_volume
|
||||
else:
|
||||
vol_diff = 0
|
||||
|
||||
score = hole_diff * 1000 + vol_diff
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best_material = material
|
||||
|
||||
if best_material:
|
||||
work_tablets.append({"Number": number, "Material": best_material})
|
||||
slot_none.remove(number)
|
||||
|
||||
if not work_tablets:
|
||||
return
|
||||
|
||||
matrix_id = str(uuid.uuid4())
|
||||
matrix_info = {
|
||||
"MatrixId": matrix_id,
|
||||
"MatrixName": matrix_id,
|
||||
"WorkTablets": work_tablets +
|
||||
[{"Number": number, "Material": {"uuid": "730067cf07ae43849ddf4034299030e9"}} for number in slot_none],
|
||||
}
|
||||
res = api.add_WorkTablet_Matrix(matrix_info)
|
||||
if res.get("Success"):
|
||||
backend.matrix_id = matrix_id
|
||||
backend.matrix_info = matrix_info
|
||||
|
||||
# 重新计算所有槽位的位置(初始化时 deck 可能为空,此时才有资源)
|
||||
pipetting_positions = []
|
||||
plate_positions = []
|
||||
for child in self.deck.children:
|
||||
number = self._get_slot_number(child)
|
||||
|
||||
if number is None:
|
||||
continue
|
||||
|
||||
pos = self.plr_pos_to_prcxi(child)
|
||||
plate_positions.append({"Number": number, "XPos": pos.x, "YPos": pos.y, "ZPos": pos.z})
|
||||
|
||||
if child.children:
|
||||
pip_pos = self.plr_pos_to_prcxi(child.children[0], self.left_2_claw)
|
||||
else:
|
||||
pip_pos = self.plr_pos_to_prcxi(child, Coordinate(50, self.left_2_claw.y, self.left_2_claw.z))
|
||||
half_x = child.get_size_x() / 2 * abs(1 + self.x_increase)
|
||||
z_wall = child.get_size_z()
|
||||
|
||||
pipetting_positions.append({
|
||||
"Number": number,
|
||||
"XPos": pip_pos.x,
|
||||
"YPos": pip_pos.y,
|
||||
"ZPos": pip_pos.z,
|
||||
"X_Left": half_x,
|
||||
"X_Right": half_x,
|
||||
"ZAgainstTheWall": pip_pos.z - z_wall,
|
||||
"X2Pos": pip_pos.x + self.right_2_left.x,
|
||||
"Y2Pos": pip_pos.y + self.right_2_left.y,
|
||||
"Z2Pos": pip_pos.z + self.right_2_left.z,
|
||||
"X2_Left": half_x,
|
||||
"X2_Right": half_x,
|
||||
"ZAgainstTheWall2": pip_pos.z - z_wall,
|
||||
})
|
||||
|
||||
if pipetting_positions:
|
||||
api.update_pipetting_position(matrix_id, pipetting_positions)
|
||||
# 更新 backend 中的 plate_positions
|
||||
backend.plate_positions = plate_positions
|
||||
|
||||
if plate_positions:
|
||||
api.update_clamp_jaw_position(matrix_id, plate_positions)
|
||||
|
||||
|
||||
print(f"Auto-matched materials and created matrix: {matrix_id}")
|
||||
else:
|
||||
raise PRCXIError(f"Failed to create auto-matched matrix: {res.get('Message', 'Unknown error')}")
|
||||
|
||||
def plr_pos_to_prcxi(self, resource: Resource, offset: Coordinate = Coordinate(0, 0, 0)):
|
||||
resource_pos = resource.get_absolute_location(x="c",y="c",z="t")
|
||||
x = resource_pos.x
|
||||
y = resource_pos.y
|
||||
@@ -869,9 +1023,9 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
prcxi_y = (self.deck_y - y)*(1+self.y_increase) + self.y_offset
|
||||
prcxi_z = self.deck_z - z
|
||||
|
||||
prcxi_x = min(max(0, prcxi_x),self.deck_x)
|
||||
prcxi_y = min(max(0, prcxi_y),self.deck_y)
|
||||
prcxi_z = min(max(0, prcxi_z),self.deck_z)
|
||||
prcxi_x = min(max(0, prcxi_x+offset.x),self.deck_x)
|
||||
prcxi_y = min(max(0, prcxi_y+offset.y),self.deck_y)
|
||||
prcxi_z = min(max(0, prcxi_z+offset.z),self.deck_z)
|
||||
|
||||
return Coordinate(prcxi_x, prcxi_y, prcxi_z)
|
||||
|
||||
@@ -1008,6 +1162,9 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
delays: Optional[List[int]] = None,
|
||||
none_keys: List[str] = [],
|
||||
) -> TransferLiquidReturn:
|
||||
if not self._first_transfer_done:
|
||||
self._match_and_create_matrix()
|
||||
self._first_transfer_done = True
|
||||
if self.step_mode:
|
||||
await self.create_protocol(f"transfer_liquid{time.time()}")
|
||||
res = await super().transfer_liquid(
|
||||
@@ -1069,10 +1226,6 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
offsets: Optional[List[Coordinate]] = None,
|
||||
**backend_kwargs,
|
||||
):
|
||||
if self.step_mode:
|
||||
await self.create_protocol(f"单点动作{time.time()}")
|
||||
await super().pick_up_tips(tip_spots, use_channels, offsets, **backend_kwargs)
|
||||
await self.run_protocol()
|
||||
return await super().pick_up_tips(tip_spots, use_channels, offsets, **backend_kwargs)
|
||||
|
||||
async def aspirate(
|
||||
@@ -1432,7 +1585,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
||||
await asyncio.sleep(1)
|
||||
print("PRCXI9300 reset successfully.")
|
||||
|
||||
self.api_client.update_clamp_jaw_position(self.matrix_id, self.plate_positions)
|
||||
# self.api_client.update_clamp_jaw_position(self.matrix_id, self.plate_positions)
|
||||
|
||||
except ConnectionRefusedError as e:
|
||||
raise RuntimeError(
|
||||
@@ -1460,8 +1613,8 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
||||
plate_indexes = []
|
||||
for op in ops:
|
||||
plate = op.resource.parent
|
||||
deck = plate.parent.parent
|
||||
plate_index = deck.children.index(plate.parent)
|
||||
deck = plate.parent
|
||||
plate_index = deck.children.index(plate)
|
||||
# print(f"Plate index: {plate_index}, Plate name: {plate.name}")
|
||||
# print(f"Number of children in deck: {len(deck.children)}")
|
||||
|
||||
@@ -1535,8 +1688,8 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
||||
plate_indexes = []
|
||||
for op in ops:
|
||||
plate = op.resource.parent
|
||||
deck = plate.parent.parent
|
||||
plate_index = deck.children.index(plate.parent)
|
||||
deck = plate.parent
|
||||
plate_index = deck.children.index(plate)
|
||||
plate_indexes.append(plate_index)
|
||||
if len(set(plate_indexes)) != 1:
|
||||
raise ValueError(
|
||||
@@ -1595,7 +1748,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
||||
for op in targets:
|
||||
deck = op.parent.parent.parent
|
||||
plate = op.parent
|
||||
plate_index = deck.children.index(plate.parent)
|
||||
plate_index = deck.children.index(plate)
|
||||
plate_indexes.append(plate_index)
|
||||
|
||||
if len(set(plate_indexes)) != 1:
|
||||
@@ -1646,8 +1799,8 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
||||
plate_indexes = []
|
||||
for op in ops:
|
||||
plate = op.resource.parent
|
||||
deck = plate.parent.parent
|
||||
plate_index = deck.children.index(plate.parent)
|
||||
deck = plate.parent
|
||||
plate_index = deck.children.index(plate)
|
||||
plate_indexes.append(plate_index)
|
||||
|
||||
if len(set(plate_indexes)) != 1:
|
||||
@@ -1703,8 +1856,8 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
||||
plate_indexes = []
|
||||
for op in ops:
|
||||
plate = op.resource.parent
|
||||
deck = plate.parent.parent
|
||||
plate_index = deck.children.index(plate.parent)
|
||||
deck = plate.parent
|
||||
plate_index = deck.children.index(plate)
|
||||
plate_indexes.append(plate_index)
|
||||
|
||||
if len(set(plate_indexes)) != 1:
|
||||
@@ -1939,11 +2092,19 @@ class PRCXI9300Api:
|
||||
|
||||
def update_clamp_jaw_position(self, target_matrix_id: str, plate_positions: List[Dict[str, Any]]):
|
||||
position_params = {
|
||||
"MatrixId": target_matrix_id,
|
||||
"MatrixId": target_matrix_id,
|
||||
"WorkTablets": plate_positions
|
||||
}
|
||||
return self.call("IMatrix", "UpdateClampJawPosition", [position_params])
|
||||
|
||||
def update_pipetting_position(self, target_matrix_id: str, pipetting_positions: List[Dict[str, Any]]):
|
||||
"""UpdatePipettingPosition - 更新移液位置"""
|
||||
position_params = {
|
||||
"MatrixId": target_matrix_id,
|
||||
"WorkTablets": pipetting_positions
|
||||
}
|
||||
return self.call("IMatrix", "UpdatePipettingPosition", [position_params])
|
||||
|
||||
def add_WorkTablet_Matrix(self, matrix: MatrixInfo):
|
||||
return self.call("IMatrix", "AddWorkTabletMatrix2" if self.is_9320 else "AddWorkTabletMatrix", [matrix])
|
||||
|
||||
|
||||
@@ -774,7 +774,13 @@ class ResourceTreeSet(object):
|
||||
ValueError: 当建立关系时发现不一致
|
||||
"""
|
||||
# 第一步:将字典列表转换为 ResourceDictInstance 列表
|
||||
instances = [ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in raw_list]
|
||||
parsed_list = []
|
||||
for node_dict in raw_list:
|
||||
if isinstance(node_dict, str):
|
||||
import json
|
||||
node_dict = json.loads(node_dict)
|
||||
parsed_list.append(node_dict)
|
||||
instances = [ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in parsed_list]
|
||||
|
||||
# 第二步:建立映射关系
|
||||
uuid_to_instance: Dict[str, ResourceDictInstance] = {}
|
||||
|
||||
@@ -142,6 +142,16 @@ PARAM_RENAME_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
def _map_deck_slot(raw_slot: str, object_type: str = "") -> str:
|
||||
"""协议槽位 -> 实际 deck:4→13,8→14,12+trash→16,其余不变。"""
|
||||
s = "" if raw_slot is None else str(raw_slot).strip()
|
||||
if not s:
|
||||
return ""
|
||||
if s == "12" and (object_type or "").strip().lower() == "trash":
|
||||
return "16"
|
||||
return {"4": "13", "8": "14"}.get(s, s)
|
||||
|
||||
|
||||
# ---------------- Graph ----------------
|
||||
|
||||
|
||||
@@ -386,7 +396,8 @@ def build_protocol_graph(
|
||||
# 收集所有唯一的 slot
|
||||
slots_info = {} # slot -> {labware, res_id}
|
||||
for labware_id, item in labware_info.items():
|
||||
slot = str(item.get("slot", ""))
|
||||
object_type = item.get("object", "") or ""
|
||||
slot = _map_deck_slot(str(item.get("slot", "")), object_type)
|
||||
labware = item.get("labware", "")
|
||||
if slot and slot not in slots_info:
|
||||
res_id = f"{labware}_slot_{slot}"
|
||||
@@ -394,7 +405,7 @@ def build_protocol_graph(
|
||||
"labware": labware,
|
||||
"res_id": res_id,
|
||||
"labware_id": labware_id,
|
||||
"object": item.get("object", ""),
|
||||
"object": object_type,
|
||||
}
|
||||
|
||||
# 创建 Group 节点,包含所有 create_resource 节点
|
||||
@@ -476,7 +487,8 @@ def build_protocol_graph(
|
||||
if item.get("type") == "hardware":
|
||||
continue
|
||||
|
||||
slot = str(item.get("slot", ""))
|
||||
object_type = item.get("object", "") or ""
|
||||
slot = _map_deck_slot(str(item.get("slot", "")), object_type)
|
||||
wells = item.get("well", [])
|
||||
if not wells or not slot:
|
||||
continue
|
||||
@@ -484,7 +496,6 @@ def build_protocol_graph(
|
||||
# res_id 不能有空格
|
||||
res_id = str(labware_id).replace(" ", "_")
|
||||
well_count = len(wells)
|
||||
object_type = item.get("object", "")
|
||||
liquid_volume = DEFAULT_LIQUID_VOLUME if object_type == "source" else 0
|
||||
|
||||
node_id = str(uuid.uuid4())
|
||||
@@ -520,8 +531,12 @@ def build_protocol_graph(
|
||||
# set_liquid_from_plate 的输出 output_wells 用于连接 transfer_liquid
|
||||
resource_last_writer[labware_id] = f"{node_id}:output_wells"
|
||||
|
||||
# transfer_liquid 之间通过 ready 串联;若存在 trash 节点,第一个 transfer_liquid 从 trash 的 ready 开始
|
||||
# 收集所有 create_resource 节点 ID,用于让第一个 transfer_liquid 等待所有资源创建完成
|
||||
all_create_resource_node_ids = list(slot_to_create_resource.values())
|
||||
|
||||
# transfer_liquid 之间通过 ready 串联;第一个 transfer_liquid 需要等待所有 create_resource 完成
|
||||
last_control_node_id = trash_create_node_id
|
||||
is_first_action_node = True
|
||||
|
||||
# 端口名称映射:JSON 字段名 -> 实际 handle key
|
||||
INPUT_PORT_MAPPING = {
|
||||
@@ -689,7 +704,12 @@ def build_protocol_graph(
|
||||
G.add_node(node_id, **step_copy)
|
||||
|
||||
# 控制流
|
||||
if last_control_node_id is not None:
|
||||
if is_first_action_node:
|
||||
# 第一个 transfer_liquid 需要等待所有 create_resource 完成
|
||||
for cr_node_id in all_create_resource_node_ids:
|
||||
G.add_edge(cr_node_id, node_id, source_port="ready", target_port="ready")
|
||||
is_first_action_node = False
|
||||
elif last_control_node_id is not None:
|
||||
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
|
||||
last_control_node_id = node_id
|
||||
|
||||
|
||||
Reference in New Issue
Block a user