import asyncio
import collections
import contextlib
import json
import socket
import time
from typing import Any, List, Dict, Optional, TypedDict, Union, Sequence, Iterator, Literal
import pprint as pp
from pylabrobot.liquid_handling import (
    LiquidHandlerBackend,
    Pickup,
    SingleChannelAspiration,
    Drop,
    SingleChannelDispense,
    PickupTipRack,
    DropTipRack,
    MultiHeadAspirationPlate, ChatterBoxBackend, LiquidHandlerChatterboxBackend,
)
from pylabrobot.liquid_handling.standard import (
    MultiHeadAspirationContainer,
    MultiHeadDispenseContainer,
    MultiHeadDispensePlate,
    ResourcePickup,
    ResourceMove,
    ResourceDrop,
)
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
from traitlets import Int

from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract




class MaterialResource:
    """统一的液体/反应器资源，支持多孔（wells）场景：
    - wells: 列表，每个元素代表一个物料孔（unit）；
    - units: 与 wells 对齐的列表，每个元素是 {liquid_id: volume}；
    - 若传入 liquid_id + volume 或 composition，总量将**等分**到各 unit；
    """
    def __init__(
        self,
        resource_name: str,
        slot: int,
        well: List[int],
        composition: Optional[Dict[str, float]] = None,
        liquid_id: Optional[str] = None,
        volume: Union[float, int] = 0.0,
        is_supply: Optional[bool] = None,
    ):
        self.resource_name = resource_name
        self.slot = int(slot)
        self.well = list(well or [])
        self.is_supply = bool(is_supply) if is_supply is not None else (bool(composition) or (liquid_id is not None))

        # 规范化：至少有 1 个 unit
        n = max(1, len(self.well))
        self.units: List[Dict[str, float]] = [dict() for _ in range(n)]

        # 初始化内容：等分到各 unit
        if composition:
            for k, v in composition.items():
                share = float(v) / n
                for u in self.units:
                    if share > 0:
                        u[k] = u.get(k, 0.0) + share
        elif liquid_id is not None and float(volume) > 0:
            share = float(volume) / n
            for u in self.units:
                u[liquid_id] = u.get(liquid_id, 0.0) + share

    # 位置描述
    def location(self) -> Dict[str, Any]:
        return {"slot": self.slot, "well": self.well}

    def unit_count(self) -> int:
        return len(self.units)

    def unit_volume(self, idx: int) -> float:
        return float(sum(self.units[idx].values()))

    def total_volume(self) -> float:
        return float(sum(self.unit_volume(i) for i in range(self.unit_count())))

    def add_to_unit(self, idx: int, liquid_id: str, vol: Union[float, int]):
        v = float(vol)
        if v < 0:
            return
        u = self.units[idx]
        if liquid_id not in u:
           u[liquid_id] = 0.0
        if v > 0:
           u[liquid_id] += v

    def remove_from_unit(self, idx: int, total: Union[float, int]) -> Dict[str, float]:
        take = float(total)
        if take <= 0: return {}
        u = self.units[idx]
        avail = sum(u.values())
        if avail <= 0: return {}
        take = min(take, avail)
        ratio = take / avail
        removed: Dict[str, float] = {}
        for k, v in list(u.items()):
            dv = v * ratio
            nv = v - dv
            if nv < 1e-9: nv = 0.0
            u[k] = nv
            removed[k] = dv

        self.units[idx] = {k: v for k, v in u.items() if v > 0}
        return removed

    def transfer_unit_to(self, src_idx: int, other: "MaterialResource", dst_idx: int, total: Union[float, int]):
        moved = self.remove_from_unit(src_idx, total)
        for k, v in moved.items():
            other.add_to_unit(dst_idx, k, v)

    def get_resource(self) -> Dict[str, Any]:
        return {
            "resource_name": self.resource_name,
            "slot": self.slot,
            "well": self.well,
            "units": [dict(u) for u in self.units],
            "total_volume": self.total_volume(),
            "is_supply": self.is_supply,
        }

