diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
new file mode 100644
index 0000000..836e8af
--- /dev/null
+++ b/.github/workflows/python-package.yml
@@ -0,0 +1,37 @@
+name: Python package
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+ schedule:
+ - cron: '0 0 * * *'
+
+jobs:
+ build:
+
+ runs-on: [ubuntu-latest]
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.10"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v3
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+
+ - name: Install pudu package in development mode with test dependencies
+ run: |
+ pip install -e .[test]
+
+ - name: Test scripts with opentrons_simulate
+ run: |
+ pytest -s -v
\ No newline at end of file
diff --git a/README.md b/README.md
index d5c25d6..d1c1f97 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
Welcome to the PUDU (Protocol Unified Design Unit) repository, our Python package for liquid handling robot control on Synthetic Biology workflows.
-
-
+
+
## *The art of automated liquid handling*
@@ -67,3 +67,19 @@ then in the OT-2 terminal run:
## Tutorials
TODO
+
+## Manual Golden Gate protocol generation
+
+PUDU now supports generating a **human-readable manual Golden Gate protocol** from SBOL-style JSON input using `ManualAssembly`.
+
+- The class builds structured per-reaction records (product, backbone, ordered parts, enzyme, and calculated reagent volumes).
+- It renders a Markdown protocol suitable for lab use and future PDF conversion.
+- It does **not** generate OT-2 commands.
+
+Example script:
+
+```bash
+PYTHONPATH=src python scripts/generate_manual_assembly_protocol.py
+```
+
+This writes a protocol document to `scripts/manual_assembly_protocol.md`.
diff --git a/documentation/PUDU_docs.pdf b/documentation/PUDU_docs.pdf
new file mode 100644
index 0000000..bcb3eff
Binary files /dev/null and b/documentation/PUDU_docs.pdf differ
diff --git a/documentation/docs.md b/documentation/docs.md
new file mode 100644
index 0000000..d8f8d46
--- /dev/null
+++ b/documentation/docs.md
@@ -0,0 +1 @@
+docs
diff --git a/documentation/manual_assembly_protocol_example.md b/documentation/manual_assembly_protocol_example.md
new file mode 100644
index 0000000..716291d
--- /dev/null
+++ b/documentation/manual_assembly_protocol_example.md
@@ -0,0 +1,63 @@
+# Golden Gate Manual Assembly Protocol
+
+## Overview
+Golden Gate assembly is a one-pot DNA cloning method that uses a Type IIS restriction enzyme, such as BsaI, together with DNA ligase to assemble multiple DNA fragments in a predefined order.
+Because Type IIS enzymes cut outside their recognition sites, they generate custom overhangs that direct fragment assembly and allow the recognition sites to be removed from the final construct.
+In this protocol, plasmids containing DNA parts and a destination backbone are combined with the restriction enzyme and ligase in a single tube, then cycled in a thermocycler between digestion and ligation temperatures. Repetition of these cycles enriches for the correctly assembled composite plasmid, after which the enzymes are heat-inactivated and the reaction is held at 4 °C until collection.
+
+## Inputs
+- Number of target products: 2
+- `composite_1` (https://SBOL2Build.org/composite_1/1)
+- `composite_2` (https://SBOL2Build.org/composite_2/1)
+
+## Reaction summary
+| Product | Backbone | Parts | Restriction Enzyme | # DNA Components | DNA each (µL) | Enzyme (µL) | Ligase (µL) | Buffer (µL) | Water (µL) | Total (µL) |
+| --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
+| composite_1 | pSB1C3 | J23101, B0034, GFP, B0015 | BsaI | 5 | 2 | 2 | 4 | 2 | 2 | 20 |
+| composite_2 | pSB1C3 | J23106, B0034, RFP, B0015 | BsaI | 5 | 2 | 2 | 4 | 2 | 2 | 20 |
+
+## Per-reaction instructions
+
+### Product: composite_1
+URI: https://SBOL2Build.org/composite_1/1
+
+1. Label one tube as `composite_1`.
+2. Add 2 µL nuclease-free water.
+3. Add 2 µL 10X T4 DNA Ligase Buffer.
+4. Add 4 µL T4 DNA Ligase.
+5. Add 2 µL BsaI (URI: https://SBOL2Build.org/BsaI/1).
+6. Add 2 µL backbone `pSB1C3` (URI: https://sbolcanvas.org/pSB1C3/1).
+7. Add 2 µL part `J23101` (URI: https://sbolcanvas.org/J23101/1).
+8. Add 2 µL part `B0034` (URI: https://sbolcanvas.org/B0034/1).
+9. Add 2 µL part `GFP` (URI: https://sbolcanvas.org/GFP/1).
+10. Add 2 µL part `B0015` (URI: https://sbolcanvas.org/B0015/1).
+11. Mix gently by pipetting. Do not vortex unless explicitly intended.
+12. Briefly spin down if appropriate.
+
+### Product: composite_2
+URI: https://SBOL2Build.org/composite_2/1
+
+1. Label one tube as `composite_2`.
+2. Add 2 µL nuclease-free water.
+3. Add 2 µL 10X T4 DNA Ligase Buffer.
+4. Add 4 µL T4 DNA Ligase.
+5. Add 2 µL BsaI (URI: https://SBOL2Build.org/BsaI/1).
+6. Add 2 µL backbone `pSB1C3` (URI: https://sbolcanvas.org/pSB1C3/1).
+7. Add 2 µL part `J23106` (URI: https://sbolcanvas.org/J23106/1).
+8. Add 2 µL part `B0034` (URI: https://sbolcanvas.org/B0034/1).
+9. Add 2 µL part `RFP` (URI: https://sbolcanvas.org/RFP/1).
+10. Add 2 µL part `B0015` (URI: https://sbolcanvas.org/B0015/1).
+11. Mix gently by pipetting. Do not vortex unless explicitly intended.
+12. Briefly spin down if appropriate.
+
+## Thermocycling
+- Use a cycling profile that holds 42°C for 2 minutes and 16°C for 5 minutes; repeat this profile 75 times.
+- Denature/inactivate proteins by holding 60°C for 10 minutes followed by 80°C for 10 minutes.
+- Hold at 4°C until samples are collected.
+
+## Notes
+- If the assembly was designed correctly, the final product should lack the Type IIS recognition sites used during assembly.
+- This generated document is a manual instruction sheet and not an automated OT-2 protocol.
+- Assumes all DNA parts are available at suitable concentrations and added at equal per-part volume.
+- Store the assembly product at 4 °C for better stability until used for downstream applications.
+- Validate assembled plasmids by restriction digest and gel electrophoresis, Sanger sequencing, or whole-plasmid sequencing.
diff --git a/images/PUDU_Logo_dark.png b/images/PUDU_Logo_dark.png
new file mode 100644
index 0000000..56f3c0c
Binary files /dev/null and b/images/PUDU_Logo_dark.png differ
diff --git a/notebooks/Assembly_to_OT2sim.ipynb b/notebooks/Assembly_to_OT2sim.ipynb
new file mode 100644
index 0000000..8791481
--- /dev/null
+++ b/notebooks/Assembly_to_OT2sim.ipynb
@@ -0,0 +1,208 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "f62bc338",
+ "metadata": {},
+ "source": [
+ "# SBOL to robot simulation"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "id": "7c2e1157",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Collecting git+https://github.com/MyersResearchGroup/SBOL2Build.git@roles-for-RE-and-Ligases\n",
+ " Cloning https://github.com/MyersResearchGroup/SBOL2Build.git (to revision roles-for-RE-and-Ligases) to c:\\users\\lukef\\appdata\\local\\temp\\pip-req-build-jomazih6\n",
+ " Resolved https://github.com/MyersResearchGroup/SBOL2Build.git to commit 2c0a53abada3731e81e0b04ac7c391f882c74a70\n",
+ " Installing build dependencies: started\n",
+ " Installing build dependencies: finished with status 'done'\n",
+ " Getting requirements to build wheel: started\n",
+ " Getting requirements to build wheel: finished with status 'done'\n",
+ " Installing backend dependencies: started\n",
+ " Installing backend dependencies: finished with status 'done'\n",
+ " Preparing metadata (pyproject.toml): started\n",
+ " Preparing metadata (pyproject.toml): finished with status 'done'\n",
+ "Requirement already satisfied: biopython in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2build==0.0b1) (1.81)\n",
+ "Requirement already satisfied: pydna in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2build==0.0b1) (5.0.0)\n",
+ "Requirement already satisfied: sbol2 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2build==0.0b1) (1.4.1)\n",
+ "Requirement already satisfied: numpy in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from biopython->sbol2build==0.0b1) (1.21.6)\n",
+ "Requirement already satisfied: networkx in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from pydna->sbol2build==0.0b1) (2.6.3)\n",
+ "Requirement already satisfied: prettytable in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from pydna->sbol2build==0.0b1) (3.7.0)\n",
+ "Requirement already satisfied: appdirs in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from pydna->sbol2build==0.0b1) (1.4.4)\n",
+ "Requirement already satisfied: requests in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2->sbol2build==0.0b1) (2.31.0)\n",
+ "Requirement already satisfied: urllib3 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2->sbol2build==0.0b1) (2.0.7)\n",
+ "Requirement already satisfied: rdflib>=5.0 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2->sbol2build==0.0b1) (6.3.2)\n",
+ "Requirement already satisfied: packaging>=20.0 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2->sbol2build==0.0b1) (24.0)\n",
+ "Requirement already satisfied: lxml in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2->sbol2build==0.0b1) (5.3.1)\n",
+ "Requirement already satisfied: deprecated in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2->sbol2build==0.0b1) (1.2.18)\n",
+ "Requirement already satisfied: python-dateutil in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from sbol2->sbol2build==0.0b1) (2.9.0)\n",
+ "Requirement already satisfied: pyparsing<4,>=2.1.0 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from rdflib>=5.0->sbol2->sbol2build==0.0b1) (2.4.7)\n",
+ "Requirement already satisfied: isodate<0.7.0,>=0.6.0 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from rdflib>=5.0->sbol2->sbol2build==0.0b1) (0.6.1)\n",
+ "Requirement already satisfied: importlib-metadata<5.0.0,>=4.0.0 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from rdflib>=5.0->sbol2->sbol2build==0.0b1) (4.13.0)\n",
+ "Requirement already satisfied: wrapt<2,>=1.10 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from deprecated->sbol2->sbol2build==0.0b1) (1.16.0)\n",
+ "Requirement already satisfied: wcwidth in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from prettytable->pydna->sbol2build==0.0b1) (0.2.10)\n",
+ "Requirement already satisfied: six>=1.5 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from python-dateutil->sbol2->sbol2build==0.0b1) (1.17.0)\n",
+ "Requirement already satisfied: certifi>=2017.4.17 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from requests->sbol2->sbol2build==0.0b1) (2024.8.30)\n",
+ "Requirement already satisfied: idna<4,>=2.5 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from requests->sbol2->sbol2build==0.0b1) (3.10)\n",
+ "Requirement already satisfied: charset-normalizer<4,>=2 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from requests->sbol2->sbol2build==0.0b1) (3.4.1)\n",
+ "Requirement already satisfied: zipp>=0.5 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from importlib-metadata<5.0.0,>=4.0.0->rdflib>=5.0->sbol2->sbol2build==0.0b1) (3.15.0)\n",
+ "Requirement already satisfied: typing-extensions>=3.6.4 in c:\\users\\lukef\\.conda\\envs\\test\\lib\\site-packages (from importlib-metadata<5.0.0,>=4.0.0->rdflib>=5.0->sbol2->sbol2build==0.0b1) (4.7.1)\n",
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ " Running command git clone --filter=blob:none --quiet https://github.com/MyersResearchGroup/SBOL2Build.git 'C:\\Users\\lukef\\AppData\\Local\\Temp\\pip-req-build-jomazih6'\n",
+ " Running command git checkout -b roles-for-RE-and-Ligases --track origin/roles-for-RE-and-Ligases\n",
+ " branch 'roles-for-RE-and-Ligases' set up to track 'origin/roles-for-RE-and-Ligases'.\n",
+ " Switched to a new branch 'roles-for-RE-and-Ligases'\n"
+ ]
+ }
+ ],
+ "source": [
+ "pip install git+https://github.com/MyersResearchGroup/SBOL2Build.git@roles-for-RE-and-Ligases"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "id": "69c8a583",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sbol2build as sb2b"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 42,
+ "id": "e86cb15c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sbol2 as sb2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "id": "4f19b39f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_golden_gate():\n",
+ " pro_doc = sb2.Document()\n",
+ "\n",
+ " pro_doc.read('../tests/pro_in_bb.xml')\n",
+ "\n",
+ " #where I am working adding 2 and 3\n",
+ " pro2_doc = sb2.Document()\n",
+ " pro2_doc.read('../tests/pro2_in_bb.xml') #add document\n",
+ "\n",
+ " pro3_doc = sb2.Document()\n",
+ " pro3_doc.read('../tests/pro3_in_bb.xml') #add document\n",
+ " \n",
+ " rbs_doc = sb2.Document()\n",
+ " rbs_doc.read('../tests/rbs_in_bb.xml')\n",
+ "\n",
+ " cds_doc = sb2.Document()\n",
+ " cds_doc.read('../tests/cds_in_bb.xml')\n",
+ " \n",
+ " ter_doc = sb2.Document()\n",
+ " ter_doc.read('../tests/terminator_in_bb.xml')\n",
+ "\n",
+ " bb_doc = sb2.Document()\n",
+ " bb_doc.read('../tests/backbone.xml')\n",
+ "\n",
+ " part_docs = [pro_doc, rbs_doc, cds_doc, ter_doc]\n",
+ "\n",
+ " assembly_doc = sb2.Document()\n",
+ " assembly_obj = sb2b.golden_gate_assembly_plan('testassem', part_docs, bb_doc, 'BsaI', assembly_doc)\n",
+ "\n",
+ " composites = assembly_obj.run()\n",
+ "\n",
+ " assembly_doc.write('validation_assembly1.xml')\n",
+ "\n",
+ " sbol_validation_result = assembly_doc.validate()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 44,
+ "id": "5bf7f217",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "adding: UJHDBOTD_70_extracted_part\n",
+ "adding: UJHDBOTD_70_three_prime_oh\n",
+ "adding: Scar_B\n",
+ "adding: J23101\n",
+ "adding: UJHDBOTD_70_five_prime_oh\n",
+ "adding: Scar_A\n",
+ "adding: UJHDBOTD_45_extracted_part\n",
+ "adding: UJHDBOTD_45_five_prime_oh\n",
+ "adding: Scar_B\n",
+ "adding: UJHDBOTD_45_three_prime_oh\n",
+ "adding: Scar_C\n",
+ "adding: B0034\n",
+ "adding: UJHDBOTD_106_extracted_part\n",
+ "adding: GFP\n",
+ "adding: UJHDBOTD_106_five_prime_oh\n",
+ "adding: Scar_C\n",
+ "adding: UJHDBOTD_106_three_prime_oh\n",
+ "adding: Scar_D\n",
+ "adding: UJHDBOTD_134_extracted_part\n",
+ "adding: B0015\n",
+ "adding: UJHDBOTD_134_three_prime_oh\n",
+ "adding: Scar_F\n",
+ "adding: UJHDBOTD_134_five_prime_oh\n",
+ "adding: Scar_D\n",
+ "adding: UJHDBOTD_extracted_backbone\n",
+ "adding: UJHDBOTD_159_five_prime_oh\n",
+ "adding: Scar_F\n",
+ "adding: Cir_qxow\n",
+ "adding: UJHDBOTD_159_three_prime_oh\n",
+ "adding: Scar_A\n",
+ "adding: composite_1\n"
+ ]
+ }
+ ],
+ "source": [
+ "test_golden_gate()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "test",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/Assembly_to_dict.ipynb b/notebooks/Assembly_to_dict.ipynb
new file mode 100644
index 0000000..90ffa56
--- /dev/null
+++ b/notebooks/Assembly_to_dict.ipynb
@@ -0,0 +1,315 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "c924440e",
+ "metadata": {},
+ "source": [
+ "# Assembly to dict"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "id": "df62b558",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def dictionaryListCreatorPython(file):\n",
+ " import sbol2 as sb2\n",
+ " import json\n",
+ "\n",
+ " sb2.Config.setOption('sbol_typed_uris', False)\n",
+ " doc = sb2.Document()\n",
+ " doc.read(file)\n",
+ "\n",
+ " # Known SO roles\n",
+ " PRODUCT_ROLE = 'http://identifiers.org/so/SO:0000804'\n",
+ " BackBone_ROLE = 'http://identifiers.org/so/SO:0000755'\n",
+ " ENZYME_ROLE = 'http://identifiers.org/obi/OBI:0000732'\n",
+ "\n",
+ " PARTS_ROLE_LIST = [\n",
+ " 'http://identifiers.org/so/SO:0000031', 'http://identifiers.org/so/SO:0000316',\n",
+ " 'http://identifiers.org/so/SO:0001977', 'http://identifiers.org/so/SO:0001956',\n",
+ " 'http://identifiers.org/so/SO:0000188', 'http://identifiers.org/so/SO:0000839',\n",
+ " 'http://identifiers.org/so/SO:0000167', 'http://identifiers.org/so/SO:0000139',\n",
+ " 'http://identifiers.org/so/SO:0001979', 'http://identifiers.org/so/SO:0001955',\n",
+ " 'http://identifiers.org/so/SO:0001546', 'http://identifiers.org/so/SO:0001263',\n",
+ " 'http://identifiers.org/SO:0000141', 'http://identifiers.org/so/SO:0000141'\n",
+ " ]\n",
+ "\n",
+ " product_dicts = []\n",
+ " globalEnzyme = None\n",
+ "\n",
+ " for cd in doc.componentDefinitions:\n",
+ " print(f\"\\n🔍 Checking Component: {cd.displayId}\")\n",
+ " print(f\" Types: {cd.types}\")\n",
+ " print(f\" Roles: {cd.roles}\")\n",
+ "\n",
+ " if ENZYME_ROLE in cd.roles:\n",
+ " globalEnzyme = cd.identity\n",
+ " print(f\"✅ Found enzyme definition: {globalEnzyme}\")\n",
+ "\n",
+ " if PRODUCT_ROLE in cd.roles:\n",
+ " result = {\n",
+ " 'Product': cd.identity,\n",
+ " 'Backbone': None,\n",
+ " 'PartsList': [],\n",
+ " 'Restriction Enzyme': None\n",
+ " }\n",
+ "\n",
+ " for comp in cd.components:\n",
+ " sub_cd = doc.componentDefinitions.get(comp.definition)\n",
+ " if sub_cd is None:\n",
+ " print(f\"⚠️ Component definition for {comp.displayId} not found.\")\n",
+ " continue\n",
+ "\n",
+ " print(f\" → Subcomponent: {sub_cd.displayId}\")\n",
+ " print(f\" Roles: {sub_cd.roles}\")\n",
+ "\n",
+ " if BackBone_ROLE in sub_cd.roles:\n",
+ " result['Backbone'] = sub_cd.identity\n",
+ " print(f\" 🧬 Assigned Backbone: {sub_cd.identity}\")\n",
+ "\n",
+ " if any(role in PARTS_ROLE_LIST for role in sub_cd.roles):\n",
+ " result['PartsList'].append(sub_cd.identity)\n",
+ " print(f\" 🧩 Added Part: {sub_cd.identity}\")\n",
+ "\n",
+ " if not result['Backbone']:\n",
+ " print(f\"⚠️ No backbone found for product {cd.displayId}\")\n",
+ " if not result['PartsList']:\n",
+ " print(f\"⚠️ No parts found for product {cd.displayId}\")\n",
+ "\n",
+ " product_dicts.append(result)\n",
+ "\n",
+ " for entry in product_dicts:\n",
+ " entry['Restriction Enzyme'] = globalEnzyme\n",
+ "\n",
+ " with open('output.json', 'w') as json_file:\n",
+ " json.dump(product_dicts, json_file, indent=4)\n",
+ "\n",
+ " return product_dicts\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 44,
+ "id": "cef06312",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "🔍 Checking Component: BsaI\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#Protein']\n",
+ " Roles: ['http://identifiers.org/obi/OBI:0000732']\n",
+ "✅ Found enzyme definition: https://SBOL2Build.org/BsaI/1\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_70_extracted_part\n",
+ " Types: ['http://identifiers.org/so/SO:0000987', 'http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['https://identifiers.org/so/SO:0000915']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_70_three_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001933']\n",
+ "\n",
+ "🔍 Checking Component: Scar_B\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: J23101\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0000167']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_70_five_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001932']\n",
+ "\n",
+ "🔍 Checking Component: Scar_A\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_45_extracted_part\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion', 'http://identifiers.org/so/SO:0000987']\n",
+ " Roles: ['https://identifiers.org/so/SO:0000915']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_45_five_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001932']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_45_three_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001933']\n",
+ "\n",
+ "🔍 Checking Component: Scar_C\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: B0034\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0000139']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_106_extracted_part\n",
+ " Types: ['http://identifiers.org/so/SO:0000987', 'http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['https://identifiers.org/so/SO:0000915']\n",
+ "\n",
+ "🔍 Checking Component: GFP\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0000316']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_106_five_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001932']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_106_three_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001933']\n",
+ "\n",
+ "🔍 Checking Component: Scar_D\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_134_extracted_part\n",
+ " Types: ['http://identifiers.org/so/SO:0000987', 'http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['https://identifiers.org/so/SO:0000915']\n",
+ "\n",
+ "🔍 Checking Component: B0015\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0000141']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_134_three_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001933']\n",
+ "\n",
+ "🔍 Checking Component: Scar_F\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_134_five_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001932']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_extracted_backbone\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['https://identifiers.org/so/SO:0000755']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_159_five_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001932']\n",
+ "\n",
+ "🔍 Checking Component: Cir_qxow\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0000755']\n",
+ "\n",
+ "🔍 Checking Component: UJHDBOTD_159_three_prime_oh\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001933']\n",
+ "\n",
+ "🔍 Checking Component: T4_Ligase\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#Protein']\n",
+ " Roles: []\n",
+ "\n",
+ "🔍 Checking Component: Ligation_Scar_A\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: Ligation_Scar_B\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: Ligation_Scar_C\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: Ligation_Scar_D\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: Ligation_Scar_E\n",
+ " Types: ['http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ "\n",
+ "🔍 Checking Component: composite_1\n",
+ " Types: ['http://identifiers.org/so/SO:0000988', 'http://www.biopax.org/release/biopax-level3.owl#DnaRegion']\n",
+ " Roles: ['http://identifiers.org/so/SO:0000804']\n",
+ " → Subcomponent: GFP\n",
+ " Roles: ['http://identifiers.org/so/SO:0000316']\n",
+ " 🧩 Added Part: https://sbolcanvas.org/GFP/1\n",
+ " → Subcomponent: B0015\n",
+ " Roles: ['http://identifiers.org/so/SO:0000141']\n",
+ " 🧩 Added Part: https://sbolcanvas.org/B0015/1\n",
+ " → Subcomponent: Ligation_Scar_E\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ " → Subcomponent: Ligation_Scar_B\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ " → Subcomponent: Ligation_Scar_C\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ " → Subcomponent: J23101\n",
+ " Roles: ['http://identifiers.org/so/SO:0000167']\n",
+ " 🧩 Added Part: https://sbolcanvas.org/J23101/1\n",
+ " → Subcomponent: Ligation_Scar_A\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ " → Subcomponent: Ligation_Scar_D\n",
+ " Roles: ['http://identifiers.org/so/SO:0001953']\n",
+ " → Subcomponent: B0034\n",
+ " Roles: ['http://identifiers.org/so/SO:0000139']\n",
+ " 🧩 Added Part: https://sbolcanvas.org/B0034/1\n",
+ " → Subcomponent: Cir_qxow\n",
+ " Roles: ['http://identifiers.org/so/SO:0000755']\n",
+ " 🧬 Assigned Backbone: https://sbolcanvas.org/Cir_qxow/1\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "[{'Product': 'https://SBOL2Build.org/composite_1/1',\n",
+ " 'Backbone': 'https://sbolcanvas.org/Cir_qxow/1',\n",
+ " 'PartsList': ['https://sbolcanvas.org/GFP/1',\n",
+ " 'https://sbolcanvas.org/B0015/1',\n",
+ " 'https://sbolcanvas.org/J23101/1',\n",
+ " 'https://sbolcanvas.org/B0034/1'],\n",
+ " 'Restriction Enzyme': 'https://SBOL2Build.org/BsaI/1'}]"
+ ]
+ },
+ "execution_count": 44,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "dictionaryListCreatorPython(\"../tests/validation_assembly1.xml\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "46fb31a6",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "test",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/output.json b/notebooks/output.json
new file mode 100644
index 0000000..21e000a
--- /dev/null
+++ b/notebooks/output.json
@@ -0,0 +1,13 @@
+[
+ {
+ "Product": "https://SBOL2Build.org/composite_1/1",
+ "Backbone": "https://sbolcanvas.org/Cir_qxow/1",
+ "PartsList": [
+ "https://sbolcanvas.org/GFP/1",
+ "https://sbolcanvas.org/B0015/1",
+ "https://sbolcanvas.org/J23101/1",
+ "https://sbolcanvas.org/B0034/1"
+ ],
+ "Restriction Enzyme": "https://SBOL2Build.org/BsaI/1"
+ }
+]
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index c662515..dbbf46d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,19 +4,19 @@ build-backend = "setuptools.build_meta"
[project]
name = "pudupy"
-version = "1.0.0b2"
+version = "1.0.0b7"
description = "Protocol Unified Design Unit"
readme = "README.md"
requires-python = ">=3.7"
license = {file = "LICENSE"}
keywords = ["pudu", "lab", "automation", "opentrons", "synthetic biology"]
authors = [
- { name="Gonzalo Vidal", email="g.a.vidal-pena2@ncl.ac.uk" },
- { name="Carolus Vitalis", email="carlos.vidal.c@ug.uchile.cl"},
- { name="Matt Burridge", email="m.burridge1@newcastle.ac.uk"}
+ { name="Gonzalo Vidal", email="govi9038@colorado.edu" },
+ { name="Oscar Rodriguez", email="osro6012@colorado.edu"},
]
maintainers = [
- {name = "Gonzalo Vidal", email = "g.a.vidal-pena2@ncl.ac.uk"}
+ { name="Gonzalo Vidal", email="govi9038@colorado.edu" },
+ { name="Oscar Rodriguez", email="osro6012@colorado.edu"},
]
classifiers = [
"Programming Language :: Python :: 3",
@@ -25,15 +25,16 @@ classifiers = [
]
dependencies = [
- 'opentrons',
+ "opentrons>=8.4.1",
+ "xlsxwriter>=3.2.5",
]
[project.optional-dependencies]
test = [
- "pytest < 5.0.0",
- "pytest-cov[all]"
+ "pytest >= 7.0.0",
+ "pytest-cov"
]
[project.urls]
-"Homepage" = "https://github.com/RudgeLab/PUDU"
-"Bug Tracker" = "https://github.com/RudgeLab/PUDU/issues"
\ No newline at end of file
+"Homepage" = "https://github.com/MyersResearchGroup/PUDU"
+"Bug Tracker" = "https://github.com/MyersResearchGroup/PUDU/issues"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 37ac0d5..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,44 +0,0 @@
-aionotify==0.2.0
-anyio==3.3.0
-attrs==22.2.0
-biopython==1.81
-certifi==2022.12.7
-charset-normalizer==3.0.1
-click==8.1.3
-Deprecated==1.2.13
-et-xmlfile==1.1.0
-graphviz==0.20.1
-idna==3.4
-isodate==0.6.1
-jsonschema==3.0.2
-lxml==4.9.2
-numpy==1.24.2
-openpyxl==3.1.1
-opentrons==6.2.1
-opentrons-shared-data==6.2.1
-ordered-set==4.1.0
-owlrl==6.0.2
-packaging==23.0
-prettytable==2.5.0
-pydantic==1.8.2
-PyLaTeX==1.4.1
-pyparsing==2.4.7
-PyPDF2==3.0.1
-pyrsistent==0.19.3
-pyserial==3.5
-pyshacl==0.18.1
-python-dateutil==2.8.2
-rdflib==6.2.0
-requests==2.28.2
-sbol-factory==1.1.1.post1
-sbol-utilities==1.0a16
-sbol2==1.4.1
-sbol3==1.0.1
-six==1.16.0
-sniffio==1.3.0
-SPARQLWrapper==2.0.0
-typing_extensions==4.5.0
-tyto==1.4
-urllib3==1.26.14
-wcwidth==0.2.6
-wrapt==1.15.0
diff --git a/scripts/generate_manual_assembly_protocol.py b/scripts/generate_manual_assembly_protocol.py
new file mode 100644
index 0000000..ba520e1
--- /dev/null
+++ b/scripts/generate_manual_assembly_protocol.py
@@ -0,0 +1,27 @@
+import json
+import argparse
+from pathlib import Path
+
+from pudu.assembly import ManualAssembly
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate a manual Golden Gate Markdown protocol.")
+ parser.add_argument("--input", default="scripts/manual_assembly_input.json", help="Path to SBOL-style JSON input file.")
+ parser.add_argument("--output", default="scripts/manual_assembly_protocol.md", help="Path to Markdown output file.")
+ args = parser.parse_args()
+
+ input_path = Path(args.input)
+ output_path = Path(args.output)
+
+ assemblies = json.loads(input_path.read_text(encoding="utf-8"))
+
+ manual_protocol = ManualAssembly(assemblies=assemblies)
+ manual_protocol.process_assemblies()
+ manual_protocol.write_markdown(str(output_path))
+
+ print(f"Manual protocol written to {output_path}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/manual_assembly_input.json b/scripts/manual_assembly_input.json
new file mode 100644
index 0000000..05dab93
--- /dev/null
+++ b/scripts/manual_assembly_input.json
@@ -0,0 +1,24 @@
+[
+ {
+ "Product": "https://SBOL2Build.org/composite_1/1",
+ "Backbone": "https://sbolcanvas.org/pSB1C3/1",
+ "PartsList": [
+ "https://sbolcanvas.org/J23101/1",
+ "https://sbolcanvas.org/B0034/1",
+ "https://sbolcanvas.org/GFP/1",
+ "https://sbolcanvas.org/B0015/1"
+ ],
+ "Restriction Enzyme": "https://SBOL2Build.org/BsaI/1"
+ },
+ {
+ "Product": "https://SBOL2Build.org/composite_2/1",
+ "Backbone": "https://sbolcanvas.org/pSB1C3/1",
+ "PartsList": [
+ "https://sbolcanvas.org/J23106/1",
+ "https://sbolcanvas.org/B0034/1",
+ "https://sbolcanvas.org/RFP/1",
+ "https://sbolcanvas.org/B0015/1"
+ ],
+ "Restriction Enzyme": "https://SBOL2Build.org/BsaI/1"
+ }
+]
diff --git a/scripts/manual_assembly_protocol.md b/scripts/manual_assembly_protocol.md
new file mode 100644
index 0000000..716291d
--- /dev/null
+++ b/scripts/manual_assembly_protocol.md
@@ -0,0 +1,63 @@
+# Golden Gate Manual Assembly Protocol
+
+## Overview
+Golden Gate assembly is a one-pot DNA cloning method that uses a Type IIS restriction enzyme, such as BsaI, together with DNA ligase to assemble multiple DNA fragments in a predefined order.
+Because Type IIS enzymes cut outside their recognition sites, they generate custom overhangs that direct fragment assembly and allow the recognition sites to be removed from the final construct.
+In this protocol, plasmids containing DNA parts and a destination backbone are combined with the restriction enzyme and ligase in a single tube, then cycled in a thermocycler between digestion and ligation temperatures. Repetition of these cycles enriches for the correctly assembled composite plasmid, after which the enzymes are heat-inactivated and the reaction is held at 4 °C until collection.
+
+## Inputs
+- Number of target products: 2
+- `composite_1` (https://SBOL2Build.org/composite_1/1)
+- `composite_2` (https://SBOL2Build.org/composite_2/1)
+
+## Reaction summary
+| Product | Backbone | Parts | Restriction Enzyme | # DNA Components | DNA each (µL) | Enzyme (µL) | Ligase (µL) | Buffer (µL) | Water (µL) | Total (µL) |
+| --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
+| composite_1 | pSB1C3 | J23101, B0034, GFP, B0015 | BsaI | 5 | 2 | 2 | 4 | 2 | 2 | 20 |
+| composite_2 | pSB1C3 | J23106, B0034, RFP, B0015 | BsaI | 5 | 2 | 2 | 4 | 2 | 2 | 20 |
+
+## Per-reaction instructions
+
+### Product: composite_1
+URI: https://SBOL2Build.org/composite_1/1
+
+1. Label one tube as `composite_1`.
+2. Add 2 µL nuclease-free water.
+3. Add 2 µL 10X T4 DNA Ligase Buffer.
+4. Add 4 µL T4 DNA Ligase.
+5. Add 2 µL BsaI (URI: https://SBOL2Build.org/BsaI/1).
+6. Add 2 µL backbone `pSB1C3` (URI: https://sbolcanvas.org/pSB1C3/1).
+7. Add 2 µL part `J23101` (URI: https://sbolcanvas.org/J23101/1).
+8. Add 2 µL part `B0034` (URI: https://sbolcanvas.org/B0034/1).
+9. Add 2 µL part `GFP` (URI: https://sbolcanvas.org/GFP/1).
+10. Add 2 µL part `B0015` (URI: https://sbolcanvas.org/B0015/1).
+11. Mix gently by pipetting. Do not vortex unless explicitly intended.
+12. Briefly spin down if appropriate.
+
+### Product: composite_2
+URI: https://SBOL2Build.org/composite_2/1
+
+1. Label one tube as `composite_2`.
+2. Add 2 µL nuclease-free water.
+3. Add 2 µL 10X T4 DNA Ligase Buffer.
+4. Add 4 µL T4 DNA Ligase.
+5. Add 2 µL BsaI (URI: https://SBOL2Build.org/BsaI/1).
+6. Add 2 µL backbone `pSB1C3` (URI: https://sbolcanvas.org/pSB1C3/1).
+7. Add 2 µL part `J23106` (URI: https://sbolcanvas.org/J23106/1).
+8. Add 2 µL part `B0034` (URI: https://sbolcanvas.org/B0034/1).
+9. Add 2 µL part `RFP` (URI: https://sbolcanvas.org/RFP/1).
+10. Add 2 µL part `B0015` (URI: https://sbolcanvas.org/B0015/1).
+11. Mix gently by pipetting. Do not vortex unless explicitly intended.
+12. Briefly spin down if appropriate.
+
+## Thermocycling
+- Use a cycling profile that holds 42°C for 2 minutes and 16°C for 5 minutes; repeat this profile 75 times.
+- Denature/inactivate proteins by holding 60°C for 10 minutes followed by 80°C for 10 minutes.
+- Hold at 4°C until samples are collected.
+
+## Notes
+- If the assembly was designed correctly, the final product should lack the Type IIS recognition sites used during assembly.
+- This generated document is a manual instruction sheet and not an automated OT-2 protocol.
+- Assumes all DNA parts are available at suitable concentrations and added at equal per-part volume.
+- Store the assembly product at 4 °C for better stability until used for downstream applications.
+- Validate assembled plasmids by restriction digest and gel electrophoresis, Sanger sequencing, or whole-plasmid sequencing.
diff --git a/scripts/output.json b/scripts/output.json
new file mode 100644
index 0000000..57522f7
--- /dev/null
+++ b/scripts/output.json
@@ -0,0 +1,13 @@
+[
+ {
+ "Product": "https://SBOL2Build.org/composite_1/1",
+ "Backbone": "https://sbolcanvas.org/Cir_qxow/1",
+ "PartsList": [
+ "https://sbolcanvas.org/B0034/1",
+ "https://sbolcanvas.org/B0015/1",
+ "https://sbolcanvas.org/J23101/1",
+ "https://sbolcanvas.org/GFP/1"
+ ],
+ "Restriction Enzyme": "https://SBOL2Build.org/BsaI/1"
+ }
+]
\ No newline at end of file
diff --git a/scripts/run_Bacterial_Transformation.py b/scripts/run_Bacterial_Transformation.py
new file mode 100644
index 0000000..2222e4e
--- /dev/null
+++ b/scripts/run_Bacterial_Transformation.py
@@ -0,0 +1,23 @@
+from pudu.transformation import HeatShockTransformation
+from opentrons import protocol_api
+
+# metadata
+metadata = {
+'protocolName': 'PUDU Transformation',
+'author': 'Gonzalo Vidal ',
+'description': 'Automated transformation protocol',
+'apiLevel': '2.22'}
+
+transformation_data = [
+ {
+ 'Strain': 'composite_strain_1',
+ 'Chassis': 'DH5alpha',
+ 'Plasmids': ['composite_plasmid_1']
+ }
+]
+
+def run(protocol= protocol_api.ProtocolContext):
+
+ pudu_transformation = HeatShockTransformation(transformation_data=transformation_data)
+ pudu_transformation.run(protocol)
+#After simulation look at the beginning of the output to see the position of input reagents and outputs.
diff --git a/scripts/run_Bacterial_Transformation_libre.py b/scripts/run_Bacterial_Transformation_libre.py
new file mode 100644
index 0000000..165fd2a
--- /dev/null
+++ b/scripts/run_Bacterial_Transformation_libre.py
@@ -0,0 +1,495 @@
+from opentrons import protocol_api
+from typing import List, Dict
+from pudu.utils import colors
+
+
+class Transformation():
+ '''
+ Creates a protocol for automated transformation.
+
+ Attributes
+ ----------
+ volume_dna : float
+ The volume DNA in microliters. By default, 2 microliters. We suggest 2 microliters for extracted plasmid and 5 microliters for PCR products.
+ volume_competent_cells : float
+ The volume of the competent cells in microliters. By default, 50 microliters.
+ volume_recovery_media : float
+ The volume of recovery media in microliters. By default, 100 microliters.
+ replicates : int
+ The number of replicates of the assembly reaction. By default, 2.
+ thermocycler_starting_well : int
+ The starting well of the thermocycler module. By default, 0.
+ thermocycler_labware : str
+ The labware type of the thermocycler module. By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
+ thermocycler_slots : list
+ The slots of the thermocycler module. By default, [7, 8, 10, 11].
+ temperature_module_labware : str
+ The labware type of the temperature module. By default, 'opentrons_24_aluminumblock_nest_1.5ml_snapcap'.
+ temperature_module_position : int
+ The deck position of the temperature module. By default, 1.
+ tiprack_labware : str
+ The labware type of the tiprack. By default, 'opentrons_96_tiprack_20ul'.
+ tiprack_position : int
+ The deck position of the tiprack. By default, 9.
+ pipette_type : str
+ The type of pipette. By default, 'p20_single_gen2'.
+ pipette_mount : str
+ The mount of the pipette. By default, 'left'.
+ aspiration_rate : float
+ The rate of aspiration in microliters per second. By default, 0.5 microliters per second.
+ dispense_rate : float
+ The rate of dispense in microliters per second. By default, 1 microliter per second.
+ '''
+ def __init__(self,
+ list_of_dna:List = None,
+ volume_dna:float = 20,
+ competent_cells:str = None,
+ replicates:int=2,
+ thermocycler_starting_well:int = 0,
+ thermocycler_labware:str = "nest_96_wellplate_100ul_pcr_full_skirt",
+ temperature_module_labware:str = "opentrons_24_aluminumblock_nest_1.5ml_snapcap",
+ temperature_module_position:str = '1',
+ dna_plate:str = "nest_96_wellplate_100ul_pcr_full_skirt",
+ dna_plate_position:str = '2',
+ use_dna_96plate:bool = False,
+ tiprack_p20_labware:str = "opentrons_96_tiprack_20ul",
+ tiprack_p20_position:str = "9",
+ tiprack_p200_labware:str = "opentrons_96_filtertiprack_200ul",
+ tiprack_p200_position:str = "6",
+ pipette_p20:str = "p20_single_gen2",
+ pipette_p20_position:str = "left",
+ pipette_p300:str = "p300_single_gen2",
+ pipette_p300_position:str = "right",
+ aspiration_rate:float = 0.5,
+ dispense_rate:float = 1,
+ initial_dna_well:int = 0,
+ water_testing:bool = False
+ ):
+
+ if list_of_dna is None:
+ raise ValueError ("Must input a list of DNA strings")
+ else:
+ self.list_of_dna = list_of_dna
+ self.volume_dna = volume_dna
+ if competent_cells is None:
+ raise ValueError ("Must input a competent cell strings")
+ else:
+ self.competent_cells = competent_cells
+ self.replicates = replicates
+ self.thermocycler_starting_well = thermocycler_starting_well
+ self.thermocycler_labware = thermocycler_labware
+ self.temperature_module_labware = temperature_module_labware
+ self.temperature_module_position = temperature_module_position
+ self.dna_plate = dna_plate
+ self.dna_plate_position = dna_plate_position
+ self.use_dna_96plate = use_dna_96plate
+ self.tiprack_p20_labware = tiprack_p20_labware
+ self.tiprack_p20_position = tiprack_p20_position
+ self.tiprack_p200_labware = tiprack_p200_labware
+ self.tiprack_p200_position = tiprack_p200_position
+ self.pipette_p20 = pipette_p20
+ self.pipette_p20_position = pipette_p20_position
+ self.pipette_p300 = pipette_p300
+ self.pipette_p300_position = pipette_p300_position
+ self.aspiration_rate = aspiration_rate
+ self.dispense_rate = dispense_rate
+ self.initial_dna_well = initial_dna_well
+ self.water_testing = water_testing
+
+class HeatShockTransformation(Transformation):
+ '''
+ Creates a protocol for automated transformation.
+ '''
+ def __init__(self,
+ transfer_volume_dna:float = 2,
+ transfer_volume_competent_cell:float = 20,
+ tube_volume_competent_cell:float =100,
+ transfer_volume_recovery_media:float = 60,
+ tube_volume_recovery_media:float = 1200, #add a bit more to pick it properly
+ cold_incubation1:Dict = None,
+ heat_shock:Dict = None,
+ cold_incubation2:Dict = None,
+ recovery_incubation:Dict = None,
+ *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.transfer_volume_dna = transfer_volume_dna
+ self.transfer_volume_competent_cell = transfer_volume_competent_cell
+ self.tube_volume_competent_cell = tube_volume_competent_cell
+ self.transfer_volume_recovery_media = transfer_volume_recovery_media
+ self.tube_volume_recovery_media = tube_volume_recovery_media
+ if cold_incubation1 is None:
+ self.cold_incubation1 = {'temperature': 4, 'hold_time_minutes': 30}
+ else:
+ self.cold_incubation1 = cold_incubation1
+ if heat_shock is None:
+ self.heat_shock = {'temperature': 42, 'hold_time_minutes': 1}
+ else:
+ self.heat_shock = heat_shock
+ if cold_incubation2 is None:
+ self.cold_incubation2 = {'temperature': 4, 'hold_time_minutes': 2}
+ else:
+ self.cold_incubation2 = cold_incubation2
+ if recovery_incubation is None:
+ self.recovery_incubation = {'temperature': 37, 'hold_time_minutes': 60}
+ else:
+ self.recovery_incubation = recovery_incubation
+ self.recovery_incubation = recovery_incubation
+ self.dict_of_parts_in_temp_mod_position = {}
+ self.dict_of_parts_in_thermocycler = {}
+
+ def liquid_transfer(self, protocol, pipette, volume, source, dest,
+ asp_rate: float = 0.5, disp_rate: float = 1.0,
+ blow_out: bool = True, touch_tip: bool = False,
+ mix_before: float = 0.0, mix_after: float = 0.0,
+ mix_reps: int = 3, new_tip: bool = True,
+ remove_air:bool = True, drop_tip: bool = True):
+ if new_tip:
+ pipette.pick_up_tip()
+
+ if mix_before > 0:
+ pipette.mix(mix_reps, mix_before, source)
+
+ pipette.aspirate(volume, source, rate=asp_rate)
+ pipette.dispense(volume, dest, rate=disp_rate)
+
+ if mix_after > 0:
+ pipette.mix(mix_reps, mix_after, dest)
+
+ if blow_out:
+ pipette.blow_out()
+
+ if remove_air:
+ for _ in range(2):
+ pipette.aspirate(20, dest.bottom(), rate=disp_rate)
+ pipette.dispense(20, dest.bottom(8), rate=disp_rate)
+
+ if touch_tip:
+ pipette.touch_tip(radius=0.5, v_offset=-14, speed=20)
+
+ if drop_tip:
+ pipette.drop_tip()
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+ # Force water testing mode during simulation
+ if protocol.is_simulating():
+ self.water_testing = True
+ protocol.comment("Simulation detected - enabling water testing mode")
+ # Labware
+ # Load the temperature module
+ temperature_module = protocol.load_module('temperature module', self.temperature_module_position)
+ alumblock = temperature_module.load_labware(self.temperature_module_labware)
+ # Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
+ thermocycler_module = protocol.load_module('thermocycler module')
+ pcr_plate = thermocycler_module.load_labware(self.thermocycler_labware)
+ #If using the 96-well pcr plate as a dna construct source
+ if self.use_dna_96plate:
+ dna_plate = protocol.load_labware(self.dna_plate, self.dna_plate_position)
+ # Load the tiprack
+ tiprack_p20 = protocol.load_labware(self.tiprack_p20_labware, self.tiprack_p20_position)
+ tiprack_p200 = protocol.load_labware(self.tiprack_p200_labware, self.tiprack_p200_position)
+ # Load the pipette
+ pipette_p20 = protocol.load_instrument(self.pipette_p20, self.pipette_p20_position, tip_racks=[tiprack_p20])
+ pipette_p300 = protocol.load_instrument(self.pipette_p300, self.pipette_p300_position,
+ tip_racks=[tiprack_p200])
+ #Validate protocol
+ self._validate_protocol(protocol, alumblock)
+
+ #Load Reagents
+ if self.use_dna_96plate:
+ DNA_wells = self._load_dna_list(protocol, dna_plate, self.volume_dna, self.list_of_dna, initial_well=self.initial_dna_well)
+ competent_cell_wells = self._load_reagents(protocol, alumblock, self.tube_volume_competent_cell, f"Competent Cell {self.competent_cells}", self.competent_cell_tubes_needed)
+ media_wells = self._load_reagents(protocol, alumblock, self.tube_volume_recovery_media, "Media", self.media_tubes_needed, initial_well=len(competent_cell_wells))
+
+ else:
+ DNA_wells = self._load_dna_list(protocol, alumblock, self.volume_dna, self.list_of_dna)
+ competent_cell_wells = self._load_reagents(protocol, alumblock, self.tube_volume_competent_cell, f"Competent Cell {self.competent_cells}", self.competent_cell_tubes_needed, initial_well=len(DNA_wells))
+ media_wells = self._load_reagents(protocol, alumblock, self.tube_volume_recovery_media, "Media", self.media_tubes_needed, initial_well=len(DNA_wells)+len(competent_cell_wells))
+
+ #Set Temperature module and Thermocycler module to 4
+ thermocycler_module.open_lid()
+ if not self.water_testing:
+ temperature_module.set_temperature(4)
+ thermocycler_module.set_block_temperature(4)
+
+ #Load competent cells into the thermocycler
+ pipette = pipette_p300
+ self._transfer_competent_cells(protocol, pipette, pcr_plate, competent_cell_wells, self.transfer_volume_competent_cell, self.thermocycler_starting_well)
+
+ #Load DNA into the thermocycler
+ if self.transfer_volume_dna > 20:
+ pipette = pipette_p300
+ else:
+ pipette = pipette_p20
+ self._transfer_DNA(protocol, pipette, pcr_plate, DNA_wells, self.transfer_volume_dna, self.thermocycler_starting_well)
+
+ # Cold Incubation
+ thermocycler_module.close_lid()
+ profile = [
+ self.cold_incubation1, # 1st cold incubation (long)
+ self.heat_shock, # Heat shock
+ self.cold_incubation2 # 2nd cold incubation (short)
+ ]
+ if not self.water_testing:
+ thermocycler_module.execute_profile(steps=profile, repetitions=1, block_max_volume=30)
+ thermocycler_module.open_lid()
+
+ #Load liquid broth
+ pipette = pipette_p300
+ self._transfer_liquid_broth(protocol, pipette, pcr_plate, media_wells, self.transfer_volume_recovery_media, self.thermocycler_starting_well)
+
+ # Recovery Incubation
+ thermocycler_module.close_lid()
+ recovery = [
+ self.recovery_incubation
+ ]
+ if not self.water_testing:
+ thermocycler_module.execute_profile(steps=recovery, repetitions=1, block_max_volume=30)
+
+ # output
+ print('Strain and media tube in temp_mod')
+ print(self.dict_of_parts_in_temp_mod_position)
+ print('Genetically modified organisms in thermocycler')
+ print(self.dict_of_parts_in_thermocycler)
+
+ def _validate_protocol(self, protocol, labware):
+ #Number of available wells to load into
+ module_wells = len(labware.wells())
+ #Number of dna constructs to be used
+ total_constructs = len(self.list_of_dna)
+ #Number of tubes with competent cells to be used
+ self.total_transformations = total_constructs*self.replicates
+ self.transformations_per_cell_tube = self.tube_volume_competent_cell//self.transfer_volume_competent_cell
+ self.competent_cell_tubes_needed = (self.total_transformations + self.transformations_per_cell_tube - 1) // self.transformations_per_cell_tube
+ #Number of tubes with media to be used
+ self.transformations_per_media_tube = self.tube_volume_recovery_media//self.transfer_volume_recovery_media
+ self.media_tubes_needed = (self.total_transformations + self.transformations_per_media_tube - 1) // self.transformations_per_media_tube
+ if self.use_dna_96plate:
+ if self.competent_cell_tubes_needed + self.media_tubes_needed > module_wells:
+ raise ValueError(f'The number of reagents is more that {module_wells}.'
+ f'There are {self.competent_cell_tubes_needed} tubes with competent cells.'
+ f'{self.media_tubes_needed} tubes with media.'
+ f'Please modify the protocol and try again.')
+ else:
+ if total_constructs + self.competent_cell_tubes_needed + self.media_tubes_needed > module_wells:
+ raise ValueError(f'The number of reagents is more than {module_wells}.'
+ f'There are {total_constructs} DNA constructs.'
+ f'{self.competent_cell_tubes_needed} tubes with competent cells.'
+ f'{self.media_tubes_needed} tubes with media.'
+ f'Please modify the protocol and try again.')
+
+
+ def _load_dna_list(self, protocol, labware, volume, dna_list, initial_well=0, description=None, color_index = None):
+ """
+ Load individual DNA constructs into wells, one construct per well.
+
+ Parameters:
+ - protocol: Protocol context
+ - labware: Labware object (temperature module or DNA plate)
+ - volume: Volume to load in µL
+ - construct_list: List of construct names (e.g., ['GVD0011', 'GVD0013'])
+ - initial_well: Starting well index
+ - color_index: Starting color index (auto-increments if None)
+
+ Returns:
+ - List of well objects
+ """
+ wells = []
+ current_color = color_index if color_index is not None else 0
+ for i, construct in enumerate(dna_list):
+ #Get the well
+ well = labware.wells()[initial_well+i]
+ wells.append(well)
+
+ #Covert tuple to string if needed
+ if isinstance(construct, tuple):
+ construct_name = '_'.join(construct)
+ else:
+ construct_name = construct
+ #Define and load "liquid"
+ liquid = protocol.define_liquid(
+ name = construct_name,
+ description= description if description is not None else f"{construct} DNA construct",
+ display_color= colors[current_color%len(colors)]
+ )
+ well.load_liquid(liquid, volume=volume)
+
+ #Track in dictionary
+ if not self.use_dna_96plate:
+ self.dict_of_parts_in_temp_mod_position[construct_name] = well.well_name
+ current_color += 1
+ return wells
+
+ def _load_reagents(self, protocol, labware, volume, reagent_name, tube_count, initial_well=0, color_index=None):
+ """
+ Load multiple tubes of the same reagent type.
+
+ Parameters:
+ - protocol: Protocol context
+ - labware: Labware object (temperature module)
+ - volume: Volume to load in µL
+ - reagent_base_name: Base name for reagent (e.g., "Competent_Cell", "Media")
+ - tube_count: Number of tubes to load
+ - initial_well: Starting well index
+ - color_index: Starting color index (auto-increments if None)
+
+ Returns:
+ - List of well objects
+ """
+ wells = []
+ current_color = color_index if color_index is not None else 0
+ for i in range(tube_count):
+ well = labware.wells()[initial_well+i]
+ wells.append(well)
+ name = f"{reagent_name}_{i+1}"
+
+ liquid = protocol.define_liquid(
+ name = name,
+ display_color= colors[current_color%len(colors)]
+ )
+
+ well.load_liquid(liquid, volume=volume)
+ self.dict_of_parts_in_temp_mod_position[name] = well.well_name
+ current_color += 1
+ return wells
+
+ def _transfer_competent_cells(self, protocol, pipette, pcr_plate, competent_cell_wells,
+ transfer_volume_competent_cell, thermocycler_starting_well):
+ """
+ Transfer competent cells to thermocycler wells using distribute method.
+
+ Parameters:
+ - protocol: Protocol context
+ - pipette: Pipette instrument
+ - pcr_plate: Thermocycler plate
+ - competent_cell_wells: List of wells containing competent cells
+ - transfer_volume_competent_cell: Volume to transfer per well
+ - thermocycler_starting_well: Starting well index in thermocycler
+ """
+ well_index = thermocycler_starting_well
+
+ for tube_index, source_well in enumerate(competent_cell_wells):
+ #Calculate how many wells this cell tube will fill
+ remaining_transformations = self.total_transformations - (tube_index * self.transformations_per_cell_tube)
+ wells_to_fill = min(self.transformations_per_cell_tube, remaining_transformations)
+
+ #Destination wells
+ dest_wells = pcr_plate.wells()[well_index:well_index+wells_to_fill]
+
+ #Distribute
+ pipette.distribute(
+ volume=transfer_volume_competent_cell,
+ source=source_well,
+ dest=dest_wells,
+ mix_before=(3,50),
+ disposal_volume=0,
+ new_tip='once'
+ )
+
+ #Thermocycler Dictionary
+ name = f"Competent_Cell_{self.competent_cells}_{tube_index+1}"
+ for well in dest_wells:
+ if well.well_name not in self.dict_of_parts_in_thermocycler:
+ self.dict_of_parts_in_thermocycler[well.well_name] = []
+ self.dict_of_parts_in_thermocycler[well.well_name].append(name)
+
+ well_index += wells_to_fill
+
+ def _transfer_DNA(self, protocol, pipette, pcr_plate, DNA_wells, transfer_volume_dna, thermocycler_starting_well):
+ """
+ Transfer DNA constructs to thermocycler wells with replicates grouped together.
+
+ Parameters:
+ - protocol: Protocol context
+ - pipette: Pipette instrument
+ - pcr_plate: Thermocycler plate
+ - DNA_wells: List of wells containing DNA constructs
+ - transfer_volume_dna: Volume to transfer per well
+ - thermocycler_starting_well: Starting well index in thermocycler
+ """
+ for construct_index, (construct_name, source_well) in enumerate(zip(self.list_of_dna, DNA_wells)):
+ construct_well = construct_index * self.replicates + thermocycler_starting_well
+
+ for replicate in range(self.replicates):
+ dest_well = pcr_plate.wells()[construct_well+replicate]
+ #Transfer liquid
+ self.liquid_transfer(
+ protocol=protocol,
+ pipette=pipette,
+ volume=transfer_volume_dna,
+ source=source_well,
+ dest=dest_well,
+ asp_rate=self.aspiration_rate,
+ disp_rate=self.dispense_rate,
+ mix_before=transfer_volume_dna,
+ touch_tip=True
+ )
+
+ #Track in dictionary
+ if dest_well.well_name not in self.dict_of_parts_in_thermocycler:
+ self.dict_of_parts_in_thermocycler[dest_well.well_name] = []
+ self.dict_of_parts_in_thermocycler[dest_well.well_name].append(construct_name)
+
+ def _transfer_liquid_broth(self, protocol, pipette, pcr_plate, media_wells, transfer_volume_recovery_media,
+ thermocycler_starting_well):
+ """
+ Transfer recovery media to thermocycler wells using distribute method.
+
+ Parameters:
+ - protocol: Protocol context
+ - pipette: Pipette instrument
+ - pcr_plate: Thermocycler plate
+ - media_wells: List of wells containing recovery media
+ - transfer_volume_recovery_media: Volume to transfer per well
+ - thermocycler_starting_well: Starting well index in thermocycler
+ """
+ well_index = thermocycler_starting_well
+
+ for tube_index, source_well in enumerate(media_wells):
+ #Calculate how many wells this media tube will fill
+ remaining_transformations = self.total_transformations - (tube_index * self.transformations_per_media_tube)
+ wells_to_fill = min(self.transformations_per_media_tube, remaining_transformations)
+
+ # Get destination wells for this tube using .top() to avoid contamination
+ dest_wells = [pcr_plate.wells()[well_index+i].top(2) for i in range(int(wells_to_fill))]
+
+ #Distribute recovery media
+ pipette.distribute(
+ volume=transfer_volume_recovery_media,
+ source=source_well,
+ dest=dest_wells,
+ disposal_volume=0,
+ new_tip='once',
+ air_gap=10
+ )
+
+ #Track in dictionary
+ media_name = f"Media_{tube_index+1}"
+ for i in range(int(wells_to_fill)):
+ well_name = pcr_plate.wells()[well_index + i].well_name
+ if well_name not in self.dict_of_parts_in_thermocycler:
+ self.dict_of_parts_in_thermocycler[well_name] = []
+ self.dict_of_parts_in_thermocycler[well_name].append(media_name)
+
+ well_index += wells_to_fill
+
+
+
+# metadata
+metadata = {
+'protocolName': 'PUDU Transformation',
+'author': 'Gonzalo Vidal , refactored by Oscar Rodriguez',
+'description': 'Automated heat-shock transformation protocol',
+'apiLevel': '2.22'}
+
+def run(protocol= protocol_api.ProtocolContext):
+ # list_of_dna = ['GVD0011', 'GVD0013', 'GVD0015', 'GVD0016', 'GVD0019', 'GVD0020']
+ # list_of_dna = ['GVD0011']
+ list_of_dna = [('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'),
+ ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'),
+ ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'),
+ ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'),
+ ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'),
+ ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2')]
+ pudu_transformation = HeatShockTransformation(list_of_dna= list_of_dna, competent_cells = 'DH5alpha', use_dna_96plate=True, water_testing=True)
+ pudu_transformation.run(protocol)
diff --git a/scripts/run_Chemical_Transformation.py b/scripts/run_Chemical_Transformation.py
deleted file mode 100644
index 20c4bc7..0000000
--- a/scripts/run_Chemical_Transformation.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from pudu.transformation import Chemical_transformation
-from opentrons import protocol_api
-
-# metadata
-metadata = {
-'protocolName': 'PUDU Transformation',
-'author': 'Gonzalo Vidal ',
-'description': 'Automated transformation protocol',
-'apiLevel': '2.13'}
-
-def run(protocol= protocol_api.ProtocolContext):
-
- pudu_transformation = Chemical_transformation(list_of_dnas=['pro', 'rbs','cds', 'ter'], competent_cells = 'DH5alpha')
- pudu_transformation.run(protocol)
-#After simulation look at the beginning of the output to see the position of input reagents and outputs.
\ No newline at end of file
diff --git a/scripts/run_Chemical_Transformation_libre.py b/scripts/run_Chemical_Transformation_libre.py
new file mode 100644
index 0000000..2d13931
--- /dev/null
+++ b/scripts/run_Chemical_Transformation_libre.py
@@ -0,0 +1,310 @@
+from opentrons import protocol_api
+from typing import List, Dict
+
+thermo_wells = [
+'A1','A2','A3','A4','A5','A6','A7','A8','A9','A10','A11','A12',
+'B1','B2','B3','B4','B5','B6','B7','B8','B9','B10','B11','B12',
+'C1','C2','C3','C4','C5','C6','C7','C8','C9','C10','C11','C12',
+'D1','D2','D3','D4','D5','D6','D7','D8','D9','D10','D11','D12',
+'E1','E2','E3','E4','E5','E6','E7','E8','E9','E10','E11','E12',
+'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12',
+'G1','G2','G3','G4','G5','G6','G7','G8','G9','G10','G11','G12',
+'H1','H2','H3','H4','H5','H6','H7','H8','H9','H10','H11','H12'
+]
+
+temp_wells = [
+'A1','A2','A3','A4','A5','A6',
+'B1','B2','B3','B4','B5','B6',
+'C1','C2','C3','C4','C5','C6',
+'D1','D2','D3','D4','D5','D6'
+]
+
+def liquid_transfer(pipette, volume, source, destination, asp_rate:float=0.5, disp_rate:float=1.0, blow_out:bool=True, touch_tip:bool=False, mix_before:float=0.0, mix_after:float=0.0, mix_reps:int=3, new_tip:bool=True, drop_tip:bool=True):
+ if new_tip:
+ pipette.pick_up_tip()
+ if mix_before > 0:
+ pipette.mix(mix_reps, mix_before, source)
+ pipette.aspirate(volume, source, rate=asp_rate)
+ pipette.dispense(volume, destination, rate=disp_rate)
+ if mix_after > 0:
+ pipette.mix(mix_reps, mix_after, destination)
+ if blow_out:
+ pipette.blow_out()
+ if touch_tip:
+ pipette.touch_tip()
+ if drop_tip:
+ pipette.drop_tip()
+
+class Transformation():
+ '''
+ Creates a protocol for automated transformation.
+
+ Attributes
+ ----------
+ volume_dna : float
+ The volume DNA in microliters. By default, 2 microliters. We suggest 2 microliters for extracted plasmid and 5 microliters for PCR products.
+ volume_competent_cells : float
+ The volume of the competent cells in microliters. By default, 50 microliters.
+ volume_recovery_media : float
+ The volume of recovery media in microliters. By default, 100 microliters.
+ replicates : int
+ The number of replicates of the assembly reaction. By default, 2.
+ thermocycler_starting_well : int
+ The starting well of the thermocycler module. By default, 0.
+ thermocycler_labware : str
+ The labware type of the thermocycler module. By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
+ thermocycler_slots : list
+ The slots of the thermocycler module. By default, [7, 8, 10, 11].
+ temperature_module_labware : str
+ The labware type of the temperature module. By default, 'opentrons_24_aluminumblock_nest_1.5ml_snapcap'.
+ temperature_module_slot : int
+ The slot of the temperature module. By default, 1.
+ tiprack_labware : str
+ The labware type of the tiprack. By default, 'opentrons_96_tiprack_20ul'.
+ tiprack_slot : int
+ The slot of the tiprack. By default, 9.
+ pipette_type : str
+ The type of pipette. By default, 'p20_single_gen2'.
+ pipette_mount : str
+ The mount of the pipette. By default, 'left'.
+ aspiration_rate : float
+ The rate of aspiration in microliters per second. By default, 0.5 microliters per second.
+ dispense_rate : float
+ The rate of dispense in microliters per second. By default, 1 microliter per second.
+ '''
+ def __init__(self,
+ list_of_dnas:List = [], # TODO:add SBOL version
+ competent_cells: str = None, # TODO: add SBOL version
+ replicates:int=2,
+ thermocycler_starting_well:int = 0,
+ thermocycler_labware:str = 'nest_96_wellplate_100ul_pcr_full_skirt',
+ temperature_module_labware:str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ temperature_module_position:int = 1,
+
+ tiprack_p20_labware:str = 'opentrons_96_tiprack_20ul',
+ tiprack_p20_osition:int = 9,
+ tiprack_p300_labware:str = 'opentrons_96_tiprack_300ul',
+ tiprack_p300_osition:int = 6,
+ pipette_p20:str = 'p20_single_gen2',
+ pipette_p300:str = 'p300_single_gen2',
+ pipette_p20_position:str = 'left',
+ pipette_p300_position:str = 'right',
+ aspiration_rate:float=0.5,
+ dispense_rate:float=1,):
+
+ self.list_of_dnas = list_of_dnas
+ self.competent_cells = competent_cells
+ self.replicates = replicates
+ self.thermocycler_starting_well = thermocycler_starting_well
+ self.thermocycler_labware = thermocycler_labware
+ self.temperature_module_labware = temperature_module_labware
+ self.temperature_module_position = temperature_module_position
+ self.tiprack_p20_labware = tiprack_p20_labware
+ self.tiprack_p20_position = tiprack_p20_osition
+ self.tiprack_p300_labware = tiprack_p300_labware
+ self.tiprack_p300_position = tiprack_p300_osition
+ self.pipette_p20 = pipette_p20
+ self.pipette_p300 = pipette_p300
+ self.pipette_p20_position = pipette_p20_position
+ self.pipette_p300_position = pipette_p300_position
+ self.aspiration_rate = aspiration_rate
+ self.dispense_rate = dispense_rate
+
+class Chemical_transformation(Transformation):
+ '''
+ Creates a protocol for automated transformation.
+
+ Attributes
+ ----------
+ volume_dna : float
+ The volume DNA in microliters. By default, 2 microliters. We suggest 2 microliters for extracted plasmid and 5 microliters for PCR products.
+ volume_competent_cells : float
+ The volume of the competent cells in microliters. By default, 50 microliters.
+ volume_recovery_media : float
+ The volume of recovery media in microliters. By default, 100 microliters.
+ replicates : int
+ The number of replicates of the assembly reaction. By default, 2.
+ thermocycler_starting_well : int
+ The starting well of the thermocycler module. By default, 0.
+ thermocycler_labware : str
+ The labware type of the thermocycler module. By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
+ thermocycler_slots : list
+ The slots of the thermocycler module. By default, [7, 8, 10, 11].
+ temperature_module_labware : str
+ The labware type of the temperature module. By default, 'opentrons_24_aluminumblock_nest_1.5ml_snapcap'.
+ temperature_module_slot : int
+ The slot of the temperature module. By default, 1.
+ tiprack_labware : str
+ The labware type of the tiprack. By default, 'opentrons_96_tiprack_20ul'.
+ tiprack_slot : int
+ The slot of the tiprack. By default, 9.
+ pipette_type : str
+ The type of pipette. By default, 'p20_single_gen2'.
+ pipette_mount : str
+ The mount of the pipette. By default, 'left'.
+ aspiration_rate : float
+ The rate of aspiration in microliters per second. By default, 0.5 microliters per second.
+ dispense_rate : float
+ The rate of dispense in microliters per second. By default, 1 microliter per second.
+ '''
+ def __init__(self,
+ volume_dna:float = 2,
+ volume_competent_cell_to_add:float = 20,
+ volume_competent_cell_per_tube:float =100,
+ volume_recovery_media_to_add:float = 60,
+ volume_recovery_media_per_tube:float = 1200, #add a bit more to pick it properly
+ cold_incubation1:Dict = {'temperature': 4, 'hold_time_minutes': 30},
+ heat_shock:Dict = {'temperature': 42, 'hold_time_minutes': 1},
+ cold_incubation2:Dict = {'temperature': 4, 'hold_time_minutes': 2},
+ recovery_incubation:Dict = {'temperature': 37, 'hold_time_minutes': 60},
+ *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.volume_dna = volume_dna
+ self.volume_competent_cell_to_add = volume_competent_cell_to_add
+ self.volume_competent_cell_per_tube = volume_competent_cell_per_tube
+ self.volume_recovery_media = volume_recovery_media_to_add
+ self.volume_recovery_media_per_tube = volume_recovery_media_per_tube
+ self.cold_incubation1 = cold_incubation1
+ self.heat_shock = heat_shock
+ self.cold_incubation2 = cold_incubation2
+ self.recovery_incubation = recovery_incubation
+ self.dict_of_parts_in_temp_mod_position = {}
+ self.dict_of_parts_in_thermocycler = {}
+
+ metadata = {
+ 'protocolName': 'PUDU Transformation',
+ 'author': 'Gonzalo Vidal ',
+ 'description': 'Automated transformation protocol',
+ 'apiLevel': '2.13'}
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+
+ #Labware
+ #Load the magnetic module
+ tem_mod = protocol.load_module('temperature module', f'{self.temperature_module_position}')
+ tem_mod_block = tem_mod.load_labware(self.temperature_module_labware)
+ #Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
+ thermocycler_mod = protocol.load_module('thermocycler module')
+ thermocycler_mod_plate = thermocycler_mod.load_labware(self.thermocycler_labware)
+ #Load the tiprack
+ tiprack_p20 = protocol.load_labware(self.tiprack_p20_labware, f'{self.tiprack_p20_position}')
+ tiprack_p300 = protocol.load_labware(self.tiprack_p300_labware, f'{self.tiprack_p300_position}')
+ #Load the pipette
+ pipette_p20 = protocol.load_instrument(self.pipette_p20, self.pipette_p20_position, tip_racks=[tiprack_p20])
+ pipette_p300 = protocol.load_instrument(self.pipette_p300, self.pipette_p300_position, tip_racks=[tiprack_p300])
+
+ #Load the reagents
+ #Check number of compenent cells and DNAs
+ total_transformations = len(self.list_of_dnas)*self.replicates
+ transformations_per_tube = int(self.volume_competent_cell_per_tube//self.volume_competent_cell_to_add)
+ number_of_tubes_with_competent_cells_needed = int(total_transformations//transformations_per_tube+1) #TODO: make an int, maybe use sail
+ #Check number of tubes with media
+ transformations_per_media_tube = int(self.volume_recovery_media_per_tube//self.volume_recovery_media)
+ number_of_tubes_with_media_needed = int(total_transformations//transformations_per_media_tube+1) #TODO: make an int, maybe use sail
+ if len(self.list_of_dnas)+number_of_tubes_with_competent_cells_needed+number_of_tubes_with_media_needed > 24:
+ raise ValueError(f'The number of reagents is more than 24. There are {len(self.list_of_dnas)} DNAs, {number_of_tubes_with_competent_cells_needed} tubes with competent cells and {number_of_tubes_with_media_needed} tubes with media. Please change the protocol and try again.')
+ temp_wells_counter = 0
+ for dna in self.list_of_dnas:
+ self.dict_of_parts_in_temp_mod_position[dna] = temp_wells[temp_wells_counter]
+ temp_wells_counter += 1
+ for i in range(number_of_tubes_with_competent_cells_needed):
+ self.dict_of_parts_in_temp_mod_position[f'Competent_cells_tube_{i}'] = temp_wells[temp_wells_counter]
+ temp_wells_counter += 1
+ for i in range(number_of_tubes_with_media_needed):
+ self.dict_of_parts_in_temp_mod_position[f'Media_tube_{i}'] = temp_wells[temp_wells_counter]
+ temp_wells_counter += 1
+ #Set Temperature and Thermocycler module to 4
+ tem_mod.set_temperature(4)
+ thermocycler_mod.open_lid()
+ thermocycler_mod.set_block_temperature(4)
+ #Load cells into the thermocycler
+ if self.volume_competent_cell_to_add > 20:
+ pipette = pipette_p300
+ else:
+ pipette = pipette_p20
+ current_thermocycler_well_comp = self.thermocycler_starting_well
+ transformation_well = 1
+ #for r in range(self.replicates):
+ for i in range(number_of_tubes_with_competent_cells_needed):
+ for j in range(transformations_per_tube):
+ part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[current_thermocycler_well_comp]]
+ liquid_transfer(pipette, self.volume_competent_cell_to_add, tem_mod_block[self.dict_of_parts_in_temp_mod_position[f'Competent_cells_tube_{i}']], part_ubication_in_thermocyler, self.aspiration_rate, self.dispense_rate, mix_before=self.volume_competent_cell_to_add -5)
+ if j == 0:
+ self.dict_of_parts_in_thermocycler[f'Competent_cells_tube_{i}'] = [thermo_wells[current_thermocycler_well_comp]]
+ else:
+ self.dict_of_parts_in_thermocycler[f'Competent_cells_tube_{i}'].append(thermo_wells[current_thermocycler_well_comp])
+ current_thermocycler_well_comp+=1
+ if transformation_well == total_transformations:
+ break
+ transformation_well+=1
+
+ #Load DNA into the thermocycler and mix
+ if self.volume_dna > 20:
+ pipette = pipette_p300
+ else:
+ pipette = pipette_p20
+ current_thermocycler_well_dna = self.thermocycler_starting_well
+ for r in range(self.replicates):
+ for dna in self.list_of_dnas:
+ part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[current_thermocycler_well_dna]]
+ liquid_transfer(pipette, self.volume_dna, tem_mod_block[self.dict_of_parts_in_temp_mod_position[dna]], part_ubication_in_thermocyler, self.aspiration_rate, self.dispense_rate, mix_before=self.volume_dna)
+ if r == 0:
+ self.dict_of_parts_in_thermocycler[dna] = [thermo_wells[current_thermocycler_well_dna]]
+ else:
+ self.dict_of_parts_in_thermocycler[dna].append(thermo_wells[current_thermocycler_well_dna])
+ current_thermocycler_well_dna+=1
+
+ #Cold incubation
+ thermocycler_mod.close_lid()
+ profile = [
+ self.cold_incubation1, #1st cold incubation (long)
+ self.heat_shock, #Heatshock
+ self.cold_incubation2] #2nd cold incubation (short)
+ thermocycler_mod.execute_profile(steps=profile, repetitions=1, block_max_volume=30)
+ #Load LB and recovery incubation
+ thermocycler_mod.open_lid()
+ #TODO: check if there is the need for more than one tube
+ if self.volume_recovery_media > 20:
+ pipette = pipette_p300
+ else:
+ pipette = pipette_p20
+ current_thermocycler_well_media = self.thermocycler_starting_well
+ transformation_well = 1
+ for i in range(number_of_tubes_with_media_needed):
+ for j in range(transformations_per_media_tube):
+ part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[current_thermocycler_well_media]]
+ liquid_transfer(pipette, self.volume_recovery_media, tem_mod_block[self.dict_of_parts_in_temp_mod_position[f'Media_tube_{i}']], part_ubication_in_thermocyler, self.aspiration_rate, self.dispense_rate, mix_after=self.volume_recovery_media -5)
+ if j == 0:
+ self.dict_of_parts_in_thermocycler[f'Media_tube_{i}'] = [thermo_wells[current_thermocycler_well_media]]
+ else:
+ self.dict_of_parts_in_thermocycler[f'Media_tube_{i}'].append(thermo_wells[current_thermocycler_well_media])
+ current_thermocycler_well_media+=1
+ if transformation_well == total_transformations:
+ break
+ transformation_well+=1
+ thermocycler_mod.close_lid()
+ recovery = [
+ self.recovery_incubation]
+ thermocycler_mod.execute_profile(steps=recovery, repetitions=1, block_max_volume=30)
+
+ #output
+ print('Strain and media tube in temp_mod')
+ print(self.dict_of_parts_in_temp_mod_position)
+ print('Genetically modified organisms in thermocycler')
+ print(self.dict_of_parts_in_thermocycler)
+ #Optionally plate
+ #END
+
+# metadata
+metadata = {
+'protocolName': 'PUDU Transformation',
+'author': 'Gonzalo Vidal ',
+'description': 'Automated transformation protocol',
+'apiLevel': '2.13'}
+
+def run(protocol= protocol_api.ProtocolContext):
+
+ pudu_transformation = Chemical_transformation(list_of_dnas=['GVD0011','GVD0013','GVD0015', 'GVD0016', 'GVD0019', 'GVD0020'], competent_cells = 'DH5alpha')
+ pudu_transformation.run(protocol)
+#After simulation look at the beginning of the output to see the position of input reagents and outputs.
\ No newline at end of file
diff --git a/scripts/run_Domestication.py b/scripts/run_Domestication.py
index 3410657..2f55b0d 100644
--- a/scripts/run_Domestication.py
+++ b/scripts/run_Domestication.py
@@ -6,11 +6,11 @@
'protocolName': 'PUDU Domestication',
'author': 'Gonzalo Vidal ',
'description': 'Automated DNA domestication protocol',
-'apiLevel': '2.13'}
+'apiLevel': '2.22'}
def run(protocol= protocol_api.ProtocolContext):
-
- pudu_domestication = Domestication(parts=['pro', 'rbs','cds', 'ter'], acceptor_backbone='UA')
+ assembly = [{"parts": ['pro', 'rbs', 'cds', 'ter'], "backbone": 'UA', "restriction_enzyme": "BsaI"}]
+ pudu_domestication = Domestication(assemblies=assembly)
pudu_domestication.run(protocol)
#After simulation look at the beginning of the output to see the position of input reagents and outputs.
\ No newline at end of file
diff --git a/scripts/run_Loop_assembly.py b/scripts/run_Loop_assembly.py
index 2e15aab..3e57a4b 100644
--- a/scripts/run_Loop_assembly.py
+++ b/scripts/run_Loop_assembly.py
@@ -1,6 +1,6 @@
-from pudu.assembly import Loop_assembly
+from pudu.assembly import LoopAssembly
from opentrons import protocol_api
-import sbol3
+
assembly_Odd_1 = {"promoter":["j23101", "j23100"], "rbs":"B0034", "cds":"GFP", "terminator":"B0015", "receiver":"Odd_1"}
assembly_Even_2 = {"c4_receptor":"GD0001", "c4_buff_gfp":"GD0002", "spacer1":"20ins1", "spacer2":"Even_2", "receiver":"Even_2"}
@@ -11,9 +11,8 @@
'protocolName': 'PUDU Loop assembly',
'author': 'Gonzalo Vidal ',
'description': 'Automated DNA assembly Loop protocol',
-'apiLevel': '2.13'}
+'apiLevel': '2.14'}
def run(protocol= protocol_api.ProtocolContext):
-
- pudu_loop_assembly = Loop_assembly(assemblies=assemblies,)
+ pudu_loop_assembly = LoopAssembly(assemblies=assemblies,)
pudu_loop_assembly.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_Loop_assembly_libre.py b/scripts/run_Loop_assembly_libre.py
index 17a0d3d..fe2750c 100644
--- a/scripts/run_Loop_assembly_libre.py
+++ b/scripts/run_Loop_assembly_libre.py
@@ -271,7 +271,7 @@ def run(self, protocol: protocol_api.ProtocolContext):
profile = [
{'temperature': 42, 'hold_time_minutes': 2},
{'temperature': 16, 'hold_time_minutes': 5}]
- thermocycler_mod.execute_profile(steps=profile, repetitions=25, block_max_volume=30)
+ thermocycler_mod.execute_profile(steps=profile, repetitions=75, block_max_volume=30)
denaturation = [
{'temperature': 60, 'hold_time_minutes': 10},
@@ -287,7 +287,7 @@ def run(self, protocol: protocol_api.ProtocolContext):
print(self.dict_of_parts_in_thermocycler)
# assembly
-assembly_Odd_1 = {"promoter":["GVP0010", "GVP0011", "GVP0014"], "rbs":["B0033","B0034"], "cds":"sfGFP", "terminator":"B0015", "receiver":"Odd_1"}
+assembly_Odd_1 = {"promoter":["GVP0008", "GVP0010", "GVP0012", "GVP0013", "GVP0016", "GVP0017"], "rbs":"B0034", "cds":"sfGFP", "terminator":"B0015", "receiver":"Odd_1"}
#assembly_Even_2 = {"c4_receptor":"GD0001", "c4_buff_gfp":"GD0002", "spacer1":"20ins1", "spacer2":"Even_2", "receiver":"Even_2"}
assemblies = [assembly_Odd_1]
diff --git a/scripts/run_Plate_samples.py b/scripts/run_Plate_samples.py
index 5bdfc5a..e64b34c 100644
--- a/scripts/run_Plate_samples.py
+++ b/scripts/run_Plate_samples.py
@@ -1,4 +1,4 @@
-from pudu.test_setup import Plate_samples
+from pudu.sample_preparation import PlateSamples
from opentrons import protocol_api
# metadata
@@ -6,9 +6,9 @@
'protocolName': 'PUDU Plate Setup',
'author': 'Gonzalo Vidal ',
'description': 'Automated 96 well plate setup protocol',
-'apiLevel': '2.13'}
+'apiLevel': '2.14'}
def run(protocol= protocol_api.ProtocolContext):
- pudu_plate_samples = Plate_samples(samples=['s1', 's2','s3', 's4', 's5'])
+ pudu_plate_samples = PlateSamples(samples=['s1', 's2','s3', 's4', 's5'])
pudu_plate_samples.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_Plate_samples_libre.py b/scripts/run_Plate_samples_libre.py
new file mode 100644
index 0000000..768316c
--- /dev/null
+++ b/scripts/run_Plate_samples_libre.py
@@ -0,0 +1,165 @@
+from opentrons import protocol_api
+from typing import List, Union
+
+thermo_wells = [
+'A1','A2','A3','A4','A5','A6','A7','A8','A9','A10','A11','A12',
+'B1','B2','B3','B4','B5','B6','B7','B8','B9','B10','B11','B12',
+'C1','C2','C3','C4','C5','C6','C7','C8','C9','C10','C11','C12',
+'D1','D2','D3','D4','D5','D6','D7','D8','D9','D10','D11','D12',
+'E1','E2','E3','E4','E5','E6','E7','E8','E9','E10','E11','E12',
+'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12',
+'G1','G2','G3','G4','G5','G6','G7','G8','G9','G10','G11','G12',
+'H1','H2','H3','H4','H5','H6','H7','H8','H9','H10','H11','H12'
+]
+
+temp_wells = [
+'A1','A2','A3','A4','A5','A6',
+'B1','B2','B3','B4','B5','B6',
+'C1','C2','C3','C4','C5','C6',
+'D1','D2','D3','D4','D5','D6'
+]
+
+
+def liquid_transfer(pipette, volume, source, destination, asp_rate:float=0.5, disp_rate:float=1.0, blow_out:bool=True, touch_tip:bool=False, mix_before:float=0.0, mix_after:float=0.0, mix_reps:int=3, new_tip:bool=True, drop_tip:bool=True):
+ if new_tip:
+ pipette.pick_up_tip()
+ if mix_before > 0:
+ pipette.mix(mix_reps, mix_before, source)
+ pipette.aspirate(volume, source, rate=asp_rate)
+ pipette.dispense(volume, destination, rate=disp_rate)
+ if mix_after > 0:
+ pipette.mix(mix_reps, mix_after, destination)
+ if blow_out:
+ pipette.blow_out()
+ if touch_tip:
+ pipette.touch_tip()
+ if drop_tip:
+ pipette.drop_tip()
+
+#Define slots, to allocate 4 samples in each slot, lasts slots allocate in the border where border effects apply
+slot_1 = ['A2', 'B2', 'C2', 'D2']
+slot_2 = ['A3', 'B3', 'C3', 'D3']
+slot_3 = ['A4', 'B4', 'C4', 'D4']
+slot_4 = ['A5', 'B5', 'C5', 'D5']
+slot_5 = ['A6', 'B6', 'C6', 'D6']
+slot_6 = ['A7', 'B7', 'C7', 'D7']
+slot_7 = ['A8', 'B8', 'C8', 'D8']
+slot_8 = ['A9', 'B9', 'C9', 'D9']
+slot_9 = ['A10', 'B10', 'C10', 'D10']
+slot_10 = ['A11', 'B11', 'C11', 'D11']
+slot_11 = ['E2', 'F2', 'G2', 'H2']
+slot_12 = ['E3', 'F3', 'G3', 'H3']
+slot_13 = ['E4', 'F4', 'G4', 'H4']
+slot_14 = ['E5', 'F5', 'G5', 'H5']
+slot_15 = ['E6', 'F6', 'G6', 'H6']
+slot_16 = ['E7', 'F7', 'G7', 'H7']
+slot_17 = ['E8', 'F8', 'G8', 'H8']
+slot_18 = ['E9', 'F9', 'G9', 'H9']
+slot_19 = ['E10', 'F10', 'G10', 'H10']
+slot_20 = ['E11', 'F11', 'G11', 'H11']
+slot_21 = ['A1', 'B1', 'C1', 'D1']
+slot_22 = ['E1', 'F1', 'G1', 'H1']
+slot_23 = ['A12', 'B12', 'C12', 'D12']
+slot_24 = ['E12', 'F12', 'G12', 'H12']
+
+slots = [slot_1, slot_2, slot_3, slot_4, slot_5, slot_6, slot_7, slot_8, slot_9, slot_10, slot_11, slot_12, slot_13, slot_14, slot_15, slot_16, slot_17, slot_18, slot_19, slot_20, slot_21, slot_22, slot_23, slot_24]
+
+
+
+class Test_setup():
+ '''
+ Creates a protocol for the automated setting of a 96 well plate with a gradient of inducer.
+
+ '''
+ def __init__(self,
+ test_labware:str = 'corning_96_wellplate_360ul_flat',
+ test_position:int = 7,
+ aspiration_rate:float=0.5,
+ dispense_rate:float=1,):
+
+ self.test_labware = test_labware
+ self.test_position = test_position
+ self.aspiration_rate = aspiration_rate
+ self.dispense_rate = dispense_rate
+
+class Plate_samples(Test_setup):
+ '''
+ Creates a protocol for the automated setting of a 96 well plate from samples. Each sample is distributed in 4 wells of the plate.
+ '''
+ def __init__(self,samples:List,
+ sample_tube_volume:float = 1200,
+ sample_well_volume:float = 200,
+ tube_rack_labware:str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ tube_rack_position:int = 4,
+ tiprack_labware:str='opentrons_96_tiprack_300ul',
+ tiprack_position:int=9,
+ pipette:str='p300_single_gen2',
+ pipette_position:str='right',
+ use_temperature_module:bool = False,
+ starting_slot:int = 1,
+ *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.samples = samples
+ self.sample_tube_volume = sample_tube_volume
+ self.sample_well_volume = sample_well_volume
+ self.tube_rack_labware = tube_rack_labware
+ self.tube_rack_position = tube_rack_position
+ self.tiprack_labware = tiprack_labware
+ self.tiprack_position = tiprack_position
+ self.pipette = pipette
+ self.pipette_position = pipette_position
+ self.use_temperature_module = use_temperature_module
+ self.dict_of_samples_in_plate = {}
+ self.dict_of_samples_in_temp_mod_position = {}
+ self.starting_slot = starting_slot
+
+ if len(self.samples) > 24:
+ raise ValueError(f'Number of samples cant be greater than 24, you have {len(self.samples)}')
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+
+ #Labware
+ tiprack = protocol.load_labware(self.tiprack_labware, f'{self.tiprack_position}')
+ pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
+ plate = protocol.load_labware(self.test_labware, self.test_position)
+
+ if self.use_temperature_module:
+ temperature_module = protocol.load_module('Temperature Module', self.tube_rack_position)
+ tube_rack = temperature_module.load_labware(self.tube_rack_labware)
+ else:
+ tube_rack = protocol.load_labware(self.tube_rack_labware, self.tube_rack_position)
+
+ #Protocol
+
+ #Load samples
+ temp_wells_counter = 0
+ slot_counter = self.starting_slot-1
+ for sample in self.samples:
+ self.dict_of_samples_in_temp_mod_position[sample] = temp_wells[temp_wells_counter]
+ pipette.pick_up_tip()
+ for position in slots[slot_counter]:
+ liquid_transfer(pipette, self.sample_well_volume, tube_rack[self.dict_of_samples_in_temp_mod_position[sample]], plate[position], self.aspiration_rate, self.dispense_rate, mix_before=self.sample_well_volume, mix_after=self.sample_well_volume/2, new_tip=False, drop_tip=False)
+ pipette.drop_tip()
+ self.dict_of_samples_in_plate[sample] = slots[slot_counter]
+ temp_wells_counter += 1
+ slot_counter += 1
+
+ #output
+ print('Samples in tube rack')
+ print(self.dict_of_samples_in_temp_mod_position)
+ print('Samples in plate')
+ print(self.dict_of_samples_in_plate)
+ #END
+
+# metadata
+metadata = {
+'protocolName': 'PUDU Plate Setup',
+'author': 'Gonzalo Vidal ',
+'description': 'Automated 96 well plate setup protocol',
+'apiLevel': '2.13'}
+
+def run(protocol= protocol_api.ProtocolContext):
+
+ pudu_plate_samples = Plate_samples(samples=['s1', 's2','s3', 's4', 's5', 's6'], starting_slot=13)
+ pudu_plate_samples.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_Plate_supplemented_sample.py b/scripts/run_Plate_supplemented_sample.py
index 04a94bf..f2625d3 100644
--- a/scripts/run_Plate_supplemented_sample.py
+++ b/scripts/run_Plate_supplemented_sample.py
@@ -1,4 +1,4 @@
-from pudu.test_setup import Plate_supplemented_samples
+from pudu.sample_preparation import PlateWithGradient
from opentrons import protocol_api
# metadata
@@ -6,9 +6,9 @@
'protocolName': 'PUDU Plate Setup',
'author': 'Gonzalo Vidal ',
'description': 'Automated 96 well plate setup protocol',
-'apiLevel': '2.13'}
+'apiLevel': '2.14'}
def run(protocol= protocol_api.ProtocolContext):
- pudu_plate_supplemented_samples = Plate_supplemented_samples(sample_name='sample1', inducer_name='IPTG' )
+ pudu_plate_supplemented_samples = PlateWithGradient(sample_name='sample1', inducer_name='IPTG' )
pudu_plate_supplemented_samples.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_Plating.py b/scripts/run_Plating.py
new file mode 100644
index 0000000..1f37c7e
--- /dev/null
+++ b/scripts/run_Plating.py
@@ -0,0 +1,17 @@
+# metadata
+from opentrons import protocol_api
+
+from pudu.plating import Plating
+
+metadata = {
+ 'protocolName': 'Plating Protocol',
+ 'author': 'Oscar Rodriguez',
+ 'description': 'Automated Transformed Bacteria Plating Protocol',
+ 'apiLevel': '2.23'
+}
+
+# constructs = {"A1":["GVP8"],"A2":["GVP10"],"A3":["GVP12"]}
+constructs = {'A1': ['Competent_Cell_DH5alpha_1', ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'B1': ['Competent_Cell_DH5alpha_1', ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'C1': ['Competent_Cell_DH5alpha_1', ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'D1': ['Competent_Cell_DH5alpha_1', ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'E1': ['Competent_Cell_DH5alpha_1', ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'F1': ['Competent_Cell_DH5alpha_2', ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'G1': ['Competent_Cell_DH5alpha_2', ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'H1': ['Competent_Cell_DH5alpha_2', ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'A2': ['Competent_Cell_DH5alpha_2', ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'B2': ['Competent_Cell_DH5alpha_2', ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'C2': ['Competent_Cell_DH5alpha_3', ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'D2': ['Competent_Cell_DH5alpha_3', ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1']}
+def run(protocol: protocol_api.ProtocolContext):
+ plating_protocol = Plating(bacterium_locations=constructs,replicates=2)
+ plating_protocol.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_Plating_libre.py b/scripts/run_Plating_libre.py
new file mode 100644
index 0000000..180d621
--- /dev/null
+++ b/scripts/run_Plating_libre.py
@@ -0,0 +1,433 @@
+from typing import Optional, Dict
+from pudu import colors, SmartPipette
+from opentrons import protocol_api
+
+
+# construct_colors = ["#0000FF", "#FF0000", "#00FF00", "#8B4513", "#FFA500", "#000000", "#800080"]
+# class SmartPipette:
+# """
+# Wrapper for automatic volume tracking
+# """
+#
+# def __init__(self, pipette, protocol):
+# self.pipette = pipette
+# self.protocol = protocol
+# if not hasattr(protocol, 'define_liquid'):
+# raise RuntimeError("This class requires API with liquid tracking support")
+#
+# def is_conical_tube(self, well) -> bool:
+# """Check if the well is from a conical tube labware"""
+# return 'conical' in well.parent.load_name.lower()
+#
+# def get_well_volume(self, well) -> Optional[float]:
+# """Get current volume in well using pure API method"""
+# try:
+# return well.current_liquid_volume()
+# except Exception as e:
+# self.protocol.comment(f"ERROR reading volume from {well.well_name}: {e}")
+# return None
+#
+# def get_well_height(self, well) -> Optional[float]:
+# """Get current liquid height using pure API method (if available)"""
+# try:
+# if hasattr(well, 'current_liquid_height'):
+# return well.current_liquid_height()
+# else:
+# self.protocol.comment("Liquid height method not available in this API version")
+# return None
+# except Exception as e:
+# self.protocol.comment(f"ERROR reading height from {well.well_name}: {e}")
+# return None
+#
+# def get_conical_tube_aspiration_height(self, well) -> float:
+# """
+# Calculate safe aspiration height for conical tubes using proven method
+# Uses API liquid tracking to get current volume
+# """
+# # Get current volume from API
+# try:
+# current_volume = well.current_liquid_volume()
+# if current_volume is None:
+# raise ValueError("API returned None for liquid volume")
+# except Exception as e:
+# self.protocol.comment(f"ERROR: Could not get liquid volume from API: {e}")
+# return 10.0 # Safe fallback height
+#
+# max_volume = well.max_volume
+# tube_depth = well.depth - 10 # Account for threads
+# min_safe_height = 10 # mm minimum to prevent tip damage
+# meniscus_offset = 10 # mm below liquid surface
+#
+# # Calculate liquid height based on current volume
+# liquid_height = (current_volume / max_volume) * tube_depth
+# aspiration_height = max(liquid_height - meniscus_offset, min_safe_height)
+#
+# self.protocol.comment(
+# f"Conical calculation: {current_volume:.0f}µL remaining = {aspiration_height:.1f}mm height")
+# return aspiration_height
+#
+# def get_aspiration_location(self, well):
+# """
+# Get intelligent aspiration location using API volume data and proven height calculation
+# """
+# if not self.is_conical_tube(well):
+# return well
+#
+# try:
+# current_volume = well.current_liquid_volume()
+# if current_volume is None or current_volume < well.max_volume * 0.2:
+# # Less than 20% remaining - use standard aspiration
+# self.protocol.comment("Low volume detected - using standard aspiration")
+# return well
+#
+# # Use conical tube calculation
+# safe_height = self.get_conical_tube_aspiration_height(well)
+# return well.bottom(safe_height)
+#
+# except Exception as e:
+# self.protocol.comment(f"ERROR getting volume from API: {e}")
+# return well # Fallback to standard aspiration
+#
+# def liquid_transfer(self, volume: float, source, destination,
+# asp_rate: float = 0.5, disp_rate: float = 1.0,
+# blow_out: bool = True, touch_tip: bool = False,
+# mix_before: float = 0.0, mix_after: float = 0.0,
+# mix_reps: int = 3, new_tip: bool = True, drop_tip: bool = True) -> bool:
+# """
+# Transfer liquid using pure API liquid tracking for volume management
+#
+# Returns:
+# bool: True if transfer was successful, False if insufficient volume
+# """
+# # Check volume using API methods only
+# try:
+# current_volume = source.current_liquid_volume()
+# if current_volume is None:
+# self.protocol.comment("WARNING: API returned None for source volume")
+# return False
+#
+# if current_volume < volume:
+# self.protocol.comment(f"WARNING: Insufficient volume. "
+# f"Requested: {volume}µL, Available: {current_volume:.0f}µL")
+# return False
+#
+# except Exception as e:
+# self.protocol.comment(f"ERROR: Could not check source volume: {e}")
+# return False
+#
+# if new_tip:
+# self.pipette.pick_up_tip()
+#
+# # Get aspiration location using API data + proven calculation
+# aspiration_location = self.get_aspiration_location(source)
+#
+# # Mix before if requested
+# if mix_before > 0:
+# # Use current volume to limit mixing
+# try:
+# safe_mix_volume = min(mix_before, current_volume * 0.8)
+# self.pipette.mix(mix_reps, safe_mix_volume, aspiration_location)
+# except:
+# self.protocol.comment("Skipping mix_before due to API error")
+#
+# # Aspirate
+# self.pipette.aspirate(volume, aspiration_location, rate=asp_rate)
+#
+# # Dispense
+# self.pipette.dispense(volume, destination.center(), rate=disp_rate)
+#
+# # Mix after if requested
+# if mix_after > 0:
+# self.pipette.mix(mix_reps, mix_after, destination)
+#
+# if blow_out:
+# self.pipette.blow_out()
+#
+# if touch_tip:
+# self.pipette.touch_tip()
+#
+# if drop_tip:
+# self.pipette.drop_tip()
+#
+# return True
+
+class Plating():
+ """
+ Creates a protocol for automated plating of transformed bacteria
+
+ Attributes:
+
+ """
+ def __init__(self,
+ volume_total_reaction: float = 20,
+ volume_bacteria_transfer: float = 2,
+ volume_colony: float = 4,
+ volume_lb_transfer: float = 18,
+ volume_lb: float = 10000,
+ replicates: int = 1,
+ number_dilutions: int = 2,
+ max_colonies = 192,
+
+ thermocycler_starting_well: int = 0,
+ # thermocycler_labware: str = 'nest_96_wellplate_100ul_pcr_full_skirt',
+ thermocycler_labware: str = 'biorad_96_wellplate_200ul_pcr',
+
+ small_tiprack: str = 'opentrons_96_filtertiprack_20ul',
+ small_tiprack_position: str = '9',
+ initial_small_tip: str = None,
+ large_tiprack: str = 'opentrons_96_filtertiprack_200ul',
+ large_tiprack_position: str = '1',
+ initial_large_tip:str = None,
+ small_pipette: str = 'p20_single_gen2',
+ small_pipette_position: str = 'left',
+ large_pipette: str = 'p300_single_gen2',
+ large_pipette_position: str = 'right',
+
+
+ # dilution_plate: str = 'nest_96_wellplate_200ul_flat',
+ dilution_plate: str = 'nest_96_wellplate_100ul_pcr_full_skirt',
+ dilution_plate_position1: str = '2',
+ dilution_plate_position2: str = '3',
+ # agar_plate: str = 'nunc_omnitray_96grid',
+ agar_plate: str = 'nest_96_wellplate_100ul_pcr_full_skirt',
+ agar_plate_position1: str = '5',
+ agar_plate_position2: str = '6',
+ tube_rack: str = 'opentrons_15_tuberack_falcon_15ml_conical',
+ tube_rack_position: str = '4',
+ lb_tube_position: int = 0,
+
+ aspiration_rate: float = 0.5,
+ dispense_rate: float = 1,
+ bacterium_locations: Dict = None):
+
+ self.volume_total_reaction = volume_total_reaction
+ self.volume_bacteria_transfer = volume_bacteria_transfer
+ self.volume_colony = volume_colony
+ self.volume_lb_transfer = volume_lb_transfer
+ self.volume_lb = volume_lb
+ self.replicates = replicates
+ self.number_dilutions = number_dilutions
+ self.thermocycler_starting_well = thermocycler_starting_well
+ self.thermocycler_labware = thermocycler_labware
+ self.small_tiprack = small_tiprack
+ self.small_tiprack_position = small_tiprack_position
+ self.initial_small_tip = initial_small_tip
+ self.large_tiprack = large_tiprack
+ self.large_tiprack_position = large_tiprack_position
+ self.initial_large_tip = initial_large_tip
+ self.small_pipette = small_pipette
+ self.small_pipette_position = small_pipette_position
+ self.large_pipette = large_pipette
+ self.large_pipette_position = large_pipette_position
+ self.dilution_plate = dilution_plate
+ self.dilution_plate_position1 = dilution_plate_position1
+ self.dilution_plate_position2 = dilution_plate_position2
+ self.agar_plate = agar_plate
+ self.agar_plate_position1 = agar_plate_position1
+ self.agar_plate_position2 = agar_plate_position2
+ self.tube_rack = tube_rack
+ self.tube_rack_position = tube_rack_position
+ self.lb_tube_position = lb_tube_position
+ self.aspiration_rate = aspiration_rate
+ self.dispense_rate = dispense_rate
+ self.bacterium_locations = bacterium_locations
+ self.number_constructs = len(bacterium_locations)
+ self.total_colonies = self.number_constructs * self.number_dilutions * self.replicates
+ self.max_colonies = max_colonies
+ if self.total_colonies > self.max_colonies:
+ raise ValueError(f"Protocol only supports a max of {self.max_colonies} colonies")
+ if self.replicates > 8:
+ raise ValueError("Protocol only supports a max of 8 replicates")
+ if self.number_dilutions > 2:
+ raise ValueError("Protocol currently supports a max of 2 dilutions")
+
+ def calculate_plate_layout(self,protocol, plate1, plate2=None):
+ """
+ Calculate the layout for colonies on plates with dynamic buffer between dilutions
+ Returns: dict with plate assignments and well positions
+ """
+ colonies_per_dilution = self.number_constructs * self.replicates
+
+ layout = {
+ 'dilution_1': {'plate': 1, 'wells':[]},
+ 'dilution_2': {'plate': 1, 'wells':[]} if self.number_dilutions ==2 else None
+ }
+
+ #Check if we need two plates
+ if self.number_dilutions ==2 and colonies_per_dilution > 48:
+ if plate2 is None:
+ raise ValueError("Two plates required but plate2 not provided")
+ #Each Dilution gets its own plate
+ layout['dilution_1']['wells'] = plate1.wells()[:colonies_per_dilution]
+ layout['dilution_2']['plate'] = 2
+ layout['dilution_2']['wells'] = plate2.wells()[:colonies_per_dilution]
+ protocol.comment(f"Using 2 plates: {colonies_per_dilution} colonies per dilution exceeds single plate capacity")
+ elif self.number_dilutions ==2 and colonies_per_dilution <= 48:
+ #Both Dilutions Fit On One Plate
+ first_half = plate1.wells()[:colonies_per_dilution]
+ second_half = plate1.wells()[48:48+colonies_per_dilution]
+ layout['dilution_1']['wells'] = first_half
+ layout['dilution_2']['wells'] = second_half
+ protocol.comment(f"Using only one {plate1}: {colonies_per_dilution} colonies on each half")
+ else:
+ #Single dilution
+ layout['dilution_1']['wells'] = plate1.wells()[:colonies_per_dilution]
+ return layout
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+ #Labware
+ #Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
+ thermocycler = protocol.load_module('thermocyclerModuleV1')
+ thermocycler_plate = thermocycler.load_labware(self.thermocycler_labware)
+ #Load the tipracks
+ small_tiprack = protocol.load_labware(self.small_tiprack, self.small_tiprack_position)
+ large_tiprack = protocol.load_labware(self.large_tiprack, self.large_tiprack_position)
+ #Load the pipettes
+ small_pipette = protocol.load_instrument(self.small_pipette, self.small_pipette_position, tip_racks=[small_tiprack])
+ if self.initial_small_tip:
+ small_pipette.starting_tip = small_tiprack[self.initial_small_tip]
+ large_pipette = protocol.load_instrument(self.large_pipette, self.large_pipette_position, tip_racks=[large_tiprack])
+ if self.initial_large_tip:
+ large_pipette.starting_tip = large_tiprack[self.initial_large_tip]
+ #SmartPipette Wrapper to avoid dunking into the LB
+ smart_pipette = SmartPipette(large_pipette,protocol)
+ #Load the tube rack
+ tube_rack = protocol.load_labware(self.tube_rack, self.tube_rack_position)
+ lb_tube = tube_rack.wells()[self.lb_tube_position]
+ #load liquids
+ liquid_broth = protocol.define_liquid(
+ name="liquid_broth",
+ description="Liquid broth for dilutions",
+ display_color="#D2B48C"
+ )
+ lb_tube.load_liquid(liquid = liquid_broth, volume = self.volume_lb)
+ # Load bacteria into thermocycler wells
+ for i, (well_position, construct_names) in enumerate(self.bacterium_locations.items()):
+ liquid_bacteria = protocol.define_liquid(
+ name="transformed_bacteria",
+ description=f"{construct_names}",
+ display_color=colors[i%len(colors)]
+ )
+ well = thermocycler_plate[well_position]
+ well.load_liquid(liquid=liquid_bacteria, volume=self.volume_total_reaction)
+
+ # Load the dilution plates and Calculate the layout of the plates
+ dilution_plate1 = protocol.load_labware(self.dilution_plate, self.dilution_plate_position1)
+ if self.total_colonies <= len(dilution_plate1.wells()):
+ dilution_layout = self.calculate_plate_layout(protocol, dilution_plate1)
+ else:
+ dilution_plate2 = protocol.load_labware(self.dilution_plate, self.dilution_plate_position2)
+ dilution_layout = self.calculate_plate_layout(protocol, dilution_plate1, dilution_plate2)
+
+ #Load the Agar plates and Calculate the layout of the plates
+ agar_plate1 = protocol.load_labware(self.agar_plate, self.agar_plate_position1)
+ if self.total_colonies <= len(agar_plate1.wells()):
+ agar_layout = self.calculate_plate_layout(protocol, agar_plate1)
+ else:
+ agar_plate2 = protocol.load_labware(self.agar_plate, self.agar_plate_position2)
+ agar_layout = self.calculate_plate_layout(protocol, agar_plate1, agar_plate2)
+
+
+ thermocycler.set_block_temperature(4)
+ thermocycler.open_lid()
+
+ #Load the Liquid Broth into the dilution wells
+ protocol.comment("\n=== Step 1: Distributing LB to dilution wells ===")
+ # Get all wells that will receive LB (both dilutions if applicable)
+ all_dilution_wells = dilution_layout['dilution_1']['wells'][:]
+ if self.number_dilutions == 2 and dilution_layout['dilution_2']:
+ all_dilution_wells.extend(dilution_layout['dilution_2']['wells'])
+ # Distribute LB efficiently using built-in distribute method
+ # Process in chunks of 8 wells to update aspiration height
+ chunk_size = 8
+ for i in range(0, len(all_dilution_wells), chunk_size):
+ chunk_wells = all_dilution_wells[i:i + chunk_size]
+
+ # Get current aspiration location before each chunk
+ aspiration_location = smart_pipette.get_aspiration_location(lb_tube)
+ protocol.comment(f"Distributing to wells {i + 1}-{min(i + chunk_size, len(all_dilution_wells))}")
+
+ # Use built-in distribute method with updated aspiration location
+ large_pipette.distribute(
+ volume=self.volume_lb_transfer,
+ source=aspiration_location,
+ dest=chunk_wells,
+ disposal_volume=4, # For accuracy
+ new_tip='once' # Use one tip for the chunk
+ )
+
+ # Load liquid tracking for dilution wells
+ for well in chunk_wells:
+ well.load_liquid(liquid=liquid_broth, volume=self.volume_lb_transfer)
+
+ #Transfer bacteria to first dilution and process
+ protocol.comment("\n=== Step 2: Transferring bacteria and plating ===")
+
+ well_index = 0
+ for construct_position, construct_names in self.bacterium_locations.items():
+ for replicate in range(self.replicates):
+ # Get source and destination wells
+ source_well = thermocycler_plate[construct_position]
+ dilution1_well = dilution_layout['dilution_1']['wells'][well_index]
+ agar1_well = agar_layout['dilution_1']['wells'][well_index]
+
+ protocol.comment(f"\nProcessing {construct_names[0]} replicate {replicate + 1}")
+
+ # Pick up tip once for entire workflow per well
+ small_pipette.pick_up_tip()
+
+ # Transfer bacteria to dilution plate 1
+ small_pipette.aspirate(self.volume_bacteria_transfer, source_well, rate=self.aspiration_rate)
+ small_pipette.dispense(self.volume_bacteria_transfer, dilution1_well, rate=self.dispense_rate)
+
+ # Mix in dilution plate 1 (15µL mixing volume)
+ small_pipette.mix(repetitions=5, volume=19, location=dilution1_well)
+
+ # Plate on agar 1
+ small_pipette.aspirate(self.volume_colony, dilution1_well, rate=self.aspiration_rate)
+ small_pipette.dispense(self.volume_colony, agar1_well.top(-8), rate=self.dispense_rate)
+ small_pipette.blow_out()
+
+ # If we have a second dilution, continue with same tip
+ if self.number_dilutions == 2:
+ dilution2_well = dilution_layout['dilution_2']['wells'][well_index]
+ agar2_well = agar_layout['dilution_2']['wells'][well_index]
+
+ # Transfer from dilution 1 to dilution 2
+ small_pipette.aspirate(self.volume_bacteria_transfer, dilution1_well, rate=self.aspiration_rate)
+ small_pipette.dispense(self.volume_bacteria_transfer, dilution2_well, rate=self.dispense_rate)
+
+ # Mix in dilution plate 2
+ small_pipette.mix(repetitions=5, volume=19, location=dilution2_well)
+
+ # Plate on agar 2
+ small_pipette.aspirate(self.volume_colony, dilution2_well, rate=self.aspiration_rate)
+ small_pipette.dispense(self.volume_colony, agar2_well.top(-8), rate=self.dispense_rate)
+ small_pipette.blow_out()
+
+ # Drop tip after completing all transfers for this well
+ small_pipette.drop_tip()
+
+ well_index += 1
+
+ # Close thermocycler lid
+ # thermocycler.close_lid()
+ # thermocycler.deactivate_block()
+
+ protocol.comment("\n=== Plating protocol complete ===")
+ protocol.comment(f"Plated {self.number_constructs} constructs with {self.replicates} replicates")
+ protocol.comment(f"Created a total of {self.total_colonies} colonies")
+
+# metadata
+metadata = {
+ 'protocolName': 'Plating Protocol',
+ 'author': 'Oscar Rodriguez',
+ 'description': 'Automated Transformed Bacteria Plating Protocol',
+ 'apiLevel': '2.23'
+}
+
+# constructs = {"A1":["GVP8"],"A2":["GVP10"],"A3":["GVP12"]}
+constructs = {'A1': ['Competent_Cell_DH5alpha_1', ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'B1': ['Competent_Cell_DH5alpha_1', ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'C1': ['Competent_Cell_DH5alpha_1', ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'D1': ['Competent_Cell_DH5alpha_1', ('GVP0008', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'E1': ['Competent_Cell_DH5alpha_1', ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'F1': ['Competent_Cell_DH5alpha_2', ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'G1': ['Competent_Cell_DH5alpha_2', ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'H1': ['Competent_Cell_DH5alpha_2', ('GVP0010', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'A2': ['Competent_Cell_DH5alpha_2', ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'B2': ['Competent_Cell_DH5alpha_2', ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_1'), 'Media_1'], 'C2': ['Competent_Cell_DH5alpha_3', ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1'], 'D2': ['Competent_Cell_DH5alpha_3', ('GVP0012', 'B0034', 'sfGFP', 'B0015', 'Odd_1', 'replicate_2'), 'Media_1']}
+def run(protocol: protocol_api.ProtocolContext):
+ plating_protocol = Plating(bacterium_locations=constructs,replicates=5)
+ plating_protocol.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_iGEM_gfp_od.py b/scripts/run_iGEM_gfp_od.py
index eb6af16..a0b24f4 100644
--- a/scripts/run_iGEM_gfp_od.py
+++ b/scripts/run_iGEM_gfp_od.py
@@ -1,4 +1,4 @@
-from pudu.calibration import iGEM_gfp_od
+from pudu.calibration import GFPODCalibration
from opentrons import protocol_api
# metadata
@@ -6,9 +6,9 @@
'protocolName': 'iGEM GFP OD600 calibration',
'author': 'Gonzalo Vidal ',
'description': 'Protocol to perform serial dilutions of fluorescein and nanoparticles for calibration',
-'apiLevel': '2.13'}
+'apiLevel': '2.14'}
def run(protocol= protocol_api.ProtocolContext):
- pudu_calibration = iGEM_gfp_od()
+ pudu_calibration = GFPODCalibration()
pudu_calibration.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_iGEM_rgb_od.py b/scripts/run_iGEM_rgb_od.py
index 201686a..a22729c 100644
--- a/scripts/run_iGEM_rgb_od.py
+++ b/scripts/run_iGEM_rgb_od.py
@@ -1,4 +1,4 @@
-from pudu.calibration import iGEM_rgb_od
+from pudu.calibration import RGBODCalibration
from opentrons import protocol_api
# metadata
@@ -6,9 +6,9 @@
'protocolName': 'iGEM GFP OD600 calibration',
'author': 'Gonzalo Vidal ',
'description': 'Protocol to perform serial dilutions of fluorescein and nanoparticles for calibration',
-'apiLevel': '2.13'}
+'apiLevel': '2.14'}
def run(protocol= protocol_api.ProtocolContext):
- pudu_calibration = iGEM_rgb_od()
+ pudu_calibration = RGBODCalibration()
pudu_calibration.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_sbol2assembly_libre.py b/scripts/run_sbol2assembly_libre.py
new file mode 100644
index 0000000..bfd2482
--- /dev/null
+++ b/scripts/run_sbol2assembly_libre.py
@@ -0,0 +1,306 @@
+from opentrons import protocol_api
+from typing import List, Dict, Union
+from fnmatch import fnmatch
+from itertools import product
+import xlsxwriter
+
+# utils
+
+#96 well List
+plate_96_wells = [
+'A1','A2','A3','A4','A5','A6','A7','A8','A9','A10','A11','A12',
+'B1','B2','B3','B4','B5','B6','B7','B8','B9','B10','B11','B12',
+'C1','C2','C3','C4','C5','C6','C7','C8','C9','C10','C11','C12',
+'D1','D2','D3','D4','D5','D6','D7','D8','D9','D10','D11','D12',
+'E1','E2','E3','E4','E5','E6','E7','E8','E9','E10','E11','E12',
+'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12',
+'G1','G2','G3','G4','G5','G6','G7','G8','G9','G10','G11','G12',
+'H1','H2','H3','H4','H5','H6','H7','H8','H9','H10','H11','H12'
+]
+
+#filler variable equal to well List
+thermo_wells = plate_96_wells
+
+#Function to transfer Liquid
+def liquid_transfer(pipette, volume, source, destination, asp_rate:float=0.5, disp_rate:float=1.0, blow_out:bool=True, touch_tip:bool=False, mix_before:float=0.0, mix_after:float=0.0, mix_reps:int=3, new_tip:bool=True, drop_tip:bool=True):
+ if new_tip:
+ pipette.pick_up_tip()
+ if mix_before > 0:
+ pipette.mix(mix_reps, mix_before, source)
+ pipette.aspirate(volume, source, rate=asp_rate)
+ pipette.dispense(volume, destination, rate=disp_rate)
+ if mix_after > 0:
+ pipette.mix(mix_reps, mix_after, destination)
+ if blow_out:
+ pipette.blow_out()
+ if touch_tip:
+ pipette.touch_tip()
+ if drop_tip:
+ pipette.drop_tip()
+
+#new List for temporary wells
+temp_wells = [
+'A1','A2','A3','A4','A5','A6',
+'B1','B2','B3','B4','B5','B6',
+'C1','C2','C3','C4','C5','C6',
+'D1','D2','D3','D4','D5','D6'
+]
+
+#class DNA Assembly
+class DNA_assembly():
+ '''
+ Creates a protocol for automated DNA assembly.
+
+ Attributes
+ ----------
+ volume_total_reaction : float
+ The total volume of the reaction mix in microliters. By default, 20 microliters.
+ volume_part : float
+ The volume of each part in microliters. By default, 2 microliters.
+ volume_restriction_enzyme : float
+ The volume of the restriction enzyme in microliters. By default, 2 microliters.
+ volume_t4_dna_ligase : float
+ The volume of T4 DNA Ligase in microliters. By default, 4 microliters.
+ volume_t4_dna_ligase_buffer : float
+ The volume of T4 DNA Ligase Buffer in microliters. By default, 2 microliters.
+ replicates : int
+ The number of replicates of the assembly reaction. By default, 2.
+ thermocycler_starting_well : int
+ The starting well of the thermocycler module. By default, 0.
+ thermocycler_labware : str
+ The labware type of the thermocycler module. By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
+ thermocycler_slots : list
+ The slots of the thermocycler module. By default, [7, 8, 10, 11].
+ temperature_module_labware : str
+ The labware type of the temperature module. By default, 'opentrons_24_aluminumblock_nest_1.5ml_snapcap'.
+ temperature_module_slot : int
+ The slot of the temperature module. By default, 1.
+ tiprack_labware : str
+ The labware type of the tiprack. By default, 'opentrons_96_tiprack_20ul'.
+ tiprack_slot : int
+ The slot of the tiprack. By default, 9.
+ pipette_type : str
+ The type of pipette. By default, 'p20_single_gen2'.
+ pipette_mount : str
+ The mount of the pipette. By default, 'left'.
+ aspiration_rate : float
+ The rate of aspiration in microliters per second. By default, 0.5 microliters per second.
+ dispense_rate : float
+ The rate of dispense in microliters per second. By default, 1 microliter per second.
+ '''
+ #self attributes
+ def __init__(self,
+ volume_total_reaction:float = 20,
+ volume_part:float = 2,
+ volume_restriction_enzyme:float = 2,
+ volume_t4_dna_ligase:float = 4,
+ volume_t4_dna_ligase_buffer:float = 2,
+ replicates:int=1,
+ thermocycler_starting_well:int = 0,
+ thermocycler_labware:str = 'nest_96_wellplate_100ul_pcr_full_skirt',
+ temperature_module_labware:str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ temperature_module_position:int = 1,
+ tiprack_labware:str = 'opentrons_96_tiprack_20ul',
+ tiprack_position:int = 9,
+ pipette:str = 'p20_single_gen2',
+ pipette_position:str = 'left',
+ aspiration_rate:float=0.5,
+ dispense_rate:float=1,):
+
+ self.volume_total_reaction = volume_total_reaction
+ self.volume_part = volume_part
+ self.volume_restriction_enzyme = volume_restriction_enzyme
+ self.volume_t4_dna_ligase = volume_t4_dna_ligase
+ self.volume_t4_dna_ligase_buffer = volume_t4_dna_ligase_buffer
+ self.replicates = replicates
+ self.thermocycler_starting_well = thermocycler_starting_well
+ self.thermocycler_labware = thermocycler_labware
+ self.temperature_module_labware = temperature_module_labware
+ self.temperature_module_position = temperature_module_position
+ self.tiprack_labware = tiprack_labware
+ self.tiprack_position = tiprack_position
+ self.pipette = pipette
+ self.pipette_position = pipette_position
+ self.aspiration_rate = aspiration_rate
+ self.dispense_rate = dispense_rate
+
+class sbol2assembly(DNA_assembly):
+ '''
+ Creates a protocol from a dictionary for the automated assembly.
+
+ '''
+ def __init__(self, assemblies:List[Dict],
+ *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.assemblies = assemblies
+ self.dict_of_parts_in_temp_mod_position = {}
+ self.dict_of_parts_in_thermocycler = {}
+ self.assembly_plan = None
+ self.parts_set = set()
+ self.backbone_set = set()
+ self.restriction_enzyme_set = set()
+ self.combined_set = set()
+ self.has_odd = False
+ self.has_even = False
+ self.odd_combinations = []
+ self.even_combinations = []
+
+ # add parts to a set
+ for assembly in self.assemblies:
+ #part parts
+ for part in assembly["PartsList"]:
+ self.parts_set.add(part)
+ #backbone parts
+ self.backbone_set.add(assembly["Backbone"])
+ #add enzymes
+ self.restriction_enzyme_set.add(assembly["Restriction Enzyme"])
+
+ self.combined_set = self.parts_set.union(self.backbone_set)
+
+ max_parts = 19 #TODO check if this olds, 20
+ if len(self.combined_set) > max_parts:
+ raise ValueError(f'This protocol only supports assemblies with up to {max_parts} parts. Number of parts in the protocol is {len(self.parts_set)}. Printing parts set:{self.parts_set}')
+ thermocyler_available_wells = 96 - self.thermocycler_starting_well
+ thermocycler_wells_needed = (len(self.odd_combinations) + len(self.even_combinations))*self.replicates
+ if thermocycler_wells_needed > thermocyler_available_wells:
+ raise ValueError(f'According to your setup this protocol only supports assemblies with up to {thermocyler_available_wells} combinations. Number of combinations in the protocol is {thermocycler_wells_needed}.')
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+ #Labware
+ #Load temperature module
+ tem_mod = protocol.load_module('temperature module', f'{self.temperature_module_position}') #CV: Previously was '3', but the cord was not long enough
+ tem_mod_block = tem_mod.load_labware(self.temperature_module_labware)
+ #Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
+ thermocycler_mod = protocol.load_module('thermocycler module')
+ thermocycler_mod_plate = thermocycler_mod.load_labware(self.thermocycler_labware)
+ #Load the tiprack
+ tiprack = protocol.load_labware(self.tiprack_labware, f'{self.tiprack_position}')
+ #Load the pipette
+ pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
+ #Fixed volumes
+ volume_reagents = self.volume_restriction_enzyme + self.volume_t4_dna_ligase + self.volume_t4_dna_ligase_buffer
+ #Load the reagents
+ dd_h2o = tem_mod_block['A1']
+ self.dict_of_parts_in_temp_mod_position['dd_h2o'] = temp_wells[0]
+ t4_dna_ligase = tem_mod_block['A2']
+ self.dict_of_parts_in_temp_mod_position['t4_dna_ligase'] = temp_wells[1]
+ t4_dna_ligase_buffer = tem_mod_block['A3']
+ self.dict_of_parts_in_temp_mod_position['t4_dna_ligase_buffer'] = temp_wells[2]
+ temp_wells_counter = 3
+ restriction_enzyme_tube = tem_mod_block[temp_wells[temp_wells_counter]]
+ #load the enzymes
+ for enzyme in self.restriction_enzyme_set:
+ self.dict_of_parts_in_temp_mod_position[enzyme] = temp_wells[temp_wells_counter]
+ temp_wells_counter += 1
+ #Load the parts(includes backbones)
+ for part in self.combined_set:
+ self.dict_of_parts_in_temp_mod_position[part] = temp_wells[temp_wells_counter]
+ temp_wells_counter += 1
+ #Setup
+ #Set the temperature of the temperature module and the thermocycler to 4°C
+ tem_mod.set_temperature(4)
+ thermocycler_mod.open_lid()
+ thermocycler_mod.set_block_temperature(4)
+ #can be done with multichannel pipette?
+ current_thermocycler_well = self.thermocycler_starting_well
+ #build combinations
+ for assembly in self.assemblies:
+ for r in range(self.replicates):
+ volume_dd_h2o = self.volume_total_reaction - (volume_reagents + self.volume_part*len(assembly["PartsList"]))
+ liquid_transfer(pipette, volume_dd_h2o, dd_h2o, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate)
+ liquid_transfer(pipette, self.volume_t4_dna_ligase_buffer, t4_dna_ligase_buffer, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_t4_dna_ligase_buffer)
+ liquid_transfer(pipette, self.volume_t4_dna_ligase, t4_dna_ligase, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_t4_dna_ligase)
+ liquid_transfer(pipette, self.volume_restriction_enzyme, tem_mod_block[self.dict_of_parts_in_temp_mod_position[assembly['Restriction Enzyme']]], thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_restriction_enzyme)
+ #pippeting backbone
+ liquid_transfer(pipette, self.volume_part, tem_mod_block[self.dict_of_parts_in_temp_mod_position[assembly["Backbone"]]], thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_part)
+ #pippeting parts
+ for part in assembly["PartsList"]:
+ if type(part) == str:
+ part_name=part
+ else: raise ValueError(f'Part {part} is not a string nor sbol2.Component') #TODO: improve this check
+ #part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[current_thermocycler_well]]
+ liquid_transfer(pipette, self.volume_part, tem_mod_block[self.dict_of_parts_in_temp_mod_position[part_name]], thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_part)
+ #This line under this comment was written to get it to run, not run correctly
+ #need products uri
+ self.dict_of_parts_in_thermocycler[assembly["Product"]] = thermo_wells[current_thermocycler_well]
+ current_thermocycler_well+=1
+
+ protocol.comment('Take out the reagents since the temperature module will be turn off')
+ #We close the thermocycler lid and wait for the temperature to reach 42°C
+ thermocycler_mod.close_lid()
+ #The thermocycler's lid temperature is set with the following command
+ thermocycler_mod.set_lid_temperature(42)
+ tem_mod.deactivate()
+ #Cycles were made following https://pubs.acs.org/doi/10.1021/sb500366v
+ profile = [
+ {'temperature': 42, 'hold_time_minutes': 2},
+ {'temperature': 16, 'hold_time_minutes': 5}]
+ thermocycler_mod.execute_profile(steps=profile, repetitions=25, block_max_volume=30)
+
+ denaturation = [
+ {'temperature': 60, 'hold_time_minutes': 10},
+ {'temperature': 80, 'hold_time_minutes': 10}]
+ thermocycler_mod.execute_profile(steps=denaturation, repetitions=1, block_max_volume=30)
+ thermocycler_mod.set_block_temperature(4)
+ #END
+
+ def get_xlsx_output(self, name:str):
+ workbook = xlsxwriter.Workbook(f'{name}.xlsx')
+ worksheet = workbook.add_worksheet()
+ row_num = 0
+ col_num = 0
+ worksheet.write(row_num, col_num, 'Parts in temp_module')
+ row_num += 2
+ for key, value in self.dict_of_parts_in_temp_mod_position.items():
+ worksheet.write(row_num, col_num, key)
+ worksheet.write(row_num+1, col_num, value)
+ col_num += 1
+ col_num = 0
+ row_num += 4
+ worksheet.write(row_num, col_num, 'Parts in thermocycler_module')
+ row_num += 2
+ for key, value in self.dict_of_parts_in_thermocycler.items():
+ worksheet.write(row_num, col_num, key)
+ worksheet.write_column(row_num+1, col_num, value)
+ col_num += 1
+ workbook.close()
+ self.xlsx_output = workbook
+ return self.xlsx_output
+
+ #Make a print function for later
+ #output
+ #print('Parts and reagents in temp_module')
+ #print(self.dict_of_parts_in_temp_mod_position)
+ #print('Assembled parts in thermocycler_module')
+ #print(self.dict_of_parts_in_thermocycler)
+
+#Where dictionary used to be
+
+
+#create a recursive function based on the how many times
+#def assemblyRecursion(, duplicate dictionaries, amount):
+# append()
+
+#Todo find product uri
+
+# metadata
+metadata = {
+'protocolName': 'PUDU Loop assembly',
+'author': 'Gonzalo Vidal ',
+'description': 'Automated DNA assembly Loop protocol',
+'apiLevel': '2.13'}
+
+def run(protocol= protocol_api.ProtocolContext):
+
+ #Take the Json file from Assembly to Dict then set it to assembly_sbol2_uris
+ #I am taking in the name of Json file and make it assembly
+ #import using pandas or somthing to convert to usable dictionary
+ import json
+ with open("scripts/output.json", "r") as f:
+ assembly_sbol2_uris = json.load(f)
+
+
+ pudu_sbol2_assembly = sbol2assembly(assemblies=assembly_sbol2_uris)
+ pudu_sbol2_assembly.run(protocol)
+ pudu_sbol2_assembly.get_xlsx_output("SBOL_xlsx5")
diff --git a/scripts/run_sbol2assembly_with_params.py b/scripts/run_sbol2assembly_with_params.py
new file mode 100644
index 0000000..bfe14c6
--- /dev/null
+++ b/scripts/run_sbol2assembly_with_params.py
@@ -0,0 +1,87 @@
+# Example SBOL assemblies (would typically come from SynBioSuite)
+assemblies = [
+ {
+ "Product": "https://SBOL2Build.org/composite_1/1",
+ "Backbone": "https://sbolcanvas.org/pSB1C3/1",
+ "PartsList": [
+ "https://sbolcanvas.org/J23101/1",
+ "https://sbolcanvas.org/B0034/1",
+ "https://sbolcanvas.org/GFP/1",
+ "https://sbolcanvas.org/B0015/1"
+ ],
+ "Restriction Enzyme": "https://SBOL2Build.org/BsaI/1"
+ },
+ {
+ "Product": "https://SBOL2Build.org/composite_2/1",
+ "Backbone": "https://sbolcanvas.org/pSB1C3/1",
+ "PartsList": [
+ "https://sbolcanvas.org/J23100/1",
+ "https://sbolcanvas.org/B0034/1",
+ "https://sbolcanvas.org/RFP/1",
+ "https://sbolcanvas.org/B0015/1"
+ ],
+ "Restriction Enzyme": "https://SBOL2Build.org/BsaI/1"
+ }
+]
+
+# Advanced parameters
+advanced_params = {
+ "volume_part": 3,
+ "volume_total_reaction": 25,
+ "replicates": 2, # Want duplicates for this critical experiment
+ "initial_tip": "H10",
+ "thermocycler_starting_well": 0,
+ "protocol_name": "SBOL_GFP_RFP_Assembly"
+}
+
+
+from pudu.assembly import SBOLLoopAssembly
+from opentrons import protocol_api
+
+
+metadata = {
+ 'protocolName': 'PUDU SBOL Loop Assembly with Advanced Parameters',
+ 'author': 'Oscar Rodriguez',
+ 'description': 'Automated DNA assembly from SBOL with custom parameters',
+ 'apiLevel': '2.14'
+}
+
+def run(protocol: protocol_api.ProtocolContext):
+ """
+ Run SBOL Loop Assembly with advanced parameters.
+
+ This demonstrates three ways to configure the protocol:
+
+ 1. Using advanced_params dict (recommended for SynBioSuite integration)
+ 2. Using kwargs (traditional method, still supported)
+ 3. Combining both (kwargs override advanced_params)
+ """
+
+ # Method 1: Using advanced_params (recommended for programmatic generation)
+ protocol.comment("=== Running with advanced_params ===")
+ pudu_assembly = SBOLLoopAssembly(
+ assemblies=assemblies,
+ json_params=advanced_params,
+ )
+ pudu_assembly.run(protocol)
+
+ # Method 2: Traditional kwargs method (still works for backward compatibility)
+ # Uncomment to test:
+ # protocol.comment("=== Running with kwargs ===")
+ # pudu_assembly = SBOLLoopAssembly(
+ # assemblies=assemblies,
+ # volume_part=3,
+ # replicates=2,
+ # protocol_name="SBOL_Traditional"
+ # )
+ # pudu_assembly.run(protocol)
+
+ # Method 3: Combining both (kwargs override advanced_params)
+ # Uncomment to test:
+ # protocol.comment("=== Running with both (kwargs wins) ===")
+ # pudu_assembly = SBOLLoopAssembly(
+ # assemblies=assemblies,
+ # advanced_params={"volume_part": 3, "replicates": 2},
+ # replicates=4 # This overrides the replicates=2 in advanced_params
+ # )
+ # pudu_assembly.run(protocol)
\ No newline at end of file
diff --git a/scripts/run_sbol2plating_with_params.py b/scripts/run_sbol2plating_with_params.py
new file mode 100644
index 0000000..5fac924
--- /dev/null
+++ b/scripts/run_sbol2plating_with_params.py
@@ -0,0 +1,51 @@
+from pudu.plating import Plating
+from opentrons import protocol_api
+
+
+# Protocol metadata
+metadata = {
+ 'protocolName': 'PUDU Plating Test',
+ 'author': 'Oscar Rodriguez',
+ 'description': 'Test plating with new parameter approach',
+ 'apiLevel': '2.21'
+}
+
+
+def run(protocol: protocol_api.ProtocolContext):
+ """Main protocol execution function"""
+
+ # NEW APPROACH: Using plating_data + advanced_params (JSON-compatible)
+ plating_data = {
+ 'bacterium_locations': {
+ 'A1': 'GVD0011',
+ 'A2': 'GVD0013',
+ 'A3': 'GVD0015'
+ }
+ }
+
+ advanced_params = {
+ 'replicates': 2,
+ 'number_dilutions': 2,
+ 'volume_colony': 6,
+ 'thermocycler_starting_well': 0
+ }
+
+ plating = Plating(
+ plating_data=plating_data,
+ advanced_params=advanced_params
+ )
+
+ # OLD APPROACH: Using kwargs directly
+ # plating = Plating(
+ # bacterium_locations={
+ # 'A1': 'GVD0011',
+ # 'A2': 'GVD0013',
+ # 'A3': 'GVD0015'
+ # },
+ # replicates=2,
+ # number_dilutions=2,
+ # volume_colony=6,
+ # thermocycler_starting_well=0
+ # )
+
+ plating.run(protocol)
diff --git a/scripts/run_sbol2transformation_with_params.py b/scripts/run_sbol2transformation_with_params.py
new file mode 100644
index 0000000..d08713d
--- /dev/null
+++ b/scripts/run_sbol2transformation_with_params.py
@@ -0,0 +1,34 @@
+from pudu.transformation import HeatShockTransformation
+from opentrons import protocol_api
+
+
+transformation_data = [
+ {
+ 'Strain': 'GVD_strain',
+ 'Chassis': 'DH5alpha',
+ 'Plasmids': ['GVD0011', 'GVD0013', 'GVD0015']
+ }
+]
+
+# Protocol metadata
+metadata = {
+ 'protocolName': 'PUDU Transformation Test',
+ 'author': 'Oscar Rodriguez',
+ 'description': 'Test transformation with new parameter approach',
+ 'apiLevel': '2.20'
+}
+
+
+def run(protocol: protocol_api.ProtocolContext):
+ """Main protocol execution function"""
+
+ transformation = HeatShockTransformation(
+ transformation_data=transformation_data,
+ replicates=3,
+ volume_dna=25,
+ thermocycler_starting_well=0,
+ transfer_volume_dna=5,
+ tube_volume_competent_cell=150
+ )
+
+ transformation.run(protocol)
diff --git a/src/pudu/__init__.py b/src/pudu/__init__.py
index 0681205..feacf84 100644
--- a/src/pudu/__init__.py
+++ b/src/pudu/__init__.py
@@ -1,5 +1,6 @@
from .assembly import *
from .calibration import *
-from .test_setup import *
+from .sample_preparation import *
from .transformation import *
-from .utils import *
+from .plating import *
+from .utils import *
\ No newline at end of file
diff --git a/src/pudu/__pycache__/__init__.cpython-37.pyc b/src/pudu/__pycache__/__init__.cpython-37.pyc
new file mode 100644
index 0000000..a73e118
Binary files /dev/null and b/src/pudu/__pycache__/__init__.cpython-37.pyc differ
diff --git a/src/pudu/__pycache__/assembly.cpython-37.pyc b/src/pudu/__pycache__/assembly.cpython-37.pyc
new file mode 100644
index 0000000..80ee604
Binary files /dev/null and b/src/pudu/__pycache__/assembly.cpython-37.pyc differ
diff --git a/src/pudu/assembly.py b/src/pudu/assembly.py
index 718dc23..9960fe5 100644
--- a/src/pudu/assembly.py
+++ b/src/pudu/assembly.py
@@ -1,215 +1,859 @@
+import xlsxwriter
from opentrons import protocol_api
-from pudu.utils import thermo_wells, temp_wells, liquid_transfer
-from typing import List, Dict, Union
+from typing import List, Dict, Optional
from fnmatch import fnmatch
from itertools import product
+import json
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from pudu.utils import Camera, colors
-class DNA_assembly():
- '''
- Creates a protocol for automated DNA assembly.
-
- Attributes
- ----------
- volume_total_reaction : float
- The total volume of the reaction mix in microliters. By default, 20 microliters.
- volume_part : float
- The volume of each part in microliters. By default, 2 microliters.
- volume_restriction_enzyme : float
- The volume of the restriction enzyme in microliters. By default, 2 microliters.
- volume_t4_dna_ligase : float
- The volume of T4 DNA Ligase in microliters. By default, 4 microliters.
- volume_t4_dna_ligase_buffer : float
- The volume of T4 DNA Ligase Buffer in microliters. By default, 2 microliters.
- replicates : int
- The number of replicates of the assembly reaction. By default, 2.
- thermocycler_starting_well : int
- The starting well of the thermocycler module. By default, 0.
- thermocycler_labware : str
- The labware type of the thermocycler module. By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
- thermocycler_slots : list
- The slots of the thermocycler module. By default, [7, 8, 10, 11].
- temperature_module_labware : str
- The labware type of the temperature module. By default, 'opentrons_24_aluminumblock_nest_1.5ml_snapcap'.
- temperature_module_slot : int
- The slot of the temperature module. By default, 1.
- tiprack_labware : str
- The labware type of the tiprack. By default, 'opentrons_96_tiprack_20ul'.
- tiprack_slot : int
- The slot of the tiprack. By default, 9.
- pipette_type : str
- The type of pipette. By default, 'p20_single_gen2'.
- pipette_mount : str
- The mount of the pipette. By default, 'left'.
- aspiration_rate : float
- The rate of aspiration in microliters per second. By default, 0.5 microliters per second.
- dispense_rate : float
- The rate of dispense in microliters per second. By default, 1 microliter per second.
- '''
+@dataclass
+class ManualReactionRecord:
+ """Structured representation of one Golden Gate manual assembly reaction."""
+ product_uri: str
+ product_name: str
+ backbone_uri: str
+ backbone_name: str
+ part_uris: List[str]
+ part_names: List[str]
+ restriction_enzyme_uri: str
+ restriction_enzyme_name: str
+ number_of_dna_components: int
+ total_dna_volume: float
+ fixed_reagent_volume: float
+ water_volume: float
+ total_reaction_volume: float
+ reagent_additions: List[Dict[str, str]] = field(default_factory=list)
+ notes: List[str] = field(default_factory=list)
+
+class BaseAssembly(ABC):
+ """
+ Abstract base class for Loop Assembly protocols.
+ Contains shared hardware setup, liquid handling, and tip management functionality.
+ """
+
def __init__(self,
- volume_total_reaction:float = 20,
- volume_part:float = 2,
- volume_restriction_enzyme:float = 2,
- volume_t4_dna_ligase:float = 4,
- volume_t4_dna_ligase_buffer:float = 2,
- replicates:int=2,
- thermocycler_starting_well:int = 0,
- thermocycler_labware:str = 'nest_96_wellplate_100ul_pcr_full_skirt',
- temperature_module_labware:str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
- temperature_module_position:int = 1,
- tiprack_labware:str = 'opentrons_96_tiprack_20ul',
- tiprack_position:int = 9,
- pipette:str = 'p20_single_gen2',
- pipette_position:str = 'left',
- aspiration_rate:float=0.5,
- dispense_rate:float=1,):
-
- self.volume_total_reaction = volume_total_reaction
- self.volume_part = volume_part
- self.volume_restriction_enzyme = volume_restriction_enzyme
- self.volume_t4_dna_ligase = volume_t4_dna_ligase
- self.volume_t4_dna_ligase_buffer = volume_t4_dna_ligase_buffer
- self.replicates = replicates
- self.thermocycler_starting_well = thermocycler_starting_well
- self.thermocycler_labware = thermocycler_labware
- self.temperature_module_labware = temperature_module_labware
- self.temperature_module_position = temperature_module_position
- self.tiprack_labware = tiprack_labware
- self.tiprack_position = tiprack_position
- self.pipette = pipette
- self.pipette_position = pipette_position
- self.aspiration_rate = aspiration_rate
- self.dispense_rate = dispense_rate
- #END
-
-
-
-class Domestication(DNA_assembly):
- '''
- Creates a protocol for automated domestication, assembly of parts into universal acceptor backbone.
-
- '''
- def __init__(self, parts:Union[List,Dict], acceptor_backbone:str,
- *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self.parts = parts
- self.acceptor_backbone = acceptor_backbone
+ json_params: Optional[Dict] = None,
+ volume_total_reaction: float = 20,
+ volume_part: float = 2,
+ volume_restriction_enzyme: float = 2,
+ volume_t4_dna_ligase: float = 4,
+ volume_t4_dna_ligase_buffer: float = 2,
+ replicates: int = 1,
+ thermocycler_starting_well: int = 0,
+ thermocycler_labware: str = 'nest_96_wellplate_100ul_pcr_full_skirt',
+ temperature_module_labware: str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ temperature_module_position: str = '1',
+ tiprack_labware: str = 'opentrons_96_tiprack_20ul',
+ tiprack_positions: Optional[List[str]] = None,
+ pipette: str = 'p20_single_gen2',
+ pipette_position: str = 'left',
+ initial_tip: Optional[str] = None,
+ aspiration_rate: float = 0.5,
+ dispense_rate: float = 1,
+ take_picture: bool = False,
+ take_video: bool = False,
+ water_testing: bool = False,
+ output_xlsx: bool = True,
+ protocol_name: str = ''):
+
+ kwargs_params = {
+ 'volume_total_reaction': volume_total_reaction,
+ 'volume_part': volume_part,
+ 'volume_restriction_enzyme': volume_restriction_enzyme,
+ 'volume_t4_dna_ligase': volume_t4_dna_ligase,
+ 'volume_t4_dna_ligase_buffer': volume_t4_dna_ligase_buffer,
+ 'replicates': replicates,
+ 'thermocycler_starting_well': thermocycler_starting_well,
+ 'thermocycler_labware': thermocycler_labware,
+ 'temperature_module_labware': temperature_module_labware,
+ 'temperature_module_position': temperature_module_position,
+ 'tiprack_labware': tiprack_labware,
+ 'tiprack_positions': tiprack_positions,
+ 'pipette': pipette,
+ 'pipette_position': pipette_position,
+ 'initial_tip' : initial_tip,
+ 'aspiration_rate': aspiration_rate,
+ 'dispense_rate': dispense_rate,
+ 'take_picture': take_picture,
+ 'take_video': take_video,
+ 'water_testing': water_testing,
+ 'output_xlsx': output_xlsx,
+ 'protocol_name': protocol_name
+ }
+
+ params = self._merge_params(json_params, kwargs_params)
+
+ self.volume_total_reaction = params['volume_total_reaction']
+ self.volume_part = params['volume_part']
+ self.volume_restriction_enzyme = params['volume_restriction_enzyme']
+ self.volume_t4_dna_ligase = params['volume_t4_dna_ligase']
+ self.volume_t4_dna_ligase_buffer = params['volume_t4_dna_ligase_buffer']
+ self.replicates = params['replicates']
+ self.thermocycler_starting_well = params['thermocycler_starting_well']
+ self.thermocycler_labware = params['thermocycler_labware']
+ self.temperature_module_labware = params['temperature_module_labware']
+ self.temperature_module_position = params['temperature_module_position']
+ self.tiprack_labware = params['tiprack_labware']
+ if params['tiprack_positions'] is None:
+ self.tiprack_positions = ['2', '3', '4', '5', '6', '9']
+ else:
+ self.tiprack_positions = params['tiprack_positions']
+ self.pipette = params['pipette']
+ self.pipette_position = params['pipette_position']
+ self.initial_tip = params['initial_tip']
+ self.aspiration_rate = params['aspiration_rate']
+ self.dispense_rate = params['dispense_rate']
+ self.take_picture = params['take_picture']
+ self.take_video = params['take_video']
+ self.water_testing = params['water_testing']
+ self.output_xlsx = params['output_xlsx']
+ self.protocol_name = params['protocol_name']
+
+ # Shared tracking dictionaries
self.dict_of_parts_in_temp_mod_position = {}
self.dict_of_parts_in_thermocycler = {}
- #self.sbol_output = []
- #self.xlsx_output = None
-
- if len(parts) > 19:
- raise ValueError(f'This protocol only supports assemblies with up to 20 parts. Number of parts in the protocol is {len(parts)}')
-
- metadata = {
- 'protocolName': 'PUDU Domestication',
- 'author': 'Gonzalo Vidal ',
- 'description': 'Automated DNA domestication protocol',
- 'apiLevel': '2.13'}
-
- def run(self, protocol: protocol_api.ProtocolContext):
-
- #Labware
- #Load temperature module
- tem_mod = protocol.load_module('temperature module', f'{self.temperature_module_position}') #CV: Previously was '3', but the cord was not long enough
- tem_mod_block = tem_mod.load_labware(self.temperature_module_labware)
- #Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
- thermocycler_mod = protocol.load_module('thermocycler module')
- thermocycler_mod_plate = thermocycler_mod.load_labware(self.thermocycler_labware)
- #Load the tiprack
- tiprack = protocol.load_labware(self.tiprack_labware, f'{self.tiprack_position}')
- #Load the pipette
- pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
- #Fixed volumes
+ self.dna_list_for_transformation_protocol = []
+ self.product_uri_to_wells = {}
+ self.xlsx_output = None
+
+ #Initialize Camera
+ self.camera = Camera()
+ # Tip management
+ self.tip_management = {
+ 'all_racks': [],
+ 'on_deck_racks': [],
+ 'off_deck_racks': [],
+ 'available_slots': [],
+ 'tips_used': 0,
+ 'tips_per_batch': 0,
+ 'current_batch': 1,
+ 'total_batches': 1
+ }
+
+ def _merge_params(self, json_params: Dict, kwargs_params: Dict) -> Dict:
+ """
+ Merge parameters with precedence: defaults <- advanced_params <- kwargs
+
+ Args:
+ advanced_params: Dictionary of parameters from JSON/dict (optional)
+ kwargs_params: Dictionary of parameters from function kwargs
+
+ Returns:
+ Merged parameter dictionary
+
+ Raises:
+ ValueError: If advanced_params contains unknown parameters
+ """
+ # Define all valid parameter names with their defaults
+ valid_params = {
+ 'volume_total_reaction': 20,
+ 'volume_part': 2,
+ 'volume_restriction_enzyme': 2,
+ 'volume_t4_dna_ligase': 4,
+ 'volume_t4_dna_ligase_buffer': 2,
+ 'replicates': 1,
+ 'thermocycler_starting_well': 0,
+ 'thermocycler_labware': 'nest_96_wellplate_100ul_pcr_full_skirt',
+ 'temperature_module_labware': 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ 'temperature_module_position': '1',
+ 'tiprack_labware': 'opentrons_96_tiprack_20ul',
+ 'tiprack_positions': None,
+ 'pipette': 'p20_single_gen2',
+ 'pipette_position': 'left',
+ 'initial_tip': None,
+ 'aspiration_rate': 0.5,
+ 'dispense_rate': 1,
+ 'take_picture': False,
+ 'take_video': False,
+ 'water_testing': False,
+ 'output_xlsx': True,
+ 'protocol_name': ''
+ }
+
+ # Start with defaults
+ merged = valid_params.copy()
+
+ # Apply json_params if provided
+ if json_params is not None:
+ self._validate_param_structure(json_params, valid_params)
+ merged.update(json_params)
+
+ # Apply kwargs (checking against defaults to see what was explicitly passed)
+ # Only update if the kwarg value differs from the default
+ for key, value in kwargs_params.items():
+ if key in valid_params:
+ # Always use kwargs value, even if it matches default
+ # This ensures explicit kwargs override advanced_params
+ if json_params is None or key not in json_params or value != valid_params[key]:
+ merged[key] = value
+
+ return merged
+
+ def _validate_param_structure(self, advanced_params: Dict, valid_params: Dict):
+ """
+ Validate that advanced_params only contains known parameter names.
+
+ Args:
+ advanced_params: Dictionary to validate
+ valid_params: Dictionary of valid parameter names
+
+ Raises:
+ ValueError: If unknown parameters are found
+ """
+ unknown_params = set(advanced_params.keys()) - set(valid_params.keys())
+ if unknown_params:
+ raise ValueError(
+ f"Unknown parameters in advanced_params: {unknown_params}. "
+ f"Valid parameters are: {set(valid_params.keys())}"
+ )
+
+ def _validate_reaction_volumes(self, num_parts: int):
+ """
+ Validate that reaction volumes are physically possible.
+
+ Args:
+ num_parts: Number of DNA parts (including backbone) in the assembly
+
+ Raises:
+ ValueError: If volumes exceed total reaction volume
+ """
+ volume_reagents = (self.volume_restriction_enzyme +
+ self.volume_t4_dna_ligase +
+ self.volume_t4_dna_ligase_buffer)
+
+ total_parts_volume = self.volume_part * num_parts
+ total_needed = volume_reagents + total_parts_volume
+
+ if total_needed >= self.volume_total_reaction:
+ water_volume = self.volume_total_reaction - total_needed
+ raise ValueError(
+ f"Reaction volume error: Cannot fit {num_parts} parts into {self.volume_total_reaction}µL reaction.\n"
+ f" Required volumes:\n"
+ f" - Reagents (enzyme + ligase + buffer): {volume_reagents}µL\n"
+ f" - Parts ({num_parts} × {self.volume_part}µL): {total_parts_volume}µL\n"
+ f" - Water: {water_volume}µL (NEGATIVE!)\n"
+ f" Total needed: {total_needed}µL\n"
+ f" Solutions:\n"
+ f" 1. Increase 'volume_total_reaction' to at least {total_needed + 1}µL\n"
+ f" 2. Decrease 'volume_part' to at most {(self.volume_total_reaction - volume_reagents - 1) / num_parts:.1f}µL\n"
+ f" 3. Decrease reagent volumes"
+ )
+
+ def _well_to_index(self, well_name: str) -> int:
+ """
+ Convert well name (e.g., 'A1', 'H12') to 0-based index in 96-well plate.
+
+ Args:
+ well_name: Well position like 'A1', 'B3', 'H12'
+
+ Returns:
+ Zero-based index (0-95) for the well
+
+ Raises:
+ ValueError: If well_name format is invalid
+ """
+ if not well_name or len(well_name) < 2:
+ raise ValueError(f"Invalid well name: '{well_name}'. Expected format like 'A1', 'B3', 'H12'")
+
+ row = well_name[0]
+ try:
+ col = int(well_name[1:])
+ except ValueError:
+ raise ValueError(f"Invalid well name: '{well_name}'. Column must be a number (e.g., 'A1', 'H12')")
+
+ if row not in 'ABCDEFGH':
+ raise ValueError(f"Invalid well name: '{well_name}'. Row must be A-H")
+
+ if col < 1 or col > 12:
+ raise ValueError(f"Invalid well name: '{well_name}'. Column must be 1-12")
+
+ row_index = ord(row) - ord('A')
+ col_index = col - 1
+ return row_index + (col_index * 8)
+
+ def _tips_available_from_position(self, well_name: str) -> int:
+ """
+ Calculate how many tips are available starting from a given position.
+
+ Args:
+ well_name: Starting well position like 'A1', 'H12'
+
+ Returns:
+ Number of tips available from that position to H12
+ """
+ start_index = self._well_to_index(well_name)
+ return 96 - start_index
+
+ @abstractmethod
+ def process_assemblies(self):
+ """Process input assemblies - format-specific implementation"""
+ pass
+
+ @abstractmethod
+ def _load_parts_and_enzymes(self, protocol, alum_block) -> int:
+ """Load parts and enzymes onto temperature module - format-specific"""
+ pass
+
+ @abstractmethod
+ def _process_assembly_combinations(self, protocol, pipette, thermo_plate, alum_block,
+ dd_h2o, t4_dna_ligase_buffer, t4_dna_ligase,
+ volume_reagents, thermocycler_well_counter) -> int:
+ """Process all assembly combinations - format-specific"""
+ pass
+
+ @abstractmethod
+ def _calculate_total_tips_needed(self) -> int:
+ """Calculate total tips needed - format-specific implementation"""
+ pass
+
+ def setup_tip_management(self, protocol):
+ """Setup batch tip management for high-throughput applications."""
+ total_tips_needed = self._calculate_total_tips_needed()
+
+ first_rack_tips = 96
+ if self.initial_tip:
+ try:
+ first_rack_tips = self._tips_available_from_position(self.initial_tip)
+ protocol.comment(f"Starting from tip {self.initial_tip} ({first_rack_tips} tips available on first rack)")
+ except ValueError as e:
+ raise ValueError(f"Error with initial_tip parameter: {e}")
+
+ # Calculate racks needed, accounting for the partially used first rack
+ if total_tips_needed <= first_rack_tips:
+ tip_racks_needed = 1
+ else:
+ remaining_tips = total_tips_needed - first_rack_tips
+ additional_racks = (remaining_tips + 95) // 96
+ tip_racks_needed = 1 + additional_racks
+
+ available_deck_slots = self.tiprack_positions
+ max_racks_on_deck = len(available_deck_slots)
+
+ protocol.comment(f"Protocol requires {total_tips_needed} tips ({tip_racks_needed} racks)")
+
+ all_tip_racks = []
+ on_deck_racks = []
+ for i in range(min(tip_racks_needed, max_racks_on_deck)):
+ rack = protocol.load_labware(self.tiprack_labware, available_deck_slots[i])
+ all_tip_racks.append(rack)
+ on_deck_racks.append(rack)
+
+ off_deck_racks = []
+ for i in range(max_racks_on_deck, tip_racks_needed):
+ rack = protocol.load_labware(self.tiprack_labware, protocol_api.OFF_DECK)
+ all_tip_racks.append(rack)
+ off_deck_racks.append(rack)
+
+ self.tip_management.update({
+ 'all_racks': all_tip_racks,
+ 'on_deck_racks': on_deck_racks,
+ 'off_deck_racks': off_deck_racks,
+ 'available_slots': available_deck_slots,
+ 'tips_used': 0,
+ 'tips_per_batch': max_racks_on_deck * 96,
+ 'current_batch': 1,
+ 'total_batches': (tip_racks_needed + max_racks_on_deck - 1) // max_racks_on_deck
+ })
+
+ if len(off_deck_racks) > 0:
+ protocol.comment(f"Will perform {self.tip_management['total_batches'] - 1} tip rack batch swaps")
+
+ return all_tip_racks
+
+ def liquid_transfer(self, protocol, pipette, volume, source, dest,
+ asp_rate: float = 0.5, disp_rate: float = 1.0,
+ blow_out: bool = True, touch_tip: bool = False,
+ mix_before: float = 0.0, mix_after: float = 0.0,
+ mix_reps: int = 3, new_tip: bool = True,
+ drop_tip: bool = True):
+ if new_tip:
+ if self._check_if_swap_needed():
+ self._perform_tip_rack_batch_swap(protocol)
+ try:
+ pipette.pick_up_tip()
+ self._increment_tip_counter()
+ except Exception as e:
+ protocol.comment(f"Tip pickup failed with error: {e}")
+ raise
+
+ if mix_before > 0:
+ pipette.mix(mix_reps, mix_before, source)
+
+ pipette.aspirate(volume, source, rate=asp_rate)
+ pipette.dispense(volume, dest, rate=disp_rate)
+
+ if mix_after > 0:
+ pipette.mix(mix_reps, mix_after, dest)
+
+ if blow_out:
+ pipette.blow_out()
+
+ if touch_tip:
+ pipette.touch_tip(radius=0.5, v_offset=-14, speed=20)
+
+ if drop_tip:
+ pipette.drop_tip()
+
+ def get_xlsx_output(self, name: str):
+ workbook = xlsxwriter.Workbook(f"{name}.xlsx")
+ worksheet = workbook.add_worksheet()
+ row_num = 0
+ col_num = 0
+ worksheet.write(row_num, col_num, "Parts in temp_module")
+ row_num += 2
+ for key, value in self.dict_of_parts_in_temp_mod_position.items():
+ worksheet.write(row_num, col_num, key)
+ worksheet.write(row_num + 1, col_num, value)
+ col_num += 1
+ col_num = 0
+ row_num += 4
+ worksheet.write(row_num, col_num, "Parts in thermocycler_module")
+ row_num += 2
+ for key, value in self.dict_of_parts_in_thermocycler.items():
+ key_str = " + ".join(key) if isinstance(key, tuple) else str(key)
+ worksheet.write(row_num, col_num, key_str)
+ worksheet.write(row_num + 1, col_num, value)
+ col_num += 1
+ workbook.close()
+ self.xlsx_output = workbook
+ return self.xlsx_output
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+ """Main protocol execution - uses template method pattern"""
+ # Process assemblies (format-specific)
+ self.process_assemblies()
+
+ # Load hardware (shared)
+ temperature_module = protocol.load_module(module_name='temperature module',
+ location=self.temperature_module_position)
+ alum_block = temperature_module.load_labware(self.temperature_module_labware)
+
+ thermocycler_module = protocol.load_module('thermocycler module')
+ thermo_plate = thermocycler_module.load_labware(name=self.thermocycler_labware)
+
+ all_tip_racks = self.setup_tip_management(protocol)
+ pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=all_tip_racks)
+ if self.initial_tip:
+ pipette.starting_tip = self.tip_management['on_deck_racks'][0][self.initial_tip]
+ protocol.comment(f"Pipette will start from tip {self.initial_tip}")
+
+ # Load common reagents (shared)
+ dd_h2o = self._load_reagent(protocol, module_labware=alum_block, well_position=0,
+ name='Deionized Water')
+ t4_dna_ligase_buffer = self._load_reagent(protocol, module_labware=alum_block, well_position=1,
+ name='T4 DNA Ligase Buffer')
+ t4_dna_ligase = self._load_reagent(protocol, module_labware=alum_block, well_position=2,
+ name="T4 DNA Ligase")
+
+ # Load parts and enzymes (format-specific)
+ temp_module_well_counter = self._load_parts_and_enzymes(protocol, alum_block)
+
+ # Setup temperatures
+ thermocycler_module.open_lid()
+ if not self.water_testing:
+ temperature_module.set_temperature(4)
+ thermocycler_module.set_block_temperature(4)
+
+ # Media capture start
+ if self.take_picture:
+ self.camera.capture_picture(protocol, when="start")
+ if self.take_video:
+ self.camera.start_video(protocol)
+
+ # Process assemblies (format-specific)
volume_reagents = self.volume_restriction_enzyme + self.volume_t4_dna_ligase + self.volume_t4_dna_ligase_buffer
- volume_dd_h2o = self.volume_total_reaction - (volume_reagents + self.volume_part*2)
- #Load the reagents
- restriction_enzyme = tem_mod_block['A1']
- self.dict_of_parts_in_temp_mod_position['restriction_enzyme'] = temp_wells[0]
- t4_dna_ligase = tem_mod_block['A2']
- self.dict_of_parts_in_temp_mod_position['t4_dna_ligase'] = temp_wells[1]
- t4_dna_ligase_buffer = tem_mod_block['A3']
- self.dict_of_parts_in_temp_mod_position['t4_dna_ligase_buffer'] = temp_wells[2]
- dd_h2o = tem_mod_block['A4']
- self.dict_of_parts_in_temp_mod_position['dd_h2o'] = temp_wells[3]
- backbone = tem_mod_block['A5']
- self.dict_of_parts_in_temp_mod_position['backbone'] = temp_wells[4]
- temp_wells_counter = 5
- #TODO: multiple backbones
- #Setup
- #Set the temperature of the temperature module and the thermocycler to 4°C
- tem_mod.set_temperature(4)
- thermocycler_mod.open_lid()
- thermocycler_mod.set_block_temperature(4)
- #Commands for the mastermix
- ending_well = self.thermocycler_starting_well + len(self.parts)*self.replicates
- wells = thermo_wells[self.thermocycler_starting_well:ending_well] #wells = ['D6', 'D7']
- #can be done with multichannel pipette
- for well in wells:
- liquid_transfer(pipette, volume_dd_h2o, dd_h2o, thermocycler_mod_plate[well], self.aspiration_rate, self.dispense_rate)
- liquid_transfer(pipette, self.volume_t4_dna_ligase_buffer, t4_dna_ligase_buffer, thermocycler_mod_plate[well], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_t4_dna_ligase_buffer)
- liquid_transfer(pipette, self.volume_t4_dna_ligase, t4_dna_ligase, thermocycler_mod_plate[well], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_t4_dna_ligase)
- liquid_transfer(pipette, self.volume_restriction_enzyme, restriction_enzyme, thermocycler_mod_plate[well], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_restriction_enzyme)
- liquid_transfer(pipette, self.volume_part, backbone, thermocycler_mod_plate[well], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_part)
- #for well in wells:
- i = self.thermocycler_starting_well
- for part in self.parts:
- if type(part) == str:
- part_name=part
- else: raise ValueError(f'Part {part} is not a string or an sbol3.Component')
- self.dict_of_parts_in_temp_mod_position[part_name] = temp_wells[temp_wells_counter]
- for r in range(self.replicates):
- part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[i]]
- if r == 0:
- self.dict_of_parts_in_thermocycler[part_name] = [thermo_wells[i]]
- else:
- self.dict_of_parts_in_thermocycler[part_name].append(thermo_wells[i])
- liquid_transfer(pipette, self.volume_part, tem_mod_block[self.dict_of_parts_in_temp_mod_position[part_name]], part_ubication_in_thermocyler, self.aspiration_rate, self.dispense_rate, mix_before=self.volume_restriction_enzyme)
- i+=1
- temp_wells_counter += 1
-
+ thermocycler_well_counter = self._process_assembly_combinations(
+ protocol, pipette, thermo_plate, alum_block, dd_h2o,
+ t4_dna_ligase_buffer, t4_dna_ligase, volume_reagents,
+ self.thermocycler_starting_well
+ )
+
protocol.comment('Take out the reagents since the temperature module will be turn off')
- #We close the thermocycler lid and wait for the temperature to reach 42°C
- thermocycler_mod.close_lid()
- #The thermocycler's lid temperature is set with the following command
- thermocycler_mod.set_lid_temperature(42)
- tem_mod.deactivate()
- #Cycles were made following https://pubs.acs.org/doi/10.1021/sb500366v
- profile = [
- {'temperature': 42, 'hold_time_minutes': 2},
- {'temperature': 16, 'hold_time_minutes': 5}]
- thermocycler_mod.execute_profile(steps=profile, repetitions=25, block_max_volume=30)
- denaturation = [
- {'temperature': 60, 'hold_time_minutes': 10},
- {'temperature': 80, 'hold_time_minutes': 10}]
- thermocycler_mod.execute_profile(steps=denaturation, repetitions=1, block_max_volume=30)
- thermocycler_mod.set_block_temperature(4)
+ # Thermocycling
+ if not self.water_testing:
+ thermocycler_module.close_lid()
+ thermocycler_module.set_lid_temperature(42)
+ temperature_module.deactivate()
+
+ # Media capture end
+ if self.take_video:
+ self.camera.stop_video(protocol)
+ if self.take_picture:
+ self.camera.capture_picture(protocol, when="end")
- #output
+ # Execute thermocycling profiles
+ if not self.water_testing:
+ profile = [
+ {'temperature': 42, 'hold_time_minutes': 2},
+ {'temperature': 16, 'hold_time_minutes': 5}
+ ]
+ denaturation = [
+ {'temperature': 60, 'hold_time_minutes': 10},
+ {'temperature': 80, 'hold_time_minutes': 10}
+ ]
+ thermocycler_module.execute_profile(steps=profile, repetitions=75, block_max_volume=30)
+ thermocycler_module.execute_profile(steps=denaturation, repetitions=1, block_max_volume=30)
+ thermocycler_module.set_block_temperature(4)
+
+ if protocol.is_simulating():
+ if self.output_xlsx:
+ try:
+ if not self.protocol_name:
+ self.protocol_name = "Loop Assembly"
+ self.get_xlsx_output(self.protocol_name)
+ except Exception as e:
+ protocol.comment(f"Could not create Excel file: {e}")
+ # Export transformation input for next protocol (simulation only)
+ try:
+ self._export_transformation_input(protocol)
+ except Exception as e:
+ protocol.comment(f"Could not export transformation input: {e}")
+
+ # Output results
print('Parts and reagents in temp_module')
print(self.dict_of_parts_in_temp_mod_position)
- print('Domesticated parts in thermocycler_module')
+ print('Assembled parts in thermocycler_module')
print(self.dict_of_parts_in_thermocycler)
- #END
+ print('DNA list for transformation protocol')
+ print(self.dna_list_for_transformation_protocol)
+
+ # Helper methods (shared)
+ def _export_transformation_input(self, protocol):
+ """
+ Export plasmid location JSON during simulation for use by transformation protocol.
+ Format: { "product_uri": ["well1", "well2", ...], ... }
+ """
+ output_path = 'transformation_input.json'
+ with open(output_path, 'w') as f:
+ json.dump(self.product_uri_to_wells, f, indent=2)
+ protocol.comment("\n" + "="*70)
+ protocol.comment(f"Generated {output_path} for transformation protocol")
+ protocol.comment(f" Products: {len(self.product_uri_to_wells)}")
+ protocol.comment("="*70)
+ def _load_reagent(self, protocol, module_labware, well_position, name, description=None,
+ volume=1000, color_index=None):
+ """Load a reagent or DNA part onto the temperature module."""
+ well = module_labware.wells()[well_position]
+ well_name = well.well_name
-class Loop_assembly(DNA_assembly):
- '''
- Creates a protocol for the automated Odd and/or Even level Loop assembly.
+ if description is None:
+ description = name
+ if color_index is None:
+ color_index = len(self.dict_of_parts_in_temp_mod_position) % len(colors)
+
+ liquid = protocol.define_liquid(name=name, description=description,
+ display_color=colors[color_index])
+ well.load_liquid(liquid, volume=volume)
+
+ self.dict_of_parts_in_temp_mod_position[name] = well_name
+ protocol.comment(f"Loaded {name} at position {well_name}")
+
+ return well
+
+ def _increment_tip_counter(self):
+ """Increment tip usage counter"""
+ self.tip_management['tips_used'] += 1
+
+ def _check_if_swap_needed(self):
+ current_batch_tips = self.tip_management['tips_used'] % self.tip_management['tips_per_batch']
+ return (current_batch_tips == 0 and
+ self.tip_management['tips_used'] > 0 and
+ len(self.tip_management['off_deck_racks']) > 0)
+
+ def _perform_tip_rack_batch_swap(self, protocol):
+ """Perform tip rack batch swap when current batch is exhausted"""
+ available_slots = self.tip_management['available_slots']
+
+ for rack in self.tip_management['on_deck_racks']:
+ protocol.move_labware(labware=rack, new_location=protocol_api.OFF_DECK)
+
+ remaining_racks = len(self.tip_management['off_deck_racks'])
+ racks_to_move = min(len(available_slots), remaining_racks)
+
+ new_on_deck_racks = []
+ for i in range(racks_to_move):
+ rack = self.tip_management['off_deck_racks'].pop(0)
+ protocol.move_labware(labware=rack, new_location=available_slots[i])
+ new_on_deck_racks.append(rack)
+
+ self.tip_management['on_deck_racks'] = new_on_deck_racks
+ self.tip_management['current_batch'] += 1
+
+ protocol.comment(f"Tip rack batch {self.tip_management['current_batch']} ready!")
+
+class Domestication(BaseAssembly):
+ """
+ Domestication Assembly - inserts individual parts into universal acceptor backbone.
+ Each part is assembled separately with the backbone to create domesticated parts.
+ """
+
+ def __init__(self,
+ assembly_data: Optional[Dict] = None,
+ json_params: Optional[str] = None,
+ assemblies: Optional[List[Dict]] = None,
+ *args, **kwargs):
+ """
+ Initialize Domestication Assembly protocol.
+
+ Args:
+ assembly_data: Dict containing 'assemblies' key (new standardized approach)
+ advanced_params: Optional advanced parameters
+ assemblies: List of assembly dicts (backward compatibility)
+ *args, **kwargs: Passed to BaseAssembly
+ """
+ # Handle parameter precedence: assembly_data <- assemblies kwarg
+ if assembly_data is not None:
+ if 'assemblies' in assembly_data:
+ assemblies = assembly_data['assemblies']
+ else:
+ # Allow passing assemblies directly in assembly_data for flexibility
+ assemblies = assembly_data
+
+ # Validate that assemblies were provided
+ if assemblies is None:
+ raise ValueError("Must provide assemblies either via assembly_data or assemblies parameter")
+
+ super().__init__(json_params=json_params, *args, **kwargs)
+ self.assemblies = assemblies
+ self.parts_list = []
+ self.backbone = ""
+ self.restriction_enzyme = ""
+
+ def process_assemblies(self):
+ """Process domestication assembly input and validate format"""
+ self._reset_assembly_state()
+ # Domestication should have exactly one assembly
+ if len(self.assemblies) != 1:
+ raise ValueError(f"Domestication supports exactly one assembly, got {len(self.assemblies)}")
+
+ assembly = self.assemblies[0]
+ required_keys = {"parts", "backbone", "restriction_enzyme"}
+ assembly_keys = set(assembly.keys())
+
+ if not required_keys.issubset(assembly_keys):
+ missing_keys = required_keys - assembly_keys
+ raise ValueError(f"Domestication assembly missing required keys: {missing_keys}")
+
+ # Extract and validate parts
+ parts = assembly["parts"]
+ if isinstance(parts, str):
+ self.parts_list = [parts]
+ elif isinstance(parts, list):
+ self.parts_list = parts
+ else:
+ raise ValueError("Parts must be a string or list of strings")
+
+ # Extract and validate backbone (must be single value)
+ backbone = assembly["backbone"]
+ if isinstance(backbone, list):
+ if len(backbone) > 1:
+ raise ValueError("Domestication supports only one backbone")
+ self.backbone = backbone[0]
+ else:
+ self.backbone = backbone
+
+ # Extract and validate restriction enzyme (must be single value)
+ restriction_enzyme = assembly["restriction_enzyme"]
+ if isinstance(restriction_enzyme, list):
+ if len(restriction_enzyme) > 1:
+ raise ValueError("Domestication supports only one restriction enzyme")
+ self.restriction_enzyme = restriction_enzyme[0]
+ else:
+ self.restriction_enzyme = restriction_enzyme
+
+ self._validate_assembly_requirements()
+
+ def _load_parts_and_enzymes(self, protocol, alum_block) -> int:
+ """Load restriction enzyme, backbone, and parts for domestication"""
+ temp_module_well_counter = 3 # Starting after common reagents (water, ligase buffer, ligase)
+
+ # Load restriction enzyme
+ self._load_reagent(protocol, module_labware=alum_block,
+ well_position=temp_module_well_counter,
+ name=f"Restriction Enzyme {self.restriction_enzyme}")
+ temp_module_well_counter += 1
+
+ # Load backbone
+ self._load_reagent(protocol, module_labware=alum_block,
+ well_position=temp_module_well_counter,
+ name=f"Backbone {self.backbone}")
+ temp_module_well_counter += 1
+
+ # Load individual parts
+ for part in self.parts_list:
+ self._load_reagent(protocol, module_labware=alum_block,
+ well_position=temp_module_well_counter,
+ name=f"Part {part}")
+ temp_module_well_counter += 1
+
+ return temp_module_well_counter
+
+ def _process_assembly_combinations(self, protocol, pipette, thermo_plate, alum_block,
+ dd_h2o, t4_dna_ligase_buffer, t4_dna_ligase,
+ volume_reagents, thermocycler_well_counter) -> int:
+ """Process domestication assemblies - each part with backbone separately"""
+
+ # Get reagent sources
+ restriction_enzyme = alum_block[
+ self.dict_of_parts_in_temp_mod_position[f"Restriction Enzyme {self.restriction_enzyme}"]]
+ backbone_source = alum_block[self.dict_of_parts_in_temp_mod_position[f"Backbone {self.backbone}"]]
+
+ # Process each part
+ for part in self.parts_list:
+ part_source = alum_block[self.dict_of_parts_in_temp_mod_position[f"Part {part}"]]
+
+ # Process replicates for this part
+ for r in range(self.replicates):
+ dest_well = thermo_plate.wells()[thermocycler_well_counter]
+ dest_well_name = dest_well.well_name
+
+ # Calculate water volume (total - reagents - 2 parts: backbone + part)
+ volume_dd_h20 = self.volume_total_reaction - (volume_reagents + self.volume_part * 2)
+
+ # Add reagents
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=volume_dd_h20,
+ source=dd_h2o, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ touch_tip=True)
+
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_t4_dna_ligase_buffer,
+ source=t4_dna_ligase_buffer, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_t4_dna_ligase_buffer, touch_tip=True)
+
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_t4_dna_ligase,
+ source=t4_dna_ligase, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_t4_dna_ligase, touch_tip=True)
+
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_restriction_enzyme,
+ source=restriction_enzyme, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_restriction_enzyme, touch_tip=True)
+
+ # Add backbone
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_part,
+ source=backbone_source, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_part, touch_tip=True)
+
+ # Add part (don't drop tip yet)
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_part,
+ source=part_source, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_part, touch_tip=True, drop_tip=False)
+
+ # Remove air bubbles with mixing
+ mix_volume = min(self.volume_total_reaction, pipette.max_volume)
+ for _ in range(int(self.volume_total_reaction / 10)):
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=mix_volume,
+ source=dest_well.bottom(), dest=dest_well.bottom(8),
+ asp_rate=1.0, disp_rate=1.0, new_tip=False, drop_tip=False,
+ touch_tip=True)
+ pipette.drop_tip()
+
+ # Track assembly
+ assembly_name = f"Part: {part}, Replicate: {r + 1}"
+ self.dict_of_parts_in_thermocycler[assembly_name] = dest_well_name
+ self.dna_list_for_transformation_protocol.append(f"{part}_rep{r + 1}")
+
+ thermocycler_well_counter += 1
+
+ return thermocycler_well_counter
+
+ def _calculate_total_tips_needed(self, number_of_constant_reagents: int = 6) -> int:
+ """Calculate total tips needed for domestication
+
+ Args:
+ number_of_constant_reagents: water + ligase buffer + ligase + enzyme + backbone + part = 6
+ """
+ total_assemblies = len(self.parts_list) * self.replicates
+ return number_of_constant_reagents * total_assemblies
+
+ def _validate_assembly_requirements(self):
+ """Validate domestication assembly requirements"""
+ if not self.parts_list:
+ raise ValueError("No parts provided for domestication")
+
+ if not self.backbone:
+ raise ValueError("No backbone provided for domestication")
+
+ if not self.restriction_enzyme:
+ raise ValueError("No restriction enzyme provided for domestication")
+
+ # Calculate reagent positions: water(1) + ligase buffer(1) + ligase(1) + enzyme(1) + backbone(1) = 5
+ reagent_positions = 5
+ max_parts = 24 - reagent_positions
+
+ if len(self.parts_list) > max_parts:
+ raise ValueError(
+ f'This protocol only supports domestication with up to {max_parts} parts. '
+ f'Number of parts provided is {len(self.parts_list)}. '
+ f'Parts: {self.parts_list}. '
+ f'Reagent positions used: {reagent_positions}/24'
+ )
+
+ # Validate thermocycler capacity
+ available_wells = 96 - self.thermocycler_starting_well
+ wells_needed = len(self.parts_list) * self.replicates
+
+ if wells_needed > available_wells:
+ raise ValueError(
+ f'This protocol only supports assemblies with up to {available_wells} '
+ f'wells. Number of assemblies needed is {wells_needed} '
+ f'({len(self.parts_list)} parts × {self.replicates} replicates).'
+ )
+
+ self._validate_reaction_volumes(num_parts=2)
+
+ def _reset_assembly_state(self):
+ """Reset assembly processing state"""
+ self.parts_list = []
+ self.backbone = ""
+ self.restriction_enzyme = ""
+
+
+class ManualLoopAssembly(BaseAssembly):
+ """
+ Manual/Combinatorial Loop Assembly - generates combinations from roles.
+ Supports Odd/Even pattern detection for automatic enzyme selection.
+ """
+
+ def __init__(self,
+ assembly_data: Optional[Dict] = None,
+ json_params: Optional[str] = None,
+ assemblies: Optional[List[Dict]] = None,
+ *args, **kwargs):
+ """
+ Initialize Manual Loop Assembly protocol.
+
+ Args:
+ assembly_data: Dict containing 'assemblies' key (new standardized approach)
+ json_params: Optional advanced parameters
+ assemblies: List of assembly dicts (backward compatibility)
+ *args, **kwargs: Passed to BaseAssembly
+ """
+ # Handle parameter precedence: assembly_data <- assemblies kwarg
+ if assembly_data is not None:
+ if 'assemblies' in assembly_data:
+ assemblies = assembly_data['assemblies']
+ else:
+ # Allow passing assemblies directly in assembly_data for flexibility
+ assemblies = assembly_data
+
+ # Validate that assemblies were provided
+ if assemblies is None:
+ raise ValueError("Must provide assemblies either via assembly_data or assemblies parameter")
+
+ super().__init__(json_params=json_params, *args, **kwargs)
- '''
- def __init__(self, assemblies:List[Dict],
- *args, **kwargs):
- super().__init__(*args, **kwargs)
-
self.assemblies = assemblies
- self.dict_of_parts_in_temp_mod_position = {}
- self.dict_of_parts_in_thermocycler = {}
- self.assembly_plan = None
self.pattern_odd = 'Odd*'
self.pattern_even = 'Even*'
self.parts_set = set()
@@ -217,159 +861,831 @@ def __init__(self, assemblies:List[Dict],
self.has_even = False
self.odd_combinations = []
self.even_combinations = []
-
- # add parts to a set
+
+ def process_assemblies(self):
+ """Process manual format assemblies and generate combinations"""
+ self._reset_assembly_state()
+
for assembly in self.assemblies:
- list_of_list_of_parts_per_role = []
- if fnmatch(assembly['receiver'], self.pattern_odd):
+ assembly_type = self._get_assembly_type(assembly['receiver'])
+ if assembly_type == 'odd':
self.has_odd = True
- for role in assembly:
- parts = assembly[role]
- if type(parts) is str:
- parts_per_role = [parts]
- else:
- parts_per_role = parts
- for part in parts_per_role:
- self.parts_set.add(part)
- list_of_list_of_parts_per_role.append(parts_per_role)
- list_of_combinations_per_assembly = list(product(*list_of_list_of_parts_per_role))
- for combination in list_of_combinations_per_assembly:
- self.odd_combinations.append(combination)
- if fnmatch(assembly['receiver'], self.pattern_even):
- self.has_even = True
- for role in assembly:
- parts = assembly[role]
- if type(parts) is str:
- parts_per_role = [parts]
- else:
- parts_per_role = parts
- for part in parts_per_role:
- self.parts_set.add(part)
- list_of_list_of_parts_per_role.append(parts_per_role)
- list_of_combinations_per_assembly = list(product(*list_of_list_of_parts_per_role))
- for combination in list_of_combinations_per_assembly:
- self.even_combinations.append(combination)
-
- if self.has_odd and self.has_even:
- max_parts = 18
- elif self.has_odd or self.has_even:
- max_parts = 19
- else:
- raise ValueError('Assembly does not have any Even or Odd receiver, check assembly dictionaries and patterns for Odd and Even receivers')
- if len(self.parts_set) > max_parts:
- raise ValueError(f'This protocol only supports assemblies with up to {max_parts} parts. Number of parts in the protocol is {len(self.parts_set)}. Printing parts set:{self.parts_set}')
- thermocyler_available_wells = 96 - self.thermocycler_starting_well
- thermocycler_wells_needed = (len(self.odd_combinations) + len(self.even_combinations))*self.replicates
- if thermocycler_wells_needed > thermocyler_available_wells:
- raise ValueError(f'According to your setup this protocol only supports assemblies with up to {thermocyler_available_wells} combinations. Number of combinations in the protocol is {thermocycler_wells_needed}.')
-
- def run(self, protocol: protocol_api.ProtocolContext):
- #Labware
- #Load temperature module
- tem_mod = protocol.load_module('temperature module', f'{self.temperature_module_position}') #CV: Previously was '3', but the cord was not long enough
- tem_mod_block = tem_mod.load_labware(self.temperature_module_labware)
- #Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
- thermocycler_mod = protocol.load_module('thermocycler module')
- thermocycler_mod_plate = thermocycler_mod.load_labware(self.thermocycler_labware)
- #Load the tiprack
- tiprack = protocol.load_labware(self.tiprack_labware, f'{self.tiprack_position}')
- #Load the pipette
- pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
- #Fixed volumes
- volume_reagents = self.volume_restriction_enzyme + self.volume_t4_dna_ligase + self.volume_t4_dna_ligase_buffer
- #Load the reagents
- dd_h2o = tem_mod_block['A1']
- self.dict_of_parts_in_temp_mod_position['dd_h2o'] = temp_wells[0]
- t4_dna_ligase = tem_mod_block['A2']
- self.dict_of_parts_in_temp_mod_position['t4_dna_ligase'] = temp_wells[1]
- t4_dna_ligase_buffer = tem_mod_block['A3']
- self.dict_of_parts_in_temp_mod_position['t4_dna_ligase_buffer'] = temp_wells[2]
- temp_wells_counter = 3
+ combos = self._generate_combinations_for_assembly(assembly)
+ self.odd_combinations.extend(combos)
+ if assembly_type == 'even':
+ self.has_even = True
+ combos = self._generate_combinations_for_assembly(assembly)
+ self.even_combinations.extend(combos)
+
+ self._validate_assembly_requirements()
+
+ def _load_parts_and_enzymes(self, protocol, alum_block) -> int:
+ """Load enzymes and parts for manual format"""
+ temp_module_well_counter = 3 # Starting after common reagents
+
+ # Load enzymes based on Odd/Even detection
+ if self.has_odd:
+ self._load_reagent(protocol, module_labware=alum_block,
+ well_position=temp_module_well_counter,
+ name="Restriction Enzyme BSAI")
+ temp_module_well_counter += 1
+
+ if self.has_even:
+ self._load_reagent(protocol, module_labware=alum_block,
+ well_position=temp_module_well_counter,
+ name="Restriction Enzyme SAPI")
+ temp_module_well_counter += 1
+
+ # Load parts
+ for part in sorted(self.parts_set):
+ self._load_reagent(protocol, module_labware=alum_block,
+ well_position=temp_module_well_counter,
+ name=f"{part}")
+ temp_module_well_counter += 1
+
+ return temp_module_well_counter
+
+ def _process_assembly_combinations(self, protocol, pipette, thermo_plate, alum_block,
+ dd_h2o, t4_dna_ligase_buffer, t4_dna_ligase,
+ volume_reagents, thermocycler_well_counter) -> int:
+ """Process manual format combinations with automatic enzyme selection"""
+
if self.has_odd:
- restriction_enzyme_bsai = tem_mod_block[temp_wells[temp_wells_counter]]
- self.dict_of_parts_in_temp_mod_position['BsaI'] = temp_wells[temp_wells_counter]
- temp_wells_counter += 1
+ restriction_enzyme_bsai = alum_block[self.dict_of_parts_in_temp_mod_position["Restriction Enzyme BSAI"]]
+ thermocycler_well_counter = self._process_combinations(
+ protocol=protocol, pipette=pipette,
+ combinations=self.odd_combinations,
+ restriction_enzyme=restriction_enzyme_bsai,
+ thermo_plate=thermo_plate, alum_block=alum_block,
+ dd_h2o=dd_h2o, t4_dna_ligase_buffer=t4_dna_ligase_buffer,
+ t4_dna_ligase=t4_dna_ligase, volume_reagents=volume_reagents,
+ thermocycler_well_counter=thermocycler_well_counter
+ )
+
if self.has_even:
- restriction_enzyme_sapi = tem_mod_block[temp_wells[temp_wells_counter]]
- self.dict_of_parts_in_temp_mod_position['SapI'] = temp_wells[temp_wells_counter]
- temp_wells_counter += 1
- #Load the parts
- for part in self.parts_set:
- self.dict_of_parts_in_temp_mod_position[part] = temp_wells[temp_wells_counter]
- temp_wells_counter += 1
- #Setup
- #Set the temperature of the temperature module and the thermocycler to 4°C
- tem_mod.set_temperature(4)
- thermocycler_mod.open_lid()
- thermocycler_mod.set_block_temperature(4)
- #can be done with multichannel pipette?
- current_thermocycler_well = self.thermocycler_starting_well
- #build combinations
- for odd_combination in self.odd_combinations:
- #pippeting reagents
- volume_dd_h2o = self.volume_total_reaction - (volume_reagents + self.volume_part*len(odd_combination))
- liquid_transfer(pipette, volume_dd_h2o, dd_h2o, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate)
- liquid_transfer(pipette, self.volume_t4_dna_ligase_buffer, t4_dna_ligase_buffer, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_t4_dna_ligase_buffer)
- liquid_transfer(pipette, self.volume_t4_dna_ligase, t4_dna_ligase, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_t4_dna_ligase)
- liquid_transfer(pipette, self.volume_restriction_enzyme, restriction_enzyme_bsai, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_restriction_enzyme)
- #pippeting parts
+ restriction_enzyme_sapi = alum_block[self.dict_of_parts_in_temp_mod_position["Restriction Enzyme SAPI"]]
+ thermocycler_well_counter = self._process_combinations(
+ protocol=protocol, pipette=pipette,
+ combinations=self.even_combinations,
+ restriction_enzyme=restriction_enzyme_sapi,
+ thermo_plate=thermo_plate, alum_block=alum_block,
+ dd_h2o=dd_h2o, t4_dna_ligase_buffer=t4_dna_ligase_buffer,
+ t4_dna_ligase=t4_dna_ligase, volume_reagents=volume_reagents,
+ thermocycler_well_counter=thermocycler_well_counter
+ )
+
+ return thermocycler_well_counter
+
+ def _calculate_total_tips_needed(self, number_of_constant_reagents: int = 4) -> int:
+ """Calculate total tips for manual format"""
+ total_combinations = len(self.odd_combinations) + len(self.even_combinations)
+ reagent_tips = number_of_constant_reagents
+ total_reagent_tips = reagent_tips * total_combinations * self.replicates
+
+ total_part_tips = 0
+ for combination in self.odd_combinations + self.even_combinations:
+ total_part_tips += len(combination) * self.replicates
+
+ return total_reagent_tips + total_part_tips
+
+ # Manual format helper methods
+ def _reset_assembly_state(self):
+ """Reset assembly processing state"""
+ self.parts_set = set()
+ self.has_odd = False
+ self.has_even = False
+ self.odd_combinations = []
+ self.even_combinations = []
+
+ def _get_assembly_type(self, receiver_name):
+ """Determine if assembly is odd, even, or neither"""
+ if fnmatch(receiver_name, self.pattern_odd):
+ return 'odd'
+ if fnmatch(receiver_name, self.pattern_even):
+ return 'even'
+ raise ValueError(
+ f"Assembly receiver '{receiver_name}' does not match naming convention. "
+ f"Must be odd pattern '{self.pattern_odd}' or even pattern '{self.pattern_even}'. "
+ f"Check receiver naming."
+ )
+
+ def _generate_combinations_for_assembly(self, assembly):
+ """Generate all possible part combinations for a single assembly"""
+ parts_per_role = []
+ for role, parts in assembly.items():
+ if isinstance(parts, str):
+ parts_list = [parts]
+ else:
+ parts_list = list(parts)
+
+ self.parts_set.update(parts_list)
+ parts_per_role.append(parts_list)
+
+ return list(product(*parts_per_role))
+
+ def _validate_assembly_requirements(self):
+ """Validate manual assembly requirements"""
+ if not (self.has_odd or self.has_even):
+ raise ValueError(
+ "Assembly does not have any Even or Odd receiver. "
+ "Check assembly dictionaries for Odd and Even receivers."
+ )
+
+ reagent_positions = 3 + int(self.has_odd) + int(self.has_even)
+ max_parts = 24 - reagent_positions
+
+ if len(self.parts_set) > max_parts:
+ raise ValueError(
+ f'This protocol only supports assemblies with up to {max_parts} parts. '
+ f'Number of parts in the protocol is {len(self.parts_set)}. '
+ f'Parts: {self.parts_set}. '
+ f'Reagent positions used: {reagent_positions}/24'
+ )
+
+ available_wells = 96 - self.thermocycler_starting_well
+ total_combinations = len(self.odd_combinations) + len(self.even_combinations)
+ wells_needed = total_combinations * self.replicates
+
+ if wells_needed > available_wells:
+ raise ValueError(
+ f'This protocol only supports assemblies with up to {available_wells} '
+ f'combinations. Number of combinations in the protocol are {wells_needed}.'
+ )
+
+ # Validate reaction volumes for all combinations
+ for combination in self.odd_combinations + self.even_combinations:
+ num_parts = len(combination)
+ self._validate_reaction_volumes(num_parts)
+
+ def _process_combinations(self, protocol, pipette, combinations, restriction_enzyme,
+ thermo_plate, alum_block, dd_h2o, t4_dna_ligase_buffer,
+ t4_dna_ligase, volume_reagents, thermocycler_well_counter):
+ """Process combinations with specified restriction enzyme"""
+
+ for combination in combinations:
for r in range(self.replicates):
- for part in odd_combination:
- if type(part) == str:
- part_name=part
- else: raise ValueError(f'Part {part} is not a string nor sbol3.Component')
- #part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[current_thermocycler_well]]
- liquid_transfer(pipette, self.volume_part, tem_mod_block[self.dict_of_parts_in_temp_mod_position[part_name]], thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_part)
- self.dict_of_parts_in_thermocycler[odd_combination] = thermo_wells[current_thermocycler_well]
- current_thermocycler_well+=1
-
- for even_combination in self.even_combinations:
- #pippeting reagents
- volume_dd_h2o = self.volume_total_reaction - (volume_reagents + self.volume_part*len(even_combination))
- liquid_transfer(pipette, volume_dd_h2o, dd_h2o, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate)
- liquid_transfer(pipette, self.volume_t4_dna_ligase_buffer, t4_dna_ligase_buffer, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_t4_dna_ligase_buffer)
- liquid_transfer(pipette, self.volume_t4_dna_ligase, t4_dna_ligase, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_t4_dna_ligase)
- liquid_transfer(pipette, self.volume_restriction_enzyme, restriction_enzyme_sapi, thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_restriction_enzyme)
- #pippeting parts
+ dest_well = thermo_plate.wells()[thermocycler_well_counter]
+ dest_well_name = dest_well.well_name
+
+ volume_dd_h20 = self.volume_total_reaction - (volume_reagents + self.volume_part * len(combination))
+
+ # Add reagents
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=volume_dd_h20,
+ source=dd_h2o, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate, touch_tip=True)
+
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_t4_dna_ligase_buffer,
+ source=t4_dna_ligase_buffer, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_t4_dna_ligase_buffer, touch_tip=True)
+
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_t4_dna_ligase,
+ source=t4_dna_ligase, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_t4_dna_ligase, touch_tip=True)
+
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_restriction_enzyme,
+ source=restriction_enzyme, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_restriction_enzyme, touch_tip=True)
+
+ # Add parts
+ for i, part in enumerate(combination):
+ part_source = alum_block[self.dict_of_parts_in_temp_mod_position[part]]
+ if i == len(combination) - 1: # Don't drop tip on last part
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_part,
+ source=part_source, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_part, touch_tip=True, drop_tip=False)
+ else:
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_part,
+ source=part_source, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_part, touch_tip=True)
+
+ # Remove air bubbles
+ mix_volume = min(self.volume_total_reaction, pipette.max_volume)
+ for _ in range(int(self.volume_total_reaction / 10)):
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=mix_volume,
+ source=dest_well.bottom(), dest=dest_well.bottom(8),
+ asp_rate=1.0, disp_rate=1.0, new_tip=False, drop_tip=False, touch_tip=True)
+ pipette.drop_tip()
+
+ # Track combination
+ self.dict_of_parts_in_thermocycler[f"Replicate: {r + 1}, Combination: {combination}"] = dest_well_name
+ combination_name = "_".join(combination)
+ self.dna_list_for_transformation_protocol.append(f"{combination_name}_rep{r + 1}")
+ thermocycler_well_counter += 1
+
+ return thermocycler_well_counter
+
+
+class SBOLLoopAssembly(BaseAssembly):
+ """
+ SBOL Loop Assembly - handles explicit assembly dictionaries from SBOL format.
+ Each assembly dictionary represents one specific construct to build.
+ """
+
+ def __init__(self,
+ assembly_data: Optional[Dict] = None,
+ json_params: Optional[str] = None,
+ assemblies: Optional[List[Dict]] = None,
+ *args, **kwargs):
+ """
+ Initialize SBOL Loop Assembly protocol.
+
+ Args:
+ assembly_data: Dict containing 'assemblies' key (new standardized approach)
+ advanced_params: Optional advanced parameters
+ assemblies: List of assembly dicts (backward compatibility)
+ *args, **kwargs: Passed to BaseAssembly
+ """
+ # Handle parameter precedence: assembly_data <- assemblies kwarg
+ if assembly_data is not None:
+ if 'assemblies' in assembly_data:
+ assemblies = assembly_data['assemblies']
+ else:
+ # Allow passing assemblies directly in assembly_data for flexibility
+ assemblies = assembly_data
+
+ # Validate that assemblies were provided
+ if assemblies is None:
+ raise ValueError("Must provide assemblies either via assembly_data or assemblies parameter")
+
+ super().__init__(json_params=json_params, *args, **kwargs)
+ self.assemblies = assemblies
+ self.parts_set = set()
+ self.backbone_set = set()
+ self.restriction_enzyme_set = set()
+ self.combined_set = set()
+ self.assembly_combinations = [] # SBOL assemblies are explicit, not combinatorial
+
+ def process_assemblies(self):
+ """Process SBOL format assemblies - each is explicit, no combinations needed"""
+ self._reset_assembly_state()
+
+ for assembly in self.assemblies:
+ # Extract parts from PartsList
+ part_names = []
+ for part_uri in assembly["PartsList"]:
+ part_name = self._extract_name_from_uri(part_uri)
+ self.parts_set.add(part_name)
+ part_names.append(part_name)
+
+ # Extract backbone
+ backbone_name = self._extract_name_from_uri(assembly["Backbone"])
+ self.backbone_set.add(backbone_name)
+
+ # Extract restriction enzyme
+ enzyme_name = self._extract_name_from_uri(assembly["Restriction Enzyme"])
+ self.restriction_enzyme_set.add(enzyme_name)
+
+ # Extract product name
+ product_name = self._extract_name_from_uri(assembly["Product"])
+ assembly_combo = {
+ 'parts': [backbone_name] + part_names, # Include backbone as first part
+ 'enzyme': enzyme_name,
+ 'product': product_name,
+ 'product_uri': assembly["Product"]
+ }
+ self.assembly_combinations.append(assembly_combo)
+
+ self.combined_set = self.parts_set.union(self.backbone_set)
+ self._validate_assembly_requirements()
+
+ def _load_parts_and_enzymes(self, protocol, alum_block) -> int:
+ """Load enzymes and parts for SBOL format"""
+ temp_module_well_counter = 3 # Starting after common reagents
+
+ # Load all unique restriction enzymes
+ for enzyme_name in sorted(self.restriction_enzyme_set):
+ self._load_reagent(protocol, module_labware=alum_block,
+ well_position=temp_module_well_counter,
+ name=f"Restriction Enzyme {enzyme_name}")
+ temp_module_well_counter += 1
+
+ # Load all unique parts (including backbones)
+ for part in sorted(self.combined_set):
+ self._load_reagent(protocol, module_labware=alum_block,
+ well_position=temp_module_well_counter,
+ name=f"{part}")
+ temp_module_well_counter += 1
+
+ return temp_module_well_counter
+
+ def _process_assembly_combinations(self, protocol, pipette, thermo_plate, alum_block,
+ dd_h2o, t4_dna_ligase_buffer, t4_dna_ligase,
+ volume_reagents, thermocycler_well_counter) -> int:
+ """Process SBOL assembly combinations with explicit enzyme selection"""
+
+ for assembly_combo in self.assembly_combinations:
for r in range(self.replicates):
- for part in even_combination:
- if type(part) == str:
- part_name=part
- else: raise ValueError(f'Part {part} is not a string nor sbol3.Component')
- liquid_transfer(pipette, self.volume_part, tem_mod_block[self.dict_of_parts_in_temp_mod_position[part_name]], thermocycler_mod_plate[thermo_wells[current_thermocycler_well]], self.aspiration_rate, self.dispense_rate, mix_before=self.volume_part)
- self.dict_of_parts_in_thermocycler[even_combination] = thermo_wells[current_thermocycler_well]
- current_thermocycler_well+=1
-
- protocol.comment('Take out the reagents since the temperature module will be turn off')
- #We close the thermocycler lid and wait for the temperature to reach 42°C
- thermocycler_mod.close_lid()
- #The thermocycler's lid temperature is set with the following command
- thermocycler_mod.set_lid_temperature(42)
- tem_mod.deactivate()
- #Cycles were made following https://pubs.acs.org/doi/10.1021/sb500366v
- profile = [
- {'temperature': 42, 'hold_time_minutes': 2},
- {'temperature': 16, 'hold_time_minutes': 5}]
- thermocycler_mod.execute_profile(steps=profile, repetitions=25, block_max_volume=30)
+ dest_well = thermo_plate.wells()[thermocycler_well_counter]
+ dest_well_name = dest_well.well_name
+
+ parts = assembly_combo['parts']
+ enzyme_name = assembly_combo['enzyme']
+ product_name = assembly_combo['product']
+
+ volume_dd_h20 = self.volume_total_reaction - (volume_reagents + self.volume_part * len(parts))
+
+ # Add reagents
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=volume_dd_h20,
+ source=dd_h2o, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate, touch_tip=True)
+
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_t4_dna_ligase_buffer,
+ source=t4_dna_ligase_buffer, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_t4_dna_ligase_buffer, touch_tip=True)
+
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_t4_dna_ligase,
+ source=t4_dna_ligase, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_t4_dna_ligase, touch_tip=True)
+
+ # Add restriction enzyme (explicit from SBOL)
+ restriction_enzyme = alum_block[
+ self.dict_of_parts_in_temp_mod_position[f"Restriction Enzyme {enzyme_name}"]]
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_restriction_enzyme,
+ source=restriction_enzyme, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_restriction_enzyme, touch_tip=True)
+
+ # Add parts (including backbone)
+ for i, part in enumerate(parts):
+ part_source = alum_block[self.dict_of_parts_in_temp_mod_position[part]]
+ if i == len(parts) - 1: # Don't drop tip on last part
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_part,
+ source=part_source, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_part, touch_tip=True, drop_tip=False)
+ else:
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=self.volume_part,
+ source=part_source, dest=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=self.volume_part, touch_tip=True)
+
+ # Remove air bubbles
+ mix_volume = min(self.volume_total_reaction, pipette.max_volume)
+ for _ in range(int(self.volume_total_reaction / 10)):
+ self.liquid_transfer(protocol=protocol, pipette=pipette, volume=mix_volume,
+ source=dest_well.bottom(), dest=dest_well.bottom(8),
+ asp_rate=1.0, disp_rate=1.0, new_tip=False, drop_tip=False, touch_tip=True)
+ pipette.drop_tip()
+
+ # Track assembly
+ self.dict_of_parts_in_thermocycler[f"Replicate: {r + 1}, Product: {product_name}"] = dest_well_name
+ self.dna_list_for_transformation_protocol.append(f"{product_name}_rep{r + 1}")
+
+ # Track URI -> well locations for transformation export
+ product_uri = assembly_combo['product_uri']
+ if product_uri not in self.product_uri_to_wells:
+ self.product_uri_to_wells[product_uri] = []
+ self.product_uri_to_wells[product_uri].append(dest_well_name)
- denaturation = [
+ thermocycler_well_counter += 1
+
+ return thermocycler_well_counter
+
+ def _calculate_total_tips_needed(self, number_of_constant_reagents: int = 4) -> int:
+ """Calculate total tips for SBOL format"""
+ total_assemblies = len(self.assembly_combinations)
+ reagent_tips = number_of_constant_reagents
+ total_reagent_tips = reagent_tips * total_assemblies * self.replicates
+
+ total_part_tips = 0
+ for assembly_combo in self.assembly_combinations:
+ total_part_tips += len(assembly_combo['parts']) * self.replicates
+
+ return total_reagent_tips + total_part_tips
+
+ # SBOL format helper methods
+ def _reset_assembly_state(self):
+ """Reset assembly processing state"""
+ self.parts_set = set()
+ self.backbone_set = set()
+ self.restriction_enzyme_set = set()
+ self.combined_set = set()
+ self.assembly_combinations = []
+
+ def _extract_name_from_uri(self, uri: str) -> str:
+ """Extract part name from SBOL URI"""
+ # Extract the last segment after the last '/'
+ if '/' in uri:
+ name_with_version = uri.split('/')[-2]
+ # Remove version number if present (e.g., "GFP/1" -> "GFP")
+ if '/' in name_with_version:
+ return name_with_version.split('/')[0]
+ return name_with_version
+ return uri
+
+ def _validate_assembly_requirements(self):
+ """Validate SBOL assembly requirements"""
+ if not self.assembly_combinations:
+ raise ValueError("No valid SBOL assemblies found in input.")
+
+ # Calculate reagent positions: water(1) + ligase(1) + buffer(1) + unique enzymes
+ reagent_positions = 3 + len(self.restriction_enzyme_set)
+ max_parts = 24 - reagent_positions
+
+ if len(self.combined_set) > max_parts:
+ raise ValueError(
+ f'This protocol only supports assemblies with up to {max_parts} parts. '
+ f'Number of parts in the protocol is {len(self.combined_set)}. '
+ f'Parts: {self.combined_set}. '
+ f'Reagent positions used: {reagent_positions}/24'
+ )
+
+ # Validate thermocycler capacity
+ available_wells = 96 - self.thermocycler_starting_well
+ wells_needed = len(self.assembly_combinations) * self.replicates
+
+ if wells_needed > available_wells:
+ raise ValueError(
+ f'This protocol only supports assemblies with up to {available_wells} '
+ f'combinations. Number of assemblies in the protocol are {wells_needed}.'
+ )
+
+ # Validate reaction volumes for each assembly
+ for assembly_combo in self.assembly_combinations:
+ num_parts = len(assembly_combo['parts'])
+ self._validate_reaction_volumes(num_parts)
+
+
+class ManualAssembly(BaseAssembly):
+ """
+ Manual Golden Gate assembly protocol generator from SBOL-style JSON input.
+ Produces structured reaction records and renders human-readable Markdown.
+ """
+
+ def __init__(self,
+ assembly_data: Optional[Dict] = None,
+ json_params: Optional[str] = None,
+ assemblies: Optional[List[Dict]] = None,
+ thermocycling_profile: Optional[List[Dict[str, float]]] = None,
+ thermocycling_cycles: int = 75,
+ denaturation_profile: Optional[List[Dict[str, float]]] = None,
+ hold_temperature: float = 4,
+ *args, **kwargs):
+ if assembly_data is not None:
+ if isinstance(assembly_data, dict) and 'assemblies' in assembly_data:
+ assemblies = assembly_data['assemblies']
+ else:
+ assemblies = assembly_data
+
+ if assemblies is None:
+ raise ValueError("Must provide assemblies either via assembly_data or assemblies parameter")
+ if not isinstance(assemblies, list) or not assemblies:
+ raise ValueError("assemblies must be a non-empty list of SBOL-style assembly dictionaries")
+
+ super().__init__(json_params=json_params, *args, **kwargs)
+ self.assemblies = assemblies
+ self.reaction_records: List[ManualReactionRecord] = []
+ self.thermocycling_profile = thermocycling_profile or [
+ {'temperature': 42, 'hold_time_minutes': 2},
+ {'temperature': 16, 'hold_time_minutes': 5}
+ ]
+ self.thermocycling_cycles = thermocycling_cycles
+ self.denaturation_profile = denaturation_profile or [
{'temperature': 60, 'hold_time_minutes': 10},
- {'temperature': 80, 'hold_time_minutes': 10}]
- thermocycler_mod.execute_profile(steps=denaturation, repetitions=1, block_max_volume=30)
- thermocycler_mod.set_block_temperature(4)
- #END
+ {'temperature': 80, 'hold_time_minutes': 10}
+ ]
+ self.hold_temperature = hold_temperature
- #output
- print('Parts and reagents in temp_module')
- print(self.dict_of_parts_in_temp_mod_position)
- print('Assembled parts in thermocycler_module')
- print(self.dict_of_parts_in_thermocycler)
+ def process_assemblies(self):
+ """Parse and validate input assemblies, then build reaction records."""
+ self._validate_input_assemblies()
+ self.reaction_records = self._build_reaction_records()
+ return self.reaction_records
+
+ def _load_parts_and_enzymes(self, protocol, alum_block) -> int:
+ raise NotImplementedError("ManualAssembly does not load reagents onto OT-2 modules.")
+
+ def _process_assembly_combinations(self, protocol, pipette, thermo_plate, alum_block,
+ dd_h2o, t4_dna_ligase_buffer, t4_dna_ligase,
+ volume_reagents, thermocycler_well_counter) -> int:
+ raise NotImplementedError("ManualAssembly does not generate OT-2 liquid handling commands.")
+
+ def _calculate_total_tips_needed(self, number_of_constant_reagents: int = 0) -> int:
+ return 0
+
+ def _validate_input_assemblies(self):
+ required_keys = {'Product', 'Backbone', 'PartsList', 'Restriction Enzyme'}
+
+ for idx, assembly in enumerate(self.assemblies, start=1):
+ if not isinstance(assembly, dict):
+ raise ValueError(f"Assembly #{idx} is not a dictionary.")
+
+ missing = required_keys - set(assembly.keys())
+ if missing:
+ raise ValueError(
+ f"Assembly #{idx} is missing required keys: {sorted(missing)}. "
+ f"Expected keys: {sorted(required_keys)}"
+ )
+
+ if not isinstance(assembly['PartsList'], list) or not assembly['PartsList']:
+ raise ValueError(f"Assembly #{idx} has an invalid 'PartsList'. Expected a non-empty list.")
+
+ def _extract_name_from_uri(self, uri: str) -> str:
+ """Extract display name from URI; falls back safely to raw input if needed."""
+ if not isinstance(uri, str):
+ return str(uri)
+
+ segments = [segment for segment in uri.rstrip('/').split('/') if segment]
+ if not segments:
+ return uri
+
+ if len(segments) >= 2 and segments[-1].isdigit():
+ return segments[-2]
+ return segments[-1]
+
+ def _calculate_reaction_volumes(self, number_of_dna_components: int) -> Dict[str, float]:
+ total_dna_volume = number_of_dna_components * self.volume_part
+ fixed_reagent_volume = (
+ self.volume_restriction_enzyme +
+ self.volume_t4_dna_ligase +
+ self.volume_t4_dna_ligase_buffer
+ )
+ water_volume = self.volume_total_reaction - fixed_reagent_volume - total_dna_volume
+
+ if water_volume < 0:
+ raise ValueError(
+ f"Reaction volume error: Cannot fit {number_of_dna_components} DNA components into "
+ f"{self.volume_total_reaction}µL reaction.\n"
+ f" Required volumes:\n"
+ f" - DNA ({number_of_dna_components} × {self.volume_part}µL): {total_dna_volume}µL\n"
+ f" - Restriction enzyme: {self.volume_restriction_enzyme}µL\n"
+ f" - T4 DNA ligase: {self.volume_t4_dna_ligase}µL\n"
+ f" - T4 DNA ligase buffer: {self.volume_t4_dna_ligase_buffer}µL\n"
+ f" Total required: {total_dna_volume + fixed_reagent_volume}µL"
+ )
+
+ return {
+ 'total_dna_volume': total_dna_volume,
+ 'fixed_reagent_volume': fixed_reagent_volume,
+ 'water_volume': water_volume
+ }
+
+ def _build_reaction_records(self) -> List[ManualReactionRecord]:
+ records: List[ManualReactionRecord] = []
+ for assembly in self.assemblies:
+ product_uri = assembly["Product"]
+ backbone_uri = assembly["Backbone"]
+ part_uris = assembly["PartsList"]
+ enzyme_uri = assembly["Restriction Enzyme"]
+
+ product_name = self._extract_name_from_uri(product_uri)
+ backbone_name = self._extract_name_from_uri(backbone_uri)
+ part_names = [self._extract_name_from_uri(uri) for uri in part_uris]
+ enzyme_name = self._extract_name_from_uri(enzyme_uri)
+
+ number_of_dna_components = 1 + len(part_uris)
+ volume_data = self._calculate_reaction_volumes(number_of_dna_components)
+
+ reagent_additions = [
+ {'name': 'nuclease-free water', 'volume_uL': self._fmt_volume(volume_data['water_volume'])},
+ {'name': '10X T4 DNA Ligase Buffer', 'volume_uL': self._fmt_volume(self.volume_t4_dna_ligase_buffer)},
+ {'name': 'T4 DNA Ligase', 'volume_uL': self._fmt_volume(self.volume_t4_dna_ligase)},
+ {'name': enzyme_name, 'volume_uL': self._fmt_volume(self.volume_restriction_enzyme)},
+ {'name': f"backbone `{backbone_name}`", 'volume_uL': self._fmt_volume(self.volume_part)},
+ ]
+
+ for part_uri, part_name in zip(part_uris, part_names):
+ reagent_additions.append({
+ 'name': f"part `{part_name}`",
+ 'volume_uL': self._fmt_volume(self.volume_part),
+ 'uri': part_uri
+ })
+
+ record = ManualReactionRecord(
+ product_uri=product_uri,
+ product_name=product_name,
+ backbone_uri=backbone_uri,
+ backbone_name=backbone_name,
+ part_uris=part_uris,
+ part_names=part_names,
+ restriction_enzyme_uri=enzyme_uri,
+ restriction_enzyme_name=enzyme_name,
+ number_of_dna_components=number_of_dna_components,
+ total_dna_volume=volume_data['total_dna_volume'],
+ fixed_reagent_volume=volume_data['fixed_reagent_volume'],
+ water_volume=volume_data['water_volume'],
+ total_reaction_volume=self.volume_total_reaction,
+ reagent_additions=reagent_additions
+ )
+ records.append(record)
+
+ return records
+
+ def _fmt_volume(self, value: float) -> str:
+ return f"{int(value)}" if float(value).is_integer() else f"{value:.2f}"
+
+ def _render_thermocycling_section(self) -> str:
+ """High-level Golden Gate thermocycling instructions."""
+ profile_descriptions = [
+ f"{step['temperature']}°C for {step['hold_time_minutes']} minutes"
+ for step in self.thermocycling_profile
+ ]
+ denaturation_descriptions = [
+ f"{step['temperature']}°C for {step['hold_time_minutes']} minutes"
+ for step in self.denaturation_profile
+ ]
+
+ profile_sentence = " and ".join(profile_descriptions)
+ denaturation_sentence = " followed by ".join(denaturation_descriptions)
+
+ return (
+ "## Thermocycling\n"
+ f"- Use a cycling profile that holds {profile_sentence}; "
+ f"repeat this profile {self.thermocycling_cycles} times.\n"
+ f"- Denature/inactivate proteins by holding {denaturation_sentence}.\n"
+ f"- Hold at {self.hold_temperature}°C until samples are collected.\n"
+ )
+
+ def render_markdown(self) -> str:
+ """Render a complete manual protocol in Markdown format."""
+ if not self.reaction_records:
+ self.process_assemblies()
+
+ lines = [
+ "# Golden Gate Manual Assembly Protocol",
+ "",
+ "## Overview",
+ "Golden Gate assembly is a one-pot DNA cloning method that uses a Type IIS restriction enzyme, "
+ "such as BsaI, together with DNA ligase to assemble multiple DNA fragments in a predefined order.",
+ "Because Type IIS enzymes cut outside their recognition sites, they generate custom overhangs that "
+ "direct fragment assembly and allow the recognition sites to be removed from the final construct.",
+ "In this protocol, plasmids containing DNA parts and a destination backbone are combined with the "
+ "restriction enzyme and ligase in a single tube, then cycled in a thermocycler between digestion and "
+ "ligation temperatures. Repetition of these cycles enriches for the correctly assembled composite "
+ "plasmid, after which the enzymes are heat-inactivated and the reaction is held at 4 °C until collection.",
+ "",
+ "## Inputs",
+ f"- Number of target products: {len(self.reaction_records)}",
+ ]
+ for record in self.reaction_records:
+ lines.append(f"- `{record.product_name}` ({record.product_uri})")
+
+ lines.extend([
+ "",
+ "## Reaction summary",
+ "| Product | Backbone | Parts | Restriction Enzyme | # DNA Components | DNA each (µL) | Enzyme (µL) | Ligase (µL) | Buffer (µL) | Water (µL) | Total (µL) |",
+ "| --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
+ ])
+
+ for record in self.reaction_records:
+ lines.append(
+ f"| {record.product_name} | {record.backbone_name} | {', '.join(record.part_names)} | "
+ f"{record.restriction_enzyme_name} | {record.number_of_dna_components} | "
+ f"{self._fmt_volume(self.volume_part)} | {self._fmt_volume(self.volume_restriction_enzyme)} | "
+ f"{self._fmt_volume(self.volume_t4_dna_ligase)} | {self._fmt_volume(self.volume_t4_dna_ligase_buffer)} | "
+ f"{self._fmt_volume(record.water_volume)} | {self._fmt_volume(record.total_reaction_volume)} |"
+ )
+
+ lines.append("")
+ lines.append("## Per-reaction instructions")
+ lines.append("")
+
+ for record in self.reaction_records:
+ lines.append(f"### Product: {record.product_name}")
+ lines.append(f"URI: {record.product_uri}")
+ lines.append("")
+ lines.append(f"1. Label one tube as `{record.product_name}`.")
+ lines.append(f"2. Add {self._fmt_volume(record.water_volume)} µL nuclease-free water.")
+ lines.append(f"3. Add {self._fmt_volume(self.volume_t4_dna_ligase_buffer)} µL 10X T4 DNA Ligase Buffer.")
+ lines.append(f"4. Add {self._fmt_volume(self.volume_t4_dna_ligase)} µL T4 DNA Ligase.")
+ lines.append(
+ f"5. Add {self._fmt_volume(self.volume_restriction_enzyme)} µL "
+ f"{record.restriction_enzyme_name} (URI: {record.restriction_enzyme_uri})."
+ )
+ lines.append(
+ f"6. Add {self._fmt_volume(self.volume_part)} µL backbone "
+ f"`{record.backbone_name}` (URI: {record.backbone_uri})."
+ )
+
+ step_counter = 7
+ for part_name, part_uri in zip(record.part_names, record.part_uris):
+ lines.append(
+ f"{step_counter}. Add {self._fmt_volume(self.volume_part)} µL part "
+ f"`{part_name}` (URI: {part_uri})."
+ )
+ step_counter += 1
+
+ lines.append(f"{step_counter}. Mix gently by pipetting. Do not vortex unless explicitly intended.")
+ lines.append(f"{step_counter + 1}. Briefly spin down if appropriate.")
+ lines.append("")
+
+ lines.append(self._render_thermocycling_section().rstrip())
+ lines.extend([
+ "",
+ "## Notes",
+ "- Thermocylcer iterations can be increased to improve the reaction efficiency.",
+ "- Assumes all DNA parts are available at suitable concentrations and added at equal molarity. Suggested molarities are 20 fmol/µL for parts and 10 fmol/µL for backbones.",
+ "- Store the assembly product at 4 °C for better stability until used for downstream applications.",
+ "- Validate assembled plasmids by restriction digest and gel electrophoresis, Sanger sequencing, or whole-plasmid sequencing."
+ ])
+
+ return "\n".join(lines) + "\n"
+
+ def write_markdown(self, output_path: str):
+ """Write rendered Markdown protocol to disk."""
+ markdown = self.render_markdown()
+ with open(output_path, "w", encoding="utf-8") as file_handle:
+ file_handle.write(markdown)
+
+
+class LoopAssembly:
+ """
+ Factory class that auto-detects input format and returns appropriate subclass.
+ Supports both manual/combinatorial and SBOL format assemblies.
+ """
+
+ def __new__(cls, assemblies: List[Dict], *args, **kwargs):
+ """Factory method that detects format and returns appropriate instance"""
+ if not assemblies:
+ raise ValueError("No assemblies provided")
+
+ # Detect format based on first assembly
+ first_assembly = assemblies[0]
+
+ if cls._is_sbol_format(first_assembly):
+ print("Detected SBOL format - using SBOLLoopAssembly")
+ return SBOLLoopAssembly(assemblies, *args, **kwargs)
+ elif cls._is_manual_format(first_assembly):
+ print("Detected Manual format - using ManualLoopAssembly")
+ return ManualLoopAssembly(assemblies, *args, **kwargs)
+ else:
+ raise ValueError(
+ f"Unknown assembly format. Assembly must contain either:\n"
+ f"- SBOL format: 'Product', 'Backbone', 'PartsList', 'Restriction Enzyme'\n"
+ f"- Manual format: 'receiver' and role keys like 'promoter', 'rbs', etc.\n"
+ f"Found keys: {list(first_assembly.keys())}"
+ )
+
+ @staticmethod
+ def _is_sbol_format(assembly: Dict) -> bool:
+ """Check if assembly matches SBOL format"""
+ sbol_keys = {'Product', 'Backbone', 'PartsList', 'Restriction Enzyme'}
+ assembly_keys = set(assembly.keys())
+
+ # Check for SBOL-specific keys
+ has_sbol_keys = sbol_keys.issubset(assembly_keys)
+
+ # Check for URI patterns (https://)
+ has_uri_patterns = any(
+ isinstance(v, str) and v.startswith('https://')
+ for v in assembly.values()
+ ) or any(
+ isinstance(v, list) and any(
+ isinstance(item, str) and item.startswith('https://')
+ for item in v
+ )
+ for v in assembly.values()
+ )
+
+ return has_sbol_keys and has_uri_patterns
+
+ @staticmethod
+ def _is_manual_format(assembly: Dict) -> bool:
+ """Check if assembly matches manual/combinatorial format"""
+ # Must have 'receiver' key
+ if 'receiver' not in assembly:
+ return False
+
+ # Should have role-based keys (not SBOL keys)
+ sbol_keys = {'Product', 'Backbone', 'PartsList', 'Restriction Enzyme'}
+ assembly_keys = set(assembly.keys())
+
+ # Should not have SBOL keys
+ has_sbol_keys = bool(sbol_keys.intersection(assembly_keys))
+
+ # Should have role keys beyond 'receiver'
+ role_keys = assembly_keys - {'receiver'}
+ has_role_keys = len(role_keys) > 0
+
+ return not has_sbol_keys and has_role_keys
+
+# Default assemblies for testing
+DEFAULT_DOMESTICATION_ASSEMBLY = [
+ {"parts": ["part1", "part2"], "backbone": "acceptor", "restriction_enzyme": "BsaI"},
+]
-# assembly
-assembly_Odd_1 = {"promoter":["GVP0010", "GVP0011", "GVP0014"], "rbs":["B0033","B0034"], "cds":"sfGFP", "terminator":"B0015", "receiver":"Odd_1"}
-#assembly_Even_2 = {"c4_receptor":"GD0001", "c4_buff_gfp":"GD0002", "spacer1":"20ins1", "spacer2":"Even_2", "receiver":"Even_2"}
-assemblies = [assembly_Odd_1]
+DEFAULT_MANUAL_ASSEMBLIES = [
+ {"promoter": ["GVP0008"], "rbs": "B0034",
+ "cds": "sfGFP", "terminator": "B0015", "receiver": "Odd_1"}
+]
-assembly_Odd_1 = {"promoter":["j23101", "j23100"], "rbs":"B0034", "cds":"GFP", "terminator":"B0015", "receiver":"Odd_1"}
-assembly_Even_2 = {"c4_receptor":"GD0001", "c4_buff_gfp":"GD0002", "spacer1":"20ins1", "spacer2":"Even_2", "receiver":"Even_2"}
-assemblies = [assembly_Odd_1, assembly_Even_2]
\ No newline at end of file
+DEFAULT_SBOL_ASSEMBLIES = [
+ {
+ "Product": "https://SBOL2Build.org/composite_1/1",
+ "Backbone": "https://sbolcanvas.org/Cir_qxow/1",
+ "PartsList": [
+ "https://sbolcanvas.org/GFP/1",
+ "https://sbolcanvas.org/B0015/1",
+ "https://sbolcanvas.org/J23101/1",
+ "https://sbolcanvas.org/B0034/1"
+ ],
+ "Restriction Enzyme": "https://SBOL2Build.org/BsaI/1"
+ }
+]
diff --git a/src/pudu/calibration.py b/src/pudu/calibration.py
index 38a73bd..f3834cf 100644
--- a/src/pudu/calibration.py
+++ b/src/pudu/calibration.py
@@ -1,60 +1,33 @@
+from abc import ABC, abstractmethod
from opentrons import protocol_api
-from .utils import plate_96_wells, temp_wells, liquid_transfer
+from typing import List, Dict, Optional
import xlsxwriter
+from pudu.utils import SmartPipette, Camera, colors
-class Calibration():
+class BaseCalibration(ABC):
"""
- Creates a ptopocol to calibrate GFP using fluorescein (MEFL) and OD600 using nanoparticles.
+ Abstract base class for calibration protocols.
+ Contains shared hardware setup, liquid handling, and serial dilution functionality.
Refer to: https://old.igem.org/wiki/images/a/a4/InterLab_2022_-_Calibration_Protocol_v2.pdf
-
- ...
-
- Attributes
- ----------
- aspiration_rate: float
- Aspiration rate in micro liters per second (uL/s). By default 0.5 uL/s.
- dispense_rate: float
- Dispense rate in micro liters per second (uL/s). By default 1 uL/s.
- tiprack_labware: str
- Labware to use as tiprack. By default opentrons_96_tiprack_300ul.
- tiprack_position: int
- Deck position for tiprack. By default 9.
- pipette: str
- Pipette to use, can be a p300 and p1000. By default p300_single_gen2.
- pipette_position: str
- Deck position for pipette. By default left.
- calibration_plate_labware: str
- Labware to use as calibration plate. By default corning_96_wellplate_360ul_flat.
- calibration_plate_position: int
- Deck position for calibration plate. By default 7.
- use_temperature_module: bool
- Whether to use temperature module or not. By default True.
- tube_rack_labware: str
- Labware to use as tube rack. By default opentrons_24_aluminumblock_nest_1.5ml_snapcap.
- tube_rack_position: int
- Deck position for tube rack. By default 1.
- use_falcon_tubes: bool
- Whether to use falcon tubes or not. By default False.
- falcon_tube_rack_labware: str
- Labware to use as falcon tube rack. By default opentrons_6_tuberack_falcon_50ml_conical.
- falcon_tube_rack_position: int
- Deck position for falcon tube rack. By default 2.
"""
- def __init__(self,
- aspiration_rate:float=0.5,
- dispense_rate:float=1,
- tiprack_labware:str='opentrons_96_tiprack_300ul',
- tiprack_position:int=9,
- pipette:str='p300_single_gen2',
- pipette_position:str='left',
- calibration_plate_labware:str='corning_96_wellplate_360ul_flat',
- calibration_plate_position:int=7,
- tube_rack_labware:str='opentrons_24_aluminumblock_nest_1.5ml_snapcap',
- tube_rack_position:int=1,
- use_falcon_tubes:bool=False,
- falcon_tube_rack_labware:str='opentrons_6_tuberack_falcon_50ml_conical',
- falcon_tube_rack_position:int=2,
- ):
+
+ def __init__(self,
+ aspiration_rate: float = 0.5,
+ dispense_rate: float = 1.0,
+ tiprack_labware: str = 'opentrons_96_tiprack_300ul',
+ tiprack_position: str = '9',
+ pipette: str = 'p300_single_gen2',
+ pipette_position: str = 'left',
+ calibration_plate_labware: str = 'corning_96_wellplate_360ul_flat',
+ calibration_plate_position: str = '7',
+ tube_rack_labware: str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ tube_rack_position: str = '1',
+ use_falcon_tubes: bool = False,
+ falcon_tube_rack_labware: str = 'opentrons_6_tuberack_falcon_50ml_conical',
+ falcon_tube_rack_position: str = '2',
+ take_picture: bool = False,
+ take_video: bool = False,
+ water_testing: bool = False):
self.aspiration_rate = aspiration_rate
self.dispense_rate = dispense_rate
@@ -69,327 +42,407 @@ def __init__(self,
self.use_falcon_tubes = use_falcon_tubes
self.falcon_tube_rack_labware = falcon_tube_rack_labware
self.falcon_tube_rack_position = falcon_tube_rack_position
+ self.take_picture = take_picture
+ self.take_video = take_video
+ self.water_testing = water_testing
+ # Shared tracking
+ self.calibrant_positions = {}
+ self.buffer_positions = {}
+ self.camera = Camera()
+ self.smart_pipette = None
-class iGEM_gfp_od(Calibration):
- """
- Creates a ptopocol to calibrate GFP using fluorescein (MEFL) and OD600 using nanoparticles.
- Refer to: https://old.igem.org/wiki/images/a/a4/InterLab_2022_-_Calibration_Protocol_v2.pdf
+ @abstractmethod
+ def _get_calibrant_layout(self) -> Dict:
+ """Return the specific calibrant layout for this protocol"""
+ pass
- Attributes
- ----------
- aspiration_rate: float
- Aspiration rate in micro liters per second (uL/s). By default 0.5 uL/s.
- dispense_rate: float
- Dispense rate in micro liters per second (uL/s). By default 1 uL/s.
- tiprack_labware: str
- Labware to use as tiprack. By default opentrons_96_tiprack_300ul.
- tiprack_position: int
- Deck position for tiprack. By default 9.
- pipette: str
- Pipette to use, can be a p300 and p1000. By default p300_single_gen2.
- pipette_position: str
- Deck position for pipette. By default left.
- calibration_plate_labware: str
- Labware to use as calibration plate. By default corning_96_wellplate_360ul_flat.
- calibration_plate_position: int
- Deck position for calibration plate. By default 7.
- use_temperature_module: bool
- Whether to use temperature module or not. By default True.
- tube_rack_labware: str
- Labware to use as tube rack. By default opentrons_24_aluminumblock_nest_1.5ml_snapcap.
- tube_rack_position: int
- Deck position for tube rack. By default 1.
- use_falcon_tubes: bool
- Whether to use falcon tubes or not. By default False.
- falcon_tube_rack_labware: str
- Labware to use as falcon tube rack. By default opentrons_6_tuberack_falcon_50ml_conical.
- falcon_tube_rack_position: int
- Deck position for falcon tube rack. By default 2.
- """
- def __init__(self,
- *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self.sbol_output = []
-
- metadata = {
- 'protocolName': 'iGEM GFP OD600 calibration',
- 'author': 'Gonzalo Vidal ',
- 'description': 'Protocol to perform serial dilutions of fluorescein and nanoparticles for calibration',
- 'apiLevel': '2.13'}
-
- def run(self,protocol):
- #Set deck
- #Labware
- tiprack = protocol.load_labware(self.tiprack_labware, f'{self.tiprack_position}')
+ @abstractmethod
+ def _load_calibrants(self, protocol, tube_rack) -> None:
+ """Load calibrants specific to this protocol"""
+ pass
+
+ @abstractmethod
+ def _dispense_initial_calibrants(self, protocol, pipette, plate) -> None:
+ """Dispense initial calibrants to starting positions"""
+ pass
+
+ @abstractmethod
+ def _perform_serial_dilutions(self, protocol, pipette, plate) -> None:
+ """Perform serial dilutions specific to this protocol"""
+ pass
+
+ def _define_and_load_liquid(self, protocol, well, name: str, description: str = None,
+ volume: float = 1000, color_index: int = None):
+ """Define liquid and load it into specified well"""
+ if description is None:
+ description = name
+ if color_index is None:
+ color_index = len(self.calibrant_positions) % len(colors)
+
+ liquid = protocol.define_liquid(
+ name=name,
+ description=description,
+ display_color=colors[color_index]
+ )
+ well.load_liquid(liquid, volume=volume)
+ protocol.comment(f"Loaded {name} at position {well.well_name}")
+ return well
+
+ def _setup_hardware(self, protocol):
+ """Setup shared hardware components"""
+ tiprack = protocol.load_labware(self.tiprack_labware, self.tiprack_position)
pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
+ self.smart_pipette = SmartPipette(pipette, protocol)
plate = protocol.load_labware(self.calibration_plate_labware, self.calibration_plate_position)
tube_rack = protocol.load_labware(self.tube_rack_labware, self.tube_rack_position)
+
+ falcon_tube_rack = None
if self.use_falcon_tubes:
- falcon_tube_rack = protocol.load_labware(self.falcon_tube_rack_labware, self.falcon_tube_rack_position)
- #Protocol
- #add calibrants
- fluorescein_1x = tube_rack['A1']
- microspheres_1x = tube_rack['A2']
- #add dilution buffers in tube rack
- pbs_1 = tube_rack['A3']
- pbs_2 = tube_rack['A4']
- water_1 = tube_rack['A5']
- water_2 = tube_rack['A6']
- #if using falcon, remap pbs and water
- if self.use_falcon_tubes:
- #add dilution buffers in falcon tube rack
- pbs_falcon = falcon_tube_rack['A1']
- water_falcon = falcon_tube_rack['A2']
- #remap pbs and water
- pbs_1 = pbs_falcon
- pbs_2 = pbs_falcon
- water_1 = water_falcon
- water_2 = water_falcon
- #dispense PBS
- pipette.pick_up_tip()
- for well in plate_96_wells[1:12]:
- liquid_transfer(pipette, 100, pbs_1, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[13:24]:
- liquid_transfer(pipette, 100, pbs_2, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #dispense water
- pipette.pick_up_tip()
- for well in plate_96_wells[25:36]:
- liquid_transfer(pipette, 100, water_1, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[37:48]:
- liquid_transfer(pipette, 100, water_2, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #dispense fluorescein
- liquid_transfer(pipette, 200, fluorescein_1x, plate['A1'], self.aspiration_rate, self.dispense_rate, mix_before=200, mix_reps=4)
- liquid_transfer(pipette, 200, fluorescein_1x, plate['B1'], self.aspiration_rate, self.dispense_rate, mix_before=200, mix_reps=4)
- #dispense nanoparticles
- liquid_transfer(pipette, 200, microspheres_1x, plate['C1'], self.aspiration_rate, self.dispense_rate, mix_before=200, mix_reps=4)
- liquid_transfer(pipette, 200, microspheres_1x, plate['D1'], self.aspiration_rate, self.dispense_rate, mix_before=200, mix_reps=4)
- #fluorescein serial dilutions
- pipette.pick_up_tip()
- for i in range(0,11):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=200, mix_reps=4, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- pipette.pick_up_tip()
- for i in range(12,23):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=200, mix_reps=4, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #nanoparticles serial dilutions
+ falcon_tube_rack = protocol.load_labware(
+ self.falcon_tube_rack_labware,
+ self.falcon_tube_rack_position
+ )
+
+ return pipette, plate, tube_rack, falcon_tube_rack
+
+ def _load_dilution_buffers(self, protocol, tube_rack, falcon_tube_rack) -> Dict:
+ """Load PBS and water buffers with falcon tube remapping if needed"""
+ buffers = {}
+
+ if self.use_falcon_tubes and falcon_tube_rack:
+ # Use falcon tubes with liquid definition
+ pbs_falcon = self._define_and_load_liquid(
+ protocol, falcon_tube_rack['A1'], "PBS Buffer",
+ "Phosphate Buffered Saline for dilutions", volume=15000, color_index=0
+ )
+ water_falcon = self._define_and_load_liquid(
+ protocol, falcon_tube_rack['A2'], "Deionized Water",
+ "Deionized Water for dilutions", volume=15000, color_index=1
+ )
+ buffers['pbs_sources'] = [pbs_falcon, pbs_falcon]
+ buffers['water_sources'] = [water_falcon, water_falcon]
+ else:
+ # Use individual tubes with liquid definition
+ pbs_1 = self._define_and_load_liquid(
+ protocol, tube_rack['A3'], "PBS Buffer 1", volume=1000, color_index=0
+ )
+ pbs_2 = self._define_and_load_liquid(
+ protocol, tube_rack['A4'], "PBS Buffer 2", volume=1000, color_index=0
+ )
+ water_1 = self._define_and_load_liquid(
+ protocol, tube_rack['A5'], "Deionized Water 1", volume=1000, color_index=1
+ )
+ water_2 = self._define_and_load_liquid(
+ protocol, tube_rack['A6'], "Deionized Water 2", volume=1000, color_index=1
+ )
+ buffers['pbs_sources'] = [pbs_1, pbs_2]
+ buffers['water_sources'] = [water_1, water_2]
+
+ self.buffer_positions = buffers
+ return buffers
+
+ def _dispense_dilution_buffers(self, protocol, pipette, plate, buffers, wells_layout):
+ """Dispense PBS and water to designated wells"""
+ # Dispense PBS
pipette.pick_up_tip()
- for i in range(24,35):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=200, mix_reps=4, new_tip=False, drop_tip=False)
+ for wells_range, source_idx in wells_layout['pbs']:
+ target_wells = plate.wells()[wells_range[0]:wells_range[1]]
+ source = buffers['pbs_sources'][source_idx]
+ use_conical = self.use_falcon_tubes # Enable conical tube handling for falcon tubes
+
+ for well in target_wells:
+ self.smart_pipette.liquid_transfer(
+ volume=100, source=source, destination=well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ new_tip=False, drop_tip=False, use=use_conical
+ )
pipette.drop_tip()
+
+ # Dispense water
pipette.pick_up_tip()
- for i in range(36,47):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=200, mix_reps=4, new_tip=False, drop_tip=False)
+ for wells_range, source_idx in wells_layout['water']:
+ target_wells = plate.wells()[wells_range[0]:wells_range[1]]
+ source = buffers['water_sources'][source_idx]
+ use_conical = self.use_falcon_tubes # Enable conical tube handling for falcon tubes
+
+ for well in target_wells:
+ self.smart_pipette.liquid_transfer(
+ volume=100, source=source, destination=well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ new_tip=False, drop_tip=False, use=use_conical
+ )
pipette.drop_tip()
- #END
-class iGEM_rgb_od(Calibration):
+ def run(self, protocol: protocol_api.ProtocolContext):
+ """Main protocol execution using template method pattern"""
+ # Setup hardware
+ pipette, plate, tube_rack, falcon_tube_rack = self._setup_hardware(protocol)
+
+ # Load calibrants (protocol-specific)
+ self._load_calibrants(protocol, tube_rack)
+
+ # Load dilution buffers
+ buffers = self._load_dilution_buffers(protocol, tube_rack, falcon_tube_rack)
+
+ # Get layout for this specific protocol
+ layout = self._get_calibrant_layout()
+
+ # Media capture start
+ if self.take_picture:
+ self.camera.capture_picture(protocol, when="start")
+ if self.take_video:
+ self.camera.start_video(protocol)
+
+ # Dispense dilution buffers
+ self._dispense_dilution_buffers(protocol, pipette, plate, buffers, layout)
+
+ # Dispense initial calibrants (protocol-specific)
+ self._dispense_initial_calibrants(protocol, pipette, plate)
+
+ # Perform serial dilutions (protocol-specific)
+ self._perform_serial_dilutions(protocol, pipette, plate)
+
+ # Media capture end
+ if self.take_video:
+ self.camera.stop_video(protocol)
+ if self.take_picture:
+ self.camera.capture_picture(protocol, when="end")
+
+class GFPODCalibration(BaseCalibration):
"""
- Creates a ptopocol to calibrate GFP using fluorescein (MEFL), sulforhodamine 101, cascade blue and OD600 using nanoparticles.
- Refer to: https://old.igem.org/wiki/images/a/a4/InterLab_2022_-_Calibration_Protocol_v2.pdf
+ GFP and OD600 calibration using fluorescein and nanoparticles.
+ Based on iGEM 2022 calibration protocol.
+ """
+
+ def _get_calibrant_layout(self) -> Dict:
+ """Layout for GFP/OD600 calibration (2 calibrants, 2 replicates each)"""
+ return {
+ 'pbs': [((1, 12), 0), ((13, 24), 1)], # (well_range, source_index)
+ 'water': [((25, 36), 0), ((37, 48), 1)],
+ 'calibrants': {
+ 'fluorescein': ['A1', 'B1'],
+ 'microspheres': ['C1', 'D1']
+ },
+ 'dilution_series': [
+ (0, 11), # Row A fluorescein
+ (12, 23), # Row B fluorescein
+ (24, 35), # Row C microspheres
+ (36, 47) # Row D microspheres
+ ]
+ }
+
+ def _load_calibrants(self, protocol, tube_rack) -> None:
+ """Load fluorescein and microspheres"""
+ fluorescein_well = self._define_and_load_liquid(
+ protocol, tube_rack['A1'], "Fluorescein 1x",
+ "Fluorescein calibration standard", volume=1000, color_index=2
+ )
+ microspheres_well = self._define_and_load_liquid(
+ protocol, tube_rack['A2'], "Microspheres 1x",
+ "Silica nanoparticles for OD600 calibration", volume=1000, color_index=3
+ )
+
+ self.calibrant_positions['fluorescein_1x'] = fluorescein_well
+ self.calibrant_positions['microspheres_1x'] = microspheres_well
+
+ def _dispense_initial_calibrants(self, protocol, pipette, plate) -> None:
+ """Dispense fluorescein and microspheres to starting wells"""
+ # Fluorescein to A1, B1
+ for well_name in ['A1', 'B1']:
+ self.smart_pipette.liquid_transfer(
+ volume=200, source=self.calibrant_positions['fluorescein_1x'],
+ destination=plate[well_name], asp_rate=self.aspiration_rate,
+ disp_rate=self.dispense_rate, mix_before=200, mix_reps=4
+ )
- Attributes
- ----------
- aspiration_rate: float
- Aspiration rate in micro liters per second (uL/s). By default 0.5 uL/s.
- dispense_rate: float
- Dispense rate in micro liters per second (uL/s). By default 1 uL/s.
- tiprack_labware: str
- Labware to use as tiprack. By default opentrons_96_tiprack_300ul.
- tiprack_position: int
- Deck position for tiprack. By default 9.
- pipette: str
- Pipette to use, can be a p300 and p1000. By default p300_single_gen2.
- pipette_position: str
- Deck position for pipette. By default left.
- calibration_plate_labware: str
- Labware to use as calibration plate. By default corning_96_wellplate_360ul_flat.
- calibration_plate_position: int
- Deck position for calibration plate. By default 7.
- tube_rack_labware: str
- Labware to use as tube rack. By default opentrons_24_aluminumblock_nest_1.5ml_snapcap.
- tube_rack_position: int
- Deck position for tube rack. By default 1.
- use_falcon_tubes: bool
- Whether to use falcon tubes or not. By default False.
- falcon_tube_rack_labware: str
- Labware to use as falcon tube rack. By default opentrons_6_tuberack_falcon_50ml_conical.
- falcon_tube_rack_position: int
- Deck position for falcon tube rack. By default 2.
+ # Microspheres to C1, D1
+ for well_name in ['C1', 'D1']:
+ self.smart_pipette.liquid_transfer(
+ volume=200, source=self.calibrant_positions['microspheres_1x'],
+ destination=plate[well_name], asp_rate=self.aspiration_rate,
+ disp_rate=self.dispense_rate, mix_before=200, mix_reps=4
+ )
+
+ def _perform_serial_dilutions(self, protocol, pipette, plate) -> None:
+ """Perform 1:2 serial dilutions for fluorescein and microspheres"""
+ layout = self._get_calibrant_layout()
+
+ for start_idx, end_idx in layout['dilution_series']:
+ pipette.pick_up_tip()
+ for i in range(start_idx, end_idx):
+ source_well = plate.wells()[i]
+ dest_well = plate.wells()[i + 1]
+ self.smart_pipette.liquid_transfer(
+ volume=100, source=source_well, destination=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=200, mix_reps=4, new_tip=False, drop_tip=False
+ )
+ pipette.drop_tip()
+
+
+class RGBODCalibration(BaseCalibration):
+ """
+ RGB and OD600 calibration using fluorescein, sulforhodamine 101, cascade blue, and nanoparticles.
+ Extended iGEM calibration protocol.
"""
- def __init__(self,
- *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- #Does a SBOL component of this make sense?
- metadata = {
- 'protocolName': 'iGEM RGB OD600 calibration',
- 'author': 'Gonzalo Vidal ',
- 'description': 'Protocol to perform serial dilutions of fluorescein, sulforhodamine 101, cascade blue and nanoparticles for calibration',
- 'apiLevel': '2.13'}
+ def _get_calibrant_layout(self) -> Dict:
+ """Layout for RGB/OD600 calibration (4 calibrants, 2 replicates each)"""
+ return {
+ 'pbs': [((1, 12), 0), ((13, 24), 1), ((25, 36), 2), ((37, 48), 3)],
+ 'water': [((49, 60), 0), ((61, 72), 1), ((73, 84), 2), ((85, 96), 3)],
+ 'calibrants': {
+ 'fluorescein': ['A1', 'B1'],
+ 'sulforhodamine': ['C1', 'D1'],
+ 'cascade_blue': ['E1', 'F1'],
+ 'microspheres': ['G1', 'H1']
+ },
+ 'dilution_series': [
+ (0, 10), # Row A fluorescein (to well 11, discard to binit)
+ (12, 22), # Row B fluorescein
+ (24, 34), # Row C sulforhodamine
+ (36, 46), # Row D sulforhodamine
+ (48, 58), # Row E cascade blue
+ (60, 70), # Row F cascade blue
+ (72, 82), # Row G microspheres (to well 83, discard to binit)
+ (84, 94) # Row H microspheres
+ ]
+ }
- def run(self, protocol: protocol_api.ProtocolContext):
+ def _load_calibrants(self, protocol, tube_rack) -> None:
+ """Load all RGB calibrants and microspheres"""
+ sulforhodamine_well = self._define_and_load_liquid(
+ protocol, tube_rack['A1'], "Sulforhodamine 101 1x",
+ "Sulforhodamine 101 red fluorescent calibrant", volume=1000, color_index=2
+ )
+ fluorescein_well = self._define_and_load_liquid(
+ protocol, tube_rack['B1'], "Fluorescein 1x",
+ "Fluorescein green fluorescent calibrant", volume=1000, color_index=3
+ )
+ cascade_blue_well = self._define_and_load_liquid(
+ protocol, tube_rack['C1'], "Cascade Blue 1x",
+ "Cascade Blue blue fluorescent calibrant", volume=1000, color_index=4
+ )
+ microspheres_well = self._define_and_load_liquid(
+ protocol, tube_rack['D1'], "Microspheres 1x",
+ "Silica nanoparticles for OD600 calibration", volume=1000, color_index=5
+ )
+ binit_well = self._define_and_load_liquid(
+ protocol, tube_rack['A6'], "Waste Container",
+ "Container for waste disposal", volume=0, color_index=6
+ )
- #Labware
- tiprack = protocol.load_labware(self.tiprack_labware, f'{self.tiprack_position}')
- pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
- plate = protocol.load_labware(self.calibration_plate_labware, self.calibration_plate_position)
- tube_rack = protocol.load_labware(self.tube_rack_labware, self.tube_rack_position)
- if self.use_falcon_tubes:
- falcon_tube_rack = protocol.load_labware(self.falcon_tube_rack_labware, self.falcon_tube_rack_position)
- #Protocol
- #set deck
- #add calibrants
- sulforhodamine_1x = tube_rack['A1']
- fluorescein_1x = tube_rack['B1']
- cascade_blue_1x = tube_rack['C1']
- microspheres_1x = tube_rack['D1']
- #add dilution buffers in tube rack
- pbs_1 = tube_rack['A2']
- pbs_2 = tube_rack['B2']
- pbs_3 = tube_rack['C2']
- pbs_4 = tube_rack['D2']
- pbs_5 = tube_rack['A3']
- pbs_6 = tube_rack['B3']
- pbs_7 = tube_rack['C3']
- pbs_8 = tube_rack['D3']
- water_1 = tube_rack['A4']
- water_2 = tube_rack['B4']
- water_3 = tube_rack['C4']
- water_4 = tube_rack['D4']
- water_5 = tube_rack['A5']
- water_6 = tube_rack['B5']
- water_7 = tube_rack['C5']
- water_8 = tube_rack['D5']
- binit = tube_rack['A6']
-
- #TODO use 2.0 tubes
-
- #if using falcon, remap pbs and water
- if self.use_falcon_tubes:
- #add dilution buffers in falcon tube rack
+ self.calibrant_positions.update({
+ 'sulforhodamine_1x': sulforhodamine_well,
+ 'fluorescein_1x': fluorescein_well,
+ 'cascade_blue_1x': cascade_blue_well,
+ 'microspheres_1x': microspheres_well,
+ 'binit': binit_well
+ })
+
+ def _load_dilution_buffers(self, protocol, tube_rack, falcon_tube_rack) -> Dict:
+ """Extended buffer loading for RGB protocol"""
+ buffers = {}
+
+ if self.use_falcon_tubes and falcon_tube_rack:
+ # Use falcon tubes for all buffers
pbs_falcon = falcon_tube_rack['A1']
water_falcon = falcon_tube_rack['A2']
- pbs_1 = pbs_falcon
- pbs_2 = pbs_falcon
- pbs_3 = pbs_falcon
- pbs_4 = pbs_falcon
- water_1 = water_falcon
- water_2 = water_falcon
- water_3 = water_falcon
- water_4 = water_falcon
+ buffers['pbs_sources'] = [pbs_falcon] * 8
+ buffers['water_sources'] = [water_falcon] * 8
+ else:
+ # Use individual tubes
+ buffers['pbs_sources'] = [
+ tube_rack['A2'], tube_rack['B2'], tube_rack['C2'], tube_rack['D2'],
+ tube_rack['A3'], tube_rack['B3'], tube_rack['C3'], tube_rack['D3']
+ ]
+ buffers['water_sources'] = [
+ tube_rack['A4'], tube_rack['B4'], tube_rack['C4'], tube_rack['D4'],
+ tube_rack['A5'], tube_rack['B5'], tube_rack['C5'], tube_rack['D5']
+ ]
+
+ self.buffer_positions = buffers
+ return buffers
+ def _dispense_initial_calibrants(self, protocol, pipette, plate) -> None:
+ """Dispense all RGB calibrants to starting wells"""
mix_vol = 100
mix_reps = 3
- #dispense PBS
- pipette.pick_up_tip()
- #ambientar pipeta
- liquid_transfer(pipette, 100, pbs_1, pbs_1, self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False, mix_before=mix_vol, mix_reps=mix_reps)
- for well in plate_96_wells[1:12]:
- liquid_transfer(pipette, 100, pbs_1, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[13:24]:
- liquid_transfer(pipette, 100, pbs_2, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[25:36]:
- liquid_transfer(pipette, 100, pbs_3, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[37:48]:
- liquid_transfer(pipette, 100, pbs_4, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #dispense water
- pipette.pick_up_tip()
- #ambientar pipeta
- liquid_transfer(pipette, 100, pbs_1, pbs_1, self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False, mix_before=mix_vol, mix_reps=mix_reps)
- for well in plate_96_wells[49:60]:
- liquid_transfer(pipette, 100, water_1, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[61:72]:
- liquid_transfer(pipette, 100, water_2, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[73:84]:
- liquid_transfer(pipette, 100, water_3, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[85:96]:
- liquid_transfer(pipette, 100, water_4, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #dispense fluorescein
- liquid_transfer(pipette, 200, fluorescein_1x, plate['A1'], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps)
- liquid_transfer(pipette, 200, fluorescein_1x, plate['B1'], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps)
- #dispense sulforhodamine 101
- liquid_transfer(pipette, 200, sulforhodamine_1x, plate['C1'], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps)
- liquid_transfer(pipette, 200, sulforhodamine_1x, plate['D1'], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps)
- #dispense cascade blue
- liquid_transfer(pipette, 200, cascade_blue_1x, plate['E1'], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps)
- liquid_transfer(pipette, 200, cascade_blue_1x, plate['F1'], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps)
- #dispense microspheres nanoparticles
- liquid_transfer(pipette, 200, microspheres_1x, plate['G1'], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps)
- liquid_transfer(pipette, 200, microspheres_1x, plate['H1'], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps)
- #fluorescein serial dilutions
- pipette.pick_up_tip()
- for i in range(0,10):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- liquid_transfer(pipette, 100, plate[plate_96_wells[i+1]], binit, self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- pipette.pick_up_tip()
- for i in range(12,22):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- liquid_transfer(pipette, 100, plate[plate_96_wells[i+1]], binit, self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #sulforamine serial dilution
- pipette.pick_up_tip()
- for i in range(24,34):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- liquid_transfer(pipette, 100, plate[plate_96_wells[i+1]], binit, self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- pipette.pick_up_tip()
- for i in range(36,46):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- liquid_transfer(pipette, 100, plate[plate_96_wells[i+1]], binit, self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #cascade blue serial dilution
- pipette.pick_up_tip()
- for i in range(48,58):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- liquid_transfer(pipette, 100, plate[plate_96_wells[i+1]], binit, self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- pipette.pick_up_tip()
- for i in range(60,70):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- liquid_transfer(pipette, 100, plate[plate_96_wells[i+1]], binit, self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #nanoparticles serial dilutions
- pipette.pick_up_tip()
- for i in range(72,83):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- liquid_transfer(pipette, 100, plate[plate_96_wells[i+1]], binit, self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- pipette.pick_up_tip()
- for i in range(84,94):
- liquid_transfer(pipette, 100, plate[plate_96_wells[i]], plate[plate_96_wells[i+1]], self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- liquid_transfer(pipette, 100, plate[plate_96_wells[i+1]], binit, self.aspiration_rate, self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- # 1/2 dilution, filling 200 uL
- #dispense PBS
+ calibrants_wells = [
+ ('fluorescein_1x', ['A1', 'B1']),
+ ('sulforhodamine_1x', ['C1', 'D1']),
+ ('cascade_blue_1x', ['E1', 'F1']),
+ ('microspheres_1x', ['G1', 'H1'])
+ ]
+
+ for calibrant, well_names in calibrants_wells:
+ for well_name in well_names:
+ self.smart_pipette.liquid_transfer(
+ volume=200, source=self.calibrant_positions[calibrant],
+ destination=plate[well_name], asp_rate=self.aspiration_rate,
+ disp_rate=self.dispense_rate, mix_before=mix_vol, mix_reps=mix_reps
+ )
+
+ def _perform_serial_dilutions(self, protocol, pipette, plate) -> None:
+ """Perform 1:2 serial dilutions with waste disposal"""
+ layout = self._get_calibrant_layout()
+ mix_vol = 100
+ mix_reps = 3
+ binit = self.calibrant_positions['binit']
+
+ for start_idx, end_idx in layout['dilution_series']:
+ pipette.pick_up_tip()
+
+ # Serial dilutions
+ for i in range(start_idx, end_idx):
+ source_well = plate.wells()[i]
+ dest_well = plate.wells()[i + 1]
+ self.smart_pipette.liquid_transfer(
+ volume=100, source=source_well, destination=dest_well,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False
+ )
+
+ # Discard final dilution to binit
+ final_well = plate.wells()[end_idx]
+ self.smart_pipette.liquid_transfer(
+ volume=100, source=final_well, destination=binit,
+ asp_rate=self.aspiration_rate, disp_rate=self.dispense_rate,
+ mix_before=mix_vol, mix_reps=mix_reps, new_tip=False, drop_tip=False
+ )
+ pipette.drop_tip()
+
+ # Fill all wells to 200 µL with appropriate buffers
+ self._fill_wells_to_200ul(protocol, pipette, plate)
+
+ def _fill_wells_to_200ul(self, protocol, pipette, plate) -> None:
+ """Add additional 100 µL to all wells to reach 200 µL final volume"""
+ layout = self._get_calibrant_layout()
+ buffers = self.buffer_positions
+ use_conical = self.use_falcon_tubes # Enable conical tube handling for falcon tubes
+
+ # Add PBS to calibrant wells
pipette.pick_up_tip()
- for well in plate_96_wells[0:12]:
- liquid_transfer(pipette, 100, pbs_5, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[12:24]:
- liquid_transfer(pipette, 100, pbs_6, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[24:36]:
- liquid_transfer(pipette, 100, pbs_7, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[36:48]:
- liquid_transfer(pipette, 100, pbs_8, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
+ for wells_range, source_idx in layout['pbs']:
+ target_wells = plate.wells()[wells_range[0]:wells_range[1]]
+ source = buffers['pbs_sources'][source_idx]
+ for well in target_wells:
+ self.smart_pipette.liquid_transfer(
+ protocol, pipette, 100, source, well,
+ new_tip=False, drop_tip=False, use=use_conical
+ )
pipette.drop_tip()
- #dispense water
+
+ # Add water to blank wells
pipette.pick_up_tip()
- for well in plate_96_wells[48:60]:
- liquid_transfer(pipette, 100, water_5, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[60:72]:
- liquid_transfer(pipette, 100, water_6, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[72:84]:
- liquid_transfer(pipette, 100, water_7, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- for well in plate_96_wells[84:96]:
- liquid_transfer(pipette, 100, water_8, plate[well], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- #END
\ No newline at end of file
+ for wells_range, source_idx in layout['water']:
+ target_wells = plate.wells()[wells_range[0]:wells_range[1]]
+ source = buffers['water_sources'][source_idx]
+ for well in target_wells:
+ self.smart_pipette.liquid_transfer(
+ protocol, pipette, 100, source, well,
+ new_tip=False, drop_tip=False, use=use_conical
+ )
+ pipette.drop_tip()
\ No newline at end of file
diff --git a/src/pudu/generate_protocol.py b/src/pudu/generate_protocol.py
new file mode 100644
index 0000000..dc89474
--- /dev/null
+++ b/src/pudu/generate_protocol.py
@@ -0,0 +1,603 @@
+"""
+Protocol Generator for PUDU
+
+Generates standalone Opentrons protocol files from JSON inputs.
+The generated protocols can be uploaded directly to the Opentrons App.
+
+Usage:
+ # Assembly protocols
+ python -m pudu.generate_protocol input.json [params.json] -o output.py --protocol-type assembly
+
+ # Transformation protocols
+ python -m pudu.generate_protocol input.json [params.json] -o output.py --protocol-type transformation
+
+ # Plating protocols
+ python -m pudu.generate_protocol input.json [params.json] -o output.py --protocol-type plating
+"""
+
+import json
+import argparse
+import sys
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+
+# Default metadata that can be overridden
+DEFAULT_METADATA = {
+ 'protocolName': 'PUDU Protocol',
+ 'author': 'Researcher',
+ 'description': 'Automated protocol',
+ 'apiLevel': '2.21'
+}
+
+# Protocol type configurations
+PROTOCOL_CONFIGS = {
+ 'assembly': {
+ 'data_key': 'assembly_data', # Unified: all protocols use _data
+ 'data_param': 'assembly_data', # Parameter name for class init
+ 'class_map': {
+ 'SBOL': 'SBOLLoopAssembly',
+ 'Manual': 'ManualLoopAssembly',
+ 'Domestication': 'Domestication'
+ },
+ 'module': 'pudu.assembly',
+ 'metadata_name': 'PUDU Assembly Protocol'
+ },
+ 'transformation': {
+ 'data_key': 'transformation_data',
+ 'data_param': 'transformation_data', # Parameter name for class init
+ 'class_name': 'HeatShockTransformation',
+ 'module': 'pudu.transformation',
+ 'metadata_name': 'PUDU Transformation Protocol'
+ },
+ 'plating': {
+ 'data_key': 'plating_data',
+ 'data_param': 'plating_data', # Parameter name for class init
+ 'class_name': 'Plating',
+ 'module': 'pudu.plating',
+ 'metadata_name': 'PUDU Plating Protocol'
+ }
+}
+
+def detect_protocol_type(data) -> tuple[str, Optional[str]]:
+ """
+ Detect the protocol type and assembly subtype from the data structure.
+
+ Args:
+ data: Protocol data (can be dict or list)
+
+ Returns:
+ Tuple of (protocol_type, assembly_subtype)
+ - protocol_type: 'assembly', 'transformation', or 'plating'
+ - assembly_subtype: 'SBOL', 'Manual', 'Domestication', or None
+ """
+ # Handle bare list formats
+ if isinstance(data, list):
+ if not data:
+ raise ValueError("Empty data provided")
+
+ first_item = data[0]
+
+ # Check for transformation format (Strain/Chassis/Plasmids)
+ if 'Strain' in first_item and 'Chassis' in first_item and 'Plasmids' in first_item:
+ return 'transformation', None
+
+ # Check for SBOL assembly format
+ sbol_keys = {'Product', 'Backbone', 'PartsList', 'Restriction Enzyme'}
+ if sbol_keys.issubset(set(first_item.keys())):
+ return 'assembly', 'SBOL'
+
+ # Check for Manual format
+ if 'receiver' in first_item:
+ return 'assembly', 'Manual'
+
+ # Check for Domestication format
+ if 'parts' in first_item and 'backbone' in first_item and 'restriction_enzyme' in first_item:
+ return 'assembly', 'Domestication'
+
+ # Default to SBOL
+ return 'assembly', 'SBOL'
+
+ # Handle new format: wrapped dict
+ if isinstance(data, dict):
+ # Check for assembly types
+ if 'assemblies' in data:
+ assemblies = data['assemblies']
+ if not assemblies:
+ raise ValueError("No assemblies provided")
+
+ first_assembly = assemblies[0]
+
+ # Check for SBOL format
+ sbol_keys = {'Product', 'Backbone', 'PartsList', 'Restriction Enzyme'}
+ if sbol_keys.issubset(set(first_assembly.keys())):
+ return 'assembly', 'SBOL'
+
+ # Check for Manual format
+ if 'receiver' in first_assembly:
+ return 'assembly', 'Manual'
+
+ # Check for Domestication format
+ if 'parts' in first_assembly and 'backbone' in first_assembly and 'restriction_enzyme' in first_assembly:
+ return 'assembly', 'Domestication'
+
+ # Default to SBOL
+ return 'assembly', 'SBOL'
+
+ # Check for transformation (new SBOL format: list with Strain/Chassis/Plasmids)
+ if isinstance(data, list) and data and 'Strain' in data[0] and 'Chassis' in data[0] and 'Plasmids' in data[0]:
+ return 'transformation', None
+
+ # Check for plating
+ if 'bacterium_locations' in data:
+ return 'plating', None
+
+ raise ValueError("Unable to detect protocol type. Please specify --protocol-type explicitly.")
+
+def format_python_value(value, indent_level=0):
+ """
+ Format a Python value for code generation with proper indentation.
+
+ Args:
+ value: The value to format (dict, list, str, int, float, bool, None)
+ indent_level: Current indentation level
+
+ Returns:
+ Formatted string representation
+ """
+ indent = ' ' * indent_level
+
+ if value is None:
+ return 'None'
+ elif isinstance(value, bool):
+ return str(value)
+ elif isinstance(value, (int, float)):
+ return str(value)
+ elif isinstance(value, str):
+ # Escape quotes and use repr for safety
+ return repr(value)
+ elif isinstance(value, list):
+ if not value:
+ return '[]'
+ # Format list with proper indentation
+ items = [format_python_value(item, indent_level + 1) for item in value]
+ if all(isinstance(item, str) for item in value):
+ # Simple list of strings - keep on one line if short
+ formatted = ', '.join(items)
+ if len(formatted) < 100:
+ return f'[{formatted}]'
+ # Multi-line list
+ formatted_items = ',\n'.join(f'{indent} {item}' for item in items)
+ return f'[\n{formatted_items}\n{indent}]'
+ elif isinstance(value, dict):
+ if not value:
+ return '{}'
+ # Format dict with proper indentation
+ items = []
+ for key, val in value.items():
+ formatted_key = repr(key)
+ formatted_val = format_python_value(val, indent_level + 1)
+ items.append(f'{indent} {formatted_key}: {formatted_val}')
+ return '{\n' + ',\n'.join(items) + f'\n{indent}' + '}'
+ else:
+ return repr(value)
+
+def _format_annotation(ann) -> str:
+ """Format a type annotation as a clean string for display in comments."""
+ import inspect
+ if ann is inspect.Parameter.empty:
+ return ''
+ if hasattr(ann, '__name__'):
+ return ann.__name__
+ # Clean up typing generics: typing.Optional[typing.Dict] -> Optional[Dict]
+ return str(ann).replace('typing.', '')
+
+
+def generate_param_reference(protocol_type: str, class_name: str, module_str: str) -> str:
+ """
+ Generate a commented parameter reference block for the end of the protocol file.
+
+ Dynamically imports the protocol class and uses inspect to extract the full
+ __init__ signature and class docstrings, formatted as comments so users can
+ see all available parameters when making last-minute edits before running on
+ the robot.
+
+ Args:
+ protocol_type: Protocol type string (e.g. 'transformation')
+ class_name: Protocol class name (e.g. 'HeatShockTransformation')
+ module_str: Fully-qualified module path (e.g. 'pudu.transformation')
+
+ Returns:
+ Commented parameter reference block as a string, or a short error comment
+ if the class cannot be imported.
+ """
+ try:
+ import importlib
+ import inspect
+
+ mod = importlib.import_module(module_str)
+ cls = getattr(mod, class_name)
+
+ lines = []
+ lines.append("")
+ lines.append("")
+ lines.append("# " + "=" * 70)
+ lines.append(f"# PARAMETER REFERENCE — {class_name}")
+ lines.append("#")
+ lines.append("# To customize your protocol, add any of the parameters below")
+ lines.append(f"# to the {class_name}() constructor call in run() above.")
+ lines.append("# Example: protocol_instance = " + class_name + "(")
+ lines.append(f"# {PROTOCOL_CONFIGS[protocol_type]['data_param']}={PROTOCOL_CONFIGS[protocol_type]['data_key']},")
+ lines.append("# replicates=3,")
+ lines.append("# initial_tip='B1',")
+ lines.append("# )")
+ lines.append("# " + "=" * 70)
+
+ # Collect all __init__ params walking the MRO from most-specific to base.
+ # Use an ordered dict so subclass params appear first.
+ seen_params = set()
+ mro_params = [] # list of (class, [(name, default, annotation), ...])
+
+ for klass in cls.__mro__:
+ if klass is object:
+ continue
+ try:
+ sig = inspect.signature(klass.__init__)
+ except (ValueError, TypeError):
+ continue
+
+ klass_params = []
+ for name, param in sig.parameters.items():
+ if name in ('self', 'args', 'kwargs') or name in seen_params:
+ continue
+ seen_params.add(name)
+ default = param.default if param.default is not inspect.Parameter.empty else '(required)'
+ ann = _format_annotation(param.annotation)
+ klass_params.append((name, default, ann))
+
+ if klass_params:
+ mro_params.append((klass.__name__, klass_params))
+
+ # Build parameter table — group by class
+ if mro_params:
+ # Determine column widths across all params
+ all_param_entries = [(n, d, a) for _, params in mro_params for n, d, a in params]
+ max_name = max(len(n) for n, _, _ in all_param_entries)
+ max_type = max((len(a) for _, _, a in all_param_entries if a), default=0)
+
+ for klass_name, params in mro_params:
+ lines.append("#")
+ lines.append(f"# [{klass_name}]")
+ for name, default, ann in params:
+ type_col = ann.ljust(max_type) if ann else ' ' * max_type
+ default_repr = repr(default) if not isinstance(default, str) else default
+ lines.append(f"# {name:<{max_name}} {type_col} = {default_repr}")
+
+ # Append full docstrings for detailed descriptions
+ lines.append("#")
+ lines.append("# " + "-" * 70)
+ lines.append("# Full parameter descriptions:")
+
+ seen_docs: set = set()
+ for klass in cls.__mro__:
+ if klass is object:
+ continue
+ doc = inspect.getdoc(klass)
+ if not doc or doc in seen_docs:
+ continue
+ seen_docs.add(doc)
+ lines.append("#")
+ lines.append(f"# [{klass.__name__}]")
+ for doc_line in doc.split('\n'):
+ lines.append(f"# {doc_line}" if doc_line.strip() else "#")
+
+ return '\n'.join(lines)
+
+ except Exception as e:
+ return f"\n\n# (Parameter reference unavailable: {e})"
+
+
+def generate_protocol(
+ protocol_data: Any,
+ json_params: Optional[Dict] = None,
+ metadata: Optional[Dict] = None,
+ protocol_type: str = 'assembly',
+ assembly_subtype: Optional[str] = None,
+ plasmid_locations: Optional[Dict] = None
+) -> str:
+ """
+ Generate a complete Opentrons protocol file as a string.
+
+ Args:
+ protocol_data: Protocol data (list for assemblies, or dict for other protocols)
+ json_params: Optional advanced parameters dictionary
+ metadata: Optional metadata dictionary (merged with defaults)
+ protocol_type: Type of protocol ('assembly', 'transformation', 'plating')
+ assembly_subtype: Subtype for assembly protocols ('SBOL', 'Manual', 'Domestication')
+ plasmid_locations: Optional dict mapping plasmid URIs to well locations (from assembly output)
+
+ Returns:
+ Complete protocol file as a string
+ """
+ # Get protocol configuration
+ if protocol_type not in PROTOCOL_CONFIGS:
+ raise ValueError(f"Unknown protocol type: {protocol_type}")
+
+ config = PROTOCOL_CONFIGS[protocol_type]
+
+ # Merge metadata with defaults
+ final_metadata = DEFAULT_METADATA.copy()
+ final_metadata['protocolName'] = config['metadata_name']
+ if metadata:
+ final_metadata.update(metadata)
+
+ # Determine class name and data key
+ data_key = config['data_key']
+
+ if protocol_type == 'assembly':
+ if assembly_subtype is None:
+ raise ValueError("assembly_subtype required for assembly protocols")
+ class_name = config['class_map'].get(assembly_subtype, 'SBOLLoopAssembly')
+
+ # Handle both legacy (list) and new (dict) formats
+ if isinstance(protocol_data, list):
+ actual_data = protocol_data
+ elif isinstance(protocol_data, dict) and 'assemblies' in protocol_data:
+ actual_data = protocol_data['assemblies']
+ else:
+ actual_data = protocol_data
+ else:
+ class_name = config['class_name']
+ actual_data = protocol_data
+
+ module = config['module']
+
+ # Build the protocol file
+ lines = []
+
+ # Imports
+ lines.append(f"from {module} import {class_name}")
+ lines.append("from opentrons import protocol_api")
+ lines.append("")
+ lines.append("")
+
+ # Protocol data
+ lines.append(f"# Protocol data")
+ lines.append(f"{data_key} = {format_python_value(actual_data)}")
+ lines.append("")
+
+ # Plasmid locations (transformation only, from assembly output)
+ if plasmid_locations is not None:
+ lines.append("# Plasmid well locations from assembly protocol output")
+ lines.append(f"plasmid_locations = {format_python_value(plasmid_locations)}")
+ lines.append("")
+
+ # Advanced params (if provided)
+ if json_params:
+ lines.append("# Advanced parameters for protocol customization")
+ lines.append(f"json_params = {format_python_value(json_params)}")
+ lines.append("")
+
+ # Metadata
+ lines.append("# Protocol metadata")
+ lines.append(f"metadata = {format_python_value(final_metadata)}")
+ lines.append("")
+ lines.append("")
+
+ # Run function
+ lines.append("def run(protocol: protocol_api.ProtocolContext):")
+ lines.append(' """Main protocol execution function"""')
+ lines.append("")
+
+ # Protocol initialization - unified data-driven approach
+ # All protocols now use _data + advanced_params pattern
+ data_param = config['data_param']
+
+ # Build constructor arguments
+ constructor_args = [f"{data_param}={data_key}"]
+ if plasmid_locations is not None:
+ constructor_args.append("plasmid_locations=plasmid_locations")
+ if json_params:
+ constructor_args.append("json_params=json_params")
+
+ if len(constructor_args) > 1:
+ lines.append(f" protocol_instance = {class_name}(")
+ for i, arg in enumerate(constructor_args):
+ comma = "," if i < len(constructor_args) - 1 else ""
+ lines.append(f" {arg}{comma}")
+ lines.append(" )")
+ else:
+ lines.append(f" protocol_instance = {class_name}({constructor_args[0]})")
+
+ lines.append(" protocol_instance.run(protocol)")
+ lines.append("")
+
+ # Parameter reference block — appended as comments for last-minute editing
+ param_ref = generate_param_reference(protocol_type, class_name, module)
+ if param_ref:
+ lines.append(param_ref)
+
+ return '\n'.join(lines)
+
+def main():
+ """Command-line interface for protocol generation"""
+ parser = argparse.ArgumentParser(
+ description='Generate Opentrons protocol files from JSON inputs',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ # Generate assembly protocol (auto-detect type)
+ python -m pudu.generate_protocol data.json params.json -o protocol.py --protocol-type assembly
+
+ # Generate transformation protocol with assembly output
+ python -m pudu.generate_protocol synbiosuite.json params.json -o protocol.py --protocol-type transformation --plasmid-locations transformation_input.json
+
+ # Generate transformation protocol without prior assembly (manual input)
+ python -m pudu.generate_protocol synbiosuite.json params.json -o protocol.py --protocol-type transformation
+
+ # Generate plating protocol
+ python -m pudu.generate_protocol plating.json -o protocol.py --protocol-type plating
+
+ # Auto-detect protocol type
+ python -m pudu.generate_protocol data.json -o protocol.py
+
+ # Specify assembly subtype explicitly
+ python -m pudu.generate_protocol assemblies.json -o protocol.py --protocol-type assembly --assembly-type Manual
+ """
+ )
+
+ parser.add_argument(
+ 'input',
+ type=Path,
+ help='Path to protocol data JSON file'
+ )
+
+ parser.add_argument(
+ 'json_params',
+ type=Path,
+ nargs='?',
+ default=None,
+ help='Path to advanced parameters JSON file (optional)'
+ )
+
+ parser.add_argument(
+ '-o', '--output',
+ type=Path,
+ required=True,
+ help='Output protocol file path (e.g., protocol.py)'
+ )
+
+ parser.add_argument(
+ '--protocol-type',
+ choices=['assembly', 'transformation', 'plating'],
+ default=None,
+ help='Protocol type (default: auto-detect)'
+ )
+
+ parser.add_argument(
+ '--assembly-type',
+ choices=['SBOL', 'Manual', 'Domestication'],
+ default=None,
+ help='Assembly subtype for assembly protocols (default: auto-detect)'
+ )
+
+ parser.add_argument(
+ '--plasmid-locations',
+ type=Path,
+ default=None,
+ help='Path to plasmid locations JSON file from assembly simulation (transformation protocols only)'
+ )
+
+ parser.add_argument(
+ '--metadata',
+ type=Path,
+ default=None,
+ help='Path to metadata JSON file (optional)'
+ )
+
+ args = parser.parse_args()
+
+ # Load protocol data
+ try:
+ with open(args.input, 'r') as f:
+ protocol_data = json.load(f)
+ except FileNotFoundError:
+ print(f"Error: Input file not found: {args.input}", file=sys.stderr)
+ sys.exit(1)
+ except json.JSONDecodeError as e:
+ print(f"Error: Invalid JSON in input file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ # Load advanced params if provided
+ json_params = None
+ if args.json_params:
+ try:
+ with open(args.json_params, 'r') as f:
+ json_params = json.load(f)
+ except FileNotFoundError:
+ print(f"Error: Advanced params file not found: {args.json_params}", file=sys.stderr)
+ sys.exit(1)
+ except json.JSONDecodeError as e:
+ print(f"Error: Invalid JSON in advanced params file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ # Load plasmid locations if provided
+ plasmid_locations = None
+ if args.plasmid_locations:
+ try:
+ with open(args.plasmid_locations, 'r') as f:
+ plasmid_locations = json.load(f)
+ except FileNotFoundError:
+ print(f"Error: Plasmid locations file not found: {args.plasmid_locations}", file=sys.stderr)
+ sys.exit(1)
+ except json.JSONDecodeError as e:
+ print(f"Error: Invalid JSON in plasmid locations file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ # Load metadata if provided
+ metadata = None
+ if args.metadata:
+ try:
+ with open(args.metadata, 'r') as f:
+ metadata = json.load(f)
+ except FileNotFoundError:
+ print(f"Error: Metadata file not found: {args.metadata}", file=sys.stderr)
+ sys.exit(1)
+ except json.JSONDecodeError as e:
+ print(f"Error: Invalid JSON in metadata file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ # Detect or use specified assembly type
+ if args.protocol_type:
+ protocol_type = args.protocol_type
+ assembly_subtype = args.assembly_type # May be None
+
+ # If assembly type and no subtype specified, try to detect
+ if protocol_type == 'assembly' and assembly_subtype is None:
+ try:
+ _, assembly_subtype = detect_protocol_type(protocol_data)
+ print(f"Detected assembly subtype: {assembly_subtype}")
+ except ValueError as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+ else:
+ try:
+ protocol_type, assembly_subtype = detect_protocol_type(protocol_data)
+ print(f"Detected protocol type: {protocol_type}", end="")
+ if assembly_subtype:
+ print(f" ({assembly_subtype})")
+ else:
+ print()
+ except ValueError as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ # Generate protocol
+ try:
+ protocol_code = generate_protocol(
+ protocol_data=protocol_data,
+ json_params=json_params,
+ metadata=metadata,
+ protocol_type=protocol_type,
+ assembly_subtype=assembly_subtype,
+ plasmid_locations=plasmid_locations
+ )
+ except Exception as e:
+ print(f"Error generating protocol: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ # Write output
+ try:
+ with open(args.output, 'w') as f:
+ f.write(protocol_code)
+ print(f"✓ Protocol generated successfully: {args.output}")
+ print(f" Protocol type: {protocol_type}")
+ if assembly_subtype:
+ print(f" Assembly subtype: {assembly_subtype}")
+ if json_params:
+ print(f" Advanced params: {len(json_params)} parameters")
+ except IOError as e:
+ print(f"Error writing output file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
diff --git a/src/pudu/plating.py b/src/pudu/plating.py
new file mode 100644
index 0000000..a9e6a6c
--- /dev/null
+++ b/src/pudu/plating.py
@@ -0,0 +1,404 @@
+from typing import Optional, Dict
+from pudu import colors, SmartPipette
+from opentrons import protocol_api
+
+class Plating():
+ """
+ Creates a protocol for automated plating of transformed bacteria
+
+ Attributes:
+
+ """
+ def __init__(self,
+ plating_data: Optional[Dict] = None,
+ json_params: Optional[Dict] = None,
+ volume_total_reaction: float = 20,
+ volume_bacteria_transfer: float = 2,
+ volume_colony: float = 4,
+ volume_lb_transfer: float = 18,
+ volume_lb: float = 10000,
+ replicates: int = 1,
+ number_dilutions: int = 2,
+ max_colonies: int = 192,
+
+ thermocycler_starting_well: int = 0,
+ thermocycler_labware: str = 'biorad_96_wellplate_200ul_pcr',
+
+ small_tiprack: str = 'opentrons_96_filtertiprack_20ul',
+ small_tiprack_position: str = '9',
+ initial_small_tip: Optional[str] = None,
+ large_tiprack: str = 'opentrons_96_filtertiprack_200ul',
+ large_tiprack_position: str = '1',
+ initial_large_tip: Optional[str] = None,
+ small_pipette: str = 'p20_single_gen2',
+ small_pipette_position: str = 'left',
+ large_pipette: str = 'p300_single_gen2',
+ large_pipette_position: str = 'right',
+
+ dilution_plate: str = 'nest_96_wellplate_100ul_pcr_full_skirt',
+ dilution_plate_position1: str = '2',
+ dilution_plate_position2: str = '3',
+ # agar_plate: str = 'nunc_omnitray_96grid',
+ agar_plate: str = 'nest_96_wellplate_100ul_pcr_full_skirt',
+ agar_plate_position1: str = '5',
+ agar_plate_position2: str = '6',
+ tube_rack: str = 'opentrons_15_tuberack_falcon_15ml_conical',
+ tube_rack_position: str = '4',
+ lb_tube_position: int = 0,
+
+ aspiration_rate: float = 0.5,
+ dispense_rate: float = 1,
+ bacterium_locations: Optional[Dict] = None,
+ **kwargs):
+
+ # Collect kwargs for merging
+ kwargs_params = {
+ 'volume_total_reaction': volume_total_reaction,
+ 'volume_bacteria_transfer': volume_bacteria_transfer,
+ 'volume_colony': volume_colony,
+ 'volume_lb_transfer': volume_lb_transfer,
+ 'volume_lb': volume_lb,
+ 'replicates': replicates,
+ 'number_dilutions': number_dilutions,
+ 'max_colonies': max_colonies,
+ 'thermocycler_starting_well': thermocycler_starting_well,
+ 'thermocycler_labware': thermocycler_labware,
+ 'small_tiprack': small_tiprack,
+ 'small_tiprack_position': small_tiprack_position,
+ 'initial_small_tip': initial_small_tip,
+ 'large_tiprack': large_tiprack,
+ 'large_tiprack_position': large_tiprack_position,
+ 'initial_large_tip': initial_large_tip,
+ 'small_pipette': small_pipette,
+ 'small_pipette_position': small_pipette_position,
+ 'large_pipette': large_pipette,
+ 'large_pipette_position': large_pipette_position,
+ 'dilution_plate': dilution_plate,
+ 'dilution_plate_position1': dilution_plate_position1,
+ 'dilution_plate_position2': dilution_plate_position2,
+ 'agar_plate': agar_plate,
+ 'agar_plate_position1': agar_plate_position1,
+ 'agar_plate_position2': agar_plate_position2,
+ 'tube_rack': tube_rack,
+ 'tube_rack_position': tube_rack_position,
+ 'lb_tube_position': lb_tube_position,
+ 'aspiration_rate': aspiration_rate,
+ 'dispense_rate': dispense_rate,
+ 'bacterium_locations': bacterium_locations
+ }
+
+ kwargs_params.update(kwargs)
+
+ self._merged_params = self._merge_params(plating_data, json_params, kwargs_params)
+
+ if self._merged_params.get('bacterium_locations') is None:
+ raise ValueError("Must input bacterium_locations (either via plating_data, advanced_params, or bacterium_locations parameter)")
+
+ self.volume_total_reaction = self._merged_params['volume_total_reaction']
+ self.volume_bacteria_transfer = self._merged_params['volume_bacteria_transfer']
+ self.volume_colony = self._merged_params['volume_colony']
+ self.volume_lb_transfer = self._merged_params['volume_lb_transfer']
+ self.volume_lb = self._merged_params['volume_lb']
+ self.replicates = self._merged_params['replicates']
+ self.number_dilutions = self._merged_params['number_dilutions']
+ self.thermocycler_starting_well = self._merged_params['thermocycler_starting_well']
+ self.thermocycler_labware = self._merged_params['thermocycler_labware']
+ self.small_tiprack = self._merged_params['small_tiprack']
+ self.small_tiprack_position = self._merged_params['small_tiprack_position']
+ self.initial_small_tip = self._merged_params['initial_small_tip']
+ self.large_tiprack = self._merged_params['large_tiprack']
+ self.large_tiprack_position = self._merged_params['large_tiprack_position']
+ self.initial_large_tip = self._merged_params['initial_large_tip']
+ self.small_pipette = self._merged_params['small_pipette']
+ self.small_pipette_position = self._merged_params['small_pipette_position']
+ self.large_pipette = self._merged_params['large_pipette']
+ self.large_pipette_position = self._merged_params['large_pipette_position']
+ self.dilution_plate = self._merged_params['dilution_plate']
+ self.dilution_plate_position1 = self._merged_params['dilution_plate_position1']
+ self.dilution_plate_position2 = self._merged_params['dilution_plate_position2']
+ self.agar_plate = self._merged_params['agar_plate']
+ self.agar_plate_position1 = self._merged_params['agar_plate_position1']
+ self.agar_plate_position2 = self._merged_params['agar_plate_position2']
+ self.tube_rack = self._merged_params['tube_rack']
+ self.tube_rack_position = self._merged_params['tube_rack_position']
+ self.lb_tube_position = self._merged_params['lb_tube_position']
+ self.aspiration_rate = self._merged_params['aspiration_rate']
+ self.dispense_rate = self._merged_params['dispense_rate']
+ self.bacterium_locations = self._merged_params['bacterium_locations']
+ self.number_constructs = len(self.bacterium_locations)
+ self.max_colonies = self._merged_params['max_colonies']
+
+ self.total_colonies = self.number_constructs * self.number_dilutions * self.replicates
+
+ if self.total_colonies > self.max_colonies:
+ raise ValueError(f"Protocol only supports a max of {self.max_colonies} colonies")
+ if self.replicates > 8:
+ raise ValueError("Protocol only supports a max of 8 replicates")
+ if self.number_dilutions > 2:
+ raise ValueError("Protocol currently supports a max of 2 dilutions")
+
+ def _merge_params(self, plating_data: Optional[Dict], json_params: Optional[Dict], kwargs_params: Dict) -> Dict:
+ """
+ Merge parameters with precedence: defaults <- plating_data <- json_params <- kwargs
+
+ Args:
+ plating_data: Optional dict containing protocol data (bacterium_locations)
+ json_params: Optional dict containing configuration parameters
+ kwargs_params: Dict of parameters passed as kwargs
+
+ Returns:
+ Merged parameter dictionary
+ """
+ # Define defaults for all valid parameters
+ valid_params = {
+ 'volume_total_reaction': 20,
+ 'volume_bacteria_transfer': 2,
+ 'volume_colony': 4,
+ 'volume_lb_transfer': 18,
+ 'volume_lb': 10000,
+ 'replicates': 1,
+ 'number_dilutions': 2,
+ 'max_colonies': 192,
+ 'thermocycler_starting_well': 0,
+ 'thermocycler_labware': 'biorad_96_wellplate_200ul_pcr',
+ 'small_tiprack': 'opentrons_96_filtertiprack_20ul',
+ 'small_tiprack_position': '9',
+ 'initial_small_tip': None,
+ 'large_tiprack': 'opentrons_96_filtertiprack_200ul',
+ 'large_tiprack_position': '1',
+ 'initial_large_tip': None,
+ 'small_pipette': 'p20_single_gen2',
+ 'small_pipette_position': 'left',
+ 'large_pipette': 'p300_single_gen2',
+ 'large_pipette_position': 'right',
+ 'dilution_plate': 'nest_96_wellplate_100ul_pcr_full_skirt',
+ 'dilution_plate_position1': '2',
+ 'dilution_plate_position2': '3',
+ 'agar_plate': 'nest_96_wellplate_100ul_pcr_full_skirt',
+ 'agar_plate_position1': '5',
+ 'agar_plate_position2': '6',
+ 'tube_rack': 'opentrons_15_tuberack_falcon_15ml_conical',
+ 'tube_rack_position': '4',
+ 'lb_tube_position': 0,
+ 'aspiration_rate': 0.5,
+ 'dispense_rate': 1,
+ 'bacterium_locations': None
+ }
+
+ # Start with defaults
+ merged = valid_params.copy()
+
+ # Apply plating_data (if provided)
+ if plating_data is not None:
+ self._validate_param_structure(plating_data, valid_params, 'plating_data')
+ merged.update(plating_data)
+
+ # Apply json_params (if provided)
+ if json_params is not None:
+ self._validate_param_structure(json_params, valid_params, 'json_params')
+ merged.update(json_params)
+
+ # Apply kwargs (highest precedence) - only if they differ from defaults
+ for key, value in kwargs_params.items():
+ if key in valid_params:
+ # Only override if the value is explicitly different from the default
+ if value != valid_params[key]:
+ merged[key] = value
+
+ return merged
+
+ def _validate_param_structure(self, params: Dict, valid_params: Dict, param_name: str):
+ """
+ Validate that all parameters in the dict are recognized.
+
+ Args:
+ params: Dictionary to validate
+ valid_params: Dictionary of valid parameter names
+ param_name: Name of the parameter dict (for error messages)
+
+ Raises:
+ ValueError: If unknown parameters are found
+ """
+ unknown_params = set(params.keys()) - set(valid_params.keys())
+ if unknown_params:
+ raise ValueError(
+ f"Unknown parameters in {param_name}: {unknown_params}.\n"
+ f"Valid parameters are: {set(valid_params.keys())}"
+ )
+
+ def calculate_plate_layout(self,protocol, plate1, plate2=None):
+ """
+ Calculate the layout for colonies on plates with dynamic buffer between dilutions
+ Returns: dict with plate assignments and well positions
+ """
+ colonies_per_dilution = self.number_constructs * self.replicates
+
+ layout = {
+ 'dilution_1': {'plate': 1, 'wells':[]},
+ 'dilution_2': {'plate': 1, 'wells':[]} if self.number_dilutions ==2 else None
+ }
+
+ #Check if we need two plates
+ if self.number_dilutions ==2 and colonies_per_dilution > 48:
+ if plate2 is None:
+ raise ValueError("Two plates required but plate2 not provided")
+ #Each Dilution gets its own plate
+ layout['dilution_1']['wells'] = plate1.wells()[:colonies_per_dilution]
+ layout['dilution_2']['plate'] = 2
+ layout['dilution_2']['wells'] = plate2.wells()[:colonies_per_dilution]
+ protocol.comment(f"Using 2 plates: {colonies_per_dilution} colonies per dilution exceeds single plate capacity")
+ elif self.number_dilutions ==2 and colonies_per_dilution <= 48:
+ #Both Dilutions Fit On One Plate
+ first_half = plate1.wells()[:colonies_per_dilution]
+ second_half = plate1.wells()[48:48+colonies_per_dilution]
+ layout['dilution_1']['wells'] = first_half
+ layout['dilution_2']['wells'] = second_half
+ protocol.comment(f"Using only one {plate1}: {colonies_per_dilution} colonies on each half")
+ else:
+ #Single dilution
+ layout['dilution_1']['wells'] = plate1.wells()[:colonies_per_dilution]
+ return layout
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+ #Labware
+ #Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
+ thermocycler = protocol.load_module('thermocyclerModuleV1')
+ thermocycler_plate = thermocycler.load_labware(self.thermocycler_labware)
+ #Load the tipracks
+ small_tiprack = protocol.load_labware(self.small_tiprack, self.small_tiprack_position)
+ large_tiprack = protocol.load_labware(self.large_tiprack, self.large_tiprack_position)
+ #Load the pipettes
+ small_pipette = protocol.load_instrument(self.small_pipette, self.small_pipette_position, tip_racks=[small_tiprack])
+ if self.initial_small_tip:
+ small_pipette.starting_tip = small_tiprack[self.initial_small_tip]
+ large_pipette = protocol.load_instrument(self.large_pipette, self.large_pipette_position, tip_racks=[large_tiprack])
+ if self.initial_large_tip:
+ large_pipette.starting_tip = large_tiprack[self.initial_large_tip]
+ #SmartPipette Wrapper to avoid dunking into the LB
+ smart_pipette = SmartPipette(large_pipette,protocol)
+ #Load the tube rack
+ tube_rack = protocol.load_labware(self.tube_rack, self.tube_rack_position)
+ lb_tube = tube_rack.wells()[self.lb_tube_position]
+ #load liquids
+ liquid_broth = protocol.define_liquid(
+ name="liquid_broth",
+ description="Liquid broth for dilutions",
+ display_color="#D2B48C"
+ )
+ lb_tube.load_liquid(liquid = liquid_broth, volume = self.volume_lb)
+ # Load bacteria into thermocycler wells
+ for i, (well_position, construct_names) in enumerate(self.bacterium_locations.items()):
+ liquid_bacteria = protocol.define_liquid(
+ name="transformed_bacteria",
+ description=f"{construct_names}",
+ display_color=colors[i%len(colors)]
+ )
+ well = thermocycler_plate[well_position]
+ well.load_liquid(liquid=liquid_bacteria, volume=self.volume_total_reaction)
+
+ # Load the dilution plates and Calculate the layout of the plates
+ dilution_plate1 = protocol.load_labware(self.dilution_plate, self.dilution_plate_position1)
+ if self.total_colonies <= len(dilution_plate1.wells()):
+ dilution_layout = self.calculate_plate_layout(protocol, dilution_plate1)
+ else:
+ dilution_plate2 = protocol.load_labware(self.dilution_plate, self.dilution_plate_position2)
+ dilution_layout = self.calculate_plate_layout(protocol, dilution_plate1, dilution_plate2)
+
+ #Load the Agar plates and Calculate the layout of the plates
+ agar_plate1 = protocol.load_labware(self.agar_plate, self.agar_plate_position1)
+ if self.total_colonies <= len(agar_plate1.wells()):
+ agar_layout = self.calculate_plate_layout(protocol, agar_plate1)
+ else:
+ agar_plate2 = protocol.load_labware(self.agar_plate, self.agar_plate_position2)
+ agar_layout = self.calculate_plate_layout(protocol, agar_plate1, agar_plate2)
+
+
+ thermocycler.set_block_temperature(4)
+ thermocycler.open_lid()
+
+ #Load the Liquid Broth into the dilution wells
+ protocol.comment("\n=== Step 1: Distributing LB to dilution wells ===")
+ # Get all wells that will receive LB (both dilutions if applicable)
+ all_dilution_wells = dilution_layout['dilution_1']['wells'][:]
+ if self.number_dilutions == 2 and dilution_layout['dilution_2']:
+ all_dilution_wells.extend(dilution_layout['dilution_2']['wells'])
+ # Distribute LB efficiently using built-in distribute method
+ # Process in chunks of 8 wells to update aspiration height
+ chunk_size = 8
+ for i in range(0, len(all_dilution_wells), chunk_size):
+ chunk_wells = all_dilution_wells[i:i + chunk_size]
+
+ # Get current aspiration location before each chunk
+ aspiration_location = smart_pipette.get_aspiration_location(lb_tube)
+ protocol.comment(f"Distributing to wells {i + 1}-{min(i + chunk_size, len(all_dilution_wells))}")
+
+ # Use built-in distribute method with updated aspiration location
+ large_pipette.distribute(
+ volume=self.volume_lb_transfer,
+ source=aspiration_location,
+ dest=chunk_wells,
+ disposal_volume=4, # For accuracy
+ new_tip='once' # Use one tip for the chunk
+ )
+
+ # Load liquid tracking for dilution wells
+ for well in chunk_wells:
+ well.load_liquid(liquid=liquid_broth, volume=self.volume_lb_transfer)
+
+ #Transfer bacteria to first dilution and process
+ protocol.comment("\n=== Step 2: Transferring bacteria and plating ===")
+
+ well_index = 0
+ for construct_position, construct_names in self.bacterium_locations.items():
+ for replicate in range(self.replicates):
+ # Get source and destination wells
+ source_well = thermocycler_plate[construct_position]
+ dilution1_well = dilution_layout['dilution_1']['wells'][well_index]
+ agar1_well = agar_layout['dilution_1']['wells'][well_index]
+
+ protocol.comment(f"\nProcessing {construct_names[0]} replicate {replicate + 1}")
+
+ # Pick up tip once for entire workflow per well
+ small_pipette.pick_up_tip()
+
+ # Transfer bacteria to dilution plate 1
+ small_pipette.aspirate(self.volume_bacteria_transfer, source_well, rate=self.aspiration_rate)
+ small_pipette.dispense(self.volume_bacteria_transfer, dilution1_well, rate=self.dispense_rate)
+
+ # Mix in dilution plate 1 (15µL mixing volume)
+ small_pipette.mix(repetitions=5, volume=19, location=dilution1_well)
+
+ # Plate on agar 1
+ small_pipette.aspirate(self.volume_colony, dilution1_well, rate=self.aspiration_rate)
+ small_pipette.dispense(self.volume_colony, agar1_well.top(-8), rate=self.dispense_rate)
+ small_pipette.blow_out()
+
+ # If we have a second dilution, continue with same tip
+ if self.number_dilutions == 2:
+ dilution2_well = dilution_layout['dilution_2']['wells'][well_index]
+ agar2_well = agar_layout['dilution_2']['wells'][well_index]
+
+ # Transfer from dilution 1 to dilution 2
+ small_pipette.aspirate(self.volume_bacteria_transfer, dilution1_well, rate=self.aspiration_rate)
+ small_pipette.dispense(self.volume_bacteria_transfer, dilution2_well, rate=self.dispense_rate)
+
+ # Mix in dilution plate 2
+ small_pipette.mix(repetitions=5, volume=19, location=dilution2_well)
+
+ # Plate on agar 2
+ small_pipette.aspirate(self.volume_colony, dilution2_well, rate=self.aspiration_rate)
+ small_pipette.dispense(self.volume_colony, agar2_well.top(-8), rate=self.dispense_rate)
+ small_pipette.blow_out()
+
+ # Drop tip after completing all transfers for this well
+ small_pipette.drop_tip()
+
+ well_index += 1
+
+ # Close thermocycler lid
+ # thermocycler.close_lid()
+ # thermocycler.deactivate_block()
+
+ protocol.comment("\n=== Plating protocol complete ===")
+ protocol.comment(f"Plated {self.number_constructs} constructs with {self.replicates} replicates")
+ protocol.comment(f"Created a total of {self.total_colonies} colonies")
\ No newline at end of file
diff --git a/src/pudu/sample_preparation.py b/src/pudu/sample_preparation.py
new file mode 100644
index 0000000..3c1b557
--- /dev/null
+++ b/src/pudu/sample_preparation.py
@@ -0,0 +1,415 @@
+from opentrons import protocol_api
+from typing import List, Union, Optional, Tuple
+from abc import ABC, abstractmethod
+import math
+from pudu.utils import colors
+
+
+class SamplePreparation(ABC):
+ """
+ Abstract base class for all Sample Preparation protocols with shared functionality.
+ """
+
+ def __init__(self,
+ test_labware: str = 'corning_96_wellplate_360ul_flat',
+ test_position: str = '2',
+ aspiration_rate: float = 0.5,
+ dispense_rate: float = 1.0,
+ tiprack_labware: str = 'opentrons_96_filtertiprack_200ul',
+ tiprack_position: str = '9',
+ starting_tip: Optional[str] = None,
+ pipette: str = 'p300_single_gen2',
+ pipette_position: str = 'right',
+ use_temperature_module: bool = False,
+ temperature: int = 4,
+ **kwargs):
+ self.test_labware = test_labware
+ self.test_position = test_position
+ self.aspiration_rate = aspiration_rate
+ self.dispense_rate = dispense_rate
+ self.tiprack_labware = tiprack_labware
+ self.tiprack_position = tiprack_position
+ self.starting_tip = starting_tip
+ self.pipette = pipette
+ self.pipette_position = pipette_position
+ self.use_temperature_module = use_temperature_module
+ self.temperature = temperature
+
+ # Protocol tracking
+ self.result_dict = {}
+ self.liquid_tracker = {}
+
+ @abstractmethod
+ def run(self, protocol: protocol_api.ProtocolContext):
+ """Abstract method that must be implemented by subclasses."""
+ pass
+
+ def _load_standard_labware(self, protocol: protocol_api.ProtocolContext):
+ """Load standard labware common to all protocols."""
+ tiprack = protocol.load_labware(self.tiprack_labware, self.tiprack_position)
+ pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
+
+ if self.starting_tip:
+ pipette.starting_tip = tiprack[self.starting_tip]
+
+ plate = protocol.load_labware(self.test_labware, self.test_position)
+
+ return tiprack, pipette, plate
+
+ def _load_source_labware(self, protocol: protocol_api.ProtocolContext,
+ temp_module_position: str = '1',
+ temp_module_labware: str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ tube_rack_position: str = '3',
+ tube_rack_labware: str = 'opentrons_24_tuberack_nest_1.5ml_snapcap'):
+ """Load source tube labware with optional temperature control."""
+
+ if self.use_temperature_module:
+ temperature_module = protocol.load_module('Temperature Module', temp_module_position)
+ source_rack = temperature_module.load_labware(temp_module_labware)
+ temperature_module.set_temperature(self.temperature)
+ else:
+ source_rack = protocol.load_labware(tube_rack_labware, tube_rack_position)
+
+ return source_rack
+
+ def _create_slots(self, plate, replicates: int = 4):
+ """
+ Create well groupings for sample distribution.
+ Flexible slot creation based on plate size and replicate requirements.
+ """
+ columns = plate.columns()
+
+ middle_columns = columns[1:-1]
+ edge_columns = [columns[0], columns[-1]]
+ slots = []
+
+ # All top halves of middle columns first
+ slots.extend(col[:len(col) // 2] for col in middle_columns)
+ # Bottom halves of middle columns
+ slots.extend(col[len(col) // 2:] for col in middle_columns)
+ # edge/buffer columns (both halves)
+ for col in edge_columns:
+ slots.extend([col[:len(col) // 2], col[len(col) // 2:]])
+
+ return slots
+
+ def _validate_plate_capacity(self, required_wells: int, plate):
+ """Validate that plate has sufficient wells for the protocol."""
+ available_wells = len(plate.wells())
+ if required_wells > available_wells:
+ raise ValueError(f'Protocol requires {required_wells} wells but plate only has {available_wells}')
+
+ def _define_liquid(self, protocol: protocol_api.ProtocolContext,
+ name: str, description: str, color_index: int = 0):
+ """Define and track a liquid for the protocol."""
+ color = colors[color_index % len(colors)]
+ liquid = protocol.define_liquid(name=name, description=description, display_color=color)
+ self.liquid_tracker[name] = liquid
+ return liquid
+
+
+class PlateSamples(SamplePreparation):
+ """
+ Distributes multiple samples across a plate with replicates.
+ Each sample gets distributed to a specified number of wells.
+ """
+
+ def __init__(self, samples: List[str],
+ sample_volume: float = 200,
+ sample_stock_volume: float = 1200,
+ replicates: int = 4,
+ starting_slot: int = 1,
+ temp_module_position: str = '1',
+ temp_module_labware: str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ tube_rack_position: str = '3',
+ tube_rack_labware: str = 'opentrons_24_tuberack_nest_1.5ml_snapcap',
+ **kwargs):
+ super().__init__(**kwargs)
+ self.samples = samples
+ self.sample_volume = sample_volume
+ self.sample_stock_volume = sample_stock_volume
+ self.replicates = replicates
+ self.starting_slot = starting_slot
+ self.temp_module_position = temp_module_position
+ self.temp_module_labware = temp_module_labware
+ self.tube_rack_position = tube_rack_position
+ self.tube_rack_labware = tube_rack_labware
+
+ self.source_positions = {}
+ self.plate_layout = {}
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+ # Load labware
+ tiprack, pipette, plate = self._load_standard_labware(protocol)
+ source_rack = self._load_source_labware(
+ protocol, self.temp_module_position, self.temp_module_labware,
+ self.tube_rack_position, self.tube_rack_labware
+ )
+
+ # Create slots and validate
+ slots = self._create_slots(plate, self.replicates)
+ required_wells = len(self.samples) * self.replicates
+ self._validate_plate_capacity(required_wells, plate)
+
+ if len(self.samples) > len(source_rack.wells()):
+ raise ValueError(
+ f'Too many samples ({len(self.samples)}) for source rack ({len(source_rack.wells())} wells)')
+
+ if len(self.samples) > len(slots[self.starting_slot - 1:]):
+ raise ValueError(f'Too many samples ({len(self.samples)}) for available slots')
+
+ # Load samples into source rack
+ sample_wells = self._load_samples(protocol, source_rack)
+
+ # Distribute samples to plate
+ slot_counter = self.starting_slot - 1
+ for source_well, sample in sample_wells:
+ dest_wells = slots[slot_counter][:self.replicates]
+ pipette.distribute(
+ volume=self.sample_volume,
+ source=source_well,
+ dest=dest_wells,
+ disposal_volume=0
+ )
+ self.plate_layout[sample] = [well.well_name for well in dest_wells]
+ slot_counter += 1
+
+ # Store results
+ self.result_dict = {
+ 'source_positions': self.source_positions,
+ 'plate_layout': self.plate_layout
+ }
+
+ print('Sample Distribution Protocol Complete')
+ print(f'Source positions: {self.source_positions}')
+ print(f'Plate layout: {self.plate_layout}')
+
+ def _load_samples(self, protocol: protocol_api.ProtocolContext, source_rack):
+ """Load samples into source rack with liquid tracking."""
+ sample_wells = []
+ for i, sample in enumerate(self.samples):
+ liquid = self._define_liquid(protocol, sample, f"Sample: {sample}", i)
+ well = source_rack.wells()[i]
+ well.load_liquid(liquid=liquid, volume=self.sample_stock_volume)
+ self.source_positions[sample] = well.well_name
+ sample_wells.append((well, sample))
+ return sample_wells
+
+
+class PlateWithGradient(SamplePreparation):
+ """
+ Creates serial dilution gradients of an inducer with a sample.
+ Implements proper well-to-well serial dilution.
+ """
+
+ def __init__(self,
+ sample_name: str,
+ inducer_name: str,
+ initial_concentration: float = 1.0,
+ dilution_factor: float = 2.0,
+ dilution_steps: int = 8,
+ replicates: int = 3,
+ starting_row: str = 'A',
+ final_well_volume: float = 200,
+ initial_mix_ratio: float = 0.5, # fraction of inducer in initial mix
+ transfer_volume: float = 100, # volume transferred in each dilution step
+ sample_stock_volume: float = 1200,
+ inducer_stock_volume: float = 1200,
+ temp_module_position: str = '1',
+ temp_module_labware: str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ tube_rack_position: str = '3',
+ tube_rack_labware: str = 'opentrons_24_tuberack_nest_1.5ml_snapcap',
+ **kwargs):
+
+ super().__init__(**kwargs)
+ self.sample_name = sample_name
+ self.inducer_name = inducer_name
+ self.initial_concentration = initial_concentration
+ self.dilution_factor = dilution_factor
+ self.dilution_steps = dilution_steps
+ self.replicates = replicates
+ self.starting_row = starting_row
+ self.final_well_volume = final_well_volume
+ self.initial_mix_ratio = initial_mix_ratio
+ self.transfer_volume = transfer_volume
+ self.sample_stock_volume = sample_stock_volume
+ self.inducer_stock_volume = inducer_stock_volume
+ self.temp_module_position = temp_module_position
+ self.temp_module_labware = temp_module_labware
+ self.tube_rack_position = tube_rack_position
+ self.tube_rack_labware = tube_rack_labware
+
+ # Calculated properties
+ self.concentration_series = self._calculate_concentrations()
+ self.required_volumes = self._calculate_required_volumes()
+ self.source_positions = {}
+ self.plate_layout = {}
+ self.concentration_map = {}
+
+ def _calculate_concentrations(self) -> List[float]:
+ """Calculate the concentration at each dilution step."""
+ concentrations = []
+ current_conc = self.initial_concentration
+ for step in range(self.dilution_steps + 1): # +1 for initial concentration
+ concentrations.append(current_conc)
+ current_conc = current_conc / self.dilution_factor
+ return concentrations
+
+ def _calculate_required_volumes(self) -> dict:
+ """Calculate required stock volumes with safety margin."""
+ # Initial mix volume per replicate
+ initial_mix_volume = self.final_well_volume
+ total_initial_mix = initial_mix_volume * self.replicates
+
+ # Sample volume needed (including initial mix and pre-filling wells)
+ sample_per_well = self.final_well_volume - self.transfer_volume
+ sample_for_prefill = sample_per_well * self.dilution_steps * self.replicates
+ sample_for_initial = total_initial_mix * (1 - self.initial_mix_ratio)
+ total_sample = sample_for_prefill + sample_for_initial
+
+ # Inducer volume needed
+ inducer_for_initial = total_initial_mix * self.initial_mix_ratio
+ total_inducer = inducer_for_initial
+
+ # Add 20% safety margin and round up
+ safety_factor = 1.2
+ return {
+ 'sample': math.ceil(total_sample * safety_factor),
+ 'inducer': math.ceil(total_inducer * safety_factor)
+ }
+
+ def _row_letter_to_index(self, letter: str) -> int:
+ """Convert row letter to 0-based index."""
+ return ord(letter.upper()) - ord('A')
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+ # Load labware
+ tiprack, pipette, plate = self._load_standard_labware(protocol)
+ source_rack = self._load_source_labware(
+ protocol, self.temp_module_position, self.temp_module_labware,
+ self.tube_rack_position, self.tube_rack_labware
+ )
+
+ # Validate plate capacity
+ required_wells = self.replicates * (self.dilution_steps + 1)
+ self._validate_plate_capacity(required_wells, plate)
+
+ # Load stocks
+ self._load_stocks(protocol, source_rack)
+
+ # Get source wells
+ sample_well = source_rack.wells()[0]
+ inducer_well = source_rack.wells()[1]
+
+ # Calculate layout
+ start_row_idx = self._row_letter_to_index(self.starting_row)
+
+ # Pre-fill wells with sample (diluent)
+ self._prefill_wells(pipette, plate, sample_well, start_row_idx)
+
+ # Create initial mixes and perform serial dilutions
+ self._create_gradients(pipette, plate, sample_well, inducer_well, start_row_idx)
+
+ # Store results
+ self.result_dict = {
+ 'source_positions': self.source_positions,
+ 'plate_layout': self.plate_layout,
+ 'concentration_map': self.concentration_map,
+ 'concentration_series': self.concentration_series,
+ 'required_volumes': self.required_volumes
+ }
+
+ print('Serial Dilution Protocol Complete')
+ print(f'Concentration series: {self.concentration_series}')
+ print(f'Required volumes: {self.required_volumes}')
+ print(f'Source positions: {self.source_positions}')
+ print(f'Concentration map: {self.concentration_map}')
+
+ def _load_stocks(self, protocol: protocol_api.ProtocolContext, source_rack):
+ """Load sample and inducer stocks."""
+ # Sample stock
+ sample_liquid = self._define_liquid(protocol, self.sample_name,
+ f"Sample: {self.sample_name}", 0)
+ sample_well = source_rack.wells()[0]
+ sample_well.load_liquid(liquid=sample_liquid, volume=self.sample_stock_volume)
+ self.source_positions[self.sample_name] = sample_well.well_name
+
+ # Inducer stock
+ inducer_liquid = self._define_liquid(protocol, self.inducer_name,
+ f"Inducer: {self.inducer_name}", 1)
+ inducer_well = source_rack.wells()[1]
+ inducer_well.load_liquid(liquid=inducer_liquid, volume=self.inducer_stock_volume)
+ self.source_positions[self.inducer_name] = inducer_well.well_name
+
+ def _prefill_wells(self, pipette, plate, sample_well, start_row_idx):
+ """Pre-fill wells with sample to serve as diluent."""
+ diluent_volume = self.final_well_volume - self.transfer_volume
+
+ for rep in range(self.replicates):
+ row_idx = start_row_idx + rep
+ row = plate.rows()[row_idx]
+
+ # Pre-fill wells 2 through dilution_steps+1 (skip first well for initial mix)
+ dest_wells = row[1:self.dilution_steps + 1]
+
+ pipette.distribute(
+ volume=diluent_volume,
+ source=sample_well,
+ dest=dest_wells,
+ disposal_volume=0
+ )
+
+ def _create_gradients(self, pipette, plate, sample_well, inducer_well, start_row_idx):
+ """Create initial mixes and perform serial dilutions."""
+
+ # Calculate initial mix volumes
+ initial_sample_vol = self.final_well_volume * (1 - self.initial_mix_ratio)
+ initial_inducer_vol = self.final_well_volume * self.initial_mix_ratio
+
+ for rep in range(self.replicates):
+ row_idx = start_row_idx + rep
+ row = plate.rows()[row_idx]
+
+ # Create initial mix in first well
+ first_well = row[0]
+
+ # Add sample to first well
+ pipette.transfer(
+ volume=initial_sample_vol,
+ source=sample_well,
+ dest=first_well,
+ new_tip='always'
+ )
+
+ # Add inducer to first well and mix
+ pipette.transfer(
+ volume=initial_inducer_vol,
+ source=inducer_well,
+ dest=first_well,
+ mix_after=(3, self.final_well_volume * 0.5),
+ new_tip='always'
+ )
+
+ # Perform serial dilution across the row
+ for step in range(self.dilution_steps):
+ source_well = row[step]
+ dest_well = row[step + 1]
+
+ pipette.transfer(
+ volume=self.transfer_volume,
+ source=source_well,
+ dest=dest_well,
+ mix_after=(3, self.final_well_volume * 0.5),
+ new_tip='always'
+ )
+
+ # Record layout and concentrations for this replicate
+ for step in range(self.dilution_steps + 1):
+ well_name = row[step].well_name
+ concentration = self.concentration_series[step]
+
+ if f'replicate_{rep}' not in self.plate_layout:
+ self.plate_layout[f'replicate_{rep}'] = []
+
+ self.plate_layout[f'replicate_{rep}'].append(well_name)
+ self.concentration_map[well_name] = concentration
diff --git a/src/pudu/test_setup.py b/src/pudu/test_setup.py
deleted file mode 100644
index b39bf03..0000000
--- a/src/pudu/test_setup.py
+++ /dev/null
@@ -1,214 +0,0 @@
-from opentrons import protocol_api
-from typing import List, Union
-from pudu.utils import thermo_wells, temp_wells, liquid_transfer, slots, rows, row_letter_to_number
-
-class Test_setup():
- '''
- Creates a protocol for the automated setting of a 96 well plate with a gradient of inducer.
-
- '''
- def __init__(self,
- test_labware:str = 'corning_96_wellplate_360ul_flat',
- test_position:int = 7,
- aspiration_rate:float=0.5,
- dispense_rate:float=1,):
-
- self.test_labware = test_labware
- self.test_position = test_position
- self.aspiration_rate = aspiration_rate
- self.dispense_rate = dispense_rate
-
-class Plate_samples(Test_setup):
- '''
- Creates a protocol for the automated setting of a 96 well plate from samples. Each sample is distributed in 4 wells of the plate.
- '''
- def __init__(self,samples:List,
- sample_tube_volume:float = 1200,
- sample_well_volume:float = 200,
- tube_rack_labware:str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
- tube_rack_position:int = 4,
- tiprack_labware:str='opentrons_96_tiprack_300ul',
- tiprack_position:int=9,
- pipette:str='p300_single_gen2',
- pipette_position:str='right',
- use_temperature_module:bool = False,
- *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self.samples = samples
- self.sample_tube_volume = sample_tube_volume
- self.sample_well_volume = sample_well_volume
- self.tube_rack_labware = tube_rack_labware
- self.tube_rack_position = tube_rack_position
- self.tiprack_labware = tiprack_labware
- self.tiprack_position = tiprack_position
- self.pipette = pipette
- self.pipette_position = pipette_position
- self.use_temperature_module = use_temperature_module
- self.dict_of_samples_in_plate = {}
- self.dict_of_samples_in_temp_mod_position = {}
-
- if len(self.samples) > 24:
- raise ValueError(f'Number of samples cant be greater than 24, you have {len(self.samples)}')
-
- def run(self, protocol: protocol_api.ProtocolContext):
-
- #Labware
- tiprack = protocol.load_labware(self.tiprack_labware, f'{self.tiprack_position}')
- pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
- plate = protocol.load_labware(self.test_labware, self.test_position)
-
- if self.use_temperature_module:
- temperature_module = protocol.load_module('Temperature Module', self.tube_rack_position)
- tube_rack = temperature_module.load_labware(self.tube_rack_labware)
- else:
- tube_rack = protocol.load_labware(self.tube_rack_labware, self.tube_rack_position)
-
- #Protocol
-
- #Load samples
- temp_wells_counter = 0
- for sample in self.samples:
- self.dict_of_samples_in_temp_mod_position[sample] = temp_wells[temp_wells_counter]
- pipette.pick_up_tip()
- for position in slots[temp_wells_counter]:
- liquid_transfer(pipette, self.sample_well_volume, tube_rack[self.dict_of_samples_in_temp_mod_position[sample]], plate[position], self.aspiration_rate, self.dispense_rate, mix_before=self.sample_well_volume, mix_after=self.sample_well_volume/2, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- self.dict_of_samples_in_plate[sample] = slots[temp_wells_counter]
- temp_wells_counter += 1
-
- #output
- print('Samples in tube rack')
- print(self.dict_of_samples_in_temp_mod_position)
- print('Samples in plate')
- print(self.dict_of_samples_in_plate)
- #END
-
-
-
-class Plate_supplemented_samples(Test_setup):
- '''
- Creates a protocol for the automated setting of a 96 well plate with a gradient of inducer.
-
- '''
- def __init__(self,sample_name:str,
- inducer_name:str,
- inducer_initial_concentration:float = 1,
- initial_mix_inducer_volume:float = 50.0,
- initial_mix_sample_volume:float = 50.0,
- serial_dilution_volume:float = 20,
- serial_dilution_steps:int = 10,
- number_of_replicates:int = 3,
- starting_row:str = 'A',
- sample_volume_plate:float = 200,
- sample_labware:str = '96 well plate',
- aspiration_rate:float=0.5,
- dispense_rate:float=1,
- tube_rack_labware:str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
- tube_rack_position:int = 4,
- tiprack_labware:str='opentrons_96_tiprack_300ul',
- tiprack_position:int=9,
- pipette:str='p300_single_gen2',
- pipette_position:str='left',
- use_temperature_module:bool = False,
- *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self.sample_name = sample_name
- self.inducer_name = inducer_name
- self.inducer_initial_concentration = inducer_initial_concentration
- self.initial_mix_sample_volume = initial_mix_sample_volume
- self.initial_mix_inducer_volume = initial_mix_inducer_volume
- self.serial_dilution_volume = serial_dilution_volume
- self.serial_dilution_steps = serial_dilution_steps
- self.number_of_replicates = number_of_replicates
- self.starting_row = starting_row
- self.sample_volume_plate = sample_volume_plate
- self.sample_labware = sample_labware
- self.aspiration_rate = aspiration_rate
- self.dispense_rate = dispense_rate
- self.tube_rack_labware = tube_rack_labware
- self.tube_rack_position = tube_rack_position
- self.tiprack_labware = tiprack_labware
- self.tiprack_position = tiprack_position
- self.pipette = pipette
- self.pipette_position = pipette_position
- self.use_temperature_module = use_temperature_module
- self.dict_of_samples_in_plate = {}
- self.dict_of_tubes_in_temp_mod_position = {}
-
- if self.serial_dilution_steps == 0:
- raise ValueError('Number of serial dilution steps must be greater than 0')
- if self.number_of_replicates < 3:
- raise Warning('Number of replicates must be greater than 3 for statistical analysis')
- if self.number_of_replicates > 8:
- raise ValueError('Number of replicates must be less than 8 to fit in the 96 well plate')
-
- def run(self, protocol: protocol_api.ProtocolContext):
-
- #Labware
- tiprack = protocol.load_labware(self.tiprack_labware, f'{self.tiprack_position}')
- pipette = protocol.load_instrument(self.pipette, self.pipette_position, tip_racks=[tiprack])
- plate = protocol.load_labware(self.test_labware, self.test_position)
-
- if self.use_temperature_module:
- temperature_module = protocol.load_module('Temperature Module', self.tube_rack_position)
- tube_rack = temperature_module.load_labware(self.tube_rack_labware)
- else:
- tube_rack = protocol.load_labware(self.tube_rack_labware, self.tube_rack_position)
-
- # Protocol
-
- # Load inducer
- temp_wells_counter = 0
- self.dict_of_tubes_in_temp_mod_position[self.inducer_name] = temp_wells[temp_wells_counter]
- temp_wells_counter += 1
-
- # Load samples and distribute them in the plate
- start_at_row = row_letter_to_number[self.starting_row] - 1
-
- for replicate_number in range(self.number_of_replicates + 1):
- working_row_index = start_at_row + replicate_number
- self.dict_of_tubes_in_temp_mod_position[f'replicate_{replicate_number}'] = temp_wells[temp_wells_counter]
- pipette.pick_up_tip()
- for dilution_step in range(self.serial_dilution_steps):
- position = rows[working_row_index][dilution_step+1]
- liquid_transfer(pipette, self.sample_volume_plate, tube_rack[self.dict_of_tubes_in_temp_mod_position[f'replicate_{replicate_number}']], plate[position], self.aspiration_rate, self.dispense_rate, new_tip=False, drop_tip=False)
- pipette.drop_tip()
- temp_wells_counter += 1
- working_row_index += 1
-
-
- # Create initial mix on the first row
- initial_mix_position = rows[start_at_row][0]
- self.dict_of_samples_in_plate['initial_mix'] = initial_mix_position
- #pipette sample to initial mix
- liquid_transfer(pipette, self.initial_mix_sample_volume, tube_rack[self.dict_of_tubes_in_temp_mod_position['replicate_1']], plate[initial_mix_position], self.aspiration_rate, self.dispense_rate)
- #pipette inducer to initial mix
- liquid_transfer(pipette, self.initial_mix_inducer_volume, tube_rack[self.dict_of_tubes_in_temp_mod_position[self.inducer_name]], plate[initial_mix_position], self.aspiration_rate, self.dispense_rate,mix_after=self.initial_mix_sample_volume/2)
-
- # Serial dilution
- for replicate_number in range(self.number_of_replicates):
- working_row_index = start_at_row + replicate_number
- pipette.pick_up_tip()
- for dilution_step in range(self.serial_dilution_steps):
- position = rows[working_row_index][dilution_step+1]
- liquid_transfer(pipette, self.serial_dilution_volume, tube_rack[self.dict_of_tubes_in_temp_mod_position[f'replicate_{replicate_number}']], plate[position], self.aspiration_rate, self.dispense_rate, mix_before=self.sample_volume_plate/2, new_tip=False, drop_tip=False)
- self.dict_of_samples_in_plate[f'replicate_{replicate_number}_dilution{dilution_step}'] = position
- pipette.drop_tip()
- working_row_index += 1
-
- #output
- print('Samples and inducer in tube rack')
- print(self.dict_of_tubes_in_temp_mod_position)
- print('Samples in plate')
- print(self.dict_of_samples_in_plate)
- #END
-
-
-
-class Doe_test(Test_setup):
- '''
- Creates a protocol for the automated setting of a 96 well plate with a gradient of inducer.
-
- '''
diff --git a/src/pudu/transformation.py b/src/pudu/transformation.py
index b9dd9fd..b90ea0e 100644
--- a/src/pudu/transformation.py
+++ b/src/pudu/transformation.py
@@ -1,265 +1,946 @@
from opentrons import protocol_api
-from pudu.utils import thermo_wells, temp_wells, liquid_transfer
-from typing import List, Dict
+from typing import List, Dict, Optional
+from pudu.utils import colors
class Transformation():
'''
- Creates a protocol for automated transformation.
+ Base class for automated transformation protocols on the Opentrons OT-2.
+
+ Handles loading transformation data, validating parameters, and providing
+ shared utilities used by all transformation subclasses. Subclasses implement
+ the specific thermocycler workflow (e.g. heat shock).
Attributes
----------
volume_dna : float
- The volume DNA in microliters. By default, 2 microliters. We suggest 2 microliters for extracted plasmid and 5 microliters for PCR products.
- volume_competent_cells : float
- The volume of the competent cells in microliters. By default, 50 microliters.
- volume_recovery_media : float
- The volume of recovery media in microliters. By default, 100 microliters.
- replicates : int
- The number of replicates of the assembly reaction. By default, 2.
- thermocycler_starting_well : int
- The starting well of the thermocycler module. By default, 0.
+ Volume of DNA loaded into each source well, in microliters. By default,
+ 20 microliters. We suggest 2 µL for extracted plasmid and 5 µL for PCR
+ products when setting transfer_volume_dna in the subclass.
+ replicates : int
+ Number of transformation replicates per strain per assembly location.
+ By default, 2.
+ thermocycler_starting_well : int
+ Zero-indexed starting well in the thermocycler plate. By default, 0 (well A1).
thermocycler_labware : str
- The labware type of the thermocycler module. By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
- thermocycler_slots : list
- The slots of the thermocycler module. By default, [7, 8, 10, 11].
+ Labware type for the thermocycler plate.
+ By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
temperature_module_labware : str
- The labware type of the temperature module. By default, 'opentrons_24_aluminumblock_nest_1.5ml_snapcap'.
- temperature_module_slot : int
- The slot of the temperature module. By default, 1.
- tiprack_labware : str
- The labware type of the tiprack. By default, 'opentrons_96_tiprack_20ul'.
- tiprack_slot : int
- The slot of the tiprack. By default, 9.
- pipette_type : str
- The type of pipette. By default, 'p20_single_gen2'.
- pipette_mount : str
- The mount of the pipette. By default, 'left'.
+ Labware type for the aluminum block on the temperature module.
+ By default, 'opentrons_24_aluminumblock_nest_1.5ml_snapcap'.
+ temperature_module_position : str
+ Deck slot for the temperature module. By default, '1'.
+ dna_plate : str
+ Labware type for the 96-well DNA source plate (used when use_dna_96plate=True).
+ By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
+ dna_plate_position : str
+ Deck slot for the 96-well DNA source plate. By default, '2'.
+ use_dna_96plate : bool
+ If True, DNA is sourced from a 96-well plate at fixed positions given by
+ plasmid_locations. Automatically set to True when plasmid_locations is
+ provided. By default, False.
+ tiprack_p20_labware : str
+ Labware type for the p20 tip rack. By default, 'opentrons_96_tiprack_20ul'.
+ tiprack_p20_position : str
+ Deck slot for the p20 tip rack. By default, '9'.
+ tiprack_p200_labware : str
+ Labware type for the p200 tip rack.
+ By default, 'opentrons_96_filtertiprack_200ul'.
+ tiprack_p200_position : str
+ Deck slot for the p200 tip rack. By default, '6'.
+ pipette_p20 : str
+ Pipette model for the p20 single-channel. By default, 'p20_single_gen2'.
+ pipette_p20_position : str
+ Mount for the p20 pipette ('left' or 'right'). By default, 'left'.
+ pipette_p300 : str
+ Pipette model for the p300 single-channel. By default, 'p300_single_gen2'.
+ pipette_p300_position : str
+ Mount for the p300 pipette ('left' or 'right'). By default, 'right'.
aspiration_rate : float
- The rate of aspiration in microliters per second. By default, 0.5 microliters per second.
+ Relative aspiration speed as a fraction of the pipette's maximum flow
+ rate, where 1.0 is full speed and 0.5 is half speed. Lower values
+ reduce bubble formation. By default, 0.5.
dispense_rate : float
- The rate of dispense in microliters per second. By default, 1 microliter per second.
+ Relative dispense speed as a fraction of the pipette's maximum flow
+ rate, where 1.0 is full speed. By default, 1.0.
+ initial_dna_well : int
+ Zero-indexed starting well for DNA tubes on the aluminum block (used when
+ use_dna_96plate=False). By default, 0.
+ water_testing : bool
+ If True, uses water in place of competent cells and recovery media during
+ simulation/testing runs. By default, False.
'''
def __init__(self,
- list_of_dnas:List = [], # TODO:add SBOL version
- competent_cells: str = None, # TODO: add SBOL version
- replicates:int=2,
- thermocycler_starting_well:int = 0,
- thermocycler_labware:str = 'nest_96_wellplate_100ul_pcr_full_skirt',
- temperature_module_labware:str = 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
- temperature_module_position:int = 1,
-
- tiprack_p20_labware:str = 'opentrons_96_tiprack_20ul',
- tiprack_p20_osition:int = 9,
- tiprack_p300_labware:str = 'opentrons_96_tiprack_300ul',
- tiprack_p300_osition:int = 6,
- pipette_p20:str = 'p20_single_gen2',
- pipette_p300:str = 'p300_single_gen2',
- pipette_p20_position:str = 'left',
- pipette_p300_position:str = 'right',
- aspiration_rate:float=0.5,
- dispense_rate:float=1,):
-
- self.list_of_dnas = list_of_dnas
- self.competent_cells = competent_cells
- self.replicates = replicates
- self.thermocycler_starting_well = thermocycler_starting_well
- self.thermocycler_labware = thermocycler_labware
- self.temperature_module_labware = temperature_module_labware
- self.temperature_module_position = temperature_module_position
- self.tiprack_p20_labware = tiprack_p20_labware
- self.tiprack_p20_position = tiprack_p20_osition
- self.tiprack_p300_labware = tiprack_p300_labware
- self.tiprack_p300_position = tiprack_p300_osition
- self.pipette_p20 = pipette_p20
- self.pipette_p300 = pipette_p300
- self.pipette_p20_position = pipette_p20_position
- self.pipette_p300_position = pipette_p300_position
- self.aspiration_rate = aspiration_rate
- self.dispense_rate = dispense_rate
-
-class Chemical_transformation(Transformation):
+ transformation_data: Optional[List] = None,
+ plasmid_locations: Optional[Dict] = None,
+ json_params: Optional[Dict] = None,
+ volume_dna:float = 20,
+ replicates:int=2,
+ thermocycler_starting_well:int = 0,
+ thermocycler_labware:str = "nest_96_wellplate_100ul_pcr_full_skirt",
+ temperature_module_labware:str = "opentrons_24_aluminumblock_nest_1.5ml_snapcap",
+ temperature_module_position:str = '1',
+ dna_plate:str = "nest_96_wellplate_100ul_pcr_full_skirt",
+ dna_plate_position:str = '2',
+ use_dna_96plate:bool = False,
+ tiprack_p20_labware:str = "opentrons_96_tiprack_20ul",
+ tiprack_p20_position:str = "9",
+ tiprack_p200_labware:str = "opentrons_96_filtertiprack_200ul",
+ tiprack_p200_position:str = "6",
+ pipette_p20:str = "p20_single_gen2",
+ pipette_p20_position:str = "left",
+ pipette_p300:str = "p300_single_gen2",
+ pipette_p300_position:str = "right",
+ aspiration_rate:float = 0.5,
+ dispense_rate:float = 1,
+ initial_dna_well:int = 0,
+ water_testing:bool = False,
+ **kwargs
+ ):
+
+ kwargs_params = {
+ 'volume_dna': volume_dna,
+ 'replicates': replicates,
+ 'thermocycler_starting_well': thermocycler_starting_well,
+ 'thermocycler_labware': thermocycler_labware,
+ 'temperature_module_labware': temperature_module_labware,
+ 'temperature_module_position': temperature_module_position,
+ 'dna_plate': dna_plate,
+ 'dna_plate_position': dna_plate_position,
+ 'use_dna_96plate': use_dna_96plate,
+ 'tiprack_p20_labware': tiprack_p20_labware,
+ 'tiprack_p20_position': tiprack_p20_position,
+ 'tiprack_p200_labware': tiprack_p200_labware,
+ 'tiprack_p200_position': tiprack_p200_position,
+ 'pipette_p20': pipette_p20,
+ 'pipette_p20_position': pipette_p20_position,
+ 'pipette_p300': pipette_p300,
+ 'pipette_p300_position': pipette_p300_position,
+ 'aspiration_rate': aspiration_rate,
+ 'dispense_rate': dispense_rate,
+ 'initial_dna_well': initial_dna_well,
+ 'water_testing': water_testing
+ }
+ kwargs_params.update(kwargs)
+
+ self._merged_params = self._merge_params(transformation_data, json_params, kwargs_params)
+
+ # Parse and validate transformation data (new SBOL format)
+ if transformation_data is None:
+ raise ValueError("Must provide transformation_data as a list of transformation dictionaries")
+
+ self.plasmid_locations = plasmid_locations # URI -> [well, well, ...] from assembly output
+ self.transformations, self.all_plasmids, self.all_chassis = self._parse_transformation_data(transformation_data)
+
+ # Set all attributes from merged parameters
+ self.volume_dna = self._merged_params['volume_dna']
+ self.replicates = self._merged_params['replicates']
+ self.thermocycler_starting_well = self._merged_params['thermocycler_starting_well']
+ self.thermocycler_labware = self._merged_params['thermocycler_labware']
+ self.temperature_module_labware = self._merged_params['temperature_module_labware']
+ self.temperature_module_position = self._merged_params['temperature_module_position']
+ self.dna_plate = self._merged_params['dna_plate']
+ self.dna_plate_position = self._merged_params['dna_plate_position']
+ self.use_dna_96plate = self._merged_params['use_dna_96plate']
+
+ # 96-well plate and plasmid_locations are always paired:
+ # positions on the plate are fixed by the assembly run and cannot be assumed sequential
+ if self.use_dna_96plate and self.plasmid_locations is None:
+ raise ValueError("plasmid_locations must be provided when use_dna_96plate=True. "
+ "Well positions on the assembly plate are fixed and cannot be assumed sequential.")
+ if self.plasmid_locations is not None:
+ self.use_dna_96plate = True
+ self.tiprack_p20_labware = self._merged_params['tiprack_p20_labware']
+ self.tiprack_p20_position = self._merged_params['tiprack_p20_position']
+ self.tiprack_p200_labware = self._merged_params['tiprack_p200_labware']
+ self.tiprack_p200_position = self._merged_params['tiprack_p200_position']
+ self.pipette_p20 = self._merged_params['pipette_p20']
+ self.pipette_p20_position = self._merged_params['pipette_p20_position']
+ self.pipette_p300 = self._merged_params['pipette_p300']
+ self.pipette_p300_position = self._merged_params['pipette_p300_position']
+ self.aspiration_rate = self._merged_params['aspiration_rate']
+ self.dispense_rate = self._merged_params['dispense_rate']
+ self.initial_dna_well = self._merged_params['initial_dna_well']
+ self.water_testing = self._merged_params['water_testing']
+
+ def _extract_name_from_uri(self, uri: str) -> str:
+ """Extract name from SBOL URI"""
+ if '/' in uri:
+ name_with_version = uri.split('/')[-2]
+ if '/' in name_with_version:
+ return name_with_version.split('/')[0]
+ return name_with_version
+ return uri
+
+ def _parse_transformation_data(self, transformation_data):
+ """
+ Parse new SBOL-style transformation data format.
+
+ Expected format:
+ [
+ {
+ "Strain": "https://SBOL2Build.org/composite_1/1",
+ "Chassis": "https://sbolcanvas.org/DH5alpha/1",
+ "Plasmids": ["https://...", "https://..."]
+ },
+ ...
+ ]
+
+ Returns:
+ transformations: List of dicts with strain, chassis, and plasmids
+ all_plasmids: Flat list of all unique plasmids
+ all_chassis: List of unique chassis types
+ """
+ if not isinstance(transformation_data, list):
+ raise ValueError("transformation_data must be a list of transformation dictionaries")
+
+ transformations = []
+ all_plasmids_set = set()
+ all_chassis_set = set()
+
+ for idx, transformation in enumerate(transformation_data):
+ # Validate required fields
+ if 'Strain' not in transformation:
+ raise ValueError(f"Transformation {idx} missing 'Strain' field")
+ if 'Chassis' not in transformation:
+ raise ValueError(f"Transformation {idx} missing 'Chassis' field")
+ if 'Plasmids' not in transformation:
+ raise ValueError(f"Transformation {idx} missing 'Plasmids' field")
+ if not isinstance(transformation['Plasmids'], list):
+ raise ValueError(f"Transformation {idx}: 'Plasmids' must be a list")
+ if len(transformation['Plasmids']) == 0:
+ raise ValueError(f"Transformation {idx}: 'Plasmids' list cannot be empty")
+
+ # Extract names from URIs
+ strain_name = self._extract_name_from_uri(transformation['Strain'])
+ chassis_name = self._extract_name_from_uri(transformation['Chassis'])
+ plasmid_uris = transformation['Plasmids']
+ plasmid_names = [self._extract_name_from_uri(p) for p in plasmid_uris]
+
+ # If plasmid_locations provided, validate all plasmid URIs are present
+ if self.plasmid_locations is not None:
+ for uri in plasmid_uris:
+ if uri not in self.plasmid_locations:
+ raise ValueError(
+ f"Plasmid URI '{uri}' not found in plasmid_locations. "
+ f"Available URIs: {list(self.plasmid_locations.keys())}"
+ )
+
+ transformations.append({
+ 'strain': strain_name,
+ 'chassis': chassis_name,
+ 'plasmids': plasmid_names,
+ 'plasmid_uris': plasmid_uris
+ })
+
+ all_plasmids_set.update(plasmid_names)
+ all_chassis_set.add(chassis_name)
+
+ return transformations, list(sorted(all_plasmids_set)), list(sorted(all_chassis_set))
+
+ def _merge_params(self, transformation_data: Optional[Dict], advanced_params: Optional[Dict], kwargs_params: Dict) -> Dict:
+ """
+ Merge parameters with precedence: defaults <- transformation_data <- advanced_params <- kwargs
+
+ Args:
+ transformation_data: Optional dict containing protocol data (list_of_dna, competent_cells)
+ advanced_params: Optional dict containing configuration parameters
+ kwargs_params: Dict of parameters passed as kwargs
+
+ Returns:
+ Merged parameter dictionary
+ """
+ # Define defaults for all valid parameters
+ # Includes both Transformation and HeatShockTransformation parameters
+ valid_params = {
+ # Transformation base parameters
+ 'volume_dna': 20,
+ 'replicates': 2,
+ 'thermocycler_starting_well': 0,
+ 'thermocycler_labware': 'nest_96_wellplate_100ul_pcr_full_skirt',
+ 'temperature_module_labware': 'opentrons_24_aluminumblock_nest_1.5ml_snapcap',
+ 'temperature_module_position': '1',
+ 'dna_plate': 'nest_96_wellplate_100ul_pcr_full_skirt',
+ 'dna_plate_position': '2',
+ 'use_dna_96plate': False,
+ 'tiprack_p20_labware': 'opentrons_96_tiprack_20ul',
+ 'tiprack_p20_position': '9',
+ 'tiprack_p200_labware': 'opentrons_96_filtertiprack_200ul',
+ 'tiprack_p200_position': '6',
+ 'pipette_p20': 'p20_single_gen2',
+ 'pipette_p20_position': 'left',
+ 'pipette_p300': 'p300_single_gen2',
+ 'pipette_p300_position': 'right',
+ 'aspiration_rate': 0.5,
+ 'dispense_rate': 1,
+ 'initial_dna_well': 0,
+ 'water_testing': False,
+ # HeatShockTransformation-specific parameters
+ 'transfer_volume_dna': 2,
+ 'transfer_volume_competent_cell': 20,
+ 'tube_volume_competent_cell': 100,
+ 'transfer_volume_recovery_media': 60,
+ 'tube_volume_recovery_media': 1200,
+ 'cold_incubation1': None,
+ 'heat_shock': None,
+ 'cold_incubation2': None,
+ 'recovery_incubation': None
+ }
+
+ # Start with defaults
+ merged = valid_params.copy()
+
+ # Apply advanced_params (if provided)
+ if advanced_params is not None:
+ self._validate_param_structure(advanced_params, valid_params, 'advanced_params')
+ merged.update(advanced_params)
+
+ # Apply kwargs (highest precedence) - only if they differ from defaults
+ for key, value in kwargs_params.items():
+ if key in valid_params:
+ # Only override if the value is explicitly different from the default
+ if value != valid_params[key]:
+ merged[key] = value
+
+ return merged
+
+ def _validate_param_structure(self, params: Dict, valid_params: Dict, param_name: str):
+ """
+ Validate that all parameters in the dict are recognized.
+
+ Args:
+ params: Dictionary to validate
+ valid_params: Dictionary of valid parameter names
+ param_name: Name of the parameter dict (for error messages)
+
+ Raises:
+ ValueError: If unknown parameters are found
+ """
+ unknown_params = set(params.keys()) - set(valid_params.keys())
+ if unknown_params:
+ raise ValueError(
+ f"Unknown parameters in {param_name}: {unknown_params}.\n"
+ f"Valid parameters are: {set(valid_params.keys())}"
+ )
+
+class HeatShockTransformation(Transformation):
'''
- Creates a protocol for automated transformation.
+ Heat shock transformation protocol for the Opentrons OT-2.
+
+ Automates the full heat shock transformation workflow: loading DNA and competent
+ cells into a thermocycler plate, running the heat shock cycle, adding recovery
+ media, and exporting a plating map for the next protocol step.
+
+ Inherits all base parameters from Transformation. The attributes below are
+ specific to the heat shock transformation protocol.
Attributes
----------
- volume_dna : float
- The volume DNA in microliters. By default, 2 microliters. We suggest 2 microliters for extracted plasmid and 5 microliters for PCR products.
- volume_competent_cells : float
- The volume of the competent cells in microliters. By default, 50 microliters.
- volume_recovery_media : float
- The volume of recovery media in microliters. By default, 100 microliters.
- replicates : int
- The number of replicates of the assembly reaction. By default, 2.
- thermocycler_starting_well : int
- The starting well of the thermocycler module. By default, 0.
- thermocycler_labware : str
- The labware type of the thermocycler module. By default, 'nest_96_wellplate_100ul_pcr_full_skirt'.
- thermocycler_slots : list
- The slots of the thermocycler module. By default, [7, 8, 10, 11].
- temperature_module_labware : str
- The labware type of the temperature module. By default, 'opentrons_24_aluminumblock_nest_1.5ml_snapcap'.
- temperature_module_slot : int
- The slot of the temperature module. By default, 1.
- tiprack_labware : str
- The labware type of the tiprack. By default, 'opentrons_96_tiprack_20ul'.
- tiprack_slot : int
- The slot of the tiprack. By default, 9.
- pipette_type : str
- The type of pipette. By default, 'p20_single_gen2'.
- pipette_mount : str
- The mount of the pipette. By default, 'left'.
- aspiration_rate : float
- The rate of aspiration in microliters per second. By default, 0.5 microliters per second.
- dispense_rate : float
- The rate of dispense in microliters per second. By default, 1 microliter per second.
+ transfer_volume_dna : float
+ Volume of DNA to transfer into each thermocycler well, in microliters.
+ By default, 2 microliters. Note: this is the volume actually pipetted per
+ reaction, distinct from volume_dna (the volume loaded into the source well).
+ transfer_volume_competent_cell : float
+ Volume of competent cells to transfer into each thermocycler well, in
+ microliters. By default, 20 microliters.
+ tube_volume_competent_cell : float
+ Total usable volume of competent cells per tube, in microliters. Used to
+ calculate how many reactions each tube can supply before switching to the
+ next tube. By default, 100 microliters.
+ transfer_volume_recovery_media : float
+ Volume of recovery media to add to each well after heat shock, in
+ microliters. By default, 60 microliters.
+ tube_volume_recovery_media : float
+ Total usable volume of recovery media per tube, in microliters. Used to
+ calculate how many wells each tube can supply. By default, 1200 microliters.
+ cold_incubation1 : dict
+ First cold incubation step (on ice before heat shock). A dict with keys
+ 'temperature' (°C) and 'hold_time_minutes'.
+ By default, {'temperature': 4, 'hold_time_minutes': 30}.
+ heat_shock : dict
+ Heat shock step. A dict with keys 'temperature' (°C) and 'hold_time_minutes'.
+ By default, {'temperature': 42, 'hold_time_minutes': 1}.
+ cold_incubation2 : dict
+ Second cold incubation immediately after heat shock. A dict with keys
+ 'temperature' (°C) and 'hold_time_minutes'.
+ By default, {'temperature': 4, 'hold_time_minutes': 2}.
+ recovery_incubation : dict
+ Recovery incubation after recovery media addition. A dict with keys
+ 'temperature' (°C) and 'hold_time_minutes'.
+ By default, {'temperature': 37, 'hold_time_minutes': 60}.
'''
- def __init__(self,
- volume_dna:float = 2,
- volume_competent_cell_to_add:float = 20,
- volume_competent_cell_per_tube:float =100,
- volume_recovery_media_to_add:float = 60,
- volume_recovery_media_per_tube:float = 1200, #add a bit more to pick it properly
- cold_incubation1:Dict = {'temperature': 4, 'hold_time_minutes': 30},
- heat_shock:Dict = {'temperature': 42, 'hold_time_minutes': 1},
- cold_incubation2:Dict = {'temperature': 4, 'hold_time_minutes': 2},
- recovery_incubation:Dict = {'temperature': 37, 'hold_time_minutes': 60},
+ def __init__(self,
+ transformation_data: Optional[List] = None,
+ plasmid_locations: Optional[Dict] = None,
+ json_params: Optional[Dict] = None,
+ transfer_volume_dna:float = 2,
+ transfer_volume_competent_cell:float = 20,
+ tube_volume_competent_cell:float =100,
+ transfer_volume_recovery_media:float = 60,
+ tube_volume_recovery_media:float = 1200, #add a bit more to pick it properly
+ cold_incubation1:Optional[Dict] = None,
+ heat_shock:Optional[Dict] = None,
+ cold_incubation2:Optional[Dict] = None,
+ recovery_incubation:Optional[Dict] = None,
*args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self.volume_dna = volume_dna
- self.volume_competent_cell_to_add = volume_competent_cell_to_add
- self.volume_competent_cell_per_tube = volume_competent_cell_per_tube
- self.volume_recovery_media = volume_recovery_media_to_add
- self.volume_recovery_media_per_tube = volume_recovery_media_per_tube
- self.cold_incubation1 = cold_incubation1
- self.heat_shock = heat_shock
- self.cold_incubation2 = cold_incubation2
- self.recovery_incubation = recovery_incubation
+ super().__init__(
+ transformation_data=transformation_data,
+ plasmid_locations=plasmid_locations,
+ json_params=json_params,
+ transfer_volume_dna=transfer_volume_dna,
+ transfer_volume_competent_cell=transfer_volume_competent_cell,
+ tube_volume_competent_cell=tube_volume_competent_cell,
+ transfer_volume_recovery_media=transfer_volume_recovery_media,
+ tube_volume_recovery_media=tube_volume_recovery_media,
+ cold_incubation1=cold_incubation1,
+ heat_shock=heat_shock,
+ cold_incubation2=cold_incubation2,
+ recovery_incubation=recovery_incubation,
+ *args, **kwargs)
+
+ self.transfer_volume_dna = self._merged_params['transfer_volume_dna']
+ self.transfer_volume_competent_cell = self._merged_params['transfer_volume_competent_cell']
+ self.tube_volume_competent_cell = self._merged_params['tube_volume_competent_cell']
+ self.transfer_volume_recovery_media = self._merged_params['transfer_volume_recovery_media']
+ self.tube_volume_recovery_media = self._merged_params['tube_volume_recovery_media']
+
+ cold_incubation1 = self._merged_params['cold_incubation1']
+ heat_shock = self._merged_params['heat_shock']
+ cold_incubation2 = self._merged_params['cold_incubation2']
+ recovery_incubation = self._merged_params['recovery_incubation']
+
+ if cold_incubation1 is None:
+ self.cold_incubation1 = {'temperature': 4, 'hold_time_minutes': 30}
+ else:
+ self.cold_incubation1 = cold_incubation1
+
+ if heat_shock is None:
+ self.heat_shock = {'temperature': 42, 'hold_time_minutes': 1}
+ else:
+ self.heat_shock = heat_shock
+
+ if cold_incubation2 is None:
+ self.cold_incubation2 = {'temperature': 4, 'hold_time_minutes': 2}
+ else:
+ self.cold_incubation2 = cold_incubation2
+
+ if recovery_incubation is None:
+ self.recovery_incubation = {'temperature': 37, 'hold_time_minutes': 60}
+ else:
+ self.recovery_incubation = recovery_incubation
self.dict_of_parts_in_temp_mod_position = {}
self.dict_of_parts_in_thermocycler = {}
+ self.dict_of_parts_in_dna_plate = {}
+ self.plasmid_name_to_wells = {} # plasmid name -> [well_obj, ...], populated during loading
+
+ def _export_plating_input(self, protocol):
+ """
+ Export plating input JSON during simulation.
+ Args:
+ protocol: Protocol context
+ """
+ import json
+
+ plating_input = {
+ 'bacterium_locations': self.dict_of_parts_in_thermocycler
+ }
+
+ output_path = 'plating_input.json'
+ with open(output_path, 'w') as f:
+ json.dump(plating_input, f, indent=2)
+
+ protocol.comment("\n" + "="*70)
+ protocol.comment(f"Generated {output_path} for next protocol")
+ protocol.comment(f" Bacteria locations: {len(self.dict_of_parts_in_thermocycler)}")
+ protocol.comment("="*70)
- metadata = {
- 'protocolName': 'PUDU Transformation',
- 'author': 'Gonzalo Vidal ',
- 'description': 'Automated transformation protocol',
- 'apiLevel': '2.13'}
-
- def run(self, protocol: protocol_api.ProtocolContext):
-
- #Labware
- #Load the magnetic module
- tem_mod = protocol.load_module('temperature module', f'{self.temperature_module_position}')
- tem_mod_block = tem_mod.load_labware(self.temperature_module_labware)
- #Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
- thermocycler_mod = protocol.load_module('thermocycler module')
- thermocycler_mod_plate = thermocycler_mod.load_labware(self.thermocycler_labware)
- #Load the tiprack
- tiprack_p20 = protocol.load_labware(self.tiprack_p20_labware, f'{self.tiprack_p20_position}')
- tiprack_p300 = protocol.load_labware(self.tiprack_p300_labware, f'{self.tiprack_p300_position}')
- #Load the pipette
+ def liquid_transfer(self, protocol, pipette, volume, source, dest,
+ asp_rate: float = 0.5, disp_rate: float = 1.0,
+ blow_out: bool = True, touch_tip: bool = False,
+ mix_before: float = 0.0, mix_after: float = 0.0,
+ mix_reps: int = 3, new_tip: bool = True,
+ remove_air:bool = True, drop_tip: bool = True):
+ if new_tip:
+ pipette.pick_up_tip()
+
+ if mix_before > 0:
+ pipette.mix(mix_reps, mix_before, source)
+
+ pipette.aspirate(volume, source, rate=asp_rate)
+ pipette.dispense(volume, dest, rate=disp_rate)
+
+ if mix_after > 0:
+ pipette.mix(mix_reps, mix_after, dest)
+
+ if blow_out:
+ pipette.blow_out()
+
+ if remove_air:
+ for _ in range(2):
+ pipette.aspirate(20, dest.bottom(), rate=disp_rate)
+ pipette.dispense(20, dest.bottom(8), rate=disp_rate)
+
+ if touch_tip:
+ pipette.touch_tip(radius=0.5, v_offset=-14, speed=20)
+
+ if drop_tip:
+ pipette.drop_tip()
+
+ def run(self, protocol: protocol_api.ProtocolContext):
+ # Force water testing mode during simulation
+ if protocol.is_simulating():
+ self.water_testing = True
+ protocol.comment("Simulation detected - enabling water testing mode")
+ # Labware
+ # Load the temperature module
+ temperature_module = protocol.load_module('temperature module', self.temperature_module_position)
+ alumblock = temperature_module.load_labware(self.temperature_module_labware)
+ # Load the thermocycler module, its default location is on slots 7, 8, 10 and 11
+ thermocycler_module = protocol.load_module('thermocycler module')
+ pcr_plate = thermocycler_module.load_labware(self.thermocycler_labware)
+ #If using the 96-well pcr plate as a dna construct source
+ if self.use_dna_96plate:
+ dna_plate = protocol.load_labware(self.dna_plate, self.dna_plate_position)
+ # Load the tiprack
+ tiprack_p20 = protocol.load_labware(self.tiprack_p20_labware, self.tiprack_p20_position)
+ tiprack_p200 = protocol.load_labware(self.tiprack_p200_labware, self.tiprack_p200_position)
+ # Load the pipette
pipette_p20 = protocol.load_instrument(self.pipette_p20, self.pipette_p20_position, tip_racks=[tiprack_p20])
- pipette_p300 = protocol.load_instrument(self.pipette_p300, self.pipette_p300_position, tip_racks=[tiprack_p300])
-
- #Load the reagents
- #Check number of compenent cells and DNAs
- total_transformations = len(self.list_of_dnas)*self.replicates
- transformations_per_tube = int(self.volume_competent_cell_per_tube//self.volume_competent_cell_to_add)
- number_of_tubes_with_competent_cells_needed = int(total_transformations//transformations_per_tube+1) #TODO: make an int, maybe use sail
- #Check number of tubes with media
- transformations_per_media_tube = int(self.volume_recovery_media_per_tube//self.volume_recovery_media)
- number_of_tubes_with_media_needed = int(total_transformations//transformations_per_media_tube+1) #TODO: make an int, maybe use sail
- if len(self.list_of_dnas)+number_of_tubes_with_competent_cells_needed+number_of_tubes_with_media_needed > 24:
- raise ValueError(f'The number of reagents is more than 24. There are {len(self.list_of_dnas)} DNAs, {number_of_tubes_with_competent_cells_needed} tubes with competent cells and {number_of_tubes_with_media_needed} tubes with media. Please change the protocol and try again.')
- temp_wells_counter = 0
- for dna in self.list_of_dnas:
- self.dict_of_parts_in_temp_mod_position[dna] = temp_wells[temp_wells_counter]
- temp_wells_counter += 1
- for i in range(number_of_tubes_with_competent_cells_needed):
- self.dict_of_parts_in_temp_mod_position[f'Competent_cells_tube_{i}'] = temp_wells[temp_wells_counter]
- temp_wells_counter += 1
- for i in range(number_of_tubes_with_media_needed):
- self.dict_of_parts_in_temp_mod_position[f'Media_tube_{i}'] = temp_wells[temp_wells_counter]
- temp_wells_counter += 1
- #Set Temperature and Thermocycler module to 4
- tem_mod.set_temperature(4)
- thermocycler_mod.open_lid()
- thermocycler_mod.set_block_temperature(4)
- #Load cells into the thermocycler
- if self.volume_competent_cell_to_add > 20:
- pipette = pipette_p300
+ pipette_p300 = protocol.load_instrument(self.pipette_p300, self.pipette_p300_position, tip_racks=[tiprack_p200])
+ #Validate protocol
+ self._validate_protocol(protocol, alumblock)
+
+ #Load Reagents (also populates self.plasmid_name_to_wells)
+ if self.use_dna_96plate:
+ competent_cell_wells_by_chassis, media_wells = self._load_reagents_96plate(protocol, dna_plate, alumblock)
else:
- pipette = pipette_p20
- current_thermocycler_well_comp = self.thermocycler_starting_well
- transformation_well = 1
- #for r in range(self.replicates):
- for i in range(number_of_tubes_with_competent_cells_needed):
- for j in range(transformations_per_tube):
- part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[current_thermocycler_well_comp]]
- liquid_transfer(pipette, self.volume_competent_cell_to_add, tem_mod_block[self.dict_of_parts_in_temp_mod_position[f'Competent_cells_tube_{i}']], part_ubication_in_thermocyler, self.aspiration_rate, self.dispense_rate, mix_before=self.volume_competent_cell_to_add -5)
- if j == 0:
- self.dict_of_parts_in_thermocycler[f'Competent_cells_tube_{i}'] = [thermo_wells[current_thermocycler_well_comp]]
- else:
- self.dict_of_parts_in_thermocycler[f'Competent_cells_tube_{i}'].append(thermo_wells[current_thermocycler_well_comp])
- current_thermocycler_well_comp+=1
- if transformation_well == total_transformations:
- break
- transformation_well+=1
-
- #Load DNA into the thermocycler and mix
- if self.volume_dna > 20:
+ competent_cell_wells_by_chassis, media_wells = self._load_reagents_temp_module(protocol, alumblock)
+
+ #Set Temperature module and Thermocycler module to 4
+ thermocycler_module.open_lid()
+ if not self.water_testing:
+ temperature_module.set_temperature(4)
+ thermocycler_module.set_block_temperature(4)
+
+ #Load competent cells into the thermocycler
+ pipette = pipette_p300
+ self._transfer_competent_cells(protocol, pipette, pcr_plate, competent_cell_wells_by_chassis, self.transfer_volume_competent_cell, self.thermocycler_starting_well)
+
+ #Load DNA into the thermocycler
+ if self.transfer_volume_dna > 20:
pipette = pipette_p300
else:
pipette = pipette_p20
- current_thermocycler_well_dna = self.thermocycler_starting_well
- for r in range(self.replicates):
- for dna in self.list_of_dnas:
- part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[current_thermocycler_well_dna]]
- liquid_transfer(pipette, self.volume_dna, tem_mod_block[self.dict_of_parts_in_temp_mod_position[dna]], part_ubication_in_thermocyler, self.aspiration_rate, self.dispense_rate, mix_before=self.volume_dna)
- if r == 0:
- self.dict_of_parts_in_thermocycler[dna] = [thermo_wells[current_thermocycler_well_dna]]
- else:
- self.dict_of_parts_in_thermocycler[dna].append(thermo_wells[current_thermocycler_well_dna])
- current_thermocycler_well_dna+=1
-
- #Cold incubation
- thermocycler_mod.close_lid()
+ self._transfer_DNA(protocol, pipette, pcr_plate, self.transfer_volume_dna, self.thermocycler_starting_well)
+
+ # Cold Incubation
+ thermocycler_module.close_lid()
profile = [
- self.cold_incubation1, #1st cold incubation (long)
- self.heat_shock, #Heatshock
- self.cold_incubation2] #2nd cold incubation (short)
- thermocycler_mod.execute_profile(steps=profile, repetitions=1, block_max_volume=30)
- #Load LB and recovery incubation
- thermocycler_mod.open_lid()
- #TODO: check if there is the need for more than one tube
- if self.volume_recovery_media > 20:
- pipette = pipette_p300
- else:
- pipette = pipette_p20
- current_thermocycler_well_media = self.thermocycler_starting_well
- transformation_well = 1
- for i in range(number_of_tubes_with_media_needed):
- for j in range(transformations_per_media_tube):
- part_ubication_in_thermocyler = thermocycler_mod_plate[thermo_wells[current_thermocycler_well_media]]
- liquid_transfer(pipette, self.volume_recovery_media, tem_mod_block[self.dict_of_parts_in_temp_mod_position[f'Media_tube_{i}']], part_ubication_in_thermocyler, self.aspiration_rate, self.dispense_rate, mix_after=self.volume_recovery_media -5)
- if j == 0:
- self.dict_of_parts_in_thermocycler[f'Media_tube_{i}'] = [thermo_wells[current_thermocycler_well_media]]
- else:
- self.dict_of_parts_in_thermocycler[f'Media_tube_{i}'].append(thermo_wells[current_thermocycler_well_media])
- current_thermocycler_well_media+=1
- if transformation_well == total_transformations:
- break
- transformation_well+=1
- thermocycler_mod.close_lid()
+ self.cold_incubation1, # 1st cold incubation (long)
+ self.heat_shock, # Heat shock
+ self.cold_incubation2 # 2nd cold incubation (short)
+ ]
+ if not self.water_testing:
+ thermocycler_module.execute_profile(steps=profile, repetitions=1, block_max_volume=30)
+ thermocycler_module.open_lid()
+
+ #Load liquid broth
+ pipette = pipette_p300
+ self._transfer_liquid_broth(protocol, pipette, pcr_plate, media_wells, self.transfer_volume_recovery_media, self.thermocycler_starting_well)
+
+ # Recovery Incubation
+ thermocycler_module.close_lid()
recovery = [
- self.recovery_incubation]
- thermocycler_mod.execute_profile(steps=recovery, repetitions=1, block_max_volume=30)
-
- #output
+ self.recovery_incubation
+ ]
+ if not self.water_testing:
+ thermocycler_module.execute_profile(steps=recovery, repetitions=1, block_max_volume=30)
+
+ # Export plating input for next protocol (simulation only)
+ if protocol.is_simulating():
+ try:
+ self._export_plating_input(protocol)
+ except Exception as e:
+ protocol.comment(f"Could not export plating input: {e}")
+
+ # output
print('Strain and media tube in temp_mod')
print(self.dict_of_parts_in_temp_mod_position)
+ if self.use_dna_96plate:
+ print('DNA constructs in DNA plate (slot 2)')
+ print(self.dict_of_parts_in_dna_plate)
print('Genetically modified organisms in thermocycler')
print(self.dict_of_parts_in_thermocycler)
- #Optionally plate
- #END
\ No newline at end of file
+
+ def _validate_protocol(self, protocol, labware):
+ """
+ Validate protocol requirements and compute all derived counts used throughout run().
+ Sets: self.location_replicates, self.total_transformations,
+ self.transformations_per_cell_tube, self.competent_cell_tubes_by_chassis,
+ self.reactions_by_chassis, self.transformations_per_media_tube,
+ self.media_tubes_needed.
+ Raises ValueError if reagents exceed available alumblock wells.
+ """
+ #Number of available wells to load into
+ module_wells = len(labware.wells())
+
+ #Number of strains to transform
+ total_strains = len(self.transformations)
+
+ #Number of total plasmid wells needed (one well per unique plasmid)
+ total_plasmid_wells = len(self.all_plasmids)
+
+ # Number of location replicates per strain (assembly replicates of the same plasmid in plate,
+ # always 1 for temp module since plasmids are in a single sequential well)
+ if self.plasmid_locations is not None:
+ self.location_replicates = len(next(iter(self.plasmid_locations.values())))
+ else:
+ self.location_replicates = 1
+
+ #Total transformation reactions
+ self.total_transformations = total_strains * self.location_replicates * self.replicates
+
+ #Calculate competent cell tubes needed per chassis type
+ self.transformations_per_cell_tube = self.tube_volume_competent_cell // self.transfer_volume_competent_cell
+ self.competent_cell_tubes_by_chassis = {}
+ self.reactions_by_chassis = {}
+ total_competent_cell_tubes = 0
+
+ for chassis in self.all_chassis:
+ # Count how many transformations use this chassis
+ transformations_for_chassis = sum(1 for t in self.transformations if t['chassis'] == chassis)
+ reactions_for_chassis = transformations_for_chassis * self.location_replicates * self.replicates
+ self.reactions_by_chassis[chassis] = reactions_for_chassis
+ tubes_needed = (reactions_for_chassis + self.transformations_per_cell_tube - 1) // self.transformations_per_cell_tube
+ self.competent_cell_tubes_by_chassis[chassis] = tubes_needed
+ total_competent_cell_tubes += tubes_needed
+
+ #Number of tubes with media to be used
+ self.transformations_per_media_tube = self.tube_volume_recovery_media // self.transfer_volume_recovery_media
+ self.media_tubes_needed = (self.total_transformations + self.transformations_per_media_tube - 1) // self.transformations_per_media_tube
+
+ #Validate wells
+ if self.use_dna_96plate:
+ # DNA is on a separate plate, only cells and media on the temp module
+ if total_competent_cell_tubes + self.media_tubes_needed > module_wells:
+ raise ValueError(f'The number of reagents is more than {module_wells}.'
+ f'There are {total_competent_cell_tubes} tubes with competent cells ({self.competent_cell_tubes_by_chassis}).'
+ f'{self.media_tubes_needed} tubes with media.'
+ f'Please modify the protocol and try again.')
+ else:
+ # DNA, cells, and media all on the temp module
+ if total_plasmid_wells + total_competent_cell_tubes + self.media_tubes_needed > module_wells:
+ raise ValueError(f'The number of reagents is more than {module_wells}.'
+ f'There are {total_plasmid_wells} plasmid wells.'
+ f'{total_competent_cell_tubes} tubes with competent cells ({self.competent_cell_tubes_by_chassis}).'
+ f'{self.media_tubes_needed} tubes with media.'
+ f'Please modify the protocol and try again.')
+
+
+ def _load_reagents_96plate(self, protocol, dna_plate, alumblock):
+ """
+ Load all reagents for the 96-well plate workflow (plasmid_locations provided).
+ DNA constructs are loaded from the assembly output plate at their fixed positions —
+ populates self.plasmid_name_to_wells. Competent cells and media are loaded
+ sequentially onto the alumblock starting at well 0.
+
+ Parameters:
+ - protocol: Protocol context
+ - dna_plate: 96-well plate labware object (slot 2)
+ - alumblock: Temperature module labware object
+
+ Returns:
+ - competent_cell_wells_by_chassis: dict mapping chassis name to list of well objects
+ - media_wells: list of well objects
+ """
+ # Load DNA from dna_plate at actual well positions from plasmid_locations
+ # Populates self.plasmid_name_to_wells
+ self._load_dna_into_dna_plate(protocol, dna_plate)
+
+ # Load competent cells for each chassis onto alumblock
+ competent_cell_wells_by_chassis = {}
+ current_well = 0
+ for chassis in self.all_chassis:
+ tubes_needed = self.competent_cell_tubes_by_chassis[chassis]
+ wells = self._load_reagents(protocol, alumblock, self.tube_volume_competent_cell,
+ f"Competent Cell {chassis}", tubes_needed, initial_well=current_well)
+ competent_cell_wells_by_chassis[chassis] = wells
+ current_well += tubes_needed
+
+ # Load media onto alumblock
+ media_wells = self._load_reagents(protocol, alumblock, self.tube_volume_recovery_media,
+ "Media", self.media_tubes_needed, initial_well=current_well)
+
+ return competent_cell_wells_by_chassis, media_wells
+
+ def _load_dna_into_dna_plate(self, protocol, dna_plate):
+ """
+ Load DNA constructs into their fixed positions on the 96-well DNA plate.
+ Positions are determined by plasmid_locations (from assembly output or MoClo kit layout).
+ Each construct may occupy multiple wells (assembly replicates).
+ Populates self.plasmid_name_to_wells: {construct_name: [well_obj, ...]}
+
+ Parameters:
+ - protocol: Protocol context
+ - dna_plate: 96-well plate labware object on slot 2
+ """
+ current_color = 0
+ name_to_uri = {self._extract_name_from_uri(uri): uri for uri in self.plasmid_locations}
+
+ for construct_name in self.all_plasmids:
+ uri = name_to_uri[construct_name]
+ well_names = self.plasmid_locations[uri]
+ well_objects = []
+
+ for i, well_name in enumerate(well_names):
+ well = dna_plate.wells_by_name()[well_name]
+ well_objects.append(well)
+ if i == 0:
+ liquid = protocol.define_liquid(
+ name=construct_name,
+ description=f"{construct_name} DNA construct",
+ display_color=colors[current_color % len(colors)]
+ )
+ well.load_liquid(liquid, volume=self.volume_dna)
+
+ self.plasmid_name_to_wells[construct_name] = well_objects
+ self.dict_of_parts_in_dna_plate[construct_name] = well_names
+ current_color += 1
+
+ def _load_reagents_temp_module(self, protocol, alumblock):
+ """
+ Load all reagents for the temp module workflow (no plasmid_locations).
+ DNA constructs are loaded sequentially starting at initial_dna_well —
+ populates self.plasmid_name_to_wells. Competent cells and media follow
+ sequentially on the same alumblock.
+
+ Parameters:
+ - protocol: Protocol context
+ - alumblock: Temperature module labware object
+
+ Returns:
+ - competent_cell_wells_by_chassis: dict mapping chassis name to list of well objects
+ - media_wells: list of well objects
+ """
+ # Load DNA sequentially onto alumblock starting at initial_dna_well
+ # Populates self.plasmid_name_to_wells
+ self._load_dna_into_temp_module(protocol, alumblock)
+
+ # Load competent cells for each chassis, starting after the DNA wells
+ competent_cell_wells_by_chassis = {}
+ current_well = self.initial_dna_well + len(self.all_plasmids)
+ for chassis in self.all_chassis:
+ tubes_needed = self.competent_cell_tubes_by_chassis[chassis]
+ wells = self._load_reagents(protocol, alumblock, self.tube_volume_competent_cell,
+ f"Competent Cell {chassis}", tubes_needed, initial_well=current_well)
+ competent_cell_wells_by_chassis[chassis] = wells
+ current_well += tubes_needed
+
+ # Load media onto alumblock after competent cells
+ media_wells = self._load_reagents(protocol, alumblock, self.tube_volume_recovery_media,
+ "Media", self.media_tubes_needed, initial_well=current_well)
+
+ return competent_cell_wells_by_chassis, media_wells
+
+ def _load_dna_into_temp_module(self, protocol, alumblock):
+ """
+ Load DNA constructs sequentially into the temperature module alumblock.
+ Positions are assigned sequentially starting at initial_dna_well.
+ Populates self.plasmid_name_to_wells: {construct_name: [well_obj]} (single-element list).
+
+ Parameters:
+ - protocol: Protocol context
+ - alumblock: Temperature module labware object
+ """
+ current_color = 0
+
+ for i, construct_name in enumerate(self.all_plasmids):
+ well = alumblock.wells()[self.initial_dna_well + i]
+ liquid = protocol.define_liquid(
+ name=construct_name,
+ description=f"{construct_name} DNA construct",
+ display_color=colors[current_color % len(colors)]
+ )
+ well.load_liquid(liquid, volume=self.volume_dna)
+ self.plasmid_name_to_wells[construct_name] = [well]
+ self.dict_of_parts_in_temp_mod_position[construct_name] = well.well_name
+ current_color += 1
+
+ def _load_reagents(self, protocol, labware, volume, reagent_name, tube_count, initial_well=0, color_index=None):
+ """
+ Load multiple tubes of the same reagent type onto a labware object.
+ Tubes are named {reagent_name}_1, {reagent_name}_2, etc. and tracked
+ in self.dict_of_parts_in_temp_mod_position.
+
+ Parameters:
+ - protocol: Protocol context
+ - labware: Labware object to load reagents onto
+ - volume: Volume per tube in µL
+ - reagent_name: Base name for the reagent (e.g., "Competent Cell DH5alpha", "Media")
+ - tube_count: Number of tubes to load
+ - initial_well: Starting well index on the labware
+ - color_index: Starting color index for Opentrons UI (cycles through colors list)
+
+ Returns:
+ - List of well objects in order
+ """
+ wells = []
+ current_color = color_index if color_index is not None else 0
+ for i in range(tube_count):
+ well = labware.wells()[initial_well+i]
+ wells.append(well)
+ name = f"{reagent_name}_{i+1}"
+
+ liquid = protocol.define_liquid(
+ name = name,
+ display_color= colors[current_color%len(colors)]
+ )
+
+ well.load_liquid(liquid, volume=volume)
+ self.dict_of_parts_in_temp_mod_position[name] = well.well_name
+ current_color += 1
+ return wells
+
+ def _transfer_competent_cells(self, protocol, pipette, pcr_plate, competent_cell_wells_by_chassis,
+ transfer_volume_competent_cell, thermocycler_starting_well):
+ """
+ Transfer competent cells into thermocycler wells, one well per reaction.
+ Iterates self.transformations as ground truth. For each strain, fills
+ location_replicates * replicates wells with the appropriate chassis cells.
+ Tube selection is per-chassis (chassis_reaction_count) so multiple chassis
+ types each consume only their own tubes independently.
+
+ Parameters:
+ - protocol: Protocol context
+ - pipette: Pipette instrument (p300)
+ - pcr_plate: Thermocycler plate labware
+ - competent_cell_wells_by_chassis: Dict mapping chassis name to list of well objects
+ - transfer_volume_competent_cell: Volume to transfer per well in µL
+ - thermocycler_starting_well: Starting well index in thermocycler plate
+ """
+ well_index = thermocycler_starting_well
+ chassis_reaction_count = {chassis: 0 for chassis in self.all_chassis}
+
+ for transformation in self.transformations:
+ chassis = transformation['chassis']
+ cell_wells = competent_cell_wells_by_chassis[chassis]
+
+ for replicate in range(self.location_replicates * self.replicates):
+ dest_well = pcr_plate.wells()[well_index]
+
+ # Select tube based on per-chassis reaction count
+ tube_index = chassis_reaction_count[chassis] // self.transformations_per_cell_tube
+ source_well = cell_wells[tube_index]
+
+ self.liquid_transfer(
+ protocol=protocol,
+ pipette=pipette,
+ volume=transfer_volume_competent_cell,
+ source=source_well,
+ dest=dest_well,
+ asp_rate=self.aspiration_rate,
+ disp_rate=self.dispense_rate,
+ mix_before=20,
+ touch_tip=False
+ )
+
+ name = f"Competent_Cell_{chassis}"
+ if dest_well.well_name not in self.dict_of_parts_in_thermocycler:
+ self.dict_of_parts_in_thermocycler[dest_well.well_name] = [transformation['strain']]
+ self.dict_of_parts_in_thermocycler[dest_well.well_name].append(name)
+
+ chassis_reaction_count[chassis] += 1
+ well_index += 1
+
+ def _transfer_DNA(self, protocol, pipette, pcr_plate, transfer_volume_dna, thermocycler_starting_well):
+ """
+ Transfer DNA plasmids to thermocycler wells. Multiple plasmids per strain go to the same well.
+ Uses self.plasmid_name_to_wells (populated during loading) for all source well lookups —
+ no positional indexing, fully name-driven from self.transformations.
+
+ For the temp module path: each plasmid has one well → location_replicates = 1
+ For the dna plate path: each plasmid has N wells (assembly replicates) → location_replicates = N
+
+ Parameters:
+ - protocol: Protocol context
+ - pipette: Pipette instrument
+ - pcr_plate: Thermocycler plate
+ - transfer_volume_dna: Volume to transfer per plasmid
+ - thermocycler_starting_well: Starting well index in thermocycler
+ """
+ well_index = thermocycler_starting_well
+
+ for transformation in self.transformations:
+ plasmids = transformation['plasmids']
+
+ for loc_idx in range(self.location_replicates):
+ for replicate in range(self.replicates):
+ dest_well = pcr_plate.wells()[well_index]
+
+ for plasmid_name in plasmids:
+ source_well = self.plasmid_name_to_wells[plasmid_name][loc_idx]
+
+ self.liquid_transfer(
+ protocol=protocol,
+ pipette=pipette,
+ volume=transfer_volume_dna,
+ source=source_well,
+ dest=dest_well,
+ asp_rate=self.aspiration_rate,
+ disp_rate=self.dispense_rate,
+ mix_before=transfer_volume_dna,
+ touch_tip=True
+ )
+
+ if dest_well.well_name not in self.dict_of_parts_in_thermocycler:
+ self.dict_of_parts_in_thermocycler[dest_well.well_name] = []
+ self.dict_of_parts_in_thermocycler[dest_well.well_name].append(plasmid_name)
+
+ well_index += 1
+
+ def _transfer_liquid_broth(self, protocol, pipette, pcr_plate, media_wells, transfer_volume_recovery_media,
+ thermocycler_starting_well):
+ """
+ Distribute recovery media into all thermocycler wells using the pipette distribute method.
+ Each media tube fills up to transformations_per_media_tube wells before moving to the next.
+ Uses .top(2) on dest wells to avoid contamination from the pipette tip.
+ Covers self.total_transformations wells in total.
+
+ Parameters:
+ - protocol: Protocol context
+ - pipette: Pipette instrument (p300)
+ - pcr_plate: Thermocycler plate labware
+ - media_wells: List of well objects containing recovery media
+ - transfer_volume_recovery_media: Volume to distribute per well in µL
+ - thermocycler_starting_well: Starting well index in thermocycler plate
+ """
+ well_index = thermocycler_starting_well
+
+ for tube_index, source_well in enumerate(media_wells):
+ #Calculate how many wells this media tube will fill
+ remaining_transformations = self.total_transformations - (tube_index * self.transformations_per_media_tube)
+ wells_to_fill = min(self.transformations_per_media_tube, remaining_transformations)
+
+ # Get destination wells for this tube using .top() to avoid contamination
+ dest_wells = [pcr_plate.wells()[well_index+i].top(2) for i in range(int(wells_to_fill))]
+
+ #Distribute recovery media
+ pipette.distribute(
+ volume=transfer_volume_recovery_media,
+ source=source_well,
+ dest=dest_wells,
+ disposal_volume=0,
+ new_tip='once',
+ air_gap=10
+ )
+
+ #Track in dictionary
+ media_name = f"Media_{tube_index+1}"
+ for i in range(int(wells_to_fill)):
+ well_name = pcr_plate.wells()[well_index + i].well_name
+ if well_name not in self.dict_of_parts_in_thermocycler:
+ self.dict_of_parts_in_thermocycler[well_name] = []
+ self.dict_of_parts_in_thermocycler[well_name].append(media_name)
+
+ well_index += wells_to_fill
+
diff --git a/src/pudu/utils.py b/src/pudu/utils.py
index 2e490cb..cc68f91 100644
--- a/src/pudu/utils.py
+++ b/src/pudu/utils.py
@@ -1,113 +1,311 @@
+import subprocess
+import time
+from typing import Optional
-
-thermo_wells = [
-'A1','A2','A3','A4','A5','A6','A7','A8','A9','A10','A11','A12',
-'B1','B2','B3','B4','B5','B6','B7','B8','B9','B10','B11','B12',
-'C1','C2','C3','C4','C5','C6','C7','C8','C9','C10','C11','C12',
-'D1','D2','D3','D4','D5','D6','D7','D8','D9','D10','D11','D12',
-'E1','E2','E3','E4','E5','E6','E7','E8','E9','E10','E11','E12',
-'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12',
-'G1','G2','G3','G4','G5','G6','G7','G8','G9','G10','G11','G12',
-'H1','H2','H3','H4','H5','H6','H7','H8','H9','H10','H11','H12'
+colors = [
+ "#4040BF", # Blue
+ "#BF4040", # Red
+ "#40BF40", # Green
+ "#A640BF", # Purple
+ "#BFBF40", # Yellow
+ "#BF7340", # Orange
+ "#40BFBF", # Cyan
+ "#BF40A6", # Magenta
+ "#73BF40", # Lime green
+ "#4073BF", # Blue-cyan
+ "#BF8C40", # Orange-yellow
+ "#40BF73", # Green-cyan
+ "#7340BF", # Blue-purple
+ "#A6BF40", # Yellow-green
+ "#BF5940", # Red-orange
+ "#40A6BF", # Cyan-blue
+ "#BF4073", # Red-purple
+ "#59BF40", # Green
+ "#BFA640", # Orange-yellow
+ "#40BFA6", # Cyan-green
+ "#8CBF40", # Yellow-green
+ "#40BF59", # Green
+ "#40BF8C", # Green-cyan
+ "#BF40A6" # Purple-magenta
]
-temp_wells = [
-'A1','A2','A3','A4','A5','A6',
-'B1','B2','B3','B4','B5','B6',
-'C1','C2','C3','C4','C5','C6',
-'D1','D2','D3','D4','D5','D6'
-]
+class Camera:
+ """
+ Camera class for handling picture and video capture during Opentrons protocols.
-plate_96_wells = [
-'A1','A2','A3','A4','A5','A6','A7','A8','A9','A10','A11','A12',
-'B1','B2','B3','B4','B5','B6','B7','B8','B9','B10','B11','B12',
-'C1','C2','C3','C4','C5','C6','C7','C8','C9','C10','C11','C12',
-'D1','D2','D3','D4','D5','D6','D7','D8','D9','D10','D11','D12',
-'E1','E2','E3','E4','E5','E6','E7','E8','E9','E10','E11','E12',
-'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12',
-'G1','G2','G3','G4','G5','G6','G7','G8','G9','G10','G11','G12',
-'H1','H2','H3','H4','H5','H6','H7','H8','H9','H10','H11','H12'
-]
+ This class encapsulates all camera functionality including:
+ - Taking snapshots at specific protocol steps
+ - Recording video during protocol execution
+ - Handling simulation mode gracefully
+ - Managing ffmpeg processes
+ """
+
+ def __init__(self, video_size: str = "320x240", picture_size: str = "640x480",
+ video_device: str = "/dev/video0", storage_path: str = "/data/user_storage"):
+ """
+ Initialize Camera with configuration options.
+
+ Args:
+ video_size: Resolution for video recording (default: "320x240")
+ picture_size: Resolution for picture capture (default: "640x480")
+ video_device: Video device path (default: "/dev/video0")
+ storage_path: Path where media files will be saved (default: "/data/user_storage")
+ """
+ self.video_size = video_size
+ self.picture_size = picture_size
+ self.video_device = video_device
+ self.storage_path = storage_path
+ self._active_video_process: Optional[subprocess.Popen] = None
+
+ def cleanup_ffmpeg_processes(self) -> None:
+ """Clean up any running ffmpeg processes using killall."""
+ try:
+ subprocess.run(['killall', 'ffmpeg'], capture_output=True, check=False)
+ except Exception:
+ pass # Fail silently if cleanup doesn't work
+
+ def capture_picture(self, protocol, when: str = "image") -> None:
+ """
+ Take a picture at a specific protocol step.
+
+ Args:
+ protocol: Opentrons protocol context
+ when: Description of when the picture was taken (used in filename)
+
+ Returns:
+ Filename of captured image if successful, None if simulation or failed
+ """
+ if protocol.is_simulating():
+ protocol.comment(f'[SIMULATION] Taking picture at protocol step: {when}')
+ return
+
+ protocol.comment(f'Taking picture at protocol step: {when}')
+ timestamp = int(time.time())
+ filename = f'{when}_image_{timestamp}.jpg'
+ filepath = f'{self.storage_path}/{filename}'
+
+ try:
+ subprocess.check_call([
+ 'ffmpeg', '-loglevel', 'error', '-y', '-f', 'video4linux2',
+ '-s', self.picture_size, '-i', self.video_device, '-ss', '0:0:1',
+ '-frames', '1', filepath
+ ])
+ protocol.comment(f'{when.title()} picture captured: {filename}')
+
+ except subprocess.CalledProcessError as e:
+ protocol.comment(f'Warning: Picture capture failed: {e}, continuing protocol')
+
+
+ def start_video(self, protocol) -> None:
+ """
+ Start video recording.
+
+ Args:
+ protocol: Opentrons protocol context
+
+ Returns:
+ Video process handle if successful, None if simulation or failed
+ """
+ if protocol.is_simulating():
+ protocol.comment('[SIMULATION] Starting video recording')
+ return
+
+ # Clean up any existing processes
+ self.cleanup_ffmpeg_processes()
+ time.sleep(0.5) # Brief pause for cleanup
+
+ timestamp = int(time.time())
+ filename = f'video_image_{timestamp}.mp4'
+ filepath = f'{self.storage_path}/{filename}'
+
+ try:
+ video_process = subprocess.Popen([
+ 'ffmpeg', '-loglevel', 'error', '-y', '-video_size', self.video_size,
+ '-i', self.video_device, filepath
+ ])
+ self._active_video_process = video_process
+ protocol.comment(f"Video recording started: {filename}")
+ except Exception as e:
+ protocol.comment(f"Warning: Video recording failed: {e}")
+
+ def stop_video(self, protocol) -> None:
+ """
+ Stop video recording.
+
+ Args:
+ protocol: Opentrons protocol context
+ video_process: Video process to stop (uses active process if None)
+ """
+ if protocol.is_simulating():
+ protocol.comment('[SIMULATION] Stopping video recording')
+ return
+
+ # Use provided process or the active one
+ process_to_stop = self._active_video_process
+
+ if process_to_stop is None:
+ protocol.comment("No video recording process to stop")
+ return
+
+ if process_to_stop.poll() is None: # Process is still running
+ try:
+ process_to_stop.terminate()
+ process_to_stop.wait(timeout=5)
+ protocol.comment("Video recording stopped")
+ except subprocess.TimeoutExpired:
+ process_to_stop.kill()
+ process_to_stop.wait()
+ protocol.comment("Video recording force-stopped")
+ except Exception as e:
+ protocol.comment(f"Warning: Error stopping video: {e}")
+ else:
+ protocol.comment("Video recording already stopped")
+
+ # Clear active process if it was the one we stopped
+ self._active_video_process = None
+
+class SmartPipette:
+ """
+ Wrapper for automatic volume tracking
+ """
+
+ def __init__(self, pipette, protocol):
+ self.pipette = pipette
+ self.protocol = protocol
+ if not hasattr(protocol, 'define_liquid'):
+ raise RuntimeError("This class requires API with liquid tracking support")
+
+ def is_conical_tube(self, well, use: bool = False) -> bool:
+ """Check if the well is from a conical tube labware or manually set as true"""
+ return 'conical' in well.parent.load_name.lower() or use
+
+ def get_well_volume(self, well) -> Optional[float]:
+ """Get current volume in well using pure API method"""
+ try:
+ return well.current_liquid_volume()
+ except Exception as e:
+ self.protocol.comment(f"ERROR reading volume from {well.well_name}: {e}")
+ return None
+
+ def get_well_height(self, well) -> Optional[float]:
+ """Get current liquid height using pure API method (if available)"""
+ try:
+ if hasattr(well, 'current_liquid_height'):
+ return well.current_liquid_height()
+ else:
+ self.protocol.comment("Liquid height method not available in this API version")
+ return None
+ except Exception as e:
+ self.protocol.comment(f"ERROR reading height from {well.well_name}: {e}")
+ return None
+
+ def get_conical_tube_aspiration_height(self, well) -> float:
+ """
+ Calculate safe aspiration height for conical tubes using proven method
+ Uses API liquid tracking to get current volume
+ """
+ # Get current volume from API
+ try:
+ current_volume = well.current_liquid_volume()
+ if current_volume is None:
+ raise ValueError("API returned None for liquid volume")
+ except Exception as e:
+ self.protocol.comment(f"ERROR: Could not get liquid volume from API: {e}")
+ return 10.0 # Safe fallback height
+
+ max_volume = well.max_volume
+ tube_depth = well.depth - 10 # Account for threads
+ min_safe_height = 3 # mm minimum to prevent tip damage
+ meniscus_offset = 10 # mm below liquid surface
+
+ # Calculate liquid height based on current volume
+ liquid_height = (current_volume / max_volume) * tube_depth
+ aspiration_height = max(liquid_height - meniscus_offset, min_safe_height)
+
+ self.protocol.comment(
+ f"Conical calculation: {current_volume:.0f}µL remaining = {aspiration_height:.1f}mm height")
+ return aspiration_height
+
+ def get_aspiration_location(self, well, use: bool = False) -> float:
+ """
+ Get intelligent aspiration location using API volume data and proven height calculation
+ """
+ if not self.is_conical_tube(well, use=use):
+ return well
+
+ try:
+ current_volume = well.current_liquid_volume()
+ if current_volume is None or current_volume < well.max_volume * 0.2:
+ # Less than 20% remaining - use standard aspiration
+ self.protocol.comment("Low volume detected - using standard aspiration")
+ return well
+
+ # Use conical tube calculation
+ safe_height = self.get_conical_tube_aspiration_height(well)
+ return well.bottom(safe_height)
+
+ except Exception as e:
+ self.protocol.comment(f"ERROR getting volume from API: {e}")
+ return well # Fallback to standard aspiration
+
+ def liquid_transfer(self, volume: float, source, destination,
+ asp_rate: float = 0.5, disp_rate: float = 1.0,
+ blow_out: bool = True, touch_tip: bool = False,
+ mix_before: float = 0.0, mix_after: float = 0.0,
+ mix_reps: int = 3, new_tip: bool = True, drop_tip: bool = True, use:bool = False) -> bool:
+ """
+ Transfer liquid using pure API liquid tracking for volume management
+
+ Returns:
+ bool: True if transfer was successful, False if insufficient volume
+ """
+ # Check volume using API methods only
+ try:
+ current_volume = source.current_liquid_volume()
+ if current_volume is None:
+ self.protocol.comment("WARNING: API returned None for source volume")
+ return False
+
+ if current_volume < volume:
+ self.protocol.comment(f"WARNING: Insufficient volume. "
+ f"Requested: {volume}µL, Available: {current_volume:.0f}µL")
+ return False
+
+ except Exception as e:
+ self.protocol.comment(f"ERROR: Could not check source volume: {e}")
+ return False
+
+ if new_tip:
+ self.pipette.pick_up_tip()
+
+ # Get aspiration location using API data + proven calculation
+ aspiration_location = self.get_aspiration_location(source,use)
+
+ # Mix before if requested
+ if mix_before > 0:
+ # Use current volume to limit mixing
+ try:
+ safe_mix_volume = min(mix_before, current_volume * 0.8)
+ self.pipette.mix(mix_reps, safe_mix_volume, aspiration_location)
+ except:
+ self.protocol.comment("Skipping mix_before due to API error")
+
+ # Aspirate
+ self.pipette.aspirate(volume, aspiration_location, rate=asp_rate)
+
+ # Dispense
+ self.pipette.dispense(volume, destination.center(), rate=disp_rate)
+
+ # Mix after if requested
+ if mix_after > 0:
+ self.pipette.mix(mix_reps, mix_after, destination)
+
+ if blow_out:
+ self.pipette.blow_out()
+
+ if touch_tip:
+ self.pipette.touch_tip()
-def liquid_transfer(pipette, volume, source, destination, asp_rate:float=0.5, disp_rate:float=1.0, blow_out:bool=True, touch_tip:bool=False, mix_before:float=0.0, mix_after:float=0.0, mix_reps:int=3, new_tip:bool=True, drop_tip:bool=True):
- if new_tip:
- pipette.pick_up_tip()
- if mix_before > 0:
- pipette.mix(mix_reps, mix_before, source)
- pipette.aspirate(volume, source, rate=asp_rate)
- pipette.dispense(volume, destination, rate=disp_rate)
- if mix_after > 0:
- pipette.mix(mix_reps, mix_after, destination)
- if blow_out:
- pipette.blow_out()
- if touch_tip:
- pipette.touch_tip()
- if drop_tip:
- pipette.drop_tip()
-
-#Define slots, to allocate 4 samples in each slot, lasts slots allocate in the border where border effects apply
-slot_1 = ['A2', 'B2', 'C2', 'D2']
-slot_2 = ['A3', 'B3', 'C3', 'D3']
-slot_3 = ['A4', 'B4', 'C4', 'D4']
-slot_4 = ['A5', 'B5', 'C5', 'D5']
-slot_5 = ['A6', 'B6', 'C6', 'D6']
-slot_6 = ['A7', 'B7', 'C7', 'D7']
-slot_7 = ['A8', 'B8', 'C8', 'D8']
-slot_8 = ['A9', 'B9', 'C9', 'D9']
-slot_9 = ['A10', 'B10', 'C10', 'D10']
-slot_10 = ['A11', 'B11', 'C11', 'D11']
-slot_11 = ['E2', 'F2', 'G2', 'H2']
-slot_12 = ['E3', 'F3', 'G3', 'H3']
-slot_13 = ['E4', 'F4', 'G4', 'H4']
-slot_14 = ['E5', 'F5', 'G5', 'H5']
-slot_15 = ['E6', 'F6', 'G6', 'H6']
-slot_16 = ['E7', 'F7', 'G7', 'H7']
-slot_17 = ['E8', 'F8', 'G8', 'H8']
-slot_18 = ['E9', 'F9', 'G9', 'H9']
-slot_19 = ['E10', 'F10', 'G10', 'H10']
-slot_20 = ['E11', 'F11', 'G11', 'H11']
-slot_21 = ['A1', 'B1', 'C1', 'D1']
-slot_22 = ['E1', 'F1', 'G1', 'H1']
-slot_23 = ['A12', 'B12', 'C12', 'D12']
-slot_24 = ['E12', 'F12', 'G12', 'H12']
-
-slots = [slot_1, slot_2, slot_3, slot_4, slot_5, slot_6, slot_7, slot_8, slot_9, slot_10, slot_11, slot_12, slot_13, slot_14, slot_15, slot_16, slot_17, slot_18, slot_19, slot_20, slot_21, slot_22, slot_23, slot_24]
-
-#define rows
-row_a = ['A1','A2','A3','A4','A5','A6','A7','A8','A9','A10','A11','A12']
-row_b = ['B1','B2','B3','B4','B5','B6','B7','B8','B9','B10','B11','B12']
-row_c = ['C1','C2','C3','C4','C5','C6','C7','C8','C9','C10','C11','C12']
-row_d = ['D1','D2','D3','D4','D5','D6','D7','D8','D9','D10','D11','D12']
-row_e = ['E1','E2','E3','E4','E5','E6','E7','E8','E9','E10','E11','E12']
-row_f = ['F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12']
-row_g = ['G1','G2','G3','G4','G5','G6','G7','G8','G9','G10','G11','G12']
-row_h = ['H1','H2','H3','H4','H5','H6','H7','H8','H9','H10','H11','H12']
-
-rows = [row_a, row_b, row_c, row_d, row_e, row_f, row_g, row_h]
-
-#define columns
-col_1 = ['A1','B1','C1','D1','E1','F1','G1','H1']
-col_2 = ['A2','B2','C2','D2','E2','F2','G2','H2']
-col_3 = ['A3','B3','C3','D3','E3','F3','G3','H3']
-col_4 = ['A4','B4','C4','D4','E4','F4','G4','H4']
-col_5 = ['A5','B5','C5','D5','E5','F5','G5','H5']
-col_6 = ['A6','B6','C6','D6','E6','F6','G6','H6']
-col_7 = ['A7','B7','C7','D7','E7','F7','G7','H7']
-col_8 = ['A8','B8','C8','D8','E8','F8','G8','H8']
-col_9 = ['A9','B9','C9','D9','E9','F9','G9','H9']
-col_10 = ['A10','B10','C10','D10','E10','F10','G10','H10']
-col_11 = ['A11','B11','C11','D11','E11','F11','G11','H11']
-col_12 = ['A12','B12','C12','D12','E12','F12','G12','H12']
-
-columns = [col_1, col_2, col_3, col_4, col_5, col_6, col_7, col_8, col_9, col_10, col_11, col_12]
-
-position_to_row_and_column = {'A1':(1,1), 'A2':(1,2), 'A3':(1,3), 'A4':(1,4), 'A5':(1,5), 'A6':(1,6), 'A7':(1,7), 'A8':(1,8), 'A9':(1,9), 'A10':(1,10), 'A11':(1,11), 'A12':(1,12),
- 'B1':(2,1), 'B2':(2,2), 'B3':(2,3), 'B4':(2,4), 'B5':(2,5), 'B6':(2,6), 'B7':(2,7), 'B8':(2,8), 'B9':(2,9), 'B10':(2,10), 'B11':(2,11), 'B12':(2,12),
- 'C1':(3,1), 'C2':(3,2), 'C3':(3,3), 'C4':(3,4), 'C5':(3,5), 'C6':(3,6), 'C7':(3,7), 'C8':(3,8), 'C9':(3,9), 'C10':(3,10), 'C11':(3,11), 'C12':(3,12),
- 'D1':(4,1), 'D2':(4,2), 'D3':(4,3), 'D4':(4,4), 'D5':(4,5), 'D6':(4,6), 'D7':(4,7), 'D8':(4,8), 'D9':(4,9), 'D10':(4,10), 'D11':(4,11), 'D12':(4,12),
- 'E1':(5,1), 'E2':(5,2), 'E3':(5,3), 'E4':(5,4), 'E5':(5,5), 'E6':(5,6), 'E7':(5,7), 'E8':(5,8), 'E9':(5,9), 'E10':(5,10), 'E11':(5,11), 'E12':(5,12),
- 'F1':(6,1), 'F2':(6,2), 'F3':(6,3), 'F4':(6,4), 'F5':(6,5), 'F6':(6,6), 'F7':(6,7), 'F8':(6,8), 'F9':(6,9), 'F10':(6,10), 'F11':(6,11), 'F12':(6,12),
- 'G1':(7,1), 'G2':(7,2), 'G3':(7,3), 'G4':(7,4), 'G5':(7,5), 'G6':(7,6), 'G7':(7,7), 'G8':(7,8), 'G9':(7,9), 'G10':(7,10), 'G11':(7,11), 'G12':(7,12),
- 'H1':(8,1), 'H2':(8,2), 'H3':(8,3), 'H4':(8,4), 'H5':(8,5), 'H6':(8,6), 'H7':(8,7), 'H8':(8,8), 'H9':(8,9), 'H10':(8,10), 'H11':(8,11), 'H12':(8,12)}
-
-row_letter_to_number = {'A':1, 'B':2, 'C':3, 'D':4, 'E':5, 'F':6, 'G':7, 'H':8}
\ No newline at end of file
+ if drop_tip:
+ self.pipette.drop_tip()
+ return True
diff --git a/tests/Part_in_backbone_defined.xml b/tests/Part_in_backbone_defined.xml
new file mode 100644
index 0000000..113169b
--- /dev/null
+++ b/tests/Part_in_backbone_defined.xml
@@ -0,0 +1,328 @@
+
+
+
+
+ module1
+
+
+
+ j0nju1Eb_56
+
+
+
+
+
+
+
+
+ Cir_qKqs
+ 1
+ Cir
+
+
+
+
+
+
+ j0nju1Eb
+ 1
+
+
+
+
+
+
+
+ Cir_qKqs_5
+ 1
+
+
+
+
+
+
+
+ Scar_B_4
+ 1
+
+
+
+
+
+
+
+ J23101_3
+ 1
+
+
+
+
+
+
+
+ Scar_A_2
+ 1
+
+
+
+
+
+
+
+ j0nju1EbAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 39
+
+
+
+
+
+
+
+
+
+ j0nju1EbAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 40
+ 43
+
+
+
+
+
+
+
+
+
+ j0nju1EbAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ j0nju1EbAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 44
+ 2095
+
+
+
+
+
+
+
+
+
+ j0nju1EbConstraint1
+ 1
+
+
+
+
+
+
+
+
+ j0nju1EbConstraint3
+ 1
+
+
+
+
+
+
+
+
+ j0nju1EbConstraint2
+ 1
+
+
+
+
+
+
+
+
+
+ Scar_B
+ 1
+ Scar_B
+
+
+
+
+
+
+ J23101
+ 1
+ J23101
+
+
+
+
+
+
+ Scar_A
+ 1
+ Scar_A
+
+
+
+
+
+
+ j0nju1Eb_sequence
+ GGAGtttacagctagctcagtcctaggtattatgctagcTACTCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ Cir_qKqs_sequence
+ CGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ J23101_sequence
+ tttacagctagctcagtcctaggtattatgctagc
+
+
+
+
+ Scar_A_sequence
+ GGAG
+
+
+
+
+ Scar_B_sequence
+ TACT
+
+
+
+
+ Cir_qKqs_Layout
+
+
+
+
+ module1_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ j0nju1Eb_56
+
+
+
+
+
+
+ j0nju1Eb_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_A_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ J23101_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_B_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_qKqs_5
+
+
+
+
+
+
+ Scar_A_Layout
+
+
+
+
+ Scar_B_Layout
+
+
+
+
+ J23101_Layout
+
+
+
diff --git a/tests/Part_in_backbone_example.xml b/tests/Part_in_backbone_example.xml
new file mode 100644
index 0000000..5752831
--- /dev/null
+++ b/tests/Part_in_backbone_example.xml
@@ -0,0 +1,328 @@
+
+
+
+
+ module1
+
+
+
+ XJK9paob_2
+
+
+
+
+
+
+
+
+ Scar_bWtJ
+ 1
+ Scar
+
+
+
+
+
+
+ Scar_dAK6
+ 1
+ Scar
+
+
+
+
+
+
+ Pro_zVcE
+ 1
+ Pro
+
+
+
+
+
+
+ Cir_wQ7v
+ 1
+ Cir
+
+
+
+
+
+
+ XJK9paob
+ 1
+
+
+
+
+
+
+
+ Scar_dAK6_4
+ 1
+
+
+
+
+
+
+
+ Pro_zVcE_3
+ 1
+
+
+
+
+
+
+
+ Scar_bWtJ_2
+ 1
+
+
+
+
+
+
+
+ Cir_wQ7v_5
+ 1
+
+
+
+
+
+
+
+ XJK9paobAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 12
+ 15
+
+
+
+
+
+
+
+
+
+ XJK9paobAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 16
+ 39
+
+
+
+
+
+
+
+
+
+ XJK9paobAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 11
+
+
+
+
+
+
+
+
+
+ XJK9paobAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ XJK9paobConstraint1
+ 1
+
+
+
+
+
+
+
+
+ XJK9paobConstraint2
+ 1
+
+
+
+
+
+
+
+
+ XJK9paobConstraint3
+ 1
+
+
+
+
+
+
+
+
+
+ Scar_bWtJ_sequence
+ AAAA
+
+
+
+
+ XJK9paob_sequence
+ AAAATTTTTTTCCCCGGGGGGGGGGGGGGGGGGGGGGGG
+
+
+
+
+ Pro_zVcE_sequence
+ TTTTTTT
+
+
+
+
+ Scar_dAK6_sequence
+ CCCC
+
+
+
+
+ Cir_wQ7v_sequence
+ GGGGGGGGGGGGGGGGGGGGGGGG
+
+
+
+
+ Pro_zVcE_Layout
+
+
+
+
+ Scar_bWtJ_Layout
+
+
+
+
+ module1_Layout
+
+
+
+ 640.0
+ 310.0
+ 152.0
+ 100.0
+ XJK9paob_2
+
+
+
+
+
+
+ Cir_wQ7v_Layout
+
+
+
+
+ Scar_dAK6_Layout
+
+
+
+
+ XJK9paob_Layout
+
+
+
+ 640.0
+ 310.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_bWtJ_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ Pro_zVcE_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_dAK6_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_wQ7v_5
+
+
+
+
+
diff --git a/tests/RyanTest.xml b/tests/RyanTest.xml
new file mode 100644
index 0000000..8afebde
--- /dev/null
+++ b/tests/RyanTest.xml
@@ -0,0 +1,1579 @@
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_reactant
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_digestion_interaction
+
+
+ restriction
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_70_product
+
+
+
+
+
+
+ UJHDBOTD_70_reactant
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_product
+
+
+
+ UJHDBOTD_106_digestion_interaction
+
+
+
+ 1
+ restriction
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_reactant
+
+ 1
+
+
+
+ 1
+
+
+
+
+
+
+ restriction
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_backbone_reactant
+
+
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
+ UJHDBOTD_backbone_product
+
+
+
+ UJHDBOTD_digestion_interaction
+
+
+
+
+ testassem
+
+
+
+
+
+ 1
+ ligation
+
+
+
+
+
+
+
+
+ UJHDBOTD_extracted_backbone_ligation
+
+ 1
+
+
+
+
+ 1
+ UJHDBOTD_106_extracted_part_ligation
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_134_extracted_part_ligation
+
+
+
+
+
+
+
+
+
+ 1
+ composite_1_product
+
+
+
+
+
+ 1
+
+
+
+ 1
+ UJHDBOTD_70_extracted_part_ligation
+
+
+
+
+ composite_1_ligation_interaction
+
+
+
+
+
+ 1
+ UJHDBOTD_45_extracted_part_ligation
+
+
+
+
+ 1
+
+
+ 1
+
+
+
+ UJHDBOTD_70_digestion_product
+
+
+
+
+
+ 1
+ BsaI_enzyme
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_45_digestion_product
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+ UJHDBOTD_backbone_digestion_product
+
+
+
+
+ UJHDBOTD_45_digestion_interaction
+
+ 1
+
+
+ UJHDBOTD_45_reactant
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+ restriction
+
+
+
+
+
+
+ UJHDBOTD_45_product
+
+
+ 1
+
+
+
+
+
+
+
+ composite_1
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_134_digestion_product
+
+
+
+
+ 1
+
+
+ UJHDBOTD_70_reactant
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_backbone_reactant
+
+
+
+
+
+
+
+
+ T4_Ligase
+
+
+
+ 1
+
+
+
+
+ 1
+ UJHDBOTD_45_reactant
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_digestion_product
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_reactant
+ 1
+
+
+
+
+
+
+ UJHDBOTD_134_product
+ 1
+
+
+
+
+
+
+ 1
+ restriction
+
+
+
+
+
+ 1
+
+ UJHDBOTD_134_digestion_interaction
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_reactant
+ 1
+
+
+
+
+ 1
+ BsaI
+ BsaI
+
+ Restriction enzyme BsaI from REBASE.
+
+
+
+
+
+
+
+
+
+ 1
+
+ UJHDBOTD_70_three_prime_oh_component
+
+
+
+
+
+ 1
+
+
+
+ 1
+ five_prime_oh_location
+ 4
+
+ 1
+
+
+
+ five_prime_overhang
+
+
+
+ UJHDBOTD_70_extracted_part
+
+
+
+
+
+
+
+ 43
+ 1
+ three_prime_oh_location
+ 40
+
+
+
+ 1
+ three_prime_overhang
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_five_prime_oh_component
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_part
+
+
+
+
+ UJHDBOTD_70_part_location
+ 39
+
+ 1
+ 5
+
+
+
+
+
+
+
+
+ J23101_3
+
+ 1
+
+
+ 1
+
+
+
+
+ UJHDBOTD_70_three_prime_oh
+
+
+
+ 1
+
+
+ 1
+ Scar_B
+ Scar_B
+
+
+
+
+
+
+ 1
+ J23101
+ J23101
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ UJHDBOTD_70_five_prime_oh
+
+
+
+ Scar_A
+ 1
+
+ Scar_A
+
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_45_part_location
+ 25
+
+
+ 5
+ 1
+
+
+ UJHDBOTD_45_part
+
+
+
+ UJHDBOTD_45_extracted_part
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_45_five_prime_oh_component
+
+
+
+
+
+
+ 1
+
+ three_prime_overhang
+
+
+
+ 26
+
+ three_prime_oh_location
+ 1
+ 29
+
+
+
+
+
+ 1
+
+
+
+ 1
+ five_prime_overhang
+
+
+
+ 1
+ five_prime_oh_location
+ 4
+
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTD_45_three_prime_oh_component
+ 1
+
+
+
+
+
+
+ 1
+
+
+ B0034_3
+
+
+
+
+
+ 1
+
+ UJHDBOTD_45_five_prime_oh
+
+
+
+
+
+
+
+ UJHDBOTD_45_three_prime_oh
+
+
+
+
+ 1
+
+
+
+
+ Scar_C
+
+
+ Scar_C
+ 1
+
+
+
+
+ B0034
+
+
+ B0034
+ 1
+
+
+
+
+
+
+
+ GFP_3
+ 1
+
+
+
+
+ UJHDBOTD_106_part
+
+
+
+
+ 5
+ 721
+ UJHDBOTD_106_part_location
+
+ 1
+
+
+
+ 1
+
+
+
+
+ 1
+
+
+
+
+ 1
+ three_prime_oh_location
+ 725
+
+ 722
+
+
+
+ three_prime_overhang
+
+
+ UJHDBOTD_106_extracted_part
+
+ 1
+
+
+
+
+
+ 1
+ UJHDBOTD_106_five_prime_oh_component
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_three_prime_oh_component
+ 1
+
+
+
+
+
+
+
+ five_prime_overhang
+
+
+
+ five_prime_oh_location
+
+ 4
+ 1
+ 1
+
+
+ 1
+
+
+
+
+
+
+
+ 1
+
+ GFP
+
+ GFP
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_five_prime_oh
+
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_106_three_prime_oh
+
+
+ 1
+ Scar_D
+
+
+
+ Scar_D
+
+
+
+ 1
+
+
+ UJHDBOTD_134_five_prime_oh_component
+
+
+ 1
+
+
+
+
+ UJHDBOTD_134_extracted_part
+
+
+
+ UJHDBOTD_134_three_prime_oh_component
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+ B0015_3
+
+
+
+
+ five_prime_overhang
+
+
+
+
+
+ five_prime_oh_location
+ 4
+ 1
+ 1
+
+
+ 1
+
+
+
+
+
+
+
+ UJHDBOTD_134_part
+ 1
+
+
+
+ 133
+ 1
+ UJHDBOTD_134_part_location
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ three_prime_overhang
+
+
+
+ three_prime_oh_location
+ 137
+
+ 134
+ 1
+
+
+
+
+
+
+
+
+ B0015
+
+ 1
+
+ B0015
+
+
+
+ UJHDBOTD_134_three_prime_oh
+
+
+
+
+ 1
+
+
+ Scar_F
+
+ 1
+
+
+ Scar_F
+
+
+
+ 1
+ UJHDBOTD_134_five_prime_oh
+
+
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_159_three_prime_oh_component
+
+ 1
+
+
+
+
+
+ UJHDBOTD_159_backbone
+
+
+ UJHDBOTD_159_backbone_location
+ 5
+
+ 1
+ 2199
+
+
+
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_159_five_prime_oh_component
+ 1
+
+
+
+
+
+
+
+ three_prime_overhang
+ 1
+
+
+ 2200
+
+ three_prime_oh_location
+
+ 2203
+ 1
+
+
+
+
+
+
+
+
+
+ five_prime_overhang
+
+
+ 1
+ five_prime_oh_location
+ 4
+
+
+ 1
+
+
+
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+ UJHDBOTD_extracted_backbone
+ 1
+
+
+
+
+
+ UJHDBOTD_159_five_prime_oh
+
+
+
+
+ 1
+
+
+
+
+ Cir
+
+
+ 1
+ Cir_qxow
+
+
+ 1
+
+ UJHDBOTD_159_three_prime_oh
+
+
+
+
+
+
+ 1
+
+ T4_Ligase
+
+ T4_Ligase
+
+
+
+ 1
+
+
+
+ Ligation_Scar_A
+
+
+
+
+
+
+ 1
+
+
+
+
+ Ligation_Scar_B
+
+
+ Ligation_Scar_C
+
+
+ 1
+
+
+
+
+
+
+
+
+ Ligation_Scar_D
+ 1
+
+
+
+
+
+
+
+ Ligation_Scar_E
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+ Cir_qxow_5_annotation
+
+
+
+ 2199
+
+ Cir_qxow_5_location
+ 5
+ 1
+
+
+
+
+
+
+
+
+
+ 1
+
+ Cir_qxow
+
+
+
+
+
+
+
+
+
+ 2264
+ 2267
+ Ligation_Scar_D_location
+
+ 1
+
+
+
+
+ 1
+ Ligation_Scar_D_annotation
+
+
+ composite_1
+
+
+ J23101_3_annotation
+
+
+ 1
+
+
+
+ 2238
+ 2204
+ 1
+ J23101_3_location
+
+
+
+
+
+
+
+
+
+
+ Ligation_Scar_C_location
+
+ 2242
+
+ 1
+ 2239
+
+
+ 1
+ Ligation_Scar_C_annotation
+
+
+
+
+
+ GFP
+
+ 1
+
+
+
+
+
+
+
+
+
+
+ B0015
+ 1
+
+
+
+
+
+ 1
+
+
+ Ligation_Scar_B
+
+
+
+
+
+ B0034_3_annotation
+ 1
+
+
+ 1
+ B0034_3_location
+
+ 2243
+ 2263
+
+
+
+
+
+
+
+ J23101
+
+
+
+ 1
+
+
+
+
+
+
+ 1
+ Ligation_Scar_B_annotation
+
+
+ 2203
+ 1
+ Ligation_Scar_B_location
+
+
+ 2200
+
+
+
+
+ 1
+
+
+
+
+ 2989
+ 3117
+ 1
+ B0015_3_location
+
+
+
+
+ 1
+
+ B0015_3_annotation
+
+
+
+
+
+
+
+
+
+
+ 2985
+ Ligation_Scar_E_location
+ 2988
+ 1
+
+
+
+
+ Ligation_Scar_E_annotation
+ 1
+
+
+
+
+ Ligation_Scar_A_annotation
+ 1
+
+
+
+ 1
+ 4
+ 1
+
+ Ligation_Scar_A_location
+
+
+
+
+
+
+
+
+
+
+ 1
+
+ B0034
+
+
+
+
+
+
+ 1
+
+
+ 2984
+ GFP_3_location
+ 1
+ 2268
+
+
+
+
+ GFP_3_annotation
+
+
+
+ composite_1
+
+
+ Ligation_Scar_D
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+ Ligation_Scar_C
+
+
+
+
+
+
+ Ligation_Scar_A
+
+ 1
+
+
+
+
+ Ligation_Scar_E
+
+
+
+ 1
+
+
+
+
+
+ UJHDBOTD_70_extracted_part_seq
+
+ GGAGtttacagctagctcagtcctaggtattatgctagcTACT
+ 1
+
+
+
+
+ 1
+
+ UJHDBOTD_70_three_prime_oh_sequence
+ TACT
+
+
+ Scar_B_sequence
+
+ TACT
+
+
+
+ J23101_sequence
+
+
+ tttacagctagctcagtcctaggtattatgctagc
+
+
+ GGAG
+ 1
+
+
+
+ UJHDBOTD_70_five_prime_oh_sequence
+
+
+
+
+ Scar_A_sequence
+ GGAG
+
+
+
+ 1
+ TACTagagaaagaggagaaatactaaatg
+ UJHDBOTD_45_extracted_part_seq
+
+
+
+
+
+
+ UJHDBOTD_45_five_prime_oh_sequence
+ 1
+ TACT
+
+
+
+ UJHDBOTD_45_three_prime_oh_sequence
+
+
+ 1
+ aatg
+
+
+ Scar_C_sequence
+
+ aatg
+
+
+
+ B0034_sequence
+
+ agagaaagaggagaaatacta
+
+
+
+
+ UJHDBOTD_106_extracted_part_seq
+
+ 1
+ aatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagctt
+
+
+
+
+ GFP_sequence
+ gtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaa
+
+
+
+
+
+ 1
+ aatg
+ UJHDBOTD_106_five_prime_oh_sequence
+
+
+
+
+ 1
+ UJHDBOTD_106_three_prime_oh_sequence
+ gctt
+
+
+
+
+ Scar_D_sequence
+
+ gctt
+
+
+ UJHDBOTD_134_extracted_part_seq
+ 1
+ gcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttatacgct
+
+
+
+
+
+ ccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+
+ B0015_sequence
+
+
+ cgct
+
+
+ 1
+
+ UJHDBOTD_134_three_prime_oh_sequence
+
+
+ cgct
+
+
+ Scar_F_sequence
+
+
+ UJHDBOTD_134_five_prime_oh_sequence
+
+ 1
+
+ gctt
+
+
+
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgggag
+
+
+ UJHDBOTD_extracted_backbone_seq
+ 1
+
+
+
+ 1
+ cgct
+
+ UJHDBOTD_159_five_prime_oh_sequence
+
+
+
+
+ Cir_qxow_sequence
+
+ gcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatg
+
+
+
+ UJHDBOTD_159_three_prime_oh_sequence
+ ggag
+ 1
+
+
+
+
+
+ 1
+ Ligation_Scar_A_sequence
+
+ cgct
+
+
+
+ ggag
+ 1
+
+ Ligation_Scar_B_sequence
+
+
+
+
+ 1
+ TACT
+ Ligation_Scar_C_sequence
+
+
+ Ligation_Scar_D_sequence
+ aatg
+
+ 1
+
+
+
+ gctt
+
+ Ligation_Scar_E_sequence
+ 1
+
+
+
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgGGAGtttacagctagctcagtcctaggtattatgctagcTACTagagaaagaggagaaatactaaatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+
+ 1
+ composite_1_seq
+
+
+
diff --git a/tests/backbone.xml b/tests/backbone.xml
new file mode 100644
index 0000000..f2a69e2
--- /dev/null
+++ b/tests/backbone.xml
@@ -0,0 +1,328 @@
+
+
+
+
+ module1
+
+
+
+ UJHDBOTD_159
+
+
+
+
+
+
+
+
+ Scar_F
+ 1
+ Scar_F
+
+
+
+
+
+
+ RFP_cassette
+ 1
+ RFP_cassette
+
+
+
+
+
+
+ UJHDBOTD
+ 1
+
+
+
+
+
+
+
+ Scar_F_4
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+
+
+
+
+
+ Scar_A_2
+ 1
+
+
+
+
+
+
+
+ RFP_cassette_3
+ 1
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 1092
+ 3286
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 1088
+ 1091
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 1087
+
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint1
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint3
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint2
+ 1
+
+
+
+
+
+
+
+
+
+ Cir_qxow
+ 1
+ Cir
+
+
+
+
+
+
+ Scar_A
+ 1
+ Scar_A
+
+
+
+
+
+
+ UJHDBOTD_sequence
+ ggagtgagacccaatacgcaaaccgcctctccccgcgcgttggccgattcattaatgcagctggcacgacaggtttcccgactggaaagcgggcagtgagcgcaacgcaattaatgtgagttagctcactcattaggcaccccaggctttacactttatgcttccggctcgtatgttgtgtggaattgtgagcggataacaatttcacacatactagagaaagaggagaaatactagatggcttcctccgaagacgttatcaaagagttcatgcgtttcaaagttcgtatggaaggttccgttaacggtcacgagttcgaaatcgaaggtgaaggtgaaggtcgtccgtacgaaggtacccagaccgctaaactgaaagttaccaaaggtggtccgctgccgttcgcttgggacatcctgtccccgcagttccagtacggttccaaagcttacgttaaacacccggctgacatcccggactacctgaaactgtccttcccggaaggtttcaaatgggaacgtgttatgaacttcgaagacggtggtgttgttaccgttacccaggactcctccctgcaagacggtgagttcatctacaaagttaaactgcgtggtaccaacttcccgtccgacggtccggttatgcagaaaaaaaccatgggttgggaagcttccaccgaacgtatgtacccggaagacggtgctctgaaaggtgaaatcaaaatgcgtctgaaactgaaagacggtggtcactacgacgctgaagttaaaaccacctacatggctaaaaaaccggttcagctgccgggtgcttacaaaaccgacatcaaactggacatcacctcccacaacgaagactacaccatcgttgaacagtacgaacgtgctgaaggtcgtcactccaccggtgcttaataacgctgatagtgctagtgtagatcgctactagagccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttataggtctcacgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatg
+
+
+
+
+ Scar_F_sequence
+ cgct
+
+
+
+
+ Cir_qxow_sequence
+ gcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatg
+
+
+
+
+ RFP_cassette_sequence
+ tgagacccaatacgcaaaccgcctctccccgcgcgttggccgattcattaatgcagctggcacgacaggtttcccgactggaaagcgggcagtgagcgcaacgcaattaatgtgagttagctcactcattaggcaccccaggctttacactttatgcttccggctcgtatgttgtgtggaattgtgagcggataacaatttcacacatactagagaaagaggagaaatactagatggcttcctccgaagacgttatcaaagagttcatgcgtttcaaagttcgtatggaaggttccgttaacggtcacgagttcgaaatcgaaggtgaaggtgaaggtcgtccgtacgaaggtacccagaccgctaaactgaaagttaccaaaggtggtccgctgccgttcgcttgggacatcctgtccccgcagttccagtacggttccaaagcttacgttaaacacccggctgacatcccggactacctgaaactgtccttcccggaaggtttcaaatgggaacgtgttatgaacttcgaagacggtggtgttgttaccgttacccaggactcctccctgcaagacggtgagttcatctacaaagttaaactgcgtggtaccaacttcccgtccgacggtccggttatgcagaaaaaaaccatgggttgggaagcttccaccgaacgtatgtacccggaagacggtgctctgaaaggtgaaatcaaaatgcgtctgaaactgaaagacggtggtcactacgacgctgaagttaaaaccacctacatggctaaaaaaccggttcagctgccgggtgcttacaaaaccgacatcaaactggacatcacctcccacaacgaagactacaccatcgttgaacagtacgaacgtgctgaaggtcgtcactccaccggtgcttaataacgctgatagtgctagtgtagatcgctactagagccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttataggtctca
+
+
+
+
+ Scar_A_sequence
+ ggag
+
+
+
+
+ Cir_qxow_Layout
+
+
+
+
+ Scar_A_Layout
+
+
+
+
+ UJHDBOTD_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_A_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ RFP_cassette_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_F_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_qxow_5
+
+
+
+
+
+
+ Scar_F_Layout
+
+
+
+
+ RFP_cassette_Layout
+
+
+
+
+ module1_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ UJHDBOTD_159
+
+
+
+
+
diff --git a/tests/cds_in_bb.xml b/tests/cds_in_bb.xml
new file mode 100644
index 0000000..54eb11d
--- /dev/null
+++ b/tests/cds_in_bb.xml
@@ -0,0 +1,328 @@
+
+
+
+
+ module1
+
+
+
+ UJHDBOTD_106
+
+
+
+
+
+
+
+
+ GFP
+ 1
+ GFP
+
+
+
+
+
+
+ Scar_C
+ 1
+ Scar_C
+
+
+
+
+
+
+ Scar_D
+ 1
+ Scar_D
+
+
+
+
+
+
+ Cir_qxow
+ 1
+ Cir
+
+
+
+
+
+
+ UJHDBOTD
+ 1
+
+
+
+
+
+
+
+ Scar_D_4
+ 1
+
+
+
+
+
+
+
+ GFP_3
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+
+
+
+
+
+ Scar_C_2
+ 1
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 722
+ 725
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 721
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 726
+ 2777
+
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint3
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint2
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint1
+ 1
+
+
+
+
+
+
+
+
+
+ Scar_C_sequence
+ aatg
+
+
+
+
+ GFP_sequence
+ gtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaa
+
+
+
+
+ Scar_D_sequence
+ gctt
+
+
+
+
+ UJHDBOTD_sequence
+ aatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagcttCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ Cir_qxow_sequence
+ CGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ UJHDBOTD_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_C_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ GFP_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_D_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_qxow_5
+
+
+
+
+
+
+ module1_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ UJHDBOTD_106
+
+
+
+
+
+
+ GFP_Layout
+
+
+
+
+ Cir_qxow_Layout
+
+
+
+
+ Scar_D_Layout
+
+
+
+
+ Scar_C_Layout
+
+
+
diff --git a/tests/ligation_test_forreal.xml b/tests/ligation_test_forreal.xml
new file mode 100644
index 0000000..9893bef
--- /dev/null
+++ b/tests/ligation_test_forreal.xml
@@ -0,0 +1,627 @@
+
+
+ 1
+
+
+ UJHDBOTD_backbone
+
+
+
+
+
+
+ 1
+ 2203
+
+ 2200
+
+ three_prime_fusion_site_location
+
+
+
+
+ fusion_sites_annotation
+ 1
+
+
+ 1
+ 4
+
+ 1
+
+ five_prime_fusion_site_location
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ 1
+ 722
+ three_prime_fusion_site_location
+
+
+ 725
+
+
+ fusion_sites_annotation
+
+
+
+
+ 1
+
+
+ five_prime_fusion_site_location
+ 4
+ 1
+
+
+ 1
+
+
+
+
+ UJHDBOTD_106_part_extract
+
+
+
+ UJHDBOTD_70_part_extract
+ 1
+
+
+
+ fusion_sites_annotation
+
+
+ 1
+
+
+
+ five_prime_fusion_site_location
+ 4
+ 1
+
+ 1
+
+
+
+
+ 1
+ three_prime_fusion_site_location
+
+
+ 40
+ 43
+
+
+
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_45_part_extract
+
+
+
+
+
+
+ three_prime_fusion_site_location
+ 1
+ 29
+
+ 26
+
+
+
+
+ 4
+
+ 1
+ five_prime_fusion_site_location
+ 1
+
+
+
+
+ 1
+ fusion_sites_annotation
+
+
+
+
+
+ UJHDBOTD_134_part_extract
+
+ 1
+
+
+
+
+
+
+ 1
+ fusion_sites_annotation
+
+
+ 134
+ 1
+
+
+ 137
+ three_prime_fusion_site_location
+
+
+
+
+ 1
+ 4
+ 1
+
+ five_prime_fusion_site_location
+
+
+
+
+
+
+
+
+ BsaI
+ 1
+
+
+
+ Restriction enzyme BsaI from REBASE.
+ BsaI
+
+
+
+
+ UJHDBOTD_70_part_extract
+ 1
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_backbone
+
+
+
+ 1
+
+
+
+ composite_1_UJHDBOTD_106_UJHDBOTD_134_UJHDBOTD_backbone_UJHDBOTD_70_UJHDBOTD_45
+
+
+
+ 1
+ UJHDBOTD_45_part_extract
+
+
+
+
+
+
+
+ UJHDBOTD_106_part_extract
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_part_extract
+ 1
+
+
+
+
+
+ composite_1_UJHDBOTD_106_UJHDBOTD_134_UJHDBOTD_backbone_UJHDBOTD_70_UJHDBOTD_45
+
+
+ UJHDBOTD_backbone_seq
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgggag
+
+ 1
+
+
+
+
+ UJHDBOTD_106_part_extract_seq
+
+ aatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagctt
+ 1
+
+
+ UJHDBOTD_70_part_extract_seq
+ 1
+ GGAGtttacagctagctcagtcctaggtattatgctagcTACT
+
+
+
+
+ TACTagagaaagaggagaaatactaaatg
+ 1
+
+ UJHDBOTD_45_part_extract_seq
+
+
+
+
+
+ UJHDBOTD_134_part_extract_seq
+ 1
+ gcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttatacgct
+
+
+
+ 1
+ aatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttatacgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgGGAGtttacagctagctcagtcctaggtattatgctagcTACTagagaaagaggagaaatacta
+
+ composite_1_UJHDBOTD_106_UJHDBOTD_134_UJHDBOTD_backbone_UJHDBOTD_70_UJHDBOTD_45_seq
+
+
+
+
+ UJHDBOTD_134_reaction_component
+
+
+
+
+ 1
+
+
+
+
+ 1
+
+ UJHDBOTD_106_product_component
+
+
+
+
+
+
+
+
+ 1
+ BsaI_enzyme
+
+
+
+
+
+
+
+ UJHDBOTD_45_reactant_component
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_reactant_participation
+
+
+
+
+
+
+ restriction
+
+
+
+ 1
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_70_product_participation
+
+
+ UJHDBOTD_70_asssembly_plan_interaction
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+ restriction
+
+
+
+
+
+
+
+ UJHDBOTD_106_reactant_participation
+ 1
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_product_participation
+
+
+
+
+ UJHDBOTD_106_asssembly_plan_interaction
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_backbone_reactant_component
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_70_reactant_component
+ 1
+
+
+
+
+
+ UJHDBOTD_134_product_component
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_reaction_component
+
+ 1
+
+
+
+
+
+
+
+ UJHDBOTD_45_reactant_participation
+
+
+ 1
+
+
+
+ UJHDBOTD_45_asssembly_plan_interaction
+
+
+
+
+
+
+ 1
+ UJHDBOTD_45_product_participation
+
+
+
+
+
+
+
+ 1
+ restriction
+
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_134_reactant_component
+
+
+ 1
+
+
+
+
+
+ UJHDBOTD_70_reaction_component
+
+
+
+ 1
+
+
+ 1
+
+
+
+
+ UJHDBOTD_106_reactant_component
+
+ 1
+
+
+
+ bb_assembly_plan
+
+
+ UJHDBOTD_45_product_component
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+ 1
+
+ restriction
+
+
+
+
+
+
+
+ 1
+
+ UJHDBOTD_134_reactant_participation
+
+
+
+
+
+ 1
+ UJHDBOTD_134_product_participation
+
+
+
+
+
+ UJHDBOTD_134_asssembly_plan_interaction
+ 1
+
+
+
+
+
+ 1
+ UJHDBOTD_70_product_component
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_backbone_reactant_participation
+
+
+
+
+ restriction
+
+
+
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTD_backbone_product_participation
+ 1
+
+
+ asssembly_plan_interaction
+ 1
+
+
+
+
+
+ UJHDBOTD_backbone_reaction_component
+
+ 1
+
+
+
+
+
+
+
+ UJHDBOTD_backbone_product_component
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+ UJHDBOTD_45_reaction_component
+
+
+
+
+
+
diff --git a/tests/out_doc.xml b/tests/out_doc.xml
new file mode 100644
index 0000000..5df6b98
--- /dev/null
+++ b/tests/out_doc.xml
@@ -0,0 +1,1580 @@
+
+
+
+
+
+
+ UJHDBOTD_45_reactant
+
+
+ 1
+
+
+ testassem
+
+
+
+
+ 1
+
+
+ UJHDBOTD_backbone_product
+
+
+ 1
+
+
+
+ UJHDBOTD_digestion_interaction
+
+
+
+ UJHDBOTD_backbone_reactant
+
+
+ 1
+
+
+
+
+
+
+ restriction
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_106_reactant
+
+
+
+ 1
+
+
+
+
+
+
+ 1
+
+
+
+ BsaI_enzyme
+
+
+
+
+
+
+ UJHDBOTD_45_reactant
+
+
+ 1
+
+
+
+
+
+ 1
+
+ restriction
+
+
+
+
+ UJHDBOTD_45_digestion_interaction
+ 1
+
+
+
+
+ 1
+
+ UJHDBOTD_45_product
+
+
+
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_70_digestion_product
+
+
+
+
+
+ UJHDBOTD_106_digestion_interaction
+
+
+
+
+ 1
+ UJHDBOTD_106_product
+
+
+
+
+
+
+
+ restriction
+ 1
+
+
+
+
+ 1
+
+
+
+
+ UJHDBOTD_106_reactant
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+ T4_Ligase
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_45_digestion_product
+
+
+
+
+
+ UJHDBOTD_backbone_reactant
+ 1
+
+
+
+
+
+ 1
+
+
+ 1
+
+
+
+
+ UJHDBOTD_106_digestion_product
+
+
+
+
+
+ 1
+ UJHDBOTD_70_reactant
+
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_backbone_digestion_product
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_reactant
+ 1
+
+
+
+
+
+ UJHDBOTD_134_digestion_product
+
+
+
+
+ 1
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+ restriction
+
+
+ UJHDBOTD_70_digestion_interaction
+
+
+
+
+
+ 1
+ UJHDBOTD_70_reactant
+
+
+
+
+ UJHDBOTD_70_product
+
+ 1
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ ligation
+
+ 1
+
+
+
+
+
+ 1
+ UJHDBOTD_134_extracted_part_ligation
+
+
+
+
+
+
+ UJHDBOTD_extracted_backbone_ligation
+
+ 1
+
+
+
+
+ composite_1_ligation_interaction
+
+
+
+
+ UJHDBOTD_70_extracted_part_ligation
+ 1
+
+
+
+
+
+
+ composite_1_product
+
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_extracted_part_ligation
+
+
+
+
+
+ UJHDBOTD_45_extracted_part_ligation
+
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+ composite_1
+
+
+
+
+
+
+
+
+
+
+
+ restriction
+ 1
+
+
+
+
+ UJHDBOTD_134_digestion_interaction
+ 1
+
+
+ UJHDBOTD_134_reactant
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+ UJHDBOTD_134_product
+
+
+
+
+
+
+ BsaI
+ Restriction enzyme BsaI from REBASE.
+
+
+ 1
+
+ BsaI
+
+
+
+
+
+
+ J23101_3
+
+ 1
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_part
+
+
+ 1
+ UJHDBOTD_70_part_location
+ 5
+ 39
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ 4
+ 1
+ 1
+
+ five_prime_oh_location
+
+
+
+ five_prime_overhang
+
+
+ UJHDBOTD_70_extracted_part
+
+
+
+
+ 1
+
+
+
+
+ 40
+ 1
+ 43
+
+ three_prime_oh_location
+
+
+ three_prime_overhang
+
+
+
+
+
+ 1
+
+ UJHDBOTD_70_three_prime_oh_component
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_70_five_prime_oh_component
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_70_three_prime_oh
+
+
+
+
+
+ 1
+
+
+
+
+
+
+ Scar_B
+ Scar_B
+ 1
+
+
+
+
+
+ J23101
+ 1
+ J23101
+
+
+
+
+
+
+
+ UJHDBOTD_70_five_prime_oh
+ 1
+
+
+
+ Scar_A
+
+
+ 1
+ Scar_A
+
+
+
+
+
+
+ UJHDBOTD_45_part
+ 1
+
+
+
+ 25
+ UJHDBOTD_45_part_location
+
+ 1
+ 5
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_45_five_prime_oh_component
+ 1
+
+
+
+
+
+
+
+
+
+ 26
+
+ three_prime_oh_location
+ 29
+
+ 1
+
+
+ three_prime_overhang
+ 1
+
+
+ UJHDBOTD_45_extracted_part
+
+
+
+
+
+
+
+ B0034_3
+ 1
+
+
+
+
+
+
+
+ UJHDBOTD_45_three_prime_oh_component
+ 1
+
+
+
+
+
+
+ 1
+
+
+ five_prime_overhang
+
+ 1
+
+
+
+ five_prime_oh_location
+
+
+ 1
+ 4
+ 1
+
+
+
+
+
+
+ UJHDBOTD_45_five_prime_oh
+
+
+
+
+
+ 1
+
+
+
+
+ 1
+
+
+ UJHDBOTD_45_three_prime_oh
+
+
+
+
+ 1
+ Scar_C
+ Scar_C
+
+
+
+
+
+ 1
+ B0034
+
+
+
+
+ B0034
+
+
+
+
+
+ GFP_3
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+ UJHDBOTD_106_part
+
+
+ 721
+
+ 5
+
+ 1
+ UJHDBOTD_106_part_location
+
+
+
+
+
+
+ 1
+
+
+
+
+ 1
+ three_prime_oh_location
+ 722
+ 725
+
+
+
+ three_prime_overhang
+
+
+
+
+
+
+
+ UJHDBOTD_106_five_prime_oh_component
+
+
+
+ 1
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_three_prime_oh_component
+
+
+
+ UJHDBOTD_106_extracted_part
+ 1
+
+
+
+ 1
+
+
+
+ 1
+
+ five_prime_oh_location
+ 1
+ 4
+
+
+
+
+ five_prime_overhang
+
+
+
+
+
+ 1
+
+ GFP
+
+
+
+ GFP
+
+
+ UJHDBOTD_106_five_prime_oh
+
+ 1
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_three_prime_oh
+ 1
+
+
+
+
+
+
+
+ Scar_D
+
+ Scar_D
+ 1
+
+
+
+
+
+
+
+ UJHDBOTD_134_five_prime_oh_component
+ 1
+
+
+
+ 1
+
+
+
+
+
+ 4
+
+ 1
+ five_prime_oh_location
+
+ 1
+
+
+
+ five_prime_overhang
+ 1
+
+
+
+
+ 1
+
+ B0015_3
+
+
+
+
+
+
+
+
+
+
+ 134
+ three_prime_oh_location
+
+ 137
+ 1
+
+
+ 1
+
+ three_prime_overhang
+
+
+
+
+
+
+
+
+
+
+ 5
+
+ 1
+
+ 133
+ UJHDBOTD_134_part_location
+
+
+ UJHDBOTD_134_part
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_134_three_prime_oh_component
+
+
+ UJHDBOTD_134_extracted_part
+
+
+
+ B0015
+
+
+ 1
+ B0015
+
+
+
+
+
+
+ 1
+ UJHDBOTD_134_three_prime_oh
+
+
+
+
+
+
+ 1
+ Scar_F
+
+
+ Scar_F
+
+
+
+
+ 1
+ UJHDBOTD_134_five_prime_oh
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_159_five_prime_oh_component
+ 1
+
+
+
+
+
+ UJHDBOTD_159_backbone
+
+
+
+ 1
+ 2199
+ UJHDBOTD_159_backbone_location
+
+
+ 5
+
+
+ 1
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+
+
+
+
+ 1
+
+
+ three_prime_overhang
+
+
+
+
+ three_prime_oh_location
+ 2200
+ 2203
+
+ 1
+
+
+
+ 1
+
+
+ UJHDBOTD_extracted_backbone
+
+
+
+
+ UJHDBOTD_159_three_prime_oh_component
+
+
+ 1
+
+
+
+
+
+ five_prime_overhang
+
+
+
+ 4
+ five_prime_oh_location
+ 1
+
+ 1
+
+
+
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTD_159_five_prime_oh
+
+ 1
+
+
+
+ Cir
+
+
+
+
+ Cir_qxow
+ 1
+
+
+ UJHDBOTD_159_three_prime_oh
+
+
+
+ 1
+
+
+
+
+ T4_Ligase
+
+
+ 1
+ T4_Ligase
+
+
+
+ 1
+
+
+
+
+
+ Ligation_Scar_A
+
+
+
+ 1
+ Ligation_Scar_B
+
+
+
+
+
+
+
+
+
+
+
+ Ligation_Scar_C
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+ Ligation_Scar_D
+
+
+
+
+
+
+
+ Ligation_Scar_E
+ 1
+
+
+
+
+
+
+
+
+ B0034_3_location
+
+ 1
+ 2263
+
+ 2243
+
+
+
+ 1
+ B0034_3_annotation
+
+
+
+
+
+ Ligation_Scar_C
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+ Ligation_Scar_E
+
+
+
+
+
+ 1
+
+
+
+ 1
+ Ligation_Scar_E_location
+
+ 2988
+ 2985
+
+
+
+
+ Ligation_Scar_E_annotation
+
+
+
+
+
+
+ 1
+ J23101
+
+
+
+
+
+
+
+ Ligation_Scar_D
+
+ 1
+
+
+
+
+
+ Ligation_Scar_C_annotation
+
+
+ 1
+
+
+
+ 1
+
+ 2242
+ Ligation_Scar_C_location
+ 2239
+
+
+
+
+ composite_1
+
+
+
+
+ Ligation_Scar_D_annotation
+
+
+ 2267
+
+ Ligation_Scar_D_location
+ 1
+
+ 2264
+
+
+ 1
+
+
+
+
+
+
+ B0015
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+ 1
+ Ligation_Scar_A_location
+ 4
+ 1
+
+
+ Ligation_Scar_A_annotation
+
+
+
+
+
+
+ B0015_3_annotation
+
+ 1
+
+
+
+
+ 3117
+ 2989
+ B0015_3_location
+
+ 1
+
+
+
+
+
+
+ Ligation_Scar_A
+ 1
+
+
+
+
+
+
+
+
+
+ 2200
+ Ligation_Scar_B_location
+
+ 2203
+
+ 1
+
+
+
+ 1
+
+ Ligation_Scar_B_annotation
+
+
+
+
+
+
+
+ Ligation_Scar_B
+ 1
+
+
+
+
+
+
+
+
+ 1
+ GFP_3_annotation
+
+
+ GFP_3_location
+ 2984
+
+ 1
+
+ 2268
+
+
+
+
+
+
+
+
+ GFP
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+ 2204
+ 2238
+ J23101_3_location
+
+
+ J23101_3_annotation
+ 1
+
+
+
+
+
+
+ 1
+ Cir_qxow
+
+
+
+
+
+
+
+
+
+
+ B0034
+ 1
+
+
+ 1
+ composite_1
+
+
+
+
+ 1
+ Cir_qxow_5_annotation
+
+
+ 1
+ 2199
+ 5
+ Cir_qxow_5_location
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_70_extracted_part_seq
+ GGAGtttacagctagctcagtcctaggtattatgctagcTACT
+ 1
+
+
+
+
+
+ 1
+
+ UJHDBOTD_70_three_prime_oh_sequence
+ TACT
+
+
+ TACT
+ Scar_B_sequence
+
+
+
+
+
+ tttacagctagctcagtcctaggtattatgctagc
+
+ J23101_sequence
+
+
+
+
+ 1
+ UJHDBOTD_70_five_prime_oh_sequence
+
+ GGAG
+
+
+
+ GGAG
+ Scar_A_sequence
+
+
+
+
+ UJHDBOTD_45_extracted_part_seq
+
+ 1
+ TACTagagaaagaggagaaatactaaatg
+
+
+ UJHDBOTD_45_five_prime_oh_sequence
+ TACT
+
+
+
+ 1
+
+
+ aatg
+ UJHDBOTD_45_three_prime_oh_sequence
+
+
+
+ 1
+
+
+
+ Scar_C_sequence
+ aatg
+
+
+
+ B0034_sequence
+
+
+ agagaaagaggagaaatacta
+
+
+
+ UJHDBOTD_106_extracted_part_seq
+
+ aatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagctt
+ 1
+
+
+
+ GFP_sequence
+ gtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaa
+
+
+
+ 1
+ aatg
+
+
+
+ UJHDBOTD_106_five_prime_oh_sequence
+
+
+
+ 1
+
+
+ UJHDBOTD_106_three_prime_oh_sequence
+ gctt
+
+
+ Scar_D_sequence
+
+ gctt
+
+
+
+ UJHDBOTD_134_extracted_part_seq
+ gcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttatacgct
+
+
+ 1
+
+
+
+
+ B0015_sequence
+ ccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+
+
+
+ cgct
+
+ UJHDBOTD_134_three_prime_oh_sequence
+
+ 1
+
+
+
+ cgct
+
+ Scar_F_sequence
+
+
+
+
+ gctt
+
+ UJHDBOTD_134_five_prime_oh_sequence
+ 1
+
+
+
+ UJHDBOTD_extracted_backbone_seq
+ 1
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgggag
+
+
+
+
+ cgct
+
+
+ 1
+ UJHDBOTD_159_five_prime_oh_sequence
+
+
+
+ gcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatg
+ Cir_qxow_sequence
+
+
+
+
+ ggag
+
+
+ 1
+ UJHDBOTD_159_three_prime_oh_sequence
+
+
+ Ligation_Scar_A_sequence
+ cgct
+
+
+ 1
+
+
+ ggag
+
+
+ Ligation_Scar_B_sequence
+ 1
+
+
+
+ TACT
+ Ligation_Scar_C_sequence
+ 1
+
+
+
+ Ligation_Scar_D_sequence
+
+ aatg
+
+ 1
+
+
+ Ligation_Scar_E_sequence
+ gctt
+ 1
+
+
+
+
+
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgGGAGtttacagctagctcagtcctaggtattatgctagcTACTagagaaagaggagaaatactaaatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+ composite_1_seq
+ 1
+
+
+
diff --git a/tests/pro2_in_bb.xml b/tests/pro2_in_bb.xml
new file mode 100644
index 0000000..a51d262
--- /dev/null
+++ b/tests/pro2_in_bb.xml
@@ -0,0 +1,330 @@
+
+
+
+
+ module1
+
+
+
+ UJHDBOTD_3
+
+
+
+
+
+
+
+
+ Scar_B
+ 1
+ Scar_B
+
+
+
+
+
+
+ UJHDBOTD
+ 1
+
+
+
+
+
+
+
+ Bba_J23106_3
+ 1
+
+
+
+
+
+
+
+ Scar_A_2
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+
+
+
+
+
+ Scar_B_4
+ 1
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 44
+ 2095
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 40
+ 43
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 39
+
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint3
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint2
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint1
+ 1
+
+
+
+
+
+
+
+
+
+ Bba_J23106
+ 1
+
+ Bba_J23106
+ Constitutive promoter. From Anderson promoter collection, suitable for e. coli.
+
+
+
+
+
+
+ Cir_qxow
+ 1
+ Cir
+
+
+
+
+
+
+ Scar_A
+ 1
+ Scar_A
+
+
+
+
+
+
+ Bba_J23106_sequence
+ tttacggctagctcagtcctaggtatagtgctagc
+
+
+
+
+ Cir_qxow_sequence
+ CGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ UJHDBOTD_sequence
+ GGAGtttacggctagctcagtcctaggtatagtgctagcTACTCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ Scar_A_sequence
+ GGAG
+
+
+
+
+ Scar_B_sequence
+ TACT
+
+
+
+
+ module1_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ UJHDBOTD_3
+
+
+
+
+
+
+ UJHDBOTD_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_A_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ Bba_J23106_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_B_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_qxow_5
+
+
+
+
+
+
+ Cir_qxow_Layout
+
+
+
+
+ Scar_A_Layout
+
+
+
+
+ Scar_B_Layout
+
+
+
+
+ Bba_J23106_Layout
+
+
+
diff --git a/tests/pro3_in_bb.xml b/tests/pro3_in_bb.xml
new file mode 100644
index 0000000..bacd529
--- /dev/null
+++ b/tests/pro3_in_bb.xml
@@ -0,0 +1,330 @@
+
+
+
+
+ module1
+
+
+
+ UJHDBOTD_3
+
+
+
+
+
+
+
+
+ UJHDBOTD
+ 1
+
+
+
+
+
+
+
+ Bba_J23107_3
+ 1
+
+
+
+
+
+
+
+ Scar_A_2
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+
+
+
+
+
+ Scar_B_4
+ 1
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 44
+ 2095
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 40
+ 43
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 39
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint1
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint3
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint2
+ 1
+
+
+
+
+
+
+
+
+
+ Scar_B
+ 1
+ Scar_B
+
+
+
+
+
+
+ Bba_J23107
+ 1
+
+ Bba_J23107
+ Constitutive promoter. From Anderson promoter collection, suitable for e. coli.
+
+
+
+
+
+
+ Cir_qxow
+ 1
+ Cir
+
+
+
+
+
+
+ Scar_A
+ 1
+ Scar_A
+
+
+
+
+
+
+ Bba_J23107_sequence
+ tttacggctagctcagccctaggtattatgctagc
+
+
+
+
+ Cir_qxow_sequence
+ CGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ UJHDBOTD_sequence
+ GGAGtttacggctagctcagtcctaggtatagtgctagcTACTCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ Scar_A_sequence
+ GGAG
+
+
+
+
+ Scar_B_sequence
+ TACT
+
+
+
+
+ module1_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ UJHDBOTD_3
+
+
+
+
+
+
+ Cir_qxow_Layout
+
+
+
+
+ UJHDBOTD_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_A_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ Bba_J23107_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_B_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_qxow_5
+
+
+
+
+
+
+ Bba_J23107_Layout
+
+
+
+
+ Scar_A_Layout
+
+
+
+
+ Scar_B_Layout
+
+
+
diff --git a/tests/pro_in_bb.xml b/tests/pro_in_bb.xml
new file mode 100644
index 0000000..773ce8d
--- /dev/null
+++ b/tests/pro_in_bb.xml
@@ -0,0 +1,328 @@
+
+
+
+
+ module1
+
+
+
+ UJHDBOTD_70
+
+
+
+
+
+
+
+
+ Scar_B
+ 1
+ Scar_B
+
+
+
+
+
+
+ UJHDBOTD
+ 1
+
+
+
+
+
+
+
+ J23101_3
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+
+
+
+
+
+ Scar_B_4
+ 1
+
+
+
+
+
+
+
+ Scar_A_2
+ 1
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 39
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 44
+ 2095
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 40
+ 43
+
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint3
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint2
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint1
+ 1
+
+
+
+
+
+
+
+
+
+ J23101
+ 1
+ J23101
+
+
+
+
+
+
+ Cir_qxow
+ 1
+ Cir
+
+
+
+
+
+
+ Scar_A
+ 1
+ Scar_A
+
+
+
+
+
+
+ UJHDBOTD_sequence
+ GGAGtttacagctagctcagtcctaggtattatgctagcTACTCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ J23101_sequence
+ tttacagctagctcagtcctaggtattatgctagc
+
+
+
+
+ Cir_qxow_sequence
+ CGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ Scar_A_sequence
+ GGAG
+
+
+
+
+ Scar_B_sequence
+ TACT
+
+
+
+
+ module1_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ UJHDBOTD_70
+
+
+
+
+
+
+ Cir_qxow_Layout
+
+
+
+
+ Scar_A_Layout
+
+
+
+
+ UJHDBOTD_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_A_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ J23101_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_B_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_qxow_5
+
+
+
+
+
+
+ Scar_B_Layout
+
+
+
+
+ J23101_Layout
+
+
+
diff --git a/tests/rbs_in_bb.xml b/tests/rbs_in_bb.xml
new file mode 100644
index 0000000..a1ddebd
--- /dev/null
+++ b/tests/rbs_in_bb.xml
@@ -0,0 +1,328 @@
+
+
+
+
+ module1
+
+
+
+ UJHDBOTD_45
+
+
+
+
+
+
+
+
+ UJHDBOTD
+ 1
+
+
+
+
+
+
+
+ Scar_B_2
+ 1
+
+
+
+
+
+
+
+ B0034_3
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+
+
+
+
+
+ Scar_C_4
+ 1
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 30
+ 2081
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 26
+ 29
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 25
+
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint3
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint1
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint2
+ 1
+
+
+
+
+
+
+
+
+
+ Scar_B
+ 1
+ Scar_B
+
+
+
+
+
+
+ Scar_C
+ 1
+ Scar_C
+
+
+
+
+
+
+ B0034
+ 1
+ B0034
+
+
+
+
+
+
+ Cir_qxow
+ 1
+ Cir
+
+
+
+
+
+
+ Scar_C_sequence
+ aatg
+
+
+
+
+ B0034_sequence
+ agagaaagaggagaaatacta
+
+
+
+
+ Cir_qxow_sequence
+ CGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ UJHDBOTD_sequence
+ TACTagagaaagaggagaaatactaaatgCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ Scar_B_sequence
+ TACT
+
+
+
+
+ module1_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ UJHDBOTD_45
+
+
+
+
+
+
+ B0034_Layout
+
+
+
+
+ UJHDBOTD_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_B_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ B0034_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_C_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_qxow_5
+
+
+
+
+
+
+ Cir_qxow_Layout
+
+
+
+
+ Scar_B_Layout
+
+
+
+
+ Scar_C_Layout
+
+
+
diff --git a/tests/terminator_in_bb.xml b/tests/terminator_in_bb.xml
new file mode 100644
index 0000000..704245a
--- /dev/null
+++ b/tests/terminator_in_bb.xml
@@ -0,0 +1,328 @@
+
+
+
+
+ module1
+
+
+
+ UJHDBOTD_134
+
+
+
+
+
+
+
+
+ B0015
+ 1
+ B0015
+
+
+
+
+
+
+ Scar_F
+ 1
+ Scar_F
+
+
+
+
+
+
+ UJHDBOTD
+ 1
+
+
+
+
+
+
+
+ B0015_3
+ 1
+
+
+
+
+
+
+
+ Scar_F_4
+ 1
+
+
+
+
+
+
+
+ Scar_D_2
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation2
+ 1
+
+
+
+ location2
+ 1
+ 134
+ 137
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation1
+ 1
+
+
+
+ location1
+ 1
+ 5
+ 133
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation0
+ 1
+
+
+
+ location0
+ 1
+ 1
+ 4
+
+
+
+
+
+
+
+
+
+ UJHDBOTDAnnotation3
+ 1
+
+
+
+ location3
+ 1
+ 138
+ 2189
+
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint2
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint3
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTDConstraint1
+ 1
+
+
+
+
+
+
+
+
+
+ Scar_D
+ 1
+ Scar_D
+
+
+
+
+
+
+ Cir_qxow
+ 1
+ Cir
+
+
+
+
+
+
+ Scar_F_sequence
+ cgct
+
+
+
+
+ Scar_D_sequence
+ gctt
+
+
+
+
+ B0015_sequence
+ ccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+
+
+
+
+ Cir_qxow_sequence
+ CGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ UJHDBOTD_sequence
+ gcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttatacgctCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtcttcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcg
+
+
+
+
+ B0015_Layout
+
+
+
+
+ Cir_qxow_Layout
+
+
+
+
+ module1_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ UJHDBOTD_134
+
+
+
+
+
+
+ UJHDBOTD_Layout
+
+
+
+ 695.0
+ 457.0
+ 152.0
+ 100.0
+ container
+
+
+
+
+ 0.0
+ 50.0
+ 152.0
+ 1.0
+ backbone
+
+
+
+
+ 1.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_D_2
+
+
+
+
+
+ 51.0
+ 0.0
+ 50.0
+ 100.0
+ B0015_3
+
+
+
+
+
+ 101.0
+ 0.0
+ 50.0
+ 100.0
+ Scar_F_4
+
+
+
+
+
+ 151.0
+ 0.0
+ 1.0
+ 100.0
+ Cir_qxow_5
+
+
+
+
+
+
+ Scar_D_Layout
+
+
+
+
+ Scar_F_Layout
+
+
+
diff --git a/tests/test_all_scripts.py b/tests/test_all_scripts.py
new file mode 100644
index 0000000..b02d862
--- /dev/null
+++ b/tests/test_all_scripts.py
@@ -0,0 +1,34 @@
+import unittest
+import subprocess
+import pathlib
+
+SCRIPTS_DIR = pathlib.Path("scripts")
+
+class TestAllScripts(unittest.TestCase):
+ def test_all_scripts_with_simulator(self):
+ # Make sure we found the folder
+ self.assertTrue(SCRIPTS_DIR.exists(), f"Scripts dir not found: {SCRIPTS_DIR}")
+ script_files = sorted(SCRIPTS_DIR.glob("*.py"), key=lambda p: p.name.lower())
+
+ for script_path in script_files:
+ with self.subTest(script=script_path.name):
+ print(f"\n=== Simulating: {script_path.name} ===")
+ result = subprocess.run(
+ ["opentrons_simulate", str(script_path)],
+ capture_output=True,
+ text=True
+ )
+ # Ensure simulator exits successfully
+ self.assertEqual(
+ result.returncode, 0,
+ msg=f"Simulation failed for {script_path}:\n{result.stderr}"
+ )
+ #TODO: Create edge case tests that fail appropriately
+ #Some example edge cases:
+ #Too many reagents for the labware
+ #Not enough available wells for the thermocycler
+ #Too many colonies for the plating
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_manual_assembly.py b/tests/test_manual_assembly.py
new file mode 100644
index 0000000..1f36e54
--- /dev/null
+++ b/tests/test_manual_assembly.py
@@ -0,0 +1,61 @@
+import unittest
+
+from pudu.assembly import ManualAssembly
+
+
+class TestManualAssembly(unittest.TestCase):
+ def setUp(self):
+ self.assemblies = [
+ {
+ "Product": "https://SBOL2Build.org/composite_1/1",
+ "Backbone": "https://sbolcanvas.org/pSB1C3/1",
+ "PartsList": [
+ "https://sbolcanvas.org/J23101/1",
+ "https://sbolcanvas.org/B0034/1",
+ "https://sbolcanvas.org/GFP/1",
+ "https://sbolcanvas.org/B0015/1",
+ ],
+ "Restriction Enzyme": "https://SBOL2Build.org/BsaI/1",
+ }
+ ]
+
+ def test_extract_name_from_uri(self):
+ assembly = ManualAssembly(assemblies=self.assemblies)
+ self.assertEqual(assembly._extract_name_from_uri("https://sbolcanvas.org/GFP/1"), "GFP")
+ self.assertEqual(assembly._extract_name_from_uri("https://SBOL2Build.org/composite_1/1"), "composite_1")
+
+ def test_volume_calculation(self):
+ assembly = ManualAssembly(assemblies=self.assemblies)
+ volumes = assembly._calculate_reaction_volumes(number_of_dna_components=5)
+ self.assertEqual(volumes["water_volume"], 2)
+ self.assertEqual(volumes["fixed_reagent_volume"], 8)
+ self.assertEqual(volumes["total_dna_volume"], 10)
+
+ def test_invalid_overfilled_reaction(self):
+ assembly = ManualAssembly(
+ assemblies=self.assemblies,
+ volume_total_reaction=10,
+ volume_part=2,
+ volume_restriction_enzyme=2,
+ volume_t4_dna_ligase=4,
+ volume_t4_dna_ligase_buffer=2,
+ )
+ with self.assertRaises(ValueError) as error:
+ assembly._calculate_reaction_volumes(number_of_dna_components=5)
+ self.assertIn("Cannot fit", str(error.exception))
+
+ def test_markdown_rendering_contains_sections_and_parts(self):
+ assembly = ManualAssembly(assemblies=self.assemblies)
+ markdown = assembly.render_markdown()
+
+ self.assertIn("# Golden Gate Manual Assembly Protocol", markdown)
+ self.assertIn("## Reaction summary", markdown)
+ self.assertIn("DNA each (µL)", markdown)
+ self.assertIn("### Product: composite_1", markdown)
+ self.assertIn("Add 2 µL part `J23101`", markdown)
+ self.assertIn("## Thermocycling", markdown)
+ self.assertIn("repeat this profile 75 times", markdown)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/validation_assembly.xml b/tests/validation_assembly.xml
new file mode 100644
index 0000000..7944404
--- /dev/null
+++ b/tests/validation_assembly.xml
@@ -0,0 +1,1580 @@
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_reactant
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_digestion_interaction
+
+
+ restriction
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_70_product
+
+
+
+
+
+
+ UJHDBOTD_70_reactant
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_product
+
+
+
+ UJHDBOTD_106_digestion_interaction
+
+
+
+ 1
+ restriction
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_reactant
+
+ 1
+
+
+
+ 1
+
+
+
+
+
+
+ restriction
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_backbone_reactant
+
+
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
+ UJHDBOTD_backbone_product
+
+
+
+ UJHDBOTD_digestion_interaction
+
+
+
+
+ testassem
+
+
+
+
+
+ 1
+ ligation
+
+
+
+
+
+
+
+
+ UJHDBOTD_extracted_backbone_ligation
+
+ 1
+
+
+
+
+ 1
+ UJHDBOTD_106_extracted_part_ligation
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_134_extracted_part_ligation
+
+
+
+
+
+
+
+
+
+ 1
+ composite_1_product
+
+
+
+
+
+ 1
+
+
+
+ 1
+ UJHDBOTD_70_extracted_part_ligation
+
+
+
+
+ composite_1_ligation_interaction
+
+
+
+
+
+ 1
+ UJHDBOTD_45_extracted_part_ligation
+
+
+
+
+ 1
+
+
+ 1
+
+
+
+ UJHDBOTD_70_digestion_product
+
+
+
+
+
+ 1
+ BsaI_enzyme
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_45_digestion_product
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+ UJHDBOTD_backbone_digestion_product
+
+
+
+
+ UJHDBOTD_45_digestion_interaction
+
+ 1
+
+
+ UJHDBOTD_45_reactant
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+ restriction
+
+
+
+
+
+
+ UJHDBOTD_45_product
+
+
+ 1
+
+
+
+
+
+
+
+ composite_1
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_134_digestion_product
+
+
+
+
+ 1
+
+
+ UJHDBOTD_70_reactant
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_backbone_reactant
+
+
+
+
+
+
+
+
+ T4_Ligase
+
+
+
+ 1
+
+
+
+
+ 1
+ UJHDBOTD_45_reactant
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_digestion_product
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_reactant
+ 1
+
+
+
+
+
+
+ UJHDBOTD_134_product
+ 1
+
+
+
+
+
+
+ 1
+ restriction
+
+
+
+
+
+ 1
+
+ UJHDBOTD_134_digestion_interaction
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_reactant
+ 1
+
+
+
+
+ 1
+ BsaI
+ BsaI
+
+ Restriction enzyme BsaI from REBASE.
+
+
+
+
+
+
+
+
+
+ 1
+
+ UJHDBOTD_70_three_prime_oh_component
+
+
+
+
+
+ 1
+
+
+
+ 1
+ five_prime_oh_location
+ 4
+
+ 1
+
+
+
+ five_prime_overhang
+
+
+
+ UJHDBOTD_70_extracted_part
+
+
+
+
+
+
+
+ 43
+ 1
+ three_prime_oh_location
+ 40
+
+
+
+ 1
+ three_prime_overhang
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_five_prime_oh_component
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_part
+
+
+
+
+ UJHDBOTD_70_part_location
+ 39
+
+ 1
+ 5
+
+
+
+
+
+
+
+
+ J23101_3
+
+ 1
+
+
+ 1
+
+
+
+
+ UJHDBOTD_70_three_prime_oh
+
+
+
+ 1
+
+
+ 1
+ Scar_B
+ Scar_B
+
+
+
+
+
+
+ 1
+ J23101
+ J23101
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ UJHDBOTD_70_five_prime_oh
+
+
+
+ Scar_A
+ 1
+
+ Scar_A
+
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_45_part_location
+ 25
+
+
+ 5
+ 1
+
+
+ UJHDBOTD_45_part
+
+
+
+ UJHDBOTD_45_extracted_part
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_45_five_prime_oh_component
+
+
+
+
+
+
+ 1
+
+ three_prime_overhang
+
+
+
+ 26
+
+ three_prime_oh_location
+ 1
+ 29
+
+
+
+
+
+ 1
+
+
+
+ 1
+ five_prime_overhang
+
+
+
+ 1
+ five_prime_oh_location
+ 4
+
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTD_45_three_prime_oh_component
+ 1
+
+
+
+
+
+
+ 1
+
+
+ B0034_3
+
+
+
+
+
+ 1
+
+ UJHDBOTD_45_five_prime_oh
+
+
+
+
+
+
+
+ UJHDBOTD_45_three_prime_oh
+
+
+
+
+ 1
+
+
+
+
+ Scar_C
+
+
+ Scar_C
+ 1
+
+
+
+
+ B0034
+
+
+ B0034
+ 1
+
+
+
+
+
+
+
+ GFP_3
+ 1
+
+
+
+
+ UJHDBOTD_106_part
+
+
+
+
+ 5
+ 721
+ UJHDBOTD_106_part_location
+
+ 1
+
+
+
+ 1
+
+
+
+
+ 1
+
+
+
+
+ 1
+ three_prime_oh_location
+ 725
+
+ 722
+
+
+
+ three_prime_overhang
+
+
+ UJHDBOTD_106_extracted_part
+
+ 1
+
+
+
+
+
+ 1
+ UJHDBOTD_106_five_prime_oh_component
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_three_prime_oh_component
+ 1
+
+
+
+
+
+
+
+ five_prime_overhang
+
+
+
+ five_prime_oh_location
+
+ 4
+ 1
+ 1
+
+
+ 1
+
+
+
+
+
+
+
+ 1
+
+ GFP
+
+ GFP
+
+
+
+
+
+
+ 1
+ UJHDBOTD_106_five_prime_oh
+
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_106_three_prime_oh
+
+
+ 1
+ Scar_D
+
+
+
+ Scar_D
+
+
+
+ 1
+
+
+ UJHDBOTD_134_five_prime_oh_component
+
+
+ 1
+
+
+
+
+ UJHDBOTD_134_extracted_part
+
+
+
+ UJHDBOTD_134_three_prime_oh_component
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+ B0015_3
+
+
+
+
+ five_prime_overhang
+
+
+
+
+
+ five_prime_oh_location
+ 4
+ 1
+ 1
+
+
+ 1
+
+
+
+
+
+
+
+ UJHDBOTD_134_part
+ 1
+
+
+
+ 133
+ 1
+ UJHDBOTD_134_part_location
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ three_prime_overhang
+
+
+
+ three_prime_oh_location
+ 137
+
+ 134
+ 1
+
+
+
+
+
+
+
+
+ B0015
+
+ 1
+
+ B0015
+
+
+
+ UJHDBOTD_134_three_prime_oh
+
+
+
+
+ 1
+
+
+ Scar_F
+
+ 1
+
+
+ Scar_F
+
+
+
+ 1
+ UJHDBOTD_134_five_prime_oh
+
+
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_159_three_prime_oh_component
+
+ 1
+
+
+
+
+
+ UJHDBOTD_159_backbone
+
+
+ UJHDBOTD_159_backbone_location
+ 5
+
+ 1
+ 2199
+
+
+
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_159_five_prime_oh_component
+ 1
+
+
+
+
+
+
+
+ three_prime_overhang
+ 1
+
+
+ 2200
+
+ three_prime_oh_location
+
+ 2203
+ 1
+
+
+
+
+
+
+
+
+
+ five_prime_overhang
+
+
+ 1
+ five_prime_oh_location
+ 4
+
+
+ 1
+
+
+
+ 1
+
+
+
+
+
+
+
+ Cir_qxow_5
+ 1
+
+
+ UJHDBOTD_extracted_backbone
+ 1
+
+
+
+
+
+ UJHDBOTD_159_five_prime_oh
+
+
+
+
+ 1
+
+
+
+
+ Cir
+
+
+ 1
+ Cir_qxow
+
+
+ 1
+
+ UJHDBOTD_159_three_prime_oh
+
+
+
+
+
+
+ 1
+
+ T4_Ligase
+
+ T4_Ligase
+
+
+
+ 1
+
+
+
+ Ligation_Scar_A
+
+
+
+
+
+
+ 1
+
+
+
+
+ Ligation_Scar_B
+
+
+ Ligation_Scar_C
+
+
+ 1
+
+
+
+
+
+
+
+
+ Ligation_Scar_D
+ 1
+
+
+
+
+
+
+
+ Ligation_Scar_E
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+ Cir_qxow_5_annotation
+
+
+
+ 2199
+
+ Cir_qxow_5_location
+ 5
+ 1
+
+
+
+
+
+
+
+
+
+ 1
+
+ Cir_qxow
+
+
+
+
+
+
+
+
+
+ 2264
+ 2267
+ Ligation_Scar_D_location
+
+ 1
+
+
+
+
+ 1
+ Ligation_Scar_D_annotation
+
+
+ composite_1
+
+
+ J23101_3_annotation
+
+
+ 1
+
+
+
+ 2238
+ 2204
+ 1
+ J23101_3_location
+
+
+
+
+
+
+
+
+
+
+ Ligation_Scar_C_location
+
+ 2242
+
+ 1
+ 2239
+
+
+ 1
+ Ligation_Scar_C_annotation
+
+
+
+
+
+ GFP
+
+ 1
+
+
+
+
+
+
+
+
+
+
+ B0015
+ 1
+
+
+
+
+
+ 1
+
+
+ Ligation_Scar_B
+
+
+
+
+
+ B0034_3_annotation
+ 1
+
+
+
+ 1
+ B0034_3_location
+
+ 2243
+ 2263
+
+
+
+
+
+
+
+ J23101
+
+
+
+ 1
+
+
+
+
+
+
+ 1
+ Ligation_Scar_B_annotation
+
+
+ 2203
+ 1
+ Ligation_Scar_B_location
+
+
+ 2200
+
+
+
+
+ 1
+
+
+
+
+ 2989
+ 3117
+ 1
+ B0015_3_location
+
+
+
+
+ 1
+
+ B0015_3_annotation
+
+
+
+
+
+
+
+
+
+
+ 2985
+ Ligation_Scar_E_location
+ 2988
+ 1
+
+
+
+
+ Ligation_Scar_E_annotation
+ 1
+
+
+
+
+ Ligation_Scar_A_annotation
+ 1
+
+
+
+ 1
+ 4
+ 1
+
+ Ligation_Scar_A_location
+
+
+
+
+
+
+
+
+
+
+ 1
+
+ B0034
+
+
+
+
+
+
+ 1
+
+
+ 2984
+ GFP_3_location
+ 1
+ 2268
+
+
+
+
+ GFP_3_annotation
+
+
+
+ composite_1
+
+
+ Ligation_Scar_D
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+ Ligation_Scar_C
+
+
+
+
+
+
+ Ligation_Scar_A
+
+ 1
+
+
+
+
+ Ligation_Scar_E
+
+
+
+ 1
+
+
+
+
+
+ UJHDBOTD_70_extracted_part_seq
+
+ GGAGtttacagctagctcagtcctaggtattatgctagcTACT
+ 1
+
+
+
+
+ 1
+
+ UJHDBOTD_70_three_prime_oh_sequence
+ TACT
+
+
+ Scar_B_sequence
+
+ TACT
+
+
+
+ J23101_sequence
+
+
+ tttacagctagctcagtcctaggtattatgctagc
+
+
+ GGAG
+ 1
+
+
+
+ UJHDBOTD_70_five_prime_oh_sequence
+
+
+
+
+ Scar_A_sequence
+ GGAG
+
+
+
+ 1
+ TACTagagaaagaggagaaatactaaatg
+ UJHDBOTD_45_extracted_part_seq
+
+
+
+
+
+
+ UJHDBOTD_45_five_prime_oh_sequence
+ 1
+ TACT
+
+
+
+ UJHDBOTD_45_three_prime_oh_sequence
+
+
+ 1
+ aatg
+
+
+ Scar_C_sequence
+
+ aatg
+
+
+
+ B0034_sequence
+
+ agagaaagaggagaaatacta
+
+
+
+
+ UJHDBOTD_106_extracted_part_seq
+
+ 1
+ aatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagctt
+
+
+
+
+ GFP_sequence
+ gtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaa
+
+
+
+
+
+ 1
+ aatg
+ UJHDBOTD_106_five_prime_oh_sequence
+
+
+
+
+ 1
+ UJHDBOTD_106_three_prime_oh_sequence
+ gctt
+
+
+
+
+ Scar_D_sequence
+
+ gctt
+
+
+ UJHDBOTD_134_extracted_part_seq
+ 1
+ gcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttatacgct
+
+
+
+
+
+ ccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+
+ B0015_sequence
+
+
+ cgct
+
+
+ 1
+
+ UJHDBOTD_134_three_prime_oh_sequence
+
+
+ cgct
+
+
+ Scar_F_sequence
+
+
+ UJHDBOTD_134_five_prime_oh_sequence
+
+ 1
+
+ gctt
+
+
+
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgggag
+
+
+ UJHDBOTD_extracted_backbone_seq
+ 1
+
+
+
+ 1
+ cgct
+
+ UJHDBOTD_159_five_prime_oh_sequence
+
+
+
+
+ Cir_qxow_sequence
+
+ gcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatg
+
+
+
+ UJHDBOTD_159_three_prime_oh_sequence
+ ggag
+ 1
+
+
+
+
+
+ 1
+ Ligation_Scar_A_sequence
+
+ cgct
+
+
+
+ ggag
+ 1
+
+ Ligation_Scar_B_sequence
+
+
+
+
+ 1
+ TACT
+ Ligation_Scar_C_sequence
+
+
+ Ligation_Scar_D_sequence
+ aatg
+
+ 1
+
+
+
+ gctt
+
+ Ligation_Scar_E_sequence
+ 1
+
+
+
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgGGAGtttacagctagctcagtcctaggtattatgctagcTACTagagaaagaggagaaatactaaatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+
+ 1
+ composite_1_seq
+
+
+
diff --git a/tests/validation_assembly1.xml b/tests/validation_assembly1.xml
new file mode 100644
index 0000000..1237f14
--- /dev/null
+++ b/tests/validation_assembly1.xml
@@ -0,0 +1,1581 @@
+
+
+
+
+
+
+
+ 1
+
+ BsaI_enzyme
+
+
+ 1
+
+
+ UJHDBOTD_70_reactant
+
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_106_digestion_interaction
+ 1
+
+
+
+
+
+ UJHDBOTD_106_product
+ 1
+
+
+
+
+
+
+
+ UJHDBOTD_106_reactant
+ 1
+
+
+
+
+
+
+ 1
+ restriction
+
+
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_digestion_product
+ 1
+
+
+
+ testassem
+
+
+ UJHDBOTD_45_digestion_interaction
+
+
+
+ 1
+
+
+
+ UJHDBOTD_45_product
+
+
+ 1
+
+
+
+
+
+
+ UJHDBOTD_45_reactant
+ 1
+
+
+
+
+
+ 1
+
+
+ restriction
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_reactant
+
+ 1
+
+
+
+
+
+
+ T4_Ligase
+ 1
+
+
+
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_backbone_digestion_product
+
+
+
+
+
+ UJHDBOTD_106_digestion_product
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTD_backbone_reactant
+
+
+
+
+ 1
+
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_106_extracted_part_ligation
+
+
+
+
+ 1
+
+
+ composite_1_product
+
+
+
+ 1
+ composite_1_ligation_interaction
+
+
+
+ 1
+
+
+
+ UJHDBOTD_70_extracted_part_ligation
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_45_extracted_part_ligation
+
+
+
+
+
+
+
+ 1
+
+ ligation
+
+
+
+
+
+ UJHDBOTD_134_extracted_part_ligation
+
+
+ 1
+
+
+
+
+
+ UJHDBOTD_extracted_backbone_ligation
+ 1
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_134_digestion_interaction
+
+
+
+ restriction
+
+ 1
+
+
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_134_product
+
+
+
+
+ 1
+
+ UJHDBOTD_134_reactant
+
+
+
+
+
+
+
+
+ composite_1
+
+
+
+ 1
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_45_digestion_product
+
+
+
+
+
+ 1
+
+
+
+ UJHDBOTD_70_digestion_product
+
+
+
+
+
+ 1
+
+ UJHDBOTD_106_reactant
+
+
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_45_reactant
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_70_reactant
+ 1
+
+
+ UJHDBOTD_70_digestion_interaction
+
+ 1
+
+
+
+ restriction
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTD_70_product
+
+ 1
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_backbone_reactant
+
+ 1
+
+
+
+
+ UJHDBOTD_digestion_interaction
+ 1
+
+
+
+
+
+ restriction
+ 1
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_backbone_product
+
+
+
+
+
+
+
+ BsaI
+ 1
+
+ BsaI
+
+
+ Restriction enzyme BsaI from REBASE.
+
+
+
+
+
+
+
+
+
+ 40
+ three_prime_oh_location
+ 1
+
+ 43
+
+
+ three_prime_overhang
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+ J23101_3
+
+
+
+
+ UJHDBOTD_70_three_prime_oh_component
+ 1
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_70_five_prime_oh_component
+
+ 1
+
+
+
+
+
+
+ 1
+ five_prime_overhang
+
+
+
+ 1
+
+ 1
+ five_prime_oh_location
+ 4
+
+
+
+
+
+
+
+
+ UJHDBOTD_70_extracted_part
+
+
+ 1
+
+
+ UJHDBOTD_70_part
+
+
+ UJHDBOTD_70_part_location
+ 39
+ 1
+
+ 5
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+ UJHDBOTD_70_three_prime_oh
+
+
+
+ 1
+
+ Scar_B
+
+ Scar_B
+
+
+
+
+ 1
+ J23101
+
+ J23101
+
+
+
+
+
+
+
+ UJHDBOTD_70_five_prime_oh
+ 1
+
+
+
+
+ Scar_A
+
+
+
+ 1
+
+ Scar_A
+
+
+
+
+
+
+ UJHDBOTD_45_part
+
+
+
+ 5
+ 25
+ UJHDBOTD_45_part_location
+
+ 1
+
+
+
+ 1
+
+
+
+
+
+
+
+ 1
+
+
+
+ 29
+ 1
+ 26
+
+ three_prime_oh_location
+
+
+
+ three_prime_overhang
+
+
+
+
+
+ 1
+
+ UJHDBOTD_45_three_prime_oh_component
+
+
+
+
+ UJHDBOTD_45_extracted_part
+
+
+
+
+
+ UJHDBOTD_45_five_prime_oh_component
+ 1
+
+
+
+ 1
+
+
+
+
+
+ B0034_3
+ 1
+
+
+
+
+
+
+ five_prime_overhang
+
+ 1
+
+
+ 1
+
+ five_prime_oh_location
+ 1
+
+ 4
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_45_five_prime_oh
+
+ 1
+
+
+
+
+
+
+ 1
+ UJHDBOTD_45_three_prime_oh
+
+
+
+
+ Scar_C
+
+
+ Scar_C
+
+ 1
+
+
+
+ B0034
+
+
+
+ 1
+ B0034
+
+
+
+ UJHDBOTD_106_extracted_part
+
+
+ three_prime_overhang
+
+
+
+
+ 1
+ 725
+
+ three_prime_oh_location
+ 722
+
+
+
+ 1
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_106_five_prime_oh_component
+
+
+
+
+
+
+ five_prime_overhang
+ 1
+
+
+
+
+
+ 4
+ 1
+ five_prime_oh_location
+ 1
+
+
+
+
+
+
+
+ GFP_3
+ 1
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_part
+
+ 1
+
+
+
+ 721
+ UJHDBOTD_106_part_location
+ 1
+ 5
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_106_three_prime_oh_component
+
+ 1
+
+
+
+ 1
+
+
+
+
+
+
+ 1
+ GFP
+
+ GFP
+
+
+ 1
+
+
+
+ UJHDBOTD_106_five_prime_oh
+
+
+
+
+
+
+
+ 1
+
+ UJHDBOTD_106_three_prime_oh
+
+
+
+ 1
+
+ Scar_D
+
+ Scar_D
+
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_part_location
+ 1
+ 133
+ 5
+
+
+
+
+ 1
+ UJHDBOTD_134_part
+
+
+
+
+
+
+ five_prime_overhang
+
+
+ 1
+
+
+ 4
+ five_prime_oh_location
+ 1
+
+
+ 1
+
+
+
+
+ 1
+ UJHDBOTD_134_extracted_part
+
+
+
+
+ B0015_3
+ 1
+
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_five_prime_oh_component
+
+ 1
+
+
+
+
+
+
+ three_prime_overhang
+ 1
+
+
+
+ three_prime_oh_location
+
+ 137
+ 1
+ 134
+
+
+
+
+
+
+
+
+ UJHDBOTD_134_three_prime_oh_component
+
+ 1
+
+
+
+
+
+
+ B0015
+
+ B0015
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+
+ UJHDBOTD_134_three_prime_oh
+
+
+
+ 1
+ Scar_F
+
+
+
+ Scar_F
+
+
+
+
+
+
+ 1
+ UJHDBOTD_134_five_prime_oh
+
+
+
+ UJHDBOTD_extracted_backbone
+
+
+
+
+ three_prime_overhang
+ 1
+
+
+
+ 2200
+ three_prime_oh_location
+ 2203
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+ UJHDBOTD_159_five_prime_oh_component
+
+
+
+
+
+
+
+
+
+ 5
+ 1
+
+ UJHDBOTD_159_backbone_location
+
+ 2199
+
+
+ 1
+
+ UJHDBOTD_159_backbone
+
+
+
+
+
+ 1
+
+
+ Cir_qxow_5
+
+
+
+
+
+ UJHDBOTD_159_three_prime_oh_component
+
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+ 4
+ 1
+ five_prime_oh_location
+
+
+
+
+ five_prime_overhang
+ 1
+
+
+
+
+
+
+
+
+
+ 1
+
+ UJHDBOTD_159_five_prime_oh
+
+
+
+
+ Cir_qxow
+ Cir
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+ UJHDBOTD_159_three_prime_oh
+
+
+
+
+
+ 1
+ T4_Ligase
+ T4_Ligase
+
+
+
+ Ligation_Scar_A
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ Ligation_Scar_B
+ 1
+
+
+
+
+
+ Ligation_Scar_C
+
+
+ 1
+
+
+
+
+
+ Ligation_Scar_D
+
+
+
+
+
+ 1
+
+
+ Ligation_Scar_E
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ Cir_qxow_5_annotation
+
+
+ 5
+ 2199
+
+
+ Cir_qxow_5_location
+ 1
+
+
+
+ 1
+
+
+
+
+
+
+
+ Ligation_Scar_B
+
+ 1
+
+
+
+
+ 1
+
+
+
+ 1
+
+ GFP_3_location
+ 2268
+ 2984
+
+
+
+ GFP_3_annotation
+
+
+
+
+
+
+ 1
+
+ B0034
+
+
+
+ composite_1
+
+
+ 1
+
+
+
+ J23101
+
+
+
+
+ Ligation_Scar_E_annotation
+
+
+
+
+ Ligation_Scar_E_location
+ 1
+ 2985
+
+ 2988
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+ 1
+ Ligation_Scar_B_location
+ 2203
+
+ 2200
+
+
+
+
+ Ligation_Scar_B_annotation
+
+
+
+
+
+ 1
+ J23101_3_annotation
+
+
+
+
+ 1
+ 2238
+ 2204
+ J23101_3_location
+
+
+
+
+
+
+
+
+
+
+ Cir_qxow
+ 1
+
+
+
+
+
+ 1
+ Ligation_Scar_D
+
+
+
+
+
+
+
+ GFP
+
+
+ 1
+
+
+
+
+
+
+
+
+ B0015
+ 1
+
+
+ 1
+
+
+ 1
+
+
+
+ Ligation_Scar_C_location
+
+ 2242
+
+ 1
+ 2239
+
+
+ Ligation_Scar_C_annotation
+
+
+
+
+
+
+ Ligation_Scar_A
+ 1
+
+
+
+
+
+
+
+
+
+ Ligation_Scar_E
+ 1
+
+
+
+
+
+
+
+
+ Ligation_Scar_C
+ 1
+
+
+
+
+
+
+
+
+ 1
+
+ 2264
+ Ligation_Scar_D_location
+ 2267
+
+
+
+ 1
+ Ligation_Scar_D_annotation
+
+
+
+
+
+ 1
+ Ligation_Scar_A_annotation
+
+
+
+ 1
+
+
+ 1
+ Ligation_Scar_A_location
+ 4
+
+
+
+
+
+
+
+
+
+
+ 2243
+
+
+ 1
+ B0034_3_location
+ 2263
+
+
+ 1
+ B0034_3_annotation
+
+
+
+
+
+
+ 1
+
+
+
+
+ 3117
+ 2989
+ 1
+ B0015_3_location
+
+
+
+
+ B0015_3_annotation
+
+
+ composite_1
+
+
+
+ GGAGtttacagctagctcagtcctaggtattatgctagcTACT
+ 1
+
+ UJHDBOTD_70_extracted_part_seq
+
+
+
+
+ TACT
+
+ UJHDBOTD_70_three_prime_oh_sequence
+ 1
+
+
+
+ TACT
+
+ Scar_B_sequence
+
+
+ tttacagctagctcagtcctaggtattatgctagc
+
+
+ J23101_sequence
+
+
+
+
+
+ 1
+ UJHDBOTD_70_five_prime_oh_sequence
+ GGAG
+
+
+ GGAG
+
+ Scar_A_sequence
+
+
+
+
+ TACTagagaaagaggagaaatactaaatg
+ UJHDBOTD_45_extracted_part_seq
+ 1
+
+
+
+ 1
+ UJHDBOTD_45_five_prime_oh_sequence
+
+
+
+ TACT
+
+
+
+
+
+ aatg
+ 1
+ UJHDBOTD_45_three_prime_oh_sequence
+
+
+ aatg
+ Scar_C_sequence
+
+
+
+
+ B0034_sequence
+
+
+ agagaaagaggagaaatacta
+
+
+ 1
+ UJHDBOTD_106_extracted_part_seq
+ aatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagctt
+
+
+
+
+ gtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaa
+
+
+ GFP_sequence
+
+
+ aatg
+ UJHDBOTD_106_five_prime_oh_sequence
+ 1
+
+
+
+
+
+ UJHDBOTD_106_three_prime_oh_sequence
+
+
+ 1
+ gctt
+
+
+
+ gctt
+ Scar_D_sequence
+
+
+
+
+ UJHDBOTD_134_extracted_part_seq
+ 1
+
+ gcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttatacgct
+
+
+
+
+ B0015_sequence
+ ccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+
+
+
+ UJHDBOTD_134_three_prime_oh_sequence
+
+ 1
+
+
+ cgct
+
+
+ Scar_F_sequence
+
+ cgct
+
+
+
+
+ UJHDBOTD_134_five_prime_oh_sequence
+
+
+ gctt
+ 1
+
+
+
+
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgggag
+ UJHDBOTD_extracted_backbone_seq
+ 1
+
+
+
+
+ cgct
+ 1
+
+ UJHDBOTD_159_five_prime_oh_sequence
+
+
+
+ gcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatg
+
+ Cir_qxow_sequence
+
+
+ 1
+
+
+ UJHDBOTD_159_three_prime_oh_sequence
+ ggag
+
+
+
+
+ Ligation_Scar_A_sequence
+ 1
+
+ cgct
+
+
+ ggag
+
+
+ Ligation_Scar_B_sequence
+ 1
+
+
+
+ TACT
+
+ Ligation_Scar_C_sequence
+ 1
+
+
+
+
+ 1
+ aatg
+ Ligation_Scar_D_sequence
+
+
+ Ligation_Scar_E_sequence
+
+ 1
+ gctt
+
+
+
+ 1
+ composite_1_seq
+ cgctgcatgaagagcctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgctcttcaatgGGAGtttacagctagctcagtcctaggtattatgctagcTACTagagaaagaggagaaatactaaatggtgagcaagggcgaggagctgttcaccggggtggtgcccatcctggtcgagctggacggcgacgtaaacggccacaagttcagcgtgtccggcgagggcgagggcgatgccacctacggcaagctgaccctgaagttcatctgcaccaccggcaagctgcccgtgccctggcccaccctcgtgaccaccttcagctacggcgtgcagtgcttcagccgctaccccgaccacatgaagcagcacgacttcttcaagtccgccatgcccgaaggctacgtccaggagcgcaccatcttcttcaaggacgacggcaactacaagacccgcgccgaggtgaagttcgagggcgacaccctggtgaaccgcatcgagctgaagggcatcgacttcaaggaggacggcaacatcctggggcacaagctggagtacaactacaacagccacaacgtctatatcatggccgacaagcagaagaacggcatcaaggtgaacttcaagatccgccacaacatcgaggacggcagcgtgcagctcgccgaccactaccagcagaacacccccatcggcgacggccccgtgctgctgcccgacaaccactacctgagcacccagtccgccctgagcaaagaccccaacgagaagcgcgatcacatggtcctgctggagttcgtgaccgccgccgggatcactcacggcatggacgagctgtacaagtaagcttccaggcatcaaataaaacgaaaggctcagtcgaaagactgggcctttcgttttatctgttgtttgtcggtgaacgctctctactagagtcacactggctcaccttcgggtgggcctttctgcgtttata
+
+
+
+