Source code for AFL.automation.prepare.OT2DeckWebAppMixin

import json
import pathlib

from AFL.automation.APIServer.Driver import Driver


# Limited set of labware options for quick loading via the dashboard.
LABWARE_OPTIONS = {
    "opentrons/opentrons_96_tiprack_10ul": "Opentrons 96 Tiprack 10uL",
    "opentrons/opentrons_96_tiprack_300ul": "Opentrons 96 Tiprack 300uL",
    "opentrons/opentrons_96_tiprack_1000ul": "Opentrons 96 Tiprack 1000uL",
    "opentrons/corning_96_wellplate_360ul_flat": "Corning 96 Well Plate",
    "opentrons/nest_96_wellplate_2ml_deep": "NEST 2mL 96 Deep Well Plate",
    "custom_beta/nest_96_wellplate_1p6ml_deep_afl": "NEST 1.6mL 96 Deep Well Plate (AFL Definition)",
    "custom_beta/nist_pneumatic_loader": "NIST Pneumatic Loader (slot 10 only)",
    "custom_beta/nist_6_20ml_vials": "NIST 6 x 20mL vial carrier",
    "custom_beta/nist_2_100ml_bottles": "NIST 2 x 100mL bottle carrier",
    "heaterShakerModuleV1": "HeaterShaker Module (still needs labware atop it!)",
    "abgenestorage_96_wellplate_1200ul": "Abgenestorage 96 Wellplate 1200 ul",
    "aps1296wellon300ulrack_96_wellplate_300ul": "12IDB Sample Collection Plate (in tiprack!!!)",
}


