Compare commits

..

3 Commits

12 changed files with 560 additions and 648 deletions

View File

@@ -0,0 +1,67 @@
{
"nodes": [
{
"id": "reaction_station_bioyond",
"name": "reaction_station_bioyond",
"children": [
],
"parent": null,
"type": "device",
"class": "workstation.bioyond",
"config": {
"bioyond_config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44388",
"workflow_mappings": {
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
}
},
"deck": {
"_resource_child_name": "Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
},
"protocol_type": []
},
"data": {},
"children": [
"Bioyond_Deck"
]
},
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"sample_id": null,
"children": [
],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "OTDeck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "OTDeck",
"size_x": 624.3,
"size_y": 565.2,
"size_z": 900,
"with_trash": false,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
}
]
}

View File

@@ -8,7 +8,7 @@ from datetime import datetime, timezone
from unilabos.device_comms.rpc import BaseRequest from unilabos.device_comms.rpc import BaseRequest
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
import json import json
from config import WORKFLOW_TO_SECTION_MAP, WORKFLOW_STEP_IDS, LOCATION_MAPPING from unilabos.devices.workstation.bioyond_studio.config import WORKFLOW_TO_SECTION_MAP, WORKFLOW_STEP_IDS, LOCATION_MAPPING
class SimpleLogger: class SimpleLogger:

View File

@@ -12,7 +12,7 @@ from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC
from unilabos.utils.log import logger from unilabos.utils.log import logger
from unilabos.resources.graphio import resource_bioyond_to_plr from unilabos.resources.graphio import resource_bioyond_to_plr
from .config import API_CONFIG, WORKFLOW_MAPPINGS from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS
class BioyondResourceSynchronizer(ResourceSynchronizer): class BioyondResourceSynchronizer(ResourceSynchronizer):
@@ -101,7 +101,7 @@ class BioyondWorkstation(WorkstationBase):
def __init__( def __init__(
self, self,
bioyond_config: Optional[Dict[str, Any]] = None, bioyond_config: Optional[Dict[str, Any]] = None,
deck: Optional[str, Any] = None, deck: Optional[Any] = None,
*args, *args,
**kwargs, **kwargs,
): ):
@@ -240,28 +240,6 @@ class BioyondWorkstation(WorkstationBase):
"message": str(e) "message": str(e)
} }
def get_bioyond_status(self) -> Dict[str, Any]:
"""获取Bioyond系统状态"""
try:
material_manager = self.material_management
return {
"bioyond_connected": material_manager.bioyond_api_client is not None,
"sync_interval": material_manager.sync_interval,
"total_resources": len(material_manager.plr_resources),
"deck_size": {
"x": material_manager.plr_deck.size_x,
"y": material_manager.plr_deck.size_y,
"z": material_manager.plr_deck.size_z
},
"bioyond_config": self.bioyond_config
}
except Exception as e:
logger.error(f"获取Bioyond状态失败: {e}")
return {
"error": str(e)
}
def load_bioyond_data_from_file(self, file_path: str) -> bool: def load_bioyond_data_from_file(self, file_path: str) -> bool:
"""从文件加载Bioyond数据用于测试""" """从文件加载Bioyond数据用于测试"""
try: try:

View File