def transfer_liquid(
    sources: MaterialResource,
    targets: MaterialResource,
    unit_volume: Optional[Union[float, int]] = None,
    tip: Optional[str] = None, #这里应该是指定种类的
) -> Dict[str, Any]:
    try:
        vol_each = float(unit_volume)
    except (TypeError, ValueError):
        return {"action": "transfer_liquid", "error": "invalid unit_volume"}
    if vol_each <= 0:
        return {"action": "transfer_liquid", "error": "non-positive volume"}

    ns, nt = sources.unit_count(), targets.unit_count()
    # one-to-many: 从单个 source unit(0) 扇出到目标各 unit
    if ns == 1 and nt >= 1:
        for j in range(nt):
            sources.transfer_unit_to(0, targets, j, vol_each)
    # many-to-many: 数量相同，逐一对应
    elif ns == nt and ns > 0:
        for i in range(ns):
            sources.transfer_unit_to(i, targets, i, vol_each)
    else:
        raise ValueError(f"Unsupported mapping: sources={ns} units, targets={nt} units. Only 1->N or N->N are allowed.")

    return {
        "action": "transfer_liquid",
        "sources": sources.get_resource(),
        "targets": targets.get_resource(),
        "unit_volume": unit_volume,
        "tip": tip, 
    }

def plan_transfer(pm: "ProtocolManager", **kwargs) -> Dict[str, Any]:
    """Shorthand to add a non-committing transfer to a ProtocolManager.
    Accepts the same kwargs as ProtocolManager.add_transfer.
    """
    return pm.add_transfer(**kwargs)

