diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 90c862f8e22..78bd42f8146 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,7 +35,7 @@ jobs: - name: Check documentation run: | - rm -rf docs/build docs/_autosummary + make clean-docs make docs-check deploy_docs: @@ -71,7 +71,7 @@ jobs: env: DOCS_VERSION: ${{ steps.version.outputs.slug }} run: | - rm -rf docs/build docs/_autosummary + make clean-docs make docs - name: Clean build artifacts diff --git a/.gitignore b/.gitignore index 01db94c3ec0..2e6fc786294 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ pyhamilton/LAY-BACKUP .ipynb_checkpoints *.egg-info *.log +test_logs/ build/lib myenv diff --git a/Makefile b/Makefile index 2e23b332765..852a91e7186 100644 --- a/Makefile +++ b/Makefile @@ -5,25 +5,28 @@ endif .PHONY: docs docs-fast docs-check docs-linkcheck clean-docs lint test +DOCS_GENERATED_DIRS = docs/_autosummary docs/api/_autosummary docs/jupyter_execute docs/user_guide/jupyter_execute + docs: + rm -rf $(DOCS_GENERATED_DIRS) sphinx-build -b html docs docs/build/ -j 16 -W docs-fast: echo "building docs without api for speed" + rm -rf $(DOCS_GENERATED_DIRS) sphinx-build -t no-api -b html docs docs/build/ -j 16 -W docs-check: + rm -rf $(DOCS_GENERATED_DIRS) sphinx-build -b dummy docs docs/build/ -j 16 -W docs-linkcheck: + rm -rf $(DOCS_GENERATED_DIRS) sphinx-build -b linkcheck docs docs/build/linkcheck -j 16 -W clean-docs: rm -rf docs/build - rm -rf docs/_autosummary - rm -rf docs/api/_autosummary - rm -rf docs/jupyter_execute - rm -rf docs/user_guide/jupyter_execute + rm -rf $(DOCS_GENERATED_DIRS) TRACKED_PY = $(shell git ls-files 'pylabrobot/*.py' 'pylabrobot/*.ipynb') diff --git a/docs/_exts/pylabrobot_labware_library.py b/docs/_exts/pylabrobot_labware_library.py new file mode 100644 index 00000000000..c2c171a9acf --- /dev/null +++ b/docs/_exts/pylabrobot_labware_library.py @@ -0,0 +1,599 @@ +from __future__ import annotations + +import importlib +import inspect +import json +import re +from html import escape +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, get_type_hints + +from pylabrobot.resources import generate_geometry_library +from pylabrobot.resources.resource import Resource + + +LIBRARY_RELATIVE_ROOT = Path("resources") / "library" +GEOMETRY_INDEX_FILENAME = "labware_library_index.json" +RESOURCE_ALIASES = { + "Azenta4titudeFrameStar_96_wellplate_skirted": "Azenta4titudeFrameStar_96_wellplate_200ul_Vb", + "BioRad_384_DWP_50uL_Vb": "BioRad_384_wellplate_50uL_Vb", + "CellTreat_6_DWP_16300ul_Fb": "CellTreat_6_wellplate_16300ul_Fb", + "Cor_12_wellplate_6900ul_Fb": "Cor_Cos_12_wellplate_6900ul_Fb", + "Cor_24_wellplate_3470ul_Fb": "Cor_Cos_24_wellplate_3470ul_Fb", + "Cor_48_wellplate_1620ul_Fb": "Cor_Cos_48_wellplate_1620ul_Fb", + "Cos_96_wellplate_2mL_Vb": "Cor_96_wellplate_2mL_Vb", + "Cos_6_wellplate_16800ul_Fb": "Cor_Cos_6_wellplate_16800ul_Fb", + "Hamilton_mfx_plateholder_DWP_metal_tapped": "hamilton_mfx_plateholder_DWP_metal_tapped", + "PLT_CAR_P3AC": "PLT_CAR_P3AC_A00", +} + +MANUFACTURER_NAMES = { + "agenbio": "AgenBio", + "agilent": "Agilent", + "alpaqua": "Alpaqua", + "azenta": "Azenta", + "bioer": "Bioer", + "biorad": "Bio-Rad", + "boekel": "Boekel", + "btx": "BTX", + "celltreat": "CellTreat", + "cellvis": "CellVis", + "corning": "Corning Inc.", + "diy": "DIY", + "eppendorf": "Eppendorf", + "greiner": "Greiner Bio-One", + "hamilton": "Hamilton", + "imcs": "IMCS", + "nest": "NEST", + "opentrons": "Opentrons", + "perkin_elmer": "Perkin Elmer", + "porvair": "Porvair", + "revvity": "Revvity", + "sergi": "Sergi", + "tecan": "Tecan", + "thermo_fisher": "Thermo Fisher Scientific", + "vwr": "VWR", +} + +SECTION_NAMES = { + "adapters": "Adapters", + "carrier": "Carriers", + "deck": "Decks", + "hamilton_decks": "Decks", + "mfx_carriers": "MFX Carriers", + "mfx_modules": "MFX Modules", + "nimbus_decks": "Decks", + "plate_adapters": "Plate Adapters", + "plate_carriers": "Plate Carriers", + "plates": "Plates", + "tip_carriers": "Tip Carriers", + "tip_racks": "Tip Racks", + "trash": "Trash", + "trough_carriers": "Trough Carriers", + "troughs": "Troughs", + "tube_carriers": "Tube Carriers", + "tube_racks": "Tube Racks", + "vantage_decks": "Decks", + "wash": "Wash Stations", +} + + +def _library_doc_paths(srcdir: str) -> List[Path]: + library_root = Path(srcdir) / LIBRARY_RELATIVE_ROOT + return sorted(path for path in library_root.rglob("*.md") if path.is_file()) + + +# Structural inline tags that pre-PR Sphinx/MyST rendered from these cells. +# Re-enabled here so the library doesn't regress files that used inline HTML. +_ALLOWED_TAGS = ("br", "p", "ul", "ol", "li", "b", "strong", "i", "em", "sub", "sup") +_SAFE_HREF = re.compile(r"(?i)^(https?:|mailto:)") + + +def _render_cell_html(text: str) -> str: + """Safe-by-construction: escape everything, then re-enable only a fixed + allowlist of attribute-less structural tags plus sanitised . Anything + else (scripts, on* handlers, unsafe schemes) stays escaped as inert text.""" + out = escape(text, quote=True) + + def _md_link(match: "re.Match") -> str: + label, href = match.group(1), match.group(2) + if not re.match(r"(?i)^(https?:|mailto:|/|#)", href): + return label + return f'{label}' + + out = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", _md_link, out) + + tags = "|".join(_ALLOWED_TAGS) + out = re.sub( + rf"<(/?)({tags})\s*/?>", + lambda m: f"<{m.group(1)}{m.group(2).lower()}>", + out, + flags=re.IGNORECASE, + ) + + def _anchor(match: "re.Match") -> str: + href = match.group(1) + if _SAFE_HREF.match(href): + return f'' + return "" + + out = re.sub(r"<a\s+href="(.*?)"\s*>", _anchor, out, flags=re.IGNORECASE) + out = re.sub(r"</a>", "", out, flags=re.IGNORECASE) + return out + + +def _description_to_html(description: str, definition_name: str) -> str: + parts = [ + part.strip() + for part in re.split(r"", description, flags=re.IGNORECASE) + if part.strip() + ] + if parts and parts[0].strip("'` ") == definition_name: + parts = parts[1:] + return "
".join(_render_cell_html(part) for part in parts) + + +def _page_title(markdown: str, fallback: str) -> str: + for line in markdown.splitlines(): + match = re.match(r"^#\s+(.+?)\s*$", line) + if match: + return match.group(1) + return fallback + + +def _company_url(markdown: str) -> Optional[str]: + # Manufacturer pages lead with a reference link whose label varies + # ("Company Page", "Company page:", "Wikipedia page:", ...), so take the + # first markdown link in the preamble (before the first ## heading). + preamble = re.split(r"^#{2,4}\s", markdown, maxsplit=1, flags=re.MULTILINE)[0] + match = re.search(r"\[[^\]]+\]\((https?://[^)]+)\)", preamble) + return match.group(1).strip() if match else None + + +def _blurb(markdown: str) -> Optional[str]: + # The "about" text is the first blockquote in the preamble. + preamble = re.split(r"^#{2,4}\s", markdown, maxsplit=1, flags=re.MULTILINE)[0] + lines: List[str] = [] + for line in preamble.splitlines(): + quoted = re.match(r"^>\s?(.*)$", line) + if quoted is not None: + lines.append(quoted.group(1).strip()) + elif lines: + break + text = " ".join(part for part in lines if part).strip() + return text or None + + +def _brand_tree(markdown: str) -> Optional[str]: + # Some manufacturer pages (e.g. Thermo Fisher) hand-curate the brand + # hierarchy as an ASCII tree in a fenced code block in the preamble. + preamble = re.split(r"^#{2,4}\s", markdown, maxsplit=1, flags=re.MULTILINE)[0] + match = re.search(r"```[^\n]*\n(.*?)\n```", preamble, flags=re.DOTALL) + return match.group(1).rstrip("\n") if match else None + + +def _image_path_from_cell(cell: str, doc_relative_path: Path) -> Optional[str]: + match = re.search(r"!\[[^\]]*]\(([^)]+)\)", cell) + if match is None: + return None + image_path = match.group(1).strip() + if image_path.startswith(("http://", "https://", "/")): + return image_path + resolved = doc_relative_path.parent / image_path + if "img" in resolved.parts: + img_index = resolved.parts.index("img") + return ("_static/" + "/".join(resolved.parts[img_index + 1:])).replace("//", "/") + return resolved.as_posix() + + +def _extract_labware_entries_from_markdown( + markdown: str, + doc_relative_path: Path, +) -> List[Dict[str, Any]]: + entries: List[Dict[str, Any]] = [] + manufacturer = _page_title(markdown, doc_relative_path.stem.replace("_", " ").title()) + heading_stack: List[Any] = [] # of (level, title), level in 2..4 + + for line in markdown.splitlines(): + stripped = line.strip() + heading_match = re.match(r"^(#{2,4})\s+(.+?)\s*$", stripped) + if heading_match: + level = len(heading_match.group(1)) + while heading_stack and heading_stack[-1][0] >= level: + heading_stack.pop() + heading_stack.append((level, heading_match.group(2))) + continue + + if not stripped.startswith("|"): + continue + + cells = [cell.strip() for cell in stripped.split("|")[1:-1]] + if len(cells) < 3: + continue + + matches = re.findall(r"`([A-Za-z][A-Za-z0-9_]*)`", cells[-1]) + if len(matches) == 0: + continue + + section_path = [title for _level, title in heading_stack] + current_section = section_path[-1] if section_path else "" + image_path = _image_path_from_cell(cells[1], doc_relative_path) + for definition_name in matches: + entries.append({ + "definition": definition_name, + "manufacturer": manufacturer, + "section": current_section, + "section_path": section_path, + "description_html": _description_to_html(cells[0], definition_name), + "image": image_path, + "page": doc_relative_path.with_suffix(".html").as_posix(), + }) + + return entries + + +def _markdown_library_entries(srcdir: str) -> Dict[str, Dict[str, Any]]: + entries: Dict[str, Dict[str, Any]] = {} + for doc_path in _library_doc_paths(srcdir): + doc_relative_path = doc_path.relative_to(srcdir) + for entry in _extract_labware_entries_from_markdown( + doc_path.read_text(encoding="utf-8"), + doc_relative_path, + ): + entries.setdefault(entry["definition"], entry) + + return entries + + +def _manufacturers_index(srcdir: str) -> Dict[str, Dict[str, Any]]: + manufacturers: Dict[str, Dict[str, Any]] = {} + for doc_path in _library_doc_paths(srcdir): + markdown = doc_path.read_text(encoding="utf-8") + fallback = doc_path.relative_to(srcdir).stem.replace("_", " ").title() + name = _page_title(markdown, fallback) + manufacturers.setdefault( + name, + { + "company_url": _company_url(markdown), + "blurb": _blurb(markdown), + "brand_tree": _brand_tree(markdown), + }, + ) + return manufacturers + + +def _resource_factory_registry() -> Dict[str, Callable[..., Any]]: + resources_module = importlib.import_module("pylabrobot.resources") + registry: Dict[str, Callable[..., Any]] = {} + + for name in dir(resources_module): + if name.startswith("_"): + continue + value = getattr(resources_module, name) + if inspect.isfunction(value): + registry[name] = value + + resources_root = Path(resources_module.__file__).resolve().parent + for module_path in resources_root.rglob("*.py"): + if module_path.name == "__init__.py" or module_path.name.endswith("_tests.py"): + continue + relative_path = module_path.relative_to(resources_root).with_suffix("") + module_name = "pylabrobot.resources." + ".".join(relative_path.parts) + try: + module = importlib.import_module(module_name) + except Exception: + continue + for name, value in vars(module).items(): + if name.startswith("_") or not inspect.isfunction(value): + continue + registry.setdefault(name, value) + + return registry + + +def _resolve_definition_callable( + definition_name: str, + registry: Dict[str, Callable[..., Any]], +) -> tuple[Optional[str], Optional[Callable[..., Any]]]: + candidate_name = RESOURCE_ALIASES.get(definition_name, definition_name) + if candidate_name in registry: + return candidate_name, registry[candidate_name] + + lowered = candidate_name.lower() + matches = [name for name in registry if name.lower() == lowered] + if len(matches) == 1: + match = matches[0] + return match, registry[match] + + return None, None + + +def _build_resource_definition( + definition_name: str, + registry: Dict[str, Callable[..., Any]], +) -> tuple[Optional[Resource], Optional[str]]: + resolved_name, definition = _resolve_definition_callable(definition_name, registry) + if definition is None or resolved_name is None: + return None, "definition callable could not be resolved" + + signature = inspect.signature(definition) + kwargs: Dict[str, Any] = {} + args: List[Any] = [] + + parameters = list(signature.parameters.values()) + if any(parameter.name == "name" for parameter in parameters): + kwargs["name"] = definition_name + if any(parameter.name == "modules" for parameter in parameters): + kwargs["modules"] = {} + elif len(parameters) > 0 and "name" not in signature.parameters: + first = parameters[0] + if first.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) and first.default is inspect.Parameter.empty: + args.append(definition_name) + + try: + resource = definition(*args, **kwargs) + except Exception as error: + return None, f"{error.__class__.__name__}: {error}" + + if not isinstance(resource, Resource): + return None, f"factory returned {resource.__class__.__name__}, not Resource" + + return resource, None + + +def _title_from_slug(slug: str) -> str: + return slug.replace("_", " ").replace("-", " ").title() + + +def _manufacturer_from_definition(definition: Callable[..., Any]) -> str: + parts = definition.__module__.split(".") + try: + resources_index = parts.index("resources") + except ValueError: + return "Other" + + if len(parts) <= resources_index + 1: + return "Other" + + slug = parts[resources_index + 1] + return MANUFACTURER_NAMES.get(slug, _title_from_slug(slug)) + + +def _section_from_definition( + definition: Callable[..., Any], + resource: Optional[Resource] = None, +) -> str: + module_slug = definition.__module__.split(".")[-1] + if module_slug in SECTION_NAMES: + return SECTION_NAMES[module_slug] + + if resource is not None: + category = getattr(resource, "category", None) + if isinstance(category, str) and category: + return _title_from_slug(category) + + return resource.__class__.__name__ + + return _title_from_slug(module_slug) + + +def _fallback_description_html( + definition: Callable[..., Any], + resource: Optional[Resource] = None, +) -> str: + if resource is None: + docstring = inspect.getdoc(definition) + if docstring: + first_paragraph = docstring.split("\n\n", maxsplit=1)[0].strip() + if first_paragraph: + return _render_cell_html(first_paragraph) + return "" + + details = [ + f"Type: {escape(resource.__class__.__name__)}", + ( + "Size: " + f"{resource.get_size_x():g} x {resource.get_size_y():g} x {resource.get_size_z():g} mm" + ), + ] + if resource.model is not None: + details.append(f"Model: {escape(resource.model)}") + return "
".join(details) + + +def _enrichment_for_definition( + definition_name: str, + markdown_entries: Dict[str, Dict[str, Any]], +) -> Optional[Dict[str, Any]]: + if definition_name in markdown_entries: + return markdown_entries[definition_name] + + aliases = [ + alias + for alias, target in RESOURCE_ALIASES.items() + if target == definition_name and alias in markdown_entries + ] + if len(aliases) > 0: + return markdown_entries[aliases[0]] + + return None + + +def _library_entry_from_resource( + definition_name: str, + definition: Callable[..., Any], + resource: Optional[Resource], + markdown_entries: Dict[str, Dict[str, Any]], +) -> Dict[str, Any]: + enrichment = _enrichment_for_definition(definition_name, markdown_entries) + if enrichment is not None: + entry = dict(enrichment) + entry["definition"] = definition_name + entry["section"] = entry.get("section") or _section_from_definition(definition, resource) + entry["section_path"] = entry.get("section_path") or [entry["section"]] + if not entry.get("description_html"): + entry["description_html"] = _fallback_description_html(definition, resource) + return entry + + section = _section_from_definition(definition, resource) + return { + "definition": definition_name, + "manufacturer": _manufacturer_from_definition(definition), + "section": section, + "section_path": [section], + "description_html": _fallback_description_html(definition, resource), + "image": None, + "page": "resources/library.html", + } + + +def _is_resource_factory(definition: Callable[..., Any]) -> bool: + if not _has_library_factory_signature(definition): + return False + + try: + hints = get_type_hints(definition) + except Exception: + hints = {} + + return_type = hints.get("return", inspect.signature(definition).return_annotation) + if return_type is inspect.Signature.empty: + return False + + if inspect.isclass(return_type): + try: + return issubclass(return_type, Resource) + except TypeError: + return False + + return_annotation = str(return_type) + return any( + resource_type in return_annotation + for resource_type in ( + "Resource", + "Plate", + "Lid", + "TipRack", + "TubeRack", + "Carrier", + "Trough", + "Trash", + "Deck", + "PlateAdapter", + "ResourceHolder", + "PetriDish", + "Tecan", + ) + ) + + +def _has_library_factory_signature(definition: Callable[..., Any]) -> bool: + try: + parameters = inspect.signature(definition).parameters.values() + except (TypeError, ValueError): + return False + + for parameter in parameters: + if parameter.kind in ( + inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD, + ): + continue + if parameter.default is not inspect.Parameter.empty: + continue + if parameter.name in {"name", "modules"}: + continue + return False + + return True + + +def build_labware_library_index(srcdir: str) -> Dict[str, Any]: + resources: Dict[str, Any] = {} + entries: List[Dict[str, Any]] = [] + failures: Dict[str, Dict[str, str]] = {} + markdown_entries = _markdown_library_entries(srcdir) + registry = _resource_factory_registry() + + for definition_name in sorted(registry): + definition = registry[definition_name] + if not _is_resource_factory(definition): + continue + + resource, instantiation_error = _build_resource_definition(definition_name, registry) + + entry = _library_entry_from_resource( + definition_name, + definition, + resource if isinstance(resource, Resource) else None, + markdown_entries, + ) + if isinstance(resource, Resource): + try: + resources[definition_name] = generate_geometry_library(resource) + except Exception as error: + failures[definition_name] = { + "stage": "geometry", + "reason": f"{error.__class__.__name__}: {error}", + } + elif instantiation_error is not None: + failures[definition_name] = { + "stage": "instantiation", + "reason": instantiation_error, + } + entry["has_geometry"] = definition_name in resources + entries.append(entry) + + entries.sort(key=lambda entry: ( + entry["manufacturer"].lower(), + entry["section"].lower(), + entry["definition"].lower(), + )) + + return { + "items": entries, + "resources": resources, + "manufacturers": _manufacturers_index(srcdir), + "diagnostics": { + "markdown_enriched_items": sum( + 1 + for entry in entries + if _enrichment_for_definition(entry["definition"], markdown_entries) is not None + ), + "items_without_geometry": len(entries) - len(resources), + "failures": failures, + }, + } + + +def _write_geometry_index(app) -> None: + if app.builder.format != "html": + return + + geometry_index = build_labware_library_index(app.srcdir) + target_dir = Path(app.outdir) / "_static" + target_dir.mkdir(parents=True, exist_ok=True) + target_path = target_dir / GEOMETRY_INDEX_FILENAME + target_path.write_text( + json.dumps(geometry_index, separators=(",", ":"), ensure_ascii=True), + encoding="utf-8", + ) + + +def _build_finished(app, exception: Optional[Exception]) -> None: + if exception is not None: + return + _write_geometry_index(app) + + +def setup(app): + app.connect("build-finished", _build_finished) + + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/_exts/pylabrobot_labware_library_tests.py b/docs/_exts/pylabrobot_labware_library_tests.py new file mode 100644 index 00000000000..a9f0055b56e --- /dev/null +++ b/docs/_exts/pylabrobot_labware_library_tests.py @@ -0,0 +1,48 @@ +import sys +import unittest +from pathlib import Path + +sys.path.append(str(Path(__file__).resolve().parent)) + +from pylabrobot_labware_library import ( + _markdown_library_entries, + build_labware_library_index, +) + + +class LabwareLibraryTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.index = build_labware_library_index("docs") + cls.items_by_definition = { + item["definition"]: item + for item in cls.index["items"] + } + + def test_library_includes_python_only_resource_definitions(self): + markdown_entries = _markdown_library_entries("docs") + + self.assertNotIn("Microplate_96_Well", markdown_entries) + self.assertIn("Microplate_96_Well", self.items_by_definition) + self.assertTrue(self.items_by_definition["Microplate_96_Well"]["has_geometry"]) + + def test_library_keeps_markdown_enrichment_when_available(self): + item = self.items_by_definition["hamilton_96_tiprack_1000uL_filter"] + + self.assertEqual(item["manufacturer"], "Hamilton") + self.assertEqual(item["section_path"], ["Consumables", "TipRacks"]) + self.assertIsNotNone(item["image"]) + self.assertTrue(item["has_geometry"]) + + def test_opentrons_factories_are_listed_when_download_fails(self): + self.assertIn("opentrons_96_tiprack_300ul", self.items_by_definition) + + if not self.items_by_definition["opentrons_96_tiprack_300ul"]["has_geometry"]: + self.assertIn( + "opentrons_96_tiprack_300ul", + self.index["diagnostics"]["failures"], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/_static/plr_geometry_viewer.js b/docs/_static/plr_geometry_viewer.js new file mode 100644 index 00000000000..cb953d8b806 --- /dev/null +++ b/docs/_static/plr_geometry_viewer.js @@ -0,0 +1,1316 @@ +(function () { + "use strict"; + + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + function niceStep(span, targetCount) { + const raw = Math.max(span, 1) / Math.max(targetCount, 1); + const magnitude = Math.pow(10, Math.floor(Math.log10(raw))); + const normalized = raw / magnitude; + let factor; + if (normalized <= 1) { + factor = 1; + } else if (normalized <= 2) { + factor = 2; + } else if (normalized <= 5) { + factor = 5; + } else { + factor = 10; + } + return factor * magnitude; + } + + const AXIS_COLORS = { x: "#e84a4a", y: "#4caf3e", z: "#3b7dd8" }; + + // Minimum on-screen spacing (canvas px) between drawn tick labels; closer + // ones are skipped so they don't pile up when an axis goes near edge-on. + const TICK_MIN_GAP = 30; + + function colorForPrototype(prototype) { + const geometry = prototype.geometry || {}; + const type = prototype.type || ""; + const category = prototype.category || ""; + + if (geometry.shape === "deck") return "#dbe7f1"; + if (type.includes("Carrier")) return "#9eb3c4"; + if (type.includes("Plate")) return "#4a5f73"; + if (type.includes("TipRack")) return "#8f6e34"; + if (type.includes("TubeRack")) return "#5f7c55"; + if (type.includes("Well")) return "#66b8c4"; + if (type.includes("TipSpot")) return "#d8c48a"; + if (category === "trash") return "#9f7c7c"; + return "#8ea3b6"; + } + + function hexToRgb(hex) { + const normalized = hex.replace("#", ""); + const value = parseInt(normalized, 16); + return { + r: (value >> 16) & 255, + g: (value >> 8) & 255, + b: value & 255, + }; + } + + function shadeColor(hex, factor) { + const rgb = hexToRgb(hex); + const scale = clamp(factor, 0.45, 1.2); + return `rgb(${Math.round(rgb.r * scale)}, ${Math.round(rgb.g * scale)}, ${Math.round(rgb.b * scale)})`; + } + + function readThemeColors(root) { + const probe = root || document.documentElement; + const style = getComputedStyle(probe); + const bg = style.getPropertyValue("--pst-color-background").trim() || "#f7fafc"; + const surface = style.getPropertyValue("--pst-color-surface").trim() || bg; + const text = style.getPropertyValue("--pst-color-text-base").trim() || "#17334a"; + return { bg, surface, text }; + } + + function multiplyMatrix(a, b) { + return [ + [ + a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0], + a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1], + a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2], + ], + [ + a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0], + a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1], + a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2], + ], + [ + a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0], + a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1], + a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2], + ], + ]; + } + + function rotationMatrix(rotation) { + const [rx, ry, rz] = (rotation || [0, 0, 0]).map((degrees) => (degrees * Math.PI) / 180); + + const cx = Math.cos(rx); + const sx = Math.sin(rx); + const cy = Math.cos(ry); + const sy = Math.sin(ry); + const cz = Math.cos(rz); + const sz = Math.sin(rz); + + const mx = [ + [1, 0, 0], + [0, cx, -sx], + [0, sx, cx], + ]; + const my = [ + [cy, 0, sy], + [0, 1, 0], + [-sy, 0, cy], + ]; + const mz = [ + [cz, -sz, 0], + [sz, cz, 0], + [0, 0, 1], + ]; + + return multiplyMatrix(mz, multiplyMatrix(my, mx)); + } + + function applyMatrix(point, matrix) { + return { + x: point.x * matrix[0][0] + point.y * matrix[0][1] + point.z * matrix[0][2], + y: point.x * matrix[1][0] + point.y * matrix[1][1] + point.z * matrix[1][2], + z: point.x * matrix[2][0] + point.y * matrix[2][1] + point.z * matrix[2][2], + }; + } + + function translatePoint(point, offset) { + return { + x: point.x + offset[0], + y: point.y + offset[1], + z: point.z + offset[2], + }; + } + + function createBoxGeometry(size) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + + const vertices = [ + { x: 0, y: 0, z: 0 }, + { x: sx, y: 0, z: 0 }, + { x: sx, y: sy, z: 0 }, + { x: 0, y: sy, z: 0 }, + { x: 0, y: 0, z: sz }, + { x: sx, y: 0, z: sz }, + { x: sx, y: sy, z: sz }, + { x: 0, y: sy, z: sz }, + ]; + + const faces = [ + [0, 1, 2, 3], + [4, 5, 6, 7], + [0, 1, 5, 4], + [1, 2, 6, 5], + [2, 3, 7, 6], + [3, 0, 4, 7], + ]; + + return { vertices, faces }; + } + + function createOffsetBoxGeometry(box) { + const geometry = createBoxGeometry([box.sx, box.sy, box.sz]); + geometry.vertices = geometry.vertices.map((vertex) => ({ + x: vertex.x + box.x, + y: vertex.y + box.y, + z: vertex.z + box.z, + })); + return geometry; + } + + function mergeGeometries(geometries) { + const merged = { vertices: [], faces: [] }; + geometries.forEach((geometry) => { + const offset = merged.vertices.length; + geometry.vertices.forEach((vertex) => merged.vertices.push(vertex)); + geometry.faces.forEach((face) => { + merged.faces.push(face.map((index) => index + offset)); + }); + }); + return merged; + } + + function createTrayGeometry(size) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + const wallThickness = clamp(Math.min(sx, sy) * 0.055, 2.2, 6.5); + const baseThickness = clamp(sz * 0.18, 1.8, Math.max(2.2, sz * 0.28)); + + return mergeGeometries([ + createOffsetBoxGeometry({ x: 0, y: 0, z: 0, sx, sy, sz: baseThickness }), + createOffsetBoxGeometry({ x: 0, y: 0, z: baseThickness, sx, sy: wallThickness, sz: sz - baseThickness }), + createOffsetBoxGeometry({ + x: 0, + y: sy - wallThickness, + z: baseThickness, + sx, + sy: wallThickness, + sz: sz - baseThickness, + }), + createOffsetBoxGeometry({ + x: 0, + y: wallThickness, + z: baseThickness, + sx: wallThickness, + sy: sy - wallThickness * 2, + sz: sz - baseThickness, + }), + createOffsetBoxGeometry({ + x: sx - wallThickness, + y: wallThickness, + z: baseThickness, + sx: wallThickness, + sy: sy - wallThickness * 2, + sz: sz - baseThickness, + }), + ]); + } + + function createCylinderGeometry(size, segments) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + const radiusX = sx / 2; + const radiusY = sy / 2; + const centerX = sx / 2; + const centerY = sy / 2; + const vertices = []; + const top = []; + const bottom = []; + const faces = []; + + for (let index = 0; index < segments; index += 1) { + const theta = (Math.PI * 2 * index) / segments; + const x = centerX + Math.cos(theta) * radiusX; + const y = centerY + Math.sin(theta) * radiusY; + bottom.push(vertices.length); + vertices.push({ x, y, z: 0 }); + top.push(vertices.length); + vertices.push({ x, y, z: sz }); + } + + faces.push(bottom.slice().reverse()); + faces.push(top.slice()); + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([ + bottom[index], + bottom[next], + top[next], + top[index], + ]); + } + + return { vertices, faces }; + } + + // V-bottom: straight upper cylinder + cone tapering to a centre apex. + function createVBottomGeometry(size, segments) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + const radiusX = sx / 2; + const radiusY = sy / 2; + const centerX = sx / 2; + const centerY = sy / 2; + const coneTopZ = sz * 0.2; // lower 20% of the depth is the V taper + const vertices = []; + const shoulder = []; + const top = []; + + for (let index = 0; index < segments; index += 1) { + const theta = (Math.PI * 2 * index) / segments; + const x = centerX + Math.cos(theta) * radiusX; + const y = centerY + Math.sin(theta) * radiusY; + shoulder.push(vertices.length); + vertices.push({ x, y, z: coneTopZ }); + top.push(vertices.length); + vertices.push({ x, y, z: sz }); + } + const apex = vertices.length; + vertices.push({ x: centerX, y: centerY, z: 0 }); + + const faces = [top.slice()]; + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([shoulder[index], shoulder[next], top[next], top[index]]); + faces.push([apex, shoulder[next], shoulder[index]]); + } + return { vertices, faces }; + } + + // U-bottom: straight upper cylinder + a rounded (quarter-ellipse) cap. + function createUBottomGeometry(size, segments, lat) { + const sx = size[0]; + const sy = size[1]; + const sz = size[2]; + const radiusX = sx / 2; + const radiusY = sy / 2; + const centerX = sx / 2; + const centerY = sy / 2; + const capH = Math.min((radiusX + radiusY) / 2, sz * 0.6); + const vertices = []; + const rings = []; + + for (let r = 1; r <= lat; r += 1) { + const tt = r / lat; // fraction up the cap, 1 == equator + const z = capH * tt; + const factor = Math.sin((tt * Math.PI) / 2); // 0 -> full radius + const ring = []; + for (let index = 0; index < segments; index += 1) { + const theta = (Math.PI * 2 * index) / segments; + ring.push(vertices.length); + vertices.push({ + x: centerX + Math.cos(theta) * radiusX * factor, + y: centerY + Math.sin(theta) * radiusY * factor, + z, + }); + } + rings.push(ring); + } + const top = []; + for (let index = 0; index < segments; index += 1) { + const theta = (Math.PI * 2 * index) / segments; + top.push(vertices.length); + vertices.push({ + x: centerX + Math.cos(theta) * radiusX, + y: centerY + Math.sin(theta) * radiusY, + z: sz, + }); + } + const apex = vertices.length; + vertices.push({ x: centerX, y: centerY, z: 0 }); + + const faces = [top.slice()]; + const first = rings[0]; + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([apex, first[next], first[index]]); + } + for (let r = 0; r < lat - 1; r += 1) { + const a = rings[r]; + const b = rings[r + 1]; + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([a[index], a[next], b[next], b[index]]); + } + } + const equator = rings[lat - 1]; + for (let index = 0; index < segments; index += 1) { + const next = (index + 1) % segments; + faces.push([equator[index], equator[next], top[next], top[index]]); + } + return { vertices, faces }; + } + + function createShapeGeometry(prototype) { + const geometry = prototype.geometry || {}; + const size = displaySizeForPrototype(prototype); + const type = prototype.type || ""; + + if (geometry.shape === "well" && geometry.cross_section === "circle") { + if (geometry.bottom === "V") { + return createVBottomGeometry(size, 18); + } + if (geometry.bottom === "U") { + return createUBottomGeometry(size, 18, 3); + } + return createCylinderGeometry(size, 18); + } + + if (geometry.shape === "tip_spot") { + return createCylinderGeometry(size, 12); + } + + if ( + type.includes("Plate") || + type.includes("TipRack") || + type.includes("TubeRack") + ) { + return createTrayGeometry(size); + } + + return createBoxGeometry(size); + } + + function displaySizeForPrototype(prototype) { + const size = (prototype.size || [10, 10, 10]).slice(); + if (size[2] > 0) { + return size; + } + + const type = prototype.type || ""; + const minPlanar = Math.max(1, Math.min(size[0], size[1])); + let fallbackHeight = Math.max(2, minPlanar * 0.1); + + if (type.includes("TipSpot")) { + fallbackHeight = Math.max(2, minPlanar * 0.3); + } else if (type.includes("Well")) { + fallbackHeight = Math.max(2, minPlanar * 0.35); + } else if (type.includes("Holder")) { + fallbackHeight = Math.max(3, minPlanar * 0.18); + } + + size[2] = fallbackHeight; + return size; + } + + function normalizeCatalog(catalog) { + if (!catalog || !catalog.prototypes || !catalog.instances) { + return []; + } + + const drawables = []; + Object.entries(catalog.instances).forEach(([name, instance]) => { + const prototype = catalog.prototypes[instance.prototype]; + if (!prototype || !instance.pose) { + return; + } + + const baseGeometry = createShapeGeometry(prototype); + const matrix = rotationMatrix(instance.rotation || [0, 0, 0]); + const translatedVertices = baseGeometry.vertices.map((vertex) => + translatePoint(applyMatrix(vertex, matrix), instance.pose), + ); + const outline = createOutlinePoints(prototype, matrix, instance.pose); + + drawables.push({ + name, + prototype, + color: colorForPrototype(prototype), + layer: drawableLayer(prototype), + alpha: drawableAlpha(prototype), + vertices: translatedVertices, + faces: baseGeometry.faces, + outline, + }); + }); + + return drawables; + } + + function createOutlinePoints(prototype, matrix, pose) { + const type = prototype.type || ""; + if ( + !type.includes("Plate") && + !type.includes("TipRack") && + !type.includes("TubeRack") + ) { + return null; + } + + const size = displaySizeForPrototype(prototype); + const z = size[2]; + const top = [ + { x: 0, y: 0, z }, + { x: size[0], y: 0, z }, + { x: size[0], y: size[1], z }, + { x: 0, y: size[1], z }, + ].map((corner) => translatePoint(applyMatrix(corner, matrix), pose)); + const bottom = [ + { x: 0, y: 0, z: 0 }, + { x: size[0], y: 0, z: 0 }, + { x: size[0], y: size[1], z: 0 }, + { x: 0, y: size[1], z: 0 }, + ].map((corner) => translatePoint(applyMatrix(corner, matrix), pose)); + + return { + top, + bottom, + verticals: [ + [bottom[0], top[0]], + [bottom[1], top[1]], + [bottom[2], top[2]], + [bottom[3], top[3]], + ], + }; + } + + function drawableLayer(prototype) { + const geometry = prototype.geometry || {}; + const type = prototype.type || ""; + + if (geometry.shape === "deck") return 0; + if (type.includes("Carrier") || type.includes("Holder")) return 1; + if (type.includes("Plate") || type.includes("TipRack") || type.includes("TubeRack")) return 2; + if (type.includes("Well") || type.includes("TipSpot")) return 4; + return 3; + } + + function drawableAlpha(prototype) { + const type = prototype.type || ""; + const geometry = prototype.geometry || {}; + + if (geometry.shape === "deck") return 0.38; + if (type.includes("Plate") || type.includes("TipRack") || type.includes("TubeRack")) return 0.84; + if (type.includes("Well") || type.includes("TipSpot")) return 0.68; + return 0.78; + } + + function computeBounds(drawables) { + const points = []; + drawables.forEach((drawable) => { + drawable.vertices.forEach((vertex) => points.push(vertex)); + }); + + if (points.length === 0) { + return { + min: { x: 0, y: 0, z: 0 }, + max: { x: 1, y: 1, z: 1 }, + center: { x: 0.5, y: 0.5, z: 0.5 }, + span: 1, + }; + } + + const min = { x: Infinity, y: Infinity, z: Infinity }; + const max = { x: -Infinity, y: -Infinity, z: -Infinity }; + points.forEach((point) => { + min.x = Math.min(min.x, point.x); + min.y = Math.min(min.y, point.y); + min.z = Math.min(min.z, point.z); + max.x = Math.max(max.x, point.x); + max.y = Math.max(max.y, point.y); + max.z = Math.max(max.z, point.z); + }); + + const center = { + x: (min.x + max.x) / 2, + y: (min.y + max.y) / 2, + z: (min.z + max.z) / 2, + }; + + const span = Math.max(max.x - min.x, max.y - min.y, max.z - min.z, 1); + return { min, max, center, span }; + } + + class CanvasCatalogViewer { + constructor(root) { + this.root = root; + this.canvas = document.createElement("canvas"); + this.canvas.className = "plr-geometry-canvas"; + this.root.innerHTML = ""; + this.root.appendChild(this.canvas); + this.context = this.canvas.getContext("2d"); + + this.homeButton = document.createElement("button"); + this.homeButton.type = "button"; + this.homeButton.className = "plr-geometry-home"; + this.homeButton.title = "Reset view"; + this.homeButton.setAttribute("aria-label", "Reset view"); + this.homeButton.innerHTML = + ''; + this.root.appendChild(this.homeButton); + this.drawables = []; + this.bounds = computeBounds([]); + this.defaultRotation = { yaw: -0.95 + Math.PI / 2, pitch: -0.9 }; + this.rotation = { ...this.defaultRotation }; + this.zoom = 1; + this.pan = { x: 0, y: 0 }; + this.pixelRatio = 1; + this._proj = null; // per-frame projection cache (see _updateProjection) + this._rafId = null; // pending requestAnimationFrame, for render coalescing + this.isDragging = false; + this.dragMode = "orbit"; + this.lastPointer = { x: 0, y: 0 }; + + this.handlePointerDown = this.handlePointerDown.bind(this); + this.handlePointerMove = this.handlePointerMove.bind(this); + this.handlePointerUp = this.handlePointerUp.bind(this); + this.handleWheel = this.handleWheel.bind(this); + this.handleDoubleClick = this.handleDoubleClick.bind(this); + this.handleHomeClick = this.handleHomeClick.bind(this); + this.handleResize = this.handleResize.bind(this); + this.resize = this.resize.bind(this); + + this.canvas.addEventListener("pointerdown", this.handlePointerDown); + window.addEventListener("pointermove", this.handlePointerMove); + window.addEventListener("pointerup", this.handlePointerUp); + this.canvas.addEventListener("wheel", this.handleWheel, { passive: false }); + this.canvas.addEventListener("dblclick", this.handleDoubleClick); + this.homeButton.addEventListener("click", this.handleHomeClick); + window.addEventListener("resize", this.handleResize); + if (typeof ResizeObserver !== "undefined") { + this.resizeObserver = new ResizeObserver(this.handleResize); + this.resizeObserver.observe(this.root); + } + + this.handleResize(); + } + + destroy() { + this.canvas.removeEventListener("pointerdown", this.handlePointerDown); + window.removeEventListener("pointermove", this.handlePointerMove); + window.removeEventListener("pointerup", this.handlePointerUp); + this.canvas.removeEventListener("wheel", this.handleWheel); + this.canvas.removeEventListener("dblclick", this.handleDoubleClick); + this.homeButton.removeEventListener("click", this.handleHomeClick); + window.removeEventListener("resize", this.handleResize); + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + if (this._rafId !== null) { + window.cancelAnimationFrame(this._rafId); + this._rafId = null; + } + } + + resetView() { + this.rotation = { ...this.defaultRotation }; + this.zoom = 1; + this.pan = { x: 0, y: 0 }; + this.render(); + } + + handleDoubleClick(event) { + event.preventDefault(); + this.resetView(); + } + + handleHomeClick() { + this.resetView(); + this.homeButton.classList.add("plr-geometry-home--active"); + window.setTimeout(() => { + this.homeButton.classList.remove("plr-geometry-home--active"); + }, 220); + } + + setCatalog(catalog) { + this.drawables = normalizeCatalog(catalog); + this.bounds = computeBounds(this.drawables); + this.zoom = 1; + this.pan = { x: 0, y: 0 }; + this.render(); + } + + handlePointerDown(event) { + if (event.button === 1) { + event.preventDefault(); // suppress middle-click autoscroll + } + this.dragMode = event.button === 1 || event.shiftKey ? "pan" : "orbit"; + this.isDragging = true; + this.lastPointer = { x: event.clientX, y: event.clientY }; + this.canvas.setPointerCapture(event.pointerId); + } + + handlePointerMove(event) { + if (!this.isDragging) { + return; + } + + const deltaX = event.clientX - this.lastPointer.x; + const deltaY = event.clientY - this.lastPointer.y; + this.lastPointer = { x: event.clientX, y: event.clientY }; + if (this.dragMode === "pan") { + // pointer deltas are CSS px; project() outputs device px. + this.pan.x += deltaX * this.pixelRatio; + this.pan.y += deltaY * this.pixelRatio; + } else { + this.rotation.yaw += deltaX * 0.01; + this.rotation.pitch = clamp( + this.rotation.pitch + deltaY * 0.01, + -Math.PI / 2, + Math.PI / 2, + ); + } + this.requestRender(); + } + + handlePointerUp(event) { + this.isDragging = false; + if (event.pointerId != null && this.canvas.hasPointerCapture(event.pointerId)) { + this.canvas.releasePointerCapture(event.pointerId); + } + } + + handleWheel(event) { + event.preventDefault(); + const zoomDelta = event.deltaY > 0 ? 0.92 : 1.08; + this.zoom = clamp(this.zoom * zoomDelta, 0.25, 6); + this.requestRender(); + } + + handleResize() { + const width = Math.max(this.root.clientWidth, 200); + const height = Math.max(this.root.clientHeight, 200); + this.pixelRatio = Math.min(window.devicePixelRatio || 1, 2); + this.canvas.width = Math.floor(width * this.pixelRatio); + this.canvas.height = Math.floor(height * this.pixelRatio); + this.canvas.style.width = `${width}px`; + this.canvas.style.height = `${height}px`; + this.requestRender(); + } + + resize() { + this.handleResize(); + } + + // Per-frame transform constants. The camera is fixed for a whole render, + // so the trig/scale are computed once here instead of per projected vertex. + _updateProjection() { + const c = this.bounds.center; + this._proj = { + cx: c.x, + cy: c.y, + cz: c.z, + cosYaw: Math.cos(this.rotation.yaw), + sinYaw: Math.sin(this.rotation.yaw), + cosPitch: Math.cos(this.rotation.pitch), + sinPitch: Math.sin(this.rotation.pitch), + scale: + (Math.min(this.canvas.width, this.canvas.height) / (this.bounds.span * 1.4)) * this.zoom, + halfW: this.canvas.width / 2, + halfH: this.canvas.height / 2, + panX: this.pan.x, + panY: this.pan.y, + }; + } + + project(point) { + const p = this._proj || (this._updateProjection(), this._proj); + const cx = point.x - p.cx; + const cy = point.y - p.cy; + const cz = point.z - p.cz; + const yawedX = cx * p.cosYaw - cy * p.sinYaw; + const yawedY = cx * p.sinYaw + cy * p.cosYaw; + const pitchedY = yawedY * p.cosPitch - cz * p.sinPitch; + const pitchedZ = yawedY * p.sinPitch + cz * p.cosPitch; + return { + x: p.halfW + yawedX * p.scale + p.panX, + y: p.halfH - pitchedY * p.scale + p.panY, + depth: pitchedZ, + }; + } + + rulerUiScale() { + // Keep rulers a bit larger at baseline and grow with zoom so labels and + // axis strokes stay legible while inspecting details. + return clamp(1.1 + (this.zoom - 1) * 0.28, 1.0, 2.0); + } + + drawBackground() { + const ctx = this.context; + const theme = readThemeColors(this.root); + const gradient = ctx.createLinearGradient(0, 0, 0, this.canvas.height); + gradient.addColorStop(0, theme.bg); + gradient.addColorStop(1, theme.surface); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + drawRuler() { + const ctx = this.context; + const theme = readThemeColors(this.root); + const groundZ = this.bounds.min.z; + + const spanX = this.bounds.max.x - this.bounds.min.x; + const spanY = this.bounds.max.y - this.bounds.min.y; + const step = Math.max(niceStep(spanX, 8), niceStep(spanY, 8)); + + const x0 = Math.floor(this.bounds.min.x / step) * step; + const x1 = Math.ceil(this.bounds.max.x / step) * step; + const y0 = Math.floor(this.bounds.min.y / step) * step; + const y1 = Math.ceil(this.bounds.max.y / step) * step; + const epsilon = step * 1e-6; + const labelGap = step * 0.2; + const uiScale = this.rulerUiScale(); + const tickMinGap = TICK_MIN_GAP * uiScale; + + ctx.save(); + ctx.strokeStyle = theme.text; + + // Minor mm grid. + ctx.lineWidth = Math.max(1, 0.9 * uiScale); + ctx.globalAlpha = 0.18; + for (let x = x0; x <= x1 + epsilon; x += step) { + const start = this.project({ x, y: y0, z: groundZ }); + const end = this.project({ x, y: y1, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + } + for (let y = y0; y <= y1 + epsilon; y += step) { + const start = this.project({ x: x0, y, z: groundZ }); + const end = this.project({ x: x1, y, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + } + + // Origin axes (Blender-style colors: X red, Y green). + ctx.globalAlpha = 0.9; + ctx.lineWidth = 3 * uiScale; + const xAxisStart = this.project({ x: x0, y: y0, z: groundZ }); + const xAxisEnd = this.project({ x: x1, y: y0, z: groundZ }); + ctx.strokeStyle = AXIS_COLORS.x; + ctx.beginPath(); + ctx.moveTo(xAxisStart.x, xAxisStart.y); + ctx.lineTo(xAxisEnd.x, xAxisEnd.y); + ctx.stroke(); + const yAxisEnd = this.project({ x: x0, y: y1, z: groundZ }); + ctx.strokeStyle = AXIS_COLORS.y; + ctx.beginPath(); + ctx.moveTo(xAxisStart.x, xAxisStart.y); + ctx.lineTo(yAxisEnd.x, yAxisEnd.y); + ctx.stroke(); + + // Tick marks: short stubs pointing outward toward each axis's labels. + ctx.lineWidth = 2 * uiScale; + const tickLen = step * 0.12; + ctx.strokeStyle = AXIS_COLORS.x; + for (let x = x0; x <= x1 + epsilon; x += step) { + const tickStart = this.project({ x, y: y0, z: groundZ }); + const tickEnd = this.project({ x, y: y0 - tickLen, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(tickStart.x, tickStart.y); + ctx.lineTo(tickEnd.x, tickEnd.y); + ctx.stroke(); + } + ctx.strokeStyle = AXIS_COLORS.y; + for (let y = y0; y <= y1 + epsilon; y += step) { + const tickStart = this.project({ x: x0, y, z: groundZ }); + const tickEnd = this.project({ x: x0 - tickLen, y, z: groundZ }); + ctx.beginPath(); + ctx.moveTo(tickStart.x, tickStart.y); + ctx.lineTo(tickEnd.x, tickEnd.y); + ctx.stroke(); + } + + // Tick labels in mm (matched to axis colors). + ctx.globalAlpha = 0.9; + ctx.font = `bold ${Math.round(12 * uiScale)}px system-ui, -apple-system, sans-serif`; + ctx.fillStyle = AXIS_COLORS.x; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + let lastXLabel = null; + for (let x = x0; x <= x1 + epsilon; x += step) { + const point = this.project({ x, y: y0 - labelGap, z: groundZ }); + if ( + lastXLabel !== null && + Math.hypot(point.x - lastXLabel.x, point.y - lastXLabel.y) < tickMinGap + ) { + continue; + } + lastXLabel = point; + ctx.fillText(String(Math.round(x)), point.x, point.y + 5 * uiScale); + } + ctx.fillStyle = AXIS_COLORS.y; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + let lastYLabel = null; + for (let y = y0; y <= y1 + epsilon; y += step) { + const point = this.project({ x: x0 - labelGap, y, z: groundZ - step * 0.2 }); + if ( + lastYLabel !== null && + Math.hypot(point.x - lastYLabel.x, point.y - lastYLabel.y) < tickMinGap + ) { + continue; + } + lastYLabel = point; + ctx.fillText(String(Math.round(y)), point.x - 7 * uiScale, point.y); + } + + // Axis unit captions. + ctx.globalAlpha = 1; + ctx.font = `bold ${Math.round(11 * uiScale)}px system-ui, -apple-system, sans-serif`; + const xCaption = this.project({ x: x1, y: y0, z: groundZ }); + ctx.fillStyle = AXIS_COLORS.x; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillText("X (mm)", xCaption.x + 25 * uiScale, xCaption.y); + const yCaption = this.project({ x: x0, y: y1, z: groundZ }); + ctx.fillStyle = AXIS_COLORS.y; + ctx.textAlign = "right"; + ctx.textBaseline = "bottom"; + ctx.fillText("Y (mm)", yCaption.x - 7 * uiScale, yCaption.y - 22 * uiScale); + + ctx.restore(); + } + + drawZAxis() { + const z0 = this.bounds.min.z; + const z1 = this.bounds.max.z; + if (z1 - z0 < 1e-6) { + return; + } + + const step = niceStep(z1 - z0, 4); + const zStart = Math.floor(z0 / step) * step; + const zEnd = Math.ceil(z1 / step) * step; + const epsilon = step * 1e-6; + + // Share the origin corner with the X/Y axes (matplotlib/Blender convention) + // so all three meet at one point and Z rides the front silhouette edge + // rather than piercing the translucent body. + const gridStep = Math.max( + niceStep(this.bounds.max.x - this.bounds.min.x, 8), + niceStep(this.bounds.max.y - this.bounds.min.y, 8), + ); + const corner = { + x: Math.floor(this.bounds.min.x / gridStep) * gridStep, + y: Math.floor(this.bounds.min.y / gridStep) * gridStep, + }; + + const centerScreen = this.project({ + x: this.bounds.center.x, + y: this.bounds.center.y, + z: z0, + }); + const baseScreen = this.project({ x: corner.x, y: corner.y, z: z0 }); + const outwardSign = baseScreen.x >= centerScreen.x ? 1 : -1; + const uiScale = this.rulerUiScale(); + const tickMinGap = TICK_MIN_GAP * uiScale; + + const ctx = this.context; + ctx.save(); + ctx.strokeStyle = AXIS_COLORS.z; + ctx.fillStyle = AXIS_COLORS.z; + + ctx.globalAlpha = 0.9; + ctx.lineWidth = 3 * uiScale; + const axisBottom = this.project({ x: corner.x, y: corner.y, z: zStart }); + const axisTop = this.project({ x: corner.x, y: corner.y, z: zEnd }); + ctx.beginPath(); + ctx.moveTo(axisBottom.x, axisBottom.y); + ctx.lineTo(axisTop.x, axisTop.y); + ctx.stroke(); + + ctx.font = `bold ${Math.round(12 * uiScale)}px system-ui, -apple-system, sans-serif`; + ctx.textAlign = outwardSign > 0 ? "left" : "right"; + ctx.textBaseline = "middle"; + let lastZLabel = null; + for (let z = zStart; z <= zEnd + epsilon; z += step) { + const point = this.project({ x: corner.x, y: corner.y, z }); + if ( + lastZLabel !== null && + Math.hypot(point.x - lastZLabel.x, point.y - lastZLabel.y) < tickMinGap + ) { + continue; + } + lastZLabel = point; + ctx.beginPath(); + ctx.moveTo(point.x, point.y); + ctx.lineTo(point.x + outwardSign * 6 * uiScale, point.y); + ctx.stroke(); + ctx.fillText(String(Math.round(z)), point.x + outwardSign * 10 * uiScale, point.y); + } + + ctx.globalAlpha = 1; + ctx.font = `bold ${Math.round(11 * uiScale)}px system-ui, -apple-system, sans-serif`; + ctx.textBaseline = "bottom"; + ctx.fillText("Z (mm)", axisTop.x + outwardSign * 10 * uiScale, axisTop.y - 8 * uiScale); + + ctx.restore(); + } + + drawDrawables() { + const items = []; + // Outline segments share this layer so they depth-interleave with the + // wells/tip-spots (Well/TipSpot get layer 4 in drawableLayer()) instead + // of being painted on top as a flat overlay. + const outlineLayer = 4; + + this.drawables.forEach((drawable) => { + drawable.faces.forEach((face) => { + const projected = face.map((vertexIndex) => this.project(drawable.vertices[vertexIndex])); + const depth = + projected.reduce((sum, point) => sum + point.depth, 0) / Math.max(projected.length, 1); + items.push({ + kind: "poly", + points: projected, + depth, + color: drawable.color, + layer: drawable.layer, + alpha: drawable.alpha, + }); + }); + + if (drawable.outline) { + const color = shadeColor(drawable.color, 0.42); + // Subdivide each edge so it depth-sorts against the well field in + // many short pieces (one average depth per whole edge can't be + // partly-behind / partly-in-front of 96 wells). Still an + // approximation -- exact occlusion would need a depth buffer. + const SUBDIV = 18; + const addSegment = (a, b, width) => { + let prev = this.project(a); + for (let k = 1; k <= SUBDIV; k += 1) { + const t = k / SUBDIV; + const next = this.project({ + x: a.x + (b.x - a.x) * t, + y: a.y + (b.y - a.y) * t, + z: a.z + (b.z - a.z) * t, + }); + items.push({ + kind: "stroke", + a: prev, + b: next, + depth: (prev.depth + next.depth) / 2, + color, + width, + layer: outlineLayer, + }); + prev = next; + } + }; + const ring = (pts, width) => { + for (let i = 0; i < pts.length; i += 1) { + addSegment(pts[i], pts[(i + 1) % pts.length], width); + } + }; + ring(drawable.outline.top, 3); + ring(drawable.outline.bottom, 1.5); + drawable.outline.verticals.forEach((pair) => addSegment(pair[0], pair[1], 1.5)); + } + }); + + items.sort((left, right) => { + if (left.layer !== right.layer) { + return left.layer - right.layer; + } + return left.depth - right.depth; + }); + + const ctx = this.context; + const edgeColor = readThemeColors(this.root).text; + items.forEach((item) => { + if (item.kind === "stroke") { + ctx.globalAlpha = 1; + ctx.strokeStyle = item.color; + ctx.lineWidth = item.width; + ctx.beginPath(); + ctx.moveTo(item.a.x, item.a.y); + ctx.lineTo(item.b.x, item.b.y); + ctx.stroke(); + return; + } + + const shade = + item.points.length >= 4 + ? shadeColor(item.color, 0.9 + (item.depth / (this.bounds.span || 1)) * 0.15) + : item.color; + + ctx.beginPath(); + ctx.moveTo(item.points[0].x, item.points[0].y); + for (let index = 1; index < item.points.length; index += 1) { + ctx.lineTo(item.points[index].x, item.points[index].y); + } + ctx.closePath(); + ctx.fillStyle = shade; + ctx.globalAlpha = item.alpha; + ctx.fill(); + ctx.globalAlpha = 0.28; + ctx.strokeStyle = edgeColor; + ctx.lineWidth = 1; + ctx.stroke(); + ctx.globalAlpha = 1; + }); + } + + requestRender() { + if (this._rafId !== null) { + return; + } + this._rafId = window.requestAnimationFrame(() => { + this._rafId = null; + this.render(); + }); + } + + render() { + if (!this.context) { + return; + } + + this._updateProjection(); + this.drawBackground(); + this.drawRuler(); + this.drawDrawables(); + this.drawZAxis(); + this.drawSizeReadout(); + this.drawOrientationCube(); + } + + drawSizeReadout() { + const ctx = this.context; + const sizeX = this.bounds.max.x - this.bounds.min.x; + const sizeY = this.bounds.max.y - this.bounds.min.y; + const sizeZ = this.bounds.max.z - this.bounds.min.z; + if (sizeX <= 0 && sizeY <= 0 && sizeZ <= 0) { + return; + } + + const fmt = (value) => (Math.round(value * 100) / 100).toFixed(2); + const lines = [`${fmt(sizeX)} × ${fmt(sizeY)} × ${fmt(sizeZ)} mm`]; + + const wells = this.drawables.filter( + (d) => d.prototype.geometry && d.prototype.geometry.shape === "well", + ).length; + const tips = this.drawables.filter( + (d) => d.prototype.geometry && d.prototype.geometry.shape === "tip_spot", + ).length; + if (wells > 0) { + lines.push(`${wells} well${wells === 1 ? "" : "s"}`); + } else if (tips > 0) { + lines.push(`${tips} Tip Spot${tips === 1 ? "" : "s"}`); + } + + ctx.save(); + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + let y = 12; + lines.forEach((line, index) => { + ctx.font = index === 0 + ? "bold 13px system-ui, -apple-system, sans-serif" + : "12px system-ui, -apple-system, sans-serif"; + ctx.fillStyle = readThemeColors(this.root).text; + ctx.globalAlpha = index === 0 ? 0.95 : 0.7; + ctx.fillText(line, 14, y); + y += index === 0 ? 19 : 16; + }); + ctx.restore(); + } + + drawOrientationCube() { + const ctx = this.context; + const dpr = this.pixelRatio; + const halfCss = 31.5; // half widget size in CSS px + const marginCss = 16; + const topCss = 14; // top-right corner, right of the home button + const cx = this.canvas.width - (marginCss + halfCss) * dpr; + const cy = (topCss + halfCss) * dpr; + const s = halfCss * dpr * 0.6; // projected half-extent of the cube + + const cosYaw = Math.cos(this.rotation.yaw); + const sinYaw = Math.sin(this.rotation.yaw); + const cosPitch = Math.cos(this.rotation.pitch); + const sinPitch = Math.sin(this.rotation.pitch); + + const projectCubePoint = (p) => { + const yx = p.x * cosYaw - p.y * sinYaw; + const yy = p.x * sinYaw + p.y * cosYaw; + const py = yy * cosPitch - p.z * sinPitch; + const pz = yy * sinPitch + p.z * cosPitch; + return { x: cx + yx * s, y: cy - py * s, depth: pz }; + }; + + const add = (a, b, t) => ({ + x: a.x + b.x * t, + y: a.y + b.y * t, + z: a.z + b.z * t, + }); + const depthAt = (p) => projectCubePoint(p).depth; + + // Chamfered cube: each corner cut by a plane -> 6 octagon faces + 8 + // corner triangles. `chamfer` is the fraction of each edge removed. + const chamfer = 0.504; + const k = 1 - chamfer; + const ring = [ + [1, k], [k, 1], [-k, 1], [-1, k], + [-1, -k], [-k, -1], [k, -1], [1, -k], + ]; + // (b, d) in-plane coords -> 3D, per axis (0=x,1=y,2=z), at axis=sg. + const place = { + 0: (sg, b, d) => ({ x: sg, y: b, z: d }), + 1: (sg, b, d) => ({ x: d, y: sg, z: b }), + 2: (sg, b, d) => ({ x: b, y: d, z: sg }), + }; + const inPlaneU = { 0: { x: 0, y: 1, z: 0 }, 1: { x: 1, y: 0, z: 0 }, 2: { x: 1, y: 0, z: 0 } }; + const inPlaneW = { 0: { x: 0, y: 0, z: 1 }, 1: { x: 0, y: 0, z: 1 }, 2: { x: 0, y: 1, z: 0 } }; + const axisNormal = (axis, sg) => ({ + x: axis === 0 ? sg : 0, + y: axis === 1 ? sg : 0, + z: axis === 2 ? sg : 0, + }); + + const faces = []; + + [ + // Faces colored by the axis their long edge spans: FRONT/BACK run + // along X (red), LEFT/RIGHT along Y (green), TOP/BOTTOM up Z (blue). + { axis: 2, sg: 1, label: "TOP", color: AXIS_COLORS.z }, + { axis: 2, sg: -1, label: "BOTTOM", color: AXIS_COLORS.z }, + { axis: 0, sg: 1, label: "RIGHT", color: AXIS_COLORS.y }, + { axis: 0, sg: -1, label: "LEFT", color: AXIS_COLORS.y }, + { axis: 1, sg: 1, label: "BACK", color: AXIS_COLORS.x }, + { axis: 1, sg: -1, label: "FRONT", color: AXIS_COLORS.x }, + ].forEach((f) => { + const pts3 = ring.map(([b, d]) => place[f.axis](f.sg, b, d)); + const center3 = axisNormal(f.axis, f.sg); + const normal = center3; + const visible = depthAt(add(center3, normal, 0.02)) > depthAt(center3); + const pts = pts3.map(projectCubePoint); + const depth = pts.reduce((sum, p) => sum + p.depth, 0) / pts.length; + faces.push({ + kind: "oct", + label: f.label, + color: f.color, + pts, + depth, + visible, + center3, + uW: inPlaneU[f.axis], + wW: inPlaneW[f.axis], + }); + }); + + [-1, 1].forEach((sx) => { + [-1, 1].forEach((sy) => { + [-1, 1].forEach((sz) => { + const pts3 = [ + { x: sx * k, y: sy, z: sz }, + { x: sx, y: sy * k, z: sz }, + { x: sx, y: sy, z: sz * k }, + ]; + const centroid = { + x: (pts3[0].x + pts3[1].x + pts3[2].x) / 3, + y: (pts3[0].y + pts3[1].y + pts3[2].y) / 3, + z: (pts3[0].z + pts3[1].z + pts3[2].z) / 3, + }; + const inv = 1 / Math.sqrt(3); + const normal = { x: sx * inv, y: sy * inv, z: sz * inv }; + const visible = depthAt(add(centroid, normal, 0.02)) > depthAt(centroid); + const pts = pts3.map(projectCubePoint); + const depth = pts.reduce((sum, p) => sum + p.depth, 0) / 3; + faces.push({ kind: "chamfer", pts, depth, visible }); + }); + }); + }); + + faces.sort((a, b) => a.depth - b.depth); + + const theme = readThemeColors(this.root); + ctx.save(); + ctx.lineJoin = "round"; + faces.forEach((f) => { + ctx.beginPath(); + ctx.moveTo(f.pts[0].x, f.pts[0].y); + for (let i = 1; i < f.pts.length; i += 1) { + ctx.lineTo(f.pts[i].x, f.pts[i].y); + } + ctx.closePath(); + + if (f.kind === "chamfer") { + ctx.globalAlpha = f.visible ? 0.5 : 0.12; + ctx.fillStyle = "rgba(150, 165, 180, 0.9)"; + ctx.fill(); + ctx.globalAlpha = f.visible ? 0.45 : 0.12; + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(120, 135, 150, 0.9)"; + ctx.stroke(); + return; + } + + ctx.globalAlpha = f.visible ? 0.82 : 0.18; + ctx.fillStyle = "rgba(255, 255, 255, 0.85)"; + ctx.fill(); + ctx.globalAlpha = f.visible ? 0.9 : 0.25; + ctx.lineWidth = Math.max(1, dpr); + ctx.strokeStyle = f.color; + ctx.stroke(); + + if (!f.visible) { + return; + } + const fc = projectCubePoint(f.center3); + const pU = projectCubePoint(add(f.center3, f.uW, 1)); + const pW = projectCubePoint(add(f.center3, f.wW, 1)); + let ux = pU.x - fc.x; + let uy = pU.y - fc.y; + let vx = pW.x - fc.x; + let vy = pW.y - fc.y; + const exLen = Math.hypot(ux, uy); + const eyLen = Math.hypot(vx, vy); + if (exLen > 4 && eyLen > 4) { + // Unit in-plane basis keeps the face's shear/foreshorten so the + // label reads as painted on the surface; sign-normalize so it + // stays roughly upright rather than mirrored/upside-down. + ux /= exLen; + uy /= exLen; + vx /= eyLen; + vy /= eyLen; + if (ux < 0) { + ux = -ux; + uy = -uy; + } + if (vy < 0) { + vx = -vx; + vy = -vy; + } + const fontPx = Math.min(exLen, eyLen) * 0.486; + ctx.save(); + ctx.setTransform(ux, uy, vx, vy, fc.x, fc.y); + ctx.globalAlpha = 0.95; + ctx.fillStyle = theme.text; + ctx.font = `bold ${fontPx}px system-ui, -apple-system, sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(f.label, 0, 0); + ctx.restore(); + } + }); + ctx.restore(); + } + } + + window.PLRGeometryViewer = { + CanvasCatalogViewer, + }; +})(); diff --git a/docs/_static/plr_labware_library.css b/docs/_static/plr_labware_library.css new file mode 100644 index 00000000000..b4be13215d1 --- /dev/null +++ b/docs/_static/plr_labware_library.css @@ -0,0 +1,410 @@ +.plr-library-toolbar { + display: grid; + grid-template-columns: minmax(240px, 1fr) minmax(160px, 220px) minmax(160px, 220px) auto; + gap: 0.75rem; + align-items: end; + margin: 1rem 0 1.25rem; + padding: 0.85rem; + border: 1px solid var(--pst-color-border); + border-radius: 8px; + background: var(--pst-color-surface); +} + +.plr-library-search, +.plr-library-filter { + display: grid; + gap: 0.35rem; +} + +.plr-library-search label, +.plr-library-filter label { + color: var(--pst-color-text-muted); + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; +} + +.plr-library-search input, +.plr-library-filter select { + width: 100%; + min-height: 2.4rem; + border: 1px solid var(--pst-color-border); + border-radius: 6px; + background: var(--pst-color-on-background); + color: var(--pst-color-text-base); + font: inherit; + padding: 0.45rem 0.65rem; +} + +.plr-library-count { + color: var(--pst-color-text-muted); + font-size: 0.9rem; + white-space: nowrap; +} + +.plr-library-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(285px, 1fr)); + margin: 1rem 0 2rem; +} + +.plr-library-group + .plr-library-group { + margin-top: 1rem; +} + +.plr-library-group__title { + margin: 0.5rem 0 0.25rem; + color: var(--pst-color-text-base); + font-size: 1.1rem; +} + +.plr-library-loading, +.plr-library-empty { + padding: 1rem; + border: 1px solid var(--pst-color-border); + border-radius: 8px; + background: var(--pst-color-surface); + color: var(--pst-color-text-muted); +} + +.plr-library-card { + display: grid; + grid-template-rows: 170px 1fr; + overflow: hidden; + border: 1px solid var(--pst-color-border); + border-radius: 8px; + background: var(--pst-color-on-background); + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.08); +} + +.plr-library-card__media { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + border: 0; + border-bottom: 1px solid var(--pst-color-border); + padding: 0.75rem; + background: #ffffff; + cursor: pointer; +} + +.plr-library-card__media:hover { + filter: brightness(0.97); +} + +.plr-library-card__media img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + mix-blend-mode: multiply; +} + +.plr-library-card__placeholder { + color: #6b7f91; + font-size: 0.95rem; +} + +.plr-library-card__body { + display: flex; + flex-direction: column; + gap: 0.55rem; + padding: 0.95rem; +} + +.plr-library-card__manufacturer { + color: var(--pst-color-text-muted); + font-size: 0.76rem; + font-weight: 700; + text-transform: uppercase; +} + +.plr-library-card__title { + margin: 0; + color: var(--pst-color-text-base); + font-size: 0.98rem; + line-height: 1.3; + overflow-wrap: anywhere; +} + +.plr-library-card__section { + width: fit-content; + max-width: 100%; + padding: 0.22rem 0.45rem; + border-radius: 6px; + background: var(--pst-color-surface); + color: var(--pst-color-text-base); + font-size: 0.78rem; + overflow-wrap: anywhere; +} + +.plr-library-card__description { + flex: 1; + color: var(--pst-color-text-base); + font-size: 0.86rem; + line-height: 1.45; + max-height: 10rem; + overflow-y: auto; +} + +.plr-library-card__description ul, +.plr-library-card__description ol { + margin: 0.25rem 0; + padding-left: 1.1rem; +} + +.plr-library-card__description li { + margin: 0.05rem 0; +} + +.plr-library-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-top: auto; + padding-top: 0.2rem; +} + +.plr-library-card__source { + color: var(--pst-color-link); + font-size: 0.86rem; +} + +.plr-library-card__action { + border: 0; + border-radius: 6px; + padding: 0.45rem 0.675rem; + font-size: 0.9em; + background: var(--pst-color-primary); + color: var(--pst-color-background); + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} + +.plr-library-card__action:hover { + filter: brightness(0.92); +} + +.plr-library-card__action:disabled { + background: var(--pst-color-text-muted); + cursor: not-allowed; + opacity: 0.6; +} + +.plr-library-modal[hidden] { + display: none; +} + +.plr-library-modal { + position: fixed; + inset: 0; + z-index: 1200; +} + +.plr-library-modal__backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.58); +} + +.plr-library-modal__dialog { + position: relative; + display: grid; + grid-template-rows: auto 1fr; + width: min(1080px, calc(100vw - 2rem)); + height: min(760px, calc(100vh - 2rem)); + margin: 1rem auto; + overflow: hidden; + border-radius: 8px; + background: var(--pst-color-background); + box-shadow: 0 32px 80px rgba(0, 0, 0, 0.4); +} + +.plr-library-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.9rem 1.1rem; + border-bottom: 1px solid var(--pst-color-border); + background: var(--pst-color-on-background); +} + +.plr-library-modal__eyebrow { + margin: 0 0 0.2rem; + color: var(--pst-color-text-muted); + font-size: 0.76rem; + font-weight: 700; + text-transform: uppercase; +} + +.plr-library-modal__title { + margin: 0; + color: var(--pst-color-text-base); + font-size: 1.05rem; + overflow-wrap: anywhere; +} + +.plr-library-modal__close { + border: 0; + border-radius: 6px; + padding: 0.55rem 0.8rem; + background: var(--pst-color-primary); + color: var(--pst-color-background); + cursor: pointer; +} + +.plr-library-modal__stage { + min-height: 420px; + position: relative; +} + +.plr-geometry-home { + position: absolute; + top: 14px; + right: 90px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + border: 0; + border-radius: 6px; + cursor: pointer; + color: var(--pst-color-text-base); + opacity: 0.55; + background: rgba(255, 255, 255, 0.75); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + line-height: 0; + transition: opacity 0.15s, transform 0.15s, background 0.15s, box-shadow 0.15s; + z-index: 5; +} + +.plr-geometry-home:hover { + opacity: 1; + transform: scale(1.12); + background: rgba(0, 255, 255, 0.4); + box-shadow: 0 0 10px rgba(0, 255, 255, 0.8); +} + +.plr-geometry-home--active { + opacity: 1; + background: rgba(255, 230, 0, 0.65); + box-shadow: 0 0 12px rgba(255, 230, 0, 0.9); +} + +.plr-geometry-canvas { + display: block; + width: 100%; + height: 100%; +} + +.plr-library-modal-open { + overflow: hidden; +} + +@media (max-width: 900px) { + .plr-library-toolbar { + grid-template-columns: 1fr; + } + + .plr-library-count { + white-space: normal; + } + + .plr-library-modal__dialog { + width: calc(100vw - 1rem); + height: calc(100vh - 1rem); + margin: 0.5rem auto; + } + + .plr-library-card { + grid-template-rows: 145px 1fr; + } +} + +.plr-library-manufacturer { + margin-bottom: 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--pst-color-border, rgba(128, 128, 128, 0.3)); + border-radius: 8px; + background: var(--pst-color-surface, rgba(128, 128, 128, 0.05)); +} + +.plr-library-manufacturer > summary { + cursor: pointer; + font-weight: 600; + display: flex; + align-items: baseline; + gap: 0.6rem; + list-style: none; + user-select: none; +} + +.plr-library-manufacturer > summary::-webkit-details-marker { + display: none; +} + +.plr-library-manufacturer > summary::before { + content: "▸"; + display: inline-block; + margin-right: 0.1rem; + opacity: 0.65; + transition: transform 0.15s ease; +} + +.plr-library-manufacturer[open] > summary::before { + transform: rotate(90deg); +} + +.plr-library-manufacturer > summary:hover { + color: var(--pst-color-primary, inherit); +} + +.plr-library-manufacturer__name { + font-size: 1.05rem; +} + +.plr-library-manufacturer__meta { + font-size: 0.85rem; + opacity: 0.7; +} + +.plr-library-manufacturer__company { + display: inline-block; + margin: 0.5rem 0 0.25rem; + font-size: 0.9rem; +} + +.plr-library-manufacturer__subhead { + margin: 0.75rem 0 0.25rem; + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + opacity: 0.6; +} + +.plr-library-manufacturer__blurb { + margin: 0.5rem 0 0; + font-size: 0.9rem; + line-height: 1.55; + opacity: 0.85; +} + +.plr-library-manufacturer__tree { + margin: 0; + padding: 0.6rem 0.8rem; + overflow-x: auto; + font-size: 0.82rem; + line-height: 1.4; + border: 1px solid var(--pst-color-border, rgba(128, 128, 128, 0.3)); + border-radius: 6px; + background: rgba(128, 128, 128, 0.12); + color: var(--pst-color-text-base, currentColor); + white-space: pre; +} + diff --git a/docs/_static/plr_labware_library.js b/docs/_static/plr_labware_library.js new file mode 100644 index 00000000000..2b98d7aaa7e --- /dev/null +++ b/docs/_static/plr_labware_library.js @@ -0,0 +1,391 @@ +(function () { + "use strict"; + + const state = { + index: null, + query: "", + manufacturer: "All", + section: "All", + selectedDefinition: null, + viewer: null, + }; + + function getUrlRoot() { + if (window.DOCUMENTATION_OPTIONS && window.DOCUMENTATION_OPTIONS.URL_ROOT) { + return window.DOCUMENTATION_OPTIONS.URL_ROOT; + } + if (document.documentElement && document.documentElement.dataset.content_root) { + return document.documentElement.dataset.content_root; + } + return ""; + } + + function staticUrl(path) { + return `${getUrlRoot()}${path}`; + } + + function libraryIndexUrl() { + const currentScript = + document.currentScript || + document.querySelector('script[src*="plr_labware_library.js"]'); + if (currentScript && currentScript.src) { + return new URL("labware_library_index.json", currentScript.src).toString(); + } + return staticUrl("_static/labware_library_index.json"); + } + + function element(tagName, className, text) { + const node = document.createElement(tagName); + if (className) node.className = className; + if (text) node.textContent = text; + return node; + } + + function unique(values) { + return Array.from(new Set(values.filter(Boolean))).sort((left, right) => + left.localeCompare(right), + ); + } + + function itemMatchesFilters(item) { + const haystack = [ + item.definition, + item.manufacturer, + item.section, + item.description_html, + ] + .join(" ") + .toLowerCase(); + + if (state.query && haystack.indexOf(state.query.toLowerCase()) === -1) { + return false; + } + if (state.manufacturer !== "All" && item.manufacturer !== state.manufacturer) { + return false; + } + if (state.section !== "All" && item.section !== state.section) { + return false; + } + return true; + } + + function setSelectOptions(select, values, selectedValue) { + select.innerHTML = ""; + ["All"].concat(values).forEach((value) => { + const option = document.createElement("option"); + option.value = value; + option.textContent = value; + option.selected = value === selectedValue; + select.appendChild(option); + }); + } + + function createModal() { + const overlay = document.createElement("div"); + overlay.className = "plr-library-modal"; + overlay.setAttribute("hidden", "hidden"); + overlay.innerHTML = ` +
+ + `; + document.body.appendChild(overlay); + return overlay; + } + + function ensureModal() { + if (state.modal) { + return state.modal; + } + + const viewerApi = window.PLRGeometryViewer; + if (!viewerApi || !viewerApi.CanvasCatalogViewer) { + return null; + } + + const modal = createModal(); + const stage = modal.querySelector(".plr-library-modal__stage"); + state.viewer = new viewerApi.CanvasCatalogViewer(stage); + state.modal = modal; + + function closeModal() { + modal.setAttribute("hidden", "hidden"); + document.body.classList.remove("plr-library-modal-open"); + } + + modal.querySelector(".plr-library-modal__close").addEventListener("click", closeModal); + modal.querySelector(".plr-library-modal__backdrop").addEventListener("click", closeModal); + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") closeModal(); + }); + + return modal; + } + + function openModel(definitionName) { + const modal = ensureModal(); + const library = state.index.resources[definitionName]; + if (!modal || !library || !state.viewer) { + return; + } + + const modalTitle = modal.querySelector(".plr-library-modal__title"); + modalTitle.textContent = ""; + const modalCode = document.createElement("code"); + modalCode.textContent = definitionName; + modalTitle.appendChild(modalCode); + modal.removeAttribute("hidden"); + document.body.classList.add("plr-library-modal-open"); + state.viewer.setCatalog(library); + window.requestAnimationFrame(() => state.viewer.resize()); + } + + function createCard(item) { + const card = element("article", "plr-library-card"); + + const media = element("button", "plr-library-card__media"); + media.type = "button"; + media.setAttribute("aria-label", `Open 3D preview for ${item.definition}`); + if (item.image) { + const image = document.createElement("img"); + image.src = item.image.startsWith("http") || item.image.startsWith("/") + ? item.image + : staticUrl(item.image); + image.alt = item.definition; + image.loading = "lazy"; + media.appendChild(image); + } else { + media.appendChild(element("div", "plr-library-card__placeholder", "No image")); + } + media.addEventListener("click", () => openModel(item.definition)); + + const body = element("div", "plr-library-card__body"); + const manufacturer = element("div", "plr-library-card__manufacturer", item.manufacturer); + const title = element("h3", "plr-library-card__title"); + const titleCode = document.createElement("code"); + titleCode.textContent = item.definition; + title.appendChild(titleCode); + const section = element("div", "plr-library-card__section", item.section || "Resource"); + const description = element("div", "plr-library-card__description"); + description.innerHTML = item.description_html || ""; + + const footer = element("div", "plr-library-card__footer"); + const modelButton = element("button", "plr-library-card__action", "View 3D"); + modelButton.type = "button"; + modelButton.disabled = !item.has_geometry; + modelButton.addEventListener("click", () => openModel(item.definition)); + + footer.appendChild(modelButton); + body.appendChild(manufacturer); + body.appendChild(title); + body.appendChild(section); + if (item.description_html) body.appendChild(description); + body.appendChild(footer); + card.appendChild(media); + card.appendChild(body); + return card; + } + + function buildSectionTree(items) { + const tree = { children: new Map(), count: 0 }; + items.forEach((item) => { + const path = + Array.isArray(item.section_path) && item.section_path.length + ? item.section_path + : [item.section || "Other"]; + let node = tree; + tree.count += 1; + path.forEach((seg) => { + if (!node.children.has(seg)) { + node.children.set(seg, { children: new Map(), count: 0 }); + } + node = node.children.get(seg); + node.count += 1; + }); + }); + return tree; + } + + function createManufacturerPanel(name, items) { + const meta = (state.index.manufacturers || {})[name] || {}; + const details = document.createElement("details"); + details.className = "plr-library-manufacturer"; + details.open = false; + + const breakdown = Array.from(buildSectionTree(items).children.entries()) + .map(([title, node]) => `${title} (${node.count})`) + .join(" · "); + const summary = document.createElement("summary"); + summary.appendChild(element("span", "plr-library-manufacturer__name", name)); + summary.appendChild( + element("span", "plr-library-manufacturer__meta", breakdown), + ); + details.appendChild(summary); + + if (meta.company_url) { + const link = document.createElement("a"); + link.className = "plr-library-manufacturer__company"; + link.href = meta.company_url; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.textContent = /wikipedia\.org/i.test(meta.company_url) + ? "Wikipedia ↗" + : "Website ↗"; + details.appendChild(link); + } + + if (meta.blurb) { + details.appendChild( + element("p", "plr-library-manufacturer__blurb", meta.blurb), + ); + } + + if (meta.brand_tree) { + details.appendChild( + element("div", "plr-library-manufacturer__subhead", "Brand structure"), + ); + const pre = element("pre", "plr-library-manufacturer__tree", meta.brand_tree); + details.appendChild(pre); + } + + return details; + } + + function renderLibrary(root) { + const items = (state.index.items || []).filter(itemMatchesFilters); + const results = root.querySelector(".plr-library-results"); + const count = root.querySelector(".plr-library-count"); + results.innerHTML = ""; + count.textContent = `${items.length} resources`; + + if (items.length === 0) { + results.appendChild(element("div", "plr-library-empty", "No labware matches these filters.")); + return; + } + + if (state.manufacturer !== "All") { + results.appendChild(createManufacturerPanel(state.manufacturer, items)); + } + + if (state.section !== "All") { + const grid = element("div", "plr-library-grid"); + items.forEach((item) => grid.appendChild(createCard(item))); + results.appendChild(grid); + return; + } + + const groups = new Map(); + items.forEach((item) => { + const key = item.section || "Other"; + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(item); + }); + + groups.forEach((groupItems, sectionName) => { + const group = element("section", "plr-library-group"); + group.appendChild(element("h2", "plr-library-group__title", sectionName)); + const grid = element("div", "plr-library-grid"); + groupItems.forEach((item) => grid.appendChild(createCard(item))); + group.appendChild(grid); + results.appendChild(group); + }); + } + + function renderLibraryShell(root) { + root.innerHTML = ` +
+ +
+ + +
+
+ + +
+
+
+
+ `; + + const search = root.querySelector("#plr-library-search-input"); + const manufacturer = root.querySelector("#plr-library-manufacturer"); + const section = root.querySelector("#plr-library-section"); + setSelectOptions(manufacturer, unique(state.index.items.map((item) => item.manufacturer)), state.manufacturer); + setSelectOptions(section, unique(state.index.items.map((item) => item.section)), state.section); + search.value = state.query; + + search.addEventListener("input", () => { + state.query = search.value; + writeUrlState(); + renderLibrary(root); + }); + manufacturer.addEventListener("change", () => { + state.manufacturer = manufacturer.value; + writeUrlState(); + renderLibrary(root); + }); + section.addEventListener("change", () => { + state.section = section.value; + writeUrlState(); + renderLibrary(root); + }); + + renderLibrary(root); + } + + function readUrlState() { + const params = new URLSearchParams(window.location.search); + state.query = params.get("q") || ""; + state.manufacturer = params.get("manufacturer") || "All"; + state.section = params.get("section") || "All"; + } + + function writeUrlState() { + const params = new URLSearchParams(window.location.search); + if (state.query) params.set("q", state.query); + else params.delete("q"); + if (state.manufacturer && state.manufacturer !== "All") params.set("manufacturer", state.manufacturer); + else params.delete("manufacturer"); + if (state.section && state.section !== "All") params.set("section", state.section); + else params.delete("section"); + const queryString = params.toString(); + const newUrl = `${window.location.pathname}${queryString ? "?" + queryString : ""}${window.location.hash}`; + window.history.replaceState(null, "", newUrl); + } + + function initializeLibraryPage() { + const root = document.getElementById("plr-labware-library"); + if (!root) { + return; + } + + readUrlState(); + root.innerHTML = `
Loading library...
`; + fetch(libraryIndexUrl()) + .then((response) => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }) + .then((index) => { + state.index = index; + renderLibraryShell(root); + }) + .catch((error) => { + root.innerHTML = `
Could not load the generated library index: ${error.message}
`; + }); + } + + document.addEventListener("DOMContentLoaded", initializeLibraryPage); +})(); diff --git a/docs/_templates/autosummary/attribute.rst b/docs/_templates/autosummary/attribute.rst new file mode 100644 index 00000000000..4ad94123a2c --- /dev/null +++ b/docs/_templates/autosummary/attribute.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoattribute:: {{ objname }} diff --git a/docs/_templates/autosummary/data.rst b/docs/_templates/autosummary/data.rst new file mode 100644 index 00000000000..ad6fafdd46a --- /dev/null +++ b/docs/_templates/autosummary/data.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autodata:: {{ objname }} diff --git a/docs/_templates/autosummary/exception.rst b/docs/_templates/autosummary/exception.rst new file mode 100644 index 00000000000..752bc57414b --- /dev/null +++ b/docs/_templates/autosummary/exception.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoexception:: {{ objname }} diff --git a/docs/_templates/autosummary/function.rst b/docs/_templates/autosummary/function.rst new file mode 100644 index 00000000000..eade6e8536e --- /dev/null +++ b/docs/_templates/autosummary/function.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autofunction:: {{ objname }} diff --git a/docs/_templates/autosummary/method.rst b/docs/_templates/autosummary/method.rst new file mode 100644 index 00000000000..b8eaf012e69 --- /dev/null +++ b/docs/_templates/autosummary/method.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. automethod:: {{ objname }} diff --git a/docs/_templates/autosummary/property.rst b/docs/_templates/autosummary/property.rst new file mode 100644 index 00000000000..12c1dee318e --- /dev/null +++ b/docs/_templates/autosummary/property.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoproperty:: {{ objname }} diff --git a/docs/api/pylabrobot.resources.rst b/docs/api/pylabrobot.resources.rst index 5cbc3ceee2e..0b1f51f9e55 100644 --- a/docs/api/pylabrobot.resources.rst +++ b/docs/api/pylabrobot.resources.rst @@ -14,6 +14,7 @@ Resources represent on-deck liquid handling equipment, including tip racks, plat Container Coordinate Deck + generate_geometry_library ItemizedResource utils.create_equally_spaced_2d Lid @@ -26,6 +27,7 @@ Resources represent on-deck liquid handling equipment, including tip racks, plat ResourceHolder ResourceStack Rotation + save_geometry_library tip.Tip TipCarrier TipRack diff --git a/docs/conf.py b/docs/conf.py index a6e9359e41b..41efe6d898c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,7 @@ import os import shutil import sys +from pathlib import Path sys.path.insert(0, os.path.abspath("..")) # Allow importing local Sphinx extensions (e.g., pylabrobot_cards) @@ -38,6 +39,7 @@ "sphinx.ext.napoleon", "sphinx.ext.autodoc", "pylabrobot_cards", # NEW: PLR cards (plrcard/plrcardgrid + compat) + "pylabrobot_labware_library", "sphinx.ext.autosummary", "sphinx.ext.autosectionlabel", "sphinx.ext.intersphinx", @@ -65,6 +67,8 @@ "Thumbs.db", ".DS_Store", "jupyter_execute", + "resources/library/*.md", + "resources/library/**/*.md", ] autodoc_default_options = { @@ -98,10 +102,16 @@ html_css_files = list(globals().get("html_css_files", [])) if "plr_cards.css" not in html_css_files: html_css_files.append("plr_cards.css") # served from _static/plr_cards.css +if "plr_labware_library.css" not in html_css_files: + html_css_files.append("plr_labware_library.css") html_js_files = list(globals().get("html_js_files", [])) if "plr_cards.js" not in html_js_files: html_js_files.append("plr_cards.js") # served from _static/plr_cards.js +if "plr_geometry_viewer.js" not in html_js_files: + html_js_files.append("plr_geometry_viewer.js") +if "plr_labware_library.js" not in html_js_files: + html_js_files.append("plr_labware_library.js") # NOTE: templates_path already includes "_templates", which is where # plr_card_grid.html should live. @@ -193,8 +203,36 @@ "tilting.html": "user_guide/tilting.html", "heating-shaking.html": "user_guide/heating_shaking.html", "fans.html": "user_guide/fans.html", + "resources/catalog.html": "resources/library.html", + "resources/geometry-catalog.html": "resources/library.html", } + +def _library_redirect_target(source_html_path: str) -> str: + target = "resources/library.html" + source_dir = os.path.dirname(source_html_path) + if not source_dir: + return target + return os.path.relpath(target, start=source_dir).replace("\\", "/") + + +def _library_doc_redirects() -> dict[str, str]: + redirects_map: dict[str, str] = {} + library_root = Path(__file__).parent / "resources" / "library" + for markdown_path in library_root.rglob("*.md"): + relative_markdown = markdown_path.relative_to(Path(__file__).parent) + source_html = relative_markdown.with_suffix(".html").as_posix() + redirects_map[source_html] = _library_redirect_target(source_html) + + # Also redirect extension-less style links such as /resources/library/hamilton. + if markdown_path.stem != "index": + folder_style_source = (relative_markdown.with_suffix("") / "index.html").as_posix() + redirects_map[folder_style_source] = _library_redirect_target(folder_style_source) + return redirects_map + + +redirects.update(_library_doc_redirects()) + html_sidebars = { "api/**": ["search-field"], } diff --git a/docs/resources/carrier/plate-carrier/plate_carrier.ipynb b/docs/resources/carrier/plate-carrier/plate_carrier.ipynb index cfb5a304848..3fbe2d6bb42 100644 --- a/docs/resources/carrier/plate-carrier/plate_carrier.ipynb +++ b/docs/resources/carrier/plate-carrier/plate_carrier.ipynb @@ -15,7 +15,7 @@ "source": [ "## Using a plate carrier\n", "\n", - "The PyLabRobot Resource Library (PLR-RL) has a big number of predefined carriers. You can find these in the [PLR-RL docs](../../index.md). [Hamilton Plate Carriers](../../library/hamilton.md#plate-carriers) may be of particular interest." + "The PyLabRobot Resource Library has a big number of predefined carriers. You can find these in the [resource library](../../library.md)." ] }, { diff --git a/docs/resources/index.md b/docs/resources/index.md index 4a86bf7ca1c..845add74f7a 100644 --- a/docs/resources/index.md +++ b/docs/resources/index.md @@ -12,7 +12,7 @@ The PLR Resource Management System consists of two key components, each serving 1. **Resource Ontology System** - The ***'blueprint'*** of PLR's physical definition framework, responsible for defining physical resources, modeling their distinct behaviors, and dynamically managing their relationships (i.e. tracking their *state*). 2. **Resource Library** - - The ***'catalog'*** of premade resource definitions. + - The ***'library'*** of premade resource definitions. This provides reusable, standardized definitions that enhance consistency and interoperability across automation workflows. This ensures smooth integration, scalability, and efficient resource utilization. @@ -136,11 +136,13 @@ resource-stack/resource-stack ## Resource Library -The PyLabRobot Resource Library (PLR-RL) is PyLabRobot's open-source, crowd-sourced collection of pre-made resource definitions. +The PyLabRobot Resource Library is PyLabRobot's open-source, crowd-sourced collection of pre-made resource definitions. Laboratories across the world use an almost infinite number of different resources (e.g. plates, tubes, liquid handlers, microscopes, arms, ...). We believe the way to most efficiently capture the largest portion of this resource superset is via crowd-sourcing and iteratively peer-reviewing definitions. If you cannot find something, please contribute what you are looking for! +Open the {doc}`library page ` to search the resource library across manufacturers and inspect generated 3D previews for labware definitions. +
@@ -167,28 +169,5 @@ If you cannot find something, please contribute what you are looking for! ```{toctree} :caption: Resource Library -library/agenbio -library/agilent -library/alpaqua -library/azenta -library/bioer -library/biorad -library/boekel -library/celltreat -library/cellvis -library/corning -library/eppendorf -library/falcon -library/greiner -library/hamilton -library/imcs -library/nest -library/opentrons -library/perkin_elmer -library/porvair -library/revvity -library/sergi -library/thermo_fisher -library/vwr -library/diy/index +library ``` diff --git a/docs/resources/library.md b/docs/resources/library.md new file mode 100644 index 00000000000..63b46734483 --- /dev/null +++ b/docs/resources/library.md @@ -0,0 +1,8 @@ +# Resource Library + +The PyLabRobot Resource Library is rendered from the resource definitions used by PLR itself. +The library below is generated during the docs build and includes manufacturer, type, definition details, images, and 3D previews from the same geometry metadata returned by `generate_geometry_library`. + +```{raw} html +
+``` diff --git a/docs/resources/library/agenbio.md b/docs/resources/library/agenbio.md index e735ad94bf8..f6533d08e94 100644 --- a/docs/resources/library/agenbio.md +++ b/docs/resources/library/agenbio.md @@ -4,9 +4,9 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'AGenBio_4_troughplate_75000uL_Vb'
Part no.: RES-75-4MW
[manufacturer website](https://agenbio.en.made-in-china.com/product/ZTqYVMiCkpcF/China-Medical-Consumable-Plastic-Reagent-Reservoir-Disposable-4-Channel-Troughs-Reagent-Reservoir.html?) | ![](img/agenbio/AGenBio_4_troughplate_75000uL_Vb.webp) | `AGenBio_4_troughplate_75000uL_Vb` | -| 'AGenBio_1_troughplate_190000uL_Fl'
Part no.: RES-190-F
[manufacturer website](https://agenbio.en.made-in-china.com/product/pZWaBIPvZMkm/China-Res-190-F-Lad-Consumables-of-Flat-Reservoir.html) | ![](img/agenbio/AGenBio_1_troughplate_190000uL_Fl.webp) | `AGenBio_1_troughplate_190000uL_Fl` | -| 'AGenBio_1_troughplate_100000uL_Fl'
Part no.: RES-100-F
[manufacturer website](https://agenbio.en.made-in-china.com/product/rxgRnesJIjcQ/China-100ml-Flat-Bottom-Single-Well-Low-Profile-Design-Reagent-Reservoir.html) | ![](img/agenbio/AGenBio_1_troughplate_100000uL_Fl.jpg) | `AGenBio_1_troughplate_100000uL_Fl` | -| `AGenBio_96_wellplate_Ub_2200ul`
Part no.: P-2.2-SQG-96
[manufacturer website](https://agenbio.en.made-in-china.com/product/GfoUzYcARahV/China-2-2ml-Square-Well-96-Square-Deep-Well-Plate-Profile-Concave-U-Bottom-96-Deep-Well-Plate.html) | ![](img/agenbio/AGenBio_96_wellplate_Ub_2200ul.avif) | `AGenBio_96_wellplate_Ub_2200ul` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------- | +| 'AGenBio_4_troughplate_75000uL_Vb'
Part no.: RES-75-4MW
[manufacturer website](https://agenbio.en.made-in-china.com/product/ZTqYVMiCkpcF/China-Medical-Consumable-Plastic-Reagent-Reservoir-Disposable-4-Channel-Troughs-Reagent-Reservoir.html?) | ![](img/agenbio/AGenBio_4_troughplate_75000uL_Vb.webp) | `AGenBio_4_troughplate_75000uL_Vb` | +| 'AGenBio_1_troughplate_190000uL_Fl'
Part no.: RES-190-F
[manufacturer website](https://agenbio.en.made-in-china.com/product/pZWaBIPvZMkm/China-Res-190-F-Lad-Consumables-of-Flat-Reservoir.html) | ![](img/agenbio/AGenBio_1_troughplate_190000uL_Fl.webp) | `AGenBio_1_troughplate_190000uL_Fl` | +| 'AGenBio_1_troughplate_100000uL_Fl'
Part no.: RES-100-F
[manufacturer website](https://agenbio.en.made-in-china.com/product/rxgRnesJIjcQ/China-100ml-Flat-Bottom-Single-Well-Low-Profile-Design-Reagent-Reservoir.html) | ![](img/agenbio/AGenBio_1_troughplate_100000uL_Fl.jpg) | `AGenBio_1_troughplate_100000uL_Fl` | +| `AGenBio_96_wellplate_Ub_2200ul`
Part no.: P-2.2-SQG-96
[manufacturer website](https://agenbio.en.made-in-china.com/product/GfoUzYcARahV/China-2-2ml-Square-Well-96-Square-Deep-Well-Plate-Profile-Concave-U-Bottom-96-Deep-Well-Plate.html) | ![](img/agenbio/AGenBio_96_wellplate_Ub_2200ul.avif) | `AGenBio_96_wellplate_Ub_2200ul` | diff --git a/docs/resources/library/agilent.md b/docs/resources/library/agilent.md index 4a1fbdf6075..89d0844944c 100644 --- a/docs/resources/library/agilent.md +++ b/docs/resources/library/agilent.md @@ -4,7 +4,7 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'agilent_96_wellplate_150uL_Vb'
Part no.: 5042-8502
[manufacturer website](https://www.agilent.com/store/en_US/Prod-5042-8502/5042-8502) | ![](img/agilent/agilent_96_wellplate_150uL_Vb.jpg) | `agilent_96_wellplate_150uL_Vb` | -| 'Agilent_2_reservoir_144mL_Vb'
Part no.: 203852-100
[manufacturer website](https://www.agilent.com/store/en_US/Prod-203852-100/203852-100?srsltid=AfmBOorcg-D2jAf9VwaEDFvo7ZCQ-_G14cndKReIcpSBsFA0YjxNgDKm) | ![](img/agilent/Agilent_2_reservoir_144mL_Vb.jpg) | `Agilent_2_reservoir_144mL_Vb` | \ No newline at end of file +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------- | +| 'agilent_96_wellplate_150uL_Vb'
Part no.: 5042-8502
[manufacturer website](https://www.agilent.com/store/en_US/Prod-5042-8502/5042-8502) | ![](img/agilent/agilent_96_wellplate_150uL_Vb.jpg) | `agilent_96_wellplate_150uL_Vb` | +| 'Agilent_2_reservoir_144mL_Vb'
Part no.: 203852-100
[manufacturer website](https://www.agilent.com/store/en_US/Prod-203852-100/203852-100?srsltid=AfmBOorcg-D2jAf9VwaEDFvo7ZCQ-_G14cndKReIcpSBsFA0YjxNgDKm) | ![](img/agilent/Agilent_2_reservoir_144mL_Vb.jpg) | `Agilent_2_reservoir_144mL_Vb` | \ No newline at end of file diff --git a/docs/resources/library/alpaqua.md b/docs/resources/library/alpaqua.md index 7ca7f097f9a..7ae69befb69 100644 --- a/docs/resources/library/alpaqua.md +++ b/docs/resources/library/alpaqua.md @@ -7,6 +7,6 @@ Our products include a line of innovative, high performance magnet plates built ## Labware -| Description | Image | PLR definition | -|-|-|-| -| 'Alpaqua_96_magnum_flx'
Part no.: A000400
[manufacturer website](https://www.alpaqua.com/product/magnum-flx/) | ![](img/alpaqua/Alpaqua_96_magnum_flx.jpg) | `Alpaqua_96_magnum_flx` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------------- | +| 'alpaqua_96_plateadapter_magnum_flx'
Part no.: A000400
[manufacturer website](https://www.alpaqua.com/product/magnum-flx/) | ![](img/alpaqua/Alpaqua_96_magnum_flx.jpg) | `alpaqua_96_plateadapter_magnum_flx` | diff --git a/docs/resources/library/azenta.md b/docs/resources/library/azenta.md index 34ed3ea1a28..4193ea5e340 100644 --- a/docs/resources/library/azenta.md +++ b/docs/resources/library/azenta.md @@ -8,6 +8,6 @@ Company wikipedia: [Azenta](https://en.wikipedia.org/wiki/Azenta) ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Azenta4titudeFrameStar_96_wellplate_skirted'

- Man. part no.: 4ti-0960
- Supplier part no.: PCR1232
- [manufacturer website](https://www.azenta.com/products/framestar-96-well-skirted-pcr-plate)
- [supplier website](https://www.scientificlabs.co.uk/product/pcr-plates/PCR1232)
- working volume: <100µl
- total well capacity: 200µl| ![](img/azenta/azenta_4titude_96PCR_4ti-0960.jpg) | `Azenta4titudeFrameStar_96_wellplate_skirted` | +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | --------------------------------------------- | +| 'Azenta4titudeFrameStar_96_wellplate_200ul_Vb'

- Man. part no.: 4ti-0960
- Supplier part no.: PCR1232
- [manufacturer website](https://www.azenta.com/products/framestar-96-well-skirted-pcr-plate)
- [supplier website](https://www.scientificlabs.co.uk/product/pcr-plates/PCR1232)
- working volume: <100µl
- total well capacity: 200µl | ![](img/azenta/azenta_4titude_96PCR_4ti-0960.jpg) | `Azenta4titudeFrameStar_96_wellplate_200ul_Vb` | diff --git a/docs/resources/library/bioer.md b/docs/resources/library/bioer.md index a05a5493c31..6df79aef430 100644 --- a/docs/resources/library/bioer.md +++ b/docs/resources/library/bioer.md @@ -4,6 +4,6 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------ | | 'BioER_96_wellplate_Vb_2200uL'
Part no.: BSH06M1T-A
[manufacturer website](https://en.bioer.com/en/ConsumablesCenter/info_itemid_3261_lcid_134.html) | ![](img/bioer/BioER_96_wellplate_Vb_2200uL.jpg) | `BioER_96_wellplate_Vb_2200uL` | diff --git a/docs/resources/library/biorad.md b/docs/resources/library/biorad.md index 183aabed8d8..98f19b567d4 100644 --- a/docs/resources/library/biorad.md +++ b/docs/resources/library/biorad.md @@ -4,6 +4,6 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'BioRad_384_DWP_50uL_Vb'
Part no.: HSP3805
[manufacturer website](https://www.bio-rad.com/en-us/sku/HSP3805-hard-shell-384-well-pcr-plates-thin-wall-skirted-clear-white?ID=HSP3805) | ![](img/biorad/BioRad_384_DWP_50uL_Vb.webp) | `BioRad_384_DWP_50uL_Vb` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | ------------------------ | +| 'BioRad_384_wellplate_50uL_Vb'
Part no.: HSP3805
[manufacturer website](https://www.bio-rad.com/en-us/sku/HSP3805-hard-shell-384-well-pcr-plates-thin-wall-skirted-clear-white?ID=HSP3805) | ![](img/biorad/BioRad_384_DWP_50uL_Vb.webp) | `BioRad_384_wellplate_50uL_Vb` | diff --git a/docs/resources/library/boekel.md b/docs/resources/library/boekel.md index 5e23166b935..19d643b8ab8 100644 --- a/docs/resources/library/boekel.md +++ b/docs/resources/library/boekel.md @@ -9,9 +9,9 @@ The following rack exists in 4 orientations: - 1.5/2ml microcentrifuge tubes = `boekel_1_5mL_microcentrifuge_carrier` - ?ml microcentrifuge tubes = `boekel_mini_microcentrifuge_carrier` -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier50mL.jpg) | `boekel_50mL_falcon_carrier` | -| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier15mL.jpg) | `boekel_15mL_falcon_carrier` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------- | +| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier50mL.jpg) | `boekel_50mL_falcon_carrier` | +| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier15mL.jpg) | `boekel_15mL_falcon_carrier` | | Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier1_5mL.jpg) | `boekel_1_5mL_microcentrifuge_carrier` | -| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier_mini.jpg) | `boekel_mini_microcentrifuge_carrier` | +| Multi Tube Rack For 50ml Conical, 15ml Conical, And Microcentrifuge Tubes, PN:120008 [manufacturer website](https://www.boekelsci.com/multi-tube-rack-for-50ml-conical-15ml-conical-and-microcentrifuge-tubes-pn-120008.html) | ![](img/boekel/boekel_carrier_mini.jpg) | `boekel_mini_microcentrifuge_carrier` | diff --git a/docs/resources/library/celltreat.md b/docs/resources/library/celltreat.md index 5753bf26c7e..c50b70434bd 100644 --- a/docs/resources/library/celltreat.md +++ b/docs/resources/library/celltreat.md @@ -2,17 +2,17 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'CellTreat_6_DWP_16300ul_Fb'
Part no.: 229105
[manufacturer website](https://www.celltreat.com/product/229105/) | ![](img/celltreat/CellTreat_6_DWP_16300ul_Fb.jpg) | `CellTreat_6_DWP_16300ul_Fb` | -| Same as 229590 (229590 is sold with lids) 'CellTreat_96_wellplate_350ul_Ub'
Part no.: 229591
[manufacturer website](https://www.celltreat.com/product/229591/) | ![](img/celltreat/CellTreat_96_wellplate_350ul_Ub.jpg) | `CellTreat_96_wellplate_350ul_Ub` | -| 229195 and 229196 'CellTreat_96_wellplate_350ul_Fb'
Part no.: 229195
[manufacturer website](https://www.celltreat.com/product/229195/)
are treated | ![](img/celltreat/CellTreat_96_wellplate_350ul_Fb.jpg) | `CellTreat_96_wellplate_350ul_Fb` | -| 229562 (not sterile), 229566 (sterile) 'CellTreat_12_troughplate_15000ul_Vb'
[manufacturer website](https://www.celltreat.com/product/229562)
are treated | ![](img/celltreat/CellTreat_12_troughplate_15000ul_Vb.jpg) | `CellTreat_12_troughplate_15000ul_Vb` | -| 229123, 229124
[manufacturer website](https://www.celltreat.com/product/229123) | ![](img/celltreat/CellTreat_24_wellplate_3300ul_Fb.jpg) | `CellTreat_24_wellplate_3300ul_Fb` | +| Description | Image | PLR definition | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------- | +| 'CellTreat_6_wellplate_16300ul_Fb'
Part no.: 229105
[manufacturer website](https://www.celltreat.com/product/229105/) | ![](img/celltreat/CellTreat_6_DWP_16300ul_Fb.jpg) | `CellTreat_6_wellplate_16300ul_Fb` | +| Same as 229590 (229590 is sold with lids) 'CellTreat_96_wellplate_350ul_Ub'
Part no.: 229591
[manufacturer website](https://www.celltreat.com/product/229591/) | ![](img/celltreat/CellTreat_96_wellplate_350ul_Ub.jpg) | `CellTreat_96_wellplate_350ul_Ub` | +| 229195 and 229196 'CellTreat_96_wellplate_350ul_Fb'
Part no.: 229195
[manufacturer website](https://www.celltreat.com/product/229195/)
are treated | ![](img/celltreat/CellTreat_96_wellplate_350ul_Fb.jpg) | `CellTreat_96_wellplate_350ul_Fb` | +| 229562 (not sterile), 229566 (sterile) 'CellTreat_12_troughplate_15000ul_Vb'
[manufacturer website](https://www.celltreat.com/product/229562)
are treated | ![](img/celltreat/CellTreat_12_troughplate_15000ul_Vb.jpg) | `CellTreat_12_troughplate_15000ul_Vb` | +| 229123, 229124
[manufacturer website](https://www.celltreat.com/product/229123) | ![](img/celltreat/CellTreat_24_wellplate_3300ul_Fb.jpg) | `CellTreat_24_wellplate_3300ul_Fb` | ## Tubes -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------- | | 229414 (sterile)
[manufacturer website](https://www.celltreat.com/product/229414/)
- bottom_type=TubeBottomType.V | ![](img/celltreat/celltreat_15000ul_centrifuge_tube_Vb.webp) | `celltreat_15000ul_centrifuge_tube_Vb` | diff --git a/docs/resources/library/cellvis.md b/docs/resources/library/cellvis.md index 4dff0e88973..37198a54a3b 100644 --- a/docs/resources/library/cellvis.md +++ b/docs/resources/library/cellvis.md @@ -4,7 +4,7 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------- | | 'CellVis_24_wellplate_3600uL_Fb'
Part no.: P24-1.5P
[manufacturer website](https://www.cellvis.com/_24-well-plate-with--number-1.5-glass-like-polymer-coverslip-bottom-tissue-culture-treated-for-better-cell-attachment-than-cover-glass_/product_detail.php?product_id=65) | ![](img/cellvis/CellVis_24_wellplate_3600uL_Fb.jpg) | `CellVis_24_wellplate_3600uL_Fb` | -| 'CellVis_96_wellplate_350uL_Fb'
Part no.: P96-1.5H-N
[manufacturer website](https://www.cellvis.com/_96-well-glass-bottom-plate-with-high-performance-number-1.5-cover-glass_/product_detail.php?product_id=50) | ![](img/cellvis/CellVis_96_wellplate_350uL_Fb.jpg) | `CellVis_96_wellplate_350uL_Fb` | +| 'CellVis_96_wellplate_350uL_Fb'
Part no.: P96-1.5H-N
[manufacturer website](https://www.cellvis.com/_96-well-glass-bottom-plate-with-high-performance-number-1.5-cover-glass_/product_detail.php?product_id=50) | ![](img/cellvis/CellVis_96_wellplate_350uL_Fb.jpg) | `CellVis_96_wellplate_350uL_Fb` | diff --git a/docs/resources/library/corning.md b/docs/resources/library/corning.md index 1248f83c2ef..b2cbe8773fb 100644 --- a/docs/resources/library/corning.md +++ b/docs/resources/library/corning.md @@ -41,9 +41,9 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e (axygen-plates)= ### Plates -| Description | Image | PLR definition | -|-|-|-| -| 'Cor_Axy_24_wellplate_10mL_Vb'
Part no.: P-DW-10ML-24-C-S
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Genomics-&-Molecular-Biology/Automation-Consumables/Deep-Well-Plate/Axygen%C2%AE-Deep-Well-and-Assay-Plates/p/P-DW-10ML-24-C-S) | ![](img/corning_axygen/Cor_Axy_24_wellplate_10mL_Vb.jpg) | `Cor_Axy_24_wellplate_10mL_Vb` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------- | +| 'Cor_Axy_24_wellplate_10mL_Vb'
Part no.: P-DW-10ML-24-C-S
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Genomics-&-Molecular-Biology/Automation-Consumables/Deep-Well-Plate/Axygen%C2%AE-Deep-Well-and-Assay-Plates/p/P-DW-10ML-24-C-S) | ![](img/corning_axygen/Cor_Axy_24_wellplate_10mL_Vb.jpg) | `Cor_Axy_24_wellplate_10mL_Vb` | | 'Cor_Axy_96_wellplate_500uL_Ub'
Part no.: P-96-450V-C-S ("-S" indicates sterile labware)
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Genomics-&-Molecular-Biology/Automation-Consumables/Deep-Well-Plate/Axygen%C2%AE-Deep-Well-and-Assay-Plates/p/P-96-450V-C-S) | ![](img/corning_axygen/Cor_Axy_96_wellplate_500uL_Ub.png) | `Cor_Axy_96_wellplate_500uL_Ub` | ## Corning - Costar @@ -51,13 +51,13 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e (costar-plates)= ### Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Cos_6_wellplate_16800ul_Fb'
Part no.s:
  • [3335 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3335)
  • [3506 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3506)
  • [3516 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)
  • [3471 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3471)

- Material: ?
- Cleanliness: 3516: sterilized by gamma irradiation
- Nonreversible lids with condensation rings to reduce contamination
- Treated for optimal cell attachment
- Cell growth area: 9.5 cm² (approx.)
- Total volume: 16.8 mL | ![](img/corning_costar/Cos_6_wellplate_16800ul_Fb.jpg) | `Cos_6_wellplate_16800ul_Fb` | -| 'Cor_12_wellplate_6900ul_Fb'
Part no.s:
  • [3336 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3336)
  • [3512 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3512)
  • [3513 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3513)

- Total volume: 6.9 mL | ![](img/corning_costar/Cor_12_wellplate_6900ul_Fb.jpg) | `Cor_12_wellplate_6900ul_Fb` | -| 'Cor_24_wellplate_3470ul_Fb'
Part no.s:
  • [3337 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3337)
  • [3524 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3524)
  • [3526 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3526)
  • [3527 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3527)
  • [3473 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3473)

- Total volume: 3.47 mL | ![](img/corning_costar/Cor_24_wellplate_3470ul_Fb.jpg) | `Cor_24_wellplate_3470ul_Fb` | -| 'Cor_48_wellplate_1620ul_Fb'
Part no.: 3548
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3548)

- Material: TC-treated polystyrene
- Cleanliness: sterile
- Total volume: 1.62 mL | ![](img/corning_costar/Cor_48_wellplate_1620ul_Fb.jpg) | `Cor_48_wellplate_1620ul_Fb` | -| 'Cos_96_wellplate_2mL_Vb'
Part no.: 3516
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)

- Material: Polypropylene
- Resistant to many common organic solvents (e.g., DMSO, ethanol, methanol)
- 3960: Sterile and DNase- and RNase-free
- Total volume: 2 mL
- Features uniform skirt heights for greater robotic gripping surface| ![](img/corning_costar/Cos_96_wellplate_2mL_Vb.jpg) | `Cos_96_wellplate_2mL_Vb` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------- | +| 'Cor_Cos_6_wellplate_16800ul_Fb'
Part no.s:
  • [3335 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3335)
  • [3506 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3506)
  • [3516 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)
  • [3471 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3471)

- Material: ?
- Cleanliness: 3516: sterilized by gamma irradiation
- Nonreversible lids with condensation rings to reduce contamination
- Treated for optimal cell attachment
- Cell growth area: 9.5 cm² (approx.)
- Total volume: 16.8 mL | ![](img/corning_costar/Cos_6_wellplate_16800ul_Fb.jpg) | `Cor_Cos_6_wellplate_16800ul_Fb` | +| 'Cor_Cos_12_wellplate_6900ul_Fb'
Part no.s:
  • [3336 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3336)
  • [3512 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3512)
  • [3513 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3513)

- Total volume: 6.9 mL | ![](img/corning_costar/Cor_12_wellplate_6900ul_Fb.jpg) | `Cor_Cos_12_wellplate_6900ul_Fb` | +| 'Cor_Cos_24_wellplate_3470ul_Fb'
Part no.s:
  • [3337 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3337)
  • [3524 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3524)
  • [3526 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3526)
  • [3527 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3527)
  • [3473 manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3473)

- Total volume: 3.47 mL | ![](img/corning_costar/Cor_24_wellplate_3470ul_Fb.jpg) | `Cor_Cos_24_wellplate_3470ul_Fb` | +| 'Cor_Cos_48_wellplate_1620ul_Fb'
Part no.: 3548
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon%C2%AE-96-well-Polystyrene-Microplates/p/3548)

- Material: TC-treated polystyrene
- Cleanliness: sterile
- Total volume: 1.62 mL | ![](img/corning_costar/Cor_48_wellplate_1620ul_Fb.jpg) | `Cor_Cos_48_wellplate_1620ul_Fb` | +| 'Cor_96_wellplate_2mL_Vb'
Part no.: 3516
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/Microplates/Assay-Microplates/96-Well-Microplates/Costar%C2%AE-Multiple-Well-Cell-Culture-Plates/p/3516)

- Material: Polypropylene
- Resistant to many common organic solvents (e.g., DMSO, ethanol, methanol)
- 3960: Sterile and DNase- and RNase-free
- Total volume: 2 mL
- Features uniform skirt heights for greater robotic gripping surface | ![](img/corning_costar/Cos_96_wellplate_2mL_Vb.jpg) | `Cor_96_wellplate_2mL_Vb` | 'Cor_96_wellplate_360ul_Fb'
Part no.: 353376
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/NL/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353376)

- Material: TC-treated polystyrene
- Cleanliness: sterile
- Total volume: 392 uL
- Working volume: 25-340 uL | ![](img/corning_costar/Cor_96_wellplate_360ul_Fb.jpg) | `Cor_96_wellplate_360ul_Fb` | ## Falcon @@ -65,16 +65,16 @@ Company page: [Corning - Axygen® Brand Products](https://www.corning.com/emea/e (falcon-plates)= ### Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| Falcon_96_wellplate_Fl [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Falcon_96_wellplate_Fl` -| Falcon_96_wellplate_Rb [manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353077) | ![](img/falcon/Falcon_96_wellplate_Rb.jpg) | `Falcon_96_wellplate_Rb` -| Falcon_96_wellplate_Fl_Black [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Falcon_96_wellplate_Fl_Black` +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------- | +| Cor_Falcon_96_wellplate_250ul_Rb [manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353077) | ![](img/falcon/Falcon_96_wellplate_Rb.jpg) | `Cor_Falcon_96_wellplate_250ul_Rb` | +| Part number: 353072 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Cor_Falcon_96_wellplate_275ul_Fb` | +| Part number: 353219 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Cor_Falcon_96_wellplate_340ul_Fb_Black` | ### Tubes -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `falcon_tube_50mL` -| 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `falcon_tube_15mL` -| Falcon_tube_14mL_Rb
Corning cat. no.: 352059
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Falcon_tube_14mL_Rb` +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------------- | +| 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `Cor_Falcon_tube_50mL_Vb` | +| 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `Cor_Falcon_tube_15mL_Vb` | +| Falcon_tube_14mL_Rb
Corning cat. no.: 352059
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Cor_Falcon_tube_14mL_Rb` | diff --git a/docs/resources/library/diy/davidnedrud.md b/docs/resources/library/diy/davidnedrud.md index f1d5923327e..78f8c81ffec 100644 --- a/docs/resources/library/diy/davidnedrud.md +++ b/docs/resources/library/diy/davidnedrud.md @@ -3,6 +3,6 @@ I created a 3D printed part for the Hamilton MFX carrier that can hold opentrons modules. ## MFX modules -| Description | Image | PLR definition | -| - | - | - | -| 'hamilton_mfx_opentrons_module'
This 3D printed module accepts Opentrons hardware. I tested with Opentrons temperature module
[OnShape link to part](https://cad.onshape.com/documents/71f70c40910fd15876f75d76/w/81912f5001c1f8dcb28dfd3b/e/da8c964d83d158897c596d21) | ![](../img/davidnedrud/hamilton_mfx_opentrons_module_1.jpeg) ![](../img/davidnedrud/hamilton_mfx_opentrons_module_2.jpeg)| `hamilton_mfx_opentrons_module` | +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | +| 'hamilton_mfx_opentrons_module'
This 3D printed module accepts Opentrons hardware. I tested with Opentrons temperature module
[OnShape link to part](https://cad.onshape.com/documents/71f70c40910fd15876f75d76/w/81912f5001c1f8dcb28dfd3b/e/da8c964d83d158897c596d21) | ![](../img/davidnedrud/hamilton_mfx_opentrons_module_1.jpeg) ![](../img/davidnedrud/hamilton_mfx_opentrons_module_2.jpeg) | `hamilton_mfx_opentrons_module` | diff --git a/docs/resources/library/diy/grindbio.md b/docs/resources/library/diy/grindbio.md index 2ab0a5bea0e..814f4a3682c 100644 --- a/docs/resources/library/diy/grindbio.md +++ b/docs/resources/library/diy/grindbio.md @@ -3,6 +3,6 @@ GrindBio created a 3D printed part for one of the Hamilton modules (cat.-no. 188042). The purpose of the part is to lower the module to better accommodate deepwell plates on top of magnet blocks. The lower height also accommodates taller labware. ## MFX modules -| Description | Image | PLR definition | -| - | - | - | -| 'Hamilton_MFX_plateholder_DWP_metal_tapped_10mm_3dprint'
3D printed supports accept Hamilton MFX DWP Module (cat.-no. 188042 / 188042-00)
[OnShape link to part](https://cad.onshape.com/documents/87b79aea22945656e1849b61/w/1d28384d184c23a6551facf8/e/3313021cc0b2fe3c5e005547)
Read more about assembly [here](https://labautomation.io/t/adapters-for-hamilton-carrier-188039/6561)| ![](../img/grindbio/3d-supports-for-Hamilton-module.jpeg) | `Hamilton_MFX_plateholder_DWP_metal_tapped_10mm_3dprint` | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------- | +| 'Hamilton_MFX_plateholder_DWP_metal_tapped_10mm_3dprint'
3D printed supports accept Hamilton MFX DWP Module (cat.-no. 188042 / 188042-00)
[OnShape link to part](https://cad.onshape.com/documents/87b79aea22945656e1849b61/w/1d28384d184c23a6551facf8/e/3313021cc0b2fe3c5e005547)
Read more about assembly [here](https://labautomation.io/t/adapters-for-hamilton-carrier-188039/6561) | ![](../img/grindbio/3d-supports-for-Hamilton-module.jpeg) | `Hamilton_MFX_plateholder_DWP_metal_tapped_10mm_3dprint` | diff --git a/docs/resources/library/eppendorf.md b/docs/resources/library/eppendorf.md index 5094690a55c..8c2c19b0984 100644 --- a/docs/resources/library/eppendorf.md +++ b/docs/resources/library/eppendorf.md @@ -10,16 +10,17 @@ Company page: [Eppendorf Wikipedia](https://en.wikipedia.org/wiki/Eppendorf_(com ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Eppendorf_96_wellplate_250ul_Vb'
Part no.: 0030133374
[manufacturer website](https://www.eppendorf.com/gb-en/Products/Laboratory-Consumables/Plates/Eppendorf-twintec-PCR-Plates-p-0030133374)

- Material: polycarbonate (frame), polypropylene (wells)
- part of the twin.tec(R) product line
- WARNING: not ANSI/SLAS 1-2004 footprint dimensions (123x81 mm^2!) ==> requires `PlateAdapter`
- 'Can be divided into 4 segments of 24 wells each to prevent waste and save money'. | ![](img/eppendorf/Eppendorf_96_wellplate_250ul_Vb_COMPLETE.png) ![](img/eppendorf/Eppendorf_96_wellplate_250ul_Vb_DIVIDED.png) | `Eppendorf_96_wellplate_250ul_Vb` | -| 'eppendorf_96_wellplate_500ul_Vb.avif'
Part no.: 951032107
[manufacturer website](https://www.eppendorf.com/us-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951032107) | ![](img/eppendorf/eppendorf_96_wellplate_500ul_Vb.avif) | `'eppendorf_96_wellplate_500ul_Vb` | -| 'eppendorf_96_wellplate_1000ul_Vb.avif'
Part no.: 951032921
[manufacturer website](https://www.eppendorf.com/ca-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951032921) | ![](img/eppendorf/eppendorf_96_wellplate_1000ul_Vb.avif) | `'eppendorf_96_wellplate_1000ul_Vb` | -| 'eppendorf_96_wellplate_2000ul_Vb.avif'
Part no.: 951033502
[manufacturer website](https://www.eppendorf.com/ca-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951033502) | ![](img/eppendorf/eppendorf_96_wellplate_2000ul_Vb.avif) | `'eppendorf_96_wellplate_2000ul_Vb` | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | +| 'Eppendorf_96_wellplate_250ul_Vb'
Part no.: 0030133374
[manufacturer website](https://www.eppendorf.com/gb-en/Products/Laboratory-Consumables/Plates/Eppendorf-twintec-PCR-Plates-p-0030133374)

- Material: polycarbonate (frame), polypropylene (wells)
- part of the twin.tec(R) product line
- WARNING: not ANSI/SLAS 1-2004 footprint dimensions (123x81 mm^2!) ==> requires `PlateAdapter`
- 'Can be divided into 4 segments of 24 wells each to prevent waste and save money'. | ![](img/eppendorf/Eppendorf_96_wellplate_250ul_Vb_COMPLETE.png) ![](img/eppendorf/Eppendorf_96_wellplate_250ul_Vb_DIVIDED.png) | `Eppendorf_96_wellplate_250ul_Vb` | +| 'eppendorf_96_wellplate_500ul_Vb.avif'
Part no.: 951032107
[manufacturer website](https://www.eppendorf.com/us-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951032107) | ![](img/eppendorf/eppendorf_96_wellplate_500ul_Vb.avif) | `'eppendorf_96_wellplate_500ul_Vb` | +| 'eppendorf_96_wellplate_1000ul_Vb.avif'
Part no.: 951032921
[manufacturer website](https://www.eppendorf.com/ca-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951032921) | ![](img/eppendorf/eppendorf_96_wellplate_1000ul_Vb.avif) | `'eppendorf_96_wellplate_1000ul_Vb` | +| 'eppendorf_96_wellplate_2000ul_Vb.avif'
Part no.: 951033502
[manufacturer website](https://www.eppendorf.com/ca-en/Products/Lab-Consumables/Plates/Protein-LoBind-Plates-p-951033502) | ![](img/eppendorf/eppendorf_96_wellplate_2000ul_Vb.avif) | `'eppendorf_96_wellplate_2000ul_Vb` | ## Tubes -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| "Common eppendorf tube"
Part no.: 022431021 (DNA), 022431081 (protein)
[manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698791) | ![](img/eppendorf/Eppendorf_DNA_LoBind_1_5ml_Vb.webp) | `Eppendorf_DNA_LoBind_1_5ml_Vb` `Eppendorf_Protein_LoBind_1_5ml_Vb` | -| "Common eppendorf tube"
Part no.: 022431048
[manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698792) | ![](img/eppendorf/Eppendorf_DNA_LoBind_2ml_Ub.jpg) | `Eppendorf_DNA_LoBind_2ml_Ub` | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------- | +| Eppendorf LoBind tube, 1.5 mL (DNA)
Part no.: 022431021
[manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698791) | ![](img/eppendorf/Eppendorf_DNA_LoBind_1_5ml_Vb.webp) | `Eppendorf_DNA_LoBind_1_5ml_Vb` | +| Eppendorf LoBind tube, 1.5 mL (protein)
Part no.: 022431081
[manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698791) | ![](img/eppendorf/Eppendorf_DNA_LoBind_1_5ml_Vb.webp) | `Eppendorf_Protein_LoBind_1_5ml_Vb` | +| "Common eppendorf tube"
Part no.: 022431048
[manufacturer website](https://www.fishersci.com/shop/products/dna-lobind-microcentrifuge-tubes/13698792) | ![](img/eppendorf/Eppendorf_DNA_LoBind_2ml_Ub.jpg) | `Eppendorf_DNA_LoBind_2ml_Ub` | diff --git a/docs/resources/library/falcon.md b/docs/resources/library/falcon.md deleted file mode 100644 index b85a4e929ae..00000000000 --- a/docs/resources/library/falcon.md +++ /dev/null @@ -1,17 +0,0 @@ -# Falcon - -# Plates - -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| Part number: 353072 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-cell-culture-treated-flat-bottom-microplate/087722C) | ![](img/falcon/Falcon_96_wellplate_Fl.webp) | `Cor_Falcon_96_wellplate_275ul_Fb` -| Part number: 353077 [manufacturer website](https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/Assay-Microplates/96-Well-Microplates/Falcon®-96-well-Polystyrene-Microplates/p/353077) | ![](img/falcon/Falcon_96_wellplate_Rb.jpg) | `Cor_Falcon_96_wellplate_250ul_Rb` -| Part number: 353219 [manufacturer website](https://www.fishersci.com/shop/products/falcon-96-well-imaging-plate-lid/08772225) | ![](img/falcon/Falcon_96_wellplate_Fl_Black.jpg.webp) | `Cor_Falcon_96_wellplate_340ul_Fb_Black` - -## Tubes - -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 50mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-50ml-conical-centrifuge-tubes-2/1495949A) | ![](img/falcon/falcon-tube-50mL.webp) | `Cor_Falcon_tube_50mL_Vb` -| 15mL Falcon Tube [manufacturer website](https://www.fishersci.com/shop/products/falcon-15ml-conical-centrifuge-tubes-5/p-193301) | ![](img/falcon/falcon-tube-15mL.webp) | `Cor_Falcon_tube_15mL_Vb` -| Falcon_tube_14mL_Rb
Corning cat. no.: 352059
[manufacturer website](https://ecatalog.corning.com/life-sciences/b2b/UK/en/General-Labware/Tubes/Tubes,-Round-Bottom/Falcon%C2%AE-Round-Bottom-High-clarity-Polypropylene-Tube/p/352059) | ![](img/falcon/Falcon_tube_14mL_Rb.jpg) | `Cor_Falcon_tube_14mL_Rb` diff --git a/docs/resources/library/greiner.md b/docs/resources/library/greiner.md index a33411a1717..652151eb934 100644 --- a/docs/resources/library/greiner.md +++ b/docs/resources/library/greiner.md @@ -7,7 +7,7 @@ Company page: [Greiner Bio-One](https://www.gbo.com/en-gb/company) ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Greiner_384_wellplate_28ul_Fb'
Part no.: 784075 (white), 784076 (black), 784101 (transparent)
[manufacturer website](https://shop.gbo.com/en/england/products/bioscience/microplates/384-well-microplates/384-well-small-volume-hibase-microplates/784075.html) | ![](img/greiner/Greiner_384_wellplate_28ul_Fb.png) | `Greiner_384_wellplate_28ul_Fb` -| 'greiner_96_wellplate_200uL_Vb'
Part no.: 652260
[manufacturer website](https://shop.gbo.com/en/usa/products/bioscience/molecular-biology/pcr-microplates/652260.html)
NOTE: This is a half-skirt plate and requires a plate adapter to use| ![](img/greiner/greiner_96_wellplate_200uL_Vb.jpg) | `greiner_96_wellplate_200uL_Vb` +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------- | +| 'Greiner_384_wellplate_28ul_Fb'
Part no.: 784075 (white), 784076 (black), 784101 (transparent)
[manufacturer website](https://shop.gbo.com/en/england/products/bioscience/microplates/384-well-microplates/384-well-small-volume-hibase-microplates/784075.html) | ![](img/greiner/Greiner_384_wellplate_28ul_Fb.png) | `Greiner_384_wellplate_28ul_Fb` | +| 'greiner_96_wellplate_200uL_Vb'
Part no.: 652260
[manufacturer website](https://shop.gbo.com/en/usa/products/bioscience/molecular-biology/pcr-microplates/652260.html)
NOTE: This is a half-skirt plate and requires a plate adapter to use | ![](img/greiner/greiner_96_wellplate_200uL_Vb.jpg) | `greiner_96_wellplate_200uL_Vb` | diff --git a/docs/resources/library/hamilton.md b/docs/resources/library/hamilton.md index 82a9d43e59f..d66c27b095e 100644 --- a/docs/resources/library/hamilton.md +++ b/docs/resources/library/hamilton.md @@ -1,4 +1,4 @@ -# Hamilton STAR "ML_STAR" +# Hamilton Company history: [Hamilton Robotics history](https://www.hamiltoncompany.com/history) @@ -8,92 +8,92 @@ Company history: [Hamilton Robotics history](https://www.hamiltoncompany.com/his ### Tip carriers -| Description | Image | PLR definition | -| - | - | - | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | ----------------- | | 'TIP_CAR_480_A00'
Part no.: 182085
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182085)
Carrier for 5x 96 tip (10μl, 50μl, 300μl, 1000μl) racks or 5x 24 tip (5ml) racks (6T) | ![](img/hamilton/TIP_CAR_480_A00_182085.jpg) | `TIP_CAR_480_A00` | -| 'TIP_CAR_288_C00'
Part no.: 182060
[manufacturer website](https://www.hamiltoncompany.com/other-robotics/182060)
Carrier for 3x 96 tip (10μl, 50μl, 300μl, 1000μl) racks or 3x 24 tip (5ml) racks (4T) | ![](img/hamilton/TIP_CAR_288_C00.jpg.avif) | `TIP_CAR_288_C00` | +| 'TIP_CAR_288_C00'
Part no.: 182060
[manufacturer website](https://www.hamiltoncompany.com/other-robotics/182060)
Carrier for 3x 96 tip (10μl, 50μl, 300μl, 1000μl) racks or 3x 24 tip (5ml) racks (4T) | ![](img/hamilton/TIP_CAR_288_C00.jpg.avif) | `TIP_CAR_288_C00` | ### Plate carriers -| Description | Image | PLR definition | -| - | - | - | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | ------------------ | | 'PLT_CAR_L5AC_A00'
Part no.: 182090
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182090)
Carrier for 5x 96 Deep Well Plates or for 5x 384 tip racks (e.g.384HEAD_384TIPS_50μl) (6T) | ![](img/hamilton/PLT_CAR_L5AC_A00_182090.jpg) | `PLT_CAR_L5AC_A00` | -| 'PLT_CAR_L5MD_A00'
Part no.: 182365/02
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
Carries five ANSI/SLAS footprint MTPs in landscape orientation. Occupies six tracks.| ![](img/hamilton/182365-Plate-Carrier.webp) | `PLT_CAR_L5MD_A00` | -| 'PLT_CAR_P3AC'
Part no.: 182365/03
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
Hamilton Deepwell Plate Carrier for 3 Plates (Portrait, 6 tracks wide)| ![](img/hamilton/PLT_CAR_P3AC.jpg) | `PLT_CAR_P3AC` | -| 'PLT_CAR_L5_DWP'
Part no.: 93522-01/03
manufacturer website?
Hamilton Plate Carrier for 5 Plates (Landscape, 6 tracks wide). Plastic tabs. | ![](img/hamilton/PLT_CAR_L5_DWP.jpg) | `PLT_CAR_L5_DWP` | +| 'PLT_CAR_L5MD_A00'
Part no.: 182365/02
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
Carries five ANSI/SLAS footprint MTPs in landscape orientation. Occupies six tracks. | ![](img/hamilton/182365-Plate-Carrier.webp) | `PLT_CAR_L5MD_A00` | +| 'PLT_CAR_P3AC_A00'
Part no.: 182365/03
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/182365)
Hamilton Deepwell Plate Carrier for 3 Plates (Portrait, 6 tracks wide) | ![](img/hamilton/PLT_CAR_P3AC.jpg) | `PLT_CAR_P3AC_A00` | +| 'PLT_CAR_L5_DWP'
Part no.: 93522-01/03
manufacturer website?
Hamilton Plate Carrier for 5 Plates (Landscape, 6 tracks wide). Plastic tabs. | ![](img/hamilton/PLT_CAR_L5_DWP.jpg) | `PLT_CAR_L5_DWP` | ### MFX carriers See [MFX Carrier documentation](/resources/carrier/mfx-carrier/mfx_carrier). -| Description | Image | PLR definition | -| - | - | - | -| 'hamilton_mfx_carrier_L5_base'
Part no.: 188039
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188039)
Labware carrier base for up to 5 Multiflex Modules
Occupies 6 tracks (6T). | ![](img/hamilton/hamilton_mfx_carrier_L5_base.jpg) | `hamilton_mfx_carrier_L5_base` | -| 'MFX_CAR_L4_SHAKER'
Part no.: 187001
[secondary supplier website](https://www.testmart.com/estore/unit.cfm/PIPPET/HAMROB/187001/automated_pippetting_devices_and_systems/8.html) (cannot find information on Hamilton website)
Sometimes referred to as "PLT_CAR_L4_SHAKER" by Hamilton.
Template carrier with 4 positions for Hamilton Heater Shaker.
Occupies 7 tracks (7T). Can be screwed onto the deck. | ![](img/hamilton/MFX_CAR_L4_SHAKER_187001.png) | `MFX_CAR_L4_SHAKER` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------ | +| 'hamilton_mfx_carrier_L5_base'
Part no.: 188039
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188039)
Labware carrier base for up to 5 Multiflex Modules
Occupies 6 tracks (6T). | ![](img/hamilton/hamilton_mfx_carrier_L5_base.jpg) | `hamilton_mfx_carrier_L5_base` | +| 'MFX_CAR_L4_SHAKER'
Part no.: 187001
[secondary supplier website](https://www.testmart.com/estore/unit.cfm/PIPPET/HAMROB/187001/automated_pippetting_devices_and_systems/8.html) (cannot find information on Hamilton website)
Sometimes referred to as "PLT_CAR_L4_SHAKER" by Hamilton.
Template carrier with 4 positions for Hamilton Heater Shaker.
Occupies 7 tracks (7T). Can be screwed onto the deck. | ![](img/hamilton/MFX_CAR_L4_SHAKER_187001.png) | `MFX_CAR_L4_SHAKER` | ### MFX modules See [MFX Carrier documentation](/resources/carrier/mfx-carrier/mfx_carrier). -| Description | Image | PLR definition | -| - | - | - | -| 'MFX_TIP_module'
Part no.: 188160 or 188040
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188040)
Module to position a high-, standard-, low volume or 5ml tip rack (but not a 384 tip rack) | ![](img/hamilton/MFX_TIP_module_188040.jpg) | `MFX_TIP_module` | -| 'hamilton_mfx_plateholder_DWP_flat'
Part no.: 188229
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188229) (<-non-functional link?)
MFX DWP module rack-based | ![](img/hamilton/MFX_DWP_RB_module_188229_.jpg) | `hamilton_mfx_plateholder_DWP_flat` | -| 'MFX_DWP_module_flat'
Part no.: 6601988-01
manufacturer website unknown | ![](img/hamilton/MFX_DWP_module_flat.jpg) | `MFX_DWP_module_flat` | -| 'Hamilton_mfx_plateholder_DWP_metal_tapped'
Part no.: 188042
[manufacturer website](https://www.hamiltoncompany.com/other-robotics/188042)
Carries a single ANSI/SLAS footprint DWP, a Matrix or Micronics tube rack, or a Nunc reagent reservoir. Occupies one MFX site.| ![](img/hamilton/hamilton_MFX_plateholder_DWP_metal_tapped.png) | `Hamilton_mfx_plateholder_DWP_metal_tapped` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------- | +| 'MFX_TIP_module'
Part no.: 188160 or 188040
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188040)
Module to position a high-, standard-, low volume or 5ml tip rack (but not a 384 tip rack) | ![](img/hamilton/MFX_TIP_module_188040.jpg) | `MFX_TIP_module` | +| 'hamilton_mfx_plateholder_DWP_flat'
Part no.: 188229
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188229) (<-non-functional link?)
MFX DWP module rack-based | ![](img/hamilton/MFX_DWP_RB_module_188229_.jpg) | `hamilton_mfx_plateholder_DWP_flat` | +| 'MFX_DWP_module_flat'
Part no.: 6601988-01
manufacturer website unknown | ![](img/hamilton/MFX_DWP_module_flat.jpg) | `MFX_DWP_module_flat` | +| 'hamilton_mfx_plateholder_DWP_metal_tapped'
Part no.: 188042
[manufacturer website](https://www.hamiltoncompany.com/other-robotics/188042)
Carries a single ANSI/SLAS footprint DWP, a Matrix or Micronics tube rack, or a Nunc reagent reservoir. Occupies one MFX site. | ![](img/hamilton/hamilton_MFX_plateholder_DWP_metal_tapped.png) | `hamilton_mfx_plateholder_DWP_metal_tapped` | ### Tube carriers Sometimes called "sample carriers" in Hamilton jargon. -| Description | Image | PLR definition | -| - | - | - | -| 'Tube_CAR_24_A00'
Part no.: 173400
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/173400)
Carries 24 "sample" tubes with 14.5–18 mm outer diameter, 60–120 mm high. Occupies one track. | ![](img/hamilton/Tube_CAR_24_A00.png) | `Tube_CAR_24_A00` | -| 'hamilton_tube_carrier_32_a00_insert_eppendorf_1_5mL'
Part no.: 173410
[manufacturer website](https://www.hamiltoncompany.com/other-robotics/173410)
Carries 32 `Eppendorf_DNA_LoBind_1_5ml_Vb` or `Eppendorf_Protein_LoBind_1_5ml_Vb` tubes. Occupies one track. | ![](img/hamilton/Tube_CAR_32_A00.png.avif) | `hamilton_tube_carrier_32_a00_insert_eppendorf_1_5mL` | -| 'hamilton_tube_carrier_12_b00'
Part no.: 182045
[manufacturer website](https://www.hamiltoncompany.com/other-robotics/50-ml-falcon-tube-carrier)
Carries 12 "sample" tubes with 30 mm outer diameter, 115 mm high. Occupies two tracks. | ![](img/hamilton/hamilton_tube_carrier_12_b00.jpg) | `hamilton_tube_carrier_12_b00` | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------- | +| 'Tube_CAR_24_A00'
Part no.: 173400
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/173400)
Carries 24 "sample" tubes with 14.5–18 mm outer diameter, 60–120 mm high. Occupies one track. | ![](img/hamilton/Tube_CAR_24_A00.png) | `Tube_CAR_24_A00` | +| 'hamilton_tube_carrier_32_a00_insert_eppendorf_1_5mL'
Part no.: 173410
[manufacturer website](https://www.hamiltoncompany.com/other-robotics/173410)
Carries 32 `Eppendorf_DNA_LoBind_1_5ml_Vb` or `Eppendorf_Protein_LoBind_1_5ml_Vb` tubes. Occupies one track. | ![](img/hamilton/Tube_CAR_32_A00.png.avif) | `hamilton_tube_carrier_32_a00_insert_eppendorf_1_5mL` | +| 'hamilton_tube_carrier_12_b00'
Part no.: 182045
[manufacturer website](https://www.hamiltoncompany.com/other-robotics/50-ml-falcon-tube-carrier)
Carries 12 "sample" tubes with 30 mm outer diameter, 115 mm high. Occupies two tracks. | ![](img/hamilton/hamilton_tube_carrier_12_b00.jpg) | `hamilton_tube_carrier_12_b00` | ### Trough carriers Sometimes called "reagent carriers" in Hamilton jargon. -| Description | Image | PLR definition | -| - | - | - | -| 'Trough_CAR_4R200_A00'
Part no.: 185436 (same as 96890-01?)
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/96890-01)
Trough carrier for 4x 200ml troughs. 2 tracks(T) wide. | ![](img/hamilton/Trough_CAR_4R200_A00.png) | `Trough_CAR_4R200_A00` | -| 'Trough_CAR_5R60_A00'
Part no.: 53646-01
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/reagent-reservoir-carrier-5-x-60-ml-2)
Trough carrier for 5x 60ml troughs. 1 track(T) wide. | ![](img/hamilton/Trough_CAR_5R60_A00.jpg) | `Trough_CAR_5R60_A00` | +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------- | +| 'Trough_CAR_4R200_A00'
Part no.: 185436 (same as 96890-01?)
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/96890-01)
Trough carrier for 4x 200ml troughs. 2 tracks(T) wide. | ![](img/hamilton/Trough_CAR_4R200_A00.png) | `Trough_CAR_4R200_A00` | +| 'Trough_CAR_5R60_A00'
Part no.: 53646-01
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/reagent-reservoir-carrier-5-x-60-ml-2)
Trough carrier for 5x 60ml troughs. 1 track(T) wide. | ![](img/hamilton/Trough_CAR_5R60_A00.jpg) | `Trough_CAR_5R60_A00` | ## Plate Adapters -| Description | Image | PLR definition | -| - | - | - | +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------- | | 'Hamilton_96_adapter_188182'
Part no.: 188182
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/188182) (<-non-functional link?)
Adapter for 96 well PCR plate, plunged. Does not have an ANSI/SLAS footprint -> requires assignment with specified location. | ![](img/hamilton/hamilton_96_adapter_188182.png) | `Hamilton_96_adapter_188182` | ## Consumables ### TipRacks -| Description | Image | PLR definition | -| - | - | - | -| Hamilton 96 tip rack 10uL.
Hamilton name: `LT`.
Part number 235900 (non-sterile), 235935 (sterile).
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/10-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_10uL.jpg.avif) | `hamilton_96_tiprack_10uL` | -| Hamilton 96 tip rack 10uL filter.
Hamilton name: `LTF`.
Part number: 235936 (sterile), 235901 (non-sterile).
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/10-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_10uL_filter.jpg.avif) | `hamilton_96_tiprack_10uL_filter` | -| Hamilton 96 tip rack 50uL.
Hamilton name: `TIP_50ul`
Part number: 235966 (non-sterile), 235978 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL.jpg.avif) | `hamilton_96_tiprack_50uL` | -| Hamilton 96 tip rack 50uL filter.
Hamilton name: `TIP_50ul_w_filter`
Part number: 235948 (non-sterile), 235979 (sterile), 235829 (clear, non-sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL_filter.jpg.avif) | `hamilton_96_tiprack_50uL_filter` | -| Hamilton 96 nested tip rack (NTR) 50uL.
Hamilton name: ?
Part number: 235947 (non-sterile), 235964 (clear, non-sterile), 235987 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL_NTR.jpg.avif) | `hamilton_96_tiprack_50uL_NTR` | -| Hamilton 96 tip rack 300uL.
Hamilton name: `ST`
Part number: 235834 (clear, non-sterile), 235902 (non-sterile), 235937 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/300-μl-co-re-ii-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL.jpg.avif) | `hamilton_96_tiprack_300uL` | -| Hamilton 96 tip rack 300uL filter.
Hamilton name: `STF`
Part number: 235830 (clear), 235903 (non-sterile), 235938 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/300-μl-co-re-ii-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL.jpg.avif) | `hamilton_96_tiprack_300uL_filter` | -| Hamilton 96 tip rack 300uL filter slim.
Hamilton name: `STF_Slim`
Part number: 235646
[Hamilton website](https://www.hamiltoncompany.com/consumables/300-µl-slim-conductive-tips?part-number=235646)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL_filter_slim.jpg.avif) | `hamilton_96_tiprack_300uL_filter_slim` | -| Hamilton 96 tip rack 300uL filter ultra wide bore.
Hamilton name: `STF_ULTRAWIDE`
Part number: 235449
[Hamilton website](https://www.hamiltoncompany.com/consumables/300-µl-co-re-ii-wide-bore-conductive-tips?part-number=235449)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL_filter_wide.avif) | `hamilton_96_tiprack_300uL_filter_ultrawide` | -| Hamilton 96 tip rack 1000uL.
Hamilton name: `HT`
Part number: 235822 (clear, non-sterile), 235904 (non-sterile), 235939 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/1000-µl-co-re-ii-disposable-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL.jpg.avif) | `hamilton_96_tiprack_1000uL` | -| Hamilton 96 tip rack 1000uL filter.
Hamilton name: `HTF`
Part number: 235820 (clear), 235905 (non-sterile), 235940 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/1000-µl-co-re-ii-disposable-tips)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL.jpg.avif) | `hamilton_96_tiprack_1000uL_filter` | -| Hamilton 96 tip rack 1000uL filter wide (1.2mm orifice Size).
Hamilton name: `HTF_WIDE`
Part number: 235678 (non-sterile), 235677 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables/1000-µl-co-re-ii-wide-bore-conductive-tips?part-number=235678)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL_filter_wide.jpg.avif) | `hamilton_96_tiprack_1000uL_filter_wide` | -| Hamilton 96 tip rack 1000uL filter ultra wide (3.2mm orifice Size).
Hamilton name: `HTF_ULTRAWIDE`
Part number: 235541 (non-sterile), 235842 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables/1000-µl-co-re-ii-wide-bore-conductive-tips?part-number=235541)| ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL_filter_ultrawide.jpg.avif) | `hamilton_96_tiprack_1000uL_filter_ultrawide` | -| Hamilton 24 tip rack 4000uL filter.
Hamilton name: `FourmlTF`
Part number: 184021 (non-sterile), 184023 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/4000-μl-5000-μl-co-re-tips)| ![](img/hamilton/tip_racks/hamilton_24_tiprack_4000uL_filter.jpg.avif) | `hamilton_24_tiprack_4000uL_filter` | -| Hamilton 24 tip rack 5000uL.
Hamilton name: `FivemlT`
Part number: 184020 (non-sterile), 184022 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/4000-μl-5000-μl-co-re-tips)| ![](img/hamilton/tip_racks/hamilton_24_tiprack_5000uL.jpg.avif) | `hamilton_24_tiprack_5000uL` | +| Description | Image | PLR definition | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------- | +| Hamilton 96 tip rack 10uL.
Hamilton name: `LT`.
Part number 235900 (non-sterile), 235935 (sterile).
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/10-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_10uL.jpg.avif) | `hamilton_96_tiprack_10uL` | +| Hamilton 96 tip rack 10uL filter.
Hamilton name: `LTF`.
Part number: 235936 (sterile), 235901 (non-sterile).
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/10-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_10uL_filter.jpg.avif) | `hamilton_96_tiprack_10uL_filter` | +| Hamilton 96 tip rack 50uL.
Hamilton name: `TIP_50ul`
Part number: 235966 (non-sterile), 235978 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL.jpg.avif) | `hamilton_96_tiprack_50uL` | +| Hamilton 96 tip rack 50uL filter.
Hamilton name: `TIP_50ul_w_filter`
Part number: 235948 (non-sterile), 235979 (sterile), 235829 (clear, non-sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL_filter.jpg.avif) | `hamilton_96_tiprack_50uL_filter` | +| Hamilton 96 nested tip rack (NTR) 50uL.
Hamilton name: ?
Part number: 235947 (non-sterile), 235964 (clear, non-sterile), 235987 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/50-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_50uL_NTR.jpg.avif) | `hamilton_96_tiprack_50uL_NTR` | +| Hamilton 96 tip rack 300uL.
Hamilton name: `ST`
Part number: 235834 (clear, non-sterile), 235902 (non-sterile), 235937 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/300-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL.jpg.avif) | `hamilton_96_tiprack_300uL` | +| Hamilton 96 tip rack 300uL filter.
Hamilton name: `STF`
Part number: 235830 (clear), 235903 (non-sterile), 235938 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/300-μl-co-re-ii-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL.jpg.avif) | `hamilton_96_tiprack_300uL_filter` | +| Hamilton 96 tip rack 300uL filter slim.
Hamilton name: `STF_Slim`
Part number: 235646
[Hamilton website](https://www.hamiltoncompany.com/consumables/300-µl-slim-conductive-tips?part-number=235646) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL_filter_slim.jpg.avif) | `hamilton_96_tiprack_300uL_filter_slim` | +| Hamilton 96 tip rack 300uL filter ultra wide bore.
Hamilton name: `STF_ULTRAWIDE`
Part number: 235449
[Hamilton website](https://www.hamiltoncompany.com/consumables/300-µl-co-re-ii-wide-bore-conductive-tips?part-number=235449) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_300uL_filter_wide.avif) | `hamilton_96_tiprack_300uL_filter_ultrawide` | +| Hamilton 96 tip rack 1000uL.
Hamilton name: `HT`
Part number: 235822 (clear, non-sterile), 235904 (non-sterile), 235939 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/1000-µl-co-re-ii-disposable-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL.jpg.avif) | `hamilton_96_tiprack_1000uL` | +| Hamilton 96 tip rack 1000uL filter.
Hamilton name: `HTF`
Part number: 235820 (clear), 235905 (non-sterile), 235940 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/1000-µl-co-re-ii-disposable-tips) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL.jpg.avif) | `hamilton_96_tiprack_1000uL_filter` | +| Hamilton 96 tip rack 1000uL filter wide (1.2mm orifice Size).
Hamilton name: `HTF_WIDE`
Part number: 235678 (non-sterile), 235677 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables/1000-µl-co-re-ii-wide-bore-conductive-tips?part-number=235678) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL_filter_wide.jpg.avif) | `hamilton_96_tiprack_1000uL_filter_wide` | +| Hamilton 96 tip rack 1000uL filter ultra wide (3.2mm orifice Size).
Hamilton name: `HTF_ULTRAWIDE`
Part number: 235541 (non-sterile), 235842 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables/1000-µl-co-re-ii-wide-bore-conductive-tips?part-number=235541) | ![](img/hamilton/tip_racks/hamilton_96_tiprack_1000uL_filter_ultrawide.jpg.avif) | `hamilton_96_tiprack_1000uL_filter_ultrawide` | +| Hamilton 24 tip rack 4000uL filter.
Hamilton name: `FourmlTF`
Part number: 184021 (non-sterile), 184023 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/4000-μl-5000-μl-co-re-tips) | ![](img/hamilton/tip_racks/hamilton_24_tiprack_4000uL_filter.jpg.avif) | `hamilton_24_tiprack_4000uL_filter` | +| Hamilton 24 tip rack 5000uL.
Hamilton name: `FivemlT`
Part number: 184020 (non-sterile), 184022 (sterile)
[Hamilton website](https://www.hamiltoncompany.com/consumables-labware-accessories/co-re-and-co-re-ii-tips/4000-μl-5000-μl-co-re-tips) | ![](img/hamilton/tip_racks/hamilton_24_tiprack_5000uL.jpg.avif) | `hamilton_24_tiprack_5000uL` | ### Troughs -| Description | Image | PLR definition | -| - | - | - | -| 'Hamilton_1_trough_60mL_Vb'
Part no.: 56694-01 (natural/white), 56694-02 (black), 56694-03 (black, bulk)
[manufacturer website](https://www.hamiltoncompany.com/consumables/60-ml-reagent-reservoirs)
Trough 60ml, w lid, self-standing. Barcode-compliant.
Compatible with Trough_CAR_5R60_A00 (53646-01). | ![](img/hamilton/hamilton_1_trough_60mL_Vb_56694-01.png) | `hamilton_1_trough_60mL_Vb` | -| 'hamilton_1_trough_120ml_Vb'
Part no.: 194052 (white/translucent)
[manufacturer website](https://www.hamiltoncompany.com/consumables/120-ml-reagent-reservoir-self-standing-with-barcode-label?srsltid=AfmBOoobNQhgF9KPbwURilJLIDO_pJhS3AnUXMRQ5QATOYMemUnU-aUU)
Trough 120mL, without lid, self-standing, transparent.
Compatible with Trough_CAR_3R120 (194058). | ![](img/hamilton/hamilton_1_trough_120mL_Vb.png) | `hamilton_1_trough_120mL_Vb` | -| 'Hamilton_1_trough_200mL_Vb'
Part no.: 56695-01 (white/translucent), 56695-02 (black/conductive)
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/56695-02)
Trough 200ml, w lid, self-standing, Black.
Compatible with Trough_CAR_4R200_A00 (185436). | ![](img/hamilton/hamilton_1_trough_200ml_Vb.jpg) | `hamilton_1_trough_200mL_Vb` | +| Description | Image | PLR definition | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------- | +| 'Hamilton_1_trough_60mL_Vb'
Part no.: 56694-01 (natural/white), 56694-02 (black), 56694-03 (black, bulk)
[manufacturer website](https://www.hamiltoncompany.com/consumables/60-ml-reagent-reservoirs)
Trough 60ml, w lid, self-standing. Barcode-compliant.
Compatible with Trough_CAR_5R60_A00 (53646-01). | ![](img/hamilton/hamilton_1_trough_60mL_Vb_56694-01.png) | `hamilton_1_trough_60mL_Vb` | +| 'hamilton_1_trough_120ml_Vb'
Part no.: 194052 (white/translucent)
[manufacturer website](https://www.hamiltoncompany.com/consumables/120-ml-reagent-reservoir-self-standing-with-barcode-label?srsltid=AfmBOoobNQhgF9KPbwURilJLIDO_pJhS3AnUXMRQ5QATOYMemUnU-aUU)
Trough 120mL, without lid, self-standing, transparent.
Compatible with Trough_CAR_3R120 (194058). | ![](img/hamilton/hamilton_1_trough_120mL_Vb.png) | `hamilton_1_trough_120mL_Vb` | +| 'Hamilton_1_trough_200mL_Vb'
Part no.: 56695-01 (white/translucent), 56695-02 (black/conductive)
[manufacturer website](https://www.hamiltoncompany.com/automated-liquid-handling/other-robotics/56695-02)
Trough 200ml, w lid, self-standing, Black.
Compatible with Trough_CAR_4R200_A00 (185436). | ![](img/hamilton/hamilton_1_trough_200ml_Vb.jpg) | `hamilton_1_trough_200mL_Vb` | diff --git a/docs/resources/library/imcs.md b/docs/resources/library/imcs.md index 91df7cbaaec..1506a49114c 100644 --- a/docs/resources/library/imcs.md +++ b/docs/resources/library/imcs.md @@ -8,6 +8,6 @@ Automated high-throughput protein purification ## TipRacks -| Description | Image | PLR definition | -| - | - | - | -|IMCS tips for automated protein purification. 300uL. Part numbers:
  • 04T-H8R80A-1-5-96
  • 04T-H8R80A-1-10-96
  • 04T-H8R80P-1-5-96
  • 04T-H8R80P-1-10-96
  • 04T-H8R72-1-2-96
  • 04T-H8R72-1-5-96
  • 04T-H8R72-1-10-96
  • 04T-H8R72Q-1-10-96
  • 04T-H8R85P-1-5-96
  • 04T-H8R85P-1-10-96
  • 04T-H8R88F-1-2-96
  • 04T-H8R88F-1-5-96
  • 04T-H8R88F-1-10-96
  • 04T-H8R89-1-10-96
  • 04T-H8D20F-1A-3-96
  • 04T-H8CD20F-1A-3-96
  • 04T-H8R68-1-5-96
  • 04T-H8R73-1-10-96
  • 04T-H8R05-1-2-96
  • 04T-H8R05-1-5-96
  • 04T-H8R41-1-2-96
  • 04T-H8R53-1-2-96
  • 04T-H8R52-1-2-96
  • 04T-H8R52-1-5-96
  • 04T-H8R30-1-2-96
  • 04T-H8R30-1-5-96
  • 04T-H8R03R-1-5-96
  • 04T-H8R02R-1-2-96
  • 04T-H8R02R-1-5-96
  • 04T-I3R73-1-10-

[OEM website](https://imcstips.com/) | picture not available | `imcs_96_tiprack_300uL_filter` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------------------------ | +| IMCS tips for automated protein purification, 300 uL.
Part no.: 04T-H8R80A-1-5-96, 04T-H8R80A-1-10-96, 04T-H8R80P-1-5-96, 04T-H8R80P-1-10-96, 04T-H8R72-1-2-96, 04T-H8R72-1-5-96, 04T-H8R72-1-10-96, 04T-H8R72Q-1-10-96, 04T-H8R85P-1-5-96, 04T-H8R85P-1-10-96, 04T-H8R88F-1-2-96, 04T-H8R88F-1-5-96, 04T-H8R88F-1-10-96, 04T-H8R89-1-10-96, 04T-H8D20F-1A-3-96, 04T-H8CD20F-1A-3-96, 04T-H8R68-1-5-96, 04T-H8R73-1-10-96, 04T-H8R05-1-2-96, 04T-H8R05-1-5-96, 04T-H8R41-1-2-96, 04T-H8R53-1-2-96, 04T-H8R52-1-2-96, 04T-H8R52-1-5-96, 04T-H8R30-1-2-96, 04T-H8R30-1-5-96, 04T-H8R03R-1-5-96, 04T-H8R02R-1-2-96, 04T-H8R02R-1-5-96
[OEM website](https://imcstips.com/) | | `imcs_96_tiprack_300uL_filter` | diff --git a/docs/resources/library/nest.md b/docs/resources/library/nest.md index 19ee44fdf53..3cb6c0546dc 100644 --- a/docs/resources/library/nest.md +++ b/docs/resources/library/nest.md @@ -4,10 +4,10 @@ Wuxi NEST Biotechnology Co., Ltd. a leading life science plastic consumables man ## Plates -| Description | Image | PLR definition | -|-|-|-| -| 'nest_1_troughplate_195000uL_Vb'
Part no.: 360101
[manufacturer website](https://www.nest-biotech.com/reagent-reserviors/59178416.html)
- Material: polypropylene | ![](img/nest/nest_1_troughplate_195000uL_Vb.webp) | `nest_1_troughplate_195000uL_Vb` | -| 'nest_1_troughplate_185000uL_Vb'
Part no.: 360101
[manufacturer website](https://www.nest-biotech.com/reagent-reserviors/59178415.html)
- Material: polypropylene | ![](img/nest/nest_1_troughplate_185000uL_Vb.webp) | `nest_1_troughplate_185000uL_Vb` | -| 'nest_8_troughplate_22000uL_Vb'
Part no.: 360101
[manufacturer website](https://www.nestscientificusa.com/product/detail/513006470820794368)
- Material: polypropylene | ![](img/nest/nest_8_troughplate_22000uL_Vb.jpg) | `nest_8_troughplate_22000uL_Vb` | -| 'nest_12_troughplate_15000uL_Vb'
Part no.: 360102
[manufacturer website](https://www.nestscientificusa.com/product/detail/513006470820794368)
- Material: polypropylene | ![](img/nest/nest_12_troughplate_15000uL_Vb.jpg) | `nest_12_troughplate_15000uL_Vb` | -| 'NEST_96_wellplate_2200uL_Ub'
Part no.: 503062
[manufacturer website](https://www.nest-biotech.com/deep-well-plates/59253727.html)
- Material: polypropylene | ![](img/nest/NEST_96_wellplate_2200uL_Ub.png) | `NEST_96_wellplate_2200uL_Ub` | \ No newline at end of file +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | -------------------------------- | +| 'nest_1_troughplate_195000uL_Vb'
Part no.: 360101
[manufacturer website](https://www.nest-biotech.com/reagent-reserviors/59178416.html)
- Material: polypropylene | ![](img/nest/nest_1_troughplate_195000uL_Vb.webp) | `nest_1_troughplate_195000uL_Vb` | +| 'nest_1_troughplate_185000uL_Vb'
Part no.: 360101
[manufacturer website](https://www.nest-biotech.com/reagent-reserviors/59178415.html)
- Material: polypropylene | ![](img/nest/nest_1_troughplate_185000uL_Vb.webp) | `nest_1_troughplate_185000uL_Vb` | +| 'nest_8_troughplate_22000uL_Vb'
Part no.: 360101
[manufacturer website](https://web.archive.org/web/20240909185532/https://www.nestscientificusa.com/product/detail/513006470820794368)
- Material: polypropylene | ![](img/nest/nest_8_troughplate_22000uL_Vb.jpg) | `nest_8_troughplate_22000uL_Vb` | +| 'nest_12_troughplate_15000uL_Vb'
Part no.: 360102
[manufacturer website](https://web.archive.org/web/20240909185532/https://www.nestscientificusa.com/product/detail/513006470820794368)
- Material: polypropylene | ![](img/nest/nest_12_troughplate_15000uL_Vb.jpg) | `nest_12_troughplate_15000uL_Vb` | +| 'NEST_96_wellplate_2200uL_Ub'
Part no.: 503062
[manufacturer website](https://www.nest-biotech.com/deep-well-plates/59253727.html)
- Material: polypropylene | ![](img/nest/NEST_96_wellplate_2200uL_Ub.png) | `NEST_96_wellplate_2200uL_Ub` | \ No newline at end of file diff --git a/docs/resources/library/opentrons.md b/docs/resources/library/opentrons.md index fa1f2a39187..992768f311d 100644 --- a/docs/resources/library/opentrons.md +++ b/docs/resources/library/opentrons.md @@ -40,8 +40,8 @@ Unfortunately, most of the other labware (plates) is missing information that is - `opentrons_24_tuberack_nest_1_5ml_screwcap` - `opentrons_24_tuberack_nest_1_5ml_snapcap` -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | | 'opentrons_24_tuberack_generic_1point5ml_snapcap_short'
Part no.: 3405002
[manufacturer website](https://www.thingiverse.com/thing:3405002) | ![](img/opentrons/ot2-1.5mL-tube-rack-24w.png) | `opentrons_24_tuberack_generic_1point5ml_snapcap_short` | - `opentrons_24_tuberack_nest_2ml_screwcap` diff --git a/docs/resources/library/perkin_elmer.md b/docs/resources/library/perkin_elmer.md index 20d8278351b..2303621dbbc 100644 --- a/docs/resources/library/perkin_elmer.md +++ b/docs/resources/library/perkin_elmer.md @@ -2,6 +2,6 @@ ## Plates -| Description | Image | PLR definition | -|-|-|-| +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------- | | 'PerkinElmer_96_wellplate_400ul_Fb'
Part no.: 6005680, 6005688 and 6005689
[manufacturer website](https://www.revvity.com/product/culturplate-96-lid-50w-6055680) | ![](img/perkin_elmer/PerkinElmer_96_wellplate_400ul_Fb.webp) | `PerkinElmer_96_wellplate_400ul_Fb` | diff --git a/docs/resources/library/porvair.md b/docs/resources/library/porvair.md index cb3b8d531ab..951b596a93a 100644 --- a/docs/resources/library/porvair.md +++ b/docs/resources/library/porvair.md @@ -6,12 +6,12 @@ Company history: [Porvair Filtration Group](https://www.porvairfiltration.com/ab ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------- | | 'Porvair_24_wellplate_Vb'
Part no.: 390108
[manufacturer website](https://www.microplates.com/product/78-ml-reservoir-plate-2-rows-of-12-v-bottom/) | ![](img/porvair/Porvair_24_wellplate_Vb.jpg) | `Porvair_24_wellplate_Vb` | ## Reservoirs -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Porvair_6_reservoir_47ml_Vb'
Part no.: 6008280
[manufacturer website](https://www.microplates.com/product/282-ml-reservoir-plate-6-columns-v-bottom/)
- Material: Polypropylene
- Sterilization compatibility: Autoclaving (15 minutes at 121°C) or Gamma Irradiation
- Chemical resistance: "High chemical resistance"
- Temperature resistance: high: -196°C to + 121°C
- Cleanliness: 390015: Free of detectable DNase, RNase
- ANSI/SLAS-format for compatibility with automated systems
- Tolerances: "Uniform external dimensions and tolerances"| ![](img/porvair/porvair_6x47_reservoir_390015.jpg) | `Porvair_6_reservoir_47ml_Vb` | +| Description | Image | PLR definition | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------- | +| 'Porvair_6_reservoir_47ml_Vb'
Part no.: 6008280
[manufacturer website](https://www.microplates.com/product/282-ml-reservoir-plate-6-columns-v-bottom/)
- Material: Polypropylene
- Sterilization compatibility: Autoclaving (15 minutes at 121°C) or Gamma Irradiation
- Chemical resistance: "High chemical resistance"
- Temperature resistance: high: -196°C to + 121°C
- Cleanliness: 390015: Free of detectable DNase, RNase
- ANSI/SLAS-format for compatibility with automated systems
- Tolerances: "Uniform external dimensions and tolerances" | ![](img/porvair/porvair_6x47_reservoir_390015.jpg) | `Porvair_6_reservoir_47ml_Vb` | diff --git a/docs/resources/library/revvity.md b/docs/resources/library/revvity.md index fcdacfd40da..a0189140a0d 100644 --- a/docs/resources/library/revvity.md +++ b/docs/resources/library/revvity.md @@ -6,6 +6,6 @@ Company wikipedia: [Revvity, Inc. (formerly PerkinElmer, Inc.)](https://en.wikip ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Revvity_384_wellplate_28ul_Ub'
Part no.: 6008280
[manufacturer website](https://www.revvity.com/product/proxiplate-384-plus-50w-6008280) | ![](img/revvity/Revvity_384_wellplate_28ul_Ub.jpg) | `Revvity_384_wellplate_28ul_Ub` +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------- | +| 'Revvity_384_wellplate_28ul_Ub'
Part no.: 6008280
[manufacturer website](https://www.revvity.com/product/proxiplate-384-plus-50w-6008280) | ![](img/revvity/Revvity_384_wellplate_28ul_Ub.jpg) | `Revvity_384_wellplate_28ul_Ub` | diff --git a/docs/resources/library/sergi.md b/docs/resources/library/sergi.md index 0b05975b281..48a53085ce9 100644 --- a/docs/resources/library/sergi.md +++ b/docs/resources/library/sergi.md @@ -2,8 +2,8 @@ Company page: [Sergi Lab Supplies](https://sergilabsupplies.com/?srsltid=AfmBOoqk2e3QkpWxvWEtSXS4ySJVoly7hvdiji_ehH5-s6tM3gi67SMu) -## Plate Adapterrs +## Plate Adapters -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------- | | 'SergiLabSupplies_96_MagneticRack_250ul_Vb'
Part no.: 1047
[manufacturer website](https://sergilabsupplies.com/collections/magnetic-racks/products/96-wells-magnetic-rack-for-dna-rna-and-other-molecules-purification)
A separator for purifying DNA, RNA or other biomolecules with magnetic beads | ![](img/sergi/SergiLabSupplies_96_MagneticRack_250ul_Vb.jpg) | `SergiLabSupplies_96_MagneticRack_250ul_Vb` | diff --git a/docs/resources/library/thermo_fisher.md b/docs/resources/library/thermo_fisher.md index 7cb7482834b..52752b1aa79 100644 --- a/docs/resources/library/thermo_fisher.md +++ b/docs/resources/library/thermo_fisher.md @@ -26,23 +26,23 @@ Thermo Fisher Scientific Inc. (TFS, aka "Thermo") ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'Thermo_TS_96_wellplate_1200ul_Rb'
Part no.: AB-1127 or 10243223
[manufacturer website](https://www.fishersci.co.uk/shop/products/product/10243223)

- Material: Polypropylene (AB-1068, polystyrene)
| ![](img/thermo_fisher/Thermo_TS_96_wellplate_1200ul_Rb.webp) | `Thermo_TS_96_wellplate_1200ul_Rb` | -| 'Thermo_TS_Nunc_96_wellplate_300uL_Fb'
Part no.: 165305
[manufacturer website](https://www.fishersci.com/shop/products/nunc-microwell-96-well-cell-culture-treated-flat-bottom-microplate/1256670#)
| ![](img/thermo_fisher/Thermo_TS_Nunc_96_wellplate_300uL_Fb.webp) | `Thermo_TS_Nunc_96_wellplate_300uL_Fb` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | --------------------------------------------- | +| 'Thermo_TS_96_wellplate_1200ul_Rb'
Part no.: AB-1127 or 10243223
[manufacturer website](https://www.fishersci.co.uk/shop/products/product/10243223)

- Material: Polypropylene (AB-1068, polystyrene)
| ![](img/thermo_fisher/Thermo_TS_96_wellplate_1200ul_Rb.webp) | `Thermo_TS_96_wellplate_1200ul_Rb` | +| 'Thermo_TS_Nunc_96_wellplate_300uL_Fb'
Part no.: 165305
[manufacturer website](https://www.fishersci.com/shop/products/nunc-microwell-96-well-cell-culture-treated-flat-bottom-microplate/1256670#)
| ![](img/thermo_fisher/Thermo_TS_Nunc_96_wellplate_300uL_Fb.webp) | `Thermo_TS_Nunc_96_wellplate_300uL_Fb` | | 'Thermo_AB_96_wellplate_300ul_Vb_EnduraPlate'
Part no.: 4483354 (TFS) or 15273005 (FS) (= with barcode)
Part no.: 16698853 (FS) (= **without** barcode)
[manufacturer website](https://www.thermofisher.com/order/catalog/product/4483354)

- Material: Polycarbonate, Polypropylene
- plate_type: semi-skirted
- product line: "MicroAmp"
- (sub)product line: "EnduraPlate" | ![](img/thermo_fisher/Thermo_AB_96_wellplate_300ul_Vb_EnduraPlate.png) | `Thermo_AB_96_wellplate_300ul_Vb_EnduraPlate` | -| 'Thermo_Nunc_96_well_plate_1300uL_Rb'
Part no.: 26025X | ![](img/thermo_fisher/Thermo_Nunc_96_well_plate_1300uL_Rb.jpg) | `Thermo_Nunc_96_well_plate_1300uL_Rb` | -| 'thermo_AB_96_wellplate_300ul_Vb_MicroAmp'
Part no.: N8010560/4316813 (w/o barcode)
Part no.: 4306737/4326659 (with barcode) | ![](img/thermo_fisher/thermo_AB_96_wellplate_300ul_Vb_MicroAmp.webp) | `thermo_AB_96_wellplate_300ul_Vb_MicroAmp` | -| 'thermo_AB_384_wellplate_40uL_Vb_MicroAmp'
Part no.: 4309849, 4326270, 4343814 (with barcode), 4343370 (w/o barcode). | ![](img/thermo_fisher/thermo_AB_384_wellplate_40uL_Vb_MicroAmp.jpg) | `thermo_AB_384_wellplate_40uL_Vb_MicroAmp` | +| 'Thermo_Nunc_96_well_plate_1300uL_Rb'
Part no.: 26025X | ![](img/thermo_fisher/Thermo_Nunc_96_well_plate_1300uL_Rb.jpg) | `Thermo_Nunc_96_well_plate_1300uL_Rb` | +| 'thermo_AB_96_wellplate_300ul_Vb_MicroAmp'
Part no.: N8010560/4316813 (w/o barcode)
Part no.: 4306737/4326659 (with barcode) | ![](img/thermo_fisher/thermo_AB_96_wellplate_300ul_Vb_MicroAmp.webp) | `thermo_AB_96_wellplate_300ul_Vb_MicroAmp` | +| 'thermo_AB_384_wellplate_40uL_Vb_MicroAmp'
Part no.: 4309849, 4326270, 4343814 (with barcode), 4343370 (w/o barcode). | ![](img/thermo_fisher/thermo_AB_384_wellplate_40uL_Vb_MicroAmp.jpg) | `thermo_AB_384_wellplate_40uL_Vb_MicroAmp` | ## Troughs -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'thermo_nunc_1_troughplate_90000uL_Fb_omnitray'
Part no.: 165218, 140156, 242811, 264728 | ![](img/thermo_fisher/thermo_nunc_1_troughplate_90000uL_Fb_omnitray.jpg.avif) | `thermo_nunc_1_troughplate_90000uL_Fb_omnitray` | -| 'ThermoFisherMatrixTrough8094'
Part no.: 8094
[manufacturer website](https://www.thermofisher.com/order/catalog/product/8094) | ![](img/thermo_fisher/ThermoFisherMatrixTrough8094.jpg.avif) | `ThermoFisherMatrixTrough8094` | -| 'thermo_TS_nalgene_1_troughplate_300mL_Fb'
Part no.: 12001300 (non-sterile), 12001301 (sterile)
[manufacturer website](https://www.fishersci.com/shop/products/nalgene-disposable-polypropylene-robotic-reservoirs/12565571)| ![](img/thermo_fisher/thermo_TS_nalgene_1_troughplate_300mL_Fb.jpeg) | `thermo_TS_nalgene_1_troughplate_300mL_Fb` | +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------- | +| 'thermo_nunc_1_troughplate_90000uL_Fb_omnitray'
Part no.: 165218, 140156, 242811, 264728 | ![](img/thermo_fisher/thermo_nunc_1_troughplate_90000uL_Fb_omnitray.jpg.avif) | `thermo_nunc_1_troughplate_90000uL_Fb_omnitray` | +| 'ThermoFisherMatrixTrough8094'
Part no.: 8094
[manufacturer website](https://www.thermofisher.com/order/catalog/product/8094) | ![](img/thermo_fisher/ThermoFisherMatrixTrough8094.jpg.avif) | `ThermoFisherMatrixTrough8094` | +| 'thermo_TS_nalgene_1_troughplate_300mL_Fb'
Part no.: 12001300 (non-sterile), 12001301 (sterile)
[manufacturer website](https://www.fishersci.com/shop/products/nalgene-disposable-polypropylene-robotic-reservoirs/12565571) | ![](img/thermo_fisher/thermo_TS_nalgene_1_troughplate_300mL_Fb.jpeg) | `thermo_TS_nalgene_1_troughplate_300mL_Fb` | ## Plate Adapters -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| -| 'thermo_AB_96_plateadapter_MicroAmp'
Part no.: 4312063
[manufacturer website](https://www.thermofisher.com/order/catalog/product/4312063)| ![](img/thermo_fisher/thermo_AB_96_plateadapter_MicroAmp.jpg) | `thermo_AB_96_plateadapter_MicroAmp` | +| Description | Image | PLR definition | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------ | +| 'thermo_AB_96_plateadapter_MicroAmp'
Part no.: 4312063
[manufacturer website](https://www.thermofisher.com/order/catalog/product/4312063) | ![](img/thermo_fisher/thermo_AB_96_plateadapter_MicroAmp.jpg) | `thermo_AB_96_plateadapter_MicroAmp` | diff --git a/docs/resources/library/vwr.md b/docs/resources/library/vwr.md index 62cae2f87ce..4b4445edb3d 100644 --- a/docs/resources/library/vwr.md +++ b/docs/resources/library/vwr.md @@ -4,13 +4,13 @@ Company page: [Wikipedia](https://en.wikipedia.org/wiki/VWR_International) ## Troughs -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -------------------------- | | 'VWRReagentReservoirs25mL'
Part no.: 89094
[manufacturer website](https://us.vwr.com/store/product/4694822/vwr-disposable-pipetting-reservoirs)
Polystyrene Reservoirs | ![](img/vwr/VWRReagentReservoirs25mL.jpg) | `VWRReagentReservoirs25mL` | ## Plates -| Description | Image | PLR definition | -|--------------------|--------------------|--------------------| +| Description | Image | PLR definition | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------- | | 'VWR_1_troughplate_195000uL_Ub'
Part no.: 77575-302
[manufacturer website](https://www.avantorsciences.com/us/en/product/47763965/vwr-multi-channel-polypropylene-reagent-reservoirs?isCatNumSearch=true&searchedCatalogNumber=77575-302)
Polypropylene multi-channel reagent reservoirs | ![](img/vwr/VWR_1_troughplate_195000uL_Ub.jpg) | `VWR_1_troughplate_195000uL_Ub` | -| 'VWR_96_wellplate_2mL_Vb'
Part no.: 76329-998
[manufacturer website](https://us-prod2.vwr.com/store/product/26915641/vwr-96-well-deep-well-plates-with-automation-notches)
Polypropylene multi-channel reagent reservoirs | ![](img/vwr/VWR_96_wellplate_2mL_Vb.jpg) | `VWR_96_wellplate_2mL_Vb` | +| 'VWR_96_wellplate_2mL_Vb'
Part no.: 76329-998
[manufacturer website](https://us.vwr.com/store/product/26915641/vwr-96-well-deep-well-plates-with-automation-notches)
Polypropylene multi-channel reagent reservoirs | ![](img/vwr/VWR_96_wellplate_2mL_Vb.jpg) | `VWR_96_wellplate_2mL_Vb` | diff --git a/docs/resources/library_convention.md b/docs/resources/library_convention.md new file mode 100644 index 00000000000..0a43b74f767 --- /dev/null +++ b/docs/resources/library_convention.md @@ -0,0 +1,210 @@ +--- +orphan: true +--- + +# Resource Library file convention (PROPOSAL — for review) + +Status: **proposal, not yet enforced or wired.** This describes the target +structure for `docs/resources/library/.md` so that (a) the full +manufacturer / OEM information is captured deterministically and (b) the +resources are organised into a consistent, machine-readable hierarchy that the +Resource Library can render and CI can validate. + +It deliberately replaces heuristics in today's library extension ("first link +in the preamble", "first blockquote", "first fenced code block") with explicit, +reserved structure. + +--- + +## 1. One file per manufacturer + +- Exactly **one H1**, the first heading in the file: the canonical manufacturer + name. This is both the displayed name and the de-duplication key. + - `# Corning Inc.` +- The filename is the slug (`corning.md`); it is *not* the source of the name. + +## 2. OEM metadata (explicit, labelled — never inferred) + +Immediately after the H1, an **optional** bullet list of labelled links. The +list, any individual key, and the whole block are all optional — many +manufacturers (smaller OEMs, regional suppliers, the DIY entries) have no +external page at all, and that is normal, not a violation. + +Recognised keys (case-insensitive, value = one absolute URL): + +- **`Website:`** — the manufacturer's own page. This is the only key worth + treating as the "primary" reference when present. +- Any number of *additional, optional* labelled links, e.g. + **`Wikipedia:`**, **`Catalog:`**, **`Datasheet:`**. **None of these is + expected** — `Wikipedia:` in particular must not be assumed, since most + manufacturers have no Wikipedia page. + +```markdown +- **Website:** https://www.corning.com +- **Wikipedia:** https://en.wikipedia.org/wiki/Corning_Inc. (optional extra) +``` + +A file with no links is fully conformant. **Fallback:** no metadata → no +reference link in the panel; only `Website:` → a "Website ↗" link; a +`Wikipedia:` present → an additional "Wikipedia ↗" link. The panel never +implies a missing key should exist. + +Rationale: today the library grabs "whichever link appears first", so some +manufacturers surface a Wikipedia link and others a homepage purely by +ordering accident. Explicit, optional labels remove the ambiguity without +making any particular source mandatory. + +## 3. Reserved info sections (all optional) + +These headings are **reserved** and **optional**. When present they hold +manufacturer information and are **excluded from the resource organisation +tree**; when absent the panel simply omits that part — never an error. The +names, *if used*, must be spelled exactly, but **no file is required to have +them**. The only mandatory content is §1 (one H1) and §5 (at least one +resource table whose definitions resolve); everything in this section is +optional enrichment. + +### `## About` (optional) + +A single descriptive paragraph (plain prose or one blockquote). Replaces the +"first blockquote in the preamble" heuristic. +**Fallback:** if absent, no description paragraph is shown — not an error. + +```markdown +## About + +Corning Incorporated is an American multinational technology company that +specialises in specialty glass, ceramics, and related materials. +``` + +### `## Brand structure` (optional) + +A human-curated overview of the manufacturer's brand hierarchy, as a fenced +code block (ASCII tree) and/or prose. This is narrative context for humans; it +is **not** the source of truth for how resources are organised (see §4). +**Fallback:** if absent, the panel shows only the heading-derived breakdown +from §4 (which every file has) — not an error. + +```markdown +## Brand structure + +​``` +Thermo Fisher Scientific Inc. +├── Applied Biosystems +│ └── MicroAmp +└── Thermo Scientific + ├── Nalgene + └── Nunc +​``` +``` + +## 4. Resource organisation = heading nesting + +The organisational tree the library renders is derived **only** from the +`##` / `###` / `####` heading nesting of the non-reserved sections — not from +the `## Brand structure` art (which only a few files have today). + +- `##` = top-level grouping. A manufacturer chooses one consistent axis: + **brand-first** (`## Costar`, `## Axygen`) **or** **category-first** + (`## Plates`, `## Tip Racks`) — not a mix. +- `###` / `####` = finer grouping under that (e.g. `## Costar` → `### Plates`). +- A section that directly contains resources must end in a definition table. + +## 5. Definition table (fixed shape) + +Every leaf section that lists resources contains exactly one table with this +exact header: + +```markdown +| Description | Image | PLR definition | +|-|-|-| +|
Part no.:
[manufacturer website]() | ![](img//) | `Exact_PLR_Definition` | +``` + +Rules per row: + +- **Description**: free text. Optional `Part no.: …` and a + `[manufacturer website]()` link, `
`-separated. +- **Image**: a markdown image whose path resolves to a real file under + `docs/resources/library/img//…`, or an absolute URL. +- **PLR definition**: exactly **one** backticked identifier matching + `^[A-Za-z][A-Za-z0-9_]*$`, and it must resolve to a callable in + `pylabrobot.resources`. One definition per row. + +## 6. Worked example + +```markdown +# Corning Inc. + +- **Website:** https://www.corning.com +- **Wikipedia:** https://en.wikipedia.org/wiki/Corning_Inc. + +## About + +> Corning Incorporated is an American multinational technology company +> specialising in specialty glass and ceramics. + +## Costar + +### Plates + +| Description | Image | PLR definition | +|-|-|-| +| 96-well, 2 mL, V-bottom
Part no.: 3960
[manufacturer website](https://ecatalog.corning.com/3960) | ![](img/corning/Cor_96_wellplate_2mL_Vb.jpg) | `Cor_96_wellplate_2mL_Vb` | + +## Axygen + +### Plates + +| Description | Image | PLR definition | +|-|-|-| +| 384-well PCR, V-bottom
Part no.: PCR-384 | ![](img/corning/Axy_384.jpg) | `Axy_384_wellplate_50uL_Vb` | +``` + +--- + +## What CI would enforce (objective) + +Mechanically checkable, proposed to **fail** CI on violation. Split into the +mandatory core and conditional checks that only apply to *optional* content +when it is present. + +**Required (mandatory core — every file):** + +1. Exactly one H1. +2. At least one resource table, and every PLR definition in it is a single + regex-valid identifier that resolves to a real `pylabrobot.resources` + factory. +3. Every definition table matches the fixed header. +4. Every image path resolves to an existing file. +5. No duplicate PLR definition names across the whole library. + +**Conditional (optional content — checked only if present):** + +6. OEM metadata list, *if present*, uses only recognised labels with valid + absolute URLs. +7. Reserved sections are **not required**. A section titled exactly + `## About` or `## Brand structure` is treated as reserved (excluded from + the resource tree); near-miss spellings are flagged so they are not + silently parsed as resource categories. +8. No resource table placed where the parser expects an info section, or + vice-versa. + +## What is NOT in scope for CI (author / project decision) + +These are real consistency issues but are **naming/structure policy**, not +mechanics, and tie into the open resource library / +plural-vs-singular discussion. The spec *recommends* a rule but the project +owner decides; CI should not impose it: + +- Heading **naming**: Title Case, singular vs plural ("Tip Rack" vs + "TipRacks" vs "Tip racks"), and the typo class ("Plate Adapterrs"). + *Recommendation:* Title Case, singular category nouns. +- Whether organisation is **brand-first or category-first** per manufacturer. + +## Migration note + +Adopting this fully requires: updating the library extension to parse the +explicit metadata + reserved sections + heading-derived tree, migrating the +~23 existing vendor files, then enabling the CI. That is a separate, +author-coordinated effort; this document is the spec to agree on first. diff --git a/docs/resources/library_convention_example.md b/docs/resources/library_convention_example.md new file mode 100644 index 00000000000..b425429fb6a --- /dev/null +++ b/docs/resources/library_convention_example.md @@ -0,0 +1,75 @@ +--- +orphan: true +--- + +# Example vendor file (reference for library_convention.md) + +This is a complete, conformant reference for the proposed +`docs/resources/library/.md` convention. It is **not** a real vendor +page; it is shown below as a literal code block so it is not built as a page +and its placeholder images are not resolved. Copy this skeleton when adding a +manufacturer. + +````markdown +# Acme Labware Inc. + +- **Website:** https://www.acme-labware.example +- **Wikipedia:** https://en.wikipedia.org/wiki/Example + +## About + +> Acme Labware Inc. is a fictional manufacturer of plates, tip racks, and +> reservoirs, used here purely to demonstrate the file convention. + +## Brand structure + +``` +Acme Labware Inc. +├── AcmePure (consumables brand) +│ ├── Plates +│ └── Reservoirs +└── AcmeTips (tips brand) + └── Tip Racks +``` + +## AcmePure + +### Plates + +| Description | Image | PLR definition | +|-|-|-| +| 96-well, 2 mL, V-bottom
Part no.: AP-9620
[manufacturer website](https://www.acme-labware.example/p/AP-9620) | ![](img/acme/Acme_96_wellplate_2mL_Vb.jpg) | `Acme_96_wellplate_2mL_Vb` | +| 384-well, 120 uL, flat bottom
Part no.: AP-3841
[manufacturer website](https://www.acme-labware.example/p/AP-3841) | ![](img/acme/Acme_384_wellplate_120uL_Fb.jpg) | `Acme_384_wellplate_120uL_Fb` | + +### Reservoirs + +| Description | Image | PLR definition | +|-|-|-| +| Single-channel reservoir, 290 mL
Part no.: AP-RES290 | ![](img/acme/Acme_1_troughplate_290000uL_Vb.jpg) | `Acme_1_troughplate_290000uL_Vb` | + +## AcmeTips + +### Tip Racks + +| Description | Image | PLR definition | +|-|-|-| +| 96 tips, 1000 uL, filtered
Part no.: AT-1000F
[manufacturer website](https://www.acme-labware.example/p/AT-1000F) | ![](img/acme/Acme_96_tiprack_1000uL_filtered.jpg) | `Acme_96_tiprack_1000uL_filtered` | +```` + +## Why this is conformant + +- **One H1** (`# Acme Labware Inc.`) — the canonical name + dedup key. +- **OEM metadata** as a labelled list (`Website:`, optional `Wikipedia:`) — + captured unambiguously, no "first link" guessing; all keys optional. +- **`## About`** (optional) reserved section holds the description (not a + scraped blockquote heuristic). +- **`## Brand structure`** (optional) reserved section holds the human-curated + overview; it is *not* used to build the library tree. +- **Organisation = heading nesting**: brand-first here + (`## AcmePure` → `### Plates`/`### Reservoirs`, `## AcmeTips` → + `### Tip Racks`), one consistent axis, reserved sections excluded. +- **Definition tables**: exact `| Description | Image | PLR definition |` + header; one backticked, regex-valid PLR definition per row; image paths + under `img//…`; optional `Part no.` and `[manufacturer website]`. +- Headings use Title Case, singular category nouns ("Tip Rack", "Plate") — + the *recommended* (not CI-enforced) naming policy. diff --git a/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb b/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb index 8ba4ebe5348..8aaad4aada5 100644 --- a/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb +++ b/docs/user_guide/01_material-handling/heating_shaking/hamilton.ipynb @@ -212,7 +212,7 @@ "\n", "Before you can use the Hamilton Heater Shaker in combination with a Hamilton STAR liquid handler, you need to assign it to the deck. This is needed when, for example, you want to use the iSWAP or CoRe grippers to move a plate to or from the heater shaker. This is also required to get the heater shaker to show up in the Visualizer.\n", "\n", - "Here's one example of assigning a Hamilton Heater Shaker to the deck using a `MFX_CAR_P3_SHAKER`. Note that you can use any carrier, or even directly place heater shakers on the deck if you like. See the [Hamilton STAR resources page](/resources/library/hamilton) for carriers." + "Here's one example of assigning a Hamilton Heater Shaker to the deck using a `MFX_CAR_P3_SHAKER`. Note that you can use any carrier, or even directly place heater shakers on the deck if you like. See the [resource library](../../../resources/library.md) for carriers." ] }, { diff --git a/pylabrobot/resources/__init__.py b/pylabrobot/resources/__init__.py index 4652e9a859b..c94079dcf03 100644 --- a/pylabrobot/resources/__init__.py +++ b/pylabrobot/resources/__init__.py @@ -25,6 +25,12 @@ from .diy import * from .eppendorf import * from .errors import ResourceNotFoundError +from .geometry import ( + generate_geometry_catalog, + generate_geometry_library, + save_geometry_catalog, + save_geometry_library, +) from .greiner import * from .hamilton import * from .itemized_resource import ItemizedResource diff --git a/pylabrobot/resources/corning/falcon/tubes.py b/pylabrobot/resources/corning/falcon/tubes.py index 8929b0d93ff..a637123bcfe 100644 --- a/pylabrobot/resources/corning/falcon/tubes.py +++ b/pylabrobot/resources/corning/falcon/tubes.py @@ -84,7 +84,7 @@ def Cor_Falcon_tube_14mL_Rb(name: str) -> Tube: def falcon_tube_50mL(name: str) -> Tube: raise NotImplementedError( - "falcon_tube_50mL definition is deprecated. Use Cor_Falcon_tube_50mL instead." + "falcon_tube_50mL definition is deprecated. Use Cor_Falcon_tube_50mL_Vb instead." ) diff --git a/pylabrobot/resources/geometry.py b/pylabrobot/resources/geometry.py new file mode 100644 index 00000000000..d98a3578281 --- /dev/null +++ b/pylabrobot/resources/geometry.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +from pylabrobot.resources.container import Container +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.resource import Resource +from pylabrobot.resources.rotation import Rotation +from pylabrobot.resources.tip_rack import TipSpot +from pylabrobot.resources.well import Well + + +def generate_geometry_library(root: Resource) -> Dict[str, Any]: + """Generate a renderer-oriented geometry library entry for a resource tree. + + The library entry is intentionally separate from the package data: call this on a deck + or labware resource after building a layout, then write the returned dict to JSON + if a simulator needs it. + """ + + prototypes: Dict[str, Dict[str, Any]] = {} + instances: Dict[str, Dict[str, Any]] = {} + + for resource in _walk_resources(root): + prototype = _resource_geometry_prototype(resource) + prototype_id = _geometry_prototype_id(prototype) + prototypes.setdefault(prototype_id, prototype) + + instance: Dict[str, Any] = { + "prototype": prototype_id, + "parent": resource.parent.name if resource.parent is not None else None, + "pose": _resource_pose(resource), + "rotation": _rotation_values(resource.get_absolute_rotation()), + } + if len(resource.children) > 0: + instance["children"] = [child.name for child in resource.children] + instances[resource.name] = instance + + return { + "root": root.name, + "prototypes": prototypes, + "instances": instances, + } + + +def save_geometry_library( + root: Resource, + path: Union[str, Path], + indent: Optional[int] = 2, +) -> None: + """Generate a geometry library entry and write it to a JSON file.""" + + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(generate_geometry_library(root), indent=indent), encoding="utf-8") + + +def generate_geometry_catalog(root: Resource) -> Dict[str, Any]: + """Deprecated alias for :func:`generate_geometry_library`.""" + + return generate_geometry_library(root) + + +def save_geometry_catalog( + root: Resource, + path: Union[str, Path], + indent: Optional[int] = 2, +) -> None: + """Deprecated alias for :func:`save_geometry_library`.""" + + save_geometry_library(root, path, indent=indent) + + +def _walk_resources(root: Resource) -> List[Resource]: + resources = [root] + for child in root.children: + resources.extend(_walk_resources(child)) + return resources + + +def _resource_geometry_prototype(resource: Resource) -> Dict[str, Any]: + prototype: Dict[str, Any] = { + "type": resource.__class__.__name__, + "category": resource.category, + "size": _coordinate_values( + Coordinate(resource.get_size_x(), resource.get_size_y(), resource.get_size_z()) + ), + "geometry": _resource_geometry_hints(resource), + } + if resource.model is not None: + prototype["model"] = resource.model + return prototype + + +def _resource_geometry_hints(resource: Resource) -> Dict[str, Any]: + if isinstance(resource, Well): + geometry: Dict[str, Any] = { + "shape": "well", + "cross_section": resource.cross_section_type.value, + "bottom": resource.bottom_type.value, + } + if resource._material_z_thickness is not None: + geometry["material_z_thickness"] = resource._material_z_thickness + if len(resource.no_go_zones) > 0: + geometry["no_go_zones"] = _no_go_zones_to_values(resource.no_go_zones) + return geometry + + if isinstance(resource, TipSpot): + return {"shape": "tip_spot"} + + if isinstance(resource, Container): + geometry = {"shape": "container"} + if resource._material_z_thickness is not None: + geometry["material_z_thickness"] = resource._material_z_thickness + if len(resource.no_go_zones) > 0: + geometry["no_go_zones"] = _no_go_zones_to_values(resource.no_go_zones) + return geometry + + if resource.category == "deck": + return {"shape": "deck"} + + return {"shape": "box"} + + +def _geometry_prototype_id(prototype: Dict[str, Any]) -> str: + payload = json.dumps(prototype, sort_keys=True, separators=(",", ":")) + digest = hashlib.sha1(payload.encode("utf-8")).hexdigest()[:10] + return f"{prototype['type']}_{digest}" + + +def _coordinate_or_none(resource: Resource, x: str, y: str, z: str) -> Optional[List[float]]: + try: + return _coordinate_values(resource.get_absolute_location(x=x, y=y, z=z)) + except Exception: + return None + + +def _resource_pose(resource: Resource) -> Optional[List[float]]: + pose = _coordinate_or_none(resource, x="l", y="f", z="b") + if pose is None and resource.parent is None: + return [0, 0, 0] + return pose + + +def _coordinate_values(coordinate: Coordinate) -> List[float]: + return [coordinate.x, coordinate.y, coordinate.z] + + +def _rotation_values(rotation: Rotation) -> List[float]: + return [rotation.x, rotation.y, rotation.z] + + +def _no_go_zones_to_values( + no_go_zones: List[tuple[Coordinate, Coordinate]], +) -> List[List[List[float]]]: + return [ + [_coordinate_values(front_left_bottom), _coordinate_values(back_right_top)] + for front_left_bottom, back_right_top in no_go_zones + ] diff --git a/pylabrobot/resources/geometry_tests.py b/pylabrobot/resources/geometry_tests.py new file mode 100644 index 00000000000..89206e64810 --- /dev/null +++ b/pylabrobot/resources/geometry_tests.py @@ -0,0 +1,42 @@ +import json +import unittest + +from pylabrobot.resources import ( + Cor_96_wellplate_360ul_Fb, + generate_geometry_library, + hamilton_96_tiprack_1000uL_filter, +) +from pylabrobot.resources.hamilton import STARLetDeck + + +class GeometryLibraryTests(unittest.TestCase): + def setUp(self): + self.deck = STARLetDeck() + self.tip_rack = hamilton_96_tiprack_1000uL_filter(name="tip_rack") + self.plate = Cor_96_wellplate_360ul_Fb(name="plate") + self.deck.assign_child_resource(self.tip_rack, rails=3) + self.deck.assign_child_resource(self.plate, rails=9) + + def test_generate_geometry_library_for_deck(self): + library = generate_geometry_library(self.deck) + json.dumps(library) + + self.assertEqual(library["root"], "deck") + self.assertIn("plate", library["instances"]) + self.assertIn("plate_well_A1", library["instances"]) + + well_instance = library["instances"]["plate_well_A1"] + well_prototype = library["prototypes"][well_instance["prototype"]] + self.assertEqual(well_prototype["geometry"]["shape"], "well") + self.assertEqual(well_prototype["geometry"]["cross_section"], "circle") + self.assertEqual(len(well_instance["pose"]), 3) + + def test_generate_geometry_library_for_single_labware(self): + plate = Cor_96_wellplate_360ul_Fb(name="standalone_plate") + library = generate_geometry_library(plate) + + self.assertEqual(library["root"], "standalone_plate") + self.assertIn("standalone_plate", library["instances"]) + self.assertIn("standalone_plate_well_A1", library["instances"]) + self.assertEqual(library["instances"]["standalone_plate"]["pose"], [0, 0, 0]) + self.assertLess(len(library["prototypes"]), len(library["instances"])) diff --git a/pylabrobot/resources/opentrons/load.py b/pylabrobot/resources/opentrons/load.py index 868363a0e7e..84ecfdf8e5f 100644 --- a/pylabrobot/resources/opentrons/load.py +++ b/pylabrobot/resources/opentrons/load.py @@ -11,7 +11,7 @@ def _download_file(url: str, local_path: str) -> bytes: - with urllib.request.urlopen(url) as response, open(local_path, "wb") as out_file: + with urllib.request.urlopen(url, timeout=10) as response, open(local_path, "wb") as out_file: data = response.read() out_file.write(data) return data # type: ignore