[docs] class OT2DeckWebAppMixin: @staticmethod def _generate_ot2_well_svg(labware_data, available_tips=None, size=90, labware_uuid=None, compact=False): if not labware_data: return "" definition = labware_data.get('definition', {}) wells = definition.get('wells', {}) if not wells: return "" labware_type = definition.get('metadata', {}).get('displayCategory', 'default') is_tiprack = labware_type == 'tipRack' or 'tiprack' in definition.get('parameters', {}).get('loadName', '').lower() available_tips_for_labware = set() if is_tiprack and labware_uuid and available_tips: for mount_tips in available_tips.values(): for tip_labware_uuid, well_name in mount_tips: if tip_labware_uuid == labware_uuid: available_tips_for_labware.add(well_name) well_count = len(wells) if compact: if well_count <= 8: cols = well_count rows = 1 elif well_count <= 24: cols = 6 rows = (well_count + 5) // 6 elif well_count <= 96: cols = 12 rows = 8 else: cols = 12 rows = (well_count + 11) // 12 cell_width = size / max(cols, 6) cell_height = size / max(rows, 4) colors = { 'tipRack_available': '#4caf50', 'tipRack_used': '#f44336', 'tipRack': '#ffa726', 'wellPlate': '#42a5f5', 'reservoir': '#66bb6a', 'default': '#90a4ae', } svg_elements = [] well_names = list(wells.keys()) for i, well_name in enumerate(well_names[:min(well_count, rows * cols)]): row = i % rows col = i // rows x = col * cell_width + cell_width / 4 y = row * cell_height + cell_height / 4 if is_tiprack and labware_uuid: if well_name in available_tips_for_labware: color = colors['tipRack_available'] status = "Available" else: color = colors['tipRack_used'] status = "Used" tooltip = f"{well_name} - {status}" else: color = colors.get(labware_type, '#90a4ae') tooltip = well_name svg_elements.append( f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{min(cell_width, cell_height)/3:.1f}" ' f'fill="{color}" stroke="#333" stroke-width="0.3">' f'<title>{tooltip}</title></circle>' ) return ( f'<svg width="{size}" height="{size}" ' f'style="border: 1px solid #ddd; border-radius: 3px;">{"".join(svg_elements)}</svg>' ) labware_width = definition.get('dimensions', {}).get('xDimension', 127.76) labware_height = definition.get('dimensions', {}).get('yDimension', 85.48) scale_x = size / labware_width scale_y = (size * 0.75) / labware_height svg_elements = [] well_colors = { 'tipRack_available': '#4caf50', 'tipRack_used': '#f44336', 'tipRack_default': '#ffa726', 'wellPlate': '#42a5f5', 'reservoir': '#66bb6a', 'default': '#90a4ae', } for well_name, well_info in wells.items(): x = well_info.get('x', 0) * scale_x y = (labware_height - well_info.get('y', 0)) * scale_y shape = well_info.get('shape', 'circular') if is_tiprack and labware_uuid: if well_name in available_tips_for_labware: well_color = well_colors['tipRack_available'] tip_status = "Available" else: well_color = well_colors['tipRack_used'] tip_status = "Used" tooltip = f"{well_name} - {tip_status}" else: well_color = well_colors.get(labware_type, well_colors['default']) tooltip = well_name if shape == 'circular': diameter = well_info.get('diameter', 5) * min(scale_x, scale_y) radius = diameter / 2 svg_elements.append( f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{radius:.1f}" ' f'fill="{well_color}" stroke="#333" stroke-width="0.5" opacity="0.8">' f'<title>{tooltip}</title></circle>' ) elif shape == 'rectangular': well_width = well_info.get('xDimension', 8) * scale_x well_height = well_info.get('yDimension', 8) * scale_y rect_x = x - well_width / 2 rect_y = y - well_height / 2 svg_elements.append( f'<rect x="{rect_x:.1f}" y="{rect_y:.1f}" ' f'width="{well_width:.1f}" height="{well_height:.1f}" ' f'fill="{well_color}" stroke="#333" stroke-width="0.5" opacity="0.8">' f'<title>{tooltip}</title></rect>' ) if svg_elements: return f'<svg width="{size}" height="{int(size*0.75)}" style="margin: 5px 0;">{"".join(svg_elements)}</svg>' return "" def _get_ot2_slot_info(self, slot_num, compact): if slot_num == "Trash": return {"name": "Trash", "type": "trash", "color": "#ffcdd2", "svg": ""} slot_str = str(slot_num) info = {"name": "Empty", "type": "empty", "color": "#f5f5f5", "svg": ""} has_labware = slot_str in self.config["loaded_labware"] has_module = slot_str in self.config["loaded_modules"] if has_labware: labware_id, labware_type, labware_data = self.config["loaded_labware"][slot_str] definition = labware_data.get('definition', {}) display_name = definition.get('metadata', {}).get('displayName', labware_type) is_tiprack = ( 'tiprack' in labware_type.lower() or definition.get('metadata', {}).get('displayCategory') == 'tipRack' ) wells = list(definition.get('wells', {}).keys()) def _well_key(w): import re m = re.match(r"([A-Za-z]+)(\d+)", w) if m: row = m.group(1) col = int(m.group(2)) return (row, col) return (w, 0) wells = sorted(wells, key=_well_key) mounts = [] if is_tiprack: for m, d in self.config['loaded_instruments'].items(): if labware_id in d.get('tip_racks', []): mounts.append(m) info.update({ "name": display_name[:20] + ("..." if len(display_name) > 20 else ""), "type": "labware", "color": "#bbdefb", "svg": self._generate_ot2_well_svg( labware_data, available_tips=self.config.get("available_tips", {}), size=50 if compact else 90, labware_uuid=labware_id, compact=compact, ) }) if is_tiprack: info['tiprack'] = True info['mounts'] = mounts info['color'] = '#fff3e0' info['target_count'] = len(wells) info['targets'] = ','.join([f"{slot_str}{w}" for w in wells]) if has_module: _, module_type = self.config["loaded_modules"][slot_str] module_name = module_type.replace('ModuleV1', '').replace('Module', ' Mod') if has_labware: info["name"] = f"{module_name}<br><small>{info['name']}</small>" info["color"] = "#c8e6c9" else: info.update({ "name": module_name, "type": "module_only", "color": "#e1bee7", }) return info
[docs] @Driver.unqueued(render_hint='html') def visualize_deck(self, mode='full', **kwargs): slot_layout = [ [10, 11, "Trash"], [7, 8, 9], [4, 5, 6], [1, 2, 3], ] slot_infos = {} for row in slot_layout: for slot in row: info = self._get_ot2_slot_info(slot, compact=(mode == 'simple')) slot_label = "T" if slot == "Trash" else str(slot) info["slot_label"] = slot_label info["click_attr"] = "" if info["type"] in ["empty", "module_only"]: info["click_attr"] = f"onclick=\"showLabwareOptions('{slot}')\" style=\"cursor:pointer;\"" info["buttons"] = ''.join([ f"<button style='margin-top:4px;font-size:10px;' onclick=\"resetTipracks('{m}')\">Reset</button>" for m in info.get('mounts', []) ]) if info.get('target_count', 0) > 10: target_str = info.get('targets', '') slot_id = str(slot) info["buttons"] += ( f"<button style='margin-top:4px;font-size:10px;' " f"onclick=\"openPrepTargetDialog('{slot_id}','{target_str}')\">" "Manage Targets</button>" ) slot_infos[str(slot)] = info base = pathlib.Path(__file__).parent.parent / "apps" / "ot2_deck" html_template = (base / "ot2_deck.html").read_text() css = (base / "css" / "style.css").read_text() js = (base / "js" / "main.js").read_text() from jinja2 import Template template = Template(html_template) return template.render( slot_layout=slot_layout, slot_infos=slot_infos, loaded_instruments=self.config.get('loaded_instruments', {}), mode=mode, deck_data_json=json.dumps({"labwareChoices": LABWARE_OPTIONS}), inline_css=css, inline_js=js, )