class ProtocolManager:
    """Plan/track transfers and back‑solve minimum initial volumes.

    Use add_transfer(...) to register steps (no mutation).
    Use compute_min_initials(...) to infer the minimal starting volume of each liquid
    per resource required to execute the plan in order.
    """

    # ---------- lifecycle ----------
    def __init__(self):
        # queued logical steps (keep live refs to MaterialResource)
        self.steps: List[Dict[str, Any]] = []

        # simple tip catalog; choose the smallest that meets min_aspirate and capacity*safety
        self.tip_catalog = [
            {"name": "TIP_10uL",   "capacity": 10.0,   "min_aspirate": 0.5},
            {"name": "TIP_20uL",   "capacity": 20.0,   "min_aspirate": 1.0},
            {"name": "TIP_50uL",   "capacity": 50.0,   "min_aspirate": 2.0},
            {"name": "TIP_200uL",  "capacity": 200.0,  "min_aspirate": 5.0},
            {"name": "TIP_300uL",  "capacity": 300.0,  "min_aspirate": 10.0},
            {"name": "TIP_1000uL", "capacity": 1000.0, "min_aspirate": 20.0},
        ]

        # stable labels for unknown liquids per resource (A, B, C, ..., AA, AB, ...)
        self._unknown_labels: Dict[MaterialResource, str] = {}
        self._unknown_label_counter: int = 0

    # ---------- public API ----------
    def recommend_tip(self, unit_volume: float, safety: float = 1.10) -> str:
        v = float(unit_volume)
        # prefer: meets min_aspirate and capacity with safety margin; else fallback to capacity-only; else max capacity
        eligible = [t for t in self.tip_catalog if t["min_aspirate"] <= v and t["capacity"] >= v * safety]
        if not eligible:
            eligible = [t for t in self.tip_catalog if t["capacity"] >= v]
        return min(eligible or self.tip_catalog, key=lambda t: t["capacity"]) ["name"]

    def get_tip_capacity(self, tip_name: str) -> Optional[float]:
        for t in self.tip_catalog:
            if t["name"] == tip_name:
                return t["capacity"]
        return None

    def add_transfer(
        self,
        sources: MaterialResource,
        targets: MaterialResource,
        unit_volume: Union[float, int],
        tip: Optional[str] = None,
    ) -> Dict[str, Any]:
        step = {
            "action": "transfer_liquid",
            "sources": sources,
            "targets": targets,
            "unit_volume": float(unit_volume),
            "tip": tip or self.recommend_tip(unit_volume),
        }
        self.steps.append(step)
        # return a serializable shadow (no mutation)
        return {
            "action": "transfer_liquid",
            "sources": sources.get_resource(),
            "targets": targets.get_resource(),
            "unit_volume": step["unit_volume"],
            "tip": step["tip"],
        }

    @staticmethod
    def _liquid_keys_of(resource: MaterialResource) -> List[str]:
        keys: set[str] = set()
        for u in resource.units:
            keys.update(u.keys())
        return sorted(keys)

    @staticmethod
    def _fanout_multiplier(ns: int, nt: int) -> Optional[int]:
        """Return the number of liquid movements for a mapping shape.
        1->N: N moves; N->N: N moves; otherwise unsupported (None).
        """
        if ns == 1 and nt >= 1:
            return nt
        if ns == nt and ns > 0:
            return ns
        return None

    # ---------- planning core ----------
    def compute_min_initials(
        self,
        use_initial: bool = False,
        external_only: bool = True,
    ) -> Dict[str, Dict[str, float]]:
        """Simulate the plan (non‑mutating) and return minimal starting volumes per resource/liquid."""
        ledger: Dict[MaterialResource, Dict[str, float]] = {}
        min_seen: Dict[MaterialResource, Dict[str, float]] = {}

        def _ensure(res: MaterialResource) -> None:
            if res in ledger:
                return
            declared = self._liquid_keys_of(res)
            if use_initial:
                # sum actual held amounts across units
                totals = {k: 0.0 for k in declared}
                for u in res.units:
                    for k, v in u.items():
                        totals[k] = totals.get(k, 0.0) + float(v)
                ledger[res] = totals
            else:
                ledger[res] = {k: 0.0 for k in declared}
            min_seen[res] = {k: ledger[res].get(k, 0.0) for k in ledger[res]}

        def _proportions(src: MaterialResource, src_bal: Dict[str, float]) -> tuple[List[str], Dict[str, float]]:
            keys = list(src_bal.keys())
            total_pos = sum(x for x in src_bal.values() if x > 0)

            # if ledger has no keys yet, seed from declared types on the resource
            if not keys:
                keys = self._liquid_keys_of(src)
                for k in keys:
                    src_bal.setdefault(k, 0.0)
                    min_seen[src].setdefault(k, 0.0)

            if total_pos > 0:
                # proportional to current positive balances
                props = {k: (src_bal.get(k, 0.0) / total_pos) for k in keys}
                return keys, props

            # no material currently: evenly from known keys, or assign an unknown label
            if keys:
                eq = 1.0 / len(keys)
                return keys, {k: eq for k in keys}

            unk = self._label_for_unknown(src)
            keys = [unk]
            src_bal.setdefault(unk, 0.0)
            min_seen[src].setdefault(unk, 0.0)
            return keys, {unk: 1.0}

        for step in self.steps:
            if step.get("action") != "transfer_liquid":
                continue

            src: MaterialResource = step["sources"]
            dst: MaterialResource = step["targets"]
            vol = float(step["unit_volume"])
            if vol <= 0:
                continue

            _ensure(src)
            _ensure(dst)

            mult = self._fanout_multiplier(src.unit_count(), dst.unit_count())
            if not mult:
                continue  # unsupported mapping shape for this planner

            eff_vol = vol * mult
            src_bal = ledger[src]
            keys, props = _proportions(src, src_bal)

            # subtract from src; track minima; accumulate to dst
            moved: Dict[str, float] = {}
            for k in keys:
                dv = eff_vol * props[k]
                src_bal[k] = src_bal.get(k, 0.0) - dv
                moved[k] = dv
                prev_min = min_seen[src].get(k, 0.0)
                if src_bal[k] < prev_min:
                    min_seen[src][k] = src_bal[k]

            dst_bal = ledger[dst]
            for k, dv in moved.items():
                dst_bal[k] = dst_bal.get(k, 0.0) + dv
                min_seen[dst].setdefault(k, dst_bal[k])

        # convert minima (negative) to required initials
        result: Dict[str, Dict[str, float]] = {}
        for res, mins in min_seen.items():
            if external_only and not getattr(res, "is_supply", False):
                continue
            need = {liq: max(0.0, -mn) for liq, mn in mins.items() if mn < 0.0}
            if need:
                result[res.resource_name] = need
        return result

    def compute_tip_consumption(self) -> Dict[str, Any]:
        """Compute how many tips are consumed at each transfer step, and aggregate by tip type.
        Rule: each liquid movement (source unit -> target unit) consumes one tip.
        For supported shapes: 1->N uses N tips; N->N uses N tips.
        """
        per_step: List[Dict[str, Any]] = []
        totals_by_tip: Dict[str, int] = {}

        for i, s in enumerate(self.steps):
            if s.get("action") != "transfer_liquid":
                continue
            ns = s["sources"].unit_count()
            nt = s["targets"].unit_count()
            moves = self._fanout_multiplier(ns, nt) or 0
            tip_name = s.get("tip") or self.recommend_tip(s["unit_volume"])  # per-step tip may vary
            per_step.append({
                "idx": i,
                "tip": tip_name,
                "tips_used": moves,
                "moves": moves,
            })
            totals_by_tip[tip_name] = totals_by_tip.get(tip_name, 0) + int(moves)

        return {"per_step": per_step, "totals_by_tip": totals_by_tip}

    def compute_min_initials_with_tips(
        self,
        use_initial: bool = False,
        external_only: bool = True,
    ) -> Dict[str, Any]:
        needs = self.compute_min_initials(use_initial=use_initial, external_only=external_only)
        step_tips: List[Dict[str, Any]] = []
        totals_by_tip: Dict[str, int] = {}

        for i, s in enumerate(self.steps):
            if s.get("action") != "transfer_liquid":
                continue
            ns = s["sources"].unit_count()
            nt = s["targets"].unit_count()
            moves = self._fanout_multiplier(ns, nt) or 0
            tip_name = s.get("tip") or self.recommend_tip(s["unit_volume"])  # step-specific tip
            totals_by_tip[self.get_tip_capacity(tip_name)] = totals_by_tip.get(tip_name, 0) + int(moves)

            step_tips.append({
                "idx": i,
                "tip": tip_name,
                "tip_capacity": self.get_tip_capacity(tip_name),
                "unit_volume": s["unit_volume"],
                "tips_used": moves,
            })
        return {"liquid_setup": needs, "step_tips": step_tips, "totals_by_tip": totals_by_tip}

    # ---------- unknown labels ----------
    def _index_to_letters(self, idx: int) -> str:
        """0->A, 1->B, ... 25->Z, 26->AA, 27->AB ... (Excel-like)"""
        s: List[str] = []
        idx = int(idx)
        while True:
            idx, r = divmod(idx, 26)
            s.append(chr(ord('A') + r))
            if idx == 0:
                break
            idx -= 1  # Excel-style carry
        return "".join(reversed(s))

    def _label_for_unknown(self, res: MaterialResource) -> str:
        """Assign a stable unknown-liquid label (A/B/C/...) per resource."""
        if res not in self._unknown_labels:
            lab = self._index_to_letters(self._unknown_label_counter)
            self._unknown_label_counter += 1
            self._unknown_labels[res] = lab
        return self._unknown_labels[res]