@@ -1,481 +0,0 @@
reaction_station_bioyong:
category:
- reaction_station_bioyong
class:
action_value_mappings:
drip_back:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationDripBack_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
torque_variation:
type: string
volume:
type: string
required:
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationDripBack_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationDripBack_Result
type: object
required:
- goal
title: ReactionStationDripBack
type: object
type: ReactionStationDripBack
liquid_feeding_beaker:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationLiquidFeed_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
titration_type:
type: string
torque_variation:
type: string
volume:
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationLiquidFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationLiquidFeed_Result
type: object
required:
- goal
title: ReactionStationLiquidFeed
type: object
type: ReactionStationLiquidFeed
liquid_feeding_solvents:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationLiquidFeed_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
titration_type:
type: string
torque_variation:
type: string
volume:
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationLiquidFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationLiquidFeed_Result
type: object
required:
- goal
title: ReactionStationLiquidFeed
type: object
type: ReactionStationLiquidFeed
liquid_feeding_titration:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationLiquidFeed_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
titration_type:
type: string
torque_variation:
type: string
volume:
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationLiquidFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationLiquidFeed_Result
type: object
required:
- goal
title: ReactionStationLiquidFeed
type: object
type: ReactionStationLiquidFeed
liquid_feeding_vials_non_titration:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationLiquidFeed_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
titration_type:
type: string
torque_variation:
type: string
volume:
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationLiquidFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationLiquidFeed_Result
type: object
required:
- goal
title: ReactionStationLiquidFeed
type: object
type: ReactionStationLiquidFeed
process_and_execute_workflow:
feedback: {}
goal:
task_name: task_name
workflow_name: workflow_name
goal_default:
task_name: ''
workflow_name: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationProExecu_Feedback
type: object
goal:
properties:
task_name:
type: string
workflow_name:
type: string
required:
- workflow_name
- task_name
title: ReactionStationProExecu_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationProExecu_Result
type: object
required:
- goal
title: ReactionStationProExecu
type: object
type: ReactionStationProExecu
reactor_taken_in:
feedback: {}
goal:
assign_material_name: assign_material_name
cutoff: cutoff
temperature: temperature
goal_default:
assign_material_name: ''
cutoff: ''
temperature: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationReaTackIn_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
cutoff:
type: string
temperature:
type: string
required:
- cutoff
- temperature
- assign_material_name
title: ReactionStationReaTackIn_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationReaTackIn_Result
type: object
required:
- goal
title: ReactionStationReaTackIn
type: object
type: ReactionStationReaTackIn
reactor_taken_out:
feedback: {}
goal: {}
goal_default:
command: ''
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: SendCmd_Feedback
type: object
goal:
properties:
command:
type: string
required:
- command
title: SendCmd_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SendCmd_Result
type: object
required:
- goal
title: SendCmd
type: object
type: SendCmd
solid_feeding_vials:
feedback: {}
goal:
assign_material_name: assign_material_name
material_id: material_id
time: time
torque_variation: torque_variation
goal_default:
assign_material_name: ''
material_id: ''
time: ''
torque_variation: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationSolidFeedVial_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
material_id:
type: string
time:
type: string
torque_variation:
type: string
required:
- assign_material_name
- material_id
- time
- torque_variation
title: ReactionStationSolidFeedVial_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationSolidFeedVial_Result
type: object
required:
- goal
title: ReactionStationSolidFeedVial
type: object
type: ReactionStationSolidFeedVial
module: unilabos.devices.reaction_station.reaction_station_bioyong:BioyongV1RPC
status_types: {}
type: python
config_info: []
description: reaction_station_bioyong Device
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0

View File

