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 typing import Optional, List, Dict, Any
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:

View File

@@ -12,7 +12,7 @@ from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC
from unilabos.utils.log import logger
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):
@@ -101,7 +101,7 @@ class BioyondWorkstation(WorkstationBase):
def __init__(
self,
bioyond_config: Optional[Dict[str, Any]] = None,
deck: Optional[str, Any] = None,
deck: Optional[Any] = None,
*args,
**kwargs,
):
@@ -240,28 +240,6 @@ class BioyondWorkstation(WorkstationBase):
"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:
"""从文件加载Bioyond数据用于测试"""
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: []
type: object
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:
category:
- bottle_carriers
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_1BottleCarrier
handles: [ ]
handles: []
icon: ''
init_param_schema: { }
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_1FlaskCarrier:
category:
- bottle_carriers
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_1FlaskCarrier
handles: [ ]
handles: []
icon: ''
init_param_schema: { }
init_param_schema: {}
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
# 命名约定:试剂瓶-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_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
# 创建6个位置坐标 (2行 x 3列)
locations = []
for row in range(2):
for col in range(3):
x = start_x + col * bottle_spacing_x
y = start_y + row * bottle_spacing_y
z = 5.0 # 架位底部
locations.append(Coordinate(x, y, z))
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
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(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=locations,
resource_size_x=bottle_diameter,
resource_size_y=bottle_diameter,
name_prefix=name,
),
sites=sites,
model="BIOYOND_Electrolyte_6VialCarrier",
)
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_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
# 创建6个位置坐标 (2行 x 3列)
locations = []
for row in range(2):
for col in range(3):
x = start_x + col * bottle_spacing_x
y = start_y + row * bottle_spacing_y
z = 5.0 # 架位底部
locations.append(Coordinate(x, y, z))
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
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(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=locations,
resource_size_x=bottle_diameter,
resource_size_y=bottle_diameter,
name_prefix=name,
),
sites=sites,
model="BIOYOND_PolymerStation_6VialCarrier",
)
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
import numpy as np
import networkx as nx
from pylabrobot.resources import ResourceHolder
from unilabos_msgs.msg import Resource
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 + \
(detail.get("x", 0) - 1) * plr_material.num_items_x + \
(detail.get("y", 0) - 1)
bottle = plr_material[number].resource
bottle = plr_material[number]
bottle.code = detail.get("code", "")
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 + \
(loc.get("x", 0) - 1) * warehouse.num_items_x + \
(loc.get("z", 0) - 1)
if 0 <= idx < warehouse.num_items_x * warehouse.num_items_y * warehouse.num_items_z:
if warehouse[idx].resource is None:
if 0 <= idx < warehouse.capacity:
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
warehouse[idx] = plr_material
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 pylabrobot.resources import Coordinate, Resource
from pylabrobot.resources.carrier import Carrier, PlateHolder, ResourceHolder, create_homogeneous_resources
from pylabrobot.resources.deck import Deck
from pylabrobot.resources import Coordinate
from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources
from unilabos.resources.itemized_carrier import ItemizedCarrier
class WareHouse(Carrier[ResourceHolder]):
class WareHouse(ItemizedCarrier):
"""4x4x1堆栈载体类 - 可容纳16个板位的载体4层x4行x1列"""
def __init__(
@@ -21,6 +21,7 @@ class WareHouse(Carrier[ResourceHolder]):
item_dy: float = 10.0,
item_dz: float = 10.0,
removed_positions: Optional[List[int]] = None,
empty: bool = False,
category: str = "warehouse",
model: Optional[str] = None,
):
@@ -44,13 +45,7 @@ class WareHouse(Carrier[ResourceHolder]):
sites = create_homogeneous_resources(
klass=ResourceHolder,
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),
],
locations=locations,
resource_size_x=127.0,
resource_size_y=86.0,
name_prefix=name,
@@ -61,12 +56,14 @@ class WareHouse(Carrier[ResourceHolder]):
size_x=dx + item_dx * num_items_x,
size_y=dy + item_dy * num_items_y,
size_z=dz + item_dz * num_items_z,
# ordered_items=ordered_items,
# ordering=ordering,
sites=sites,
category=category,
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):
raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col))