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. -PUDU logo -PUDU night logo +PUDU logo +PUDU night logo ## *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 + + + +