@@ -6044,3 +6044,101 @@ workstation:
required: [] required: []
type: object type: object
version: 1.0.0 version: 1.0.0
workstation.bioyond:
category:
- work_station
class:
action_value_mappings:
auto-execute_bioyond_sync_workflow:
feedback: {}
goal: {}
goal_default:
parameters: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
parameters:
type: object
required:
- parameters
type: object
result: {}
required:
- goal
title: execute_bioyond_sync_workflow参数
type: object
type: UniLabJsonCommandAsync
auto-execute_bioyond_update_workflow:
feedback: {}
goal: {}
goal_default:
parameters: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
parameters:
type: object
required:
- parameters
type: object
result: {}
required:
- goal
title: execute_bioyond_update_workflow参数
type: object
type: UniLabJsonCommandAsync
auto-load_bioyond_data_from_file:
feedback: {}
goal: {}
goal_default:
file_path: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
type: string
required:
- file_path
type: object
result: {}
required:
- goal
title: load_bioyond_data_from_file参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
status_types: {}
type: python
config_info: []
description: ''
handles: []
icon: 反应站.webp
init_param_schema:
config:
properties:
bioyond_config:
type: string
deck:
type: string
required: []
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -1,38 +1,36 @@
BIOYOND_PolymerStation_6VialCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6VialCarrier
handles: [ ]
icon: ''
init_param_schema: { }
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_1BottleCarrier: BIOYOND_PolymerStation_1BottleCarrier:
category: category:
- bottle_carriers - bottle_carriers
class: class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier
type: pylabrobot type: pylabrobot
description: BIOYOND_PolymerStation_1BottleCarrier description: BIOYOND_PolymerStation_1BottleCarrier
handles: [ ] handles: []
icon: '' icon: ''
init_param_schema: { } init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerStation_1FlaskCarrier: BIOYOND_PolymerStation_1FlaskCarrier:
category: category:
- bottle_carriers - bottle_carriers
class: class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier
type: pylabrobot type: pylabrobot
description: BIOYOND_PolymerStation_1FlaskCarrier description: BIOYOND_PolymerStation_1FlaskCarrier
handles: [ ] handles: []
icon: '' icon: ''
init_param_schema: { } init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerStation_6VialCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6VialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,6 +1,6 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.bottle_carrier import Bottle, BottleCarrier from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial # 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
@@ -22,27 +22,29 @@ def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
# 创建6个位置坐标 (2行 x 3列) sites = create_ordered_items_2d(
locations = [] klass=ResourceHolder,
for row in range(2): num_items_x=3,
for col in range(3): num_items_y=2,
x = start_x + col * bottle_spacing_x dx=start_x,
y = start_y + row * bottle_spacing_y dy=start_y,
z = 5.0 # 架位底部 dz=5.0,
locations.append(Coordinate(x, y, z)) item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier( carrier = BottleCarrier(
name=name, name=name,
size_x=carrier_size_x, size_x=carrier_size_x,
size_y=carrier_size_y, size_y=carrier_size_y,
size_z=carrier_size_z, size_z=carrier_size_z,
sites=create_homogeneous_resources( sites=sites,
klass=ResourceHolder,
locations=locations,
resource_size_x=bottle_diameter,
resource_size_y=bottle_diameter,
name_prefix=name,
),
model="BIOYOND_Electrolyte_6VialCarrier", model="BIOYOND_Electrolyte_6VialCarrier",
) )
carrier.num_items_x = 3 carrier.num_items_x = 3
@@ -107,27 +109,29 @@ def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier:
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
# 创建6个位置坐标 (2行 x 3列) sites = create_ordered_items_2d(
locations = [] klass=ResourceHolder,
for row in range(2): num_items_x=3,
for col in range(3): num_items_y=2,
x = start_x + col * bottle_spacing_x dx=start_x,
y = start_y + row * bottle_spacing_y dy=start_y,
z = 5.0 # 架位底部 dz=5.0,
locations.append(Coordinate(x, y, z)) item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier( carrier = BottleCarrier(
name=name, name=name,
size_x=carrier_size_x, size_x=carrier_size_x,
size_y=carrier_size_y, size_y=carrier_size_y,
size_z=carrier_size_z, size_z=carrier_size_z,
sites=create_homogeneous_resources( sites=sites,
klass=ResourceHolder,
locations=locations,
resource_size_x=bottle_diameter,
resource_size_y=bottle_diameter,
name_prefix=name,
),
model="BIOYOND_PolymerStation_6VialCarrier", model="BIOYOND_PolymerStation_6VialCarrier",
) )
carrier.num_items_x = 3 carrier.num_items_x = 3

View File

@@ -1,4 +1,4 @@
from unilabos.resources.bottle_carrier import Bottle, BottleCarrier from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数 # 工厂函数

View File