# 在这一步传输目前有的物料
class LabResource:
    def __init__(self):
        self.tipracks = []
        self.plates = []
        self.trash = []

    def add_tipracks(self, tiprack: List[TipRack]):
        self.tipracks.extend(tiprack)
    def add_plates(self, plate: List[Plate]):
        self.plates.extend(plate)
    def add_trash(self, trash: List[Plate]):
        self.trash.extend(trash)
    def get_resources_info(self) -> Dict[str, Any]:
        tipracks = [{"name": tr.name, "max_volume": tr.children[0].tracker._tip.maximal_volume, "count": len(tr.children)} for tr in self.tipracks]
        plates = [{"name": pl.name, "max_volume": pl.children[0].max_volume, "count": len(pl.children)} for pl in self.plates]
        trash = [{"name": t.name, "max_volume": t.children[0].max_volume, "count": len(t.children)} for t in self.trash]
        return {
            "tipracks": tipracks,
            "plates": plates,
            "trash": trash
        }

from typing import Dict, Any

import time
class DefaultLayout:

    def __init__(self, product_name: str = "PRCXI9300"):
        self.labresource = {}
        if product_name not in ["PRCXI9300", "PRCXI9320"]:
            raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.")

        if product_name == "PRCXI9300":
            self.rows = 2
            self.columns = 3
            self.layout = [1, 2, 3, 4, 5, 6]
            self.trash_slot = 3
            self.waste_liquid_slot = 6

        elif product_name == "PRCXI9320":
            self.rows = 3
            self.columns = 4
            self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
            self.trash_slot = 16
            self.waste_liquid_slot = 12
            self.default_layout =  {"MatrixId":f"{time.time()}","MatrixName":f"{time.time()}","MatrixCount":16,"WorkTablets":
            [{"Number": 1, "Code": "T1", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 2, "Code": "T2", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 3, "Code": "T3", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 4, "Code": "T4", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 5, "Code": "T5", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 6, "Code": "T6", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 7, "Code": "T7", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 8, "Code": "T8", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 9, "Code": "T9", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 10, "Code": "T10", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 11, "Code": "T11", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 12, "Code": "T12", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}},  # 这个设置成废液槽，用储液槽表示
  {"Number": 13, "Code": "T13", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 14, "Code": "T14", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 15, "Code": "T15", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
  {"Number": 16, "Code": "T16", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}} # 这个设置成垃圾桶，用储液槽表示
]
}

    def get_layout(self) -> Dict[str, Any]:
        return {
            "rows": self.rows,
            "columns": self.columns,
            "layout": self.layout,
            "trash_slot": self.trash_slot,
            "waste_liquid_slot": self.waste_liquid_slot
        }

    def get_trash_slot(self) -> int:
        return self.trash_slot
    
    def get_waste_liquid_slot(self) -> int:
        return self.waste_liquid_slot

    def add_lab_resource(self, material_info):
        self.labresource = material_info

    def recommend_layout(self, needs: Dict[str, int]) -> Dict[str, Any]:
        """根据 needs 推荐布局"""
        for k, v in needs.items():
            if k not in self.labresource:
                raise ValueError(f"Material {k} not found in lab resources.")
        
        # 预留位置12和16不动
        reserved_positions = {12, 16}
        available_positions = [i for i in range(1, 17) if i not in reserved_positions]
        
        # 计算总需求
        total_needed = sum(needs.values())
        if total_needed > len(available_positions):
            raise ValueError(f"需要 {total_needed} 个位置，但只有 {len(available_positions)} 个可用位置（排除位置12和16）")
        
        # 依次分配位置
        current_pos = 0
        for material_name, count in needs.items():
            material_uuid = self.labresource[material_name]['uuid']
            material_enum = self.labresource[material_name]['materialEnum']
            
            for _ in range(count):
                if current_pos >= len(available_positions):
                    raise ValueError("位置不足，无法分配更多物料")
                
                position = available_positions[current_pos]
                # 找到对应的tablet并更新
                for tablet in self.default_layout['WorkTablets']:
                    if tablet['Number'] == position:
                        tablet['Material']['uuid'] = material_uuid
                        tablet['Material']['materialEnum'] = material_enum
                        break
                
                current_pos += 1
        
        return self.default_layout


