mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 09:49:16 +00:00
Compare commits
2 Commits
d85ff540c4
...
1a267729e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a267729e4 | ||
|
|
b11f6eac55 |
@@ -10,6 +10,7 @@ from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, Liqu
|
|||||||
from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod
|
from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod
|
||||||
from pylabrobot.liquid_handling.standard import GripDirection
|
from pylabrobot.liquid_handling.standard import GripDirection
|
||||||
from pylabrobot.resources.errors import TooLittleLiquidError, TooLittleVolumeError
|
from pylabrobot.resources.errors import TooLittleLiquidError, TooLittleVolumeError
|
||||||
|
from pylabrobot.resources.volume_tracker import no_volume_tracking
|
||||||
from pylabrobot.resources import (
|
from pylabrobot.resources import (
|
||||||
Resource,
|
Resource,
|
||||||
TipRack,
|
TipRack,
|
||||||
@@ -207,54 +208,66 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
offsets: Optional[List[Coordinate]] = None,
|
offsets: Optional[List[Coordinate]] = None,
|
||||||
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,
|
||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "custom",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
):
|
):
|
||||||
if spread == "":
|
if spread == "":
|
||||||
spread = "wide"
|
spread = "custom"
|
||||||
|
|
||||||
def _safe_aspirate_volumes(_resources: Sequence[Container], _vols: List[float]) -> List[float]:
|
for res in resources:
|
||||||
"""将 aspirate 体积裁剪到源容器当前液量范围内,避免 volume tracker 报错。"""
|
tracker = getattr(res, "tracker", None)
|
||||||
safe: List[float] = []
|
if tracker is None or getattr(tracker, "is_disabled", False):
|
||||||
for res, vol in zip(_resources, _vols):
|
continue
|
||||||
req = max(float(vol), 0.0)
|
history = getattr(tracker, "liquid_history", None)
|
||||||
used_volume = None
|
if tracker.get_used_volume() <= 0 and isinstance(history, list) and len(history) == 0:
|
||||||
|
fill_vol = tracker.max_volume if tracker.max_volume > 0 else 50000
|
||||||
try:
|
try:
|
||||||
tracker = getattr(res, "tracker", None)
|
tracker.add_liquid(fill_vol)
|
||||||
if bool(getattr(tracker, "is_disabled", False)):
|
|
||||||
# tracker 关闭时(例如预吸空气),不按液体体积裁剪
|
|
||||||
safe.append(req)
|
|
||||||
continue
|
|
||||||
get_used = getattr(tracker, "get_used_volume", None)
|
|
||||||
if callable(get_used):
|
|
||||||
used_volume = get_used()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
used_volume = None
|
tracker.liquid_history.append(("auto_init", fill_vol))
|
||||||
|
|
||||||
if isinstance(used_volume, (int, float)):
|
|
||||||
req = min(req, max(float(used_volume), 0.0))
|
|
||||||
safe.append(req)
|
|
||||||
return safe
|
|
||||||
|
|
||||||
actual_vols = _safe_aspirate_volumes(resources, vols)
|
|
||||||
if actual_vols != vols and hasattr(self, "_ros_node") and self._ros_node is not None:
|
|
||||||
self._ros_node.lab_logger().warning(f"[aspirate] volume adjusted, requested_vols={vols}, actual_vols={actual_vols}")
|
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.aspirate(
|
try:
|
||||||
resources,
|
return await self._simulate_handler.aspirate(
|
||||||
actual_vols,
|
resources,
|
||||||
use_channels,
|
vols,
|
||||||
flow_rates,
|
use_channels,
|
||||||
offsets,
|
flow_rates,
|
||||||
liquid_height,
|
offsets,
|
||||||
blow_out_air_volume,
|
liquid_height,
|
||||||
spread,
|
blow_out_air_volume,
|
||||||
**backend_kwargs,
|
spread,
|
||||||
)
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
except (TooLittleLiquidError, TooLittleVolumeError) as e:
|
||||||
|
tracker_info = []
|
||||||
|
for r in resources:
|
||||||
|
t = r.tracker
|
||||||
|
tracker_info.append(
|
||||||
|
f"{r.name}(used={t.get_used_volume():.1f}, "
|
||||||
|
f"free={t.get_free_volume():.1f}, max={r.max_volume})"
|
||||||
|
)
|
||||||
|
if hasattr(self, "_ros_node") and self._ros_node is not None:
|
||||||
|
self._ros_node.lab_logger().warning(
|
||||||
|
f"[aspirate] volume tracker error, bypassing tracking. "
|
||||||
|
f"error={e}, vols={vols}, trackers={tracker_info}"
|
||||||
|
)
|
||||||
|
with no_volume_tracking():
|
||||||
|
return await self._simulate_handler.aspirate(
|
||||||
|
resources,
|
||||||
|
vols,
|
||||||
|
use_channels,
|
||||||
|
flow_rates,
|
||||||
|
offsets,
|
||||||
|
liquid_height,
|
||||||
|
blow_out_air_volume,
|
||||||
|
spread,
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await super().aspirate(
|
await super().aspirate(
|
||||||
resources,
|
resources,
|
||||||
actual_vols,
|
vols,
|
||||||
use_channels,
|
use_channels,
|
||||||
flow_rates,
|
flow_rates,
|
||||||
offsets,
|
offsets,
|
||||||
@@ -267,7 +280,7 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
||||||
await super().aspirate(
|
await super().aspirate(
|
||||||
resources,
|
resources,
|
||||||
actual_vols,
|
vols,
|
||||||
use_channels,
|
use_channels,
|
||||||
flow_rates,
|
flow_rates,
|
||||||
offsets,
|
offsets,
|
||||||
@@ -278,35 +291,15 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
except TooLittleLiquidError:
|
|
||||||
# 再兜底一次:按实时可用液量重算后重试,避免状态更新竞争导致的瞬时不足
|
|
||||||
retry_vols = _safe_aspirate_volumes(resources, actual_vols)
|
|
||||||
if any(v > 0 for v in retry_vols):
|
|
||||||
await super().aspirate(
|
|
||||||
resources,
|
|
||||||
retry_vols,
|
|
||||||
use_channels,
|
|
||||||
flow_rates,
|
|
||||||
offsets,
|
|
||||||
liquid_height,
|
|
||||||
blow_out_air_volume,
|
|
||||||
spread,
|
|
||||||
**backend_kwargs,
|
|
||||||
)
|
|
||||||
actual_vols = retry_vols
|
|
||||||
else:
|
|
||||||
actual_vols = retry_vols
|
|
||||||
|
|
||||||
res_samples = []
|
res_samples = []
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
# 处理 use_channels 为 None 的情况(通常用于单通道操作)
|
|
||||||
if use_channels is None:
|
if use_channels is None:
|
||||||
# 对于单通道操作,推断通道为 [0]
|
|
||||||
channels_to_use = [0] * len(resources)
|
channels_to_use = [0] * len(resources)
|
||||||
else:
|
else:
|
||||||
channels_to_use = use_channels
|
channels_to_use = use_channels
|
||||||
|
|
||||||
for resource, volume, channel in zip(resources, actual_vols, channels_to_use):
|
for resource, volume, channel in zip(resources, vols, channels_to_use):
|
||||||
sample_uuid_value = getattr(resource, "unilabos_extra", {}).get(EXTRA_SAMPLE_UUID, None)
|
sample_uuid_value = getattr(resource, "unilabos_extra", {}).get(EXTRA_SAMPLE_UUID, None)
|
||||||
res_samples.append({"name": resource.name, "sample_uuid": sample_uuid_value})
|
res_samples.append({"name": resource.name, "sample_uuid": sample_uuid_value})
|
||||||
res_volumes.append(volume)
|
res_volumes.append(volume)
|
||||||
@@ -353,17 +346,43 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
actual_vols = _safe_dispense_volumes(resources, vols)
|
actual_vols = _safe_dispense_volumes(resources, vols)
|
||||||
|
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.dispense(
|
try:
|
||||||
resources,
|
return await self._simulate_handler.dispense(
|
||||||
actual_vols,
|
resources,
|
||||||
use_channels,
|
actual_vols,
|
||||||
flow_rates,
|
use_channels,
|
||||||
offsets,
|
flow_rates,
|
||||||
liquid_height,
|
offsets,
|
||||||
blow_out_air_volume,
|
liquid_height,
|
||||||
spread,
|
blow_out_air_volume,
|
||||||
**backend_kwargs,
|
spread,
|
||||||
)
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
except (TooLittleLiquidError, TooLittleVolumeError) as e:
|
||||||
|
tracker_info = []
|
||||||
|
for r in resources:
|
||||||
|
t = r.tracker
|
||||||
|
tracker_info.append(
|
||||||
|
f"{r.name}(used={t.get_used_volume():.1f}, "
|
||||||
|
f"free={t.get_free_volume():.1f}, max={r.max_volume})"
|
||||||
|
)
|
||||||
|
if hasattr(self, "_ros_node") and self._ros_node is not None:
|
||||||
|
self._ros_node.lab_logger().warning(
|
||||||
|
f"[dispense] volume tracker error, bypassing tracking. "
|
||||||
|
f"error={e}, vols={actual_vols}, trackers={tracker_info}"
|
||||||
|
)
|
||||||
|
with no_volume_tracking():
|
||||||
|
return await self._simulate_handler.dispense(
|
||||||
|
resources,
|
||||||
|
actual_vols,
|
||||||
|
use_channels,
|
||||||
|
flow_rates,
|
||||||
|
offsets,
|
||||||
|
liquid_height,
|
||||||
|
blow_out_air_volume,
|
||||||
|
spread,
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await super().dispense(
|
await super().dispense(
|
||||||
resources,
|
resources,
|
||||||
@@ -847,6 +866,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
local = cast(Union[Container, TipRack], plr)
|
local = cast(Union[Container, TipRack], plr)
|
||||||
if hasattr(plr, "unilabos_extra") and hasattr(local, "unilabos_extra"):
|
if hasattr(plr, "unilabos_extra") and hasattr(local, "unilabos_extra"):
|
||||||
local.unilabos_extra = getattr(plr, "unilabos_extra", {}).copy()
|
local.unilabos_extra = getattr(plr, "unilabos_extra", {}).copy()
|
||||||
|
if local is not plr and hasattr(plr, "tracker") and hasattr(local, "tracker"):
|
||||||
|
local_tracker = local.tracker
|
||||||
|
plr_tracker = plr.tracker
|
||||||
|
local_history = getattr(local_tracker, "liquid_history", None)
|
||||||
|
plr_history = getattr(plr_tracker, "liquid_history", None)
|
||||||
|
if (isinstance(local_history, list) and len(local_history) == 0
|
||||||
|
and isinstance(plr_history, list) and len(plr_history) > 0):
|
||||||
|
local_tracker.liquid_history = list(plr_history)
|
||||||
resolved.append(local)
|
resolved.append(local)
|
||||||
if len(resolved) != len(uuids):
|
if len(resolved) != len(uuids):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -856,8 +883,18 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
resolved = _resolve_from_local_by_uuids()
|
resolved = _resolve_from_local_by_uuids()
|
||||||
|
|
||||||
result = list(items)
|
result = list(items)
|
||||||
for (idx, _), plr in zip(dict_items, resolved):
|
for (idx, orig_dict), res in zip(dict_items, resolved):
|
||||||
result[idx] = plr
|
if isinstance(orig_dict, dict) and hasattr(res, "tracker"):
|
||||||
|
tracker = res.tracker
|
||||||
|
local_history = getattr(tracker, "liquid_history", None)
|
||||||
|
if isinstance(local_history, list) and len(local_history) == 0:
|
||||||
|
data = orig_dict.get("data") or {}
|
||||||
|
dict_history = data.get("liquid_history")
|
||||||
|
if isinstance(dict_history, list) and len(dict_history) > 0:
|
||||||
|
tracker.liquid_history = [
|
||||||
|
(name, float(vol)) for name, vol in dict_history
|
||||||
|
]
|
||||||
|
result[idx] = res
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1058,7 +1095,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(sources)):
|
for _ in range(len(sources)):
|
||||||
tip = []
|
tip = []
|
||||||
for __ in range(len(use_channels)):
|
for __ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.append(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
resources=[sources[_]],
|
resources=[sources[_]],
|
||||||
@@ -1098,7 +1135,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(sources), 8):
|
for i in range(0, len(sources), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.append(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = waste_liquid[i : i + 8]
|
current_targets = waste_liquid[i : i + 8]
|
||||||
current_reagent_sources = sources[i : i + 8]
|
current_reagent_sources = sources[i : i + 8]
|
||||||
@@ -1192,7 +1229,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(targets)):
|
for _ in range(len(targets)):
|
||||||
tip = []
|
tip = []
|
||||||
for x in range(len(use_channels)):
|
for x in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.append(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1244,7 +1281,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.append(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
current_reagent_sources = reagent_sources[i : i + 8]
|
current_reagent_sources = reagent_sources[i : i + 8]
|
||||||
@@ -1526,7 +1563,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
delays = kwargs.get('delays')
|
delays = kwargs.get('delays')
|
||||||
|
|
||||||
tip = []
|
tip = []
|
||||||
tip.extend(self._get_next_tip())
|
tip.append(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
blow_out_air_volume_before_vol = 0.0
|
blow_out_air_volume_before_vol = 0.0
|
||||||
if blow_out_air_volume_before is not None and len(blow_out_air_volume_before) > 0:
|
if blow_out_air_volume_before is not None and len(blow_out_air_volume_before) > 0:
|
||||||
@@ -1729,9 +1766,13 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
||||||
"""Yield tips from a list of TipRacks one-by-one until depleted."""
|
"""Yield tips from a list of TipRacks one-by-one until depleted."""
|
||||||
for rack in tip_racks:
|
for rack in tip_racks:
|
||||||
for tip in rack:
|
if isinstance(rack, TipSpot):
|
||||||
yield tip
|
yield rack
|
||||||
# raise RuntimeError("Out of tips!")
|
elif hasattr(rack, "get_all_items"):
|
||||||
|
yield from rack.get_all_items()
|
||||||
|
else:
|
||||||
|
for tip in rack:
|
||||||
|
yield tip
|
||||||
|
|
||||||
def _get_next_tip(self):
|
def _get_next_tip(self):
|
||||||
"""从 current_tip 迭代器获取下一个 tip,耗尽时抛出明确错误而非 StopIteration"""
|
"""从 current_tip 迭代器获取下一个 tip,耗尽时抛出明确错误而非 StopIteration"""
|
||||||
|
|||||||
@@ -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__(name, size_x, size_y, size_z)
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user