@@ -1,72 +0,0 @@
"""
自动化液体处理工作站物料类定义 - 简化版
Automated Liquid Handling Station Resource Classes - Simplified Version
"""
from __future__ import annotations
from typing import Dict, Optional
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.container import Container
from pylabrobot.resources.carrier import TubeCarrier
from pylabrobot.resources.resource_holder import ResourceHolder
class Bottle(Container):
"""瓶子类 - 简化版,不追踪瓶盖"""
def __init__(
self,
name: str,
diameter: float,
height: float,
max_volume: float,
barcode: Optional[str] = "",
category: str = "container",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=diameter,
size_y=diameter,
size_z=height,
max_volume=max_volume,
category=category,
model=model,
)
self.diameter = diameter
self.height = height
self.barcode = barcode
def serialize(self) -> dict:
return {
**super().serialize(),
"diameter": self.diameter,
"height": self.height,
"barcode": self.barcode,
}
class BottleCarrier(TubeCarrier):
"""瓶载架 - 直接继承自 TubeCarrier"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
sites: Optional[Dict[int, ResourceHolder]] = None,
category: str = "bottle_carrier",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
sites=sites,
category=category,
model=model,
)

View File

@@ -4,6 +4,7 @@ import json
from typing import Union, Any, Dict from typing import Union, Any, Dict
import numpy as np import numpy as np
import networkx as nx import networkx as nx
from pylabrobot.resources import ResourceHolder
from unilabos_msgs.msg import Resource from unilabos_msgs.msg import Resource
from unilabos.resources.container import RegularContainer from unilabos.resources.container import RegularContainer
@@ -507,7 +508,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
number = (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y + \ number = (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y + \
(detail.get("x", 0) - 1) * plr_material.num_items_x + \ (detail.get("x", 0) - 1) * plr_material.num_items_x + \
(detail.get("y", 0) - 1) (detail.get("y", 0) - 1)
bottle = plr_material[number].resource bottle = plr_material[number]
bottle.code = detail.get("code", "") bottle.code = detail.get("code", "")
bottle.tracker.liquids = [(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)] bottle.tracker.liquids = [(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)]
@@ -520,8 +521,8 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
idx = (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y + \ idx = (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y + \
(loc.get("x", 0) - 1) * warehouse.num_items_x + \ (loc.get("x", 0) - 1) * warehouse.num_items_x + \
(loc.get("z", 0) - 1) (loc.get("z", 0) - 1)
if 0 <= idx < warehouse.num_items_x * warehouse.num_items_y * warehouse.num_items_z: if 0 <= idx < warehouse.capacity:
if warehouse[idx].resource is None: if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
warehouse[idx] = plr_material warehouse[idx] = plr_material
return plr_materials return plr_materials

View File

@@ -0,0 +1,322 @@
"""
自动化液体处理工作站物料类定义 - 简化版
Automated Liquid Handling Station Resource Classes - Simplified Version
"""
from __future__ import annotations
from typing import Dict, Optional
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.container import Container
from pylabrobot.resources.resource_holder import ResourceHolder
from pylabrobot.resources import Resource as ResourcePLR
class Bottle(Container):
"""瓶子类 - 简化版,不追踪瓶盖"""
def __init__(
self,
name: str,
diameter: float,
height: float,
max_volume: float,
size_x: float = 0.0,
size_y: float = 0.0,
size_z: float = 0.0,
barcode: Optional[str] = "",
category: str = "container",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=diameter,
size_y=diameter,
size_z=height,
max_volume=max_volume,
category=category,
model=model,
)
self.diameter = diameter
self.height = height
self.barcode = barcode
def serialize(self) -> dict:
return {
**super().serialize(),
"diameter": self.diameter,
"height": self.height,
"barcode": self.barcode,
}
from string import ascii_uppercase as LETTERS
from typing import Dict, List, Optional, Type, TypeVar, Union, Sequence, Tuple
import pylabrobot
from pylabrobot.resources.resource_holder import ResourceHolder
T = TypeVar("T", bound=ResourceHolder)
S = TypeVar("S", bound=ResourceHolder)
class ItemizedCarrier(ResourcePLR):
"""Base class for all carriers."""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
category: Optional[str] = "carrier",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
model=model,
)
sites = sites or {}
self.sites: List[Optional[ResourcePLR]] = list(sites.values())
self._ordering = sites
self.num_items = len(self.sites)
self.child_locations: Dict[str, Coordinate] = {}
for spot, resource in sites.items():
if resource is not None and getattr(resource, "location", None) is None:
raise ValueError(f"resource {resource} has no location")
if resource is not None:
self.child_locations[spot] = resource.location
else:
self.child_locations[spot] = Coordinate.zero()
@property
def capacity(self):
"""The number of sites on this carrier."""
return len(self.sites)
def __len__(self) -> int:
"""Return the number of sites on this carrier."""
return len(self.sites)
def assign_child_resource(
self,
resource: ResourcePLR,
location: Optional[Coordinate],
reassign: bool = True,
spot: Optional[int] = None,
):
idx = spot if spot is not None else len(self.sites)
if not reassign and self.sites[idx] is not None:
raise ValueError(f"a site with index {idx} already exists")
super().assign_child_resource(resource, location=location, reassign=reassign)
self.sites[idx] = resource
def assign_resource_to_site(self, resource: ResourcePLR, spot: int):
if self.sites[spot] is not None and not isinstance(self.sites[spot], ResourceHolder):
raise ValueError(f"spot {spot} already has a resource, {resource}")
self.assign_child_resource(resource, location=self.child_locations.get(str(spot)), spot=spot)
def unassign_child_resource(self, resource: ResourcePLR):
found = False
for spot, res in enumerate(self.sites):
if res == resource:
self.sites[spot] = None
found = True
break
if not found:
raise ValueError(f"Resource {resource} is not assigned to this carrier")
if hasattr(resource, "unassign"):
resource.unassign()
def __getitem__(
self,
identifier: Union[str, int, Sequence[int], Sequence[str], slice, range],
) -> Union[List[T], T]:
"""Get the items with the given identifier.
This is a convenience method for getting the items with the given identifier. It is equivalent
to :meth:`get_items`, but adds support for slicing and supports single items in the same
functional call. Note that the return type will always be a list, even if a single item is
requested.
Examples:
Getting the items with identifiers "A1" through "E1":
>>> items["A1:E1"]
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
Getting the items with identifiers 0 through 4 (note that this is the same as above):
>>> items[range(5)]
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
Getting items with a slice (note that this is the same as above):
>>> items[0:5]
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
Getting a single item:
>>> items[0]
[<Item A1>]
"""
if isinstance(identifier, str):
if ":" in identifier: # multiple # TODO: deprecate this, use `"A1":"E1"` instead (slice)
return self.get_items(identifier)
return self.get_item(identifier) # single
if isinstance(identifier, int):
return self.get_item(identifier)
if isinstance(identifier, (slice, range)):
start, stop = identifier.start, identifier.stop
if isinstance(identifier.start, str):
start = list(self._ordering.keys()).index(identifier.start)
elif identifier.start is None:
start = 0
if isinstance(identifier.stop, str):
stop = list(self._ordering.keys()).index(identifier.stop)
elif identifier.stop is None:
stop = self.num_items
identifier = list(range(start, stop, identifier.step or 1))
return self.get_items(identifier)
if isinstance(identifier, (list, tuple)):
return self.get_items(identifier)
raise TypeError(f"Invalid identifier type: {type(identifier)}")
def get_item(self, identifier: Union[str, int, Tuple[int, int]]) -> T:
"""Get the item with the given identifier.
Args:
identifier: The identifier of the item. Either a string, an integer, or a tuple. If an
integer, it is the index of the item in the list of items (counted from 0, top to bottom, left
to right). If a string, it uses transposed MS Excel style notation, e.g. "A1" for the first
item, "B1" for the item below that, etc. If a tuple, it is (row, column).
Raises:
IndexError: If the identifier is out of range. The range is 0 to self.num_items-1 (inclusive).
"""
if isinstance(identifier, tuple):
row, column = identifier
identifier = LETTERS[row] + str(column + 1) # standard transposed-Excel style notation
if isinstance(identifier, str):
try:
identifier = list(self._ordering.keys()).index(identifier)
except ValueError as e:
raise IndexError(
f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'."
) from e
if not 0 <= identifier < self.capacity:
raise IndexError(
f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'."
)
# Cast child to item type. Children will always be `T`, but the type checker doesn't know that.
return self.sites[identifier]
def get_items(self, identifiers: Union[str, Sequence[int], Sequence[str]]) -> List[T]:
"""Get the items with the given identifier.
Args:
identifier: Deprecated. Use `identifiers` instead. # TODO(deprecate-ordered-items)
identifiers: The identifiers of the items. Either a string range or a list of integers. If a
string, it uses transposed MS Excel style notation. Regions of items can be specified using
a colon, e.g. "A1:H1" for the first column. If a list of integers, it is the indices of the
items in the list of items (counted from 0, top to bottom, left to right).
Examples:
Getting the items with identifiers "A1" through "E1":
>>> items.get_items("A1:E1")
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
Getting the items with identifiers 0 through 4:
>>> items.get_items(range(5))
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
"""
if isinstance(identifiers, str):
identifiers = pylabrobot.utils.expand_string_range(identifiers)
return [self.get_item(i) for i in identifiers]
def __setitem__(self, idx: Union[int, str], resource: Optional[ResourcePLR]):
"""Assign a resource to this carrier."""
if resource is None: # setting to None
assigned_resource = self[idx]
if assigned_resource is not None:
self.unassign_child_resource(assigned_resource)
else:
idx = list(self._ordering.keys()).index(idx) if isinstance(idx, str) else idx
self.assign_resource_to_site(resource, spot=idx)
def __delitem__(self, idx: int):
"""Unassign a resource from this carrier."""
assigned_resource = self[idx]
if assigned_resource is not None:
self.unassign_child_resource(assigned_resource)
def get_resources(self) -> List[ResourcePLR]:
"""Get all resources assigned to this carrier."""
return [resource for resource in self.sites.values() if resource is not None]
def __eq__(self, other):
return super().__eq__(other) and self.sites == other.sites
def get_free_sites(self) -> List[int]:
return [spot for spot, resource in self.sites.items() if resource is None]
def serialize(self):
return {
**super().serialize(),
"slots": [{
"label": str(identifier),
"visible": True if self[identifier] is not None else False,
"position": {"x": location.x, "y": location.y, "z": location.z},
"size": {"width": self._size_x, "height": self._size_y, "depth": self._size_z},
"content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"]
} for identifier, location in self.child_locations.items()]
}
class BottleCarrier(ItemizedCarrier):
"""瓶载架 - 直接继承自 TubeCarrier"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
category: str = "bottle_carrier",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
sites=sites,
category=category,
model=model,
)