if __name__ == "__main__":
    with open("prcxi_material.json", "r") as f:
        material_info = json.load(f)
    
    layout = DefaultLayout("PRCXI9320")
    layout.add_lab_resource(material_info)
    plan = layout.recommend_layout({
        "10μL加长 Tip头": 2,
        "300μL Tip头": 2, 
        "96深孔板": 2,
    })
    



# if __name__ == "__main__":
#     # ---- 资源：SUP 供液（X），中间板 R1（4 孔空），目标板 R2（4 孔空）----
# #     sup = MaterialResource("SUP", slot=5, well=[1], liquid_id="X", volume=10000)      
# #     r1  = MaterialResource("R1",  slot=6, well=[1,2,3,4,5,6,7,8])
# #     r2  = MaterialResource("R2",  slot=7, well=[1,2,3,4,5,6,7,8])

# #     pm = ProtocolManager()
# #     # 步骤1：SUP -> R1，1->N 扇出，每孔 50 uL（总 200 uL）
# #     pm.add_transfer(sup, r1, unit_volume=10.0)
# #     # 步骤2：R1 -> R2，N->N 对应，每对 25 uL（总 100 uL；来自 R1 中已存在的混合物 X）
# #     pm.add_transfer(r1, r2, unit_volume=120.0)

# #     out = pm.compute_min_initials_with_tips()
# #     # layout_planer = DefaultLayout('PRCXI9320')
# #     # print(layout_planer.get_layout())
# #     # print("回推最小需求：", out["liquid_setup"])     # {'SUP': {'X': 200.0}}
# #     # print("步骤枪头建议：", out["step_tips"]) # [{'idx':0,'tip':'TIP_200uL','unit_volume':50.0}, {'idx':1,'tip':'TIP_50uL','unit_volume':25.0}]

