mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 11:39:43 +00:00
Compare commits
22 Commits
dev
...
1a267729e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a267729e4 | ||
|
|
b11f6eac55 | ||
|
|
d85ff540c4 | ||
|
|
5f45a0b81b | ||
|
|
6bf9a319c7 | ||
|
|
74f0d5ee65 | ||
|
|
2596d48a2f | ||
|
|
2ac1a3242a | ||
|
|
5d208c832b | ||
|
|
786498904d | ||
|
|
a9ea9f425d | ||
|
|
b3bc951cae | ||
|
|
01df4f1115 | ||
|
|
e1074f06d2 | ||
|
|
0dc273f366 | ||
|
|
2e5fac26b3 | ||
|
|
5c2da9b793 | ||
|
|
45efbfcd12 | ||
|
|
8da6fdfd0b | ||
|
|
29ea9909a5 | ||
|
|
ee6307a568 | ||
|
|
8a0116c852 |
File diff suppressed because it is too large
Load Diff
@@ -103,7 +103,7 @@ class PRCXI9300Deck(Deck):
|
|||||||
|
|
||||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||||
sites: Optional[List[Dict[str, Any]]] = None, **kwargs):
|
sites: Optional[List[Dict[str, Any]]] = None, **kwargs):
|
||||||
super().__init__(size_x, size_y, size_z, name)
|
super().__init__( size_x, size_y, size_z, name=name)
|
||||||
if sites is not None:
|
if sites is not None:
|
||||||
self.sites: List[Dict[str, Any]] = [dict(s) for s in sites]
|
self.sites: List[Dict[str, Any]] = [dict(s) for s in sites]
|
||||||
else:
|
else:
|
||||||
@@ -120,6 +120,7 @@ class PRCXI9300Deck(Deck):
|
|||||||
self._ordering = collections.OrderedDict(
|
self._ordering = collections.OrderedDict(
|
||||||
(site["label"], None) for site in self.sites
|
(site["label"], None) for site in self.sites
|
||||||
)
|
)
|
||||||
|
self.root = self.get_root()
|
||||||
|
|
||||||
def _get_site_location(self, idx: int) -> Coordinate:
|
def _get_site_location(self, idx: int) -> Coordinate:
|
||||||
pos = self.sites[idx]["position"]
|
pos = self.sites[idx]["position"]
|
||||||
@@ -162,7 +163,10 @@ class PRCXI9300Deck(Deck):
|
|||||||
raise ValueError(f"No available site on deck '{self.name}' for resource '{resource.name}'")
|
raise ValueError(f"No available site on deck '{self.name}' for resource '{resource.name}'")
|
||||||
|
|
||||||
if not reassign and self._get_site_resource(idx) is not None:
|
if not reassign and self._get_site_resource(idx) is not None:
|
||||||
raise ValueError(f"Site {idx} ('{self.sites[idx]['label']}') is already occupied")
|
existing = self.root.get_resource(resource.name)
|
||||||
|
if existing is not resource and existing.parent is not None:
|
||||||
|
existing.parent.unassign_child_resource(existing)
|
||||||
|
|
||||||
|
|
||||||
loc = self._get_site_location(idx)
|
loc = self._get_site_location(idx)
|
||||||
super().assign_child_resource(resource, location=loc, reassign=reassign)
|
super().assign_child_resource(resource, location=loc, reassign=reassign)
|
||||||
@@ -794,6 +798,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
touch_tip: bool = False,
|
touch_tip: bool = False,
|
||||||
liquid_height: Optional[List[Optional[float]]] = None,
|
liquid_height: Optional[List[Optional[float]]] = None,
|
||||||
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||||||
|
blow_out_air_volume_before: Optional[List[Optional[float]]] = None,
|
||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
is_96_well: bool = False,
|
is_96_well: bool = False,
|
||||||
mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
|
mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
|
||||||
@@ -804,7 +809,9 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
delays: Optional[List[int]] = None,
|
delays: Optional[List[int]] = None,
|
||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
) -> TransferLiquidReturn:
|
) -> TransferLiquidReturn:
|
||||||
return await super().transfer_liquid(
|
if self.step_mode:
|
||||||
|
await self.create_protocol(f"transfer_liquid{time.time()}")
|
||||||
|
res = await super().transfer_liquid(
|
||||||
sources,
|
sources,
|
||||||
targets,
|
targets,
|
||||||
tip_racks,
|
tip_racks,
|
||||||
@@ -817,6 +824,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
touch_tip=touch_tip,
|
touch_tip=touch_tip,
|
||||||
liquid_height=liquid_height,
|
liquid_height=liquid_height,
|
||||||
blow_out_air_volume=blow_out_air_volume,
|
blow_out_air_volume=blow_out_air_volume,
|
||||||
|
blow_out_air_volume_before=blow_out_air_volume_before,
|
||||||
spread=spread,
|
spread=spread,
|
||||||
is_96_well=is_96_well,
|
is_96_well=is_96_well,
|
||||||
mix_stage=mix_stage,
|
mix_stage=mix_stage,
|
||||||
@@ -827,6 +835,9 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
delays=delays,
|
delays=delays,
|
||||||
none_keys=none_keys,
|
none_keys=none_keys,
|
||||||
)
|
)
|
||||||
|
if self.step_mode:
|
||||||
|
await self.run_protocol()
|
||||||
|
return res
|
||||||
|
|
||||||
async def custom_delay(self, seconds=0, msg=None):
|
async def custom_delay(self, seconds=0, msg=None):
|
||||||
return await super().custom_delay(seconds, msg)
|
return await super().custom_delay(seconds, msg)
|
||||||
@@ -843,9 +854,10 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
offsets: Optional[Coordinate] = None,
|
offsets: Optional[Coordinate] = None,
|
||||||
mix_rate: Optional[float] = None,
|
mix_rate: Optional[float] = None,
|
||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
|
use_channels: Optional[List[int]] = [0],
|
||||||
):
|
):
|
||||||
return await self._unilabos_backend.mix(
|
return await self._unilabos_backend.mix(
|
||||||
targets, mix_time, mix_vol, height_to_bottom, offsets, mix_rate, none_keys
|
targets, mix_time, mix_vol, height_to_bottom, offsets, mix_rate, none_keys, use_channels
|
||||||
)
|
)
|
||||||
|
|
||||||
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
||||||
@@ -1274,9 +1286,15 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
offsets: Optional[Coordinate] = None,
|
offsets: Optional[Coordinate] = None,
|
||||||
mix_rate: Optional[float] = None,
|
mix_rate: Optional[float] = None,
|
||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
|
use_channels: Optional[List[int]] = [0],
|
||||||
):
|
):
|
||||||
"""Mix liquid in the specified resources."""
|
"""Mix liquid in the specified resources."""
|
||||||
|
if use_channels == [0]:
|
||||||
|
axis = "Left"
|
||||||
|
elif use_channels == [1]:
|
||||||
|
axis = "Right"
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid use channels: " + str(use_channels))
|
||||||
plate_indexes = []
|
plate_indexes = []
|
||||||
for op in targets:
|
for op in targets:
|
||||||
deck = op.parent.parent.parent
|
deck = op.parent.parent.parent
|
||||||
|
|||||||
@@ -4976,13 +4976,13 @@ liquid_handler.biomek:
|
|||||||
handler_key: tip_rack
|
handler_key: tip_rack
|
||||||
label: tip_rack
|
label: tip_rack
|
||||||
output:
|
output:
|
||||||
- data_key: liquid
|
- data_key: sources
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources_out
|
handler_key: sources_out
|
||||||
label: sources
|
label: sources
|
||||||
- data_key: liquid
|
- data_key: targets
|
||||||
data_source: executor
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_out
|
handler_key: targets_out
|
||||||
label: targets
|
label: targets
|
||||||
@@ -7656,6 +7656,43 @@ liquid_handler.prcxi:
|
|||||||
title: iter_tips参数
|
title: iter_tips参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-magnetic_action:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
height: null
|
||||||
|
is_wait: null
|
||||||
|
module_no: null
|
||||||
|
time: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
height:
|
||||||
|
type: integer
|
||||||
|
is_wait:
|
||||||
|
type: boolean
|
||||||
|
module_no:
|
||||||
|
type: integer
|
||||||
|
time:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- time
|
||||||
|
- module_no
|
||||||
|
- height
|
||||||
|
- is_wait
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: magnetic_action参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommandAsync
|
||||||
auto-move_to:
|
auto-move_to:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -7689,6 +7726,31 @@ liquid_handler.prcxi:
|
|||||||
title: move_to参数
|
title: move_to参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommandAsync
|
type: UniLabJsonCommandAsync
|
||||||
|
auto-plr_pos_to_prcxi:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
resource: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
resource:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- resource
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: plr_pos_to_prcxi参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-post_init:
|
auto-post_init:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -7809,6 +7871,47 @@ liquid_handler.prcxi:
|
|||||||
title: shaker_action参数
|
title: shaker_action参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommandAsync
|
type: UniLabJsonCommandAsync
|
||||||
|
auto-shaking_incubation_action:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
amplitude: null
|
||||||
|
is_wait: null
|
||||||
|
module_no: null
|
||||||
|
temperature: null
|
||||||
|
time: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
amplitude:
|
||||||
|
type: integer
|
||||||
|
is_wait:
|
||||||
|
type: boolean
|
||||||
|
module_no:
|
||||||
|
type: integer
|
||||||
|
temperature:
|
||||||
|
type: integer
|
||||||
|
time:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- time
|
||||||
|
- module_no
|
||||||
|
- amplitude
|
||||||
|
- is_wait
|
||||||
|
- temperature
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: shaking_incubation_action参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommandAsync
|
||||||
auto-touch_tip:
|
auto-touch_tip:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -10034,116 +10137,28 @@ liquid_handler.prcxi:
|
|||||||
type: Transfer
|
type: Transfer
|
||||||
transfer_liquid:
|
transfer_liquid:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal: {}
|
||||||
asp_flow_rates: asp_flow_rates
|
|
||||||
asp_vols: asp_vols
|
|
||||||
blow_out_air_volume: blow_out_air_volume
|
|
||||||
delays: delays
|
|
||||||
dis_flow_rates: dis_flow_rates
|
|
||||||
dis_vols: dis_vols
|
|
||||||
is_96_well: is_96_well
|
|
||||||
liquid_height: liquid_height
|
|
||||||
mix_liquid_height: mix_liquid_height
|
|
||||||
mix_rate: mix_rate
|
|
||||||
mix_stage: mix_stage
|
|
||||||
mix_times: mix_times
|
|
||||||
mix_vol: mix_vol
|
|
||||||
none_keys: none_keys
|
|
||||||
offsets: offsets
|
|
||||||
sources: sources
|
|
||||||
spread: spread
|
|
||||||
targets: targets
|
|
||||||
tip_racks: tip_racks
|
|
||||||
touch_tip: touch_tip
|
|
||||||
use_channels: use_channels
|
|
||||||
goal_default:
|
goal_default:
|
||||||
asp_flow_rates:
|
asp_flow_rates: null
|
||||||
- 0.0
|
asp_vols: null
|
||||||
asp_vols:
|
blow_out_air_volume: null
|
||||||
- 0.0
|
blow_out_air_volume_before: null
|
||||||
blow_out_air_volume:
|
delays: null
|
||||||
- 0.0
|
dis_flow_rates: null
|
||||||
delays:
|
dis_vols: null
|
||||||
- 0
|
|
||||||
dis_flow_rates:
|
|
||||||
- 0.0
|
|
||||||
dis_vols:
|
|
||||||
- 0.0
|
|
||||||
is_96_well: false
|
is_96_well: false
|
||||||
liquid_height:
|
liquid_height: null
|
||||||
- 0.0
|
mix_liquid_height: null
|
||||||
mix_liquid_height: 0.0
|
mix_rate: null
|
||||||
mix_rate: 0
|
mix_stage: none
|
||||||
mix_stage: ''
|
mix_times: null
|
||||||
mix_times: 0
|
mix_vol: null
|
||||||
mix_vol: 0
|
none_keys: []
|
||||||
none_keys:
|
offsets: null
|
||||||
- ''
|
sources: null
|
||||||
offsets:
|
spread: wide
|
||||||
- x: 0.0
|
targets: null
|
||||||
y: 0.0
|
tip_racks: null
|
||||||
z: 0.0
|
|
||||||
sources:
|
|
||||||
- category: ''
|
|
||||||
children: []
|
|
||||||
config: ''
|
|
||||||
data: ''
|
|
||||||
id: ''
|
|
||||||
name: ''
|
|
||||||
parent: ''
|
|
||||||
pose:
|
|
||||||
orientation:
|
|
||||||
w: 1.0
|
|
||||||
x: 0.0
|
|
||||||
y: 0.0
|
|
||||||
z: 0.0
|
|
||||||
position:
|
|
||||||
x: 0.0
|
|
||||||
y: 0.0
|
|
||||||
z: 0.0
|
|
||||||
sample_id: ''
|
|
||||||
type: ''
|
|
||||||
spread: ''
|
|
||||||
targets:
|
|
||||||
- category: ''
|
|
||||||
children: []
|
|
||||||
config: ''
|
|
||||||
data: ''
|
|
||||||
id: ''
|
|
||||||
name: ''
|
|
||||||
parent: ''
|
|
||||||
pose:
|
|
||||||
orientation:
|
|
||||||
w: 1.0
|
|
||||||
x: 0.0
|
|
||||||
y: 0.0
|
|
||||||
z: 0.0
|
|
||||||
position:
|
|
||||||
x: 0.0
|
|
||||||
y: 0.0
|
|
||||||
z: 0.0
|
|
||||||
sample_id: ''
|
|
||||||
type: ''
|
|
||||||
tip_racks:
|
|
||||||
- category: ''
|
|
||||||
children: []
|
|
||||||
config: ''
|
|
||||||
data: ''
|
|
||||||
id: ''
|
|
||||||
name: ''
|
|
||||||
parent: ''
|
|
||||||
pose:
|
|
||||||
orientation:
|
|
||||||
w: 1.0
|
|
||||||
x: 0.0
|
|
||||||
y: 0.0
|
|
||||||
z: 0.0
|
|
||||||
position:
|
|
||||||
x: 0.0
|
|
||||||
y: 0.0
|
|
||||||
z: 0.0
|
|
||||||
sample_id: ''
|
|
||||||
type: ''
|
|
||||||
touch_tip: false
|
touch_tip: false
|
||||||
use_channels:
|
use_channels:
|
||||||
- 0
|
- 0
|
||||||
@@ -10159,7 +10174,7 @@ liquid_handler.prcxi:
|
|||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_identifier
|
handler_key: targets_identifier
|
||||||
label: 转移目标
|
label: 转移目标
|
||||||
- data_key: tip_rack
|
- data_key: tip_racks
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: tip_rack_identifier
|
handler_key: tip_rack_identifier
|
||||||
@@ -10183,11 +10198,7 @@ liquid_handler.prcxi:
|
|||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback: {}
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: LiquidHandlerTransfer_Feedback
|
|
||||||
type: object
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
asp_flow_rates:
|
asp_flow_rates:
|
||||||
@@ -10202,6 +10213,10 @@ liquid_handler.prcxi:
|
|||||||
items:
|
items:
|
||||||
type: number
|
type: number
|
||||||
type: array
|
type: array
|
||||||
|
blow_out_air_volume_before:
|
||||||
|
items:
|
||||||
|
type: number
|
||||||
|
type: array
|
||||||
delays:
|
delays:
|
||||||
items:
|
items:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -10217,6 +10232,7 @@ liquid_handler.prcxi:
|
|||||||
type: number
|
type: number
|
||||||
type: array
|
type: array
|
||||||
is_96_well:
|
is_96_well:
|
||||||
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
liquid_height:
|
liquid_height:
|
||||||
items:
|
items:
|
||||||
@@ -10229,6 +10245,7 @@ liquid_handler.prcxi:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
mix_stage:
|
mix_stage:
|
||||||
|
default: none
|
||||||
type: string
|
type: string
|
||||||
mix_times:
|
mix_times:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -10239,6 +10256,7 @@ liquid_handler.prcxi:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
none_keys:
|
none_keys:
|
||||||
|
default: []
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
@@ -10334,6 +10352,7 @@ liquid_handler.prcxi:
|
|||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
spread:
|
spread:
|
||||||
|
default: wide
|
||||||
type: string
|
type: string
|
||||||
targets:
|
targets:
|
||||||
items:
|
items:
|
||||||
@@ -10486,6 +10505,7 @@ liquid_handler.prcxi:
|
|||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
touch_tip:
|
touch_tip:
|
||||||
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
use_channels:
|
use_channels:
|
||||||
items:
|
items:
|
||||||
@@ -10494,45 +10514,221 @@ liquid_handler.prcxi:
|
|||||||
type: integer
|
type: integer
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- asp_vols
|
|
||||||
- dis_vols
|
|
||||||
- sources
|
- sources
|
||||||
- targets
|
- targets
|
||||||
- tip_racks
|
- tip_racks
|
||||||
- use_channels
|
- asp_vols
|
||||||
- asp_flow_rates
|
- dis_vols
|
||||||
- dis_flow_rates
|
|
||||||
- offsets
|
|
||||||
- touch_tip
|
|
||||||
- liquid_height
|
|
||||||
- blow_out_air_volume
|
|
||||||
- spread
|
|
||||||
- is_96_well
|
|
||||||
- mix_stage
|
|
||||||
- mix_times
|
|
||||||
- mix_vol
|
|
||||||
- mix_rate
|
|
||||||
- mix_liquid_height
|
|
||||||
- delays
|
|
||||||
- none_keys
|
|
||||||
title: LiquidHandlerTransfer_Goal
|
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
$defs:
|
||||||
|
ResourceDict:
|
||||||
|
properties:
|
||||||
|
class:
|
||||||
|
description: Resource class name
|
||||||
|
title: Class
|
||||||
|
type: string
|
||||||
|
config:
|
||||||
|
additionalProperties: true
|
||||||
|
description: Resource configuration
|
||||||
|
title: Config
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
additionalProperties: true
|
||||||
|
description: 'Resource data, eg: container liquid data'
|
||||||
|
title: Data
|
||||||
|
type: object
|
||||||
|
description:
|
||||||
|
default: ''
|
||||||
|
description: Resource description
|
||||||
|
title: Description
|
||||||
|
type: string
|
||||||
|
extra:
|
||||||
|
additionalProperties: true
|
||||||
|
description: 'Extra data, eg: slot index'
|
||||||
|
title: Extra
|
||||||
|
type: object
|
||||||
|
icon:
|
||||||
|
default: ''
|
||||||
|
description: Resource icon
|
||||||
|
title: Icon
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
description: Resource ID
|
||||||
|
title: Id
|
||||||
|
type: string
|
||||||
|
model:
|
||||||
|
additionalProperties: true
|
||||||
|
description: Resource model
|
||||||
|
title: Model
|
||||||
|
type: object
|
||||||
|
name:
|
||||||
|
description: Resource name
|
||||||
|
title: Name
|
||||||
|
type: string
|
||||||
|
parent:
|
||||||
|
anyOf:
|
||||||
|
- $ref: '#/$defs/ResourceDict'
|
||||||
|
- type: 'null'
|
||||||
|
default: null
|
||||||
|
description: Parent resource object
|
||||||
|
parent_uuid:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
- type: 'null'
|
||||||
|
default: null
|
||||||
|
description: Parent resource uuid
|
||||||
|
title: Parent Uuid
|
||||||
|
pose:
|
||||||
|
$ref: '#/$defs/ResourceDictPosition'
|
||||||
|
description: Resource position
|
||||||
|
schema:
|
||||||
|
additionalProperties: true
|
||||||
|
description: Resource schema
|
||||||
|
title: Schema
|
||||||
|
type: object
|
||||||
|
type:
|
||||||
|
anyOf:
|
||||||
|
- const: device
|
||||||
|
type: string
|
||||||
|
- type: string
|
||||||
|
description: Resource type
|
||||||
|
title: Type
|
||||||
|
uuid:
|
||||||
|
description: Resource UUID
|
||||||
|
title: Uuid
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- uuid
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
- class
|
||||||
|
- config
|
||||||
|
- data
|
||||||
|
- extra
|
||||||
|
title: ResourceDict
|
||||||
|
type: object
|
||||||
|
ResourceDictPosition:
|
||||||
|
properties:
|
||||||
|
cross_section_type:
|
||||||
|
default: rectangle
|
||||||
|
description: Cross section type
|
||||||
|
enum:
|
||||||
|
- rectangle
|
||||||
|
- circle
|
||||||
|
- rounded_rectangle
|
||||||
|
title: Cross Section Type
|
||||||
|
type: string
|
||||||
|
layout:
|
||||||
|
default: x-y
|
||||||
|
description: Resource layout
|
||||||
|
enum:
|
||||||
|
- 2d
|
||||||
|
- x-y
|
||||||
|
- z-y
|
||||||
|
- x-z
|
||||||
|
title: Layout
|
||||||
|
type: string
|
||||||
|
position:
|
||||||
|
$ref: '#/$defs/ResourceDictPositionObject'
|
||||||
|
description: Resource position
|
||||||
|
position3d:
|
||||||
|
$ref: '#/$defs/ResourceDictPositionObject'
|
||||||
|
description: Resource position in 3D space
|
||||||
|
rotation:
|
||||||
|
$ref: '#/$defs/ResourceDictPositionObject'
|
||||||
|
description: Resource rotation
|
||||||
|
scale:
|
||||||
|
$ref: '#/$defs/ResourceDictPositionScale'
|
||||||
|
description: Resource scale
|
||||||
|
size:
|
||||||
|
$ref: '#/$defs/ResourceDictPositionSize'
|
||||||
|
description: Resource size
|
||||||
|
title: ResourceDictPosition
|
||||||
|
type: object
|
||||||
|
ResourceDictPositionObject:
|
||||||
|
properties:
|
||||||
|
x:
|
||||||
|
default: 0.0
|
||||||
|
description: X coordinate
|
||||||
|
title: X
|
||||||
|
type: number
|
||||||
|
y:
|
||||||
|
default: 0.0
|
||||||
|
description: Y coordinate
|
||||||
|
title: Y
|
||||||
|
type: number
|
||||||
|
z:
|
||||||
|
default: 0.0
|
||||||
|
description: Z coordinate
|
||||||
|
title: Z
|
||||||
|
type: number
|
||||||
|
title: ResourceDictPositionObject
|
||||||
|
type: object
|
||||||
|
ResourceDictPositionScale:
|
||||||
|
properties:
|
||||||
|
x:
|
||||||
|
default: 0.0
|
||||||
|
description: x scale
|
||||||
|
title: X
|
||||||
|
type: number
|
||||||
|
y:
|
||||||
|
default: 0.0
|
||||||
|
description: y scale
|
||||||
|
title: Y
|
||||||
|
type: number
|
||||||
|
z:
|
||||||
|
default: 0.0
|
||||||
|
description: z scale
|
||||||
|
title: Z
|
||||||
|
type: number
|
||||||
|
title: ResourceDictPositionScale
|
||||||
|
type: object
|
||||||
|
ResourceDictPositionSize:
|
||||||
|
properties:
|
||||||
|
depth:
|
||||||
|
default: 0.0
|
||||||
|
description: Depth
|
||||||
|
title: Depth
|
||||||
|
type: number
|
||||||
|
height:
|
||||||
|
default: 0.0
|
||||||
|
description: Height
|
||||||
|
title: Height
|
||||||
|
type: number
|
||||||
|
width:
|
||||||
|
default: 0.0
|
||||||
|
description: Width
|
||||||
|
title: Width
|
||||||
|
type: number
|
||||||
|
title: ResourceDictPositionSize
|
||||||
|
type: object
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
sources:
|
||||||
type: string
|
items:
|
||||||
success:
|
items:
|
||||||
type: boolean
|
$ref: '#/$defs/ResourceDict'
|
||||||
|
type: array
|
||||||
|
title: Sources
|
||||||
|
type: array
|
||||||
|
targets:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/$defs/ResourceDict'
|
||||||
|
type: array
|
||||||
|
title: Targets
|
||||||
|
type: array
|
||||||
required:
|
required:
|
||||||
- return_info
|
- sources
|
||||||
- success
|
- targets
|
||||||
title: LiquidHandlerTransfer_Result
|
title: TransferLiquidReturn
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: LiquidHandlerTransfer
|
title: transfer_liquid参数
|
||||||
type: object
|
type: object
|
||||||
type: LiquidHandlerTransfer
|
type: UniLabJsonCommandAsync
|
||||||
module: unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Handler
|
module: unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Handler
|
||||||
status_types:
|
status_types:
|
||||||
reset_ok: bool
|
reset_ok: bool
|
||||||
@@ -10555,6 +10751,12 @@ liquid_handler.prcxi:
|
|||||||
type: string
|
type: string
|
||||||
deck:
|
deck:
|
||||||
type: object
|
type: object
|
||||||
|
deck_y:
|
||||||
|
default: 400
|
||||||
|
type: string
|
||||||
|
deck_z:
|
||||||
|
default: 300
|
||||||
|
type: string
|
||||||
host:
|
host:
|
||||||
type: string
|
type: string
|
||||||
is_9320:
|
is_9320:
|
||||||
@@ -10565,17 +10767,44 @@ liquid_handler.prcxi:
|
|||||||
type: string
|
type: string
|
||||||
port:
|
port:
|
||||||
type: integer
|
type: integer
|
||||||
|
rail_interval:
|
||||||
|
default: 0
|
||||||
|
type: string
|
||||||
|
rail_nums:
|
||||||
|
default: 4
|
||||||
|
type: string
|
||||||
|
rail_width:
|
||||||
|
default: 27.5
|
||||||
|
type: string
|
||||||
setup:
|
setup:
|
||||||
default: true
|
default: true
|
||||||
type: string
|
type: string
|
||||||
simulator:
|
simulator:
|
||||||
default: false
|
default: false
|
||||||
type: string
|
type: string
|
||||||
|
start_rail:
|
||||||
|
default: 2
|
||||||
|
type: string
|
||||||
step_mode:
|
step_mode:
|
||||||
default: false
|
default: false
|
||||||
type: string
|
type: string
|
||||||
timeout:
|
timeout:
|
||||||
type: number
|
type: number
|
||||||
|
x_increase:
|
||||||
|
default: -0.003636
|
||||||
|
type: string
|
||||||
|
x_offset:
|
||||||
|
default: -0.8
|
||||||
|
type: string
|
||||||
|
xy_coupling:
|
||||||
|
default: -0.0045
|
||||||
|
type: string
|
||||||
|
y_increase:
|
||||||
|
default: -0.003636
|
||||||
|
type: string
|
||||||
|
y_offset:
|
||||||
|
default: -37.98
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- deck
|
- deck
|
||||||
- host
|
- host
|
||||||
|
|||||||
6884
unilabos/registry/resources/opentrons/lab.yaml
Normal file
6884
unilabos/registry/resources/opentrons/lab.yaml
Normal file
File diff suppressed because it is too large
Load Diff
3681
unilabos/resources/lab_resources.py
Normal file
3681
unilabos/resources/lab_resources.py
Normal file
File diff suppressed because it is too large
Load Diff
1
unilabos/resources/opentrons_custom_labware_defs.json
Normal file
1
unilabos/resources/opentrons_custom_labware_defs.json
Normal file
File diff suppressed because one or more lines are too long
@@ -534,10 +534,17 @@ class ResourceTreeSet(object):
|
|||||||
trees.append(tree_instance)
|
trees.append(tree_instance)
|
||||||
return cls(trees)
|
return cls(trees)
|
||||||
|
|
||||||
def to_plr_resources(self, skip_devices=True) -> List["PLRResource"]:
|
def to_plr_resources(
|
||||||
|
self, skip_devices: bool = True, requested_uuids: Optional[List[str]] = None
|
||||||
|
) -> List["PLRResource"]:
|
||||||
"""
|
"""
|
||||||
将 ResourceTreeSet 转换为 PLR 资源列表
|
将 ResourceTreeSet 转换为 PLR 资源列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skip_devices: 是否跳过 device 类型节点
|
||||||
|
requested_uuids: 若指定,则按此 UUID 顺序返回对应资源(用于批量查询时一一对应),
|
||||||
|
否则返回各树的根节点列表
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[PLRResource]: PLR 资源实例列表
|
List[PLRResource]: PLR 资源实例列表
|
||||||
"""
|
"""
|
||||||
@@ -593,6 +600,71 @@ class ResourceTreeSet(object):
|
|||||||
d["model"] = res.config.get("model", None)
|
d["model"] = res.config.get("model", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
# deserialize 会单独处理的元数据 key,不传给构造函数
|
||||||
|
_META_KEYS = {"type", "parent_name", "location", "children", "rotation", "barcode"}
|
||||||
|
# deserialize 自定义逻辑使用的 key(如 TipSpot 用 prototype_tip 构建 make_tip),需保留
|
||||||
|
_DESERIALIZE_PRESERVED_KEYS = {"prototype_tip"}
|
||||||
|
|
||||||
|
def remove_incompatible_params(plr_d: dict) -> None:
|
||||||
|
"""递归移除 PLR 类不接受的参数,避免 deserialize 报错。
|
||||||
|
- 移除构造函数不接受的参数(如 compute_height_from_volume、ordering、category)
|
||||||
|
- 对 TubeRack:将 ordering 转为 ordered_items
|
||||||
|
- 保留 deserialize 自定义逻辑需要的 key(如 prototype_tip)
|
||||||
|
"""
|
||||||
|
if "type" in plr_d:
|
||||||
|
sub_cls = find_subclass(plr_d["type"], PLRResource)
|
||||||
|
if sub_cls is not None:
|
||||||
|
spec = inspect.signature(sub_cls)
|
||||||
|
valid_params = set(spec.parameters.keys())
|
||||||
|
# TubeRack 特殊处理:先转换 ordering,再参与后续过滤
|
||||||
|
if "ordering" not in valid_params and "ordering" in plr_d:
|
||||||
|
ordering = plr_d.pop("ordering", None)
|
||||||
|
if sub_cls.__name__ == "TubeRack":
|
||||||
|
plr_d["ordered_items"] = (
|
||||||
|
_ordering_to_ordered_items(plr_d, ordering)
|
||||||
|
if ordering
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
# 移除构造函数不接受的参数(保留 META 和 deserialize 自定义逻辑需要的 key)
|
||||||
|
for key in list(plr_d.keys()):
|
||||||
|
if (
|
||||||
|
key not in _META_KEYS
|
||||||
|
and key not in _DESERIALIZE_PRESERVED_KEYS
|
||||||
|
and key not in valid_params
|
||||||
|
):
|
||||||
|
plr_d.pop(key, None)
|
||||||
|
for child in plr_d.get("children", []):
|
||||||
|
remove_incompatible_params(child)
|
||||||
|
|
||||||
|
def _ordering_to_ordered_items(plr_d: dict, ordering: dict) -> dict:
|
||||||
|
"""将 ordering 转为 ordered_items,从 children 构建 Tube 对象"""
|
||||||
|
from pylabrobot.resources import Tube, Coordinate
|
||||||
|
from pylabrobot.serializer import deserialize as plr_deserialize
|
||||||
|
|
||||||
|
children = plr_d.get("children", [])
|
||||||
|
ordered_items = {}
|
||||||
|
for idx, (ident, child_name) in enumerate(ordering.items()):
|
||||||
|
child_data = children[idx] if idx < len(children) else None
|
||||||
|
if child_data is None:
|
||||||
|
continue
|
||||||
|
loc_data = child_data.get("location")
|
||||||
|
loc = (
|
||||||
|
plr_deserialize(loc_data)
|
||||||
|
if loc_data
|
||||||
|
else Coordinate(0, 0, 0)
|
||||||
|
)
|
||||||
|
tube = Tube(
|
||||||
|
name=child_data.get("name", child_name or ident),
|
||||||
|
size_x=child_data.get("size_x", 10),
|
||||||
|
size_y=child_data.get("size_y", 10),
|
||||||
|
size_z=child_data.get("size_z", 50),
|
||||||
|
max_volume=child_data.get("max_volume", 1000),
|
||||||
|
)
|
||||||
|
tube.location = loc
|
||||||
|
ordered_items[ident] = tube
|
||||||
|
plr_d["children"] = [] # 已并入 ordered_items,避免重复反序列化
|
||||||
|
return ordered_items
|
||||||
|
|
||||||
plr_resources = []
|
plr_resources = []
|
||||||
tracker = DeviceNodeResourceTracker()
|
tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
@@ -612,9 +684,7 @@ class ResourceTreeSet(object):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
||||||
)
|
)
|
||||||
spec = inspect.signature(sub_cls)
|
remove_incompatible_params(plr_dict)
|
||||||
if "category" not in spec.parameters:
|
|
||||||
plr_dict.pop("category", None)
|
|
||||||
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
||||||
from pylabrobot.resources import Coordinate
|
from pylabrobot.resources import Coordinate
|
||||||
from pylabrobot.serializer import deserialize
|
from pylabrobot.serializer import deserialize
|
||||||
@@ -628,12 +698,47 @@ class ResourceTreeSet(object):
|
|||||||
plr_resources.append(plr_resource)
|
plr_resources.append(plr_resource)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"转换 PLR 资源失败: {e} {str(plr_dict)[:1000]}")
|
logger.error(f"转换 PLR 资源失败: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(f"堆栈: {traceback.format_exc()}")
|
logger.error(f"堆栈: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
if requested_uuids:
|
||||||
|
# 按请求的 UUID 顺序返回对应资源(从整棵树中按 uuid 提取)
|
||||||
|
# 优先使用 tracker.uuid_to_resources;若映射缺失,再递归遍历 PLR 树兜底搜索。
|
||||||
|
def _find_plr_by_uuid(roots: List["PLRResource"], uid: str) -> Optional["PLRResource"]:
|
||||||
|
stack = list(roots)
|
||||||
|
while stack:
|
||||||
|
node = stack.pop()
|
||||||
|
node_uid = getattr(node, "unilabos_uuid", None)
|
||||||
|
if node_uid == uid:
|
||||||
|
return node
|
||||||
|
children = getattr(node, "children", None) or []
|
||||||
|
stack.extend(children)
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = []
|
||||||
|
missing_uuids = []
|
||||||
|
for uid in requested_uuids:
|
||||||
|
found = tracker.uuid_to_resources.get(uid)
|
||||||
|
if found is None:
|
||||||
|
found = _find_plr_by_uuid(plr_resources, uid)
|
||||||
|
if found is not None:
|
||||||
|
# 回填缓存,后续相同 uuid 可直接命中
|
||||||
|
tracker.uuid_to_resources[uid] = found
|
||||||
|
if found is None:
|
||||||
|
missing_uuids.append(uid)
|
||||||
|
else:
|
||||||
|
result.append(found)
|
||||||
|
|
||||||
|
if missing_uuids:
|
||||||
|
raise ValueError(
|
||||||
|
f"请求的 UUID 未在资源树中找到: {missing_uuids}。"
|
||||||
|
f"可用 UUID 数量: {len(tracker.uuid_to_resources)},"
|
||||||
|
f"资源树数量: {len(self.trees)}"
|
||||||
|
)
|
||||||
|
return result
|
||||||
return plr_resources
|
return plr_resources
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
- 遍历 workflow 数组,为每个动作创建步骤节点
|
- 遍历 workflow 数组,为每个动作创建步骤节点
|
||||||
- 参数重命名: asp_vol -> asp_vols, dis_vol -> dis_vols, asp_flow_rate -> asp_flow_rates, dis_flow_rate -> dis_flow_rates
|
- 参数重命名: asp_vol -> asp_vols, dis_vol -> dis_vols, asp_flow_rate -> asp_flow_rates, dis_flow_rate -> dis_flow_rates
|
||||||
|
- 参数输入转换: liquid_height(按 wells 扩展);mix_stage/mix_times/mix_vol/mix_rate/mix_liquid_height 保持标量
|
||||||
- 参数扩展: 根据 targets 的 wells 数量,将单值扩展为数组
|
- 参数扩展: 根据 targets 的 wells 数量,将单值扩展为数组
|
||||||
例: asp_vol=100.0, targets 有 3 个 wells -> asp_vols=[100.0, 100.0, 100.0]
|
例: asp_vol=100.0, targets 有 3 个 wells -> asp_vols=[100.0, 100.0, 100.0]
|
||||||
- 连接处理: 如果 sources/targets 已通过 set_liquid_from_plate 连接,参数值改为 []
|
- 连接处理: 如果 sources/targets 已通过 set_liquid_from_plate 连接,参数值改为 []
|
||||||
@@ -119,11 +120,14 @@ DEVICE_NAME_DEFAULT = "PRCXI" # transfer_liquid, set_liquid_from_plate 等动
|
|||||||
# 节点类型
|
# 节点类型
|
||||||
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
||||||
|
|
||||||
|
CLASS_NAMES_MAPPING = {
|
||||||
|
"plate": "PRCXI_BioER_96_wellplate",
|
||||||
|
"tip_rack": "PRCXI_300ul_Tips",
|
||||||
|
}
|
||||||
# create_resource 节点默认参数
|
# create_resource 节点默认参数
|
||||||
CREATE_RESOURCE_DEFAULTS = {
|
CREATE_RESOURCE_DEFAULTS = {
|
||||||
"device_id": "/PRCXI",
|
"device_id": "/PRCXI",
|
||||||
"parent_template": "/PRCXI/PRCXI_Deck",
|
"parent_template": "/PRCXI/PRCXI_Deck",
|
||||||
"class_name": "PRCXI_BioER_96_wellplate",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 默认液体体积 (uL)
|
# 默认液体体积 (uL)
|
||||||
@@ -367,11 +371,10 @@ def build_protocol_graph(
|
|||||||
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
labware_info: reagent 信息字典,格式为 {name: {slot, well}, ...},用于 set_liquid 和 well 查找
|
labware_info: labware 信息字典,格式为 {name: {slot, well, labware, ...}, ...}
|
||||||
protocol_steps: 协议步骤列表
|
protocol_steps: 协议步骤列表
|
||||||
workstation_name: 工作站名称
|
workstation_name: 工作站名称
|
||||||
action_resource_mapping: action 到 resource_name 的映射字典,可选
|
action_resource_mapping: action 到 resource_name 的映射字典,可选
|
||||||
labware_defs: labware 定义列表,格式为 [{"name": "...", "slot": "1", "type": "lab_xxx"}, ...]
|
|
||||||
"""
|
"""
|
||||||
G = WorkflowGraph()
|
G = WorkflowGraph()
|
||||||
resource_last_writer = {} # reagent_name -> "node_id:port"
|
resource_last_writer = {} # reagent_name -> "node_id:port"
|
||||||
@@ -379,7 +382,21 @@ def build_protocol_graph(
|
|||||||
|
|
||||||
protocol_steps = refactor_data(protocol_steps, action_resource_mapping)
|
protocol_steps = refactor_data(protocol_steps, action_resource_mapping)
|
||||||
|
|
||||||
# ==================== 第一步:按 slot 创建 create_resource 节点 ====================
|
# ==================== 第一步:按 slot 去重创建 create_resource 节点 ====================
|
||||||
|
# 收集所有唯一的 slot
|
||||||
|
slots_info = {} # slot -> {labware, res_id}
|
||||||
|
for labware_id, item in labware_info.items():
|
||||||
|
slot = str(item.get("slot", ""))
|
||||||
|
labware = item.get("labware", "")
|
||||||
|
if slot and slot not in slots_info:
|
||||||
|
res_id = f"{labware}_slot_{slot}"
|
||||||
|
slots_info[slot] = {
|
||||||
|
"labware": labware,
|
||||||
|
"res_id": res_id,
|
||||||
|
"labware_id": labware_id,
|
||||||
|
"object": item.get("object", ""),
|
||||||
|
}
|
||||||
|
|
||||||
# 创建 Group 节点,包含所有 create_resource 节点
|
# 创建 Group 节点,包含所有 create_resource 节点
|
||||||
group_node_id = str(uuid.uuid4())
|
group_node_id = str(uuid.uuid4())
|
||||||
G.add_node(
|
G.add_node(
|
||||||
@@ -395,41 +412,40 @@ def build_protocol_graph(
|
|||||||
param=None,
|
param=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 直接使用 JSON 中的 labware 定义,每个 slot 一条记录,type 即 class_name
|
# 为每个唯一的 slot 创建 create_resource 节点
|
||||||
res_index = 0
|
for slot, info in slots_info.items():
|
||||||
for lw in (labware_defs or []):
|
|
||||||
slot = str(lw.get("slot", ""))
|
|
||||||
if not slot or slot in slot_to_create_resource:
|
|
||||||
continue # 跳过空 slot 或已处理的 slot
|
|
||||||
|
|
||||||
lw_name = lw.get("name", f"slot {slot}")
|
|
||||||
lw_type = lw.get("type", CREATE_RESOURCE_DEFAULTS["class_name"])
|
|
||||||
res_id = f"plate_slot_{slot}"
|
|
||||||
|
|
||||||
res_index += 1
|
|
||||||
node_id = str(uuid.uuid4())
|
node_id = str(uuid.uuid4())
|
||||||
|
res_id = info["res_id"]
|
||||||
|
res_type_name = info["labware"].lower().replace(".", "point")
|
||||||
|
object_type = info.get("object", "")
|
||||||
|
res_type_name = f"lab_{res_type_name}"
|
||||||
|
if object_type == "trash":
|
||||||
|
res_type_name = "PRCXI_trash"
|
||||||
G.add_node(
|
G.add_node(
|
||||||
node_id,
|
node_id,
|
||||||
template_name="create_resource",
|
template_name="create_resource",
|
||||||
resource_name="host_node",
|
resource_name="host_node",
|
||||||
name=lw_name,
|
name=f"{res_type_name}_slot{slot}",
|
||||||
description=f"Create {lw_name}",
|
description=f"Create plate on slot {slot}",
|
||||||
lab_node_type="Labware",
|
lab_node_type="Labware",
|
||||||
footer="create_resource-host_node",
|
footer="create_resource-host_node",
|
||||||
device_name=DEVICE_NAME_HOST,
|
device_name=DEVICE_NAME_HOST,
|
||||||
type=NODE_TYPE_DEFAULT,
|
type=NODE_TYPE_DEFAULT,
|
||||||
parent_uuid=group_node_id,
|
parent_uuid=group_node_id, # 指向 Group 节点
|
||||||
minimized=True,
|
minimized=True, # 折叠显示
|
||||||
param={
|
param={
|
||||||
"res_id": res_id,
|
"res_id": res_id,
|
||||||
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
||||||
"class_name": lw_type,
|
"class_name": res_type_name,
|
||||||
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"],
|
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot),
|
||||||
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||||
"slot_on_deck": slot,
|
"slot_on_deck": slot,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
slot_to_create_resource[slot] = node_id
|
slot_to_create_resource[slot] = node_id
|
||||||
|
if object_type == "tiprack":
|
||||||
|
resource_last_writer[info["labware_id"]] = f"{node_id}:labware"
|
||||||
|
# create_resource 之间不需要 ready 连接
|
||||||
|
|
||||||
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
|
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
|
||||||
# 创建 Group 节点,包含所有 set_liquid_from_plate 节点
|
# 创建 Group 节点,包含所有 set_liquid_from_plate 节点
|
||||||
@@ -464,6 +480,8 @@ def build_protocol_graph(
|
|||||||
# res_id 不能有空格
|
# res_id 不能有空格
|
||||||
res_id = str(labware_id).replace(" ", "_")
|
res_id = str(labware_id).replace(" ", "_")
|
||||||
well_count = len(wells)
|
well_count = len(wells)
|
||||||
|
object_type = item.get("object", "")
|
||||||
|
liquid_volume = DEFAULT_LIQUID_VOLUME if object_type == "source" else 0
|
||||||
|
|
||||||
node_id = str(uuid.uuid4())
|
node_id = str(uuid.uuid4())
|
||||||
set_liquid_index += 1
|
set_liquid_index += 1
|
||||||
@@ -484,7 +502,7 @@ def build_protocol_graph(
|
|||||||
"plate": [], # 通过连接传递
|
"plate": [], # 通过连接传递
|
||||||
"well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"]
|
"well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"]
|
||||||
"liquid_names": [res_id] * well_count,
|
"liquid_names": [res_id] * well_count,
|
||||||
"volumes": [DEFAULT_LIQUID_VOLUME] * well_count,
|
"volumes": [liquid_volume] * well_count,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -511,6 +529,7 @@ def build_protocol_graph(
|
|||||||
"reagent": "reagent",
|
"reagent": "reagent",
|
||||||
"solvent": "solvent",
|
"solvent": "solvent",
|
||||||
"compound": "compound",
|
"compound": "compound",
|
||||||
|
"tip_racks": "tip_rack_identifier",
|
||||||
}
|
}
|
||||||
|
|
||||||
OUTPUT_PORT_MAPPING = {
|
OUTPUT_PORT_MAPPING = {
|
||||||
@@ -525,8 +544,17 @@ def build_protocol_graph(
|
|||||||
"compound": "compound",
|
"compound": "compound",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 需要根据 wells 数量扩展的参数列表(复数形式)
|
# 需要根据 wells 数量扩展的参数列表:
|
||||||
EXPAND_BY_WELLS_PARAMS = ["asp_vols", "dis_vols", "asp_flow_rates", "dis_flow_rates"]
|
# - 复数参数(asp_vols 等)支持单值自动扩展
|
||||||
|
# - liquid_height 按 wells 扩展为数组
|
||||||
|
# - mix_* 参数保持标量,避免被转换为 list
|
||||||
|
EXPAND_BY_WELLS_PARAMS = [
|
||||||
|
"asp_vols",
|
||||||
|
"dis_vols",
|
||||||
|
"asp_flow_rates",
|
||||||
|
"dis_flow_rates",
|
||||||
|
"liquid_height",
|
||||||
|
]
|
||||||
|
|
||||||
# 处理协议步骤
|
# 处理协议步骤
|
||||||
for step in protocol_steps:
|
for step in protocol_steps:
|
||||||
@@ -540,6 +568,57 @@ def build_protocol_graph(
|
|||||||
if old_name in params:
|
if old_name in params:
|
||||||
params[new_name] = params.pop(old_name)
|
params[new_name] = params.pop(old_name)
|
||||||
|
|
||||||
|
# touch_tip 输入归一化:
|
||||||
|
# - 支持 bool / 0/1 / "true"/"false" / 单元素 list
|
||||||
|
# - 最终统一为 bool 标量,避免被下游误当作序列处理
|
||||||
|
if "touch_tip" in params:
|
||||||
|
touch_tip_value = params.get("touch_tip")
|
||||||
|
if isinstance(touch_tip_value, list):
|
||||||
|
if len(touch_tip_value) == 1:
|
||||||
|
touch_tip_value = touch_tip_value[0]
|
||||||
|
elif len(touch_tip_value) == 0:
|
||||||
|
touch_tip_value = False
|
||||||
|
else:
|
||||||
|
warnings.append(f"touch_tip 期望标量,但收到长度为 {len(touch_tip_value)} 的列表,使用首个值")
|
||||||
|
touch_tip_value = touch_tip_value[0]
|
||||||
|
if isinstance(touch_tip_value, str):
|
||||||
|
norm = touch_tip_value.strip().lower()
|
||||||
|
if norm in {"true", "1", "yes", "y", "on"}:
|
||||||
|
touch_tip_value = True
|
||||||
|
elif norm in {"false", "0", "no", "n", "off", ""}:
|
||||||
|
touch_tip_value = False
|
||||||
|
else:
|
||||||
|
warnings.append(f"touch_tip 字符串值无法识别: {touch_tip_value},按 True 处理")
|
||||||
|
touch_tip_value = True
|
||||||
|
elif isinstance(touch_tip_value, (int, float)):
|
||||||
|
touch_tip_value = bool(touch_tip_value)
|
||||||
|
elif touch_tip_value is None:
|
||||||
|
touch_tip_value = False
|
||||||
|
else:
|
||||||
|
touch_tip_value = bool(touch_tip_value)
|
||||||
|
params["touch_tip"] = touch_tip_value
|
||||||
|
|
||||||
|
# delays 输入归一化:
|
||||||
|
# - 支持标量(int/float/字符串数字)与 list
|
||||||
|
# - 最终统一为数字列表,供下游按 delays[0]/delays[1] 使用
|
||||||
|
if "delays" in params:
|
||||||
|
delays_value = params.get("delays")
|
||||||
|
if delays_value is None or delays_value == "":
|
||||||
|
params["delays"] = []
|
||||||
|
else:
|
||||||
|
raw_list = delays_value if isinstance(delays_value, list) else [delays_value]
|
||||||
|
normalized_delays = []
|
||||||
|
for delay_item in raw_list:
|
||||||
|
if isinstance(delay_item, str):
|
||||||
|
delay_item = delay_item.strip()
|
||||||
|
if delay_item == "":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
normalized_delays.append(float(delay_item))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
warnings.append(f"delays 包含无法转换为数字的值: {delay_item},已忽略")
|
||||||
|
params["delays"] = normalized_delays
|
||||||
|
|
||||||
# 处理输入连接
|
# 处理输入连接
|
||||||
for param_key, target_port in INPUT_PORT_MAPPING.items():
|
for param_key, target_port in INPUT_PORT_MAPPING.items():
|
||||||
resource_name = params.get(param_key)
|
resource_name = params.get(param_key)
|
||||||
|
|||||||
Reference in New Issue
Block a user