View File

@@ -1,11 +1,11 @@
import json
from typing import Optional, List from typing import Optional, List
from pylabrobot.resources import Coordinate, Resource from pylabrobot.resources import Coordinate
from pylabrobot.resources.carrier import Carrier, PlateHolder, ResourceHolder, create_homogeneous_resources from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources
from pylabrobot.resources.deck import Deck
from unilabos.resources.itemized_carrier import ItemizedCarrier
class WareHouse(Carrier[ResourceHolder]): class WareHouse(ItemizedCarrier):
"""4x4x1堆栈载体类 - 可容纳16个板位的载体4层x4行x1列""" """4x4x1堆栈载体类 - 可容纳16个板位的载体4层x4行x1列"""
def __init__( def __init__(
@@ -21,6 +21,7 @@ class WareHouse(Carrier[ResourceHolder]):
item_dy: float = 10.0, item_dy: float = 10.0,
item_dz: float = 10.0, item_dz: float = 10.0,
removed_positions: Optional[List[int]] = None, removed_positions: Optional[List[int]] = None,
empty: bool = False,
category: str = "warehouse", category: str = "warehouse",
model: Optional[str] = None, model: Optional[str] = None,
): ):
@@ -44,13 +45,7 @@ class WareHouse(Carrier[ResourceHolder]):
sites = create_homogeneous_resources( sites = create_homogeneous_resources(
klass=ResourceHolder, klass=ResourceHolder,
locations=[ locations=locations,
Coordinate(4.0, 8.5, 86.15),
Coordinate(4.0, 104.5, 86.15),
Coordinate(4.0, 200.5, 86.15),
Coordinate(4.0, 296.5, 86.15),
Coordinate(4.0, 392.5, 86.15),
],
resource_size_x=127.0, resource_size_x=127.0,
resource_size_y=86.0, resource_size_y=86.0,
name_prefix=name, name_prefix=name,
@@ -61,12 +56,14 @@ class WareHouse(Carrier[ResourceHolder]):
size_x=dx + item_dx * num_items_x, size_x=dx + item_dx * num_items_x,
size_y=dy + item_dy * num_items_y, size_y=dy + item_dy * num_items_y,
size_z=dz + item_dz * num_items_z, size_z=dz + item_dz * num_items_z,
# ordered_items=ordered_items,
# ordering=ordering,
sites=sites, sites=sites,
category=category, category=category,
model=model, model=model,
) )
def get_site_by_layer_position(self, row: int, col: int, layer: int) -> PlateHolder: def get_site_by_layer_position(self, row: int, col: int, layer: int) -> ResourceHolder:
if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1): if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1):
raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col)) raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col))