# # #     # 实际执行（可选）
# # #     transfer_liquid(sup, r1, unit_volume=50.0)
# # #     transfer_liquid(r1, r2, unit_volume=25.0)
# # #     print("执行后 SUP：", sup.get_resource())  # 总体积 -200
# # #     print("执行后 R1：",  r1.get_resource())   # 每孔 25 uL（50 进 -25 出）
# # #     print("执行后 R2：",  r2.get_resource())   # 每孔 25 uL


# #     from pylabrobot.resources.opentrons.tube_racks import *
# #     from pylabrobot.resources.opentrons.plates import *
# #     from pylabrobot.resources.opentrons.tip_racks import *
# #     from pylabrobot.resources.opentrons.reservoirs import *

# #     plate = [locals()['nest_96_wellplate_2ml_deep'](name="thermoscientificnunc_96_wellplate_2000ul"), locals()['corning_96_wellplate_360ul_flat'](name="corning_96_wellplate_360ul_flat")]
# #     tiprack = [locals()['opentrons_96_tiprack_300ul'](name="opentrons_96_tiprack_300ul"), locals()['opentrons_96_tiprack_1000ul'](name="opentrons_96_tiprack_1000ul")]
# #     trash = [locals()['axygen_1_reservoir_90ml'](name="axygen_1_reservoir_90ml")]

# #     from pprint import pprint

# #     lab_resource = LabResource()
# #     lab_resource.add_tipracks(tiprack)
# #     lab_resource.add_plates(plate)
# #     lab_resource.add_trash(trash)

# #     layout_planer = DefaultLayout('PRCXI9300')
# #     layout_planer.add_lab_resource(lab_resource)
# #     layout_planer.recommend_layout(out)

#     with open("prcxi_material.json", "r") as f:
#         material_info = json.load(f)
#     # print("当前实验物料信息：", material_info)

#     layout = DefaultLayout("PRCXI9320")
#     layout.add_lab_resource(material_info)
#     print(layout.default_layout['WorkTablets'])
#     # plan = layout.recommend_layout({
#     #     "10μL加长 Tip头": 2,
#     #     "300μL Tip头": 2,
#     #     "96深孔板": 2,
#     # })



