Files
Uni-Lab-OS/unilabos/devices/liquid_handling/prcxi/prcxi.py
Xuwznln 9aeffebde1 0.10.7 Update (#101)
* Cleanup registry to be easy-understanding (#76)

* delete deprecated mock devices

* rename categories

* combine chromatographic devices

* rename rviz simulation nodes

* organic virtual devices

* parse vessel_id

* run registry completion before merge

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>

* fix: workstation handlers and vessel_id parsing

* fix: working dir error when input config path
feat: report publish topic when error

* modify default discovery_interval to 15s

* feat: add trace log level

* feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79)

* fix: drop_tips not using auto resource select

* fix: discard_tips error

* fix: discard_tips

* fix: prcxi_res

* add: prcxi res
fix: startup slow

* feat: workstation example

* fix pumps and liquid_handler handle

* feat: 优化protocol node节点运行日志

* fix all protocol_compilers and remove deprecated devices

* feat: 新增use_remote_resource参数

* fix and remove redundant info

* bugfixes on organic protocols

* fix filter protocol

* fix protocol node

* 临时兼容错误的driver写法

* fix: prcxi import error

* use call_async in all service to avoid deadlock

* fix: figure_resource

* Update recipe.yaml

* add workstation template and battery example

* feat: add sk & ak

* update workstation base

* Create workstation_architecture.md

* refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode

* refactor: ProtocolNode→WorkstationNode

* Add:msgs.action (#83)

* update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84)

* Add:msgs.action

* update: 将版本号从 0.10.3 更新为 0.10.4

* simplify resource system

* uncompleted refactor

* example for use WorkstationBase

* feat: websocket

* feat: websocket test

* feat: workstation example

* feat: action status

* fix: station自己的方法注册错误

* fix: 还原protocol node处理方法

* fix: build

* fix: missing job_id key

* ws test version 1

* ws test version 2

* ws protocol

* 增加物料关系上传日志

* 增加物料关系上传日志

* 修正物料关系上传

* 修复工站的tracker实例追踪失效问题

* 增加handle检测,增加material edge关系上传

* 修复event loop错误

* 修复edge上报错误

* 修复async错误

* 更新schema的title字段

* 主机节点信息等支持自动刷新

* 注册表编辑器

* 修复status密集发送时,消息出错

* 增加addr参数

* fix: addr param

* fix: addr param

* 取消labid 和 强制config输入

* Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup

- Created LiquidHandlerSetGroup.action with fields for group name, wells, and volumes.
- Created LiquidHandlerTransferGroup.action with fields for source and target group names and unit volume.
- Both actions include response fields for return information and success status.

* Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists

* Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml

* result_info改为字典类型

* 新增uat的地址替换

* runze multiple pump support

(cherry picked from commit 49354fcf39)

* remove runze multiple software obtainer

(cherry picked from commit 8bcc92a394)

* support multiple backbone

(cherry picked from commit 4771ff2347)

* Update runze pump format

* Correct runze multiple backbone

* Update runze_multiple_backbone

* Correct runze pump multiple receive method.

* Correct runze pump multiple receive method.

* 对于PRCXI9320的transfer_group,一对多和多对多

* 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5

* fix import error

* fix dupe upload registry

* refactor ws client

* add server timeout

* Fix: run-column with correct vessel id (#86)

* fix run_column

* Update run_column_protocol.py

(cherry picked from commit e5aa4d940a)

* resource_update use resource_add

* 新增版位推荐功能

* 重新规定了版位推荐的入参

* update registry with nested obj

* fix protocol node log_message, added create_resource return value

* fix protocol node log_message, added create_resource return value

* try fix add protocol

* fix resource_add

* 修复移液站错误的aspirate注册表

* Feature/xprbalance-zhida (#80)

* feat(devices): add Zhida GC/MS pretreatment automation workstation

* feat(devices): add mettler_toledo xpr balance

* balance

* 重新补全zhida注册表

* PRCXI9320 json

* PRCXI9320 json

* PRCXI9320 json

* fix resource download

* remove class for resource

* bump version to 0.10.6

* 更新所有注册表

* 修复protocolnode的兼容性

* 修复protocolnode的兼容性

* Update install md

* Add Defaultlayout

* 更新物料接口

* fix dict to tree/nested-dict converter

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* frontend_docs

* create/update resources with POST/PUT for big amount/ small amount data

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

* Workstation templates: Resources and its CRUD, and workstation tasks (#95)

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

---------

Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>

* 更新物料接口

* Workstation dev yb2 (#100)

* Refactor and extend reaction station action messages

* Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities

- Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability.
- Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input.
- Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input.
- Refined `create_diamine_solution_task` to include additional parameters for better task configuration.
- Enhanced schema descriptions and default values for improved user guidance.

* 修复to_plr_resources

* add update remove

* 支持选择器注册表自动生成
支持转运物料

* 修复资源添加

* 修复transfer_resource_to_another生成

* 更新transfer_resource_to_another参数,支持spot入参

* 新增test_resource动作

* fix host_node error

* fix host_node test_resource error

* fix host_node test_resource error

* 过滤本地动作

* 移动内部action以兼容host node

* 修复同步任务报错不显示的bug

* feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了

* update todo

* modify bioyond/plr converter, bioyond resource registry, and tests

* pass the tests

* update todo

* add conda-pack-build.yml

* add auto install script for conda-pack-build.yml

(cherry picked from commit 172599adcf)

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* Add version in __init__.py
Update conda-pack-build.yml
Add create_zip_archive.py

* Update conda-pack-build.yml

* Update conda-pack-build.yml (with mamba)

* Update conda-pack-build.yml

* Fix FileNotFoundError

* Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined>

* Fix unilabos msgs search error

* Fix environment_check.py

* Update recipe.yaml

* Update registry. Update uuid loop figure method. Update install docs.

* Fix nested conda pack

* Fix one-key installation path error

* Bump version to 0.10.7

* Workshop bj (#99)

* Add LaiYu Liquid device integration and tests

Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included.

* feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档

refactor: 重新组织LaiYu_Liquid模块目录结构
docs: 添加SOPA移液器和步进电机控制指令文档
fix: 修正设备配置中的最大体积默认值
test: 新增工作台配置测试用例
chore: 删除过时的测试脚本和配置文件

* add

* 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用

- 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py
- 更新所有相关文件中的导入引用
- 保持代码功能不变,仅改善命名一致性
- 测试确认所有导入正常工作

* 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出

- 添加 LaiYuLiquidBackend 到导入列表
- 添加 LaiYuLiquidBackend 到 __all__ 导出列表
- 确保所有主要类都可以正确导入

* 修复大小写文件夹名字

* 电池装配工站二次开发教程(带目录)上传至dev (#94)

* 电池装配工站二次开发教程

* Update intro.md

* 物料教程

* 更新物料教程,json格式注释

* Update prcxi driver & fix transfer_liquid mix_times (#90)

* Update prcxi driver & fix transfer_liquid mix_times

* fix: correct mix_times type

* Update liquid_handler registry

* test: prcxi.py

* Update registry from pr

* fix ony-key script not exist

* clean files

---------

Co-authored-by: Junhan Chang <changjh@dp.tech>
Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>
Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: LccLink <1951855008@qq.com>
Co-authored-by: lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com>
Co-authored-by: shiyubo0410 <shiyubo@dp.tech>
2025-10-12 23:34:26 +08:00

1822 lines
66 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import collections
import contextlib
import json
import os
import socket
import time
from typing import Any, List, Dict, Optional, Tuple, TypedDict, Union, Sequence, Iterator, Literal
from pylabrobot.liquid_handling import (
LiquidHandlerBackend,
Pickup,
SingleChannelAspiration,
Drop,
SingleChannelDispense,
PickupTipRack,
DropTipRack,
MultiHeadAspirationPlate,
ChatterBoxBackend,
LiquidHandlerChatterboxBackend,
)
from pylabrobot.liquid_handling.standard import (
MultiHeadAspirationContainer,
MultiHeadDispenseContainer,
MultiHeadDispensePlate,
ResourcePickup,
ResourceMove,
ResourceDrop,
)
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
class PRCXIError(RuntimeError):
"""Lilith 返回 Success=false 时抛出的业务异常"""
class Material(TypedDict): # 和Plate同关系
uuid: str
Code: Optional[str]
Name: Optional[str]
SummaryName: Optional[str]
PipetteHeight: Optional[int]
materialEnum: Optional[int]
class WorkTablets(TypedDict):
Number: int
Code: str
Material: Dict[str, Any]
class MatrixInfo(TypedDict):
MatrixId: str
MatrixName: str
MatrixCount: int
WorkTablets: list[WorkTablets]
class PRCXI9300Deck(Deck):
"""PRCXI 9300 的专用 Deck 类,继承自 Deck。
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
"""
def __init__(self, name: str, size_x: float, size_y: float, size_z: float):
super().__init__(name, size_x, size_y, size_z)
self.slots = [None] * 6 # PRCXI 9300 有 6 个槽位
class PRCXI9300Container(Plate, TipRack):
"""PRCXI 9300 的专用 Container 类,继承自 Plate和TipRack。
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str,
ordering: collections.OrderedDict,
model: Optional[str] = None,
):
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
self._unilabos_state = {}
def load_state(self, state: Dict[str, Any]) -> None:
"""从给定的状态加载工作台信息。"""
super().load_state(state)
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
data = super().serialize_state()
data.update(self._unilabos_state)
return data
class PRCXI9300Trash(Trash):
"""PRCXI 9300 的专用 Trash 类,继承自 Trash。
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
"""
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, category: str, **kwargs):
if name != "trash":
name = "trash"
print("PRCXI9300Trash name must be 'trash', using 'trash' instead.")
super().__init__(name, size_x, size_y, size_z, category=category, **kwargs)
self._unilabos_state = {}
def load_state(self, state: Dict[str, Any]) -> None:
"""从给定的状态加载工作台信息。"""
# super().load_state(state)
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
data = super().serialize_state()
data.update(self._unilabos_state)
return data
class PRCXI9300Handler(LiquidHandlerAbstract):
support_touch_tip = False
@property
def reset_ok(self) -> bool:
"""检查设备是否已重置成功。"""
if self._unilabos_backend.debug:
return True
return self._unilabos_backend.is_reset_ok
def __init__(
self,
deck: Deck,
host: str,
port: int,
timeout: float,
channel_num=8,
axis="Left",
setup=True,
debug=False,
simulator=False,
matrix_id="",
is_9320=False,
):
tablets_info = []
count = 0
for child in deck.children:
if "Material" in child._unilabos_state:
count += 1
tablets_info.append(
WorkTablets(Number=count, Code=f"T{count}", Material=child._unilabos_state["Material"])
)
if is_9320:
print("当前设备是9320")
self._unilabos_backend = PRCXI9300Backend(
tablets_info, host, port, timeout, channel_num, axis, setup, debug, matrix_id, is_9320
)
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
return super().set_liquid(wells, liquid_names, volumes)
def set_group(self, group_name: str, wells: List[Well], volumes: List[float]):
return super().set_group(group_name, wells, volumes)
async def transfer_group(self, source_group_name: str, target_group_name: str, unit_volume: float):
return await super().transfer_group(source_group_name, target_group_name, unit_volume)
async def create_protocol(
self,
protocol_name: str = "",
protocol_description: str = "",
protocol_version: str = "",
protocol_author: str = "",
protocol_date: str = "",
protocol_type: str = "",
none_keys: List[str] = [],
):
self._unilabos_backend.create_protocol(protocol_name)
async def run_protocol(self):
return self._unilabos_backend.run_protocol()
async def remove_liquid(
self,
vols: List[float],
sources: Sequence[Container],
waste_liquid: Optional[Container] = None,
*,
use_channels: Optional[List[int]] = None,
flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
delays: Optional[List[int]] = None,
is_96_well: Optional[bool] = False,
top: Optional[List[float]] = None,
none_keys: List[str] = [],
):
return await super().remove_liquid(
vols,
sources,
waste_liquid,
use_channels=use_channels,
flow_rates=flow_rates,
offsets=offsets,
liquid_height=liquid_height,
blow_out_air_volume=blow_out_air_volume,
spread=spread,
delays=delays,
is_96_well=is_96_well,
top=top,
none_keys=none_keys,
)
async def add_liquid(
self,
asp_vols: Union[List[float], float],
dis_vols: Union[List[float], float],
reagent_sources: Sequence[Container],
targets: Sequence[Container],
*,
use_channels: Optional[List[int]] = None,
flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
is_96_well: bool = False,
delays: Optional[List[int]] = None,
mix_time: Optional[int] = None,
mix_vol: Optional[int] = None,
mix_rate: Optional[int] = None,
mix_liquid_height: Optional[float] = None,
none_keys: List[str] = [],
):
return await super().add_liquid(
asp_vols,
dis_vols,
reagent_sources,
targets,
use_channels=use_channels,
flow_rates=flow_rates,
offsets=offsets,
liquid_height=liquid_height,
blow_out_air_volume=blow_out_air_volume,
spread=spread,
is_96_well=is_96_well,
delays=delays,
mix_time=mix_time,
mix_vol=mix_vol,
mix_rate=mix_rate,
mix_liquid_height=mix_liquid_height,
none_keys=none_keys,
)
async def transfer_liquid(
self,
sources: Sequence[Container],
targets: Sequence[Container],
tip_racks: Sequence[TipRack],
*,
use_channels: Optional[List[int]] = None,
asp_vols: Union[List[float], float],
dis_vols: Union[List[float], float],
asp_flow_rates: Optional[List[Optional[float]]] = None,
dis_flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
touch_tip: bool = False,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Literal["wide", "tight", "custom"] = "wide",
is_96_well: bool = False,
mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
mix_times: Optional[List[int]] = None,
mix_vol: Optional[int] = None,
mix_rate: Optional[int] = None,
mix_liquid_height: Optional[float] = None,
delays: Optional[List[int]] = None,
none_keys: List[str] = [],
):
return await super().transfer_liquid(
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):
return await super().custom_delay(seconds, msg)
async def touch_tip(self, targets: Sequence[Container]):
return await super().touch_tip(targets)
async def mix(
self,
targets: Sequence[Container],
mix_time: int = None,
mix_vol: Optional[int] = None,
height_to_bottom: Optional[float] = None,
offsets: Optional[Coordinate] = None,
mix_rate: Optional[float] = None,
none_keys: List[str] = [],
):
return await self._unilabos_backend.mix(
targets, mix_time, mix_vol, height_to_bottom, offsets, mix_rate, none_keys
)
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
return super().iter_tips(tip_racks)
async def pick_up_tips(
self,
tip_spots: List[TipSpot],
use_channels: Optional[List[int]] = None,
offsets: Optional[List[Coordinate]] = None,
**backend_kwargs,
):
return await super().pick_up_tips(tip_spots, use_channels, offsets, **backend_kwargs)
async def aspirate(
self,
resources: Sequence[Container],
vols: List[float],
use_channels: Optional[List[int]] = None,
flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Literal["wide", "tight", "custom"] = "wide",
**backend_kwargs,
):
return await super().aspirate(
resources,
vols,
use_channels,
flow_rates,
offsets,
liquid_height,
blow_out_air_volume,
spread,
**backend_kwargs,
)
async def drop_tips(
self,
tip_spots: Sequence[Union[TipSpot, Trash]],
use_channels: Optional[List[int]] = None,
offsets: Optional[List[Coordinate]] = None,
allow_nonzero_volume: bool = False,
**backend_kwargs,
):
return await super().drop_tips(tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs)
async def dispense(
self,
resources: Sequence[Container],
vols: List[float],
use_channels: Optional[List[int]] = None,
flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Literal["wide", "tight", "custom"] = "wide",
**backend_kwargs,
):
return await super().dispense(
resources,
vols,
use_channels,
flow_rates,
offsets,
liquid_height,
blow_out_air_volume,
spread,
**backend_kwargs,
)
async def discard_tips(
self,
use_channels: Optional[List[int]] = None,
allow_nonzero_volume: bool = True,
offsets: Optional[List[Coordinate]] = None,
**backend_kwargs,
):
return await super().discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
def set_tiprack(self, tip_racks: Sequence[TipRack]):
super().set_tiprack(tip_racks)
async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0):
return await super().move_to(well, dis_to_top, channel)
class PRCXI9300Backend(LiquidHandlerBackend):
"""PRCXI 9300 的后端实现,继承自 LiquidHandlerBackend。
该类提供了与 PRCXI 9300 设备进行通信的基本方法,包括方案管理、自动化控制、运行状态查询等。
"""
_num_channels = 8 # 默认通道数为 8
_is_reset_ok = False
@property
def is_reset_ok(self) -> bool:
self._is_reset_ok = self.api_client.get_reset_status()
return self._is_reset_ok
matrix_info: MatrixInfo
protocol_name: str
steps_todo_list = []
def __init__(
self,
tablets_info: list[WorkTablets],
host: str = "127.0.0.1",
port: int = 9999,
timeout: float = 10.0,
channel_num: int = 8,
axis: str = "Left",
setup=True,
debug=False,
matrix_id="",
is_9320=False,
) -> None:
super().__init__()
self.tablets_info = tablets_info
self.matrix_id = matrix_id
self.api_client = PRCXI9300Api(host, port, timeout, axis, debug, is_9320)
self.host, self.port, self.timeout = host, port, timeout
self._num_channels = channel_num
self._execute_setup = setup
self.debug = debug
def create_protocol(self, protocol_name):
self.protocol_name = protocol_name
self.steps_todo_list = []
def run_protocol(self):
assert self.is_reset_ok, "PRCXI9300Backend is not reset successfully. Please call setup() first."
run_time = time.time()
self.matrix_info = MatrixInfo(
MatrixId=f"{int(run_time)}",
MatrixName=f"protocol_{run_time}",
MatrixCount=len(self.tablets_info),
WorkTablets=self.tablets_info,
)
# print(json.dumps(self.matrix_info, indent=2))
if not len(self.matrix_id):
res = self.api_client.add_WorkTablet_Matrix(self.matrix_info)
assert res["Success"], f"Failed to create matrix: {res.get('Message', 'Unknown error')}"
print(f"PRCXI9300Backend created matrix with ID: {self.matrix_info['MatrixId']}, result: {res}")
solution_id = self.api_client.add_solution(
f"protocol_{run_time}", self.matrix_info["MatrixId"], self.steps_todo_list
)
else:
print(f"PRCXI9300Backend using predefined worktable {self.matrix_id}, skipping matrix creation.")
solution_id = self.api_client.add_solution(f"protocol_{run_time}", self.matrix_id, self.steps_todo_list)
print(f"PRCXI9300Backend created solution with ID: {solution_id}")
self.api_client.load_solution(solution_id)
print(json.dumps(self.steps_todo_list, indent=2))
return self.api_client.start()
@classmethod
def check_channels(cls, use_channels: List[int]) -> List[int]:
"""检查通道是否符合要求PRCXI9300Backend 只支持所有 8 个通道。"""
if use_channels != [0, 1, 2, 3, 4, 5, 6, 7]:
print("PRCXI9300Backend only supports all 8 channels, using default [0, 1, 2, 3, 4, 5, 6, 7].")
return [0, 1, 2, 3, 4, 5, 6, 7]
return use_channels
async def setup(self):
await super().setup()
try:
if self._execute_setup:
self.api_client.call("IAutomation", "Reset")
while not self.is_reset_ok:
print("Waiting for PRCXI9300 to reset...")
await asyncio.sleep(1)
print("PRCXI9300 reset successfully.")
except ConnectionRefusedError as e:
raise RuntimeError(
f"Failed to connect to PRCXI9300 API at {self.host}:{self.port}. "
"Please ensure the PRCXI9300 service is running."
) from e
async def stop(self):
self.api_client.call("IAutomation", "Stop")
async def pick_up_tips(self, ops: List[Pickup], use_channels: List[int] = None):
"""Pick up tips from the specified resource."""
plate_indexes = []
for op in ops:
plate = op.resource.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)}")
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in ops:
tipspot = op.resource
tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
hole_row = 1
if self._num_channels == 1:
hole_row = tipspot_index % 8 + 1
step = self.api_client.Load(
dosage=0,
plate_no=PlateNo,
is_whole_plate=False,
hole_row=hole_row,
hole_col=hole_col,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers=f"{(hole_col - 1) * 8 + hole_row}" if self._num_channels == 1 else "1,2,3,4,5",
)
self.steps_todo_list.append(step)
async def drop_tips(self, ops: List[Drop], use_channels: List[int] = None):
"""Pick up tips from the specified resource."""
# 检查trash #
if ops[0].resource.name == "trash":
PlateNo = ops[0].resource.parent.children.index(ops[0].resource) + 1
step = self.api_client.UnLoad(
dosage=0,
plate_no=PlateNo,
is_whole_plate=False,
hole_row=1,
hole_col=3,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{1}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
return
# print(ops[0].resource.parent.children.index(ops[0].resource))
plate_indexes = []
for op in ops:
plate = op.resource.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError(
"All drop_tips must be from the same plate. Found different plates: " + str(plate_indexes)
)
tip_columns = []
for op in ops:
tipspot = op.resource
tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError(
"All drop_tips must be from the same tip column. Found different columns: " + str(tip_columns)
)
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
if self.channel_num == 1:
hole_row = tipspot_index % 8 + 1
step = self.api_client.UnLoad(
dosage=0,
plate_no=PlateNo,
is_whole_plate=False,
hole_row=hole_row,
hole_col=hole_col,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def mix(
self,
targets: Sequence[Container],
mix_time: int = None,
mix_vol: Optional[int] = None,
height_to_bottom: Optional[float] = None,
offsets: Optional[Coordinate] = None,
mix_rate: Optional[float] = None,
none_keys: List[str] = [],
):
"""Mix liquid in the specified resources."""
plate_indexes = []
for op in targets:
deck = op.parent.parent
plate = op.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in targets:
tipspot_index = op.parent.children.index(op)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError(
"All pickups must be from the same tip column. Found different columns: " + str(tip_columns)
)
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
hole_row = 1
if self.num_channels == 1:
hole_row = tipspot_index % 8 + 1
assert mix_time > 0
step = self.api_client.Blending(
dosage=mix_vol,
plate_no=PlateNo,
is_whole_plate=False,
hole_row=hole_row,
hole_col=hole_col,
blending_times=mix_time,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def aspirate(self, ops: List[SingleChannelAspiration], use_channels: List[int] = None):
"""Aspirate liquid from the specified resources."""
plate_indexes = []
for op in ops:
plate = op.resource.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in ops:
tipspot = op.resource
tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError(
"All pickups must be from the same tip column. Found different columns: " + str(tip_columns)
)
volumes = [op.volume for op in ops]
if len(set(volumes)) != 1:
raise ValueError("All aspirate volumes must be the same. Found different volumes: " + str(volumes))
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
hole_row = 1
if self.num_channels == 1:
hole_row = tipspot_index % 8 + 1
step = self.api_client.Imbibing(
dosage=int(volumes[0]),
plate_no=PlateNo,
is_whole_plate=False,
hole_row=hole_row,
hole_col=hole_col,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def dispense(self, ops: List[SingleChannelDispense], use_channels: List[int] = None):
"""Dispense liquid into the specified resources."""
plate_indexes = []
for op in ops:
plate = op.resource.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All dispense must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in ops:
tipspot = op.resource
tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError(
"All dispense must be from the same tip column. Found different columns: " + str(tip_columns)
)
volumes = [op.volume for op in ops]
if len(set(volumes)) != 1:
raise ValueError("All dispense volumes must be the same. Found different volumes: " + str(volumes))
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
hole_row = 1
if self.num_channels == 1:
hole_row = tipspot_index % 8 + 1
step = self.api_client.Tapping(
dosage=int(volumes[0]),
plate_no=PlateNo,
is_whole_plate=False,
hole_row=hole_row,
hole_col=hole_col,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def pick_up_tips96(self, pickup: PickupTipRack):
raise NotImplementedError("The PRCXI backend does not support the 96 head.")
async def drop_tips96(self, drop: DropTipRack):
raise NotImplementedError("The PRCXI backend does not support the 96 head.")
async def aspirate96(self, aspiration: Union[MultiHeadAspirationPlate, MultiHeadAspirationContainer]):
raise NotImplementedError("The Opentrons backend does not support the 96 head.")
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
raise NotImplementedError("The Opentrons backend does not support the 96 head.")
async def pick_up_resource(self, pickup: ResourcePickup):
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
async def move_picked_up_resource(self, move: ResourceMove):
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
async def drop_resource(self, drop: ResourceDrop):
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
def can_pick_up_tip(self, channel_idx: int, tip: Tip) -> bool:
return True # PRCXI9300Backend does not have tip compatibility issues
def serialize(self) -> dict:
raise NotImplementedError()
@property
def num_channels(self) -> int:
return self._num_channels
class PRCXI9300Api:
def __init__(
self,
host: str = "127.0.0.1",
port: int = 9999,
timeout: float = 10.0,
axis="Left",
debug: bool = False,
is_9320: bool = False,
) -> None:
self.host, self.port, self.timeout = host, port, timeout
self.debug = debug
self.axis = axis
self.is_9320 = is_9320
@staticmethod
def _len_prefix(n: int) -> bytes:
return bytes.fromhex(format(n, "016x"))
def _raw_request(self, payload: str) -> str:
if self.debug:
return " "
with contextlib.closing(socket.socket()) as sock:
sock.settimeout(self.timeout)
sock.connect((self.host, self.port))
data = payload.encode()
sock.sendall(self._len_prefix(len(data)) + data)
chunks, first = [], True
while True:
chunk = sock.recv(4096)
if not chunk:
break
if first:
chunk, first = chunk[8:], False
chunks.append(chunk)
return b"".join(chunks).decode()
# ---------------------------------------------------- 方案相关ISolution
def list_solutions(self) -> List[Dict[str, Any]]:
"""GetSolutionList"""
return self.call("ISolution", "GetSolutionList")
def load_solution(self, solution_id: str) -> bool:
"""LoadSolution"""
return self.call("ISolution", "LoadSolution", [solution_id])
def add_solution(self, name: str, matrix_id: str, steps: List[Dict[str, Any]]) -> str:
"""AddSolution → 返回新方案 GUID"""
return self.call("ISolution", "AddSolution", [name, matrix_id, steps])
# ---------------------------------------------------- 自动化控制IAutomation
def start(self) -> bool:
return self.call("IAutomation", "Start")
def call(self, service: str, method: str, params: Optional[list] = None) -> Any:
payload = json.dumps(
{"ServiceName": service, "MethodName": method, "Paramters": params or []}, separators=(",", ":")
)
resp = json.loads(self._raw_request(payload))
if not resp.get("Success", False):
raise PRCXIError(resp.get("Msg", "Unknown error"))
data = resp.get("Data")
try:
return json.loads(data)
except (TypeError, json.JSONDecodeError):
return data
def pause(self) -> bool:
"""Pause"""
return self.call("IAutomation", "Pause")
def resume(self) -> bool:
"""Resume"""
return self.call("IAutomation", "Resume")
def get_error_code(self) -> Optional[str]:
"""GetErrorCode"""
return self.call("IAutomation", "GetErrorCode")
def get_reset_status(self) -> bool:
"""GetErrorCode"""
if self.debug:
return True
res = self.call("IAutomation", "GetResetStatus")
return not res
def clear_error_code(self) -> bool:
"""RemoveErrorCodet"""
return self.call("IAutomation", "RemoveErrorCodet")
# ---------------------------------------------------- 运行状态IMachineState
def step_state_list(self) -> List[Dict[str, Any]]:
"""GetStepStateList"""
return self.call("IMachineState", "GetStepStateList")
def step_status(self, seq_num: int) -> Dict[str, Any]:
"""GetStepStatus"""
return self.call("IMachineState", "GetStepStatus", [seq_num])
def step_state(self, seq_num: int) -> Dict[str, Any]:
"""GetStepState"""
return self.call("IMachineState", "GetStepState", [seq_num])
def axis_location(self, axis_num: int = 1) -> Dict[str, Any]:
"""GetLocation"""
return self.call("IMachineState", "GetLocation", [axis_num])
# ---------------------------------------------------- 版位矩阵IMatrix
def get_all_materials(self) -> Dict[str, Any]:
"""GetStepState"""
return self.call("IMatrix", "GetAllMaterial", [])
def list_matrices(self) -> List[Dict[str, Any]]:
"""GetWorkTabletMatrices"""
return self.call("IMatrix", "GetWorkTabletMatrices")
def matrix_by_id(self, matrix_id: str) -> Dict[str, Any]:
"""GetWorkTabletMatrixById"""
return self.call("IMatrix", "GetWorkTabletMatrixById", [matrix_id])
def add_WorkTablet_Matrix(self, matrix: MatrixInfo):
return self.call("IMatrix", "AddWorkTabletMatrix2" if self.is_9320 else "AddWorkTabletMatrix", [matrix])
def Load(
self,
dosage: int,
plate_no: int,
is_whole_plate: bool,
hole_row: int,
hole_col: int,
blending_times: int,
balance_height: int,
plate_or_hole: str,
hole_numbers: str,
assist_fun1: str = "",
assist_fun2: str = "",
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": self.axis,
"Function": "Load",
"DosageNum": dosage,
"PlateNo": plate_no,
"IsWholePlate": is_whole_plate,
"HoleRow": hole_row,
"HoleCol": hole_col,
"BlendingTimes": blending_times,
"BalanceHeight": balance_height,
"PlateOrHoleNum": plate_or_hole,
"AssistFun1": assist_fun1,
"AssistFun2": assist_fun2,
"AssistFun3": assist_fun3,
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method,
}
def Imbibing(
self,
dosage: int,
plate_no: int,
is_whole_plate: bool,
hole_row: int,
hole_col: int,
blending_times: int,
balance_height: int,
plate_or_hole: str,
hole_numbers: str,
assist_fun1: str = "",
assist_fun2: str = "",
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": self.axis,
"Function": "Imbibing",
"DosageNum": dosage,
"PlateNo": plate_no,
"IsWholePlate": is_whole_plate,
"HoleRow": hole_row,
"HoleCol": hole_col,
"BlendingTimes": blending_times,
"BalanceHeight": balance_height,
"PlateOrHoleNum": plate_or_hole,
"AssistFun1": assist_fun1,
"AssistFun2": assist_fun2,
"AssistFun3": assist_fun3,
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method,
}
def Tapping(
self,
dosage: int,
plate_no: int,
is_whole_plate: bool,
hole_row: int,
hole_col: int,
blending_times: int,
balance_height: int,
plate_or_hole: str,
hole_numbers: str,
assist_fun1: str = "",
assist_fun2: str = "",
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": self.axis,
"Function": "Tapping",
"DosageNum": dosage,
"PlateNo": plate_no,
"IsWholePlate": is_whole_plate,
"HoleRow": hole_row,
"HoleCol": hole_col,
"BlendingTimes": blending_times,
"BalanceHeight": balance_height,
"PlateOrHoleNum": plate_or_hole,
"AssistFun1": assist_fun1,
"AssistFun2": assist_fun2,
"AssistFun3": assist_fun3,
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method,
}
def Blending(
self,
dosage: int,
plate_no: int,
is_whole_plate: bool,
hole_row: int,
hole_col: int,
blending_times: int,
balance_height: int,
plate_or_hole: str,
hole_numbers: str,
assist_fun1: str = "",
assist_fun2: str = "",
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": self.axis,
"Function": "Blending",
"DosageNum": dosage,
"PlateNo": plate_no,
"IsWholePlate": is_whole_plate,
"HoleRow": hole_row,
"HoleCol": hole_col,
"BlendingTimes": blending_times,
"BalanceHeight": balance_height,
"PlateOrHoleNum": plate_or_hole,
"AssistFun1": assist_fun1,
"AssistFun2": assist_fun2,
"AssistFun3": assist_fun3,
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method,
}
def UnLoad(
self,
dosage: int,
plate_no: int,
is_whole_plate: bool,
hole_row: int,
hole_col: int,
blending_times: int,
balance_height: int,
plate_or_hole: str,
hole_numbers: str,
assist_fun1: str = "",
assist_fun2: str = "",
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": self.axis,
"Function": "UnLoad",
"DosageNum": dosage,
"PlateNo": plate_no,
"IsWholePlate": is_whole_plate,
"HoleRow": hole_row,
"HoleCol": hole_col,
"BlendingTimes": blending_times,
"BalanceHeight": balance_height,
"PlateOrHoleNum": plate_or_hole,
"AssistFun1": assist_fun1,
"AssistFun2": assist_fun2,
"AssistFun3": assist_fun3,
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method,
}
class DefaultLayout:
def __init__(self, product_name: str = "PRCXI9300"):
self.labresource = {}
if product_name not in ["PRCXI9300", "PRCXI9320"]:
raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.")
if product_name == "PRCXI9300":
self.rows = 2
self.columns = 3
self.layout = [1, 2, 3, 4, 5, 6]
self.trash_slot = 3
self.waste_liquid_slot = 6
elif product_name == "PRCXI9320":
self.rows = 3
self.columns = 4
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
self.trash_slot = 16
self.waste_liquid_slot = 12
self.default_layout = {"MatrixId":f"{time.time()}","MatrixName":f"{time.time()}","MatrixCount":16,"WorkTablets":
[{"Number": 1, "Code": "T1", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 2, "Code": "T2", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 3, "Code": "T3", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 4, "Code": "T4", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 5, "Code": "T5", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 6, "Code": "T6", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 7, "Code": "T7", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 8, "Code": "T8", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 9, "Code": "T9", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 10, "Code": "T10", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 11, "Code": "T11", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 12, "Code": "T12", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}}, # 这个设置成废液槽,用储液槽表示
{"Number": 13, "Code": "T13", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 14, "Code": "T14", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 15, "Code": "T15", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 16, "Code": "T16", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}} # 这个设置成垃圾桶,用储液槽表示
]
}
def get_layout(self) -> Dict[str, Any]:
return {
"rows": self.rows,
"columns": self.columns,
"layout": self.layout,
"trash_slot": self.trash_slot,
"waste_liquid_slot": self.waste_liquid_slot
}
def get_trash_slot(self) -> int:
return self.trash_slot
def get_waste_liquid_slot(self) -> int:
return self.waste_liquid_slot
def add_lab_resource(self, material_info):
self.labresource = material_info
def recommend_layout(self, needs: List[Tuple[str, str, int]]) -> Dict[str, Any]:
layout_list = []
for reagent_name, material_name, count in needs:
if material_name not in self.labresource:
raise ValueError(f"Material {reagent_name} not found in lab resources.")
# 预留位置12和16不动
reserved_positions = {12, 16}
available_positions = [i for i in range(1, 17) if i not in reserved_positions]
# 计算总需求
total_needed = sum(count for _, _, count in needs)
if total_needed > len(available_positions):
raise ValueError(f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置排除位置12和16")
# 依次分配位置
current_pos = 0
for reagent_name, material_name, count in needs:
material_uuid = self.labresource[material_name]['uuid']
material_enum = self.labresource[material_name]['materialEnum']
for _ in range(count):
if current_pos >= len(available_positions):
raise ValueError("位置不足,无法分配更多物料")
position = available_positions[current_pos]
# 找到对应的tablet并更新
for tablet in self.default_layout['WorkTablets']:
if tablet['Number'] == position:
tablet['Material']['uuid'] = material_uuid
tablet['Material']['materialEnum'] = material_enum
layout_list.append(dict(reagent_name=reagent_name, material_name=material_name, positions=position))
break
current_pos += 1
return self.default_layout, layout_list
if __name__ == "__main__":
# Example usage
# 1. 用导出的json给每个T1 T2板子设定相应的物料如果是孔板和枪头盒要对应区分
# 2. backend需要支持num channel为1的情况
# 3. 设计一个单点动作流程,可以跑
# 4.
# deck = PRCXI9300Deck(name="PRCXI_Deck_9300", size_x=100, size_y=100, size_z=100)
# from pylabrobot.resources.opentrons.tip_racks import opentrons_96_tiprack_300ul,opentrons_96_tiprack_10ul
# from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
# def get_well_container(name: str) -> PRCXI9300Container:
# well_containers = corning_96_wellplate_360ul_flat(name).serialize()
# plate = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="plate",
# ordering=well_containers["ordering"])
# plate_serialized = plate.serialize()
# plate_serialized["parent_name"] = deck.name
# well_containers.update({k: v for k, v in plate_serialized.items() if k not in ["children"]})
# new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
# return new_plate
# def get_tip_rack(name: str) -> PRCXI9300Container:
# tip_racks = opentrons_96_tiprack_300ul("name").serialize()
# tip_rack = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="tip_rack",
# ordering=tip_racks["ordering"])
# tip_rack_serialized = tip_rack.serialize()
# tip_rack_serialized["parent_name"] = deck.name
# tip_racks.update({k: v for k, v in tip_rack_serialized.items() if k not in ["children"]})
# new_tip_rack: PRCXI9300Container = PRCXI9300Container.deserialize(tip_racks)
# return new_tip_rack
# plate1 = get_tip_rack("RackT1")
# plate1.load_state({
# "Material": {
# "uuid": "076250742950465b9d6ea29a225dfb00",
# "Code": "ZX-001-300",
# "Name": "300μL Tip头"
# }
# })
# plate2 = get_well_container("PlateT2")
# plate2.load_state({
# "Material": {
# "uuid": "57b1e4711e9e4a32b529f3132fc5931f",
# "Code": "ZX-019-2.2",
# "Name": "96深孔板"
# }
# })
# plate3 = PRCXI9300Trash("trash", size_x=50, size_y=100, size_z=10, category="trash")
# plate3.load_state({
# "Material": {
# "uuid": "730067cf07ae43849ddf4034299030e9"
# }
# })
# plate4 = get_well_container("PlateT4")
# plate4.load_state({
# "Material": {
# "uuid": "57b1e4711e9e4a32b529f3132fc5931f",
# "Code": "ZX-019-2.2",
# "Name": "96深孔板"
# }
# })
# plate5 = get_well_container("PlateT5")
# plate5.load_state({
# "Material": {
# "uuid": "57b1e4711e9e4a32b529f3132fc5931f",
# "Code": "ZX-019-2.2",
# "Name": "96深孔板"
# }
# })
# plate6 = get_well_container("PlateT6")
# plate6.load_state({
# "Material": {
# "uuid": "57b1e4711e9e4a32b529f3132fc5931f",
# "Code": "ZX-019-2.2",
# "Name": "96深孔板"
# }
# })
# deck.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
# deck.assign_child_resource(plate2, location=Coordinate(0, 0, 0))
# deck.assign_child_resource(plate3, location=Coordinate(0, 0, 0))
# deck.assign_child_resource(plate4, location=Coordinate(0, 0, 0))
# deck.assign_child_resource(plate5, location=Coordinate(0, 0, 0))
# deck.assign_child_resource(plate6, location=Coordinate(0, 0, 0))
# # # plate_2_liquids = [[('water', 500)]]*96
# # # plate2.set_well_liquids(plate_2_liquids)
# handler = PRCXI9300Handler(deck=deck, host="10.181.214.132", port=9999,
# timeout=10.0, setup=False, debug=False,
# simulator=True,
# matrix_id="71593",
# channel_num=8, axis="Left") # Initialize the handler with the deck and host settings
# plate_2_liquids = handler.set_group("water", plate2.children[:8], [200]*8)
# plate5_liquids = handler.set_group("master_mix", plate5.children[:8], [100]*8)
# handler.set_tiprack([plate1])
# asyncio.run(handler.setup()) # Initialize the handler and setup the connection
# from pylabrobot.resources import set_volume_tracking
# from pylabrobot.resources import set_tip_tracking
# set_volume_tracking(enabled=True)
# from unilabos.resources.graphio import *
# # A = tree_to_list([resource_plr_to_ulab(deck)])
# # with open("deck_9300_new.json", "w", encoding="utf-8") as f:
# # json.dump(A, f, indent=4, ensure_ascii=False)
# asyncio.run(handler.create_protocol(protocol_name="Test Protocol")) # Initialize the backend and setup the connection
# asyncio.run(handler.transfer_group("water", "master_mix", 100)) # Reset tip tracking
# asyncio.run(handler.pick_up_tips(plate1.children[:8],[0,1,2,3,4,5,6,7]))
# print(plate1.children[:8])
# asyncio.run(handler.aspirate(plate2.children[:8],[50]*8, [0,1,2,3,4,5,6,7]))
# print(plate2.children[:8])
# asyncio.run(handler.dispense(plate5.children[:8],[50]*8,[0,1,2,3,4,5,6,7]))
# print(plate5.children[:8])
# #asyncio.run(handler.drop_tips(tip_rack.children[8:16],[0,1,2,3,4,5,6,7]))
# asyncio.run(handler.discard_tips([0,1,2,3,4,5,6,7]))
# asyncio.run(handler.mix(well_containers.children[:8
# ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
# #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
# asyncio.run(handler.add_liquid(
# asp_vols=[100]*16,
# dis_vols=[100]*16,
# reagent_sources=plate2.children[:16],
# targets=plate5.children[:16],
# use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# flow_rates=[None] * 32,
# offsets=[Coordinate(0, 0, 0)] * 32,
# liquid_height=[None] * 16,
# blow_out_air_volume=[None] * 16,
# delays=None,
# mix_time=3,
# mix_vol=50,
# spread="wide",
# ))
# asyncio.run(handler.run_protocol()) # Run the protocol
# asyncio.run(handler.remove_liquid(
# vols=[100]*16,
# sources=plate2.children[-16:],
# waste_liquid=plate5.children[:16], # 这个有些奇怪,但是好像也只能这么写
# use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# flow_rates=[None] * 32,
# offsets=[Coordinate(0, 0, 0)] * 32,
# liquid_height=[None] * 32,
# blow_out_air_volume=[None] * 32,
# spread="wide",
# ))
# acid = [20]*8+[40]*8+[60]*8+[80]*8+[100]*8+[120]*8+[140]*8+[160]*8+[180]*8+[200]*8+[220]*8+[240]*8
# alkaline = acid[::-1] # Reverse the acid list for alkaline
# asyncio.run(handler.transfer_liquid(
# asp_vols=acid,
# dis_vols=acid,
# tip_racks=[plate1],
# sources=plate2.children[:],
# targets=plate5.children[:],
# use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# offsets=[Coordinate(0, 0, 0)] * 32,
# asp_flow_rates=[None] * 16,
# dis_flow_rates=[None] * 16,
# liquid_height=[None] * 32,
# blow_out_air_volume=[None] * 32,
# mix_times=3,
# mix_vol=50,
# spread="wide",
# ))
# asyncio.run(handler.run_protocol()) # Run the protocol
# # input("Running protocol...")
# # input("Press Enter to continue...") # Wait for user input before proceeding
# # print("PRCXI9300Handler initialized with deck and host settings.")
### 9320 ###
deck = PRCXI9300Deck(name="PRCXI_Deck", size_x=100, size_y=100, size_z=100)
from pylabrobot.resources.opentrons.tip_racks import tipone_96_tiprack_200ul, opentrons_96_tiprack_10ul
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
def get_well_container(name: str) -> PRCXI9300Container:
well_containers = corning_96_wellplate_360ul_flat(name).serialize()
plate = PRCXI9300Container(
name=name, size_x=50, size_y=50, size_z=10, category="plate", ordering=well_containers["ordering"]
)
plate_serialized = plate.serialize()
plate_serialized["parent_name"] = deck.name
well_containers.update({k: v for k, v in plate_serialized.items() if k not in ["children"]})
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
return new_plate
def get_tip_rack(name: str, child_prefix: str="tip") -> PRCXI9300Container:
tip_racks = opentrons_96_tiprack_10ul(name).serialize()
tip_rack = PRCXI9300Container(
name=name, size_x=50, size_y=50, size_z=10, category="tip_rack", ordering=collections.OrderedDict({
k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()
})
)
tip_rack_serialized = tip_rack.serialize()
tip_rack_serialized["parent_name"] = deck.name
tip_racks.update({k: v for k, v in tip_rack_serialized.items() if k not in ["children"]})
new_tip_rack: PRCXI9300Container = PRCXI9300Container.deserialize(tip_racks)
return new_tip_rack
plate1 = get_tip_rack("RackT1")
plate1.load_state(
{"Material": {"uuid": "068b3815e36b4a72a59bae017011b29f", "Code": "ZX-001-10+", "Name": "10μL加长 Tip头"}}
)
plate2 = get_well_container("PlateT2")
plate2.load_state(
{"Material": {"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f", "Code": "ZX-78-096", "Name": "细菌培养皿"}}
)
plate3 = get_well_container("PlateT3")
plate3.load_state(
{
"Material": {
"uuid": "04211a2dc93547fe9bf6121eac533650",
}
}
)
plate4 = get_well_container("PlateT4")
plate4.load_state(
{"Material": {"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f", "Code": "ZX-78-096", "Name": "细菌培养皿"}}
)
plate5 = get_tip_rack("RackT5")
plate5.load_state(
{
"Material": {
"uuid": "076250742950465b9d6ea29a225dfb00",
"Code": "ZX-001-300",
"SupplyType": 1,
"Name": "300μL Tip头",
}
}
)
plate6 = get_well_container("PlateT6")
plate6.load_state(
{
"Material": {
"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7",
"Code": "1",
"SupplyType": 1,
"Name": "ep适配器",
"SummaryName": "ep适配器",
}
}
)
plate7 = PRCXI9300Container(
name="plateT7", size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict()
)
plate7.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
plate8 = get_tip_rack("PlateT8")
plate8.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
plate9 = get_well_container("PlateT9")
plate9.load_state(
{
"Material": {
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
"Code": "ZX-58-0001",
"SupplyType": 2,
"Name": "全裙边 PCR适配器",
"SummaryName": "全裙边 PCR适配器",
}
}
)
plate10 = get_well_container("PlateT10")
plate10.load_state(
{
"Material": {
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
"Code": "ZX-58-0001",
"SupplyType": 2,
"Name": "全裙边 PCR适配器",
"SummaryName": "全裙边 PCR适配器",
}
}
)
plate11 = get_well_container("PlateT11")
plate11.load_state(
{
"Material": {
"uuid": "04211a2dc93547fe9bf6121eac533650",
}
}
)
plate12 = get_well_container("PlateT12")
plate12.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
plate13 = get_well_container("PlateT13")
plate13.load_state(
{
"Material": {
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
"Code": "ZX-58-0001",
"SupplyType": 2,
"Name": "全裙边 PCR适配器",
"SummaryName": "全裙边 PCR适配器",
}
}
),
plate14 = get_well_container("PlateT14")
plate14.load_state(
{
"Material": {
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
"Code": "ZX-58-0001",
"SupplyType": 2,
"Name": "全裙边 PCR适配器",
"SummaryName": "全裙边 PCR适配器",
}
}
),
plate15 = get_well_container("PlateT15")
plate15.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
trash = PRCXI9300Trash(name="trash", size_x=50, size_y=50, size_z=10, category="trash")
trash.load_state({"Material": {"uuid": "730067cf07ae43849ddf4034299030e9"}})
# container_for_nothing = PRCXI9300Container(name="container_for_nothing", size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict())
deck.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate2, location=Coordinate(0, 0, 0))
deck.assign_child_resource(
PRCXI9300Container(
name="container_for_nothin3",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
deck.assign_child_resource(plate4, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate5, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate6, location=Coordinate(0, 0, 0))
deck.assign_child_resource(
PRCXI9300Container(
name="container_for_nothing7",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
deck.assign_child_resource(
PRCXI9300Container(
name="container_for_nothing8",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
deck.assign_child_resource(plate9, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate10, location=Coordinate(0, 0, 0))
deck.assign_child_resource(
PRCXI9300Container(
name="container_for_nothing11",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
deck.assign_child_resource(
PRCXI9300Container(
name="container_for_nothing12",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
deck.assign_child_resource(plate13, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate14, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate15, location=Coordinate(0, 0, 0))
deck.assign_child_resource(trash, location=Coordinate(0, 0, 0))
from unilabos.resources.graphio import tree_to_list, resource_plr_to_ulab
A = tree_to_list([resource_plr_to_ulab(deck)])
with open("deck.json", "w", encoding="utf-8") as f:
json.dump(A, f, indent=4, ensure_ascii=False)
handler = PRCXI9300Handler(
deck=deck,
host="192.168.0.121",
port=9999,
timeout=10.0,
setup=True,
debug=False,
matrix_id="5de524d0-3f95-406c-86dd-f83626ebc7cb",
channel_num=1,
axis="Right",
simulator=False,
is_9320=True,
)
backend: PRCXI9300Backend = handler.backend
from pylabrobot.resources import set_volume_tracking
set_volume_tracking(enabled=True)
# res = backend.api_client.get_all_materials()
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
handler.set_tiprack([plate1, plate5]) # Set the tip rack for the handler
handler.set_liquid([plate9.get_well("H12")], ["water"], [5])
asyncio.run(handler.create_protocol(protocol_name="Test Protocol"))
asyncio.run(handler.pick_up_tips([plate5.get_item("C5")], [0]))
asyncio.run(handler.aspirate([plate9.get_item("H12")], [5], [0]))
for well in plate13.get_all_items():
# well_pos = well.name.split("_")[1] # 走一行
# if well_pos.startswith("A"):
if well.name.startswith("PlateT13"): # 走整个Plate
asyncio.run(handler.dispense([well], [0.01], [0]))
# asyncio.run(handler.dispense([plate10.get_item("H12")], [1], [0]))
# asyncio.run(handler.dispense([plate13.get_item("A1")], [1], [0]))
# asyncio.run(handler.dispense([plate14.get_item("C5")], [1], [0]))
asyncio.run(handler.mix([plate10.get_item("H12")], mix_time=3, mix_vol=5))
asyncio.run(handler.discard_tips([0]))
asyncio.run(handler.run_protocol())
time.sleep(5)
os._exit(0)
# 第一种情景:一个孔往多个孔加液
# plate_2_liquids = handler.set_group("water", [plate2.children[0]], [300])
# plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
# 第二个情景:多个孔往多个孔加液(但是个数得对应)
plate_2_liquids = handler.set_group("water", plate2.children[:23], [300]*23)
plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
# A = tree_to_list([resource_plr_to_ulab(deck)])
# # with open("deck.json", "w", encoding="utf-8") as f:
# # json.dump(A, f, indent=4, ensure_ascii=False)
# print(plate11.get_well(0).tracker.get_used_volume())
# Initialize the backend and setup the connection
asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking
# asyncio.run(handler.pick_up_tips([plate8.children[8]],[0]))
# print(plate8.children[8])
# asyncio.run(handler.run_protocol())
# asyncio.run(handler.aspirate([plate11.children[0]],[10], [0]))
# print(plate11.children[0])
# # asyncio.run(handler.run_protocol())
# asyncio.run(handler.dispense([plate1.children[0]],[10],[0]))
# print(plate1.children[0])
# asyncio.run(handler.run_protocol())
# asyncio.run(handler.mix([plate1.children[0]], mix_time=3, mix_vol=5, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
# print(plate1.children[0])
# asyncio.run(handler.discard_tips([0]))
# asyncio.run(handler.add_liquid(
# asp_vols=[10]*7,
# dis_vols=[10]*7,
# reagent_sources=plate11.children[:7],
# targets=plate1.children[2:9],
# use_channels=[0],
# flow_rates=[None] * 7,
# offsets=[Coordinate(0, 0, 0)] * 7,
# liquid_height=[None] * 7,
# blow_out_air_volume=[None] * 2,
# delays=None,
# mix_time=3,
# mix_vol=5,
# spread="custom",
# ))
# asyncio.run(handler.run_protocol()) # Run the protocol
# # # asyncio.run(handler.transfer_liquid(
# # # asp_vols=[10]*2,
# # # dis_vols=[10]*2,
# # # sources=plate11.children[:2],
# # # targets=plate11.children[-2:],
# # # use_channels=[0],
# # # offsets=[Coordinate(0, 0, 0)] * 4,
# # # liquid_height=[None] * 2,
# # # blow_out_air_volume=[None] * 2,
# # # delays=None,
# # # mix_times=3,
# # # mix_vol=5,
# # # spread="wide",
# # # tip_racks=[plate8]
# # # ))
# # # asyncio.run(handler.remove_liquid(
# # # vols=[10]*2,
# # # sources=plate11.children[:2],
# # # waste_liquid=plate11.children[43],
# # # use_channels=[0],
# # # offsets=[Coordinate(0, 0, 0)] * 4,
# # # liquid_height=[None] * 2,
# # # blow_out_air_volume=[None] * 2,
# # # delays=None,
# # # spread="wide"
# # # ))
# # asyncio.run(handler.run_protocol())
# # # asyncio.run(handler.discard_tips())
# # # asyncio.run(handler.mix(well_containers.children[:8
# # # ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
# # #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
# # # asyncio.run(handler.remove_liquid(
# # # vols=[100]*16,
# # # sources=well_containers.children[-16:],
# # # waste_liquid=well_containers.children[:16], # 这个有些奇怪,但是好像也只能这么写
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# # # flow_rates=[None] * 32,
# # # offsets=[Coordinate(0, 0, 0)] * 32,
# # # liquid_height=[None] * 32,
# # # blow_out_air_volume=[None] * 32,
# # # spread="wide",
# # # ))
# # # asyncio.run(handler.transfer_liquid(
# # # asp_vols=[100]*16,
# # # dis_vols=[100]*16,
# # # tip_racks=[tip_rack],
# # # sources=well_containers.children[-16:],
# # # targets=well_containers.children[:16],
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# # # offsets=[Coordinate(0, 0, 0)] * 32,
# # # asp_flow_rates=[None] * 16,
# # # dis_flow_rates=[None] * 16,
# # # liquid_height=[None] * 32,
# # # blow_out_air_volume=[None] * 32,
# # # mix_times=3,
# # # mix_vol=50,
# # # spread="wide",
# # # ))
# # print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
# # # input("pick_up_tips add step")
#asyncio.run(handler.run_protocol()) # Run the protocol
# # # input("Running protocol...")
# # # input("Press Enter to continue...") # Wait for user input before proceeding
# # # print("PRCXI9300Handler initialized with deck and host settings.")
# 一些推荐版位组合的测试样例:
# 一些推荐版位组合的测试样例:
with open("prcxi_material.json", "r") as f:
material_info = json.load(f)
layout = DefaultLayout("PRCXI9320")
layout.add_lab_resource(material_info)
MatrixLayout_1, dict_1 = layout.recommend_layout([
("reagent_1", "96 细胞培养皿", 3),
("reagent_2", "12道储液槽", 1),
("reagent_3", "200μL Tip头", 7),
("reagent_4", "10μL加长 Tip头", 1),
])
print(dict_1)
MatrixLayout_2, dict_2 = layout.recommend_layout([
("reagent_1", "96深孔板", 4),
("reagent_2", "12道储液槽", 1),
("reagent_3", "200μL Tip头", 1),
("reagent_4", "10μL加长 Tip头", 1),
])
# with open("prcxi_material.json", "r") as f:
# material_info = json.load(f)
# layout = DefaultLayout("PRCXI9320")
# layout.add_lab_resource(material_info)
# MatrixLayout_1, dict_1 = layout.recommend_layout([
# ("reagent_1", "96 细胞培养皿", 3),
# ("reagent_2", "12道储液槽", 1),
# ("reagent_3", "200μL Tip头", 7),
# ("reagent_4", "10μL加长 Tip头", 1),
# ])
# print(dict_1)
# MatrixLayout_2, dict_2 = layout.recommend_layout([
# ("reagent_1", "96深孔板", 4),
# ("reagent_2", "12道储液槽", 1),
# ("reagent_3", "200μL Tip头", 1),
# ("reagent_4", "10μL加长 Tip头", 1),
# ])